@tanstack/hotkeys 0.23.0 → 0.25.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.
Files changed (53) hide show
  1. package/dist/_app-hotkeys-factory.d.ts +1 -3
  2. package/dist/_app-hotkeys-factory.js +2 -3
  3. package/dist/_event-target.js +40 -7
  4. package/dist/_keyboard-event.js +8 -5
  5. package/dist/_manager-ref.js +1 -2
  6. package/dist/_match.js +5 -3
  7. package/dist/_named-keys.d.ts +1 -3
  8. package/dist/_named-keys.js +1 -2
  9. package/dist/_recorder-chord.js +7 -13
  10. package/dist/_recording-guard.js +1 -2
  11. package/dist/_registration.d.ts +1 -3
  12. package/dist/_registration.js +11 -6
  13. package/dist/_server.js +3 -4
  14. package/dist/_store.d.ts +1 -3
  15. package/dist/_store.js +1 -2
  16. package/dist/_target-listeners.js +11 -3
  17. package/dist/adapter.d.ts +10 -8
  18. package/dist/adapter.js +9 -5
  19. package/dist/app-hotkeys.d.ts +4 -5
  20. package/dist/app-hotkeys.js +10 -21
  21. package/dist/conflicts.d.ts +2 -4
  22. package/dist/conflicts.js +5 -6
  23. package/dist/constants.d.ts +2 -4
  24. package/dist/constants.js +1 -2
  25. package/dist/display-labels.d.ts +1 -3
  26. package/dist/display-labels.js +1 -2
  27. package/dist/format.d.ts +2 -4
  28. package/dist/format.js +4 -5
  29. package/dist/hint.d.ts +1 -3
  30. package/dist/hint.js +1 -2
  31. package/dist/hotkey-manager.d.ts +23 -4
  32. package/dist/hotkey-manager.js +138 -30
  33. package/dist/hotkey-recorder.d.ts +19 -7
  34. package/dist/hotkey-recorder.js +8 -11
  35. package/dist/hotkey.types.d.ts +1 -3
  36. package/dist/index.d.ts +8 -8
  37. package/dist/index.js +3 -3
  38. package/dist/key-state-tracker.d.ts +1 -3
  39. package/dist/key-state-tracker.js +2 -3
  40. package/dist/key.types.d.ts +5 -6
  41. package/dist/match.d.ts +4 -5
  42. package/dist/match.js +7 -7
  43. package/dist/parse.d.ts +2 -3
  44. package/dist/parse.js +10 -19
  45. package/dist/platform.d.ts +1 -3
  46. package/dist/platform.js +1 -2
  47. package/dist/validate.d.ts +33 -13
  48. package/dist/validate.js +51 -34
  49. package/dist/vendor/@tanstack/store/dist/store.d.ts +0 -2
  50. package/dist/vendor/@tanstack/store/dist/types.d.ts +0 -2
  51. package/package.json +4 -4
  52. package/skills/app-hotkeys/SKILL.md +1 -1
  53. package/skills/hotkeys-core/SKILL.md +3 -1
@@ -1,5 +1,3 @@
1
1
  import { AppHotkeys, AppHotkeysStore, AppHotkeysStoreOptions } from "./app-hotkeys.js";
2
- //#region src/_app-hotkeys-factory.d.ts
3
2
  /** Creates the store for app hotkeys from `defineAppHotkeys`. */
4
- export declare function createAppHotkeysStore<T extends AppHotkeys>(appHotkeys: T, options: AppHotkeysStoreOptions<T>): AppHotkeysStore<T>;
5
- //#endregion
3
+ export declare function createAppHotkeysStore<T extends AppHotkeys>(appHotkeys: T, options: AppHotkeysStoreOptions<T>): AppHotkeysStore<T>;
@@ -1,13 +1,12 @@
1
- //#region src/_app-hotkeys-factory.ts
1
+ // src/_app-hotkeys-factory.ts
2
2
  let factory;
3
3
  function setAppHotkeysStoreFactory(create) {
4
4
  factory = create;
5
5
  }
6
6
  /** Creates the store for app hotkeys from `defineAppHotkeys`. */
7
7
  function createAppHotkeysStore(appHotkeys, options) {
8
- if (!factory) throw new Error("[hotkeys] App hotkeys must come from defineAppHotkeys.");
8
+ if (!factory) throw new Error("[hotkeys] App hotkeys must come from defineAppHotkeys");
9
9
  return factory(appHotkeys, options);
10
10
  }
11
11
 
12
- //#endregion
13
12
  export { createAppHotkeysStore, setAppHotkeysStoreFactory };
@@ -1,6 +1,7 @@
1
+ import { codeToLogicalKey } from "./_keyboard-event.js";
1
2
  import { normalizeKeyName } from "./constants.js";
2
3
 
3
- //#region src/_event-target.ts
4
+ // src/_event-target.ts
4
5
  function isElement(node) {
5
6
  return node?.nodeType === 1;
6
7
  }
@@ -220,11 +221,11 @@ function getOwnedKeys(element) {
220
221
  * are never owned; Enter and Space are owned only without modifiers, while
221
222
  * navigation and typeahead keys allow Shift (for example, to extend a selection).
222
223
  */
223
- function ownsKey(element, event, key) {
224
+ function ownsKey(element, event, key, produced) {
224
225
  if (event.ctrlKey || event.metaKey || event.altKey) return false;
225
226
  const owned = getOwnedKeys(element);
226
227
  if (key === "Enter" || key === "Space") return !event.shiftKey && owned.includes(key);
227
- return owned.includes(key) || owned.includes(TYPEAHEAD) && /^.$/u.test(event.key);
228
+ return owned.includes(key) || owned.includes(TYPEAHEAD) && /^.$/u.test(produced);
228
229
  }
229
230
  /**
230
231
  * Finds the first element on the event's way out that blocks it, walking out
@@ -250,6 +251,38 @@ function findBlocker(event, doc, test) {
250
251
  function blocks({ elements, index }, target) {
251
252
  return index >= 0 && (isDocumentLevel(target) || index < elements.indexOf(target));
252
253
  }
254
+ /** Open modal dialogs and popovers that close on Escape, per event. */
255
+ const layerCache = /* @__PURE__ */ new WeakMap();
256
+ /** Whether `node` is `layer` or inside it, across shadow roots. */
257
+ function isInside(node, layer) {
258
+ for (; node; node = composedParent(node)) if (node === layer) return true;
259
+ return false;
260
+ }
261
+ /**
262
+ * Returns whether Escape will close a modal `<dialog>` or an open popover that
263
+ * `target` isn't inside. The browser closes it only when Escape's keydown
264
+ * isn't prevented, so hotkeys outside it leave Escape alone.
265
+ */
266
+ function closesNativeLayer(event, target) {
267
+ if (event.type !== "keydown" || event.key !== "Escape") return false;
268
+ let layers = layerCache.get(event);
269
+ if (!layers) {
270
+ const doc = getTargetDocument(target);
271
+ const focused = getDeepActiveElement(doc);
272
+ const query = (selector) => {
273
+ try {
274
+ return [...doc.querySelectorAll(selector)];
275
+ } catch {
276
+ return [];
277
+ }
278
+ };
279
+ const dialogs = query("dialog:modal");
280
+ const holding = dialogs.filter((dialog) => isInside(focused, dialog));
281
+ layers = [...holding.length ? holding : dialogs, ...query(":popover-open").filter((layer) => layer.popover !== "manual")];
282
+ layerCache.set(event, layers);
283
+ }
284
+ return layers.some((layer) => isDocumentLevel(target) || !isInside(target, layer));
285
+ }
253
286
  const ignoreCache = /* @__PURE__ */ new WeakMap();
254
287
  /**
255
288
  * Returns whether a hotkey on `target` should skip an event because a control
@@ -264,8 +297,9 @@ const ignoreCache = /* @__PURE__ */ new WeakMap();
264
297
  function shouldIgnoreInputEvent(event, target) {
265
298
  let blocker = ignoreCache.get(event);
266
299
  if (!blocker) {
267
- const key = normalizeKeyName(event.key);
268
- blocker = findBlocker(event, getTargetDocument(target), (element) => isInputElement(element) || ownsKey(element, event, key));
300
+ const produced = event.key === "Unidentified" ? codeToLogicalKey(event.code) ?? event.code : event.key;
301
+ const key = normalizeKeyName(produced);
302
+ blocker = findBlocker(event, getTargetDocument(target), (element) => isInputElement(element) || ownsKey(element, event, key, produced));
269
303
  ignoreCache.set(event, blocker);
270
304
  }
271
305
  return blocks(blocker, target);
@@ -279,5 +313,4 @@ function isTypingEvent(event, target) {
279
313
  return blocks(findBlocker(event, getTargetDocument(target), isInputElement), target);
280
314
  }
281
315
 
282
- //#endregion
283
- export { getTargetDocument, isDocumentLevel, isInputElement, isTypingEvent, shouldIgnoreInputEvent, targetHasFocus };
316
+ export { closesNativeLayer, getTargetDocument, isDocumentLevel, isInputElement, isTypingEvent, shouldIgnoreInputEvent, targetHasFocus };
@@ -1,6 +1,6 @@
1
1
  import { MODIFIER_ALIASES, PUNCTUATION_CODE_MAP, normalizeKeyName } from "./constants.js";
2
2
 
3
- //#region src/_keyboard-event.ts
3
+ // src/_keyboard-event.ts
4
4
  /** Normalizes Unicode key names and modifier flags while retaining physical code and IME state. */
5
5
  function normalizeKeyboardEvent(event, platform) {
6
6
  let altGraph = false;
@@ -22,7 +22,7 @@ function normalizeKeyboardEvent(event, platform) {
22
22
  meta: Boolean(event.metaKey),
23
23
  altGraph: altGraph && !controlAlt,
24
24
  location: event.location,
25
- isComposing: Boolean(event.isComposing) || event.keyCode === 229 || key === "Process" || key === "Unidentified"
25
+ isComposing: Boolean(event.isComposing) || event.keyCode === 229 || key === "Process" || key === "Unidentified" && (!event.code || event.code === "Unidentified" || isModifierCode(event.code))
26
26
  };
27
27
  }
28
28
  /**
@@ -77,6 +77,10 @@ function isCodeName(code) {
77
77
  function physicalCodeOf(token) {
78
78
  return token.startsWith("[") && token.endsWith("]") && isCodeName(token.slice(1, -1)) ? token.slice(1, -1) : void 0;
79
79
  }
80
+ /** Whether a `KeyboardEvent.code` is a modifier key, such as `ShiftLeft`. */
81
+ function isModifierCode(code) {
82
+ return /^(Control|Shift|Alt|Meta)(Left|Right)$/.test(code);
83
+ }
80
84
  /** Splits modifier tokens while retaining a trailing literal plus: `Control++` → `['Control', '+']`. */
81
85
  function splitChordParts(hotkey) {
82
86
  if (!hotkey.endsWith("+")) return hotkey.split("+");
@@ -105,7 +109,7 @@ function keysEqual(left, right) {
105
109
  }
106
110
  /** Rejects reserved physical-token syntax in logical object fields while allowing Unicode characters. */
107
111
  function assertLogicalKey(key) {
108
- if (/^\[.*\]$/s.test(key.trim())) throw new Error("Logical key must not contain a bracketed physical code. Use the code field instead.");
112
+ if (/^\[.*\]$/s.test(key.trim())) throw new Error(`[hotkeys] Put '${key}' in the code field, not key`);
109
113
  }
110
114
  /**
111
115
  * Whether a key is written like a key the library knows: one character,
@@ -116,5 +120,4 @@ function isKeyNameShape(key) {
116
120
  return /^(?:.|F(?:[1-9]|1\d|2[0-4])|[A-Z][a-z][A-Za-z\d]*)$/su.test(key);
117
121
  }
118
122
 
119
- //#endregion
120
- export { altGraphTypedCharacter, assertLogicalKey, codeToLogicalKey, isAsciiLetter, isCodeName, isKeyNameShape, isLatinLetter, isPrintableKey, keysEqual, modifierAlias, normalizeKeyboardEvent, physicalCodeOf, splitChordParts };
123
+ export { altGraphTypedCharacter, assertLogicalKey, codeToLogicalKey, isAsciiLetter, isCodeName, isKeyNameShape, isLatinLetter, isModifierCode, isPrintableKey, keysEqual, modifierAlias, normalizeKeyboardEvent, physicalCodeOf, splitChordParts };
@@ -1,4 +1,4 @@
1
- //#region src/_manager-ref.ts
1
+ // src/_manager-ref.ts
2
2
  /**
3
3
  * The HotkeyManager singleton, once created. Code that only reads the live
4
4
  * registry, such as conflict checks, reads this instead of importing the
@@ -6,5 +6,4 @@
6
6
  */
7
7
  const managerRef = { current: null };
8
8
 
9
- //#endregion
10
9
  export { managerRef };
package/dist/_match.js CHANGED
@@ -1,8 +1,8 @@
1
- import { detectPlatform } from "./platform.js";
2
1
  import { altGraphTypedCharacter, codeToLogicalKey, isAsciiLetter, isLatinLetter, isPrintableKey, keysEqual, normalizeKeyboardEvent } from "./_keyboard-event.js";
3
2
  import { parseHotkey } from "./parse.js";
3
+ import { detectPlatform } from "./platform.js";
4
4
 
5
- //#region src/_match.ts
5
+ // src/_match.ts
6
6
  /** Matches an event and reports the winning candidate and release identity. */
7
7
  function matchKeyboardEvent(event, hotkey, platform = detectPlatform()) {
8
8
  const parsed = typeof hotkey === "string" ? parseHotkey(hotkey, platform) : hotkey;
@@ -44,6 +44,8 @@ function matchKeyboardEvent(event, hotkey, platform = detectPlatform()) {
44
44
  * output that can't name a shortcut falls back:
45
45
  * - non-Latin letters and symbols, such as Russian `ы` or macOS Option's `–`
46
46
  * - dead keys
47
+ * - `Unidentified` keys, except on letter positions, which layouts such as
48
+ * AZERTY rearrange. Chrome on Linux Wayland reports Ctrl+` this way.
47
49
  * - anything on the digit row, where AZERTY types `&` and `é` for 1 and 2
48
50
  * - ASCII symbols typed with Shift, such as `!` for Shift+1
49
51
  * - Latin letters on punctuation keys with Control or Command, so German
@@ -54,6 +56,7 @@ function matchKeyboardEvent(event, hotkey, platform = detectPlatform()) {
54
56
  * sits. AZERTY's `+` on the US `/` position is `+`, not `/`.
55
57
  */
56
58
  function positionStandsIn({ key, code, ctrl, meta, shift, alt }) {
59
+ if (key === "Unidentified") return !code.startsWith("Key");
57
60
  if (key !== "Dead" && !isPrintableKey(key)) return false;
58
61
  if (code.startsWith("Digit")) return true;
59
62
  if (isAsciiLetter(key)) return false;
@@ -77,5 +80,4 @@ function parsedChordsEqual(a, b) {
77
80
  return a.ctrl === b.ctrl && a.alt === b.alt && a.shift === b.shift && a.meta === b.meta && (a.code !== void 0 || b.code !== void 0 ? a.code === b.code : keysEqual(a.key, b.key));
78
81
  }
79
82
 
80
- //#endregion
81
83
  export { matchKeyboardEvent, parsedChordsEqual };
@@ -1,6 +1,4 @@
1
- //#region src/_named-keys.d.ts
2
1
  /** Names defined for both KeyboardEvent.key and KeyboardEvent.code, beyond the basic key groups. */
3
2
  export declare const SHARED_NAMED_KEYS: readonly ["CapsLock", "ContextMenu", "Convert", "KanaMode", "NonConvert", "Help", "Insert", "NumLock", "Fn", "FnLock", "PrintScreen", "ScrollLock", "Pause", "BrowserBack", "BrowserFavorites", "BrowserForward", "BrowserHome", "BrowserRefresh", "BrowserSearch", "BrowserStop", "Eject", "LaunchMail", "MediaPlayPause", "MediaStop", "MediaTrackNext", "MediaTrackPrevious", "Power", "AudioVolumeDown", "AudioVolumeMute", "AudioVolumeUp", "WakeUp", "Hyper", "Super", "Again", "Copy", "Cut", "Find", "Open", "Paste", "Props", "Select", "Undo", "Hiragana", "Katakana"];
4
3
  /** Logical counterparts whose physical positions use different names (for example Sleep and Lang1). */
5
- export declare const LOGICAL_ONLY_NAMED_KEYS: readonly ["Clear", "Cancel", "Standby", "LaunchApplication1", "LaunchApplication2", "LaunchMediaPlayer", "HangulMode", "HanjaMode", "Zenkaku", "Hankaku", "ZenkakuHankaku"];
6
- //#endregion
4
+ export declare const LOGICAL_ONLY_NAMED_KEYS: readonly ["Clear", "Cancel", "Standby", "LaunchApplication1", "LaunchApplication2", "LaunchMediaPlayer", "HangulMode", "HanjaMode", "Zenkaku", "Hankaku", "ZenkakuHankaku"];
@@ -1,4 +1,4 @@
1
- //#region src/_named-keys.ts
1
+ // src/_named-keys.ts
2
2
  /** Names defined for both KeyboardEvent.key and KeyboardEvent.code, beyond the basic key groups. */
3
3
  const SHARED_NAMED_KEYS = [
4
4
  "CapsLock",
@@ -81,5 +81,4 @@ const EDITING_KEY_NAMES = [
81
81
  "Delete"
82
82
  ];
83
83
 
84
- //#endregion
85
84
  export { EDITING_KEY_NAMES, LOGICAL_ONLY_NAMED_KEYS, NAVIGATION_KEY_NAMES, SHARED_NAMED_KEYS };
@@ -1,11 +1,12 @@
1
- import { normalizeKeyName } from "./constants.js";
2
1
  import { isCodeName, normalizeKeyboardEvent } from "./_keyboard-event.js";
2
+ import { normalizeKeyName } from "./constants.js";
3
3
  import { isModifierKey, normalizeHotkey, parseKeyboardEvent } from "./parse.js";
4
4
 
5
- //#region src/_recorder-chord.ts
5
+ // src/_recorder-chord.ts
6
6
  /**
7
7
  * Converts a keydown to a replayable chord, preserving physical codes by default.
8
- * Returns null for composition, modifier-only input, or unusable physical input.
8
+ * Returns null for composition, modifier-only input, unusable physical input,
9
+ * or an `Unidentified` key in key mode.
9
10
  * Key mode retains the produced character; neither mode guesses layout letters.
10
11
  */
11
12
  function hotkeyChordFromKeydown(event, platform, recordBy = "code") {
@@ -20,7 +21,7 @@ function hotkeyChordFromKeydown(event, platform, recordBy = "code") {
20
21
  code: event.code
21
22
  }, platform);
22
23
  }
23
- return parsed.key ? normalizeHotkey(parsed, platform) : null;
24
+ return parsed.key && parsed.key !== "Unidentified" ? normalizeHotkey(parsed, platform) : null;
24
25
  }
25
26
  /**
26
27
  * Explains why a physical chord cannot be recorded before syntax/policy checks.
@@ -28,19 +29,12 @@ function hotkeyChordFromKeydown(event, platform, recordBy = "code") {
28
29
  */
29
30
  function chordRejection(event, options) {
30
31
  if (options.recordBy === "key") return void 0;
31
- if (normalizeKeyboardEvent(event, options.platform).altGraph) return {
32
- reason: "alt-graph",
33
- message: "AltGraph character entry cannot be recorded as a physical shortcut. Use key mode for characters."
34
- };
35
- if (!hasUsableCode(event)) return {
36
- reason: "missing-code",
37
- message: "This event has no usable physical key code."
38
- };
32
+ if (normalizeKeyboardEvent(event, options.platform).altGraph) return { reason: "alt-graph" };
33
+ if (!hasUsableCode(event)) return { reason: "missing-code" };
39
34
  }
40
35
  /** Whether the event reports a real physical key code to record. */
41
36
  function hasUsableCode(event) {
42
37
  return isCodeName(event.code) && event.code !== "Unidentified";
43
38
  }
44
39
 
45
- //#endregion
46
40
  export { chordRejection, hotkeyChordFromKeydown };
@@ -1,6 +1,6 @@
1
1
  import { MODIFIER_KEYS, normalizeKeyName } from "./constants.js";
2
2
 
3
- //#region src/_recording-guard.ts
3
+ // src/_recording-guard.ts
4
4
  const sessions = /* @__PURE__ */ new Set();
5
5
  const captured = /* @__PURE__ */ new WeakSet();
6
6
  const pending = /* @__PURE__ */ new Map();
@@ -51,5 +51,4 @@ function isRecordingEvent(event) {
51
51
  return sessions.size > 0 || captured.has(event) || pending.has(identity(event));
52
52
  }
53
53
 
54
- //#endregion
55
54
  export { beginRecording, captureRecordingEvent, endRecording, isRecordingEvent };
@@ -1,4 +1,3 @@
1
- //#region src/_registration.d.ts
2
1
  /**
3
2
  * Merges option layers left to right. A key set to `undefined` keeps the
4
3
  * earlier layer's value, so an unset option keeps its default. Getters are
@@ -10,5 +9,4 @@ export declare function definedOptions<T extends object>(...layers: ReadonlyArra
10
9
  * `undefined` as "clear". An unset option takes its default, or stays
11
10
  * `undefined` to clear an earlier value when it has no default.
12
11
  */
13
- export declare function withDefaults<T extends object>(defaults: object | undefined, options: T | undefined): T;
14
- //#endregion
12
+ export declare function withDefaults<T extends object>(defaults: object | undefined, options: T | undefined): T;
@@ -1,4 +1,6 @@
1
- //#region src/_registration.ts
1
+ import { isModifierCode, modifierAlias } from "./_keyboard-event.js";
2
+
3
+ // src/_registration.ts
2
4
  /** The scope of hotkeys that do not name one. It is always active unless an exclusive scope is active. */
3
5
  const GLOBAL_SCOPE = "global";
4
6
  /**
@@ -42,9 +44,13 @@ function withDefaults(defaults, options) {
42
44
  ...definedOptions(defaults, options)
43
45
  };
44
46
  }
45
- /** Whether a parsed chord has a key. An empty one would match key-less events. */
47
+ /**
48
+ * Whether a parsed chord has a key that isn't a modifier. A chord without one
49
+ * registers nothing: an empty key would match key-less events, and a
50
+ * modifier-only chord isn't supported.
51
+ */
46
52
  function chordHasKey(chord) {
47
- return !!(chord.code || chord.key);
53
+ return chord.code !== void 0 ? !!chord.code && !isModifierCode(chord.code) : !!chord.key && !modifierAlias(chord.key);
48
54
  }
49
55
  /**
50
56
  * Resolves `ignoreInputs: 'auto'` from the hotkey (the first step of a sequence).
@@ -66,10 +72,10 @@ function getDefaultIgnoreInputs(parsedChord) {
66
72
  function handleConflict(conflictingId, keyDisplay, conflictBehavior, unregister) {
67
73
  if (conflictBehavior === "allow") return;
68
74
  if (conflictBehavior === "warn") {
69
- if (process.env.NODE_ENV !== "production") console.warn(`'${keyDisplay}' is already registered. Multiple handlers may be triggered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to suppress this warning.`);
75
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") console.warn(`[hotkeys] '${keyDisplay}' is already registered`);
70
76
  return;
71
77
  }
72
- if (conflictBehavior === "error") throw new Error(`'${keyDisplay}' is already registered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to allow multiple registrations.`);
78
+ if (conflictBehavior === "error") throw new Error(`[hotkeys] '${keyDisplay}' is already registered`);
73
79
  unregister(conflictingId);
74
80
  }
75
81
  /** Compare updates without publishing unchanged options, including fresh metadata objects. */
@@ -80,5 +86,4 @@ function optionsEqual(left, right) {
80
86
  return keys.length === Object.keys(b).length && keys.every((key) => key === "meta" && a[key] && b[key] ? optionsEqual(a[key], b[key]) : Object.is(a[key], b[key]));
81
87
  }
82
88
 
83
- //#endregion
84
89
  export { GLOBAL_SCOPE, chordHasKey, defaultHotkeyOptions, definedOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual, withDefaults };
package/dist/_server.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/_server.ts
1
+ // src/_server.ts
2
2
  const SERVER_AGENT = /^(?:Node\.js\/|Deno\/|Bun\/|Cloudflare-Workers)/;
3
3
  /** Whether this runs on a server: no `navigator`, or a server runtime's. */
4
4
  function isServerRuntime() {
@@ -11,11 +11,10 @@ let warned = false;
11
11
  * users on macOS and Windows after hydration.
12
12
  */
13
13
  function warnServerPlatform() {
14
- if (process.env.NODE_ENV !== "production" && !warned && isServerRuntime()) {
14
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production" && !warned && isServerRuntime()) {
15
15
  warned = true;
16
- console.warn("[TanStack Hotkeys] A hotkey label was formatted during server rendering without a platform, so it uses 'linux'. Mac and Windows users get a different label after hydration. Read the platform from the request with `detectPlatform(userAgent)`, set it as the `platform` of your hotkeys provider or scope, and format labels with your adapter's formatter, such as `useFormatForDisplay`. Or render the labels only in the browser. See the server rendering section of the Displaying Hotkeys guide.");
16
+ console.warn("[hotkeys] Server-rendered label uses 'linux'. Set the provider's platform with detectPlatform(userAgent)");
17
17
  }
18
18
  }
19
19
 
20
- //#endregion
21
20
  export { isServerRuntime, warnServerPlatform };
package/dist/_store.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Store as Store$1 } from "./vendor/@tanstack/store/dist/store.js";
2
- //#region src/_store.d.ts
3
2
  /**
4
3
  * The part of TanStack Store's `Store` that TanStack Hotkeys uses. Its stores
5
4
  * work with TanStack Store's framework adapters, such as `useSelector` from
@@ -10,5 +9,4 @@ export type Store<T> = Pick<Store$1<T>, 'state' | 'get' | 'setState' | 'subscrib
10
9
  * Creates a store. Listeners run synchronously after each change, and an
11
10
  * update to an equal value (by `Object.is`) notifies no one.
12
11
  */
13
- export declare function createStore<T>(initialState: T): Store<T>;
14
- //#endregion
12
+ export declare function createStore<T>(initialState: T): Store<T>;
package/dist/_store.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/_store.ts
1
+ // src/_store.ts
2
2
  const queue = /* @__PURE__ */ new Set();
3
3
  function notify(listeners) {
4
4
  for (const listener of listeners) queue.add(listener);
@@ -39,5 +39,4 @@ function createStore(initialState) {
39
39
  };
40
40
  }
41
41
 
42
- //#endregion
43
42
  export { createStore };
@@ -1,4 +1,4 @@
1
- //#region src/_target-listeners.ts
1
+ // src/_target-listeners.ts
2
2
  /** Shares keyboard listeners within each target and capture phase. */
3
3
  var TargetListeners = class {
4
4
  dispatch;
@@ -7,10 +7,19 @@ var TargetListeners = class {
7
7
  constructor(dispatch) {
8
8
  this.dispatch = dispatch;
9
9
  }
10
- add(target, capture, id) {
10
+ /**
11
+ * Adds a registration to the target's shared listener. With `last`, an
12
+ * existing listener moves after listeners added since it, such as one a
13
+ * framework attached later to delegate key events.
14
+ */
15
+ add(target, capture, id, last = false) {
11
16
  if (typeof document === "undefined") return;
12
17
  const phases = this.#targets.get(target) ?? /* @__PURE__ */ new Map();
13
18
  let group = phases.get(capture);
19
+ if (group && last) for (const type of ["keydown", "keyup"]) {
20
+ target.removeEventListener(type, group[type], capture);
21
+ target.addEventListener(type, group[type], capture);
22
+ }
14
23
  if (!group) {
15
24
  const ids = /* @__PURE__ */ new Set();
16
25
  const listener = (eventType) => (rawEvent) => {
@@ -66,5 +75,4 @@ var TargetListeners = class {
66
75
  }
67
76
  };
68
77
 
69
- //#endregion
70
78
  export { TargetListeners };
package/dist/adapter.d.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { Store, createStore } from "./_store.js";
2
- import { HotkeyCallback, HotkeyChord, HotkeySequence, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
3
- import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyOptions, HotkeySequenceOptions, Target } from "./hotkey-manager.js";
4
1
  import { AppHotkey, AppHotkeysStore } from "./app-hotkeys.js";
2
+ import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyOptions, HotkeySequenceOptions, Target } from "./hotkey-manager.js";
3
+ import { HotkeyCallback, HotkeyChord, HotkeySequence, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
5
4
  import { HotkeyPlatform } from "./platform.js";
5
+ import { Store, createStore } from "./_store.js";
6
6
  import { definedOptions, withDefaults } from "./_registration.js";
7
7
  import { createAppHotkeysStore } from "@tanstack/hotkeys";
8
- //#region src/adapter.d.ts
9
8
  /**
10
9
  * A hotkey and its callback, with optional per-binding options. Chords accept
11
10
  * `requireReset`; sequences accept `timeout`.
@@ -26,8 +25,12 @@ export type HotkeyDefinition = {
26
25
  };
27
26
  /** Options shared by every definition. Each option applies only to the kinds that support it. */
28
27
  export type HotkeyBindingsCommonOptions = HotkeyChordOptions & HotkeySequenceOptions;
29
- /** Reconciles adapter registrations while retaining handles for option updates. */
30
- export declare function createHotkeyBindings(): {
28
+ /**
29
+ * Reconciles adapter registrations while retaining handles for option updates.
30
+ * Adapters for frameworks that delegate key events, such as React, pass
31
+ * `{ afterDelegatedHandlers: true }`.
32
+ */
33
+ export declare function createHotkeyBindings(adapterOptions?: Pick<HotkeyOptions, 'afterDelegatedHandlers'>): {
31
34
  /**
32
35
  * Registers new definitions, updates existing ones in place, and removes
33
36
  * stale ones. Definitions with no hotkey (undefined, null, empty string, or
@@ -37,7 +40,7 @@ export declare function createHotkeyBindings(): {
37
40
  destroy(): void;
38
41
  };
39
42
  /** Options with a framework-specific target, such as a ref. */
40
- export type WithTarget<TOptions, TTarget> = Omit<TOptions, 'target' | 'appHotkeyId'> & {
43
+ export type WithTarget<TOptions, TTarget> = Omit<TOptions, 'target' | 'appHotkeyId' | 'afterDelegatedHandlers'> & {
41
44
  /** The element to listen on. Defaults to document. */
42
45
  target?: TTarget;
43
46
  };
@@ -180,5 +183,4 @@ export declare function getHotkeysProviderScopeKey(options: {
180
183
  * Returns a function that releases the activation.
181
184
  */
182
185
  export declare function activateHotkeysProviderScope(key: string): () => void;
183
- //#endregion
184
186
  export { type Store, createAppHotkeysStore, createStore, definedOptions, withDefaults };
package/dist/adapter.js CHANGED
@@ -1,11 +1,15 @@
1
- import { warnServerPlatform } from "./_server.js";
2
1
  import { chordHasKey, defaultHotkeyOptions, definedOptions, withDefaults } from "./_registration.js";
2
+ import { warnServerPlatform } from "./_server.js";
3
3
  import { createStore } from "./_store.js";
4
4
  import { createAppHotkeysStore, detectPlatform, getAppHotkeyRegistrationOptions, getHotkeyManager, isAppHotkey, isHotkeySequence, normalizeHotkey, parseHotkey, resolveAppHotkey } from "@tanstack/hotkeys";
5
5
 
6
- //#region src/adapter.ts
7
- /** Reconciles adapter registrations while retaining handles for option updates. */
8
- function createHotkeyBindings() {
6
+ // src/adapter.ts
7
+ /**
8
+ * Reconciles adapter registrations while retaining handles for option updates.
9
+ * Adapters for frameworks that delegate key events, such as React, pass
10
+ * `{ afterDelegatedHandlers: true }`.
11
+ */
12
+ function createHotkeyBindings(adapterOptions = {}) {
9
13
  const registrations = /* @__PURE__ */ new Map();
10
14
  return {
11
15
  /**
@@ -20,6 +24,7 @@ function createHotkeyBindings() {
20
24
  const options = {
21
25
  ...defaultHotkeyOptions,
22
26
  ...merged,
27
+ ...adapterOptions,
23
28
  target: "target" in merged ? merged.target ?? null : typeof document === "undefined" ? null : document,
24
29
  platform: merged.platform ?? detectPlatform()
25
30
  };
@@ -173,5 +178,4 @@ function activateHotkeysProviderScope(key) {
173
178
  });
174
179
  }
175
180
 
176
- //#endregion
177
181
  export { activateHotkeysProviderScope, createAppHotkeysStore, createHotkeyBindings, createStore, definedOptions, getHotkeysProviderScopeKey, getServerPlatformSnapshot, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys, withDefaults };
@@ -1,7 +1,6 @@
1
1
  import { Store } from "./_store.js";
2
- import { Hotkey, HotkeyChord, HotkeyMeta, HotkeySequence } from "./hotkey.types.js";
3
2
  import { HotkeyChordOptions, HotkeyRegistrationView, HotkeySequenceOptions } from "./hotkey-manager.js";
4
- //#region src/app-hotkeys.d.ts
3
+ import { Hotkey, HotkeyChord, HotkeyMeta, HotkeySequence } from "./hotkey.types.js";
5
4
  type Platform = 'mac' | 'windows' | 'linux';
6
5
  type DeclaredOptionKeys = 'capture' | 'conflictBehavior' | 'eventType' | 'ignoreInputs' | 'preventDefault' | 'stopPropagation';
7
6
  /** Declared options for an app hotkey whose default is a chord. */
@@ -136,8 +135,8 @@ export interface AppHotkeysState<T extends AppHotkeys = AppHotkeys> {
136
135
  /** Options for `AppHotkeysStore`. */
137
136
  export interface AppHotkeysStoreOptions<T extends AppHotkeys = AppHotkeys> {
138
137
  /**
139
- * The user's saved custom hotkeys. Invalid entries are ignored with a
140
- * warning. Passing a different value to `setOptions` later, such as data
138
+ * The user's saved custom hotkeys. Unknown ids and invalid entries are
139
+ * ignored. Passing a different value to `setOptions` later, such as data
141
140
  * that finished loading or another user's data, replaces the current
142
141
  * custom hotkeys. `undefined` leaves them unchanged; a change to `null`
143
142
  * clears them.
@@ -254,4 +253,4 @@ export declare function provideAppHotkeys<T extends AppHotkeys>(appHotkeys: T, o
254
253
  store: AppHotkeysStore<T>;
255
254
  dispose: () => void;
256
255
  };
257
- //#endregion
256
+ export {};
@@ -1,13 +1,13 @@
1
- import { detectPlatform } from "./platform.js";
2
- import { isHotkeySequence, simplifyHotkey } from "./parse.js";
3
- import { validateHotkey } from "./validate.js";
1
+ import { setAppHotkeysStoreFactory } from "./_app-hotkeys-factory.js";
4
2
  import { GLOBAL_SCOPE } from "./_registration.js";
5
- import { areHotkeysEqual } from "./match.js";
6
3
  import { createStore } from "./_store.js";
7
4
  import { getHotkeyManager } from "./hotkey-manager.js";
8
- import { setAppHotkeysStoreFactory } from "./_app-hotkeys-factory.js";
5
+ import { areHotkeysEqual } from "./match.js";
6
+ import { isHotkeySequence, simplifyHotkey } from "./parse.js";
7
+ import { detectPlatform } from "./platform.js";
8
+ import { validateHotkey } from "./validate.js";
9
9
 
10
- //#region src/app-hotkeys.ts
10
+ // src/app-hotkeys.ts
11
11
  /**
12
12
  * Declares every app hotkey up front, without registering any, so cheat
13
13
  * sheets and settings screens can list hotkeys whose components have not
@@ -69,22 +69,15 @@ function applyCustomHotkeys(appHotkeys, customHotkeys) {
69
69
  }
70
70
  return result;
71
71
  }
72
- /** Keeps only valid custom hotkeys for known ids, warning about the rest. */
72
+ /** Keeps only valid custom hotkeys for known ids and silently drops the rest. */
73
73
  function sanitizeCustomHotkeys(appHotkeys, saved) {
74
74
  const result = {};
75
75
  if (typeof saved !== "object" || saved === null) return result;
76
76
  for (const [id, value] of Object.entries(saved)) {
77
77
  if (value === void 0) continue;
78
- if (!Object.hasOwn(appHotkeys, id)) {
79
- if (process.env.NODE_ENV !== "production") console.warn(`Ignoring custom hotkey for unknown app hotkey '${id}'`);
80
- continue;
81
- }
78
+ if (!Object.hasOwn(appHotkeys, id)) continue;
82
79
  const hotkey = parseCustomHotkey(value);
83
- if (hotkey === void 0) {
84
- if (process.env.NODE_ENV !== "production") console.warn(`Ignoring invalid custom hotkey for app hotkey '${id}':`, value);
85
- continue;
86
- }
87
- result[id] = hotkey;
80
+ if (hotkey !== void 0) result[id] = hotkey;
88
81
  }
89
82
  return result;
90
83
  }
@@ -232,10 +225,7 @@ var AppHotkeysStore = class {
232
225
  */
233
226
  setCustomHotkey(id, hotkey) {
234
227
  const appHotkey = this.get(id);
235
- if (!appHotkey) {
236
- if (process.env.NODE_ENV !== "production") console.warn(`Cannot set a custom hotkey for unknown app hotkey '${id}'`);
237
- return [];
238
- }
228
+ if (!appHotkey) return [];
239
229
  const next = hotkey === null ? null : simplifyHotkey(hotkey);
240
230
  if (next !== null) {
241
231
  const conflicts = this.findConflicts(id, next);
@@ -346,5 +336,4 @@ function provideAppHotkeys(appHotkeys, options = {}) {
346
336
  };
347
337
  }
348
338
 
349
- //#endregion
350
339
  export { AppHotkeysStore, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey };
@@ -1,6 +1,5 @@
1
- import { RegisterableHotkey } from "./hotkey.types.js";
2
1
  import { HotkeyRegistrationView } from "./hotkey-manager.js";
3
- //#region src/conflicts.d.ts
2
+ import { RegisterableHotkey } from "./hotkey.types.js";
4
3
  /** A live registration that overlaps a candidate hotkey. */
5
4
  export interface HotkeyConflict {
6
5
  registration: HotkeyRegistrationView;
@@ -28,5 +27,4 @@ export interface HotkeyConflictOptions {
28
27
  * With source events, also checks observed logical/physical overlap. Does not
29
28
  * discover unmounted handlers, external listeners, or other keyboard layouts.
30
29
  */
31
- export declare function findHotkeyConflicts(candidate: RegisterableHotkey, options?: HotkeyConflictOptions): Array<HotkeyConflict>;
32
- //#endregion
30
+ export declare function findHotkeyConflicts(candidate: RegisterableHotkey, options?: HotkeyConflictOptions): Array<HotkeyConflict>;
package/dist/conflicts.js CHANGED
@@ -1,12 +1,12 @@
1
- import { detectPlatform } from "./platform.js";
2
- import { isHotkeySequence, parseHotkey } from "./parse.js";
3
- import { parsedChordsEqual } from "./_match.js";
4
- import { matchesKeyboardEvent } from "./match.js";
5
1
  import { getTargetDocument } from "./_event-target.js";
6
2
  import { managerRef } from "./_manager-ref.js";
3
+ import { parsedChordsEqual } from "./_match.js";
7
4
  import { toHotkeyRegistrationView } from "./hotkey-manager.js";
5
+ import { matchesKeyboardEvent } from "./match.js";
6
+ import { isHotkeySequence, parseHotkey } from "./parse.js";
7
+ import { detectPlatform } from "./platform.js";
8
8
 
9
- //#region src/conflicts.ts
9
+ // src/conflicts.ts
10
10
  /** Whether two targets share a document and one contains the other (or is its window). */
11
11
  function targetsOverlap(a, b) {
12
12
  if (a === b) return true;
@@ -42,5 +42,4 @@ function findHotkeyConflicts(candidate, options = {}) {
42
42
  return conflicts;
43
43
  }
44
44
 
45
- //#endregion
46
45
  export { findHotkeyConflicts };