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