use-kbd 0.4.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
@@ -70,9 +70,7 @@ function useActionsRegistry(options = {}) {
70
70
  const filterRedundantOverrides = useCallback((overrides2) => {
71
71
  const filtered = {};
72
72
  for (const [key, actionOrActions] of Object.entries(overrides2)) {
73
- if (actionOrActions === "") {
74
- continue;
75
- } else if (Array.isArray(actionOrActions)) {
73
+ if (actionOrActions === "") ; else if (Array.isArray(actionOrActions)) {
76
74
  const nonDefaultActions = actionOrActions.filter((a) => !isDefaultBinding(key, a));
77
75
  if (nonDefaultActions.length > 0) {
78
76
  filtered[key] = nonDefaultActions.length === 1 ? nonDefaultActions[0] : nonDefaultActions;
@@ -136,10 +134,10 @@ function useActionsRegistry(options = {}) {
136
134
  actionsRef.current.delete(id);
137
135
  setActionsVersion((v) => v + 1);
138
136
  }, []);
139
- const execute = useCallback((id) => {
137
+ const execute = useCallback((id, captures) => {
140
138
  const action = actionsRef.current.get(id);
141
139
  if (action && (action.config.enabled ?? true)) {
142
- action.config.handler();
140
+ action.config.handler(void 0, captures);
143
141
  }
144
142
  }, []);
145
143
  const keymap = useMemo(() => {
@@ -163,9 +161,7 @@ function useActionsRegistry(options = {}) {
163
161
  }
164
162
  }
165
163
  for (const [key, actionOrActions] of Object.entries(overrides)) {
166
- if (actionOrActions === "") {
167
- continue;
168
- } else {
164
+ if (actionOrActions === "") ; else {
169
165
  const actions2 = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
170
166
  for (const actionId of actions2) {
171
167
  addToKey(key, actionId);
@@ -281,6 +277,70 @@ function useActionsRegistry(options = {}) {
281
277
  resetOverrides
282
278
  ]);
283
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
+ }
284
344
 
285
345
  // src/constants.ts
286
346
  var DEFAULT_SEQUENCE_TIMEOUT = Infinity;
@@ -318,7 +378,11 @@ function isShiftedSymbol(key) {
318
378
  }
319
379
  function isMac() {
320
380
  if (typeof navigator === "undefined") return false;
321
- 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);
322
386
  }
323
387
  function normalizeKey(key) {
324
388
  const keyMap = {
@@ -353,7 +417,7 @@ function formatKeyForDisplay(key) {
353
417
  "space": "Space",
354
418
  "escape": "Esc",
355
419
  "enter": "\u21B5",
356
- "tab": "Tab",
420
+ "tab": "\u21E5",
357
421
  "backspace": "\u232B",
358
422
  "delete": "Del",
359
423
  "arrowup": "\u2191",
@@ -493,13 +557,6 @@ function parseHotkeyString(hotkeyStr) {
493
557
  const parts = hotkeyStr.trim().split(/\s+/);
494
558
  return parts.map(parseSingleCombination);
495
559
  }
496
- function parseCombinationId(id) {
497
- const sequence = parseHotkeyString(id);
498
- if (sequence.length === 0) {
499
- return { key: "", modifiers: { ctrl: false, alt: false, shift: false, meta: false } };
500
- }
501
- return sequence[0];
502
- }
503
560
  var NO_MODIFIERS = { ctrl: false, alt: false, shift: false, meta: false };
504
561
  function parseSeqElem(str) {
505
562
  if (str === "\\d") {
@@ -615,6 +672,13 @@ function isPrefix(a, b) {
615
672
  function combinationsEqual(a, b) {
616
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;
617
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
+ }
618
682
  function isDigitKey(key) {
619
683
  return /^[0-9]$/.test(key);
620
684
  }
@@ -735,28 +799,77 @@ function getSequenceCompletions(pendingKeys, keymap) {
735
799
  if (pendingKeys.length === 0) return [];
736
800
  const completions = [];
737
801
  for (const [hotkeyStr, actionOrActions] of Object.entries(keymap)) {
738
- const sequence = parseHotkeyString(hotkeyStr);
739
802
  const keySeq = parseKeySeq(hotkeyStr);
740
- if (sequence.length <= pendingKeys.length) continue;
741
- let isPrefix2 = true;
742
- for (let i = 0; i < pendingKeys.length; i++) {
743
- if (!combinationsEqual(pendingKeys[i], sequence[i])) {
744
- isPrefix2 = false;
745
- break;
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++;
746
839
  }
747
840
  }
748
- if (isPrefix2) {
749
- const remainingKeySeq = keySeq.slice(pendingKeys.length);
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);
750
857
  const nextKeys = formatKeySeq(remainingKeySeq).display;
751
- const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
752
858
  completions.push({
753
859
  nextKeys,
860
+ nextKeySeq: remainingKeySeq,
754
861
  fullSequence: hotkeyStr,
755
862
  display: formatKeySeq(keySeq),
756
- actions
863
+ actions,
864
+ isComplete: false,
865
+ captures: captures.length > 0 ? captures : void 0
757
866
  });
758
867
  }
759
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
+ });
760
873
  return completions;
761
874
  }
762
875
  function getActionBindings(keymap) {
@@ -847,7 +960,7 @@ function searchActions(query, actions, keymap) {
847
960
  }
848
961
  const matched = labelMatch.matched || descMatch.matched || groupMatch.matched || idMatch.matched || keywordScore > 0;
849
962
  if (!matched && query) continue;
850
- 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;
851
964
  results.push({
852
965
  id,
853
966
  action,
@@ -925,6 +1038,9 @@ function advanceMatchState(state, pattern, combo) {
925
1038
  const digitValue = parseInt(elem.partial, 10);
926
1039
  newState[i] = { type: "digits", value: digitValue };
927
1040
  pos = i + 1;
1041
+ if (pos >= pattern.length) {
1042
+ return { status: "failed" };
1043
+ }
928
1044
  break;
929
1045
  }
930
1046
  }
@@ -1047,7 +1163,7 @@ function useHotkeys(keymap, handlers, options = {}) {
1047
1163
  }
1048
1164
  return false;
1049
1165
  }, [preventDefault, stopPropagation]);
1050
- const tryExecuteKeySeq = useCallback((matchKey, matchState, captures, e) => {
1166
+ const tryExecuteKeySeq = useCallback((matchKey, captures, e) => {
1051
1167
  for (const entry of parsedKeymapRef.current) {
1052
1168
  if (entry.key === matchKey) {
1053
1169
  for (const action of entry.actions) {
@@ -1103,7 +1219,24 @@ function useHotkeys(keymap, handlers, options = {}) {
1103
1219
  }
1104
1220
  if (e.key === "Enter" && pendingKeysRef.current.length > 0) {
1105
1221
  e.preventDefault();
1106
- 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
+ }
1107
1240
  clearPending();
1108
1241
  if (!executed) {
1109
1242
  onSequenceCancel?.();
@@ -1116,14 +1249,57 @@ function useHotkeys(keymap, handlers, options = {}) {
1116
1249
  return;
1117
1250
  }
1118
1251
  const currentCombo = eventToCombination(e);
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);
1258
+ }
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);
1282
+ }
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
+ }
1290
+ }
1291
+ }
1292
+ return;
1293
+ }
1294
+ }
1119
1295
  const newSequence = [...pendingKeysRef.current, currentCombo];
1120
- let keySeqMatched = false;
1121
- let keySeqPartial = false;
1296
+ const completeMatches = [];
1297
+ let hasPartials = false;
1122
1298
  const matchStates = matchStatesRef.current;
1123
- const hasPartialMatches = matchStates.size > 0;
1299
+ const hadPartialMatches = matchStates.size > 0;
1124
1300
  for (const entry of parsedKeymapRef.current) {
1125
1301
  let state = matchStates.get(entry.key);
1126
- if (hasPartialMatches && !state) {
1302
+ if (hadPartialMatches && !state) {
1127
1303
  continue;
1128
1304
  }
1129
1305
  if (!state) {
@@ -1132,22 +1308,27 @@ function useHotkeys(keymap, handlers, options = {}) {
1132
1308
  }
1133
1309
  const result = advanceMatchState(state, entry.keySeq, currentCombo);
1134
1310
  if (result.status === "matched") {
1135
- if (tryExecuteKeySeq(entry.key, result.state, result.captures, e)) {
1136
- clearPending();
1137
- keySeqMatched = true;
1138
- break;
1139
- }
1311
+ completeMatches.push({
1312
+ key: entry.key,
1313
+ state: result.state,
1314
+ captures: result.captures
1315
+ });
1316
+ matchStates.delete(entry.key);
1140
1317
  } else if (result.status === "partial") {
1141
1318
  matchStates.set(entry.key, result.state);
1142
- keySeqPartial = true;
1319
+ hasPartials = true;
1143
1320
  } else {
1144
1321
  matchStates.delete(entry.key);
1145
1322
  }
1146
1323
  }
1147
- if (keySeqMatched) {
1148
- return;
1324
+ if (completeMatches.length === 1 && !hasPartials) {
1325
+ const match = completeMatches[0];
1326
+ if (tryExecuteKeySeq(match.key, match.captures, e)) {
1327
+ clearPending();
1328
+ return;
1329
+ }
1149
1330
  }
1150
- if (keySeqPartial) {
1331
+ if (completeMatches.length > 0 || hasPartials) {
1151
1332
  setPendingKeys(newSequence);
1152
1333
  setIsAwaitingSequence(true);
1153
1334
  if (pendingKeysRef.current.length === 0) {
@@ -1230,8 +1411,11 @@ function useHotkeys(keymap, handlers, options = {}) {
1230
1411
  }
1231
1412
  }
1232
1413
  if (pendingKeysRef.current.length > 0) {
1233
- clearPending();
1234
- onSequenceCancel?.();
1414
+ setPendingKeys(newSequence);
1415
+ if (preventDefault) {
1416
+ e.preventDefault();
1417
+ }
1418
+ return;
1235
1419
  }
1236
1420
  const singleMatch = tryExecute([currentCombo], e);
1237
1421
  if (!singleMatch) {
@@ -1293,7 +1477,8 @@ var HotkeysContext = createContext(null);
1293
1477
  var DEFAULT_CONFIG = {
1294
1478
  storageKey: "use-kbd",
1295
1479
  sequenceTimeout: DEFAULT_SEQUENCE_TIMEOUT,
1296
- disableConflicts: true,
1480
+ disableConflicts: false,
1481
+ // Keep conflicting bindings active; SeqM handles disambiguation
1297
1482
  minViewportWidth: 768,
1298
1483
  enableOnTouch: false
1299
1484
  };
@@ -1306,6 +1491,7 @@ function HotkeysProvider({
1306
1491
  ...configProp
1307
1492
  }), [configProp]);
1308
1493
  const registry = useActionsRegistry({ storageKey: config.storageKey });
1494
+ const endpointsRegistry = useOmnibarEndpointsRegistry();
1309
1495
  const [isEnabled, setIsEnabled] = useState(true);
1310
1496
  useEffect(() => {
1311
1497
  if (typeof window === "undefined") return;
@@ -1397,6 +1583,7 @@ function HotkeysProvider({
1397
1583
  );
1398
1584
  const value = useMemo(() => ({
1399
1585
  registry,
1586
+ endpointsRegistry,
1400
1587
  isEnabled,
1401
1588
  isModalOpen,
1402
1589
  openModal,
@@ -1424,6 +1611,7 @@ function HotkeysProvider({
1424
1611
  getCompletions
1425
1612
  }), [
1426
1613
  registry,
1614
+ endpointsRegistry,
1427
1615
  isEnabled,
1428
1616
  isModalOpen,
1429
1617
  openModal,
@@ -1448,7 +1636,7 @@ function HotkeysProvider({
1448
1636
  searchActionsHelper,
1449
1637
  getCompletions
1450
1638
  ]);
1451
- 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 }) }) });
1452
1640
  }
1453
1641
  function useHotkeysContext() {
1454
1642
  const context = useContext(HotkeysContext);
@@ -1536,6 +1724,38 @@ function useActions(actions) {
1536
1724
  )
1537
1725
  ]);
1538
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
+ }
1539
1759
  function useEventCallback(fn) {
1540
1760
  const ref = useRef(fn);
1541
1761
  ref.current = fn;
@@ -1784,7 +2004,6 @@ function useRecordHotkey(options = {}) {
1784
2004
  };
1785
2005
  }, [isRecording, preventDefault, sequenceTimeout, clearTimeout_, submit, cancel, onCapture, onTab, onShiftTab]);
1786
2006
  const display = sequence ? formatCombination(sequence) : null;
1787
- const combination = sequence && sequence.length > 0 ? sequence[0] : null;
1788
2007
  return {
1789
2008
  isRecording,
1790
2009
  startRecording,
@@ -1794,13 +2013,11 @@ function useRecordHotkey(options = {}) {
1794
2013
  display,
1795
2014
  pendingKeys,
1796
2015
  activeKeys,
1797
- sequenceTimeout,
1798
- combination
1799
- // deprecated
2016
+ sequenceTimeout
1800
2017
  };
1801
2018
  }
1802
2019
  function useEditableHotkeys(defaults, handlers, options = {}) {
1803
- const { storageKey, disableConflicts = true, ...hotkeyOptions } = options;
2020
+ const { storageKey, disableConflicts = false, ...hotkeyOptions } = options;
1804
2021
  const [overrides, setOverrides] = useState(() => {
1805
2022
  if (!storageKey || typeof window === "undefined") return {};
1806
2023
  try {
@@ -1896,6 +2113,7 @@ function useEditableHotkeys(defaults, handlers, options = {}) {
1896
2113
  };
1897
2114
  }
1898
2115
  var { max: max2, min } = Math;
2116
+ var DEFAULT_DEBOUNCE_MS = 150;
1899
2117
  function useOmnibar(options) {
1900
2118
  const {
1901
2119
  actions,
@@ -1904,17 +2122,27 @@ function useOmnibar(options) {
1904
2122
  openKey = "meta+k",
1905
2123
  enabled = true,
1906
2124
  onExecute,
2125
+ onExecuteRemote,
1907
2126
  onOpen,
1908
2127
  onClose,
1909
- maxResults = 10
2128
+ maxResults = 10,
2129
+ endpointsRegistry,
2130
+ debounceMs = DEFAULT_DEBOUNCE_MS
1910
2131
  } = options;
1911
2132
  const [isOpen, setIsOpen] = useState(false);
1912
2133
  const [query, setQuery] = useState("");
1913
2134
  const [selectedIndex, setSelectedIndex] = useState(0);
2135
+ const [endpointStates, setEndpointStates] = useState(/* @__PURE__ */ new Map());
1914
2136
  const handlersRef = useRef(handlers);
1915
2137
  handlersRef.current = handlers;
1916
2138
  const onExecuteRef = useRef(onExecute);
1917
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;
1918
2146
  const omnibarKeymap = useMemo(() => {
1919
2147
  if (!enabled) return {};
1920
2148
  return { [openKey]: "omnibar:toggle" };
@@ -1940,12 +2168,189 @@ function useOmnibar(options) {
1940
2168
  const allResults = searchActions(query, actions, keymap);
1941
2169
  return allResults.slice(0, maxResults);
1942
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;
1943
2348
  const completions = useMemo(() => {
1944
2349
  return getSequenceCompletions(pendingKeys, keymap);
1945
2350
  }, [pendingKeys, keymap]);
1946
2351
  useEffect(() => {
1947
2352
  setSelectedIndex(0);
1948
- }, [results]);
2353
+ }, [results, remoteResults]);
1949
2354
  const open = useCallback(() => {
1950
2355
  setIsOpen(true);
1951
2356
  setQuery("");
@@ -1972,8 +2377,8 @@ function useOmnibar(options) {
1972
2377
  });
1973
2378
  }, [onOpen, onClose]);
1974
2379
  const selectNext = useCallback(() => {
1975
- setSelectedIndex((prev) => min(prev + 1, results.length - 1));
1976
- }, [results.length]);
2380
+ setSelectedIndex((prev) => min(prev + 1, totalResults - 1));
2381
+ }, [totalResults]);
1977
2382
  const selectPrev = useCallback(() => {
1978
2383
  setSelectedIndex((prev) => max2(prev - 1, 0));
1979
2384
  }, []);
@@ -1981,15 +2386,47 @@ function useOmnibar(options) {
1981
2386
  setSelectedIndex(0);
1982
2387
  }, []);
1983
2388
  const execute = useCallback((actionId) => {
1984
- const id = actionId ?? results[selectedIndex]?.id;
1985
- if (!id) return;
1986
- close();
1987
- if (handlersRef.current?.[id]) {
1988
- const event = new KeyboardEvent("keydown", { key: "Enter" });
1989
- handlersRef.current[id](event);
1990
- }
1991
- onExecuteRef.current?.(id);
1992
- }, [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]);
1993
2430
  useEffect(() => {
1994
2431
  if (!isOpen) return;
1995
2432
  const handleKeyDown = (e) => {
@@ -2031,7 +2468,12 @@ function useOmnibar(options) {
2031
2468
  query,
2032
2469
  setQuery,
2033
2470
  results,
2471
+ remoteResults,
2472
+ isLoadingRemote,
2473
+ endpointPagination,
2474
+ loadMore,
2034
2475
  selectedIndex,
2476
+ totalResults,
2035
2477
  selectNext,
2036
2478
  selectPrev,
2037
2479
  execute,
@@ -2149,6 +2591,26 @@ function Backspace({ className, style }) {
2149
2591
  }
2150
2592
  );
2151
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
+ }
2152
2614
  function getKeyIcon(key) {
2153
2615
  switch (key.toLowerCase()) {
2154
2616
  case "arrowup":
@@ -2163,6 +2625,8 @@ function getKeyIcon(key) {
2163
2625
  return Enter;
2164
2626
  case "backspace":
2165
2627
  return Backspace;
2628
+ case "tab":
2629
+ return Tab;
2166
2630
  default:
2167
2631
  return null;
2168
2632
  }
@@ -2267,7 +2731,6 @@ var Alt = forwardRef(
2267
2731
  )
2268
2732
  );
2269
2733
  Alt.displayName = "Alt";
2270
- var isMac2 = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
2271
2734
  function getModifierIcon(modifier) {
2272
2735
  switch (modifier) {
2273
2736
  case "meta":
@@ -2279,7 +2742,7 @@ function getModifierIcon(modifier) {
2279
2742
  case "opt":
2280
2743
  return Option;
2281
2744
  case "alt":
2282
- return isMac2 ? Option : Alt;
2745
+ return isMac() ? Option : Alt;
2283
2746
  }
2284
2747
  }
2285
2748
  var ModifierIcon = forwardRef(
@@ -2289,28 +2752,47 @@ var ModifierIcon = forwardRef(
2289
2752
  }
2290
2753
  );
2291
2754
  ModifierIcon.displayName = "ModifierIcon";
2292
- function KeyCombo({ combo }) {
2293
- const { key, modifiers } = combo;
2294
- const parts = [];
2755
+ function renderModifierIcons(modifiers, className = "kbd-modifier-icon") {
2756
+ const icons = [];
2295
2757
  if (modifiers.meta) {
2296
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "meta", className: "kbd-modifier-icon" }, "meta"));
2758
+ icons.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "meta", className }, "meta"));
2297
2759
  }
2298
2760
  if (modifiers.ctrl) {
2299
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "ctrl", className: "kbd-modifier-icon" }, "ctrl"));
2761
+ icons.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "ctrl", className }, "ctrl"));
2300
2762
  }
2301
2763
  if (modifiers.alt) {
2302
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "alt", className: "kbd-modifier-icon" }, "alt"));
2764
+ icons.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "alt", className }, "alt"));
2303
2765
  }
2304
2766
  if (modifiers.shift) {
2305
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }, "shift"));
2767
+ icons.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "shift", className }, "shift"));
2306
2768
  }
2307
- const KeyIcon = getKeyIcon(key);
2308
- if (KeyIcon) {
2309
- parts.push(/* @__PURE__ */ jsx(KeyIcon, { className: "kbd-key-icon" }, "key"));
2310
- } else {
2311
- parts.push(/* @__PURE__ */ jsx("span", { children: formatKeyForDisplay(key) }, "key"));
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);
2312
2782
  }
2313
- return /* @__PURE__ */ jsx(Fragment, { children: parts });
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
+ ] });
2314
2796
  }
2315
2797
  function SeqElemDisplay({ elem }) {
2316
2798
  if (elem.type === "digit") {
@@ -2462,7 +2944,7 @@ function KeybindingEditor({
2462
2944
  return Array.from(allActions).map((action) => {
2463
2945
  const key = actionMap.get(action) ?? defaultActionMap.get(action) ?? "";
2464
2946
  const defaultKey = defaultActionMap.get(action) ?? "";
2465
- const combo = parseCombinationId(key);
2947
+ const combo = parseHotkeyString(key);
2466
2948
  const display = formatCombination(combo);
2467
2949
  const conflictActions = conflicts.get(key);
2468
2950
  return {
@@ -2619,15 +3101,30 @@ function LookupModal({ defaultBinding = "meta+shift+k" } = {}) {
2619
3101
  const filteredBindings = useMemo(() => {
2620
3102
  if (pendingKeys.length === 0) return allBindings;
2621
3103
  return allBindings.filter((result) => {
2622
- if (result.sequence.length < pendingKeys.length) return false;
2623
- for (let i = 0; i < pendingKeys.length; i++) {
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++) {
2624
3108
  const pending = pendingKeys[i];
2625
- const target = result.sequence[i];
2626
- if (pending.key !== target.key) return false;
2627
- if (pending.modifiers.ctrl !== target.modifiers.ctrl) return false;
2628
- if (pending.modifiers.alt !== target.modifiers.alt) return false;
2629
- if (pending.modifiers.shift !== target.modifiers.shift) return false;
2630
- if (pending.modifiers.meta !== target.modifiers.meta) return false;
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
+ }
2631
3128
  }
2632
3129
  return true;
2633
3130
  });
@@ -2739,7 +3236,7 @@ function LookupModal({ defaultBinding = "meta+shift+k" } = {}) {
2739
3236
  },
2740
3237
  onMouseEnter: () => setSelectedIndex(index),
2741
3238
  children: [
2742
- /* @__PURE__ */ jsx("kbd", { className: "kbd-kbd", children: result.display }),
3239
+ /* @__PURE__ */ jsx("span", { className: "kbd-lookup-binding", children: renderKeySeq(result.keySeq) }),
2743
3240
  /* @__PURE__ */ jsx("span", { className: "kbd-lookup-labels", children: result.labels.join(", ") })
2744
3241
  ]
2745
3242
  },
@@ -2783,6 +3280,7 @@ function Omnibar({
2783
3280
  onOpen: onOpenProp,
2784
3281
  onClose: onCloseProp,
2785
3282
  onExecute: onExecuteProp,
3283
+ onExecuteRemote: onExecuteRemoteProp,
2786
3284
  maxResults = 10,
2787
3285
  placeholder = "Type a command...",
2788
3286
  children,
@@ -2820,13 +3318,25 @@ function Omnibar({
2820
3318
  ctx.openOmnibar();
2821
3319
  }
2822
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]);
2823
3328
  const {
2824
3329
  isOpen: internalIsOpen,
2825
3330
  close,
2826
3331
  query,
2827
3332
  setQuery,
2828
3333
  results,
3334
+ remoteResults,
3335
+ isLoadingRemote,
3336
+ endpointPagination,
3337
+ loadMore,
2829
3338
  selectedIndex,
3339
+ totalResults,
2830
3340
  selectNext,
2831
3341
  selectPrev,
2832
3342
  execute,
@@ -2843,9 +3353,22 @@ function Omnibar({
2843
3353
  onOpen: handleOpen,
2844
3354
  onClose: handleClose,
2845
3355
  onExecute: handleExecute,
2846
- maxResults
3356
+ onExecuteRemote: handleExecuteRemote,
3357
+ maxResults,
3358
+ endpointsRegistry: ctx?.endpointsRegistry
2847
3359
  });
2848
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]);
2849
3372
  useEffect(() => {
2850
3373
  if (isOpen) {
2851
3374
  requestAnimationFrame(() => {
@@ -2853,6 +3376,38 @@ function Omnibar({
2853
3376
  });
2854
3377
  }
2855
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]);
2856
3411
  useEffect(() => {
2857
3412
  if (!isOpen) return;
2858
3413
  const handleGlobalKeyDown = (e) => {
@@ -2902,7 +3457,12 @@ function Omnibar({
2902
3457
  query,
2903
3458
  setQuery,
2904
3459
  results,
3460
+ remoteResults,
3461
+ isLoadingRemote,
3462
+ endpointPagination,
3463
+ loadMore,
2905
3464
  selectedIndex,
3465
+ totalResults,
2906
3466
  selectNext,
2907
3467
  selectPrev,
2908
3468
  execute,
@@ -2930,21 +3490,61 @@ function Omnibar({
2930
3490
  spellCheck: false
2931
3491
  }
2932
3492
  ),
2933
- /* @__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(
2934
- "div",
2935
- {
2936
- className: `kbd-omnibar-result ${i === selectedIndex ? "selected" : ""}`,
2937
- onClick: () => execute(result.id),
2938
- 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
+ ]
2939
3504
  },
2940
- children: [
2941
- /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-result-label", children: result.action.label }),
2942
- result.action.group && /* @__PURE__ */ jsx("span", { className: "kbd-omnibar-result-category", children: result.action.group }),
2943
- 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)) })
2944
- ]
2945
- },
2946
- result.id
2947
- )) })
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
+ ] }) })
2948
3548
  ] }) });
2949
3549
  }
2950
3550
  function SequenceModal() {
@@ -2955,41 +3555,99 @@ function SequenceModal() {
2955
3555
  sequenceTimeoutStartedAt: timeoutStartedAt,
2956
3556
  sequenceTimeout,
2957
3557
  getCompletions,
2958
- registry
3558
+ registry,
3559
+ executeAction
2959
3560
  } = useHotkeysContext();
3561
+ const [selectedIndex, setSelectedIndex] = useState(0);
3562
+ const [hasInteracted, setHasInteracted] = useState(false);
2960
3563
  const completions = useMemo(() => {
2961
3564
  if (pendingKeys.length === 0) return [];
2962
3565
  return getCompletions(pendingKeys);
2963
3566
  }, [getCompletions, pendingKeys]);
2964
- const formattedPendingKeys = useMemo(() => {
2965
- if (pendingKeys.length === 0) return "";
2966
- return formatCombination(pendingKeys).display;
2967
- }, [pendingKeys]);
2968
- const getActionLabel = (actionId) => {
2969
- const action = registry.actions.get(actionId);
2970
- return action?.config.label || actionId;
2971
- };
2972
- const groupedCompletions = useMemo(() => {
2973
- const byNextKey = /* @__PURE__ */ new Map();
3567
+ const flatCompletions = useMemo(() => {
3568
+ const items = [];
2974
3569
  for (const c of completions) {
2975
- const existing = byNextKey.get(c.nextKeys);
2976
- if (existing) {
2977
- existing.push(c);
2978
- } else {
2979
- 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
+ });
2980
3578
  }
2981
3579
  }
2982
- return byNextKey;
3580
+ return items;
2983
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
+ };
2984
3642
  if (!isAwaitingSequence || pendingKeys.length === 0) {
2985
3643
  return null;
2986
3644
  }
2987
3645
  return /* @__PURE__ */ jsx("div", { className: "kbd-sequence-backdrop", onClick: cancelSequence, children: /* @__PURE__ */ jsxs("div", { className: "kbd-sequence", onClick: (e) => e.stopPropagation(), children: [
2988
3646
  /* @__PURE__ */ jsxs("div", { className: "kbd-sequence-current", children: [
2989
- /* @__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)) }),
2990
3648
  /* @__PURE__ */ jsx("span", { className: "kbd-sequence-ellipsis", children: "\u2026" })
2991
3649
  ] }),
2992
- timeoutStartedAt && /* @__PURE__ */ jsx(
3650
+ shouldShowTimeout && /* @__PURE__ */ jsx(
2993
3651
  "div",
2994
3652
  {
2995
3653
  className: "kbd-sequence-timeout",
@@ -2997,15 +3655,19 @@ function SequenceModal() {
2997
3655
  },
2998
3656
  timeoutStartedAt
2999
3657
  ),
3000
- completions.length > 0 && /* @__PURE__ */ jsx("div", { className: "kbd-sequence-completions", children: Array.from(groupedCompletions.entries()).map(([nextKey, comps]) => /* @__PURE__ */ jsxs("div", { className: "kbd-sequence-completion", children: [
3001
- /* @__PURE__ */ jsx("kbd", { className: "kbd-kbd", children: nextKey }),
3002
- /* @__PURE__ */ jsx("span", { className: "kbd-sequence-arrow", children: "\u2192" }),
3003
- /* @__PURE__ */ jsx("span", { className: "kbd-sequence-actions", children: comps.flatMap((c) => c.actions).map((action, i) => /* @__PURE__ */ jsxs("span", { children: [
3004
- i > 0 && ", ",
3005
- getActionLabel(action)
3006
- ] }, action)) })
3007
- ] }, nextKey)) }),
3008
- 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" })
3009
3671
  ] }) });
3010
3672
  }
3011
3673
  var DefaultTooltip = ({ children }) => /* @__PURE__ */ jsx(Fragment, { children });
@@ -3028,6 +3690,7 @@ function organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder,
3028
3690
  return groupNames?.[groupKey] ?? groupKey;
3029
3691
  };
3030
3692
  for (const [actionId, bindings] of actionBindings) {
3693
+ if (actionRegistry?.[actionId]?.hideFromModal) continue;
3031
3694
  includedActions.add(actionId);
3032
3695
  const { name } = parseActionId(actionId);
3033
3696
  const groupName = getGroupName(actionId);
@@ -3044,6 +3707,7 @@ function organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder,
3044
3707
  if (actionRegistry && showUnbound) {
3045
3708
  for (const [actionId, action] of Object.entries(actionRegistry)) {
3046
3709
  if (includedActions.has(actionId)) continue;
3710
+ if (action.hideFromModal) continue;
3047
3711
  const { name } = parseActionId(actionId);
3048
3712
  const groupName = getGroupName(actionId);
3049
3713
  if (!groupMap.has(groupName)) {
@@ -3084,27 +3748,10 @@ function KeyDisplay({
3084
3748
  combo,
3085
3749
  className
3086
3750
  }) {
3087
- const { key, modifiers } = combo;
3088
- const parts = [];
3089
- if (modifiers.meta) {
3090
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "meta", className: "kbd-modifier-icon" }, "meta"));
3091
- }
3092
- if (modifiers.ctrl) {
3093
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "ctrl", className: "kbd-modifier-icon" }, "ctrl"));
3094
- }
3095
- if (modifiers.alt) {
3096
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "alt", className: "kbd-modifier-icon" }, "alt"));
3097
- }
3098
- if (modifiers.shift) {
3099
- parts.push(/* @__PURE__ */ jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }, "shift"));
3100
- }
3101
- const KeyIcon = getKeyIcon(key);
3102
- if (KeyIcon) {
3103
- parts.push(/* @__PURE__ */ jsx(KeyIcon, { className: "kbd-key-icon" }, "key"));
3104
- } else {
3105
- parts.push(/* @__PURE__ */ jsx("span", { children: formatKeyForDisplay(key) }, "key"));
3106
- }
3107
- return /* @__PURE__ */ jsx("span", { className, children: parts });
3751
+ return /* @__PURE__ */ jsxs("span", { className, children: [
3752
+ renderModifierIcons(combo.modifiers),
3753
+ renderKeyContent(combo.key)
3754
+ ] });
3108
3755
  }
3109
3756
  function SeqElemDisplay2({ elem, className }) {
3110
3757
  const Tooltip = useContext(TooltipContext);
@@ -3132,7 +3779,6 @@ function BindingDisplay2({
3132
3779
  }) {
3133
3780
  const sequence = parseHotkeyString(binding);
3134
3781
  const keySeq = parseKeySeq(binding);
3135
- formatKeySeq(keySeq);
3136
3782
  let kbdClassName = "kbd-kbd";
3137
3783
  if (editable && !isEditing) kbdClassName += " editable";
3138
3784
  if (isEditing) kbdClassName += " editing";
@@ -3305,18 +3951,17 @@ function ShortcutsModal({
3305
3951
  ctx.closeModal();
3306
3952
  }
3307
3953
  }, [onCloseProp, ctx]);
3308
- useCallback(() => {
3309
- if (ctx?.openModal) {
3310
- ctx.openModal();
3311
- } else {
3312
- setInternalIsOpen(true);
3313
- }
3314
- }, [ctx]);
3315
3954
  useAction(ACTION_MODAL, {
3316
3955
  label: "Show shortcuts",
3317
3956
  group: "Global",
3318
3957
  defaultBindings: defaultBinding ? [defaultBinding] : [],
3319
- handler: useCallback(() => ctx?.toggleModal() ?? setInternalIsOpen((prev) => !prev), [ctx?.toggleModal])
3958
+ handler: useCallback(() => {
3959
+ if (ctx) {
3960
+ ctx.toggleModal();
3961
+ } else {
3962
+ setInternalIsOpen((prev) => !prev);
3963
+ }
3964
+ }, [ctx])
3320
3965
  });
3321
3966
  const checkConflict = useCallback((newKey, forAction) => {
3322
3967
  const existingActions = keymap[newKey];
@@ -3760,6 +4405,6 @@ function ShortcutsModal({
3760
4405
  ] }) }) });
3761
4406
  }
3762
4407
 
3763
- export { ACTION_LOOKUP, ACTION_MODAL, ACTION_OMNIBAR, ActionsRegistryContext, Alt, Backspace, Command, Ctrl, DEFAULT_SEQUENCE_TIMEOUT, DIGITS_PLACEHOLDER, DIGIT_PLACEHOLDER, Down, Enter, HotkeysProvider, Kbd, KbdLookup, KbdModal, KbdOmnibar, Kbds, Key, KeybindingEditor, Left, LookupModal, ModifierIcon, Omnibar, Option, Right, SequenceModal, Shift, ShortcutsModal, Up, countPlaceholders, createTwoColumnRenderer, extractCaptures, findConflicts, formatBinding, formatCombination, formatKeyForDisplay, formatKeySeq, fuzzyMatch, getActionBindings, getConflictsArray, getKeyIcon, getModifierIcon, getSequenceCompletions, hasConflicts, hasDigitPlaceholders, hotkeySequenceToKeySeq, isDigitPlaceholder, isMac, isModifierKey, isPlaceholderSentinel, isSequence, isShiftedSymbol, keySeqToHotkeySequence, normalizeKey, parseCombinationId, parseHotkeyString, parseKeySeq, searchActions, useAction, useActions, useActionsRegistry, useEditableHotkeys, useHotkeys, useHotkeysContext, useMaybeHotkeysContext, useOmnibar, useRecordHotkey };
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 };
3764
4409
  //# sourceMappingURL=index.js.map
3765
4410
  //# sourceMappingURL=index.js.map