import { useState, useEffect, useMemo, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useFlowStore } from '../../store/useFlowStore'; import { PATHS } from '../../data/paths'; import { Icon } from './Icon'; import { CATEGORY_ICONS } from '../../utils/iconMap'; export function CommandPalette() { const isOpen = useFlowStore((s) => s.isCommandPaletteOpen); const toggle = useFlowStore((s) => s.toggleCommandPalette); const selectPath = useFlowStore((s) => s.selectPath); const setActiveStep = useFlowStore((s) => s.setActiveStep); const [query, setQuery] = useState(''); const [selectedIndex, setSelectedIndex] = useState(0); // Keyboard shortcut to open useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); toggle(); } if (e.key === 'Escape' && isOpen) { toggle(); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [isOpen, toggle]); // Reset on open — adjust state during render (not inside an effect) so it does // not trigger an extra cascading render (react-hooks/set-state-in-effect). const [wasOpen, setWasOpen] = useState(isOpen); if (isOpen !== wasOpen) { setWasOpen(isOpen); if (isOpen) { setQuery(''); setSelectedIndex(0); } } const results = useMemo(() => { if (!query.trim()) { return PATHS.map((p) => ({ id: `path-${p.id}`, type: 'path' as const, label: `Path ${p.id}: ${p.label}`, description: p.description, category: p.category, pathId: p.id, })); } const q = query.toLowerCase(); const items: Array<{ id: string; type: 'path' | 'step'; label: string; description: string; category?: string; pathId: number; stepIndex?: number; }> = []; for (const path of PATHS) { if (path.label.toLowerCase().includes(q) || path.description.toLowerCase().includes(q)) { items.push({ id: `path-${path.id}`, type: 'path', label: `Path ${path.id}: ${path.label}`, description: path.description, category: path.category, pathId: path.id, }); } for (let i = 0; i < path.steps.length; i++) { const step = path.steps[i]; if (step.title.toLowerCase().includes(q) || step.handler.toLowerCase().includes(q)) { items.push({ id: `step-${path.id}-${i}`, type: 'step', label: `${step.title}`, description: `Path ${path.id} > Step ${i + 1} > ${step.handler}`, pathId: path.id, stepIndex: i, }); } } } return items.slice(0, 20); }, [query]); const handleSelect = useCallback((item: typeof results[0]) => { selectPath(item.pathId); if (item.type === 'step' && item.stepIndex !== undefined) { setTimeout(() => setActiveStep(item.stepIndex!), 50); } toggle(); }, [selectPath, setActiveStep, toggle]); // Keyboard navigation useEffect(() => { if (!isOpen) return; const handler = (e: KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex((i) => Math.min(i + 1, results.length - 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex((i) => Math.max(i - 1, 0)); } else if (e.key === 'Enter' && results[selectedIndex]) { handleSelect(results[selectedIndex]); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [isOpen, selectedIndex, results, handleSelect]); return ( {isOpen && ( <> {/* Search input */}
{ setQuery(e.target.value); setSelectedIndex(0); }} className="flex-1 bg-transparent border-none text-white text-sm placeholder-slate-500 focus:outline-none focus:ring-0" autoFocus /> ESC
{/* Results */}
{results.length === 0 ? (
No results found
) : ( results.map((item, index) => ( )) )}
{/* Footer hint */}
↑↓ Navigate Select Esc Close
)}
); }