@tanstack/hotkeys 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,16 +1,15 @@
1
1
  import { ALL_KEYS, EDITING_KEYS, FUNCTION_KEYS, LETTER_KEYS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, isSingleLetterKey, normalizeKeyName } from "./constants.js";
2
2
  import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
3
3
  import { detectPlatform, resolveModifier } from "./platform.js";
4
- import { hasNonModifierKey, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey } from "./parse.js";
5
- import { formatForDisplay, formatHotkey, formatHotkeySequence, formatWithLabels } from "./format.js";
4
+ import { hasNonModifierKey, isHotkeySequence, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey, simplifyHotkey } from "./parse.js";
5
+ import { formatForDisplay, formatHotkey } from "./format.js";
6
6
  import { assertValidHotkey, checkHotkey, validateHotkey } from "./validate.js";
7
- import { areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, matchesKeyboardEvent } from "./match.js";
7
+ import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, matchesKeyboardEvent } from "./match.js";
8
8
  import { matchesHeldModifiers } from "./hint.js";
9
9
  import { HotkeyManager, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
10
- import { DEFAULT_SEQUENCE_TIMEOUT, SequenceManager, createSequenceMatcher, getSequenceManager } from "./sequence-manager.js";
11
10
  import { findHotkeyConflicts } from "./conflicts.js";
11
+ import { AppHotkeysStore, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey } from "./app-hotkeys.js";
12
12
  import { KeyStateTracker, getKeyStateTracker } from "./key-state-tracker.js";
13
13
  import { HotkeyRecorder } from "./hotkey-recorder.js";
14
- import { HotkeySequenceRecorder } from "./hotkey-sequence-recorder.js";
15
14
 
16
- export { ALL_KEYS, DEFAULT_SEQUENCE_TIMEOUT, EDITING_KEYS, FUNCTION_KEYS, HotkeyManager, HotkeyRecorder, HotkeySequenceRecorder, KEY_DISPLAY_SYMBOLS, KeyStateTracker, LETTER_KEYS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, SequenceManager, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, formatHotkeySequence, formatWithLabels, getHotkeyManager, getKeyStateTracker, getSequenceManager, hasNonModifierKey, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeKeyName, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey, resolveModifier, toHotkeyRegistrationView, validateHotkey };
15
+ export { ALL_KEYS, AppHotkeysStore, DEFAULT_SEQUENCE_TIMEOUT, EDITING_KEYS, FUNCTION_KEYS, HotkeyManager, HotkeyRecorder, KEY_DISPLAY_SYMBOLS, KeyStateTracker, LETTER_KEYS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeKeyName, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, provideAppHotkeys, publishAppHotkeysStore, rawHotkeyToParsedHotkey, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
package/dist/match.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Hotkey, HotkeyCallback, ParsedHotkey, RegisterableHotkey } from "./hotkey.types.js";
1
+ import { HotkeyCallback, HotkeyChord, HotkeySequence, ParsedHotkey, RegisterableHotkey } from "./hotkey.types.js";
2
2
  //#region src/match.d.ts
3
3
  export interface KeyboardEventMatch {
4
4
  matched: boolean;
@@ -38,7 +38,7 @@ export interface KeyboardEventMatch {
38
38
  * })
39
39
  * ```
40
40
  */
41
- export declare function matchesKeyboardEvent(event: KeyboardEvent, hotkey: Hotkey | ParsedHotkey, platform?: 'mac' | 'windows' | 'linux'): boolean;
41
+ export declare function matchesKeyboardEvent(event: KeyboardEvent, hotkey: HotkeyChord | ParsedHotkey, platform?: 'mac' | 'windows' | 'linux'): boolean;
42
42
  /**
43
43
  * Options for creating a hotkey handler.
44
44
  */
@@ -68,8 +68,8 @@ export interface CreateHotkeyHandlerOptions {
68
68
  * document.addEventListener('keydown', handler)
69
69
  * ```
70
70
  */
71
- export declare function createHotkeyHandler(hotkey: Hotkey | ParsedHotkey, callback: HotkeyCallback, options?: CreateHotkeyHandlerOptions): (event: KeyboardEvent) => void;
72
- export type MultiHotkeyHandler = { [K in Hotkey]?: HotkeyCallback; };
71
+ export declare function createHotkeyHandler(hotkey: HotkeyChord | ParsedHotkey, callback: HotkeyCallback, options?: CreateHotkeyHandlerOptions): (event: KeyboardEvent) => void;
72
+ export type MultiHotkeyHandler = { [K in HotkeyChord]?: HotkeyCallback; };
73
73
  /**
74
74
  * Creates a handler that matches multiple hotkeys.
75
75
  *
@@ -90,9 +90,42 @@ export type MultiHotkeyHandler = { [K in Hotkey]?: HotkeyCallback; };
90
90
  */
91
91
  export declare function createMultiHotkeyHandler(handlers: MultiHotkeyHandler, options?: CreateHotkeyHandlerOptions): (event: KeyboardEvent) => void;
92
92
  /**
93
- * Compares normalized binding identity, including all four modifiers.
93
+ * Compares normalized hotkey identity, including all four modifiers.
94
+ * Sequences are equal when every step is equal; a one-step sequence equals its chord.
94
95
  * Aliases resolve on the supplied platform. A physical code and a logical key
95
96
  * remain distinct even when one keyboard event could match both.
96
97
  */
97
98
  export declare function areHotkeysEqual(left: RegisterableHotkey, right: RegisterableHotkey, platform?: "mac" | "windows" | "linux"): boolean;
99
+ /** Default timeout between sequence steps (in milliseconds). */
100
+ export declare const DEFAULT_SEQUENCE_TIMEOUT = 1000;
101
+ /**
102
+ * Creates a simple sequence matcher for one-off use.
103
+ *
104
+ * This is a low-level helper that does not support ignoreInputs, target,
105
+ * or other HotkeyOptions. Callers must handle input filtering themselves
106
+ * if attaching to document.
107
+ *
108
+ * @param sequence - The sequence of hotkeys to match
109
+ * @param options - Options including timeout
110
+ * @returns An object with match() and reset() methods
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * const matcher = createSequenceMatcher(['G', 'G'], { timeout: 500 })
115
+ *
116
+ * document.addEventListener('keydown', (event) => {
117
+ * if (matcher.match(event)) {
118
+ * console.log('Sequence matched!')
119
+ * }
120
+ * })
121
+ * ```
122
+ */
123
+ export declare function createSequenceMatcher(sequence: HotkeySequence, options?: {
124
+ timeout?: number;
125
+ platform?: 'mac' | 'windows' | 'linux';
126
+ }): {
127
+ match: (event: KeyboardEvent) => boolean;
128
+ reset: () => void;
129
+ getProgress: () => number;
130
+ };
98
131
  //#endregion
package/dist/match.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { normalizeKeyName } from "./constants.js";
1
2
  import { detectPlatform } from "./platform.js";
2
- import { keysEqual } from "./_keyboard-event.js";
3
- import { parseHotkey, parseRegisterableHotkey } from "./parse.js";
3
+ import { keysEqual, normalizeKeyboardEvent } from "./_keyboard-event.js";
4
+ import { isHotkeySequence, isModifierKey, parseHotkey, parseRegisterableHotkey } from "./parse.js";
4
5
  import { matchKeyboardEvent } from "./_match.js";
5
6
 
6
7
  //#region src/match.ts
@@ -126,15 +127,82 @@ function formatParsedHotkey(parsed) {
126
127
  return parts.join("+");
127
128
  }
128
129
  /**
129
- * Compares normalized binding identity, including all four modifiers.
130
+ * Compares normalized hotkey identity, including all four modifiers.
131
+ * Sequences are equal when every step is equal; a one-step sequence equals its chord.
130
132
  * Aliases resolve on the supplied platform. A physical code and a logical key
131
133
  * remain distinct even when one keyboard event could match both.
132
134
  */
133
135
  function areHotkeysEqual(left, right, platform = detectPlatform()) {
136
+ const a = isHotkeySequence(left) ? left : [left];
137
+ const b = isHotkeySequence(right) ? right : [right];
138
+ return a.length === b.length && a.every((step, index) => areChordsEqual(step, b[index], platform));
139
+ }
140
+ function areChordsEqual(left, right, platform) {
134
141
  const a = parseRegisterableHotkey(left, platform);
135
142
  const b = parseRegisterableHotkey(right, platform);
136
143
  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));
137
144
  }
145
+ /** Default timeout between sequence steps (in milliseconds). */
146
+ const DEFAULT_SEQUENCE_TIMEOUT = 1e3;
147
+ /**
148
+ * Creates a simple sequence matcher for one-off use.
149
+ *
150
+ * This is a low-level helper that does not support ignoreInputs, target,
151
+ * or other HotkeyOptions. Callers must handle input filtering themselves
152
+ * if attaching to document.
153
+ *
154
+ * @param sequence - The sequence of hotkeys to match
155
+ * @param options - Options including timeout
156
+ * @returns An object with match() and reset() methods
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * const matcher = createSequenceMatcher(['G', 'G'], { timeout: 500 })
161
+ *
162
+ * document.addEventListener('keydown', (event) => {
163
+ * if (matcher.match(event)) {
164
+ * console.log('Sequence matched!')
165
+ * }
166
+ * })
167
+ * ```
168
+ */
169
+ function createSequenceMatcher(sequence, options = {}) {
170
+ const platform = options.platform ?? detectPlatform();
171
+ const timeout = options.timeout ?? 1e3;
172
+ const parsedSequence = sequence.map((hotkey) => parseHotkey(hotkey, platform));
173
+ let currentIndex = 0;
174
+ let lastKeyTime = 0;
175
+ return {
176
+ match(event) {
177
+ if (event.type === "keydown" && event.repeat || normalizeKeyboardEvent(event, platform).isComposing || isModifierKey(normalizeKeyName(event.key))) return false;
178
+ const now = Date.now();
179
+ if (currentIndex > 0 && now - lastKeyTime > timeout) currentIndex = 0;
180
+ const expected = parsedSequence[currentIndex];
181
+ if (!expected) return false;
182
+ if (matchesKeyboardEvent(event, expected, platform)) {
183
+ lastKeyTime = now;
184
+ currentIndex++;
185
+ if (currentIndex >= parsedSequence.length) {
186
+ currentIndex = 0;
187
+ return true;
188
+ }
189
+ } else if (currentIndex > 0) {
190
+ if (matchesKeyboardEvent(event, parsedSequence[0], platform)) {
191
+ currentIndex = 1;
192
+ lastKeyTime = now;
193
+ } else currentIndex = 0;
194
+ }
195
+ return false;
196
+ },
197
+ reset() {
198
+ currentIndex = 0;
199
+ lastKeyTime = 0;
200
+ },
201
+ getProgress() {
202
+ return currentIndex;
203
+ }
204
+ };
205
+ }
138
206
 
139
207
  //#endregion
140
- export { areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, matchesKeyboardEvent };
208
+ export { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, matchesKeyboardEvent };
package/dist/parse.d.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  import { Key } from "./key.types.js";
2
+ import { Hotkey, HotkeyChord, HotkeySequence, ParsedHotkey, RawHotkey, RegisterableHotkey, RegisterableHotkeyChord } from "./hotkey.types.js";
2
3
  import { MODIFIER_ALIASES } from "./constants.js";
3
- import { Hotkey, ParsedHotkey, RawHotkey, RegisterableHotkey } from "./hotkey.types.js";
4
4
  //#region src/parse.d.ts
5
+ /**
6
+ * Whether a hotkey is a sequence. Use this instead of `Array.isArray`, which
7
+ * does not narrow readonly arrays.
8
+ */
9
+ export declare function isHotkeySequence(hotkey: RegisterableHotkey): hotkey is HotkeySequence;
10
+ /**
11
+ * Collapses a one-step sequence to its chord without otherwise changing the hotkey.
12
+ * Registration APIs treat `['Mod+S']` exactly like `'Mod+S'`.
13
+ */
14
+ export declare function simplifyHotkey<T extends RegisterableHotkey>(hotkey: T): T extends HotkeySequence ? Hotkey : T;
5
15
  /**
6
16
  * Parses a hotkey string into its component parts.
7
17
  *
@@ -16,7 +26,7 @@ import { Hotkey, ParsedHotkey, RawHotkey, RegisterableHotkey } from "./hotkey.ty
16
26
  * parseHotkey('Control+Shift+A') // { key: 'A', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] }
17
27
  * ```
18
28
  */
19
- export declare function parseHotkey(hotkey: Hotkey | (string & {}), platform?: 'mac' | 'windows' | 'linux'): ParsedHotkey;
29
+ export declare function parseHotkey(hotkey: HotkeyChord | (string & {}), platform?: 'mac' | 'windows' | 'linux'): ParsedHotkey;
20
30
  /**
21
31
  * Converts a RawHotkey object to a ParsedHotkey.
22
32
  * Optional modifier booleans default to false; modifiers array is derived from them.
@@ -59,15 +69,17 @@ export declare function rawHotkeyToParsedHotkey(raw: RawHotkey, platform?: 'mac'
59
69
  * normalizeHotkey('esc') // 'Escape'
60
70
  * ```
61
71
  */
62
- export declare function normalizeHotkey(hotkey: Key | (string & {}), platform?: 'mac' | 'windows' | 'linux'): Hotkey;
72
+ export declare function normalizeHotkey(hotkey: Key | (string & {}), platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
63
73
  /**
64
74
  * Same canonical string as {@link normalizeHotkey}, but from an already-parsed hotkey.
65
75
  */
66
- export declare function normalizeHotkeyFromParsed(parsed: ParsedHotkey, platform?: 'mac' | 'windows' | 'linux'): Hotkey;
76
+ export declare function normalizeHotkeyFromParsed(parsed: ParsedHotkey, platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
67
77
  /**
68
- * Normalizes a string or {@link RawHotkey} object to the same canonical hotkey string.
78
+ * Normalizes a chord (string or {@link RawHotkey} object) or a sequence to canonical form.
79
+ * Each sequence step is normalized, and a one-step sequence becomes its chord.
69
80
  * Use this in framework adapters instead of branching on `formatHotkey(rawHotkeyToParsedHotkey(...))`.
70
81
  */
82
+ export declare function normalizeRegisterableHotkey(hotkey: RegisterableHotkeyChord, platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
71
83
  export declare function normalizeRegisterableHotkey(hotkey: RegisterableHotkey, platform?: 'mac' | 'windows' | 'linux'): Hotkey;
72
84
  /**
73
85
  * Checks if a string is a recognized modifier token (including aliases).
@@ -104,7 +116,7 @@ export declare function parseKeyboardEvent(event: KeyboardEvent, platform?: 'mac
104
116
  * @param event - The keyboard event (typically `keydown`)
105
117
  * @param platform - Target platform for `Mod` eligibility
106
118
  */
107
- export declare function normalizeHotkeyFromEvent(event: KeyboardEvent, platform?: 'mac' | 'windows' | 'linux'): Hotkey;
119
+ export declare function normalizeHotkeyFromEvent(event: KeyboardEvent, platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
108
120
  /**
109
121
  * Checks if a hotkey or ParsedHotkey contains at least one non-modifier key.
110
122
  *
@@ -122,10 +134,10 @@ export declare function normalizeHotkeyFromEvent(event: KeyboardEvent, platform?
122
134
  * hasNonModifierKey(parseHotkey('Mod+A')) // true
123
135
  * ```
124
136
  */
125
- export declare function hasNonModifierKey(hotkey: Hotkey | ParsedHotkey | (string & {}), platform?: 'mac' | 'windows' | 'linux'): boolean;
137
+ export declare function hasNonModifierKey(hotkey: HotkeyChord | ParsedHotkey | (string & {}), platform?: 'mac' | 'windows' | 'linux'): boolean;
126
138
  /**
127
139
  * Parses a string or raw binding, resolving Mod for the supplied platform.
128
140
  * Physical tokens such as `[KeyQ]` retain `code`; they never become logical Q.
129
141
  */
130
- export declare function parseRegisterableHotkey(hotkey: RegisterableHotkey, platform?: "mac" | "windows" | "linux"): ParsedHotkey;
142
+ export declare function parseRegisterableHotkey(hotkey: RegisterableHotkeyChord, platform?: "mac" | "windows" | "linux"): ParsedHotkey;
131
143
  //#endregion
package/dist/parse.js CHANGED
@@ -4,6 +4,16 @@ import { assertLogicalKey, normalizeKeyboardEvent, splitHotkeyParts } from "./_k
4
4
 
5
5
  //#region src/parse.ts
6
6
  /**
7
+ * Whether a hotkey is a sequence. Use this instead of `Array.isArray`, which
8
+ * does not narrow readonly arrays.
9
+ */
10
+ function isHotkeySequence(hotkey) {
11
+ return Array.isArray(hotkey);
12
+ }
13
+ function simplifyHotkey(hotkey) {
14
+ return isHotkeySequence(hotkey) && hotkey.length === 1 ? hotkey[0] : hotkey;
15
+ }
16
+ /**
7
17
  * Parses a hotkey string into its component parts.
8
18
  *
9
19
  * @param hotkey - The hotkey string to parse (e.g., 'Mod+Shift+S')
@@ -138,11 +148,11 @@ function normalizeHotkey(hotkey, platform = detectPlatform()) {
138
148
  function normalizeHotkeyFromParsed(parsed, platform = detectPlatform()) {
139
149
  return normalizedHotkeyStringFromParsed(parsed, platform);
140
150
  }
141
- /**
142
- * Normalizes a string or {@link RawHotkey} object to the same canonical hotkey string.
143
- * Use this in framework adapters instead of branching on `formatHotkey(rawHotkeyToParsedHotkey(...))`.
144
- */
145
151
  function normalizeRegisterableHotkey(hotkey, platform = detectPlatform()) {
152
+ if (isHotkeySequence(hotkey)) {
153
+ const steps = hotkey.map((step) => normalizeHotkey(step, platform));
154
+ return steps.length === 1 ? steps[0] : steps;
155
+ }
146
156
  return typeof hotkey === "string" ? normalizeHotkey(hotkey, platform) : normalizeHotkeyFromParsed(rawHotkeyToParsedHotkey(hotkey, platform), platform);
147
157
  }
148
158
  /**
@@ -234,4 +244,4 @@ function parseRegisterableHotkey(hotkey, platform = detectPlatform()) {
234
244
  }
235
245
 
236
246
  //#endregion
237
- export { hasNonModifierKey, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey };
247
+ export { hasNonModifierKey, isHotkeySequence, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey, simplifyHotkey };
@@ -6,8 +6,8 @@ export type RecorderKeyMode = 'code' | 'key';
6
6
  export interface RecorderRejection {
7
7
  reason: 'missing-code' | 'alt-graph' | 'invalid' | 'conflict' | 'validation';
8
8
  message: string;
9
+ /** The rejected chord or sequence, when one was recorded. */
9
10
  hotkey?: Hotkey;
10
- sequence?: Array<Hotkey>;
11
11
  conflicts?: Array<HotkeyConflict>;
12
12
  }
13
13
  export interface RecorderOptions {
@@ -20,12 +20,15 @@ export interface RecorderOptions {
20
20
  /** Rejected candidates leave recording active. */
21
21
  onReject?: (rejection: RecorderRejection) => void;
22
22
  }
23
+ /** Validation context for a recorded chord or sequence. */
23
24
  export interface HotkeyRecorderValidationContext {
25
+ /** The event that recorded the last chord. */
24
26
  event: KeyboardEvent;
27
+ /** The last chord, parsed. */
25
28
  parsedHotkey: ParsedHotkey;
26
- }
27
- export interface HotkeySequenceRecorderValidationContext {
29
+ /** The event that recorded each step. */
28
30
  events: ReadonlyArray<KeyboardEvent>;
31
+ /** Every step, parsed. */
29
32
  parsedSequence: ReadonlyArray<ParsedHotkey>;
30
33
  }
31
34
  //#endregion
@@ -1,7 +1,10 @@
1
1
  import { Hotkey } from "./hotkey.types.js";
2
2
  //#region src/validate.d.ts
3
+ /** A hotkey to validate: a chord string, a sequence, or untyped input such as saved data. */
4
+ export type ValidatableHotkey = Hotkey | (string & {}) | ReadonlyArray<string>;
3
5
  /**
4
- * Validates a hotkey string and returns any warnings or errors.
6
+ * Validates a hotkey and returns any warnings or errors.
7
+ * Sequences are validated step by step, and messages name the failing step.
5
8
  *
6
9
  * Checks for:
7
10
  * - Valid syntax (modifier+...+key format)
@@ -18,9 +21,12 @@ import { Hotkey } from "./hotkey.types.js";
18
21
  *
19
22
  * validateHotkey('')
20
23
  * // { valid: false, warnings: [], errors: ['Hotkey cannot be empty'] }
24
+ *
25
+ * validateHotkey(['G', 'Foo+H'])
26
+ * // { valid: false, warnings: [], errors: ["Step 2: Unknown modifier: 'Foo'"] }
21
27
  * ```
22
28
  */
23
- export declare function validateHotkey(hotkey: Hotkey | (string & {})): ValidationResult;
29
+ export declare function validateHotkey(hotkey: ValidatableHotkey): ValidationResult;
24
30
  /**
25
31
  * Validates a hotkey and throws an error if invalid.
26
32
  * Useful for development-time validation.
@@ -34,7 +40,7 @@ export declare function validateHotkey(hotkey: Hotkey | (string & {})): Validati
34
40
  * assertValidHotkey('') // Throws Error: Invalid hotkey: Hotkey cannot be empty
35
41
  * ```
36
42
  */
37
- export declare function assertValidHotkey(hotkey: Hotkey | (string & {})): void;
43
+ export declare function assertValidHotkey(hotkey: ValidatableHotkey): void;
38
44
  /**
39
45
  * Validates a hotkey and logs warnings to the console.
40
46
  * Useful for development-time feedback.
@@ -49,7 +55,7 @@ export declare function assertValidHotkey(hotkey: Hotkey | (string & {})): void;
49
55
  * // Returns: true
50
56
  * ```
51
57
  */
52
- export declare function checkHotkey(hotkey: Hotkey | (string & {})): boolean;
58
+ export declare function checkHotkey(hotkey: ValidatableHotkey): boolean;
53
59
  /**
54
60
  * Result of validating a hotkey string.
55
61
  */
package/dist/validate.js CHANGED
@@ -3,7 +3,8 @@ import { splitHotkeyParts } from "./_keyboard-event.js";
3
3
 
4
4
  //#region src/validate.ts
5
5
  /**
6
- * Validates a hotkey string and returns any warnings or errors.
6
+ * Validates a hotkey and returns any warnings or errors.
7
+ * Sequences are validated step by step, and messages name the failing step.
7
8
  *
8
9
  * Checks for:
9
10
  * - Valid syntax (modifier+...+key format)
@@ -20,9 +21,30 @@ import { splitHotkeyParts } from "./_keyboard-event.js";
20
21
  *
21
22
  * validateHotkey('')
22
23
  * // { valid: false, warnings: [], errors: ['Hotkey cannot be empty'] }
24
+ *
25
+ * validateHotkey(['G', 'Foo+H'])
26
+ * // { valid: false, warnings: [], errors: ["Step 2: Unknown modifier: 'Foo'"] }
23
27
  * ```
24
28
  */
25
29
  function validateHotkey(hotkey) {
30
+ if (typeof hotkey !== "string") {
31
+ if (hotkey.length === 0) return {
32
+ valid: false,
33
+ warnings: [],
34
+ errors: ["Sequence cannot be empty"]
35
+ };
36
+ if (hotkey.length === 1) return validateChord(hotkey[0]);
37
+ const results = hotkey.map(validateChord);
38
+ const prefixed = (key) => results.flatMap((result, i) => result[key].map((message) => `Step ${i + 1}: ${message}`));
39
+ return {
40
+ valid: results.every((result) => result.valid),
41
+ warnings: prefixed("warnings"),
42
+ errors: prefixed("errors")
43
+ };
44
+ }
45
+ return validateChord(hotkey);
46
+ }
47
+ function validateChord(hotkey) {
26
48
  const warnings = [];
27
49
  const errors = [];
28
50
  if (!hotkey || hotkey.trim() === "") return {
@@ -82,7 +104,7 @@ function isKnownKey(key) {
82
104
  */
83
105
  function assertValidHotkey(hotkey) {
84
106
  const result = validateHotkey(hotkey);
85
- if (!result.valid) throw new Error(`Invalid hotkey '${hotkey}': ${result.errors.join(", ")}`);
107
+ if (!result.valid) throw new Error(`Invalid hotkey '${String(hotkey)}': ${result.errors.join(", ")}`);
86
108
  }
87
109
  /**
88
110
  * Validates a hotkey and logs warnings to the console.
@@ -100,8 +122,8 @@ function assertValidHotkey(hotkey) {
100
122
  */
101
123
  function checkHotkey(hotkey) {
102
124
  const result = validateHotkey(hotkey);
103
- if (result.errors.length > 0) console.error(`Hotkey '${hotkey}' errors:`, result.errors.join("; "));
104
- if (result.warnings.length > 0) console.warn(`Hotkey '${hotkey}' warnings:`, result.warnings.join("; "));
125
+ if (result.errors.length > 0) console.error(`Hotkey '${String(hotkey)}' errors:`, result.errors.join("; "));
126
+ if (result.warnings.length > 0) console.warn(`Hotkey '${String(hotkey)}' warnings:`, result.warnings.join("; "));
105
127
  return result.valid;
106
128
  }
107
129
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/hotkeys",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  "types": "./dist/index.d.ts",
28
28
  "exports": {
29
29
  ".": "./dist/index.js",
30
+ "./adapter": "./dist/adapter.js",
30
31
  "./package.json": "./package.json"
31
32
  },
32
33
  "sideEffects": false,
@@ -37,7 +38,7 @@
37
38
  "dist"
38
39
  ],
39
40
  "dependencies": {
40
- "@tanstack/store": "^0.11.1"
41
+ "@tanstack/store": "^0.11.2"
41
42
  },
42
43
  "scripts": {
43
44
  "clean": "premove ./build ./dist",
@@ -1,83 +0,0 @@
1
- import { HotkeySequence } from "./sequence-manager.js";
2
- import { HotkeySequenceRecorderValidationContext, RecorderOptions } from "./recorder-options.js";
3
- import { Store } from "@tanstack/store";
4
- //#region src/hotkey-sequence-recorder.d.ts
5
- /**
6
- * How the user can commit a recorded sequence from the keyboard.
7
- * - `'enter'`: plain Enter (no modifiers) commits when at least one step exists.
8
- * - `'none'`: only {@link HotkeySequenceRecorder.commit} finishes recording (or idle timeout if set).
9
- */
10
- export type HotkeySequenceRecorderCommitKeys = 'enter' | 'none';
11
- /**
12
- * State interface for the HotkeySequenceRecorder.
13
- */
14
- export interface HotkeySequenceRecorderState {
15
- /** Whether recording is currently active */
16
- isRecording: boolean;
17
- /** Chords captured so far in the current recording session */
18
- steps: HotkeySequence;
19
- /** The last successfully committed sequence, or null if none / after starting a new session */
20
- recordedSequence: HotkeySequence | null;
21
- }
22
- /**
23
- * Options for configuring a HotkeySequenceRecorder instance.
24
- */
25
- export interface HotkeySequenceRecorderOptions extends RecorderOptions {
26
- /** Validate the completed sequence. Rejection preserves the steps for editing. */
27
- validate?: (sequence: HotkeySequence, context: HotkeySequenceRecorderValidationContext) => boolean | string;
28
- /** Callback when a sequence is successfully recorded */
29
- onRecord: (sequence: HotkeySequence) => void;
30
- /** Optional callback when recording is cancelled (Escape pressed) */
31
- onCancel?: () => void;
32
- /** Optional callback when the sequence is cleared (Backspace/Delete with no steps) */
33
- onClear?: () => void;
34
- /**
35
- * Whether plain Enter commits the current steps. Ignored when {@link commitKeys} is `'none'`.
36
- * @default true
37
- */
38
- commitOnEnter?: boolean;
39
- /**
40
- * Keyboard commit mode. When `'none'`, use {@link HotkeySequenceRecorder.commit} (and optional idle timeout).
41
- * @default 'enter'
42
- */
43
- commitKeys?: HotkeySequenceRecorderCommitKeys;
44
- /**
45
- * Milliseconds of inactivity after the **last completed chord** before auto-committing.
46
- * The timer does not run while waiting for the first chord (`steps.length === 0`).
47
- */
48
- idleTimeoutMs?: number;
49
- /**
50
- * Whether to ignore keyboard events from input-like elements (text inputs,
51
- * textarea, select, contenteditable). When true, typing in inputs passes
52
- * through normally instead of being captured as a sequence recording.
53
- * Escape always works regardless of this setting.
54
- * @default true
55
- */
56
- ignoreInputs?: boolean;
57
- }
58
- /**
59
- * Framework-agnostic class for recording multi-chord sequences (Vim-style shortcuts).
60
- *
61
- * Each step is captured like a single hotkey chord. Press **Enter** (no modifiers) to commit
62
- * when {@link HotkeySequenceRecorderOptions.commitKeys} is `'enter'` (default), **Escape** to cancel,
63
- * **Backspace/Delete** to remove the last step or clear when empty.
64
- */
65
- export declare class HotkeySequenceRecorder {
66
- #private;
67
- readonly store: Store<HotkeySequenceRecorderState>;
68
- constructor(options: HotkeySequenceRecorderOptions);
69
- /** Merges current callbacks and options without discarding recorded steps. */
70
- setOptions(options: Partial<HotkeySequenceRecorderOptions>): void;
71
- /** Starts a fresh recording; repeated starts during an active session are ignored. */
72
- start(): void;
73
- /**
74
- * Commit the current steps as a sequence. No-op if fewer than one step.
75
- */
76
- commit(): void;
77
- /** Stops and discards in-progress steps without invoking onCancel. */
78
- stop(): void;
79
- /** Stops, discards in-progress steps, and notifies onCancel. */
80
- cancel(): void;
81
- destroy(): void;
82
- }
83
- //#endregion