use-kbd 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1064 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as react from 'react';
3
+ import { ReactNode, RefObject, CSSProperties, ComponentType } from 'react';
4
+
5
+ /**
6
+ * Represents a single key press (possibly with modifiers)
7
+ */
8
+ interface KeyCombination {
9
+ /** The main key (lowercase, e.g., 'k', 'enter', 'arrowup') */
10
+ key: string;
11
+ /** Modifier keys pressed */
12
+ modifiers: {
13
+ ctrl: boolean;
14
+ alt: boolean;
15
+ shift: boolean;
16
+ meta: boolean;
17
+ };
18
+ }
19
+ /**
20
+ * Represents a hotkey - either a single key or a sequence of keys.
21
+ * Single key: [{ key: 'k', modifiers: {...} }]
22
+ * Sequence: [{ key: '2', ... }, { key: 'w', ... }]
23
+ */
24
+ type HotkeySequence = KeyCombination[];
25
+ /**
26
+ * Platform-aware display format for a key combination or sequence
27
+ */
28
+ interface KeyCombinationDisplay {
29
+ /** Human-readable string (e.g., "⌘⇧K" on Mac, "Ctrl+Shift+K" elsewhere, "2 W" for sequence) */
30
+ display: string;
31
+ /** Canonical ID for storage/comparison (e.g., "ctrl+shift+k", "2 w" for sequence) */
32
+ id: string;
33
+ /** Whether this is a sequence (multiple keys pressed in order) */
34
+ isSequence: boolean;
35
+ }
36
+ /**
37
+ * Result from the useRecordHotkey hook
38
+ */
39
+ interface RecordHotkeyResult {
40
+ /** Whether currently recording */
41
+ isRecording: boolean;
42
+ /** Start recording - returns cancel function */
43
+ startRecording: () => () => void;
44
+ /** Cancel recording */
45
+ cancel: () => void;
46
+ /** Commit pending keys immediately (if any), otherwise cancel */
47
+ commit: () => void;
48
+ /** The captured sequence (null until complete) */
49
+ sequence: HotkeySequence | null;
50
+ /** Display strings for the sequence */
51
+ display: KeyCombinationDisplay | null;
52
+ /** Keys captured so far during recording (for live UI feedback) */
53
+ pendingKeys: HotkeySequence;
54
+ /** The key currently being held (for live UI feedback during recording) */
55
+ activeKeys: KeyCombination | null;
56
+ /**
57
+ * @deprecated Use `sequence` instead
58
+ */
59
+ combination: KeyCombination | null;
60
+ }
61
+ /**
62
+ * Options for useRecordHotkey
63
+ */
64
+ interface RecordHotkeyOptions {
65
+ /** Called when a sequence is captured (timeout or Enter) */
66
+ onCapture?: (sequence: HotkeySequence, display: KeyCombinationDisplay) => void;
67
+ /** Called when recording is cancelled */
68
+ onCancel?: () => void;
69
+ /** Called when Tab is pressed during recording (for advancing to next field) */
70
+ onTab?: () => void;
71
+ /** Called when Shift+Tab is pressed during recording (for going to previous field) */
72
+ onShiftTab?: () => void;
73
+ /** Prevent default on captured keys (default: true) */
74
+ preventDefault?: boolean;
75
+ /** Timeout in ms before sequence is submitted (default: 1000) */
76
+ sequenceTimeout?: number;
77
+ /** When true, pause the auto-submit timeout (useful for conflict warnings). Default: false */
78
+ pauseTimeout?: boolean;
79
+ }
80
+ /**
81
+ * Definition of an action that can be triggered by hotkeys or omnibar
82
+ */
83
+ interface ActionDefinition {
84
+ /** Display label for the action */
85
+ label: string;
86
+ /** Longer description (shown in omnibar, tooltips) */
87
+ description?: string;
88
+ /** Group for organizing in shortcuts modal (e.g., "Metrics", "Time Range") */
89
+ group?: string;
90
+ /** Additional search keywords */
91
+ keywords?: string[];
92
+ /** Icon identifier (user provides rendering) */
93
+ icon?: string;
94
+ /** Whether the action is currently enabled (default: true) */
95
+ enabled?: boolean;
96
+ }
97
+ /**
98
+ * Registry of all available actions
99
+ */
100
+ type ActionRegistry = Record<string, ActionDefinition>;
101
+ /**
102
+ * An action with its current keybinding(s) and search match info
103
+ */
104
+ interface ActionSearchResult {
105
+ /** Action ID */
106
+ id: string;
107
+ /** Action definition */
108
+ action: ActionDefinition;
109
+ /** Current keybindings for this action */
110
+ bindings: string[];
111
+ /** Fuzzy match score (higher = better match) */
112
+ score: number;
113
+ /** Matched ranges in label for highlighting */
114
+ labelMatches: Array<[number, number]>;
115
+ }
116
+ /**
117
+ * A possible completion for a partially-typed sequence
118
+ */
119
+ interface SequenceCompletion {
120
+ /** The next key(s) needed to complete this sequence */
121
+ nextKeys: string;
122
+ /** The full hotkey string */
123
+ fullSequence: string;
124
+ /** Display format for the full sequence */
125
+ display: KeyCombinationDisplay;
126
+ /** Actions triggered by this sequence */
127
+ actions: string[];
128
+ }
129
+
130
+ /**
131
+ * Hotkey definition - maps key combinations/sequences to action names
132
+ */
133
+ type HotkeyMap = Record<string, string | string[]>;
134
+ /**
135
+ * Handler map - maps action names to handler functions
136
+ */
137
+ type HandlerMap = Record<string, (e: KeyboardEvent) => void>;
138
+ interface UseHotkeysOptions {
139
+ /** Whether hotkeys are enabled (default: true) */
140
+ enabled?: boolean;
141
+ /** Element to attach listeners to (default: window) */
142
+ target?: HTMLElement | Window | null;
143
+ /** Prevent default on matched hotkeys (default: true) */
144
+ preventDefault?: boolean;
145
+ /** Stop propagation on matched hotkeys (default: true) */
146
+ stopPropagation?: boolean;
147
+ /** Enable hotkeys even when focused on input/textarea/select (default: false) */
148
+ enableOnFormTags?: boolean;
149
+ /** Timeout in ms for sequences (default: 1000) */
150
+ sequenceTimeout?: number;
151
+ /** What happens on timeout: 'submit' executes current sequence, 'cancel' resets (default: 'submit') */
152
+ onTimeout?: 'submit' | 'cancel';
153
+ /** Called when sequence input starts */
154
+ onSequenceStart?: (keys: HotkeySequence) => void;
155
+ /** Called when sequence progresses (new key added) */
156
+ onSequenceProgress?: (keys: HotkeySequence) => void;
157
+ /** Called when sequence is cancelled (timeout with 'cancel' mode, or no match) */
158
+ onSequenceCancel?: () => void;
159
+ }
160
+ interface UseHotkeysResult {
161
+ /** Keys pressed so far in current sequence */
162
+ pendingKeys: HotkeySequence;
163
+ /** Whether currently awaiting more keys in a sequence */
164
+ isAwaitingSequence: boolean;
165
+ /** Cancel the current sequence */
166
+ cancelSequence: () => void;
167
+ /** When the current sequence timeout started (null if not awaiting) */
168
+ timeoutStartedAt: number | null;
169
+ /** The sequence timeout duration in ms */
170
+ sequenceTimeout: number;
171
+ }
172
+ /**
173
+ * Hook to register keyboard shortcuts with sequence support.
174
+ *
175
+ * @example
176
+ * ```tsx
177
+ * // Single keys
178
+ * const { pendingKeys } = useHotkeys(
179
+ * { 't': 'setTemp', 'ctrl+s': 'save' },
180
+ * { setTemp: () => setMetric('temp'), save: handleSave }
181
+ * )
182
+ *
183
+ * // Sequences
184
+ * const { pendingKeys, isAwaitingSequence } = useHotkeys(
185
+ * { '2 w': 'twoWeeks', '2 d': 'twoDays' },
186
+ * { twoWeeks: () => setRange('2w'), twoDays: () => setRange('2d') },
187
+ * { sequenceTimeout: 1000 }
188
+ * )
189
+ * ```
190
+ */
191
+ declare function useHotkeys(keymap: HotkeyMap, handlers: HandlerMap, options?: UseHotkeysOptions): UseHotkeysResult;
192
+
193
+ interface UseEditableHotkeysOptions extends UseHotkeysOptions {
194
+ /** localStorage key for persistence (omit to disable persistence) */
195
+ storageKey?: string;
196
+ /** When true, keys with multiple actions bound are disabled (default: true) */
197
+ disableConflicts?: boolean;
198
+ }
199
+ interface UseEditableHotkeysResult {
200
+ /** Current keymap (defaults merged with user overrides) */
201
+ keymap: HotkeyMap;
202
+ /** Update a single keybinding */
203
+ setBinding: (action: string, key: string) => void;
204
+ /** Update multiple keybindings at once */
205
+ setKeymap: (overrides: Partial<HotkeyMap>) => void;
206
+ /** Reset all overrides to defaults */
207
+ reset: () => void;
208
+ /** User overrides only (for inspection/export) */
209
+ overrides: Partial<HotkeyMap>;
210
+ /** Map of key -> actions[] for keys with multiple actions bound */
211
+ conflicts: Map<string, string[]>;
212
+ /** Whether there are any conflicts in the current keymap */
213
+ hasConflicts: boolean;
214
+ /** Keys pressed so far in current sequence */
215
+ pendingKeys: HotkeySequence;
216
+ /** Whether currently awaiting more keys in a sequence */
217
+ isAwaitingSequence: boolean;
218
+ /** Cancel the current sequence */
219
+ cancelSequence: () => void;
220
+ /** When the current sequence timeout started (null if not awaiting) */
221
+ timeoutStartedAt: number | null;
222
+ /** The sequence timeout duration in ms */
223
+ sequenceTimeout: number;
224
+ }
225
+ /**
226
+ * Wraps useHotkeys with editable keybindings and optional persistence.
227
+ *
228
+ * @example
229
+ * ```tsx
230
+ * const { keymap, setBinding, reset } = useEditableHotkeys(
231
+ * { 't': 'setTemp', 'c': 'setCO2' },
232
+ * { setTemp: () => setMetric('temp'), setCO2: () => setMetric('co2') },
233
+ * { storageKey: 'app-hotkeys' }
234
+ * )
235
+ * ```
236
+ */
237
+ declare function useEditableHotkeys(defaults: HotkeyMap, handlers: HandlerMap, options?: UseEditableHotkeysOptions): UseEditableHotkeysResult;
238
+
239
+ interface UseOmnibarOptions {
240
+ /** Registry of available actions */
241
+ actions: ActionRegistry;
242
+ /** Handlers for actions (optional - if not provided, use onExecute callback) */
243
+ handlers?: HandlerMap;
244
+ /** Current keymap (to show bindings in results) */
245
+ keymap?: HotkeyMap;
246
+ /** Hotkey to open omnibar (default: 'meta+k') */
247
+ openKey?: string;
248
+ /** Whether omnibar hotkey is enabled (default: true) */
249
+ enabled?: boolean;
250
+ /** Called when an action is executed (if handlers not provided, or in addition to) */
251
+ onExecute?: (actionId: string) => void;
252
+ /** Called when omnibar opens */
253
+ onOpen?: () => void;
254
+ /** Called when omnibar closes */
255
+ onClose?: () => void;
256
+ /** Maximum number of results to show (default: 10) */
257
+ maxResults?: number;
258
+ }
259
+ interface UseOmnibarResult {
260
+ /** Whether omnibar is open */
261
+ isOpen: boolean;
262
+ /** Open the omnibar */
263
+ open: () => void;
264
+ /** Close the omnibar */
265
+ close: () => void;
266
+ /** Toggle the omnibar */
267
+ toggle: () => void;
268
+ /** Current search query */
269
+ query: string;
270
+ /** Set the search query */
271
+ setQuery: (query: string) => void;
272
+ /** Search results (filtered and sorted) */
273
+ results: ActionSearchResult[];
274
+ /** Currently selected result index */
275
+ selectedIndex: number;
276
+ /** Select the next result */
277
+ selectNext: () => void;
278
+ /** Select the previous result */
279
+ selectPrev: () => void;
280
+ /** Execute the selected action (or a specific action by ID) */
281
+ execute: (actionId?: string) => void;
282
+ /** Reset selection to first result */
283
+ resetSelection: () => void;
284
+ /** Sequence completions based on pending keys */
285
+ completions: SequenceCompletion[];
286
+ /** Keys pressed so far in current sequence (from useHotkeys) */
287
+ pendingKeys: HotkeySequence;
288
+ /** Whether currently awaiting more keys in a sequence */
289
+ isAwaitingSequence: boolean;
290
+ }
291
+ /**
292
+ * Hook for implementing an omnibar/command palette.
293
+ *
294
+ * @example
295
+ * ```tsx
296
+ * const ACTIONS: ActionRegistry = {
297
+ * 'metric:temp': { label: 'Temperature', category: 'Metrics' },
298
+ * 'metric:co2': { label: 'CO₂', category: 'Metrics' },
299
+ * 'save': { label: 'Save', description: 'Save current settings' },
300
+ * }
301
+ *
302
+ * function App() {
303
+ * const {
304
+ * isOpen, open, close,
305
+ * query, setQuery,
306
+ * results,
307
+ * selectedIndex, selectNext, selectPrev,
308
+ * execute,
309
+ * } = useOmnibar({
310
+ * actions: ACTIONS,
311
+ * handlers: HANDLERS,
312
+ * keymap: KEYMAP,
313
+ * })
314
+ *
315
+ * return (
316
+ * <>
317
+ * {isOpen && (
318
+ * <div className="omnibar">
319
+ * <input
320
+ * value={query}
321
+ * onChange={e => setQuery(e.target.value)}
322
+ * onKeyDown={e => {
323
+ * if (e.key === 'ArrowDown') selectNext()
324
+ * if (e.key === 'ArrowUp') selectPrev()
325
+ * if (e.key === 'Enter') execute()
326
+ * if (e.key === 'Escape') close()
327
+ * }}
328
+ * />
329
+ * {results.map((result, i) => (
330
+ * <div
331
+ * key={result.id}
332
+ * className={i === selectedIndex ? 'selected' : ''}
333
+ * onClick={() => execute(result.id)}
334
+ * >
335
+ * {result.action.label}
336
+ * {result.bindings.length > 0 && (
337
+ * <kbd>{result.bindings[0]}</kbd>
338
+ * )}
339
+ * </div>
340
+ * ))}
341
+ * </div>
342
+ * )}
343
+ * </>
344
+ * )
345
+ * }
346
+ * ```
347
+ */
348
+ declare function useOmnibar(options: UseOmnibarOptions): UseOmnibarResult;
349
+
350
+ interface KeybindingEditorProps {
351
+ /** Current keymap */
352
+ keymap: HotkeyMap;
353
+ /** Default keymap (for reset functionality) */
354
+ defaults: HotkeyMap;
355
+ /** Descriptions for actions */
356
+ descriptions?: Record<string, string>;
357
+ /** Called when a binding changes */
358
+ onChange: (action: string, key: string) => void;
359
+ /** Called when reset is requested */
360
+ onReset?: () => void;
361
+ /** CSS class for the container */
362
+ className?: string;
363
+ /** Custom render function */
364
+ children?: (props: KeybindingEditorRenderProps) => ReactNode;
365
+ }
366
+ interface KeybindingEditorRenderProps {
367
+ bindings: BindingInfo[];
368
+ editingAction: string | null;
369
+ /** Keys already pressed and released (waiting for timeout or more keys) */
370
+ pendingKeys: HotkeySequence;
371
+ /** Keys currently being held down */
372
+ activeKeys: KeyCombination | null;
373
+ startEditing: (action: string) => void;
374
+ cancelEditing: () => void;
375
+ reset: () => void;
376
+ conflicts: Map<string, string[]>;
377
+ }
378
+ interface BindingInfo {
379
+ action: string;
380
+ key: string;
381
+ display: KeyCombinationDisplay;
382
+ description: string;
383
+ isDefault: boolean;
384
+ hasConflict: boolean;
385
+ }
386
+ /**
387
+ * UI component for editing keybindings.
388
+ *
389
+ * @example
390
+ * ```tsx
391
+ * <KeybindingEditor
392
+ * keymap={keymap}
393
+ * defaults={DEFAULT_KEYMAP}
394
+ * descriptions={{ save: 'Save document' }}
395
+ * onChange={(action, key) => setBinding(action, key)}
396
+ * onReset={() => reset()}
397
+ * />
398
+ * ```
399
+ */
400
+ declare function KeybindingEditor({ keymap, defaults, descriptions, onChange, onReset, className, children, }: KeybindingEditorProps): react_jsx_runtime.JSX.Element;
401
+
402
+ interface ShortcutGroup {
403
+ name: string;
404
+ shortcuts: Array<{
405
+ actionId: string;
406
+ label: string;
407
+ description?: string;
408
+ bindings: string[];
409
+ }>;
410
+ }
411
+ /**
412
+ * Props passed to custom group renderers
413
+ */
414
+ interface GroupRendererProps {
415
+ /** The group being rendered */
416
+ group: ShortcutGroup;
417
+ /** Render a cell for an action (handles editing state, kbd styling) */
418
+ renderCell: (actionId: string, keys: string[]) => ReactNode;
419
+ /** Render a single editable kbd element */
420
+ renderEditableKbd: (actionId: string, key: string, showRemove?: boolean) => ReactNode;
421
+ /** Render the add button for an action */
422
+ renderAddButton: (actionId: string) => ReactNode;
423
+ /** Start editing a specific binding */
424
+ startEditing: (actionId: string, key: string) => void;
425
+ /** Start adding a new binding to an action */
426
+ startAdding: (actionId: string) => void;
427
+ /** Remove a binding */
428
+ removeBinding: (actionId: string, key: string) => void;
429
+ /** Whether currently recording a hotkey */
430
+ isRecording: boolean;
431
+ /** Action currently being edited */
432
+ editingAction: string | null;
433
+ /** Key currently being edited */
434
+ editingKey: string | null;
435
+ /** Action currently being added to */
436
+ addingAction: string | null;
437
+ }
438
+ /**
439
+ * Custom renderer for a group. Return null to use default rendering.
440
+ */
441
+ type GroupRenderer = (props: GroupRendererProps) => ReactNode;
442
+ interface ShortcutsModalProps {
443
+ /**
444
+ * The hotkey map to display.
445
+ * If not provided, uses keymap from HotkeysContext.
446
+ */
447
+ keymap?: HotkeyMap;
448
+ /**
449
+ * Default keymap (for showing reset indicators).
450
+ * If not provided, uses defaults from HotkeysContext.
451
+ */
452
+ defaults?: HotkeyMap;
453
+ /** Labels for actions (action ID -> label). Falls back to action.label from context. */
454
+ labels?: Record<string, string>;
455
+ /** Descriptions for actions (action ID -> description). Falls back to action.description from context. */
456
+ descriptions?: Record<string, string>;
457
+ /** Group definitions: action prefix -> display name (e.g., { metric: 'Metrics' }). Falls back to action.group from context. */
458
+ groups?: Record<string, string>;
459
+ /** Ordered list of group names (if omitted, groups are sorted alphabetically) */
460
+ groupOrder?: string[];
461
+ /**
462
+ * Custom renderers for specific groups.
463
+ * Key is the group name, value is a render function.
464
+ * Groups without custom renderers use the default single-column layout.
465
+ */
466
+ groupRenderers?: Record<string, GroupRenderer>;
467
+ /**
468
+ * Control visibility externally.
469
+ * If not provided, uses isModalOpen from HotkeysContext.
470
+ */
471
+ isOpen?: boolean;
472
+ /**
473
+ * Called when modal should close.
474
+ * If not provided, uses closeModal from HotkeysContext.
475
+ */
476
+ onClose?: () => void;
477
+ /** Hotkey to open modal (default: '?'). Set to empty string to disable. */
478
+ openKey?: string;
479
+ /**
480
+ * Whether to auto-register the open hotkey (default: true).
481
+ * When using HotkeysContext, the provider already handles this, so set to false.
482
+ */
483
+ autoRegisterOpen?: boolean;
484
+ /** Enable editing mode */
485
+ editable?: boolean;
486
+ /** Called when a binding changes (required if editable) */
487
+ onBindingChange?: (action: string, oldKey: string | null, newKey: string) => void;
488
+ /** Called when a binding is added (required if editable) */
489
+ onBindingAdd?: (action: string, key: string) => void;
490
+ /** Called when a binding is removed */
491
+ onBindingRemove?: (action: string, key: string) => void;
492
+ /** Called when reset is requested */
493
+ onReset?: () => void;
494
+ /** Whether to allow multiple bindings per action (default: true) */
495
+ multipleBindings?: boolean;
496
+ /** Custom render function for the modal content */
497
+ children?: (props: ShortcutsModalRenderProps) => ReactNode;
498
+ /** CSS class for the backdrop */
499
+ backdropClassName?: string;
500
+ /** CSS class for the modal container */
501
+ modalClassName?: string;
502
+ /** Modal title (default: "Keyboard Shortcuts") */
503
+ title?: string;
504
+ /** Hint text shown below title (e.g., "Click any key to customize") */
505
+ hint?: string;
506
+ }
507
+ interface ShortcutsModalRenderProps {
508
+ groups: ShortcutGroup[];
509
+ close: () => void;
510
+ editable: boolean;
511
+ editingAction: string | null;
512
+ editingBindingIndex: number | null;
513
+ pendingKeys: HotkeySequence;
514
+ activeKeys: KeyCombination | null;
515
+ conflicts: Map<string, string[]>;
516
+ startEditing: (action: string, bindingIndex?: number) => void;
517
+ cancelEditing: () => void;
518
+ removeBinding: (action: string, key: string) => void;
519
+ reset: () => void;
520
+ }
521
+ /**
522
+ * Modal component for displaying and optionally editing keyboard shortcuts.
523
+ *
524
+ * Uses CSS classes from styles.css. Override via CSS custom properties:
525
+ * --kbd-bg, --kbd-text, --kbd-kbd-bg, etc.
526
+ *
527
+ * @example
528
+ * ```tsx
529
+ * // Read-only display
530
+ * <ShortcutsModal
531
+ * keymap={HOTKEYS}
532
+ * labels={{ 'metric:temp': 'Temperature' }}
533
+ * />
534
+ *
535
+ * // Editable with callbacks
536
+ * <ShortcutsModal
537
+ * keymap={keymap}
538
+ * defaults={DEFAULT_KEYMAP}
539
+ * labels={labels}
540
+ * editable
541
+ * onBindingChange={(action, oldKey, newKey) => updateBinding(action, newKey)}
542
+ * onBindingRemove={(action, key) => removeBinding(action, key)}
543
+ * />
544
+ * ```
545
+ */
546
+ declare function ShortcutsModal({ keymap: keymapProp, defaults: defaultsProp, labels: labelsProp, descriptions: descriptionsProp, groups: groupNamesProp, groupOrder, groupRenderers, isOpen: isOpenProp, onClose: onCloseProp, openKey, autoRegisterOpen, editable, onBindingChange, onBindingAdd, onBindingRemove, onReset, multipleBindings, children, backdropClassName, modalClassName, title, hint, }: ShortcutsModalProps): react_jsx_runtime.JSX.Element | null;
547
+
548
+ /**
549
+ * Configuration for a row in a two-column table
550
+ */
551
+ interface TwoColumnRow {
552
+ /** Label for the row (first column) */
553
+ label: ReactNode;
554
+ /** Action ID for the left/first column */
555
+ leftAction: string;
556
+ /** Action ID for the right/second column */
557
+ rightAction: string;
558
+ }
559
+ /**
560
+ * Configuration for creating a two-column group renderer
561
+ */
562
+ interface TwoColumnConfig {
563
+ /** Column headers: [label, left, right] */
564
+ headers: [string, string, string];
565
+ /**
566
+ * Extract rows from the group's shortcuts.
567
+ * Return array of { label, leftAction, rightAction }.
568
+ */
569
+ getRows: (group: ShortcutGroup) => TwoColumnRow[];
570
+ }
571
+ /**
572
+ * Create a GroupRenderer that displays shortcuts in a two-column table.
573
+ *
574
+ * @example
575
+ * ```tsx
576
+ * // Pair actions by suffix (left:temp/right:temp)
577
+ * const YAxisRenderer = createTwoColumnRenderer({
578
+ * headers: ['Metric', 'Left', 'Right'],
579
+ * getRows: (group) => {
580
+ * const metrics = ['temp', 'co2', 'humid']
581
+ * return metrics.map(m => ({
582
+ * label: m,
583
+ * leftAction: `left:${m}`,
584
+ * rightAction: `right:${m}`,
585
+ * }))
586
+ * },
587
+ * })
588
+ *
589
+ * // Explicit pairs
590
+ * const NavRenderer = createTwoColumnRenderer({
591
+ * headers: ['Navigation', 'Back', 'Forward'],
592
+ * getRows: () => [
593
+ * { label: 'Page', leftAction: 'nav:prev', rightAction: 'nav:next' },
594
+ * ],
595
+ * })
596
+ * ```
597
+ */
598
+ declare function createTwoColumnRenderer(config: TwoColumnConfig): ({ group, renderCell }: GroupRendererProps) => ReactNode;
599
+
600
+ interface OmnibarProps {
601
+ /**
602
+ * Registry of available actions.
603
+ * If not provided, uses actions from HotkeysContext.
604
+ */
605
+ actions?: ActionRegistry;
606
+ /**
607
+ * Handlers for actions.
608
+ * If not provided, uses handlers from HotkeysContext, falling back to executeAction.
609
+ */
610
+ handlers?: HandlerMap;
611
+ /**
612
+ * Current keymap (to show bindings in results).
613
+ * If not provided, uses keymap from HotkeysContext.
614
+ */
615
+ keymap?: HotkeyMap;
616
+ /** Hotkey to open omnibar (default: 'meta+k'). Set to empty string to disable. */
617
+ openKey?: string;
618
+ /**
619
+ * Whether omnibar hotkey is enabled.
620
+ * When using HotkeysContext, defaults to false (provider handles it).
621
+ */
622
+ enabled?: boolean;
623
+ /**
624
+ * Control visibility externally.
625
+ * If not provided, uses isOmnibarOpen from HotkeysContext.
626
+ */
627
+ isOpen?: boolean;
628
+ /** Called when omnibar opens */
629
+ onOpen?: () => void;
630
+ /**
631
+ * Called when omnibar closes.
632
+ * If not provided, uses closeOmnibar from HotkeysContext.
633
+ */
634
+ onClose?: () => void;
635
+ /**
636
+ * Called when an action is executed.
637
+ * If not provided, uses executeAction from HotkeysContext.
638
+ */
639
+ onExecute?: (actionId: string) => void;
640
+ /** Maximum number of results to show (default: 10) */
641
+ maxResults?: number;
642
+ /** Placeholder text for input (default: 'Type a command...') */
643
+ placeholder?: string;
644
+ /** Custom render function */
645
+ children?: (props: OmnibarRenderProps) => ReactNode;
646
+ /** CSS class for the backdrop */
647
+ backdropClassName?: string;
648
+ /** CSS class for the omnibar container */
649
+ omnibarClassName?: string;
650
+ }
651
+ interface OmnibarRenderProps {
652
+ query: string;
653
+ setQuery: (query: string) => void;
654
+ results: ActionSearchResult[];
655
+ selectedIndex: number;
656
+ selectNext: () => void;
657
+ selectPrev: () => void;
658
+ execute: (actionId?: string) => void;
659
+ close: () => void;
660
+ completions: SequenceCompletion[];
661
+ pendingKeys: HotkeySequence;
662
+ isAwaitingSequence: boolean;
663
+ inputRef: RefObject<HTMLInputElement | null>;
664
+ }
665
+ /**
666
+ * Omnibar/command palette component for searching and executing actions.
667
+ *
668
+ * Uses CSS classes from styles.css. Override via CSS custom properties:
669
+ * --kbd-bg, --kbd-text, --kbd-accent, etc.
670
+ *
671
+ * @example
672
+ * ```tsx
673
+ * <Omnibar
674
+ * actions={ACTIONS}
675
+ * handlers={HANDLERS}
676
+ * keymap={KEYMAP}
677
+ * onExecute={(id) => console.log('Executed:', id)}
678
+ * />
679
+ * ```
680
+ */
681
+ declare function Omnibar({ actions: actionsProp, handlers: handlersProp, keymap: keymapProp, openKey, enabled: enabledProp, isOpen: isOpenProp, onOpen: onOpenProp, onClose: onCloseProp, onExecute: onExecuteProp, maxResults, placeholder, children, backdropClassName, omnibarClassName, }: OmnibarProps): react_jsx_runtime.JSX.Element | null;
682
+
683
+ /**
684
+ * Detect if running on macOS
685
+ */
686
+ declare function isMac(): boolean;
687
+ /**
688
+ * Normalize a key name to a canonical form
689
+ */
690
+ declare function normalizeKey(key: string): string;
691
+ /**
692
+ * Format a key for display (platform-aware)
693
+ */
694
+ declare function formatKeyForDisplay(key: string): string;
695
+ /**
696
+ * Convert a KeyCombination or HotkeySequence to display format
697
+ */
698
+ declare function formatCombination(combo: KeyCombination): KeyCombinationDisplay;
699
+ declare function formatCombination(sequence: HotkeySequence): KeyCombinationDisplay;
700
+ /**
701
+ * Check if a key is a modifier key
702
+ */
703
+ declare function isModifierKey(key: string): boolean;
704
+ /**
705
+ * Check if a hotkey string represents a sequence (space-separated keys)
706
+ */
707
+ declare function isSequence(hotkeyStr: string): boolean;
708
+ /**
709
+ * Parse a hotkey string to a HotkeySequence.
710
+ * Handles both single keys ("ctrl+k") and sequences ("2 w", "ctrl+k ctrl+c")
711
+ */
712
+ declare function parseHotkeyString(hotkeyStr: string): HotkeySequence;
713
+ /**
714
+ * Parse a combination ID back to a KeyCombination (single key only)
715
+ * @deprecated Use parseHotkeyString for sequence support
716
+ */
717
+ declare function parseCombinationId(id: string): KeyCombination;
718
+ /**
719
+ * Conflict detection result
720
+ */
721
+ interface KeyConflict {
722
+ /** The key combination that has a conflict */
723
+ key: string;
724
+ /** Actions bound to this key */
725
+ actions: string[];
726
+ /** Type of conflict */
727
+ type: 'duplicate' | 'prefix';
728
+ }
729
+ /**
730
+ * Find conflicts in a keymap.
731
+ * Detects:
732
+ * - Duplicate: multiple actions bound to the exact same key/sequence
733
+ * - Prefix: one hotkey is a prefix of another (e.g., "2" and "2 w")
734
+ *
735
+ * @param keymap - HotkeyMap to check for conflicts
736
+ * @returns Map of key -> actions[] for keys with conflicts
737
+ */
738
+ declare function findConflicts(keymap: Record<string, string | string[]>): Map<string, string[]>;
739
+ /**
740
+ * Check if a keymap has any conflicts
741
+ */
742
+ declare function hasConflicts(keymap: Record<string, string | string[]>): boolean;
743
+ /**
744
+ * Get conflicts as an array of KeyConflict objects
745
+ */
746
+ declare function getConflictsArray(keymap: Record<string, string | string[]>): KeyConflict[];
747
+
748
+ /**
749
+ * Get possible completions for a partially-typed sequence.
750
+ *
751
+ * @example
752
+ * ```tsx
753
+ * const keymap = { '2 w': 'twoWeeks', '2 d': 'twoDays', 't': 'temp' }
754
+ * const pending = parseHotkeyString('2')
755
+ * const completions = getSequenceCompletions(pending, keymap)
756
+ * // Returns:
757
+ * // [
758
+ * // { nextKeys: 'w', fullSequence: '2 w', actions: ['twoWeeks'], ... },
759
+ * // { nextKeys: 'd', fullSequence: '2 d', actions: ['twoDays'], ... },
760
+ * // ]
761
+ * ```
762
+ */
763
+ declare function getSequenceCompletions(pendingKeys: HotkeySequence, keymap: Record<string, string | string[]>): SequenceCompletion[];
764
+ /**
765
+ * Build a map of action -> keys[] from a keymap
766
+ */
767
+ declare function getActionBindings(keymap: Record<string, string | string[]>): Map<string, string[]>;
768
+ /**
769
+ * Fuzzy match result
770
+ */
771
+ interface FuzzyMatchResult {
772
+ /** Whether the pattern matched */
773
+ matched: boolean;
774
+ /** Match score (higher = better) */
775
+ score: number;
776
+ /** Matched character ranges for highlighting [start, end] */
777
+ ranges: Array<[number, number]>;
778
+ }
779
+ /**
780
+ * Perform fuzzy matching of a pattern against text.
781
+ * Returns match info including score and ranges for highlighting.
782
+ *
783
+ * Scoring:
784
+ * - Consecutive matches score higher
785
+ * - Matches at word boundaries score higher
786
+ * - Earlier matches score higher
787
+ */
788
+ declare function fuzzyMatch(pattern: string, text: string): FuzzyMatchResult;
789
+ /**
790
+ * Search actions by query with fuzzy matching.
791
+ *
792
+ * @example
793
+ * ```tsx
794
+ * const results = searchActions('temp', actions, keymap)
795
+ * // Returns ActionSearchResult[] sorted by relevance
796
+ * ```
797
+ */
798
+ declare function searchActions(query: string, actions: ActionRegistry, keymap?: Record<string, string | string[]>): ActionSearchResult[];
799
+
800
+ interface ActionConfig {
801
+ /** Human-readable label for omnibar/modal */
802
+ label: string;
803
+ /** Group name for organizing in modal */
804
+ group?: string;
805
+ /** Default key bindings (user can override) */
806
+ defaultBindings?: string[];
807
+ /** Search keywords for omnibar */
808
+ keywords?: string[];
809
+ /** The action handler */
810
+ handler: () => void;
811
+ /** Whether action is currently enabled (default: true) */
812
+ enabled?: boolean;
813
+ /** Priority for conflict resolution (higher wins, default: 0) */
814
+ priority?: number;
815
+ }
816
+ /**
817
+ * Register an action with the hotkeys system.
818
+ *
819
+ * Actions are automatically unregistered when the component unmounts,
820
+ * making this ideal for colocating actions with their handlers.
821
+ *
822
+ * @example
823
+ * ```tsx
824
+ * function DataTable() {
825
+ * const { prevPage, nextPage } = usePagination()
826
+ *
827
+ * useAction('table:prev-page', {
828
+ * label: 'Previous page',
829
+ * group: 'Table Navigation',
830
+ * defaultBindings: [','],
831
+ * handler: prevPage,
832
+ * })
833
+ *
834
+ * useAction('table:next-page', {
835
+ * label: 'Next page',
836
+ * group: 'Table Navigation',
837
+ * defaultBindings: ['.'],
838
+ * handler: nextPage,
839
+ * })
840
+ * }
841
+ * ```
842
+ */
843
+ declare function useAction(id: string, config: ActionConfig): void;
844
+ /**
845
+ * Register multiple actions at once.
846
+ * Useful when you have several related actions in one component.
847
+ *
848
+ * @example
849
+ * ```tsx
850
+ * useActions({
851
+ * 'left:temp': { label: 'Temperature', defaultBindings: ['t'], handler: () => setMetric('temp') },
852
+ * 'left:co2': { label: 'CO₂', defaultBindings: ['c'], handler: () => setMetric('co2') },
853
+ * })
854
+ * ```
855
+ */
856
+ declare function useActions(actions: Record<string, ActionConfig>): void;
857
+
858
+ interface RegisteredAction {
859
+ config: ActionConfig;
860
+ registeredAt: number;
861
+ }
862
+ interface ActionsRegistryValue {
863
+ /** Register an action. Called by useAction on mount. */
864
+ register: (id: string, config: ActionConfig) => void;
865
+ /** Unregister an action. Called by useAction on unmount. */
866
+ unregister: (id: string) => void;
867
+ /** Execute an action by ID */
868
+ execute: (id: string) => void;
869
+ /** Currently registered actions */
870
+ actions: Map<string, RegisteredAction>;
871
+ /** Computed keymap from registered actions + user overrides */
872
+ keymap: HotkeyMap;
873
+ /** Action registry for omnibar search */
874
+ actionRegistry: ActionRegistry;
875
+ /** Get all bindings for an action (defaults + overrides) */
876
+ getBindingsForAction: (id: string) => string[];
877
+ /** User's binding overrides */
878
+ overrides: Record<string, string | string[]>;
879
+ /** Set a user override for a binding */
880
+ setBinding: (actionId: string, key: string) => void;
881
+ /** Remove a binding */
882
+ removeBinding: (key: string) => void;
883
+ /** Reset all overrides */
884
+ resetOverrides: () => void;
885
+ }
886
+ declare const ActionsRegistryContext: react.Context<ActionsRegistryValue | null>;
887
+ interface UseActionsRegistryOptions {
888
+ /** localStorage key for persisting user overrides */
889
+ storageKey?: string;
890
+ }
891
+ /**
892
+ * Hook to create an actions registry.
893
+ * Used internally by HotkeysProvider.
894
+ */
895
+ declare function useActionsRegistry(options?: UseActionsRegistryOptions): ActionsRegistryValue;
896
+
897
+ /**
898
+ * Configuration for the HotkeysProvider.
899
+ */
900
+ interface HotkeysConfig {
901
+ /** Storage key for persisting user binding overrides */
902
+ storageKey?: string;
903
+ /** Timeout in ms before a sequence auto-submits (default: 1000) */
904
+ sequenceTimeout?: number;
905
+ /** When true, keys with conflicts are disabled (default: true) */
906
+ disableConflicts?: boolean;
907
+ /** Minimum viewport width to enable hotkeys (false = always enabled) */
908
+ minViewportWidth?: number | false;
909
+ /** Whether to show hotkey UI on touch-only devices (default: false) */
910
+ enableOnTouch?: boolean;
911
+ /** Key sequence to open shortcuts modal (false to disable) */
912
+ modalTrigger?: string | false;
913
+ /** Key sequence to open omnibar (false to disable) */
914
+ omnibarTrigger?: string | false;
915
+ }
916
+ /**
917
+ * Context value for hotkeys.
918
+ */
919
+ interface HotkeysContextValue {
920
+ /** The actions registry */
921
+ registry: ActionsRegistryValue;
922
+ /** Whether hotkeys are enabled (based on viewport/touch) */
923
+ isEnabled: boolean;
924
+ /** Modal open state */
925
+ isModalOpen: boolean;
926
+ /** Open the shortcuts modal */
927
+ openModal: () => void;
928
+ /** Close the shortcuts modal */
929
+ closeModal: () => void;
930
+ /** Toggle the shortcuts modal */
931
+ toggleModal: () => void;
932
+ /** Omnibar open state */
933
+ isOmnibarOpen: boolean;
934
+ /** Open the omnibar */
935
+ openOmnibar: () => void;
936
+ /** Close the omnibar */
937
+ closeOmnibar: () => void;
938
+ /** Toggle the omnibar */
939
+ toggleOmnibar: () => void;
940
+ /** Execute an action by ID */
941
+ executeAction: (id: string) => void;
942
+ /** Sequence state: pending key combinations */
943
+ pendingKeys: HotkeySequence;
944
+ /** Sequence state: whether waiting for more keys */
945
+ isAwaitingSequence: boolean;
946
+ /** Sequence state: when the timeout started */
947
+ sequenceTimeoutStartedAt: number | null;
948
+ /** Sequence state: timeout duration in ms */
949
+ sequenceTimeout: number;
950
+ /** Map of key -> actions[] for keys with multiple actions bound */
951
+ conflicts: Map<string, string[]>;
952
+ /** Whether there are any conflicts */
953
+ hasConflicts: boolean;
954
+ /** Search actions by query */
955
+ searchActions: (query: string) => ReturnType<typeof searchActions>;
956
+ /** Get sequence completions for pending keys */
957
+ getCompletions: (pendingKeys: HotkeySequence) => ReturnType<typeof getSequenceCompletions>;
958
+ }
959
+ interface HotkeysProviderProps {
960
+ config?: HotkeysConfig;
961
+ children: ReactNode;
962
+ }
963
+ /**
964
+ * Provider for hotkey registration via useAction.
965
+ *
966
+ * Components register their own actions using the useAction hook.
967
+ *
968
+ * @example
969
+ * ```tsx
970
+ * function App() {
971
+ * return (
972
+ * <HotkeysProvider config={{ storageKey: 'my-app' }}>
973
+ * <Dashboard />
974
+ * <ShortcutsModal />
975
+ * <Omnibar />
976
+ * <SequenceModal />
977
+ * </HotkeysProvider>
978
+ * )
979
+ * }
980
+ *
981
+ * function Dashboard() {
982
+ * const { save } = useDocument()
983
+ *
984
+ * useAction('doc:save', {
985
+ * label: 'Save document',
986
+ * group: 'Document',
987
+ * defaultBindings: ['meta+s'],
988
+ * handler: save,
989
+ * })
990
+ *
991
+ * return <Editor />
992
+ * }
993
+ * ```
994
+ */
995
+ declare function HotkeysProvider({ config: configProp, children, }: HotkeysProviderProps): react_jsx_runtime.JSX.Element;
996
+ /**
997
+ * Hook to access the hotkeys context.
998
+ * Must be used within a HotkeysProvider.
999
+ */
1000
+ declare function useHotkeysContext(): HotkeysContextValue;
1001
+ /**
1002
+ * Hook to optionally access hotkeys context.
1003
+ */
1004
+ declare function useMaybeHotkeysContext(): HotkeysContextValue | null;
1005
+
1006
+ /**
1007
+ * Hook to record a keyboard shortcut (single key or sequence) from user input.
1008
+ *
1009
+ * Recording behavior:
1010
+ * - Each key press (after modifiers released) adds to the sequence
1011
+ * - Enter key submits the current sequence
1012
+ * - Timeout submits the current sequence (configurable)
1013
+ * - Escape cancels recording
1014
+ *
1015
+ * @example
1016
+ * ```tsx
1017
+ * function KeybindingEditor() {
1018
+ * const { isRecording, startRecording, sequence, display, pendingKeys, activeKeys } = useRecordHotkey({
1019
+ * onCapture: (sequence, display) => {
1020
+ * console.log('Captured:', display.display) // "2 W" or "⌘K"
1021
+ * saveKeybinding(display.id) // "2 w" or "meta+k"
1022
+ * },
1023
+ * sequenceTimeout: 1000,
1024
+ * })
1025
+ *
1026
+ * return (
1027
+ * <button onClick={() => startRecording()}>
1028
+ * {isRecording
1029
+ * ? (pendingKeys.length > 0
1030
+ * ? formatCombination(pendingKeys).display + '...'
1031
+ * : 'Press keys...')
1032
+ * : (display?.display ?? 'Click to set')}
1033
+ * </button>
1034
+ * )
1035
+ * }
1036
+ * ```
1037
+ */
1038
+ declare function useRecordHotkey(options?: RecordHotkeyOptions): RecordHotkeyResult;
1039
+
1040
+ declare function SequenceModal(): react_jsx_runtime.JSX.Element | null;
1041
+
1042
+ interface ModifierIconProps {
1043
+ className?: string;
1044
+ style?: CSSProperties;
1045
+ }
1046
+ /** Command/Meta key icon (⌘) */
1047
+ declare function CommandIcon({ className, style }: ModifierIconProps): react_jsx_runtime.JSX.Element;
1048
+ /** Control key icon (^) - chevron/caret */
1049
+ declare function CtrlIcon({ className, style }: ModifierIconProps): react_jsx_runtime.JSX.Element;
1050
+ /** Shift key icon (⇧) - hollow arrow */
1051
+ declare function ShiftIcon({ className, style }: ModifierIconProps): react_jsx_runtime.JSX.Element;
1052
+ /** Option key icon (⌥) - macOS style */
1053
+ declare function OptIcon({ className, style }: ModifierIconProps): react_jsx_runtime.JSX.Element;
1054
+ /** Alt key icon (⎇) - Windows style, though "Alt" text is more common on Windows */
1055
+ declare function AltIcon({ className, style }: ModifierIconProps): react_jsx_runtime.JSX.Element;
1056
+ type ModifierType = 'meta' | 'ctrl' | 'shift' | 'alt' | 'opt';
1057
+ /** Get the appropriate icon component for a modifier key */
1058
+ declare function getModifierIcon(modifier: ModifierType): ComponentType<ModifierIconProps>;
1059
+ /** Render a modifier icon by name */
1060
+ declare function ModifierIcon({ modifier, ...props }: ModifierIconProps & {
1061
+ modifier: ModifierType;
1062
+ }): react_jsx_runtime.JSX.Element;
1063
+
1064
+ export { type ActionConfig, type ActionDefinition, type ActionRegistry, type ActionSearchResult, ActionsRegistryContext, type ActionsRegistryValue, AltIcon, type BindingInfo, CommandIcon, CtrlIcon, type FuzzyMatchResult, type GroupRenderer, type GroupRendererProps, type HandlerMap, type HotkeyMap, type HotkeySequence, type HotkeysConfig, type HotkeysContextValue, HotkeysProvider, type HotkeysProviderProps, type KeyCombination, type KeyCombinationDisplay, type KeyConflict, KeybindingEditor, type KeybindingEditorProps, type KeybindingEditorRenderProps, ModifierIcon, type ModifierIconProps, type ModifierType, Omnibar, type OmnibarProps, type OmnibarRenderProps, OptIcon, type RecordHotkeyOptions, type RecordHotkeyResult, type RegisteredAction, type SequenceCompletion, SequenceModal, ShiftIcon, type ShortcutGroup, ShortcutsModal, type ShortcutsModalProps, type ShortcutsModalRenderProps, type TwoColumnConfig, type TwoColumnRow, type UseEditableHotkeysOptions, type UseEditableHotkeysResult, type UseHotkeysOptions, type UseHotkeysResult, type UseOmnibarOptions, type UseOmnibarResult, createTwoColumnRenderer, findConflicts, formatCombination, formatKeyForDisplay, fuzzyMatch, getActionBindings, getConflictsArray, getModifierIcon, getSequenceCompletions, hasConflicts, isMac, isModifierKey, isSequence, normalizeKey, parseCombinationId, parseHotkeyString, searchActions, useAction, useActions, useActionsRegistry, useEditableHotkeys, useHotkeys, useHotkeysContext, useMaybeHotkeysContext, useOmnibar, useRecordHotkey };