@tanstack/hotkeys 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/dist/_event-target.js +1 -2
  2. package/dist/_keyboard-event.js +1 -2
  3. package/dist/_match.js +1 -2
  4. package/dist/_named-keys.d.ts +3 -5
  5. package/dist/_named-keys.js +1 -2
  6. package/dist/_recorder-chord.js +1 -2
  7. package/dist/_recording-guard.js +1 -2
  8. package/dist/_registration.js +1 -2
  9. package/dist/conflicts.d.ts +4 -7
  10. package/dist/conflicts.js +1 -2
  11. package/dist/constants.d.ts +14 -17
  12. package/dist/constants.js +8 -9
  13. package/dist/display-labels.d.ts +7 -10
  14. package/dist/display-labels.js +1 -2
  15. package/dist/format.d.ts +9 -12
  16. package/dist/format.js +9 -7
  17. package/dist/hint.d.ts +3 -6
  18. package/dist/hint.js +1 -2
  19. package/dist/hotkey-manager.d.ts +8 -10
  20. package/dist/hotkey-manager.js +21 -3
  21. package/dist/hotkey-recorder.d.ts +4 -7
  22. package/dist/hotkey-recorder.js +9 -6
  23. package/dist/hotkey-sequence-recorder.d.ts +5 -8
  24. package/dist/hotkey-sequence-recorder.js +6 -7
  25. package/dist/hotkey.types.d.ts +15 -18
  26. package/dist/key-state-tracker.d.ts +4 -7
  27. package/dist/key-state-tracker.js +6 -3
  28. package/dist/key.types.d.ts +17 -20
  29. package/dist/match.d.ts +8 -11
  30. package/dist/match.js +1 -2
  31. package/dist/parse.d.ts +11 -14
  32. package/dist/parse.js +5 -4
  33. package/dist/platform.d.ts +3 -6
  34. package/dist/platform.js +1 -2
  35. package/dist/recorder-options.d.ts +6 -9
  36. package/dist/sequence-manager.d.ts +11 -14
  37. package/dist/sequence-manager.js +12 -7
  38. package/dist/validate.d.ts +5 -8
  39. package/dist/validate.js +1 -2
  40. package/package.json +8 -13
  41. package/dist/_event-target.cjs +0 -73
  42. package/dist/_event-target.cjs.map +0 -1
  43. package/dist/_event-target.js.map +0 -1
  44. package/dist/_keyboard-event.cjs +0 -83
  45. package/dist/_keyboard-event.cjs.map +0 -1
  46. package/dist/_keyboard-event.js.map +0 -1
  47. package/dist/_match.cjs +0 -49
  48. package/dist/_match.cjs.map +0 -1
  49. package/dist/_match.js.map +0 -1
  50. package/dist/_named-keys.cjs +0 -68
  51. package/dist/_named-keys.cjs.map +0 -1
  52. package/dist/_named-keys.d.cts +0 -8
  53. package/dist/_named-keys.js.map +0 -1
  54. package/dist/_recorder-chord.cjs +0 -44
  55. package/dist/_recorder-chord.cjs.map +0 -1
  56. package/dist/_recorder-chord.js.map +0 -1
  57. package/dist/_recording-guard.cjs +0 -59
  58. package/dist/_recording-guard.cjs.map +0 -1
  59. package/dist/_recording-guard.js.map +0 -1
  60. package/dist/_registration.cjs +0 -54
  61. package/dist/_registration.cjs.map +0 -1
  62. package/dist/_registration.js.map +0 -1
  63. package/dist/conflicts.cjs +0 -55
  64. package/dist/conflicts.cjs.map +0 -1
  65. package/dist/conflicts.d.cts +0 -39
  66. package/dist/conflicts.js.map +0 -1
  67. package/dist/constants.cjs +0 -410
  68. package/dist/constants.cjs.map +0 -1
  69. package/dist/constants.d.cts +0 -165
  70. package/dist/constants.js.map +0 -1
  71. package/dist/display-labels.cjs +0 -119
  72. package/dist/display-labels.cjs.map +0 -1
  73. package/dist/display-labels.d.cts +0 -93
  74. package/dist/display-labels.js.map +0 -1
  75. package/dist/format.cjs +0 -85
  76. package/dist/format.cjs.map +0 -1
  77. package/dist/format.d.cts +0 -99
  78. package/dist/format.js.map +0 -1
  79. package/dist/hint.cjs +0 -24
  80. package/dist/hint.cjs.map +0 -1
  81. package/dist/hint.d.cts +0 -23
  82. package/dist/hint.js.map +0 -1
  83. package/dist/hotkey-manager.cjs +0 -414
  84. package/dist/hotkey-manager.cjs.map +0 -1
  85. package/dist/hotkey-manager.d.cts +0 -234
  86. package/dist/hotkey-manager.js.map +0 -1
  87. package/dist/hotkey-recorder.cjs +0 -235
  88. package/dist/hotkey-recorder.cjs.map +0 -1
  89. package/dist/hotkey-recorder.d.cts +0 -119
  90. package/dist/hotkey-recorder.js.map +0 -1
  91. package/dist/hotkey-sequence-recorder.cjs +0 -255
  92. package/dist/hotkey-sequence-recorder.cjs.map +0 -1
  93. package/dist/hotkey-sequence-recorder.d.cts +0 -86
  94. package/dist/hotkey-sequence-recorder.js.map +0 -1
  95. package/dist/hotkey.types.d.cts +0 -236
  96. package/dist/index.cjs +0 -71
  97. package/dist/index.d.cts +0 -18
  98. package/dist/key-state-tracker.cjs +0 -197
  99. package/dist/key-state-tracker.cjs.map +0 -1
  100. package/dist/key-state-tracker.d.cts +0 -107
  101. package/dist/key-state-tracker.js.map +0 -1
  102. package/dist/key.types.d.cts +0 -82
  103. package/dist/match.cjs +0 -144
  104. package/dist/match.cjs.map +0 -1
  105. package/dist/match.d.cts +0 -101
  106. package/dist/match.js.map +0 -1
  107. package/dist/parse.cjs +0 -245
  108. package/dist/parse.cjs.map +0 -1
  109. package/dist/parse.d.cts +0 -134
  110. package/dist/parse.js.map +0 -1
  111. package/dist/platform.cjs +0 -55
  112. package/dist/platform.cjs.map +0 -1
  113. package/dist/platform.d.cts +0 -44
  114. package/dist/platform.js.map +0 -1
  115. package/dist/recorder-options.d.cts +0 -34
  116. package/dist/sequence-manager.cjs +0 -441
  117. package/dist/sequence-manager.cjs.map +0 -1
  118. package/dist/sequence-manager.d.cts +0 -172
  119. package/dist/sequence-manager.js.map +0 -1
  120. package/dist/validate.cjs +0 -112
  121. package/dist/validate.cjs.map +0 -1
  122. package/dist/validate.d.cts +0 -67
  123. package/dist/validate.js.map +0 -1
  124. package/src/_event-target.ts +0 -157
  125. package/src/_keyboard-event.ts +0 -101
  126. package/src/_match.ts +0 -96
  127. package/src/_named-keys.ts +0 -62
  128. package/src/_recorder-chord.ts +0 -76
  129. package/src/_recording-guard.ts +0 -63
  130. package/src/_registration.ts +0 -79
  131. package/src/conflicts.ts +0 -125
  132. package/src/constants.ts +0 -455
  133. package/src/display-labels.ts +0 -123
  134. package/src/format.ts +0 -201
  135. package/src/hint.ts +0 -42
  136. package/src/hotkey-manager.ts +0 -814
  137. package/src/hotkey-recorder.ts +0 -348
  138. package/src/hotkey-sequence-recorder.ts +0 -410
  139. package/src/hotkey.types.ts +0 -269
  140. package/src/index.ts +0 -17
  141. package/src/key-state-tracker.ts +0 -261
  142. package/src/key.types.ts +0 -268
  143. package/src/match.ts +0 -210
  144. package/src/parse.ts +0 -369
  145. package/src/platform.ts +0 -66
  146. package/src/recorder-options.ts +0 -34
  147. package/src/sequence-manager.ts +0 -769
  148. package/src/validate.ts +0 -179
@@ -1,410 +0,0 @@
1
- import { Store } from '@tanstack/store'
2
- import { findHotkeyConflicts } from './conflicts'
3
- import {
4
- beginRecording,
5
- captureRecordingEvent,
6
- endRecording,
7
- } from './_recording-guard'
8
- import { chordRejection, hotkeyChordFromKeydown } from './_recorder-chord'
9
- import { normalizeKeyboardEvent } from './_keyboard-event'
10
- import { isModifierKey, parseHotkey } from './parse'
11
- import { validateHotkey } from './validate'
12
- import { detectPlatform } from './platform'
13
- import { shouldIgnoreInputEvent } from './_event-target'
14
- import type {
15
- HotkeySequenceRecorderValidationContext,
16
- RecorderOptions,
17
- } from './recorder-options'
18
- import type { HotkeySequence } from './sequence-manager'
19
-
20
- /**
21
- * How the user can commit a recorded sequence from the keyboard.
22
- * - `'enter'`: plain Enter (no modifiers) commits when at least one step exists.
23
- * - `'none'`: only {@link HotkeySequenceRecorder.commit} finishes recording (or idle timeout if set).
24
- */
25
- export type HotkeySequenceRecorderCommitKeys = 'enter' | 'none'
26
-
27
- /**
28
- * State interface for the HotkeySequenceRecorder.
29
- */
30
- export interface HotkeySequenceRecorderState {
31
- /** Whether recording is currently active */
32
- isRecording: boolean
33
- /** Chords captured so far in the current recording session */
34
- steps: HotkeySequence
35
- /** The last successfully committed sequence, or null if none / after starting a new session */
36
- recordedSequence: HotkeySequence | null
37
- }
38
-
39
- /**
40
- * Options for configuring a HotkeySequenceRecorder instance.
41
- */
42
- export interface HotkeySequenceRecorderOptions extends RecorderOptions {
43
- /** Validate the completed sequence. Rejection preserves the steps for editing. */
44
- validate?: (
45
- sequence: HotkeySequence,
46
- context: HotkeySequenceRecorderValidationContext,
47
- ) => boolean | string
48
- /** Callback when a sequence is successfully recorded */
49
- onRecord: (sequence: HotkeySequence) => void
50
- /** Optional callback when recording is cancelled (Escape pressed) */
51
- onCancel?: () => void
52
- /** Optional callback when the sequence is cleared (Backspace/Delete with no steps) */
53
- onClear?: () => void
54
- /**
55
- * Whether plain Enter commits the current steps. Ignored when {@link commitKeys} is `'none'`.
56
- * @default true
57
- */
58
- commitOnEnter?: boolean
59
- /**
60
- * Keyboard commit mode. When `'none'`, use {@link HotkeySequenceRecorder.commit} (and optional idle timeout).
61
- * @default 'enter'
62
- */
63
- commitKeys?: HotkeySequenceRecorderCommitKeys
64
- /**
65
- * Milliseconds of inactivity after the **last completed chord** before auto-committing.
66
- * The timer does not run while waiting for the first chord (`steps.length === 0`).
67
- */
68
- idleTimeoutMs?: number
69
- /**
70
- * Whether to ignore keyboard events from input-like elements (text inputs,
71
- * textarea, select, contenteditable). When true, typing in inputs passes
72
- * through normally instead of being captured as a sequence recording.
73
- * Escape always works regardless of this setting.
74
- * @default true
75
- */
76
- ignoreInputs?: boolean
77
- }
78
-
79
- const defaultHotkeySequenceRecorderOptions: Pick<
80
- HotkeySequenceRecorderOptions,
81
- 'commitOnEnter' | 'commitKeys'
82
- > = {
83
- commitOnEnter: true,
84
- commitKeys: 'enter',
85
- }
86
-
87
- /** Resolves the Enter policy, with commitKeys: none overriding the legacy boolean. */
88
- function resolvedCommitOnEnter(
89
- options: HotkeySequenceRecorderOptions,
90
- ): boolean {
91
- if (options.commitKeys === 'none') {
92
- return false
93
- }
94
- return options.commitOnEnter !== false
95
- }
96
-
97
- /**
98
- * Framework-agnostic class for recording multi-chord sequences (Vim-style shortcuts).
99
- *
100
- * Each step is captured like a single hotkey chord. Press **Enter** (no modifiers) to commit
101
- * when {@link HotkeySequenceRecorderOptions.commitKeys} is `'enter'` (default), **Escape** to cancel,
102
- * **Backspace/Delete** to remove the last step or clear when empty.
103
- */
104
- export class HotkeySequenceRecorder {
105
- readonly store: Store<HotkeySequenceRecorderState> =
106
- new Store<HotkeySequenceRecorderState>({
107
- isRecording: false,
108
- steps: [],
109
- recordedSequence: null,
110
- })
111
-
112
- #keydownHandler: ((event: KeyboardEvent) => void) | null = null
113
- #options: HotkeySequenceRecorderOptions
114
- #platform: 'mac' | 'windows' | 'linux'
115
- #events: Array<KeyboardEvent> = []
116
- #idleTimer: ReturnType<typeof setTimeout> | null = null
117
-
118
- constructor(options: HotkeySequenceRecorderOptions) {
119
- this.#options = {
120
- ...defaultHotkeySequenceRecorderOptions,
121
- ...options,
122
- }
123
- this.#platform = detectPlatform()
124
- }
125
-
126
- /** Merges current callbacks and options without discarding recorded steps. */
127
- setOptions(options: Partial<HotkeySequenceRecorderOptions>): void {
128
- this.#options = {
129
- ...defaultHotkeySequenceRecorderOptions,
130
- ...this.#options,
131
- ...options,
132
- }
133
- }
134
-
135
- /** Cancels a pending idle commit so it cannot fire after editing or stopping. */
136
- #clearIdleTimer(): void {
137
- if (this.#idleTimer !== null) {
138
- clearTimeout(this.#idleTimer)
139
- this.#idleTimer = null
140
- }
141
- }
142
-
143
- /** Restarts optional idle commit after a step is recorded or edited. */
144
- #scheduleIdleTimer(): void {
145
- this.#clearIdleTimer()
146
- const ms = this.#options.idleTimeoutMs
147
- if (ms === undefined || ms <= 0) {
148
- return
149
- }
150
- const steps = this.store.state.steps
151
- if (steps.length === 0) {
152
- return
153
- }
154
- this.#idleTimer = setTimeout(() => {
155
- this.#idleTimer = null
156
- if (!this.#keydownHandler) {
157
- return
158
- }
159
- if (this.store.state.steps.length >= 1) {
160
- this.commit()
161
- }
162
- }, ms)
163
- }
164
-
165
- /** Publishes edited steps while preserving the remaining recorder state. */
166
- #patchSteps(updater: (prev: HotkeySequence) => HotkeySequence): void {
167
- this.store.setState((s) => ({
168
- ...s,
169
- steps: updater(s.steps),
170
- }))
171
- }
172
-
173
- /** Starts a fresh recording; repeated starts during an active session are ignored. */
174
- start(): void {
175
- if (this.#keydownHandler) {
176
- return
177
- }
178
-
179
- this.#clearIdleTimer()
180
- this.#events = []
181
- beginRecording(this)
182
- this.store.setState(() => ({
183
- isRecording: true,
184
- steps: [],
185
- recordedSequence: null,
186
- }))
187
-
188
- const handler = (event: KeyboardEvent) => {
189
- if (!this.#keydownHandler) {
190
- return
191
- }
192
-
193
- // If ignoreInputs is enabled (default) and focus is in an input element,
194
- // let the event pass through so the user can type normally.
195
- // Escape is the exception — it should always cancel recording.
196
- if (
197
- this.#options.ignoreInputs !== false &&
198
- event.key !== 'Escape' &&
199
- shouldIgnoreInputEvent(event, document, document)
200
- )
201
- return
202
-
203
- const platform = this.#options.platform ?? this.#platform
204
- if (normalizeKeyboardEvent(event, platform).isComposing) return
205
- captureRecordingEvent(event)
206
- event.preventDefault()
207
- event.stopPropagation()
208
- if (event.repeat) return
209
-
210
- if (event.key === 'Escape') {
211
- this.cancel()
212
- return
213
- }
214
-
215
- if (event.key === 'Backspace' || event.key === 'Delete') {
216
- if (
217
- !event.ctrlKey &&
218
- !event.shiftKey &&
219
- !event.altKey &&
220
- !event.metaKey
221
- ) {
222
- const steps = this.store.state.steps
223
- if (steps.length === 0) {
224
- this.stop()
225
- this.#options.onClear?.()
226
- return
227
- }
228
- this.#events.pop()
229
- this.#patchSteps((prev) => prev.slice(0, -1))
230
- const next = this.store.state.steps
231
- if (next.length === 0) {
232
- this.#clearIdleTimer()
233
- } else {
234
- this.#scheduleIdleTimer()
235
- }
236
- return
237
- }
238
- }
239
-
240
- const enterCommits =
241
- resolvedCommitOnEnter(this.#options) &&
242
- event.key === 'Enter' &&
243
- !event.ctrlKey &&
244
- !event.shiftKey &&
245
- !event.altKey &&
246
- !event.metaKey
247
-
248
- if (enterCommits && this.store.state.steps.length >= 1) {
249
- this.commit()
250
- return
251
- }
252
-
253
- if (enterCommits && this.store.state.steps.length === 0) {
254
- return
255
- }
256
-
257
- if (isModifierKey(event.key) || event.key === 'AltGraph') return
258
- const rejection = chordRejection(event, { ...this.#options, platform })
259
- if (rejection) {
260
- this.#options.onReject?.(rejection)
261
- return
262
- }
263
- const finalHotkey = hotkeyChordFromKeydown(
264
- event,
265
- platform,
266
- this.#options.recordBy,
267
- )
268
- if (finalHotkey === null) {
269
- return
270
- }
271
-
272
- const validation = validateHotkey(finalHotkey)
273
- if (!validation.valid) {
274
- this.#options.onReject?.({
275
- reason: 'invalid',
276
- message: validation.errors.join('; '),
277
- hotkey: finalHotkey,
278
- })
279
- return
280
- }
281
- this.#events.push(event)
282
- this.#patchSteps((prev) => [...prev, finalHotkey])
283
- this.#scheduleIdleTimer()
284
- }
285
-
286
- this.#keydownHandler = handler
287
- this.#addListener(handler)
288
- }
289
-
290
- /**
291
- * Commit the current steps as a sequence. No-op if fewer than one step.
292
- */
293
- commit(): void {
294
- const steps = this.store.state.steps
295
- if (steps.length < 1) {
296
- return
297
- }
298
-
299
- const sequence = [...steps]
300
- const platform = this.#options.platform ?? this.#platform
301
- this.#clearIdleTimer()
302
- const accepted = this.#options.validate?.(sequence, {
303
- events: [...this.#events],
304
- parsedSequence: sequence.map((step) => parseHotkey(step, platform)),
305
- })
306
- if (accepted !== undefined && accepted !== true) {
307
- this.#options.onReject?.({
308
- reason: 'validation',
309
- message:
310
- typeof accepted === 'string'
311
- ? accepted
312
- : 'This sequence is not allowed.',
313
- sequence,
314
- })
315
- return
316
- }
317
- if (this.#options.detectConflicts) {
318
- const conflicts = findHotkeyConflicts(sequence, {
319
- ...(typeof this.#options.detectConflicts === 'object'
320
- ? this.#options.detectConflicts
321
- : {}),
322
- platform,
323
- events: this.#events,
324
- })
325
- if (conflicts.length) {
326
- this.#options.onReject?.({
327
- reason: 'conflict',
328
- message: 'This sequence conflicts with a registered binding.',
329
- sequence,
330
- conflicts,
331
- })
332
- return
333
- }
334
- }
335
- endRecording(this)
336
- this.#events = []
337
-
338
- if (this.#keydownHandler) {
339
- this.#removeListener(this.#keydownHandler)
340
- this.#keydownHandler = null
341
- }
342
- this.#clearIdleTimer()
343
-
344
- this.store.setState(() => ({
345
- isRecording: false,
346
- steps: [],
347
- recordedSequence: sequence,
348
- }))
349
-
350
- this.#options.onRecord(sequence)
351
- }
352
-
353
- /** Stops and discards in-progress steps without invoking onCancel. */
354
- stop(): void {
355
- endRecording(this)
356
- this.#events = []
357
- if (this.#keydownHandler) {
358
- this.#removeListener(this.#keydownHandler)
359
- this.#keydownHandler = null
360
- }
361
- this.#clearIdleTimer()
362
- this.store.setState(() => ({
363
- isRecording: false,
364
- steps: [],
365
- recordedSequence: null,
366
- }))
367
- }
368
-
369
- /** Stops, discards in-progress steps, and notifies onCancel. */
370
- cancel(): void {
371
- endRecording(this)
372
- this.#events = []
373
- if (this.#keydownHandler) {
374
- this.#removeListener(this.#keydownHandler)
375
- this.#keydownHandler = null
376
- }
377
- this.#clearIdleTimer()
378
- this.store.setState(() => ({
379
- isRecording: false,
380
- steps: [],
381
- recordedSequence: null,
382
- }))
383
- this.#options.onCancel?.()
384
- }
385
-
386
- /** Captures keydowns before application handlers, with an SSR-safe document check. */
387
- #addListener(handler: (event: KeyboardEvent) => void): void {
388
- if (typeof document === 'undefined') {
389
- return
390
- }
391
- document.addEventListener('keydown', handler, true)
392
- }
393
-
394
- /** Detaches the capture listener when recording ends. */
395
- #removeListener(handler: (event: KeyboardEvent) => void): void {
396
- if (typeof document === 'undefined') {
397
- return
398
- }
399
- document.removeEventListener('keydown', handler, true)
400
- }
401
-
402
- destroy(): void {
403
- this.stop()
404
- this.store.setState(() => ({
405
- isRecording: false,
406
- steps: [],
407
- recordedSequence: null,
408
- }))
409
- }
410
- }
@@ -1,269 +0,0 @@
1
- import type { CanonicalModifier, Key, LogicalKey } from './key.types'
2
-
3
- // =============================================================================
4
- // Hotkey Types
5
- // =============================================================================
6
-
7
- /**
8
- * Single modifier + key combinations.
9
- * Uses canonical modifiers (4) + Mod (1) = 5 modifiers.
10
- *
11
- * The `Mod` modifier is platform-adaptive:
12
- * - **macOS**: Resolves to `Meta` (Command key ⌘)
13
- * - **Windows/Linux**: Resolves to `Control` (Ctrl key)
14
- *
15
- * This enables cross-platform hotkey definitions that automatically adapt to the platform.
16
- * For example, `Mod+S` becomes `Command+S` on Mac and `Ctrl+S` on Windows/Linux.
17
- */
18
- export type SingleModifierHotkey =
19
- | `Control+${Key}`
20
- | `Alt+${Key}`
21
- | `Shift+${Key}`
22
- | `Meta+${Key}`
23
- | `Mod+${Key}`
24
-
25
- /**
26
- * Two modifier + key combinations.
27
- *
28
- * **Platform-adaptive `Mod` combinations:**
29
- * - `Mod+Alt` and `Mod+Shift` are included (safe on all platforms)
30
- * - `Mod+Control` and `Mod+Meta` are excluded because they create duplicate modifiers:
31
- * - `Mod+Control` duplicates `Control` on Windows/Linux (Mod = Control)
32
- * - `Mod+Meta` duplicates `Meta` on macOS (Mod = Meta)
33
- */
34
- export type TwoModifierHotkey =
35
- | `Control+Alt+${Key}`
36
- | `Control+Shift+${Key}`
37
- | `Control+Meta+${Key}`
38
- | `Alt+Shift+${Key}`
39
- | `Alt+Meta+${Key}`
40
- | `Shift+Meta+${Key}`
41
- | `Mod+Alt+${Key}`
42
- | `Mod+Shift+${Key}`
43
-
44
- /**
45
- * Three modifier + key combinations.
46
- *
47
- * **Platform-adaptive `Mod` combinations:**
48
- * - `Mod+Alt+Shift` is included (safe on all platforms)
49
- * - `Mod+Control+Shift` and `Mod+Shift+Meta` are excluded because they create duplicate modifiers:
50
- * - `Mod+Control+Shift` duplicates `Control` on Windows/Linux (Mod = Control)
51
- * - `Mod+Shift+Meta` duplicates `Meta` on macOS (Mod = Meta)
52
- */
53
- export type ThreeModifierHotkey =
54
- | `Control+Alt+Shift+${Key}`
55
- | `Control+Alt+Meta+${Key}`
56
- | `Control+Shift+Meta+${Key}`
57
- | `Alt+Shift+Meta+${Key}`
58
- | `Mod+Alt+Shift+${Key}`
59
-
60
- /**
61
- * Four modifier + key combinations.
62
- *
63
- * Only the canonical `Control+Alt+Shift+Meta` combination is included.
64
- *
65
- * **Why no `Mod` combinations?**
66
- * Since `Mod` resolves to either `Control` (Windows/Linux) or `Meta` (macOS), any
67
- * four-modifier combination with `Mod` would create duplicate modifiers on one platform.
68
- * For example:
69
- * - `Mod+Control+Alt+Shift` → duplicates `Control` on Windows/Linux
70
- * - `Mod+Alt+Shift+Meta` → duplicates `Meta` on macOS
71
- */
72
- export type FourModifierHotkey = `Control+Alt+Shift+Meta+${Key}`
73
-
74
- /**
75
- * A type-safe hotkey string.
76
- *
77
- * Provides autocomplete for:
78
- * - All single keys (letters, numbers, function keys, navigation, editing, punctuation)
79
- * - Single modifier + common key (Control+S, Mod+A, Mod+/, etc.)
80
- * - Two modifiers + common key (Mod+Shift+S, Control+Alt+A, etc.)
81
- * - Three modifiers + common key (Control+Alt+Shift+A, Mod+Alt+Shift+S, etc.)
82
- * - Four modifiers + common key (Control+Alt+Shift+Meta+A, etc.)
83
- *
84
- * ## Modifier Names
85
- *
86
- * Use canonical modifier names:
87
- * - `Control` (not Ctrl) - The Control key
88
- * - `Alt` (not Option) - The Alt key (Option on macOS)
89
- * - `Meta` (not Command/Cmd) - The Meta/Command key (macOS only)
90
- * - `Shift` - The Shift key
91
- *
92
- * ## Platform-Adaptive `Mod` Modifier
93
- *
94
- * The `Mod` modifier is a special platform-adaptive modifier that automatically resolves
95
- * to the "primary modifier" on each platform:
96
- *
97
- * - **macOS**: `Mod` → `Meta` (Command key ⌘)
98
- * - **Windows/Linux**: `Mod` → `Control` (Ctrl key)
99
- *
100
- * This enables cross-platform hotkey definitions that work correctly on all platforms
101
- * without platform-specific code. The `Mod` modifier is resolved at runtime based on
102
- * the detected platform.
103
- *
104
- * **When to use `Mod` vs platform-specific modifiers:**
105
- * - Use `Mod` for cross-platform shortcuts (e.g., `Mod+S` for save)
106
- * - Use `Meta` or `Control` when you need platform-specific behavior
107
- * - Use `Mod` when you want your shortcuts to follow platform conventions automatically
108
- *
109
- * **Limitations:**
110
- * - `Mod+Control` and `Mod+Meta` combinations are not allowed (they create duplicate
111
- * modifiers on one platform)
112
- * - In four-modifier combinations, only canonical modifiers are allowed (no `Mod`)
113
- *
114
- * @example
115
- * ```ts
116
- * // Cross-platform shortcuts (recommended)
117
- * const save: Hotkey = 'Mod+S' // Command+S on Mac, Ctrl+S on Windows/Linux
118
- * const saveAs: Hotkey = 'Mod+Shift+S' // Command+Shift+S on Mac, Ctrl+Shift+S elsewhere
119
- * const comment: Hotkey = 'Mod+/' // Command+/ on Mac, Ctrl+/ elsewhere
120
- *
121
- * // Platform-specific shortcuts
122
- * const macOnly: Hotkey = 'Meta+S' // Command+S on Mac only
123
- * const windowsOnly: Hotkey = 'Control+S' // Ctrl+S on Windows/Linux only
124
- * ```
125
- */
126
- export type Hotkey =
127
- | Key
128
- | SingleModifierHotkey
129
- | TwoModifierHotkey
130
- | ThreeModifierHotkey
131
- | FourModifierHotkey
132
-
133
- /** Resolved modifier flags shared by both parsed binding variants. */
134
- export interface ParsedModifiers {
135
- /** Whether Control is required. */
136
- ctrl: boolean
137
- /** Whether Shift is required. */
138
- shift: boolean
139
- /** Whether Alt is required. */
140
- alt: boolean
141
- /** Whether Meta (Command) is required. */
142
- meta: boolean
143
- /** Required modifiers in canonical order. */
144
- modifiers: Array<CanonicalModifier>
145
- }
146
-
147
- /**
148
- * A parsed binding matches either a logical key or a physical code, never both.
149
- * Check `code !== undefined` to narrow to a physical binding. Its logical
150
- * character is unknown without a keyboard event and is not stored in `key`.
151
- *
152
- * @example
153
- * parseHotkey('Alt+S') // { key: 'S', alt: true, ... }
154
- * parseHotkey('Alt+[KeyS]') // { code: 'KeyS', alt: true, ... } — no key property
155
- */
156
- export type ParsedHotkey = ParsedModifiers &
157
- (
158
- | { key: LogicalKey | (string & {}); code?: never }
159
- | { code: string; key?: never }
160
- )
161
-
162
- /**
163
- * A raw hotkey object for programmatic registration.
164
- *
165
- * Like `ParsedHotkey` but without `modifiers` (derived from booleans)
166
- * and with optional modifier booleans (default to `false` when omitted).
167
- * Use with `HotkeyManager.register()` and `useHotkey()` when you prefer
168
- * object form over a string.
169
- *
170
- * The `mod` modifier is platform-adaptive: Command on macOS, Control on Windows/Linux.
171
- * Pass `platform` when converting to ParsedHotkey (e.g., via `options.platform`).
172
- *
173
- * @example
174
- * ```ts
175
- * useHotkey({ key: 'S', mod: true }, handler) // Mod+S (cross-platform)
176
- * useHotkey({ key: 'S', ctrl: true }, handler) // Control+S
177
- * useHotkey({ key: 'Escape' }, handler) // Escape (no modifiers)
178
- * useHotkey({ key: 'A', shift: true, meta: true }, handler) // Shift+Meta+A
179
- * useHotkey({ key: 'S', mod: true, shift: true }, handler) // Mod+Shift+S
180
- * useHotkey({ code: 'KeyW', mod: true }, handler) // Physical Mod+KeyW
181
- * useHotkey({ code: 'NumpadAdd', mod: true }, handler) // Physical numpad plus
182
- * ```
183
- */
184
- /** Modifier flags shared by logical-key and physical-code registrations. */
185
- export interface RawModifiers {
186
- /** The non-modifier key (e.g., 'S', 'Escape', 'F1'). */
187
- /** Platform-adaptive modifier: Command on macOS, Control on Windows/Linux. Defaults to false. */
188
- mod?: boolean
189
- /** Whether the Control key is required. Defaults to false. */
190
- ctrl?: boolean
191
- /** Whether the Shift key is required. Defaults to false. */
192
- shift?: boolean
193
- /** Whether the Alt key is required. Defaults to false. */
194
- alt?: boolean
195
- /** Whether the Meta (Command) key is required. Defaults to false. */
196
- meta?: boolean
197
- }
198
-
199
- export type RawHotkey =
200
- | ({ key: LogicalKey | (string & {}); code?: never } & RawModifiers)
201
- | ({ code: string; key?: never } & RawModifiers)
202
-
203
- /**
204
- * A hotkey that can be passed to `HotkeyManager.register()` and `useHotkey()`.
205
- * Either a type-safe string (`Hotkey`) or a raw object (`RawHotkey`).
206
- */
207
- export type RegisterableHotkey = Hotkey | RawHotkey
208
-
209
- /**
210
- * Context passed to hotkey callbacks along with the keyboard event.
211
- */
212
- export interface HotkeyCallbackContext {
213
- /** The original hotkey string that was registered */
214
- hotkey: Hotkey
215
- /** The parsed representation of the hotkey */
216
- parsedHotkey: ParsedHotkey
217
- }
218
-
219
- /**
220
- * Callback function type for hotkey handlers.
221
- *
222
- * @param event - The keyboard event that triggered the hotkey
223
- * @param context - Additional context including the hotkey and parsed hotkey
224
- *
225
- * @example
226
- * ```ts
227
- * const handler: HotkeyCallback = (event, { hotkey, parsedHotkey }) => {
228
- * console.log(`Hotkey ${hotkey} was pressed`)
229
- * console.log(`Modifiers:`, parsedHotkey.modifiers)
230
- * }
231
- * ```
232
- */
233
- export type HotkeyCallback = (
234
- event: KeyboardEvent,
235
- context: HotkeyCallbackContext,
236
- ) => void
237
-
238
- /**
239
- * Metadata for hotkey and sequence registrations.
240
- * Includes `name`, `description`, and `group` by default. Extend via declaration merging:
241
- *
242
- * @example
243
- * ```ts
244
- * declare module '@tanstack/hotkeys' {
245
- * interface HotkeyMeta {
246
- * category?: string
247
- * icon?: string
248
- * }
249
- * }
250
- * ```
251
- */
252
- export interface HotkeyMeta {
253
- /** Human-readable name for this hotkey (e.g., "Save Document") */
254
- name?: string
255
- /** Description of what this hotkey does */
256
- description?: string
257
- /** Optional display group. Does not affect matching or scope. */
258
- group?: string
259
- }
260
-
261
- /**
262
- * Behavior when registering a hotkey/sequence that conflicts with an existing registration.
263
- *
264
- * - `'warn'` - Log a warning to the console but allow both registrations (default)
265
- * - `'error'` - Throw an error and prevent the new registration
266
- * - `'replace'` - Unregister the existing registration and register the new one
267
- * - `'allow'` - Allow multiple registrations without warning
268
- */
269
- export type ConflictBehavior = 'warn' | 'error' | 'replace' | 'allow'
package/src/index.ts DELETED
@@ -1,17 +0,0 @@
1
- export * from './constants'
2
- export * from './display-labels'
3
- export * from './platform'
4
- export * from './key.types'
5
- export * from './hotkey.types'
6
- export * from './format'
7
- export * from './parse'
8
- export * from './validate'
9
- export * from './match'
10
- export * from './hint'
11
- export * from './conflicts'
12
- export * from './hotkey-manager'
13
- export * from './sequence-manager'
14
- export * from './key-state-tracker'
15
- export * from './recorder-options'
16
- export * from './hotkey-recorder'
17
- export * from './hotkey-sequence-recorder'