@tanstack/hotkeys 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +12 -14
  16. package/dist/format.js +17 -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
package/src/match.ts DELETED
@@ -1,210 +0,0 @@
1
- import { matchKeyboardEvent } from './_match'
2
- import { parseHotkey, parseRegisterableHotkey } from './parse'
3
- import { detectPlatform } from './platform'
4
- import { keysEqual } from './_keyboard-event'
5
- import type {
6
- Hotkey,
7
- HotkeyCallback,
8
- HotkeyCallbackContext,
9
- ParsedHotkey,
10
- RegisterableHotkey,
11
- } from './hotkey.types'
12
-
13
- export interface KeyboardEventMatch {
14
- matched: boolean
15
- score: 0 | 1 | 2 | 3
16
- source?: 'key' | 'code' | 'fallback'
17
- identity?: { key: string; code: string }
18
- }
19
-
20
- /**
21
- * Checks if a KeyboardEvent matches a hotkey.
22
- *
23
- * Physical bindings such as `Mod+[KeyS]` match `event.code` exactly.
24
- * Logical bindings use `event.key`, with a fallback to `code`
25
- * for letter keys, digit keys (0-9), and punctuation keys when `key` produces special
26
- * characters (e.g., macOS Option+letter, Shift+number, or Option+punctuation).
27
- * Letter keys are matched case-insensitively.
28
- *
29
- * Also handles "dead key" events where `event.key` is `'Dead'` instead of the expected
30
- * character. This commonly occurs on macOS with Option+letter combinations (e.g., Option+E,
31
- * Option+I, Option+U, Option+N) and on Windows/Linux with international keyboard layouts.
32
- * In these cases, `event.code` is used to determine the physical key.
33
- *
34
- * @param event - The KeyboardEvent to check
35
- * @param hotkey - The hotkey string or ParsedHotkey to match against
36
- * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
37
- * @returns True if the event matches the hotkey
38
- *
39
- * @example
40
- * ```ts
41
- * document.addEventListener('keydown', (event) => {
42
- * if (matchesKeyboardEvent(event, 'Mod+S')) {
43
- * event.preventDefault()
44
- * handleSave()
45
- * }
46
- * })
47
- * ```
48
- */
49
- export function matchesKeyboardEvent(
50
- event: KeyboardEvent,
51
- hotkey: Hotkey | ParsedHotkey,
52
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
53
- ): boolean {
54
- return matchKeyboardEvent(event, hotkey, platform).matched
55
- }
56
-
57
- /**
58
- * Options for creating a hotkey handler.
59
- */
60
- export interface CreateHotkeyHandlerOptions {
61
- /** Prevent the default browser action when the hotkey matches. Defaults to true */
62
- preventDefault?: boolean
63
- /** Stop event propagation when the hotkey matches. Defaults to true */
64
- stopPropagation?: boolean
65
- /** The target platform for resolving 'Mod' */
66
- platform?: 'mac' | 'windows' | 'linux'
67
- }
68
-
69
- /**
70
- * Creates a keyboard event handler that calls the callback when the hotkey matches.
71
- *
72
- * @param hotkey - The hotkey string or ParsedHotkey to match
73
- * @param callback - The function to call when the hotkey matches
74
- * @param options - Options for matching and handling
75
- * @returns A function that can be used as an event handler
76
- *
77
- * @example
78
- * ```ts
79
- * const handler = createHotkeyHandler('Mod+S', (event, { hotkey, parsedHotkey }) => {
80
- * console.log(`${hotkey} was pressed`)
81
- * handleSave()
82
- * })
83
- *
84
- * document.addEventListener('keydown', handler)
85
- * ```
86
- */
87
- export function createHotkeyHandler(
88
- hotkey: Hotkey | ParsedHotkey,
89
- callback: HotkeyCallback,
90
- options: CreateHotkeyHandlerOptions = {},
91
- ): (event: KeyboardEvent) => void {
92
- const { preventDefault = true, stopPropagation = true, platform } = options
93
- const resolvedPlatform = platform ?? detectPlatform()
94
-
95
- const hotkeyString: Hotkey =
96
- typeof hotkey === 'string' ? hotkey : formatParsedHotkey(hotkey)
97
- const parsed =
98
- typeof hotkey === 'string' ? parseHotkey(hotkey, resolvedPlatform) : hotkey
99
-
100
- const context: HotkeyCallbackContext = {
101
- hotkey: hotkeyString,
102
- parsedHotkey: parsed,
103
- }
104
-
105
- return (event: KeyboardEvent) => {
106
- if (matchesKeyboardEvent(event, parsed, resolvedPlatform)) {
107
- if (preventDefault) {
108
- event.preventDefault()
109
- }
110
- if (stopPropagation) {
111
- event.stopPropagation()
112
- }
113
- callback(event, context)
114
- }
115
- }
116
- }
117
-
118
- export type MultiHotkeyHandler = { [K in Hotkey]?: HotkeyCallback }
119
-
120
- /**
121
- * Creates a handler that matches multiple hotkeys.
122
- *
123
- * @param handlers - A map of hotkey strings to their handlers
124
- * @param options - Options for matching and handling
125
- * @returns A function that can be used as an event handler
126
- *
127
- * @example
128
- * ```ts
129
- * const handler = createMultiHotkeyHandler({
130
- * 'Mod+S': (event, { hotkey }) => handleSave(),
131
- * 'Mod+Z': (event, { hotkey }) => handleUndo(),
132
- * 'Mod+Shift+Z': (event, { hotkey }) => handleRedo(),
133
- * })
134
- *
135
- * document.addEventListener('keydown', handler)
136
- * ```
137
- */
138
- export function createMultiHotkeyHandler(
139
- handlers: MultiHotkeyHandler,
140
- options: CreateHotkeyHandlerOptions = {},
141
- ): (event: KeyboardEvent) => void {
142
- const { preventDefault = true, stopPropagation = true, platform } = options
143
- const resolvedPlatform = platform ?? detectPlatform()
144
-
145
- // Pre-parse all hotkeys for efficiency
146
- const parsedHandlers = Object.entries(handlers)
147
- .filter((entry): entry is [string, HotkeyCallback] => Boolean(entry[1]))
148
- .map(([hotkey, handler]) => {
149
- const parsed = parseHotkey(hotkey as Hotkey, resolvedPlatform)
150
- const context: HotkeyCallbackContext = {
151
- hotkey: hotkey as Hotkey,
152
- parsedHotkey: parsed,
153
- }
154
- return { parsed, handler, context }
155
- })
156
-
157
- return (event: KeyboardEvent) => {
158
- for (const { parsed, handler, context } of parsedHandlers) {
159
- if (matchesKeyboardEvent(event, parsed, resolvedPlatform)) {
160
- if (preventDefault) {
161
- event.preventDefault()
162
- }
163
- if (stopPropagation) {
164
- event.stopPropagation()
165
- }
166
- handler(event, context)
167
- return // Only handle the first match
168
- }
169
- }
170
- }
171
- }
172
-
173
- /**
174
- * Formats a ParsedHotkey back to a hotkey string.
175
- * Used internally to provide the hotkey string in callback context.
176
- */
177
- function formatParsedHotkey(parsed: ParsedHotkey): Hotkey {
178
- const parts: Array<string> = []
179
-
180
- if (parsed.ctrl) parts.push('Control')
181
- if (parsed.alt) parts.push('Alt')
182
- if (parsed.shift) parts.push('Shift')
183
- if (parsed.meta) parts.push('Meta')
184
- parts.push(parsed.code !== undefined ? `[${parsed.code}]` : parsed.key)
185
-
186
- return parts.join('+') as Hotkey
187
- }
188
-
189
- /**
190
- * Compares normalized binding identity, including all four modifiers.
191
- * Aliases resolve on the supplied platform. A physical code and a logical key
192
- * remain distinct even when one keyboard event could match both.
193
- */
194
- export function areHotkeysEqual(
195
- left: RegisterableHotkey,
196
- right: RegisterableHotkey,
197
- platform = detectPlatform(),
198
- ): boolean {
199
- const a = parseRegisterableHotkey(left, platform)
200
- const b = parseRegisterableHotkey(right, platform)
201
- return (
202
- a.ctrl === b.ctrl &&
203
- a.alt === b.alt &&
204
- a.shift === b.shift &&
205
- a.meta === b.meta &&
206
- (a.code !== undefined || b.code !== undefined
207
- ? a.code === b.code
208
- : keysEqual(a.key, b.key))
209
- )
210
- }
package/src/parse.ts DELETED
@@ -1,369 +0,0 @@
1
- import {
2
- assertLogicalKey,
3
- normalizeKeyboardEvent,
4
- splitHotkeyParts,
5
- } from './_keyboard-event'
6
- import { MODIFIER_ALIASES, MODIFIER_ORDER, normalizeKeyName } from './constants'
7
- import { detectPlatform, resolveModifier } from './platform'
8
- import type { CanonicalModifier, Key } from './key.types'
9
- import type {
10
- Hotkey,
11
- ParsedHotkey,
12
- RawHotkey,
13
- RegisterableHotkey,
14
- } from './hotkey.types'
15
-
16
- /**
17
- * Parses a hotkey string into its component parts.
18
- *
19
- * @param hotkey - The hotkey string to parse (e.g., 'Mod+Shift+S')
20
- * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
21
- * @returns A ParsedHotkey with either logical key or physical code and modifier flags
22
- *
23
- * @example
24
- * ```ts
25
- * parseHotkey('Mod+S') // On Mac: { key: 'S', ctrl: false, shift: false, alt: false, meta: true, modifiers: ['Meta'] }
26
- * parseHotkey('Mod+S') // On Windows: { key: 'S', ctrl: true, shift: false, alt: false, meta: false, modifiers: ['Control'] }
27
- * parseHotkey('Control+Shift+A') // { key: 'A', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] }
28
- * ```
29
- */
30
- export function parseHotkey(
31
- hotkey: Hotkey | (string & {}),
32
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
33
- ): ParsedHotkey {
34
- const parts = splitHotkeyParts(hotkey)
35
- const modifiers: Set<CanonicalModifier> = new Set()
36
- let key = ''
37
-
38
- for (let i = 0; i < parts.length; i++) {
39
- const part = parts[i]!.trim()
40
-
41
- if (i === parts.length - 1) {
42
- // Last part is always the key
43
- key = normalizeKeyName(part)
44
- } else {
45
- // All other parts are modifiers
46
- const alias =
47
- MODIFIER_ALIASES[part] ?? MODIFIER_ALIASES[part.toLowerCase()]
48
-
49
- if (alias) {
50
- const resolved = resolveModifier(alias, platform)
51
- modifiers.add(resolved)
52
- } else {
53
- // Unknown modifier, treat as part of the key if it's the only part
54
- // or ignore if there are more parts
55
- if (parts.length === 1) {
56
- key = normalizeKeyName(part)
57
- }
58
- }
59
- }
60
- }
61
-
62
- // If no key was found (empty string), use the last part as-is
63
- if (!key && parts.length > 0) {
64
- key = normalizeKeyName(parts[parts.length - 1]!.trim())
65
- }
66
-
67
- const code = /^\[([A-Za-z][A-Za-z0-9]*)\]$/.exec(key)?.[1]
68
- return {
69
- ...(code !== undefined ? { code } : { key }),
70
- ctrl: modifiers.has('Control'),
71
- shift: modifiers.has('Shift'),
72
- alt: modifiers.has('Alt'),
73
- meta: modifiers.has('Meta'),
74
- modifiers: MODIFIER_ORDER.filter((m) => modifiers.has(m)),
75
- }
76
- }
77
-
78
- /**
79
- * Converts a RawHotkey object to a ParsedHotkey.
80
- * Optional modifier booleans default to false; modifiers array is derived from them.
81
- * When `mod` is true, it is resolved to Control or Meta based on platform.
82
- *
83
- * @param raw - The raw hotkey object
84
- * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
85
- * @returns A ParsedHotkey suitable for matching and formatting
86
- *
87
- * @example
88
- * ```ts
89
- * rawHotkeyToParsedHotkey({ key: 'Escape' })
90
- * // { key: 'Escape', ctrl: false, shift: false, alt: false, meta: false, modifiers: [] }
91
- *
92
- * rawHotkeyToParsedHotkey({ key: 'S', mod: true }, 'mac')
93
- * // { key: 'S', ctrl: false, shift: false, alt: false, meta: true, modifiers: ['Meta'] }
94
- *
95
- * rawHotkeyToParsedHotkey({ key: 'S', mod: true, shift: true }, 'windows')
96
- * // { key: 'S', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] }
97
- * ```
98
- */
99
- export function rawHotkeyToParsedHotkey(
100
- raw: RawHotkey,
101
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
102
- ): ParsedHotkey {
103
- if (raw.code === undefined) assertLogicalKey(raw.key)
104
- let ctrl = raw.ctrl ?? false
105
- const shift = raw.shift ?? false
106
- const alt = raw.alt ?? false
107
- let meta = raw.meta ?? false
108
-
109
- if (raw.mod) {
110
- const resolved = resolveModifier('Mod', platform)
111
- if (resolved === 'Control') {
112
- ctrl = true
113
- } else {
114
- meta = true
115
- }
116
- }
117
-
118
- const modifiers: Array<CanonicalModifier> = MODIFIER_ORDER.filter((m) => {
119
- switch (m) {
120
- case 'Control':
121
- return ctrl
122
- case 'Shift':
123
- return shift
124
- case 'Alt':
125
- return alt
126
- case 'Meta':
127
- return meta
128
- default:
129
- return false
130
- }
131
- })
132
- return {
133
- ...(raw.code !== undefined
134
- ? { code: raw.code }
135
- : { key: normalizeKeyName(raw.key) }),
136
- ctrl,
137
- shift,
138
- alt,
139
- meta,
140
- modifiers,
141
- }
142
- }
143
-
144
- /**
145
- * Serializes a parsed hotkey to the canonical string form used for registration
146
- * and storage: `Mod` first when the platform allows, then `Alt`, then `Shift`;
147
- * otherwise full `Control+Alt+Shift+Meta` order.
148
- */
149
- function normalizedHotkeyStringFromParsed(
150
- parsed: ParsedHotkey,
151
- platform: 'mac' | 'windows' | 'linux',
152
- ): Hotkey {
153
- if (parsed.code === undefined) assertLogicalKey(parsed.key)
154
- const canUseMod =
155
- platform === 'mac'
156
- ? parsed.meta && !parsed.ctrl
157
- : parsed.ctrl && !parsed.meta
158
-
159
- const parts: Array<string> = []
160
-
161
- if (canUseMod) {
162
- parts.push('Mod')
163
- if (parsed.alt) {
164
- parts.push('Alt')
165
- }
166
- if (parsed.shift) {
167
- parts.push('Shift')
168
- }
169
- } else {
170
- for (const modifier of MODIFIER_ORDER) {
171
- if (parsed.modifiers.includes(modifier)) {
172
- parts.push(modifier)
173
- }
174
- }
175
- }
176
-
177
- parts.push(
178
- parsed.code !== undefined
179
- ? `[${parsed.code}]`
180
- : normalizeKeyName(parsed.key),
181
- )
182
- return parts.join('+') as Hotkey
183
- }
184
-
185
- /**
186
- * Normalizes a hotkey string to its canonical form.
187
- *
188
- * - When `Mod` is allowed for the platform (Command on Mac without Control;
189
- * Control on Windows/Linux without Meta): emits `Mod`, then `Alt`, then `Shift`,
190
- * then the key (e.g. `Mod+Shift+E`, `Mod+S`).
191
- * - Otherwise: literal modifiers in `Control+Alt+Shift+Meta` order, then the key.
192
- * - Resolves aliases and normalizes key casing (e.g. `esc` → `Escape`, `a` → `A`).
193
- *
194
- * @param hotkey - The hotkey string to normalize
195
- * @param platform - The target platform for resolving `Mod` (defaults to auto-detection)
196
- * @returns The normalized hotkey string
197
- *
198
- * @example
199
- * ```ts
200
- * normalizeHotkey('shift+meta+e', 'mac') // 'Mod+Shift+E'
201
- * normalizeHotkey('ctrl+a', 'windows') // 'Mod+A'
202
- * normalizeHotkey('esc') // 'Escape'
203
- * ```
204
- */
205
- export function normalizeHotkey(
206
- hotkey: Key | (string & {}),
207
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
208
- ): Hotkey {
209
- return normalizedHotkeyStringFromParsed(
210
- parseHotkey(hotkey, platform),
211
- platform,
212
- )
213
- }
214
-
215
- /**
216
- * Same canonical string as {@link normalizeHotkey}, but from an already-parsed hotkey.
217
- */
218
- export function normalizeHotkeyFromParsed(
219
- parsed: ParsedHotkey,
220
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
221
- ): Hotkey {
222
- return normalizedHotkeyStringFromParsed(parsed, platform)
223
- }
224
-
225
- /**
226
- * Normalizes a string or {@link RawHotkey} object to the same canonical hotkey string.
227
- * Use this in framework adapters instead of branching on `formatHotkey(rawHotkeyToParsedHotkey(...))`.
228
- */
229
- export function normalizeRegisterableHotkey(
230
- hotkey: RegisterableHotkey,
231
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
232
- ): Hotkey {
233
- return typeof hotkey === 'string'
234
- ? normalizeHotkey(hotkey, platform)
235
- : normalizeHotkeyFromParsed(
236
- rawHotkeyToParsedHotkey(hotkey, platform),
237
- platform,
238
- )
239
- }
240
-
241
- /**
242
- * Checks if a string is a recognized modifier token (including aliases).
243
- *
244
- * For a `KeyboardEvent`, use `isModifier(normalizeKeyName(event.key))` so DOM
245
- * spellings like `OS` / `Win` match the same alias table.
246
- *
247
- * @param key - Key name or alias (e.g. from a hotkey string or `event.key`)
248
- * @returns True if the string is a recognized modifier
249
- */
250
- export function isModifierKey(
251
- key: Key | (string & {}),
252
- ): key is keyof typeof MODIFIER_ALIASES {
253
- return key in MODIFIER_ALIASES || key.toLowerCase() in MODIFIER_ALIASES
254
- }
255
-
256
- /**
257
- * Parses a KeyboardEvent into a ParsedHotkey object.
258
- *
259
- * This function extracts the key and modifier state from a keyboard event
260
- * and converts it into the same format used by `parseHotkey()`.
261
- *
262
- * @param event - The KeyboardEvent to parse
263
- * @param platform - The target platform for resolving modifiers (defaults to auto-detection)
264
- * @returns A ParsedHotkey object representing the keyboard event
265
- *
266
- * @example
267
- * ```ts
268
- * document.addEventListener('keydown', (event) => {
269
- * const parsed = parseKeyboardEvent(event)
270
- * console.log(parsed) // { key: 'S', ctrl: true, shift: false, ... }
271
- * })
272
- * ```
273
- */
274
- export function parseKeyboardEvent(
275
- event: KeyboardEvent,
276
- platform?: 'mac' | 'windows' | 'linux',
277
- ): ParsedHotkey {
278
- const normalized = normalizeKeyboardEvent(event, platform)
279
- // AltGraph is a character-production modifier, not an ordinary Control+Alt
280
- // shortcut. Record the produced glyph instead of a synthetic Ctrl+Alt chord.
281
- const ctrl = normalized.altGraph ? false : normalized.ctrl
282
- const alt = normalized.altGraph ? false : normalized.alt
283
- // Shift is a real modifier even during AltGraph input; preserve it for replay.
284
- const shift = normalized.shift
285
-
286
- // Build modifiers array in canonical order
287
- const modifiers: Array<CanonicalModifier> = []
288
- if (ctrl) modifiers.push('Control')
289
- if (alt) modifiers.push('Alt')
290
- if (shift) modifiers.push('Shift')
291
- if (normalized.meta) modifiers.push('Meta')
292
-
293
- return {
294
- key: normalized.key,
295
- ctrl,
296
- shift,
297
- alt,
298
- meta: normalized.meta,
299
- modifiers,
300
- }
301
- }
302
-
303
- /**
304
- * Normalizes a keyboard event to the same canonical hotkey string as {@link normalizeHotkey}.
305
- *
306
- * @param event - The keyboard event (typically `keydown`)
307
- * @param platform - Target platform for `Mod` eligibility
308
- */
309
- export function normalizeHotkeyFromEvent(
310
- event: KeyboardEvent,
311
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
312
- ): Hotkey {
313
- return normalizedHotkeyStringFromParsed(
314
- parseKeyboardEvent(event, platform),
315
- platform,
316
- )
317
- }
318
-
319
- /**
320
- * Checks if a hotkey or ParsedHotkey contains at least one non-modifier key.
321
- *
322
- * This is useful for validating that a recorded hotkey is complete and not
323
- * just a combination of modifiers without an action key.
324
- *
325
- * @param hotkey - The hotkey string or ParsedHotkey to check
326
- * @param platform - The target platform for parsing (defaults to auto-detection)
327
- * @returns True if the hotkey contains at least one non-modifier key
328
- *
329
- * @example
330
- * ```ts
331
- * hasNonModifierKey('Control+Shift+S') // true
332
- * hasNonModifierKey('Control+Shift') // false (no action key)
333
- * hasNonModifierKey(parseHotkey('Mod+A')) // true
334
- * ```
335
- */
336
- export function hasNonModifierKey(
337
- hotkey: Hotkey | ParsedHotkey | (string & {}),
338
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
339
- ): boolean {
340
- const parsed =
341
- typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey
342
-
343
- if (parsed.code !== undefined) {
344
- // Physical modifier codes identify a side, but still are not action keys.
345
- return (
346
- parsed.code.length > 0 &&
347
- !/^(Control|Shift|Alt|Meta)(Left|Right)$/.test(parsed.code)
348
- )
349
- }
350
-
351
- // Check if the key part is actually a modifier
352
- const keyIsModifier = isModifierKey(parsed.key)
353
-
354
- // A valid hotkey must have a non-modifier key
355
- return !keyIsModifier && parsed.key.length > 0
356
- }
357
-
358
- /**
359
- * Parses a string or raw binding, resolving Mod for the supplied platform.
360
- * Physical tokens such as `[KeyQ]` retain `code`; they never become logical Q.
361
- */
362
- export function parseRegisterableHotkey(
363
- hotkey: RegisterableHotkey,
364
- platform = detectPlatform(),
365
- ): ParsedHotkey {
366
- return typeof hotkey === 'string'
367
- ? parseHotkey(hotkey, platform)
368
- : rawHotkeyToParsedHotkey(hotkey, platform)
369
- }
package/src/platform.ts DELETED
@@ -1,66 +0,0 @@
1
- import type { CanonicalModifier } from './key.types'
2
-
3
- /**
4
- * Detects the current platform based on browser navigator properties.
5
- *
6
- * Used internally to resolve platform-adaptive modifiers like 'Mod' (Command on Mac,
7
- * Control elsewhere) and for platform-specific hotkey formatting.
8
- *
9
- * @returns The detected platform: 'mac', 'windows', or 'linux'
10
- * @remarks Defaults to 'linux' in SSR environments where navigator is undefined
11
- *
12
- * @example
13
- * ```ts
14
- * const platform = detectPlatform() // 'mac' | 'windows' | 'linux'
15
- * const modifier = resolveModifier('Mod', platform) // 'Meta' on Mac, 'Control' elsewhere
16
- * ```
17
- */
18
- export function detectPlatform(): 'mac' | 'windows' | 'linux' {
19
- if (typeof navigator === 'undefined') {
20
- return 'linux' // Default for SSR
21
- }
22
-
23
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
24
- const platform = navigator.platform?.toLowerCase() ?? ''
25
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
26
- const userAgent = navigator.userAgent?.toLowerCase() ?? ''
27
-
28
- if (platform.includes('mac') || userAgent.includes('mac')) {
29
- return 'mac'
30
- }
31
- if (platform.includes('win') || userAgent.includes('win')) {
32
- return 'windows'
33
- }
34
- return 'linux'
35
- }
36
-
37
- /**
38
- * Resolves the platform-adaptive 'Mod' modifier to the appropriate canonical modifier.
39
- *
40
- * The 'Mod' token represents the "primary modifier" on each platform:
41
- * - macOS: 'Meta' (Command key ⌘)
42
- * - Windows/Linux: 'Control' (Ctrl key)
43
- *
44
- * This enables cross-platform hotkey definitions like 'Mod+S' that automatically
45
- * map to Command+S on Mac and Ctrl+S on Windows/Linux.
46
- *
47
- * @param modifier - The modifier to resolve. If 'Mod', resolves based on platform.
48
- * @param platform - The target platform. Defaults to auto-detection.
49
- * @returns The canonical modifier name ('Control', 'Shift', 'Alt', or 'Meta')
50
- *
51
- * @example
52
- * ```ts
53
- * resolveModifier('Mod', 'mac') // 'Meta'
54
- * resolveModifier('Mod', 'windows') // 'Control'
55
- * resolveModifier('Control', 'mac') // 'Control' (unchanged)
56
- * ```
57
- */
58
- export function resolveModifier(
59
- modifier: CanonicalModifier | 'Mod',
60
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
61
- ): CanonicalModifier {
62
- if (modifier === 'Mod') {
63
- return platform === 'mac' ? 'Meta' : 'Control'
64
- }
65
- return modifier
66
- }
@@ -1,34 +0,0 @@
1
- import type { Hotkey, ParsedHotkey } from './hotkey.types'
2
- import type { HotkeyConflict, HotkeyConflictOptions } from './conflicts'
3
-
4
- /** Event property used to record a binding. Physical codes are the default. */
5
- export type RecorderKeyMode = 'code' | 'key'
6
-
7
- export interface RecorderRejection {
8
- reason: 'missing-code' | 'alt-graph' | 'invalid' | 'conflict' | 'validation'
9
- message: string
10
- hotkey?: Hotkey
11
- sequence?: Array<Hotkey>
12
- conflicts?: Array<HotkeyConflict>
13
- }
14
-
15
- export interface RecorderOptions {
16
- /** Physical code recording is the default. Choose key to record logical characters. */
17
- recordBy?: RecorderKeyMode
18
- /** Platform used for Mod and AltGraph handling. Defaults to platform detection. */
19
- platform?: 'mac' | 'windows' | 'linux'
20
- /** Opt in to live-registry conflicts, including sequence prefixes. Default: false. */
21
- detectConflicts?: boolean | HotkeyConflictOptions
22
- /** Rejected candidates leave recording active. */
23
- onReject?: (rejection: RecorderRejection) => void
24
- }
25
-
26
- export interface HotkeyRecorderValidationContext {
27
- event: KeyboardEvent
28
- parsedHotkey: ParsedHotkey
29
- }
30
-
31
- export interface HotkeySequenceRecorderValidationContext {
32
- events: ReadonlyArray<KeyboardEvent>
33
- parsedSequence: ReadonlyArray<ParsedHotkey>
34
- }