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