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/dist/index.js ADDED
@@ -0,0 +1,2707 @@
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import { createContext, useRef, useState, useCallback, useMemo, useEffect, useContext, Fragment as Fragment$1 } from 'react';
3
+ import { max, min } from '@rdub/base';
4
+
5
+ // src/TwoColumnRenderer.tsx
6
+ function createTwoColumnRenderer(config) {
7
+ const { headers, getRows } = config;
8
+ const [labelHeader, leftHeader, rightHeader] = headers;
9
+ return function TwoColumnRenderer({ group, renderCell }) {
10
+ const bindingsMap = new Map(
11
+ group.shortcuts.map((s) => [s.actionId, s.bindings])
12
+ );
13
+ const rows = getRows(group);
14
+ return /* @__PURE__ */ jsxs("table", { className: "kbd-table", children: [
15
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
16
+ /* @__PURE__ */ jsx("th", { children: labelHeader }),
17
+ /* @__PURE__ */ jsx("th", { children: leftHeader }),
18
+ /* @__PURE__ */ jsx("th", { children: rightHeader })
19
+ ] }) }),
20
+ /* @__PURE__ */ jsx("tbody", { children: rows.map(({ label, leftAction, rightAction }, i) => {
21
+ const leftBindings = bindingsMap.get(leftAction) ?? [];
22
+ const rightBindings = bindingsMap.get(rightAction) ?? [];
23
+ if (leftBindings.length === 0 && rightBindings.length === 0) return null;
24
+ return /* @__PURE__ */ jsxs("tr", { children: [
25
+ /* @__PURE__ */ jsx("td", { children: label }),
26
+ /* @__PURE__ */ jsx("td", { children: leftAction ? renderCell(leftAction, leftBindings) : "-" }),
27
+ /* @__PURE__ */ jsx("td", { children: rightAction ? renderCell(rightAction, rightBindings) : "-" })
28
+ ] }, i);
29
+ }) })
30
+ ] });
31
+ };
32
+ }
33
+ var ActionsRegistryContext = createContext(null);
34
+ function useActionsRegistry(options = {}) {
35
+ const { storageKey } = options;
36
+ const actionsRef = useRef(/* @__PURE__ */ new Map());
37
+ const [actionsVersion, setActionsVersion] = useState(0);
38
+ const [overrides, setOverrides] = useState(() => {
39
+ if (!storageKey || typeof window === "undefined") return {};
40
+ try {
41
+ const stored = localStorage.getItem(storageKey);
42
+ return stored ? JSON.parse(stored) : {};
43
+ } catch {
44
+ return {};
45
+ }
46
+ });
47
+ const [removedDefaults, setRemovedDefaults] = useState(() => {
48
+ if (!storageKey || typeof window === "undefined") return {};
49
+ try {
50
+ const stored = localStorage.getItem(`${storageKey}-removed`);
51
+ return stored ? JSON.parse(stored) : {};
52
+ } catch {
53
+ return {};
54
+ }
55
+ });
56
+ const isDefaultBinding = useCallback((key, actionId) => {
57
+ const action = actionsRef.current.get(actionId);
58
+ return action?.config.defaultBindings?.includes(key) ?? false;
59
+ }, []);
60
+ const filterRedundantOverrides = useCallback((overrides2) => {
61
+ const filtered = {};
62
+ for (const [key, actionOrActions] of Object.entries(overrides2)) {
63
+ if (actionOrActions === "") {
64
+ continue;
65
+ } else if (Array.isArray(actionOrActions)) {
66
+ const nonDefaultActions = actionOrActions.filter((a) => !isDefaultBinding(key, a));
67
+ if (nonDefaultActions.length > 0) {
68
+ filtered[key] = nonDefaultActions.length === 1 ? nonDefaultActions[0] : nonDefaultActions;
69
+ }
70
+ } else {
71
+ if (!isDefaultBinding(key, actionOrActions)) {
72
+ filtered[key] = actionOrActions;
73
+ }
74
+ }
75
+ }
76
+ return filtered;
77
+ }, [isDefaultBinding]);
78
+ const updateOverrides = useCallback((update) => {
79
+ setOverrides((prev) => {
80
+ const newOverrides = typeof update === "function" ? update(prev) : update;
81
+ const filteredOverrides = filterRedundantOverrides(newOverrides);
82
+ if (storageKey && typeof window !== "undefined") {
83
+ try {
84
+ if (Object.keys(filteredOverrides).length === 0) {
85
+ localStorage.removeItem(storageKey);
86
+ } else {
87
+ localStorage.setItem(storageKey, JSON.stringify(filteredOverrides));
88
+ }
89
+ } catch {
90
+ }
91
+ }
92
+ return filteredOverrides;
93
+ });
94
+ }, [storageKey, filterRedundantOverrides]);
95
+ const updateRemovedDefaults = useCallback((update) => {
96
+ setRemovedDefaults((prev) => {
97
+ const newRemoved = typeof update === "function" ? update(prev) : update;
98
+ const filtered = {};
99
+ for (const [action, keys] of Object.entries(newRemoved)) {
100
+ if (keys.length > 0) {
101
+ filtered[action] = keys;
102
+ }
103
+ }
104
+ if (storageKey && typeof window !== "undefined") {
105
+ try {
106
+ const key = `${storageKey}-removed`;
107
+ if (Object.keys(filtered).length === 0) {
108
+ localStorage.removeItem(key);
109
+ } else {
110
+ localStorage.setItem(key, JSON.stringify(filtered));
111
+ }
112
+ } catch {
113
+ }
114
+ }
115
+ return filtered;
116
+ });
117
+ }, [storageKey]);
118
+ const register = useCallback((id, config) => {
119
+ actionsRef.current.set(id, {
120
+ config,
121
+ registeredAt: Date.now()
122
+ });
123
+ setActionsVersion((v) => v + 1);
124
+ }, []);
125
+ const unregister = useCallback((id) => {
126
+ actionsRef.current.delete(id);
127
+ setActionsVersion((v) => v + 1);
128
+ }, []);
129
+ const execute = useCallback((id) => {
130
+ const action = actionsRef.current.get(id);
131
+ if (action && (action.config.enabled ?? true)) {
132
+ action.config.handler();
133
+ }
134
+ }, []);
135
+ const keymap = useMemo(() => {
136
+ const map = {};
137
+ const addToKey = (key, actionId) => {
138
+ const existing = map[key];
139
+ if (existing) {
140
+ const existingArray = Array.isArray(existing) ? existing : [existing];
141
+ if (!existingArray.includes(actionId)) {
142
+ map[key] = [...existingArray, actionId];
143
+ }
144
+ } else {
145
+ map[key] = actionId;
146
+ }
147
+ };
148
+ for (const [id, { config }] of actionsRef.current) {
149
+ for (const binding of config.defaultBindings ?? []) {
150
+ const removedForAction = removedDefaults[id] ?? [];
151
+ if (removedForAction.includes(binding)) continue;
152
+ addToKey(binding, id);
153
+ }
154
+ }
155
+ for (const [key, actionOrActions] of Object.entries(overrides)) {
156
+ if (actionOrActions === "") {
157
+ continue;
158
+ } else {
159
+ const actions2 = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
160
+ for (const actionId of actions2) {
161
+ addToKey(key, actionId);
162
+ }
163
+ }
164
+ }
165
+ return map;
166
+ }, [actionsVersion, overrides, removedDefaults]);
167
+ const actionRegistry = useMemo(() => {
168
+ const registry = {};
169
+ for (const [id, { config }] of actionsRef.current) {
170
+ registry[id] = {
171
+ label: config.label,
172
+ group: config.group,
173
+ keywords: config.keywords
174
+ };
175
+ }
176
+ return registry;
177
+ }, [actionsVersion]);
178
+ const getBindingsForAction = useCallback((actionId) => {
179
+ const bindings = [];
180
+ for (const [key, action] of Object.entries(keymap)) {
181
+ const actions2 = Array.isArray(action) ? action : [action];
182
+ if (actions2.includes(actionId)) {
183
+ bindings.push(key);
184
+ }
185
+ }
186
+ return bindings;
187
+ }, [keymap]);
188
+ const setBinding = useCallback((actionId, key) => {
189
+ updateOverrides((prev) => ({
190
+ ...prev,
191
+ [key]: actionId
192
+ }));
193
+ }, [updateOverrides]);
194
+ const removeBinding = useCallback((key) => {
195
+ const actionsWithDefault = [];
196
+ for (const [id, { config }] of actionsRef.current) {
197
+ if (config.defaultBindings?.includes(key)) {
198
+ actionsWithDefault.push(id);
199
+ }
200
+ }
201
+ if (actionsWithDefault.length > 0) {
202
+ updateRemovedDefaults((prev) => {
203
+ const next = { ...prev };
204
+ for (const actionId of actionsWithDefault) {
205
+ const existing = next[actionId] ?? [];
206
+ if (!existing.includes(key)) {
207
+ next[actionId] = [...existing, key];
208
+ }
209
+ }
210
+ return next;
211
+ });
212
+ }
213
+ updateOverrides((prev) => {
214
+ const { [key]: _, ...rest } = prev;
215
+ return rest;
216
+ });
217
+ }, [updateOverrides, updateRemovedDefaults]);
218
+ const resetOverrides = useCallback(() => {
219
+ updateOverrides({});
220
+ updateRemovedDefaults({});
221
+ }, [updateOverrides, updateRemovedDefaults]);
222
+ const actions = useMemo(() => {
223
+ return new Map(actionsRef.current);
224
+ }, [actionsVersion]);
225
+ return useMemo(() => ({
226
+ register,
227
+ unregister,
228
+ execute,
229
+ actions,
230
+ keymap,
231
+ actionRegistry,
232
+ getBindingsForAction,
233
+ overrides,
234
+ setBinding,
235
+ removeBinding,
236
+ resetOverrides
237
+ }), [
238
+ register,
239
+ unregister,
240
+ execute,
241
+ actions,
242
+ keymap,
243
+ actionRegistry,
244
+ getBindingsForAction,
245
+ overrides,
246
+ setBinding,
247
+ removeBinding,
248
+ resetOverrides
249
+ ]);
250
+ }
251
+ function isMac() {
252
+ if (typeof navigator === "undefined") return false;
253
+ return /Mac|iPod|iPhone|iPad/.test(navigator.platform);
254
+ }
255
+ function normalizeKey(key) {
256
+ const keyMap = {
257
+ " ": "space",
258
+ "Escape": "escape",
259
+ "Enter": "enter",
260
+ "Tab": "tab",
261
+ "Backspace": "backspace",
262
+ "Delete": "delete",
263
+ "ArrowUp": "arrowup",
264
+ "ArrowDown": "arrowdown",
265
+ "ArrowLeft": "arrowleft",
266
+ "ArrowRight": "arrowright",
267
+ "Home": "home",
268
+ "End": "end",
269
+ "PageUp": "pageup",
270
+ "PageDown": "pagedown"
271
+ };
272
+ if (key in keyMap) {
273
+ return keyMap[key];
274
+ }
275
+ if (key.length === 1) {
276
+ return key.toLowerCase();
277
+ }
278
+ if (/^F\d{1,2}$/.test(key)) {
279
+ return key.toLowerCase();
280
+ }
281
+ return key.toLowerCase();
282
+ }
283
+ function formatKeyForDisplay(key) {
284
+ const displayMap = {
285
+ "space": "Space",
286
+ "escape": "Esc",
287
+ "enter": "\u21B5",
288
+ "tab": "Tab",
289
+ "backspace": "\u232B",
290
+ "delete": "Del",
291
+ "arrowup": "\u2191",
292
+ "arrowdown": "\u2193",
293
+ "arrowleft": "\u2190",
294
+ "arrowright": "\u2192",
295
+ "home": "Home",
296
+ "end": "End",
297
+ "pageup": "PgUp",
298
+ "pagedown": "PgDn"
299
+ };
300
+ if (key in displayMap) {
301
+ return displayMap[key];
302
+ }
303
+ if (/^f\d{1,2}$/.test(key)) {
304
+ return key.toUpperCase();
305
+ }
306
+ if (key.length === 1) {
307
+ return key.toUpperCase();
308
+ }
309
+ return key;
310
+ }
311
+ function formatSingleCombination(combo) {
312
+ const mac = isMac();
313
+ const parts = [];
314
+ const idParts = [];
315
+ if (combo.modifiers.ctrl) {
316
+ parts.push(mac ? "\u2303" : "Ctrl");
317
+ idParts.push("ctrl");
318
+ }
319
+ if (combo.modifiers.meta) {
320
+ parts.push(mac ? "\u2318" : "Win");
321
+ idParts.push("meta");
322
+ }
323
+ if (combo.modifiers.alt) {
324
+ parts.push(mac ? "\u2325" : "Alt");
325
+ idParts.push("alt");
326
+ }
327
+ if (combo.modifiers.shift) {
328
+ parts.push(mac ? "\u21E7" : "Shift");
329
+ idParts.push("shift");
330
+ }
331
+ parts.push(formatKeyForDisplay(combo.key));
332
+ idParts.push(combo.key);
333
+ return {
334
+ display: mac ? parts.join("") : parts.join("+"),
335
+ id: idParts.join("+")
336
+ };
337
+ }
338
+ function formatCombination(input) {
339
+ if (Array.isArray(input)) {
340
+ if (input.length === 0) {
341
+ return { display: "", id: "", isSequence: false };
342
+ }
343
+ if (input.length === 1) {
344
+ const single2 = formatSingleCombination(input[0]);
345
+ return { ...single2, isSequence: false };
346
+ }
347
+ const formatted = input.map(formatSingleCombination);
348
+ return {
349
+ display: formatted.map((f) => f.display).join(" "),
350
+ id: formatted.map((f) => f.id).join(" "),
351
+ isSequence: true
352
+ };
353
+ }
354
+ const single = formatSingleCombination(input);
355
+ return { ...single, isSequence: false };
356
+ }
357
+ function isModifierKey(key) {
358
+ return ["Control", "Alt", "Shift", "Meta"].includes(key);
359
+ }
360
+ var SHIFTED_CHARS = /* @__PURE__ */ new Set([
361
+ "~",
362
+ "!",
363
+ "@",
364
+ "#",
365
+ "$",
366
+ "%",
367
+ "^",
368
+ "&",
369
+ "*",
370
+ "(",
371
+ ")",
372
+ "_",
373
+ "+",
374
+ "{",
375
+ "}",
376
+ "|",
377
+ ":",
378
+ '"',
379
+ "<",
380
+ ">",
381
+ "?"
382
+ ]);
383
+ function isShiftedChar(key) {
384
+ return SHIFTED_CHARS.has(key);
385
+ }
386
+ function isSequence(hotkeyStr) {
387
+ return hotkeyStr.includes(" ");
388
+ }
389
+ function parseSingleCombination(str) {
390
+ if (str.length === 1 && /^[A-Z]$/.test(str)) {
391
+ return {
392
+ key: str.toLowerCase(),
393
+ modifiers: { ctrl: false, alt: false, shift: true, meta: false }
394
+ };
395
+ }
396
+ const parts = str.toLowerCase().split("+");
397
+ const key = parts[parts.length - 1];
398
+ return {
399
+ key,
400
+ modifiers: {
401
+ ctrl: parts.includes("ctrl") || parts.includes("control"),
402
+ alt: parts.includes("alt") || parts.includes("option"),
403
+ shift: parts.includes("shift"),
404
+ meta: parts.includes("meta") || parts.includes("cmd") || parts.includes("command")
405
+ }
406
+ };
407
+ }
408
+ function parseHotkeyString(hotkeyStr) {
409
+ if (!hotkeyStr.trim()) return [];
410
+ const parts = hotkeyStr.trim().split(/\s+/);
411
+ return parts.map(parseSingleCombination);
412
+ }
413
+ function parseCombinationId(id) {
414
+ const sequence = parseHotkeyString(id);
415
+ if (sequence.length === 0) {
416
+ return { key: "", modifiers: { ctrl: false, alt: false, shift: false, meta: false } };
417
+ }
418
+ return sequence[0];
419
+ }
420
+ function isPrefix(a, b) {
421
+ if (a.length >= b.length) return false;
422
+ for (let i = 0; i < a.length; i++) {
423
+ if (!combinationsEqual(a[i], b[i])) return false;
424
+ }
425
+ return true;
426
+ }
427
+ function combinationsEqual(a, b) {
428
+ return a.key === b.key && a.modifiers.ctrl === b.modifiers.ctrl && a.modifiers.alt === b.modifiers.alt && a.modifiers.shift === b.modifiers.shift && a.modifiers.meta === b.modifiers.meta;
429
+ }
430
+ function findConflicts(keymap) {
431
+ const conflicts = /* @__PURE__ */ new Map();
432
+ const entries = Object.entries(keymap).map(([key, actionOrActions]) => ({
433
+ key,
434
+ sequence: parseHotkeyString(key),
435
+ actions: Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
436
+ }));
437
+ const keyToActions = /* @__PURE__ */ new Map();
438
+ for (const { key, actions } of entries) {
439
+ const existing = keyToActions.get(key) ?? [];
440
+ keyToActions.set(key, [...existing, ...actions]);
441
+ }
442
+ for (const [key, actions] of keyToActions) {
443
+ if (actions.length > 1) {
444
+ conflicts.set(key, actions);
445
+ }
446
+ }
447
+ for (let i = 0; i < entries.length; i++) {
448
+ for (let j = i + 1; j < entries.length; j++) {
449
+ const a = entries[i];
450
+ const b = entries[j];
451
+ if (isPrefix(a.sequence, b.sequence)) {
452
+ const existingA = conflicts.get(a.key) ?? [];
453
+ if (!existingA.includes(`prefix of: ${b.key}`)) {
454
+ conflicts.set(a.key, [...existingA, ...a.actions, `prefix of: ${b.key}`]);
455
+ }
456
+ const existingB = conflicts.get(b.key) ?? [];
457
+ if (!existingB.includes(`has prefix: ${a.key}`)) {
458
+ conflicts.set(b.key, [...existingB, ...b.actions, `has prefix: ${a.key}`]);
459
+ }
460
+ } else if (isPrefix(b.sequence, a.sequence)) {
461
+ const existingB = conflicts.get(b.key) ?? [];
462
+ if (!existingB.includes(`prefix of: ${a.key}`)) {
463
+ conflicts.set(b.key, [...existingB, ...b.actions, `prefix of: ${a.key}`]);
464
+ }
465
+ const existingA = conflicts.get(a.key) ?? [];
466
+ if (!existingA.includes(`has prefix: ${b.key}`)) {
467
+ conflicts.set(a.key, [...existingA, ...a.actions, `has prefix: ${b.key}`]);
468
+ }
469
+ }
470
+ }
471
+ }
472
+ return conflicts;
473
+ }
474
+ function hasConflicts(keymap) {
475
+ return findConflicts(keymap).size > 0;
476
+ }
477
+ function getConflictsArray(keymap) {
478
+ const conflicts = findConflicts(keymap);
479
+ return Array.from(conflicts.entries()).map(([key, actions]) => ({
480
+ key,
481
+ actions: actions.filter((a) => !a.startsWith("prefix of:") && !a.startsWith("has prefix:")),
482
+ type: actions.some((a) => a.startsWith("prefix of:") || a.startsWith("has prefix:")) ? "prefix" : "duplicate"
483
+ }));
484
+ }
485
+ function getSequenceCompletions(pendingKeys, keymap) {
486
+ if (pendingKeys.length === 0) return [];
487
+ const completions = [];
488
+ for (const [hotkeyStr, actionOrActions] of Object.entries(keymap)) {
489
+ const sequence = parseHotkeyString(hotkeyStr);
490
+ if (sequence.length <= pendingKeys.length) continue;
491
+ let isPrefix2 = true;
492
+ for (let i = 0; i < pendingKeys.length; i++) {
493
+ if (!combinationsEqual(pendingKeys[i], sequence[i])) {
494
+ isPrefix2 = false;
495
+ break;
496
+ }
497
+ }
498
+ if (isPrefix2) {
499
+ const remainingKeys = sequence.slice(pendingKeys.length);
500
+ const nextKeys = formatCombination(remainingKeys).id;
501
+ const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
502
+ completions.push({
503
+ nextKeys,
504
+ fullSequence: hotkeyStr,
505
+ display: formatCombination(sequence),
506
+ actions
507
+ });
508
+ }
509
+ }
510
+ return completions;
511
+ }
512
+ function getActionBindings(keymap) {
513
+ const actionToKeys = /* @__PURE__ */ new Map();
514
+ for (const [key, actionOrActions] of Object.entries(keymap)) {
515
+ const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
516
+ for (const action of actions) {
517
+ const existing = actionToKeys.get(action) ?? [];
518
+ actionToKeys.set(action, [...existing, key]);
519
+ }
520
+ }
521
+ const stackNone = actionToKeys.get("stack:none");
522
+ const regionNyc = actionToKeys.get("region:nyc");
523
+ if (stackNone || regionNyc) {
524
+ console.log("getActionBindings:", { "stack:none": stackNone, "region:nyc": regionNyc });
525
+ }
526
+ return actionToKeys;
527
+ }
528
+ function fuzzyMatch(pattern, text) {
529
+ if (!pattern) return { matched: true, score: 1, ranges: [] };
530
+ if (!text) return { matched: false, score: 0, ranges: [] };
531
+ const patternLower = pattern.toLowerCase();
532
+ const textLower = text.toLowerCase();
533
+ let patternIdx = 0;
534
+ let score = 0;
535
+ let consecutiveBonus = 0;
536
+ let lastMatchIdx = -2;
537
+ const ranges = [];
538
+ let rangeStart = -1;
539
+ for (let textIdx = 0; textIdx < textLower.length && patternIdx < patternLower.length; textIdx++) {
540
+ if (textLower[textIdx] === patternLower[patternIdx]) {
541
+ let matchScore = 1;
542
+ if (lastMatchIdx === textIdx - 1) {
543
+ consecutiveBonus += 1;
544
+ matchScore += consecutiveBonus;
545
+ } else {
546
+ consecutiveBonus = 0;
547
+ }
548
+ if (textIdx === 0 || /[\s\-_./]/.test(text[textIdx - 1])) {
549
+ matchScore += 2;
550
+ }
551
+ if (text[textIdx] === text[textIdx].toUpperCase() && /[a-z]/.test(text[textIdx].toLowerCase())) {
552
+ matchScore += 1;
553
+ }
554
+ matchScore -= textIdx * 0.01;
555
+ score += matchScore;
556
+ lastMatchIdx = textIdx;
557
+ patternIdx++;
558
+ if (rangeStart === -1) {
559
+ rangeStart = textIdx;
560
+ }
561
+ } else {
562
+ if (rangeStart !== -1) {
563
+ ranges.push([rangeStart, lastMatchIdx + 1]);
564
+ rangeStart = -1;
565
+ }
566
+ }
567
+ }
568
+ if (rangeStart !== -1) {
569
+ ranges.push([rangeStart, lastMatchIdx + 1]);
570
+ }
571
+ const matched = patternIdx === patternLower.length;
572
+ if (matched && textLower === patternLower) {
573
+ score += 10;
574
+ }
575
+ if (matched && textLower.startsWith(patternLower)) {
576
+ score += 5;
577
+ }
578
+ return { matched, score, ranges };
579
+ }
580
+ function searchActions(query, actions, keymap) {
581
+ const actionBindings = keymap ? getActionBindings(keymap) : /* @__PURE__ */ new Map();
582
+ const results = [];
583
+ for (const [id, action] of Object.entries(actions)) {
584
+ if (action.enabled === false) continue;
585
+ const labelMatch = fuzzyMatch(query, action.label);
586
+ const descMatch = action.description ? fuzzyMatch(query, action.description) : { matched: false, score: 0};
587
+ const groupMatch = action.group ? fuzzyMatch(query, action.group) : { matched: false, score: 0};
588
+ const idMatch = fuzzyMatch(query, id);
589
+ let keywordScore = 0;
590
+ if (action.keywords) {
591
+ for (const keyword of action.keywords) {
592
+ const kwMatch = fuzzyMatch(query, keyword);
593
+ if (kwMatch.matched) {
594
+ keywordScore = max(keywordScore, kwMatch.score);
595
+ }
596
+ }
597
+ }
598
+ const matched = labelMatch.matched || descMatch.matched || groupMatch.matched || idMatch.matched || keywordScore > 0;
599
+ if (!matched && query) continue;
600
+ const score = (labelMatch.matched ? labelMatch.score * 3 : 0) + (descMatch.matched ? descMatch.score * 1.5 : 0) + (groupMatch.matched ? groupMatch.score * 1 : 0) + (idMatch.matched ? idMatch.score * 0.5 : 0) + keywordScore * 2;
601
+ results.push({
602
+ id,
603
+ action,
604
+ bindings: actionBindings.get(id) ?? [],
605
+ score,
606
+ labelMatches: labelMatch.ranges
607
+ });
608
+ }
609
+ results.sort((a, b) => b.score - a.score);
610
+ return results;
611
+ }
612
+
613
+ // src/useHotkeys.ts
614
+ function eventToCombination(e) {
615
+ return {
616
+ key: normalizeKey(e.key),
617
+ modifiers: {
618
+ ctrl: e.ctrlKey,
619
+ alt: e.altKey,
620
+ shift: e.shiftKey,
621
+ meta: e.metaKey
622
+ }
623
+ };
624
+ }
625
+ function isPartialMatch(pending, target) {
626
+ if (pending.length >= target.length) return false;
627
+ for (let i = 0; i < pending.length; i++) {
628
+ if (!combinationsMatch(pending[i], target[i])) {
629
+ return false;
630
+ }
631
+ }
632
+ return true;
633
+ }
634
+ function combinationsMatch(event, target) {
635
+ const shiftMatches = isShiftedChar(event.key) ? target.modifiers.shift ? event.modifiers.shift : true : event.modifiers.shift === target.modifiers.shift;
636
+ return event.modifiers.ctrl === target.modifiers.ctrl && event.modifiers.alt === target.modifiers.alt && shiftMatches && event.modifiers.meta === target.modifiers.meta && event.key === target.key;
637
+ }
638
+ function sequencesMatch(a, b) {
639
+ if (a.length !== b.length) return false;
640
+ for (let i = 0; i < a.length; i++) {
641
+ if (!combinationsMatch(a[i], b[i])) {
642
+ return false;
643
+ }
644
+ }
645
+ return true;
646
+ }
647
+ function useHotkeys(keymap, handlers, options = {}) {
648
+ const {
649
+ enabled = true,
650
+ target,
651
+ preventDefault = true,
652
+ stopPropagation = true,
653
+ enableOnFormTags = false,
654
+ sequenceTimeout = 1e3,
655
+ onTimeout = "submit",
656
+ onSequenceStart,
657
+ onSequenceProgress,
658
+ onSequenceCancel
659
+ } = options;
660
+ const [pendingKeys, setPendingKeys] = useState([]);
661
+ const [isAwaitingSequence, setIsAwaitingSequence] = useState(false);
662
+ const [timeoutStartedAt, setTimeoutStartedAt] = useState(null);
663
+ const handlersRef = useRef(handlers);
664
+ handlersRef.current = handlers;
665
+ const keymapRef = useRef(keymap);
666
+ keymapRef.current = keymap;
667
+ const timeoutRef = useRef(null);
668
+ const pendingKeysRef = useRef([]);
669
+ pendingKeysRef.current = pendingKeys;
670
+ const parsedKeymapRef = useRef([]);
671
+ useEffect(() => {
672
+ parsedKeymapRef.current = Object.entries(keymap).map(([key, actionOrActions]) => ({
673
+ key,
674
+ sequence: parseHotkeyString(key),
675
+ actions: Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
676
+ }));
677
+ }, [keymap]);
678
+ const clearPending = useCallback(() => {
679
+ setPendingKeys([]);
680
+ setIsAwaitingSequence(false);
681
+ setTimeoutStartedAt(null);
682
+ if (timeoutRef.current) {
683
+ clearTimeout(timeoutRef.current);
684
+ timeoutRef.current = null;
685
+ }
686
+ }, []);
687
+ const cancelSequence = useCallback(() => {
688
+ clearPending();
689
+ onSequenceCancel?.();
690
+ }, [clearPending, onSequenceCancel]);
691
+ const tryExecute = useCallback((sequence, e) => {
692
+ for (const entry of parsedKeymapRef.current) {
693
+ if (sequencesMatch(sequence, entry.sequence)) {
694
+ for (const action of entry.actions) {
695
+ const handler = handlersRef.current[action];
696
+ if (handler) {
697
+ if (preventDefault) {
698
+ e.preventDefault();
699
+ }
700
+ if (stopPropagation) {
701
+ e.stopPropagation();
702
+ }
703
+ handler(e);
704
+ return true;
705
+ }
706
+ }
707
+ }
708
+ }
709
+ return false;
710
+ }, [preventDefault, stopPropagation]);
711
+ const hasPotentialMatch = useCallback((sequence) => {
712
+ for (const entry of parsedKeymapRef.current) {
713
+ if (isPartialMatch(sequence, entry.sequence) || sequencesMatch(sequence, entry.sequence)) {
714
+ return true;
715
+ }
716
+ }
717
+ return false;
718
+ }, []);
719
+ const hasSequenceExtension = useCallback((sequence) => {
720
+ for (const entry of parsedKeymapRef.current) {
721
+ if (entry.sequence.length > sequence.length && isPartialMatch(sequence, entry.sequence)) {
722
+ return true;
723
+ }
724
+ }
725
+ return false;
726
+ }, []);
727
+ useEffect(() => {
728
+ if (!enabled) return;
729
+ const targetElement = target ?? window;
730
+ const handleKeyDown = (e) => {
731
+ if (!enableOnFormTags) {
732
+ const eventTarget = e.target;
733
+ if (eventTarget instanceof HTMLInputElement || eventTarget instanceof HTMLTextAreaElement || eventTarget instanceof HTMLSelectElement || eventTarget.isContentEditable) {
734
+ return;
735
+ }
736
+ }
737
+ if (isModifierKey(e.key)) {
738
+ return;
739
+ }
740
+ if (timeoutRef.current) {
741
+ clearTimeout(timeoutRef.current);
742
+ timeoutRef.current = null;
743
+ }
744
+ if (e.key === "Enter" && pendingKeysRef.current.length > 0) {
745
+ e.preventDefault();
746
+ const executed = tryExecute(pendingKeysRef.current, e);
747
+ clearPending();
748
+ if (!executed) {
749
+ onSequenceCancel?.();
750
+ }
751
+ return;
752
+ }
753
+ if (e.key === "Escape" && pendingKeysRef.current.length > 0) {
754
+ e.preventDefault();
755
+ cancelSequence();
756
+ return;
757
+ }
758
+ const currentCombo = eventToCombination(e);
759
+ const newSequence = [...pendingKeysRef.current, currentCombo];
760
+ const exactMatch = tryExecute(newSequence, e);
761
+ if (exactMatch) {
762
+ clearPending();
763
+ return;
764
+ }
765
+ if (hasPotentialMatch(newSequence)) {
766
+ if (hasSequenceExtension(newSequence)) {
767
+ setPendingKeys(newSequence);
768
+ setIsAwaitingSequence(true);
769
+ if (pendingKeysRef.current.length === 0) {
770
+ onSequenceStart?.(newSequence);
771
+ } else {
772
+ onSequenceProgress?.(newSequence);
773
+ }
774
+ setTimeoutStartedAt(Date.now());
775
+ timeoutRef.current = setTimeout(() => {
776
+ if (onTimeout === "submit") {
777
+ setPendingKeys((current) => {
778
+ if (current.length > 0) {
779
+ onSequenceCancel?.();
780
+ }
781
+ return [];
782
+ });
783
+ setIsAwaitingSequence(false);
784
+ setTimeoutStartedAt(null);
785
+ } else {
786
+ setPendingKeys([]);
787
+ setIsAwaitingSequence(false);
788
+ setTimeoutStartedAt(null);
789
+ onSequenceCancel?.();
790
+ }
791
+ timeoutRef.current = null;
792
+ }, sequenceTimeout);
793
+ if (preventDefault) {
794
+ e.preventDefault();
795
+ }
796
+ return;
797
+ }
798
+ }
799
+ if (pendingKeysRef.current.length > 0) {
800
+ clearPending();
801
+ onSequenceCancel?.();
802
+ }
803
+ const singleMatch = tryExecute([currentCombo], e);
804
+ if (!singleMatch) {
805
+ if (hasSequenceExtension([currentCombo])) {
806
+ setPendingKeys([currentCombo]);
807
+ setIsAwaitingSequence(true);
808
+ onSequenceStart?.([currentCombo]);
809
+ if (preventDefault) {
810
+ e.preventDefault();
811
+ }
812
+ setTimeoutStartedAt(Date.now());
813
+ timeoutRef.current = setTimeout(() => {
814
+ if (onTimeout === "submit") {
815
+ setPendingKeys([]);
816
+ setIsAwaitingSequence(false);
817
+ setTimeoutStartedAt(null);
818
+ onSequenceCancel?.();
819
+ } else {
820
+ setPendingKeys([]);
821
+ setIsAwaitingSequence(false);
822
+ setTimeoutStartedAt(null);
823
+ onSequenceCancel?.();
824
+ }
825
+ timeoutRef.current = null;
826
+ }, sequenceTimeout);
827
+ }
828
+ }
829
+ };
830
+ targetElement.addEventListener("keydown", handleKeyDown);
831
+ return () => {
832
+ targetElement.removeEventListener("keydown", handleKeyDown);
833
+ if (timeoutRef.current) {
834
+ clearTimeout(timeoutRef.current);
835
+ }
836
+ };
837
+ }, [
838
+ enabled,
839
+ target,
840
+ preventDefault,
841
+ stopPropagation,
842
+ enableOnFormTags,
843
+ sequenceTimeout,
844
+ onTimeout,
845
+ clearPending,
846
+ cancelSequence,
847
+ tryExecute,
848
+ hasPotentialMatch,
849
+ hasSequenceExtension,
850
+ onSequenceStart,
851
+ onSequenceProgress,
852
+ onSequenceCancel
853
+ ]);
854
+ return { pendingKeys, isAwaitingSequence, cancelSequence, timeoutStartedAt, sequenceTimeout };
855
+ }
856
+ var HotkeysContext = createContext(null);
857
+ var DEFAULT_CONFIG = {
858
+ storageKey: "use-kbd",
859
+ sequenceTimeout: 1e3,
860
+ disableConflicts: true,
861
+ minViewportWidth: 768,
862
+ enableOnTouch: false,
863
+ modalTrigger: "?",
864
+ omnibarTrigger: "meta+k"
865
+ };
866
+ function HotkeysProvider({
867
+ config: configProp = {},
868
+ children
869
+ }) {
870
+ const config = useMemo(() => ({
871
+ ...DEFAULT_CONFIG,
872
+ ...configProp
873
+ }), [configProp]);
874
+ const registry = useActionsRegistry({ storageKey: config.storageKey });
875
+ const [isEnabled, setIsEnabled] = useState(true);
876
+ useEffect(() => {
877
+ if (typeof window === "undefined") return;
878
+ const checkEnabled = () => {
879
+ if (config.minViewportWidth !== false) {
880
+ if (window.innerWidth < config.minViewportWidth) {
881
+ setIsEnabled(false);
882
+ return;
883
+ }
884
+ }
885
+ if (!config.enableOnTouch) {
886
+ const hasHover = window.matchMedia("(hover: hover)").matches;
887
+ if (!hasHover) {
888
+ setIsEnabled(false);
889
+ return;
890
+ }
891
+ }
892
+ setIsEnabled(true);
893
+ };
894
+ checkEnabled();
895
+ window.addEventListener("resize", checkEnabled);
896
+ return () => window.removeEventListener("resize", checkEnabled);
897
+ }, [config.minViewportWidth, config.enableOnTouch]);
898
+ const modalStorageKey = `${config.storageKey}-modal-open`;
899
+ const [isModalOpen, setIsModalOpen] = useState(() => {
900
+ if (typeof window === "undefined") return false;
901
+ return sessionStorage.getItem(modalStorageKey) === "true";
902
+ });
903
+ useEffect(() => {
904
+ sessionStorage.setItem(modalStorageKey, String(isModalOpen));
905
+ }, [modalStorageKey, isModalOpen]);
906
+ const openModal = useCallback(() => setIsModalOpen(true), []);
907
+ const closeModal = useCallback(() => setIsModalOpen(false), []);
908
+ const toggleModal = useCallback(() => setIsModalOpen((prev) => !prev), []);
909
+ const [isOmnibarOpen, setIsOmnibarOpen] = useState(false);
910
+ const openOmnibar = useCallback(() => setIsOmnibarOpen(true), []);
911
+ const closeOmnibar = useCallback(() => setIsOmnibarOpen(false), []);
912
+ const toggleOmnibar = useCallback(() => setIsOmnibarOpen((prev) => !prev), []);
913
+ const keymap = useMemo(() => {
914
+ const map = { ...registry.keymap };
915
+ if (config.modalTrigger !== false) {
916
+ map[config.modalTrigger] = "__hotkeys:modal";
917
+ }
918
+ if (config.omnibarTrigger !== false) {
919
+ map[config.omnibarTrigger] = "__hotkeys:omnibar";
920
+ }
921
+ return map;
922
+ }, [registry.keymap, config.modalTrigger, config.omnibarTrigger]);
923
+ const conflicts = useMemo(() => findConflicts(keymap), [keymap]);
924
+ const hasConflicts2 = conflicts.size > 0;
925
+ const effectiveKeymap = useMemo(() => {
926
+ if (!config.disableConflicts || conflicts.size === 0) {
927
+ return keymap;
928
+ }
929
+ const filtered = {};
930
+ for (const [key, action] of Object.entries(keymap)) {
931
+ if (!conflicts.has(key)) {
932
+ filtered[key] = action;
933
+ }
934
+ }
935
+ return filtered;
936
+ }, [keymap, conflicts, config.disableConflicts]);
937
+ const handlers = useMemo(() => {
938
+ const map = {};
939
+ for (const [id, action] of registry.actions) {
940
+ map[id] = action.config.handler;
941
+ }
942
+ map["__hotkeys:modal"] = toggleModal;
943
+ map["__hotkeys:omnibar"] = toggleOmnibar;
944
+ return map;
945
+ }, [registry.actions, toggleModal, toggleOmnibar]);
946
+ const hotkeysEnabled = isEnabled && !isModalOpen && !isOmnibarOpen;
947
+ const {
948
+ pendingKeys,
949
+ isAwaitingSequence,
950
+ timeoutStartedAt: sequenceTimeoutStartedAt,
951
+ sequenceTimeout
952
+ } = useHotkeys(effectiveKeymap, handlers, {
953
+ enabled: hotkeysEnabled,
954
+ sequenceTimeout: config.sequenceTimeout
955
+ });
956
+ const searchActionsHelper = useCallback(
957
+ (query) => searchActions(query, registry.actionRegistry, keymap),
958
+ [registry.actionRegistry, keymap]
959
+ );
960
+ const getCompletions = useCallback(
961
+ (pending) => getSequenceCompletions(pending, keymap),
962
+ [keymap]
963
+ );
964
+ const value = useMemo(() => ({
965
+ registry,
966
+ isEnabled,
967
+ isModalOpen,
968
+ openModal,
969
+ closeModal,
970
+ toggleModal,
971
+ isOmnibarOpen,
972
+ openOmnibar,
973
+ closeOmnibar,
974
+ toggleOmnibar,
975
+ executeAction: registry.execute,
976
+ pendingKeys,
977
+ isAwaitingSequence,
978
+ sequenceTimeoutStartedAt,
979
+ sequenceTimeout,
980
+ conflicts,
981
+ hasConflicts: hasConflicts2,
982
+ searchActions: searchActionsHelper,
983
+ getCompletions
984
+ }), [
985
+ registry,
986
+ isEnabled,
987
+ isModalOpen,
988
+ openModal,
989
+ closeModal,
990
+ toggleModal,
991
+ isOmnibarOpen,
992
+ openOmnibar,
993
+ closeOmnibar,
994
+ toggleOmnibar,
995
+ pendingKeys,
996
+ isAwaitingSequence,
997
+ sequenceTimeoutStartedAt,
998
+ sequenceTimeout,
999
+ conflicts,
1000
+ hasConflicts2,
1001
+ searchActionsHelper,
1002
+ getCompletions
1003
+ ]);
1004
+ return /* @__PURE__ */ jsx(ActionsRegistryContext.Provider, { value: registry, children: /* @__PURE__ */ jsx(HotkeysContext.Provider, { value, children }) });
1005
+ }
1006
+ function useHotkeysContext() {
1007
+ const context = useContext(HotkeysContext);
1008
+ if (!context) {
1009
+ throw new Error("useHotkeysContext must be used within a HotkeysProvider");
1010
+ }
1011
+ return context;
1012
+ }
1013
+ function useMaybeHotkeysContext() {
1014
+ return useContext(HotkeysContext);
1015
+ }
1016
+ function useAction(id, config) {
1017
+ const registry = useContext(ActionsRegistryContext);
1018
+ if (!registry) {
1019
+ throw new Error("useAction must be used within a HotkeysProvider");
1020
+ }
1021
+ const registryRef = useRef(registry);
1022
+ registryRef.current = registry;
1023
+ const handlerRef = useRef(config.handler);
1024
+ handlerRef.current = config.handler;
1025
+ const enabledRef = useRef(config.enabled ?? true);
1026
+ enabledRef.current = config.enabled ?? true;
1027
+ useEffect(() => {
1028
+ registryRef.current.register(id, {
1029
+ ...config,
1030
+ handler: () => {
1031
+ if (enabledRef.current) {
1032
+ handlerRef.current();
1033
+ }
1034
+ }
1035
+ });
1036
+ return () => {
1037
+ registryRef.current.unregister(id);
1038
+ };
1039
+ }, [
1040
+ id,
1041
+ config.label,
1042
+ config.group,
1043
+ // Compare bindings by value
1044
+ JSON.stringify(config.defaultBindings),
1045
+ JSON.stringify(config.keywords),
1046
+ config.priority
1047
+ ]);
1048
+ }
1049
+ function useActions(actions) {
1050
+ const registry = useContext(ActionsRegistryContext);
1051
+ if (!registry) {
1052
+ throw new Error("useActions must be used within a HotkeysProvider");
1053
+ }
1054
+ const registryRef = useRef(registry);
1055
+ registryRef.current = registry;
1056
+ const handlersRef = useRef({});
1057
+ const enabledRef = useRef({});
1058
+ for (const [id, config] of Object.entries(actions)) {
1059
+ handlersRef.current[id] = config.handler;
1060
+ enabledRef.current[id] = config.enabled ?? true;
1061
+ }
1062
+ useEffect(() => {
1063
+ for (const [id, config] of Object.entries(actions)) {
1064
+ registryRef.current.register(id, {
1065
+ ...config,
1066
+ handler: () => {
1067
+ if (enabledRef.current[id]) {
1068
+ handlersRef.current[id]?.();
1069
+ }
1070
+ }
1071
+ });
1072
+ }
1073
+ return () => {
1074
+ for (const id of Object.keys(actions)) {
1075
+ registryRef.current.unregister(id);
1076
+ }
1077
+ };
1078
+ }, [
1079
+ // Re-register if action set changes
1080
+ JSON.stringify(
1081
+ Object.entries(actions).map(([id, c]) => [
1082
+ id,
1083
+ c.label,
1084
+ c.group,
1085
+ c.defaultBindings,
1086
+ c.keywords,
1087
+ c.priority
1088
+ ])
1089
+ )
1090
+ ]);
1091
+ }
1092
+ function useEventCallback(fn) {
1093
+ const ref = useRef(fn);
1094
+ ref.current = fn;
1095
+ return useCallback(((...args) => ref.current?.(...args)), []);
1096
+ }
1097
+ function useRecordHotkey(options = {}) {
1098
+ const {
1099
+ onCapture: onCaptureProp,
1100
+ onCancel: onCancelProp,
1101
+ onTab: onTabProp,
1102
+ onShiftTab: onShiftTabProp,
1103
+ preventDefault = true,
1104
+ sequenceTimeout = 1e3,
1105
+ pauseTimeout = false
1106
+ } = options;
1107
+ const onCapture = useEventCallback(onCaptureProp);
1108
+ const onCancel = useEventCallback(onCancelProp);
1109
+ const onTab = useEventCallback(onTabProp);
1110
+ const onShiftTab = useEventCallback(onShiftTabProp);
1111
+ const [isRecording, setIsRecording] = useState(false);
1112
+ const [sequence, setSequence] = useState(null);
1113
+ const [pendingKeys, setPendingKeys] = useState([]);
1114
+ const [activeKeys, setActiveKeys] = useState(null);
1115
+ const pressedKeysRef = useRef(/* @__PURE__ */ new Set());
1116
+ const hasNonModifierRef = useRef(false);
1117
+ const currentComboRef = useRef(null);
1118
+ const timeoutRef = useRef(null);
1119
+ const pauseTimeoutRef = useRef(pauseTimeout);
1120
+ pauseTimeoutRef.current = pauseTimeout;
1121
+ const pendingKeysRef = useRef([]);
1122
+ const clearTimeout_ = useCallback(() => {
1123
+ if (timeoutRef.current) {
1124
+ clearTimeout(timeoutRef.current);
1125
+ timeoutRef.current = null;
1126
+ }
1127
+ }, []);
1128
+ const submit = useCallback((seq) => {
1129
+ if (seq.length === 0) return;
1130
+ const display2 = formatCombination(seq);
1131
+ clearTimeout_();
1132
+ pressedKeysRef.current.clear();
1133
+ hasNonModifierRef.current = false;
1134
+ currentComboRef.current = null;
1135
+ setSequence(seq);
1136
+ pendingKeysRef.current = [];
1137
+ setPendingKeys([]);
1138
+ setIsRecording(false);
1139
+ setActiveKeys(null);
1140
+ onCapture?.(seq, display2);
1141
+ }, [clearTimeout_, onCapture]);
1142
+ const cancel = useCallback(() => {
1143
+ clearTimeout_();
1144
+ setIsRecording(false);
1145
+ pendingKeysRef.current = [];
1146
+ setPendingKeys([]);
1147
+ setActiveKeys(null);
1148
+ pressedKeysRef.current.clear();
1149
+ hasNonModifierRef.current = false;
1150
+ currentComboRef.current = null;
1151
+ onCancel?.();
1152
+ }, [clearTimeout_, onCancel]);
1153
+ const commit = useCallback(() => {
1154
+ const current = pendingKeysRef.current;
1155
+ if (current.length > 0) {
1156
+ submit(current);
1157
+ } else {
1158
+ cancel();
1159
+ }
1160
+ }, [submit, cancel]);
1161
+ const startRecording = useCallback(() => {
1162
+ clearTimeout_();
1163
+ setIsRecording(true);
1164
+ setSequence(null);
1165
+ pendingKeysRef.current = [];
1166
+ setPendingKeys([]);
1167
+ setActiveKeys(null);
1168
+ pressedKeysRef.current.clear();
1169
+ hasNonModifierRef.current = false;
1170
+ currentComboRef.current = null;
1171
+ return cancel;
1172
+ }, [cancel, clearTimeout_]);
1173
+ useEffect(() => {
1174
+ if (pauseTimeout) {
1175
+ if (timeoutRef.current) {
1176
+ clearTimeout(timeoutRef.current);
1177
+ timeoutRef.current = null;
1178
+ }
1179
+ } else if (isRecording && pendingKeysRef.current.length > 0 && !timeoutRef.current) {
1180
+ const currentSequence = pendingKeysRef.current;
1181
+ timeoutRef.current = setTimeout(() => {
1182
+ submit(currentSequence);
1183
+ }, sequenceTimeout);
1184
+ }
1185
+ }, [pauseTimeout, isRecording, sequenceTimeout, submit]);
1186
+ useEffect(() => {
1187
+ if (!isRecording) return;
1188
+ const handleKeyDown = (e) => {
1189
+ if (e.key === "Tab") {
1190
+ clearTimeout_();
1191
+ const pendingSeq = [...pendingKeysRef.current];
1192
+ if (hasNonModifierRef.current && currentComboRef.current) {
1193
+ pendingSeq.push(currentComboRef.current);
1194
+ }
1195
+ pendingKeysRef.current = [];
1196
+ setPendingKeys([]);
1197
+ pressedKeysRef.current.clear();
1198
+ hasNonModifierRef.current = false;
1199
+ currentComboRef.current = null;
1200
+ setActiveKeys(null);
1201
+ setIsRecording(false);
1202
+ if (pendingSeq.length > 0) {
1203
+ const display2 = formatCombination(pendingSeq);
1204
+ onCapture?.(pendingSeq, display2);
1205
+ }
1206
+ if (!e.shiftKey && onTab) {
1207
+ e.preventDefault();
1208
+ e.stopPropagation();
1209
+ onTab();
1210
+ } else if (e.shiftKey && onShiftTab) {
1211
+ e.preventDefault();
1212
+ e.stopPropagation();
1213
+ onShiftTab();
1214
+ }
1215
+ return;
1216
+ }
1217
+ if (preventDefault) {
1218
+ e.preventDefault();
1219
+ e.stopPropagation();
1220
+ }
1221
+ clearTimeout_();
1222
+ if (e.key === "Enter") {
1223
+ setPendingKeys((current) => {
1224
+ if (current.length > 0) {
1225
+ submit(current);
1226
+ }
1227
+ return current;
1228
+ });
1229
+ return;
1230
+ }
1231
+ if (e.key === "Escape") {
1232
+ cancel();
1233
+ return;
1234
+ }
1235
+ let key = e.key;
1236
+ if (e.altKey && e.code.startsWith("Key")) {
1237
+ key = e.code.slice(3).toLowerCase();
1238
+ } else if (e.altKey && e.code.startsWith("Digit")) {
1239
+ key = e.code.slice(5);
1240
+ }
1241
+ pressedKeysRef.current.add(key);
1242
+ const combo = {
1243
+ key: "",
1244
+ modifiers: {
1245
+ ctrl: e.ctrlKey,
1246
+ alt: e.altKey,
1247
+ shift: e.shiftKey,
1248
+ meta: e.metaKey
1249
+ }
1250
+ };
1251
+ for (const k of pressedKeysRef.current) {
1252
+ if (!isModifierKey(k)) {
1253
+ combo.key = normalizeKey(k);
1254
+ hasNonModifierRef.current = true;
1255
+ break;
1256
+ }
1257
+ }
1258
+ if (combo.key) {
1259
+ currentComboRef.current = combo;
1260
+ setActiveKeys(combo);
1261
+ } else {
1262
+ setActiveKeys({
1263
+ key: "",
1264
+ modifiers: combo.modifiers
1265
+ });
1266
+ }
1267
+ };
1268
+ const handleKeyUp = (e) => {
1269
+ if (preventDefault) {
1270
+ e.preventDefault();
1271
+ e.stopPropagation();
1272
+ }
1273
+ let key = e.key;
1274
+ if (e.altKey && e.code.startsWith("Key")) {
1275
+ key = e.code.slice(3).toLowerCase();
1276
+ } else if (e.altKey && e.code.startsWith("Digit")) {
1277
+ key = e.code.slice(5);
1278
+ }
1279
+ pressedKeysRef.current.delete(key);
1280
+ const shouldComplete = pressedKeysRef.current.size === 0 || e.key === "Meta" && hasNonModifierRef.current;
1281
+ if (shouldComplete && hasNonModifierRef.current && currentComboRef.current) {
1282
+ const combo = currentComboRef.current;
1283
+ pressedKeysRef.current.clear();
1284
+ hasNonModifierRef.current = false;
1285
+ currentComboRef.current = null;
1286
+ setActiveKeys(null);
1287
+ const newSequence = [...pendingKeysRef.current, combo];
1288
+ pendingKeysRef.current = newSequence;
1289
+ setPendingKeys(newSequence);
1290
+ clearTimeout_();
1291
+ if (!pauseTimeoutRef.current) {
1292
+ timeoutRef.current = setTimeout(() => {
1293
+ submit(newSequence);
1294
+ }, sequenceTimeout);
1295
+ }
1296
+ }
1297
+ };
1298
+ window.addEventListener("keydown", handleKeyDown, true);
1299
+ window.addEventListener("keyup", handleKeyUp, true);
1300
+ return () => {
1301
+ window.removeEventListener("keydown", handleKeyDown, true);
1302
+ window.removeEventListener("keyup", handleKeyUp, true);
1303
+ clearTimeout_();
1304
+ };
1305
+ }, [isRecording, preventDefault, sequenceTimeout, clearTimeout_, submit, cancel, onCapture, onTab, onShiftTab]);
1306
+ const display = sequence ? formatCombination(sequence) : null;
1307
+ const combination = sequence && sequence.length > 0 ? sequence[0] : null;
1308
+ return {
1309
+ isRecording,
1310
+ startRecording,
1311
+ cancel,
1312
+ commit,
1313
+ sequence,
1314
+ display,
1315
+ pendingKeys,
1316
+ activeKeys,
1317
+ combination
1318
+ // deprecated
1319
+ };
1320
+ }
1321
+ function useEditableHotkeys(defaults, handlers, options = {}) {
1322
+ const { storageKey, disableConflicts = true, ...hotkeyOptions } = options;
1323
+ const [overrides, setOverrides] = useState(() => {
1324
+ if (!storageKey || typeof window === "undefined") return {};
1325
+ try {
1326
+ const stored = localStorage.getItem(storageKey);
1327
+ return stored ? JSON.parse(stored) : {};
1328
+ } catch {
1329
+ return {};
1330
+ }
1331
+ });
1332
+ useEffect(() => {
1333
+ if (!storageKey || typeof window === "undefined") return;
1334
+ try {
1335
+ if (Object.keys(overrides).length === 0) {
1336
+ localStorage.removeItem(storageKey);
1337
+ } else {
1338
+ localStorage.setItem(storageKey, JSON.stringify(overrides));
1339
+ }
1340
+ } catch {
1341
+ }
1342
+ }, [storageKey, overrides]);
1343
+ const keymap = useMemo(() => {
1344
+ const actionToKey = {};
1345
+ for (const [key, action] of Object.entries(defaults)) {
1346
+ const actions = Array.isArray(action) ? action : [action];
1347
+ for (const a of actions) {
1348
+ actionToKey[a] = key;
1349
+ }
1350
+ }
1351
+ for (const [key, action] of Object.entries(overrides)) {
1352
+ if (action === void 0) continue;
1353
+ const actions = Array.isArray(action) ? action : [action];
1354
+ for (const a of actions) {
1355
+ actionToKey[a] = key;
1356
+ }
1357
+ }
1358
+ const result = {};
1359
+ for (const [action, key] of Object.entries(actionToKey)) {
1360
+ if (result[key]) {
1361
+ const existing = result[key];
1362
+ result[key] = Array.isArray(existing) ? [...existing, action] : [existing, action];
1363
+ } else {
1364
+ result[key] = action;
1365
+ }
1366
+ }
1367
+ return result;
1368
+ }, [defaults, overrides]);
1369
+ const conflicts = useMemo(() => findConflicts(keymap), [keymap]);
1370
+ const hasConflictsValue = conflicts.size > 0;
1371
+ const effectiveKeymap = useMemo(() => {
1372
+ if (!disableConflicts || conflicts.size === 0) {
1373
+ return keymap;
1374
+ }
1375
+ const filtered = {};
1376
+ for (const [key, action] of Object.entries(keymap)) {
1377
+ if (!conflicts.has(key)) {
1378
+ filtered[key] = action;
1379
+ }
1380
+ }
1381
+ return filtered;
1382
+ }, [keymap, conflicts, disableConflicts]);
1383
+ const { pendingKeys, isAwaitingSequence, cancelSequence, timeoutStartedAt, sequenceTimeout } = useHotkeys(effectiveKeymap, handlers, hotkeyOptions);
1384
+ const setBinding = useCallback((action, key) => {
1385
+ setOverrides((prev) => {
1386
+ const cleaned = {};
1387
+ for (const [k, v] of Object.entries(prev)) {
1388
+ const actions = Array.isArray(v) ? v : [v];
1389
+ if (k === key || !actions.includes(action)) {
1390
+ cleaned[k] = v;
1391
+ }
1392
+ }
1393
+ return { ...cleaned, [key]: action };
1394
+ });
1395
+ }, []);
1396
+ const setKeymap = useCallback((newOverrides) => {
1397
+ setOverrides((prev) => ({ ...prev, ...newOverrides }));
1398
+ }, []);
1399
+ const reset = useCallback(() => {
1400
+ setOverrides({});
1401
+ }, []);
1402
+ return {
1403
+ keymap,
1404
+ setBinding,
1405
+ setKeymap,
1406
+ reset,
1407
+ overrides,
1408
+ conflicts,
1409
+ hasConflicts: hasConflictsValue,
1410
+ pendingKeys,
1411
+ isAwaitingSequence,
1412
+ cancelSequence,
1413
+ timeoutStartedAt,
1414
+ sequenceTimeout
1415
+ };
1416
+ }
1417
+ function useOmnibar(options) {
1418
+ const {
1419
+ actions,
1420
+ handlers,
1421
+ keymap = {},
1422
+ openKey = "meta+k",
1423
+ enabled = true,
1424
+ onExecute,
1425
+ onOpen,
1426
+ onClose,
1427
+ maxResults = 10
1428
+ } = options;
1429
+ const [isOpen, setIsOpen] = useState(false);
1430
+ const [query, setQuery] = useState("");
1431
+ const [selectedIndex, setSelectedIndex] = useState(0);
1432
+ const handlersRef = useRef(handlers);
1433
+ handlersRef.current = handlers;
1434
+ const onExecuteRef = useRef(onExecute);
1435
+ onExecuteRef.current = onExecute;
1436
+ const omnibarKeymap = useMemo(() => {
1437
+ if (!enabled) return {};
1438
+ return { [openKey]: "omnibar:toggle" };
1439
+ }, [enabled, openKey]);
1440
+ const { pendingKeys, isAwaitingSequence } = useHotkeys(
1441
+ omnibarKeymap,
1442
+ {
1443
+ "omnibar:toggle": () => {
1444
+ setIsOpen((prev) => {
1445
+ const next = !prev;
1446
+ if (next) {
1447
+ onOpen?.();
1448
+ } else {
1449
+ onClose?.();
1450
+ }
1451
+ return next;
1452
+ });
1453
+ }
1454
+ },
1455
+ { enabled }
1456
+ );
1457
+ const results = useMemo(() => {
1458
+ const allResults = searchActions(query, actions, keymap);
1459
+ return allResults.slice(0, maxResults);
1460
+ }, [query, actions, keymap, maxResults]);
1461
+ const completions = useMemo(() => {
1462
+ return getSequenceCompletions(pendingKeys, keymap);
1463
+ }, [pendingKeys, keymap]);
1464
+ useEffect(() => {
1465
+ setSelectedIndex(0);
1466
+ }, [results]);
1467
+ const open = useCallback(() => {
1468
+ setIsOpen(true);
1469
+ setQuery("");
1470
+ setSelectedIndex(0);
1471
+ onOpen?.();
1472
+ }, [onOpen]);
1473
+ const close = useCallback(() => {
1474
+ setIsOpen(false);
1475
+ setQuery("");
1476
+ setSelectedIndex(0);
1477
+ onClose?.();
1478
+ }, [onClose]);
1479
+ const toggle = useCallback(() => {
1480
+ setIsOpen((prev) => {
1481
+ const next = !prev;
1482
+ if (next) {
1483
+ setQuery("");
1484
+ setSelectedIndex(0);
1485
+ onOpen?.();
1486
+ } else {
1487
+ onClose?.();
1488
+ }
1489
+ return next;
1490
+ });
1491
+ }, [onOpen, onClose]);
1492
+ const selectNext = useCallback(() => {
1493
+ setSelectedIndex((prev) => min(prev + 1, results.length - 1));
1494
+ }, [results.length]);
1495
+ const selectPrev = useCallback(() => {
1496
+ setSelectedIndex((prev) => max(prev - 1, 0));
1497
+ }, []);
1498
+ const resetSelection = useCallback(() => {
1499
+ setSelectedIndex(0);
1500
+ }, []);
1501
+ const execute = useCallback((actionId) => {
1502
+ const id = actionId ?? results[selectedIndex]?.id;
1503
+ if (!id) return;
1504
+ close();
1505
+ if (handlersRef.current?.[id]) {
1506
+ const event = new KeyboardEvent("keydown", { key: "Enter" });
1507
+ handlersRef.current[id](event);
1508
+ }
1509
+ onExecuteRef.current?.(id);
1510
+ }, [results, selectedIndex, close]);
1511
+ useEffect(() => {
1512
+ if (!isOpen) return;
1513
+ const handleKeyDown = (e) => {
1514
+ const target = e.target;
1515
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
1516
+ if (e.key === "Escape") {
1517
+ e.preventDefault();
1518
+ close();
1519
+ }
1520
+ return;
1521
+ }
1522
+ switch (e.key) {
1523
+ case "Escape":
1524
+ e.preventDefault();
1525
+ close();
1526
+ break;
1527
+ case "ArrowDown":
1528
+ e.preventDefault();
1529
+ selectNext();
1530
+ break;
1531
+ case "ArrowUp":
1532
+ e.preventDefault();
1533
+ selectPrev();
1534
+ break;
1535
+ case "Enter":
1536
+ e.preventDefault();
1537
+ execute();
1538
+ break;
1539
+ }
1540
+ };
1541
+ window.addEventListener("keydown", handleKeyDown);
1542
+ return () => window.removeEventListener("keydown", handleKeyDown);
1543
+ }, [isOpen, close, selectNext, selectPrev, execute]);
1544
+ return {
1545
+ isOpen,
1546
+ open,
1547
+ close,
1548
+ toggle,
1549
+ query,
1550
+ setQuery,
1551
+ results,
1552
+ selectedIndex,
1553
+ selectNext,
1554
+ selectPrev,
1555
+ execute,
1556
+ resetSelection,
1557
+ completions,
1558
+ pendingKeys,
1559
+ isAwaitingSequence
1560
+ };
1561
+ }
1562
+ function buildActionMap(keymap) {
1563
+ const map = /* @__PURE__ */ new Map();
1564
+ for (const [key, actionOrActions] of Object.entries(keymap)) {
1565
+ const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
1566
+ for (const action of actions) {
1567
+ map.set(action, key);
1568
+ }
1569
+ }
1570
+ return map;
1571
+ }
1572
+ function KeybindingEditor({
1573
+ keymap,
1574
+ defaults,
1575
+ descriptions,
1576
+ onChange,
1577
+ onReset,
1578
+ className,
1579
+ children
1580
+ }) {
1581
+ const [editingAction, setEditingAction] = useState(null);
1582
+ const actionMap = useMemo(() => buildActionMap(keymap), [keymap]);
1583
+ const defaultActionMap = useMemo(() => buildActionMap(defaults), [defaults]);
1584
+ const conflicts = useMemo(() => findConflicts(keymap), [keymap]);
1585
+ const { isRecording, startRecording, cancel, pendingKeys, activeKeys } = useRecordHotkey({
1586
+ onCapture: useCallback(
1587
+ (_sequence, display) => {
1588
+ if (editingAction) {
1589
+ onChange(editingAction, display.id);
1590
+ setEditingAction(null);
1591
+ }
1592
+ },
1593
+ [editingAction, onChange]
1594
+ ),
1595
+ onCancel: useCallback(() => {
1596
+ setEditingAction(null);
1597
+ }, [])
1598
+ });
1599
+ const startEditing = useCallback(
1600
+ (action) => {
1601
+ setEditingAction(action);
1602
+ startRecording();
1603
+ },
1604
+ [startRecording]
1605
+ );
1606
+ const cancelEditing = useCallback(() => {
1607
+ cancel();
1608
+ setEditingAction(null);
1609
+ }, [cancel]);
1610
+ const reset = useCallback(() => {
1611
+ onReset?.();
1612
+ }, [onReset]);
1613
+ const getRecordingDisplay = () => {
1614
+ if (pendingKeys.length === 0 && (!activeKeys || !activeKeys.key)) {
1615
+ return "Press keys...";
1616
+ }
1617
+ let display = pendingKeys.length > 0 ? formatCombination(pendingKeys).display : "";
1618
+ if (activeKeys && activeKeys.key) {
1619
+ if (display) display += " \u2192 ";
1620
+ display += formatCombination([activeKeys]).display;
1621
+ }
1622
+ return display + "...";
1623
+ };
1624
+ const bindings = useMemo(() => {
1625
+ const allActions = /* @__PURE__ */ new Set([...actionMap.keys(), ...defaultActionMap.keys()]);
1626
+ return Array.from(allActions).map((action) => {
1627
+ const key = actionMap.get(action) ?? defaultActionMap.get(action) ?? "";
1628
+ const defaultKey = defaultActionMap.get(action) ?? "";
1629
+ const combo = parseCombinationId(key);
1630
+ const display = formatCombination(combo);
1631
+ const conflictActions = conflicts.get(key);
1632
+ return {
1633
+ action,
1634
+ key,
1635
+ display,
1636
+ description: descriptions?.[action] ?? action,
1637
+ isDefault: key === defaultKey,
1638
+ hasConflict: conflictActions !== void 0 && conflictActions.length > 1
1639
+ };
1640
+ }).sort((a, b) => a.action.localeCompare(b.action));
1641
+ }, [actionMap, defaultActionMap, descriptions, conflicts]);
1642
+ if (children) {
1643
+ return /* @__PURE__ */ jsx(Fragment, { children: children({
1644
+ bindings,
1645
+ editingAction,
1646
+ pendingKeys,
1647
+ activeKeys,
1648
+ startEditing,
1649
+ cancelEditing,
1650
+ reset,
1651
+ conflicts
1652
+ }) });
1653
+ }
1654
+ return /* @__PURE__ */ jsxs("div", { className, children: [
1655
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "16px" }, children: [
1656
+ /* @__PURE__ */ jsx("h3", { style: { margin: 0 }, children: "Keybindings" }),
1657
+ onReset && /* @__PURE__ */ jsx(
1658
+ "button",
1659
+ {
1660
+ onClick: reset,
1661
+ style: {
1662
+ padding: "6px 12px",
1663
+ backgroundColor: "#f5f5f5",
1664
+ border: "1px solid #ddd",
1665
+ borderRadius: "4px",
1666
+ cursor: "pointer"
1667
+ },
1668
+ children: "Reset to defaults"
1669
+ }
1670
+ )
1671
+ ] }),
1672
+ /* @__PURE__ */ jsxs("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
1673
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1674
+ /* @__PURE__ */ jsx("th", { style: { textAlign: "left", padding: "8px", borderBottom: "2px solid #ddd" }, children: "Action" }),
1675
+ /* @__PURE__ */ jsx("th", { style: { textAlign: "left", padding: "8px", borderBottom: "2px solid #ddd" }, children: "Keybinding" }),
1676
+ /* @__PURE__ */ jsx("th", { style: { width: "80px", padding: "8px", borderBottom: "2px solid #ddd" } })
1677
+ ] }) }),
1678
+ /* @__PURE__ */ jsx("tbody", { children: bindings.map(({ action, display, description, isDefault, hasConflict }) => {
1679
+ const isEditing = editingAction === action;
1680
+ return /* @__PURE__ */ jsxs("tr", { style: { backgroundColor: hasConflict ? "#fff3cd" : void 0 }, children: [
1681
+ /* @__PURE__ */ jsxs("td", { style: { padding: "8px", borderBottom: "1px solid #eee" }, children: [
1682
+ description,
1683
+ !isDefault && /* @__PURE__ */ jsx("span", { style: { marginLeft: "8px", fontSize: "0.75rem", color: "#666" }, children: "(modified)" })
1684
+ ] }),
1685
+ /* @__PURE__ */ jsxs("td", { style: { padding: "8px", borderBottom: "1px solid #eee" }, children: [
1686
+ isEditing ? /* @__PURE__ */ jsx(
1687
+ "kbd",
1688
+ {
1689
+ style: {
1690
+ backgroundColor: "#e3f2fd",
1691
+ padding: "4px 8px",
1692
+ borderRadius: "4px",
1693
+ border: "2px solid #2196f3",
1694
+ fontFamily: "monospace"
1695
+ },
1696
+ children: getRecordingDisplay()
1697
+ }
1698
+ ) : /* @__PURE__ */ jsx(
1699
+ "kbd",
1700
+ {
1701
+ style: {
1702
+ backgroundColor: "#f5f5f5",
1703
+ padding: "4px 8px",
1704
+ borderRadius: "4px",
1705
+ border: "1px solid #ddd",
1706
+ fontFamily: "monospace"
1707
+ },
1708
+ children: display.display
1709
+ }
1710
+ ),
1711
+ hasConflict && !isEditing && /* @__PURE__ */ jsx("span", { style: { marginLeft: "8px", color: "#856404", fontSize: "0.75rem" }, children: "\u26A0 Conflict" })
1712
+ ] }),
1713
+ /* @__PURE__ */ jsx("td", { style: { padding: "8px", borderBottom: "1px solid #eee", textAlign: "center" }, children: isEditing ? /* @__PURE__ */ jsx(
1714
+ "button",
1715
+ {
1716
+ onClick: cancelEditing,
1717
+ style: {
1718
+ padding: "4px 8px",
1719
+ backgroundColor: "#f5f5f5",
1720
+ border: "1px solid #ddd",
1721
+ borderRadius: "4px",
1722
+ cursor: "pointer",
1723
+ fontSize: "0.875rem"
1724
+ },
1725
+ children: "Cancel"
1726
+ }
1727
+ ) : /* @__PURE__ */ jsx(
1728
+ "button",
1729
+ {
1730
+ onClick: () => startEditing(action),
1731
+ disabled: isRecording,
1732
+ style: {
1733
+ padding: "4px 8px",
1734
+ backgroundColor: "#f5f5f5",
1735
+ border: "1px solid #ddd",
1736
+ borderRadius: "4px",
1737
+ cursor: isRecording ? "not-allowed" : "pointer",
1738
+ fontSize: "0.875rem",
1739
+ opacity: isRecording ? 0.5 : 1
1740
+ },
1741
+ children: "Edit"
1742
+ }
1743
+ ) })
1744
+ ] }, action);
1745
+ }) })
1746
+ ] })
1747
+ ] });
1748
+ }
1749
+ var baseStyle = {
1750
+ width: "1.2em",
1751
+ height: "1.2em",
1752
+ marginRight: "2px",
1753
+ verticalAlign: "middle"
1754
+ };
1755
+ var wideStyle = {
1756
+ ...baseStyle,
1757
+ width: "1.4em"
1758
+ };
1759
+ function CommandIcon({ className, style }) {
1760
+ return /* @__PURE__ */ jsx(
1761
+ "svg",
1762
+ {
1763
+ className,
1764
+ style: { ...baseStyle, ...style },
1765
+ viewBox: "0 0 24 24",
1766
+ fill: "currentColor",
1767
+ children: /* @__PURE__ */ jsx("path", { d: "M6 4a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2v4H6a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2h4v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2h-2v-4h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v2h-4V6a2 2 0 0 0-2-2H6zm4 6h4v4h-4v-4z" })
1768
+ }
1769
+ );
1770
+ }
1771
+ function CtrlIcon({ className, style }) {
1772
+ return /* @__PURE__ */ jsx(
1773
+ "svg",
1774
+ {
1775
+ className,
1776
+ style: { ...baseStyle, ...style },
1777
+ viewBox: "0 0 24 24",
1778
+ fill: "none",
1779
+ stroke: "currentColor",
1780
+ strokeWidth: "3",
1781
+ strokeLinecap: "round",
1782
+ strokeLinejoin: "round",
1783
+ children: /* @__PURE__ */ jsx("path", { d: "M6 15l6-6 6 6" })
1784
+ }
1785
+ );
1786
+ }
1787
+ function ShiftIcon({ className, style }) {
1788
+ return /* @__PURE__ */ jsx(
1789
+ "svg",
1790
+ {
1791
+ className,
1792
+ style: { ...wideStyle, ...style },
1793
+ viewBox: "0 0 28 24",
1794
+ fill: "none",
1795
+ stroke: "currentColor",
1796
+ strokeWidth: "2",
1797
+ strokeLinejoin: "round",
1798
+ children: /* @__PURE__ */ jsx("path", { d: "M14 3L3 14h6v7h10v-7h6L14 3z" })
1799
+ }
1800
+ );
1801
+ }
1802
+ function OptIcon({ className, style }) {
1803
+ return /* @__PURE__ */ jsx(
1804
+ "svg",
1805
+ {
1806
+ className,
1807
+ style: { ...baseStyle, ...style },
1808
+ viewBox: "0 0 24 24",
1809
+ fill: "none",
1810
+ stroke: "currentColor",
1811
+ strokeWidth: "2.5",
1812
+ strokeLinecap: "round",
1813
+ strokeLinejoin: "round",
1814
+ children: /* @__PURE__ */ jsx("path", { d: "M4 6h6l8 12h6M14 6h6" })
1815
+ }
1816
+ );
1817
+ }
1818
+ function AltIcon({ className, style }) {
1819
+ return /* @__PURE__ */ jsx(
1820
+ "svg",
1821
+ {
1822
+ className,
1823
+ style: { ...baseStyle, ...style },
1824
+ viewBox: "0 0 24 24",
1825
+ fill: "none",
1826
+ stroke: "currentColor",
1827
+ strokeWidth: "2.5",
1828
+ strokeLinecap: "round",
1829
+ strokeLinejoin: "round",
1830
+ children: /* @__PURE__ */ jsx("path", { d: "M4 18h8M12 18l4-6M12 18l4 0M16 12l4-6h-8" })
1831
+ }
1832
+ );
1833
+ }
1834
+ var isMac2 = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
1835
+ function getModifierIcon(modifier) {
1836
+ switch (modifier) {
1837
+ case "meta":
1838
+ return CommandIcon;
1839
+ case "ctrl":
1840
+ return CtrlIcon;
1841
+ case "shift":
1842
+ return ShiftIcon;
1843
+ case "opt":
1844
+ return OptIcon;
1845
+ case "alt":
1846
+ return isMac2 ? OptIcon : AltIcon;
1847
+ }
1848
+ }
1849
+ function ModifierIcon({ modifier, ...props }) {
1850
+ const Icon = getModifierIcon(modifier);
1851
+ return /* @__PURE__ */ jsx(Icon, { ...props });
1852
+ }
1853
+ function BindingBadge({ binding }) {
1854
+ const sequence = parseHotkeyString(binding);
1855
+ return /* @__PURE__ */ jsx("kbd", { className: "kbd-kbd", children: sequence.map((combo, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
1856
+ i > 0 && /* @__PURE__ */ jsx("span", { className: "kbd-sequence-sep", children: " " }),
1857
+ combo.modifiers.meta && /* @__PURE__ */ jsx(ModifierIcon, { modifier: "meta", className: "kbd-modifier-icon" }),
1858
+ combo.modifiers.ctrl && /* @__PURE__ */ jsx(ModifierIcon, { modifier: "ctrl", className: "kbd-modifier-icon" }),
1859
+ combo.modifiers.alt && /* @__PURE__ */ jsx(ModifierIcon, { modifier: "alt", className: "kbd-modifier-icon" }),
1860
+ combo.modifiers.shift && /* @__PURE__ */ jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }),
1861
+ /* @__PURE__ */ jsx("span", { children: combo.key.length === 1 ? combo.key.toUpperCase() : combo.key })
1862
+ ] }, i)) });
1863
+ }
1864
+ function Omnibar({
1865
+ actions: actionsProp,
1866
+ handlers: handlersProp,
1867
+ keymap: keymapProp,
1868
+ openKey = "meta+k",
1869
+ enabled: enabledProp,
1870
+ isOpen: isOpenProp,
1871
+ onOpen: onOpenProp,
1872
+ onClose: onCloseProp,
1873
+ onExecute: onExecuteProp,
1874
+ maxResults = 10,
1875
+ placeholder = "Type a command...",
1876
+ children,
1877
+ backdropClassName = "kbd-omnibar-backdrop",
1878
+ omnibarClassName = "kbd-omnibar"
1879
+ }) {
1880
+ const inputRef = useRef(null);
1881
+ const ctx = useMaybeHotkeysContext();
1882
+ const actions = actionsProp ?? ctx?.registry.actionRegistry ?? {};
1883
+ const keymap = keymapProp ?? ctx?.registry.keymap ?? {};
1884
+ const enabled = enabledProp ?? !ctx;
1885
+ const handleExecute = useCallback((actionId) => {
1886
+ if (onExecuteProp) {
1887
+ onExecuteProp(actionId);
1888
+ } else if (ctx?.executeAction) {
1889
+ ctx.executeAction(actionId);
1890
+ }
1891
+ }, [onExecuteProp, ctx]);
1892
+ const handleClose = useCallback(() => {
1893
+ if (onCloseProp) {
1894
+ onCloseProp();
1895
+ } else if (ctx?.closeOmnibar) {
1896
+ ctx.closeOmnibar();
1897
+ }
1898
+ }, [onCloseProp, ctx]);
1899
+ const handleOpen = useCallback(() => {
1900
+ if (onOpenProp) {
1901
+ onOpenProp();
1902
+ } else if (ctx?.openOmnibar) {
1903
+ ctx.openOmnibar();
1904
+ }
1905
+ }, [onOpenProp, ctx]);
1906
+ const {
1907
+ isOpen: internalIsOpen,
1908
+ close,
1909
+ query,
1910
+ setQuery,
1911
+ results,
1912
+ selectedIndex,
1913
+ selectNext,
1914
+ selectPrev,
1915
+ execute,
1916
+ completions,
1917
+ pendingKeys,
1918
+ isAwaitingSequence
1919
+ } = useOmnibar({
1920
+ actions,
1921
+ handlers: handlersProp,
1922
+ keymap,
1923
+ openKey,
1924
+ enabled: isOpenProp === void 0 && ctx === null ? enabled : false,
1925
+ // Disable hotkey if controlled or using context
1926
+ onOpen: handleOpen,
1927
+ onClose: handleClose,
1928
+ onExecute: handleExecute,
1929
+ maxResults
1930
+ });
1931
+ const isOpen = isOpenProp ?? ctx?.isOmnibarOpen ?? internalIsOpen;
1932
+ useEffect(() => {
1933
+ if (isOpen) {
1934
+ requestAnimationFrame(() => {
1935
+ inputRef.current?.focus();
1936
+ });
1937
+ }
1938
+ }, [isOpen]);
1939
+ const handleKeyDown = useCallback(
1940
+ (e) => {
1941
+ switch (e.key) {
1942
+ case "Escape":
1943
+ e.preventDefault();
1944
+ close();
1945
+ break;
1946
+ case "ArrowDown":
1947
+ e.preventDefault();
1948
+ selectNext();
1949
+ break;
1950
+ case "ArrowUp":
1951
+ e.preventDefault();
1952
+ selectPrev();
1953
+ break;
1954
+ case "Enter":
1955
+ e.preventDefault();
1956
+ execute();
1957
+ break;
1958
+ }
1959
+ },
1960
+ [close, selectNext, selectPrev, execute]
1961
+ );
1962
+ const handleBackdropClick = useCallback(
1963
+ (e) => {
1964
+ if (e.target === e.currentTarget) {
1965
+ close();
1966
+ }
1967
+ },
1968
+ [close]
1969
+ );
1970
+ if (!isOpen) return null;
1971
+ if (children) {
1972
+ return /* @__PURE__ */ jsx(Fragment, { children: children({
1973
+ query,
1974
+ setQuery,
1975
+ results,
1976
+ selectedIndex,
1977
+ selectNext,
1978
+ selectPrev,
1979
+ execute,
1980
+ close,
1981
+ completions,
1982
+ pendingKeys,
1983
+ isAwaitingSequence,
1984
+ inputRef
1985
+ }) });
1986
+ }
1987
+ return /* @__PURE__ */ jsx("div", { className: backdropClassName, onClick: handleBackdropClick, children: /* @__PURE__ */ jsxs("div", { className: omnibarClassName, role: "dialog", "aria-modal": "true", "aria-label": "Command palette", children: [
1988
+ /* @__PURE__ */ jsx(
1989
+ "input",
1990
+ {
1991
+ ref: inputRef,
1992
+ type: "text",
1993
+ className: "kbd-omnibar-input",
1994
+ value: query,
1995
+ onChange: (e) => setQuery(e.target.value),
1996
+ onKeyDown: handleKeyDown,
1997
+ placeholder,
1998
+ autoComplete: "off",
1999
+ autoCorrect: "off",
2000
+ autoCapitalize: "off",
2001
+ spellCheck: false
2002
+ }
2003
+ ),
2004
+ /* @__PURE__ */ jsx("div", { className: "kbd-omnibar-results", children: results.length === 0 ? /* @__PURE__ */ jsx("div", { className: "kbd-omnibar-no-results", children: query ? "No matching commands" : "Start typing to search commands..." }) : results.map((result, i) => /* @__PURE__ */ jsxs(
2005
+ "div",
2006
+ {
2007
+ className: `kbd-omnibar-result ${i === selectedIndex ? "selected" : ""}`,
2008
+ onClick: () => execute(result.id),
2009
+ onMouseEnter: () => {
2010
+ },
2011
+ children: [
2012
+ /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-result-label", children: result.action.label }),
2013
+ result.action.group && /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-result-category", children: result.action.group }),
2014
+ result.bindings.length > 0 && /* @__PURE__ */ jsx("div", { className: "kbd-omnibar-result-bindings", children: result.bindings.slice(0, 2).map((binding) => /* @__PURE__ */ jsx(BindingBadge, { binding }, binding)) })
2015
+ ]
2016
+ },
2017
+ result.id
2018
+ )) })
2019
+ ] }) });
2020
+ }
2021
+ function SequenceModal() {
2022
+ const {
2023
+ pendingKeys,
2024
+ isAwaitingSequence,
2025
+ sequenceTimeoutStartedAt: timeoutStartedAt,
2026
+ sequenceTimeout,
2027
+ getCompletions,
2028
+ registry
2029
+ } = useHotkeysContext();
2030
+ const completions = useMemo(() => {
2031
+ if (pendingKeys.length === 0) return [];
2032
+ return getCompletions(pendingKeys);
2033
+ }, [getCompletions, pendingKeys]);
2034
+ const formattedPendingKeys = useMemo(() => {
2035
+ if (pendingKeys.length === 0) return "";
2036
+ return formatCombination(pendingKeys).display;
2037
+ }, [pendingKeys]);
2038
+ const getActionLabel = (actionId) => {
2039
+ const action = registry.actions.get(actionId);
2040
+ return action?.config.label || actionId;
2041
+ };
2042
+ const groupedCompletions = useMemo(() => {
2043
+ const byNextKey = /* @__PURE__ */ new Map();
2044
+ for (const c of completions) {
2045
+ const existing = byNextKey.get(c.nextKeys);
2046
+ if (existing) {
2047
+ existing.push(c);
2048
+ } else {
2049
+ byNextKey.set(c.nextKeys, [c]);
2050
+ }
2051
+ }
2052
+ return byNextKey;
2053
+ }, [completions]);
2054
+ if (!isAwaitingSequence || pendingKeys.length === 0) {
2055
+ return null;
2056
+ }
2057
+ return /* @__PURE__ */ jsx("div", { className: "kbd-sequence-backdrop", children: /* @__PURE__ */ jsxs("div", { className: "kbd-sequence", children: [
2058
+ /* @__PURE__ */ jsxs("div", { className: "kbd-sequence-current", children: [
2059
+ /* @__PURE__ */ jsx("kbd", { className: "kbd-sequence-keys", children: formattedPendingKeys }),
2060
+ /* @__PURE__ */ jsx("span", { className: "kbd-sequence-ellipsis", children: "\u2026" })
2061
+ ] }),
2062
+ timeoutStartedAt && /* @__PURE__ */ jsx(
2063
+ "div",
2064
+ {
2065
+ className: "kbd-sequence-timeout",
2066
+ style: { animationDuration: `${sequenceTimeout}ms` }
2067
+ },
2068
+ timeoutStartedAt
2069
+ ),
2070
+ completions.length > 0 && /* @__PURE__ */ jsx("div", { className: "kbd-sequence-completions", children: Array.from(groupedCompletions.entries()).map(([nextKey, comps]) => /* @__PURE__ */ jsxs("div", { className: "kbd-sequence-completion", children: [
2071
+ /* @__PURE__ */ jsx("kbd", { className: "kbd-kbd", children: nextKey.toUpperCase() }),
2072
+ /* @__PURE__ */ jsx("span", { className: "kbd-sequence-arrow", children: "\u2192" }),
2073
+ /* @__PURE__ */ jsx("span", { className: "kbd-sequence-actions", children: comps.flatMap((c) => c.actions).map((action, i) => /* @__PURE__ */ jsxs("span", { children: [
2074
+ i > 0 && ", ",
2075
+ getActionLabel(action)
2076
+ ] }, action)) })
2077
+ ] }, nextKey)) }),
2078
+ completions.length === 0 && /* @__PURE__ */ jsx("div", { className: "kbd-sequence-empty", children: "No matching shortcuts" })
2079
+ ] }) });
2080
+ }
2081
+ function parseActionId(actionId) {
2082
+ const colonIndex = actionId.indexOf(":");
2083
+ if (colonIndex > 0) {
2084
+ return { group: actionId.slice(0, colonIndex), name: actionId.slice(colonIndex + 1) };
2085
+ }
2086
+ return { group: "General", name: actionId };
2087
+ }
2088
+ function organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder) {
2089
+ const actionBindings = getActionBindings(keymap);
2090
+ const groupMap = /* @__PURE__ */ new Map();
2091
+ for (const [actionId, bindings] of actionBindings) {
2092
+ const { group: groupKey, name } = parseActionId(actionId);
2093
+ const groupName = groupNames?.[groupKey] ?? groupKey;
2094
+ if (!groupMap.has(groupName)) {
2095
+ groupMap.set(groupName, { name: groupName, shortcuts: [] });
2096
+ }
2097
+ groupMap.get(groupName).shortcuts.push({
2098
+ actionId,
2099
+ label: labels?.[actionId] ?? name,
2100
+ description: descriptions?.[actionId],
2101
+ bindings
2102
+ });
2103
+ }
2104
+ for (const group of groupMap.values()) {
2105
+ group.shortcuts.sort((a, b) => a.actionId.localeCompare(b.actionId));
2106
+ }
2107
+ const groups = Array.from(groupMap.values());
2108
+ if (groupOrder) {
2109
+ groups.sort((a, b) => {
2110
+ const aIdx = groupOrder.indexOf(a.name);
2111
+ const bIdx = groupOrder.indexOf(b.name);
2112
+ if (aIdx === -1 && bIdx === -1) return a.name.localeCompare(b.name);
2113
+ if (aIdx === -1) return 1;
2114
+ if (bIdx === -1) return -1;
2115
+ return aIdx - bIdx;
2116
+ });
2117
+ } else {
2118
+ groups.sort((a, b) => {
2119
+ if (a.name === "General") return 1;
2120
+ if (b.name === "General") return -1;
2121
+ return a.name.localeCompare(b.name);
2122
+ });
2123
+ }
2124
+ return groups;
2125
+ }
2126
+ function KeyDisplay({
2127
+ combo,
2128
+ className
2129
+ }) {
2130
+ const { key, modifiers } = combo;
2131
+ const parts = [];
2132
+ if (modifiers.meta) {
2133
+ parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "meta", className: "kbd-modifier-icon" }, "meta"));
2134
+ }
2135
+ if (modifiers.ctrl) {
2136
+ parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "ctrl", className: "kbd-modifier-icon" }, "ctrl"));
2137
+ }
2138
+ if (modifiers.alt) {
2139
+ parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "alt", className: "kbd-modifier-icon" }, "alt"));
2140
+ }
2141
+ if (modifiers.shift) {
2142
+ parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }, "shift"));
2143
+ }
2144
+ const keyDisplay = key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1);
2145
+ parts.push(/* @__PURE__ */ jsx("span", { children: keyDisplay }, "key"));
2146
+ return /* @__PURE__ */ jsx("span", { className, children: parts });
2147
+ }
2148
+ function BindingDisplay({
2149
+ binding,
2150
+ className,
2151
+ editable,
2152
+ isEditing,
2153
+ isConflict,
2154
+ isPendingConflict,
2155
+ isDefault,
2156
+ onEdit,
2157
+ onRemove,
2158
+ pendingKeys,
2159
+ activeKeys
2160
+ }) {
2161
+ const sequence = parseHotkeyString(binding);
2162
+ const display = formatCombination(sequence);
2163
+ let kbdClassName = "kbd-kbd";
2164
+ if (editable && !isEditing) kbdClassName += " editable";
2165
+ if (isEditing) kbdClassName += " editing";
2166
+ if (isConflict) kbdClassName += " conflict";
2167
+ if (isPendingConflict) kbdClassName += " pending-conflict";
2168
+ if (isDefault) kbdClassName += " default-binding";
2169
+ if (className) kbdClassName += " " + className;
2170
+ const handleClick = editable && onEdit ? onEdit : void 0;
2171
+ if (isEditing) {
2172
+ let content;
2173
+ if (pendingKeys && pendingKeys.length > 0) {
2174
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
2175
+ pendingKeys.map((combo, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
2176
+ i > 0 && /* @__PURE__ */ jsx("span", { className: "kbd-sequence-sep", children: " " }),
2177
+ /* @__PURE__ */ jsx(KeyDisplay, { combo })
2178
+ ] }, i)),
2179
+ activeKeys && activeKeys.key && /* @__PURE__ */ jsxs(Fragment, { children: [
2180
+ /* @__PURE__ */ jsx("span", { className: "kbd-sequence-sep", children: " \u2192 " }),
2181
+ /* @__PURE__ */ jsx(KeyDisplay, { combo: activeKeys })
2182
+ ] }),
2183
+ /* @__PURE__ */ jsx("span", { children: "..." })
2184
+ ] });
2185
+ } else if (activeKeys && activeKeys.key) {
2186
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
2187
+ /* @__PURE__ */ jsx(KeyDisplay, { combo: activeKeys }),
2188
+ /* @__PURE__ */ jsx("span", { children: "..." })
2189
+ ] });
2190
+ } else {
2191
+ content = "...";
2192
+ }
2193
+ return /* @__PURE__ */ jsx("kbd", { className: kbdClassName, tabIndex: editable ? 0 : void 0, children: content });
2194
+ }
2195
+ return /* @__PURE__ */ jsxs("kbd", { className: kbdClassName, onClick: handleClick, tabIndex: editable ? 0 : void 0, onKeyDown: editable && onEdit ? (e) => {
2196
+ if (e.key === "Enter" || e.key === " ") {
2197
+ e.preventDefault();
2198
+ onEdit();
2199
+ }
2200
+ } : void 0, children: [
2201
+ display.isSequence ? sequence.map((combo, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
2202
+ i > 0 && /* @__PURE__ */ jsx("span", { className: "kbd-sequence-sep", children: " " }),
2203
+ /* @__PURE__ */ jsx(KeyDisplay, { combo })
2204
+ ] }, i)) : /* @__PURE__ */ jsx(KeyDisplay, { combo: sequence[0] }),
2205
+ editable && onRemove && /* @__PURE__ */ jsx(
2206
+ "button",
2207
+ {
2208
+ className: "kbd-remove-btn",
2209
+ onClick: (e) => {
2210
+ e.stopPropagation();
2211
+ onRemove();
2212
+ },
2213
+ "aria-label": "Remove binding",
2214
+ children: "\xD7"
2215
+ }
2216
+ )
2217
+ ] });
2218
+ }
2219
+ function ShortcutsModal({
2220
+ keymap: keymapProp,
2221
+ defaults: defaultsProp,
2222
+ labels: labelsProp,
2223
+ descriptions: descriptionsProp,
2224
+ groups: groupNamesProp,
2225
+ groupOrder,
2226
+ groupRenderers,
2227
+ isOpen: isOpenProp,
2228
+ onClose: onCloseProp,
2229
+ openKey = "?",
2230
+ autoRegisterOpen,
2231
+ editable = false,
2232
+ onBindingChange,
2233
+ onBindingAdd,
2234
+ onBindingRemove,
2235
+ onReset,
2236
+ multipleBindings = true,
2237
+ children,
2238
+ backdropClassName = "kbd-backdrop",
2239
+ modalClassName = "kbd-modal",
2240
+ title = "Keyboard Shortcuts",
2241
+ hint
2242
+ }) {
2243
+ const ctx = useMaybeHotkeysContext();
2244
+ const contextLabels = useMemo(() => {
2245
+ const registry = ctx?.registry.actionRegistry;
2246
+ if (!registry) return void 0;
2247
+ const labels2 = {};
2248
+ for (const [id, action] of Object.entries(registry)) {
2249
+ labels2[id] = action.label;
2250
+ }
2251
+ return labels2;
2252
+ }, [ctx?.registry.actionRegistry]);
2253
+ const contextDescriptions = useMemo(() => {
2254
+ const registry = ctx?.registry.actionRegistry;
2255
+ if (!registry) return void 0;
2256
+ const descriptions2 = {};
2257
+ for (const [id, action] of Object.entries(registry)) {
2258
+ if (action.description) descriptions2[id] = action.description;
2259
+ }
2260
+ return descriptions2;
2261
+ }, [ctx?.registry.actionRegistry]);
2262
+ const contextGroups = useMemo(() => {
2263
+ const registry = ctx?.registry.actionRegistry;
2264
+ if (!registry) return void 0;
2265
+ const groups = {};
2266
+ for (const action of Object.values(registry)) {
2267
+ if (action.group) {
2268
+ const prefix = action.group.toLowerCase().replace(/[\s-]/g, "");
2269
+ groups[prefix] = action.group;
2270
+ }
2271
+ }
2272
+ return groups;
2273
+ }, [ctx?.registry.actionRegistry]);
2274
+ const keymap = keymapProp ?? ctx?.registry.keymap ?? {};
2275
+ const defaults = defaultsProp;
2276
+ const labels = labelsProp ?? contextLabels;
2277
+ const descriptions = descriptionsProp ?? contextDescriptions;
2278
+ const groupNames = groupNamesProp ?? contextGroups;
2279
+ const handleBindingChange = onBindingChange ?? (ctx ? (action, oldKey, newKey) => {
2280
+ if (oldKey) ctx.registry.removeBinding(oldKey);
2281
+ ctx.registry.setBinding(action, newKey);
2282
+ } : void 0);
2283
+ const handleBindingAdd = onBindingAdd ?? (ctx ? (action, key) => {
2284
+ ctx.registry.setBinding(action, key);
2285
+ } : void 0);
2286
+ const handleBindingRemove = onBindingRemove ?? (ctx ? (_action, key) => {
2287
+ ctx.registry.removeBinding(key);
2288
+ } : void 0);
2289
+ const handleReset = onReset ?? (ctx ? () => {
2290
+ ctx.registry.resetOverrides();
2291
+ } : void 0);
2292
+ const shouldAutoRegisterOpen = autoRegisterOpen ?? !ctx;
2293
+ const [internalIsOpen, setInternalIsOpen] = useState(false);
2294
+ const isOpen = isOpenProp ?? ctx?.isModalOpen ?? internalIsOpen;
2295
+ const [editingAction, setEditingAction] = useState(null);
2296
+ const [editingKey, setEditingKey] = useState(null);
2297
+ const [addingAction, setAddingAction] = useState(null);
2298
+ const [pendingConflict, setPendingConflict] = useState(null);
2299
+ const editingActionRef = useRef(null);
2300
+ const editingKeyRef = useRef(null);
2301
+ const addingActionRef = useRef(null);
2302
+ const conflicts = useMemo(() => findConflicts(keymap), [keymap]);
2303
+ const actionBindings = useMemo(() => getActionBindings(keymap), [keymap]);
2304
+ const close = useCallback(() => {
2305
+ setInternalIsOpen(false);
2306
+ setEditingAction(null);
2307
+ setEditingKey(null);
2308
+ setAddingAction(null);
2309
+ editingActionRef.current = null;
2310
+ editingKeyRef.current = null;
2311
+ addingActionRef.current = null;
2312
+ setPendingConflict(null);
2313
+ if (onCloseProp) {
2314
+ onCloseProp();
2315
+ } else if (ctx?.closeModal) {
2316
+ ctx.closeModal();
2317
+ }
2318
+ }, [onCloseProp, ctx]);
2319
+ const open = useCallback(() => {
2320
+ if (ctx?.openModal) {
2321
+ ctx.openModal();
2322
+ } else {
2323
+ setInternalIsOpen(true);
2324
+ }
2325
+ }, [ctx]);
2326
+ const checkConflict = useCallback((newKey, forAction) => {
2327
+ const existingActions = keymap[newKey];
2328
+ if (!existingActions) return null;
2329
+ const actions = Array.isArray(existingActions) ? existingActions : [existingActions];
2330
+ const conflicts2 = actions.filter((a) => a !== forAction);
2331
+ return conflicts2.length > 0 ? conflicts2 : null;
2332
+ }, [keymap]);
2333
+ const { isRecording, startRecording, cancel, pendingKeys, activeKeys } = useRecordHotkey({
2334
+ onCapture: useCallback(
2335
+ (_sequence, display) => {
2336
+ const currentAddingAction = addingActionRef.current;
2337
+ const currentEditingAction = editingActionRef.current;
2338
+ const currentEditingKey = editingKeyRef.current;
2339
+ const actionToUpdate = currentAddingAction || currentEditingAction;
2340
+ if (!actionToUpdate) return;
2341
+ const conflictActions = checkConflict(display.id, actionToUpdate);
2342
+ if (conflictActions && conflictActions.length > 0) {
2343
+ setPendingConflict({
2344
+ action: actionToUpdate,
2345
+ key: display.id,
2346
+ conflictsWith: conflictActions
2347
+ });
2348
+ return;
2349
+ }
2350
+ if (currentAddingAction) {
2351
+ handleBindingAdd?.(currentAddingAction, display.id);
2352
+ } else if (currentEditingAction && currentEditingKey) {
2353
+ handleBindingChange?.(currentEditingAction, currentEditingKey, display.id);
2354
+ }
2355
+ editingActionRef.current = null;
2356
+ editingKeyRef.current = null;
2357
+ addingActionRef.current = null;
2358
+ setEditingAction(null);
2359
+ setEditingKey(null);
2360
+ setAddingAction(null);
2361
+ },
2362
+ [checkConflict, handleBindingChange, handleBindingAdd]
2363
+ ),
2364
+ onCancel: useCallback(() => {
2365
+ editingActionRef.current = null;
2366
+ editingKeyRef.current = null;
2367
+ addingActionRef.current = null;
2368
+ setEditingAction(null);
2369
+ setEditingKey(null);
2370
+ setAddingAction(null);
2371
+ setPendingConflict(null);
2372
+ }, []),
2373
+ // Tab to next/prev editable kbd and start editing
2374
+ onTab: useCallback(() => {
2375
+ const editables = Array.from(document.querySelectorAll(".kbd-kbd.editable, .kbd-kbd.editing"));
2376
+ const current = document.querySelector(".kbd-kbd.editing");
2377
+ const currentIndex = current ? editables.indexOf(current) : -1;
2378
+ const nextIndex = (currentIndex + 1) % editables.length;
2379
+ const next = editables[nextIndex];
2380
+ if (next) {
2381
+ next.focus();
2382
+ next.click();
2383
+ }
2384
+ }, []),
2385
+ onShiftTab: useCallback(() => {
2386
+ const editables = Array.from(document.querySelectorAll(".kbd-kbd.editable, .kbd-kbd.editing"));
2387
+ const current = document.querySelector(".kbd-kbd.editing");
2388
+ const currentIndex = current ? editables.indexOf(current) : -1;
2389
+ const prevIndex = currentIndex <= 0 ? editables.length - 1 : currentIndex - 1;
2390
+ const prev = editables[prevIndex];
2391
+ if (prev) {
2392
+ prev.focus();
2393
+ prev.click();
2394
+ }
2395
+ }, []),
2396
+ pauseTimeout: pendingConflict !== null
2397
+ });
2398
+ const startEditingBinding = useCallback(
2399
+ (action, key) => {
2400
+ addingActionRef.current = null;
2401
+ editingActionRef.current = action;
2402
+ editingKeyRef.current = key;
2403
+ setAddingAction(null);
2404
+ setEditingAction(action);
2405
+ setEditingKey(key);
2406
+ setPendingConflict(null);
2407
+ startRecording();
2408
+ },
2409
+ [startRecording]
2410
+ );
2411
+ const startAddingBinding = useCallback(
2412
+ (action) => {
2413
+ editingActionRef.current = null;
2414
+ editingKeyRef.current = null;
2415
+ addingActionRef.current = action;
2416
+ setEditingAction(null);
2417
+ setEditingKey(null);
2418
+ setAddingAction(action);
2419
+ setPendingConflict(null);
2420
+ startRecording();
2421
+ },
2422
+ [startRecording]
2423
+ );
2424
+ const startEditing = useCallback(
2425
+ (action, bindingIndex) => {
2426
+ const bindings = actionBindings.get(action) ?? [];
2427
+ if (bindingIndex !== void 0 && bindings[bindingIndex]) {
2428
+ startEditingBinding(action, bindings[bindingIndex]);
2429
+ } else {
2430
+ startAddingBinding(action);
2431
+ }
2432
+ },
2433
+ [actionBindings, startEditingBinding, startAddingBinding]
2434
+ );
2435
+ const cancelEditing = useCallback(() => {
2436
+ cancel();
2437
+ editingActionRef.current = null;
2438
+ editingKeyRef.current = null;
2439
+ addingActionRef.current = null;
2440
+ setEditingAction(null);
2441
+ setEditingKey(null);
2442
+ setAddingAction(null);
2443
+ setPendingConflict(null);
2444
+ }, [cancel]);
2445
+ const removeBinding = useCallback(
2446
+ (action, key) => {
2447
+ handleBindingRemove?.(action, key);
2448
+ },
2449
+ [handleBindingRemove]
2450
+ );
2451
+ const reset = useCallback(() => {
2452
+ handleReset?.();
2453
+ }, [handleReset]);
2454
+ const renderEditableKbd = useCallback(
2455
+ (actionId, key, showRemove = false) => {
2456
+ const isEditingThis = editingAction === actionId && editingKey === key && !addingAction;
2457
+ const conflictActions = conflicts.get(key);
2458
+ const isConflict = conflictActions && conflictActions.length > 1;
2459
+ const isDefault = defaults ? (() => {
2460
+ const defaultAction = defaults[key];
2461
+ if (!defaultAction) return false;
2462
+ const defaultActions = Array.isArray(defaultAction) ? defaultAction : [defaultAction];
2463
+ return defaultActions.includes(actionId);
2464
+ })() : true;
2465
+ return /* @__PURE__ */ jsx(
2466
+ BindingDisplay,
2467
+ {
2468
+ binding: key,
2469
+ editable,
2470
+ isEditing: isEditingThis,
2471
+ isConflict,
2472
+ isDefault,
2473
+ onEdit: () => {
2474
+ if (isRecording && !(editingAction === actionId && editingKey === key)) {
2475
+ if (pendingKeys.length > 0) {
2476
+ const display = formatCombination(pendingKeys);
2477
+ const currentAddingAction = addingActionRef.current;
2478
+ const currentEditingAction = editingActionRef.current;
2479
+ const currentEditingKey = editingKeyRef.current;
2480
+ if (currentAddingAction) {
2481
+ handleBindingAdd?.(currentAddingAction, display.id);
2482
+ } else if (currentEditingAction && currentEditingKey) {
2483
+ handleBindingChange?.(currentEditingAction, currentEditingKey, display.id);
2484
+ }
2485
+ }
2486
+ cancel();
2487
+ }
2488
+ startEditingBinding(actionId, key);
2489
+ },
2490
+ onRemove: editable && showRemove ? () => removeBinding(actionId, key) : void 0,
2491
+ pendingKeys,
2492
+ activeKeys
2493
+ },
2494
+ key
2495
+ );
2496
+ },
2497
+ [editingAction, editingKey, addingAction, conflicts, defaults, editable, startEditingBinding, removeBinding, pendingKeys, activeKeys, isRecording, cancel, handleBindingAdd, handleBindingChange]
2498
+ );
2499
+ const renderAddButton = useCallback(
2500
+ (actionId) => {
2501
+ const isAddingThis = addingAction === actionId;
2502
+ if (isAddingThis) {
2503
+ return /* @__PURE__ */ jsx(
2504
+ BindingDisplay,
2505
+ {
2506
+ binding: "",
2507
+ isEditing: true,
2508
+ pendingKeys,
2509
+ activeKeys
2510
+ }
2511
+ );
2512
+ }
2513
+ return /* @__PURE__ */ jsx(
2514
+ "button",
2515
+ {
2516
+ className: "kbd-add-btn",
2517
+ onClick: () => {
2518
+ if (isRecording && !isAddingThis) {
2519
+ if (pendingKeys.length > 0) {
2520
+ const display = formatCombination(pendingKeys);
2521
+ const currentAddingAction = addingActionRef.current;
2522
+ const currentEditingAction = editingActionRef.current;
2523
+ const currentEditingKey = editingKeyRef.current;
2524
+ if (currentAddingAction) {
2525
+ handleBindingAdd?.(currentAddingAction, display.id);
2526
+ } else if (currentEditingAction && currentEditingKey) {
2527
+ handleBindingChange?.(currentEditingAction, currentEditingKey, display.id);
2528
+ }
2529
+ }
2530
+ cancel();
2531
+ }
2532
+ startAddingBinding(actionId);
2533
+ },
2534
+ children: "+"
2535
+ }
2536
+ );
2537
+ },
2538
+ [addingAction, pendingKeys, activeKeys, startAddingBinding, isRecording, cancel, handleBindingAdd, handleBindingChange]
2539
+ );
2540
+ const renderCell = useCallback(
2541
+ (actionId, keys) => {
2542
+ return /* @__PURE__ */ jsxs("span", { className: "kbd-action-bindings", children: [
2543
+ keys.map((key) => /* @__PURE__ */ jsx(Fragment$1, { children: renderEditableKbd(actionId, key, true) }, key)),
2544
+ editable && multipleBindings && renderAddButton(actionId)
2545
+ ] });
2546
+ },
2547
+ [renderEditableKbd, renderAddButton, editable, multipleBindings]
2548
+ );
2549
+ const groupRendererProps = useMemo(() => ({
2550
+ renderCell,
2551
+ renderEditableKbd,
2552
+ renderAddButton,
2553
+ startEditing: startEditingBinding,
2554
+ startAdding: startAddingBinding,
2555
+ removeBinding,
2556
+ isRecording,
2557
+ editingAction,
2558
+ editingKey,
2559
+ addingAction
2560
+ }), [renderCell, renderEditableKbd, renderAddButton, startEditingBinding, startAddingBinding, removeBinding, isRecording, editingAction, editingKey, addingAction]);
2561
+ const modalKeymap = shouldAutoRegisterOpen ? { [openKey]: "openShortcuts" } : {};
2562
+ useHotkeys(
2563
+ { ...modalKeymap, escape: "closeShortcuts" },
2564
+ {
2565
+ openShortcuts: open,
2566
+ closeShortcuts: close
2567
+ },
2568
+ { enabled: shouldAutoRegisterOpen || isOpen }
2569
+ );
2570
+ useEffect(() => {
2571
+ if (!isOpen || !editingAction && !addingAction) return;
2572
+ const handleEscape = (e) => {
2573
+ if (e.key === "Escape") {
2574
+ e.preventDefault();
2575
+ e.stopPropagation();
2576
+ cancelEditing();
2577
+ }
2578
+ };
2579
+ window.addEventListener("keydown", handleEscape, true);
2580
+ return () => window.removeEventListener("keydown", handleEscape, true);
2581
+ }, [isOpen, editingAction, addingAction, cancelEditing]);
2582
+ const handleBackdropClick = useCallback(
2583
+ (e) => {
2584
+ if (e.target === e.currentTarget) {
2585
+ close();
2586
+ }
2587
+ },
2588
+ [close]
2589
+ );
2590
+ const handleModalClick = useCallback(
2591
+ (e) => {
2592
+ if (!editingAction && !addingAction) return;
2593
+ const target = e.target;
2594
+ if (target.closest(".kbd-kbd.editing")) return;
2595
+ if (target.closest(".kbd-kbd.editable")) return;
2596
+ if (target.closest(".kbd-add-btn")) return;
2597
+ cancelEditing();
2598
+ },
2599
+ [editingAction, addingAction, cancelEditing]
2600
+ );
2601
+ const shortcutGroups = useMemo(
2602
+ () => organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder),
2603
+ [keymap, labels, descriptions, groupNames, groupOrder]
2604
+ );
2605
+ if (!isOpen) return null;
2606
+ if (children) {
2607
+ return /* @__PURE__ */ jsx(Fragment, { children: children({
2608
+ groups: shortcutGroups,
2609
+ close,
2610
+ editable,
2611
+ editingAction,
2612
+ editingBindingIndex: null,
2613
+ // deprecated, use editingKey
2614
+ pendingKeys,
2615
+ activeKeys,
2616
+ conflicts,
2617
+ startEditing,
2618
+ cancelEditing,
2619
+ removeBinding,
2620
+ reset
2621
+ }) });
2622
+ }
2623
+ const renderGroup = (group) => {
2624
+ const customRenderer = groupRenderers?.[group.name];
2625
+ if (customRenderer) {
2626
+ return customRenderer({ group, ...groupRendererProps });
2627
+ }
2628
+ return group.shortcuts.map(({ actionId, label, description, bindings }) => /* @__PURE__ */ jsxs("div", { className: "kbd-action", children: [
2629
+ /* @__PURE__ */ jsx("span", { className: "kbd-action-label", title: description, children: label }),
2630
+ renderCell(actionId, bindings)
2631
+ ] }, actionId));
2632
+ };
2633
+ return /* @__PURE__ */ jsx("div", { className: backdropClassName, onClick: handleBackdropClick, children: /* @__PURE__ */ jsxs("div", { className: modalClassName, role: "dialog", "aria-modal": "true", "aria-label": "Keyboard shortcuts", onClick: handleModalClick, children: [
2634
+ /* @__PURE__ */ jsxs("div", { className: "kbd-modal-header", children: [
2635
+ /* @__PURE__ */ jsx("h2", { className: "kbd-modal-title", children: title }),
2636
+ /* @__PURE__ */ jsxs("div", { className: "kbd-modal-header-buttons", children: [
2637
+ editable && handleReset && /* @__PURE__ */ jsx("button", { className: "kbd-reset-btn", onClick: reset, children: "Reset" }),
2638
+ /* @__PURE__ */ jsx("button", { className: "kbd-modal-close", onClick: close, "aria-label": "Close", children: "\xD7" })
2639
+ ] })
2640
+ ] }),
2641
+ hint && /* @__PURE__ */ jsx("p", { className: "kbd-hint", children: hint }),
2642
+ shortcutGroups.map((group) => /* @__PURE__ */ jsxs("div", { className: "kbd-group", children: [
2643
+ /* @__PURE__ */ jsx("h3", { className: "kbd-group-title", children: group.name }),
2644
+ renderGroup(group)
2645
+ ] }, group.name)),
2646
+ pendingConflict && /* @__PURE__ */ jsxs("div", { className: "kbd-conflict-warning", style: {
2647
+ padding: "12px",
2648
+ marginTop: "16px",
2649
+ backgroundColor: "var(--kbd-warning-bg)",
2650
+ borderRadius: "var(--kbd-radius-sm)",
2651
+ border: "1px solid var(--kbd-warning)"
2652
+ }, children: [
2653
+ /* @__PURE__ */ jsxs("p", { style: { margin: "0 0 8px", color: "var(--kbd-warning)" }, children: [
2654
+ "This key is already bound to: ",
2655
+ pendingConflict.conflictsWith.join(", ")
2656
+ ] }),
2657
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px" }, children: [
2658
+ /* @__PURE__ */ jsx(
2659
+ "button",
2660
+ {
2661
+ onClick: () => {
2662
+ if (addingActionRef.current) {
2663
+ handleBindingAdd?.(pendingConflict.action, pendingConflict.key);
2664
+ } else if (editingKeyRef.current) {
2665
+ handleBindingChange?.(pendingConflict.action, editingKeyRef.current, pendingConflict.key);
2666
+ }
2667
+ editingActionRef.current = null;
2668
+ editingKeyRef.current = null;
2669
+ addingActionRef.current = null;
2670
+ setEditingAction(null);
2671
+ setEditingKey(null);
2672
+ setAddingAction(null);
2673
+ setPendingConflict(null);
2674
+ },
2675
+ style: {
2676
+ padding: "4px 12px",
2677
+ backgroundColor: "var(--kbd-accent)",
2678
+ color: "white",
2679
+ border: "none",
2680
+ borderRadius: "var(--kbd-radius-sm)",
2681
+ cursor: "pointer"
2682
+ },
2683
+ children: "Override"
2684
+ }
2685
+ ),
2686
+ /* @__PURE__ */ jsx(
2687
+ "button",
2688
+ {
2689
+ onClick: cancelEditing,
2690
+ style: {
2691
+ padding: "4px 12px",
2692
+ backgroundColor: "var(--kbd-bg-secondary)",
2693
+ border: "1px solid var(--kbd-border)",
2694
+ borderRadius: "var(--kbd-radius-sm)",
2695
+ cursor: "pointer"
2696
+ },
2697
+ children: "Cancel"
2698
+ }
2699
+ )
2700
+ ] })
2701
+ ] })
2702
+ ] }) });
2703
+ }
2704
+
2705
+ export { ActionsRegistryContext, AltIcon, CommandIcon, CtrlIcon, HotkeysProvider, KeybindingEditor, ModifierIcon, Omnibar, OptIcon, SequenceModal, ShiftIcon, ShortcutsModal, 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 };
2706
+ //# sourceMappingURL=index.js.map
2707
+ //# sourceMappingURL=index.js.map