use-kbd 0.3.0 → 0.4.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 CHANGED
@@ -2,9 +2,19 @@
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var react = require('react');
5
- var base = require('@rdub/base');
6
5
 
7
- // src/TwoColumnRenderer.tsx
6
+ // src/types.ts
7
+ function extractCaptures(state) {
8
+ return state.filter(
9
+ (e) => (e.type === "digit" || e.type === "digits") && e.value !== void 0
10
+ ).map((e) => e.value);
11
+ }
12
+ function isDigitPlaceholder(elem) {
13
+ return elem.type === "digit" || elem.type === "digits";
14
+ }
15
+ function countPlaceholders(seq) {
16
+ return seq.filter(isDigitPlaceholder).length;
17
+ }
8
18
  function createTwoColumnRenderer(config) {
9
19
  const { headers, getRows } = config;
10
20
  const [labelHeader, leftHeader, rightHeader] = headers;
@@ -187,34 +197,55 @@ function useActionsRegistry(options = {}) {
187
197
  }
188
198
  return bindings;
189
199
  }, [keymap]);
200
+ const getFirstBindingForAction = react.useCallback((actionId) => {
201
+ return getBindingsForAction(actionId)[0];
202
+ }, [getBindingsForAction]);
190
203
  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
+ if (isDefaultBinding(key, actionId)) {
204
205
  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];
206
+ const existing = prev[actionId] ?? [];
207
+ if (existing.includes(key)) {
208
+ const filtered = existing.filter((k) => k !== key);
209
+ if (filtered.length === 0) {
210
+ const { [actionId]: _, ...rest } = prev;
211
+ return rest;
210
212
  }
213
+ return { ...prev, [actionId]: filtered };
211
214
  }
212
- return next;
215
+ return prev;
216
+ });
217
+ } else {
218
+ updateOverrides((prev) => ({
219
+ ...prev,
220
+ [key]: actionId
221
+ }));
222
+ }
223
+ }, [updateOverrides, updateRemovedDefaults, isDefaultBinding]);
224
+ const removeBinding = react.useCallback((actionId, key) => {
225
+ const action = actionsRef.current.get(actionId);
226
+ const isDefault = action?.config.defaultBindings?.includes(key);
227
+ if (isDefault) {
228
+ updateRemovedDefaults((prev) => {
229
+ const existing = prev[actionId] ?? [];
230
+ if (existing.includes(key)) return prev;
231
+ return { ...prev, [actionId]: [...existing, key] };
213
232
  });
214
233
  }
215
234
  updateOverrides((prev) => {
216
- const { [key]: _, ...rest } = prev;
217
- return rest;
235
+ const boundAction = prev[key];
236
+ if (boundAction === actionId) {
237
+ const { [key]: _, ...rest } = prev;
238
+ return rest;
239
+ }
240
+ if (Array.isArray(boundAction) && boundAction.includes(actionId)) {
241
+ const newActions = boundAction.filter((a) => a !== actionId);
242
+ if (newActions.length === 0) {
243
+ const { [key]: _, ...rest } = prev;
244
+ return rest;
245
+ }
246
+ return { ...prev, [key]: newActions.length === 1 ? newActions[0] : newActions };
247
+ }
248
+ return prev;
218
249
  });
219
250
  }, [updateOverrides, updateRemovedDefaults]);
220
251
  const resetOverrides = react.useCallback(() => {
@@ -232,6 +263,7 @@ function useActionsRegistry(options = {}) {
232
263
  keymap,
233
264
  actionRegistry,
234
265
  getBindingsForAction,
266
+ getFirstBindingForAction,
235
267
  overrides,
236
268
  setBinding,
237
269
  removeBinding,
@@ -244,12 +276,48 @@ function useActionsRegistry(options = {}) {
244
276
  keymap,
245
277
  actionRegistry,
246
278
  getBindingsForAction,
279
+ getFirstBindingForAction,
247
280
  overrides,
248
281
  setBinding,
249
282
  removeBinding,
250
283
  resetOverrides
251
284
  ]);
252
285
  }
286
+
287
+ // src/constants.ts
288
+ var DEFAULT_SEQUENCE_TIMEOUT = Infinity;
289
+ var ACTION_MODAL = "__hotkeys:modal";
290
+ var ACTION_OMNIBAR = "__hotkeys:omnibar";
291
+ var ACTION_LOOKUP = "__hotkeys:lookup";
292
+
293
+ // src/utils.ts
294
+ var { max } = Math;
295
+ var SHIFTED_SYMBOLS = /* @__PURE__ */ new Set([
296
+ "!",
297
+ "@",
298
+ "#",
299
+ "$",
300
+ "%",
301
+ "^",
302
+ "&",
303
+ "*",
304
+ "(",
305
+ ")",
306
+ "_",
307
+ "+",
308
+ "{",
309
+ "}",
310
+ "|",
311
+ ":",
312
+ '"',
313
+ "<",
314
+ ">",
315
+ "?",
316
+ "~"
317
+ ]);
318
+ function isShiftedSymbol(key) {
319
+ return SHIFTED_SYMBOLS.has(key);
320
+ }
253
321
  function isMac() {
254
322
  if (typeof navigator === "undefined") return false;
255
323
  return /Mac|iPod|iPhone|iPad/.test(navigator.platform);
@@ -310,7 +378,18 @@ function formatKeyForDisplay(key) {
310
378
  }
311
379
  return key;
312
380
  }
381
+ var DIGIT_PLACEHOLDER = "__DIGIT__";
382
+ var DIGITS_PLACEHOLDER = "__DIGITS__";
383
+ function isPlaceholderSentinel(key) {
384
+ return key === DIGIT_PLACEHOLDER || key === DIGITS_PLACEHOLDER;
385
+ }
313
386
  function formatSingleCombination(combo) {
387
+ if (combo.key === DIGIT_PLACEHOLDER) {
388
+ return { display: "#", id: "\\d" };
389
+ }
390
+ if (combo.key === DIGITS_PLACEHOLDER) {
391
+ return { display: "##", id: "\\d+" };
392
+ }
314
393
  const mac = isMac();
315
394
  const parts = [];
316
395
  const idParts = [];
@@ -356,6 +435,10 @@ function formatCombination(input) {
356
435
  const single = formatSingleCombination(input);
357
436
  return { ...single, isSequence: false };
358
437
  }
438
+ function formatBinding(binding) {
439
+ const parsed = parseHotkeyString(binding);
440
+ return formatCombination(parsed).display;
441
+ }
359
442
  function isModifierKey(key) {
360
443
  return ["Control", "Alt", "Shift", "Meta"].includes(key);
361
444
  }
@@ -419,6 +502,111 @@ function parseCombinationId(id) {
419
502
  }
420
503
  return sequence[0];
421
504
  }
505
+ var NO_MODIFIERS = { ctrl: false, alt: false, shift: false, meta: false };
506
+ function parseSeqElem(str) {
507
+ if (str === "\\d") {
508
+ return { type: "digit" };
509
+ }
510
+ if (str === "\\d+") {
511
+ return { type: "digits" };
512
+ }
513
+ if (str.length === 1 && /^[A-Z]$/.test(str)) {
514
+ return {
515
+ type: "key",
516
+ key: str.toLowerCase(),
517
+ modifiers: { ctrl: false, alt: false, shift: true, meta: false }
518
+ };
519
+ }
520
+ const parts = str.toLowerCase().split("+");
521
+ const key = parts[parts.length - 1];
522
+ return {
523
+ type: "key",
524
+ key,
525
+ modifiers: {
526
+ ctrl: parts.includes("ctrl") || parts.includes("control"),
527
+ alt: parts.includes("alt") || parts.includes("option"),
528
+ shift: parts.includes("shift"),
529
+ meta: parts.includes("meta") || parts.includes("cmd") || parts.includes("command")
530
+ }
531
+ };
532
+ }
533
+ function parseKeySeq(hotkeyStr) {
534
+ if (!hotkeyStr.trim()) return [];
535
+ const parts = hotkeyStr.trim().split(/\s+/);
536
+ return parts.map(parseSeqElem);
537
+ }
538
+ function formatSeqElem(elem) {
539
+ if (elem.type === "digit") {
540
+ return { display: "\u27E8#\u27E9", id: "\\d" };
541
+ }
542
+ if (elem.type === "digits") {
543
+ return { display: "\u27E8##\u27E9", id: "\\d+" };
544
+ }
545
+ const mac = isMac();
546
+ const parts = [];
547
+ const idParts = [];
548
+ if (elem.modifiers.ctrl) {
549
+ parts.push(mac ? "\u2303" : "Ctrl");
550
+ idParts.push("ctrl");
551
+ }
552
+ if (elem.modifiers.meta) {
553
+ parts.push(mac ? "\u2318" : "Win");
554
+ idParts.push("meta");
555
+ }
556
+ if (elem.modifiers.alt) {
557
+ parts.push(mac ? "\u2325" : "Alt");
558
+ idParts.push("alt");
559
+ }
560
+ if (elem.modifiers.shift) {
561
+ parts.push(mac ? "\u21E7" : "Shift");
562
+ idParts.push("shift");
563
+ }
564
+ parts.push(formatKeyForDisplay(elem.key));
565
+ idParts.push(elem.key);
566
+ return {
567
+ display: mac ? parts.join("") : parts.join("+"),
568
+ id: idParts.join("+")
569
+ };
570
+ }
571
+ function formatKeySeq(seq) {
572
+ if (seq.length === 0) {
573
+ return { display: "", id: "", isSequence: false };
574
+ }
575
+ const formatted = seq.map(formatSeqElem);
576
+ if (seq.length === 1) {
577
+ return { ...formatted[0], isSequence: false };
578
+ }
579
+ return {
580
+ display: formatted.map((f) => f.display).join(" "),
581
+ id: formatted.map((f) => f.id).join(" "),
582
+ isSequence: true
583
+ };
584
+ }
585
+ function hasDigitPlaceholders(seq) {
586
+ return seq.some((elem) => elem.type === "digit" || elem.type === "digits");
587
+ }
588
+ function keySeqToHotkeySequence(seq) {
589
+ return seq.map((elem) => {
590
+ if (elem.type === "digit") {
591
+ return { key: "\\d", modifiers: NO_MODIFIERS };
592
+ }
593
+ if (elem.type === "digits") {
594
+ return { key: "\\d+", modifiers: NO_MODIFIERS };
595
+ }
596
+ return { key: elem.key, modifiers: elem.modifiers };
597
+ });
598
+ }
599
+ function hotkeySequenceToKeySeq(seq) {
600
+ return seq.map((combo) => {
601
+ if (combo.key === "\\d" && !combo.modifiers.ctrl && !combo.modifiers.alt && !combo.modifiers.shift && !combo.modifiers.meta) {
602
+ return { type: "digit" };
603
+ }
604
+ if (combo.key === "\\d+" && !combo.modifiers.ctrl && !combo.modifiers.alt && !combo.modifiers.shift && !combo.modifiers.meta) {
605
+ return { type: "digits" };
606
+ }
607
+ return { type: "key", key: combo.key, modifiers: combo.modifiers };
608
+ });
609
+ }
422
610
  function isPrefix(a, b) {
423
611
  if (a.length >= b.length) return false;
424
612
  for (let i = 0; i < a.length; i++) {
@@ -429,11 +617,43 @@ function isPrefix(a, b) {
429
617
  function combinationsEqual(a, b) {
430
618
  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
619
  }
620
+ function isDigitKey(key) {
621
+ return /^[0-9]$/.test(key);
622
+ }
623
+ function seqElemsCouldConflict(a, b) {
624
+ if (a.type === "digit" && b.type === "digit") return true;
625
+ if (a.type === "digit" && b.type === "key" && isDigitKey(b.key)) return true;
626
+ if (a.type === "key" && isDigitKey(a.key) && b.type === "digit") return true;
627
+ if (a.type === "digits" && b.type === "digits") return true;
628
+ if (a.type === "digits" && b.type === "digit") return true;
629
+ if (a.type === "digit" && b.type === "digits") return true;
630
+ if (a.type === "digits" && b.type === "key" && isDigitKey(b.key)) return true;
631
+ if (a.type === "key" && isDigitKey(a.key) && b.type === "digits") return true;
632
+ if (a.type === "key" && b.type === "key") {
633
+ 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;
634
+ }
635
+ return false;
636
+ }
637
+ function keySeqIsPrefix(a, b) {
638
+ if (a.length >= b.length) return false;
639
+ for (let i = 0; i < a.length; i++) {
640
+ if (!seqElemsCouldConflict(a[i], b[i])) return false;
641
+ }
642
+ return true;
643
+ }
644
+ function keySeqsCouldConflict(a, b) {
645
+ if (a.length !== b.length) return false;
646
+ for (let i = 0; i < a.length; i++) {
647
+ if (!seqElemsCouldConflict(a[i], b[i])) return false;
648
+ }
649
+ return true;
650
+ }
432
651
  function findConflicts(keymap) {
433
652
  const conflicts = /* @__PURE__ */ new Map();
434
653
  const entries = Object.entries(keymap).map(([key, actionOrActions]) => ({
435
654
  key,
436
655
  sequence: parseHotkeyString(key),
656
+ keySeq: parseKeySeq(key),
437
657
  actions: Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
438
658
  }));
439
659
  const keyToActions = /* @__PURE__ */ new Map();
@@ -450,7 +670,36 @@ function findConflicts(keymap) {
450
670
  for (let j = i + 1; j < entries.length; j++) {
451
671
  const a = entries[i];
452
672
  const b = entries[j];
453
- if (isPrefix(a.sequence, b.sequence)) {
673
+ if (keySeqsCouldConflict(a.keySeq, b.keySeq) && a.key !== b.key) {
674
+ const existingA = conflicts.get(a.key) ?? [];
675
+ if (!existingA.includes(`conflicts with: ${b.key}`)) {
676
+ conflicts.set(a.key, [...existingA, ...a.actions, `conflicts with: ${b.key}`]);
677
+ }
678
+ const existingB = conflicts.get(b.key) ?? [];
679
+ if (!existingB.includes(`conflicts with: ${a.key}`)) {
680
+ conflicts.set(b.key, [...existingB, ...b.actions, `conflicts with: ${a.key}`]);
681
+ }
682
+ continue;
683
+ }
684
+ if (keySeqIsPrefix(a.keySeq, b.keySeq)) {
685
+ const existingA = conflicts.get(a.key) ?? [];
686
+ if (!existingA.includes(`prefix of: ${b.key}`)) {
687
+ conflicts.set(a.key, [...existingA, ...a.actions, `prefix of: ${b.key}`]);
688
+ }
689
+ const existingB = conflicts.get(b.key) ?? [];
690
+ if (!existingB.includes(`has prefix: ${a.key}`)) {
691
+ conflicts.set(b.key, [...existingB, ...b.actions, `has prefix: ${a.key}`]);
692
+ }
693
+ } else if (keySeqIsPrefix(b.keySeq, a.keySeq)) {
694
+ const existingB = conflicts.get(b.key) ?? [];
695
+ if (!existingB.includes(`prefix of: ${a.key}`)) {
696
+ conflicts.set(b.key, [...existingB, ...b.actions, `prefix of: ${a.key}`]);
697
+ }
698
+ const existingA = conflicts.get(a.key) ?? [];
699
+ if (!existingA.includes(`has prefix: ${b.key}`)) {
700
+ conflicts.set(a.key, [...existingA, ...a.actions, `has prefix: ${b.key}`]);
701
+ }
702
+ } else if (isPrefix(a.sequence, b.sequence)) {
454
703
  const existingA = conflicts.get(a.key) ?? [];
455
704
  if (!existingA.includes(`prefix of: ${b.key}`)) {
456
705
  conflicts.set(a.key, [...existingA, ...a.actions, `prefix of: ${b.key}`]);
@@ -489,6 +738,7 @@ function getSequenceCompletions(pendingKeys, keymap) {
489
738
  const completions = [];
490
739
  for (const [hotkeyStr, actionOrActions] of Object.entries(keymap)) {
491
740
  const sequence = parseHotkeyString(hotkeyStr);
741
+ const keySeq = parseKeySeq(hotkeyStr);
492
742
  if (sequence.length <= pendingKeys.length) continue;
493
743
  let isPrefix2 = true;
494
744
  for (let i = 0; i < pendingKeys.length; i++) {
@@ -498,13 +748,13 @@ function getSequenceCompletions(pendingKeys, keymap) {
498
748
  }
499
749
  }
500
750
  if (isPrefix2) {
501
- const remainingKeys = sequence.slice(pendingKeys.length);
502
- const nextKeys = formatCombination(remainingKeys).id;
751
+ const remainingKeySeq = keySeq.slice(pendingKeys.length);
752
+ const nextKeys = formatKeySeq(remainingKeySeq).display;
503
753
  const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
504
754
  completions.push({
505
755
  nextKeys,
506
756
  fullSequence: hotkeyStr,
507
- display: formatCombination(sequence),
757
+ display: formatKeySeq(keySeq),
508
758
  actions
509
759
  });
510
760
  }
@@ -593,7 +843,7 @@ function searchActions(query, actions, keymap) {
593
843
  for (const keyword of action.keywords) {
594
844
  const kwMatch = fuzzyMatch(query, keyword);
595
845
  if (kwMatch.matched) {
596
- keywordScore = base.max(keywordScore, kwMatch.score);
846
+ keywordScore = max(keywordScore, kwMatch.score);
597
847
  }
598
848
  }
599
849
  }
@@ -646,6 +896,92 @@ function sequencesMatch(a, b) {
646
896
  }
647
897
  return true;
648
898
  }
899
+ function isDigit(key) {
900
+ return /^[0-9]$/.test(key);
901
+ }
902
+ function initMatchState(seq) {
903
+ return seq.map((elem) => {
904
+ if (elem.type === "digit") return { type: "digit" };
905
+ if (elem.type === "digits") return { type: "digits" };
906
+ return { type: "key", key: elem.key, modifiers: elem.modifiers };
907
+ });
908
+ }
909
+ function matchesKeyElem(combo, elem) {
910
+ const shiftMatches = isShiftedChar(combo.key) ? elem.modifiers.shift ? combo.modifiers.shift : true : combo.modifiers.shift === elem.modifiers.shift;
911
+ return combo.modifiers.ctrl === elem.modifiers.ctrl && combo.modifiers.alt === elem.modifiers.alt && shiftMatches && combo.modifiers.meta === elem.modifiers.meta && combo.key === elem.key;
912
+ }
913
+ function advanceMatchState(state, pattern, combo) {
914
+ const newState = [...state];
915
+ let pos = 0;
916
+ for (let i = 0; i < state.length; i++) {
917
+ const elem = state[i];
918
+ if (elem.type === "key" && !elem.matched) break;
919
+ if (elem.type === "digit" && elem.value === void 0) break;
920
+ if (elem.type === "digits" && elem.value === void 0) {
921
+ if (!elem.partial) break;
922
+ if (isDigit(combo.key)) {
923
+ const newPartial = (elem.partial || "") + combo.key;
924
+ newState[i] = { type: "digits", partial: newPartial };
925
+ return { status: "partial", state: newState };
926
+ } else {
927
+ const digitValue = parseInt(elem.partial, 10);
928
+ newState[i] = { type: "digits", value: digitValue };
929
+ pos = i + 1;
930
+ break;
931
+ }
932
+ }
933
+ pos++;
934
+ }
935
+ if (pos >= pattern.length) {
936
+ return { status: "failed" };
937
+ }
938
+ const currentPattern = pattern[pos];
939
+ if (currentPattern.type === "digit") {
940
+ if (!isDigit(combo.key) || combo.modifiers.ctrl || combo.modifiers.alt || combo.modifiers.meta) {
941
+ return { status: "failed" };
942
+ }
943
+ newState[pos] = { type: "digit", value: parseInt(combo.key, 10) };
944
+ } else if (currentPattern.type === "digits") {
945
+ if (!isDigit(combo.key) || combo.modifiers.ctrl || combo.modifiers.alt || combo.modifiers.meta) {
946
+ return { status: "failed" };
947
+ }
948
+ newState[pos] = { type: "digits", partial: combo.key };
949
+ } else {
950
+ if (!matchesKeyElem(combo, currentPattern)) {
951
+ return { status: "failed" };
952
+ }
953
+ newState[pos] = { type: "key", key: currentPattern.key, modifiers: currentPattern.modifiers, matched: true };
954
+ }
955
+ const isComplete = newState.every((elem) => {
956
+ if (elem.type === "key") return elem.matched === true;
957
+ if (elem.type === "digit") return elem.value !== void 0;
958
+ if (elem.type === "digits") return elem.value !== void 0;
959
+ return false;
960
+ });
961
+ if (isComplete) {
962
+ const captures = newState.filter(
963
+ (e) => (e.type === "digit" || e.type === "digits") && e.value !== void 0
964
+ ).map((e) => e.value);
965
+ return { status: "matched", state: newState, captures };
966
+ }
967
+ return { status: "partial", state: newState };
968
+ }
969
+ function isCollectingDigits(state) {
970
+ return state.some((elem) => elem.type === "digits" && elem.partial !== void 0 && elem.value === void 0);
971
+ }
972
+ function finalizeDigits(state) {
973
+ return state.map((elem) => {
974
+ if (elem.type === "digits" && elem.partial !== void 0 && elem.value === void 0) {
975
+ return { type: "digits", value: parseInt(elem.partial, 10) };
976
+ }
977
+ return elem;
978
+ });
979
+ }
980
+ function extractMatchCaptures(state) {
981
+ return state.filter(
982
+ (e) => (e.type === "digit" || e.type === "digits") && e.value !== void 0
983
+ ).map((e) => e.value);
984
+ }
649
985
  function useHotkeys(keymap, handlers, options = {}) {
650
986
  const {
651
987
  enabled = true,
@@ -653,7 +989,7 @@ function useHotkeys(keymap, handlers, options = {}) {
653
989
  preventDefault = true,
654
990
  stopPropagation = true,
655
991
  enableOnFormTags = false,
656
- sequenceTimeout = 1e3,
992
+ sequenceTimeout = DEFAULT_SEQUENCE_TIMEOUT,
657
993
  onTimeout = "submit",
658
994
  onSequenceStart,
659
995
  onSequenceProgress,
@@ -669,11 +1005,13 @@ function useHotkeys(keymap, handlers, options = {}) {
669
1005
  const timeoutRef = react.useRef(null);
670
1006
  const pendingKeysRef = react.useRef([]);
671
1007
  pendingKeysRef.current = pendingKeys;
1008
+ const matchStatesRef = react.useRef(/* @__PURE__ */ new Map());
672
1009
  const parsedKeymapRef = react.useRef([]);
673
1010
  react.useEffect(() => {
674
1011
  parsedKeymapRef.current = Object.entries(keymap).map(([key, actionOrActions]) => ({
675
1012
  key,
676
1013
  sequence: parseHotkeyString(key),
1014
+ keySeq: parseKeySeq(key),
677
1015
  actions: Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
678
1016
  }));
679
1017
  }, [keymap]);
@@ -681,6 +1019,7 @@ function useHotkeys(keymap, handlers, options = {}) {
681
1019
  setPendingKeys([]);
682
1020
  setIsAwaitingSequence(false);
683
1021
  setTimeoutStartedAt(null);
1022
+ matchStatesRef.current.clear();
684
1023
  if (timeoutRef.current) {
685
1024
  clearTimeout(timeoutRef.current);
686
1025
  timeoutRef.current = null;
@@ -690,7 +1029,7 @@ function useHotkeys(keymap, handlers, options = {}) {
690
1029
  clearPending();
691
1030
  onSequenceCancel?.();
692
1031
  }, [clearPending, onSequenceCancel]);
693
- const tryExecute = react.useCallback((sequence, e) => {
1032
+ const tryExecute = react.useCallback((sequence, e, captures) => {
694
1033
  for (const entry of parsedKeymapRef.current) {
695
1034
  if (sequencesMatch(sequence, entry.sequence)) {
696
1035
  for (const action of entry.actions) {
@@ -702,7 +1041,27 @@ function useHotkeys(keymap, handlers, options = {}) {
702
1041
  if (stopPropagation) {
703
1042
  e.stopPropagation();
704
1043
  }
705
- handler(e);
1044
+ handler(e, captures);
1045
+ return true;
1046
+ }
1047
+ }
1048
+ }
1049
+ }
1050
+ return false;
1051
+ }, [preventDefault, stopPropagation]);
1052
+ const tryExecuteKeySeq = react.useCallback((matchKey, matchState, captures, e) => {
1053
+ for (const entry of parsedKeymapRef.current) {
1054
+ if (entry.key === matchKey) {
1055
+ for (const action of entry.actions) {
1056
+ const handler = handlersRef.current[action];
1057
+ if (handler) {
1058
+ if (preventDefault) {
1059
+ e.preventDefault();
1060
+ }
1061
+ if (stopPropagation) {
1062
+ e.stopPropagation();
1063
+ }
1064
+ handler(e, captures.length > 0 ? captures : void 0);
706
1065
  return true;
707
1066
  }
708
1067
  }
@@ -732,7 +1091,8 @@ function useHotkeys(keymap, handlers, options = {}) {
732
1091
  const handleKeyDown = (e) => {
733
1092
  if (!enableOnFormTags) {
734
1093
  const eventTarget = e.target;
735
- if (eventTarget instanceof HTMLInputElement || eventTarget instanceof HTMLTextAreaElement || eventTarget instanceof HTMLSelectElement || eventTarget.isContentEditable) {
1094
+ const isTextInput = eventTarget instanceof HTMLInputElement && ["text", "email", "password", "search", "tel", "url", "number", "date", "datetime-local", "month", "time", "week"].includes(eventTarget.type);
1095
+ if (isTextInput || eventTarget instanceof HTMLTextAreaElement || eventTarget instanceof HTMLSelectElement || eventTarget.isContentEditable) {
736
1096
  return;
737
1097
  }
738
1098
  }
@@ -759,6 +1119,77 @@ function useHotkeys(keymap, handlers, options = {}) {
759
1119
  }
760
1120
  const currentCombo = eventToCombination(e);
761
1121
  const newSequence = [...pendingKeysRef.current, currentCombo];
1122
+ let keySeqMatched = false;
1123
+ let keySeqPartial = false;
1124
+ const matchStates = matchStatesRef.current;
1125
+ const hasPartialMatches = matchStates.size > 0;
1126
+ for (const entry of parsedKeymapRef.current) {
1127
+ let state = matchStates.get(entry.key);
1128
+ if (hasPartialMatches && !state) {
1129
+ continue;
1130
+ }
1131
+ if (!state) {
1132
+ state = initMatchState(entry.keySeq);
1133
+ matchStates.set(entry.key, state);
1134
+ }
1135
+ const result = advanceMatchState(state, entry.keySeq, currentCombo);
1136
+ if (result.status === "matched") {
1137
+ if (tryExecuteKeySeq(entry.key, result.state, result.captures, e)) {
1138
+ clearPending();
1139
+ keySeqMatched = true;
1140
+ break;
1141
+ }
1142
+ } else if (result.status === "partial") {
1143
+ matchStates.set(entry.key, result.state);
1144
+ keySeqPartial = true;
1145
+ } else {
1146
+ matchStates.delete(entry.key);
1147
+ }
1148
+ }
1149
+ if (keySeqMatched) {
1150
+ return;
1151
+ }
1152
+ if (keySeqPartial) {
1153
+ setPendingKeys(newSequence);
1154
+ setIsAwaitingSequence(true);
1155
+ if (pendingKeysRef.current.length === 0) {
1156
+ onSequenceStart?.(newSequence);
1157
+ } else {
1158
+ onSequenceProgress?.(newSequence);
1159
+ }
1160
+ if (preventDefault) {
1161
+ e.preventDefault();
1162
+ }
1163
+ if (Number.isFinite(sequenceTimeout)) {
1164
+ setTimeoutStartedAt(Date.now());
1165
+ timeoutRef.current = setTimeout(() => {
1166
+ for (const [key, state] of matchStates.entries()) {
1167
+ if (isCollectingDigits(state)) {
1168
+ const finalizedState = finalizeDigits(state);
1169
+ const entry = parsedKeymapRef.current.find((e2) => e2.key === key);
1170
+ if (entry) {
1171
+ const isComplete = finalizedState.every((elem) => {
1172
+ if (elem.type === "key") return elem.matched === true;
1173
+ if (elem.type === "digit") return elem.value !== void 0;
1174
+ if (elem.type === "digits") return elem.value !== void 0;
1175
+ return false;
1176
+ });
1177
+ if (isComplete) {
1178
+ void extractMatchCaptures(finalizedState);
1179
+ }
1180
+ }
1181
+ }
1182
+ }
1183
+ setPendingKeys([]);
1184
+ setIsAwaitingSequence(false);
1185
+ setTimeoutStartedAt(null);
1186
+ matchStatesRef.current.clear();
1187
+ onSequenceCancel?.();
1188
+ timeoutRef.current = null;
1189
+ }, sequenceTimeout);
1190
+ }
1191
+ return;
1192
+ }
762
1193
  const exactMatch = tryExecute(newSequence, e);
763
1194
  if (exactMatch) {
764
1195
  clearPending();
@@ -773,25 +1204,27 @@ function useHotkeys(keymap, handlers, options = {}) {
773
1204
  } else {
774
1205
  onSequenceProgress?.(newSequence);
775
1206
  }
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);
1207
+ if (Number.isFinite(sequenceTimeout)) {
1208
+ setTimeoutStartedAt(Date.now());
1209
+ timeoutRef.current = setTimeout(() => {
1210
+ if (onTimeout === "submit") {
1211
+ setPendingKeys((current) => {
1212
+ if (current.length > 0) {
1213
+ onSequenceCancel?.();
1214
+ }
1215
+ return [];
1216
+ });
1217
+ setIsAwaitingSequence(false);
1218
+ setTimeoutStartedAt(null);
1219
+ } else {
1220
+ setPendingKeys([]);
1221
+ setIsAwaitingSequence(false);
1222
+ setTimeoutStartedAt(null);
1223
+ onSequenceCancel?.();
1224
+ }
1225
+ timeoutRef.current = null;
1226
+ }, sequenceTimeout);
1227
+ }
795
1228
  if (preventDefault) {
796
1229
  e.preventDefault();
797
1230
  }
@@ -811,21 +1244,23 @@ function useHotkeys(keymap, handlers, options = {}) {
811
1244
  if (preventDefault) {
812
1245
  e.preventDefault();
813
1246
  }
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);
1247
+ if (Number.isFinite(sequenceTimeout)) {
1248
+ setTimeoutStartedAt(Date.now());
1249
+ timeoutRef.current = setTimeout(() => {
1250
+ if (onTimeout === "submit") {
1251
+ setPendingKeys([]);
1252
+ setIsAwaitingSequence(false);
1253
+ setTimeoutStartedAt(null);
1254
+ onSequenceCancel?.();
1255
+ } else {
1256
+ setPendingKeys([]);
1257
+ setIsAwaitingSequence(false);
1258
+ setTimeoutStartedAt(null);
1259
+ onSequenceCancel?.();
1260
+ }
1261
+ timeoutRef.current = null;
1262
+ }, sequenceTimeout);
1263
+ }
829
1264
  }
830
1265
  }
831
1266
  };
@@ -847,6 +1282,7 @@ function useHotkeys(keymap, handlers, options = {}) {
847
1282
  clearPending,
848
1283
  cancelSequence,
849
1284
  tryExecute,
1285
+ tryExecuteKeySeq,
850
1286
  hasPotentialMatch,
851
1287
  hasSequenceExtension,
852
1288
  onSequenceStart,
@@ -858,12 +1294,10 @@ function useHotkeys(keymap, handlers, options = {}) {
858
1294
  var HotkeysContext = react.createContext(null);
859
1295
  var DEFAULT_CONFIG = {
860
1296
  storageKey: "use-kbd",
861
- sequenceTimeout: 1e3,
1297
+ sequenceTimeout: DEFAULT_SEQUENCE_TIMEOUT,
862
1298
  disableConflicts: true,
863
1299
  minViewportWidth: 768,
864
- enableOnTouch: false,
865
- modalTrigger: "?",
866
- omnibarTrigger: "meta+k"
1300
+ enableOnTouch: false
867
1301
  };
868
1302
  function HotkeysProvider({
869
1303
  config: configProp = {},
@@ -912,16 +1346,12 @@ function HotkeysProvider({
912
1346
  const openOmnibar = react.useCallback(() => setIsOmnibarOpen(true), []);
913
1347
  const closeOmnibar = react.useCallback(() => setIsOmnibarOpen(false), []);
914
1348
  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]);
1349
+ const [isLookupOpen, setIsLookupOpen] = react.useState(false);
1350
+ const openLookup = react.useCallback(() => setIsLookupOpen(true), []);
1351
+ const closeLookup = react.useCallback(() => setIsLookupOpen(false), []);
1352
+ const toggleLookup = react.useCallback(() => setIsLookupOpen((prev) => !prev), []);
1353
+ const [isEditingBinding, setIsEditingBinding] = react.useState(false);
1354
+ const keymap = registry.keymap;
925
1355
  const conflicts = react.useMemo(() => findConflicts(keymap), [keymap]);
926
1356
  const hasConflicts2 = conflicts.size > 0;
927
1357
  const effectiveKeymap = react.useMemo(() => {
@@ -941,20 +1371,24 @@ function HotkeysProvider({
941
1371
  for (const [id, action] of registry.actions) {
942
1372
  map[id] = action.config.handler;
943
1373
  }
944
- map["__hotkeys:modal"] = toggleModal;
945
- map["__hotkeys:omnibar"] = toggleOmnibar;
946
1374
  return map;
947
- }, [registry.actions, toggleModal, toggleOmnibar]);
948
- const hotkeysEnabled = isEnabled && !isModalOpen && !isOmnibarOpen;
1375
+ }, [registry.actions]);
1376
+ const hotkeysEnabled = isEnabled && !isEditingBinding && !isOmnibarOpen && !isLookupOpen;
949
1377
  const {
950
1378
  pendingKeys,
951
1379
  isAwaitingSequence,
1380
+ cancelSequence,
952
1381
  timeoutStartedAt: sequenceTimeoutStartedAt,
953
1382
  sequenceTimeout
954
1383
  } = useHotkeys(effectiveKeymap, handlers, {
955
1384
  enabled: hotkeysEnabled,
956
1385
  sequenceTimeout: config.sequenceTimeout
957
1386
  });
1387
+ react.useEffect(() => {
1388
+ if (isAwaitingSequence && isModalOpen) {
1389
+ closeModal();
1390
+ }
1391
+ }, [isAwaitingSequence, isModalOpen, closeModal]);
958
1392
  const searchActionsHelper = react.useCallback(
959
1393
  (query) => searchActions(query, registry.actionRegistry, keymap),
960
1394
  [registry.actionRegistry, keymap]
@@ -974,9 +1408,16 @@ function HotkeysProvider({
974
1408
  openOmnibar,
975
1409
  closeOmnibar,
976
1410
  toggleOmnibar,
1411
+ isLookupOpen,
1412
+ openLookup,
1413
+ closeLookup,
1414
+ toggleLookup,
1415
+ isEditingBinding,
1416
+ setIsEditingBinding,
977
1417
  executeAction: registry.execute,
978
1418
  pendingKeys,
979
1419
  isAwaitingSequence,
1420
+ cancelSequence,
980
1421
  sequenceTimeoutStartedAt,
981
1422
  sequenceTimeout,
982
1423
  conflicts,
@@ -994,8 +1435,14 @@ function HotkeysProvider({
994
1435
  openOmnibar,
995
1436
  closeOmnibar,
996
1437
  toggleOmnibar,
1438
+ isLookupOpen,
1439
+ openLookup,
1440
+ closeLookup,
1441
+ toggleLookup,
1442
+ isEditingBinding,
997
1443
  pendingKeys,
998
1444
  isAwaitingSequence,
1445
+ cancelSequence,
999
1446
  sequenceTimeoutStartedAt,
1000
1447
  sequenceTimeout,
1001
1448
  conflicts,
@@ -1029,9 +1476,9 @@ function useAction(id, config) {
1029
1476
  react.useEffect(() => {
1030
1477
  registryRef.current.register(id, {
1031
1478
  ...config,
1032
- handler: () => {
1479
+ handler: (e, captures) => {
1033
1480
  if (enabledRef.current) {
1034
- handlerRef.current();
1481
+ handlerRef.current(e, captures);
1035
1482
  }
1036
1483
  }
1037
1484
  });
@@ -1065,9 +1512,9 @@ function useActions(actions) {
1065
1512
  for (const [id, config] of Object.entries(actions)) {
1066
1513
  registryRef.current.register(id, {
1067
1514
  ...config,
1068
- handler: () => {
1515
+ handler: (e, captures) => {
1069
1516
  if (enabledRef.current[id]) {
1070
- handlersRef.current[id]?.();
1517
+ handlersRef.current[id]?.(e, captures);
1071
1518
  }
1072
1519
  }
1073
1520
  });
@@ -1103,7 +1550,7 @@ function useRecordHotkey(options = {}) {
1103
1550
  onTab: onTabProp,
1104
1551
  onShiftTab: onShiftTabProp,
1105
1552
  preventDefault = true,
1106
- sequenceTimeout = 1e3,
1553
+ sequenceTimeout = DEFAULT_SEQUENCE_TIMEOUT,
1107
1554
  pauseTimeout = false
1108
1555
  } = options;
1109
1556
  const onCapture = useEventCallback(onCaptureProp);
@@ -1121,6 +1568,7 @@ function useRecordHotkey(options = {}) {
1121
1568
  const pauseTimeoutRef = react.useRef(pauseTimeout);
1122
1569
  pauseTimeoutRef.current = pauseTimeout;
1123
1570
  const pendingKeysRef = react.useRef([]);
1571
+ const hashCycleRef = react.useRef(0);
1124
1572
  const clearTimeout_ = react.useCallback(() => {
1125
1573
  if (timeoutRef.current) {
1126
1574
  clearTimeout(timeoutRef.current);
@@ -1150,6 +1598,7 @@ function useRecordHotkey(options = {}) {
1150
1598
  pressedKeysRef.current.clear();
1151
1599
  hasNonModifierRef.current = false;
1152
1600
  currentComboRef.current = null;
1601
+ hashCycleRef.current = 0;
1153
1602
  onCancel?.();
1154
1603
  }, [clearTimeout_, onCancel]);
1155
1604
  const commit = react.useCallback(() => {
@@ -1170,6 +1619,7 @@ function useRecordHotkey(options = {}) {
1170
1619
  pressedKeysRef.current.clear();
1171
1620
  hasNonModifierRef.current = false;
1172
1621
  currentComboRef.current = null;
1622
+ hashCycleRef.current = 0;
1173
1623
  return cancel;
1174
1624
  }, [cancel, clearTimeout_]);
1175
1625
  react.useEffect(() => {
@@ -1180,9 +1630,13 @@ function useRecordHotkey(options = {}) {
1180
1630
  }
1181
1631
  } else if (isRecording && pendingKeysRef.current.length > 0 && !timeoutRef.current) {
1182
1632
  const currentSequence = pendingKeysRef.current;
1183
- timeoutRef.current = setTimeout(() => {
1633
+ if (sequenceTimeout === 0) {
1184
1634
  submit(currentSequence);
1185
- }, sequenceTimeout);
1635
+ } else if (Number.isFinite(sequenceTimeout)) {
1636
+ timeoutRef.current = setTimeout(() => {
1637
+ submit(currentSequence);
1638
+ }, sequenceTimeout);
1639
+ }
1186
1640
  }
1187
1641
  }, [pauseTimeout, isRecording, sequenceTimeout, submit]);
1188
1642
  react.useEffect(() => {
@@ -1241,22 +1695,23 @@ function useRecordHotkey(options = {}) {
1241
1695
  key = e.code.slice(5);
1242
1696
  }
1243
1697
  pressedKeysRef.current.add(key);
1698
+ let nonModifierKey = "";
1699
+ for (const k of pressedKeysRef.current) {
1700
+ if (!isModifierKey(k)) {
1701
+ nonModifierKey = normalizeKey(k);
1702
+ hasNonModifierRef.current = true;
1703
+ break;
1704
+ }
1705
+ }
1244
1706
  const combo = {
1245
- key: "",
1707
+ key: nonModifierKey,
1246
1708
  modifiers: {
1247
1709
  ctrl: e.ctrlKey,
1248
1710
  alt: e.altKey,
1249
- shift: e.shiftKey,
1711
+ shift: e.shiftKey && !isShiftedSymbol(nonModifierKey),
1250
1712
  meta: e.metaKey
1251
1713
  }
1252
1714
  };
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
1715
  if (combo.key) {
1261
1716
  currentComboRef.current = combo;
1262
1717
  setActiveKeys(combo);
@@ -1281,16 +1736,41 @@ function useRecordHotkey(options = {}) {
1281
1736
  pressedKeysRef.current.delete(key);
1282
1737
  const shouldComplete = pressedKeysRef.current.size === 0 || e.key === "Meta" && hasNonModifierRef.current;
1283
1738
  if (shouldComplete && hasNonModifierRef.current && currentComboRef.current) {
1284
- const combo = currentComboRef.current;
1739
+ let combo = currentComboRef.current;
1285
1740
  pressedKeysRef.current.clear();
1286
1741
  hasNonModifierRef.current = false;
1287
1742
  currentComboRef.current = null;
1288
1743
  setActiveKeys(null);
1289
- const newSequence = [...pendingKeysRef.current, combo];
1744
+ let newSequence;
1745
+ const noModifiers = !combo.modifiers.ctrl && !combo.modifiers.alt && !combo.modifiers.meta && !combo.modifiers.shift;
1746
+ if (combo.key === "#" && noModifiers) {
1747
+ const pending = pendingKeysRef.current;
1748
+ const lastCombo = pending[pending.length - 1];
1749
+ if (hashCycleRef.current === 0) {
1750
+ combo = { key: DIGIT_PLACEHOLDER, modifiers: { ctrl: false, alt: false, shift: false, meta: false } };
1751
+ newSequence = [...pending, combo];
1752
+ hashCycleRef.current = 1;
1753
+ } else if (hashCycleRef.current === 1 && lastCombo?.key === DIGIT_PLACEHOLDER) {
1754
+ newSequence = [...pending.slice(0, -1), { key: DIGITS_PLACEHOLDER, modifiers: { ctrl: false, alt: false, shift: false, meta: false } }];
1755
+ hashCycleRef.current = 2;
1756
+ } else if (hashCycleRef.current === 2 && lastCombo?.key === DIGITS_PLACEHOLDER) {
1757
+ newSequence = [...pending.slice(0, -1), { key: "#", modifiers: { ctrl: false, alt: false, shift: false, meta: false } }];
1758
+ hashCycleRef.current = 3;
1759
+ } else {
1760
+ combo = { key: DIGIT_PLACEHOLDER, modifiers: { ctrl: false, alt: false, shift: false, meta: false } };
1761
+ newSequence = [...pending, combo];
1762
+ hashCycleRef.current = 1;
1763
+ }
1764
+ } else {
1765
+ hashCycleRef.current = 0;
1766
+ newSequence = [...pendingKeysRef.current, combo];
1767
+ }
1290
1768
  pendingKeysRef.current = newSequence;
1291
1769
  setPendingKeys(newSequence);
1292
1770
  clearTimeout_();
1293
- if (!pauseTimeoutRef.current) {
1771
+ if (sequenceTimeout === 0) {
1772
+ submit(newSequence);
1773
+ } else if (!pauseTimeoutRef.current && Number.isFinite(sequenceTimeout)) {
1294
1774
  timeoutRef.current = setTimeout(() => {
1295
1775
  submit(newSequence);
1296
1776
  }, sequenceTimeout);
@@ -1316,6 +1796,7 @@ function useRecordHotkey(options = {}) {
1316
1796
  display,
1317
1797
  pendingKeys,
1318
1798
  activeKeys,
1799
+ sequenceTimeout,
1319
1800
  combination
1320
1801
  // deprecated
1321
1802
  };
@@ -1416,6 +1897,7 @@ function useEditableHotkeys(defaults, handlers, options = {}) {
1416
1897
  sequenceTimeout
1417
1898
  };
1418
1899
  }
1900
+ var { max: max2, min } = Math;
1419
1901
  function useOmnibar(options) {
1420
1902
  const {
1421
1903
  actions,
@@ -1492,10 +1974,10 @@ function useOmnibar(options) {
1492
1974
  });
1493
1975
  }, [onOpen, onClose]);
1494
1976
  const selectNext = react.useCallback(() => {
1495
- setSelectedIndex((prev) => base.min(prev + 1, results.length - 1));
1977
+ setSelectedIndex((prev) => min(prev + 1, results.length - 1));
1496
1978
  }, [results.length]);
1497
1979
  const selectPrev = react.useCallback(() => {
1498
- setSelectedIndex((prev) => base.max(prev - 1, 0));
1980
+ setSelectedIndex((prev) => max2(prev - 1, 0));
1499
1981
  }, []);
1500
1982
  const resetSelection = react.useCallback(() => {
1501
1983
  setSelectedIndex(0);
@@ -1561,23 +2043,377 @@ function useOmnibar(options) {
1561
2043
  isAwaitingSequence
1562
2044
  };
1563
2045
  }
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,
2046
+ var baseStyle = {
2047
+ width: "1em",
2048
+ height: "1em",
2049
+ verticalAlign: "middle"
2050
+ };
2051
+ function Up({ className, style }) {
2052
+ return /* @__PURE__ */ jsxRuntime.jsx(
2053
+ "svg",
2054
+ {
2055
+ className,
2056
+ style: { ...baseStyle, ...style },
2057
+ viewBox: "0 0 24 24",
2058
+ fill: "none",
2059
+ stroke: "currentColor",
2060
+ strokeWidth: "3",
2061
+ strokeLinecap: "round",
2062
+ strokeLinejoin: "round",
2063
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 19V5M5 12l7-7 7 7" })
2064
+ }
2065
+ );
2066
+ }
2067
+ function Down({ className, style }) {
2068
+ return /* @__PURE__ */ jsxRuntime.jsx(
2069
+ "svg",
2070
+ {
2071
+ className,
2072
+ style: { ...baseStyle, ...style },
2073
+ viewBox: "0 0 24 24",
2074
+ fill: "none",
2075
+ stroke: "currentColor",
2076
+ strokeWidth: "3",
2077
+ strokeLinecap: "round",
2078
+ strokeLinejoin: "round",
2079
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 5v14M5 12l7 7 7-7" })
2080
+ }
2081
+ );
2082
+ }
2083
+ function Left({ className, style }) {
2084
+ return /* @__PURE__ */ jsxRuntime.jsx(
2085
+ "svg",
2086
+ {
2087
+ className,
2088
+ style: { ...baseStyle, ...style },
2089
+ viewBox: "0 0 24 24",
2090
+ fill: "none",
2091
+ stroke: "currentColor",
2092
+ strokeWidth: "3",
2093
+ strokeLinecap: "round",
2094
+ strokeLinejoin: "round",
2095
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 12H5M12 5l-7 7 7 7" })
2096
+ }
2097
+ );
2098
+ }
2099
+ function Right({ className, style }) {
2100
+ return /* @__PURE__ */ jsxRuntime.jsx(
2101
+ "svg",
2102
+ {
2103
+ className,
2104
+ style: { ...baseStyle, ...style },
2105
+ viewBox: "0 0 24 24",
2106
+ fill: "none",
2107
+ stroke: "currentColor",
2108
+ strokeWidth: "3",
2109
+ strokeLinecap: "round",
2110
+ strokeLinejoin: "round",
2111
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 12h14M12 5l7 7-7 7" })
2112
+ }
2113
+ );
2114
+ }
2115
+ function Enter({ className, style }) {
2116
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2117
+ "svg",
2118
+ {
2119
+ className,
2120
+ style: { ...baseStyle, ...style },
2121
+ viewBox: "0 0 24 24",
2122
+ fill: "none",
2123
+ stroke: "currentColor",
2124
+ strokeWidth: "3",
2125
+ strokeLinecap: "round",
2126
+ strokeLinejoin: "round",
2127
+ children: [
2128
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 10l-4 4 4 4" }),
2129
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 6v8a2 2 0 01-2 2H5" })
2130
+ ]
2131
+ }
2132
+ );
2133
+ }
2134
+ function Backspace({ className, style }) {
2135
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2136
+ "svg",
2137
+ {
2138
+ className,
2139
+ style: { ...baseStyle, ...style },
2140
+ viewBox: "0 0 24 24",
2141
+ fill: "none",
2142
+ stroke: "currentColor",
2143
+ strokeWidth: "2",
2144
+ strokeLinecap: "round",
2145
+ strokeLinejoin: "round",
2146
+ children: [
2147
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 4H8l-7 8 7 8h13a2 2 0 002-2V6a2 2 0 00-2-2z" }),
2148
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "9", x2: "12", y2: "15" }),
2149
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "9", x2: "18", y2: "15" })
2150
+ ]
2151
+ }
2152
+ );
2153
+ }
2154
+ function getKeyIcon(key) {
2155
+ switch (key.toLowerCase()) {
2156
+ case "arrowup":
2157
+ return Up;
2158
+ case "arrowdown":
2159
+ return Down;
2160
+ case "arrowleft":
2161
+ return Left;
2162
+ case "arrowright":
2163
+ return Right;
2164
+ case "enter":
2165
+ return Enter;
2166
+ case "backspace":
2167
+ return Backspace;
2168
+ default:
2169
+ return null;
2170
+ }
2171
+ }
2172
+ var baseStyle2 = {
2173
+ width: "1.2em",
2174
+ height: "1.2em",
2175
+ marginRight: "2px",
2176
+ verticalAlign: "middle"
2177
+ };
2178
+ var wideStyle = {
2179
+ ...baseStyle2,
2180
+ width: "1.4em"
2181
+ };
2182
+ var Command = react.forwardRef(
2183
+ ({ className, style, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
2184
+ "svg",
2185
+ {
2186
+ ref,
2187
+ className,
2188
+ style: { ...baseStyle2, ...style },
2189
+ viewBox: "0 0 24 24",
2190
+ fill: "currentColor",
2191
+ ...props,
2192
+ 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" })
2193
+ }
2194
+ )
2195
+ );
2196
+ Command.displayName = "Command";
2197
+ var Ctrl = react.forwardRef(
2198
+ ({ className, style, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
2199
+ "svg",
2200
+ {
2201
+ ref,
2202
+ className,
2203
+ style: { ...baseStyle2, ...style },
2204
+ viewBox: "0 0 24 24",
2205
+ fill: "none",
2206
+ stroke: "currentColor",
2207
+ strokeWidth: "3",
2208
+ strokeLinecap: "round",
2209
+ strokeLinejoin: "round",
2210
+ ...props,
2211
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 15l6-6 6 6" })
2212
+ }
2213
+ )
2214
+ );
2215
+ Ctrl.displayName = "Ctrl";
2216
+ var Shift = react.forwardRef(
2217
+ ({ className, style, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
2218
+ "svg",
2219
+ {
2220
+ ref,
2221
+ className,
2222
+ style: { ...wideStyle, ...style },
2223
+ viewBox: "0 0 28 24",
2224
+ fill: "none",
2225
+ stroke: "currentColor",
2226
+ strokeWidth: "2",
2227
+ strokeLinejoin: "round",
2228
+ ...props,
2229
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 3L3 14h6v7h10v-7h6L14 3z" })
2230
+ }
2231
+ )
2232
+ );
2233
+ Shift.displayName = "Shift";
2234
+ var Option = react.forwardRef(
2235
+ ({ className, style, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
2236
+ "svg",
2237
+ {
2238
+ ref,
2239
+ className,
2240
+ style: { ...baseStyle2, ...style },
2241
+ viewBox: "0 0 24 24",
2242
+ fill: "none",
2243
+ stroke: "currentColor",
2244
+ strokeWidth: "2.5",
2245
+ strokeLinecap: "round",
2246
+ strokeLinejoin: "round",
2247
+ ...props,
2248
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 6h6l8 12h6M14 6h6" })
2249
+ }
2250
+ )
2251
+ );
2252
+ Option.displayName = "Option";
2253
+ var Alt = react.forwardRef(
2254
+ ({ className, style, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
2255
+ "svg",
2256
+ {
2257
+ ref,
2258
+ className,
2259
+ style: { ...baseStyle2, ...style },
2260
+ viewBox: "0 0 24 24",
2261
+ fill: "none",
2262
+ stroke: "currentColor",
2263
+ strokeWidth: "2.5",
2264
+ strokeLinecap: "round",
2265
+ strokeLinejoin: "round",
2266
+ ...props,
2267
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 18h8M12 18l4-6M12 18l4 0M16 12l4-6h-8" })
2268
+ }
2269
+ )
2270
+ );
2271
+ Alt.displayName = "Alt";
2272
+ var isMac2 = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
2273
+ function getModifierIcon(modifier) {
2274
+ switch (modifier) {
2275
+ case "meta":
2276
+ return Command;
2277
+ case "ctrl":
2278
+ return Ctrl;
2279
+ case "shift":
2280
+ return Shift;
2281
+ case "opt":
2282
+ return Option;
2283
+ case "alt":
2284
+ return isMac2 ? Option : Alt;
2285
+ }
2286
+ }
2287
+ var ModifierIcon = react.forwardRef(
2288
+ ({ modifier, ...props }, ref) => {
2289
+ const Icon = getModifierIcon(modifier);
2290
+ return /* @__PURE__ */ jsxRuntime.jsx(Icon, { ref, ...props });
2291
+ }
2292
+ );
2293
+ ModifierIcon.displayName = "ModifierIcon";
2294
+ function KeyCombo({ combo }) {
2295
+ const { key, modifiers } = combo;
2296
+ const parts = [];
2297
+ if (modifiers.meta) {
2298
+ parts.push(/* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "meta", className: "kbd-modifier-icon" }, "meta"));
2299
+ }
2300
+ if (modifiers.ctrl) {
2301
+ parts.push(/* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "ctrl", className: "kbd-modifier-icon" }, "ctrl"));
2302
+ }
2303
+ if (modifiers.alt) {
2304
+ parts.push(/* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "alt", className: "kbd-modifier-icon" }, "alt"));
2305
+ }
2306
+ if (modifiers.shift) {
2307
+ parts.push(/* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }, "shift"));
2308
+ }
2309
+ const KeyIcon = getKeyIcon(key);
2310
+ if (KeyIcon) {
2311
+ parts.push(/* @__PURE__ */ jsxRuntime.jsx(KeyIcon, { className: "kbd-key-icon" }, "key"));
2312
+ } else {
2313
+ parts.push(/* @__PURE__ */ jsxRuntime.jsx("span", { children: formatKeyForDisplay(key) }, "key"));
2314
+ }
2315
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: parts });
2316
+ }
2317
+ function SeqElemDisplay({ elem }) {
2318
+ if (elem.type === "digit") {
2319
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-placeholder", title: "Any single digit (0-9)", children: "#" });
2320
+ }
2321
+ if (elem.type === "digits") {
2322
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-placeholder", title: "One or more digits (0-9)", children: "##" });
2323
+ }
2324
+ return /* @__PURE__ */ jsxRuntime.jsx(KeyCombo, { combo: { key: elem.key, modifiers: elem.modifiers } });
2325
+ }
2326
+ function BindingDisplay({ binding }) {
2327
+ const sequence = parseKeySeq(binding);
2328
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: sequence.map((elem, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
2329
+ i > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-sequence-sep", children: " " }),
2330
+ /* @__PURE__ */ jsxRuntime.jsx(SeqElemDisplay, { elem })
2331
+ ] }, i)) });
2332
+ }
2333
+ function Kbd({
2334
+ action,
2335
+ separator = " / ",
2336
+ all = false,
2337
+ fallback = null,
2338
+ className,
2339
+ clickable = true
2340
+ }) {
2341
+ const ctx = useMaybeHotkeysContext();
2342
+ const warnedRef = react.useRef(false);
2343
+ const bindings = ctx ? all ? ctx.registry.getBindingsForAction(action) : [ctx.registry.getFirstBindingForAction(action)].filter(Boolean) : [];
2344
+ react.useEffect(() => {
2345
+ if (!ctx) return;
2346
+ if (warnedRef.current) return;
2347
+ const timer = setTimeout(() => {
2348
+ if (!ctx.registry.actions.has(action)) {
2349
+ console.warn(`Kbd: Action "${action}" not found in registry`);
2350
+ warnedRef.current = true;
2351
+ }
2352
+ }, 100);
2353
+ return () => clearTimeout(timer);
2354
+ }, [ctx, action]);
2355
+ if (!ctx) {
2356
+ return null;
2357
+ }
2358
+ if (bindings.length === 0) {
2359
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
2360
+ }
2361
+ const content = bindings.map((binding, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
2362
+ i > 0 && separator,
2363
+ /* @__PURE__ */ jsxRuntime.jsx(BindingDisplay, { binding })
2364
+ ] }, binding));
2365
+ if (clickable) {
2366
+ return /* @__PURE__ */ jsxRuntime.jsx(
2367
+ "kbd",
2368
+ {
2369
+ className: `${className || ""} kbd-clickable`.trim(),
2370
+ onClick: () => ctx.executeAction(action),
2371
+ role: "button",
2372
+ tabIndex: 0,
2373
+ onKeyDown: (e) => {
2374
+ if (e.key === "Enter" || e.key === " ") {
2375
+ e.preventDefault();
2376
+ ctx.executeAction(action);
2377
+ }
2378
+ },
2379
+ children: content
2380
+ }
2381
+ );
2382
+ }
2383
+ return /* @__PURE__ */ jsxRuntime.jsx("kbd", { className, children: content });
2384
+ }
2385
+ function Key(props) {
2386
+ return /* @__PURE__ */ jsxRuntime.jsx(Kbd, { ...props, clickable: false });
2387
+ }
2388
+ function Kbds(props) {
2389
+ return /* @__PURE__ */ jsxRuntime.jsx(Kbd, { ...props, all: true });
2390
+ }
2391
+ function KbdModal(props) {
2392
+ return /* @__PURE__ */ jsxRuntime.jsx(Kbd, { ...props, action: ACTION_MODAL });
2393
+ }
2394
+ function KbdOmnibar(props) {
2395
+ return /* @__PURE__ */ jsxRuntime.jsx(Kbd, { ...props, action: ACTION_OMNIBAR });
2396
+ }
2397
+ function KbdLookup(props) {
2398
+ return /* @__PURE__ */ jsxRuntime.jsx(Kbd, { ...props, action: ACTION_LOOKUP });
2399
+ }
2400
+ function buildActionMap(keymap) {
2401
+ const map = /* @__PURE__ */ new Map();
2402
+ for (const [key, actionOrActions] of Object.entries(keymap)) {
2403
+ const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
2404
+ for (const action of actions) {
2405
+ map.set(action, key);
2406
+ }
2407
+ }
2408
+ return map;
2409
+ }
2410
+ function KeybindingEditor({
2411
+ keymap,
2412
+ defaults,
2413
+ descriptions,
2414
+ onChange,
2415
+ onReset,
2416
+ className,
1581
2417
  children
1582
2418
  }) {
1583
2419
  const [editingAction, setEditingAction] = react.useState(null);
@@ -1748,127 +2584,203 @@ function KeybindingEditor({
1748
2584
  ] })
1749
2585
  ] });
1750
2586
  }
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" })
2587
+ function LookupModal({ defaultBinding = "meta+shift+k" } = {}) {
2588
+ const {
2589
+ isLookupOpen,
2590
+ closeLookup,
2591
+ toggleLookup,
2592
+ registry,
2593
+ executeAction
2594
+ } = useHotkeysContext();
2595
+ useAction(ACTION_LOOKUP, {
2596
+ label: "Key lookup",
2597
+ group: "Global",
2598
+ defaultBindings: defaultBinding ? [defaultBinding] : [],
2599
+ handler: react.useCallback(() => toggleLookup(), [toggleLookup])
2600
+ });
2601
+ const [pendingKeys, setPendingKeys] = react.useState([]);
2602
+ const [selectedIndex, setSelectedIndex] = react.useState(0);
2603
+ const allBindings = react.useMemo(() => {
2604
+ const results = [];
2605
+ const keymap = registry.keymap;
2606
+ for (const [binding, actionOrActions] of Object.entries(keymap)) {
2607
+ if (binding.startsWith("__")) continue;
2608
+ const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
2609
+ const sequence = parseHotkeyString(binding);
2610
+ const keySeq = parseKeySeq(binding);
2611
+ const display = formatKeySeq(keySeq).display;
2612
+ const labels = actions.map((actionId) => {
2613
+ const action = registry.actions.get(actionId);
2614
+ return action?.config.label || actionId;
2615
+ });
2616
+ results.push({ binding, sequence, keySeq, display, actions, labels });
2617
+ }
2618
+ results.sort((a, b) => a.binding.localeCompare(b.binding));
2619
+ return results;
2620
+ }, [registry.keymap, registry.actions]);
2621
+ const filteredBindings = react.useMemo(() => {
2622
+ if (pendingKeys.length === 0) return allBindings;
2623
+ return allBindings.filter((result) => {
2624
+ if (result.sequence.length < pendingKeys.length) return false;
2625
+ for (let i = 0; i < pendingKeys.length; i++) {
2626
+ const pending = pendingKeys[i];
2627
+ const target = result.sequence[i];
2628
+ if (pending.key !== target.key) return false;
2629
+ if (pending.modifiers.ctrl !== target.modifiers.ctrl) return false;
2630
+ if (pending.modifiers.alt !== target.modifiers.alt) return false;
2631
+ if (pending.modifiers.shift !== target.modifiers.shift) return false;
2632
+ if (pending.modifiers.meta !== target.modifiers.meta) return false;
2633
+ }
2634
+ return true;
2635
+ });
2636
+ }, [allBindings, pendingKeys]);
2637
+ const groupedByNextKey = react.useMemo(() => {
2638
+ const groups = /* @__PURE__ */ new Map();
2639
+ for (const result of filteredBindings) {
2640
+ if (result.sequence.length > pendingKeys.length) {
2641
+ const nextCombo = result.sequence[pendingKeys.length];
2642
+ const nextKey = formatCombination([nextCombo]).display;
2643
+ const existing = groups.get(nextKey) || [];
2644
+ existing.push(result);
2645
+ groups.set(nextKey, existing);
2646
+ } else {
2647
+ const existing = groups.get("") || [];
2648
+ existing.push(result);
2649
+ groups.set("", existing);
2650
+ }
1817
2651
  }
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" })
2652
+ return groups;
2653
+ }, [filteredBindings, pendingKeys]);
2654
+ const formattedPendingKeys = react.useMemo(() => {
2655
+ if (pendingKeys.length === 0) return "";
2656
+ return formatCombination(pendingKeys).display;
2657
+ }, [pendingKeys]);
2658
+ react.useEffect(() => {
2659
+ if (isLookupOpen) {
2660
+ setPendingKeys([]);
2661
+ setSelectedIndex(0);
1833
2662
  }
1834
- );
2663
+ }, [isLookupOpen]);
2664
+ react.useEffect(() => {
2665
+ setSelectedIndex(0);
2666
+ }, [filteredBindings.length]);
2667
+ react.useEffect(() => {
2668
+ if (!isLookupOpen) return;
2669
+ const handleKeyDown = (e) => {
2670
+ if (e.key === "Escape") {
2671
+ e.preventDefault();
2672
+ if (pendingKeys.length > 0) {
2673
+ setPendingKeys([]);
2674
+ } else {
2675
+ closeLookup();
2676
+ }
2677
+ return;
2678
+ }
2679
+ if (e.key === "Backspace") {
2680
+ e.preventDefault();
2681
+ setPendingKeys((prev) => prev.slice(0, -1));
2682
+ return;
2683
+ }
2684
+ if (e.key === "ArrowDown") {
2685
+ e.preventDefault();
2686
+ setSelectedIndex((prev) => Math.min(prev + 1, filteredBindings.length - 1));
2687
+ return;
2688
+ }
2689
+ if (e.key === "ArrowUp") {
2690
+ e.preventDefault();
2691
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
2692
+ return;
2693
+ }
2694
+ if (e.key === "Enter") {
2695
+ e.preventDefault();
2696
+ const selected = filteredBindings[selectedIndex];
2697
+ if (selected && selected.actions.length > 0) {
2698
+ closeLookup();
2699
+ executeAction(selected.actions[0]);
2700
+ }
2701
+ return;
2702
+ }
2703
+ if (isModifierKey(e.key)) return;
2704
+ e.preventDefault();
2705
+ const newCombo = {
2706
+ key: normalizeKey(e.key),
2707
+ modifiers: {
2708
+ ctrl: e.ctrlKey,
2709
+ alt: e.altKey,
2710
+ shift: e.shiftKey,
2711
+ meta: e.metaKey
2712
+ }
2713
+ };
2714
+ setPendingKeys((prev) => [...prev, newCombo]);
2715
+ };
2716
+ window.addEventListener("keydown", handleKeyDown);
2717
+ return () => window.removeEventListener("keydown", handleKeyDown);
2718
+ }, [isLookupOpen, pendingKeys, filteredBindings, selectedIndex, closeLookup, executeAction]);
2719
+ const handleBackdropClick = react.useCallback(() => {
2720
+ closeLookup();
2721
+ }, [closeLookup]);
2722
+ if (!isLookupOpen) return null;
2723
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-lookup-backdrop", onClick: handleBackdropClick, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-lookup", onClick: (e) => e.stopPropagation(), children: [
2724
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-lookup-header", children: [
2725
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-lookup-search", children: formattedPendingKeys ? /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-sequence-keys", children: formattedPendingKeys }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-lookup-placeholder", children: "Type keys to filter..." }) }),
2726
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "kbd-lookup-hint", children: [
2727
+ "\u2191\u2193 navigate \xB7 Enter select \xB7 Esc ",
2728
+ pendingKeys.length > 0 ? "clear" : "close",
2729
+ " \xB7 \u232B back"
2730
+ ] })
2731
+ ] }),
2732
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-lookup-results", children: filteredBindings.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-lookup-empty", children: "No matching shortcuts" }) : filteredBindings.map((result, index) => /* @__PURE__ */ jsxRuntime.jsxs(
2733
+ "div",
2734
+ {
2735
+ className: `kbd-lookup-result ${index === selectedIndex ? "selected" : ""}`,
2736
+ onClick: () => {
2737
+ closeLookup();
2738
+ if (result.actions.length > 0) {
2739
+ executeAction(result.actions[0]);
2740
+ }
2741
+ },
2742
+ onMouseEnter: () => setSelectedIndex(index),
2743
+ children: [
2744
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-kbd", children: result.display }),
2745
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-lookup-labels", children: result.labels.join(", ") })
2746
+ ]
2747
+ },
2748
+ result.binding
2749
+ )) }),
2750
+ pendingKeys.length > 0 && groupedByNextKey.size > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-lookup-continuations", children: [
2751
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-lookup-continuations-label", children: "Continue with:" }),
2752
+ Array.from(groupedByNextKey.keys()).filter((k) => k !== "").slice(0, 8).map((nextKey) => /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-kbd kbd-small", children: nextKey }, nextKey)),
2753
+ groupedByNextKey.size > 9 && /* @__PURE__ */ jsxRuntime.jsx("span", { children: "..." })
2754
+ ] })
2755
+ ] }) });
1835
2756
  }
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;
2757
+ function SeqElemBadge({ elem }) {
2758
+ if (elem.type === "digit") {
2759
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-placeholder", title: "Any single digit (0-9)", children: "#" });
1849
2760
  }
1850
- }
1851
- function ModifierIcon({ modifier, ...props }) {
1852
- const Icon = getModifierIcon(modifier);
1853
- return /* @__PURE__ */ jsxRuntime.jsx(Icon, { ...props });
2761
+ if (elem.type === "digits") {
2762
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-placeholder", title: "One or more digits (0-9)", children: "##" });
2763
+ }
2764
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2765
+ elem.modifiers.meta && /* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "meta", className: "kbd-modifier-icon" }),
2766
+ elem.modifiers.ctrl && /* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "ctrl", className: "kbd-modifier-icon" }),
2767
+ elem.modifiers.alt && /* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "alt", className: "kbd-modifier-icon" }),
2768
+ elem.modifiers.shift && /* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }),
2769
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatKeyForDisplay(elem.key) })
2770
+ ] });
1854
2771
  }
1855
2772
  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: [
2773
+ const keySeq = parseKeySeq(binding);
2774
+ return /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-kbd", children: keySeq.map((elem, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
1858
2775
  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 })
2776
+ /* @__PURE__ */ jsxRuntime.jsx(SeqElemBadge, { elem })
1864
2777
  ] }, i)) });
1865
2778
  }
1866
2779
  function Omnibar({
1867
2780
  actions: actionsProp,
1868
2781
  handlers: handlersProp,
1869
2782
  keymap: keymapProp,
1870
- openKey = "meta+k",
1871
- enabled: enabledProp,
2783
+ defaultBinding = "meta+k",
1872
2784
  isOpen: isOpenProp,
1873
2785
  onOpen: onOpenProp,
1874
2786
  onClose: onCloseProp,
@@ -1883,7 +2795,12 @@ function Omnibar({
1883
2795
  const ctx = useMaybeHotkeysContext();
1884
2796
  const actions = actionsProp ?? ctx?.registry.actionRegistry ?? {};
1885
2797
  const keymap = keymapProp ?? ctx?.registry.keymap ?? {};
1886
- const enabled = enabledProp ?? !ctx;
2798
+ useAction(ACTION_OMNIBAR, {
2799
+ label: "Command palette",
2800
+ group: "Global",
2801
+ defaultBindings: defaultBinding ? [defaultBinding] : [],
2802
+ handler: react.useCallback(() => ctx?.toggleOmnibar(), [ctx?.toggleOmnibar])
2803
+ });
1887
2804
  const handleExecute = react.useCallback((actionId) => {
1888
2805
  if (onExecuteProp) {
1889
2806
  onExecuteProp(actionId);
@@ -1922,9 +2839,9 @@ function Omnibar({
1922
2839
  actions,
1923
2840
  handlers: handlersProp,
1924
2841
  keymap,
1925
- openKey,
1926
- enabled: isOpenProp === void 0 && ctx === null ? enabled : false,
1927
- // Disable hotkey if controlled or using context
2842
+ openKey: "",
2843
+ // Trigger is handled via useAction, not useOmnibar
2844
+ enabled: false,
1928
2845
  onOpen: handleOpen,
1929
2846
  onClose: handleClose,
1930
2847
  onExecute: handleExecute,
@@ -1938,6 +2855,18 @@ function Omnibar({
1938
2855
  });
1939
2856
  }
1940
2857
  }, [isOpen]);
2858
+ react.useEffect(() => {
2859
+ if (!isOpen) return;
2860
+ const handleGlobalKeyDown = (e) => {
2861
+ if (e.key === "Escape") {
2862
+ e.preventDefault();
2863
+ e.stopPropagation();
2864
+ close();
2865
+ }
2866
+ };
2867
+ document.addEventListener("keydown", handleGlobalKeyDown, true);
2868
+ return () => document.removeEventListener("keydown", handleGlobalKeyDown, true);
2869
+ }, [isOpen, close]);
1941
2870
  const handleKeyDown = react.useCallback(
1942
2871
  (e) => {
1943
2872
  switch (e.key) {
@@ -2024,6 +2953,7 @@ function SequenceModal() {
2024
2953
  const {
2025
2954
  pendingKeys,
2026
2955
  isAwaitingSequence,
2956
+ cancelSequence,
2027
2957
  sequenceTimeoutStartedAt: timeoutStartedAt,
2028
2958
  sequenceTimeout,
2029
2959
  getCompletions,
@@ -2056,7 +2986,7 @@ function SequenceModal() {
2056
2986
  if (!isAwaitingSequence || pendingKeys.length === 0) {
2057
2987
  return null;
2058
2988
  }
2059
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-sequence-backdrop", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-sequence", children: [
2989
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-sequence-backdrop", onClick: cancelSequence, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-sequence", onClick: (e) => e.stopPropagation(), children: [
2060
2990
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-sequence-current", children: [
2061
2991
  /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-sequence-keys", children: formattedPendingKeys }),
2062
2992
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-sequence-ellipsis", children: "\u2026" })
@@ -2070,7 +3000,7 @@ function SequenceModal() {
2070
3000
  timeoutStartedAt
2071
3001
  ),
2072
3002
  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() }),
3003
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-kbd", children: nextKey }),
2074
3004
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-sequence-arrow", children: "\u2192" }),
2075
3005
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-sequence-actions", children: comps.flatMap((c) => c.actions).map((action, i) => /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
2076
3006
  i > 0 && ", ",
@@ -2080,6 +3010,8 @@ function SequenceModal() {
2080
3010
  completions.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-sequence-empty", children: "No matching shortcuts" })
2081
3011
  ] }) });
2082
3012
  }
3013
+ var DefaultTooltip = ({ children }) => /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
3014
+ var TooltipContext = react.createContext(DefaultTooltip);
2083
3015
  function parseActionId(actionId) {
2084
3016
  const colonIndex = actionId.indexOf(":");
2085
3017
  if (colonIndex > 0) {
@@ -2087,22 +3019,47 @@ function parseActionId(actionId) {
2087
3019
  }
2088
3020
  return { group: "General", name: actionId };
2089
3021
  }
2090
- function organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder) {
3022
+ function organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder, actionRegistry, showUnbound = true) {
2091
3023
  const actionBindings = getActionBindings(keymap);
2092
3024
  const groupMap = /* @__PURE__ */ new Map();
3025
+ const includedActions = /* @__PURE__ */ new Set();
3026
+ const getGroupName = (actionId) => {
3027
+ const registeredGroup = actionRegistry?.[actionId]?.group;
3028
+ if (registeredGroup) return registeredGroup;
3029
+ const { group: groupKey } = parseActionId(actionId);
3030
+ return groupNames?.[groupKey] ?? groupKey;
3031
+ };
2093
3032
  for (const [actionId, bindings] of actionBindings) {
2094
- const { group: groupKey, name } = parseActionId(actionId);
2095
- const groupName = groupNames?.[groupKey] ?? groupKey;
3033
+ includedActions.add(actionId);
3034
+ const { name } = parseActionId(actionId);
3035
+ const groupName = getGroupName(actionId);
2096
3036
  if (!groupMap.has(groupName)) {
2097
3037
  groupMap.set(groupName, { name: groupName, shortcuts: [] });
2098
3038
  }
2099
3039
  groupMap.get(groupName).shortcuts.push({
2100
3040
  actionId,
2101
- label: labels?.[actionId] ?? name,
3041
+ label: labels?.[actionId] ?? actionRegistry?.[actionId]?.label ?? name,
2102
3042
  description: descriptions?.[actionId],
2103
3043
  bindings
2104
3044
  });
2105
3045
  }
3046
+ if (actionRegistry && showUnbound) {
3047
+ for (const [actionId, action] of Object.entries(actionRegistry)) {
3048
+ if (includedActions.has(actionId)) continue;
3049
+ const { name } = parseActionId(actionId);
3050
+ const groupName = getGroupName(actionId);
3051
+ if (!groupMap.has(groupName)) {
3052
+ groupMap.set(groupName, { name: groupName, shortcuts: [] });
3053
+ }
3054
+ groupMap.get(groupName).shortcuts.push({
3055
+ actionId,
3056
+ label: labels?.[actionId] ?? action.label ?? name,
3057
+ description: descriptions?.[actionId],
3058
+ bindings: []
3059
+ // No bindings
3060
+ });
3061
+ }
3062
+ }
2106
3063
  for (const group of groupMap.values()) {
2107
3064
  group.shortcuts.sort((a, b) => a.actionId.localeCompare(b.actionId));
2108
3065
  }
@@ -2143,11 +3100,25 @@ function KeyDisplay({
2143
3100
  if (modifiers.shift) {
2144
3101
  parts.push(/* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }, "shift"));
2145
3102
  }
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"));
3103
+ const KeyIcon = getKeyIcon(key);
3104
+ if (KeyIcon) {
3105
+ parts.push(/* @__PURE__ */ jsxRuntime.jsx(KeyIcon, { className: "kbd-key-icon" }, "key"));
3106
+ } else {
3107
+ parts.push(/* @__PURE__ */ jsxRuntime.jsx("span", { children: formatKeyForDisplay(key) }, "key"));
3108
+ }
2148
3109
  return /* @__PURE__ */ jsxRuntime.jsx("span", { className, children: parts });
2149
3110
  }
2150
- function BindingDisplay({
3111
+ function SeqElemDisplay2({ elem, className }) {
3112
+ const Tooltip = react.useContext(TooltipContext);
3113
+ if (elem.type === "digit") {
3114
+ return /* @__PURE__ */ jsxRuntime.jsx(Tooltip, { title: "Any single digit (0-9)", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `kbd-placeholder ${className || ""}`, children: "#" }) });
3115
+ }
3116
+ if (elem.type === "digits") {
3117
+ return /* @__PURE__ */ jsxRuntime.jsx(Tooltip, { title: "One or more digits (0-9)", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `kbd-placeholder ${className || ""}`, children: "##" }) });
3118
+ }
3119
+ return /* @__PURE__ */ jsxRuntime.jsx(KeyDisplay, { combo: { key: elem.key, modifiers: elem.modifiers }, className });
3120
+ }
3121
+ function BindingDisplay2({
2151
3122
  binding,
2152
3123
  className,
2153
3124
  editable,
@@ -2158,10 +3129,12 @@ function BindingDisplay({
2158
3129
  onEdit,
2159
3130
  onRemove,
2160
3131
  pendingKeys,
2161
- activeKeys
3132
+ activeKeys,
3133
+ timeoutDuration = DEFAULT_SEQUENCE_TIMEOUT
2162
3134
  }) {
2163
3135
  const sequence = parseHotkeyString(binding);
2164
- const display = formatCombination(sequence);
3136
+ const keySeq = parseKeySeq(binding);
3137
+ formatKeySeq(keySeq);
2165
3138
  let kbdClassName = "kbd-kbd";
2166
3139
  if (editable && !isEditing) kbdClassName += " editable";
2167
3140
  if (isEditing) kbdClassName += " editing";
@@ -2192,7 +3165,17 @@ function BindingDisplay({
2192
3165
  } else {
2193
3166
  content = "...";
2194
3167
  }
2195
- return /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: kbdClassName, tabIndex: editable ? 0 : void 0, children: content });
3168
+ return /* @__PURE__ */ jsxRuntime.jsxs("kbd", { className: kbdClassName, tabIndex: editable ? 0 : void 0, children: [
3169
+ content,
3170
+ pendingKeys && pendingKeys.length > 0 && Number.isFinite(timeoutDuration) && /* @__PURE__ */ jsxRuntime.jsx(
3171
+ "span",
3172
+ {
3173
+ className: "kbd-timeout-bar",
3174
+ style: { animationDuration: `${timeoutDuration}ms` }
3175
+ },
3176
+ pendingKeys.length
3177
+ )
3178
+ ] });
2196
3179
  }
2197
3180
  return /* @__PURE__ */ jsxRuntime.jsxs("kbd", { className: kbdClassName, onClick: handleClick, tabIndex: editable ? 0 : void 0, onKeyDown: editable && onEdit ? (e) => {
2198
3181
  if (e.key === "Enter" || e.key === " ") {
@@ -2200,10 +3183,13 @@ function BindingDisplay({
2200
3183
  onEdit();
2201
3184
  }
2202
3185
  } : void 0, children: [
2203
- display.isSequence ? sequence.map((combo, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
3186
+ keySeq.length > 1 ? keySeq.map((elem, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
2204
3187
  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] }),
3188
+ /* @__PURE__ */ jsxRuntime.jsx(SeqElemDisplay2, { elem })
3189
+ ] }, i)) : keySeq.length === 1 ? /* @__PURE__ */ jsxRuntime.jsx(SeqElemDisplay2, { elem: keySeq[0] }) : (
3190
+ // Fallback for legacy parsing
3191
+ /* @__PURE__ */ jsxRuntime.jsx(KeyDisplay, { combo: sequence[0] })
3192
+ ),
2207
3193
  editable && onRemove && /* @__PURE__ */ jsxRuntime.jsx(
2208
3194
  "button",
2209
3195
  {
@@ -2228,8 +3214,7 @@ function ShortcutsModal({
2228
3214
  groupRenderers,
2229
3215
  isOpen: isOpenProp,
2230
3216
  onClose: onCloseProp,
2231
- openKey = "?",
2232
- autoRegisterOpen,
3217
+ defaultBinding = "?",
2233
3218
  editable = false,
2234
3219
  onBindingChange,
2235
3220
  onBindingAdd,
@@ -2240,7 +3225,9 @@ function ShortcutsModal({
2240
3225
  backdropClassName = "kbd-backdrop",
2241
3226
  modalClassName = "kbd-modal",
2242
3227
  title = "Keyboard Shortcuts",
2243
- hint
3228
+ hint,
3229
+ showUnbound,
3230
+ TooltipComponent: TooltipComponentProp = DefaultTooltip
2244
3231
  }) {
2245
3232
  const ctx = useMaybeHotkeysContext();
2246
3233
  const contextLabels = react.useMemo(() => {
@@ -2279,28 +3266,30 @@ function ShortcutsModal({
2279
3266
  const descriptions = descriptionsProp ?? contextDescriptions;
2280
3267
  const groupNames = groupNamesProp ?? contextGroups;
2281
3268
  const handleBindingChange = onBindingChange ?? (ctx ? (action, oldKey, newKey) => {
2282
- if (oldKey) ctx.registry.removeBinding(oldKey);
3269
+ if (oldKey) ctx.registry.removeBinding(action, oldKey);
2283
3270
  ctx.registry.setBinding(action, newKey);
2284
3271
  } : void 0);
2285
3272
  const handleBindingAdd = onBindingAdd ?? (ctx ? (action, key) => {
2286
3273
  ctx.registry.setBinding(action, key);
2287
3274
  } : void 0);
2288
- const handleBindingRemove = onBindingRemove ?? (ctx ? (_action, key) => {
2289
- ctx.registry.removeBinding(key);
3275
+ const handleBindingRemove = onBindingRemove ?? (ctx ? (action, key) => {
3276
+ ctx.registry.removeBinding(action, key);
2290
3277
  } : void 0);
2291
3278
  const handleReset = onReset ?? (ctx ? () => {
2292
3279
  ctx.registry.resetOverrides();
2293
3280
  } : void 0);
2294
- const shouldAutoRegisterOpen = autoRegisterOpen ?? !ctx;
2295
3281
  const [internalIsOpen, setInternalIsOpen] = react.useState(false);
2296
3282
  const isOpen = isOpenProp ?? ctx?.isModalOpen ?? internalIsOpen;
2297
3283
  const [editingAction, setEditingAction] = react.useState(null);
2298
3284
  const [editingKey, setEditingKey] = react.useState(null);
2299
3285
  const [addingAction, setAddingAction] = react.useState(null);
2300
3286
  const [pendingConflict, setPendingConflict] = react.useState(null);
3287
+ const [hasPendingConflictState, setHasPendingConflictState] = react.useState(false);
2301
3288
  const editingActionRef = react.useRef(null);
2302
3289
  const editingKeyRef = react.useRef(null);
2303
3290
  const addingActionRef = react.useRef(null);
3291
+ const setIsEditingBindingRef = react.useRef(ctx?.setIsEditingBinding);
3292
+ setIsEditingBindingRef.current = ctx?.setIsEditingBinding;
2304
3293
  const conflicts = react.useMemo(() => findConflicts(keymap), [keymap]);
2305
3294
  const actionBindings = react.useMemo(() => getActionBindings(keymap), [keymap]);
2306
3295
  const close = react.useCallback(() => {
@@ -2318,13 +3307,19 @@ function ShortcutsModal({
2318
3307
  ctx.closeModal();
2319
3308
  }
2320
3309
  }, [onCloseProp, ctx]);
2321
- const open = react.useCallback(() => {
3310
+ react.useCallback(() => {
2322
3311
  if (ctx?.openModal) {
2323
3312
  ctx.openModal();
2324
3313
  } else {
2325
3314
  setInternalIsOpen(true);
2326
3315
  }
2327
3316
  }, [ctx]);
3317
+ useAction(ACTION_MODAL, {
3318
+ label: "Show shortcuts",
3319
+ group: "Global",
3320
+ defaultBindings: defaultBinding ? [defaultBinding] : [],
3321
+ handler: react.useCallback(() => ctx?.toggleModal() ?? setInternalIsOpen((prev) => !prev), [ctx?.toggleModal])
3322
+ });
2328
3323
  const checkConflict = react.useCallback((newKey, forAction) => {
2329
3324
  const existingActions = keymap[newKey];
2330
3325
  if (!existingActions) return null;
@@ -2332,7 +3327,24 @@ function ShortcutsModal({
2332
3327
  const conflicts2 = actions.filter((a) => a !== forAction);
2333
3328
  return conflicts2.length > 0 ? conflicts2 : null;
2334
3329
  }, [keymap]);
2335
- const { isRecording, startRecording, cancel, pendingKeys, activeKeys } = useRecordHotkey({
3330
+ const combinationsEqual2 = react.useCallback((a, b) => {
3331
+ 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;
3332
+ }, []);
3333
+ const isSequencePrefix = react.useCallback((a, b) => {
3334
+ if (a.length >= b.length) return false;
3335
+ for (let i = 0; i < a.length; i++) {
3336
+ if (!combinationsEqual2(a[i], b[i])) return false;
3337
+ }
3338
+ return true;
3339
+ }, [combinationsEqual2]);
3340
+ const sequencesEqual = react.useCallback((a, b) => {
3341
+ if (a.length !== b.length) return false;
3342
+ for (let i = 0; i < a.length; i++) {
3343
+ if (!combinationsEqual2(a[i], b[i])) return false;
3344
+ }
3345
+ return true;
3346
+ }, [combinationsEqual2]);
3347
+ const { isRecording, startRecording, cancel, pendingKeys, activeKeys, sequenceTimeout } = useRecordHotkey({
2336
3348
  onCapture: react.useCallback(
2337
3349
  (_sequence, display) => {
2338
3350
  const currentAddingAction = addingActionRef.current;
@@ -2360,6 +3372,7 @@ function ShortcutsModal({
2360
3372
  setEditingAction(null);
2361
3373
  setEditingKey(null);
2362
3374
  setAddingAction(null);
3375
+ setIsEditingBindingRef.current?.(false);
2363
3376
  },
2364
3377
  [checkConflict, handleBindingChange, handleBindingAdd]
2365
3378
  ),
@@ -2371,6 +3384,7 @@ function ShortcutsModal({
2371
3384
  setEditingKey(null);
2372
3385
  setAddingAction(null);
2373
3386
  setPendingConflict(null);
3387
+ setIsEditingBindingRef.current?.(false);
2374
3388
  }, []),
2375
3389
  // Tab to next/prev editable kbd and start editing
2376
3390
  onTab: react.useCallback(() => {
@@ -2395,7 +3409,7 @@ function ShortcutsModal({
2395
3409
  prev.click();
2396
3410
  }
2397
3411
  }, []),
2398
- pauseTimeout: pendingConflict !== null
3412
+ pauseTimeout: pendingConflict !== null || hasPendingConflictState
2399
3413
  });
2400
3414
  const startEditingBinding = react.useCallback(
2401
3415
  (action, key) => {
@@ -2406,9 +3420,10 @@ function ShortcutsModal({
2406
3420
  setEditingAction(action);
2407
3421
  setEditingKey(key);
2408
3422
  setPendingConflict(null);
3423
+ ctx?.setIsEditingBinding(true);
2409
3424
  startRecording();
2410
3425
  },
2411
- [startRecording]
3426
+ [startRecording, ctx?.setIsEditingBinding]
2412
3427
  );
2413
3428
  const startAddingBinding = react.useCallback(
2414
3429
  (action) => {
@@ -2419,9 +3434,10 @@ function ShortcutsModal({
2419
3434
  setEditingKey(null);
2420
3435
  setAddingAction(action);
2421
3436
  setPendingConflict(null);
3437
+ ctx?.setIsEditingBinding(true);
2422
3438
  startRecording();
2423
3439
  },
2424
- [startRecording]
3440
+ [startRecording, ctx?.setIsEditingBinding]
2425
3441
  );
2426
3442
  const startEditing = react.useCallback(
2427
3443
  (action, bindingIndex) => {
@@ -2443,7 +3459,8 @@ function ShortcutsModal({
2443
3459
  setEditingKey(null);
2444
3460
  setAddingAction(null);
2445
3461
  setPendingConflict(null);
2446
- }, [cancel]);
3462
+ ctx?.setIsEditingBinding(false);
3463
+ }, [cancel, ctx?.setIsEditingBinding]);
2447
3464
  const removeBinding = react.useCallback(
2448
3465
  (action, key) => {
2449
3466
  handleBindingRemove?.(action, key);
@@ -2453,6 +3470,31 @@ function ShortcutsModal({
2453
3470
  const reset = react.useCallback(() => {
2454
3471
  handleReset?.();
2455
3472
  }, [handleReset]);
3473
+ const pendingConflictInfo = react.useMemo(() => {
3474
+ if (!isRecording || pendingKeys.length === 0) {
3475
+ return { hasConflict: false, conflictingKeys: /* @__PURE__ */ new Set() };
3476
+ }
3477
+ const conflictingKeys = /* @__PURE__ */ new Set();
3478
+ for (const key of Object.keys(keymap)) {
3479
+ if (editingKey && key.toLowerCase() === editingKey.toLowerCase()) continue;
3480
+ const keySequence = parseHotkeyString(key);
3481
+ if (sequencesEqual(pendingKeys, keySequence)) {
3482
+ conflictingKeys.add(key);
3483
+ continue;
3484
+ }
3485
+ if (isSequencePrefix(pendingKeys, keySequence)) {
3486
+ conflictingKeys.add(key);
3487
+ continue;
3488
+ }
3489
+ if (isSequencePrefix(keySequence, pendingKeys)) {
3490
+ conflictingKeys.add(key);
3491
+ }
3492
+ }
3493
+ return { hasConflict: conflictingKeys.size > 0, conflictingKeys };
3494
+ }, [isRecording, pendingKeys, keymap, editingKey, sequencesEqual, isSequencePrefix]);
3495
+ react.useEffect(() => {
3496
+ setHasPendingConflictState(pendingConflictInfo.hasConflict);
3497
+ }, [pendingConflictInfo.hasConflict]);
2456
3498
  const renderEditableKbd = react.useCallback(
2457
3499
  (actionId, key, showRemove = false) => {
2458
3500
  const isEditingThis = editingAction === actionId && editingKey === key && !addingAction;
@@ -2464,13 +3506,15 @@ function ShortcutsModal({
2464
3506
  const defaultActions = Array.isArray(defaultAction) ? defaultAction : [defaultAction];
2465
3507
  return defaultActions.includes(actionId);
2466
3508
  })() : true;
3509
+ const isPendingConflict = pendingConflictInfo.conflictingKeys.has(key);
2467
3510
  return /* @__PURE__ */ jsxRuntime.jsx(
2468
- BindingDisplay,
3511
+ BindingDisplay2,
2469
3512
  {
2470
3513
  binding: key,
2471
3514
  editable,
2472
3515
  isEditing: isEditingThis,
2473
3516
  isConflict,
3517
+ isPendingConflict,
2474
3518
  isDefault,
2475
3519
  onEdit: () => {
2476
3520
  if (isRecording && !(editingAction === actionId && editingKey === key)) {
@@ -2491,24 +3535,27 @@ function ShortcutsModal({
2491
3535
  },
2492
3536
  onRemove: editable && showRemove ? () => removeBinding(actionId, key) : void 0,
2493
3537
  pendingKeys,
2494
- activeKeys
3538
+ activeKeys,
3539
+ timeoutDuration: pendingConflictInfo.hasConflict ? Infinity : sequenceTimeout
2495
3540
  },
2496
3541
  key
2497
3542
  );
2498
3543
  },
2499
- [editingAction, editingKey, addingAction, conflicts, defaults, editable, startEditingBinding, removeBinding, pendingKeys, activeKeys, isRecording, cancel, handleBindingAdd, handleBindingChange]
3544
+ [editingAction, editingKey, addingAction, conflicts, defaults, editable, startEditingBinding, removeBinding, pendingKeys, activeKeys, isRecording, cancel, handleBindingAdd, handleBindingChange, sequenceTimeout, pendingConflictInfo]
2500
3545
  );
2501
3546
  const renderAddButton = react.useCallback(
2502
3547
  (actionId) => {
2503
3548
  const isAddingThis = addingAction === actionId;
2504
3549
  if (isAddingThis) {
2505
3550
  return /* @__PURE__ */ jsxRuntime.jsx(
2506
- BindingDisplay,
3551
+ BindingDisplay2,
2507
3552
  {
2508
3553
  binding: "",
2509
3554
  isEditing: true,
3555
+ isPendingConflict: pendingConflictInfo.hasConflict,
2510
3556
  pendingKeys,
2511
- activeKeys
3557
+ activeKeys,
3558
+ timeoutDuration: pendingConflictInfo.hasConflict ? Infinity : sequenceTimeout
2512
3559
  }
2513
3560
  );
2514
3561
  }
@@ -2537,13 +3584,14 @@ function ShortcutsModal({
2537
3584
  }
2538
3585
  );
2539
3586
  },
2540
- [addingAction, pendingKeys, activeKeys, startAddingBinding, isRecording, cancel, handleBindingAdd, handleBindingChange]
3587
+ [addingAction, pendingKeys, activeKeys, startAddingBinding, isRecording, cancel, handleBindingAdd, handleBindingChange, sequenceTimeout, pendingConflictInfo]
2541
3588
  );
2542
3589
  const renderCell = react.useCallback(
2543
3590
  (actionId, keys) => {
3591
+ const showAddButton = editable && (multipleBindings || keys.length === 0);
2544
3592
  return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "kbd-action-bindings", children: [
2545
3593
  keys.map((key) => /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: renderEditableKbd(actionId, key, true) }, key)),
2546
- editable && multipleBindings && renderAddButton(actionId)
3594
+ showAddButton && renderAddButton(actionId)
2547
3595
  ] });
2548
3596
  },
2549
3597
  [renderEditableKbd, renderAddButton, editable, multipleBindings]
@@ -2560,14 +3608,10 @@ function ShortcutsModal({
2560
3608
  editingKey,
2561
3609
  addingAction
2562
3610
  }), [renderCell, renderEditableKbd, renderAddButton, startEditingBinding, startAddingBinding, removeBinding, isRecording, editingAction, editingKey, addingAction]);
2563
- const modalKeymap = shouldAutoRegisterOpen ? { [openKey]: "openShortcuts" } : {};
2564
3611
  useHotkeys(
2565
- { ...modalKeymap, escape: "closeShortcuts" },
2566
- {
2567
- openShortcuts: open,
2568
- closeShortcuts: close
2569
- },
2570
- { enabled: shouldAutoRegisterOpen || isOpen }
3612
+ { escape: "closeShortcuts" },
3613
+ { closeShortcuts: close },
3614
+ { enabled: isOpen }
2571
3615
  );
2572
3616
  react.useEffect(() => {
2573
3617
  if (!isOpen || !editingAction && !addingAction) return;
@@ -2581,6 +3625,19 @@ function ShortcutsModal({
2581
3625
  window.addEventListener("keydown", handleEscape, true);
2582
3626
  return () => window.removeEventListener("keydown", handleEscape, true);
2583
3627
  }, [isOpen, editingAction, addingAction, cancelEditing]);
3628
+ react.useEffect(() => {
3629
+ if (!isOpen || !ctx) return;
3630
+ const handleMetaK = (e) => {
3631
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
3632
+ e.preventDefault();
3633
+ e.stopPropagation();
3634
+ close();
3635
+ ctx.openOmnibar();
3636
+ }
3637
+ };
3638
+ window.addEventListener("keydown", handleMetaK, true);
3639
+ return () => window.removeEventListener("keydown", handleMetaK, true);
3640
+ }, [isOpen, ctx, close]);
2584
3641
  const handleBackdropClick = react.useCallback(
2585
3642
  (e) => {
2586
3643
  if (e.target === e.currentTarget) {
@@ -2600,9 +3657,10 @@ function ShortcutsModal({
2600
3657
  },
2601
3658
  [editingAction, addingAction, cancelEditing]
2602
3659
  );
3660
+ const effectiveShowUnbound = showUnbound ?? editable;
2603
3661
  const shortcutGroups = react.useMemo(
2604
- () => organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder),
2605
- [keymap, labels, descriptions, groupNames, groupOrder]
3662
+ () => organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder, ctx?.registry.actionRegistry, effectiveShowUnbound),
3663
+ [keymap, labels, descriptions, groupNames, groupOrder, ctx?.registry.actionRegistry, effectiveShowUnbound]
2606
3664
  );
2607
3665
  if (!isOpen) return null;
2608
3666
  if (children) {
@@ -2632,7 +3690,7 @@ function ShortcutsModal({
2632
3690
  renderCell(actionId, bindings)
2633
3691
  ] }, actionId));
2634
3692
  };
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: [
3693
+ return /* @__PURE__ */ jsxRuntime.jsx(TooltipContext.Provider, { value: TooltipComponentProp, children: /* @__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
3694
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-modal-header", children: [
2637
3695
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "kbd-modal-title", children: title }),
2638
3696
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-modal-header-buttons", children: [
@@ -2701,37 +3759,68 @@ function ShortcutsModal({
2701
3759
  )
2702
3760
  ] })
2703
3761
  ] })
2704
- ] }) });
3762
+ ] }) }) });
2705
3763
  }
2706
3764
 
3765
+ exports.ACTION_LOOKUP = ACTION_LOOKUP;
3766
+ exports.ACTION_MODAL = ACTION_MODAL;
3767
+ exports.ACTION_OMNIBAR = ACTION_OMNIBAR;
2707
3768
  exports.ActionsRegistryContext = ActionsRegistryContext;
2708
- exports.AltIcon = AltIcon;
2709
- exports.CommandIcon = CommandIcon;
2710
- exports.CtrlIcon = CtrlIcon;
3769
+ exports.Alt = Alt;
3770
+ exports.Backspace = Backspace;
3771
+ exports.Command = Command;
3772
+ exports.Ctrl = Ctrl;
3773
+ exports.DEFAULT_SEQUENCE_TIMEOUT = DEFAULT_SEQUENCE_TIMEOUT;
3774
+ exports.DIGITS_PLACEHOLDER = DIGITS_PLACEHOLDER;
3775
+ exports.DIGIT_PLACEHOLDER = DIGIT_PLACEHOLDER;
3776
+ exports.Down = Down;
3777
+ exports.Enter = Enter;
2711
3778
  exports.HotkeysProvider = HotkeysProvider;
3779
+ exports.Kbd = Kbd;
3780
+ exports.KbdLookup = KbdLookup;
3781
+ exports.KbdModal = KbdModal;
3782
+ exports.KbdOmnibar = KbdOmnibar;
3783
+ exports.Kbds = Kbds;
3784
+ exports.Key = Key;
2712
3785
  exports.KeybindingEditor = KeybindingEditor;
3786
+ exports.Left = Left;
3787
+ exports.LookupModal = LookupModal;
2713
3788
  exports.ModifierIcon = ModifierIcon;
2714
3789
  exports.Omnibar = Omnibar;
2715
- exports.OptIcon = OptIcon;
3790
+ exports.Option = Option;
3791
+ exports.Right = Right;
2716
3792
  exports.SequenceModal = SequenceModal;
2717
- exports.ShiftIcon = ShiftIcon;
3793
+ exports.Shift = Shift;
2718
3794
  exports.ShortcutsModal = ShortcutsModal;
3795
+ exports.Up = Up;
3796
+ exports.countPlaceholders = countPlaceholders;
2719
3797
  exports.createTwoColumnRenderer = createTwoColumnRenderer;
3798
+ exports.extractCaptures = extractCaptures;
2720
3799
  exports.findConflicts = findConflicts;
3800
+ exports.formatBinding = formatBinding;
2721
3801
  exports.formatCombination = formatCombination;
2722
3802
  exports.formatKeyForDisplay = formatKeyForDisplay;
3803
+ exports.formatKeySeq = formatKeySeq;
2723
3804
  exports.fuzzyMatch = fuzzyMatch;
2724
3805
  exports.getActionBindings = getActionBindings;
2725
3806
  exports.getConflictsArray = getConflictsArray;
3807
+ exports.getKeyIcon = getKeyIcon;
2726
3808
  exports.getModifierIcon = getModifierIcon;
2727
3809
  exports.getSequenceCompletions = getSequenceCompletions;
2728
3810
  exports.hasConflicts = hasConflicts;
3811
+ exports.hasDigitPlaceholders = hasDigitPlaceholders;
3812
+ exports.hotkeySequenceToKeySeq = hotkeySequenceToKeySeq;
3813
+ exports.isDigitPlaceholder = isDigitPlaceholder;
2729
3814
  exports.isMac = isMac;
2730
3815
  exports.isModifierKey = isModifierKey;
3816
+ exports.isPlaceholderSentinel = isPlaceholderSentinel;
2731
3817
  exports.isSequence = isSequence;
3818
+ exports.isShiftedSymbol = isShiftedSymbol;
3819
+ exports.keySeqToHotkeySequence = keySeqToHotkeySequence;
2732
3820
  exports.normalizeKey = normalizeKey;
2733
3821
  exports.parseCombinationId = parseCombinationId;
2734
3822
  exports.parseHotkeyString = parseHotkeyString;
3823
+ exports.parseKeySeq = parseKeySeq;
2735
3824
  exports.searchActions = searchActions;
2736
3825
  exports.useAction = useAction;
2737
3826
  exports.useActions = useActions;