import { useState, useEffect, useMemo, useRef, type KeyboardEvent as ReactKeyboardEvent } from 'react' import { Icon, type IconName } from './Icon' import { Badge } from './ui/Badge' import { Kbd } from './ui/Kbd' import { trapTabKey, useOverlayLifecycle } from './ui/overlayBehavior' import type { PatternId } from './AIWorkflowSection' export type SearchItem = { id: string title: string description: string category: 'Patterns' | 'Components' | 'Foundations' | 'Documentation' icon: IconName targetId: string patternId?: PatternId } const SEARCH_INDEX: SearchItem[] = [ // Patterns { id: 'pat-metrics', title: 'Metrics Dashboard', description: 'Telemetry dashboard with StatCards, time range picker, and progress gauges', category: 'Patterns', icon: 'sparkles', targetId: 'application-patterns', patternId: 'metrics', }, { id: 'pat-settings', title: 'Settings & Safeguards', description: 'Preferences panel with API keys, toggle switches, and alert banners', category: 'Patterns', icon: 'settings', targetId: 'application-patterns', patternId: 'settings', }, { id: 'pat-table', title: 'Service Monitor Table', description: 'Real-time service health table with status filtering and search', category: 'Patterns', icon: 'layers', targetId: 'application-patterns', patternId: 'table', }, { id: 'pat-deploy', title: 'Canary Deployment Flow', description: 'Interactive rollout modal with live progress simulation', category: 'Patterns', icon: 'terminal', targetId: 'application-patterns', patternId: 'deploy', }, // Components { id: 'comp-buttons', title: 'Buttons & Action States', description: 'Primary, secondary, destructive, ghost variants with resting, hover, active and focus states', category: 'Components', icon: 'arrow-right', targetId: 'buttons', }, { id: 'comp-icons', title: 'Lucide Icons Explorer', description: 'Over 1,400+ tree-shaken SVG icons with search, category filtering and 1-click copy', category: 'Components', icon: 'sparkles', targetId: 'icons', }, { id: 'comp-forms', title: 'Form Controls & Inputs', description: 'Accessible text inputs, secret key revealers, and toggle switches', category: 'Components', icon: 'eye', targetId: 'form-controls', }, { id: 'comp-code', title: 'Composable UI Primitives', description: 'Zero-drift Dialog, Sheet, Tabs, Table, Alert, Progress, Switch, and StatCard', category: 'Components', icon: 'code', targetId: 'composable-by-default', }, // Foundations { id: 'found-colors', title: 'Color Tokens (CSS Color 4 lab())', description: 'Pitch black dark theme and pure white daylight light theme tokens', category: 'Foundations', icon: 'sun', targetId: 'color-tokens', }, { id: 'found-type', title: 'Typography & Scale', description: 'Modular rem-based 9px to 70px scale with Manrope and DM Mono families', category: 'Foundations', icon: 'file', targetId: 'typography', }, // Documentation { id: 'doc-md', title: 'Markdown Editorial Layer', description: 'Changelogs, release notes, and documentation typography', category: 'Documentation', icon: 'file', targetId: 'markdown-rendering', }, { id: 'doc-cm', title: 'CodeMirror Editor Theme', description: 'Syntax-highlighted code editor preview with line numbers and token palette', category: 'Documentation', icon: 'terminal', targetId: 'codemirror-theme', }, ] type SearchPaletteProps = { isOpen: boolean onClose: () => void onSelect: (item: SearchItem) => void initialQuery?: string } const LISTBOX_ID = 'search-palette-listbox' /** Command-palette modal built on the same overlay behavior as Dialog/Sheet: focus trap, focus restore, scroll lock, Escape — plus combobox semantics (role=listbox + aria-activedescendant) for the results. */ export function SearchPalette({ isOpen, onClose, onSelect, initialQuery = '' }: SearchPaletteProps) { const [query, setQuery] = useState(initialQuery) const [selectedIndex, setSelectedIndex] = useState(0) const panelRef = useRef(null) const inputRef = useRef(null) /* Same lifecycle as DialogContent: scroll lock + focus capture/restore. */ useOverlayLifecycle(isOpen, panelRef) useEffect(() => { if (isOpen) { inputRef.current?.focus() } }, [isOpen, initialQuery]) const results = useMemo(() => { if (!query.trim()) return SEARCH_INDEX const q = query.toLowerCase() return SEARCH_INDEX.filter( item => item.title.toLowerCase().includes(q) || item.description.toLowerCase().includes(q) || item.category.toLowerCase().includes(q), ) }, [query]) /* Keep the selection inside the result set when the query shrinks it. */ const activeIndex = results.length === 0 ? -1 : Math.min(selectedIndex, results.length - 1) const activeItem = activeIndex >= 0 ? results[activeIndex] : undefined // Keyboard navigation (combobox pattern: arrows move aria-activedescendant) useEffect(() => { if (!isOpen) return const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault() onClose() } else if (e.key === 'ArrowDown') { e.preventDefault() setSelectedIndex(prev => (results.length ? (prev + 1) % results.length : 0)) } else if (e.key === 'ArrowUp') { e.preventDefault() setSelectedIndex(prev => (results.length ? (prev - 1 + results.length) % results.length : 0)) } else if (e.key === 'Enter' && activeItem) { e.preventDefault() onSelect(activeItem) onClose() } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [isOpen, results.length, activeItem, onClose, onSelect]) const handlePanelKeyDown = (e: ReactKeyboardEvent) => { trapTabKey(e, panelRef.current) } /* Closed palette must not render at all: the backdrop is `position: fixed; inset: 0`, so rendering it unconditionally left the modal permanently on screen (and ate every click on the page underneath). */ if (!isOpen) return null return (
e.stopPropagation()} > {/* Search Input Bar */}
0} aria-controls={LISTBOX_ID} aria-activedescendant={activeItem ? `search-option-${activeItem.id}` : undefined} aria-label="Search components, tokens, and docs" className="search-palette-input" placeholder="Type a command, component, or token..." value={query} onChange={e => { setQuery(e.target.value) setSelectedIndex(0) }} /> {query && ( )} ESC
{/* Results List */}
{results.length > 0 ? (
    {results.map((item, idx) => { const isSelected = idx === activeIndex return (
  • { onSelect(item) onClose() }} onMouseEnter={() => setSelectedIndex(idx)} >
    {item.title} {item.category}

    {item.description}

    {isSelected && (
    ↵
    )}
  • ) })}
) : (

No results found for “{query}”

Try searching for “Buttons”, “Metrics”, “Tokens”, or “Icons”.
)}
{/* Footer info */}
↑ ↓ to navigate
↵ to select
ESC to close
) }