import { useEffect, useMemo, useRef, useState } from 'react'; import type { IconType } from 'react-icons'; import * as Io5Icons from 'react-icons/io5'; import { Button } from './button'; import { Input } from './input'; interface IconOption { name: string; Icon: IconType; } /** * Convert PascalCase component name to kebab-case Ionicon name. * e.g., "IoHeartOutline" -> "heart-outline" */ function toKebabCase(componentName: string): string { // Remove "Io" prefix const withoutPrefix = componentName.slice(2); // Convert PascalCase to kebab-case return withoutPrefix .replace(/([a-z])([A-Z])/g, '$1-$2') .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') .toLowerCase(); } // Build icon options from all Io5 icons dynamically const ICON_OPTIONS: IconOption[] = Object.entries(Io5Icons) .filter(([name]) => name.startsWith('Io')) .map(([componentName, Icon]) => ({ name: toKebabCase(componentName), Icon: Icon as IconType, })) .sort((a, b) => a.name.localeCompare(b.name)); const ICONS_BY_NAME = ICON_OPTIONS.reduce>((acc, option) => { acc[option.name] = option; return acc; }, {}); interface IconPreviewProps { value?: string | null; fallback?: string; className?: string; } export function IconPreview({ value, fallback = '?', className }: IconPreviewProps) { const resolved = value ?? fallback; const Icon = resolved ? ICONS_BY_NAME[resolved]?.Icon : undefined; if (Icon) { return ; } return {resolved}; } interface IconPickerProps { value: string; onChange: (iconName: string) => void; } export function IconPicker({ value, onChange }: IconPickerProps) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const containerRef = useRef(null); useEffect(() => { function handleClickOutside(event: MouseEvent) { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setOpen(false); } } if (open) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [open]); const filteredIcons = useMemo(() => { const normalized = query.trim().toLowerCase(); if (!normalized) return ICON_OPTIONS; return ICON_OPTIONS.filter((option) => option.name.includes(normalized)); }, [query]); const selectedOption = ICONS_BY_NAME[value]; const SelectedIcon = selectedOption?.Icon; const buttonLabel = selectedOption ? value : value ? 'Legacy icon' : 'Select an icon'; return (
{open && (
setQuery(event.target.value)} placeholder="Search icons..." className="mb-3" />
{filteredIcons.map(({ name, Icon }) => { const isSelected = name === value; return ( ); })} {filteredIcons.length === 0 && (
No icons found
)}
)}
); }