@tanstack/hotkeys 0.19.0 → 0.21.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/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- import { LOGICAL_ONLY_NAMED_KEYS, SHARED_NAMED_KEYS } from "./_named-keys.js";
1
+ import { EDITING_KEY_NAMES, LOGICAL_ONLY_NAMED_KEYS, NAVIGATION_KEY_NAMES, SHARED_NAMED_KEYS } from "./_named-keys.js";
2
2
 
3
3
  //#region src/constants.ts
4
4
  /**
@@ -80,34 +80,7 @@ const MODIFIER_ALIASES = {
80
80
  * Used for validation and type checking. Letter keys are matched case-insensitively
81
81
  * in hotkey matching, but normalized to uppercase in canonical form.
82
82
  */
83
- const LETTER_KEYS = /* @__PURE__ */ new Set([
84
- "A",
85
- "B",
86
- "C",
87
- "D",
88
- "E",
89
- "F",
90
- "G",
91
- "H",
92
- "I",
93
- "J",
94
- "K",
95
- "L",
96
- "M",
97
- "N",
98
- "O",
99
- "P",
100
- "Q",
101
- "R",
102
- "S",
103
- "T",
104
- "U",
105
- "V",
106
- "W",
107
- "X",
108
- "Y",
109
- "Z"
110
- ]);
83
+ const LETTER_KEYS = /* @__PURE__ */ new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
111
84
  /**
112
85
  * Set of all valid number keys (0-9).
113
86
  *
@@ -115,50 +88,14 @@ const LETTER_KEYS = /* @__PURE__ */ new Set([
115
88
  * so they're excluded from Shift-based hotkey combinations to avoid
116
89
  * layout-dependent behavior.
117
90
  */
118
- const NUMBER_KEYS = /* @__PURE__ */ new Set([
119
- "0",
120
- "1",
121
- "2",
122
- "3",
123
- "4",
124
- "5",
125
- "6",
126
- "7",
127
- "8",
128
- "9"
129
- ]);
91
+ const NUMBER_KEYS = /* @__PURE__ */ new Set("0123456789");
130
92
  /**
131
93
  * Set of all valid function keys (F1-F24).
132
94
  *
133
95
  * Function keys are commonly used for system shortcuts (e.g., F12 for DevTools,
134
96
  * Alt+F4 to close windows) and application-specific commands.
135
97
  */
136
- const FUNCTION_KEYS = /* @__PURE__ */ new Set([
137
- "F1",
138
- "F2",
139
- "F3",
140
- "F4",
141
- "F5",
142
- "F6",
143
- "F7",
144
- "F8",
145
- "F9",
146
- "F10",
147
- "F11",
148
- "F12",
149
- "F13",
150
- "F14",
151
- "F15",
152
- "F16",
153
- "F17",
154
- "F18",
155
- "F19",
156
- "F20",
157
- "F21",
158
- "F22",
159
- "F23",
160
- "F24"
161
- ]);
98
+ const FUNCTION_KEYS = /* @__PURE__ */ new Set(Array.from({ length: 24 }, (_, i) => `F${i + 1}`));
162
99
  /**
163
100
  * Set of all valid navigation keys for cursor movement and document navigation.
164
101
  *
@@ -166,16 +103,7 @@ const FUNCTION_KEYS = /* @__PURE__ */ new Set([
166
103
  * These keys are commonly combined with modifiers for selection (Shift+ArrowUp) or
167
104
  * navigation shortcuts (Alt+ArrowLeft for back).
168
105
  */
169
- const NAVIGATION_KEYS = /* @__PURE__ */ new Set([
170
- "ArrowUp",
171
- "ArrowDown",
172
- "ArrowLeft",
173
- "ArrowRight",
174
- "Home",
175
- "End",
176
- "PageUp",
177
- "PageDown"
178
- ]);
106
+ const NAVIGATION_KEYS = /* @__PURE__ */ new Set(NAVIGATION_KEY_NAMES);
179
107
  /**
180
108
  * Set of all valid editing and special keys.
181
109
  *
@@ -183,14 +111,7 @@ const NAVIGATION_KEYS = /* @__PURE__ */ new Set([
183
111
  * special actions (Escape, Space). These keys are frequently combined with modifiers
184
112
  * for editor shortcuts (Mod+Enter to submit, Shift+Tab to go back).
185
113
  */
186
- const EDITING_KEYS = /* @__PURE__ */ new Set([
187
- "Enter",
188
- "Escape",
189
- "Space",
190
- "Tab",
191
- "Backspace",
192
- "Delete"
193
- ]);
114
+ const EDITING_KEYS = /* @__PURE__ */ new Set(EDITING_KEY_NAMES);
194
115
  /**
195
116
  * Set of all valid punctuation keys commonly used in keyboard shortcuts.
196
117
  *
@@ -203,40 +124,7 @@ const EDITING_KEYS = /* @__PURE__ */ new Set([
203
124
  * Note: Punctuation keys are affected by Shift (Shift+',' → '<' on US layout),
204
125
  * so they're excluded from Shift-based hotkey combinations to avoid layout-dependent behavior.
205
126
  */
206
- const PUNCTUATION_KEYS = /* @__PURE__ */ new Set([
207
- "/",
208
- "[",
209
- "]",
210
- "\\",
211
- "=",
212
- "-",
213
- ",",
214
- ".",
215
- ";",
216
- "`",
217
- "'",
218
- "+",
219
- "?",
220
- "!",
221
- "@",
222
- "#",
223
- "$",
224
- "%",
225
- "^",
226
- "&",
227
- "*",
228
- "(",
229
- ")",
230
- "_",
231
- "{",
232
- "}",
233
- "|",
234
- ":",
235
- "\"",
236
- "<",
237
- ">",
238
- "~"
239
- ]);
127
+ const PUNCTUATION_KEYS = /* @__PURE__ */ new Set("/[]\\=-,.;`'+?!@#$%^&*()_{}|:\"<>~");
240
128
  /**
241
129
  * Maps `KeyboardEvent.code` values for punctuation keys to their canonical characters.
242
130
  *
@@ -259,12 +147,15 @@ const PUNCTUATION_CODE_MAP = {
259
147
  Semicolon: ";",
260
148
  Slash: "/"
261
149
  };
150
+ function union(...groups) {
151
+ return new Set(groups.flatMap((group) => [...group]));
152
+ }
262
153
  /**
263
154
  * Set of all valid non-modifier keys.
264
155
  *
265
- * This is the union of all key type sets (letters, numbers, function keys, navigation,
266
- * editing, and punctuation). Used primarily for validation to check if a key string
267
- * is recognized and will have type-safe autocomplete support.
156
+ * This is the union of all key type sets (letters, numbers, function keys,
157
+ * navigation, editing, punctuation, and other named keys): every key with
158
+ * type-safe autocomplete support.
268
159
  *
269
160
  * @see {@link LETTER_KEYS}
270
161
  * @see {@link NUMBER_KEYS}
@@ -273,52 +164,23 @@ const PUNCTUATION_CODE_MAP = {
273
164
  * @see {@link EDITING_KEYS}
274
165
  * @see {@link PUNCTUATION_KEYS}
275
166
  */
276
- const ALL_KEYS = /* @__PURE__ */ new Set([
277
- ...LETTER_KEYS,
278
- ...NUMBER_KEYS,
279
- ...FUNCTION_KEYS,
280
- ...NAVIGATION_KEYS,
281
- ...EDITING_KEYS,
282
- ...PUNCTUATION_KEYS,
283
- ...SHARED_NAMED_KEYS,
284
- ...LOGICAL_ONLY_NAMED_KEYS
285
- ]);
286
- /**
287
- * Maps lowercase key names and aliases to their canonical form, so lookups
288
- * are case-insensitive: `'esc'`, `'Esc'`, and `'ESCAPE'` all become `'Escape'`.
289
- * Canonical names come from {@link ALL_KEYS}, so validation and normalization
290
- * share one spelling for every key.
291
- */
292
- const CANONICAL_KEY_NAMES = /* @__PURE__ */ new Map([
293
- ...Array.from(ALL_KEYS, (key) => [key.toLowerCase(), key]),
294
- ["plus", "+"],
295
- ["esc", "Escape"],
296
- ["return", "Enter"],
297
- [" ", "Space"],
298
- ["spacebar", "Space"],
299
- ["del", "Delete"],
300
- ["up", "ArrowUp"],
301
- ["down", "ArrowDown"],
302
- ["left", "ArrowLeft"],
303
- ["right", "ArrowRight"],
304
- ["pgup", "PageUp"],
305
- ["pgdn", "PageDown"]
306
- ]);
167
+ const ALL_KEYS = /* @__PURE__ */ union(LETTER_KEYS, NUMBER_KEYS, FUNCTION_KEYS, NAVIGATION_KEYS, EDITING_KEYS, PUNCTUATION_KEYS, SHARED_NAMED_KEYS, LOGICAL_ONLY_NAMED_KEYS);
307
168
  /** Whether a key is a single Unicode letter, such as `'a'` or `'é'`. */
308
169
  function isSingleLetterKey(key) {
309
170
  return /^\p{Letter}$/u.test(key);
310
171
  }
311
172
  /**
312
- * Normalizes a key name to its canonical form.
173
+ * Normalizes a key name from a `KeyboardEvent` or a hotkey: single letters
174
+ * become uppercase, and the space character becomes `Space`. Other names are
175
+ * returned as written, because key names are case-sensitive.
313
176
  *
314
- * @param key - The key name to normalize (can be an alias, lowercase, etc.)
177
+ * @param key - The key name to normalize
315
178
  * @returns The canonical key name
316
179
  *
317
180
  * @example
318
181
  * ```ts
319
- * normalizeKeyName('esc') // 'Escape'
320
182
  * normalizeKeyName('a') // 'A'
321
- * normalizeKeyName('f1') // 'F1'
183
+ * normalizeKeyName(' ') // 'Space'
322
184
  * normalizeKeyName('ArrowUp') // 'ArrowUp' (already canonical)
323
185
  * ```
324
186
  */
@@ -328,7 +190,7 @@ function normalizeKeyName(key) {
328
190
  const upper = key.toUpperCase();
329
191
  return upper.length === 1 ? upper : key;
330
192
  }
331
- return CANONICAL_KEY_NAMES.get(key.toLowerCase()) ?? key;
193
+ return key === " " ? "Space" : key;
332
194
  }
333
195
 
334
196
  //#endregion
package/dist/format.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CanonicalModifier } from "./key.types.js";
1
2
  import { ParsedHotkeyChord, RegisterableHotkeyChord } from "./hotkey.types.js";
2
3
  //#region src/format.d.ts
3
4
  /**
@@ -17,8 +18,11 @@ import { ParsedHotkeyChord, RegisterableHotkeyChord } from "./hotkey.types.js";
17
18
  export declare function formatHotkey(parsed: ParsedHotkeyChord): string;
18
19
  export declare function formatHotkey(parsed: ReadonlyArray<ParsedHotkeyChord>): Array<string>;
19
20
  export declare function formatHotkey(parsed: ParsedHotkeyChord | ReadonlyArray<ParsedHotkeyChord>): string | Array<string>;
20
- /** A single chord to display: a string, raw object, or parsed hotkey. */
21
- export type DisplayHotkeyChord = RegisterableHotkeyChord | ParsedHotkeyChord | (string & {});
21
+ /**
22
+ * A single chord to display: a hotkey string, which includes single keys, a
23
+ * modifier on its own, a raw object such as `{ key }`, or a parsed hotkey.
24
+ */
25
+ export type DisplayHotkeyChord = RegisterableHotkeyChord | ParsedHotkeyChord | CanonicalModifier;
22
26
  /** A hotkey to display: a chord or a sequence of chords. */
23
27
  export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChord>;
24
28
  /**
@@ -35,7 +39,10 @@ export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChor
35
39
  * Sequence steps are formatted individually and joined with `sequenceSeparator`
36
40
  * (a space by default).
37
41
  *
38
- * @param hotkey - A logical or physical string, raw object, ParsedHotkeyChord, or sequence
42
+ * A key reported by a `KeyboardEvent`, such as a held key from
43
+ * `KeyStateTracker`, can be outside the key types: pass it as `{ key }`.
44
+ *
45
+ * @param hotkey - A logical or physical hotkey string, raw object, ParsedHotkeyChord, or sequence
39
46
  * @param options - Formatting options
40
47
  * @returns Display text, or individual key labels when parts is true (one array per step for sequences)
41
48
  *
@@ -50,6 +57,12 @@ export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChor
50
57
  * formatForDisplay('Escape')
51
58
  * // Returns: 'Esc' (on all platforms)
52
59
  *
60
+ * formatForDisplay('Shift', { platform: 'mac' })
61
+ * // Returns: '⇧'
62
+ *
63
+ * formatForDisplay({ key: heldKey })
64
+ * // A key from a KeyboardEvent, such as a held key
65
+ *
53
66
  * formatForDisplay(['G', 'H'])
54
67
  * // Returns: 'G H'
55
68
  * ```
package/dist/format.js CHANGED
@@ -45,20 +45,25 @@ function formatChordParts(hotkey, options) {
45
45
  };
46
46
  segments.sort((a, b) => modifierIndex(a) - modifierIndex(b));
47
47
  }
48
- return segments.map((segment) => {
49
- const modifier = modifierAlias(segment);
50
- if (modifier) return platform === "mac" ? modifierSymbols ? MAC_MODIFIER_SYMBOLS[modifier] : MAC_MODIFIER_LABELS[modifier] : platform === "windows" ? WINDOWS_MODIFIER_LABELS[modifier] : LINUX_MODIFIER_LABELS[modifier];
51
- const code = physicalCodeOf(segment);
52
- const label = options.keyLabels?.[code ?? segment];
53
- if (label !== void 0) return label;
54
- const numpad = code?.startsWith("Numpad") ?? false;
55
- let displayKey = code ?? segment;
56
- const mappedKey = code ? options.layoutMap?.get(code) : void 0;
57
- if (mappedKey) displayKey = normalizeKeyName(mappedKey);
58
- else if (code) displayKey = numpad ? code.slice(6) : codeToLogicalKey(code) ?? code;
59
- const display = (keySymbols ? KEY_DISPLAY_SYMBOLS : PUNCTUATION_KEY_DISPLAY_LABELS)[displayKey] ?? displayKey;
60
- return numpad ? `Numpad ${display.replace(/([a-z])([A-Z])/g, "$1 $2")}` : display;
61
- });
48
+ return segments.map((segment) => formatKey(segment, platform, modifierSymbols, keySymbols, options));
49
+ }
50
+ /** Own properties only, so names such as `toString` never read Object.prototype. */
51
+ function lookup(table, name) {
52
+ return table && Object.hasOwn(table, name) ? table[name] : void 0;
53
+ }
54
+ function formatKey(segment, platform, modifierSymbols, keySymbols, options) {
55
+ const modifier = modifierAlias(segment);
56
+ if (modifier) return platform === "mac" ? modifierSymbols ? MAC_MODIFIER_SYMBOLS[modifier] : MAC_MODIFIER_LABELS[modifier] : platform === "windows" ? WINDOWS_MODIFIER_LABELS[modifier] : LINUX_MODIFIER_LABELS[modifier];
57
+ const code = physicalCodeOf(segment);
58
+ const label = lookup(options.keyLabels, code ?? segment);
59
+ if (label !== void 0) return label;
60
+ const numpad = code?.startsWith("Numpad") ?? false;
61
+ let displayKey = code ?? segment;
62
+ const mappedKey = code ? options.layoutMap?.get(code) : void 0;
63
+ if (mappedKey) displayKey = normalizeKeyName(mappedKey);
64
+ else if (code) displayKey = numpad ? code.slice(6) : codeToLogicalKey(code) ?? code;
65
+ const display = lookup(keySymbols ? KEY_DISPLAY_SYMBOLS : PUNCTUATION_KEY_DISPLAY_LABELS, displayKey) ?? displayKey;
66
+ return numpad ? `Numpad ${display.replace(/([a-z])([A-Z])/g, "$1 $2")}` : display;
62
67
  }
63
68
 
64
69
  //#endregion
package/dist/hint.d.ts CHANGED
@@ -11,11 +11,12 @@ export interface HeldModifierOptions {
11
11
  * By default, any nonempty subset of the binding's modifiers qualifies; an
12
12
  * unrelated modifier hides the hint. `exact` requires every binding modifier.
13
13
  * Nonmodifier keys are ignored. This does not check registration or focus.
14
- * A sequence reveals its hint from its first step.
14
+ * A sequence reveals its hint from its first step. Returns false for no
15
+ * hotkey (`undefined`, `null`, or `''`).
15
16
  *
16
17
  * @example
17
18
  * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt']) // true
18
19
  * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt', 'Control']) // false
19
20
  */
20
- export declare function matchesHeldModifiers(hotkey: RegisterableHotkey, heldKeys: ReadonlyArray<string>, options?: HeldModifierOptions): boolean;
21
+ export declare function matchesHeldModifiers(hotkey: RegisterableHotkey | null | undefined, heldKeys: ReadonlyArray<string>, options?: HeldModifierOptions): boolean;
21
22
  //#endregion
package/dist/hint.js CHANGED
@@ -7,13 +7,15 @@ import { isHotkeySequence, parseHotkey } from "./parse.js";
7
7
  * By default, any nonempty subset of the binding's modifiers qualifies; an
8
8
  * unrelated modifier hides the hint. `exact` requires every binding modifier.
9
9
  * Nonmodifier keys are ignored. This does not check registration or focus.
10
- * A sequence reveals its hint from its first step.
10
+ * A sequence reveals its hint from its first step. Returns false for no
11
+ * hotkey (`undefined`, `null`, or `''`).
11
12
  *
12
13
  * @example
13
14
  * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt']) // true
14
15
  * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt', 'Control']) // false
15
16
  */
16
17
  function matchesHeldModifiers(hotkey, heldKeys, options = {}) {
18
+ if (!hotkey) return false;
17
19
  if (heldKeys.includes("AltGraph")) return false;
18
20
  const held = [...new Set(heldKeys.map(normalizeKeyName).filter((key) => MODIFIER_KEYS.has(key)))];
19
21
  const first = isHotkeySequence(hotkey) ? hotkey[0] : hotkey;
@@ -1,6 +1,6 @@
1
+ import { Store } from "./_store.js";
1
2
  import { ConflictBehavior, HotkeyCallback, HotkeyCallbackContextFor, HotkeyChord, HotkeyMeta, HotkeySequence, ParsedHotkeyChord, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
2
3
  import { AppHotkey, AppHotkeysStore } from "./app-hotkeys.js";
3
- import { Store } from "@tanstack/store";
4
4
  //#region src/hotkey-manager.d.ts
5
5
  /** An element, document, or window that hotkeys can listen on. */
6
6
  export type Target = HTMLElement | Document | Window;
@@ -366,15 +366,6 @@ export declare class HotkeyManager {
366
366
  * Resets the progress of every in-progress sequence.
367
367
  */
368
368
  resetSequenceProgress(): void;
369
- /**
370
- * Triggers a registration's callback programmatically from devtools.
371
- * Creates a synthetic KeyboardEvent from the chord (or a sequence's last step)
372
- * and invokes the callback.
373
- *
374
- * @param id - The registration ID to trigger
375
- * @returns True if the registration was found and triggered
376
- */
377
- triggerRegistration(id: string): boolean;
378
369
  /**
379
370
  * Gets the number of registered hotkeys, chords and sequences.
380
371
  */
@@ -394,10 +385,11 @@ export declare class HotkeyManager {
394
385
  * only while focus is inside that element. An app hotkey matches
395
386
  * registrations by id, so custom hotkeys apply.
396
387
  *
397
- * @param hotkey - A chord, sequence, or app hotkey
388
+ * @param hotkey - A chord, sequence, or app hotkey. No hotkey
389
+ * (`undefined`, `null`, or `''`) returns false.
398
390
  * @returns True if a registration that can fire exists
399
391
  */
400
- hasActiveRegistration(hotkey: RegisterableHotkey | AppHotkey): boolean;
392
+ hasActiveRegistration(hotkey: RegisterableHotkey | AppHotkey | null | undefined): boolean;
401
393
  /**
402
394
  * Destroys the manager and removes all listeners.
403
395
  */
@@ -1,16 +1,16 @@
1
1
  import { normalizeKeyName } from "./constants.js";
2
2
  import { detectPlatform } from "./platform.js";
3
- import { keysEqual, normalizeKeyboardEvent } from "./_keyboard-event.js";
3
+ import { isKeyNameShape, keysEqual, normalizeKeyboardEvent } from "./_keyboard-event.js";
4
4
  import { isHotkeySequence, isModifierKey, parseHotkey, simplifyHotkey } from "./parse.js";
5
5
  import { formatHotkey } from "./format.js";
6
6
  import { matchKeyboardEvent, parsedChordsEqual } from "./_match.js";
7
+ import { GLOBAL_SCOPE, chordHasKey, defaultHotkeyOptions, definedOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual } from "./_registration.js";
7
8
  import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual } from "./match.js";
8
9
  import { getTargetDocument, isDocumentLevel, shouldIgnoreInputEvent, targetHasFocus } from "./_event-target.js";
10
+ import { createStore } from "./_store.js";
9
11
  import { TargetListeners } from "./_target-listeners.js";
10
12
  import { managerRef } from "./_manager-ref.js";
11
13
  import { isRecordingEvent } from "./_recording-guard.js";
12
- import { GLOBAL_SCOPE, defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual } from "./_registration.js";
13
- import { Store } from "@tanstack/store";
14
14
 
15
15
  //#region src/hotkey-manager.ts
16
16
  /** Whether a registration can fire now. Without a manager, scopes don't apply. */
@@ -57,6 +57,13 @@ function chordString(chord, parsed) {
57
57
  function generateId() {
58
58
  return `hotkey_${++registrationIdCounter}`;
59
59
  }
60
+ /** Defaults for a registration of the given kind. */
61
+ function registrationDefaults(kind) {
62
+ return {
63
+ ...defaultHotkeyOptions,
64
+ ...kind === "sequence" ? { timeout: DEFAULT_SEQUENCE_TIMEOUT } : { requireReset: false }
65
+ };
66
+ }
60
67
  /** Drops options that do not apply to a registration's kind. */
61
68
  function optionsForKind(kind, options) {
62
69
  if (kind === "chord") {
@@ -127,14 +134,14 @@ var HotkeyManager = class HotkeyManager {
127
134
  * }
128
135
  * ```
129
136
  */
130
- registrations = new Store(/* @__PURE__ */ new Map());
137
+ registrations = createStore(/* @__PURE__ */ new Map());
131
138
  /**
132
139
  * The app hotkeys store published by a framework provider, so tools such as
133
140
  * devtools can list declared hotkeys that are not registered.
134
141
  */
135
- appHotkeys = new Store(null);
142
+ appHotkeys = createStore(null);
136
143
  /** Active scopes. Subscribe to show which hotkeys are suspended. */
137
- scopes = new Store({
144
+ scopes = createStore({
138
145
  active: [],
139
146
  exclusive: null,
140
147
  allowed: [],
@@ -194,7 +201,7 @@ var HotkeyManager = class HotkeyManager {
194
201
  if (scope === "global") return () => {};
195
202
  const { exclusive } = options;
196
203
  const allow = exclusive ? exclusive === true ? [] : exclusive.allow : null;
197
- if (allow && [...this.#scopeActivations].some((other) => other.allow && other.scope === scope)) console.warn(`Exclusive hotkey scope '${scope}' is already active. Hotkeys from both activations will fire; give each a unique scope.`);
204
+ if (process.env.NODE_ENV !== "production" && allow && [...this.#scopeActivations].some((other) => other.allow && other.scope === scope)) console.warn(`Exclusive hotkey scope '${scope}' is already active. Hotkeys from both activations will fire; give each a unique scope.`);
198
205
  const activation = {
199
206
  scope,
200
207
  parent: options.parent ?? "global",
@@ -296,6 +303,7 @@ var HotkeyManager = class HotkeyManager {
296
303
  this.registrations.setState((prev) => new Map(prev));
297
304
  }
298
305
  register(input, callback, options = {}) {
306
+ if (input == null) throw new Error("A hotkey is required. Skip registration when there is no hotkey.");
299
307
  const hotkey = simplifyHotkey(input);
300
308
  if (isHotkeySequence(hotkey) && hotkey.length === 0) throw new Error("Sequence must contain at least one hotkey");
301
309
  if (typeof document === "undefined" && !options.target) {
@@ -319,15 +327,14 @@ var HotkeyManager = class HotkeyManager {
319
327
  const platform = options.platform ?? this.#platform;
320
328
  const target = options.target ?? document;
321
329
  const sequence = isHotkeySequence(hotkey);
330
+ const kind = sequence ? "sequence" : "chord";
322
331
  const common = {
323
332
  id,
324
333
  callback,
325
334
  options: {
326
- ...defaultHotkeyOptions,
327
- ...sequence ? { timeout: DEFAULT_SEQUENCE_TIMEOUT } : { requireReset: false },
328
- ...optionsForKind(sequence ? "sequence" : "chord", options),
329
- platform,
330
- ignoreInputs: options.ignoreInputs ?? "auto"
335
+ ...registrationDefaults(kind),
336
+ ...optionsForKind(kind, definedOptions(options)),
337
+ platform
331
338
  },
332
339
  target,
333
340
  triggerCount: 0,
@@ -353,6 +360,11 @@ var HotkeyManager = class HotkeyManager {
353
360
  parsedChord
354
361
  };
355
362
  }
363
+ const steps = registration.kind === "sequence" ? registration.parsedSequence : [registration.parsedChord];
364
+ if (!steps.every(chordHasKey)) throw new Error(`Hotkey '${formatForMessage(registration.hotkey)}' has no key. Skip registration when there is no hotkey.`);
365
+ if (process.env.NODE_ENV !== "production") {
366
+ for (const { key } of steps) if (key !== void 0 && !isKeyNameShape(key)) console.warn(`Unknown key '${key}' in '${formatForMessage(registration.hotkey)}'. Key names are case-sensitive, such as 'Escape' and 'F1', so this hotkey may never fire.`);
367
+ }
356
368
  const conflicting = this.#findConflictingRegistration(registration);
357
369
  if (conflicting) handleConflict(conflicting.id, formatForMessage(registration.hotkey), options.conflictBehavior ?? "warn", (conflictId) => this.#unregister(conflictId));
358
370
  this.registrations.setState((prev) => new Map(prev).set(id, registration));
@@ -376,10 +388,15 @@ var HotkeyManager = class HotkeyManager {
376
388
  manager.registrations.setState((prev) => {
377
389
  const reg = prev.get(id);
378
390
  if (!reg) return prev;
379
- const options = {
380
- ...reg.options,
381
- ...optionsForKind(reg.kind, newOptions)
391
+ const defaults = {
392
+ ...registrationDefaults(reg.kind),
393
+ platform: reg.options.platform
382
394
  };
395
+ const options = { ...reg.options };
396
+ const entries = Object.entries(optionsForKind(reg.kind, newOptions));
397
+ for (const [key, value] of entries) if (value !== void 0) options[key] = value;
398
+ else if (key in defaults) options[key] = defaults[key];
399
+ else delete options[key];
383
400
  if (optionsEqual(reg.options, options)) return prev;
384
401
  const capture = !!reg.options.capture;
385
402
  if (capture !== !!options.capture) {
@@ -662,31 +679,6 @@ var HotkeyManager = class HotkeyManager {
662
679
  this.#notify();
663
680
  }
664
681
  /**
665
- * Triggers a registration's callback programmatically from devtools.
666
- * Creates a synthetic KeyboardEvent from the chord (or a sequence's last step)
667
- * and invokes the callback.
668
- *
669
- * @param id - The registration ID to trigger
670
- * @returns True if the registration was found and triggered
671
- */
672
- triggerRegistration(id) {
673
- const registration = this.registrations.state.get(id);
674
- if (!registration) return false;
675
- const context = this.#callbackContext(registration);
676
- const parsed = registration.kind === "chord" ? registration.parsedChord : registration.parsedSequence[registration.parsedSequence.length - 1];
677
- const syntheticEvent = new KeyboardEvent(registration.options.eventType ?? "keydown", {
678
- ...parsed.code !== void 0 ? { code: parsed.code } : { key: parsed.key },
679
- ctrlKey: parsed.ctrl,
680
- shiftKey: parsed.shift,
681
- altKey: parsed.alt,
682
- metaKey: parsed.meta,
683
- bubbles: true,
684
- cancelable: true
685
- });
686
- this.#fire(registration, syntheticEvent, context);
687
- return true;
688
- }
689
- /**
690
682
  * Gets the number of registered hotkeys, chords and sequences.
691
683
  */
692
684
  getRegistrationCount() {
@@ -710,10 +702,12 @@ var HotkeyManager = class HotkeyManager {
710
702
  * only while focus is inside that element. An app hotkey matches
711
703
  * registrations by id, so custom hotkeys apply.
712
704
  *
713
- * @param hotkey - A chord, sequence, or app hotkey
705
+ * @param hotkey - A chord, sequence, or app hotkey. No hotkey
706
+ * (`undefined`, `null`, or `''`) returns false.
714
707
  * @returns True if a registration that can fire exists
715
708
  */
716
709
  hasActiveRegistration(hotkey) {
710
+ if (!hotkey) return false;
717
711
  const id = typeof hotkey === "object" && "~appHotkey" in hotkey ? hotkey.id : void 0;
718
712
  for (const registration of this.registrations.state.values()) if (this.#isLive(registration) && targetHasFocus(registration.target) && (id === void 0 ? areHotkeysEqual(registration.hotkey, hotkey, registration.options.platform) : registration.options.appHotkeyId === id)) return true;
719
713
  return false;
@@ -1,6 +1,6 @@
1
+ import { Store } from "./_store.js";
1
2
  import { Hotkey, HotkeyChord, HotkeySequence, ParsedHotkeyChord } from "./hotkey.types.js";
2
3
  import { HotkeyConflict, HotkeyConflictOptions } from "./conflicts.js";
3
- import { Store } from "@tanstack/store";
4
4
  //#region src/hotkey-recorder.d.ts
5
5
  /** Event property used to record a binding. Physical codes are the default. */
6
6
  export type RecorderKeyMode = 'code' | 'key';
@@ -1,12 +1,11 @@
1
1
  import { detectPlatform } from "./platform.js";
2
- import { normalizeKeyboardEvent } from "./_keyboard-event.js";
2
+ import { normalizeKeyboardEvent, splitChordParts } from "./_keyboard-event.js";
3
3
  import { isModifierKey, parseHotkey, simplifyHotkey } from "./parse.js";
4
- import { validateHotkey } from "./validate.js";
5
4
  import { isTypingEvent } from "./_event-target.js";
5
+ import { createStore } from "./_store.js";
6
6
  import { beginRecording, captureRecordingEvent, endRecording } from "./_recording-guard.js";
7
7
  import { findHotkeyConflicts } from "./conflicts.js";
8
8
  import { chordRejection, hotkeyChordFromKeydown } from "./_recorder-chord.js";
9
- import { Store } from "@tanstack/store";
10
9
 
11
10
  //#region src/hotkey-recorder.ts
12
11
  const initialState = {
@@ -49,7 +48,7 @@ const initialState = {
49
48
  * ```
50
49
  */
51
50
  var HotkeyRecorder = class {
52
- store = new Store(initialState);
51
+ store = createStore(initialState);
53
52
  #keydownHandler = null;
54
53
  #optionsSource;
55
54
  #optionOverrides = {};
@@ -156,11 +155,10 @@ var HotkeyRecorder = class {
156
155
  }
157
156
  const chord = hotkeyChordFromKeydown(event, platform, options.recordBy);
158
157
  if (chord === null) return;
159
- const validation = validateHotkey(chord);
160
- if (!validation.valid) {
158
+ if (splitChordParts(chord).some((part) => !part.trim())) {
161
159
  options.onReject?.({
162
160
  reason: "invalid",
163
- message: validation.errors.join("; "),
161
+ message: "Whitespace keys cannot be recorded",
164
162
  hotkey: chord
165
163
  });
166
164
  return;
@@ -1,4 +1,4 @@
1
- import { Store } from "@tanstack/store";
1
+ import { Store } from "./_store.js";
2
2
  //#region src/key-state-tracker.d.ts
3
3
  /**
4
4
  * State interface for the KeyStateTracker.
@@ -1,5 +1,5 @@
1
1
  import { MODIFIER_KEYS, normalizeKeyName } from "./constants.js";
2
- import { Store } from "@tanstack/store";
2
+ import { createStore } from "./_store.js";
3
3
 
4
4
  //#region src/key-state-tracker.ts
5
5
  /**
@@ -51,7 +51,7 @@ var KeyStateTracker = class KeyStateTracker {
51
51
  * The TanStack Store instance containing the tracker state.
52
52
  * Use this to subscribe to state changes or access current state.
53
53
  */
54
- store = new Store(getDefaultKeyStateTrackerState());
54
+ store = createStore(getDefaultKeyStateTrackerState());
55
55
  #heldEntries = /* @__PURE__ */ new Map();
56
56
  /** Window listeners as `[type, listener, capture]`, removed together on destroy. */
57
57
  #listeners = [];
package/dist/match.js CHANGED
@@ -4,6 +4,7 @@ import { normalizeKeyboardEvent } from "./_keyboard-event.js";
4
4
  import { isHotkeySequence, isModifierKey, parseHotkey } from "./parse.js";
5
5
  import { formatHotkey } from "./format.js";
6
6
  import { matchKeyboardEvent, parsedChordsEqual } from "./_match.js";
7
+ import { chordHasKey } from "./_registration.js";
7
8
 
8
9
  //#region src/match.ts
9
10
  /**
@@ -36,7 +37,9 @@ import { matchKeyboardEvent, parsedChordsEqual } from "./_match.js";
36
37
  * ```
37
38
  */
38
39
  function matchesKeyboardEvent(event, chord, platform = detectPlatform()) {
39
- return matchKeyboardEvent(event, chord, platform).matched;
40
+ const parsed = typeof chord === "string" ? parseHotkey(chord, platform) : chord;
41
+ if (!chordHasKey(parsed)) return false;
42
+ return matchKeyboardEvent(event, parsed, platform).matched;
40
43
  }
41
44
  /**
42
45
  * Creates a keyboard event handler that calls the callback when the chord