@remcostoeten/use-shortcut 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,928 +1 @@
1
- import { useRef, useMemo, useEffect } from 'react';
2
-
3
- // src/constants.ts
4
- var OS = {
5
- MAC: "mac",
6
- WINDOWS: "windows",
7
- LINUX: "linux"
8
- };
9
- var Platform = OS;
10
- var _cachedPlatform = null;
11
- function detectPlatform() {
12
- if (_cachedPlatform) return _cachedPlatform;
13
- if (typeof navigator === "undefined") {
14
- _cachedPlatform = OS.WINDOWS;
15
- return _cachedPlatform;
16
- }
17
- const uaPlatform = navigator.userAgentData?.platform?.toLowerCase();
18
- const platform = (uaPlatform ?? navigator.platform ?? navigator.userAgent ?? "").toLowerCase();
19
- if (platform.includes("mac") || platform.includes("iphone") || platform.includes("ipad") || platform.includes("ipod")) {
20
- _cachedPlatform = OS.MAC;
21
- return _cachedPlatform;
22
- }
23
- if (platform.includes("linux") || platform.includes("android")) {
24
- _cachedPlatform = OS.LINUX;
25
- return _cachedPlatform;
26
- }
27
- if (platform.includes("win")) {
28
- _cachedPlatform = OS.WINDOWS;
29
- return _cachedPlatform;
30
- }
31
- _cachedPlatform = OS.WINDOWS;
32
- return _cachedPlatform;
33
- }
34
- var ModifierKey = {
35
- META: "meta",
36
- CTRL: "ctrl",
37
- ALT: "alt",
38
- SHIFT: "shift"
39
- };
40
- var ModifierAliases = {
41
- command: ModifierKey.META,
42
- cmd: ModifierKey.META,
43
- "\u2318": ModifierKey.META,
44
- meta: ModifierKey.META,
45
- win: ModifierKey.META,
46
- windows: ModifierKey.META,
47
- super: ModifierKey.META,
48
- mod: ModifierKey.META,
49
- control: ModifierKey.CTRL,
50
- ctrl: ModifierKey.CTRL,
51
- "\u2303": ModifierKey.CTRL,
52
- ctl: ModifierKey.CTRL,
53
- alt: ModifierKey.ALT,
54
- option: ModifierKey.ALT,
55
- opt: ModifierKey.ALT,
56
- "\u2325": ModifierKey.ALT,
57
- shift: ModifierKey.SHIFT,
58
- "\u21E7": ModifierKey.SHIFT,
59
- shft: ModifierKey.SHIFT
60
- };
61
- var SpecialKeyMap = {
62
- up: "ArrowUp",
63
- down: "ArrowDown",
64
- left: "ArrowLeft",
65
- right: "ArrowRight",
66
- home: "Home",
67
- end: "End",
68
- pageup: "PageUp",
69
- pagedown: "PageDown",
70
- enter: "Enter",
71
- return: "Enter",
72
- space: " ",
73
- spacebar: " ",
74
- tab: "Tab",
75
- backspace: "Backspace",
76
- delete: "Delete",
77
- del: "Delete",
78
- escape: "Escape",
79
- esc: "Escape",
80
- f1: "F1",
81
- f2: "F2",
82
- f3: "F3",
83
- f4: "F4",
84
- f5: "F5",
85
- f6: "F6",
86
- f7: "F7",
87
- f8: "F8",
88
- f9: "F9",
89
- f10: "F10",
90
- f11: "F11",
91
- f12: "F12",
92
- plus: "+",
93
- minus: "-",
94
- comma: ",",
95
- period: ".",
96
- slash: "/",
97
- backslash: "\\",
98
- bracket: "[",
99
- closebracket: "]"
100
- };
101
- var ModifierDisplaySymbols = {
102
- [OS.MAC]: {
103
- [ModifierKey.META]: "\u2318",
104
- [ModifierKey.CTRL]: "\u2303",
105
- [ModifierKey.ALT]: "\u2325",
106
- [ModifierKey.SHIFT]: "\u21E7"
107
- },
108
- [OS.WINDOWS]: {
109
- [ModifierKey.META]: "Ctrl",
110
- [ModifierKey.CTRL]: "Ctrl",
111
- [ModifierKey.ALT]: "Alt",
112
- [ModifierKey.SHIFT]: "Shift"
113
- },
114
- [OS.LINUX]: {
115
- [ModifierKey.META]: "Super",
116
- [ModifierKey.CTRL]: "Ctrl",
117
- [ModifierKey.ALT]: "Alt",
118
- [ModifierKey.SHIFT]: "Shift"
119
- }
120
- };
121
- var ModifierDisplayOrder = {
122
- [OS.MAC]: [ModifierKey.CTRL, ModifierKey.ALT, ModifierKey.SHIFT, ModifierKey.META],
123
- [OS.WINDOWS]: [ModifierKey.META, ModifierKey.ALT, ModifierKey.SHIFT, ModifierKey.CTRL],
124
- [OS.LINUX]: [ModifierKey.META, ModifierKey.ALT, ModifierKey.SHIFT, ModifierKey.CTRL]
125
- };
126
-
127
- // src/parser.ts
128
- function _normalizeKeyToken(key) {
129
- return key === " " ? "space" : key.toLowerCase();
130
- }
131
- function parseShortcut(shortcut) {
132
- const platform = detectPlatform();
133
- const normalized = shortcut.toLowerCase().trim();
134
- const parts = normalized.split(/[\s+-]+/).filter(Boolean);
135
- if (parts.length === 0) {
136
- throw new Error(`Invalid shortcut: "${shortcut}"`);
137
- }
138
- const modifiers = {
139
- meta: false,
140
- ctrl: false,
141
- alt: false,
142
- shift: false
143
- };
144
- let key = parts.pop();
145
- for (const part of parts) {
146
- const modifierKey = ModifierAliases[part];
147
- if (modifierKey) {
148
- if (part === "mod") {
149
- if (platform === Platform.MAC) {
150
- modifiers.meta = true;
151
- } else {
152
- modifiers.ctrl = true;
153
- }
154
- } else {
155
- modifiers[modifierKey] = true;
156
- }
157
- } else {
158
- key = part + key;
159
- }
160
- }
161
- const normalizedKey = SpecialKeyMap[key] || key;
162
- return {
163
- modifiers,
164
- key: normalizedKey.length === 1 ? normalizedKey.toLowerCase() : normalizedKey,
165
- original: shortcut
166
- };
167
- }
168
- function parseShortcuts(shortcuts) {
169
- const shortcutArray = Array.isArray(shortcuts) ? shortcuts : [shortcuts];
170
- return shortcutArray.map(parseShortcut);
171
- }
172
- function getModifiersFromEvent(event) {
173
- return {
174
- meta: event.metaKey,
175
- ctrl: event.ctrlKey,
176
- alt: event.altKey,
177
- shift: event.shiftKey
178
- };
179
- }
180
- function matchesShortcut(event, parsed) {
181
- const eventModifiers = getModifiersFromEvent(event);
182
- const eventKey = _normalizeKeyToken(event.key);
183
- const modifiersMatch = eventModifiers.meta === parsed.modifiers.meta && eventModifiers.ctrl === parsed.modifiers.ctrl && eventModifiers.alt === parsed.modifiers.alt && eventModifiers.shift === parsed.modifiers.shift;
184
- const keyMatches = eventKey === _normalizeKeyToken(parsed.key);
185
- return modifiersMatch && keyMatches;
186
- }
187
- function matchesAnyShortcut(event, parsedShortcuts) {
188
- return parsedShortcuts.some((parsed) => matchesShortcut(event, parsed));
189
- }
190
-
191
- // src/formatter.ts
192
- var _BASE_DISPLAY_NAMES = {
193
- ArrowUp: "\u2191",
194
- ArrowDown: "\u2193",
195
- ArrowLeft: "\u2190",
196
- ArrowRight: "\u2192",
197
- Home: "Home",
198
- End: "End",
199
- PageUp: "PgUp",
200
- PageDown: "PgDn"
201
- };
202
- var _MAC_DISPLAY_NAMES = {
203
- ..._BASE_DISPLAY_NAMES,
204
- Enter: "\u21A9",
205
- Tab: "\u21E5",
206
- Escape: "\u238B",
207
- Backspace: "\u232B",
208
- Delete: "\u2326",
209
- " ": "\u2423"
210
- };
211
- var _NON_MAC_DISPLAY_NAMES = {
212
- ..._BASE_DISPLAY_NAMES,
213
- Enter: "Enter",
214
- Tab: "Tab",
215
- Escape: "Esc",
216
- Backspace: "Backspace",
217
- Delete: "Del",
218
- " ": "Space"
219
- };
220
- function formatShortcut(shortcut, platform) {
221
- const targetPlatform = platform ?? detectPlatform();
222
- const parsed = parseShortcut(shortcut);
223
- const symbols = ModifierDisplaySymbols[targetPlatform];
224
- const order = ModifierDisplayOrder[targetPlatform];
225
- const parts = [];
226
- for (const modifier of order) {
227
- if (parsed.modifiers[modifier]) {
228
- parts.push(symbols[modifier]);
229
- }
230
- }
231
- const displayKey = _formatKey(parsed.key, targetPlatform);
232
- parts.push(displayKey);
233
- const separator = targetPlatform === OS.MAC ? "" : "+";
234
- return parts.join(separator);
235
- }
236
- function _formatKey(key, platform) {
237
- const displayNames = platform === OS.MAC ? _MAC_DISPLAY_NAMES : _NON_MAC_DISPLAY_NAMES;
238
- return displayNames[key] || key.toUpperCase();
239
- }
240
-
241
- // src/runtime/debug.ts
242
- function _debugLog(debug, ...args) {
243
- if (debug) {
244
- console.log("[useShortcut]", ...args);
245
- }
246
- }
247
-
248
- // src/runtime/keys.ts
249
- function _getActiveModifierTokens(modifiers) {
250
- const platform = detectPlatform();
251
- const order = ModifierDisplayOrder[platform];
252
- const tokens = [];
253
- for (const key of order) {
254
- if (key === ModifierKey.CTRL && modifiers.ctrl) tokens.push("ctrl");
255
- if (key === ModifierKey.ALT && modifiers.alt) tokens.push("alt");
256
- if (key === ModifierKey.SHIFT && modifiers.shift) tokens.push("shift");
257
- if (key === ModifierKey.META && modifiers.cmd) tokens.push("cmd");
258
- }
259
- return tokens;
260
- }
261
- function _buildComboString(modifiers, key) {
262
- const tokens = _getActiveModifierTokens(modifiers);
263
- return [...tokens, key].join("+");
264
- }
265
- function _formatSequenceDisplay(steps) {
266
- return steps.map((step) => formatShortcut(step)).join(" then ");
267
- }
268
- function _canonicalizeParsed(parsed) {
269
- const modifiers = [];
270
- if (parsed.modifiers.ctrl) modifiers.push("ctrl");
271
- if (parsed.modifiers.alt) modifiers.push("alt");
272
- if (parsed.modifiers.shift) modifiers.push("shift");
273
- if (parsed.modifiers.meta) modifiers.push("cmd");
274
- const key = parsed.key === " " || parsed.key === "Spacebar" ? "space" : parsed.key.toLowerCase();
275
- return [...modifiers, key].join("+");
276
- }
277
- function _eventToCombo(event) {
278
- const modifiers = [];
279
- if (event.ctrlKey) modifiers.push("ctrl");
280
- if (event.altKey) modifiers.push("alt");
281
- if (event.shiftKey) modifiers.push("shift");
282
- if (event.metaKey) modifiers.push("cmd");
283
- const key = event.key === " " || event.key === "Spacebar" ? "space" : event.key.toLowerCase();
284
- return [...modifiers, key].join("+");
285
- }
286
- function _eventToMatchStep(event) {
287
- return _eventToCombo(event);
288
- }
289
-
290
- // src/runtime/conflicts.ts
291
- function _isPrefix(a, b) {
292
- if (a.length > b.length) return false;
293
- for (let i = 0; i < a.length; i += 1) {
294
- if (a[i] !== b[i]) {
295
- return false;
296
- }
297
- }
298
- return true;
299
- }
300
- function _detectConflict(newSteps, existingSteps) {
301
- const newCanonicalSteps = newSteps.map(_canonicalizeParsed);
302
- const existingCanonicalSteps = existingSteps.map(_canonicalizeParsed);
303
- const newCombo = newCanonicalSteps.join(" ");
304
- const existingCombo = existingCanonicalSteps.join(" ");
305
- if (newCombo === existingCombo) return "exact";
306
- if (_isPrefix(newCanonicalSteps, existingCanonicalSteps) || _isPrefix(existingCanonicalSteps, newCanonicalSteps)) {
307
- return "sequence-prefix";
308
- }
309
- return null;
310
- }
311
- function _emitConflict(registry, conflict) {
312
- const conflictWarnings = registry.options.conflictWarnings ?? true;
313
- if (registry.options.onConflict) {
314
- registry.options.onConflict(conflict);
315
- return;
316
- }
317
- if (!conflictWarnings) return;
318
- console.warn(
319
- `[useShortcut] Conflict detected (${conflict.reason}) between "${conflict.combo}" and "${conflict.existingCombo}"`
320
- );
321
- }
322
-
323
- // src/runtime/guards.ts
324
- var _IGNORED_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT"]);
325
- var _EXCEPT_PREDICATES = {
326
- input: (e) => {
327
- if (!(e.target instanceof HTMLElement)) return false;
328
- const target = e.target;
329
- return _IGNORED_TAGS.has(target.tagName);
330
- },
331
- editable: (e) => {
332
- if (!(e.target instanceof HTMLElement)) return false;
333
- const target = e.target;
334
- return target.isContentEditable;
335
- },
336
- typing: (e) => {
337
- if (!(e.target instanceof HTMLElement)) return false;
338
- const target = e.target;
339
- return _IGNORED_TAGS.has(target.tagName) || target.isContentEditable;
340
- },
341
- modal: () => {
342
- if (typeof document === "undefined" || typeof document.querySelector !== "function")
343
- return false;
344
- return document.querySelector('[data-modal="true"], [role="dialog"]') !== null;
345
- },
346
- disabled: (e) => {
347
- if (!(e.target instanceof HTMLElement)) return false;
348
- const target = e.target;
349
- return target.hasAttribute("disabled") || target.getAttribute("aria-disabled") === "true";
350
- }
351
- };
352
- function _shouldExcept(event, except) {
353
- if (!except) return false;
354
- if (typeof except === "function") {
355
- return except(event);
356
- }
357
- if (Array.isArray(except)) {
358
- return except.some((preset) => _EXCEPT_PREDICATES[preset]?.(event));
359
- }
360
- return _EXCEPT_PREDICATES[except]?.(event) ?? false;
361
- }
362
- function _normalizeScopes(scopes) {
363
- if (!scopes) return [];
364
- return (Array.isArray(scopes) ? scopes : [scopes]).map((scope) => scope.trim()).filter(Boolean);
365
- }
366
- function _scopeMatch(requiredScopes, activeScopes) {
367
- if (requiredScopes.size === 0) return true;
368
- for (const required of requiredScopes) {
369
- if (activeScopes.has(required)) return true;
370
- }
371
- return false;
372
- }
373
- function _isPureModifier(event) {
374
- const key = event.key.toLowerCase();
375
- return key === "shift" || key === "control" || key === "alt" || key === "meta";
376
- }
377
-
378
- // src/runtime/listener.ts
379
- function _sortEntries(entries) {
380
- if (entries.length <= 1) return entries;
381
- return [...entries].sort((a, b) => {
382
- if (b.priority !== a.priority) return b.priority - a.priority;
383
- return a.id - b.id;
384
- });
385
- }
386
- function _dispatchRegistryEvent(registry, event) {
387
- const runtimeOptions = registry.options;
388
- if (runtimeOptions.disabled) return;
389
- if (runtimeOptions.eventFilter && !runtimeOptions.eventFilter(event)) return;
390
- const candidateCombos = /* @__PURE__ */ new Set();
391
- const firstStepCombos = registry.firstStepIndex.get(_eventToMatchStep(event));
392
- if (firstStepCombos) {
393
- for (const combo of firstStepCombos) candidateCombos.add(combo);
394
- }
395
- for (const combo of registry.activeSequenceCombos) {
396
- candidateCombos.add(combo);
397
- }
398
- for (const combo of candidateCombos) {
399
- const comboEntries = registry.listeners.get(combo);
400
- if (!comboEntries) continue;
401
- const orderedEntries = _sortEntries(comboEntries);
402
- for (const item of orderedEntries) {
403
- if (!item.isEnabled) continue;
404
- if (!_scopeMatch(item.scopes, registry.activeScopes)) {
405
- continue;
406
- }
407
- if (runtimeOptions.ignoreInputs !== false && !item.except) {
408
- const targetEl = event.target;
409
- if (targetEl && (_IGNORED_TAGS.has(targetEl.tagName) || targetEl.isContentEditable)) {
410
- continue;
411
- }
412
- }
413
- if (_shouldExcept(event, item.except)) {
414
- _debugLog(runtimeOptions.debug, "Skipped due to except condition:", combo);
415
- continue;
416
- }
417
- const expected = item.parsedSteps[item.progress];
418
- const now = Date.now();
419
- if (item.progress > 0 && now - item.lastMatchedAt > item.sequenceTimeout) {
420
- item.progress = 0;
421
- }
422
- let matched = false;
423
- if (matchesShortcut(event, expected)) {
424
- item.progress += 1;
425
- item.lastMatchedAt = now;
426
- if (item.progress === item.parsedSteps.length) {
427
- matched = true;
428
- item.progress = 0;
429
- }
430
- } else if (item.progress > 0 && matchesShortcut(event, item.parsedSteps[0])) {
431
- item.progress = 1;
432
- item.lastMatchedAt = now;
433
- } else {
434
- item.progress = 0;
435
- }
436
- for (const cb of item.attemptCallbacks) {
437
- cb(matched, event);
438
- }
439
- if (!matched) continue;
440
- _debugLog(runtimeOptions.debug, "MATCHED:", combo);
441
- if (item.preventDefault) {
442
- event.preventDefault();
443
- }
444
- if (item.stopPropagation) {
445
- event.stopPropagation();
446
- }
447
- const executeHandler = () => item.userHandler(event);
448
- if (item.delay > 0) {
449
- _debugLog(runtimeOptions.debug, "Delaying execution by", item.delay, "ms");
450
- setTimeout(executeHandler, item.delay);
451
- } else {
452
- executeHandler();
453
- }
454
- if (item.stopOnMatch) {
455
- break;
456
- }
457
- }
458
- if (comboEntries.some((entry) => entry.progress > 0)) {
459
- registry.activeSequenceCombos.add(combo);
460
- } else {
461
- registry.activeSequenceCombos.delete(combo);
462
- }
463
- }
464
- }
465
- function _attachRegistryListener(registry) {
466
- if (registry.listener) return;
467
- const target = registry.options.target ?? (typeof window !== "undefined" ? window : null);
468
- if (!target) return;
469
- const eventType = registry.options.eventType ?? "keydown";
470
- const listener = (event) => _dispatchRegistryEvent(registry, event);
471
- target.addEventListener(eventType, listener);
472
- registry.listener = listener;
473
- registry.listenerTarget = target;
474
- registry.listenerEventType = eventType;
475
- _debugLog(registry.options.debug, "Listener attached");
476
- }
477
- function _detachRegistryListener(registry) {
478
- if (!registry.listener || !registry.listenerTarget) return;
479
- registry.listenerTarget.removeEventListener(registry.listenerEventType, registry.listener);
480
- registry.listener = null;
481
- registry.listenerTarget = null;
482
- _debugLog(registry.options.debug, "Listener detached");
483
- }
484
-
485
- // src/runtime/binding.ts
486
- function _createBinding(state, handler, handlerOptions = {}, registry) {
487
- const { options, except: stateExcept } = state;
488
- const rawSteps = state.steps;
489
- if (rawSteps.length === 0) {
490
- throw new Error("[useShortcut] No key specified. Use .key() to set the action key.");
491
- }
492
- const parsedSteps = rawSteps.map((step) => parseShortcut(step));
493
- const combo = parsedSteps.map(_canonicalizeParsed).join(" ");
494
- const display = _formatSequenceDisplay(rawSteps);
495
- const debug = options.debug ?? false;
496
- const except = stateExcept ?? handlerOptions.except;
497
- for (const [existingCombo, entries] of registry.listeners.entries()) {
498
- for (const existing of entries) {
499
- if (existingCombo === combo) continue;
500
- const reason = _detectConflict(parsedSteps, existing.parsedSteps);
501
- if (!reason) continue;
502
- _emitConflict(registry, { combo, existingCombo, reason });
503
- }
504
- }
505
- const isEnabled = !handlerOptions.disabled && !options.disabled;
506
- const delay = handlerOptions.delay ?? options.delay ?? 0;
507
- const sequenceTimeout = handlerOptions.sequenceTimeout ?? options.sequenceTimeout ?? 800;
508
- const requiredScopes = new Set(_normalizeScopes(state.scopes ?? handlerOptions.scopes));
509
- const attemptCallbacks = /* @__PURE__ */ new Set();
510
- _debugLog(debug, "Registering:", combo, "\u2192", display, {
511
- parsedSteps,
512
- except: !!except,
513
- scopes: [...requiredScopes]
514
- });
515
- const entry = {
516
- id: registry.nextId++,
517
- userHandler: handler,
518
- isEnabled,
519
- attemptCallbacks,
520
- parsedSteps,
521
- scopes: requiredScopes,
522
- progress: 0,
523
- lastMatchedAt: 0,
524
- except,
525
- delay,
526
- sequenceTimeout,
527
- preventDefault: handlerOptions.preventDefault !== false,
528
- stopPropagation: handlerOptions.stopPropagation ?? false,
529
- stopOnMatch: handlerOptions.stopOnMatch ?? false,
530
- priority: handlerOptions.priority ?? 0
531
- };
532
- const comboEntries = registry.listeners.get(combo);
533
- if (comboEntries) {
534
- comboEntries.push(entry);
535
- } else {
536
- registry.listeners.set(combo, [entry]);
537
- const firstStep = _canonicalizeParsed(parsedSteps[0]);
538
- const indexedCombos = registry.firstStepIndex.get(firstStep);
539
- if (indexedCombos) {
540
- indexedCombos.add(combo);
541
- } else {
542
- registry.firstStepIndex.set(firstStep, /* @__PURE__ */ new Set([combo]));
543
- }
544
- }
545
- _attachRegistryListener(registry);
546
- const unbindEntry = () => {
547
- const currentEntries = registry.listeners.get(combo);
548
- if (!currentEntries) return;
549
- const nextEntries = currentEntries.filter((item) => item.id !== entry.id);
550
- if (nextEntries.length === 0) {
551
- registry.listeners.delete(combo);
552
- registry.activeSequenceCombos.delete(combo);
553
- const firstStep = _canonicalizeParsed(parsedSteps[0]);
554
- const indexedCombos = registry.firstStepIndex.get(firstStep);
555
- if (indexedCombos) {
556
- indexedCombos.delete(combo);
557
- if (indexedCombos.size === 0) {
558
- registry.firstStepIndex.delete(firstStep);
559
- }
560
- }
561
- _debugLog(debug, "Unregistered:", combo);
562
- } else {
563
- registry.listeners.set(combo, nextEntries);
564
- }
565
- if (registry.listeners.size === 0) {
566
- _detachRegistryListener(registry);
567
- }
568
- };
569
- return {
570
- unbind: unbindEntry,
571
- display,
572
- combo,
573
- trigger: () => handler(new KeyboardEvent(registry.options.eventType ?? "keydown")),
574
- get isEnabled() {
575
- return entry.isEnabled;
576
- },
577
- enable: () => {
578
- entry.isEnabled = true;
579
- },
580
- disable: () => {
581
- entry.isEnabled = false;
582
- },
583
- onAttempt: (callback) => {
584
- entry.attemptCallbacks.add(callback);
585
- return () => entry.attemptCallbacks.delete(callback);
586
- }
587
- };
588
- }
589
-
590
- // src/runtime/recording.ts
591
- function _createRecorder(options) {
592
- return (recordingOptions = {}) => {
593
- return new Promise((resolve, reject) => {
594
- const target = recordingOptions.target ?? options.target ?? (typeof window !== "undefined" ? window : null);
595
- const eventType = recordingOptions.eventType ?? options.eventType ?? "keydown";
596
- if (!target) {
597
- reject(new Error("[useShortcut] Cannot record shortcut without a target."));
598
- return;
599
- }
600
- let timeout;
601
- const listener = (event) => {
602
- const keyboardEvent = event;
603
- if (_isPureModifier(keyboardEvent)) return;
604
- keyboardEvent.preventDefault();
605
- target.removeEventListener(eventType, listener);
606
- if (timeout) clearTimeout(timeout);
607
- resolve(_eventToCombo(keyboardEvent));
608
- };
609
- target.addEventListener(eventType, listener);
610
- const timeoutMs = recordingOptions.timeoutMs;
611
- if (timeoutMs && timeoutMs > 0) {
612
- timeout = setTimeout(() => {
613
- target.removeEventListener(eventType, listener);
614
- reject(new Error(`[useShortcut] Recording timed out after ${timeoutMs}ms.`));
615
- }, timeoutMs);
616
- }
617
- });
618
- };
619
- }
620
-
621
- // src/builder.ts
622
- var _MODIFIER_KEYS = /* @__PURE__ */ new Set(["ctrl", "shift", "alt", "cmd", "mod"]);
623
- function _createShortcutBuilder(options = {}) {
624
- const registry = {
625
- listeners: /* @__PURE__ */ new Map(),
626
- firstStepIndex: /* @__PURE__ */ new Map(),
627
- activeSequenceCombos: /* @__PURE__ */ new Set(),
628
- options,
629
- activeScopes: new Set(_normalizeScopes(options.activeScopes)),
630
- nextId: 1,
631
- listener: null,
632
- listenerTarget: null,
633
- listenerEventType: options.eventType ?? "keydown"
634
- };
635
- _debugLog(options.debug, "Builder created with options:", options);
636
- function _createProxy(currentState) {
637
- return new Proxy({}, {
638
- get(_, prop) {
639
- if (prop === "__debug") {
640
- return currentState.options.debug;
641
- }
642
- if (_MODIFIER_KEYS.has(prop)) {
643
- const platform = detectPlatform();
644
- const modKey = prop === "mod" ? platform === Platform.MAC ? "cmd" : "ctrl" : prop;
645
- const newState = {
646
- ...currentState,
647
- modifiers: { ...currentState.modifiers, [modKey]: true }
648
- };
649
- _debugLog(currentState.options.debug, `Chain: +${prop} \u2192`, newState.modifiers);
650
- return _createProxy(newState);
651
- }
652
- if (prop === "in") {
653
- return (scopes) => {
654
- const nextScopes = [..._normalizeScopes(currentState.scopes), ..._normalizeScopes(scopes)];
655
- const newState = {
656
- ...currentState,
657
- scopes: nextScopes
658
- };
659
- return _createProxy(newState);
660
- };
661
- }
662
- if (prop === "setScopes") {
663
- return (scopes) => {
664
- registry.activeScopes = new Set(_normalizeScopes(scopes));
665
- };
666
- }
667
- if (prop === "enableScope") {
668
- return (scope) => {
669
- if (!scope?.trim()) return;
670
- registry.activeScopes.add(scope.trim());
671
- };
672
- }
673
- if (prop === "disableScope") {
674
- return (scope) => {
675
- if (!scope?.trim()) return;
676
- registry.activeScopes.delete(scope.trim());
677
- };
678
- }
679
- if (prop === "getScopes") {
680
- return () => [...registry.activeScopes];
681
- }
682
- if (prop === "isScopeActive") {
683
- return (scope) => registry.activeScopes.has(scope);
684
- }
685
- if (prop === "record") {
686
- return _createRecorder(registry.options);
687
- }
688
- if (prop === "key") {
689
- return (key) => {
690
- const nextStep = _buildComboString(currentState.modifiers, key);
691
- const newState = {
692
- ...currentState,
693
- modifiers: {},
694
- steps: [...currentState.steps, nextStep]
695
- };
696
- _debugLog(currentState.options.debug, `Chain: .key("${key}")`);
697
- return _createProxy(newState);
698
- };
699
- }
700
- if (prop === "then") {
701
- return (key) => {
702
- const nextStep = String(key).trim().toLowerCase();
703
- if (!nextStep) {
704
- throw new Error("[useShortcut] .then() requires a non-empty key or shortcut step.");
705
- }
706
- const newState = {
707
- ...currentState,
708
- steps: [...currentState.steps, nextStep]
709
- };
710
- _debugLog(currentState.options.debug, `Chain: .then("${nextStep}")`);
711
- return _createProxy(newState);
712
- };
713
- }
714
- if (prop === "except") {
715
- return (condition) => {
716
- const newState = {
717
- ...currentState,
718
- except: condition
719
- };
720
- _debugLog(currentState.options.debug, "Chain: .except()", condition);
721
- return _createProxy(newState);
722
- };
723
- }
724
- if (prop === "on") {
725
- return (handler, handlerOptions) => {
726
- return _createBinding(currentState, handler, handlerOptions, registry);
727
- };
728
- }
729
- if (prop === "handle") {
730
- return (opts) => {
731
- const { handler, ...rest } = opts;
732
- return _createBinding(currentState, handler, rest, registry);
733
- };
734
- }
735
- return void 0;
736
- }
737
- });
738
- }
739
- const initialState = {
740
- modifiers: {},
741
- steps: [],
742
- options
743
- };
744
- return {
745
- builder: _createProxy(initialState),
746
- registry
747
- };
748
- }
749
-
750
- // src/hook.ts
751
- function _areShortcutMapKeysEqual(a, b) {
752
- if (Array.isArray(a) && Array.isArray(b)) {
753
- if (a.length !== b.length) return false;
754
- for (let i = 0; i < a.length; i += 1) {
755
- if (a[i] !== b[i]) return false;
756
- }
757
- return true;
758
- }
759
- if (!Array.isArray(a) && !Array.isArray(b)) {
760
- return a === b;
761
- }
762
- return false;
763
- }
764
- function _areShortcutMapsEquivalent(a, b) {
765
- const aKeys = Object.keys(a);
766
- const bKeys = Object.keys(b);
767
- if (aKeys.length !== bKeys.length) return false;
768
- for (const key of aKeys) {
769
- const aEntry = a[key];
770
- const bEntry = b[key];
771
- if (!bEntry) return false;
772
- if (!_areShortcutMapKeysEqual(aEntry.keys, bEntry.keys)) return false;
773
- if (aEntry.handler !== bEntry.handler) return false;
774
- if (aEntry.options !== bEntry.options) return false;
775
- }
776
- return true;
777
- }
778
- function _normalizeShortcutMapKeys(keys) {
779
- if (Array.isArray(keys)) {
780
- return keys.map((key) => key.trim()).filter(Boolean);
781
- }
782
- const trimmed = keys.trim();
783
- if (!trimmed) return [];
784
- if (trimmed.includes(" then ")) {
785
- return trimmed.split(/\s+then\s+/i).map((key) => key.trim()).filter(Boolean);
786
- }
787
- if (trimmed.includes(" ") && !trimmed.includes("+")) {
788
- return trimmed.split(/\s+/).map((key) => key.trim()).filter(Boolean);
789
- }
790
- return [trimmed];
791
- }
792
- function _applyStep(builder, step) {
793
- const tokens = step.toLowerCase().split("+").map((token) => token.trim()).filter(Boolean);
794
- if (tokens.length === 0) {
795
- throw new Error("[useShortcutMap] Invalid step: empty shortcut step");
796
- }
797
- const key = tokens.pop();
798
- let chain = builder;
799
- for (const token of tokens) {
800
- if (token === "ctrl" || token === "control") {
801
- chain = chain.ctrl;
802
- continue;
803
- }
804
- if (token === "shift") {
805
- chain = chain.shift;
806
- continue;
807
- }
808
- if (token === "alt" || token === "option") {
809
- chain = chain.alt;
810
- continue;
811
- }
812
- if (token === "cmd" || token === "command" || token === "meta") {
813
- chain = chain.cmd;
814
- continue;
815
- }
816
- if (token === "mod") {
817
- chain = chain.mod;
818
- continue;
819
- }
820
- throw new Error(`[useShortcutMap] Unsupported modifier token "${token}" in step "${step}"`);
821
- }
822
- return chain.key(key);
823
- }
824
- function registerShortcutMap(builder, shortcutMap) {
825
- const results = {};
826
- for (const id of Object.keys(shortcutMap)) {
827
- const entry = shortcutMap[id];
828
- const steps = _normalizeShortcutMapKeys(entry.keys);
829
- if (steps.length === 0) {
830
- throw new Error(`[useShortcutMap] Shortcut "${String(id)}" has no key steps`);
831
- }
832
- let chain = _applyStep(builder, steps[0]);
833
- for (const step of steps.slice(1)) {
834
- chain = chain.then(step);
835
- }
836
- results[id] = chain.on(entry.handler, entry.options);
837
- }
838
- return results;
839
- }
840
- function useShortcut(options = {}) {
841
- const optionsRef = useRef(options);
842
- optionsRef.current = options;
843
- const { builder, registry } = useMemo(() => {
844
- return _createShortcutBuilder(optionsRef.current);
845
- }, []);
846
- useEffect(() => {
847
- registry.options = optionsRef.current;
848
- if (optionsRef.current.activeScopes !== void 0) {
849
- const scopes = Array.isArray(optionsRef.current.activeScopes) ? optionsRef.current.activeScopes : [optionsRef.current.activeScopes];
850
- registry.activeScopes = new Set(scopes.map((scope) => scope.trim()).filter(Boolean));
851
- }
852
- }, [registry, options]);
853
- useEffect(() => {
854
- return () => {
855
- registry.listeners.clear();
856
- registry.firstStepIndex.clear();
857
- registry.activeSequenceCombos.clear();
858
- if (registry.listener && registry.listenerTarget) {
859
- registry.listenerTarget.removeEventListener(registry.listenerEventType, registry.listener);
860
- registry.listener = null;
861
- registry.listenerTarget = null;
862
- }
863
- };
864
- }, [registry]);
865
- return builder;
866
- }
867
- function useShortcutMap(shortcutMap, options = {}) {
868
- const $ = useShortcut(options);
869
- const stableShortcutMapRef = useRef(shortcutMap);
870
- if (!_areShortcutMapsEquivalent(stableShortcutMapRef.current, shortcutMap)) {
871
- stableShortcutMapRef.current = shortcutMap;
872
- }
873
- const stableShortcutMap = stableShortcutMapRef.current;
874
- const resultsRef = useRef({});
875
- useEffect(() => {
876
- const registrations = registerShortcutMap($, stableShortcutMap);
877
- const results = resultsRef.current;
878
- for (const key of Object.keys(results)) {
879
- delete results[key];
880
- }
881
- Object.assign(results, registrations);
882
- return () => {
883
- for (const result of Object.values(registrations)) {
884
- result.unbind();
885
- }
886
- for (const key of Object.keys(results)) {
887
- delete results[key];
888
- }
889
- };
890
- }, [$, stableShortcutMap]);
891
- return resultsRef.current;
892
- }
893
- function createShortcutGroup() {
894
- const results = [];
895
- return {
896
- add: (...entries) => {
897
- results.push(...entries);
898
- },
899
- addMany: (entries) => {
900
- if (Array.isArray(entries)) {
901
- results.push(...entries);
902
- return;
903
- }
904
- results.push(...Object.values(entries));
905
- },
906
- unbindAll: () => {
907
- for (const entry of results) {
908
- entry.unbind();
909
- }
910
- results.length = 0;
911
- },
912
- clear: () => {
913
- results.length = 0;
914
- },
915
- getResults: () => [...results]
916
- };
917
- }
918
- function useShortcutGroup() {
919
- const groupRef = useRef(null);
920
- if (!groupRef.current) {
921
- groupRef.current = createShortcutGroup();
922
- }
923
- return groupRef.current;
924
- }
925
-
926
- export { ModifierAliases, ModifierDisplayOrder, ModifierDisplaySymbols, ModifierKey, Platform, SpecialKeyMap, createShortcutGroup, detectPlatform, formatShortcut, matchesAnyShortcut, matchesShortcut, parseShortcut, parseShortcuts, registerShortcutMap, useShortcut, useShortcutGroup, useShortcutMap };
927
- //# sourceMappingURL=index.mjs.map
928
- //# sourceMappingURL=index.mjs.map
1
+ import {useRef,useMemo,useEffect}from'react';var m={MAC:"mac",WINDOWS:"windows",LINUX:"linux"},R=m,y=null;function E(){if(y)return y;if(typeof navigator>"u")return y=m.WINDOWS,y;let e=(navigator.userAgentData?.platform?.toLowerCase()??navigator.platform??navigator.userAgent??"").toLowerCase();return e.includes("mac")||e.includes("iphone")||e.includes("ipad")||e.includes("ipod")?(y=m.MAC,y):e.includes("linux")||e.includes("android")?(y=m.LINUX,y):(e.includes("win"),y=m.WINDOWS,y)}var p={META:"meta",CTRL:"ctrl",ALT:"alt",SHIFT:"shift"},F={command:p.META,cmd:p.META,"\u2318":p.META,meta:p.META,win:p.META,windows:p.META,super:p.META,mod:p.META,control:p.CTRL,ctrl:p.CTRL,"\u2303":p.CTRL,ctl:p.CTRL,alt:p.ALT,option:p.ALT,opt:p.ALT,"\u2325":p.ALT,shift:p.SHIFT,"\u21E7":p.SHIFT,shft:p.SHIFT},N={up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",pageup:"PageUp",pagedown:"PageDown",enter:"Enter",return:"Enter",space:" ",spacebar:" ",tab:"Tab",backspace:"Backspace",delete:"Delete",del:"Delete",escape:"Escape",esc:"Escape",f1:"F1",f2:"F2",f3:"F3",f4:"F4",f5:"F5",f6:"F6",f7:"F7",f8:"F8",f9:"F9",f10:"F10",f11:"F11",f12:"F12",plus:"+",minus:"-",comma:",",period:".",slash:"/",backslash:"\\",bracket:"[",closebracket:"]"},U={[m.MAC]:{[p.META]:"\u2318",[p.CTRL]:"\u2303",[p.ALT]:"\u2325",[p.SHIFT]:"\u21E7"},[m.WINDOWS]:{[p.META]:"Ctrl",[p.CTRL]:"Ctrl",[p.ALT]:"Alt",[p.SHIFT]:"Shift"},[m.LINUX]:{[p.META]:"Super",[p.CTRL]:"Ctrl",[p.ALT]:"Alt",[p.SHIFT]:"Shift"}},L={[m.MAC]:[p.CTRL,p.ALT,p.SHIFT,p.META],[m.WINDOWS]:[p.META,p.ALT,p.SHIFT,p.CTRL],[m.LINUX]:[p.META,p.ALT,p.SHIFT,p.CTRL]};function G(t){return t===" "?"space":t.toLowerCase()}function k(t){let e=E(),o=t.toLowerCase().trim().split(/[\s+-]+/).filter(Boolean);if(o.length===0)throw new Error(`Invalid shortcut: "${t}"`);let n={meta:false,ctrl:false,alt:false,shift:false},s=o.pop();for(let i of o){let u=F[i];u?i==="mod"?e===R.MAC?n.meta=true:n.ctrl=true:n[u]=true:s=i+s;}let c=N[s]||s;return {modifiers:n,key:c.length===1?c.toLowerCase():c,original:t}}function gt(t){return (Array.isArray(t)?t:[t]).map(k)}function yt(t){return {meta:t.metaKey,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey}}function P(t,e){let r=yt(t),o=G(t.key),n=r.meta===e.modifiers.meta&&r.ctrl===e.modifiers.ctrl&&r.alt===e.modifiers.alt&&r.shift===e.modifiers.shift,s=o===G(e.key);return n&&s}function bt(t,e){return e.some(r=>P(t,r))}var X={ArrowUp:"\u2191",ArrowDown:"\u2193",ArrowLeft:"\u2190",ArrowRight:"\u2192",Home:"Home",End:"End",PageUp:"PgUp",PageDown:"PgDn"},Et={...X,Enter:"\u21A9",Tab:"\u21E5",Escape:"\u238B",Backspace:"\u232B",Delete:"\u2326"," ":"\u2423"},Tt={...X,Enter:"Enter",Tab:"Tab",Escape:"Esc",Backspace:"Backspace",Delete:"Del"," ":"Space"};function q(t,e){let r=e??E(),o=k(t),n=U[r],s=L[r],c=[];for(let f of s)o.modifiers[f]&&c.push(n[f]);let i=Mt(o.key,r);c.push(i);let u=r===m.MAC?"":"+";return c.join(u)}function Mt(t,e){return (e===m.MAC?Et:Tt)[t]||t.toUpperCase()}function At(t){return t==="ctrl"||t==="alt"||t==="shift"||t==="cmd"}function z(t){return t.split("+").map(e=>e.trim()).filter(Boolean)}function Y(t){if(t)return t===true?{console:true}:t}function v(t){let e=Y(t);return e?e.console!==false:false}function h(t,...e){v(t)&&console.log("[useShortcut]",...e);}function J(t,e){return {key:t.key,code:t.code,location:t.location,repeat:t.repeat,keyCode:"keyCode"in t?t.keyCode:void 0,which:"which"in t?t.which:void 0,combo:e,modifiers:{meta:t.metaKey,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey}}}function Ct(t,e,r){return e.has(t)?"match":r.has(t)?"wrong-order":"mismatch"}function Q(t,e,r){return t.map((o,n)=>{let s=e[n];if(!s)return {index:n,expected:o,status:"pending",tokens:[]};let c=new Set(z(o)),i=new Set(t.slice(n+1).flatMap(z)),u=z(s).map((l,b,_)=>({token:l,kind:At(l)||b<_.length-1?"modifier":"key",status:Ct(l,c,i)}));if(s===o)return {index:n,expected:o,actual:s,status:r||n<e.length-1?"match":"partial",tokens:u};let f=t.slice(n+1).includes(s)?"wrong-order":"mismatch";return {index:n,expected:o,actual:s,status:f,tokens:u}})}function V(t,e,r,o){if(o)return "matched";let n=t.slice(0,r);return n.length>0&&n.every(s=>s.status==="match"||s.status==="partial")?r<e?"partial":"mismatch":n.some(s=>s.status==="wrong-order")?"wrong-order":"mismatch"}function Z(t,e){if(!v(t))return;let r=Y(t),o=[];if(r?.includeCode&&e.input.code&&o.push(`code=${e.input.code}`),r?.includeLocation&&o.push(`location=${String(e.input.location)}`),r?.includeKeyCode&&(typeof e.input.keyCode=="number"&&o.push(`keyCode=${String(e.input.keyCode)}`),typeof e.input.which=="number"&&o.push(`which=${String(e.input.which)}`)),e.attempts.length===0){console.log("[useShortcut]","key",e.input.combo,...o);return}for(let n of e.attempts)console.log("[useShortcut]",n.status.toUpperCase(),`${e.input.combo} -> ${n.combo}`,...o);}function xt(t){let e=E(),r=L[e],o=[];for(let n of r)n===p.CTRL&&t.ctrl&&o.push("ctrl"),n===p.ALT&&t.alt&&o.push("alt"),n===p.SHIFT&&t.shift&&o.push("shift"),n===p.META&&t.cmd&&o.push("cmd");return o}function tt(t,e){return [...xt(t),e].join("+")}function et(t){return t.map(e=>q(e)).join(" then ")}function A(t){let e=[];t.modifiers.ctrl&&e.push("ctrl"),t.modifiers.alt&&e.push("alt"),t.modifiers.shift&&e.push("shift"),t.modifiers.meta&&e.push("cmd");let r=t.key===" "||t.key==="Spacebar"?"space":t.key.toLowerCase();return [...e,r].join("+")}function $(t){let e=[];t.ctrlKey&&e.push("ctrl"),t.altKey&&e.push("alt"),t.shiftKey&&e.push("shift"),t.metaKey&&e.push("cmd");let r=t.key===" "||t.key==="Spacebar"?"space":t.key.toLowerCase();return [...e,r].join("+")}function rt(t){return $(t)}function ot(t,e){if(t.length>e.length)return false;for(let r=0;r<t.length;r+=1)if(t[r]!==e[r])return false;return true}function nt(t,e){let r=t.map(A),o=e.map(A),n=r.join(" "),s=o.join(" ");return n===s?"exact":ot(r,o)||ot(o,r)?"sequence-prefix":null}function st(t,e){let r=t.options.conflictWarnings??true;if(t.options.onConflict){t.options.onConflict(e);return}r&&console.warn(`[useShortcut] Conflict detected (${e.reason}) between "${e.combo}" and "${e.existingCombo}"`);}var K=new Set(["INPUT","TEXTAREA","SELECT"]),it={input:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return K.has(e.tagName)},editable:t=>t.target instanceof HTMLElement?t.target.isContentEditable:false,typing:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return K.has(e.tagName)||e.isContentEditable},modal:()=>typeof document>"u"||typeof document.querySelector!="function"?false:document.querySelector('[data-modal="true"], [role="dialog"]')!==null,disabled:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return e.hasAttribute("disabled")||e.getAttribute("aria-disabled")==="true"}};function ct(t,e){return e?typeof e=="function"?e(t):Array.isArray(e)?e.some(r=>it[r]?.(t)):it[e]?.(t)??false:false}function C(t){return t?(Array.isArray(t)?t:[t]).map(e=>e.trim()).filter(Boolean):[]}function ut(t,e){if(t.size===0)return true;for(let r of t)if(e.has(r))return true;return false}function at(t){let e=t.key.toLowerCase();return e==="shift"||e==="control"||e==="alt"||e==="meta"}function wt(t){return t.length<=1?t:[...t].sort((e,r)=>r.priority!==e.priority?r.priority-e.priority:e.id-r.id)}function kt(t,e){let r=t.options;if(r.disabled||r.eventFilter&&!r.eventFilter(e))return;let o=rt(e),n=J(e,o),s=[],c=new Set,i=t.debugListeners.size>0||v(r.debug)||[...t.listeners.values()].some(l=>l.some(b=>b.attemptCallbacks.size>0)),u=t.firstStepIndex.get(o);if(u)for(let l of u)c.add(l);for(let l of t.activeSequenceCombos)c.add(l);if(i)for(let l of t.listeners.keys())c.add(l);for(let l of c){let b=t.listeners.get(l);if(!b)continue;let _=wt(b);for(let a of _){if(!a.isEnabled||!ut(a.scopes,t.activeScopes))continue;if(r.ignoreInputs!==false&&!a.except){let S=e.target;if(S&&(K.has(S.tagName)||S.isContentEditable))continue}if(ct(e,a.except)){h(r.debug,"Skipped due to except condition:",l);continue}let O=a.parsedSteps[a.progress],T=Date.now();a.progress>0&&T-a.lastMatchedAt>a.sequenceTimeout&&(a.progress=0),a.debugHistory.length>0&&T-a.lastDebugAt>a.sequenceTimeout&&(a.debugHistory=[]);let H=a.progress,M=false;P(e,O)?(a.progress+=1,a.lastMatchedAt=T,a.progress===a.parsedSteps.length&&(M=true,a.progress=0)):a.progress>0&&P(e,a.parsedSteps[0])?(a.progress=1,a.lastMatchedAt=T):a.progress=0,a.lastDebugAt=T,a.debugHistory.push(o),a.debugHistory.length>a.expectedSteps.length&&a.debugHistory.shift();let g=a.debugHistory.slice(-a.expectedSteps.length),D=Q(a.expectedSteps,g,M),B={combo:a.combo,display:a.display,description:a.description,status:V(D,a.expectedSteps.length,g.length,M),matched:M,progress:a.progress,expectedSteps:a.expectedSteps,actualSteps:g,stepIndex:H,input:n,steps:D};s.push(B);for(let S of a.attemptCallbacks)S(M,e,B);if(!M)continue;h(r.debug,"MATCHED:",l),a.preventDefault&&e.preventDefault(),a.stopPropagation&&e.stopPropagation();let d=()=>a.userHandler(e);if(a.delay>0?(h(r.debug,"Delaying execution by",a.delay,"ms"),setTimeout(d,a.delay)):d(),a.stopOnMatch)break}b.some(a=>a.progress>0)?t.activeSequenceCombos.add(l):t.activeSequenceCombos.delete(l);}let f={input:n,attempts:s};if(t.debugListeners.size>0)for(let l of t.debugListeners)l(f);Z(r.debug,f);}function pt(t){if(t.listener)return;let e=t.options.target??(typeof window<"u"?window:null);if(!e)return;let r=t.options.eventType??"keydown",o=n=>kt(t,n);e.addEventListener(r,o),t.listener=o,t.listenerTarget=e,t.listenerEventType=r,h(t.options.debug,"Listener attached");}function lt(t){!t.listener||!t.listenerTarget||(t.listenerTarget.removeEventListener(t.listenerEventType,t.listener),t.listener=null,t.listenerTarget=null,h(t.options.debug,"Listener detached"));}function j(t,e,r={},o){let{options:n,except:s}=t,c=t.steps;if(c.length===0)throw new Error("[useShortcut] No key specified. Use .key() to set the action key.");let i=c.map(d=>k(d)),u=i.map(A).join(" "),f=et(c),l=n.debug??false,b=s??r.except;for(let[d,S]of o.listeners.entries())for(let x of S){if(d===u)continue;let w=nt(i,x.parsedSteps);w&&st(o,{combo:u,existingCombo:d,reason:w});}let _=!r.disabled&&!n.disabled,a=r.delay??n.delay??0,O=r.sequenceTimeout??n.sequenceTimeout??800,T=new Set(C(t.scopes??r.scopes)),H=i.map(A),M=new Set;h(l,"Registering:",u,"\u2192",f,{parsedSteps:i,except:!!b,scopes:[...T]});let g={id:o.nextId++,userHandler:e,isEnabled:_,combo:u,display:f,description:r.description,attemptCallbacks:M,parsedSteps:i,expectedSteps:H,scopes:T,progress:0,lastMatchedAt:0,debugHistory:[],lastDebugAt:0,except:b,delay:a,sequenceTimeout:O,preventDefault:r.preventDefault!==false,stopPropagation:r.stopPropagation??false,stopOnMatch:r.stopOnMatch??false,priority:r.priority??0},D=o.listeners.get(u);if(D)D.push(g);else {o.listeners.set(u,[g]);let d=A(i[0]),S=o.firstStepIndex.get(d);S?S.add(u):o.firstStepIndex.set(d,new Set([u]));}return pt(o),{unbind:()=>{let d=o.listeners.get(u);if(!d)return;let S=d.filter(x=>x.id!==g.id);if(S.length===0){o.listeners.delete(u),o.activeSequenceCombos.delete(u);let x=A(i[0]),w=o.firstStepIndex.get(x);w&&(w.delete(u),w.size===0&&o.firstStepIndex.delete(x)),h(l,"Unregistered:",u);}else o.listeners.set(u,S);o.listeners.size===0&&lt(o);},display:f,combo:u,trigger:()=>e(new KeyboardEvent(o.options.eventType??"keydown")),get isEnabled(){return g.isEnabled},enable:()=>{g.isEnabled=true;},disable:()=>{g.isEnabled=false;},onAttempt:d=>(g.attemptCallbacks.add(d),()=>g.attemptCallbacks.delete(d))}}function ft(t){return (e={})=>new Promise((r,o)=>{let n=e.target??t.target??(typeof window<"u"?window:null),s=e.eventType??t.eventType??"keydown";if(!n){o(new Error("[useShortcut] Cannot record shortcut without a target."));return}let c,i=f=>{let l=f;at(l)||(l.preventDefault(),n.removeEventListener(s,i),c&&clearTimeout(c),r($(l)));};n.addEventListener(s,i);let u=e.timeoutMs;u&&u>0&&(c=setTimeout(()=>{n.removeEventListener(s,i),o(new Error(`[useShortcut] Recording timed out after ${u}ms.`));},u));})}var _t=new Set(["ctrl","shift","alt","cmd","mod"]);function dt(t={}){let e={listeners:new Map,firstStepIndex:new Map,activeSequenceCombos:new Set,options:t,activeScopes:new Set(C(t.activeScopes)),nextId:1,debugListeners:new Set,listener:null,listenerTarget:null,listenerEventType:t.eventType??"keydown"};h(t.debug,"Builder created with options:",t);function r(n){return new Proxy({},{get(s,c){if(c==="__debug")return n.options.debug;if(_t.has(c)){let i=E(),u=c==="mod"?i===R.MAC?"cmd":"ctrl":c,f={...n,modifiers:{...n.modifiers,[u]:true}};return h(n.options.debug,`Chain: +${c} \u2192`,f.modifiers),r(f)}if(c==="in")return i=>{let u=[...C(n.scopes),...C(i)],f={...n,scopes:u};return r(f)};if(c==="setScopes")return i=>{e.activeScopes=new Set(C(i));};if(c==="enableScope")return i=>{i?.trim()&&e.activeScopes.add(i.trim());};if(c==="disableScope")return i=>{i?.trim()&&e.activeScopes.delete(i.trim());};if(c==="getScopes")return ()=>[...e.activeScopes];if(c==="isScopeActive")return i=>e.activeScopes.has(i);if(c==="onDebug")return i=>(e.debugListeners.add(i),()=>e.debugListeners.delete(i));if(c==="record")return ft(e.options);if(c==="key")return i=>{let u=tt(n.modifiers,i),f={...n,modifiers:{},steps:[...n.steps,u]};return h(n.options.debug,`Chain: .key("${i}")`),r(f)};if(c==="then")return i=>{let u=String(i).trim().toLowerCase();if(!u)throw new Error("[useShortcut] .then() requires a non-empty key or shortcut step.");let f={...n,steps:[...n.steps,u]};return h(n.options.debug,`Chain: .then("${u}")`),r(f)};if(c==="except")return i=>{let u={...n,except:i};return h(n.options.debug,"Chain: .except()",i),r(u)};if(c==="on")return (i,u)=>j(n,i,u,e);if(c==="handle")return i=>{let{handler:u,...f}=i;return j(n,u,f,e)}}})}return {builder:r({modifiers:{},steps:[],options:t}),registry:e}}function Rt(t,e){if(Array.isArray(t)&&Array.isArray(e)){if(t.length!==e.length)return false;for(let r=0;r<t.length;r+=1)if(t[r]!==e[r])return false;return true}return !Array.isArray(t)&&!Array.isArray(e)?t===e:false}function Lt(t,e){let r=Object.keys(t),o=Object.keys(e);if(r.length!==o.length)return false;for(let n of r){let s=t[n],c=e[n];if(!c||!Rt(s.keys,c.keys)||s.handler!==c.handler||s.options!==c.options)return false}return true}function Pt(t){if(Array.isArray(t))return t.map(r=>r.trim()).filter(Boolean);let e=t.trim();return e?e.includes(" then ")?e.split(/\s+then\s+/i).map(r=>r.trim()).filter(Boolean):e.includes(" ")&&!e.includes("+")?e.split(/\s+/).map(r=>r.trim()).filter(Boolean):[e]:[]}function vt(t,e){let r=e.toLowerCase().split("+").map(s=>s.trim()).filter(Boolean);if(r.length===0)throw new Error("[useShortcutMap] Invalid step: empty shortcut step");let o=r.pop(),n=t;for(let s of r){if(s==="ctrl"||s==="control"){n=n.ctrl;continue}if(s==="shift"){n=n.shift;continue}if(s==="alt"||s==="option"){n=n.alt;continue}if(s==="cmd"||s==="command"||s==="meta"){n=n.cmd;continue}if(s==="mod"){n=n.mod;continue}throw new Error(`[useShortcutMap] Unsupported modifier token "${s}" in step "${e}"`)}return n.key(o)}function mt(t,e){let r={};for(let o of Object.keys(e)){let n=e[o],s=Pt(n.keys);if(s.length===0)throw new Error(`[useShortcutMap] Shortcut "${String(o)}" has no key steps`);let c=vt(t,s[0]);for(let i of s.slice(1))c=c.then(i);r[o]=c.on(n.handler,n.options);}return r}function ht(t={}){let e=useRef(t);e.current=t;let{builder:r,registry:o}=useMemo(()=>dt(e.current),[]);return useEffect(()=>{if(o.options=e.current,e.current.activeScopes!==void 0){let n=Array.isArray(e.current.activeScopes)?e.current.activeScopes:[e.current.activeScopes];o.activeScopes=new Set(n.map(s=>s.trim()).filter(Boolean));}},[o,t]),useEffect(()=>()=>{o.listeners.clear(),o.firstStepIndex.clear(),o.activeSequenceCombos.clear(),o.listener&&o.listenerTarget&&(o.listenerTarget.removeEventListener(o.listenerEventType,o.listener),o.listener=null,o.listenerTarget=null);},[o]),r}function Kt(t,e={}){let r=ht(e),o=useRef(t);Lt(o.current,t)||(o.current=t);let n=o.current,s=useRef({});return useEffect(()=>{let c=mt(r,n),i=s.current;for(let u of Object.keys(i))delete i[u];return Object.assign(i,c),()=>{for(let u of Object.values(c))u.unbind();for(let u of Object.keys(i))delete i[u];}},[r,n]),s.current}function St(){let t=[];return {add:(...e)=>{t.push(...e);},addMany:e=>{if(Array.isArray(e)){t.push(...e);return}t.push(...Object.values(e));},unbindAll:()=>{for(let e of t)e.unbind();t.length=0;},clear:()=>{t.length=0;},getResults:()=>[...t]}}function It(){let t=useRef(null);return t.current||(t.current=St()),t.current}export{F as ModifierAliases,L as ModifierDisplayOrder,U as ModifierDisplaySymbols,p as ModifierKey,R as Platform,N as SpecialKeyMap,St as createShortcutGroup,E as detectPlatform,q as formatShortcut,bt as matchesAnyShortcut,P as matchesShortcut,k as parseShortcut,gt as parseShortcuts,mt as registerShortcutMap,ht as useShortcut,It as useShortcutGroup,Kt as useShortcutMap};