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.
- package/README.md +216 -0
- package/dist/index.cjs +2746 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1064 -0
- package/dist/index.d.ts +1064 -0
- package/dist/index.js +2707 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
- package/src/styles.css +526 -0
package/dist/index.d.cts
ADDED
|
@@ -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 };
|