11"use client" ;
22
3+ import { useMemo } from "react" ;
34import FocusTrap from "@/components/FocusTrap" ;
5+ import { useShortcutRegistry , type ShortcutDefinition } from "@/context/ShortcutRegistry" ;
46
5- interface ShortcutEntry {
6- keys : string [ ] ;
7- description : string ;
7+ interface Props {
8+ onClose : ( ) => void ;
89}
910
10- const SHORTCUTS : ShortcutEntry [ ] = [
11- { keys : [ "?" ] , description : "Open keyboard shortcuts reference" } ,
12- { keys : [ "⌘" , "K" ] , description : "Open command palette" } ,
13- { keys : [ "N" ] , description : "Create new invoice (on dashboard)" } ,
14- { keys : [ "Esc" ] , description : "Close modal / dismiss overlay" } ,
15- { keys : [ "G" , "D" ] , description : "Navigate to Dashboard" } ,
16- { keys : [ "G" , "S" ] , description : "Navigate to Search" } ,
17- { keys : [ "G" , "L" ] , description : "Navigate to Leaderboard" } ,
18- ] ;
11+ // ── Helpers ───────────────────────────────────────────────────────────────────
1912
20- interface Props {
21- onClose : ( ) => void ;
13+ /**
14+ * Group shortcuts by their `group` field.
15+ * Groups are returned in the order they first appear in the registry,
16+ * with "General" always first when present.
17+ */
18+ function groupShortcuts (
19+ shortcuts : ShortcutDefinition [ ] ,
20+ ) : Array < { group : string ; entries : ShortcutDefinition [ ] } > {
21+ const map = new Map < string , ShortcutDefinition [ ] > ( ) ;
22+
23+ // Always seed General first so it stays at the top
24+ map . set ( "General" , [ ] ) ;
25+
26+ for ( const s of shortcuts ) {
27+ // Hide internal chord-activation entries from the overlay
28+ if ( s . id === "global:g-chord" ) continue ;
29+
30+ const group = s . group ?? "General" ;
31+ if ( ! map . has ( group ) ) map . set ( group , [ ] ) ;
32+ map . get ( group ) ! . push ( s ) ;
33+ }
34+
35+ // Remove empty groups
36+ return Array . from ( map . entries ( ) )
37+ . filter ( ( [ , entries ] ) => entries . length > 0 )
38+ . map ( ( [ group , entries ] ) => ( { group, entries } ) ) ;
39+ }
40+
41+ // ── Kbd chip ──────────────────────────────────────────────────────────────────
42+
43+ function KbdKey ( { label } : { label : string } ) {
44+ return (
45+ < kbd className = "inline-flex items-center justify-center px-1.5 py-0.5 min-w-[1.5rem] rounded-md text-xs font-mono font-semibold text-gray-200 bg-gray-700 border border-gray-600 shadow-sm shadow-black/30" >
46+ { label }
47+ </ kbd >
48+ ) ;
2249}
2350
51+ // ── Modal ─────────────────────────────────────────────────────────────────────
52+
2453/**
2554 * KeyboardShortcutsModal
2655 *
27- * A full-screen overlay that lists all global keyboard shortcuts.
28- * Triggered by pressing `?` outside text inputs, or clicking the `?`
29- * icon in the header. Closed by pressing Escape (via FocusTrap) or
56+ * A help overlay that lists **all shortcuts registered via ShortcutRegistry**.
57+ * Shortcuts are grouped by their `group` field (defaults to "General").
58+ *
59+ * Triggered by pressing `?` outside text inputs, or clicking the `?` icon in
60+ * the header. Closed by pressing Escape (handled in useKeyboardShortcuts) or
3061 * clicking the backdrop / close button.
62+ *
63+ * Components register shortcuts with `useRegisterShortcuts` — the overlay
64+ * automatically reflects additions and removals without any manual wiring.
3165 */
3266export default function KeyboardShortcutsModal ( { onClose } : Props ) {
67+ const { shortcuts } = useShortcutRegistry ( ) ;
68+ const grouped = useMemo ( ( ) => groupShortcuts ( shortcuts ) , [ shortcuts ] ) ;
69+
3370 return (
3471 < div
3572 className = "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm px-4"
@@ -40,17 +77,17 @@ export default function KeyboardShortcutsModal({ onClose }: Props) {
4077 >
4178 < FocusTrap onClose = { onClose } >
4279 < div
43- className = "relative w-full max-w-lg rounded-2xl border border-gray-700/60 bg-gray-900/95 shadow-2xl shadow-black/60 p-0 overflow-hidden"
80+ className = "relative w-full max-w-lg rounded-2xl border border-gray-700/60 bg-gray-900/95 shadow-2xl shadow-black/60 overflow-hidden max-h-[85vh] flex flex-col "
4481 onClick = { ( e ) => e . stopPropagation ( ) }
4582 >
4683 { /* Gradient accent strip */ }
47- < div className = "h-1 w-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500" />
84+ < div className = "h-1 w-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 shrink-0 " />
4885
49- < div className = "px-6 py-5 " >
86+ < div className = "px-6 pt-5 pb-4 overflow-y-auto " >
5087 { /* Header */ }
5188 < div className = "flex items-center justify-between mb-5" >
5289 < div className = "flex items-center gap-2.5" >
53- < span className = "inline-flex items-center justify-center w-8 h-8 rounded-lg bg-indigo-500/20 border border-indigo-500/30" >
90+ < span className = "inline-flex items-center justify-center w-8 h-8 rounded-lg bg-indigo-500/20 border border-indigo-500/30 shrink-0 " >
5491 < svg
5592 xmlns = "http://www.w3.org/2000/svg"
5693 className = "h-4 w-4 text-indigo-400"
@@ -60,26 +97,28 @@ export default function KeyboardShortcutsModal({ onClose }: Props) {
6097 strokeWidth = { 2 }
6198 aria-hidden = "true"
6299 >
63- < path
64- strokeLinecap = "round"
65- strokeLinejoin = "round"
66- d = "M11 4a1 1 0 011-1h.01a1 1 0 110 2H12a1 1 0 01-1-1zm0 4a1 1 0 011-1h.01a1 1 0 110 2H12a1 1 0 01-1-1zM5 8a1 1 0 011-1h12a1 1 0 110 2H6a1 1 0 01-1-1zM5 12a1 1 0 011-1h12a1 1 0 110 2H6a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H6a1 1 0 01-1-1z"
67- />
100+ < rect x = "2" y = "4" width = "20" height = "16" rx = "2" />
101+ < path d = "M6 8h.01M10 8h.01M14 8h.01M18 8h.01M6 12h.01M10 12h.01M14 12h4M6 16h12" />
68102 </ svg >
69103 </ span >
70- < h2
71- id = "kbd-shortcuts-title"
72- className = "text-base font-semibold text-gray-100 tracking-tight"
73- >
74- Keyboard Shortcuts
75- </ h2 >
104+ < div >
105+ < h2
106+ id = "kbd-shortcuts-title"
107+ className = "text-base font-semibold text-gray-100 tracking-tight"
108+ >
109+ Keyboard Shortcuts
110+ </ h2 >
111+ < p className = "text-xs text-gray-500 mt-0.5" >
112+ { shortcuts . length } shortcut{ shortcuts . length !== 1 ? "s" : "" } registered
113+ </ p >
114+ </ div >
76115 </ div >
77116
78117 < button
79118 id = "kbd-shortcuts-close-btn"
80119 onClick = { onClose }
81120 aria-label = "Close keyboard shortcuts"
82- className = "p-1.5 rounded-lg text-gray-400 hover:text-gray-100 hover:bg-gray-700/60 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
121+ className = "p-1.5 rounded-lg text-gray-400 hover:text-gray-100 hover:bg-gray-700/60 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 shrink-0 "
83122 >
84123 < svg
85124 xmlns = "http://www.w3.org/2000/svg"
@@ -90,60 +129,66 @@ export default function KeyboardShortcutsModal({ onClose }: Props) {
90129 strokeWidth = { 2 }
91130 aria-hidden = "true"
92131 >
93- < path
94- strokeLinecap = "round"
95- strokeLinejoin = "round"
96- d = "M6 18L18 6M6 6l12 12"
97- />
132+ < path strokeLinecap = "round" strokeLinejoin = "round" d = "M6 18L18 6M6 6l12 12" />
98133 </ svg >
99134 </ button >
100135 </ div >
101136
102- { /* Shortcut table */ }
103- < table
104- className = "w-full text-sm border-separate"
105- style = { { borderSpacing : "0 4px" } }
106- aria-label = "Keyboard shortcuts list"
107- >
108- < thead >
109- < tr >
110- < th className = "text-left text-xs font-medium text-gray-500 uppercase tracking-wider pb-2 pl-3" >
111- Shortcut
112- </ th >
113- < th className = "text-left text-xs font-medium text-gray-500 uppercase tracking-wider pb-2" >
114- Action
115- </ th >
116- </ tr >
117- </ thead >
118- < tbody >
119- { SHORTCUTS . map ( ( entry ) => (
120- < tr
121- key = { entry . description }
122- className = "group"
123- >
124- < td className = "pl-3 pr-4 py-2.5 rounded-l-lg bg-gray-800/50 group-hover:bg-gray-800 transition-colors w-36" >
125- < span className = "flex items-center gap-1" >
126- { entry . keys . map ( ( k ) => (
127- < kbd
128- key = { k }
129- className = "inline-flex items-center justify-center px-1.5 py-0.5 min-w-[1.5rem] rounded-md text-xs font-mono font-semibold text-gray-200 bg-gray-700 border border-gray-600 shadow-sm shadow-black/30"
130- >
131- { k }
132- </ kbd >
137+ { /* Grouped shortcut sections */ }
138+ { grouped . length === 0 ? (
139+ < p className = "text-sm text-gray-500 text-center py-4" >
140+ No shortcuts registered.
141+ </ p >
142+ ) : (
143+ < div className = "flex flex-col gap-5" >
144+ { grouped . map ( ( { group, entries } ) => (
145+ < section key = { group } aria-labelledby = { `kbd-group-${ group } ` } >
146+ < h3
147+ id = { `kbd-group-${ group } ` }
148+ className = "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-1"
149+ >
150+ { group }
151+ </ h3 >
152+
153+ < table
154+ className = "w-full text-sm border-separate"
155+ style = { { borderSpacing : "0 3px" } }
156+ aria-label = { `${ group } keyboard shortcuts` }
157+ >
158+ < tbody >
159+ { entries . map ( ( entry ) => (
160+ < tr key = { entry . id } className = "group" >
161+ < td className = "pl-3 pr-4 py-2 rounded-l-lg bg-gray-800/50 group-hover:bg-gray-800 transition-colors w-40" >
162+ < span className = "flex items-center gap-1 flex-wrap" >
163+ { entry . keys . map ( ( k , i ) => (
164+ < span key = { `${ k } -${ i } ` } className = "flex items-center gap-1" >
165+ { i > 0 && (
166+ < span className = "text-[10px] text-gray-600 mx-0.5" > then</ span >
167+ ) }
168+ < KbdKey label = { k } />
169+ </ span >
170+ ) ) }
171+ </ span >
172+ </ td >
173+ < td className = "pr-3 py-2 rounded-r-lg bg-gray-800/50 group-hover:bg-gray-800 transition-colors text-gray-300 text-sm" >
174+ { entry . description }
175+ </ td >
176+ </ tr >
133177 ) ) }
134- </ span >
135- </ td >
136- < td className = "pr-3 py-2.5 rounded-r-lg bg-gray-800/50 group-hover:bg-gray-800 transition-colors text-gray-300" >
137- { entry . description }
138- </ td >
139- </ tr >
178+ </ tbody >
179+ </ table >
180+ </ section >
140181 ) ) }
141- </ tbody >
142- </ table >
182+ </ div >
183+ ) }
143184
144185 { /* Footer hint */ }
145186 < p className = "mt-5 text-xs text-gray-500 text-center" >
146- Shortcuts are disabled while typing in text fields.
187+ Shortcuts are disabled while typing in text fields.{ " " }
188+ < kbd className = "rounded bg-gray-700 border border-gray-600 px-1 py-0.5 text-[10px] font-mono text-gray-300" >
189+ Esc
190+ </ kbd > { " " }
191+ to close.
147192 </ p >
148193 </ div >
149194 </ div >
0 commit comments