hotkey-router 0.0.3 โ†’ 0.2.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.
package/README.md CHANGED
@@ -1,333 +1,479 @@
1
- # hotkey-router
2
-
3
- [![npm version](https://img.shields.io/npm/v/hotkey-router.svg)](https://www.npmjs.com/package/hotkey-router)
4
- [![bundle size](https://img.shields.io/bundlephobia/minzip/hotkey-router)](https://bundlephobia.com/package/hotkey-router)
5
- [![GitHub stars](https://img.shields.io/github/stars/iWhatty/hotkey-router-js?style=social)](https://github.com/iWhatty/hotkey-router-js)
6
-
7
- **A tiny, deterministic keyboard routing engine for modern web apps.**
8
-
9
- Hotkey Router is not a key utility.
10
- It is a predictable, plugin-first routing layer for keyboard shortcuts.
11
-
12
- * โšก O(1) dispatch
13
- * ๐Ÿงฉ Plugin-safe lifecycle management
14
- * ๐ŸŽฏ Deterministic winner selection (priority + recency)
15
- * ๐Ÿ›‘ Input-safe by default
16
- * ๐Ÿงช Testable via `trigger()`
17
- * ๐Ÿ“ฆ 4.9 kB minified
18
- * ๐Ÿ—œ 2.2 kB minified + gzipped
19
- * ๐Ÿšซ Zero dependencies
20
-
21
- ---
22
-
23
- ## Philosophy
24
-
25
- Hotkey Router follows three core rules:
26
-
27
- 1. **Predictable routing** โ€” Highest priority wins. Ties go to the most recently bound handler.
28
- 2. **Safe composition** โ€” Plugins can register and unregister without affecting others.
29
- 3. **Modern only** โ€” Built for modern browsers using `KeyboardEvent.key`.
30
-
31
- No keycodes. No legacy IE hacks. No hidden global scope state.
32
-
33
- ---
34
-
35
- ## Install
36
-
37
- ```bash
38
- npm install hotkey-router
39
- ```
40
-
41
- ### ESM
42
-
43
- ```js
44
- import hotkeys from 'hotkey-router'
45
- ```
46
-
47
- ### CommonJS
48
-
49
- ```js
50
- const hotkeys = require('hotkey-router')
51
- ```
52
-
53
- ### CDN (ESM)
54
-
55
- ```js
56
- import hotkeys from 'https://cdn.jsdelivr.net/npm/hotkey-router/dist/hotkey-router.min.js'
57
- ```
58
-
59
- ---
60
-
61
- # Basic Usage
62
-
63
- ```js
64
- import hotkeys from 'hotkey-router'
65
-
66
- // Simple keydown
67
- hotkeys.bind('ctrl+k', () => {
68
- openCommandPalette()
69
- })
70
-
71
- // Keyup using " up" suffix
72
- hotkeys.bind('ctrl+p up', () => {
73
- console.log('Released CTRL+P')
74
- })
75
-
76
- // AHK-style modifiers also supported
77
- hotkeys.bind('^k', () => {
78
- openCommandPalette() // ctrl+k
79
- })
80
-
81
- // Plugin grouping
82
- hotkeys.registerPlugin('docs', {
83
- 'ctrl+f': openSearch,
84
- 'escape': closeSearch,
85
- })
86
- ```
87
-
88
- ---
89
-
90
- # Routing Model
91
-
92
- When multiple handlers match the same hotkey:
93
-
94
- 1. Highest `priority` wins
95
- 2. If equal priority โ†’ newest binding wins
96
-
97
- This makes modal overrides simple:
98
-
99
- ```js
100
- hotkeys.bind('escape', closeModal, null, {
101
- priority: 100,
102
- preventDefault: true,
103
- })
104
- ```
105
-
106
- ---
107
-
108
- # API
109
-
110
- ## `bind(hotkey, handler, plugin?, options?)`
111
-
112
- Register a hotkey.
113
-
114
- Returns an `off()` function.
115
-
116
- ```js
117
- const off = hotkeys.bind('mod+k', openPalette)
118
-
119
- // Later
120
- off()
121
- ```
122
-
123
- ### Options
124
-
125
- ```ts
126
- {
127
- preventDefault?: boolean
128
- stopPropagation?: boolean
129
- stopImmediatePropagation?: boolean
130
- repeat?: boolean // default false on keydown
131
- once?: boolean
132
- when?: (event) => boolean
133
- allowIn?: (event) => boolean
134
- priority?: number // default 0
135
- }
136
- ```
137
-
138
- ### Examples
139
-
140
- Ignore repeat keydown (default behavior):
141
-
142
- ```js
143
- hotkeys.bind('j', nextItem)
144
- ```
145
-
146
- Allow inside inputs:
147
-
148
- ```js
149
- hotkeys.bind('mod+k', openPalette, null, {
150
- allowIn: () => true,
151
- preventDefault: true,
152
- })
153
- ```
154
-
155
- Conditional binding:
156
-
157
- ```js
158
- hotkeys.bind('delete', deleteItem, null, {
159
- when: () => selectionCount() > 0,
160
- })
161
- ```
162
-
163
- Run once:
164
-
165
- ```js
166
- hotkeys.bind('ctrl+s', saveDraft, null, { once: true })
167
- ```
168
-
169
- ---
170
-
171
- ## `unbind(hotkey, handler?)`
172
-
173
- Remove bindings.
174
-
175
- ```js
176
- hotkeys.unbind('ctrl+k')
177
- hotkeys.unbind('ctrl+k', openPalette)
178
- ```
179
-
180
- ---
181
-
182
- ## `registerPlugin(name, map)`
183
-
184
- Batch register hotkeys under a plugin namespace.
185
-
186
- ```js
187
- const unregister = hotkeys.registerPlugin('files', {
188
- 'mod+o': openFile,
189
- 'delete': deleteFile,
190
- })
191
-
192
- // Later
193
- unregister()
194
- ```
195
-
196
- Plugin cleanup is isolated โ€” removing one plugin never affects other bindings.
197
-
198
- ---
199
-
200
- ## `unregisterPlugin(name)`
201
-
202
- Remove all hotkeys associated with a plugin.
203
-
204
- ---
205
-
206
- ## `pause()` / `resume()`
207
-
208
- Temporarily disable or re-enable all routing.
209
-
210
- ---
211
-
212
- ## `ignoreInput(boolean = true)`
213
-
214
- By default, hotkeys do **not** fire inside:
215
-
216
- * `<input>`
217
- * `<textarea>`
218
- * `<select>`
219
- * `[contenteditable]`
220
- * `role="textbox"`
221
-
222
- Override per-binding with `allowIn()`.
223
-
224
- ---
225
-
226
- ## `init(options?)`
227
-
228
- Manually attach listeners.
229
-
230
- ```js
231
- hotkeys.init({
232
- target: window,
233
- capture: false,
234
- })
235
- ```
236
-
237
- Auto-initializes on `window` by default (browser environments).
238
-
239
- ---
240
-
241
- ## `destroy()`
242
-
243
- Removes all listeners and clears internal state.
244
-
245
- ---
246
-
247
- ## `trigger(hotkey, options?)`
248
-
249
- Programmatically trigger a hotkey.
250
- Useful for testing.
251
-
252
- ```js
253
- hotkeys.trigger('ctrl+k')
254
- ```
255
-
256
- Returns `true` if a handler ran.
257
-
258
- ---
259
-
260
- # Supported Syntax
261
-
262
- ## Standard
263
-
264
- * `ctrl+k`
265
- * `shift+a`
266
- * `ctrl+k up`
267
- * `mod+s` (meta on macOS, ctrl elsewhere)
268
- * `ctrl++` or `ctrl+plus`
269
-
270
- ## AHK-Style Prefix Modifiers
271
-
272
- * `^k` โ†’ `ctrl+k`
273
- * `!k` โ†’ `alt+k`
274
- * `+k` โ†’ `shift+k`
275
- * `#k` โ†’ `meta+k`
276
- * `^!k` โ†’ `ctrl+alt+k`
277
-
278
- Modifiers must appear before the base key.
279
-
280
- ## Aliases Supported
281
-
282
- ### Modifiers
283
-
284
- * `ctrl`, `control`, `โŒƒ`
285
- * `shift`, `โ‡ง`, `+`
286
- * `alt`, `option`, `โŒฅ`, `!`
287
- * `meta`, `cmd`, `command`, `win`, `โŒ˜`, `#`
288
- * `mod` (meta on macOS, ctrl elsewhere)
289
-
290
- ### Navigation / Special Keys
291
-
292
- * `escape`, `esc`
293
- * `enter`, `return`
294
- * `space`
295
- * `tab`
296
- * `backspace`
297
- * `delete`, `del`
298
- * `home`, `end`
299
- * `pageup`, `pgup`
300
- * `pagedown`, `pgdn`
301
- * `up`, `down`, `left`, `right`
302
- * `f1`โ€“`f19`
303
-
304
- Keys are case-insensitive.
305
-
306
- ---
307
-
308
- # What This Is Not
309
-
310
- * Not a keycode polyfill
311
- * Not a legacy browser shim
312
- * Not a global scope manager
313
- * Not a VSCode-style sequence engine
314
-
315
- This is a small, deterministic routing layer for modern applications.
316
-
317
- ---
318
-
319
- # Browser Support
320
-
321
- Modern browsers supporting:
322
-
323
- * `KeyboardEvent.key`
324
- * `Map`
325
- * `addEventListener`
326
-
327
- Chrome, Firefox, Safari, Edge.
328
-
329
- ---
330
-
331
- # License
332
-
333
- See LICENSE file for details.
1
+ # hotkey-router
2
+
3
+ [![npm version](https://img.shields.io/npm/v/hotkey-router.svg)](https://www.npmjs.com/package/hotkey-router)
4
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/hotkey-router)](https://bundlephobia.com/package/hotkey-router)
5
+ [![GitHub stars](https://img.shields.io/github/stars/iWhatty/hotkey-router-js?style=social)](https://github.com/iWhatty/hotkey-router-js)
6
+
7
+ **A tiny, deterministic keyboard routing engine for modern web apps.**
8
+
9
+ Hotkey Router is not a key utility.
10
+ It is a predictable, plugin-first routing layer for keyboard shortcuts.
11
+
12
+ * โšก O(1) dispatch
13
+ * ๐Ÿงฉ Plugin-safe lifecycle management
14
+ * ๐ŸŽฏ Deterministic winner selection (priority + recency)
15
+ * ๐Ÿ›‘ Input-safe by default
16
+ * ๐Ÿงช Testable via `trigger()`
17
+ * ๐Ÿšจ Optional opt-in browser/OS conflict warnings (tree-shakable)
18
+ * ๐Ÿ“ฆ ~3 kB minified + gzipped (core); ~7.5 kB with conflict warnings
19
+ * ๐Ÿšซ Zero dependencies
20
+
21
+ ---
22
+
23
+ ## Philosophy
24
+
25
+ Hotkey Router follows three core rules:
26
+
27
+ 1. **Predictable routing** โ€” Highest priority wins. Ties go to the most recently bound handler.
28
+ 2. **Safe composition** โ€” Plugins can register and unregister without affecting others.
29
+ 3. **Modern only** โ€” Built for modern browsers using `KeyboardEvent.key`.
30
+
31
+ No keycodes. No legacy IE hacks. No hidden global scope state.
32
+
33
+ ---
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ npm install hotkey-router
39
+ ```
40
+
41
+ ### ESM
42
+
43
+ ```js
44
+ import hotkeys from 'hotkey-router'
45
+ ```
46
+
47
+ ### CommonJS
48
+
49
+ ```js
50
+ const hotkeys = require('hotkey-router')
51
+ ```
52
+
53
+ ### CDN (ESM)
54
+
55
+ ```js
56
+ import hotkeys from 'https://cdn.jsdelivr.net/npm/hotkey-router/dist/hotkey-router.min.js'
57
+ ```
58
+
59
+ ---
60
+
61
+ # Basic Usage
62
+
63
+ ```js
64
+ import hotkeys from 'hotkey-router'
65
+
66
+ // Simple keydown
67
+ hotkeys.bind('ctrl+k', () => {
68
+ openCommandPalette()
69
+ })
70
+
71
+ // Keyup using " up" suffix
72
+ hotkeys.bind('ctrl+p up', () => {
73
+ console.log('Released CTRL+P')
74
+ })
75
+
76
+ // AHK-style modifiers also supported
77
+ hotkeys.bind('^k', () => {
78
+ openCommandPalette() // ctrl+k
79
+ })
80
+
81
+ // Bare-modifier bindings (Alt-as-mode UX)
82
+ hotkeys.bind('alt', enterSelectMode) // fires on Alt keydown
83
+ hotkeys.bind('alt up', exitSelectMode) // fires on Alt keyup
84
+
85
+ // Layout-stable matching via KeyboardEvent.code (cross-platform Alt+letter
86
+ // โ€” works on macOS where Option remaps Alt+X -> โ‰ˆ)
87
+ hotkeys.bind('alt+code:KeyX', deleteHovered, null, { preventDefault: true })
88
+
89
+ // Plugin grouping
90
+ hotkeys.registerPlugin('docs', {
91
+ 'ctrl+f': openSearch,
92
+ 'escape': closeSearch,
93
+ })
94
+ ```
95
+
96
+ ## Conflict warnings (v0.2.0+)
97
+
98
+ Some combos are reserved by the browser chrome (find bar, devtools, bookmarks) or the OS (Spotlight, window management) โ€” they never reach page-world JavaScript, no matter how early you listen or whether you call `preventDefault`. Hotkey Router ships an **opt-in** reservation table that emits a soft warning at bind time, so the silent failure becomes a noisy one.
99
+
100
+ The feature is opt-in by design: the core router stays ~3 KB gzipped, and the ~5 KB reservation data is tree-shaken out entirely unless you import it.
101
+
102
+ ### Two ways to opt in
103
+
104
+ **One-line ergonomic** โ€” use the `auto` entry. Same default export as `hotkey-router`, with warnings pre-installed:
105
+
106
+ ```js
107
+ import hotkeys from 'hotkey-router/auto'
108
+
109
+ hotkeys.bind('meta+shift+f', toggleFullscreen)
110
+ // Firefox on macOS:
111
+ // [hotkey-router] "meta+shift+f" reserved by firefox on macOS:
112
+ // "Toggle fullscreen" [hard] โ€” will not fire.
113
+ ```
114
+
115
+ **Explicit install** โ€” keep using the tiny core, install warnings yourself:
116
+
117
+ ```js
118
+ import hotkeys from 'hotkey-router'
119
+ import { installReservationWarnings } from 'hotkey-router/reservations'
120
+
121
+ installReservationWarnings(hotkeys)
122
+ hotkeys.bind('meta+shift+f', toggleFullscreen)
123
+ ```
124
+
125
+ `installReservationWarnings()` returns an `uninstall` function if you ever need to detach. The warning is **never fatal** โ€” the binding is still registered, in case you're running in a browser/platform where the conflict doesn't apply.
126
+
127
+ ### Severity โ†’ log level
128
+
129
+ | Severity | Log level | Meaning |
130
+ | ----------------- | --------- | ------------------------------------------------------- |
131
+ | `hard` | `warn` | Browser intercepts before page world; combo won't fire. |
132
+ | `os` | `warn` | OS intercepts globally. |
133
+ | `menu-activation` | `warn` | Alt+letter activates the browser menu bar (Win/Linux). |
134
+ | `find-bar-only` | `info` | Reserved only when the find bar is focused. |
135
+ | `compose` | `info` | macOS Option+letter types a special char in inputs. |
136
+ | `system-text` | `info` | Mac Ctrl+letter cursor controls inside text inputs. |
137
+ | `devtools-open` | `info` | Only relevant when DevTools is already open. |
138
+
139
+ ### Per-bind opt-out
140
+
141
+ ```js
142
+ hotkeys.bind('meta+shift+f', toggleFullscreen, null, { warnOnReserved: false })
143
+ ```
144
+
145
+ To turn warnings off globally, just don't install them (use the bare `hotkey-router` entry instead of `hotkey-router/auto`).
146
+
147
+ ### Force a platform/browser (tests, SSR previews)
148
+
149
+ ```js
150
+ installReservationWarnings(hotkeys, { platform: 'mac', browser: 'firefox' })
151
+ ```
152
+
153
+ Accepted platforms: `'mac' | 'windows' | 'linux'`. Browsers: `'firefox' | 'chrome' | 'safari' | 'edge'`.
154
+
155
+ ### Caveats
156
+
157
+ - Reservations reflect **default** keybindings. Users with custom shortcuts (Edge 95+ rebinds, Firefox add-ons, OS-level customization) may not match.
158
+ - KDE/XFCE desktop reservations beyond GNOME are not yet catalogued โ€” Linux coverage is conservative.
159
+ - Layout-specific differences (Dvorak, AZERTY) change which physical key produces `event.key === 'f'`. For layout-stable bindings against the physical key, use `code:KeyX` syntax โ€” the reservation lookup normalizes both.
160
+
161
+ ### Programmatic lookup
162
+
163
+ If you want to query the table yourself (e.g. building a cheatsheet that flags conflicts):
164
+
165
+ ```js
166
+ import { lookupReservation } from 'hotkey-router/reservations'
167
+
168
+ const r = lookupReservation(
169
+ { meta: true, shift: true, key: 'f' },
170
+ { platform: 'mac', browser: 'firefox' }
171
+ )
172
+ // โ†’ { source: 'browser', action: 'Toggle fullscreen', severity: 'hard', ... }
173
+ ```
174
+
175
+ ### Extension hook (`onBind`)
176
+
177
+ Reservation warnings are built on a public `onBind` hook โ€” useful for telemetry, dev panels, or any cross-cutting concern that needs to observe every binding:
178
+
179
+ ```js
180
+ const off = hotkeys.onBind(({ combo, raw, options, plugin, id }) => {
181
+ // combo: parsed combo { ctrl, meta, alt, shift, key, code, bareModifier }
182
+ // raw: original hotkey string
183
+ })
184
+ // off() unsubscribes.
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Bare-modifier bindings (v0.1.0+)
190
+
191
+ Some UX patterns are driven by a bare modifier rather than a chord โ€” e.g. "hold Alt to enter select mode, release Alt to exit."
192
+
193
+ ```js
194
+ hotkeys.bind('alt', onAltDown) // fires on Alt keydown
195
+ hotkeys.bind('alt up', onAltUp) // fires on Alt keyup
196
+ hotkeys.bind('ctrl', onCtrlDown) // any single modifier supported
197
+ ```
198
+
199
+ Notes:
200
+ - Only **single** bare modifiers are supported. Multi-modifier bare bindings (`'ctrl+alt'`) throw โ€” add a base key for those.
201
+ - Default `repeat: false` applies, so a held modifier fires only once on keydown.
202
+ - Loose match: a bare-Alt binding fires whenever the Alt key is the one being pressed/released, regardless of which other modifier flags are also set. Add a `when` filter for exact-set semantics.
203
+
204
+ ## Code-based matching (v0.1.0+)
205
+
206
+ `KeyboardEvent.key` is layout- and modifier-dependent โ€” Alt+X gives `โ‰ˆ` on macOS, `x` on Linux/Windows. For shortcuts that should be stable across platforms, bind to `KeyboardEvent.code` instead:
207
+
208
+ ```js
209
+ hotkeys.bind('alt+code:KeyX', deleteHovered) // matches the physical X key
210
+ hotkeys.bind('ctrl+code:Digit1', goToTab1) // matches digit row, not numpad
211
+ hotkeys.bind('!code:KeyX', deleteHovered) // AHK shorthand also works
212
+ ```
213
+
214
+ The `code:` value is **case-sensitive** (matches the camelCase `KeyboardEvent.code` spec values: `KeyA`, `Digit1`, `ArrowLeft`, etc.). Only one `code:` token per binding is allowed; multiple `code:` tokens throw a parse error.
215
+
216
+ Both key-based and code-based bindings can coexist; the standard priority + recency rules pick the winner.
217
+
218
+ ---
219
+
220
+ # Routing Model
221
+
222
+ When multiple handlers match the same hotkey:
223
+
224
+ 1. Highest `priority` wins
225
+ 2. If equal priority โ†’ newest binding wins
226
+
227
+ This makes modal overrides simple:
228
+
229
+ ```js
230
+ hotkeys.bind('escape', closeModal, null, {
231
+ priority: 100,
232
+ preventDefault: true,
233
+ })
234
+ ```
235
+
236
+ ---
237
+
238
+ # API
239
+
240
+ ## `bind(hotkey, handler, plugin?, options?)`
241
+
242
+ Register a hotkey.
243
+
244
+ Returns an `off()` function.
245
+
246
+ ```js
247
+ const off = hotkeys.bind('mod+k', openPalette)
248
+
249
+ // Later
250
+ off()
251
+ ```
252
+
253
+ ### Options
254
+
255
+ ```ts
256
+ {
257
+ preventDefault?: boolean
258
+ stopPropagation?: boolean
259
+ stopImmediatePropagation?: boolean
260
+ repeat?: boolean // default false on keydown
261
+ once?: boolean
262
+ when?: (event) => boolean
263
+ allowIn?: (event) => boolean
264
+ priority?: number // default 0
265
+ warnOnReserved?: boolean // only read when conflict warnings are installed
266
+ }
267
+ ```
268
+
269
+ ### Examples
270
+
271
+ Ignore repeat keydown (default behavior):
272
+
273
+ ```js
274
+ hotkeys.bind('j', nextItem)
275
+ ```
276
+
277
+ Allow inside inputs:
278
+
279
+ ```js
280
+ hotkeys.bind('mod+k', openPalette, null, {
281
+ allowIn: () => true,
282
+ preventDefault: true,
283
+ })
284
+ ```
285
+
286
+ Conditional binding:
287
+
288
+ ```js
289
+ hotkeys.bind('delete', deleteItem, null, {
290
+ when: () => selectionCount() > 0,
291
+ })
292
+ ```
293
+
294
+ Run once:
295
+
296
+ ```js
297
+ hotkeys.bind('ctrl+s', saveDraft, null, { once: true })
298
+ ```
299
+
300
+ ---
301
+
302
+ ## `unbind(hotkey, handler?)`
303
+
304
+ Remove bindings.
305
+
306
+ ```js
307
+ hotkeys.unbind('ctrl+k')
308
+ hotkeys.unbind('ctrl+k', openPalette)
309
+ ```
310
+
311
+ ---
312
+
313
+ ## `registerPlugin(name, map)`
314
+
315
+ Batch register hotkeys under a plugin namespace.
316
+
317
+ ```js
318
+ const unregister = hotkeys.registerPlugin('files', {
319
+ 'mod+o': openFile,
320
+ 'delete': deleteFile,
321
+ })
322
+
323
+ // Later
324
+ unregister()
325
+ ```
326
+
327
+ Plugin cleanup is isolated โ€” removing one plugin never affects other bindings.
328
+
329
+ ---
330
+
331
+ ## `unregisterPlugin(name)`
332
+
333
+ Remove all hotkeys associated with a plugin.
334
+
335
+ ---
336
+
337
+ ## `onBind(hook)`
338
+
339
+ Subscribe to bind events. The hook receives `{ combo, raw, options, plugin, id }` after every successful `bind()`. Returns an unsubscribe function. Used internally by `installReservationWarnings`; exposed for telemetry, dev panels, and custom validation.
340
+
341
+ ```js
342
+ const off = hotkeys.onBind(({ raw }) => {
343
+ console.debug('bound:', raw)
344
+ })
345
+ // off() unsubscribes.
346
+ ```
347
+
348
+ Hook errors are caught and logged via `console.error` โ€” a buggy hook can't break the bind.
349
+
350
+ ---
351
+
352
+ ## `pause()` / `resume()`
353
+
354
+ Temporarily disable or re-enable all routing.
355
+
356
+ ---
357
+
358
+ ## `ignoreInput(boolean = true)`
359
+
360
+ By default, hotkeys do **not** fire inside:
361
+
362
+ * `<input>`
363
+ * `<textarea>`
364
+ * `<select>`
365
+ * `[contenteditable]`
366
+ * `role="textbox"`
367
+
368
+ Override per-binding with `allowIn()`.
369
+
370
+ ---
371
+
372
+ ## `init(options?)`
373
+
374
+ Manually attach listeners.
375
+
376
+ ```js
377
+ hotkeys.init({
378
+ target: window,
379
+ capture: false,
380
+ })
381
+ ```
382
+
383
+ Auto-initializes on `window` by default (browser environments).
384
+
385
+ ---
386
+
387
+ ## `destroy()`
388
+
389
+ Removes all listeners and clears internal state.
390
+
391
+ ---
392
+
393
+ ## `trigger(hotkey, options?)`
394
+
395
+ Programmatically trigger a hotkey.
396
+ Useful for testing.
397
+
398
+ ```js
399
+ hotkeys.trigger('ctrl+k')
400
+ ```
401
+
402
+ Returns `true` if a handler ran.
403
+
404
+ ---
405
+
406
+ # Supported Syntax
407
+
408
+ ## Standard
409
+
410
+ * `ctrl+k`
411
+ * `shift+a`
412
+ * `ctrl+k up`
413
+ * `mod+s` (meta on macOS, ctrl elsewhere)
414
+ * `ctrl++` or `ctrl+plus`
415
+
416
+ ## AHK-Style Prefix Modifiers
417
+
418
+ * `^k` โ†’ `ctrl+k`
419
+ * `!k` โ†’ `alt+k`
420
+ * `+k` โ†’ `shift+k`
421
+ * `#k` โ†’ `meta+k`
422
+ * `^!k` โ†’ `ctrl+alt+k`
423
+
424
+ Modifiers must appear before the base key.
425
+
426
+ ## Aliases Supported
427
+
428
+ ### Modifiers
429
+
430
+ * `ctrl`, `control`, `โŒƒ`
431
+ * `shift`, `โ‡ง`, `+`
432
+ * `alt`, `option`, `โŒฅ`, `!`
433
+ * `meta`, `cmd`, `command`, `win`, `โŒ˜`, `#`
434
+ * `mod` (meta on macOS, ctrl elsewhere)
435
+
436
+ ### Navigation / Special Keys
437
+
438
+ * `escape`, `esc`
439
+ * `enter`, `return`
440
+ * `space`
441
+ * `tab`
442
+ * `backspace`
443
+ * `delete`, `del`
444
+ * `home`, `end`
445
+ * `pageup`, `pgup`
446
+ * `pagedown`, `pgdn`
447
+ * `up`, `down`, `left`, `right`
448
+ * `f1`โ€“`f19`
449
+
450
+ Keys are case-insensitive.
451
+
452
+ ---
453
+
454
+ # What This Is Not
455
+
456
+ * Not a keycode polyfill
457
+ * Not a legacy browser shim
458
+ * Not a global scope manager
459
+ * Not a VSCode-style sequence engine
460
+
461
+ This is a small, deterministic routing layer for modern applications.
462
+
463
+ ---
464
+
465
+ # Browser Support
466
+
467
+ Modern browsers supporting:
468
+
469
+ * `KeyboardEvent.key`
470
+ * `Map`
471
+ * `addEventListener`
472
+
473
+ Chrome, Firefox, Safari, Edge.
474
+
475
+ ---
476
+
477
+ # License
478
+
479
+ See LICENSE file for details.