@tanstack/hotkeys 0.5.0 → 0.6.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 (45) hide show
  1. package/README.md +1 -1
  2. package/dist/constants.cjs +70 -10
  3. package/dist/constants.cjs.map +1 -1
  4. package/dist/constants.d.cts +57 -4
  5. package/dist/constants.d.ts +57 -4
  6. package/dist/constants.js +67 -10
  7. package/dist/constants.js.map +1 -1
  8. package/dist/format.cjs +20 -125
  9. package/dist/format.cjs.map +1 -1
  10. package/dist/format.d.cts +8 -72
  11. package/dist/format.d.ts +8 -72
  12. package/dist/format.js +23 -127
  13. package/dist/format.js.map +1 -1
  14. package/dist/hotkey-manager.cjs.map +1 -1
  15. package/dist/hotkey-manager.d.cts +5 -1
  16. package/dist/hotkey-manager.d.ts +5 -1
  17. package/dist/hotkey-manager.js.map +1 -1
  18. package/dist/hotkey.d.cts +2 -0
  19. package/dist/hotkey.d.ts +2 -0
  20. package/dist/index.cjs +7 -5
  21. package/dist/index.d.cts +4 -4
  22. package/dist/index.d.ts +4 -4
  23. package/dist/index.js +4 -4
  24. package/dist/parse.cjs +53 -91
  25. package/dist/parse.cjs.map +1 -1
  26. package/dist/parse.d.cts +31 -72
  27. package/dist/parse.d.ts +31 -72
  28. package/dist/parse.js +51 -89
  29. package/dist/parse.js.map +1 -1
  30. package/dist/recorder-chord.cjs +3 -2
  31. package/dist/recorder-chord.cjs.map +1 -1
  32. package/dist/recorder-chord.js +4 -3
  33. package/dist/recorder-chord.js.map +1 -1
  34. package/dist/sequence-manager.cjs +1 -1
  35. package/dist/sequence-manager.cjs.map +1 -1
  36. package/dist/sequence-manager.js +2 -2
  37. package/dist/sequence-manager.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/constants.ts +75 -21
  40. package/src/format.ts +58 -196
  41. package/src/hotkey-manager.ts +5 -1
  42. package/src/hotkey.ts +2 -0
  43. package/src/parse.ts +91 -131
  44. package/src/recorder-chord.ts +4 -5
  45. package/src/sequence-manager.ts +2 -2
package/src/constants.ts CHANGED
@@ -8,10 +8,6 @@ import type {
8
8
  PunctuationKey,
9
9
  } from './hotkey'
10
10
 
11
- // =============================================================================
12
- // Platform Detection
13
- // =============================================================================
14
-
15
11
  /**
16
12
  * Detects the current platform based on browser navigator properties.
17
13
  *
@@ -46,10 +42,6 @@ export function detectPlatform(): 'mac' | 'windows' | 'linux' {
46
42
  return 'linux'
47
43
  }
48
44
 
49
- // =============================================================================
50
- // Modifier Aliases
51
- // =============================================================================
52
-
53
45
  /**
54
46
  * Canonical order for modifiers in normalized hotkey strings.
55
47
  *
@@ -124,6 +116,12 @@ export const MODIFIER_ALIASES: Record<string, CanonicalModifier | 'Mod'> = {
124
116
  cmd: 'Meta',
125
117
  meta: 'Meta',
126
118
 
119
+ // DOM `KeyboardEvent.key` spellings for the meta / Windows key
120
+ OS: 'Meta',
121
+ os: 'Meta',
122
+ Win: 'Meta',
123
+ win: 'Meta',
124
+
127
125
  // Platform-adaptive (resolved at runtime)
128
126
  CommandOrControl: 'Mod',
129
127
  Mod: 'Mod',
@@ -162,10 +160,6 @@ export function resolveModifier(
162
160
  return modifier
163
161
  }
164
162
 
165
- // =============================================================================
166
- // Valid Keys
167
- // =============================================================================
168
-
169
163
  /**
170
164
  * Set of all valid letter keys (A-Z).
171
165
  *
@@ -311,16 +305,16 @@ export const PUNCTUATION_KEYS = new Set<PunctuationKey>([
311
305
  * fallbacks for letters and digits.
312
306
  */
313
307
  export const PUNCTUATION_CODE_MAP: Record<string, string> = {
314
- Minus: '-',
315
- Equal: '=',
316
- Slash: '/',
308
+ Backquote: '`',
309
+ Backslash: '\\',
317
310
  BracketLeft: '[',
318
311
  BracketRight: ']',
319
- Backslash: '\\',
320
312
  Comma: ',',
313
+ Equal: '=',
314
+ Minus: '-',
321
315
  Period: '.',
322
- Backquote: '`',
323
316
  Semicolon: ';',
317
+ Slash: '/',
324
318
  }
325
319
 
326
320
  /**
@@ -446,6 +440,20 @@ export function isSingleLetterKey(key: string): boolean {
446
440
  return /^\p{Letter}$/u.test(key)
447
441
  }
448
442
 
443
+ /**
444
+ * Normalizes a key name to its canonical form.
445
+ *
446
+ * @param key - The key name to normalize (can be an alias, lowercase, etc.)
447
+ * @returns The canonical key name
448
+ *
449
+ * @example
450
+ * ```ts
451
+ * normalizeKeyName('esc') // 'Escape'
452
+ * normalizeKeyName('a') // 'A'
453
+ * normalizeKeyName('f1') // 'F1'
454
+ * normalizeKeyName('ArrowUp') // 'ArrowUp' (already canonical)
455
+ * ```
456
+ */
449
457
  export function normalizeKeyName(key: string): string {
450
458
  // Check aliases first
451
459
  if (key in KEY_ALIASES) {
@@ -488,11 +496,35 @@ export function normalizeKeyName(key: string): string {
488
496
  * MAC_MODIFIER_SYMBOLS['Shift'] // '⇧'
489
497
  * ```
490
498
  */
491
- export const MAC_MODIFIER_SYMBOLS: Record<CanonicalModifier, string> = {
499
+ export const MAC_MODIFIER_SYMBOLS: Record<CanonicalModifier | 'Mod', string> = {
492
500
  Control: '⌃',
493
501
  Alt: '⌥',
494
502
  Shift: '⇧',
495
503
  Meta: '⌘',
504
+ Mod: '⌘',
505
+ }
506
+
507
+ /**
508
+ * Modifier key labels for macOS display.
509
+ *
510
+ * Used by formatting functions to display hotkeys with macOS-style text labels
511
+ * (e.g., 'Control' for Control, 'Option' for Alt, 'Cmd' for Meta) instead of symbols.
512
+ * This provides a familiar macOS look and feel in hotkey displays.
513
+ *
514
+ * @example
515
+ * ```ts
516
+ * MAC_MODIFIER_LABELS['Control'] // 'control'
517
+ * MAC_MODIFIER_LABELS['Alt'] // 'option'
518
+ * MAC_MODIFIER_LABELS['Shift'] // 'shift'
519
+ * MAC_MODIFIER_LABELS['Meta'] // 'cmd'
520
+ * ```
521
+ */
522
+ export const MAC_MODIFIER_LABELS: Record<CanonicalModifier | 'Mod', string> = {
523
+ Control: 'Control',
524
+ Alt: 'Option',
525
+ Shift: 'Shift',
526
+ Meta: 'Cmd',
527
+ Mod: 'Cmd',
496
528
  }
497
529
 
498
530
  /**
@@ -510,13 +542,35 @@ export const MAC_MODIFIER_SYMBOLS: Record<CanonicalModifier, string> = {
510
542
  * STANDARD_MODIFIER_LABELS['Shift'] // 'Shift'
511
543
  * ```
512
544
  */
513
- export const STANDARD_MODIFIER_LABELS: Record<CanonicalModifier, string> = {
545
+ export const WINDOWS_MODIFIER_LABELS: Record<
546
+ CanonicalModifier | 'Mod',
547
+ string
548
+ > = {
514
549
  Control: 'Ctrl',
515
550
  Alt: 'Alt',
516
551
  Shift: 'Shift',
517
552
  Meta: 'Win',
553
+ Mod: 'Ctrl',
518
554
  }
519
555
 
556
+ export const LINUX_MODIFIER_LABELS: Record<CanonicalModifier | 'Mod', string> =
557
+ {
558
+ ...WINDOWS_MODIFIER_LABELS,
559
+ Meta: 'Super',
560
+ }
561
+
562
+ export const PUNCTUATION_KEY_DISPLAY_LABELS = {
563
+ '`': 'Backquote',
564
+ '\\': 'Backslash',
565
+ '[': 'Left Bracket',
566
+ ']': 'Right Bracket',
567
+ ',': 'Comma',
568
+ '=': 'Equal',
569
+ '-': 'Minus',
570
+ '.': 'Period',
571
+ ';': 'Semicolon',
572
+ } as const satisfies Record<string, string>
573
+
520
574
  /**
521
575
  * Special key symbols for display formatting.
522
576
  *
@@ -532,7 +586,7 @@ export const STANDARD_MODIFIER_LABELS: Record<CanonicalModifier, string> = {
532
586
  * KEY_DISPLAY_SYMBOLS['Space'] // '␣'
533
587
  * ```
534
588
  */
535
- export const KEY_DISPLAY_SYMBOLS: Record<string, string> = {
589
+ export const KEY_DISPLAY_SYMBOLS = {
536
590
  ArrowUp: '↑',
537
591
  ArrowDown: '↓',
538
592
  ArrowLeft: '←',
@@ -543,4 +597,4 @@ export const KEY_DISPLAY_SYMBOLS: Record<string, string> = {
543
597
  Delete: '⌦',
544
598
  Tab: '⇥',
545
599
  Space: '␣',
546
- }
600
+ } as const satisfies Record<string, string>
package/src/format.ts CHANGED
@@ -1,12 +1,26 @@
1
1
  import {
2
2
  KEY_DISPLAY_SYMBOLS,
3
+ LINUX_MODIFIER_LABELS,
4
+ MAC_MODIFIER_LABELS,
3
5
  MAC_MODIFIER_SYMBOLS,
6
+ MODIFIER_ALIASES,
4
7
  MODIFIER_ORDER,
5
- STANDARD_MODIFIER_LABELS,
8
+ PUNCTUATION_KEY_DISPLAY_LABELS,
9
+ WINDOWS_MODIFIER_LABELS,
6
10
  detectPlatform,
7
11
  } from './constants'
8
- import { parseHotkey } from './parse'
9
- import type { FormatDisplayOptions, Hotkey, ParsedHotkey } from './hotkey'
12
+ import {
13
+ isModifierKey,
14
+ normalizeHotkeyFromParsed,
15
+ normalizeRegisterableHotkey,
16
+ } from './parse'
17
+ import type {
18
+ CanonicalModifier,
19
+ FormatDisplayOptions,
20
+ Hotkey,
21
+ ParsedHotkey,
22
+ RegisterableHotkey,
23
+ } from './hotkey'
10
24
 
11
25
  /**
12
26
  * Converts a hotkey sequence array to a display string.
@@ -55,8 +69,8 @@ export function formatHotkey(parsed: ParsedHotkey): string {
55
69
  /**
56
70
  * Formats a hotkey for display in a user interface.
57
71
  *
58
- * On macOS, uses symbols (⌘⇧S).
59
- * On Windows/Linux, uses text (Ctrl+Shift+S).
72
+ * On macOS, uses symbols (⌘⇧S) in the same modifier order as {@link normalizeHotkeyFromParsed}.
73
+ * On Windows/Linux, uses text (Ctrl+Shift+S) with `+` separators.
60
74
  *
61
75
  * @param hotkey - The hotkey string or ParsedHotkey to format
62
76
  * @param options - Formatting options
@@ -65,7 +79,7 @@ export function formatHotkey(parsed: ParsedHotkey): string {
65
79
  * @example
66
80
  * ```ts
67
81
  * formatForDisplay('Mod+Shift+S', { platform: 'mac' })
68
- * // Returns: '⇧⌘S'
82
+ * // Returns: '⌘ ⇧ S' (symbols separated by spaces on macOS)
69
83
  *
70
84
  * formatForDisplay('Mod+Shift+S', { platform: 'windows' })
71
85
  * // Returns: 'Ctrl+Shift+S'
@@ -75,203 +89,51 @@ export function formatHotkey(parsed: ParsedHotkey): string {
75
89
  * ```
76
90
  */
77
91
  export function formatForDisplay(
78
- hotkey: Hotkey | (string & {}) | ParsedHotkey,
92
+ hotkey: RegisterableHotkey | (string & {}),
79
93
  options: FormatDisplayOptions = {},
80
94
  ): string {
81
95
  const platform = options.platform ?? detectPlatform()
82
- const parsed =
83
- typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey
84
-
85
- if (platform === 'mac') {
86
- return formatForMac(parsed)
87
- }
88
-
89
- return formatForStandard(parsed)
90
- }
91
-
92
- /**
93
- * Formats a hotkey for macOS display using symbols.
94
- */
95
- function formatForMac(parsed: ParsedHotkey): string {
96
- const parts: Array<string> = []
97
-
98
- // Add modifiers in macOS order (typically Control, Option, Shift, Command)
99
- // But we'll use our canonical order and just use symbols
100
- for (const modifier of MODIFIER_ORDER) {
101
- if (parsed.modifiers.includes(modifier)) {
102
- parts.push(MAC_MODIFIER_SYMBOLS[modifier])
103
- }
104
- }
105
-
106
- // Add the key (use symbol if available, otherwise the key itself)
107
- const keyDisplay = KEY_DISPLAY_SYMBOLS[parsed.key] ?? parsed.key
108
- parts.push(keyDisplay)
109
-
110
- // On Mac, modifiers are typically concatenated without separators
111
- return parts.join('')
112
- }
113
-
114
- /**
115
- * Formats a hotkey for Windows/Linux display using text labels.
116
- */
117
- function formatForStandard(parsed: ParsedHotkey): string {
118
- const parts: Array<string> = []
119
-
120
- // Add modifiers in canonical order
121
- for (const modifier of MODIFIER_ORDER) {
122
- if (parsed.modifiers.includes(modifier)) {
123
- parts.push(STANDARD_MODIFIER_LABELS[modifier])
124
- }
125
- }
126
-
127
- // Add the key (use symbol/short form if available)
128
- const keyDisplay = KEY_DISPLAY_SYMBOLS[parsed.key] ?? parsed.key
129
- parts.push(keyDisplay)
130
-
131
- // On Windows/Linux, use + as separator
132
- return parts.join('+')
96
+ const useSymbols = options.useSymbols ?? true
97
+ const normalizedHotkey = normalizeRegisterableHotkey(
98
+ hotkey as RegisterableHotkey,
99
+ platform,
100
+ )
101
+ return normalizedHotkey
102
+ .split('+')
103
+ .map((segment) => {
104
+ if (isModifierKey(segment)) {
105
+ const modifierToken = (MODIFIER_ALIASES[segment] ??
106
+ MODIFIER_ALIASES[segment.toLowerCase()]) as CanonicalModifier | 'Mod'
107
+ return platform === 'mac'
108
+ ? useSymbols
109
+ ? MAC_MODIFIER_SYMBOLS[modifierToken]
110
+ : MAC_MODIFIER_LABELS[modifierToken]
111
+ : platform === 'windows'
112
+ ? WINDOWS_MODIFIER_LABELS[modifierToken]
113
+ : LINUX_MODIFIER_LABELS[modifierToken]
114
+ } else {
115
+ const keyDisplaySymbol =
116
+ useSymbols &&
117
+ KEY_DISPLAY_SYMBOLS[segment as keyof typeof KEY_DISPLAY_SYMBOLS]
118
+ if (keyDisplaySymbol) return keyDisplaySymbol
119
+
120
+ const punctuationKeyDisplayLabel =
121
+ !useSymbols &&
122
+ PUNCTUATION_KEY_DISPLAY_LABELS[
123
+ segment as keyof typeof PUNCTUATION_KEY_DISPLAY_LABELS
124
+ ]
125
+ return punctuationKeyDisplayLabel || segment
126
+ }
127
+ })
128
+ .join(platform === 'mac' && useSymbols ? ' ' : '+')
133
129
  }
134
130
 
135
131
  /**
136
- * Formats a hotkey using platform-agnostic labels.
137
- * Uses 'Cmd' on Mac and 'Ctrl' for Control, etc.
138
- *
139
- * @param hotkey - The hotkey string or ParsedHotkey to format
140
- * @param platform - The target platform
141
- * @returns A formatted string with platform-appropriate labels
132
+ * @deprecated Use {@link formatForDisplay} instead with `useSymbols: false` option.
142
133
  */
143
134
  export function formatWithLabels(
144
- hotkey: Hotkey | (string & {}),
145
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
146
- ): string {
147
- const parsed =
148
- typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey
149
- const parts: Array<string> = []
150
-
151
- // Custom labels for more readable output
152
- const labels: Record<string, string> = {
153
- Control: 'Ctrl',
154
- Alt: platform === 'mac' ? 'Option' : 'Alt',
155
- Shift: 'Shift',
156
- Meta: platform === 'mac' ? 'Cmd' : 'Win',
157
- }
158
-
159
- for (const modifier of MODIFIER_ORDER) {
160
- if (parsed.modifiers.includes(modifier)) {
161
- parts.push(labels[modifier] ?? modifier)
162
- }
163
- }
164
-
165
- // Add the key
166
- parts.push(parsed.key)
167
-
168
- return parts.join('+')
169
- }
170
-
171
- // =============================================================================
172
- // Debugging Display Labels
173
- // =============================================================================
174
-
175
- /**
176
- * Maps canonical modifier names to debugging-friendly labels per platform.
177
- */
178
- const MODIFIER_DEBUG_LABELS: Record<string, Record<string, string>> = {
179
- mac: { Meta: 'Mod (Cmd)', Control: 'Ctrl', Alt: 'Opt', Shift: 'Shift' },
180
- windows: { Control: 'Mod (Ctrl)', Meta: 'Win', Alt: 'Alt', Shift: 'Shift' },
181
- linux: { Control: 'Mod (Ctrl)', Meta: 'Super', Alt: 'Alt', Shift: 'Shift' },
182
- }
183
-
184
- /**
185
- * Options for formatting a single key for debugging display.
186
- */
187
- export interface FormatKeyDebuggingOptions {
188
- /** The target platform. Defaults to auto-detection. */
189
- platform?: 'mac' | 'windows' | 'linux'
190
- /**
191
- * Whether the input value comes from `event.key` or `event.code`.
192
- *
193
- * - `'key'` (default): Applies rich platform-aware formatting (modifier
194
- * labels, special-key symbols, etc.).
195
- * - `'code'`: Returns the value unchanged — physical key codes like
196
- * `"MetaLeft"` or `"KeyA"` are already descriptive for debugging.
197
- */
198
- source?: 'key' | 'code'
199
- }
200
-
201
- /**
202
- * Formats a single key name for debugging/devtools display.
203
- *
204
- * Unlike `formatForDisplay` which formats full hotkey strings for end-user UIs,
205
- * this function formats individual key names (from `event.key`) with rich
206
- * platform-aware labels suitable for debugging tools and developer-facing displays.
207
- *
208
- * Features:
209
- * - Modifier keys show their platform role (e.g., "Mod (Cmd)" for Meta on Mac)
210
- * - On macOS, modifier keys are prefixed with their symbol (e.g., "⌘ Mod (Cmd)")
211
- * - Special keys use display symbols (ArrowUp -> "↑", Escape -> "Esc")
212
- * - Regular keys pass through unchanged
213
- *
214
- * @param key - A single key name (e.g., "Meta", "Shift", "ArrowUp", "A")
215
- * @param options - Formatting options
216
- * @returns A formatted label suitable for debugging display
217
- *
218
- * @example
219
- * ```ts
220
- * // On macOS:
221
- * formatKeyForDebuggingDisplay('Meta') // '⌘ Mod (Cmd)'
222
- * formatKeyForDebuggingDisplay('Control') // '⌃ Ctrl'
223
- * formatKeyForDebuggingDisplay('Alt') // '⌥ Opt'
224
- * formatKeyForDebuggingDisplay('Shift') // '⇧ Shift'
225
- *
226
- * // On Windows:
227
- * formatKeyForDebuggingDisplay('Control') // 'Mod (Ctrl)'
228
- * formatKeyForDebuggingDisplay('Meta') // 'Win'
229
- *
230
- * // Special keys (all platforms):
231
- * formatKeyForDebuggingDisplay('ArrowUp') // '↑'
232
- * formatKeyForDebuggingDisplay('Escape') // 'Esc'
233
- * formatKeyForDebuggingDisplay('Space') // '␣'
234
- *
235
- * // Regular keys pass through:
236
- * formatKeyForDebuggingDisplay('A') // 'A'
237
- *
238
- * // With source: 'code', values pass through unchanged:
239
- * formatKeyForDebuggingDisplay('MetaLeft', { source: 'code' }) // 'MetaLeft'
240
- * formatKeyForDebuggingDisplay('KeyA', { source: 'code' }) // 'KeyA'
241
- * ```
242
- */
243
- export function formatKeyForDebuggingDisplay(
244
- key: string,
245
- options: FormatKeyDebuggingOptions = {},
135
+ hotkey: RegisterableHotkey,
136
+ options: Omit<FormatDisplayOptions, 'useSymbols'> = {},
246
137
  ): string {
247
- // For event.code values, pass through unchanged — they're already
248
- // descriptive for debugging (e.g. "MetaLeft", "KeyA", "ShiftRight").
249
- if (options.source === 'code') {
250
- return key
251
- }
252
-
253
- const platform = options.platform ?? detectPlatform()
254
-
255
- // Check if it's a modifier key
256
- const modLabel = MODIFIER_DEBUG_LABELS[platform]?.[key]
257
- if (modLabel) {
258
- // On Mac, prefix modifier labels with their symbol
259
- if (platform === 'mac') {
260
- const symbol =
261
- MAC_MODIFIER_SYMBOLS[key as keyof typeof MAC_MODIFIER_SYMBOLS]
262
- if (symbol) {
263
- return `${symbol} ${modLabel}`
264
- }
265
- }
266
- return modLabel
267
- }
268
-
269
- // Check if it's a special key with a display symbol
270
- const symbol = KEY_DISPLAY_SYMBOLS[key]
271
- if (symbol) {
272
- return symbol
273
- }
274
-
275
- // Regular key — pass through
276
- return key
138
+ return formatForDisplay(hotkey, { ...options, useSymbols: false })
277
139
  }
@@ -27,7 +27,11 @@ export type { ConflictBehavior }
27
27
  export interface HotkeyOptions {
28
28
  /** Behavior when this hotkey conflicts with an existing registration on the same target. Defaults to 'warn' */
29
29
  conflictBehavior?: ConflictBehavior
30
- /** Whether the hotkey is enabled. Defaults to true */
30
+ /**
31
+ * Soft-disable: when `false`, the callback does not run but the registration
32
+ * stays in `HotkeyManager` (and in devtools). Toggling this should update the
33
+ * existing handle via `setOptions` rather than unregistering. Defaults to `true`.
34
+ */
31
35
  enabled?: boolean
32
36
  /** The event type to listen for. Defaults to 'keydown' */
33
37
  eventType?: 'keydown' | 'keyup'
package/src/hotkey.ts CHANGED
@@ -368,6 +368,8 @@ export type RegisterableHotkey = Hotkey | RawHotkey
368
368
  export interface FormatDisplayOptions {
369
369
  /** The target platform. Defaults to auto-detection. */
370
370
  platform?: 'mac' | 'windows' | 'linux'
371
+ /** Whether to use symbols for the display. Defaults to true. */
372
+ useSymbols?: boolean
371
373
  }
372
374
 
373
375
  /**