'use client'; import React, { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type ReactNode, } from 'react'; import { Surface, cn, surfaceClasses, useEffectiveSurface, } from '../common'; import { PixelPortal } from '../overlay-foundation/PixelPortal'; import { OverlayBackdrop } from './_internal/OverlayBackdrop'; import { useFocusTrap } from '../hooks/useFocusTrap'; import { useEscape } from '../hooks/useEscape'; import { useScrollLock } from '../hooks/useScrollLock'; import { useEventListener } from '../hooks/useEventListener'; export interface PixelCommandItem { id: string; label: string; icon?: ReactNode; shortcut?: string; keywords?: string[]; onSelect: () => void; } export interface PixelCommandGroup { heading: string; items: PixelCommandItem[]; } export interface PixelCommandProps { open: boolean; onOpenChange: (open: boolean) => void; shortcut?: string; placeholder?: string; emptyMessage?: string; groups: PixelCommandGroup[]; surface?: Surface; } type FlatRow = | { kind: 'heading'; heading: string; key: string } | { kind: 'item'; item: PixelCommandItem; index: number; key: string }; function parseShortcut(shortcut: string): { key: string; mod: boolean; shift: boolean; alt: boolean; } { const parts = shortcut.toLowerCase().split('+').map((s) => s.trim()); let mod = false; let shift = false; let alt = false; let key = ''; for (const p of parts) { if (p === 'mod' || p === 'cmd' || p === 'ctrl' || p === 'meta') mod = true; else if (p === 'shift') shift = true; else if (p === 'alt' || p === 'opt' || p === 'option') alt = true; else key = p; } return { key, mod, shift, alt }; } function matchesShortcut(e: KeyboardEvent, parsed: ReturnType): boolean { if (e.key.toLowerCase() !== parsed.key) return false; const modPressed = e.metaKey || e.ctrlKey; if (parsed.mod !== modPressed) return false; if (parsed.shift !== e.shiftKey) return false; if (parsed.alt !== e.altKey) return false; return true; } function itemMatches(item: PixelCommandItem, query: string): boolean { if (!query) return true; const q = query.toLowerCase(); if (item.label.toLowerCase().includes(q)) return true; if (item.keywords) { for (const k of item.keywords) { if (k.toLowerCase().includes(q)) return true; } } return false; } export const PixelCommand = forwardRef( function PixelCommand( { open, onOpenChange, shortcut = 'mod+k', placeholder = 'Type a command or search…', emptyMessage = 'No results.', groups, surface: surfaceProp, }, forwardedRef, ) { const surface = useEffectiveSurface(surfaceProp); const s = surfaceClasses(surface); const [query, setQuery] = useState(''); const [highlighted, setHighlighted] = useState(0); const inputRef = useRef(null); const panelRef = useRef(null); const listboxId = useId(); const setPanelRef = useCallback( (node: HTMLDivElement | null) => { panelRef.current = node; if (typeof forwardedRef === 'function') forwardedRef(node); else if (forwardedRef && typeof forwardedRef === 'object') { (forwardedRef as React.MutableRefObject).current = node; } }, [forwardedRef], ); // Filter groups → flat list (items only, for navigation). const { rows, items } = useMemo(() => { const flatRows: FlatRow[] = []; const flatItems: PixelCommandItem[] = []; let idx = 0; for (const g of groups) { const visible = g.items.filter((it) => itemMatches(it, query)); if (visible.length === 0) continue; flatRows.push({ kind: 'heading', heading: g.heading, key: `h-${g.heading}` }); for (const it of visible) { flatRows.push({ kind: 'item', item: it, index: idx, key: `i-${it.id}` }); flatItems.push(it); idx += 1; } } return { rows: flatRows, items: flatItems }; }, [groups, query]); // Clamp highlighted index when results shrink. useEffect(() => { if (items.length === 0) { if (highlighted !== 0) setHighlighted(0); return; } if (highlighted > items.length - 1) setHighlighted(items.length - 1); }, [items.length, highlighted]); // Reset query when opening. useEffect(() => { if (open) { setQuery(''); setHighlighted(0); } }, [open]); // Autofocus input when opened. useEffect(() => { if (!open) return; const t = setTimeout(() => inputRef.current?.focus(), 0); return () => clearTimeout(t); }, [open]); // Global shortcut to toggle open. const parsedShortcut = useMemo( () => (shortcut ? parseShortcut(shortcut) : null), [shortcut], ); useEventListener( 'keydown', (e) => { if (!parsedShortcut) return; if (matchesShortcut(e, parsedShortcut)) { e.preventDefault(); onOpenChange(!open); } }, typeof window !== 'undefined' ? window : null, ); useEscape(() => { if (open) onOpenChange(false); }, open); useScrollLock(open); useFocusTrap(open, panelRef); const handleInputKeyDown = (e: ReactKeyboardEvent) => { if (items.length === 0) return; if (e.key === 'ArrowDown') { e.preventDefault(); setHighlighted((h) => (h + 1) % items.length); } else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlighted((h) => (h - 1 + items.length) % items.length); } else if (e.key === 'Home') { e.preventDefault(); setHighlighted(0); } else if (e.key === 'End') { e.preventDefault(); setHighlighted(items.length - 1); } else if (e.key === 'Enter') { e.preventDefault(); const it = items[highlighted]; if (it) { it.onSelect(); } } }; if (!open) return null; const optionIdFor = (id: string) => `${listboxId}-opt-${id}`; const activeId = items[highlighted] ? optionIdFor(items[highlighted].id) : undefined; const hasListbox = items.length > 0; return (
onOpenChange(false)} />
{'>'} { setQuery(e.target.value); setHighlighted(0); }} onKeyDown={handleInputKeyDown} className={cn( 'flex-1 bg-transparent text-sm text-retro-text outline-none placeholder:text-retro-muted', s.font, )} />
{items.length === 0 ? (
{emptyMessage}
) : (
    {rows.map((row) => { if (row.kind === 'heading') { return (
  • {row.heading}
  • ); } const it = row.item; const isActive = row.index === highlighted; return (
  • setHighlighted(row.index)} onMouseDown={(e) => { // Prevent input blur before click fires. e.preventDefault(); }} onClick={() => { it.onSelect(); }} className={cn( 'flex cursor-pointer items-center gap-2 px-2 py-1.5 text-sm text-retro-text', s.font, s.radius, isActive && 'bg-retro-surface/80 text-retro-text', !isActive && 'hover:bg-retro-surface/40', )} > {it.icon && ( {it.icon} )} {it.label} {it.shortcut && ( {it.shortcut} )}
  • ); })}
)}
); }, ); PixelCommand.displayName = 'PixelCommand';