{"version":3,"file":"Command.cjs","names":[],"sources":["../../../src/components/Command/Command.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a command palette is a filtered\n * listbox with its own keyboard contract: the body holds the query, the flattened\n * result list, the active index and the Escape/Enter handling, all of which read\n * each other on every keystroke.\n */\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport styles from \"./Command.module.css\";\nimport { usePortalHost } from \"../Portal/portal-host\";\n\nexport interface CommandItem {\n    /** Stable unique identifier for the item. */\n    id: string;\n    /** Human-readable label shown in the list and matched against the query. */\n    label: string;\n    /** Optional group heading; items sharing a group are rendered together. */\n    group?: string;\n    /** Extra terms matched against the query in addition to `label`. */\n    keywords?: string[];\n    /** Invoked when the item is chosen (click or Enter on the active row). */\n    onSelect: () => void;\n    /** Optional leading icon. */\n    icon?: ReactNode;\n}\n\nexport interface CommandProps {\n    /** Whether the palette is visible. */\n    open: boolean;\n    /** Called with the next open state (Escape, selection, backdrop click). */\n    onOpenChange: (open: boolean) => void;\n    /** Candidate items to filter and display. */\n    items: CommandItem[];\n    /** Placeholder for the search input. */\n    placeholder?: string;\n    /** Rendered when no item matches the current query. */\n    emptyMessage?: ReactNode;\n    /** Forwarded to the dialog element. */\n    className?: string;\n}\n\ninterface CommandGroup {\n    name: string | undefined;\n    items: CommandItem[];\n}\n\n/**\n * Case-insensitively test whether `item` matches `query` by substring against\n * its label and any keywords. An empty query matches everything.\n *\n * @param item - The candidate command item.\n * @param query - The lowercased search query.\n * @returns `true` when the item should be shown.\n */\nfunction matchesQuery(item: CommandItem, query: string): boolean {\n    if (query === \"\") return true;\n    if (item.label.toLowerCase().includes(query)) return true;\n    return (item.keywords ?? []).some((keyword) => keyword.toLowerCase().includes(query));\n}\n\n/**\n * Partition a flat item list into ordered groups, preserving first-seen order\n * for both groups and the items within them.\n *\n * @param items - The (already filtered) items to group.\n * @returns An ordered list of groups.\n */\nfunction groupItems(items: CommandItem[]): CommandGroup[] {\n    const groups: CommandGroup[] = [];\n    const indexByName = new Map<string | undefined, number>();\n    for (const item of items) {\n        const existing = indexByName.get(item.group);\n        if (existing === undefined) {\n            indexByName.set(item.group, groups.length);\n            groups.push({ name: item.group, items: [item] });\n        } else {\n            groups[existing]!.items.push(item);\n        }\n    }\n    return groups;\n}\n\n/**\n * A ⌘K-style command palette: an overlay dialog with a search input that\n * substring-filters items by label and keywords, groups results, and supports\n * full keyboard navigation (Arrow up/down, Enter, Escape). Traps focus while\n * open.\n */\nexport function Command({\n    open,\n    onOpenChange,\n    items,\n    placeholder = \"Type a command…\",\n    emptyMessage = \"No results\",\n    className,\n}: CommandProps) {\n    const dialogRef = useRef<HTMLDivElement>(null);\n    const [query, setQuery] = useState<string>(\"\");\n    const [activeIndex, setActiveIndex] = useState<number>(0);\n    const listId = \"tempest-command-list\";\n\n    useFocusTrap(dialogRef, open);\n\n    const filtered = useMemo<CommandItem[]>(() => {\n        const normalized = query.trim().toLowerCase();\n        const visible = items.filter((item) => matchesQuery(item, normalized));\n        return groupItems(visible).flatMap((group) => group.items);\n    }, [items, query]);\n\n    const groups = useMemo<CommandGroup[]>(() => groupItems(filtered), [filtered]);\n\n    // Reset transient state whenever the palette opens.\n    useEffect(() => {\n        if (open) {\n            setQuery(\"\");\n            setActiveIndex(0);\n        }\n    }, [open]);\n\n    // Keep the active index within bounds as the filtered list shrinks/grows.\n    useEffect(() => {\n        setActiveIndex((index) => {\n            if (filtered.length === 0) return 0;\n            return Math.min(index, filtered.length - 1);\n        });\n    }, [filtered.length]);\n\n    const portalHost = usePortalHost();\n\n    if (!open || !portalHost) return null;\n\n    function selectItem(item: CommandItem): void {\n        item.onSelect();\n        onOpenChange(false);\n    }\n\n    function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {\n        if (event.key === \"Escape\") {\n            event.preventDefault();\n            onOpenChange(false);\n            return;\n        }\n        if (event.key === \"ArrowDown\") {\n            event.preventDefault();\n            if (filtered.length === 0) return;\n            setActiveIndex((index) => (index + 1) % filtered.length);\n            return;\n        }\n        if (event.key === \"ArrowUp\") {\n            event.preventDefault();\n            if (filtered.length === 0) return;\n            setActiveIndex((index) => (index - 1 + filtered.length) % filtered.length);\n            return;\n        }\n        if (event.key === \"Enter\") {\n            event.preventDefault();\n            const item = filtered[activeIndex];\n            if (item) selectItem(item);\n        }\n    }\n\n    let renderIndex = -1;\n\n    return createPortal(\n        <div className={styles.overlay} role=\"presentation\" onClick={() => onOpenChange(false)}>\n            <div\n                ref={dialogRef}\n                role=\"dialog\"\n                aria-modal=\"true\"\n                aria-label=\"Command palette\"\n                className={cn(styles.dialog, className)}\n                onClick={(event) => event.stopPropagation()}\n            >\n                <div className={styles.inputRow}>\n                    <SearchIcon />\n                    <input\n                        type=\"text\"\n                        role=\"combobox\"\n                        aria-expanded=\"true\"\n                        aria-controls={listId}\n                        aria-autocomplete=\"list\"\n                        autoFocus\n                        className={styles.input}\n                        placeholder={placeholder}\n                        value={query}\n                        onChange={(event) => setQuery(event.target.value)}\n                        onKeyDown={handleKeyDown}\n                    />\n                </div>\n                <ul id={listId} role=\"listbox\" className={styles.list}>\n                    {filtered.length === 0 ? (\n                        <li className={styles.empty}>{emptyMessage}</li>\n                    ) : (\n                        groups.map((group) => (\n                            <li key={group.name ?? \"__ungrouped__\"} className={styles.group}>\n                                {group.name && (\n                                    <div className={styles.groupHeading}>{group.name}</div>\n                                )}\n                                <ul role=\"presentation\" className={styles.groupList}>\n                                    {group.items.map((item) => {\n                                        renderIndex += 1;\n                                        const isActive = renderIndex === activeIndex;\n                                        const index = renderIndex;\n                                        return (\n                                            <li\n                                                key={item.id}\n                                                role=\"option\"\n                                                aria-selected={isActive}\n                                                className={cn(\n                                                    styles.option,\n                                                    isActive && styles.active,\n                                                )}\n                                                onMouseMove={() => setActiveIndex(index)}\n                                                onClick={() => selectItem(item)}\n                                            >\n                                                {item.icon && (\n                                                    <span className={styles.icon}>{item.icon}</span>\n                                                )}\n                                                <span className={styles.label}>{item.label}</span>\n                                            </li>\n                                        );\n                                    })}\n                                </ul>\n                            </li>\n                        ))\n                    )}\n                </ul>\n            </div>\n        </div>,\n        portalHost,\n    );\n}\n\nfunction SearchIcon() {\n    return (\n        <svg\n            className={styles.searchIcon}\n            width=\"18\"\n            height=\"18\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            aria-hidden=\"true\"\n        >\n            <circle cx=\"11\" cy=\"11\" r=\"7\" stroke=\"currentColor\" strokeWidth=\"2\" />\n            <path d=\"M20 20l-3.5-3.5\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n        </svg>\n    );\n}\n"],"mappings":"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"}