use-kbd 0.3.0 → 0.5.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;
@@ -60,9 +70,7 @@ function useActionsRegistry(options = {}) {
60
70
  const filterRedundantOverrides = useCallback((overrides2) => {
61
71
  const filtered = {};
62
72
  for (const [key, actionOrActions] of Object.entries(overrides2)) {
63
- if (actionOrActions === "") {
64
- continue;
65
- } else if (Array.isArray(actionOrActions)) {
73
+ if (actionOrActions === "") ; else if (Array.isArray(actionOrActions)) {
66
74
  const nonDefaultActions = actionOrActions.filter((a) => !isDefaultBinding(key, a));
67
75
  if (nonDefaultActions.length > 0) {
68
76
  filtered[key] = nonDefaultActions.length === 1 ? nonDefaultActions[0] : nonDefaultActions;
@@ -126,10 +134,10 @@ function useActionsRegistry(options = {}) {
126
134
  actionsRef.current.delete(id);
127
135
  setActionsVersion((v) => v + 1);
128
136
  }, []);
129
- const execute = useCallback((id) => {
137
+ const execute = useCallback((id, captures) => {
130
138
  const action = actionsRef.current.get(id);
131
139
  if (action && (action.config.enabled ?? true)) {
132
- action.config.handler();
140
+ action.config.handler(void 0, captures);
133
141
  }
134
142
  }, []);
135
143
  const keymap = useMemo(() => {
@@ -153,9 +161,7 @@ function useActionsRegistry(options = {}) {
153
161
  }
154
162
  }
155
163
  for (const [key, actionOrActions] of Object.entries(overrides)) {
156
- if (actionOrActions === "") {
157
- continue;
158
- } else {
164
+ if (actionOrActions === "") ; else {
159
165
  const actions2 = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
160
166
  for (const actionId of actions2) {
161
167
  addToKey(key, actionId);
@@ -185,34 +191,55 @@ function useActionsRegistry(options = {}) {
185
191
  }
186
192
  return bindings;
187
193
  }, [keymap]);
194
+ const getFirstBindingForAction = useCallback((actionId) => {
195
+ return getBindingsForAction(actionId)[0];
196
+ }, [getBindingsForAction]);
188
197
  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) {
198
+ if (isDefaultBinding(key, actionId)) {
202
199
  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];
200
+ const existing = prev[actionId] ?? [];
201
+ if (existing.includes(key)) {
202
+ const filtered = existing.filter((k) => k !== key);
203
+ if (filtered.length === 0) {
204
+ const { [actionId]: _, ...rest } = prev;
205
+ return rest;
208
206
  }
207
+ return { ...prev, [actionId]: filtered };
209
208
  }
210
- return next;
209
+ return prev;
210
+ });
211
+ } else {
212
+ updateOverrides((prev) => ({
213
+ ...prev,
214
+ [key]: actionId
215
+ }));
216
+ }
217
+ }, [updateOverrides, updateRemovedDefaults, isDefaultBinding]);
218
+ const removeBinding = useCallback((actionId, key) => {
219
+ const action = actionsRef.current.get(actionId);
220
+ const isDefault = action?.config.defaultBindings?.includes(key);
221
+ if (isDefault) {
222
+ updateRemovedDefaults((prev) => {
223
+ const existing = prev[actionId] ?? [];
224
+ if (existing.includes(key)) return prev;
225
+ return { ...prev, [actionId]: [...existing, key] };
211
226
  });
212
227
  }
213
228
  updateOverrides((prev) => {
214
- const { [key]: _, ...rest } = prev;
215
- return rest;
229
+ const boundAction = prev[key];
230
+ if (boundAction === actionId) {
231
+ const { [key]: _, ...rest } = prev;
232
+ return rest;
233
+ }
234
+ if (Array.isArray(boundAction) && boundAction.includes(actionId)) {
235
+ const newActions = boundAction.filter((a) => a !== actionId);
236
+ if (newActions.length === 0) {
237
+ const { [key]: _, ...rest } = prev;
238
+ return rest;
239
+ }
240
+ return { ...prev, [key]: newActions.length === 1 ? newActions[0] : newActions };
241
+ }
242
+ return prev;
216
243
  });
217
244
  }, [updateOverrides, updateRemovedDefaults]);
218
245
  const resetOverrides = useCallback(() => {
@@ -230,6 +257,7 @@ function useActionsRegistry(options = {}) {
230
257
  keymap,
231
258
  actionRegistry,
232
259
  getBindingsForAction,
260
+ getFirstBindingForAction,
233
261
  overrides,
234
262
  setBinding,
235
263
  removeBinding,
@@ -242,15 +270,119 @@ function useActionsRegistry(options = {}) {
242
270
  keymap,
243
271
  actionRegistry,
244
272
  getBindingsForAction,
273
+ getFirstBindingForAction,
245
274
  overrides,
246
275
  setBinding,
247
276
  removeBinding,
248
277
  resetOverrides
249
278
  ]);
250
279
  }
280
+ var OmnibarEndpointsRegistryContext = createContext(null);
281
+ function useOmnibarEndpointsRegistry() {
282
+ const endpointsRef = useRef(/* @__PURE__ */ new Map());
283
+ const [endpointsVersion, setEndpointsVersion] = useState(0);
284
+ const register = useCallback((id, config) => {
285
+ endpointsRef.current.set(id, {
286
+ id,
287
+ config,
288
+ registeredAt: Date.now()
289
+ });
290
+ setEndpointsVersion((v) => v + 1);
291
+ }, []);
292
+ const unregister = useCallback((id) => {
293
+ endpointsRef.current.delete(id);
294
+ setEndpointsVersion((v) => v + 1);
295
+ }, []);
296
+ const queryEndpoint = useCallback(async (endpointId, query, pagination, signal) => {
297
+ const ep = endpointsRef.current.get(endpointId);
298
+ if (!ep) return null;
299
+ if (ep.config.enabled === false) return null;
300
+ if (query.length < (ep.config.minQueryLength ?? 2)) return null;
301
+ try {
302
+ const response = await ep.config.fetch(query, signal, pagination);
303
+ const entriesWithGroup = response.entries.map((entry) => ({
304
+ ...entry,
305
+ group: entry.group ?? ep.config.group
306
+ }));
307
+ return {
308
+ endpointId: ep.id,
309
+ entries: entriesWithGroup,
310
+ total: response.total,
311
+ hasMore: response.hasMore
312
+ };
313
+ } catch (error) {
314
+ if (error instanceof Error && error.name === "AbortError") {
315
+ return { endpointId: ep.id, entries: [] };
316
+ }
317
+ return {
318
+ endpointId: ep.id,
319
+ entries: [],
320
+ error: error instanceof Error ? error : new Error(String(error))
321
+ };
322
+ }
323
+ }, []);
324
+ const queryAll = useCallback(async (query, signal) => {
325
+ const endpoints2 = Array.from(endpointsRef.current.values());
326
+ const promises = endpoints2.filter((ep) => ep.config.enabled !== false).filter((ep) => query.length >= (ep.config.minQueryLength ?? 2)).map(async (ep) => {
327
+ const pageSize = ep.config.pageSize ?? 10;
328
+ const result = await queryEndpoint(ep.id, query, { offset: 0, limit: pageSize }, signal);
329
+ return result ?? { endpointId: ep.id, entries: [] };
330
+ });
331
+ return Promise.all(promises);
332
+ }, [queryEndpoint]);
333
+ const endpoints = useMemo(() => {
334
+ return new Map(endpointsRef.current);
335
+ }, [endpointsVersion]);
336
+ return useMemo(() => ({
337
+ register,
338
+ unregister,
339
+ endpoints,
340
+ queryAll,
341
+ queryEndpoint
342
+ }), [register, unregister, endpoints, queryAll, queryEndpoint]);
343
+ }
344
+
345
+ // src/constants.ts
346
+ var DEFAULT_SEQUENCE_TIMEOUT = Infinity;
347
+ var ACTION_MODAL = "__hotkeys:modal";
348
+ var ACTION_OMNIBAR = "__hotkeys:omnibar";
349
+ var ACTION_LOOKUP = "__hotkeys:lookup";
350
+
351
+ // src/utils.ts
352
+ var { max } = Math;
353
+ var SHIFTED_SYMBOLS = /* @__PURE__ */ new Set([
354
+ "!",
355
+ "@",
356
+ "#",
357
+ "$",
358
+ "%",
359
+ "^",
360
+ "&",
361
+ "*",
362
+ "(",
363
+ ")",
364
+ "_",
365
+ "+",
366
+ "{",
367
+ "}",
368
+ "|",
369
+ ":",
370
+ '"',
371
+ "<",
372
+ ">",
373
+ "?",
374
+ "~"
375
+ ]);
376
+ function isShiftedSymbol(key) {
377
+ return SHIFTED_SYMBOLS.has(key);
378
+ }
251
379
  function isMac() {
252
380
  if (typeof navigator === "undefined") return false;
253
- return /Mac|iPod|iPhone|iPad/.test(navigator.platform);
381
+ const platform = navigator.userAgentData?.platform;
382
+ if (platform) {
383
+ return platform === "macOS" || platform === "iOS";
384
+ }
385
+ return /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
254
386
  }
255
387
  function normalizeKey(key) {
256
388
  const keyMap = {
@@ -285,7 +417,7 @@ function formatKeyForDisplay(key) {
285
417
  "space": "Space",
286
418
  "escape": "Esc",
287
419
  "enter": "\u21B5",
288
- "tab": "Tab",
420
+ "tab": "\u21E5",
289
421
  "backspace": "\u232B",
290
422
  "delete": "Del",
291
423
  "arrowup": "\u2191",
@@ -308,7 +440,18 @@ function formatKeyForDisplay(key) {
308
440
  }
309
441
  return key;
310
442
  }
443
+ var DIGIT_PLACEHOLDER = "__DIGIT__";
444
+ var DIGITS_PLACEHOLDER = "__DIGITS__";
445
+ function isPlaceholderSentinel(key) {
446
+ return key === DIGIT_PLACEHOLDER || key === DIGITS_PLACEHOLDER;
447
+ }
311
448
  function formatSingleCombination(combo) {
449
+ if (combo.key === DIGIT_PLACEHOLDER) {
450
+ return { display: "#", id: "\\d" };
451
+ }
452
+ if (combo.key === DIGITS_PLACEHOLDER) {
453
+ return { display: "##", id: "\\d+" };
454
+ }
312
455
  const mac = isMac();
313
456
  const parts = [];
314
457
  const idParts = [];
@@ -354,6 +497,10 @@ function formatCombination(input) {
354
497
  const single = formatSingleCombination(input);
355
498
  return { ...single, isSequence: false };
356
499
  }
500
+ function formatBinding(binding) {
501
+ const parsed = parseHotkeyString(binding);
502
+ return formatCombination(parsed).display;
503
+ }
357
504
  function isModifierKey(key) {
358
505
  return ["Control", "Alt", "Shift", "Meta"].includes(key);
359
506
  }
@@ -410,12 +557,110 @@ function parseHotkeyString(hotkeyStr) {
410
557
  const parts = hotkeyStr.trim().split(/\s+/);
411
558
  return parts.map(parseSingleCombination);
412
559
  }
413
- function parseCombinationId(id) {
414
- const sequence = parseHotkeyString(id);
415
- if (sequence.length === 0) {
416
- return { key: "", modifiers: { ctrl: false, alt: false, shift: false, meta: false } };
560
+ var NO_MODIFIERS = { ctrl: false, alt: false, shift: false, meta: false };
561
+ function parseSeqElem(str) {
562
+ if (str === "\\d") {
563
+ return { type: "digit" };
564
+ }
565
+ if (str === "\\d+") {
566
+ return { type: "digits" };
567
+ }
568
+ if (str.length === 1 && /^[A-Z]$/.test(str)) {
569
+ return {
570
+ type: "key",
571
+ key: str.toLowerCase(),
572
+ modifiers: { ctrl: false, alt: false, shift: true, meta: false }
573
+ };
574
+ }
575
+ const parts = str.toLowerCase().split("+");
576
+ const key = parts[parts.length - 1];
577
+ return {
578
+ type: "key",
579
+ key,
580
+ modifiers: {
581
+ ctrl: parts.includes("ctrl") || parts.includes("control"),
582
+ alt: parts.includes("alt") || parts.includes("option"),
583
+ shift: parts.includes("shift"),
584
+ meta: parts.includes("meta") || parts.includes("cmd") || parts.includes("command")
585
+ }
586
+ };
587
+ }
588
+ function parseKeySeq(hotkeyStr) {
589
+ if (!hotkeyStr.trim()) return [];
590
+ const parts = hotkeyStr.trim().split(/\s+/);
591
+ return parts.map(parseSeqElem);
592
+ }
593
+ function formatSeqElem(elem) {
594
+ if (elem.type === "digit") {
595
+ return { display: "\u27E8#\u27E9", id: "\\d" };
596
+ }
597
+ if (elem.type === "digits") {
598
+ return { display: "\u27E8##\u27E9", id: "\\d+" };
599
+ }
600
+ const mac = isMac();
601
+ const parts = [];
602
+ const idParts = [];
603
+ if (elem.modifiers.ctrl) {
604
+ parts.push(mac ? "\u2303" : "Ctrl");
605
+ idParts.push("ctrl");
606
+ }
607
+ if (elem.modifiers.meta) {
608
+ parts.push(mac ? "\u2318" : "Win");
609
+ idParts.push("meta");
610
+ }
611
+ if (elem.modifiers.alt) {
612
+ parts.push(mac ? "\u2325" : "Alt");
613
+ idParts.push("alt");
614
+ }
615
+ if (elem.modifiers.shift) {
616
+ parts.push(mac ? "\u21E7" : "Shift");
617
+ idParts.push("shift");
618
+ }
619
+ parts.push(formatKeyForDisplay(elem.key));
620
+ idParts.push(elem.key);
621
+ return {
622
+ display: mac ? parts.join("") : parts.join("+"),
623
+ id: idParts.join("+")
624
+ };
625
+ }
626
+ function formatKeySeq(seq) {
627
+ if (seq.length === 0) {
628
+ return { display: "", id: "", isSequence: false };
629
+ }
630
+ const formatted = seq.map(formatSeqElem);
631
+ if (seq.length === 1) {
632
+ return { ...formatted[0], isSequence: false };
417
633
  }
418
- return sequence[0];
634
+ return {
635
+ display: formatted.map((f) => f.display).join(" "),
636
+ id: formatted.map((f) => f.id).join(" "),
637
+ isSequence: true
638
+ };
639
+ }
640
+ function hasDigitPlaceholders(seq) {
641
+ return seq.some((elem) => elem.type === "digit" || elem.type === "digits");
642
+ }
643
+ function keySeqToHotkeySequence(seq) {
644
+ return seq.map((elem) => {
645
+ if (elem.type === "digit") {
646
+ return { key: "\\d", modifiers: NO_MODIFIERS };
647
+ }
648
+ if (elem.type === "digits") {
649
+ return { key: "\\d+", modifiers: NO_MODIFIERS };
650
+ }
651
+ return { key: elem.key, modifiers: elem.modifiers };
652
+ });
653
+ }
654
+ function hotkeySequenceToKeySeq(seq) {
655
+ return seq.map((combo) => {
656
+ if (combo.key === "\\d" && !combo.modifiers.ctrl && !combo.modifiers.alt && !combo.modifiers.shift && !combo.modifiers.meta) {
657
+ return { type: "digit" };
658
+ }
659
+ if (combo.key === "\\d+" && !combo.modifiers.ctrl && !combo.modifiers.alt && !combo.modifiers.shift && !combo.modifiers.meta) {
660
+ return { type: "digits" };
661
+ }
662
+ return { type: "key", key: combo.key, modifiers: combo.modifiers };
663
+ });
419
664
  }
420
665
  function isPrefix(a, b) {
421
666
  if (a.length >= b.length) return false;
@@ -427,11 +672,50 @@ function isPrefix(a, b) {
427
672
  function combinationsEqual(a, b) {
428
673
  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
674
  }
675
+ function keyMatchesPattern(pending, pattern) {
676
+ if (pending.modifiers.ctrl !== pattern.modifiers.ctrl || pending.modifiers.alt !== pattern.modifiers.alt || pending.modifiers.shift !== pattern.modifiers.shift || pending.modifiers.meta !== pattern.modifiers.meta) {
677
+ return false;
678
+ }
679
+ if (pending.key === pattern.key) return true;
680
+ return /^[0-9]$/.test(pending.key) && (pattern.key === DIGIT_PLACEHOLDER || pattern.key === DIGITS_PLACEHOLDER);
681
+ }
682
+ function isDigitKey(key) {
683
+ return /^[0-9]$/.test(key);
684
+ }
685
+ function seqElemsCouldConflict(a, b) {
686
+ if (a.type === "digit" && b.type === "digit") return true;
687
+ if (a.type === "digit" && b.type === "key" && isDigitKey(b.key)) return true;
688
+ if (a.type === "key" && isDigitKey(a.key) && b.type === "digit") return true;
689
+ if (a.type === "digits" && b.type === "digits") return true;
690
+ if (a.type === "digits" && b.type === "digit") return true;
691
+ if (a.type === "digit" && b.type === "digits") return true;
692
+ if (a.type === "digits" && b.type === "key" && isDigitKey(b.key)) return true;
693
+ if (a.type === "key" && isDigitKey(a.key) && b.type === "digits") return true;
694
+ if (a.type === "key" && b.type === "key") {
695
+ 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;
696
+ }
697
+ return false;
698
+ }
699
+ function keySeqIsPrefix(a, b) {
700
+ if (a.length >= b.length) return false;
701
+ for (let i = 0; i < a.length; i++) {
702
+ if (!seqElemsCouldConflict(a[i], b[i])) return false;
703
+ }
704
+ return true;
705
+ }
706
+ function keySeqsCouldConflict(a, b) {
707
+ if (a.length !== b.length) return false;
708
+ for (let i = 0; i < a.length; i++) {
709
+ if (!seqElemsCouldConflict(a[i], b[i])) return false;
710
+ }
711
+ return true;
712
+ }
430
713
  function findConflicts(keymap) {
431
714
  const conflicts = /* @__PURE__ */ new Map();
432
715
  const entries = Object.entries(keymap).map(([key, actionOrActions]) => ({
433
716
  key,
434
717
  sequence: parseHotkeyString(key),
718
+ keySeq: parseKeySeq(key),
435
719
  actions: Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
436
720
  }));
437
721
  const keyToActions = /* @__PURE__ */ new Map();
@@ -448,7 +732,36 @@ function findConflicts(keymap) {
448
732
  for (let j = i + 1; j < entries.length; j++) {
449
733
  const a = entries[i];
450
734
  const b = entries[j];
451
- if (isPrefix(a.sequence, b.sequence)) {
735
+ if (keySeqsCouldConflict(a.keySeq, b.keySeq) && a.key !== b.key) {
736
+ const existingA = conflicts.get(a.key) ?? [];
737
+ if (!existingA.includes(`conflicts with: ${b.key}`)) {
738
+ conflicts.set(a.key, [...existingA, ...a.actions, `conflicts with: ${b.key}`]);
739
+ }
740
+ const existingB = conflicts.get(b.key) ?? [];
741
+ if (!existingB.includes(`conflicts with: ${a.key}`)) {
742
+ conflicts.set(b.key, [...existingB, ...b.actions, `conflicts with: ${a.key}`]);
743
+ }
744
+ continue;
745
+ }
746
+ if (keySeqIsPrefix(a.keySeq, b.keySeq)) {
747
+ const existingA = conflicts.get(a.key) ?? [];
748
+ if (!existingA.includes(`prefix of: ${b.key}`)) {
749
+ conflicts.set(a.key, [...existingA, ...a.actions, `prefix of: ${b.key}`]);
750
+ }
751
+ const existingB = conflicts.get(b.key) ?? [];
752
+ if (!existingB.includes(`has prefix: ${a.key}`)) {
753
+ conflicts.set(b.key, [...existingB, ...b.actions, `has prefix: ${a.key}`]);
754
+ }
755
+ } else if (keySeqIsPrefix(b.keySeq, a.keySeq)) {
756
+ const existingB = conflicts.get(b.key) ?? [];
757
+ if (!existingB.includes(`prefix of: ${a.key}`)) {
758
+ conflicts.set(b.key, [...existingB, ...b.actions, `prefix of: ${a.key}`]);
759
+ }
760
+ const existingA = conflicts.get(a.key) ?? [];
761
+ if (!existingA.includes(`has prefix: ${b.key}`)) {
762
+ conflicts.set(a.key, [...existingA, ...a.actions, `has prefix: ${b.key}`]);
763
+ }
764
+ } else if (isPrefix(a.sequence, b.sequence)) {
452
765
  const existingA = conflicts.get(a.key) ?? [];
453
766
  if (!existingA.includes(`prefix of: ${b.key}`)) {
454
767
  conflicts.set(a.key, [...existingA, ...a.actions, `prefix of: ${b.key}`]);
@@ -486,27 +799,77 @@ function getSequenceCompletions(pendingKeys, keymap) {
486
799
  if (pendingKeys.length === 0) return [];
487
800
  const completions = [];
488
801
  for (const [hotkeyStr, actionOrActions] of Object.entries(keymap)) {
489
- const sequence = parseHotkeyString(hotkeyStr);
490
- if (sequence.length <= pendingKeys.length) continue;
491
- let isPrefix2 = true;
492
- for (let i = 0; i < pendingKeys.length; i++) {
493
- if (!combinationsEqual(pendingKeys[i], sequence[i])) {
494
- isPrefix2 = false;
495
- break;
802
+ const keySeq = parseKeySeq(hotkeyStr);
803
+ const hasDigitsPlaceholder = keySeq.some((e) => e.type === "digits");
804
+ if (!hasDigitsPlaceholder && keySeq.length < pendingKeys.length) continue;
805
+ let keySeqIdx = 0;
806
+ let pendingIdx = 0;
807
+ let isMatch = true;
808
+ const captures = [];
809
+ let currentDigits = "";
810
+ for (; pendingIdx < pendingKeys.length && keySeqIdx < keySeq.length; pendingIdx++) {
811
+ const elem = keySeq[keySeqIdx];
812
+ if (elem.type === "digits") {
813
+ if (!/^[0-9]$/.test(pendingKeys[pendingIdx].key)) {
814
+ isMatch = false;
815
+ break;
816
+ }
817
+ currentDigits += pendingKeys[pendingIdx].key;
818
+ if (pendingIdx + 1 < pendingKeys.length && /^[0-9]$/.test(pendingKeys[pendingIdx + 1].key)) {
819
+ continue;
820
+ }
821
+ captures.push(parseInt(currentDigits, 10));
822
+ currentDigits = "";
823
+ keySeqIdx++;
824
+ } else if (elem.type === "digit") {
825
+ if (!/^[0-9]$/.test(pendingKeys[pendingIdx].key)) {
826
+ isMatch = false;
827
+ break;
828
+ }
829
+ captures.push(parseInt(pendingKeys[pendingIdx].key, 10));
830
+ keySeqIdx++;
831
+ } else {
832
+ const keyElem = elem;
833
+ const targetCombo = { key: keyElem.key, modifiers: keyElem.modifiers };
834
+ if (!keyMatchesPattern(pendingKeys[pendingIdx], targetCombo)) {
835
+ isMatch = false;
836
+ break;
837
+ }
838
+ keySeqIdx++;
496
839
  }
497
840
  }
498
- if (isPrefix2) {
499
- const remainingKeys = sequence.slice(pendingKeys.length);
500
- const nextKeys = formatCombination(remainingKeys).id;
501
- const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
841
+ if (pendingIdx < pendingKeys.length) {
842
+ isMatch = false;
843
+ }
844
+ if (!isMatch) continue;
845
+ const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
846
+ if (keySeqIdx === keySeq.length) {
847
+ completions.push({
848
+ nextKeys: "",
849
+ fullSequence: hotkeyStr,
850
+ display: formatKeySeq(keySeq),
851
+ actions,
852
+ isComplete: true,
853
+ captures: captures.length > 0 ? captures : void 0
854
+ });
855
+ } else {
856
+ const remainingKeySeq = keySeq.slice(keySeqIdx);
857
+ const nextKeys = formatKeySeq(remainingKeySeq).display;
502
858
  completions.push({
503
859
  nextKeys,
860
+ nextKeySeq: remainingKeySeq,
504
861
  fullSequence: hotkeyStr,
505
- display: formatCombination(sequence),
506
- actions
862
+ display: formatKeySeq(keySeq),
863
+ actions,
864
+ isComplete: false,
865
+ captures: captures.length > 0 ? captures : void 0
507
866
  });
508
867
  }
509
868
  }
869
+ completions.sort((a, b) => {
870
+ if (a.isComplete !== b.isComplete) return a.isComplete ? -1 : 1;
871
+ return a.fullSequence.localeCompare(b.fullSequence);
872
+ });
510
873
  return completions;
511
874
  }
512
875
  function getActionBindings(keymap) {
@@ -597,7 +960,7 @@ function searchActions(query, actions, keymap) {
597
960
  }
598
961
  const matched = labelMatch.matched || descMatch.matched || groupMatch.matched || idMatch.matched || keywordScore > 0;
599
962
  if (!matched && query) continue;
600
- const score = (labelMatch.matched ? labelMatch.score * 3 : 0) + (descMatch.matched ? descMatch.score * 1.5 : 0) + (groupMatch.matched ? groupMatch.score * 1 : 0) + (idMatch.matched ? idMatch.score * 0.5 : 0) + keywordScore * 2;
963
+ const score = (labelMatch.matched ? labelMatch.score * 3 : 0) + (descMatch.matched ? descMatch.score * 1.5 : 0) + (groupMatch.matched ? groupMatch.score : 0) + (idMatch.matched ? idMatch.score * 0.5 : 0) + keywordScore * 2;
601
964
  results.push({
602
965
  id,
603
966
  action,
@@ -644,6 +1007,95 @@ function sequencesMatch(a, b) {
644
1007
  }
645
1008
  return true;
646
1009
  }
1010
+ function isDigit(key) {
1011
+ return /^[0-9]$/.test(key);
1012
+ }
1013
+ function initMatchState(seq) {
1014
+ return seq.map((elem) => {
1015
+ if (elem.type === "digit") return { type: "digit" };
1016
+ if (elem.type === "digits") return { type: "digits" };
1017
+ return { type: "key", key: elem.key, modifiers: elem.modifiers };
1018
+ });
1019
+ }
1020
+ function matchesKeyElem(combo, elem) {
1021
+ const shiftMatches = isShiftedChar(combo.key) ? elem.modifiers.shift ? combo.modifiers.shift : true : combo.modifiers.shift === elem.modifiers.shift;
1022
+ return combo.modifiers.ctrl === elem.modifiers.ctrl && combo.modifiers.alt === elem.modifiers.alt && shiftMatches && combo.modifiers.meta === elem.modifiers.meta && combo.key === elem.key;
1023
+ }
1024
+ function advanceMatchState(state, pattern, combo) {
1025
+ const newState = [...state];
1026
+ let pos = 0;
1027
+ for (let i = 0; i < state.length; i++) {
1028
+ const elem = state[i];
1029
+ if (elem.type === "key" && !elem.matched) break;
1030
+ if (elem.type === "digit" && elem.value === void 0) break;
1031
+ if (elem.type === "digits" && elem.value === void 0) {
1032
+ if (!elem.partial) break;
1033
+ if (isDigit(combo.key)) {
1034
+ const newPartial = (elem.partial || "") + combo.key;
1035
+ newState[i] = { type: "digits", partial: newPartial };
1036
+ return { status: "partial", state: newState };
1037
+ } else {
1038
+ const digitValue = parseInt(elem.partial, 10);
1039
+ newState[i] = { type: "digits", value: digitValue };
1040
+ pos = i + 1;
1041
+ if (pos >= pattern.length) {
1042
+ return { status: "failed" };
1043
+ }
1044
+ break;
1045
+ }
1046
+ }
1047
+ pos++;
1048
+ }
1049
+ if (pos >= pattern.length) {
1050
+ return { status: "failed" };
1051
+ }
1052
+ const currentPattern = pattern[pos];
1053
+ if (currentPattern.type === "digit") {
1054
+ if (!isDigit(combo.key) || combo.modifiers.ctrl || combo.modifiers.alt || combo.modifiers.meta) {
1055
+ return { status: "failed" };
1056
+ }
1057
+ newState[pos] = { type: "digit", value: parseInt(combo.key, 10) };
1058
+ } else if (currentPattern.type === "digits") {
1059
+ if (!isDigit(combo.key) || combo.modifiers.ctrl || combo.modifiers.alt || combo.modifiers.meta) {
1060
+ return { status: "failed" };
1061
+ }
1062
+ newState[pos] = { type: "digits", partial: combo.key };
1063
+ } else {
1064
+ if (!matchesKeyElem(combo, currentPattern)) {
1065
+ return { status: "failed" };
1066
+ }
1067
+ newState[pos] = { type: "key", key: currentPattern.key, modifiers: currentPattern.modifiers, matched: true };
1068
+ }
1069
+ const isComplete = newState.every((elem) => {
1070
+ if (elem.type === "key") return elem.matched === true;
1071
+ if (elem.type === "digit") return elem.value !== void 0;
1072
+ if (elem.type === "digits") return elem.value !== void 0;
1073
+ return false;
1074
+ });
1075
+ if (isComplete) {
1076
+ const captures = newState.filter(
1077
+ (e) => (e.type === "digit" || e.type === "digits") && e.value !== void 0
1078
+ ).map((e) => e.value);
1079
+ return { status: "matched", state: newState, captures };
1080
+ }
1081
+ return { status: "partial", state: newState };
1082
+ }
1083
+ function isCollectingDigits(state) {
1084
+ return state.some((elem) => elem.type === "digits" && elem.partial !== void 0 && elem.value === void 0);
1085
+ }
1086
+ function finalizeDigits(state) {
1087
+ return state.map((elem) => {
1088
+ if (elem.type === "digits" && elem.partial !== void 0 && elem.value === void 0) {
1089
+ return { type: "digits", value: parseInt(elem.partial, 10) };
1090
+ }
1091
+ return elem;
1092
+ });
1093
+ }
1094
+ function extractMatchCaptures(state) {
1095
+ return state.filter(
1096
+ (e) => (e.type === "digit" || e.type === "digits") && e.value !== void 0
1097
+ ).map((e) => e.value);
1098
+ }
647
1099
  function useHotkeys(keymap, handlers, options = {}) {
648
1100
  const {
649
1101
  enabled = true,
@@ -651,7 +1103,7 @@ function useHotkeys(keymap, handlers, options = {}) {
651
1103
  preventDefault = true,
652
1104
  stopPropagation = true,
653
1105
  enableOnFormTags = false,
654
- sequenceTimeout = 1e3,
1106
+ sequenceTimeout = DEFAULT_SEQUENCE_TIMEOUT,
655
1107
  onTimeout = "submit",
656
1108
  onSequenceStart,
657
1109
  onSequenceProgress,
@@ -667,11 +1119,13 @@ function useHotkeys(keymap, handlers, options = {}) {
667
1119
  const timeoutRef = useRef(null);
668
1120
  const pendingKeysRef = useRef([]);
669
1121
  pendingKeysRef.current = pendingKeys;
1122
+ const matchStatesRef = useRef(/* @__PURE__ */ new Map());
670
1123
  const parsedKeymapRef = useRef([]);
671
1124
  useEffect(() => {
672
1125
  parsedKeymapRef.current = Object.entries(keymap).map(([key, actionOrActions]) => ({
673
1126
  key,
674
1127
  sequence: parseHotkeyString(key),
1128
+ keySeq: parseKeySeq(key),
675
1129
  actions: Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
676
1130
  }));
677
1131
  }, [keymap]);
@@ -679,6 +1133,7 @@ function useHotkeys(keymap, handlers, options = {}) {
679
1133
  setPendingKeys([]);
680
1134
  setIsAwaitingSequence(false);
681
1135
  setTimeoutStartedAt(null);
1136
+ matchStatesRef.current.clear();
682
1137
  if (timeoutRef.current) {
683
1138
  clearTimeout(timeoutRef.current);
684
1139
  timeoutRef.current = null;
@@ -688,7 +1143,7 @@ function useHotkeys(keymap, handlers, options = {}) {
688
1143
  clearPending();
689
1144
  onSequenceCancel?.();
690
1145
  }, [clearPending, onSequenceCancel]);
691
- const tryExecute = useCallback((sequence, e) => {
1146
+ const tryExecute = useCallback((sequence, e, captures) => {
692
1147
  for (const entry of parsedKeymapRef.current) {
693
1148
  if (sequencesMatch(sequence, entry.sequence)) {
694
1149
  for (const action of entry.actions) {
@@ -700,7 +1155,27 @@ function useHotkeys(keymap, handlers, options = {}) {
700
1155
  if (stopPropagation) {
701
1156
  e.stopPropagation();
702
1157
  }
703
- handler(e);
1158
+ handler(e, captures);
1159
+ return true;
1160
+ }
1161
+ }
1162
+ }
1163
+ }
1164
+ return false;
1165
+ }, [preventDefault, stopPropagation]);
1166
+ const tryExecuteKeySeq = useCallback((matchKey, captures, e) => {
1167
+ for (const entry of parsedKeymapRef.current) {
1168
+ if (entry.key === matchKey) {
1169
+ for (const action of entry.actions) {
1170
+ const handler = handlersRef.current[action];
1171
+ if (handler) {
1172
+ if (preventDefault) {
1173
+ e.preventDefault();
1174
+ }
1175
+ if (stopPropagation) {
1176
+ e.stopPropagation();
1177
+ }
1178
+ handler(e, captures.length > 0 ? captures : void 0);
704
1179
  return true;
705
1180
  }
706
1181
  }
@@ -730,7 +1205,8 @@ function useHotkeys(keymap, handlers, options = {}) {
730
1205
  const handleKeyDown = (e) => {
731
1206
  if (!enableOnFormTags) {
732
1207
  const eventTarget = e.target;
733
- if (eventTarget instanceof HTMLInputElement || eventTarget instanceof HTMLTextAreaElement || eventTarget instanceof HTMLSelectElement || eventTarget.isContentEditable) {
1208
+ const isTextInput = eventTarget instanceof HTMLInputElement && ["text", "email", "password", "search", "tel", "url", "number", "date", "datetime-local", "month", "time", "week"].includes(eventTarget.type);
1209
+ if (isTextInput || eventTarget instanceof HTMLTextAreaElement || eventTarget instanceof HTMLSelectElement || eventTarget.isContentEditable) {
734
1210
  return;
735
1211
  }
736
1212
  }
@@ -743,7 +1219,24 @@ function useHotkeys(keymap, handlers, options = {}) {
743
1219
  }
744
1220
  if (e.key === "Enter" && pendingKeysRef.current.length > 0) {
745
1221
  e.preventDefault();
746
- const executed = tryExecute(pendingKeysRef.current, e);
1222
+ let executed = false;
1223
+ for (const [key, state] of matchStatesRef.current.entries()) {
1224
+ const finalizedState = isCollectingDigits(state) ? finalizeDigits(state) : state;
1225
+ const isComplete = finalizedState.every((elem) => {
1226
+ if (elem.type === "key") return elem.matched === true;
1227
+ if (elem.type === "digit") return elem.value !== void 0;
1228
+ if (elem.type === "digits") return elem.value !== void 0;
1229
+ return false;
1230
+ });
1231
+ if (isComplete) {
1232
+ const captures = extractMatchCaptures(finalizedState);
1233
+ executed = tryExecuteKeySeq(key, captures, e);
1234
+ if (executed) break;
1235
+ }
1236
+ }
1237
+ if (!executed) {
1238
+ executed = tryExecute(pendingKeysRef.current, e);
1239
+ }
747
1240
  clearPending();
748
1241
  if (!executed) {
749
1242
  onSequenceCancel?.();
@@ -756,49 +1249,173 @@ function useHotkeys(keymap, handlers, options = {}) {
756
1249
  return;
757
1250
  }
758
1251
  const currentCombo = eventToCombination(e);
759
- const newSequence = [...pendingKeysRef.current, currentCombo];
760
- const exactMatch = tryExecute(newSequence, e);
761
- if (exactMatch) {
762
- clearPending();
763
- return;
764
- }
765
- if (hasPotentialMatch(newSequence)) {
766
- if (hasSequenceExtension(newSequence)) {
767
- setPendingKeys(newSequence);
768
- setIsAwaitingSequence(true);
769
- if (pendingKeysRef.current.length === 0) {
770
- onSequenceStart?.(newSequence);
771
- } else {
772
- onSequenceProgress?.(newSequence);
1252
+ if (e.key === "Backspace" && pendingKeysRef.current.length > 0) {
1253
+ let backspaceMatches = false;
1254
+ for (const entry of parsedKeymapRef.current) {
1255
+ let state = matchStatesRef.current.get(entry.key);
1256
+ if (!state) {
1257
+ state = initMatchState(entry.keySeq);
773
1258
  }
774
- setTimeoutStartedAt(Date.now());
775
- timeoutRef.current = setTimeout(() => {
776
- if (onTimeout === "submit") {
777
- setPendingKeys((current) => {
778
- if (current.length > 0) {
779
- onSequenceCancel?.();
1259
+ if (isCollectingDigits(state)) {
1260
+ continue;
1261
+ }
1262
+ const result = advanceMatchState(state, entry.keySeq, currentCombo);
1263
+ if (result.status === "matched" || result.status === "partial") {
1264
+ backspaceMatches = true;
1265
+ break;
1266
+ }
1267
+ }
1268
+ if (!backspaceMatches) {
1269
+ e.preventDefault();
1270
+ const newPending = pendingKeysRef.current.slice(0, -1);
1271
+ if (newPending.length === 0) {
1272
+ clearPending();
1273
+ onSequenceCancel?.();
1274
+ } else {
1275
+ setPendingKeys(newPending);
1276
+ matchStatesRef.current.clear();
1277
+ for (const combo of newPending) {
1278
+ for (const entry of parsedKeymapRef.current) {
1279
+ let state = matchStatesRef.current.get(entry.key);
1280
+ if (!state) {
1281
+ state = initMatchState(entry.keySeq);
780
1282
  }
781
- return [];
782
- });
783
- setIsAwaitingSequence(false);
784
- setTimeoutStartedAt(null);
785
- } else {
786
- setPendingKeys([]);
787
- setIsAwaitingSequence(false);
788
- setTimeoutStartedAt(null);
789
- onSequenceCancel?.();
1283
+ const result = advanceMatchState(state, entry.keySeq, combo);
1284
+ if (result.status === "partial") {
1285
+ matchStatesRef.current.set(entry.key, result.state);
1286
+ } else {
1287
+ matchStatesRef.current.delete(entry.key);
1288
+ }
1289
+ }
790
1290
  }
791
- timeoutRef.current = null;
792
- }, sequenceTimeout);
793
- if (preventDefault) {
794
- e.preventDefault();
795
1291
  }
796
1292
  return;
797
1293
  }
798
1294
  }
799
- if (pendingKeysRef.current.length > 0) {
800
- clearPending();
801
- onSequenceCancel?.();
1295
+ const newSequence = [...pendingKeysRef.current, currentCombo];
1296
+ const completeMatches = [];
1297
+ let hasPartials = false;
1298
+ const matchStates = matchStatesRef.current;
1299
+ const hadPartialMatches = matchStates.size > 0;
1300
+ for (const entry of parsedKeymapRef.current) {
1301
+ let state = matchStates.get(entry.key);
1302
+ if (hadPartialMatches && !state) {
1303
+ continue;
1304
+ }
1305
+ if (!state) {
1306
+ state = initMatchState(entry.keySeq);
1307
+ matchStates.set(entry.key, state);
1308
+ }
1309
+ const result = advanceMatchState(state, entry.keySeq, currentCombo);
1310
+ if (result.status === "matched") {
1311
+ completeMatches.push({
1312
+ key: entry.key,
1313
+ state: result.state,
1314
+ captures: result.captures
1315
+ });
1316
+ matchStates.delete(entry.key);
1317
+ } else if (result.status === "partial") {
1318
+ matchStates.set(entry.key, result.state);
1319
+ hasPartials = true;
1320
+ } else {
1321
+ matchStates.delete(entry.key);
1322
+ }
1323
+ }
1324
+ if (completeMatches.length === 1 && !hasPartials) {
1325
+ const match = completeMatches[0];
1326
+ if (tryExecuteKeySeq(match.key, match.captures, e)) {
1327
+ clearPending();
1328
+ return;
1329
+ }
1330
+ }
1331
+ if (completeMatches.length > 0 || hasPartials) {
1332
+ setPendingKeys(newSequence);
1333
+ setIsAwaitingSequence(true);
1334
+ if (pendingKeysRef.current.length === 0) {
1335
+ onSequenceStart?.(newSequence);
1336
+ } else {
1337
+ onSequenceProgress?.(newSequence);
1338
+ }
1339
+ if (preventDefault) {
1340
+ e.preventDefault();
1341
+ }
1342
+ if (Number.isFinite(sequenceTimeout)) {
1343
+ setTimeoutStartedAt(Date.now());
1344
+ timeoutRef.current = setTimeout(() => {
1345
+ for (const [key, state] of matchStates.entries()) {
1346
+ if (isCollectingDigits(state)) {
1347
+ const finalizedState = finalizeDigits(state);
1348
+ const entry = parsedKeymapRef.current.find((e2) => e2.key === key);
1349
+ if (entry) {
1350
+ const isComplete = finalizedState.every((elem) => {
1351
+ if (elem.type === "key") return elem.matched === true;
1352
+ if (elem.type === "digit") return elem.value !== void 0;
1353
+ if (elem.type === "digits") return elem.value !== void 0;
1354
+ return false;
1355
+ });
1356
+ if (isComplete) {
1357
+ void extractMatchCaptures(finalizedState);
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+ setPendingKeys([]);
1363
+ setIsAwaitingSequence(false);
1364
+ setTimeoutStartedAt(null);
1365
+ matchStatesRef.current.clear();
1366
+ onSequenceCancel?.();
1367
+ timeoutRef.current = null;
1368
+ }, sequenceTimeout);
1369
+ }
1370
+ return;
1371
+ }
1372
+ const exactMatch = tryExecute(newSequence, e);
1373
+ if (exactMatch) {
1374
+ clearPending();
1375
+ return;
1376
+ }
1377
+ if (hasPotentialMatch(newSequence)) {
1378
+ if (hasSequenceExtension(newSequence)) {
1379
+ setPendingKeys(newSequence);
1380
+ setIsAwaitingSequence(true);
1381
+ if (pendingKeysRef.current.length === 0) {
1382
+ onSequenceStart?.(newSequence);
1383
+ } else {
1384
+ onSequenceProgress?.(newSequence);
1385
+ }
1386
+ if (Number.isFinite(sequenceTimeout)) {
1387
+ setTimeoutStartedAt(Date.now());
1388
+ timeoutRef.current = setTimeout(() => {
1389
+ if (onTimeout === "submit") {
1390
+ setPendingKeys((current) => {
1391
+ if (current.length > 0) {
1392
+ onSequenceCancel?.();
1393
+ }
1394
+ return [];
1395
+ });
1396
+ setIsAwaitingSequence(false);
1397
+ setTimeoutStartedAt(null);
1398
+ } else {
1399
+ setPendingKeys([]);
1400
+ setIsAwaitingSequence(false);
1401
+ setTimeoutStartedAt(null);
1402
+ onSequenceCancel?.();
1403
+ }
1404
+ timeoutRef.current = null;
1405
+ }, sequenceTimeout);
1406
+ }
1407
+ if (preventDefault) {
1408
+ e.preventDefault();
1409
+ }
1410
+ return;
1411
+ }
1412
+ }
1413
+ if (pendingKeysRef.current.length > 0) {
1414
+ setPendingKeys(newSequence);
1415
+ if (preventDefault) {
1416
+ e.preventDefault();
1417
+ }
1418
+ return;
802
1419
  }
803
1420
  const singleMatch = tryExecute([currentCombo], e);
804
1421
  if (!singleMatch) {
@@ -809,21 +1426,23 @@ function useHotkeys(keymap, handlers, options = {}) {
809
1426
  if (preventDefault) {
810
1427
  e.preventDefault();
811
1428
  }
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);
1429
+ if (Number.isFinite(sequenceTimeout)) {
1430
+ setTimeoutStartedAt(Date.now());
1431
+ timeoutRef.current = setTimeout(() => {
1432
+ if (onTimeout === "submit") {
1433
+ setPendingKeys([]);
1434
+ setIsAwaitingSequence(false);
1435
+ setTimeoutStartedAt(null);
1436
+ onSequenceCancel?.();
1437
+ } else {
1438
+ setPendingKeys([]);
1439
+ setIsAwaitingSequence(false);
1440
+ setTimeoutStartedAt(null);
1441
+ onSequenceCancel?.();
1442
+ }
1443
+ timeoutRef.current = null;
1444
+ }, sequenceTimeout);
1445
+ }
827
1446
  }
828
1447
  }
829
1448
  };
@@ -845,6 +1464,7 @@ function useHotkeys(keymap, handlers, options = {}) {
845
1464
  clearPending,
846
1465
  cancelSequence,
847
1466
  tryExecute,
1467
+ tryExecuteKeySeq,
848
1468
  hasPotentialMatch,
849
1469
  hasSequenceExtension,
850
1470
  onSequenceStart,
@@ -856,12 +1476,11 @@ function useHotkeys(keymap, handlers, options = {}) {
856
1476
  var HotkeysContext = createContext(null);
857
1477
  var DEFAULT_CONFIG = {
858
1478
  storageKey: "use-kbd",
859
- sequenceTimeout: 1e3,
860
- disableConflicts: true,
1479
+ sequenceTimeout: DEFAULT_SEQUENCE_TIMEOUT,
1480
+ disableConflicts: false,
1481
+ // Keep conflicting bindings active; SeqM handles disambiguation
861
1482
  minViewportWidth: 768,
862
- enableOnTouch: false,
863
- modalTrigger: "?",
864
- omnibarTrigger: "meta+k"
1483
+ enableOnTouch: false
865
1484
  };
866
1485
  function HotkeysProvider({
867
1486
  config: configProp = {},
@@ -872,6 +1491,7 @@ function HotkeysProvider({
872
1491
  ...configProp
873
1492
  }), [configProp]);
874
1493
  const registry = useActionsRegistry({ storageKey: config.storageKey });
1494
+ const endpointsRegistry = useOmnibarEndpointsRegistry();
875
1495
  const [isEnabled, setIsEnabled] = useState(true);
876
1496
  useEffect(() => {
877
1497
  if (typeof window === "undefined") return;
@@ -910,16 +1530,12 @@ function HotkeysProvider({
910
1530
  const openOmnibar = useCallback(() => setIsOmnibarOpen(true), []);
911
1531
  const closeOmnibar = useCallback(() => setIsOmnibarOpen(false), []);
912
1532
  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]);
1533
+ const [isLookupOpen, setIsLookupOpen] = useState(false);
1534
+ const openLookup = useCallback(() => setIsLookupOpen(true), []);
1535
+ const closeLookup = useCallback(() => setIsLookupOpen(false), []);
1536
+ const toggleLookup = useCallback(() => setIsLookupOpen((prev) => !prev), []);
1537
+ const [isEditingBinding, setIsEditingBinding] = useState(false);
1538
+ const keymap = registry.keymap;
923
1539
  const conflicts = useMemo(() => findConflicts(keymap), [keymap]);
924
1540
  const hasConflicts2 = conflicts.size > 0;
925
1541
  const effectiveKeymap = useMemo(() => {
@@ -939,20 +1555,24 @@ function HotkeysProvider({
939
1555
  for (const [id, action] of registry.actions) {
940
1556
  map[id] = action.config.handler;
941
1557
  }
942
- map["__hotkeys:modal"] = toggleModal;
943
- map["__hotkeys:omnibar"] = toggleOmnibar;
944
1558
  return map;
945
- }, [registry.actions, toggleModal, toggleOmnibar]);
946
- const hotkeysEnabled = isEnabled && !isModalOpen && !isOmnibarOpen;
1559
+ }, [registry.actions]);
1560
+ const hotkeysEnabled = isEnabled && !isEditingBinding && !isOmnibarOpen && !isLookupOpen;
947
1561
  const {
948
1562
  pendingKeys,
949
1563
  isAwaitingSequence,
1564
+ cancelSequence,
950
1565
  timeoutStartedAt: sequenceTimeoutStartedAt,
951
1566
  sequenceTimeout
952
1567
  } = useHotkeys(effectiveKeymap, handlers, {
953
1568
  enabled: hotkeysEnabled,
954
1569
  sequenceTimeout: config.sequenceTimeout
955
1570
  });
1571
+ useEffect(() => {
1572
+ if (isAwaitingSequence && isModalOpen) {
1573
+ closeModal();
1574
+ }
1575
+ }, [isAwaitingSequence, isModalOpen, closeModal]);
956
1576
  const searchActionsHelper = useCallback(
957
1577
  (query) => searchActions(query, registry.actionRegistry, keymap),
958
1578
  [registry.actionRegistry, keymap]
@@ -963,6 +1583,7 @@ function HotkeysProvider({
963
1583
  );
964
1584
  const value = useMemo(() => ({
965
1585
  registry,
1586
+ endpointsRegistry,
966
1587
  isEnabled,
967
1588
  isModalOpen,
968
1589
  openModal,
@@ -972,9 +1593,16 @@ function HotkeysProvider({
972
1593
  openOmnibar,
973
1594
  closeOmnibar,
974
1595
  toggleOmnibar,
1596
+ isLookupOpen,
1597
+ openLookup,
1598
+ closeLookup,
1599
+ toggleLookup,
1600
+ isEditingBinding,
1601
+ setIsEditingBinding,
975
1602
  executeAction: registry.execute,
976
1603
  pendingKeys,
977
1604
  isAwaitingSequence,
1605
+ cancelSequence,
978
1606
  sequenceTimeoutStartedAt,
979
1607
  sequenceTimeout,
980
1608
  conflicts,
@@ -983,6 +1611,7 @@ function HotkeysProvider({
983
1611
  getCompletions
984
1612
  }), [
985
1613
  registry,
1614
+ endpointsRegistry,
986
1615
  isEnabled,
987
1616
  isModalOpen,
988
1617
  openModal,
@@ -992,8 +1621,14 @@ function HotkeysProvider({
992
1621
  openOmnibar,
993
1622
  closeOmnibar,
994
1623
  toggleOmnibar,
1624
+ isLookupOpen,
1625
+ openLookup,
1626
+ closeLookup,
1627
+ toggleLookup,
1628
+ isEditingBinding,
995
1629
  pendingKeys,
996
1630
  isAwaitingSequence,
1631
+ cancelSequence,
997
1632
  sequenceTimeoutStartedAt,
998
1633
  sequenceTimeout,
999
1634
  conflicts,
@@ -1001,7 +1636,7 @@ function HotkeysProvider({
1001
1636
  searchActionsHelper,
1002
1637
  getCompletions
1003
1638
  ]);
1004
- return /* @__PURE__ */ jsx(ActionsRegistryContext.Provider, { value: registry, children: /* @__PURE__ */ jsx(HotkeysContext.Provider, { value, children }) });
1639
+ return /* @__PURE__ */ jsx(ActionsRegistryContext.Provider, { value: registry, children: /* @__PURE__ */ jsx(OmnibarEndpointsRegistryContext.Provider, { value: endpointsRegistry, children: /* @__PURE__ */ jsx(HotkeysContext.Provider, { value, children }) }) });
1005
1640
  }
1006
1641
  function useHotkeysContext() {
1007
1642
  const context = useContext(HotkeysContext);
@@ -1027,9 +1662,9 @@ function useAction(id, config) {
1027
1662
  useEffect(() => {
1028
1663
  registryRef.current.register(id, {
1029
1664
  ...config,
1030
- handler: () => {
1665
+ handler: (e, captures) => {
1031
1666
  if (enabledRef.current) {
1032
- handlerRef.current();
1667
+ handlerRef.current(e, captures);
1033
1668
  }
1034
1669
  }
1035
1670
  });
@@ -1063,9 +1698,9 @@ function useActions(actions) {
1063
1698
  for (const [id, config] of Object.entries(actions)) {
1064
1699
  registryRef.current.register(id, {
1065
1700
  ...config,
1066
- handler: () => {
1701
+ handler: (e, captures) => {
1067
1702
  if (enabledRef.current[id]) {
1068
- handlersRef.current[id]?.();
1703
+ handlersRef.current[id]?.(e, captures);
1069
1704
  }
1070
1705
  }
1071
1706
  });
@@ -1089,6 +1724,38 @@ function useActions(actions) {
1089
1724
  )
1090
1725
  ]);
1091
1726
  }
1727
+ function useOmnibarEndpoint(id, config) {
1728
+ const registry = useContext(OmnibarEndpointsRegistryContext);
1729
+ if (!registry) {
1730
+ throw new Error("useOmnibarEndpoint must be used within a HotkeysProvider");
1731
+ }
1732
+ const registryRef = useRef(registry);
1733
+ registryRef.current = registry;
1734
+ const fetchRef = useRef(config.fetch);
1735
+ fetchRef.current = config.fetch;
1736
+ const enabledRef = useRef(config.enabled ?? true);
1737
+ enabledRef.current = config.enabled ?? true;
1738
+ useEffect(() => {
1739
+ registryRef.current.register(id, {
1740
+ ...config,
1741
+ fetch: async (query, signal, pagination) => {
1742
+ if (!enabledRef.current) return { entries: [] };
1743
+ return fetchRef.current(query, signal, pagination);
1744
+ }
1745
+ });
1746
+ return () => {
1747
+ registryRef.current.unregister(id);
1748
+ };
1749
+ }, [
1750
+ id,
1751
+ config.group,
1752
+ config.priority,
1753
+ config.minQueryLength,
1754
+ config.pageSize,
1755
+ config.pagination
1756
+ // Note: we use refs for fetch and enabled, so they don't cause re-registration
1757
+ ]);
1758
+ }
1092
1759
  function useEventCallback(fn) {
1093
1760
  const ref = useRef(fn);
1094
1761
  ref.current = fn;
@@ -1101,7 +1768,7 @@ function useRecordHotkey(options = {}) {
1101
1768
  onTab: onTabProp,
1102
1769
  onShiftTab: onShiftTabProp,
1103
1770
  preventDefault = true,
1104
- sequenceTimeout = 1e3,
1771
+ sequenceTimeout = DEFAULT_SEQUENCE_TIMEOUT,
1105
1772
  pauseTimeout = false
1106
1773
  } = options;
1107
1774
  const onCapture = useEventCallback(onCaptureProp);
@@ -1119,6 +1786,7 @@ function useRecordHotkey(options = {}) {
1119
1786
  const pauseTimeoutRef = useRef(pauseTimeout);
1120
1787
  pauseTimeoutRef.current = pauseTimeout;
1121
1788
  const pendingKeysRef = useRef([]);
1789
+ const hashCycleRef = useRef(0);
1122
1790
  const clearTimeout_ = useCallback(() => {
1123
1791
  if (timeoutRef.current) {
1124
1792
  clearTimeout(timeoutRef.current);
@@ -1148,6 +1816,7 @@ function useRecordHotkey(options = {}) {
1148
1816
  pressedKeysRef.current.clear();
1149
1817
  hasNonModifierRef.current = false;
1150
1818
  currentComboRef.current = null;
1819
+ hashCycleRef.current = 0;
1151
1820
  onCancel?.();
1152
1821
  }, [clearTimeout_, onCancel]);
1153
1822
  const commit = useCallback(() => {
@@ -1168,6 +1837,7 @@ function useRecordHotkey(options = {}) {
1168
1837
  pressedKeysRef.current.clear();
1169
1838
  hasNonModifierRef.current = false;
1170
1839
  currentComboRef.current = null;
1840
+ hashCycleRef.current = 0;
1171
1841
  return cancel;
1172
1842
  }, [cancel, clearTimeout_]);
1173
1843
  useEffect(() => {
@@ -1178,9 +1848,13 @@ function useRecordHotkey(options = {}) {
1178
1848
  }
1179
1849
  } else if (isRecording && pendingKeysRef.current.length > 0 && !timeoutRef.current) {
1180
1850
  const currentSequence = pendingKeysRef.current;
1181
- timeoutRef.current = setTimeout(() => {
1851
+ if (sequenceTimeout === 0) {
1182
1852
  submit(currentSequence);
1183
- }, sequenceTimeout);
1853
+ } else if (Number.isFinite(sequenceTimeout)) {
1854
+ timeoutRef.current = setTimeout(() => {
1855
+ submit(currentSequence);
1856
+ }, sequenceTimeout);
1857
+ }
1184
1858
  }
1185
1859
  }, [pauseTimeout, isRecording, sequenceTimeout, submit]);
1186
1860
  useEffect(() => {
@@ -1239,22 +1913,23 @@ function useRecordHotkey(options = {}) {
1239
1913
  key = e.code.slice(5);
1240
1914
  }
1241
1915
  pressedKeysRef.current.add(key);
1916
+ let nonModifierKey = "";
1917
+ for (const k of pressedKeysRef.current) {
1918
+ if (!isModifierKey(k)) {
1919
+ nonModifierKey = normalizeKey(k);
1920
+ hasNonModifierRef.current = true;
1921
+ break;
1922
+ }
1923
+ }
1242
1924
  const combo = {
1243
- key: "",
1925
+ key: nonModifierKey,
1244
1926
  modifiers: {
1245
1927
  ctrl: e.ctrlKey,
1246
1928
  alt: e.altKey,
1247
- shift: e.shiftKey,
1929
+ shift: e.shiftKey && !isShiftedSymbol(nonModifierKey),
1248
1930
  meta: e.metaKey
1249
1931
  }
1250
1932
  };
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
1933
  if (combo.key) {
1259
1934
  currentComboRef.current = combo;
1260
1935
  setActiveKeys(combo);
@@ -1279,16 +1954,41 @@ function useRecordHotkey(options = {}) {
1279
1954
  pressedKeysRef.current.delete(key);
1280
1955
  const shouldComplete = pressedKeysRef.current.size === 0 || e.key === "Meta" && hasNonModifierRef.current;
1281
1956
  if (shouldComplete && hasNonModifierRef.current && currentComboRef.current) {
1282
- const combo = currentComboRef.current;
1957
+ let combo = currentComboRef.current;
1283
1958
  pressedKeysRef.current.clear();
1284
1959
  hasNonModifierRef.current = false;
1285
1960
  currentComboRef.current = null;
1286
1961
  setActiveKeys(null);
1287
- const newSequence = [...pendingKeysRef.current, combo];
1962
+ let newSequence;
1963
+ const noModifiers = !combo.modifiers.ctrl && !combo.modifiers.alt && !combo.modifiers.meta && !combo.modifiers.shift;
1964
+ if (combo.key === "#" && noModifiers) {
1965
+ const pending = pendingKeysRef.current;
1966
+ const lastCombo = pending[pending.length - 1];
1967
+ if (hashCycleRef.current === 0) {
1968
+ combo = { key: DIGIT_PLACEHOLDER, modifiers: { ctrl: false, alt: false, shift: false, meta: false } };
1969
+ newSequence = [...pending, combo];
1970
+ hashCycleRef.current = 1;
1971
+ } else if (hashCycleRef.current === 1 && lastCombo?.key === DIGIT_PLACEHOLDER) {
1972
+ newSequence = [...pending.slice(0, -1), { key: DIGITS_PLACEHOLDER, modifiers: { ctrl: false, alt: false, shift: false, meta: false } }];
1973
+ hashCycleRef.current = 2;
1974
+ } else if (hashCycleRef.current === 2 && lastCombo?.key === DIGITS_PLACEHOLDER) {
1975
+ newSequence = [...pending.slice(0, -1), { key: "#", modifiers: { ctrl: false, alt: false, shift: false, meta: false } }];
1976
+ hashCycleRef.current = 3;
1977
+ } else {
1978
+ combo = { key: DIGIT_PLACEHOLDER, modifiers: { ctrl: false, alt: false, shift: false, meta: false } };
1979
+ newSequence = [...pending, combo];
1980
+ hashCycleRef.current = 1;
1981
+ }
1982
+ } else {
1983
+ hashCycleRef.current = 0;
1984
+ newSequence = [...pendingKeysRef.current, combo];
1985
+ }
1288
1986
  pendingKeysRef.current = newSequence;
1289
1987
  setPendingKeys(newSequence);
1290
1988
  clearTimeout_();
1291
- if (!pauseTimeoutRef.current) {
1989
+ if (sequenceTimeout === 0) {
1990
+ submit(newSequence);
1991
+ } else if (!pauseTimeoutRef.current && Number.isFinite(sequenceTimeout)) {
1292
1992
  timeoutRef.current = setTimeout(() => {
1293
1993
  submit(newSequence);
1294
1994
  }, sequenceTimeout);
@@ -1304,7 +2004,6 @@ function useRecordHotkey(options = {}) {
1304
2004
  };
1305
2005
  }, [isRecording, preventDefault, sequenceTimeout, clearTimeout_, submit, cancel, onCapture, onTab, onShiftTab]);
1306
2006
  const display = sequence ? formatCombination(sequence) : null;
1307
- const combination = sequence && sequence.length > 0 ? sequence[0] : null;
1308
2007
  return {
1309
2008
  isRecording,
1310
2009
  startRecording,
@@ -1314,12 +2013,11 @@ function useRecordHotkey(options = {}) {
1314
2013
  display,
1315
2014
  pendingKeys,
1316
2015
  activeKeys,
1317
- combination
1318
- // deprecated
2016
+ sequenceTimeout
1319
2017
  };
1320
2018
  }
1321
2019
  function useEditableHotkeys(defaults, handlers, options = {}) {
1322
- const { storageKey, disableConflicts = true, ...hotkeyOptions } = options;
2020
+ const { storageKey, disableConflicts = false, ...hotkeyOptions } = options;
1323
2021
  const [overrides, setOverrides] = useState(() => {
1324
2022
  if (!storageKey || typeof window === "undefined") return {};
1325
2023
  try {
@@ -1414,6 +2112,8 @@ function useEditableHotkeys(defaults, handlers, options = {}) {
1414
2112
  sequenceTimeout
1415
2113
  };
1416
2114
  }
2115
+ var { max: max2, min } = Math;
2116
+ var DEFAULT_DEBOUNCE_MS = 150;
1417
2117
  function useOmnibar(options) {
1418
2118
  const {
1419
2119
  actions,
@@ -1422,17 +2122,27 @@ function useOmnibar(options) {
1422
2122
  openKey = "meta+k",
1423
2123
  enabled = true,
1424
2124
  onExecute,
2125
+ onExecuteRemote,
1425
2126
  onOpen,
1426
2127
  onClose,
1427
- maxResults = 10
2128
+ maxResults = 10,
2129
+ endpointsRegistry,
2130
+ debounceMs = DEFAULT_DEBOUNCE_MS
1428
2131
  } = options;
1429
2132
  const [isOpen, setIsOpen] = useState(false);
1430
2133
  const [query, setQuery] = useState("");
1431
2134
  const [selectedIndex, setSelectedIndex] = useState(0);
2135
+ const [endpointStates, setEndpointStates] = useState(/* @__PURE__ */ new Map());
1432
2136
  const handlersRef = useRef(handlers);
1433
2137
  handlersRef.current = handlers;
1434
2138
  const onExecuteRef = useRef(onExecute);
1435
2139
  onExecuteRef.current = onExecute;
2140
+ const onExecuteRemoteRef = useRef(onExecuteRemote);
2141
+ onExecuteRemoteRef.current = onExecuteRemote;
2142
+ const abortControllerRef = useRef(null);
2143
+ const debounceTimerRef = useRef(null);
2144
+ const currentQueryRef = useRef(query);
2145
+ currentQueryRef.current = query;
1436
2146
  const omnibarKeymap = useMemo(() => {
1437
2147
  if (!enabled) return {};
1438
2148
  return { [openKey]: "omnibar:toggle" };
@@ -1458,12 +2168,189 @@ function useOmnibar(options) {
1458
2168
  const allResults = searchActions(query, actions, keymap);
1459
2169
  return allResults.slice(0, maxResults);
1460
2170
  }, [query, actions, keymap, maxResults]);
2171
+ useEffect(() => {
2172
+ if (debounceTimerRef.current) {
2173
+ clearTimeout(debounceTimerRef.current);
2174
+ debounceTimerRef.current = null;
2175
+ }
2176
+ if (abortControllerRef.current) {
2177
+ abortControllerRef.current.abort();
2178
+ abortControllerRef.current = null;
2179
+ }
2180
+ if (!endpointsRegistry || !query.trim()) {
2181
+ setEndpointStates(/* @__PURE__ */ new Map());
2182
+ return;
2183
+ }
2184
+ setEndpointStates((prev) => {
2185
+ const next = new Map(prev);
2186
+ for (const [id] of endpointsRegistry.endpoints) {
2187
+ next.set(id, { entries: [], offset: 0, isLoading: true });
2188
+ }
2189
+ return next;
2190
+ });
2191
+ debounceTimerRef.current = setTimeout(async () => {
2192
+ const controller = new AbortController();
2193
+ abortControllerRef.current = controller;
2194
+ try {
2195
+ const endpointResults = await endpointsRegistry.queryAll(query, controller.signal);
2196
+ if (controller.signal.aborted) return;
2197
+ setEndpointStates(() => {
2198
+ const next = /* @__PURE__ */ new Map();
2199
+ for (const epResult of endpointResults) {
2200
+ const ep = endpointsRegistry.endpoints.get(epResult.endpointId);
2201
+ const pageSize = ep?.config.pageSize ?? 10;
2202
+ next.set(epResult.endpointId, {
2203
+ entries: epResult.entries,
2204
+ offset: pageSize,
2205
+ total: epResult.total,
2206
+ hasMore: epResult.hasMore ?? (epResult.total !== void 0 ? epResult.entries.length < epResult.total : void 0),
2207
+ isLoading: false
2208
+ });
2209
+ }
2210
+ return next;
2211
+ });
2212
+ } catch (error) {
2213
+ if (error instanceof Error && error.name === "AbortError") return;
2214
+ console.error("Omnibar endpoint query failed:", error);
2215
+ setEndpointStates((prev) => {
2216
+ const next = new Map(prev);
2217
+ for (const [id, state] of next) {
2218
+ next.set(id, { ...state, isLoading: false });
2219
+ }
2220
+ return next;
2221
+ });
2222
+ }
2223
+ }, debounceMs);
2224
+ return () => {
2225
+ if (debounceTimerRef.current) {
2226
+ clearTimeout(debounceTimerRef.current);
2227
+ }
2228
+ if (abortControllerRef.current) {
2229
+ abortControllerRef.current.abort();
2230
+ }
2231
+ };
2232
+ }, [query, endpointsRegistry, debounceMs]);
2233
+ const loadMore = useCallback(async (endpointId) => {
2234
+ if (!endpointsRegistry) return;
2235
+ const currentState = endpointStates.get(endpointId);
2236
+ if (!currentState || currentState.isLoading) return;
2237
+ if (currentState.hasMore === false) return;
2238
+ const ep = endpointsRegistry.endpoints.get(endpointId);
2239
+ if (!ep) return;
2240
+ const pageSize = ep.config.pageSize ?? 10;
2241
+ setEndpointStates((prev) => {
2242
+ const next = new Map(prev);
2243
+ const state = next.get(endpointId);
2244
+ if (state) {
2245
+ next.set(endpointId, { ...state, isLoading: true });
2246
+ }
2247
+ return next;
2248
+ });
2249
+ try {
2250
+ const controller = new AbortController();
2251
+ const result = await endpointsRegistry.queryEndpoint(
2252
+ endpointId,
2253
+ currentQueryRef.current,
2254
+ { offset: currentState.offset, limit: pageSize },
2255
+ controller.signal
2256
+ );
2257
+ if (!result) return;
2258
+ setEndpointStates((prev) => {
2259
+ const next = new Map(prev);
2260
+ const state = next.get(endpointId);
2261
+ if (state) {
2262
+ next.set(endpointId, {
2263
+ entries: [...state.entries, ...result.entries],
2264
+ offset: state.offset + pageSize,
2265
+ total: result.total ?? state.total,
2266
+ hasMore: result.hasMore ?? (result.total !== void 0 ? state.entries.length + result.entries.length < result.total : void 0),
2267
+ isLoading: false
2268
+ });
2269
+ }
2270
+ return next;
2271
+ });
2272
+ } catch (error) {
2273
+ if (error instanceof Error && error.name === "AbortError") return;
2274
+ console.error(`Omnibar loadMore failed for ${endpointId}:`, error);
2275
+ setEndpointStates((prev) => {
2276
+ const next = new Map(prev);
2277
+ const state = next.get(endpointId);
2278
+ if (state) {
2279
+ next.set(endpointId, { ...state, isLoading: false });
2280
+ }
2281
+ return next;
2282
+ });
2283
+ }
2284
+ }, [endpointsRegistry, endpointStates]);
2285
+ const remoteResults = useMemo(() => {
2286
+ if (!endpointsRegistry) return [];
2287
+ const processed = [];
2288
+ for (const [endpointId, state] of endpointStates) {
2289
+ const endpoint = endpointsRegistry.endpoints.get(endpointId);
2290
+ const priority = endpoint?.config.priority ?? 0;
2291
+ for (const entry of state.entries) {
2292
+ const labelMatch = fuzzyMatch(query, entry.label);
2293
+ const descMatch = entry.description ? fuzzyMatch(query, entry.description) : null;
2294
+ const keywordsMatch = entry.keywords?.map((k) => fuzzyMatch(query, k)) ?? [];
2295
+ let score = 0;
2296
+ let labelMatches = [];
2297
+ if (labelMatch.matched) {
2298
+ score = Math.max(score, labelMatch.score * 3);
2299
+ labelMatches = labelMatch.ranges;
2300
+ }
2301
+ if (descMatch?.matched) {
2302
+ score = Math.max(score, descMatch.score * 1.5);
2303
+ }
2304
+ for (const km of keywordsMatch) {
2305
+ if (km.matched) {
2306
+ score = Math.max(score, km.score * 2);
2307
+ }
2308
+ }
2309
+ processed.push({
2310
+ id: `${endpointId}:${entry.id}`,
2311
+ entry,
2312
+ endpointId,
2313
+ priority,
2314
+ score: score || 1,
2315
+ labelMatches
2316
+ });
2317
+ }
2318
+ }
2319
+ processed.sort((a, b) => {
2320
+ if (a.priority !== b.priority) return b.priority - a.priority;
2321
+ return b.score - a.score;
2322
+ });
2323
+ return processed;
2324
+ }, [endpointStates, endpointsRegistry, query]);
2325
+ const isLoadingRemote = useMemo(() => {
2326
+ for (const [, state] of endpointStates) {
2327
+ if (state.isLoading) return true;
2328
+ }
2329
+ return false;
2330
+ }, [endpointStates]);
2331
+ const endpointPagination = useMemo(() => {
2332
+ const info = /* @__PURE__ */ new Map();
2333
+ if (!endpointsRegistry) return info;
2334
+ for (const [endpointId, state] of endpointStates) {
2335
+ const ep = endpointsRegistry.endpoints.get(endpointId);
2336
+ info.set(endpointId, {
2337
+ endpointId,
2338
+ loaded: state.entries.length,
2339
+ total: state.total,
2340
+ hasMore: state.hasMore ?? false,
2341
+ isLoading: state.isLoading,
2342
+ mode: ep?.config.pagination ?? "none"
2343
+ });
2344
+ }
2345
+ return info;
2346
+ }, [endpointStates, endpointsRegistry]);
2347
+ const totalResults = results.length + remoteResults.length;
1461
2348
  const completions = useMemo(() => {
1462
2349
  return getSequenceCompletions(pendingKeys, keymap);
1463
2350
  }, [pendingKeys, keymap]);
1464
2351
  useEffect(() => {
1465
2352
  setSelectedIndex(0);
1466
- }, [results]);
2353
+ }, [results, remoteResults]);
1467
2354
  const open = useCallback(() => {
1468
2355
  setIsOpen(true);
1469
2356
  setQuery("");
@@ -1490,24 +2377,56 @@ function useOmnibar(options) {
1490
2377
  });
1491
2378
  }, [onOpen, onClose]);
1492
2379
  const selectNext = useCallback(() => {
1493
- setSelectedIndex((prev) => min(prev + 1, results.length - 1));
1494
- }, [results.length]);
2380
+ setSelectedIndex((prev) => min(prev + 1, totalResults - 1));
2381
+ }, [totalResults]);
1495
2382
  const selectPrev = useCallback(() => {
1496
- setSelectedIndex((prev) => max(prev - 1, 0));
2383
+ setSelectedIndex((prev) => max2(prev - 1, 0));
1497
2384
  }, []);
1498
2385
  const resetSelection = useCallback(() => {
1499
2386
  setSelectedIndex(0);
1500
2387
  }, []);
1501
2388
  const execute = useCallback((actionId) => {
1502
- const id = actionId ?? results[selectedIndex]?.id;
1503
- if (!id) return;
1504
- close();
1505
- if (handlersRef.current?.[id]) {
1506
- const event = new KeyboardEvent("keydown", { key: "Enter" });
1507
- handlersRef.current[id](event);
1508
- }
1509
- onExecuteRef.current?.(id);
1510
- }, [results, selectedIndex, close]);
2389
+ const localCount = results.length;
2390
+ if (actionId) {
2391
+ const remoteResult = remoteResults.find((r) => r.id === actionId);
2392
+ if (remoteResult) {
2393
+ close();
2394
+ const entry = remoteResult.entry;
2395
+ if ("handler" in entry && entry.handler) {
2396
+ entry.handler();
2397
+ }
2398
+ onExecuteRemoteRef.current?.(entry);
2399
+ return;
2400
+ }
2401
+ close();
2402
+ if (handlersRef.current?.[actionId]) {
2403
+ const event = new KeyboardEvent("keydown", { key: "Enter" });
2404
+ handlersRef.current[actionId](event);
2405
+ }
2406
+ onExecuteRef.current?.(actionId);
2407
+ return;
2408
+ }
2409
+ if (selectedIndex < localCount) {
2410
+ const id = results[selectedIndex]?.id;
2411
+ if (!id) return;
2412
+ close();
2413
+ if (handlersRef.current?.[id]) {
2414
+ const event = new KeyboardEvent("keydown", { key: "Enter" });
2415
+ handlersRef.current[id](event);
2416
+ }
2417
+ onExecuteRef.current?.(id);
2418
+ } else {
2419
+ const remoteIndex = selectedIndex - localCount;
2420
+ const remoteResult = remoteResults[remoteIndex];
2421
+ if (!remoteResult) return;
2422
+ close();
2423
+ const entry = remoteResult.entry;
2424
+ if ("handler" in entry && entry.handler) {
2425
+ entry.handler();
2426
+ }
2427
+ onExecuteRemoteRef.current?.(entry);
2428
+ }
2429
+ }, [results, remoteResults, selectedIndex, close]);
1511
2430
  useEffect(() => {
1512
2431
  if (!isOpen) return;
1513
2432
  const handleKeyDown = (e) => {
@@ -1549,7 +2468,12 @@ function useOmnibar(options) {
1549
2468
  query,
1550
2469
  setQuery,
1551
2470
  results,
2471
+ remoteResults,
2472
+ isLoadingRemote,
2473
+ endpointPagination,
2474
+ loadMore,
1552
2475
  selectedIndex,
2476
+ totalResults,
1553
2477
  selectNext,
1554
2478
  selectPrev,
1555
2479
  execute,
@@ -1559,6 +2483,400 @@ function useOmnibar(options) {
1559
2483
  isAwaitingSequence
1560
2484
  };
1561
2485
  }
2486
+ var baseStyle = {
2487
+ width: "1em",
2488
+ height: "1em",
2489
+ verticalAlign: "middle"
2490
+ };
2491
+ function Up({ className, style }) {
2492
+ return /* @__PURE__ */ jsx(
2493
+ "svg",
2494
+ {
2495
+ className,
2496
+ style: { ...baseStyle, ...style },
2497
+ viewBox: "0 0 24 24",
2498
+ fill: "none",
2499
+ stroke: "currentColor",
2500
+ strokeWidth: "3",
2501
+ strokeLinecap: "round",
2502
+ strokeLinejoin: "round",
2503
+ children: /* @__PURE__ */ jsx("path", { d: "M12 19V5M5 12l7-7 7 7" })
2504
+ }
2505
+ );
2506
+ }
2507
+ function Down({ className, style }) {
2508
+ return /* @__PURE__ */ jsx(
2509
+ "svg",
2510
+ {
2511
+ className,
2512
+ style: { ...baseStyle, ...style },
2513
+ viewBox: "0 0 24 24",
2514
+ fill: "none",
2515
+ stroke: "currentColor",
2516
+ strokeWidth: "3",
2517
+ strokeLinecap: "round",
2518
+ strokeLinejoin: "round",
2519
+ children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M5 12l7 7 7-7" })
2520
+ }
2521
+ );
2522
+ }
2523
+ function Left({ className, style }) {
2524
+ return /* @__PURE__ */ jsx(
2525
+ "svg",
2526
+ {
2527
+ className,
2528
+ style: { ...baseStyle, ...style },
2529
+ viewBox: "0 0 24 24",
2530
+ fill: "none",
2531
+ stroke: "currentColor",
2532
+ strokeWidth: "3",
2533
+ strokeLinecap: "round",
2534
+ strokeLinejoin: "round",
2535
+ children: /* @__PURE__ */ jsx("path", { d: "M19 12H5M12 5l-7 7 7 7" })
2536
+ }
2537
+ );
2538
+ }
2539
+ function Right({ className, style }) {
2540
+ return /* @__PURE__ */ jsx(
2541
+ "svg",
2542
+ {
2543
+ className,
2544
+ style: { ...baseStyle, ...style },
2545
+ viewBox: "0 0 24 24",
2546
+ fill: "none",
2547
+ stroke: "currentColor",
2548
+ strokeWidth: "3",
2549
+ strokeLinecap: "round",
2550
+ strokeLinejoin: "round",
2551
+ children: /* @__PURE__ */ jsx("path", { d: "M5 12h14M12 5l7 7-7 7" })
2552
+ }
2553
+ );
2554
+ }
2555
+ function Enter({ className, style }) {
2556
+ return /* @__PURE__ */ jsxs(
2557
+ "svg",
2558
+ {
2559
+ className,
2560
+ style: { ...baseStyle, ...style },
2561
+ viewBox: "0 0 24 24",
2562
+ fill: "none",
2563
+ stroke: "currentColor",
2564
+ strokeWidth: "3",
2565
+ strokeLinecap: "round",
2566
+ strokeLinejoin: "round",
2567
+ children: [
2568
+ /* @__PURE__ */ jsx("path", { d: "M9 10l-4 4 4 4" }),
2569
+ /* @__PURE__ */ jsx("path", { d: "M19 6v8a2 2 0 01-2 2H5" })
2570
+ ]
2571
+ }
2572
+ );
2573
+ }
2574
+ function Backspace({ className, style }) {
2575
+ return /* @__PURE__ */ jsxs(
2576
+ "svg",
2577
+ {
2578
+ className,
2579
+ style: { ...baseStyle, ...style },
2580
+ viewBox: "0 0 24 24",
2581
+ fill: "none",
2582
+ stroke: "currentColor",
2583
+ strokeWidth: "2",
2584
+ strokeLinecap: "round",
2585
+ strokeLinejoin: "round",
2586
+ children: [
2587
+ /* @__PURE__ */ jsx("path", { d: "M21 4H8l-7 8 7 8h13a2 2 0 002-2V6a2 2 0 00-2-2z" }),
2588
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "9", x2: "12", y2: "15" }),
2589
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "9", x2: "18", y2: "15" })
2590
+ ]
2591
+ }
2592
+ );
2593
+ }
2594
+ function Tab({ className, style }) {
2595
+ return /* @__PURE__ */ jsxs(
2596
+ "svg",
2597
+ {
2598
+ className,
2599
+ style: { ...baseStyle, ...style },
2600
+ viewBox: "0 0 24 24",
2601
+ fill: "none",
2602
+ stroke: "currentColor",
2603
+ strokeWidth: "2",
2604
+ strokeLinecap: "round",
2605
+ strokeLinejoin: "round",
2606
+ children: [
2607
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "16", y2: "12" }),
2608
+ /* @__PURE__ */ jsx("polyline", { points: "12 8 16 12 12 16" }),
2609
+ /* @__PURE__ */ jsx("line", { x1: "20", y1: "6", x2: "20", y2: "18" })
2610
+ ]
2611
+ }
2612
+ );
2613
+ }
2614
+ function getKeyIcon(key) {
2615
+ switch (key.toLowerCase()) {
2616
+ case "arrowup":
2617
+ return Up;
2618
+ case "arrowdown":
2619
+ return Down;
2620
+ case "arrowleft":
2621
+ return Left;
2622
+ case "arrowright":
2623
+ return Right;
2624
+ case "enter":
2625
+ return Enter;
2626
+ case "backspace":
2627
+ return Backspace;
2628
+ case "tab":
2629
+ return Tab;
2630
+ default:
2631
+ return null;
2632
+ }
2633
+ }
2634
+ var baseStyle2 = {
2635
+ width: "1.2em",
2636
+ height: "1.2em",
2637
+ marginRight: "2px",
2638
+ verticalAlign: "middle"
2639
+ };
2640
+ var wideStyle = {
2641
+ ...baseStyle2,
2642
+ width: "1.4em"
2643
+ };
2644
+ var Command = forwardRef(
2645
+ ({ className, style, ...props }, ref) => /* @__PURE__ */ jsx(
2646
+ "svg",
2647
+ {
2648
+ ref,
2649
+ className,
2650
+ style: { ...baseStyle2, ...style },
2651
+ viewBox: "0 0 24 24",
2652
+ fill: "currentColor",
2653
+ ...props,
2654
+ 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" })
2655
+ }
2656
+ )
2657
+ );
2658
+ Command.displayName = "Command";
2659
+ var Ctrl = forwardRef(
2660
+ ({ className, style, ...props }, ref) => /* @__PURE__ */ jsx(
2661
+ "svg",
2662
+ {
2663
+ ref,
2664
+ className,
2665
+ style: { ...baseStyle2, ...style },
2666
+ viewBox: "0 0 24 24",
2667
+ fill: "none",
2668
+ stroke: "currentColor",
2669
+ strokeWidth: "3",
2670
+ strokeLinecap: "round",
2671
+ strokeLinejoin: "round",
2672
+ ...props,
2673
+ children: /* @__PURE__ */ jsx("path", { d: "M6 15l6-6 6 6" })
2674
+ }
2675
+ )
2676
+ );
2677
+ Ctrl.displayName = "Ctrl";
2678
+ var Shift = forwardRef(
2679
+ ({ className, style, ...props }, ref) => /* @__PURE__ */ jsx(
2680
+ "svg",
2681
+ {
2682
+ ref,
2683
+ className,
2684
+ style: { ...wideStyle, ...style },
2685
+ viewBox: "0 0 28 24",
2686
+ fill: "none",
2687
+ stroke: "currentColor",
2688
+ strokeWidth: "2",
2689
+ strokeLinejoin: "round",
2690
+ ...props,
2691
+ children: /* @__PURE__ */ jsx("path", { d: "M14 3L3 14h6v7h10v-7h6L14 3z" })
2692
+ }
2693
+ )
2694
+ );
2695
+ Shift.displayName = "Shift";
2696
+ var Option = forwardRef(
2697
+ ({ className, style, ...props }, ref) => /* @__PURE__ */ jsx(
2698
+ "svg",
2699
+ {
2700
+ ref,
2701
+ className,
2702
+ style: { ...baseStyle2, ...style },
2703
+ viewBox: "0 0 24 24",
2704
+ fill: "none",
2705
+ stroke: "currentColor",
2706
+ strokeWidth: "2.5",
2707
+ strokeLinecap: "round",
2708
+ strokeLinejoin: "round",
2709
+ ...props,
2710
+ children: /* @__PURE__ */ jsx("path", { d: "M4 6h6l8 12h6M14 6h6" })
2711
+ }
2712
+ )
2713
+ );
2714
+ Option.displayName = "Option";
2715
+ var Alt = forwardRef(
2716
+ ({ className, style, ...props }, ref) => /* @__PURE__ */ jsx(
2717
+ "svg",
2718
+ {
2719
+ ref,
2720
+ className,
2721
+ style: { ...baseStyle2, ...style },
2722
+ viewBox: "0 0 24 24",
2723
+ fill: "none",
2724
+ stroke: "currentColor",
2725
+ strokeWidth: "2.5",
2726
+ strokeLinecap: "round",
2727
+ strokeLinejoin: "round",
2728
+ ...props,
2729
+ children: /* @__PURE__ */ jsx("path", { d: "M4 18h8M12 18l4-6M12 18l4 0M16 12l4-6h-8" })
2730
+ }
2731
+ )
2732
+ );
2733
+ Alt.displayName = "Alt";
2734
+ function getModifierIcon(modifier) {
2735
+ switch (modifier) {
2736
+ case "meta":
2737
+ return Command;
2738
+ case "ctrl":
2739
+ return Ctrl;
2740
+ case "shift":
2741
+ return Shift;
2742
+ case "opt":
2743
+ return Option;
2744
+ case "alt":
2745
+ return isMac() ? Option : Alt;
2746
+ }
2747
+ }
2748
+ var ModifierIcon = forwardRef(
2749
+ ({ modifier, ...props }, ref) => {
2750
+ const Icon = getModifierIcon(modifier);
2751
+ return /* @__PURE__ */ jsx(Icon, { ref, ...props });
2752
+ }
2753
+ );
2754
+ ModifierIcon.displayName = "ModifierIcon";
2755
+ function renderModifierIcons(modifiers, className = "kbd-modifier-icon") {
2756
+ const icons = [];
2757
+ if (modifiers.meta) {
2758
+ icons.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "meta", className }, "meta"));
2759
+ }
2760
+ if (modifiers.ctrl) {
2761
+ icons.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "ctrl", className }, "ctrl"));
2762
+ }
2763
+ if (modifiers.alt) {
2764
+ icons.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "alt", className }, "alt"));
2765
+ }
2766
+ if (modifiers.shift) {
2767
+ icons.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "shift", className }, "shift"));
2768
+ }
2769
+ return icons;
2770
+ }
2771
+ function renderKeyContent(key, iconClassName = "kbd-key-icon") {
2772
+ const Icon = getKeyIcon(key);
2773
+ const displayKey = formatKeyForDisplay(key);
2774
+ return Icon ? /* @__PURE__ */ jsx(Icon, { className: iconClassName }) : /* @__PURE__ */ jsx(Fragment, { children: displayKey });
2775
+ }
2776
+ function renderSeqElem(elem, index, kbdClassName = "kbd-kbd") {
2777
+ if (elem.type === "digit") {
2778
+ return /* @__PURE__ */ jsx("kbd", { className: kbdClassName, children: "\u27E8#\u27E9" }, index);
2779
+ }
2780
+ if (elem.type === "digits") {
2781
+ return /* @__PURE__ */ jsx("kbd", { className: kbdClassName, children: "\u27E8##\u27E9" }, index);
2782
+ }
2783
+ return /* @__PURE__ */ jsxs("kbd", { className: kbdClassName, children: [
2784
+ renderModifierIcons(elem.modifiers),
2785
+ renderKeyContent(elem.key)
2786
+ ] }, index);
2787
+ }
2788
+ function renderKeySeq(keySeq, kbdClassName = "kbd-kbd") {
2789
+ return keySeq.map((elem, i) => renderSeqElem(elem, i, kbdClassName));
2790
+ }
2791
+ function KeyCombo({ combo }) {
2792
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2793
+ renderModifierIcons(combo.modifiers),
2794
+ renderKeyContent(combo.key)
2795
+ ] });
2796
+ }
2797
+ function SeqElemDisplay({ elem }) {
2798
+ if (elem.type === "digit") {
2799
+ return /* @__PURE__ */ jsx("span", { className: "kbd-placeholder", title: "Any single digit (0-9)", children: "#" });
2800
+ }
2801
+ if (elem.type === "digits") {
2802
+ return /* @__PURE__ */ jsx("span", { className: "kbd-placeholder", title: "One or more digits (0-9)", children: "##" });
2803
+ }
2804
+ return /* @__PURE__ */ jsx(KeyCombo, { combo: { key: elem.key, modifiers: elem.modifiers } });
2805
+ }
2806
+ function BindingDisplay({ binding }) {
2807
+ const sequence = parseKeySeq(binding);
2808
+ return /* @__PURE__ */ jsx(Fragment, { children: sequence.map((elem, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
2809
+ i > 0 && /* @__PURE__ */ jsx("span", { className: "kbd-sequence-sep", children: " " }),
2810
+ /* @__PURE__ */ jsx(SeqElemDisplay, { elem })
2811
+ ] }, i)) });
2812
+ }
2813
+ function Kbd({
2814
+ action,
2815
+ separator = " / ",
2816
+ all = false,
2817
+ fallback = null,
2818
+ className,
2819
+ clickable = true
2820
+ }) {
2821
+ const ctx = useMaybeHotkeysContext();
2822
+ const warnedRef = useRef(false);
2823
+ const bindings = ctx ? all ? ctx.registry.getBindingsForAction(action) : [ctx.registry.getFirstBindingForAction(action)].filter(Boolean) : [];
2824
+ useEffect(() => {
2825
+ if (!ctx) return;
2826
+ if (warnedRef.current) return;
2827
+ const timer = setTimeout(() => {
2828
+ if (!ctx.registry.actions.has(action)) {
2829
+ console.warn(`Kbd: Action "${action}" not found in registry`);
2830
+ warnedRef.current = true;
2831
+ }
2832
+ }, 100);
2833
+ return () => clearTimeout(timer);
2834
+ }, [ctx, action]);
2835
+ if (!ctx) {
2836
+ return null;
2837
+ }
2838
+ if (bindings.length === 0) {
2839
+ return /* @__PURE__ */ jsx(Fragment, { children: fallback });
2840
+ }
2841
+ const content = bindings.map((binding, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
2842
+ i > 0 && separator,
2843
+ /* @__PURE__ */ jsx(BindingDisplay, { binding })
2844
+ ] }, binding));
2845
+ if (clickable) {
2846
+ return /* @__PURE__ */ jsx(
2847
+ "kbd",
2848
+ {
2849
+ className: `${className || ""} kbd-clickable`.trim(),
2850
+ onClick: () => ctx.executeAction(action),
2851
+ role: "button",
2852
+ tabIndex: 0,
2853
+ onKeyDown: (e) => {
2854
+ if (e.key === "Enter" || e.key === " ") {
2855
+ e.preventDefault();
2856
+ ctx.executeAction(action);
2857
+ }
2858
+ },
2859
+ children: content
2860
+ }
2861
+ );
2862
+ }
2863
+ return /* @__PURE__ */ jsx("kbd", { className, children: content });
2864
+ }
2865
+ function Key(props) {
2866
+ return /* @__PURE__ */ jsx(Kbd, { ...props, clickable: false });
2867
+ }
2868
+ function Kbds(props) {
2869
+ return /* @__PURE__ */ jsx(Kbd, { ...props, all: true });
2870
+ }
2871
+ function KbdModal(props) {
2872
+ return /* @__PURE__ */ jsx(Kbd, { ...props, action: ACTION_MODAL });
2873
+ }
2874
+ function KbdOmnibar(props) {
2875
+ return /* @__PURE__ */ jsx(Kbd, { ...props, action: ACTION_OMNIBAR });
2876
+ }
2877
+ function KbdLookup(props) {
2878
+ return /* @__PURE__ */ jsx(Kbd, { ...props, action: ACTION_LOOKUP });
2879
+ }
1562
2880
  function buildActionMap(keymap) {
1563
2881
  const map = /* @__PURE__ */ new Map();
1564
2882
  for (const [key, actionOrActions] of Object.entries(keymap)) {
@@ -1626,7 +2944,7 @@ function KeybindingEditor({
1626
2944
  return Array.from(allActions).map((action) => {
1627
2945
  const key = actionMap.get(action) ?? defaultActionMap.get(action) ?? "";
1628
2946
  const defaultKey = defaultActionMap.get(action) ?? "";
1629
- const combo = parseCombinationId(key);
2947
+ const combo = parseHotkeyString(key);
1630
2948
  const display = formatCombination(combo);
1631
2949
  const conflictActions = conflicts.get(key);
1632
2950
  return {
@@ -1728,149 +3046,241 @@ function KeybindingEditor({
1728
3046
  "button",
1729
3047
  {
1730
3048
  onClick: () => startEditing(action),
1731
- disabled: isRecording,
1732
- style: {
1733
- padding: "4px 8px",
1734
- backgroundColor: "#f5f5f5",
1735
- border: "1px solid #ddd",
1736
- borderRadius: "4px",
1737
- cursor: isRecording ? "not-allowed" : "pointer",
1738
- fontSize: "0.875rem",
1739
- opacity: isRecording ? 0.5 : 1
1740
- },
1741
- children: "Edit"
1742
- }
1743
- ) })
1744
- ] }, action);
1745
- }) })
1746
- ] })
1747
- ] });
1748
- }
1749
- var baseStyle = {
1750
- width: "1.2em",
1751
- height: "1.2em",
1752
- marginRight: "2px",
1753
- verticalAlign: "middle"
1754
- };
1755
- var wideStyle = {
1756
- ...baseStyle,
1757
- width: "1.4em"
1758
- };
1759
- function CommandIcon({ className, style }) {
1760
- return /* @__PURE__ */ jsx(
1761
- "svg",
1762
- {
1763
- className,
1764
- style: { ...baseStyle, ...style },
1765
- viewBox: "0 0 24 24",
1766
- fill: "currentColor",
1767
- children: /* @__PURE__ */ jsx("path", { d: "M6 4a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2v4H6a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2h4v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2h-2v-4h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v2h-4V6a2 2 0 0 0-2-2H6zm4 6h4v4h-4v-4z" })
1768
- }
1769
- );
1770
- }
1771
- function CtrlIcon({ className, style }) {
1772
- return /* @__PURE__ */ jsx(
1773
- "svg",
1774
- {
1775
- className,
1776
- style: { ...baseStyle, ...style },
1777
- viewBox: "0 0 24 24",
1778
- fill: "none",
1779
- stroke: "currentColor",
1780
- strokeWidth: "3",
1781
- strokeLinecap: "round",
1782
- strokeLinejoin: "round",
1783
- children: /* @__PURE__ */ jsx("path", { d: "M6 15l6-6 6 6" })
1784
- }
1785
- );
3049
+ disabled: isRecording,
3050
+ style: {
3051
+ padding: "4px 8px",
3052
+ backgroundColor: "#f5f5f5",
3053
+ border: "1px solid #ddd",
3054
+ borderRadius: "4px",
3055
+ cursor: isRecording ? "not-allowed" : "pointer",
3056
+ fontSize: "0.875rem",
3057
+ opacity: isRecording ? 0.5 : 1
3058
+ },
3059
+ children: "Edit"
3060
+ }
3061
+ ) })
3062
+ ] }, action);
3063
+ }) })
3064
+ ] })
3065
+ ] });
1786
3066
  }
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" })
3067
+ function LookupModal({ defaultBinding = "meta+shift+k" } = {}) {
3068
+ const {
3069
+ isLookupOpen,
3070
+ closeLookup,
3071
+ toggleLookup,
3072
+ registry,
3073
+ executeAction
3074
+ } = useHotkeysContext();
3075
+ useAction(ACTION_LOOKUP, {
3076
+ label: "Key lookup",
3077
+ group: "Global",
3078
+ defaultBindings: defaultBinding ? [defaultBinding] : [],
3079
+ handler: useCallback(() => toggleLookup(), [toggleLookup])
3080
+ });
3081
+ const [pendingKeys, setPendingKeys] = useState([]);
3082
+ const [selectedIndex, setSelectedIndex] = useState(0);
3083
+ const allBindings = useMemo(() => {
3084
+ const results = [];
3085
+ const keymap = registry.keymap;
3086
+ for (const [binding, actionOrActions] of Object.entries(keymap)) {
3087
+ if (binding.startsWith("__")) continue;
3088
+ const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
3089
+ const sequence = parseHotkeyString(binding);
3090
+ const keySeq = parseKeySeq(binding);
3091
+ const display = formatKeySeq(keySeq).display;
3092
+ const labels = actions.map((actionId) => {
3093
+ const action = registry.actions.get(actionId);
3094
+ return action?.config.label || actionId;
3095
+ });
3096
+ results.push({ binding, sequence, keySeq, display, actions, labels });
1799
3097
  }
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" })
3098
+ results.sort((a, b) => a.binding.localeCompare(b.binding));
3099
+ return results;
3100
+ }, [registry.keymap, registry.actions]);
3101
+ const filteredBindings = useMemo(() => {
3102
+ if (pendingKeys.length === 0) return allBindings;
3103
+ return allBindings.filter((result) => {
3104
+ const keySeq = result.keySeq;
3105
+ if (keySeq.length < pendingKeys.length) return false;
3106
+ let keySeqIdx = 0;
3107
+ for (let i = 0; i < pendingKeys.length && keySeqIdx < keySeq.length; i++) {
3108
+ const pending = pendingKeys[i];
3109
+ const elem = keySeq[keySeqIdx];
3110
+ const isDigit2 = /^[0-9]$/.test(pending.key);
3111
+ if (elem.type === "digits") {
3112
+ if (!isDigit2) return false;
3113
+ if (i + 1 < pendingKeys.length && /^[0-9]$/.test(pendingKeys[i + 1].key)) {
3114
+ continue;
3115
+ }
3116
+ keySeqIdx++;
3117
+ } else if (elem.type === "digit") {
3118
+ if (!isDigit2) return false;
3119
+ keySeqIdx++;
3120
+ } else {
3121
+ if (pending.key !== elem.key) return false;
3122
+ if (pending.modifiers.ctrl !== elem.modifiers.ctrl) return false;
3123
+ if (pending.modifiers.alt !== elem.modifiers.alt) return false;
3124
+ if (pending.modifiers.shift !== elem.modifiers.shift) return false;
3125
+ if (pending.modifiers.meta !== elem.modifiers.meta) return false;
3126
+ keySeqIdx++;
3127
+ }
3128
+ }
3129
+ return true;
3130
+ });
3131
+ }, [allBindings, pendingKeys]);
3132
+ const groupedByNextKey = useMemo(() => {
3133
+ const groups = /* @__PURE__ */ new Map();
3134
+ for (const result of filteredBindings) {
3135
+ if (result.sequence.length > pendingKeys.length) {
3136
+ const nextCombo = result.sequence[pendingKeys.length];
3137
+ const nextKey = formatCombination([nextCombo]).display;
3138
+ const existing = groups.get(nextKey) || [];
3139
+ existing.push(result);
3140
+ groups.set(nextKey, existing);
3141
+ } else {
3142
+ const existing = groups.get("") || [];
3143
+ existing.push(result);
3144
+ groups.set("", existing);
3145
+ }
1815
3146
  }
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" })
3147
+ return groups;
3148
+ }, [filteredBindings, pendingKeys]);
3149
+ const formattedPendingKeys = useMemo(() => {
3150
+ if (pendingKeys.length === 0) return "";
3151
+ return formatCombination(pendingKeys).display;
3152
+ }, [pendingKeys]);
3153
+ useEffect(() => {
3154
+ if (isLookupOpen) {
3155
+ setPendingKeys([]);
3156
+ setSelectedIndex(0);
1831
3157
  }
1832
- );
3158
+ }, [isLookupOpen]);
3159
+ useEffect(() => {
3160
+ setSelectedIndex(0);
3161
+ }, [filteredBindings.length]);
3162
+ useEffect(() => {
3163
+ if (!isLookupOpen) return;
3164
+ const handleKeyDown = (e) => {
3165
+ if (e.key === "Escape") {
3166
+ e.preventDefault();
3167
+ if (pendingKeys.length > 0) {
3168
+ setPendingKeys([]);
3169
+ } else {
3170
+ closeLookup();
3171
+ }
3172
+ return;
3173
+ }
3174
+ if (e.key === "Backspace") {
3175
+ e.preventDefault();
3176
+ setPendingKeys((prev) => prev.slice(0, -1));
3177
+ return;
3178
+ }
3179
+ if (e.key === "ArrowDown") {
3180
+ e.preventDefault();
3181
+ setSelectedIndex((prev) => Math.min(prev + 1, filteredBindings.length - 1));
3182
+ return;
3183
+ }
3184
+ if (e.key === "ArrowUp") {
3185
+ e.preventDefault();
3186
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
3187
+ return;
3188
+ }
3189
+ if (e.key === "Enter") {
3190
+ e.preventDefault();
3191
+ const selected = filteredBindings[selectedIndex];
3192
+ if (selected && selected.actions.length > 0) {
3193
+ closeLookup();
3194
+ executeAction(selected.actions[0]);
3195
+ }
3196
+ return;
3197
+ }
3198
+ if (isModifierKey(e.key)) return;
3199
+ e.preventDefault();
3200
+ const newCombo = {
3201
+ key: normalizeKey(e.key),
3202
+ modifiers: {
3203
+ ctrl: e.ctrlKey,
3204
+ alt: e.altKey,
3205
+ shift: e.shiftKey,
3206
+ meta: e.metaKey
3207
+ }
3208
+ };
3209
+ setPendingKeys((prev) => [...prev, newCombo]);
3210
+ };
3211
+ window.addEventListener("keydown", handleKeyDown);
3212
+ return () => window.removeEventListener("keydown", handleKeyDown);
3213
+ }, [isLookupOpen, pendingKeys, filteredBindings, selectedIndex, closeLookup, executeAction]);
3214
+ const handleBackdropClick = useCallback(() => {
3215
+ closeLookup();
3216
+ }, [closeLookup]);
3217
+ if (!isLookupOpen) return null;
3218
+ return /* @__PURE__ */ jsx("div", { className: "kbd-lookup-backdrop", onClick: handleBackdropClick, children: /* @__PURE__ */ jsxs("div", { className: "kbd-lookup", onClick: (e) => e.stopPropagation(), children: [
3219
+ /* @__PURE__ */ jsxs("div", { className: "kbd-lookup-header", children: [
3220
+ /* @__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..." }) }),
3221
+ /* @__PURE__ */ jsxs("span", { className: "kbd-lookup-hint", children: [
3222
+ "\u2191\u2193 navigate \xB7 Enter select \xB7 Esc ",
3223
+ pendingKeys.length > 0 ? "clear" : "close",
3224
+ " \xB7 \u232B back"
3225
+ ] })
3226
+ ] }),
3227
+ /* @__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(
3228
+ "div",
3229
+ {
3230
+ className: `kbd-lookup-result ${index === selectedIndex ? "selected" : ""}`,
3231
+ onClick: () => {
3232
+ closeLookup();
3233
+ if (result.actions.length > 0) {
3234
+ executeAction(result.actions[0]);
3235
+ }
3236
+ },
3237
+ onMouseEnter: () => setSelectedIndex(index),
3238
+ children: [
3239
+ /* @__PURE__ */ jsx("span", { className: "kbd-lookup-binding", children: renderKeySeq(result.keySeq) }),
3240
+ /* @__PURE__ */ jsx("span", { className: "kbd-lookup-labels", children: result.labels.join(", ") })
3241
+ ]
3242
+ },
3243
+ result.binding
3244
+ )) }),
3245
+ pendingKeys.length > 0 && groupedByNextKey.size > 1 && /* @__PURE__ */ jsxs("div", { className: "kbd-lookup-continuations", children: [
3246
+ /* @__PURE__ */ jsx("span", { className: "kbd-lookup-continuations-label", children: "Continue with:" }),
3247
+ Array.from(groupedByNextKey.keys()).filter((k) => k !== "").slice(0, 8).map((nextKey) => /* @__PURE__ */ jsx("kbd", { className: "kbd-kbd kbd-small", children: nextKey }, nextKey)),
3248
+ groupedByNextKey.size > 9 && /* @__PURE__ */ jsx("span", { children: "..." })
3249
+ ] })
3250
+ ] }) });
1833
3251
  }
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;
3252
+ function SeqElemBadge({ elem }) {
3253
+ if (elem.type === "digit") {
3254
+ return /* @__PURE__ */ jsx("span", { className: "kbd-placeholder", title: "Any single digit (0-9)", children: "#" });
1847
3255
  }
1848
- }
1849
- function ModifierIcon({ modifier, ...props }) {
1850
- const Icon = getModifierIcon(modifier);
1851
- return /* @__PURE__ */ jsx(Icon, { ...props });
3256
+ if (elem.type === "digits") {
3257
+ return /* @__PURE__ */ jsx("span", { className: "kbd-placeholder", title: "One or more digits (0-9)", children: "##" });
3258
+ }
3259
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3260
+ elem.modifiers.meta && /* @__PURE__ */ jsx(ModifierIcon, { modifier: "meta", className: "kbd-modifier-icon" }),
3261
+ elem.modifiers.ctrl && /* @__PURE__ */ jsx(ModifierIcon, { modifier: "ctrl", className: "kbd-modifier-icon" }),
3262
+ elem.modifiers.alt && /* @__PURE__ */ jsx(ModifierIcon, { modifier: "alt", className: "kbd-modifier-icon" }),
3263
+ elem.modifiers.shift && /* @__PURE__ */ jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }),
3264
+ /* @__PURE__ */ jsx("span", { children: formatKeyForDisplay(elem.key) })
3265
+ ] });
1852
3266
  }
1853
3267
  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: [
3268
+ const keySeq = parseKeySeq(binding);
3269
+ return /* @__PURE__ */ jsx("kbd", { className: "kbd-kbd", children: keySeq.map((elem, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
1856
3270
  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 })
3271
+ /* @__PURE__ */ jsx(SeqElemBadge, { elem })
1862
3272
  ] }, i)) });
1863
3273
  }
1864
3274
  function Omnibar({
1865
3275
  actions: actionsProp,
1866
3276
  handlers: handlersProp,
1867
3277
  keymap: keymapProp,
1868
- openKey = "meta+k",
1869
- enabled: enabledProp,
3278
+ defaultBinding = "meta+k",
1870
3279
  isOpen: isOpenProp,
1871
3280
  onOpen: onOpenProp,
1872
3281
  onClose: onCloseProp,
1873
3282
  onExecute: onExecuteProp,
3283
+ onExecuteRemote: onExecuteRemoteProp,
1874
3284
  maxResults = 10,
1875
3285
  placeholder = "Type a command...",
1876
3286
  children,
@@ -1881,7 +3291,12 @@ function Omnibar({
1881
3291
  const ctx = useMaybeHotkeysContext();
1882
3292
  const actions = actionsProp ?? ctx?.registry.actionRegistry ?? {};
1883
3293
  const keymap = keymapProp ?? ctx?.registry.keymap ?? {};
1884
- const enabled = enabledProp ?? !ctx;
3294
+ useAction(ACTION_OMNIBAR, {
3295
+ label: "Command palette",
3296
+ group: "Global",
3297
+ defaultBindings: defaultBinding ? [defaultBinding] : [],
3298
+ handler: useCallback(() => ctx?.toggleOmnibar(), [ctx?.toggleOmnibar])
3299
+ });
1885
3300
  const handleExecute = useCallback((actionId) => {
1886
3301
  if (onExecuteProp) {
1887
3302
  onExecuteProp(actionId);
@@ -1903,13 +3318,25 @@ function Omnibar({
1903
3318
  ctx.openOmnibar();
1904
3319
  }
1905
3320
  }, [onOpenProp, ctx]);
3321
+ const handleExecuteRemote = useCallback((entry) => {
3322
+ if (onExecuteRemoteProp) {
3323
+ onExecuteRemoteProp(entry);
3324
+ } else if ("href" in entry && entry.href) {
3325
+ window.location.href = entry.href;
3326
+ }
3327
+ }, [onExecuteRemoteProp]);
1906
3328
  const {
1907
3329
  isOpen: internalIsOpen,
1908
3330
  close,
1909
3331
  query,
1910
3332
  setQuery,
1911
3333
  results,
3334
+ remoteResults,
3335
+ isLoadingRemote,
3336
+ endpointPagination,
3337
+ loadMore,
1912
3338
  selectedIndex,
3339
+ totalResults,
1913
3340
  selectNext,
1914
3341
  selectPrev,
1915
3342
  execute,
@@ -1920,15 +3347,28 @@ function Omnibar({
1920
3347
  actions,
1921
3348
  handlers: handlersProp,
1922
3349
  keymap,
1923
- openKey,
1924
- enabled: isOpenProp === void 0 && ctx === null ? enabled : false,
1925
- // Disable hotkey if controlled or using context
3350
+ openKey: "",
3351
+ // Trigger is handled via useAction, not useOmnibar
3352
+ enabled: false,
1926
3353
  onOpen: handleOpen,
1927
3354
  onClose: handleClose,
1928
3355
  onExecute: handleExecute,
1929
- maxResults
3356
+ onExecuteRemote: handleExecuteRemote,
3357
+ maxResults,
3358
+ endpointsRegistry: ctx?.endpointsRegistry
1930
3359
  });
1931
3360
  const isOpen = isOpenProp ?? ctx?.isOmnibarOpen ?? internalIsOpen;
3361
+ const resultsContainerRef = useRef(null);
3362
+ const sentinelRefs = useRef(/* @__PURE__ */ new Map());
3363
+ const remoteResultsByEndpoint = useMemo(() => {
3364
+ const grouped = /* @__PURE__ */ new Map();
3365
+ for (const result of remoteResults) {
3366
+ const existing = grouped.get(result.endpointId) ?? [];
3367
+ existing.push(result);
3368
+ grouped.set(result.endpointId, existing);
3369
+ }
3370
+ return grouped;
3371
+ }, [remoteResults]);
1932
3372
  useEffect(() => {
1933
3373
  if (isOpen) {
1934
3374
  requestAnimationFrame(() => {
@@ -1936,6 +3376,50 @@ function Omnibar({
1936
3376
  });
1937
3377
  }
1938
3378
  }, [isOpen]);
3379
+ useEffect(() => {
3380
+ if (!isOpen) return;
3381
+ const container = resultsContainerRef.current;
3382
+ if (!container) return;
3383
+ const observer = new IntersectionObserver(
3384
+ (entries) => {
3385
+ for (const entry of entries) {
3386
+ if (!entry.isIntersecting) continue;
3387
+ const endpointId = entry.target.dataset.endpointId;
3388
+ if (!endpointId) continue;
3389
+ const paginationInfo = endpointPagination.get(endpointId);
3390
+ if (!paginationInfo) continue;
3391
+ if (paginationInfo.mode !== "scroll") continue;
3392
+ if (!paginationInfo.hasMore) continue;
3393
+ if (paginationInfo.isLoading) continue;
3394
+ loadMore(endpointId);
3395
+ }
3396
+ },
3397
+ {
3398
+ root: container,
3399
+ rootMargin: "100px",
3400
+ // Trigger slightly before sentinel is visible
3401
+ threshold: 0
3402
+ }
3403
+ );
3404
+ for (const [_endpointId, sentinel] of sentinelRefs.current) {
3405
+ if (sentinel) {
3406
+ observer.observe(sentinel);
3407
+ }
3408
+ }
3409
+ return () => observer.disconnect();
3410
+ }, [isOpen, endpointPagination, loadMore]);
3411
+ useEffect(() => {
3412
+ if (!isOpen) return;
3413
+ const handleGlobalKeyDown = (e) => {
3414
+ if (e.key === "Escape") {
3415
+ e.preventDefault();
3416
+ e.stopPropagation();
3417
+ close();
3418
+ }
3419
+ };
3420
+ document.addEventListener("keydown", handleGlobalKeyDown, true);
3421
+ return () => document.removeEventListener("keydown", handleGlobalKeyDown, true);
3422
+ }, [isOpen, close]);
1939
3423
  const handleKeyDown = useCallback(
1940
3424
  (e) => {
1941
3425
  switch (e.key) {
@@ -1973,7 +3457,12 @@ function Omnibar({
1973
3457
  query,
1974
3458
  setQuery,
1975
3459
  results,
3460
+ remoteResults,
3461
+ isLoadingRemote,
3462
+ endpointPagination,
3463
+ loadMore,
1976
3464
  selectedIndex,
3465
+ totalResults,
1977
3466
  selectNext,
1978
3467
  selectPrev,
1979
3468
  execute,
@@ -2001,65 +3490,164 @@ function Omnibar({
2001
3490
  spellCheck: false
2002
3491
  }
2003
3492
  ),
2004
- /* @__PURE__ */ jsx("div", { className: "kbd-omnibar-results", children: results.length === 0 ? /* @__PURE__ */ jsx("div", { className: "kbd-omnibar-no-results", children: query ? "No matching commands" : "Start typing to search commands..." }) : results.map((result, i) => /* @__PURE__ */ jsxs(
2005
- "div",
2006
- {
2007
- className: `kbd-omnibar-result ${i === selectedIndex ? "selected" : ""}`,
2008
- onClick: () => execute(result.id),
2009
- onMouseEnter: () => {
3493
+ /* @__PURE__ */ jsx("div", { className: "kbd-omnibar-results", ref: resultsContainerRef, children: totalResults === 0 && !isLoadingRemote ? /* @__PURE__ */ jsx("div", { className: "kbd-omnibar-no-results", children: query ? "No matching commands" : "Start typing to search commands..." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3494
+ results.map((result, i) => /* @__PURE__ */ jsxs(
3495
+ "div",
3496
+ {
3497
+ className: `kbd-omnibar-result ${i === selectedIndex ? "selected" : ""}`,
3498
+ onClick: () => execute(result.id),
3499
+ children: [
3500
+ /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-result-label", children: result.action.label }),
3501
+ result.action.group && /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-result-category", children: result.action.group }),
3502
+ result.bindings.length > 0 && /* @__PURE__ */ jsx("div", { className: "kbd-omnibar-result-bindings", children: result.bindings.slice(0, 2).map((binding) => /* @__PURE__ */ jsx(BindingBadge, { binding }, binding)) })
3503
+ ]
2010
3504
  },
2011
- children: [
2012
- /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-result-label", children: result.action.label }),
2013
- result.action.group && /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-result-category", children: result.action.group }),
2014
- result.bindings.length > 0 && /* @__PURE__ */ jsx("div", { className: "kbd-omnibar-result-bindings", children: result.bindings.slice(0, 2).map((binding) => /* @__PURE__ */ jsx(BindingBadge, { binding }, binding)) })
2015
- ]
2016
- },
2017
- result.id
2018
- )) })
3505
+ result.id
3506
+ )),
3507
+ (() => {
3508
+ let remoteIndex = 0;
3509
+ return Array.from(remoteResultsByEndpoint.entries()).map(([endpointId, endpointResults]) => {
3510
+ const paginationInfo = endpointPagination.get(endpointId);
3511
+ const showPagination = paginationInfo?.mode === "scroll" && paginationInfo.total !== void 0;
3512
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
3513
+ endpointResults.map((result) => {
3514
+ const absoluteIndex = results.length + remoteIndex;
3515
+ remoteIndex++;
3516
+ return /* @__PURE__ */ jsxs(
3517
+ "div",
3518
+ {
3519
+ className: `kbd-omnibar-result ${absoluteIndex === selectedIndex ? "selected" : ""}`,
3520
+ onClick: () => execute(result.id),
3521
+ children: [
3522
+ /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-result-label", children: result.entry.label }),
3523
+ result.entry.group && /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-result-category", children: result.entry.group }),
3524
+ result.entry.description && /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-result-description", children: result.entry.description })
3525
+ ]
3526
+ },
3527
+ result.id
3528
+ );
3529
+ }),
3530
+ paginationInfo?.mode === "scroll" && /* @__PURE__ */ jsx(
3531
+ "div",
3532
+ {
3533
+ className: "kbd-omnibar-pagination",
3534
+ ref: (el) => sentinelRefs.current.set(endpointId, el),
3535
+ "data-endpoint-id": endpointId,
3536
+ children: paginationInfo.isLoading ? /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-pagination-loading", children: "Loading more..." }) : showPagination ? /* @__PURE__ */ jsxs("span", { className: "kbd-omnibar-pagination-info", children: [
3537
+ paginationInfo.loaded,
3538
+ " of ",
3539
+ paginationInfo.total
3540
+ ] }) : paginationInfo.hasMore ? /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-pagination-more", children: "Scroll for more..." }) : null
3541
+ }
3542
+ )
3543
+ ] }, endpointId);
3544
+ });
3545
+ })(),
3546
+ isLoadingRemote && remoteResults.length === 0 && /* @__PURE__ */ jsx("div", { className: "kbd-omnibar-loading", children: "Searching..." })
3547
+ ] }) })
2019
3548
  ] }) });
2020
3549
  }
2021
3550
  function SequenceModal() {
2022
3551
  const {
2023
3552
  pendingKeys,
2024
3553
  isAwaitingSequence,
3554
+ cancelSequence,
2025
3555
  sequenceTimeoutStartedAt: timeoutStartedAt,
2026
3556
  sequenceTimeout,
2027
3557
  getCompletions,
2028
- registry
3558
+ registry,
3559
+ executeAction
2029
3560
  } = useHotkeysContext();
3561
+ const [selectedIndex, setSelectedIndex] = useState(0);
3562
+ const [hasInteracted, setHasInteracted] = useState(false);
2030
3563
  const completions = useMemo(() => {
2031
3564
  if (pendingKeys.length === 0) return [];
2032
3565
  return getCompletions(pendingKeys);
2033
3566
  }, [getCompletions, pendingKeys]);
2034
- const formattedPendingKeys = useMemo(() => {
2035
- if (pendingKeys.length === 0) return "";
2036
- return formatCombination(pendingKeys).display;
2037
- }, [pendingKeys]);
2038
- const getActionLabel = (actionId) => {
2039
- const action = registry.actions.get(actionId);
2040
- return action?.config.label || actionId;
2041
- };
2042
- const groupedCompletions = useMemo(() => {
2043
- const byNextKey = /* @__PURE__ */ new Map();
3567
+ const flatCompletions = useMemo(() => {
3568
+ const items = [];
2044
3569
  for (const c of completions) {
2045
- const existing = byNextKey.get(c.nextKeys);
2046
- if (existing) {
2047
- existing.push(c);
2048
- } else {
2049
- byNextKey.set(c.nextKeys, [c]);
3570
+ for (const action of c.actions) {
3571
+ const displayKey = c.isComplete ? "\u21B5" : c.nextKeys;
3572
+ items.push({
3573
+ completion: c,
3574
+ action,
3575
+ displayKey,
3576
+ isComplete: c.isComplete
3577
+ });
2050
3578
  }
2051
3579
  }
2052
- return byNextKey;
3580
+ return items;
2053
3581
  }, [completions]);
3582
+ const itemCount = flatCompletions.length;
3583
+ const shouldShowTimeout = timeoutStartedAt !== null && completions.length === 1 && !hasInteracted;
3584
+ useEffect(() => {
3585
+ setSelectedIndex(0);
3586
+ setHasInteracted(false);
3587
+ }, [pendingKeys]);
3588
+ const executeSelected = useCallback(() => {
3589
+ if (selectedIndex >= 0 && selectedIndex < flatCompletions.length) {
3590
+ const item = flatCompletions[selectedIndex];
3591
+ executeAction(item.action, item.completion.captures);
3592
+ cancelSequence();
3593
+ }
3594
+ }, [selectedIndex, flatCompletions, executeAction, cancelSequence]);
3595
+ useEffect(() => {
3596
+ if (!isAwaitingSequence || pendingKeys.length === 0) return;
3597
+ const handleKeyDown = (e) => {
3598
+ switch (e.key) {
3599
+ case "ArrowDown":
3600
+ e.preventDefault();
3601
+ e.stopPropagation();
3602
+ setSelectedIndex((prev) => Math.min(prev + 1, itemCount - 1));
3603
+ setHasInteracted(true);
3604
+ break;
3605
+ case "ArrowUp":
3606
+ e.preventDefault();
3607
+ e.stopPropagation();
3608
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
3609
+ setHasInteracted(true);
3610
+ break;
3611
+ case "Enter":
3612
+ e.preventDefault();
3613
+ e.stopPropagation();
3614
+ executeSelected();
3615
+ break;
3616
+ }
3617
+ };
3618
+ document.addEventListener("keydown", handleKeyDown, true);
3619
+ return () => document.removeEventListener("keydown", handleKeyDown, true);
3620
+ }, [isAwaitingSequence, pendingKeys.length, itemCount, executeSelected]);
3621
+ const renderKey = useCallback((combo, index) => {
3622
+ const { key, modifiers } = combo;
3623
+ return /* @__PURE__ */ jsxs("kbd", { className: "kbd-kbd", children: [
3624
+ renderModifierIcons(modifiers),
3625
+ renderKeyContent(key)
3626
+ ] }, index);
3627
+ }, []);
3628
+ const getActionLabel = (actionId, captures) => {
3629
+ const action = registry.actions.get(actionId);
3630
+ let label = action?.config.label || actionId;
3631
+ if (captures && captures.length > 0) {
3632
+ let captureIdx = 0;
3633
+ label = label.replace(/\bN\b/g, () => {
3634
+ if (captureIdx < captures.length) {
3635
+ return String(captures[captureIdx++]);
3636
+ }
3637
+ return "N";
3638
+ });
3639
+ }
3640
+ return label;
3641
+ };
2054
3642
  if (!isAwaitingSequence || pendingKeys.length === 0) {
2055
3643
  return null;
2056
3644
  }
2057
- return /* @__PURE__ */ jsx("div", { className: "kbd-sequence-backdrop", children: /* @__PURE__ */ jsxs("div", { className: "kbd-sequence", children: [
3645
+ return /* @__PURE__ */ jsx("div", { className: "kbd-sequence-backdrop", onClick: cancelSequence, children: /* @__PURE__ */ jsxs("div", { className: "kbd-sequence", onClick: (e) => e.stopPropagation(), children: [
2058
3646
  /* @__PURE__ */ jsxs("div", { className: "kbd-sequence-current", children: [
2059
- /* @__PURE__ */ jsx("kbd", { className: "kbd-sequence-keys", children: formattedPendingKeys }),
3647
+ /* @__PURE__ */ jsx("div", { className: "kbd-sequence-keys", children: pendingKeys.map((combo, i) => renderKey(combo, i)) }),
2060
3648
  /* @__PURE__ */ jsx("span", { className: "kbd-sequence-ellipsis", children: "\u2026" })
2061
3649
  ] }),
2062
- timeoutStartedAt && /* @__PURE__ */ jsx(
3650
+ shouldShowTimeout && /* @__PURE__ */ jsx(
2063
3651
  "div",
2064
3652
  {
2065
3653
  className: "kbd-sequence-timeout",
@@ -2067,17 +3655,23 @@ function SequenceModal() {
2067
3655
  },
2068
3656
  timeoutStartedAt
2069
3657
  ),
2070
- completions.length > 0 && /* @__PURE__ */ jsx("div", { className: "kbd-sequence-completions", children: Array.from(groupedCompletions.entries()).map(([nextKey, comps]) => /* @__PURE__ */ jsxs("div", { className: "kbd-sequence-completion", children: [
2071
- /* @__PURE__ */ jsx("kbd", { className: "kbd-kbd", children: nextKey.toUpperCase() }),
2072
- /* @__PURE__ */ jsx("span", { className: "kbd-sequence-arrow", children: "\u2192" }),
2073
- /* @__PURE__ */ jsx("span", { className: "kbd-sequence-actions", children: comps.flatMap((c) => c.actions).map((action, i) => /* @__PURE__ */ jsxs("span", { children: [
2074
- i > 0 && ", ",
2075
- getActionLabel(action)
2076
- ] }, action)) })
2077
- ] }, nextKey)) }),
2078
- completions.length === 0 && /* @__PURE__ */ jsx("div", { className: "kbd-sequence-empty", children: "No matching shortcuts" })
3658
+ flatCompletions.length > 0 && /* @__PURE__ */ jsx("div", { className: "kbd-sequence-completions", children: flatCompletions.map((item, index) => /* @__PURE__ */ jsxs(
3659
+ "div",
3660
+ {
3661
+ className: `kbd-sequence-completion ${index === selectedIndex ? "selected" : ""} ${item.isComplete ? "complete" : ""}`,
3662
+ children: [
3663
+ item.isComplete ? /* @__PURE__ */ jsx("kbd", { className: "kbd-kbd", children: "\u21B5" }) : item.completion.nextKeySeq ? renderKeySeq(item.completion.nextKeySeq) : /* @__PURE__ */ jsx("kbd", { className: "kbd-kbd", children: item.displayKey }),
3664
+ /* @__PURE__ */ jsx("span", { className: "kbd-sequence-arrow", children: "\u2192" }),
3665
+ /* @__PURE__ */ jsx("span", { className: "kbd-sequence-actions", children: getActionLabel(item.action, item.completion.captures) })
3666
+ ]
3667
+ },
3668
+ `${item.completion.fullSequence}-${item.action}`
3669
+ )) }),
3670
+ flatCompletions.length === 0 && /* @__PURE__ */ jsx("div", { className: "kbd-sequence-empty", children: "No matching shortcuts" })
2079
3671
  ] }) });
2080
3672
  }
3673
+ var DefaultTooltip = ({ children }) => /* @__PURE__ */ jsx(Fragment, { children });
3674
+ var TooltipContext = createContext(DefaultTooltip);
2081
3675
  function parseActionId(actionId) {
2082
3676
  const colonIndex = actionId.indexOf(":");
2083
3677
  if (colonIndex > 0) {
@@ -2085,22 +3679,49 @@ function parseActionId(actionId) {
2085
3679
  }
2086
3680
  return { group: "General", name: actionId };
2087
3681
  }
2088
- function organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder) {
3682
+ function organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder, actionRegistry, showUnbound = true) {
2089
3683
  const actionBindings = getActionBindings(keymap);
2090
3684
  const groupMap = /* @__PURE__ */ new Map();
3685
+ const includedActions = /* @__PURE__ */ new Set();
3686
+ const getGroupName = (actionId) => {
3687
+ const registeredGroup = actionRegistry?.[actionId]?.group;
3688
+ if (registeredGroup) return registeredGroup;
3689
+ const { group: groupKey } = parseActionId(actionId);
3690
+ return groupNames?.[groupKey] ?? groupKey;
3691
+ };
2091
3692
  for (const [actionId, bindings] of actionBindings) {
2092
- const { group: groupKey, name } = parseActionId(actionId);
2093
- const groupName = groupNames?.[groupKey] ?? groupKey;
3693
+ if (actionRegistry?.[actionId]?.hideFromModal) continue;
3694
+ includedActions.add(actionId);
3695
+ const { name } = parseActionId(actionId);
3696
+ const groupName = getGroupName(actionId);
2094
3697
  if (!groupMap.has(groupName)) {
2095
3698
  groupMap.set(groupName, { name: groupName, shortcuts: [] });
2096
3699
  }
2097
3700
  groupMap.get(groupName).shortcuts.push({
2098
3701
  actionId,
2099
- label: labels?.[actionId] ?? name,
3702
+ label: labels?.[actionId] ?? actionRegistry?.[actionId]?.label ?? name,
2100
3703
  description: descriptions?.[actionId],
2101
3704
  bindings
2102
3705
  });
2103
3706
  }
3707
+ if (actionRegistry && showUnbound) {
3708
+ for (const [actionId, action] of Object.entries(actionRegistry)) {
3709
+ if (includedActions.has(actionId)) continue;
3710
+ if (action.hideFromModal) continue;
3711
+ const { name } = parseActionId(actionId);
3712
+ const groupName = getGroupName(actionId);
3713
+ if (!groupMap.has(groupName)) {
3714
+ groupMap.set(groupName, { name: groupName, shortcuts: [] });
3715
+ }
3716
+ groupMap.get(groupName).shortcuts.push({
3717
+ actionId,
3718
+ label: labels?.[actionId] ?? action.label ?? name,
3719
+ description: descriptions?.[actionId],
3720
+ bindings: []
3721
+ // No bindings
3722
+ });
3723
+ }
3724
+ }
2104
3725
  for (const group of groupMap.values()) {
2105
3726
  group.shortcuts.sort((a, b) => a.actionId.localeCompare(b.actionId));
2106
3727
  }
@@ -2127,25 +3748,22 @@ function KeyDisplay({
2127
3748
  combo,
2128
3749
  className
2129
3750
  }) {
2130
- const { key, modifiers } = combo;
2131
- const parts = [];
2132
- if (modifiers.meta) {
2133
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "meta", className: "kbd-modifier-icon" }, "meta"));
2134
- }
2135
- if (modifiers.ctrl) {
2136
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "ctrl", className: "kbd-modifier-icon" }, "ctrl"));
2137
- }
2138
- if (modifiers.alt) {
2139
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "alt", className: "kbd-modifier-icon" }, "alt"));
3751
+ return /* @__PURE__ */ jsxs("span", { className, children: [
3752
+ renderModifierIcons(combo.modifiers),
3753
+ renderKeyContent(combo.key)
3754
+ ] });
3755
+ }
3756
+ function SeqElemDisplay2({ elem, className }) {
3757
+ const Tooltip = useContext(TooltipContext);
3758
+ if (elem.type === "digit") {
3759
+ return /* @__PURE__ */ jsx(Tooltip, { title: "Any single digit (0-9)", children: /* @__PURE__ */ jsx("span", { className: `kbd-placeholder ${className || ""}`, children: "#" }) });
2140
3760
  }
2141
- if (modifiers.shift) {
2142
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }, "shift"));
3761
+ if (elem.type === "digits") {
3762
+ return /* @__PURE__ */ jsx(Tooltip, { title: "One or more digits (0-9)", children: /* @__PURE__ */ jsx("span", { className: `kbd-placeholder ${className || ""}`, children: "##" }) });
2143
3763
  }
2144
- const keyDisplay = key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1);
2145
- parts.push(/* @__PURE__ */ jsx("span", { children: keyDisplay }, "key"));
2146
- return /* @__PURE__ */ jsx("span", { className, children: parts });
3764
+ return /* @__PURE__ */ jsx(KeyDisplay, { combo: { key: elem.key, modifiers: elem.modifiers }, className });
2147
3765
  }
2148
- function BindingDisplay({
3766
+ function BindingDisplay2({
2149
3767
  binding,
2150
3768
  className,
2151
3769
  editable,
@@ -2156,10 +3774,11 @@ function BindingDisplay({
2156
3774
  onEdit,
2157
3775
  onRemove,
2158
3776
  pendingKeys,
2159
- activeKeys
3777
+ activeKeys,
3778
+ timeoutDuration = DEFAULT_SEQUENCE_TIMEOUT
2160
3779
  }) {
2161
3780
  const sequence = parseHotkeyString(binding);
2162
- const display = formatCombination(sequence);
3781
+ const keySeq = parseKeySeq(binding);
2163
3782
  let kbdClassName = "kbd-kbd";
2164
3783
  if (editable && !isEditing) kbdClassName += " editable";
2165
3784
  if (isEditing) kbdClassName += " editing";
@@ -2190,7 +3809,17 @@ function BindingDisplay({
2190
3809
  } else {
2191
3810
  content = "...";
2192
3811
  }
2193
- return /* @__PURE__ */ jsx("kbd", { className: kbdClassName, tabIndex: editable ? 0 : void 0, children: content });
3812
+ return /* @__PURE__ */ jsxs("kbd", { className: kbdClassName, tabIndex: editable ? 0 : void 0, children: [
3813
+ content,
3814
+ pendingKeys && pendingKeys.length > 0 && Number.isFinite(timeoutDuration) && /* @__PURE__ */ jsx(
3815
+ "span",
3816
+ {
3817
+ className: "kbd-timeout-bar",
3818
+ style: { animationDuration: `${timeoutDuration}ms` }
3819
+ },
3820
+ pendingKeys.length
3821
+ )
3822
+ ] });
2194
3823
  }
2195
3824
  return /* @__PURE__ */ jsxs("kbd", { className: kbdClassName, onClick: handleClick, tabIndex: editable ? 0 : void 0, onKeyDown: editable && onEdit ? (e) => {
2196
3825
  if (e.key === "Enter" || e.key === " ") {
@@ -2198,10 +3827,13 @@ function BindingDisplay({
2198
3827
  onEdit();
2199
3828
  }
2200
3829
  } : void 0, children: [
2201
- display.isSequence ? sequence.map((combo, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
3830
+ keySeq.length > 1 ? keySeq.map((elem, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
2202
3831
  i > 0 && /* @__PURE__ */ jsx("span", { className: "kbd-sequence-sep", children: " " }),
2203
- /* @__PURE__ */ jsx(KeyDisplay, { combo })
2204
- ] }, i)) : /* @__PURE__ */ jsx(KeyDisplay, { combo: sequence[0] }),
3832
+ /* @__PURE__ */ jsx(SeqElemDisplay2, { elem })
3833
+ ] }, i)) : keySeq.length === 1 ? /* @__PURE__ */ jsx(SeqElemDisplay2, { elem: keySeq[0] }) : (
3834
+ // Fallback for legacy parsing
3835
+ /* @__PURE__ */ jsx(KeyDisplay, { combo: sequence[0] })
3836
+ ),
2205
3837
  editable && onRemove && /* @__PURE__ */ jsx(
2206
3838
  "button",
2207
3839
  {
@@ -2226,8 +3858,7 @@ function ShortcutsModal({
2226
3858
  groupRenderers,
2227
3859
  isOpen: isOpenProp,
2228
3860
  onClose: onCloseProp,
2229
- openKey = "?",
2230
- autoRegisterOpen,
3861
+ defaultBinding = "?",
2231
3862
  editable = false,
2232
3863
  onBindingChange,
2233
3864
  onBindingAdd,
@@ -2238,7 +3869,9 @@ function ShortcutsModal({
2238
3869
  backdropClassName = "kbd-backdrop",
2239
3870
  modalClassName = "kbd-modal",
2240
3871
  title = "Keyboard Shortcuts",
2241
- hint
3872
+ hint,
3873
+ showUnbound,
3874
+ TooltipComponent: TooltipComponentProp = DefaultTooltip
2242
3875
  }) {
2243
3876
  const ctx = useMaybeHotkeysContext();
2244
3877
  const contextLabels = useMemo(() => {
@@ -2277,28 +3910,30 @@ function ShortcutsModal({
2277
3910
  const descriptions = descriptionsProp ?? contextDescriptions;
2278
3911
  const groupNames = groupNamesProp ?? contextGroups;
2279
3912
  const handleBindingChange = onBindingChange ?? (ctx ? (action, oldKey, newKey) => {
2280
- if (oldKey) ctx.registry.removeBinding(oldKey);
3913
+ if (oldKey) ctx.registry.removeBinding(action, oldKey);
2281
3914
  ctx.registry.setBinding(action, newKey);
2282
3915
  } : void 0);
2283
3916
  const handleBindingAdd = onBindingAdd ?? (ctx ? (action, key) => {
2284
3917
  ctx.registry.setBinding(action, key);
2285
3918
  } : void 0);
2286
- const handleBindingRemove = onBindingRemove ?? (ctx ? (_action, key) => {
2287
- ctx.registry.removeBinding(key);
3919
+ const handleBindingRemove = onBindingRemove ?? (ctx ? (action, key) => {
3920
+ ctx.registry.removeBinding(action, key);
2288
3921
  } : void 0);
2289
3922
  const handleReset = onReset ?? (ctx ? () => {
2290
3923
  ctx.registry.resetOverrides();
2291
3924
  } : void 0);
2292
- const shouldAutoRegisterOpen = autoRegisterOpen ?? !ctx;
2293
3925
  const [internalIsOpen, setInternalIsOpen] = useState(false);
2294
3926
  const isOpen = isOpenProp ?? ctx?.isModalOpen ?? internalIsOpen;
2295
3927
  const [editingAction, setEditingAction] = useState(null);
2296
3928
  const [editingKey, setEditingKey] = useState(null);
2297
3929
  const [addingAction, setAddingAction] = useState(null);
2298
3930
  const [pendingConflict, setPendingConflict] = useState(null);
3931
+ const [hasPendingConflictState, setHasPendingConflictState] = useState(false);
2299
3932
  const editingActionRef = useRef(null);
2300
3933
  const editingKeyRef = useRef(null);
2301
3934
  const addingActionRef = useRef(null);
3935
+ const setIsEditingBindingRef = useRef(ctx?.setIsEditingBinding);
3936
+ setIsEditingBindingRef.current = ctx?.setIsEditingBinding;
2302
3937
  const conflicts = useMemo(() => findConflicts(keymap), [keymap]);
2303
3938
  const actionBindings = useMemo(() => getActionBindings(keymap), [keymap]);
2304
3939
  const close = useCallback(() => {
@@ -2316,13 +3951,18 @@ function ShortcutsModal({
2316
3951
  ctx.closeModal();
2317
3952
  }
2318
3953
  }, [onCloseProp, ctx]);
2319
- const open = useCallback(() => {
2320
- if (ctx?.openModal) {
2321
- ctx.openModal();
2322
- } else {
2323
- setInternalIsOpen(true);
2324
- }
2325
- }, [ctx]);
3954
+ useAction(ACTION_MODAL, {
3955
+ label: "Show shortcuts",
3956
+ group: "Global",
3957
+ defaultBindings: defaultBinding ? [defaultBinding] : [],
3958
+ handler: useCallback(() => {
3959
+ if (ctx) {
3960
+ ctx.toggleModal();
3961
+ } else {
3962
+ setInternalIsOpen((prev) => !prev);
3963
+ }
3964
+ }, [ctx])
3965
+ });
2326
3966
  const checkConflict = useCallback((newKey, forAction) => {
2327
3967
  const existingActions = keymap[newKey];
2328
3968
  if (!existingActions) return null;
@@ -2330,7 +3970,24 @@ function ShortcutsModal({
2330
3970
  const conflicts2 = actions.filter((a) => a !== forAction);
2331
3971
  return conflicts2.length > 0 ? conflicts2 : null;
2332
3972
  }, [keymap]);
2333
- const { isRecording, startRecording, cancel, pendingKeys, activeKeys } = useRecordHotkey({
3973
+ const combinationsEqual2 = useCallback((a, b) => {
3974
+ 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;
3975
+ }, []);
3976
+ const isSequencePrefix = useCallback((a, b) => {
3977
+ if (a.length >= b.length) return false;
3978
+ for (let i = 0; i < a.length; i++) {
3979
+ if (!combinationsEqual2(a[i], b[i])) return false;
3980
+ }
3981
+ return true;
3982
+ }, [combinationsEqual2]);
3983
+ const sequencesEqual = useCallback((a, b) => {
3984
+ if (a.length !== b.length) return false;
3985
+ for (let i = 0; i < a.length; i++) {
3986
+ if (!combinationsEqual2(a[i], b[i])) return false;
3987
+ }
3988
+ return true;
3989
+ }, [combinationsEqual2]);
3990
+ const { isRecording, startRecording, cancel, pendingKeys, activeKeys, sequenceTimeout } = useRecordHotkey({
2334
3991
  onCapture: useCallback(
2335
3992
  (_sequence, display) => {
2336
3993
  const currentAddingAction = addingActionRef.current;
@@ -2358,6 +4015,7 @@ function ShortcutsModal({
2358
4015
  setEditingAction(null);
2359
4016
  setEditingKey(null);
2360
4017
  setAddingAction(null);
4018
+ setIsEditingBindingRef.current?.(false);
2361
4019
  },
2362
4020
  [checkConflict, handleBindingChange, handleBindingAdd]
2363
4021
  ),
@@ -2369,6 +4027,7 @@ function ShortcutsModal({
2369
4027
  setEditingKey(null);
2370
4028
  setAddingAction(null);
2371
4029
  setPendingConflict(null);
4030
+ setIsEditingBindingRef.current?.(false);
2372
4031
  }, []),
2373
4032
  // Tab to next/prev editable kbd and start editing
2374
4033
  onTab: useCallback(() => {
@@ -2393,7 +4052,7 @@ function ShortcutsModal({
2393
4052
  prev.click();
2394
4053
  }
2395
4054
  }, []),
2396
- pauseTimeout: pendingConflict !== null
4055
+ pauseTimeout: pendingConflict !== null || hasPendingConflictState
2397
4056
  });
2398
4057
  const startEditingBinding = useCallback(
2399
4058
  (action, key) => {
@@ -2404,9 +4063,10 @@ function ShortcutsModal({
2404
4063
  setEditingAction(action);
2405
4064
  setEditingKey(key);
2406
4065
  setPendingConflict(null);
4066
+ ctx?.setIsEditingBinding(true);
2407
4067
  startRecording();
2408
4068
  },
2409
- [startRecording]
4069
+ [startRecording, ctx?.setIsEditingBinding]
2410
4070
  );
2411
4071
  const startAddingBinding = useCallback(
2412
4072
  (action) => {
@@ -2417,9 +4077,10 @@ function ShortcutsModal({
2417
4077
  setEditingKey(null);
2418
4078
  setAddingAction(action);
2419
4079
  setPendingConflict(null);
4080
+ ctx?.setIsEditingBinding(true);
2420
4081
  startRecording();
2421
4082
  },
2422
- [startRecording]
4083
+ [startRecording, ctx?.setIsEditingBinding]
2423
4084
  );
2424
4085
  const startEditing = useCallback(
2425
4086
  (action, bindingIndex) => {
@@ -2441,7 +4102,8 @@ function ShortcutsModal({
2441
4102
  setEditingKey(null);
2442
4103
  setAddingAction(null);
2443
4104
  setPendingConflict(null);
2444
- }, [cancel]);
4105
+ ctx?.setIsEditingBinding(false);
4106
+ }, [cancel, ctx?.setIsEditingBinding]);
2445
4107
  const removeBinding = useCallback(
2446
4108
  (action, key) => {
2447
4109
  handleBindingRemove?.(action, key);
@@ -2451,6 +4113,31 @@ function ShortcutsModal({
2451
4113
  const reset = useCallback(() => {
2452
4114
  handleReset?.();
2453
4115
  }, [handleReset]);
4116
+ const pendingConflictInfo = useMemo(() => {
4117
+ if (!isRecording || pendingKeys.length === 0) {
4118
+ return { hasConflict: false, conflictingKeys: /* @__PURE__ */ new Set() };
4119
+ }
4120
+ const conflictingKeys = /* @__PURE__ */ new Set();
4121
+ for (const key of Object.keys(keymap)) {
4122
+ if (editingKey && key.toLowerCase() === editingKey.toLowerCase()) continue;
4123
+ const keySequence = parseHotkeyString(key);
4124
+ if (sequencesEqual(pendingKeys, keySequence)) {
4125
+ conflictingKeys.add(key);
4126
+ continue;
4127
+ }
4128
+ if (isSequencePrefix(pendingKeys, keySequence)) {
4129
+ conflictingKeys.add(key);
4130
+ continue;
4131
+ }
4132
+ if (isSequencePrefix(keySequence, pendingKeys)) {
4133
+ conflictingKeys.add(key);
4134
+ }
4135
+ }
4136
+ return { hasConflict: conflictingKeys.size > 0, conflictingKeys };
4137
+ }, [isRecording, pendingKeys, keymap, editingKey, sequencesEqual, isSequencePrefix]);
4138
+ useEffect(() => {
4139
+ setHasPendingConflictState(pendingConflictInfo.hasConflict);
4140
+ }, [pendingConflictInfo.hasConflict]);
2454
4141
  const renderEditableKbd = useCallback(
2455
4142
  (actionId, key, showRemove = false) => {
2456
4143
  const isEditingThis = editingAction === actionId && editingKey === key && !addingAction;
@@ -2462,13 +4149,15 @@ function ShortcutsModal({
2462
4149
  const defaultActions = Array.isArray(defaultAction) ? defaultAction : [defaultAction];
2463
4150
  return defaultActions.includes(actionId);
2464
4151
  })() : true;
4152
+ const isPendingConflict = pendingConflictInfo.conflictingKeys.has(key);
2465
4153
  return /* @__PURE__ */ jsx(
2466
- BindingDisplay,
4154
+ BindingDisplay2,
2467
4155
  {
2468
4156
  binding: key,
2469
4157
  editable,
2470
4158
  isEditing: isEditingThis,
2471
4159
  isConflict,
4160
+ isPendingConflict,
2472
4161
  isDefault,
2473
4162
  onEdit: () => {
2474
4163
  if (isRecording && !(editingAction === actionId && editingKey === key)) {
@@ -2489,24 +4178,27 @@ function ShortcutsModal({
2489
4178
  },
2490
4179
  onRemove: editable && showRemove ? () => removeBinding(actionId, key) : void 0,
2491
4180
  pendingKeys,
2492
- activeKeys
4181
+ activeKeys,
4182
+ timeoutDuration: pendingConflictInfo.hasConflict ? Infinity : sequenceTimeout
2493
4183
  },
2494
4184
  key
2495
4185
  );
2496
4186
  },
2497
- [editingAction, editingKey, addingAction, conflicts, defaults, editable, startEditingBinding, removeBinding, pendingKeys, activeKeys, isRecording, cancel, handleBindingAdd, handleBindingChange]
4187
+ [editingAction, editingKey, addingAction, conflicts, defaults, editable, startEditingBinding, removeBinding, pendingKeys, activeKeys, isRecording, cancel, handleBindingAdd, handleBindingChange, sequenceTimeout, pendingConflictInfo]
2498
4188
  );
2499
4189
  const renderAddButton = useCallback(
2500
4190
  (actionId) => {
2501
4191
  const isAddingThis = addingAction === actionId;
2502
4192
  if (isAddingThis) {
2503
4193
  return /* @__PURE__ */ jsx(
2504
- BindingDisplay,
4194
+ BindingDisplay2,
2505
4195
  {
2506
4196
  binding: "",
2507
4197
  isEditing: true,
4198
+ isPendingConflict: pendingConflictInfo.hasConflict,
2508
4199
  pendingKeys,
2509
- activeKeys
4200
+ activeKeys,
4201
+ timeoutDuration: pendingConflictInfo.hasConflict ? Infinity : sequenceTimeout
2510
4202
  }
2511
4203
  );
2512
4204
  }
@@ -2535,13 +4227,14 @@ function ShortcutsModal({
2535
4227
  }
2536
4228
  );
2537
4229
  },
2538
- [addingAction, pendingKeys, activeKeys, startAddingBinding, isRecording, cancel, handleBindingAdd, handleBindingChange]
4230
+ [addingAction, pendingKeys, activeKeys, startAddingBinding, isRecording, cancel, handleBindingAdd, handleBindingChange, sequenceTimeout, pendingConflictInfo]
2539
4231
  );
2540
4232
  const renderCell = useCallback(
2541
4233
  (actionId, keys) => {
4234
+ const showAddButton = editable && (multipleBindings || keys.length === 0);
2542
4235
  return /* @__PURE__ */ jsxs("span", { className: "kbd-action-bindings", children: [
2543
4236
  keys.map((key) => /* @__PURE__ */ jsx(Fragment$1, { children: renderEditableKbd(actionId, key, true) }, key)),
2544
- editable && multipleBindings && renderAddButton(actionId)
4237
+ showAddButton && renderAddButton(actionId)
2545
4238
  ] });
2546
4239
  },
2547
4240
  [renderEditableKbd, renderAddButton, editable, multipleBindings]
@@ -2558,14 +4251,10 @@ function ShortcutsModal({
2558
4251
  editingKey,
2559
4252
  addingAction
2560
4253
  }), [renderCell, renderEditableKbd, renderAddButton, startEditingBinding, startAddingBinding, removeBinding, isRecording, editingAction, editingKey, addingAction]);
2561
- const modalKeymap = shouldAutoRegisterOpen ? { [openKey]: "openShortcuts" } : {};
2562
4254
  useHotkeys(
2563
- { ...modalKeymap, escape: "closeShortcuts" },
2564
- {
2565
- openShortcuts: open,
2566
- closeShortcuts: close
2567
- },
2568
- { enabled: shouldAutoRegisterOpen || isOpen }
4255
+ { escape: "closeShortcuts" },
4256
+ { closeShortcuts: close },
4257
+ { enabled: isOpen }
2569
4258
  );
2570
4259
  useEffect(() => {
2571
4260
  if (!isOpen || !editingAction && !addingAction) return;
@@ -2579,6 +4268,19 @@ function ShortcutsModal({
2579
4268
  window.addEventListener("keydown", handleEscape, true);
2580
4269
  return () => window.removeEventListener("keydown", handleEscape, true);
2581
4270
  }, [isOpen, editingAction, addingAction, cancelEditing]);
4271
+ useEffect(() => {
4272
+ if (!isOpen || !ctx) return;
4273
+ const handleMetaK = (e) => {
4274
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
4275
+ e.preventDefault();
4276
+ e.stopPropagation();
4277
+ close();
4278
+ ctx.openOmnibar();
4279
+ }
4280
+ };
4281
+ window.addEventListener("keydown", handleMetaK, true);
4282
+ return () => window.removeEventListener("keydown", handleMetaK, true);
4283
+ }, [isOpen, ctx, close]);
2582
4284
  const handleBackdropClick = useCallback(
2583
4285
  (e) => {
2584
4286
  if (e.target === e.currentTarget) {
@@ -2598,9 +4300,10 @@ function ShortcutsModal({
2598
4300
  },
2599
4301
  [editingAction, addingAction, cancelEditing]
2600
4302
  );
4303
+ const effectiveShowUnbound = showUnbound ?? editable;
2601
4304
  const shortcutGroups = useMemo(
2602
- () => organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder),
2603
- [keymap, labels, descriptions, groupNames, groupOrder]
4305
+ () => organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder, ctx?.registry.actionRegistry, effectiveShowUnbound),
4306
+ [keymap, labels, descriptions, groupNames, groupOrder, ctx?.registry.actionRegistry, effectiveShowUnbound]
2604
4307
  );
2605
4308
  if (!isOpen) return null;
2606
4309
  if (children) {
@@ -2630,7 +4333,7 @@ function ShortcutsModal({
2630
4333
  renderCell(actionId, bindings)
2631
4334
  ] }, actionId));
2632
4335
  };
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: [
4336
+ 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
4337
  /* @__PURE__ */ jsxs("div", { className: "kbd-modal-header", children: [
2635
4338
  /* @__PURE__ */ jsx("h2", { className: "kbd-modal-title", children: title }),
2636
4339
  /* @__PURE__ */ jsxs("div", { className: "kbd-modal-header-buttons", children: [
@@ -2699,9 +4402,9 @@ function ShortcutsModal({
2699
4402
  )
2700
4403
  ] })
2701
4404
  ] })
2702
- ] }) });
4405
+ ] }) }) });
2703
4406
  }
2704
4407
 
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 };
4408
+ 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, OmnibarEndpointsRegistryContext, 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, parseHotkeyString, parseKeySeq, searchActions, useAction, useActions, useActionsRegistry, useEditableHotkeys, useHotkeys, useHotkeysContext, useMaybeHotkeysContext, useOmnibar, useOmnibarEndpoint, useOmnibarEndpointsRegistry, useRecordHotkey };
2706
4409
  //# sourceMappingURL=index.js.map
2707
4410
  //# sourceMappingURL=index.js.map