@tanstack/hotkeys 0.8.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 (109) hide show
  1. package/README.md +15 -1
  2. package/dist/{manager.utils.js → _event-target.js} +2 -41
  3. package/dist/_keyboard-event.js +75 -0
  4. package/dist/_match.js +48 -0
  5. package/dist/_named-keys.d.ts +6 -0
  6. package/dist/_named-keys.js +65 -0
  7. package/dist/_recorder-chord.js +42 -0
  8. package/dist/_recording-guard.js +55 -0
  9. package/dist/_registration.js +49 -0
  10. package/dist/conflicts.d.ts +36 -0
  11. package/dist/conflicts.js +54 -0
  12. package/dist/constants.d.ts +16 -144
  13. package/dist/constants.js +55 -172
  14. package/dist/display-labels.d.ts +90 -0
  15. package/dist/display-labels.js +112 -0
  16. package/dist/format.d.ts +48 -12
  17. package/dist/format.js +35 -39
  18. package/dist/hint.d.ts +20 -0
  19. package/dist/hint.js +23 -0
  20. package/dist/hotkey-manager.d.ts +14 -12
  21. package/dist/hotkey-manager.js +70 -21
  22. package/dist/hotkey-recorder.d.ts +8 -8
  23. package/dist/hotkey-recorder.js +74 -15
  24. package/dist/hotkey-sequence-recorder.d.ts +13 -9
  25. package/dist/hotkey-sequence-recorder.js +90 -28
  26. package/dist/{hotkey.d.ts → hotkey.types.d.ts} +58 -126
  27. package/dist/index.d.ts +14 -9
  28. package/dist/index.js +10 -6
  29. package/dist/key-state-tracker.d.ts +4 -7
  30. package/dist/key-state-tracker.js +30 -27
  31. package/dist/key.types.d.ts +79 -0
  32. package/dist/match.d.ts +24 -11
  33. package/dist/match.js +19 -30
  34. package/dist/parse.d.ts +18 -15
  35. package/dist/parse.js +39 -22
  36. package/dist/platform.d.ts +41 -0
  37. package/dist/platform.js +52 -0
  38. package/dist/recorder-options.d.ts +31 -0
  39. package/dist/sequence-manager.d.ts +27 -15
  40. package/dist/sequence-manager.js +61 -30
  41. package/dist/validate.d.ts +16 -8
  42. package/dist/validate.js +8 -13
  43. package/package.json +8 -13
  44. package/dist/constants.cjs +0 -535
  45. package/dist/constants.cjs.map +0 -1
  46. package/dist/constants.d.cts +0 -290
  47. package/dist/constants.js.map +0 -1
  48. package/dist/format.cjs +0 -91
  49. package/dist/format.cjs.map +0 -1
  50. package/dist/format.d.cts +0 -60
  51. package/dist/format.js.map +0 -1
  52. package/dist/hotkey-manager.cjs +0 -383
  53. package/dist/hotkey-manager.cjs.map +0 -1
  54. package/dist/hotkey-manager.d.cts +0 -230
  55. package/dist/hotkey-manager.js.map +0 -1
  56. package/dist/hotkey-recorder.cjs +0 -179
  57. package/dist/hotkey-recorder.cjs.map +0 -1
  58. package/dist/hotkey-recorder.d.cts +0 -116
  59. package/dist/hotkey-recorder.js.map +0 -1
  60. package/dist/hotkey-sequence-recorder.cjs +0 -192
  61. package/dist/hotkey-sequence-recorder.cjs.map +0 -1
  62. package/dist/hotkey-sequence-recorder.d.cts +0 -79
  63. package/dist/hotkey-sequence-recorder.js.map +0 -1
  64. package/dist/hotkey.d.cts +0 -301
  65. package/dist/index.cjs +0 -63
  66. package/dist/index.d.cts +0 -13
  67. package/dist/key-state-tracker.cjs +0 -197
  68. package/dist/key-state-tracker.cjs.map +0 -1
  69. package/dist/key-state-tracker.d.cts +0 -107
  70. package/dist/key-state-tracker.js.map +0 -1
  71. package/dist/manager.utils.cjs +0 -115
  72. package/dist/manager.utils.cjs.map +0 -1
  73. package/dist/manager.utils.d.cts +0 -13
  74. package/dist/manager.utils.d.ts +0 -13
  75. package/dist/manager.utils.js.map +0 -1
  76. package/dist/match.cjs +0 -153
  77. package/dist/match.cjs.map +0 -1
  78. package/dist/match.d.cts +0 -85
  79. package/dist/match.js.map +0 -1
  80. package/dist/parse.cjs +0 -228
  81. package/dist/parse.cjs.map +0 -1
  82. package/dist/parse.d.cts +0 -128
  83. package/dist/parse.js.map +0 -1
  84. package/dist/recorder-chord.cjs +0 -18
  85. package/dist/recorder-chord.cjs.map +0 -1
  86. package/dist/recorder-chord.js +0 -18
  87. package/dist/recorder-chord.js.map +0 -1
  88. package/dist/sequence-manager.cjs +0 -415
  89. package/dist/sequence-manager.cjs.map +0 -1
  90. package/dist/sequence-manager.d.cts +0 -157
  91. package/dist/sequence-manager.js.map +0 -1
  92. package/dist/validate.cjs +0 -116
  93. package/dist/validate.cjs.map +0 -1
  94. package/dist/validate.d.cts +0 -56
  95. package/dist/validate.js.map +0 -1
  96. package/src/constants.ts +0 -603
  97. package/src/format.ts +0 -142
  98. package/src/hotkey-manager.ts +0 -759
  99. package/src/hotkey-recorder.ts +0 -272
  100. package/src/hotkey-sequence-recorder.ts +0 -328
  101. package/src/hotkey.ts +0 -436
  102. package/src/index.ts +0 -11
  103. package/src/key-state-tracker.ts +0 -249
  104. package/src/manager.utils.ts +0 -231
  105. package/src/match.ts +0 -259
  106. package/src/parse.ts +0 -328
  107. package/src/recorder-chord.ts +0 -25
  108. package/src/sequence-manager.ts +0 -734
  109. package/src/validate.ts +0 -171
@@ -1,231 +0,0 @@
1
- import type { ParsedHotkey } from './hotkey'
2
-
3
- /**
4
- * Behavior when registering a hotkey/sequence that conflicts with an existing registration.
5
- *
6
- * - `'warn'` - Log a warning to the console but allow both registrations (default)
7
- * - `'error'` - Throw an error and prevent the new registration
8
- * - `'replace'` - Unregister the existing registration and register the new one
9
- * - `'allow'` - Allow multiple registrations without warning
10
- */
11
- export type ConflictBehavior = 'warn' | 'error' | 'replace' | 'allow'
12
-
13
- /**
14
- * Default options for hotkey/sequence registration.
15
- * Omitted: platform, target (resolved at registration), requireReset (HotkeyManager only).
16
- */
17
- export const defaultHotkeyOptions = {
18
- preventDefault: true,
19
- stopPropagation: true,
20
- eventType: 'keydown' as const,
21
- enabled: true,
22
- ignoreInputs: true,
23
- conflictBehavior: 'warn' as ConflictBehavior,
24
- }
25
-
26
- /**
27
- * Computes the default ignoreInputs value based on the hotkey.
28
- * Ctrl/Meta shortcuts and Escape fire in inputs; single keys and Shift/Alt combos are ignored.
29
- */
30
- export function getDefaultIgnoreInputs(parsedHotkey: ParsedHotkey): boolean {
31
- if (parsedHotkey.ctrl || parsedHotkey.meta) return false // Mod+S, Ctrl+C, etc.
32
- if (parsedHotkey.key === 'Escape') return false // Close modal, etc.
33
- return true // Single keys, Shift+key, Alt+key
34
- }
35
-
36
- /**
37
- * Checks if an element is an input-like element that should be ignored for hotkeys.
38
- *
39
- * This includes:
40
- * - HTMLInputElement (all input types except button, submit, reset)
41
- * - HTMLTextAreaElement
42
- * - HTMLSelectElement
43
- * - Elements with contentEditable enabled
44
- *
45
- * Button-type inputs (button, submit, reset) are excluded so hotkeys like
46
- * Mod+S and Escape fire when the user has tabbed to a form button.
47
- */
48
- export function isInputElement(element: EventTarget | null): boolean {
49
- if (!element) {
50
- return false
51
- }
52
-
53
- if (element instanceof HTMLInputElement) {
54
- const type = element.type.toLowerCase()
55
- if (type === 'button' || type === 'submit' || type === 'reset') {
56
- return false
57
- }
58
- return true
59
- }
60
-
61
- if (
62
- element instanceof HTMLTextAreaElement ||
63
- element instanceof HTMLSelectElement
64
- ) {
65
- return true
66
- }
67
-
68
- // Check for contenteditable elements (includes "true", "", "plaintext-only",
69
- // and inherited contenteditable from ancestor elements)
70
- if (element instanceof HTMLElement && element.isContentEditable) {
71
- return true
72
- }
73
-
74
- return false
75
- }
76
-
77
- /**
78
- * Returns the focused element for the document associated with where hotkey
79
- * listeners are attached (listener root). Use this instead of the global
80
- * `document.activeElement` so registrations scoped to an iframe (or another
81
- * document) read focus from the correct tree.
82
- */
83
- export function getActiveElementForListenerTarget(
84
- target: HTMLElement | Document | Window,
85
- ): Element | null {
86
- if (typeof document === 'undefined') {
87
- return null
88
- }
89
-
90
- // Document first (nodeType covers environments where `document instanceof Document` is false)
91
- if (
92
- (typeof Document !== 'undefined' && target instanceof Document) ||
93
- (typeof Node !== 'undefined' &&
94
- (target as Node).nodeType === Node.DOCUMENT_NODE)
95
- ) {
96
- return (target as Document).activeElement
97
- }
98
-
99
- if (typeof HTMLElement !== 'undefined' && target instanceof HTMLElement) {
100
- return target.ownerDocument.activeElement ?? null
101
- }
102
-
103
- // Window (global or iframe): avoid relying on `instanceof Window` alone (test DOM quirks)
104
- return (target as Window).document.activeElement ?? null
105
- }
106
-
107
- /**
108
- * Returns whether an event should be ignored because it originated from an
109
- * input-like element other than the registration target.
110
- *
111
- * This checks:
112
- * - the currently focused element for the listener target
113
- * - the event's composed path (for shadow DOM)
114
- * - the event target as a final fallback
115
- */
116
- export function shouldIgnoreInputEvent(
117
- event: KeyboardEvent,
118
- listenerTarget: HTMLElement | Document | Window,
119
- registrationTarget: HTMLElement | Document | Window,
120
- ): boolean {
121
- const focused = getActiveElementForListenerTarget(listenerTarget)
122
- if (focused && isInputElement(focused) && focused !== registrationTarget) {
123
- return true
124
- }
125
-
126
- if (
127
- event
128
- .composedPath()
129
- .some(
130
- (element) => isInputElement(element) && element !== registrationTarget,
131
- )
132
- ) {
133
- return true
134
- }
135
-
136
- return isInputElement(event.target) && event.target !== registrationTarget
137
- }
138
-
139
- /**
140
- * Checks if an event is for the given target (originated from or bubbled to it).
141
- *
142
- * For document/window targets, also accepts document.documentElement as currentTarget
143
- * to handle Brave and other browsers where currentTarget may be documentElement
144
- * instead of document when listeners are attached to document.
145
- */
146
- export function isEventForTarget(
147
- event: KeyboardEvent,
148
- target: HTMLElement | Document | Window,
149
- ): boolean {
150
- // For Document and Window, verify that our handler was indeed called for this target.
151
- //
152
- // Browser compatibility note:
153
- // Per the DOM spec, event.currentTarget should equal the element the listener was
154
- // attached to. However, some Chromium-based browsers (notably Brave) exhibit
155
- // non-standard behavior where event.currentTarget is set to document.documentElement
156
- // (<html>) instead of document when a listener is attached to document.
157
- // This may be related to privacy/fingerprinting protections.
158
- //
159
- // To ensure cross-browser compatibility, we accept both the expected target
160
- // and document.documentElement as valid currentTarget values.
161
- // See: https://dom.spec.whatwg.org/#dom-event-currenttarget
162
- if (target === document || target === window) {
163
- return (
164
- event.currentTarget === target ||
165
- event.currentTarget === document.documentElement
166
- )
167
- }
168
-
169
- // For Window, accept window, document, or document.documentElement (browser quirks)
170
- if (target === window) {
171
- return (
172
- event.currentTarget === window ||
173
- event.currentTarget === document ||
174
- event.currentTarget === document.documentElement
175
- )
176
- }
177
-
178
- // For HTMLElement, check if event originated from or bubbled to the element
179
- if (target instanceof HTMLElement) {
180
- // Check if the event's currentTarget is the target (capturing/bubbling)
181
- if (event.currentTarget === target) {
182
- return true
183
- }
184
-
185
- // Check if the event's target is a descendant of our target
186
- if (event.target instanceof Node && target.contains(event.target)) {
187
- return true
188
- }
189
- }
190
-
191
- return false
192
- }
193
-
194
- /**
195
- * Handles conflicts between registrations based on conflict behavior.
196
- *
197
- * @param conflictingId - The ID of the conflicting registration
198
- * @param keyDisplay - Display string for the conflicting key/sequence (for error messages)
199
- * @param conflictBehavior - How to handle the conflict
200
- * @param unregister - Function to unregister by ID
201
- */
202
- export function handleConflict(
203
- conflictingId: string,
204
- keyDisplay: string,
205
- conflictBehavior: ConflictBehavior,
206
- unregister: (id: string) => void,
207
- ): void {
208
- if (conflictBehavior === 'allow') {
209
- return
210
- }
211
-
212
- if (conflictBehavior === 'warn') {
213
- console.warn(
214
- `'${keyDisplay}' is already registered. Multiple handlers will be triggered. ` +
215
- `Use conflictBehavior: 'replace' to replace the existing handler, ` +
216
- `or conflictBehavior: 'allow' to suppress this warning.`,
217
- )
218
- return
219
- }
220
-
221
- if (conflictBehavior === 'error') {
222
- throw new Error(
223
- `'${keyDisplay}' is already registered. ` +
224
- `Use conflictBehavior: 'replace' to replace the existing handler, ` +
225
- `or conflictBehavior: 'allow' to allow multiple registrations.`,
226
- )
227
- }
228
-
229
- // At this point, conflictBehavior must be 'replace'
230
- unregister(conflictingId)
231
- }
package/src/match.ts DELETED
@@ -1,259 +0,0 @@
1
- import {
2
- PUNCTUATION_CODE_MAP,
3
- detectPlatform,
4
- isSingleLetterKey,
5
- normalizeKeyName,
6
- } from './constants'
7
- import { parseHotkey } from './parse'
8
- import type {
9
- Hotkey,
10
- HotkeyCallback,
11
- HotkeyCallbackContext,
12
- ParsedHotkey,
13
- } from './hotkey'
14
-
15
- /**
16
- * Checks if a KeyboardEvent matches a hotkey.
17
- *
18
- * Uses the `key` property from KeyboardEvent for matching, with a fallback to `code`
19
- * for letter keys, digit keys (0-9), and punctuation keys when `key` produces special
20
- * characters (e.g., macOS Option+letter, Shift+number, or Option+punctuation).
21
- * Letter keys are matched case-insensitively.
22
- *
23
- * Also handles "dead key" events where `event.key` is `'Dead'` instead of the expected
24
- * character. This commonly occurs on macOS with Option+letter combinations (e.g., Option+E,
25
- * Option+I, Option+U, Option+N) and on Windows/Linux with international keyboard layouts.
26
- * In these cases, `event.code` is used to determine the physical key.
27
- *
28
- * @param event - The KeyboardEvent to check
29
- * @param hotkey - The hotkey string or ParsedHotkey to match against
30
- * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
31
- * @returns True if the event matches the hotkey
32
- *
33
- * @example
34
- * ```ts
35
- * document.addEventListener('keydown', (event) => {
36
- * if (matchesKeyboardEvent(event, 'Mod+S')) {
37
- * event.preventDefault()
38
- * handleSave()
39
- * }
40
- * })
41
- * ```
42
- */
43
- export function matchesKeyboardEvent(
44
- event: KeyboardEvent,
45
- hotkey: Hotkey | ParsedHotkey,
46
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
47
- ): boolean {
48
- const parsed =
49
- typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey
50
-
51
- // Check modifiers
52
- if (event.ctrlKey !== parsed.ctrl) {
53
- return false
54
- }
55
- if (event.shiftKey !== parsed.shift) {
56
- return false
57
- }
58
- if (event.altKey !== parsed.alt) {
59
- return false
60
- }
61
- if (event.metaKey !== parsed.meta) {
62
- return false
63
- }
64
-
65
- // Check key (case-insensitive for letters)
66
- const eventKey = normalizeKeyName(event.key)
67
- const hotkeyKey = parsed.key
68
-
69
- // For single-character keys (not dead keys), try direct event.key match first
70
- if (eventKey !== 'Dead' && eventKey.length === 1 && hotkeyKey.length === 1) {
71
- if (eventKey.toUpperCase() === hotkeyKey.toUpperCase()) {
72
- return true
73
- }
74
-
75
- // If event.key is a letter, we usually trust the keyboard layout.
76
- // For ASCII letters: always trust layout (Dvorak, Colemak, AZERTY support).
77
- // For non-ASCII letters without Alt: trust layout (e.g., Cyrillic keyboards).
78
- //
79
- // For non-ASCII letters WITH Alt held down: fall through to the event.code
80
- // fallback. macOS Option+letter combinations produce non-ASCII letters as
81
- // event.key (e.g., Option+A → 'å', Option+' → 'æ', Option+P → 'π'), but
82
- // event.code still reflects the physical key. We want Alt+A to match even
83
- // when Option+A fires event.key='å'.
84
- if (
85
- isSingleLetterKey(eventKey) &&
86
- (/^[A-Za-z]$/.test(eventKey) || !event.altKey)
87
- ) {
88
- return false
89
- }
90
- }
91
-
92
- // Fallback to event.code for dead keys or single-char mismatches where
93
- // event.key is a non-letter special character.
94
- // Dead keys: Option+letter on macOS, international layouts produce event.key === 'Dead'
95
- // Single-char mismatches: Cmd+Option+T gives '†' instead of 'T', Shift+4 gives '$'
96
- if (
97
- event.code &&
98
- (eventKey === 'Dead' || (eventKey.length === 1 && hotkeyKey.length === 1))
99
- ) {
100
- // fallback for letter keys (common with mac option + letter)
101
- if (event.code.startsWith('Key')) {
102
- const codeLetter = event.code.slice(3)
103
- if (codeLetter.length === 1 && /^[A-Za-z]$/.test(codeLetter)) {
104
- return codeLetter.toUpperCase() === hotkeyKey.toUpperCase()
105
- }
106
- }
107
-
108
- // fallback for number keys (common with mac option + num)
109
- if (event.code.startsWith('Digit')) {
110
- const codeDigit = event.code.slice(5)
111
- if (codeDigit.length === 1 && /^[0-9]$/.test(codeDigit)) {
112
- return codeDigit === hotkeyKey
113
- }
114
- }
115
- // Fallback for punctuation keys (e.g., Minus, Slash, BracketLeft).
116
- // On macOS, Option+punctuation produces composed characters (e.g., Option+- → '–'),
117
- // but event.code still reports the physical key.
118
- if (event.code in PUNCTUATION_CODE_MAP) {
119
- return PUNCTUATION_CODE_MAP[event.code] === hotkeyKey
120
- }
121
-
122
- return false
123
- }
124
-
125
- // For special keys, compare exactly (after normalization)
126
- return eventKey === hotkeyKey
127
- }
128
-
129
- /**
130
- * Options for creating a hotkey handler.
131
- */
132
- export interface CreateHotkeyHandlerOptions {
133
- /** Prevent the default browser action when the hotkey matches. Defaults to true */
134
- preventDefault?: boolean
135
- /** Stop event propagation when the hotkey matches. Defaults to true */
136
- stopPropagation?: boolean
137
- /** The target platform for resolving 'Mod' */
138
- platform?: 'mac' | 'windows' | 'linux'
139
- }
140
-
141
- /**
142
- * Creates a keyboard event handler that calls the callback when the hotkey matches.
143
- *
144
- * @param hotkey - The hotkey string or ParsedHotkey to match
145
- * @param callback - The function to call when the hotkey matches
146
- * @param options - Options for matching and handling
147
- * @returns A function that can be used as an event handler
148
- *
149
- * @example
150
- * ```ts
151
- * const handler = createHotkeyHandler('Mod+S', (event, { hotkey, parsedHotkey }) => {
152
- * console.log(`${hotkey} was pressed`)
153
- * handleSave()
154
- * })
155
- *
156
- * document.addEventListener('keydown', handler)
157
- * ```
158
- */
159
- export function createHotkeyHandler(
160
- hotkey: Hotkey | ParsedHotkey,
161
- callback: HotkeyCallback,
162
- options: CreateHotkeyHandlerOptions = {},
163
- ): (event: KeyboardEvent) => void {
164
- const { preventDefault = true, stopPropagation = true, platform } = options
165
- const resolvedPlatform = platform ?? detectPlatform()
166
-
167
- const hotkeyString: Hotkey =
168
- typeof hotkey === 'string' ? hotkey : formatParsedHotkey(hotkey)
169
- const parsed =
170
- typeof hotkey === 'string' ? parseHotkey(hotkey, resolvedPlatform) : hotkey
171
-
172
- const context: HotkeyCallbackContext = {
173
- hotkey: hotkeyString,
174
- parsedHotkey: parsed,
175
- }
176
-
177
- return (event: KeyboardEvent) => {
178
- if (matchesKeyboardEvent(event, parsed, resolvedPlatform)) {
179
- if (preventDefault) {
180
- event.preventDefault()
181
- }
182
- if (stopPropagation) {
183
- event.stopPropagation()
184
- }
185
- callback(event, context)
186
- }
187
- }
188
- }
189
-
190
- type MultiHotkeyHandler = { [K in Hotkey]?: HotkeyCallback }
191
-
192
- /**
193
- * Creates a handler that matches multiple hotkeys.
194
- *
195
- * @param handlers - A map of hotkey strings to their handlers
196
- * @param options - Options for matching and handling
197
- * @returns A function that can be used as an event handler
198
- *
199
- * @example
200
- * ```ts
201
- * const handler = createMultiHotkeyHandler({
202
- * 'Mod+S': (event, { hotkey }) => handleSave(),
203
- * 'Mod+Z': (event, { hotkey }) => handleUndo(),
204
- * 'Mod+Shift+Z': (event, { hotkey }) => handleRedo(),
205
- * })
206
- *
207
- * document.addEventListener('keydown', handler)
208
- * ```
209
- */
210
- export function createMultiHotkeyHandler(
211
- handlers: MultiHotkeyHandler,
212
- options: CreateHotkeyHandlerOptions = {},
213
- ): (event: KeyboardEvent) => void {
214
- const { preventDefault = true, stopPropagation = true, platform } = options
215
- const resolvedPlatform = platform ?? detectPlatform()
216
-
217
- // Pre-parse all hotkeys for efficiency
218
- const parsedHandlers = Object.entries(handlers)
219
- .filter((entry): entry is [string, HotkeyCallback] => Boolean(entry[1]))
220
- .map(([hotkey, handler]) => {
221
- const parsed = parseHotkey(hotkey as Hotkey, resolvedPlatform)
222
- const context: HotkeyCallbackContext = {
223
- hotkey: hotkey as Hotkey,
224
- parsedHotkey: parsed,
225
- }
226
- return { parsed, handler, context }
227
- })
228
-
229
- return (event: KeyboardEvent) => {
230
- for (const { parsed, handler, context } of parsedHandlers) {
231
- if (matchesKeyboardEvent(event, parsed, resolvedPlatform)) {
232
- if (preventDefault) {
233
- event.preventDefault()
234
- }
235
- if (stopPropagation) {
236
- event.stopPropagation()
237
- }
238
- handler(event, context)
239
- return // Only handle the first match
240
- }
241
- }
242
- }
243
- }
244
-
245
- /**
246
- * Formats a ParsedHotkey back to a hotkey string.
247
- * Used internally to provide the hotkey string in callback context.
248
- */
249
- function formatParsedHotkey(parsed: ParsedHotkey): Hotkey {
250
- const parts: Array<string> = []
251
-
252
- if (parsed.ctrl) parts.push('Control')
253
- if (parsed.alt) parts.push('Alt')
254
- if (parsed.shift) parts.push('Shift')
255
- if (parsed.meta) parts.push('Meta')
256
- parts.push(parsed.key)
257
-
258
- return parts.join('+') as Hotkey
259
- }