在React Native项目中实现鸿蒙OS(HarmonyOS)的计数排序算法,首先需要理解计数排序的基本原理和步骤。计数排序是一种非比较型排序算法,适用于一定范围内的整数排序。其核心思想是使用一个额外的数组来统计每个值的出现次数,然后根据这个统计结果来重构原数组的顺序。

计数排序的基本步骤

  1. 找出待排序数组中的最大值和最小值,以确定计数数组的范围。
  2. 创建计数数组,并初始化所有元素为0。
  3. 遍历待排序数组,统计每个元素的出现次数,存储在计数数组中。
  4. 根据计数数组重构原数组,即根据计数数组的值将元素放回原数组的正确位置。

在React Native项目中实现计数排序

  1. 创建React Native项目

如果你还没有创建React Native项目,可以使用以下命令:

npx react-native init MyHarmonyApp
cd MyHarmonyApp
  1. 实现计数排序函数

在React Native项目中,你可以在任意组件中实现计数排序。例如,在App.js文件中添加一个排序函数:

function countSort(arr) {
    if (arr.length === 0) return [];

    // 找到数组中的最大值和最小值
    let min = Math.min(...arr);
    let max = Math.max(...arr);
    let range = max - min + 1;

    // 创建计数数组并初始化
    let countArray = new Array(range).fill(0);

    // 统计每个值的出现次数
    for (let i = 0; i < arr.length; i++) {
        countArray[arr[i] - min]++;
    }

    // 重构原数组
    let sortedIndex = 0;
    for (let i = 0; i < countArray.length; i++) {
        while (countArray[i] > 0) {
            arr[sortedIndex++] = i + min;
            countArray[i]--;
        }
    }

    return arr;
}
  1. 在组件中使用计数排序函数

App.js中,你可以使用这个countSort函数来排序一个数组,并在界面上显示结果:

import React from 'react';
import { View, Text, Button } from 'react-native';

export default function App() {
  const [array, setArray] = React.useState([5, 3, 2, 8, 5, 3, 6]);
  const [sortedArray, setSortedArray] = React.useState([]);

  const handleSort = () => {
    const sorted = countSort([...array]); // 使用展开运算符复制数组以避免直接修改状态中的数组引用问题(可选)
    setSortedArray(sorted);
  };

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Original Array: {array.join(', ')}</Text>
      <Text>Sorted Array: {sortedArray.join(', ')}</Text>
      <Button title="Sort Array" onPress={handleSort} />
    </View>
  );
}

注意事项:

  • 性能问题:对于大数据集或非常大的值范围,计数排序的性能可能会下降。在这种情况下,可以考虑使用更高效的排序算法如快速排序或归并排序。
  • 空间复杂度:计数排序的空间复杂度为O(n+k),其中n是数组的长度,k是数据的范围。如果k非常大,这可能会成为一个问题。在这种情况下,可以考虑使用桶排序或其他优化策略。
  • 适用场景:计数排序最适合于范围不太大的整数排序。对于浮点数或大范围数据,其他算法可能更合适。

通过上述步骤,你可以在React Native项目中实现和使用计数排序算法。


真实项目案例演示:

import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Image } from 'react-native';

// Simple Icon Component using Unicode symbols
interface IconProps {
  name: string;
  size?: number;
  color?: string;
  style?: object;
}

const Icon: React.FC<IconProps> = ({ 
  name, 
  size = 24, 
  color = '#333333',
  style 
}) => {
  const getIconSymbol = () => {
    switch (name) {
      case 'prev': return '‹';
      case 'next': return '›';
      case 'today': return '◎';
      case 'event': return '•';
      case 'date': return '📅';
      case 'month': return '📅';
      case 'year': return '📅';
      default: return '●';
    }
  };

  return (
    <View style={[{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }, style]}>
      <Text style={{ fontSize: size * 0.8, color, includeFontPadding: false, textAlign: 'center' }}>
        {getIconSymbol()}
      </Text>
    </View>
  );
};

// Calendar Component
interface CalendarProps {
  events?: { date: string; title: string }[];
  onDateSelect?: (date: string) => void;
  selectedDate?: string;
}

const Calendar: React.FC<CalendarProps> = ({ 
  events = [], 
  onDateSelect,
  selectedDate 
}) => {
  const [currentDate, setCurrentDate] = useState(new Date());
  
  // Get days in month
  const getDaysInMonth = (year: number, month: number) => {
    return new Date(year, month + 1, 0).getDate();
  };
  
  // Get first day of month (0 = Sunday, 1 = Monday, etc)
  const getFirstDayOfMonth = (year: number, month: number) => {
    return new Date(year, month, 1).getDay();
  };
  
  // Format date as YYYY-MM-DD
  const formatDate = (date: Date) => {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  };
  
  // Check if date has events
  const hasEvents = (dateStr: string) => {
    return events.some(event => event.date === dateStr);
  };
  
  // Navigate to previous month
  const prevMonth = () => {
    setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
  };
  
  // Navigate to next month
  const nextMonth = () => {
    setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
  };
  
  // Go to today
  const goToToday = () => {
    const today = new Date();
    setCurrentDate(new Date(today.getFullYear(), today.getMonth(), 1));
    if (onDateSelect) {
      onDateSelect(formatDate(today));
    }
  };
  
  // Render calendar header
  const renderHeader = () => {
    const year = currentDate.getFullYear();
    const month = currentDate.toLocaleString('zh-CN', { month: 'long' });
    
    return (
      <View style={styles.calendarHeader}>
        <TouchableOpacity onPress={prevMonth} style={styles.navButton}>
          <Icon name="prev" size={24} color="#4a5568" />
        </TouchableOpacity>
        
        <View style={styles.monthYearContainer}>
          <Text style={styles.monthText}>{month}</Text>
          <Text style={styles.yearText}>{year}</Text>
        </View>
        
        <TouchableOpacity onPress={nextMonth} style={styles.navButton}>
          <Icon name="next" size={24} color="#4a5568" />
        </TouchableOpacity>
      </View>
    );
  };
  
  // Render weekdays
  const renderWeekdays = () => {
    const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
    
    return (
      <View style={styles.weekdaysContainer}>
        {weekdays.map((day, index) => (
          <View key={index} style={styles.weekdayCell}>
            <Text style={[styles.weekdayText, index === 0 && styles.sundayText]}>{day}</Text>
          </View>
        ))}
      </View>
    );
  };
  
  // Render calendar days
  const renderDays = () => {
    const year = currentDate.getFullYear();
    const month = currentDate.getMonth();
    const daysInMonth = getDaysInMonth(year, month);
    const firstDayOfMonth = getFirstDayOfMonth(year, month);
    
    const today = new Date();
    const isCurrentMonth = today.getFullYear() === year && today.getMonth() === month;
    const todayDate = today.getDate();
    
    const days = [];
    
    // Previous month's days
    const prevMonthDays = getDaysInMonth(year, month - 1);
    for (let i = firstDayOfMonth - 1; i >= 0; i--) {
      const day = prevMonthDays - i;
      days.push(
        <View key={`prev-${day}`} style={styles.dayCell}>
          <Text style={styles.otherMonthDay}>{day}</Text>
        </View>
      );
    }
    
    // Current month's days
    for (let day = 1; day <= daysInMonth; day++) {
      const dateObj = new Date(year, month, day);
      const dateStr = formatDate(dateObj);
      const isToday = isCurrentMonth && day === todayDate;
      const isSelected = selectedDate === dateStr;
      const hasEvent = hasEvents(dateStr);
      
      days.push(
        <TouchableOpacity 
          key={`curr-${day}`} 
          style={[
            styles.dayCell,
            isToday && styles.todayCell,
            isSelected && styles.selectedCell
          ]}
          onPress={() => onDateSelect && onDateSelect(dateStr)}
        >
          <View style={styles.dayNumberContainer}>
            <Text style={[
              styles.dayNumber,
              isToday && styles.todayText,
              isSelected && styles.selectedText
            ]}>
              {day}
            </Text>
            {hasEvent && (
              <View style={styles.eventIndicator}>
                <Icon name="event" size={8} color="#e53e3e" />
              </View>
            )}
          </View>
        </TouchableOpacity>
      );
    }
    
    // Next month's days
    const totalCells = 42; // 6 rows * 7 days
    const remainingCells = totalCells - days.length;
    for (let day = 1; day <= remainingCells; day++) {
      days.push(
        <View key={`next-${day}`} style={styles.dayCell}>
          <Text style={styles.otherMonthDay}>{day}</Text>
        </View>
      );
    }
    
    return (
      <View style={styles.daysContainer}>
        {days}
      </View>
    );
  };
  
  // Render today button
  const renderTodayButton = () => {
    return (
      <TouchableOpacity style={styles.todayButton} onPress={goToToday}>
        <Icon name="today" size={16} color="#4a5568" style={styles.todayIcon} />
        <Text style={styles.todayButtonText}>今天</Text>
      </TouchableOpacity>
    );
  };

  return (
    <View style={styles.calendarContainer}>
      {renderHeader()}
      {renderWeekdays()}
      {renderDays()}
      {renderTodayButton()}
    </View>
  );
};

// Main App Component
const CalendarComponentApp = () => {
  const [selectedDate, setSelectedDate] = useState<string>('');
  const [events] = useState([
    { date: new Date().toISOString().split('T')[0], title: '会议' },
    { date: new Date(Date.now() + 86400000).toISOString().split('T')[0], title: '生日' },
    { date: new Date(Date.now() + 172800000).toISOString().split('T')[0], title: '假期' },
  ]);

  const handleDateSelect = (date: string) => {
    setSelectedDate(date);
  };

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>计数排序组件</Text>
        <Text style={styles.headerSubtitle}>逐步演示计数排序</Text>
      </View>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>计数排序(可视化 · 薄荷风格)</Text>
        <View style={styles.ctContainer}>
          <CountingSortVisualizer />
        </View>
      </View>
      
      <View style={styles.footer}>
        <Text style={styles.footerText}>© 2023 排序演示 | 现代化UI组件库</Text>
      </View>
    </ScrollView>
  );
};

const { width } = Dimensions.get('window');

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f7fafc',
  },
  header: {
    backgroundColor: '#ffffff',
    paddingVertical: 30,
    paddingHorizontal: 20,
    marginBottom: 10,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  headerTitle: {
    fontSize: 28,
    fontWeight: '700',
    color: '#2d3748',
    textAlign: 'center',
    marginBottom: 5,
  },
  headerSubtitle: {
    fontSize: 16,
    color: '#718096',
    textAlign: 'center',
  },
  section: {
    marginBottom: 25,
  },
  sectionTitle: {
    fontSize: 20,
    fontWeight: '700',
    color: '#2d3748',
    paddingHorizontal: 20,
    paddingBottom: 15,
  },
  calendarWrapper: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 15,
    elevation: 4,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
  },
  selectedDateCard: {
    backgroundColor: '#ebf8ff',
    marginHorizontal: 15,
    borderRadius: 12,
    padding: 20,
    borderWidth: 1,
    borderColor: '#bee3f8',
  },
  selectedDateText: {
    fontSize: 18,
    fontWeight: '700',
    color: '#2b6cb0',
    textAlign: 'center',
    marginBottom: 5,
  },
  selectedDateDesc: {
    fontSize: 14,
    color: '#4a5568',
    textAlign: 'center',
  },
  demosContainer: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  demoItem: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 20,
  },
  demoItemLast: {
    marginBottom: 0,
  },
  demoIcon: {
    marginRight: 15,
  },
  demoTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#2d3748',
    marginBottom: 3,
  },
  demoDesc: {
    fontSize: 14,
    color: '#718096',
  },
  usageSection: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    marginBottom: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  codeBlock: {
    backgroundColor: '#2d3748',
    borderRadius: 8,
    padding: 15,
    marginBottom: 15,
  },
  codeText: {
    fontFamily: 'monospace',
    color: '#e2e8f0',
    fontSize: 14,
    lineHeight: 22,
  },
  description: {
    fontSize: 15,
    color: '#4a5568',
    lineHeight: 22,
  },
  featuresSection: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    marginBottom: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  featuresList: {
    paddingLeft: 10,
  },
  featureItem: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 15,
  },
  featureIcon: {
    marginRight: 15,
  },
  featureText: {
    fontSize: 16,
    color: '#2d3748',
  },
  footer: {
    paddingVertical: 20,
    alignItems: 'center',
  },
  footerText: {
    color: '#a0aec0',
    fontSize: 14,
  },
  // Calendar Styles
  calendarContainer: {
    backgroundColor: '#ffffff',
  },
  calendarHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 10,
    paddingVertical: 15,
  },
  navButton: {
    padding: 10,
  },
  monthYearContainer: {
    alignItems: 'center',
  },
  monthText: {
    fontSize: 18,
    fontWeight: '700',
    color: '#2d3748',
  },
  yearText: {
    fontSize: 14,
    color: '#718096',
  },
  weekdaysContainer: {
    flexDirection: 'row',
    borderBottomWidth: 1,
    borderBottomColor: '#edf2f7',
    paddingBottom: 10,
    marginBottom: 5,
  },
  weekdayCell: {
    flex: 1,
    alignItems: 'center',
  },
  weekdayText: {
    fontSize: 14,
    fontWeight: '600',
    color: '#4a5568',
  },
  sundayText: {
    color: '#e53e3e',
  },
  daysContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
  },
  dayCell: {
    width: '14.28%',
    aspectRatio: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  dayNumberContainer: {
    position: 'relative',
  },
  dayNumber: {
    fontSize: 16,
    color: '#4a5568',
    fontWeight: '500',
  },
  otherMonthDay: {
    fontSize: 16,
    color: '#cbd5e0',
  },
  todayCell: {
    backgroundColor: '#ebf8ff',
    borderRadius: 30,
  },
  todayText: {
    color: '#3182ce',
    fontWeight: '700',
  },
  selectedCell: {
    backgroundColor: '#3182ce',
    borderRadius: 30,
  },
  selectedText: {
    color: '#ffffff',
    fontWeight: '700',
  },
  eventIndicator: {
    position: 'absolute',
    top: -3,
    right: -3,
  },
  todayButton: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    paddingVertical: 15,
    borderTopWidth: 1,
    borderTopColor: '#edf2f7',
    marginTop: 5,
  },
  todayIcon: {
    marginRight: 8,
  },
  todayButtonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#4a5568',
  },
  msContainer: {
    marginHorizontal: 15,
  },
  msCard: {
    backgroundColor: '#0f141c',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#1f2a36',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.18,
    shadowRadius: 14,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  msHeader: {
    marginBottom: 8,
  },
  msTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#e6f1ff',
  },
  msSubtitle: {
    fontSize: 13,
    color: '#8aa0b8',
    marginTop: 4,
  },
  msControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  msBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#1a2433',
    borderWidth: 1,
    borderColor: '#28374d',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  msBtnActive: {
    backgroundColor: '#1f2d44',
    borderColor: '#34507a',
  },
  msIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  msBtnText: {
    color: '#e6f1ff',
    fontSize: 13,
    fontWeight: '600',
  },
  msChips: {
    flexDirection: 'row',
    alignSelf: 'flex-end',
    marginLeft: 8,
  },
  msChip: {
    backgroundColor: '#182030',
    borderWidth: 1,
    borderColor: '#26364d',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  msChipActive: {
    backgroundColor: '#213049',
    borderColor: '#385782',
  },
  msChipText: {
    color: '#cfe1ff',
    fontSize: 12,
    fontWeight: '600',
  },
  msBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  msBar: {
    width: 16,
    backgroundColor: '#2f7de1',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  msBarHL: {
    backgroundColor: '#49a2ff',
  },
  msBarText: {
    fontSize: 10,
    color: '#e6f1ff',
    marginBottom: 4,
    fontWeight: '600',
  },
  msFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  msFootText: {
    color: '#8aa0b8',
    fontSize: 12,
  },
  msDone: {
    color: '#43d998',
    fontWeight: '700',
  },
  bsContainer: {
    marginHorizontal: 15,
  },
  bsCard: {
    backgroundColor: '#ffffff',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#e7eef7',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.08,
    shadowRadius: 12,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  bsHeader: {
    marginBottom: 8,
  },
  bsTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#2d3748',
  },
  bsSubtitle: {
    fontSize: 13,
    color: '#718096',
    marginTop: 4,
  },
  bsControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  bsBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#f1f5fb',
    borderWidth: 1,
    borderColor: '#e1eaf6',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  bsBtnActive: {
    backgroundColor: '#e7eef7',
    borderColor: '#d9e6f5',
  },
  bsIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  bsBtnText: {
    color: '#2d3748',
    fontSize: 13,
    fontWeight: '600',
  },
  bsTargetRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginTop: 6,
  },
  bsChip: {
    backgroundColor: '#edf2f7',
    borderWidth: 1,
    borderColor: '#e2e8f0',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  bsChipText: {
    color: '#2d3748',
    fontSize: 12,
    fontWeight: '600',
  },
  bsBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  bsBar: {
    width: 16,
    backgroundColor: '#90cdf4',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  bsBarDim: {
    backgroundColor: '#c5d8ee',
  },
  bsBarMid: {
    backgroundColor: '#63b3ed',
  },
  bsBarTarget: {
    backgroundColor: '#68d391',
  },
  bsBarText: {
    fontSize: 10,
    color: '#2d3748',
    marginBottom: 4,
    fontWeight: '600',
  },
  bsFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  bsFootText: {
    color: '#718096',
    fontSize: 12,
  },
  bsDone: {
    color: '#2e7d32',
    fontWeight: '700',
  },
  lsContainer: {
    marginHorizontal: 15,
  },
  lsCard: {
    backgroundColor: '#fff7ed',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#fde4d6',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.08,
    shadowRadius: 12,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  lsHeader: {
    marginBottom: 8,
  },
  lsTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#7c2d12',
  },
  lsSubtitle: {
    fontSize: 13,
    color: '#9a3412',
    marginTop: 4,
  },
  lsControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  lsBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff1e6',
    borderWidth: 1,
    borderColor: '#ffe1cc',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  lsBtnActive: {
    backgroundColor: '#ffe8d6',
    borderColor: '#ffd9bf',
  },
  lsIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  lsBtnText: {
    color: '#7c2d12',
    fontSize: 13,
    fontWeight: '600',
  },
  lsTargetRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginTop: 6,
  },
  lsChip: {
    backgroundColor: '#ffedd5',
    borderWidth: 1,
    borderColor: '#fed7aa',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  lsChipText: {
    color: '#7c2d12',
    fontSize: 12,
    fontWeight: '600',
  },
  lsBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  lsBar: {
    width: 16,
    backgroundColor: '#fdba74',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  lsBarCur: {
    backgroundColor: '#fb923c',
  },
  lsBarTarget: {
    backgroundColor: '#22c55e',
  },
  lsBarText: {
    fontSize: 10,
    color: '#7c2d12',
    marginBottom: 4,
    fontWeight: '600',
  },
  lsFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  lsFootText: {
    color: '#9a3412',
    fontSize: 12,
  },
  lsDone: {
    color: '#16a34a',
    fontWeight: '700',
  },
  bbContainer: {
    marginHorizontal: 15,
  },
  bbCard: {
    backgroundColor: '#1B1633',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#2A2350',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.2,
    shadowRadius: 14,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  bbHeader: {
    marginBottom: 8,
  },
  bbTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#EAE7FF',
  },
  bbSubtitle: {
    fontSize: 13,
    color: '#B9B4E6',
    marginTop: 4,
  },
  bbControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  bbBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#231C44',
    borderWidth: 1,
    borderColor: '#3A2F6D',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  bbBtnActive: {
    backgroundColor: '#2B2356',
    borderColor: '#4C3F8F',
  },
  bbIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  bbBtnText: {
    color: '#EAE7FF',
    fontSize: 13,
    fontWeight: '600',
  },
  bbChips: {
    flexDirection: 'row',
    marginLeft: 8,
  },
  bbChip: {
    backgroundColor: '#211B3F',
    borderWidth: 1,
    borderColor: '#372F6B',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  bbChipActive: {
    backgroundColor: '#2A2350',
    borderColor: '#4D3F91',
  },
  bbChipText: {
    color: '#DCD7FF',
    fontSize: 12,
    fontWeight: '600',
  },
  bbBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  bbBar: {
    width: 16,
    backgroundColor: '#7C3AED',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  bbBarHL: {
    backgroundColor: '#A78BFA',
  },
  bbBarSwap: {
    backgroundColor: '#F87171',
  },
  bbBarSorted: {
    backgroundColor: '#6B7280',
  },
  bbBarText: {
    fontSize: 10,
    color: '#EAE7FF',
    marginBottom: 4,
    fontWeight: '600',
  },
  bbFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  bbFootText: {
    color: '#B9B4E6',
    fontSize: 12,
  },
  bbDone: {
    color: '#34D399',
    fontWeight: '700',
  },
  ssContainer: {
    marginHorizontal: 15,
  },
  ssCard: {
    backgroundColor: '#07242D',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#0D3A46',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.18,
    shadowRadius: 14,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  ssHeader: {
    marginBottom: 8,
  },
  ssTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#DFF6FF',
  },
  ssSubtitle: {
    fontSize: 13,
    color: '#96C9D6',
    marginTop: 4,
  },
  ssControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  ssBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#0B2E37',
    borderWidth: 1,
    borderColor: '#134753',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  ssBtnActive: {
    backgroundColor: '#0E3B46',
    borderColor: '#1B5C6A',
  },
  ssIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  ssBtnText: {
    color: '#DFF6FF',
    fontSize: 13,
    fontWeight: '600',
  },
  ssChips: {
    flexDirection: 'row',
    marginLeft: 8,
  },
  ssChip: {
    backgroundColor: '#0A2B35',
    borderWidth: 1,
    borderColor: '#134753',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  ssChipActive: {
    backgroundColor: '#0E3B46',
    borderColor: '#1B5C6A',
  },
  ssChipText: {
    color: '#CDEBF4',
    fontSize: 12,
    fontWeight: '600',
  },
  ssBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  ssBar: {
    width: 16,
    backgroundColor: '#22D3EE',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  ssBarHL: {
    backgroundColor: '#38BDF8',
  },
  ssBarMin: {
    backgroundColor: '#2DD4BF',
  },
  ssBarSwap: {
    backgroundColor: '#F59E0B',
  },
  ssBarSorted: {
    backgroundColor: '#6B7280',
  },
  ssBarText: {
    fontSize: 10,
    color: '#DFF6FF',
    marginBottom: 4,
    fontWeight: '600',
  },
  ssFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  ssFootText: {
    color: '#96C9D6',
    fontSize: 12,
  },
  ssDone: {
    color: '#34D399',
    fontWeight: '700',
  },
  bstContainer: {
    marginHorizontal: 15,
  },
  bstCard: {
    backgroundColor: '#0b1f16',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#163529',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.18,
    shadowRadius: 14,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  bstHeader: {
    marginBottom: 8,
  },
  bstTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#e7f6ef',
  },
  bstSubtitle: {
    fontSize: 13,
    color: '#a6c9bb',
    marginTop: 4,
  },
  bstControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  bstBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#123426',
    borderWidth: 1,
    borderColor: '#1a4a37',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  bstBtnActive: {
    backgroundColor: '#154234',
    borderColor: '#22624a',
  },
  bstIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  bstBtnText: {
    color: '#e7f6ef',
    fontSize: 13,
    fontWeight: '600',
  },
  bstChip: {
    backgroundColor: '#0f2c20',
    borderWidth: 1,
    borderColor: '#1a4a37',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  bstChipText: {
    color: '#cde7dc',
    fontSize: 12,
    fontWeight: '600',
  },
  bstTree: {
    position: 'relative',
    minHeight: 420,
    backgroundColor: '#0e251c',
    borderRadius: 12,
    borderWidth: 1,
    borderColor: '#153a2c',
    padding: 8,
  },
  bstNode: {
    position: 'absolute',
    width: 40,
    height: 40,
    borderRadius: 20,
    backgroundColor: '#1f6f4d',
    borderWidth: 2,
    borderColor: '#2e8a64',
    justifyContent: 'center',
    alignItems: 'center',
  },
  bstNodeHL: {
    backgroundColor: '#239f6b',
    borderColor: '#3ac18b',
  },
  bstNodeFound: {
    backgroundColor: '#1fbf72',
    borderColor: '#57d69b',
  },
  bstNodeText: {
    color: '#e7f6ef',
    fontSize: 12,
    fontWeight: '700',
  },
  bstLine: {
    position: 'absolute',
    backgroundColor: '#1a4a37',
  },
  bstFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  bstFootText: {
    color: '#a6c9bb',
    fontSize: 12,
  },
  bstDone: {
    color: '#43d998',
    fontWeight: '700',
  },
  rsContainer: {
    marginHorizontal: 15,
  },
  rsCard: {
    backgroundColor: '#FFF1F5',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#FFD6E2',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.12,
    shadowRadius: 12,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  rsHeader: {
    marginBottom: 8,
  },
  rsTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#7A163B',
  },
  rsSubtitle: {
    fontSize: 13,
    color: '#B0435B',
    marginTop: 4,
  },
  rsControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  rsBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#FFE4EA',
    borderWidth: 1,
    borderColor: '#FFC9D6',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  rsBtnActive: {
    backgroundColor: '#FFDCE5',
    borderColor: '#FFB3C6',
  },
  rsIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  rsBtnText: {
    color: '#7A163B',
    fontSize: 13,
    fontWeight: '600',
  },
  rsChips: {
    flexDirection: 'row',
    marginLeft: 8,
  },
  rsChip: {
    backgroundColor: '#FFE8EE',
    borderWidth: 1,
    borderColor: '#FFD1DE',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  rsChipActive: {
    backgroundColor: '#FFDCE5',
    borderColor: '#FFB3C6',
  },
  rsChipText: {
    color: '#7A163B',
    fontSize: 12,
    fontWeight: '600',
  },
  rsBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  rsBar: {
    width: 16,
    backgroundColor: '#FB7185',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  rsBarText: {
    fontSize: 10,
    color: '#7A163B',
    marginBottom: 4,
    fontWeight: '600',
  },
  rsBuckets: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    paddingHorizontal: 6,
    paddingVertical: 8,
  },
  rsBucket: {
    width: (width - 30) / 5 - 6,
    backgroundColor: '#FFEFF3',
    borderWidth: 1,
    borderColor: '#FFD6E2',
    borderRadius: 10,
    paddingVertical: 8,
    paddingHorizontal: 8,
    margin: 3,
    alignItems: 'center',
  },
  rsBucketLabel: {
    color: '#7A163B',
    fontSize: 12,
    fontWeight: '700',
    marginBottom: 4,
  },
  rsBucketCount: {
    color: '#B0435B',
    fontSize: 12,
    fontWeight: '600',
  },
  rsFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  rsFootText: {
    color: '#B0435B',
    fontSize: 12,
  },
  rsDone: {
    color: '#22C55E',
    fontWeight: '700',
  },
  bkContainer: {
    marginHorizontal: 15,
  },
  bkCard: {
    backgroundColor: '#FFF7E6',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#FDEBC5',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.1,
    shadowRadius: 12,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  bkHeader: {
    marginBottom: 8,
  },
  bkTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#7A4B16',
  },
  bkSubtitle: {
    fontSize: 13,
    color: '#B06C2E',
    marginTop: 4,
  },
  bkControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  bkBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#FFF0D6',
    borderWidth: 1,
    borderColor: '#F7E2B8',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  bkBtnActive: {
    backgroundColor: '#FFE8C7',
    borderColor: '#F5D79F',
  },
  bkIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  bkBtnText: {
    color: '#7A4B16',
    fontSize: 13,
    fontWeight: '600',
  },
  bkChips: {
    flexDirection: 'row',
    marginLeft: 8,
  },
  bkChip: {
    backgroundColor: '#FFF3DD',
    borderWidth: 1,
    borderColor: '#F7E2B8',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  bkChipActive: {
    backgroundColor: '#FFE8C7',
    borderColor: '#F5D79F',
  },
  bkChipText: {
    color: '#7A4B16',
    fontSize: 12,
    fontWeight: '600',
  },
  bkBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  bkBar: {
    width: 16,
    backgroundColor: '#F59E0B',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  bkBarHL: {
    backgroundColor: '#FBBF24',
  },
  bkBarCollected: {
    backgroundColor: '#6B7280',
  },
  bkBarText: {
    fontSize: 10,
    color: '#7A4B16',
    marginBottom: 4,
    fontWeight: '600',
  },
  bkBuckets: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    paddingHorizontal: 6,
    paddingVertical: 8,
  },
  bkBucket: {
    width: (width - 30) / 4 - 6,
    backgroundColor: '#FEF7E6',
    borderWidth: 1,
    borderColor: '#FDEBC5',
    borderRadius: 10,
    paddingVertical: 8,
    paddingHorizontal: 8,
    margin: 3,
    alignItems: 'center',
  },
  bkBucketActive: {
    borderColor: '#2BBBAD',
  },
  bkBucketLabel: {
    color: '#7A4B16',
    fontSize: 12,
    fontWeight: '700',
    marginBottom: 4,
  },
  bkBucketCount: {
    color: '#B06C2E',
    fontSize: 12,
    fontWeight: '600',
  },
  bkFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  bkFootText: {
    color: '#B06C2E',
    fontSize: 12,
  },
  bkDone: {
    color: '#22C55E',
    fontWeight: '700',
  },
  shContainer: {
    marginHorizontal: 15,
  },
  shCard: {
    backgroundColor: '#0F172A',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#1F2937',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.2,
    shadowRadius: 14,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  shHeader: {
    marginBottom: 8,
  },
  shTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#E2E8F0',
  },
  shSubtitle: {
    fontSize: 13,
    color: '#94A3B8',
    marginTop: 4,
  },
  shControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  shBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#111827',
    borderWidth: 1,
    borderColor: '#1F2937',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  shBtnActive: {
    backgroundColor: '#1F2937',
    borderColor: '#374151',
  },
  shIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  shBtnText: {
    color: '#E2E8F0',
    fontSize: 13,
    fontWeight: '600',
  },
  shChips: {
    flexDirection: 'row',
    marginLeft: 8,
  },
  shChip: {
    backgroundColor: '#0B1220',
    borderWidth: 1,
    borderColor: '#1F2937',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  shChipActive: {
    backgroundColor: '#172133',
    borderColor: '#334155',
  },
  shChipText: {
    color: '#C7D2FE',
    fontSize: 12,
    fontWeight: '600',
  },
  shBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  shBar: {
    width: 16,
    backgroundColor: '#22D3EE',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  shBarHL: {
    backgroundColor: '#A78BFA',
  },
  shBarSwap: {
    backgroundColor: '#F87171',
  },
  shBarGroup: {
    backgroundColor: '#0EA5E9',
  },
  shBarText: {
    fontSize: 10,
    color: '#E2E8F0',
    marginBottom: 4,
    fontWeight: '600',
  },
  shFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  shFootText: {
    color: '#94A3B8',
    fontSize: 12,
  },
  shDone: {
    color: '#34D399',
    fontWeight: '700',
  },
  ctContainer: {
    marginHorizontal: 15,
  },
  ctCard: {
    backgroundColor: '#ECFDF5',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#A7F3D0',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.1,
    shadowRadius: 12,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  ctHeader: {
    marginBottom: 8,
  },
  ctTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#065F46',
  },
  ctSubtitle: {
    fontSize: 13,
    color: '#10B981',
    marginTop: 4,
  },
  ctControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  ctBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#D1FAE5',
    borderWidth: 1,
    borderColor: '#A7F3D0',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  ctBtnActive: {
    backgroundColor: '#C7F9D7',
    borderColor: '#6EE7B7',
  },
  ctIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  ctBtnText: {
    color: '#065F46',
    fontSize: 13,
    fontWeight: '600',
  },
  ctChips: {
    flexDirection: 'row',
    marginLeft: 8,
  },
  ctChip: {
    backgroundColor: '#E7FBEF',
    borderWidth: 1,
    borderColor: '#A7F3D0',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  ctChipActive: {
    backgroundColor: '#C7F9D7',
    borderColor: '#6EE7B7',
  },
  ctChipText: {
    color: '#0F766E',
    fontSize: 12,
    fontWeight: '600',
  },
  ctBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  ctBar: {
    width: 16,
    backgroundColor: '#34D399',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  ctBarHL: {
    backgroundColor: '#10B981',
  },
  ctBarPlaced: {
    backgroundColor: '#6B7280',
  },
  ctBarText: {
    fontSize: 10,
    color: '#065F46',
    marginBottom: 4,
    fontWeight: '600',
  },
  ctBuckets: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    paddingHorizontal: 6,
    paddingVertical: 8,
  },
  ctBucket: {
    width: (width - 30) / 4 - 6,
    backgroundColor: '#F0FFF4',
    borderWidth: 1,
    borderColor: '#A7F3D0',
    borderRadius: 10,
    paddingVertical: 8,
    paddingHorizontal: 8,
    margin: 3,
    alignItems: 'center',
  },
  ctBucketActive: {
    borderColor: '#0EA5E9',
  },
  ctBucketLabel: {
    color: '#0F766E',
    fontSize: 12,
    fontWeight: '700',
    marginBottom: 4,
  },
  ctBucketCount: {
    color: '#10B981',
    fontSize: 12,
    fontWeight: '600',
  },
  ctFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  ctFootText: {
    color: '#10B981',
    fontSize: 12,
  },
  ctDone: {
    color: '#22C55E',
    fontWeight: '700',
  },
});

const ICON_BASE64_MS = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_BST = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  insert: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  remove: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  find: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_BUBBLE = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_SELECT = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_SHELL = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_COUNT = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_RADIX = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_BUCKET = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_BS = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  search: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_LS = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const MergeSortVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [steps, setSteps] = useState<{ arr: number[]; highlights: number[] }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(600);
  const timerRef = React.useRef<any>(null);
  const maxBars = 18;

  const genArray = () => {
    const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
    setArr(a);
    const s = buildSteps(a);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (a: number[]) => {
    const n = a.length;
    let src = a.slice();
    const out: { arr: number[]; highlights: number[] }[] = [{ arr: src.slice(), highlights: [] }];
    for (let size = 1; size < n; size *= 2) {
      for (let left = 0; left < n; left += 2 * size) {
        const mid = Math.min(left + size, n);
        const right = Math.min(left + 2 * size, n);
        let i = left, j = mid;
        const merged: number[] = [];
        while (i < mid && j < right) {
          if (src[i] <= src[j]) { merged.push(src[i]); i++; } else { merged.push(src[j]); j++; }
        }
        while (i < mid) { merged.push(src[i]); i++; }
        while (j < right) { merged.push(src[j]); j++; }
        for (let k = 0; k < merged.length; k++) src[left + k] = merged[k];
        out.push({ arr: src.slice(), highlights: Array.from({ length: merged.length }, (_, k) => left + k) });
      }
    }
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      setArr(steps[next].arr);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const isDone = idx === steps.length - 1 && steps.length > 1;

  return (
    <View style={styles.msCard}>
      <View style={styles.msHeader}> 
        <Text style={styles.msTitle}>归并排序 · 霓虹风格</Text>
        <Text style={styles.msSubtitle}>播放、步进、重置、随机</Text>
      </View>
      <View style={styles.msControls}>
        <TouchableOpacity style={[styles.msBtn, playing ? styles.msBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_MS.pause : ICON_BASE64_MS.play }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.msBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_MS.step }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.msBtn} onPress={resetSort} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_MS.reset }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.msBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_MS.shuffle }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>随机</Text>
        </TouchableOpacity>
        <View style={styles.msChips}>
          <TouchableOpacity style={styles.msChip} onPress={() => setSpeed(900)}><Text style={styles.msChipText}>0.5×</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.msChip, styles.msChipActive]} onPress={() => setSpeed(600)}><Text style={styles.msChipText}>1×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.msChip} onPress={() => setSpeed(300)}><Text style={styles.msChipText}>2×</Text></TouchableOpacity>
        </View>
      </View>
      <View style={styles.msBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const hl = steps[idx]?.highlights.includes(i);
          return (
            <View key={i} style={[styles.msBar, { height: h }, hl ? styles.msBarHL : null]}>
              <Text style={styles.msBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.msFooterRow}>
        <Text style={styles.msFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
        <Text style={[styles.msFootText, isDone ? styles.msDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
      </View>
    </View>
  );
};
const BinarySearchVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [target, setTarget] = useState<number>(0);
  const [steps, setSteps] = useState<{ low: number; high: number; mid: number; found: boolean }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(700);
  const timerRef = React.useRef<any>(null);
  const maxBars = 20;

  const genArray = () => {
    const base = Array.from({ length: maxBars }, () => Math.floor(10 + Math.random() * 150));
    const sorted = base.sort((a, b) => a - b);
    setArr(sorted);
    const t = sorted[Math.floor(Math.random() * sorted.length)];
    setTarget(t);
    const s = buildSteps(sorted, t);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (a: number[], t: number) => {
    let low = 0, high = a.length - 1;
    const out: { low: number; high: number; mid: number; found: boolean }[] = [];
    while (low <= high) {
      const mid = Math.floor((low + high) / 2);
      const found = a[mid] === t;
      out.push({ low, high, mid, found });
      if (found) break;
      if (a[mid] < t) low = mid + 1; else high = mid - 1;
    }
    if (out.length === 0) out.push({ low: 0, high: a.length - 1, mid: -1, found: false });
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSearch = () => { const s = buildSteps(arr.slice(), target); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();
  const incTarget = () => setTarget(t => { const nt = t + 1; const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });
  const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const isDone = idx === steps.length - 1 && steps.length > 0;
  const cur = steps[idx] || { low: 0, high: arr.length - 1, mid: -1, found: false };

  return (
    <View style={styles.bsCard}>
      <View style={styles.bsHeader}> 
        <Text style={styles.bsTitle}>二分查找 · 柔和风格</Text>
        <Text style={styles.bsSubtitle}>播放、步进、重置、随机、目标调整</Text>
      </View>
      <View style={styles.bsControls}>
        <TouchableOpacity style={[styles.bsBtn, playing ? styles.bsBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_BS.pause : ICON_BASE64_BS.play }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bsBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BS.step }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bsBtn} onPress={resetSearch} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BS.reset }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bsBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BS.shuffle }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>随机</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.bsTargetRow}>
        <TouchableOpacity style={styles.bsChip} onPress={decTarget}><Text style={styles.bsChipText}>目标 -</Text></TouchableOpacity>
        <View style={styles.bsChip}><Text style={styles.bsChipText}>当前目标:{target}</Text></View>
        <TouchableOpacity style={styles.bsChip} onPress={incTarget}><Text style={styles.bsChipText}>目标 +</Text></TouchableOpacity>
        <View style={styles.bsChip}><Text style={styles.bsChipText}>范围:{cur.low} - {cur.high}</Text></View>
      </View>
      <View style={styles.bsBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const inRange = i >= cur.low && i <= cur.high;
          const isMid = i === cur.mid;
          const isTarget = v === target && inRange;
          return (
            <View key={i} style={[styles.bsBar, { height: h }, !inRange ? styles.bsBarDim : null, isMid ? styles.bsBarMid : null, isTarget ? styles.bsBarTarget : null]}>
              <Text style={styles.bsBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.bsFooterRow}>
        <Text style={styles.bsFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
        <Text style={[styles.bsFootText, isDone && cur.found ? styles.bsDone : null]}>{isDone ? (cur.found ? '已找到目标' : '未找到') : '进行中'}</Text>
      </View>
    </View>
  );
};
const LinearSearchVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [target, setTarget] = useState<number>(0);
  const [steps, setSteps] = useState<{ idx: number; found: boolean }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(700);
  const timerRef = React.useRef<any>(null);
  const maxBars = 22;

  const genArray = () => {
    const base = Array.from({ length: maxBars }, () => Math.floor(10 + Math.random() * 150));
    setArr(base);
    const t = base[Math.floor(Math.random() * base.length)];
    setTarget(t);
    const s = buildSteps(base, t);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (a: number[], t: number) => {
    const out: { idx: number; found: boolean }[] = [];
    for (let i = 0; i < a.length; i++) {
      const found = a[i] === t;
      out.push({ idx: i, found });
      if (found) break;
    }
    if (out.length === 0) out.push({ idx: -1, found: false });
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSearch = () => { const s = buildSteps(arr.slice(), target); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();
  const incTarget = () => setTarget(t => { const nt = t + 1; const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });
  const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const isDone = idx === steps.length - 1 && steps.length > 0;
  const cur = steps[idx] || { idx: -1, found: false };

  return (
    <View style={styles.lsCard}>
      <View style={styles.lsHeader}> 
        <Text style={styles.lsTitle}>线性查找 · 暖色风格</Text>
        <Text style={styles.lsSubtitle}>播放、步进、重置、随机、目标调整</Text>
      </View>
      <View style={styles.lsControls}>
        <TouchableOpacity style={[styles.lsBtn, playing ? styles.lsBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_LS.pause : ICON_BASE64_LS.play }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.lsBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_LS.step }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.lsBtn} onPress={resetSearch} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_LS.reset }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.lsBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_LS.shuffle }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>随机</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.lsTargetRow}>
        <TouchableOpacity style={styles.lsChip} onPress={decTarget}><Text style={styles.lsChipText}>目标 -</Text></TouchableOpacity>
        <View style={styles.lsChip}><Text style={styles.lsChipText}>当前目标:{target}</Text></View>
        <TouchableOpacity style={styles.lsChip} onPress={incTarget}><Text style={styles.lsChipText}>目标 +</Text></TouchableOpacity>
        <View style={styles.lsChip}><Text style={styles.lsChipText}>索引:{cur.idx >= 0 ? cur.idx : '-'}</Text></View>
      </View>
      <View style={styles.lsBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const isCur = i === cur.idx;
          const isTarget = v === target;
          return (
            <View key={i} style={[styles.lsBar, { height: h }, isCur ? styles.lsBarCur : null, isTarget ? styles.lsBarTarget : null]}>
              <Text style={styles.lsBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.lsFooterRow}>
        <Text style={styles.lsFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
        <Text style={[styles.lsFootText, isDone && cur.found ? styles.lsDone : null]}>{isDone ? (cur.found ? '已找到目标' : '未找到') : '进行中'}</Text>
      </View>
    </View>
  );
};
const BSTVisualizer: React.FC = () => {
  const [root, setRoot] = useState<any>(null);
  const [target, setTarget] = useState<number>(0);
  const [steps, setSteps] = useState<number[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(700);
  const timerRef = React.useRef<any>(null);
  const levelGap = 90;
  const padding = 24;

  const insert = (k: number) => {
    const node = { key: k, left: null, right: null };
    if (!root) { setRoot(node); return; }
    const r = JSON.parse(JSON.stringify(root));
    let cur: any = r;
    while (true) {
      if (k < cur.key) { if (cur.left) cur = cur.left; else { cur.left = node; break; } }
      else if (k > cur.key) { if (cur.right) cur = cur.right; else { cur.right = node; break; } }
      else break;
    }
    setRoot(r);
  };

  const findPath = (k: number) => {
    const path: number[] = [];
    let cur: any = root;
    while (cur) {
      path.push(cur.key);
      if (k === cur.key) break;
      cur = k < cur.key ? cur.left : cur.right;
    }
    return path;
  };

  const minNode = (n: any) => { let c = n; while (c && c.left) c = c.left; return c; };
  const removeKey = (k: number) => {
    const del = (n: any, v: number): any => {
      if (!n) return null;
      if (v < n.key) { n.left = del(n.left, v); return n; }
      if (v > n.key) { n.right = del(n.right, v); return n; }
      if (!n.left) return n.right;
      if (!n.right) return n.left;
      const succ = minNode(n.right);
      n.key = succ.key;
      n.right = del(n.right, succ.key);
      return n;
    };
    const r = JSON.parse(JSON.stringify(root));
    setRoot(del(r, k));
  };

  const buildRandom = () => {
    let r: any = null;
    const values = Array.from({ length: 10 }, () => Math.floor(10 + Math.random() * 90));
    values.forEach(v => { r = insertInto(r, v); });
    setRoot(r);
    const t = values[Math.floor(Math.random() * values.length)];
    setTarget(t);
    const p = findPathOn(r, t);
    setSteps(p);
    setIdx(0);
    setPlaying(false);
  };

  const insertInto = (r: any, k: number) => {
    const node = { key: k, left: null, right: null };
    if (!r) return node;
    let cur = r;
    while (true) {
      if (k < cur.key) { if (cur.left) cur = cur.left; else { cur.left = node; break; } }
      else if (k > cur.key) { if (cur.right) cur = cur.right; else { cur.right = node; break; } }
      else break;
    }
    return r;
  };

  const findPathOn = (r: any, k: number) => {
    const path: number[] = [];
    let cur = r;
    while (cur) {
      path.push(cur.key);
      if (k === cur.key) break;
      cur = k < cur.key ? cur.left : cur.right;
    }
    return path;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSearch = () => { const p = findPath(target); setSteps(p); setIdx(0); setPlaying(false); };
  const shuffle = () => buildRandom();
  const incTarget = () => setTarget(t => { const nt = t + 1; const p = findPath(nt); setSteps(p); setIdx(0); return nt; });
  const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const p = findPath(nt); setSteps(p); setIdx(0); return nt; });
  const doInsert = () => { const v = Math.floor(10 + Math.random() * 90); insert(v); const p = findPath(target); setSteps(p); setIdx(0); };
  const doRemove = () => { removeKey(target); const p = findPath(target); setSteps(p); setIdx(0); };
  const clearTree = () => { setRoot(null); setSteps([]); setIdx(0); setPlaying(false); };

  React.useEffect(() => { buildRandom(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const layout = buildLayout(root);
  const curKey = steps[idx];
  const isDone = steps.length > 0 && idx === steps.length - 1 && curKey === target;

  return (
    <View style={styles.bstCard}>
      <View style={styles.bstHeader}> 
        <Text style={styles.bstTitle}>二叉搜索树 · 森林风格</Text>
        <Text style={styles.bstSubtitle}>播放、步进、插入、删除、目标调整</Text>
      </View>
      <View style={styles.bstControls}>
        <TouchableOpacity style={[styles.bstBtn, playing ? styles.bstBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_BST.pause : ICON_BASE64_BST.play }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.step }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={resetSearch} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.reset }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.shuffle }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>随机</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={doInsert} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.insert }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>插入</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={doRemove} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.remove }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>删除</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.bstControls}>
        <TouchableOpacity style={styles.bstChip} onPress={decTarget}><Text style={styles.bstChipText}>目标 -</Text></TouchableOpacity>
        <View style={styles.bstChip}><Text style={styles.bstChipText}>当前目标:{target}</Text></View>
        <TouchableOpacity style={styles.bstChip} onPress={incTarget}><Text style={styles.bstChipText}>目标 +</Text></TouchableOpacity>
      </View>
      <View style={styles.bstTree}>
        {layout.lines.map((l: any, i: number) => (
          <View key={`line-${i}`} style={[styles.bstLine, { left: l.vx, top: l.vy, width: 2, height: l.vh }]} />
        ))}
        {layout.lines.map((l: any, i: number) => (
          <View key={`hline-${i}`} style={[styles.bstLine, { left: l.hx, top: l.hy, width: l.hw, height: 2 }]} />
        ))}
        {layout.nodes.map((n: any) => (
          <View key={`node-${n.key}-${n.x}-${n.y}`} style={[styles.bstNode, { left: n.x - 20, top: n.y - 20 }, n.key === curKey ? styles.bstNodeHL : null, n.key === target && curKey === target ? styles.bstNodeFound : null]}>
            <Text style={styles.bstNodeText}>{n.key}</Text>
          </View>
        ))}
      </View>
      <View style={styles.bstFooterRow}>
        <Text style={styles.bstFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
        <Text style={[styles.bstFootText, isDone ? styles.bstDone : null]}>{isDone ? '已找到目标' : '进行中'}</Text>
      </View>
    </View>
  );

  function buildLayout(r: any) {
    const nodes: any[] = [];
    const lines: any[] = [];
    if (!r) return { nodes, lines };
    const queue: any[] = [{ n: r, level: 0, idx: 0 }];
    const levels: any = {};
    while (queue.length) {
      const item = queue.shift();
      const lvl = item.level;
      levels[lvl] = levels[lvl] || [];
      levels[lvl].push(item.n);
      if (item.n.left) queue.push({ n: item.n.left, level: lvl + 1, idx: 0 });
      if (item.n.right) queue.push({ n: item.n.right, level: lvl + 1, idx: 0 });
    }
    const contentWidth = width - padding * 2;
    const posMap: any = new Map();
    const maxLevel = Math.max(0, ...Object.keys(levels).map(v => parseInt(v, 10)));
    for (let l = 0; l <= maxLevel; l++) {
      const row = levels[l] || [];
      const count = row.length || 1;
      for (let i = 0; i < row.length; i++) {
        const x = padding + Math.round(((i + 1) * contentWidth) / (count + 1));
        const y = padding + l * levelGap + 20;
        posMap.set(row[i], { x, y });
        nodes.push({ key: row[i].key, x, y });
      }
    }
    const addLines = (n: any) => {
      const p = posMap.get(n);
      if (n.left) {
        const c = posMap.get(n.left);
        const vy = p.y + 20;
        const vh = c.y - p.y - 20;
        const vx = p.x;
        const hx = Math.min(p.x, c.x);
        const hy = c.y;
        const hw = Math.abs(c.x - p.x);
        lines.push({ vx, vy, vh, hx, hy, hw });
        addLines(n.left);
      }
      if (n.right) {
        const c = posMap.get(n.right);
        const vy = p.y + 20;
        const vh = c.y - p.y - 20;
        const vx = p.x;
        const hx = Math.min(p.x, c.x);
        const hy = c.y;
        const hw = Math.abs(c.x - p.x);
        lines.push({ vx, vy, vh, hx, hy, hw });
        addLines(n.right);
      }
    };
    addLines(r);
    return { nodes, lines };
  }
};

const BubbleSortVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [steps, setSteps] = useState<{ arr: number[]; a: number; b: number; swapped: boolean; sorted: number }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(600);
  const timerRef = React.useRef<any>(null);
  const maxBars = 18;

  const genArray = () => {
    const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
    setArr(a);
    const s = buildSteps(a);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (base: number[]) => {
    const a = base.slice();
    const n = a.length;
    const out: { arr: number[]; a: number; b: number; swapped: boolean; sorted: number }[] = [{ arr: a.slice(), a: -1, b: -1, swapped: false, sorted: n }];
    for (let i = n - 1; i > 0; i--) {
      for (let j = 0; j < i; j++) {
        const swapped = a[j] > a[j + 1];
        if (swapped) {
          const t = a[j];
          a[j] = a[j + 1];
          a[j + 1] = t;
        }
        out.push({ arr: a.slice(), a: j, b: j + 1, swapped, sorted: i });
      }
    }
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      setArr(steps[next].arr);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const cur = steps[idx] || { a: -1, b: -1, swapped: false, sorted: arr.length };
  const isDone = idx === steps.length - 1 && steps.length > 1;

  return (
    <View style={styles.bbCard}>
      <View style={styles.bbHeader}> 
        <Text style={styles.bbTitle}>冒泡排序 · 玻璃风格</Text>
        <Text style={styles.bbSubtitle}>播放、步进、重置、随机与速度</Text>
      </View>
      <View style={styles.bbControls}>
        <TouchableOpacity style={[styles.bbBtn, playing ? styles.bbBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_BUBBLE.pause : ICON_BASE64_BUBBLE.play }} style={styles.bbIcon} />
          <Text style={styles.bbBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bbBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BUBBLE.step }} style={styles.bbIcon} />
          <Text style={styles.bbBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bbBtn} onPress={resetSort} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BUBBLE.reset }} style={styles.bbIcon} />
          <Text style={styles.bbBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bbBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BUBBLE.shuffle }} style={styles.bbIcon} />
          <Text style={styles.bbBtnText}>随机</Text>
        </TouchableOpacity>
        <View style={styles.bbChips}>
          <TouchableOpacity style={styles.bbChip} onPress={() => setSpeed(900)}><Text style={styles.bbChipText}>0.5×</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.bbChip, styles.bbChipActive]} onPress={() => setSpeed(600)}><Text style={styles.bbChipText}>1×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.bbChip} onPress={() => setSpeed(300)}><Text style={styles.bbChipText}>2×</Text></TouchableOpacity>
        </View>
      </View>
      <View style={styles.bbBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const inCompare = i === cur.a || i === cur.b;
          const swapped = inCompare && cur.swapped;
          const sorted = i >= cur.sorted;
          return (
            <View key={i} style={[styles.bbBar, { height: h }, inCompare ? styles.bbBarHL : null, swapped ? styles.bbBarSwap : null, sorted ? styles.bbBarSorted : null]}>
              <Text style={styles.bbBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.bbFooterRow}>
        <Text style={styles.bbFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
        <Text style={[styles.bbFootText, isDone ? styles.bbDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
      </View>
    </View>
  );
};

const SelectionSortVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [steps, setSteps] = useState<{ arr: number[]; i: number; j: number; minIdx: number; swapped: boolean; sortedStart: number }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(600);
  const timerRef = React.useRef<any>(null);
  const maxBars = 18;

  const genArray = () => {
    const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
    setArr(a);
    const s = buildSteps(a);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (base: number[]) => {
    const a = base.slice();
    const n = a.length;
    const out: { arr: number[]; i: number; j: number; minIdx: number; swapped: boolean; sortedStart: number }[] = [{ arr: a.slice(), i: -1, j: -1, minIdx: -1, swapped: false, sortedStart: 0 }];
    for (let i = 0; i < n - 1; i++) {
      let minIdx = i;
      for (let j = i + 1; j < n; j++) {
        if (a[j] < a[minIdx]) minIdx = j;
        out.push({ arr: a.slice(), i, j, minIdx, swapped: false, sortedStart: i });
      }
      if (minIdx !== i) {
        const t = a[i]; a[i] = a[minIdx]; a[minIdx] = t;
        out.push({ arr: a.slice(), i, j: minIdx, minIdx, swapped: true, sortedStart: i });
      }
      out.push({ arr: a.slice(), i, j: -1, minIdx, swapped: false, sortedStart: i + 1 });
    }
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      setArr(steps[next].arr);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const cur = steps[idx] || { i: -1, j: -1, minIdx: -1, swapped: false, sortedStart: 0 };
  const isDone = idx === steps.length - 1 && steps.length > 1;

  return (
    <View style={styles.ssCard}>
      <View style={styles.ssHeader}> 
        <Text style={styles.ssTitle}>选择排序 · 海洋风格</Text>
        <Text style={styles.ssSubtitle}>播放、步进、重置、随机与速度</Text>
      </View>
      <View style={styles.ssControls}>
        <TouchableOpacity style={[styles.ssBtn, playing ? styles.ssBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_SELECT.pause : ICON_BASE64_SELECT.play }} style={styles.ssIcon} />
          <Text style={styles.ssBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.ssBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_SELECT.step }} style={styles.ssIcon} />
          <Text style={styles.ssBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.ssBtn} onPress={resetSort} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_SELECT.reset }} style={styles.ssIcon} />
          <Text style={styles.ssBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.ssBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_SELECT.shuffle }} style={styles.ssIcon} />
          <Text style={styles.ssBtnText}>随机</Text>
        </TouchableOpacity>
        <View style={styles.ssChips}>
          <TouchableOpacity style={styles.ssChip} onPress={() => setSpeed(900)}><Text style={styles.ssChipText}>0.5×</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.ssChip, styles.ssChipActive]} onPress={() => setSpeed(600)}><Text style={styles.ssChipText}>1×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.ssChip} onPress={() => setSpeed(300)}><Text style={styles.ssChipText}>2×</Text></TouchableOpacity>
        </View>
      </View>
      <View style={styles.ssBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const inCompare = i === cur.i || i === cur.j;
          const isMin = i === cur.minIdx;
          const swapped = inCompare && cur.swapped;
          const sorted = i < cur.sortedStart;
          return (
            <View key={i} style={[styles.ssBar, { height: h }, inCompare ? styles.ssBarHL : null, isMin ? styles.ssBarMin : null, swapped ? styles.ssBarSwap : null, sorted ? styles.ssBarSorted : null]}>
              <Text style={styles.ssBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.ssFooterRow}>
        <Text style={styles.ssFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
        <Text style={[styles.ssFootText, isDone ? styles.ssDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
      </View>
    </View>
  );
};

const RadixSortVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [steps, setSteps] = useState<{ arr: number[]; buckets: number[][]; digit: number; phase: 'dist' | 'collect'; base: number }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(600);
  const [radix, setRadix] = useState(10);
  const timerRef = React.useRef<any>(null);
  const maxBars = 18;

  const genArray = () => {
    const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 980));
    setArr(a);
    const s = buildSteps(a, radix);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const maxDigits = (a: number[], base: number) => {
    const m = Math.max(...a, 0);
    let d = 0; let v = m;
    while (v > 0) { d++; v = Math.floor(v / base); }
    return Math.max(1, d);
  };

  const buildSteps = (baseArr: number[], base: number) => {
    const a = baseArr.slice();
    const md = maxDigits(a, base);
    const out: { arr: number[]; buckets: number[][]; digit: number; phase: 'dist' | 'collect'; base: number }[] = [{ arr: a.slice(), buckets: Array.from({ length: base }, () => []), digit: 0, phase: 'dist', base }];
    for (let d = 0; d < md; d++) {
      const buckets: number[][] = Array.from({ length: base }, () => []);
      for (let i = 0; i < a.length; i++) {
        const digit = Math.floor(a[i] / Math.pow(base, d)) % base;
        buckets[digit].push(a[i]);
      }
      out.push({ arr: a.slice(), buckets: buckets.map(b => b.slice()), digit: d, phase: 'dist', base });
      const flattened: number[] = [];
      for (let b = 0; b < base; b++) {
        for (let k = 0; k < buckets[b].length; k++) flattened.push(buckets[b][k]);
      }
      for (let i = 0; i < a.length; i++) a[i] = flattened[i];
      out.push({ arr: a.slice(), buckets: buckets.map(b => b.slice()), digit: d, phase: 'collect', base });
    }
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      setArr(steps[next].arr);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSort = () => { const s = buildSteps(arr.slice(), radix); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();
  const setBase = (b: number) => { setRadix(b); const s = buildSteps(arr.slice(), b); setSteps(s); setIdx(0); setPlaying(false); };

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const cur = steps[idx] || { digit: 0, buckets: Array.from({ length: radix }, () => []), phase: 'dist', base: radix };
  const isDone = idx === steps.length - 1 && steps.length > 1;

  return (
    <View style={styles.rsCard}>
      <View style={styles.rsHeader}> 
        <Text style={styles.rsTitle}>基数排序 · 糖果风格</Text>
        <Text style={styles.rsSubtitle}>播放、步进、重置、随机、进制切换</Text>
      </View>
      <View style={styles.rsControls}>
        <TouchableOpacity style={[styles.rsBtn, playing ? styles.rsBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_RADIX.pause : ICON_BASE64_RADIX.play }} style={styles.rsIcon} />
          <Text style={styles.rsBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.rsBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_RADIX.step }} style={styles.rsIcon} />
          <Text style={styles.rsBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.rsBtn} onPress={resetSort} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_RADIX.reset }} style={styles.rsIcon} />
          <Text style={styles.rsBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.rsBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_RADIX.shuffle }} style={styles.rsIcon} />
          <Text style={styles.rsBtnText}>随机</Text>
        </TouchableOpacity>
        <View style={styles.rsChips}>
          <TouchableOpacity style={[styles.rsChip, radix === 10 ? styles.rsChipActive : null]} onPress={() => setBase(10)}><Text style={styles.rsChipText}>10进制</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.rsChip, radix === 16 ? styles.rsChipActive : null]} onPress={() => setBase(16)}><Text style={styles.rsChipText}>16进制</Text></TouchableOpacity>
          <TouchableOpacity style={styles.rsChip} onPress={() => setSpeed(900)}><Text style={styles.rsChipText}>0.5×</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.rsChip, styles.rsChipActive]} onPress={() => setSpeed(600)}><Text style={styles.rsChipText}>1×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.rsChip} onPress={() => setSpeed(300)}><Text style={styles.rsChipText}>2×</Text></TouchableOpacity>
        </View>
      </View>
      <View style={styles.rsBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          return (
            <View key={i} style={[styles.rsBar, { height: h }]}>
              <Text style={styles.rsBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.rsBuckets}>
        {Array.from({ length: cur.base }, (_, b) => (
          <View key={`b-${b}`} style={styles.rsBucket}>
            <Text style={styles.rsBucketLabel}>{b}</Text>
            <Text style={styles.rsBucketCount}>{cur.buckets[b]?.length || 0}</Text>
          </View>
        ))}
      </View>
      <View style={styles.rsFooterRow}>
        <Text style={styles.rsFootText}>位数 {cur.digit + 1} / {Math.max(maxDigits(arr, radix), 1)} · 阶段 {cur.phase === 'dist' ? '分配' : '收集'}</Text>
        <Text style={[styles.rsFootText, isDone ? styles.rsDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
      </View>
    </View>
  );
};

const BucketSortVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [steps, setSteps] = useState<{ arr: number[]; buckets: number[][]; phase: 'dist' | 'sort' | 'collect'; curIndex?: number; curBucket?: number; collected?: number; bucketCount: number; min: number; max: number }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(600);
  const [bucketCount, setBucketCount] = useState(8);
  const timerRef = React.useRef<any>(null);
  const maxBars = 18;

  const genArray = () => {
    const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
    setArr(a);
    const s = buildSteps(a, bucketCount);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (baseArr: number[], bucketsN: number) => {
    const a = baseArr.slice();
    const min = Math.min(...a);
    const max = Math.max(...a);
    const range = Math.max(1, max - min + 1);
    const buckets: number[][] = Array.from({ length: bucketsN }, () => []);
    const out: { arr: number[]; buckets: number[][]; phase: 'dist' | 'sort' | 'collect'; curIndex?: number; curBucket?: number; collected?: number; bucketCount: number; min: number; max: number }[] = [{ arr: a.slice(), buckets: buckets.map(b => b.slice()), phase: 'dist', bucketCount: bucketsN, min, max }];
    for (let i = 0; i < a.length; i++) {
      const bi = Math.min(bucketsN - 1, Math.floor(((a[i] - min) / range) * bucketsN));
      buckets[bi].push(a[i]);
      out.push({ arr: a.slice(), buckets: buckets.map(b => b.slice()), phase: 'dist', curIndex: i, bucketCount: bucketsN, min, max });
    }
    for (let b = 0; b < bucketsN; b++) {
      buckets[b].sort((x, y) => x - y);
      out.push({ arr: a.slice(), buckets: buckets.map(bb => bb.slice()), phase: 'sort', curBucket: b, bucketCount: bucketsN, min, max });
    }
    const flat: number[] = [];
    for (let b = 0; b < bucketsN; b++) for (let k = 0; k < buckets[b].length; k++) flat.push(buckets[b][k]);
    for (let i = 0; i < a.length; i++) {
      a[i] = flat[i];
      out.push({ arr: a.slice(), buckets: buckets.map(bb => bb.slice()), phase: 'collect', collected: i + 1, bucketCount: bucketsN, min, max });
    }
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      setArr(steps[next].arr);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSort = () => { const s = buildSteps(arr.slice(), bucketCount); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();
  const setBuckets = (n: number) => { setBucketCount(n); const s = buildSteps(arr.slice(), n); setSteps(s); setIdx(0); setPlaying(false); };

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const cur = steps[idx] || { phase: 'dist', buckets: Array.from({ length: bucketCount }, () => []), bucketCount };
  const isDone = idx === steps.length - 1 && steps.length > 1;

  return (
    <View style={styles.bkCard}>
      <View style={styles.bkHeader}> 
        <Text style={styles.bkTitle}>桶排序 · 沙滩风格</Text>
        <Text style={styles.bkSubtitle}>播放、步进、重置、随机、桶数切换</Text>
      </View>
      <View style={styles.bkControls}>
        <TouchableOpacity style={[styles.bkBtn, playing ? styles.bkBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_BUCKET.pause : ICON_BASE64_BUCKET.play }} style={styles.bkIcon} />
          <Text style={styles.bkBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bkBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BUCKET.step }} style={styles.bkIcon} />
          <Text style={styles.bkBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bkBtn} onPress={resetSort} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BUCKET.reset }} style={styles.bkIcon} />
          <Text style={styles.bkBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bkBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BUCKET.shuffle }} style={styles.bkIcon} />
          <Text style={styles.bkBtnText}>随机</Text>
        </TouchableOpacity>
        <View style={styles.bkChips}>
          <TouchableOpacity style={[styles.bkChip, bucketCount === 5 ? styles.bkChipActive : null]} onPress={() => setBuckets(5)}><Text style={styles.bkChipText}>5</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.bkChip, bucketCount === 8 ? styles.bkChipActive : null]} onPress={() => setBuckets(8)}><Text style={styles.bkChipText}>8</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.bkChip, bucketCount === 12 ? styles.bkChipActive : null]} onPress={() => setBuckets(12)}><Text style={styles.bkChipText}>12</Text></TouchableOpacity>
          <TouchableOpacity style={styles.bkChip} onPress={() => setSpeed(900)}><Text style={styles.bkChipText}>0.5×</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.bkChip, styles.bkChipActive]} onPress={() => setSpeed(600)}><Text style={styles.bkChipText}>1×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.bkChip} onPress={() => setSpeed(300)}><Text style={styles.bkChipText}>2×</Text></TouchableOpacity>
        </View>
      </View>
      <View style={styles.bkBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const distHL = cur.phase === 'dist' && cur.curIndex === i;
          const collectedHL = cur.phase === 'collect' && typeof cur.collected === 'number' && i < (cur.collected || 0);
          return (
            <View key={i} style={[styles.bkBar, { height: h }, distHL ? styles.bkBarHL : null, collectedHL ? styles.bkBarCollected : null]}>
              <Text style={styles.bkBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.bkBuckets}>
        {Array.from({ length: cur.bucketCount }, (_, b) => (
          <View key={`bk-${b}`} style={[styles.bkBucket, cur.phase === 'sort' && cur.curBucket === b ? styles.bkBucketActive : null]}>
            <Text style={styles.bkBucketLabel}>{b}</Text>
            <Text style={styles.bkBucketCount}>{cur.buckets[b]?.length || 0}</Text>
          </View>
        ))}
      </View>
      <View style={styles.bkFooterRow}>
        <Text style={styles.bkFootText}>阶段 {cur.phase === 'dist' ? '分配' : cur.phase === 'sort' ? '桶内排序' : '收集'} · 桶数 {cur.bucketCount}</Text>
        <Text style={[styles.bkFootText, isDone ? styles.bkDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
      </View>
    </View>
  );
};

const ShellSortVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [steps, setSteps] = useState<{ arr: number[]; i: number; j: number; gap: number; swapped: boolean; group: number }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(600);
  const timerRef = React.useRef<any>(null);
  const maxBars = 18;

  const genArray = () => {
    const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
    setArr(a);
    const s = buildSteps(a);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (base: number[]) => {
    const a = base.slice();
    const n = a.length;
    const out: { arr: number[]; i: number; j: number; gap: number; swapped: boolean; group: number }[] = [{ arr: a.slice(), i: -1, j: -1, gap: Math.floor(n / 2), swapped: false, group: 0 }];
    for (let gap = Math.floor(n / 2); gap > 0; gap = Math.floor(gap / 2)) {
      for (let j = gap; j < n; j++) {
        let temp = a[j];
        let i = j;
        out.push({ arr: a.slice(), i, j: i - gap, gap, swapped: false, group: j % gap });
        while (i >= gap && a[i - gap] > temp) {
          a[i] = a[i - gap];
          i -= gap;
          out.push({ arr: a.slice(), i, j: i - gap, gap, swapped: true, group: j % gap });
        }
        a[i] = temp;
        out.push({ arr: a.slice(), i, j: i - gap, gap, swapped: true, group: j % gap });
      }
    }
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      setArr(steps[next].arr);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const cur = steps[idx] || { i: -1, j: -1, gap: Math.floor(arr.length / 2), swapped: false, group: 0 };
  const isDone = idx === steps.length - 1 && steps.length > 1;

  return (
    <View style={styles.shCard}>
      <View style={styles.shHeader}> 
        <Text style={styles.shTitle}>希尔排序 · 暮色金属风格</Text>
        <Text style={styles.shSubtitle}>播放、步进、重置、随机与速度</Text>
      </View>
      <View style={styles.shControls}>
        <TouchableOpacity style={[styles.shBtn, playing ? styles.shBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_SHELL.pause : ICON_BASE64_SHELL.play }} style={styles.shIcon} />
          <Text style={styles.shBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.shBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_SHELL.step }} style={styles.shIcon} />
          <Text style={styles.shBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.shBtn} onPress={resetSort} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_SHELL.reset }} style={styles.shIcon} />
          <Text style={styles.shBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.shBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_SHELL.shuffle }} style={styles.shIcon} />
          <Text style={styles.shBtnText}>随机</Text>
        </TouchableOpacity>
        <View style={styles.shChips}>
          <View style={styles.shChip}><Text style={styles.shChipText}>当前间隔:{cur.gap}</Text></View>
          <TouchableOpacity style={[styles.shChip, styles.shChipActive]} onPress={() => setSpeed(600)}><Text style={styles.shChipText}>1×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.shChip} onPress={() => setSpeed(300)}><Text style={styles.shChipText}>2×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.shChip} onPress={() => setSpeed(900)}><Text style={styles.shChipText}>0.5×</Text></TouchableOpacity>
        </View>
      </View>
      <View style={styles.shBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const inCompare = i === cur.i || i === cur.j;
          const swapped = inCompare && cur.swapped;
          const inGroup = cur.gap > 0 && i % cur.gap === cur.group;
          return (
            <View key={i} style={[styles.shBar, { height: h }, inGroup ? styles.shBarGroup : null, inCompare ? styles.shBarHL : null, swapped ? styles.shBarSwap : null]}>
              <Text style={styles.shBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.shFooterRow}>
        <Text style={styles.shFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
        <Text style={[styles.shFootText, isDone ? styles.shDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
      </View>
    </View>
  );
};

const CountingSortVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [steps, setSteps] = useState<{ arr: number[]; counts: number[]; phase: 'count' | 'prefix' | 'place'; curIndex?: number; curBucket?: number; placed?: number; min: number; range: number }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(600);
  const [rangeN, setRangeN] = useState(12);
  const timerRef = React.useRef<any>(null);
  const maxBars = 18;

  const genArray = () => {
    const min = 10;
    const a = Array.from({ length: maxBars }, () => min + Math.floor(Math.random() * rangeN));
    setArr(a);
    const s = buildSteps(a, min, rangeN);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (baseArr: number[], min: number, range: number) => {
    const a = baseArr.slice();
    const counts = Array.from({ length: range }, () => 0);
    const out: { arr: number[]; counts: number[]; phase: 'count' | 'prefix' | 'place'; curIndex?: number; curBucket?: number; placed?: number; min: number; range: number }[] = [{ arr: a.slice(), counts: counts.slice(), phase: 'count', min, range }];
    for (let i = 0; i < a.length; i++) {
      const b = a[i] - min;
      counts[b] += 1;
      out.push({ arr: a.slice(), counts: counts.slice(), phase: 'count', curIndex: i, curBucket: b, min, range });
    }
    for (let i = 1; i < counts.length; i++) {
      counts[i] += counts[i - 1];
      out.push({ arr: a.slice(), counts: counts.slice(), phase: 'prefix', curBucket: i, min, range });
    }
    const outArr = Array.from({ length: a.length }, () => 0);
    for (let i = a.length - 1; i >= 0; i--) {
      const b = a[i] - min;
      const pos = counts[b] - 1;
      outArr[pos] = a[i];
      counts[b] -= 1;
      out.push({ arr: outArr.slice(), counts: counts.slice(), phase: 'place', curIndex: i, curBucket: b, placed: pos + 1, min, range });
    }
    out.push({ arr: outArr.slice(), counts: counts.slice(), phase: 'place', min, range });
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      setArr(steps[next].arr);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSort = () => { const s = buildSteps(arr.slice(), 10, rangeN); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();
  const setRange = (n: number) => { setRangeN(n); const min = 10; const a = Array.from({ length: maxBars }, () => min + Math.floor(Math.random() * n)); setArr(a); const s = buildSteps(a, min, n); setSteps(s); setIdx(0); setPlaying(false); };

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const cur = steps[idx] || { phase: 'count', counts: Array.from({ length: rangeN }, () => 0), min: 10, range: rangeN };
  const isDone = idx === steps.length - 1 && steps.length > 1;

  return (
    <View style={styles.ctCard}>
      <View style={styles.ctHeader}> 
        <Text style={styles.ctTitle}>计数排序 · 薄荷风格</Text>
        <Text style={styles.ctSubtitle}>播放、步进、重置、随机、范围切换</Text>
      </View>
      <View style={styles.ctControls}>
        <TouchableOpacity style={[styles.ctBtn, playing ? styles.ctBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_COUNT.pause : ICON_BASE64_COUNT.play }} style={styles.ctIcon} />
          <Text style={styles.ctBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.ctBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_COUNT.step }} style={styles.ctIcon} />
          <Text style={styles.ctBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.ctBtn} onPress={resetSort} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_COUNT.reset }} style={styles.ctIcon} />
          <Text style={styles.ctBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.ctBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_COUNT.shuffle }} style={styles.ctIcon} />
          <Text style={styles.ctBtnText}>随机</Text>
        </TouchableOpacity>
        <View style={styles.ctChips}>
          <TouchableOpacity style={[styles.ctChip, rangeN === 8 ? styles.ctChipActive : null]} onPress={() => setRange(8)}><Text style={styles.ctChipText}>8范围</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.ctChip, rangeN === 12 ? styles.ctChipActive : null]} onPress={() => setRange(12)}><Text style={styles.ctChipText}>12范围</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.ctChip, rangeN === 16 ? styles.ctChipActive : null]} onPress={() => setRange(16)}><Text style={styles.ctChipText}>16范围</Text></TouchableOpacity>
          <TouchableOpacity style={styles.ctChip} onPress={() => setSpeed(900)}><Text style={styles.ctChipText}>0.5×</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.ctChip, styles.ctChipActive]} onPress={() => setSpeed(600)}><Text style={styles.ctChipText}>1×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.ctChip} onPress={() => setSpeed(300)}><Text style={styles.ctChipText}>2×</Text></TouchableOpacity>
        </View>
      </View>
      <View style={styles.ctBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const inCount = cur.phase === 'count' && cur.curIndex === i;
          const placed = cur.phase === 'place' && typeof cur.placed === 'number' && i < (cur.placed || 0);
          return (
            <View key={i} style={[styles.ctBar, { height: h }, inCount ? styles.ctBarHL : null, placed ? styles.ctBarPlaced : null]}>
              <Text style={styles.ctBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.ctBuckets}>
        {Array.from({ length: cur.range }, (_, b) => (
          <View key={`ct-${b}`} style={[styles.ctBucket, cur.curBucket === b ? styles.ctBucketActive : null]}>
            <Text style={styles.ctBucketLabel}>{b + cur.min}</Text>
            <Text style={styles.ctBucketCount}>{cur.counts[b] || 0}</Text>
          </View>
        ))}
      </View>
      <View style={styles.ctFooterRow}>
        <Text style={styles.ctFootText}>阶段 {cur.phase === 'count' ? '计数' : cur.phase === 'prefix' ? '前缀和' : '放置'}</Text>
        <Text style={[styles.ctFootText, isDone ? styles.ctDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
      </View>
    </View>
  );
};
export default CalendarComponentApp;

这段React Native日历组件代码在架构设计原理上与鸿蒙系统的分布式能力有着深刻的共鸣。从组件抽象化角度来看,Icon组件通过符号映射机制实现了轻量级的图标系统,这种将视觉元素与逻辑标识分离的设计模式正是鸿蒙原子化服务思想的微观体现。每个图标都被封装成独立的功能单元,通过统一的接口规范对外提供服务,这种设计哲学与鸿蒙Ability组件的能力抽象化高度一致。

代码采用状态驱动的声明式编程范式,通过currentDate这一核心状态来管理整个界面的渲染逻辑。这种单向数据流的设计确保了界面与数据的一致性,所有日期计算、事件匹配和视觉呈现都基于这一状态派生而来。这种响应式架构与鸿蒙ArkUI的更新机制在原理层面完全相通,都是通过状态变化自动触发界面的重新组合。组件内部的状态变化会自动触发界面的重新渲染,实现了数据与UI的自动同步,这种机制与鸿蒙的状态管理在本质上相通。

在日期计算模块中,代码通过JavaScript Date对象封装了复杂的日历算法,包括月份天数计算、首日星期定位等核心功能。这种将复杂逻辑隐藏在简洁接口之后的设计模式,与鸿蒙系统服务的抽象化封装有着相同的设计理念。getDaysInMonth函数通过设置下个月的第0天来获取当月最后一天的巧妙算法,展现了与鸿蒙系统级API相似的设计智慧。

请添加图片描述

事件管理机制采用数据过滤模式,通过hasEvents函数实现日期与业务数据的动态关联。这种松耦合的数据绑定方式与鸿蒙的分布式数据管理能力在架构思想上高度契合,都是通过统一的标识符来实现不同数据源之间的关联映射。通过events数组与日期字符串的匹配,实现了业务数据与界面显示的松耦合关联,这种设计理念正是鸿蒙分布式数据管理能力的微观体现。

界面渲染采用分层架构设计,将日历拆分为头部导航、星期栏和日期网格三个独立的渲染模块。这种关注点分离的设计原则使得每个组件都可以独立开发和测试,与鸿蒙的弹性部署能力形成了完美的呼应。每个渲染函数都专注于特定的界面区域,这种模块化的思维方式正是构建大型复杂应用系统的关键所在。

交互设计方面,TouchableOpacity组件提供了原生的触控反馈体验,这种对用户体验细节的关注与鸿蒙的人机交互设计理念完全吻合。导航按钮的点击事件处理展现了完整的用户操作响应链路,从触控输入到状态变更再到界面更新,这一完整的交互闭环与鸿蒙的分布式交互机制在原理层面高度一致。整个组件的国际化处理也展现了与鸿蒙全球化支持相一致的设计思路。月份名称通过toLocaleString方法根据系统语言自动适配,这种动态本地化机制正是鸿蒙多语言架构的核心特征。


打包

接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

在这里插入图片描述

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

在这里插入图片描述

最后运行效果图如下显示:

请添加图片描述

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐