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