()
| 9 | import { cn } from "@/lib/utils"; |
| 10 | |
| 11 | export function ShortcutsHelp() { |
| 12 | const { helpOpen, closeHelp, commands } = useCommandRegistry(); |
| 13 | const [filter, setFilter] = useState(""); |
| 14 | |
| 15 | const groups = useMemo(() => { |
| 16 | const q = filter.toLowerCase(); |
| 17 | return SHORTCUT_CATEGORIES.map((cat) => ({ |
| 18 | category: cat, |
| 19 | commands: commands.filter( |
| 20 | (c) => |
| 21 | c.category === cat && |
| 22 | c.keys.length > 0 && |
| 23 | (!q || |
| 24 | c.label.toLowerCase().includes(q) || |
| 25 | c.description.toLowerCase().includes(q)) |
| 26 | ), |
| 27 | })).filter((g) => g.commands.length > 0); |
| 28 | }, [commands, filter]); |
| 29 | |
| 30 | return ( |
| 31 | <Dialog.Root open={helpOpen} onOpenChange={(open) => !open && closeHelp()}> |
| 32 | <Dialog.Portal> |
| 33 | <Dialog.Overlay className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" /> |
| 34 | <Dialog.Content |
| 35 | className={cn( |
| 36 | "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50", |
| 37 | "w-full max-w-2xl max-h-[80vh] flex flex-col", |
| 38 | "bg-surface-900 border border-surface-700 rounded-xl shadow-2xl", |
| 39 | "data-[state=open]:animate-in data-[state=closed]:animate-out", |
| 40 | "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", |
| 41 | "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95" |
| 42 | )} |
| 43 | > |
| 44 | {/* Header */} |
| 45 | <div className="flex items-center justify-between px-5 py-4 border-b border-surface-800 flex-shrink-0"> |
| 46 | <div> |
| 47 | <Dialog.Title className="text-sm font-semibold text-surface-100"> |
| 48 | Keyboard Shortcuts |
| 49 | </Dialog.Title> |
| 50 | <Dialog.Description className="text-xs text-surface-500 mt-0.5"> |
| 51 | Press <kbd className="inline-flex items-center h-4 px-1 rounded bg-surface-800 border border-surface-700 text-[10px] font-mono">?</kbd> anytime to open this panel |
| 52 | </Dialog.Description> |
| 53 | </div> |
| 54 | <Dialog.Close className="p-1.5 rounded-md text-surface-500 hover:text-surface-100 hover:bg-surface-800 transition-colors"> |
| 55 | <X className="w-4 h-4" /> |
| 56 | </Dialog.Close> |
| 57 | </div> |
| 58 | |
| 59 | {/* Search */} |
| 60 | <div className="px-4 py-2.5 border-b border-surface-800 flex-shrink-0"> |
| 61 | <div className="flex items-center gap-2 bg-surface-800 rounded-lg px-3 py-1.5"> |
| 62 | <Search className="w-3.5 h-3.5 text-surface-500 flex-shrink-0" /> |
| 63 | <input |
| 64 | value={filter} |
| 65 | onChange={(e) => setFilter(e.target.value)} |
| 66 | placeholder="Filter shortcuts..." |
| 67 | className="flex-1 bg-transparent text-sm text-surface-100 placeholder:text-surface-500 focus:outline-none" |
| 68 | autoFocus |
nothing calls this directly
no test coverage detected