All files / src/components/ui EmojiPicker.tsx

0% Statements 0/70
0% Branches 0/1
0% Functions 0/1
0% Lines 0/70

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104                                                                                                                                                                                                               
/**
 * EmojiPicker - Emoji selection component for Agent onboarding
 *
 * Displays categorized emoji presets for users to choose from.
 */
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '../../lib/utils';
import { EMOJI_PRESETS, ALL_EMOJIS } from '../../lib/personality-presets';
 
type EmojiCategory = 'all' | 'animals' | 'objects' | 'expressions';
 
export interface EmojiPickerProps {
  value?: string;
  onChange: (emoji: string) => void;
  className?: string;
}
 
const categoryLabels: Record<EmojiCategory, string> = {
  all: '全部',
  animals: '动物',
  objects: '物体',
  expressions: '表情',
};
 
export function EmojiPicker({ value, onChange, className }: EmojiPickerProps) {
  const [activeCategory, setActiveCategory] = useState<EmojiCategory>('all');
 
  const getEmojisForCategory = (category: EmojiCategory): string[] => {
    if (category === 'all') {
      return ALL_EMOJIS;
    }
    return EMOJI_PRESETS[category] || [];
  };
 
  const emojis = getEmojisForCategory(activeCategory);
 
  return (
    <div className={cn('space-y-3', className)}>
      {/* Category Tabs */}
      <div className="flex gap-1 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg">
        {(Object.keys(categoryLabels) as EmojiCategory[]).map((category) => (
          <button
            key={category}
            type="button"
            onClick={() => setActiveCategory(category)}
            className={cn(
              'flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-colors',
              activeCategory === category
                ? 'bg-white dark:bg-gray-700 text-primary shadow-sm'
                : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
            )}
          >
            {categoryLabels[category]}
          </button>
        ))}
      </div>
 
      {/* Emoji Grid */}
      <motion.div
        layout
        className="grid grid-cols-8 gap-1"
      >
        <AnimatePresence mode="popLayout">
          {emojis.map((emoji) => (
            <motion.button
              key={emoji}
              type="button"
              layout
              initial={{ opacity: 0, scale: 0.8 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.8 }}
              transition={{ duration: 0.15 }}
              onClick={() => onChange(emoji)}
              className={cn(
                'w-9 h-9 flex items-center justify-center text-xl rounded-lg transition-all',
                'hover:bg-gray-100 dark:hover:bg-gray-700',
                'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1',
                value === emoji && 'bg-primary/10 ring-2 ring-primary'
              )}
            >
              {emoji}
            </motion.button>
          ))}
        </AnimatePresence>
      </motion.div>
 
      {/* Selected Preview */}
      {value && (
        <motion.div
          initial={{ opacity: 0, y: 5 }}
          animate={{ opacity: 1, y: 0 }}
          className="flex items-center gap-2 p-2 bg-primary/5 rounded-lg"
        >
          <span className="text-2xl">{value}</span>
          <span className="text-sm text-gray-600 dark:text-gray-400">
            已选择
          </span>
        </motion.div>
      )}
    </div>
  );
}