@tanstack/hotkeys 0.11.0 → 0.12.1

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.
@@ -1,4 +1,4 @@
1
- import { Hotkey } from "./hotkey.types.js";
1
+ import { Hotkey, HotkeyChord, HotkeySequence } from "./hotkey.types.js";
2
2
  import { HotkeyRecorderValidationContext, RecorderOptions } from "./recorder-options.js";
3
3
  import { Store } from "@tanstack/store";
4
4
  //#region src/hotkey-recorder.d.ts
@@ -8,78 +8,97 @@ import { Store } from "@tanstack/store";
8
8
  export interface HotkeyRecorderState {
9
9
  /** Whether recording is currently active */
10
10
  isRecording: boolean;
11
- /** The currently recorded hotkey (for live preview) */
11
+ /** Chords recorded so far in a multi-step recording (empty for `maxSteps: 1`) */
12
+ steps: HotkeySequence;
13
+ /** The last committed hotkey: a chord, or a sequence of two or more steps */
12
14
  recordedHotkey: Hotkey | null;
13
15
  }
14
- /**
15
- * Options for configuring a HotkeyRecorder instance.
16
- */
17
- export interface HotkeyRecorderOptions extends RecorderOptions {
18
- /** Return true to accept, or false/a message to reject while staying in recording mode. */
19
- validate?: (hotkey: Hotkey, context: HotkeyRecorderValidationContext) => boolean | string;
20
- /** Callback when a hotkey is successfully recorded */
21
- onRecord: (hotkey: Hotkey) => void;
16
+ /** Which keys commit a multi-step recording before it reaches `maxSteps`. */
17
+ export type HotkeyRecorderCommitKeys = 'enter' | 'none';
18
+ interface HotkeyRecorderBaseOptions extends RecorderOptions {
22
19
  /** Optional callback when recording is cancelled (Escape pressed) */
23
20
  onCancel?: () => void;
24
- /** Optional callback when shortcut is cleared (Backspace/Delete pressed) */
21
+ /**
22
+ * Optional callback when the recording is cleared: Backspace/Delete with no
23
+ * modifiers and no recorded steps
24
+ */
25
25
  onClear?: () => void;
26
26
  /**
27
27
  * Whether to ignore keyboard events from input-like elements (text inputs,
28
- * textarea, select, contenteditable). When true, typing in inputs passes
29
- * through normally instead of being captured as a hotkey recording.
30
- * Escape always works regardless of this setting.
31
- * @default true
28
+ * textarea, select, contenteditable) while recording. When true, typing in
29
+ * inputs passes through normally (Escape still cancels). Defaults to true.
32
30
  */
33
31
  ignoreInputs?: boolean;
34
32
  }
33
+ /** Options for recording a single chord. This is the default (`maxSteps: 1`). */
34
+ export interface HotkeyChordRecorderOptions extends HotkeyRecorderBaseOptions {
35
+ /** Maximum number of chords to record. Default: 1 */
36
+ maxSteps?: 1;
37
+ /** Return true to accept, or false/a message to reject while staying in recording mode. */
38
+ validate?: (hotkey: HotkeyChord, context: HotkeyRecorderValidationContext) => boolean | string;
39
+ /** Callback when a chord is successfully recorded */
40
+ onRecord: (hotkey: HotkeyChord) => void;
41
+ }
42
+ /**
43
+ * Options for recording up to `maxSteps` chords. A one-step recording commits
44
+ * as a chord string; two or more steps commit as a sequence.
45
+ */
46
+ export interface HotkeySequenceRecorderOptions extends HotkeyRecorderBaseOptions {
47
+ /** Maximum number of chords to record. Reaching it commits automatically. Use `Infinity` for no limit. */
48
+ maxSteps: number;
49
+ /** Return true to accept, or false/a message to reject while staying in recording mode. */
50
+ validate?: (hotkey: Hotkey, context: HotkeyRecorderValidationContext) => boolean | string;
51
+ /** Callback when a hotkey is successfully recorded */
52
+ onRecord: (hotkey: Hotkey) => void;
53
+ /** Whether plain Enter commits the recorded steps. Default: 'enter' */
54
+ commitKeys?: HotkeyRecorderCommitKeys;
55
+ /** Commit automatically after this many milliseconds without a new step. Disabled when unset or 0. */
56
+ idleTimeoutMs?: number;
57
+ }
58
+ /**
59
+ * Options for HotkeyRecorder. `maxSteps` (default 1) selects between recording
60
+ * a single chord and recording a sequence.
61
+ */
62
+ export type HotkeyRecorderOptions = HotkeyChordRecorderOptions | HotkeySequenceRecorderOptions;
35
63
  /**
36
64
  * Framework-agnostic class for recording keyboard shortcuts.
37
65
  *
38
- * This class handles all the complexity of capturing keyboard events,
39
- * converting them to hotkey strings, and handling edge cases like
40
- * Escape to cancel or Backspace/Delete to clear.
66
+ * With the default `maxSteps: 1`, the first valid chord commits. With a larger
67
+ * `maxSteps`, each chord adds a step; the recording commits on Enter (unless
68
+ * `commitKeys: 'none'`), after `idleTimeoutMs`, or when it reaches `maxSteps`.
69
+ * Backspace/Delete removes the last step, and on an empty recording calls
70
+ * `onClear`. Escape cancels. A one-step result is a chord string.
41
71
  *
42
72
  * State Management:
43
73
  * - Uses TanStack Store for reactive state management
44
74
  * - State can be accessed via `recorder.store.state` when using the class directly
45
- * - When using framework adapters (React), use `useSelector` hooks for reactive state
75
+ * - When using framework adapters, use the `useHotkeyRecorder` hook for reactive state
46
76
  *
47
77
  * @example
48
78
  * ```ts
49
79
  * const recorder = new HotkeyRecorder({
50
80
  * onRecord: (hotkey) => {
51
- * console.log('Recorded:', hotkey)
52
- * },
53
- * onCancel: () => {
54
- * console.log('Recording cancelled')
81
+ * console.log('Recorded:', hotkey) // 'Mod+S'
55
82
  * },
56
83
  * })
57
84
  *
58
- * // Start recording
59
- * recorder.start()
60
- *
61
- * // Access state directly
62
- * console.log(recorder.store.state.isRecording) // true
63
- *
64
- * // Subscribe to changes with TanStack Store
65
- * const unsubscribe = recorder.store.subscribe(() => {
66
- * console.log('Recording:', recorder.store.state.isRecording)
85
+ * const sequenceRecorder = new HotkeyRecorder({
86
+ * maxSteps: 3,
87
+ * onRecord: (hotkey) => {
88
+ * console.log('Recorded:', hotkey) // 'Mod+S' or ['G', 'H']
89
+ * },
67
90
  * })
68
91
  *
69
- * // Cleanup
92
+ * recorder.start()
93
+ * // ... later
70
94
  * recorder.destroy()
71
- * unsubscribe()
72
95
  * ```
73
96
  */
74
97
  export declare class HotkeyRecorder {
75
98
  #private;
76
- /**
77
- * The TanStack Store instance containing the recorder state.
78
- * Use this to subscribe to state changes or access current state.
79
- */
80
99
  readonly store: Store<HotkeyRecorderState>;
81
- /** Options may be a getter to read current configuration during an active session. */
82
- constructor(options: HotkeyRecorderOptions | (() => HotkeyRecorderOptions));
100
+ constructor(options: HotkeyChordRecorderOptions | (() => HotkeyChordRecorderOptions));
101
+ constructor(options: HotkeySequenceRecorderOptions | (() => HotkeySequenceRecorderOptions));
83
102
  /**
84
103
  * Updates the recorder options, including callbacks.
85
104
  * This allows framework adapters to sync callback changes without recreating the recorder.
@@ -89,10 +108,15 @@ export declare class HotkeyRecorder {
89
108
  * Start recording a new hotkey.
90
109
  *
91
110
  * Sets up a keydown event listener that captures keyboard events
92
- * and converts them to hotkey strings. Recording continues until
93
- * a valid hotkey is recorded, Escape is pressed, or stop/cancel is called.
111
+ * and records chords according to `maxSteps`.
94
112
  */
95
113
  start(): void;
114
+ /**
115
+ * Commits the recorded steps after validation and conflict checks.
116
+ * Called automatically at `maxSteps`, on Enter, or after `idleTimeoutMs`.
117
+ * A rejected commit leaves recording active with its steps intact.
118
+ */
119
+ commit(): void;
96
120
  /**
97
121
  * Stop recording (same as cancel, but doesn't call onCancel).
98
122
  *
@@ -1,6 +1,6 @@
1
1
  import { detectPlatform } from "./platform.js";
2
2
  import { normalizeKeyboardEvent } from "./_keyboard-event.js";
3
- import { isModifierKey, parseHotkey } from "./parse.js";
3
+ import { isModifierKey, parseHotkey, simplifyHotkey } from "./parse.js";
4
4
  import { validateHotkey } from "./validate.js";
5
5
  import { beginRecording, captureRecordingEvent, endRecording } from "./_recording-guard.js";
6
6
  import { shouldIgnoreInputEvent } from "./_event-target.js";
@@ -9,65 +9,62 @@ import { chordRejection, hotkeyChordFromKeydown } from "./_recorder-chord.js";
9
9
  import { Store } from "@tanstack/store";
10
10
 
11
11
  //#region src/hotkey-recorder.ts
12
+ const initialState = {
13
+ isRecording: false,
14
+ steps: [],
15
+ recordedHotkey: null
16
+ };
12
17
  /**
13
18
  * Framework-agnostic class for recording keyboard shortcuts.
14
19
  *
15
- * This class handles all the complexity of capturing keyboard events,
16
- * converting them to hotkey strings, and handling edge cases like
17
- * Escape to cancel or Backspace/Delete to clear.
20
+ * With the default `maxSteps: 1`, the first valid chord commits. With a larger
21
+ * `maxSteps`, each chord adds a step; the recording commits on Enter (unless
22
+ * `commitKeys: 'none'`), after `idleTimeoutMs`, or when it reaches `maxSteps`.
23
+ * Backspace/Delete removes the last step, and on an empty recording calls
24
+ * `onClear`. Escape cancels. A one-step result is a chord string.
18
25
  *
19
26
  * State Management:
20
27
  * - Uses TanStack Store for reactive state management
21
28
  * - State can be accessed via `recorder.store.state` when using the class directly
22
- * - When using framework adapters (React), use `useSelector` hooks for reactive state
29
+ * - When using framework adapters, use the `useHotkeyRecorder` hook for reactive state
23
30
  *
24
31
  * @example
25
32
  * ```ts
26
33
  * const recorder = new HotkeyRecorder({
27
34
  * onRecord: (hotkey) => {
28
- * console.log('Recorded:', hotkey)
29
- * },
30
- * onCancel: () => {
31
- * console.log('Recording cancelled')
35
+ * console.log('Recorded:', hotkey) // 'Mod+S'
32
36
  * },
33
37
  * })
34
38
  *
35
- * // Start recording
36
- * recorder.start()
37
- *
38
- * // Access state directly
39
- * console.log(recorder.store.state.isRecording) // true
40
- *
41
- * // Subscribe to changes with TanStack Store
42
- * const unsubscribe = recorder.store.subscribe(() => {
43
- * console.log('Recording:', recorder.store.state.isRecording)
39
+ * const sequenceRecorder = new HotkeyRecorder({
40
+ * maxSteps: 3,
41
+ * onRecord: (hotkey) => {
42
+ * console.log('Recorded:', hotkey) // 'Mod+S' or ['G', 'H']
43
+ * },
44
44
  * })
45
45
  *
46
- * // Cleanup
46
+ * recorder.start()
47
+ * // ... later
47
48
  * recorder.destroy()
48
- * unsubscribe()
49
49
  * ```
50
50
  */
51
51
  var HotkeyRecorder = class {
52
- /**
53
- * The TanStack Store instance containing the recorder state.
54
- * Use this to subscribe to state changes or access current state.
55
- */
56
- store = new Store({
57
- isRecording: false,
58
- recordedHotkey: null
59
- });
52
+ store = new Store(initialState);
60
53
  #keydownHandler = null;
61
54
  #optionsSource;
62
55
  #optionOverrides = {};
56
+ #platform;
57
+ #events = [];
58
+ #idleTimer = null;
63
59
  get #options() {
64
60
  return {
65
61
  ...typeof this.#optionsSource === "function" ? this.#optionsSource() : this.#optionsSource,
66
62
  ...this.#optionOverrides
67
63
  };
68
64
  }
69
- #platform;
70
- /** Options may be a getter to read current configuration during an active session. */
65
+ get #maxSteps() {
66
+ return Math.max(1, this.#options.maxSteps ?? 1);
67
+ }
71
68
  constructor(options) {
72
69
  this.#optionsSource = options;
73
70
  this.#platform = detectPlatform();
@@ -82,19 +79,35 @@ var HotkeyRecorder = class {
82
79
  ...options
83
80
  };
84
81
  }
82
+ #clearIdleTimer() {
83
+ if (this.#idleTimer !== null) {
84
+ clearTimeout(this.#idleTimer);
85
+ this.#idleTimer = null;
86
+ }
87
+ }
88
+ #scheduleIdleTimer() {
89
+ this.#clearIdleTimer();
90
+ const ms = this.#maxSteps > 1 ? this.#options.idleTimeoutMs : void 0;
91
+ if (ms === void 0 || ms <= 0 || this.store.state.steps.length === 0) return;
92
+ this.#idleTimer = setTimeout(() => {
93
+ this.#idleTimer = null;
94
+ if (this.#keydownHandler && this.store.state.steps.length >= 1) this.commit();
95
+ }, ms);
96
+ }
85
97
  /**
86
98
  * Start recording a new hotkey.
87
99
  *
88
100
  * Sets up a keydown event listener that captures keyboard events
89
- * and converts them to hotkey strings. Recording continues until
90
- * a valid hotkey is recorded, Escape is pressed, or stop/cancel is called.
101
+ * and records chords according to `maxSteps`.
91
102
  */
92
103
  start() {
93
104
  if (this.#keydownHandler) return;
105
+ this.#clearIdleTimer();
106
+ this.#events = [];
94
107
  beginRecording(this);
95
108
  this.store.setState(() => ({
96
- isRecording: true,
97
- recordedHotkey: null
109
+ ...initialState,
110
+ isRecording: true
98
111
  }));
99
112
  const handler = (event) => {
100
113
  if (!this.#keydownHandler) return;
@@ -109,120 +122,152 @@ var HotkeyRecorder = class {
109
122
  this.cancel();
110
123
  return;
111
124
  }
112
- if (event.key === "Backspace" || event.key === "Delete") {
113
- if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
125
+ const unmodified = !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;
126
+ const multiStep = this.#maxSteps > 1;
127
+ if ((event.key === "Backspace" || event.key === "Delete") && unmodified) {
128
+ if (this.store.state.steps.length === 0) {
114
129
  this.stop();
115
130
  this.#options.onClear?.();
116
131
  return;
117
132
  }
133
+ this.#events.pop();
134
+ this.store.setState((s) => ({
135
+ ...s,
136
+ steps: s.steps.slice(0, -1)
137
+ }));
138
+ if (this.store.state.steps.length === 0) this.#clearIdleTimer();
139
+ else this.#scheduleIdleTimer();
140
+ return;
141
+ }
142
+ const options = this.#options;
143
+ if (multiStep && options.commitKeys !== "none" && event.key === "Enter" && unmodified) {
144
+ if (this.store.state.steps.length >= 1) this.commit();
145
+ return;
118
146
  }
119
147
  if (isModifierKey(event.key) || event.key === "AltGraph") return;
120
148
  const rejection = chordRejection(event, {
121
- ...this.#options,
149
+ ...options,
122
150
  platform
123
151
  });
124
152
  if (rejection) {
125
- this.#options.onReject?.(rejection);
153
+ options.onReject?.(rejection);
126
154
  return;
127
155
  }
128
- const finalHotkey = hotkeyChordFromKeydown(event, platform, this.#options.recordBy);
129
- if (finalHotkey === null) return;
130
- const validation = validateHotkey(finalHotkey);
156
+ const chord = hotkeyChordFromKeydown(event, platform, options.recordBy);
157
+ if (chord === null) return;
158
+ const validation = validateHotkey(chord);
131
159
  if (!validation.valid) {
132
- this.#options.onReject?.({
160
+ options.onReject?.({
133
161
  reason: "invalid",
134
162
  message: validation.errors.join("; "),
135
- hotkey: finalHotkey
136
- });
137
- return;
138
- }
139
- const accepted = this.#options.validate?.(finalHotkey, {
140
- event,
141
- parsedHotkey: parseHotkey(finalHotkey, platform)
142
- });
143
- if (accepted !== void 0 && accepted !== true) {
144
- this.#options.onReject?.({
145
- reason: "validation",
146
- message: typeof accepted === "string" ? accepted : "This shortcut is not allowed.",
147
- hotkey: finalHotkey
163
+ hotkey: chord
148
164
  });
149
165
  return;
150
166
  }
151
- if (this.#options.detectConflicts) {
152
- const conflicts = findHotkeyConflicts(finalHotkey, {
153
- ...typeof this.#options.detectConflicts === "object" ? this.#options.detectConflicts : {},
154
- platform,
155
- events: [event]
156
- });
157
- if (conflicts.length) {
158
- this.#options.onReject?.({
159
- reason: "conflict",
160
- message: "This shortcut conflicts with a registered binding.",
161
- hotkey: finalHotkey,
162
- conflicts
163
- });
164
- return;
165
- }
166
- }
167
- endRecording(this);
168
- const handlerToRemove = this.#keydownHandler;
169
- if (handlerToRemove) {
170
- this.#removeListener(handlerToRemove);
171
- this.#keydownHandler = null;
172
- }
173
- this.store.setState(() => ({
174
- isRecording: false,
175
- recordedHotkey: finalHotkey
167
+ this.#events.push(event);
168
+ this.store.setState((s) => ({
169
+ ...s,
170
+ steps: [...s.steps, chord]
176
171
  }));
177
- this.#options.onRecord(finalHotkey);
172
+ if (this.store.state.steps.length >= this.#maxSteps) this.commit();
173
+ else this.#scheduleIdleTimer();
178
174
  };
179
175
  this.#keydownHandler = handler;
180
176
  this.#addListener(handler);
181
177
  }
182
178
  /**
183
- * Stop recording (same as cancel, but doesn't call onCancel).
184
- *
185
- * Removes the event listener and resets the recording state.
179
+ * Commits the recorded steps after validation and conflict checks.
180
+ * Called automatically at `maxSteps`, on Enter, or after `idleTimeoutMs`.
181
+ * A rejected commit leaves recording active with its steps intact.
186
182
  */
187
- stop() {
183
+ commit() {
184
+ const steps = this.store.state.steps;
185
+ if (steps.length < 1) return;
186
+ const options = this.#options;
187
+ const platform = options.platform ?? this.#platform;
188
+ const hotkey = simplifyHotkey(steps);
189
+ const single = this.#maxSteps === 1;
190
+ this.#clearIdleTimer();
191
+ const rejectAndKeepRecording = (rejection) => {
192
+ if (single) {
193
+ this.#events = [];
194
+ this.store.setState((s) => ({
195
+ ...s,
196
+ steps: []
197
+ }));
198
+ }
199
+ options.onReject?.(rejection);
200
+ };
201
+ const parsedSequence = steps.map((step) => parseHotkey(step, platform));
202
+ const accepted = options.validate?.(hotkey, {
203
+ event: this.#events[this.#events.length - 1],
204
+ parsedHotkey: parsedSequence[parsedSequence.length - 1],
205
+ events: [...this.#events],
206
+ parsedSequence
207
+ });
208
+ if (accepted !== void 0 && accepted !== true) {
209
+ rejectAndKeepRecording({
210
+ reason: "validation",
211
+ message: typeof accepted === "string" ? accepted : `This ${single ? "shortcut" : "sequence"} is not allowed.`,
212
+ hotkey
213
+ });
214
+ return;
215
+ }
216
+ if (options.detectConflicts) {
217
+ const conflicts = findHotkeyConflicts(hotkey, {
218
+ ...typeof options.detectConflicts === "object" ? options.detectConflicts : {},
219
+ platform,
220
+ events: this.#events
221
+ });
222
+ if (conflicts.length) {
223
+ rejectAndKeepRecording({
224
+ reason: "conflict",
225
+ message: `This ${single ? "shortcut" : "sequence"} conflicts with a registered hotkey.`,
226
+ hotkey,
227
+ conflicts
228
+ });
229
+ return;
230
+ }
231
+ }
232
+ this.#finish(hotkey);
233
+ options.onRecord(hotkey);
234
+ }
235
+ /** Stops listening and stores the final state. */
236
+ #finish(recordedHotkey) {
188
237
  endRecording(this);
238
+ this.#events = [];
239
+ this.#clearIdleTimer();
189
240
  if (this.#keydownHandler) {
190
241
  this.#removeListener(this.#keydownHandler);
191
242
  this.#keydownHandler = null;
192
243
  }
193
244
  this.store.setState(() => ({
194
- isRecording: false,
195
- recordedHotkey: null
245
+ ...initialState,
246
+ recordedHotkey
196
247
  }));
197
248
  }
198
249
  /**
250
+ * Stop recording (same as cancel, but doesn't call onCancel).
251
+ *
252
+ * Removes the event listener and resets the recording state.
253
+ */
254
+ stop() {
255
+ this.#finish(null);
256
+ }
257
+ /**
199
258
  * Cancel recording without saving.
200
259
  *
201
260
  * Removes the event listener, resets the recording state, and calls
202
261
  * the onCancel callback if provided.
203
262
  */
204
263
  cancel() {
205
- endRecording(this);
206
- if (this.#keydownHandler) {
207
- this.#removeListener(this.#keydownHandler);
208
- this.#keydownHandler = null;
209
- }
210
- this.store.setState(() => ({
211
- isRecording: false,
212
- recordedHotkey: null
213
- }));
264
+ this.#finish(null);
214
265
  this.#options.onCancel?.();
215
266
  }
216
- /**
217
- * Adds the keydown event listener to the document.
218
- */
219
267
  #addListener(handler) {
220
268
  if (typeof document === "undefined") return;
221
269
  document.addEventListener("keydown", handler, true);
222
270
  }
223
- /**
224
- * Removes the keydown event listener from the document.
225
- */
226
271
  #removeListener(handler) {
227
272
  if (typeof document === "undefined") return;
228
273
  document.removeEventListener("keydown", handler, true);
@@ -234,11 +279,7 @@ var HotkeyRecorder = class {
234
279
  * all event listeners are properly removed.
235
280
  */
236
281
  destroy() {
237
- this.stop();
238
- this.store.setState(() => ({
239
- isRecording: false,
240
- recordedHotkey: null
241
- }));
282
+ this.#finish(null);
242
283
  }
243
284
  };
244
285
 
@@ -88,16 +88,16 @@ export type FourModifierHotkey = `Control+Alt+Shift+Meta+${Key}`;
88
88
  * @example
89
89
  * ```ts
90
90
  * // Cross-platform shortcuts (recommended)
91
- * const save: Hotkey = 'Mod+S' // Command+S on Mac, Ctrl+S on Windows/Linux
92
- * const saveAs: Hotkey = 'Mod+Shift+S' // Command+Shift+S on Mac, Ctrl+Shift+S elsewhere
93
- * const comment: Hotkey = 'Mod+/' // Command+/ on Mac, Ctrl+/ elsewhere
91
+ * const save: HotkeyChord = 'Mod+S' // Command+S on Mac, Ctrl+S on Windows/Linux
92
+ * const saveAs: HotkeyChord = 'Mod+Shift+S' // Command+Shift+S on Mac, Ctrl+Shift+S elsewhere
93
+ * const comment: HotkeyChord = 'Mod+/' // Command+/ on Mac, Ctrl+/ elsewhere
94
94
  *
95
95
  * // Platform-specific shortcuts
96
- * const macOnly: Hotkey = 'Meta+S' // Command+S on Mac only
97
- * const windowsOnly: Hotkey = 'Control+S' // Ctrl+S on Windows/Linux only
96
+ * const macOnly: HotkeyChord = 'Meta+S' // Command+S on Mac only
97
+ * const windowsOnly: HotkeyChord = 'Control+S' // Ctrl+S on Windows/Linux only
98
98
  * ```
99
99
  */
100
- export type Hotkey = Key | SingleModifierHotkey | TwoModifierHotkey | ThreeModifierHotkey | FourModifierHotkey;
100
+ export type HotkeyChord = Key | SingleModifierHotkey | TwoModifierHotkey | ThreeModifierHotkey | FourModifierHotkey;
101
101
  /** Resolved modifier flags shared by both parsed binding variants. */
102
102
  export interface ParsedModifiers {
103
103
  /** Whether Control is required. */
@@ -171,17 +171,31 @@ export type RawHotkey = ({
171
171
  key?: never;
172
172
  } & RawModifiers);
173
173
  /**
174
- * A hotkey that can be passed to `HotkeyManager.register()` and `useHotkey()`.
175
- * Either a type-safe string (`Hotkey`) or a raw object (`RawHotkey`).
174
+ * A sequence of chords pressed one after another (Vim-style), such as `['G', 'G']`.
175
+ *
176
+ * Steps may include modifiers; the same modifier can appear on consecutive
177
+ * steps (e.g. `['Shift+R', 'Shift+T']`). Modifier-only key events do not
178
+ * advance or reset matching. Automatic repeats and IME composition are ignored.
179
+ * A one-step sequence is treated as its single chord.
180
+ */
181
+ export type HotkeySequence = ReadonlyArray<HotkeyChord>;
182
+ /**
183
+ * A hotkey: one chord (`'Mod+S'`) or a sequence of chords (`['G', 'G']`).
184
+ */
185
+ export type Hotkey = HotkeyChord | HotkeySequence;
186
+ /** A single chord as a type-safe string (`HotkeyChord`) or a raw object (`RawHotkey`). */
187
+ export type RegisterableHotkeyChord = HotkeyChord | RawHotkey;
188
+ /**
189
+ * Anything that can be registered: a chord string, a raw chord object, or a sequence.
176
190
  */
177
- export type RegisterableHotkey = Hotkey | RawHotkey;
191
+ export type RegisterableHotkey = RegisterableHotkeyChord | HotkeySequence;
178
192
  /**
179
193
  * Context passed to hotkey callbacks along with the keyboard event.
180
194
  */
181
195
  export interface HotkeyCallbackContext {
182
- /** The original hotkey string that was registered */
196
+ /** The registered hotkey: a chord string or a sequence */
183
197
  hotkey: Hotkey;
184
- /** The parsed representation of the hotkey */
198
+ /** The parsed chord that completed the match (the last step of a sequence) */
185
199
  parsedHotkey: ParsedHotkey;
186
200
  }
187
201
  /**
@@ -200,7 +214,7 @@ export interface HotkeyCallbackContext {
200
214
  */
201
215
  export type HotkeyCallback = (event: KeyboardEvent, context: HotkeyCallbackContext) => void;
202
216
  /**
203
- * Metadata for hotkey and sequence registrations.
217
+ * Metadata for hotkey registrations, chords and sequences alike.
204
218
  * Includes `name`, `description`, and `group` by default. Extend via declaration merging:
205
219
  *
206
220
  * @example
package/dist/index.d.ts CHANGED
@@ -1,18 +1,17 @@
1
1
  import { CanonicalModifier, EditingKey, FunctionKey, IndividualKey, Key, LetterKey, LogicalKey, Modifier, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PhysicalKey, PhysicalKeyCode, PunctuationKey } from "./key.types.js";
2
- import { ConflictBehavior, FourModifierHotkey, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyMeta, ParsedHotkey, ParsedModifiers, RawHotkey, RawModifiers, RegisterableHotkey, SingleModifierHotkey, ThreeModifierHotkey, TwoModifierHotkey } from "./hotkey.types.js";
3
- import { HotkeyManager, HotkeyOptions, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
4
- import { DEFAULT_SEQUENCE_TIMEOUT, HotkeySequence, SequenceManager, SequenceOptions, SequenceRegistration, SequenceRegistrationHandle, SequenceRegistrationView, Target, createSequenceMatcher, getSequenceManager } from "./sequence-manager.js";
2
+ import { ConflictBehavior, FourModifierHotkey, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyChord, HotkeyMeta, HotkeySequence, ParsedHotkey, ParsedModifiers, RawHotkey, RawModifiers, RegisterableHotkey, RegisterableHotkeyChord, SingleModifierHotkey, ThreeModifierHotkey, TwoModifierHotkey } from "./hotkey.types.js";
3
+ import { HotkeyChordOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyManager, HotkeyOptions, HotkeyOptionsFor, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeySequenceOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, Target, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
4
+ import { AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CustomHotkeys, DefinedAppHotkeys, Register, RegisteredAppHotkeys, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey } from "./app-hotkeys.js";
5
5
  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";
6
6
  import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
7
7
  import { detectPlatform, resolveModifier } from "./platform.js";
8
- import { DisplayHotkey, FormatDisplayOptions, formatForDisplay, formatHotkey, formatHotkeySequence, formatWithLabels } from "./format.js";
9
- import { hasNonModifierKey, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey } from "./parse.js";
10
- import { ValidationResult, assertValidHotkey, checkHotkey, validateHotkey } from "./validate.js";
11
- import { CreateHotkeyHandlerOptions, KeyboardEventMatch, MultiHotkeyHandler, areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, matchesKeyboardEvent } from "./match.js";
8
+ import { DisplayHotkey, DisplayHotkeyChord, FormatDisplayOptions, formatForDisplay, formatHotkey } from "./format.js";
9
+ import { hasNonModifierKey, isHotkeySequence, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey, simplifyHotkey } from "./parse.js";
10
+ import { ValidatableHotkey, ValidationResult, assertValidHotkey, checkHotkey, validateHotkey } from "./validate.js";
11
+ import { CreateHotkeyHandlerOptions, DEFAULT_SEQUENCE_TIMEOUT, KeyboardEventMatch, MultiHotkeyHandler, areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, matchesKeyboardEvent } from "./match.js";
12
12
  import { HeldModifierOptions, matchesHeldModifiers } from "./hint.js";
13
13
  import { HotkeyConflict, HotkeyConflictOptions, findHotkeyConflicts } from "./conflicts.js";
14
14
  import { KeyStateTracker, KeyStateTrackerState, getKeyStateTracker } from "./key-state-tracker.js";
15
- import { HotkeyRecorderValidationContext, HotkeySequenceRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection } from "./recorder-options.js";
16
- import { HotkeyRecorder, HotkeyRecorderOptions, HotkeyRecorderState } from "./hotkey-recorder.js";
17
- import { HotkeySequenceRecorder, HotkeySequenceRecorderCommitKeys, HotkeySequenceRecorderOptions, HotkeySequenceRecorderState } from "./hotkey-sequence-recorder.js";
18
- export { ALL_KEYS, CanonicalModifier, ConflictBehavior, CreateHotkeyHandlerOptions, DEFAULT_SEQUENCE_TIMEOUT, DisplayHotkey, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkey, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyRecorder, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeySequence, HotkeySequenceRecorder, HotkeySequenceRecorderCommitKeys, HotkeySequenceRecorderOptions, HotkeySequenceRecorderState, HotkeySequenceRecorderValidationContext, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkey, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkey, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, RegisterableHotkey, SequenceManager, SequenceOptions, SequenceRegistration, SequenceRegistrationHandle, SequenceRegistrationView, SingleModifierHotkey, Target, ThreeModifierHotkey, TwoModifierHotkey, ValidationResult, 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
+ import { HotkeyRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection } from "./recorder-options.js";
16
+ import { HotkeyChordRecorderOptions, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeySequenceRecorderOptions } from "./hotkey-recorder.js";
17
+ export { ALL_KEYS, AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CanonicalModifier, ConflictBehavior, CreateHotkeyHandlerOptions, CustomHotkeys, DEFAULT_SEQUENCE_TIMEOUT, DefinedAppHotkeys, DisplayHotkey, DisplayHotkeyChord, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkey, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyChord, HotkeyChordOptions, HotkeyChordRecorderOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyOptionsFor, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeySequence, HotkeySequenceOptions, HotkeySequenceRecorderOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkey, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkey, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, Register, RegisterableHotkey, RegisterableHotkeyChord, RegisteredAppHotkeys, SingleModifierHotkey, Target, ThreeModifierHotkey, TwoModifierHotkey, ValidatableHotkey, ValidationResult, 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 };