@tanstack/hotkeys 0.7.1 → 0.9.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 (158) hide show
  1. package/README.md +15 -1
  2. package/dist/{manager.utils.js → _event-target.cjs} +5 -41
  3. package/dist/_event-target.cjs.map +1 -0
  4. package/dist/{manager.utils.cjs → _event-target.js} +3 -47
  5. package/dist/_event-target.js.map +1 -0
  6. package/dist/_keyboard-event.cjs +83 -0
  7. package/dist/_keyboard-event.cjs.map +1 -0
  8. package/dist/_keyboard-event.js +76 -0
  9. package/dist/_keyboard-event.js.map +1 -0
  10. package/dist/_match.cjs +49 -0
  11. package/dist/_match.cjs.map +1 -0
  12. package/dist/_match.js +49 -0
  13. package/dist/_match.js.map +1 -0
  14. package/dist/_named-keys.cjs +68 -0
  15. package/dist/_named-keys.cjs.map +1 -0
  16. package/dist/_named-keys.d.cts +8 -0
  17. package/dist/_named-keys.d.ts +8 -0
  18. package/dist/_named-keys.js +66 -0
  19. package/dist/_named-keys.js.map +1 -0
  20. package/dist/_recorder-chord.cjs +44 -0
  21. package/dist/_recorder-chord.cjs.map +1 -0
  22. package/dist/_recorder-chord.js +43 -0
  23. package/dist/_recorder-chord.js.map +1 -0
  24. package/dist/_recording-guard.cjs +59 -0
  25. package/dist/_recording-guard.cjs.map +1 -0
  26. package/dist/_recording-guard.js +56 -0
  27. package/dist/_recording-guard.js.map +1 -0
  28. package/dist/_registration.cjs +54 -0
  29. package/dist/_registration.cjs.map +1 -0
  30. package/dist/_registration.js +50 -0
  31. package/dist/_registration.js.map +1 -0
  32. package/dist/conflicts.cjs +55 -0
  33. package/dist/conflicts.cjs.map +1 -0
  34. package/dist/conflicts.d.cts +39 -0
  35. package/dist/conflicts.d.ts +39 -0
  36. package/dist/conflicts.js +55 -0
  37. package/dist/conflicts.js.map +1 -0
  38. package/dist/constants.cjs +46 -171
  39. package/dist/constants.cjs.map +1 -1
  40. package/dist/constants.d.cts +4 -129
  41. package/dist/constants.d.ts +4 -129
  42. package/dist/constants.js +48 -164
  43. package/dist/constants.js.map +1 -1
  44. package/dist/display-labels.cjs +119 -0
  45. package/dist/display-labels.cjs.map +1 -0
  46. package/dist/display-labels.d.cts +93 -0
  47. package/dist/display-labels.d.ts +93 -0
  48. package/dist/display-labels.js +113 -0
  49. package/dist/display-labels.js.map +1 -0
  50. package/dist/format.cjs +30 -36
  51. package/dist/format.cjs.map +1 -1
  52. package/dist/format.d.cts +46 -7
  53. package/dist/format.d.ts +46 -7
  54. package/dist/format.js +31 -37
  55. package/dist/format.js.map +1 -1
  56. package/dist/hint.cjs +24 -0
  57. package/dist/hint.cjs.map +1 -0
  58. package/dist/hint.d.cts +23 -0
  59. package/dist/hint.d.ts +23 -0
  60. package/dist/hint.js +24 -0
  61. package/dist/hint.js.map +1 -0
  62. package/dist/hotkey-manager.cjs +53 -22
  63. package/dist/hotkey-manager.cjs.map +1 -1
  64. package/dist/hotkey-manager.d.cts +6 -2
  65. package/dist/hotkey-manager.d.ts +6 -2
  66. package/dist/hotkey-manager.js +49 -18
  67. package/dist/hotkey-manager.js.map +1 -1
  68. package/dist/hotkey-recorder.cjs +67 -11
  69. package/dist/hotkey-recorder.cjs.map +1 -1
  70. package/dist/hotkey-recorder.d.cts +6 -3
  71. package/dist/hotkey-recorder.d.ts +6 -3
  72. package/dist/hotkey-recorder.js +66 -10
  73. package/dist/hotkey-recorder.js.map +1 -1
  74. package/dist/hotkey-sequence-recorder.cjs +85 -22
  75. package/dist/hotkey-sequence-recorder.cjs.map +1 -1
  76. package/dist/hotkey-sequence-recorder.d.cts +9 -2
  77. package/dist/hotkey-sequence-recorder.d.ts +9 -2
  78. package/dist/hotkey-sequence-recorder.js +84 -21
  79. package/dist/hotkey-sequence-recorder.js.map +1 -1
  80. package/dist/{hotkey.d.ts → hotkey.types.d.cts} +55 -120
  81. package/dist/{hotkey.d.cts → hotkey.types.d.ts} +55 -120
  82. package/dist/index.cjs +18 -10
  83. package/dist/index.d.cts +14 -9
  84. package/dist/index.d.ts +14 -9
  85. package/dist/index.js +10 -6
  86. package/dist/key-state-tracker.cjs +24 -24
  87. package/dist/key-state-tracker.cjs.map +1 -1
  88. package/dist/key-state-tracker.js +24 -24
  89. package/dist/key-state-tracker.js.map +1 -1
  90. package/dist/key.types.d.cts +82 -0
  91. package/dist/key.types.d.ts +82 -0
  92. package/dist/match.cjs +21 -30
  93. package/dist/match.cjs.map +1 -1
  94. package/dist/match.d.cts +19 -3
  95. package/dist/match.d.ts +19 -3
  96. package/dist/match.js +19 -29
  97. package/dist/match.js.map +1 -1
  98. package/dist/parse.cjs +43 -26
  99. package/dist/parse.cjs.map +1 -1
  100. package/dist/parse.d.cts +10 -4
  101. package/dist/parse.d.ts +10 -4
  102. package/dist/parse.js +35 -19
  103. package/dist/parse.js.map +1 -1
  104. package/dist/platform.cjs +55 -0
  105. package/dist/platform.cjs.map +1 -0
  106. package/dist/platform.d.cts +44 -0
  107. package/dist/platform.d.ts +44 -0
  108. package/dist/platform.js +53 -0
  109. package/dist/platform.js.map +1 -0
  110. package/dist/recorder-options.d.cts +34 -0
  111. package/dist/recorder-options.d.ts +34 -0
  112. package/dist/sequence-manager.cjs +54 -28
  113. package/dist/sequence-manager.cjs.map +1 -1
  114. package/dist/sequence-manager.d.cts +18 -3
  115. package/dist/sequence-manager.d.ts +18 -3
  116. package/dist/sequence-manager.js +49 -23
  117. package/dist/sequence-manager.js.map +1 -1
  118. package/dist/validate.cjs +6 -10
  119. package/dist/validate.cjs.map +1 -1
  120. package/dist/validate.d.cts +13 -2
  121. package/dist/validate.d.ts +13 -2
  122. package/dist/validate.js +7 -11
  123. package/dist/validate.js.map +1 -1
  124. package/package.json +2 -2
  125. package/src/{manager.utils.ts → _event-target.ts} +0 -74
  126. package/src/_keyboard-event.ts +101 -0
  127. package/src/_match.ts +96 -0
  128. package/src/_named-keys.ts +62 -0
  129. package/src/_recorder-chord.ts +76 -0
  130. package/src/_recording-guard.ts +63 -0
  131. package/src/_registration.ts +79 -0
  132. package/src/conflicts.ts +125 -0
  133. package/src/constants.ts +48 -196
  134. package/src/display-labels.ts +123 -0
  135. package/src/format.ts +111 -52
  136. package/src/hint.ts +42 -0
  137. package/src/hotkey-manager.ts +95 -40
  138. package/src/hotkey-recorder.ts +92 -16
  139. package/src/hotkey-sequence-recorder.ts +110 -28
  140. package/src/{hotkey.ts → hotkey.types.ts} +57 -224
  141. package/src/index.ts +11 -5
  142. package/src/key-state-tracker.ts +36 -24
  143. package/src/key.types.ts +268 -0
  144. package/src/match.ts +41 -90
  145. package/src/parse.ts +67 -26
  146. package/src/platform.ts +66 -0
  147. package/src/recorder-options.ts +34 -0
  148. package/src/sequence-manager.ts +77 -42
  149. package/src/validate.ts +29 -21
  150. package/dist/manager.utils.cjs.map +0 -1
  151. package/dist/manager.utils.d.cts +0 -13
  152. package/dist/manager.utils.d.ts +0 -13
  153. package/dist/manager.utils.js.map +0 -1
  154. package/dist/recorder-chord.cjs +0 -18
  155. package/dist/recorder-chord.cjs.map +0 -1
  156. package/dist/recorder-chord.js +0 -18
  157. package/dist/recorder-chord.js.map +0 -1
  158. package/src/recorder-chord.ts +0 -25
package/src/constants.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { LOGICAL_ONLY_NAMED_KEYS, SHARED_NAMED_KEYS } from './_named-keys'
1
2
  import type {
2
3
  CanonicalModifier,
3
4
  EditingKey,
@@ -6,41 +7,7 @@ import type {
6
7
  NavigationKey,
7
8
  NumberKey,
8
9
  PunctuationKey,
9
- } from './hotkey'
10
-
11
- /**
12
- * Detects the current platform based on browser navigator properties.
13
- *
14
- * Used internally to resolve platform-adaptive modifiers like 'Mod' (Command on Mac,
15
- * Control elsewhere) and for platform-specific hotkey formatting.
16
- *
17
- * @returns The detected platform: 'mac', 'windows', or 'linux'
18
- * @remarks Defaults to 'linux' in SSR environments where navigator is undefined
19
- *
20
- * @example
21
- * ```ts
22
- * const platform = detectPlatform() // 'mac' | 'windows' | 'linux'
23
- * const modifier = resolveModifier('Mod', platform) // 'Meta' on Mac, 'Control' elsewhere
24
- * ```
25
- */
26
- export function detectPlatform(): 'mac' | 'windows' | 'linux' {
27
- if (typeof navigator === 'undefined') {
28
- return 'linux' // Default for SSR
29
- }
30
-
31
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
32
- const platform = navigator.platform?.toLowerCase() ?? ''
33
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
34
- const userAgent = navigator.userAgent?.toLowerCase() ?? ''
35
-
36
- if (platform.includes('mac') || userAgent.includes('mac')) {
37
- return 'mac'
38
- }
39
- if (platform.includes('win') || userAgent.includes('win')) {
40
- return 'windows'
41
- }
42
- return 'linux'
43
- }
10
+ } from './key.types'
44
11
 
45
12
  /**
46
13
  * Canonical order for modifiers in normalized hotkey strings.
@@ -129,37 +96,6 @@ export const MODIFIER_ALIASES: Record<string, CanonicalModifier | 'Mod'> = {
129
96
  mod: 'Mod',
130
97
  }
131
98
 
132
- /**
133
- * Resolves the platform-adaptive 'Mod' modifier to the appropriate canonical modifier.
134
- *
135
- * The 'Mod' token represents the "primary modifier" on each platform:
136
- * - macOS: 'Meta' (Command key ⌘)
137
- * - Windows/Linux: 'Control' (Ctrl key)
138
- *
139
- * This enables cross-platform hotkey definitions like 'Mod+S' that automatically
140
- * map to Command+S on Mac and Ctrl+S on Windows/Linux.
141
- *
142
- * @param modifier - The modifier to resolve. If 'Mod', resolves based on platform.
143
- * @param platform - The target platform. Defaults to auto-detection.
144
- * @returns The canonical modifier name ('Control', 'Shift', 'Alt', or 'Meta')
145
- *
146
- * @example
147
- * ```ts
148
- * resolveModifier('Mod', 'mac') // 'Meta'
149
- * resolveModifier('Mod', 'windows') // 'Control'
150
- * resolveModifier('Control', 'mac') // 'Control' (unchanged)
151
- * ```
152
- */
153
- export function resolveModifier(
154
- modifier: CanonicalModifier | 'Mod',
155
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
156
- ): CanonicalModifier {
157
- if (modifier === 'Mod') {
158
- return platform === 'mac' ? 'Meta' : 'Control'
159
- }
160
- return modifier
161
- }
162
-
163
99
  /**
164
100
  * Set of all valid letter keys (A-Z).
165
101
  *
@@ -216,7 +152,7 @@ export const NUMBER_KEYS = new Set<NumberKey>([
216
152
  ])
217
153
 
218
154
  /**
219
- * Set of all valid function keys (F1-F12).
155
+ * Set of all valid function keys (F1-F24).
220
156
  *
221
157
  * Function keys are commonly used for system shortcuts (e.g., F12 for DevTools,
222
158
  * Alt+F4 to close windows) and application-specific commands.
@@ -234,6 +170,18 @@ export const FUNCTION_KEYS = new Set<FunctionKey>([
234
170
  'F10',
235
171
  'F11',
236
172
  'F12',
173
+ 'F13',
174
+ 'F14',
175
+ 'F15',
176
+ 'F16',
177
+ 'F17',
178
+ 'F18',
179
+ 'F19',
180
+ 'F20',
181
+ 'F21',
182
+ 'F22',
183
+ 'F23',
184
+ 'F24',
237
185
  ])
238
186
 
239
187
  /**
@@ -293,6 +241,28 @@ export const PUNCTUATION_KEYS = new Set<PunctuationKey>([
293
241
  '.',
294
242
  ';',
295
243
  '`',
244
+ "'",
245
+ '+',
246
+ '?',
247
+ '!',
248
+ '@',
249
+ '#',
250
+ '$',
251
+ '%',
252
+ '^',
253
+ '&',
254
+ '*',
255
+ '(',
256
+ ')',
257
+ '_',
258
+ '{',
259
+ '}',
260
+ '|',
261
+ ':',
262
+ '"',
263
+ '<',
264
+ '>',
265
+ '~',
296
266
  ])
297
267
 
298
268
  /**
@@ -313,6 +283,7 @@ export const PUNCTUATION_CODE_MAP: Record<string, string> = {
313
283
  Equal: '=',
314
284
  Minus: '-',
315
285
  Period: '.',
286
+ Quote: "'",
316
287
  Semicolon: ';',
317
288
  Slash: '/',
318
289
  }
@@ -338,8 +309,15 @@ export const ALL_KEYS = new Set([
338
309
  ...NAVIGATION_KEYS,
339
310
  ...EDITING_KEYS,
340
311
  ...PUNCTUATION_KEYS,
312
+ ...SHARED_NAMED_KEYS,
313
+ ...LOGICAL_ONLY_NAMED_KEYS,
341
314
  ])
342
315
 
316
+ // Share canonical spellings with validation, including newly supported named keys.
317
+ const CANONICAL_KEY_NAMES = new Map<string, string>(
318
+ Array.from(ALL_KEYS, (key) => [key.toLowerCase(), key]),
319
+ )
320
+
343
321
  /**
344
322
  * Maps key name aliases to their canonical form.
345
323
  *
@@ -360,6 +338,8 @@ export const ALL_KEYS = new Set([
360
338
  * ```
361
339
  */
362
340
  const KEY_ALIASES: Record<string, string> = {
341
+ Plus: '+',
342
+ plus: '+',
363
343
  // Escape variants
364
344
  Esc: 'Escape',
365
345
  esc: 'Escape',
@@ -471,133 +451,5 @@ export function normalizeKeyName(key: string): string {
471
451
  return upper.length === 1 ? upper : key
472
452
  }
473
453
 
474
- // Check if it's a function key (normalize case)
475
- const upperKey = key.toUpperCase()
476
- if (/^F([1-9]|1[0-2])$/.test(upperKey)) {
477
- return upperKey
478
- }
479
-
480
- return key
454
+ return CANONICAL_KEY_NAMES.get(key.toLowerCase()) ?? key
481
455
  }
482
-
483
- // =============================================================================
484
- // Display Symbols
485
- // =============================================================================
486
-
487
- /**
488
- * Modifier key symbols for macOS display.
489
- *
490
- * Used by formatting functions to display hotkeys with macOS-style symbols
491
- * (e.g., ⌘ for Command, ⌃ for Control) instead of text labels. This provides
492
- * a native macOS look and feel in hotkey displays.
493
- *
494
- * @example
495
- * ```ts
496
- * MAC_MODIFIER_SYMBOLS['Meta'] // '⌘'
497
- * MAC_MODIFIER_SYMBOLS['Control'] // '⌃'
498
- * MAC_MODIFIER_SYMBOLS['Alt'] // '⌥'
499
- * MAC_MODIFIER_SYMBOLS['Shift'] // '⇧'
500
- * ```
501
- */
502
- export const MAC_MODIFIER_SYMBOLS: Record<CanonicalModifier | 'Mod', string> = {
503
- Control: '⌃',
504
- Alt: '⌥',
505
- Shift: '⇧',
506
- Meta: '⌘',
507
- Mod: '⌘',
508
- }
509
-
510
- /**
511
- * Modifier key labels for macOS display.
512
- *
513
- * Used by formatting functions to display hotkeys with macOS-style text labels
514
- * (e.g., 'Control' for Control, 'Option' for Alt, 'Cmd' for Meta) instead of symbols.
515
- * This provides a familiar macOS look and feel in hotkey displays.
516
- *
517
- * @example
518
- * ```ts
519
- * MAC_MODIFIER_LABELS['Control'] // 'control'
520
- * MAC_MODIFIER_LABELS['Alt'] // 'option'
521
- * MAC_MODIFIER_LABELS['Shift'] // 'shift'
522
- * MAC_MODIFIER_LABELS['Meta'] // 'cmd'
523
- * ```
524
- */
525
- export const MAC_MODIFIER_LABELS: Record<CanonicalModifier | 'Mod', string> = {
526
- Control: 'Control',
527
- Alt: 'Option',
528
- Shift: 'Shift',
529
- Meta: 'Cmd',
530
- Mod: 'Cmd',
531
- }
532
-
533
- /**
534
- * Modifier key labels for Windows/Linux display.
535
- *
536
- * Used by formatting functions to display hotkeys with standard text labels
537
- * (e.g., 'Ctrl' for Control, 'Win' for Meta/Windows key) instead of symbols.
538
- * This provides a familiar Windows/Linux look and feel in hotkey displays.
539
- *
540
- * @example
541
- * ```ts
542
- * STANDARD_MODIFIER_LABELS['Control'] // 'Ctrl'
543
- * STANDARD_MODIFIER_LABELS['Meta'] // 'Win'
544
- * STANDARD_MODIFIER_LABELS['Alt'] // 'Alt'
545
- * STANDARD_MODIFIER_LABELS['Shift'] // 'Shift'
546
- * ```
547
- */
548
- export const WINDOWS_MODIFIER_LABELS: Record<
549
- CanonicalModifier | 'Mod',
550
- string
551
- > = {
552
- Control: 'Ctrl',
553
- Alt: 'Alt',
554
- Shift: 'Shift',
555
- Meta: 'Win',
556
- Mod: 'Ctrl',
557
- }
558
-
559
- export const LINUX_MODIFIER_LABELS: Record<CanonicalModifier | 'Mod', string> =
560
- {
561
- ...WINDOWS_MODIFIER_LABELS,
562
- Meta: 'Super',
563
- }
564
-
565
- export const PUNCTUATION_KEY_DISPLAY_LABELS = {
566
- '`': 'Backquote',
567
- '\\': 'Backslash',
568
- '[': 'Left Bracket',
569
- ']': 'Right Bracket',
570
- ',': 'Comma',
571
- '=': 'Equal',
572
- '-': 'Minus',
573
- '.': 'Period',
574
- ';': 'Semicolon',
575
- } as const satisfies Record<string, string>
576
-
577
- /**
578
- * Special key symbols for display formatting.
579
- *
580
- * Maps certain keys to their visual symbols for better readability in hotkey displays.
581
- * Used by formatting functions to show symbols like ↑ for ArrowUp or ↵ for Enter
582
- * instead of text labels.
583
- *
584
- * @example
585
- * ```ts
586
- * KEY_DISPLAY_SYMBOLS['ArrowUp'] // '↑'
587
- * KEY_DISPLAY_SYMBOLS['Enter'] // '↵'
588
- * KEY_DISPLAY_SYMBOLS['Escape'] // 'Esc'
589
- * KEY_DISPLAY_SYMBOLS['Space'] // '␣'
590
- * ```
591
- */
592
- export const KEY_DISPLAY_SYMBOLS = {
593
- ArrowUp: '↑',
594
- ArrowDown: '↓',
595
- ArrowLeft: '←',
596
- ArrowRight: '→',
597
- Enter: '↵',
598
- Escape: 'Esc',
599
- Backspace: '⌫',
600
- Delete: '⌦',
601
- Tab: '⇥',
602
- Space: '␣',
603
- } as const satisfies Record<string, string>
@@ -0,0 +1,123 @@
1
+ import type { CanonicalModifier } from './key.types'
2
+
3
+ // =============================================================================
4
+ // Display Symbols
5
+ // =============================================================================
6
+
7
+ /**
8
+ * Modifier key symbols for macOS display.
9
+ *
10
+ * Used by formatting functions to display hotkeys with macOS-style symbols
11
+ * (e.g., ⌘ for Command, ⌃ for Control) instead of text labels. This provides
12
+ * a native macOS look and feel in hotkey displays.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * MAC_MODIFIER_SYMBOLS['Meta'] // '⌘'
17
+ * MAC_MODIFIER_SYMBOLS['Control'] // '⌃'
18
+ * MAC_MODIFIER_SYMBOLS['Alt'] // '⌥'
19
+ * MAC_MODIFIER_SYMBOLS['Shift'] // '⇧'
20
+ * ```
21
+ */
22
+ export const MAC_MODIFIER_SYMBOLS: Record<CanonicalModifier | 'Mod', string> = {
23
+ Control: '⌃',
24
+ Alt: '⌥',
25
+ Shift: '⇧',
26
+ Meta: '⌘',
27
+ Mod: '⌘',
28
+ }
29
+
30
+ /**
31
+ * Modifier key labels for macOS display.
32
+ *
33
+ * Used by formatting functions to display hotkeys with macOS-style text labels
34
+ * (e.g., 'Control' for Control, 'Option' for Alt, 'Cmd' for Meta) instead of symbols.
35
+ * This provides a familiar macOS look and feel in hotkey displays.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * MAC_MODIFIER_LABELS['Control'] // 'control'
40
+ * MAC_MODIFIER_LABELS['Alt'] // 'option'
41
+ * MAC_MODIFIER_LABELS['Shift'] // 'shift'
42
+ * MAC_MODIFIER_LABELS['Meta'] // 'cmd'
43
+ * ```
44
+ */
45
+ export const MAC_MODIFIER_LABELS: Record<CanonicalModifier | 'Mod', string> = {
46
+ Control: 'Control',
47
+ Alt: 'Option',
48
+ Shift: 'Shift',
49
+ Meta: 'Cmd',
50
+ Mod: 'Cmd',
51
+ }
52
+
53
+ /**
54
+ * Modifier key labels for Windows/Linux display.
55
+ *
56
+ * Used by formatting functions to display hotkeys with standard text labels
57
+ * (e.g., 'Ctrl' for Control, 'Win' for Meta/Windows key) instead of symbols.
58
+ * This provides a familiar Windows/Linux look and feel in hotkey displays.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * STANDARD_MODIFIER_LABELS['Control'] // 'Ctrl'
63
+ * STANDARD_MODIFIER_LABELS['Meta'] // 'Win'
64
+ * STANDARD_MODIFIER_LABELS['Alt'] // 'Alt'
65
+ * STANDARD_MODIFIER_LABELS['Shift'] // 'Shift'
66
+ * ```
67
+ */
68
+ export const WINDOWS_MODIFIER_LABELS: Record<
69
+ CanonicalModifier | 'Mod',
70
+ string
71
+ > = {
72
+ Control: 'Ctrl',
73
+ Alt: 'Alt',
74
+ Shift: 'Shift',
75
+ Meta: 'Win',
76
+ Mod: 'Ctrl',
77
+ }
78
+
79
+ export const LINUX_MODIFIER_LABELS: Record<CanonicalModifier | 'Mod', string> =
80
+ {
81
+ ...WINDOWS_MODIFIER_LABELS,
82
+ Meta: 'Super',
83
+ }
84
+
85
+ export const PUNCTUATION_KEY_DISPLAY_LABELS = {
86
+ '`': 'Backquote',
87
+ '\\': 'Backslash',
88
+ '[': 'Left Bracket',
89
+ ']': 'Right Bracket',
90
+ ',': 'Comma',
91
+ '=': 'Equal',
92
+ '-': 'Minus',
93
+ '.': 'Period',
94
+ ';': 'Semicolon',
95
+ } as const satisfies Record<string, string>
96
+
97
+ /**
98
+ * Special key symbols for display formatting.
99
+ *
100
+ * Maps certain keys to their visual symbols for better readability in hotkey displays.
101
+ * Used by formatting functions to show symbols like ↑ for ArrowUp or ↵ for Enter
102
+ * instead of text labels.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * KEY_DISPLAY_SYMBOLS['ArrowUp'] // '↑'
107
+ * KEY_DISPLAY_SYMBOLS['Enter'] // '↵'
108
+ * KEY_DISPLAY_SYMBOLS['Escape'] // 'Esc'
109
+ * KEY_DISPLAY_SYMBOLS['Space'] // '␣'
110
+ * ```
111
+ */
112
+ export const KEY_DISPLAY_SYMBOLS = {
113
+ ArrowUp: '↑',
114
+ ArrowDown: '↓',
115
+ ArrowLeft: '←',
116
+ ArrowRight: '→',
117
+ Enter: '↵',
118
+ Escape: 'Esc',
119
+ Backspace: '⌫',
120
+ Delete: '⌦',
121
+ Tab: '⇥',
122
+ Space: '␣',
123
+ } as const satisfies Record<string, string>
package/src/format.ts CHANGED
@@ -3,27 +3,28 @@ import {
3
3
  LINUX_MODIFIER_LABELS,
4
4
  MAC_MODIFIER_LABELS,
5
5
  MAC_MODIFIER_SYMBOLS,
6
- MODIFIER_ALIASES,
7
- MODIFIER_ORDER,
8
6
  PUNCTUATION_KEY_DISPLAY_LABELS,
9
7
  WINDOWS_MODIFIER_LABELS,
10
- detectPlatform,
8
+ } from './display-labels'
9
+ import {
10
+ MODIFIER_ALIASES,
11
+ MODIFIER_ORDER,
12
+ PUNCTUATION_CODE_MAP,
13
+ normalizeKeyName,
11
14
  } from './constants'
15
+ import { detectPlatform } from './platform'
16
+ import { assertLogicalKey, splitHotkeyParts } from './_keyboard-event'
12
17
  import {
13
18
  isModifierKey,
14
19
  normalizeHotkeyFromParsed,
15
20
  normalizeRegisterableHotkey,
16
21
  } from './parse'
17
- import type {
18
- CanonicalModifier,
19
- FormatDisplayOptions,
20
- Hotkey,
21
- ParsedHotkey,
22
- RegisterableHotkey,
23
- } from './hotkey'
22
+ import type { CanonicalModifier } from './key.types'
23
+ import type { Hotkey, ParsedHotkey, RegisterableHotkey } from './hotkey.types'
24
24
 
25
25
  /**
26
- * Converts a hotkey sequence array to a display string.
26
+ * Joins stored sequence steps with spaces, preserving physical code tokens.
27
+ * For user-facing labels, call formatForDisplay on each step instead.
27
28
  *
28
29
  * @param sequence - Array of hotkey strings that form the sequence
29
30
  * @returns A space-separated string (e.g. ['G','G'] → 'G G')
@@ -51,6 +52,7 @@ export function formatHotkeySequence(sequence: Array<Hotkey>): string {
51
52
  * ```
52
53
  */
53
54
  export function formatHotkey(parsed: ParsedHotkey): string {
55
+ if (parsed.code === undefined) assertLogicalKey(parsed.key)
54
56
  const parts: Array<string> = []
55
57
 
56
58
  // Add modifiers in canonical order
@@ -61,21 +63,27 @@ export function formatHotkey(parsed: ParsedHotkey): string {
61
63
  }
62
64
 
63
65
  // Add the key
64
- parts.push(parsed.key)
66
+ parts.push(parsed.code !== undefined ? `[${parsed.code}]` : parsed.key)
65
67
 
66
68
  return parts.join('+')
67
69
  }
68
70
 
71
+ export type DisplayHotkey = RegisterableHotkey | ParsedHotkey | (string & {})
72
+
69
73
  /**
70
74
  * Formats a hotkey for display in a user interface.
71
75
  *
72
76
  * On macOS, uses symbols (⌘⇧S) in the same modifier order as {@link normalizeHotkeyFromParsed}.
73
77
  * On Windows/Linux, uses text (Ctrl+Shift+S) with `+` separators.
74
78
  * The separator can be customized with `separatorToken`.
79
+ * Physical codes use conventional display labels: `[KeyS]` becomes `S` and
80
+ * `[Digit2]` becomes `2`. These labels do not infer the active keyboard layout
81
+ * or change matching. Supply a resolved `layoutMap` for layout-aware labels;
82
+ * `keyLabels` overrides both layout entries and fallback labels.
75
83
  *
76
- * @param hotkey - The hotkey string or ParsedHotkey to format
84
+ * @param hotkey - A logical or physical string, raw binding, or ParsedHotkey
77
85
  * @param options - Formatting options
78
- * @returns A formatted string suitable for display
86
+ * @returns Display text, or individual key labels when parts is true
79
87
  *
80
88
  * @example
81
89
  * ```ts
@@ -90,45 +98,73 @@ export function formatHotkey(parsed: ParsedHotkey): string {
90
98
  * ```
91
99
  */
92
100
  export function formatForDisplay(
93
- hotkey: RegisterableHotkey | (string & {}),
101
+ hotkey: DisplayHotkey,
102
+ options: FormatDisplayOptions & { parts: true },
103
+ ): Array<string>
104
+ export function formatForDisplay(
105
+ hotkey: DisplayHotkey,
106
+ options?: FormatDisplayOptions & { parts?: false },
107
+ ): string
108
+ export function formatForDisplay(
109
+ hotkey: DisplayHotkey,
110
+ options: FormatDisplayOptions,
111
+ ): string | Array<string>
112
+ export function formatForDisplay(
113
+ hotkey: DisplayHotkey,
94
114
  options: FormatDisplayOptions = {},
95
- ): string {
115
+ ): string | Array<string> {
96
116
  const platform = options.platform ?? detectPlatform()
97
- const useSymbols = options.useSymbols ?? true
98
- const separatorToken =
99
- options.separatorToken ?? (platform === 'mac' && useSymbols ? ' ' : '+')
100
- const normalizedHotkey = normalizeRegisterableHotkey(
101
- hotkey as RegisterableHotkey,
102
- platform,
103
- )
104
- return normalizedHotkey
105
- .split('+')
106
- .map((segment) => {
107
- if (isModifierKey(segment)) {
108
- const modifierToken = (MODIFIER_ALIASES[segment] ??
109
- MODIFIER_ALIASES[segment.toLowerCase()]) as CanonicalModifier | 'Mod'
110
- return platform === 'mac'
111
- ? useSymbols
112
- ? MAC_MODIFIER_SYMBOLS[modifierToken]
113
- : MAC_MODIFIER_LABELS[modifierToken]
114
- : platform === 'windows'
115
- ? WINDOWS_MODIFIER_LABELS[modifierToken]
116
- : LINUX_MODIFIER_LABELS[modifierToken]
117
- } else {
118
- const keyDisplaySymbol =
119
- useSymbols &&
120
- KEY_DISPLAY_SYMBOLS[segment as keyof typeof KEY_DISPLAY_SYMBOLS]
121
- if (keyDisplaySymbol) return keyDisplaySymbol
122
-
123
- const punctuationKeyDisplayLabel =
124
- !useSymbols &&
125
- PUNCTUATION_KEY_DISPLAY_LABELS[
126
- segment as keyof typeof PUNCTUATION_KEY_DISPLAY_LABELS
127
- ]
128
- return punctuationKeyDisplayLabel || segment
129
- }
130
- })
131
- .join(separatorToken)
117
+ const symbols = options.useSymbols ?? true
118
+ const modifierSymbols =
119
+ typeof symbols === 'boolean' ? symbols : (symbols.modifiers ?? true)
120
+ const keySymbols =
121
+ typeof symbols === 'boolean' ? symbols : (symbols.keys ?? true)
122
+ const separator =
123
+ options.separatorToken ??
124
+ (platform === 'mac' && modifierSymbols ? ' ' : '+')
125
+ const normalized =
126
+ typeof hotkey === 'object' && 'modifiers' in hotkey
127
+ ? normalizeHotkeyFromParsed(hotkey, platform)
128
+ : normalizeRegisterableHotkey(hotkey as RegisterableHotkey, platform)
129
+ const parts = splitHotkeyParts(normalized).map((segment) => {
130
+ if (isModifierKey(segment)) {
131
+ const modifier = (MODIFIER_ALIASES[segment] ??
132
+ MODIFIER_ALIASES[segment.toLowerCase()]) as CanonicalModifier | 'Mod'
133
+ return platform === 'mac'
134
+ ? modifierSymbols
135
+ ? MAC_MODIFIER_SYMBOLS[modifier]
136
+ : MAC_MODIFIER_LABELS[modifier]
137
+ : platform === 'windows'
138
+ ? WINDOWS_MODIFIER_LABELS[modifier]
139
+ : LINUX_MODIFIER_LABELS[modifier]
140
+ }
141
+ const code = /^\[([A-Za-z][A-Za-z0-9]*)\]$/.exec(segment)?.[1]
142
+ const label = options.keyLabels?.[code ?? segment]
143
+ if (label !== undefined) return label
144
+ // Shorten conventional position labels for display only; stored identity is unchanged.
145
+ const numpad = code?.startsWith('Numpad') ?? false
146
+ let displayKey = code ?? segment
147
+ const mappedKey = code ? options.layoutMap?.get(code) : undefined
148
+ if (mappedKey) {
149
+ // Layout entries are logical labels; normalize their case and whitespace names.
150
+ displayKey = normalizeKeyName(mappedKey)
151
+ } else if (code) {
152
+ if (/^Key[A-Z]$/.test(code)) displayKey = code.slice(3)
153
+ else if (/^Digit[0-9]$/.test(code)) displayKey = code.slice(5)
154
+ else if (numpad) displayKey = code.slice(6)
155
+ else displayKey = PUNCTUATION_CODE_MAP[code] ?? code
156
+ }
157
+ const labels = keySymbols
158
+ ? KEY_DISPLAY_SYMBOLS
159
+ : PUNCTUATION_KEY_DISPLAY_LABELS
160
+ const display =
161
+ (labels as Partial<Record<string, string>>)[displayKey] ?? displayKey
162
+ // Retain numpad identity while making longer names such as MemoryAdd readable.
163
+ return numpad
164
+ ? `Numpad ${display.replace(/([a-z])([A-Z])/g, '$1 $2')}`
165
+ : display
166
+ })
167
+ return options.parts ? parts : parts.join(separator)
132
168
  }
133
169
 
134
170
  /**
@@ -136,7 +172,30 @@ export function formatForDisplay(
136
172
  */
137
173
  export function formatWithLabels(
138
174
  hotkey: RegisterableHotkey,
139
- options: Omit<FormatDisplayOptions, 'useSymbols'> = {},
175
+ options: Omit<FormatDisplayOptions, 'useSymbols' | 'parts'> = {},
140
176
  ): string {
141
177
  return formatForDisplay(hotkey, { ...options, useSymbols: false })
142
178
  }
179
+
180
+ /**
181
+ * Options for formatting hotkeys for display.
182
+ */
183
+ export interface FormatDisplayOptions {
184
+ /** The target platform. Defaults to auto-detection. */
185
+ platform?: 'mac' | 'windows' | 'linux'
186
+ /** Whether to use symbols for the display. Defaults to true. */
187
+ useSymbols?: boolean | { modifiers?: boolean; keys?: boolean }
188
+ /** Return individual key labels instead of a joined string. Default: false. */
189
+ parts?: boolean
190
+ /**
191
+ * An already-resolved code-to-key map, such as KeyboardLayoutMap or Map.
192
+ * Used only for physical bindings. Mapped keys receive normal display formatting;
193
+ * missing or empty entries use conventional fallback labels. The caller owns
194
+ * loading and refreshing the map; this formatter never calls browser APIs.
195
+ */
196
+ layoutMap?: { get: (code: string) => string | undefined }
197
+ /** Final display overrides for logical keys or physical codes; take precedence over layoutMap. Never changes matching. */
198
+ keyLabels?: Readonly<Record<string, string>>
199
+ /** Override the separator between display tokens. Defaults to platform-specific formatting when null/undefined. */
200
+ separatorToken?: string | null
201
+ }
package/src/hint.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { MODIFIER_KEYS, normalizeKeyName } from './constants'
2
+ import { parseRegisterableHotkey } from './parse'
3
+ import type { CanonicalModifier } from './key.types'
4
+ import type { RegisterableHotkey } from './hotkey.types'
5
+
6
+ export interface HeldModifierOptions {
7
+ /** Resolve Mod for this platform. Defaults to platform detection. */
8
+ platform?: 'mac' | 'windows' | 'linux'
9
+ /** Require every binding modifier to be held. Default: false (nonempty subset). */
10
+ exact?: boolean
11
+ }
12
+
13
+ /**
14
+ * Returns whether held modifiers should reveal a shortcut hint.
15
+ * By default, any nonempty subset of the binding's modifiers qualifies; an
16
+ * unrelated modifier hides the hint. `exact` requires every binding modifier.
17
+ * Nonmodifier keys are ignored. This does not check registration or focus.
18
+ *
19
+ * @example
20
+ * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt']) // true
21
+ * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt', 'Control']) // false
22
+ */
23
+ export function matchesHeldModifiers(
24
+ hotkey: RegisterableHotkey,
25
+ heldKeys: ReadonlyArray<string>,
26
+ options: HeldModifierOptions = {},
27
+ ): boolean {
28
+ // AltGraph (AltGr) types alternate characters such as @ or € on many layouts.
29
+ // It can synthesize Ctrl+Alt; character entry should not reveal shortcut hints.
30
+ if (heldKeys.includes('AltGraph')) return false
31
+ const held = [
32
+ ...new Set(
33
+ heldKeys.map(normalizeKeyName).filter((key) => MODIFIER_KEYS.has(key)),
34
+ ),
35
+ ]
36
+ const { modifiers } = parseRegisterableHotkey(hotkey, options.platform)
37
+ return (
38
+ held.length > 0 &&
39
+ held.every((key) => modifiers.includes(key as CanonicalModifier)) &&
40
+ (!options.exact || held.length === modifiers.length)
41
+ )
42
+ }