hotkey-router 0.2.0 โ†’ 0.2.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.
package/README.md CHANGED
@@ -1,56 +1,45 @@
1
1
  # hotkey-router
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/hotkey-router.svg)](https://www.npmjs.com/package/hotkey-router)
3
+ [![npm](https://img.shields.io/npm/v/hotkey-router)](https://www.npmjs.com/package/hotkey-router)
4
+ [![downloads](https://img.shields.io/npm/dm/hotkey-router)](https://www.npmjs.com/package/hotkey-router)
4
5
  [![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
+ [![license](https://img.shields.io/npm/l/hotkey-router)](https://github.com/iWhatty/hotkey-router-js/blob/main/LICENSE)
7
+ [![stars](https://img.shields.io/github/stars/iWhatty/hotkey-router-js?style=social)](https://github.com/iWhatty/hotkey-router-js)
6
8
 
7
- **A tiny, deterministic keyboard routing engine for modern web apps.**
9
+ A tiny, deterministic keyboard routing engine for modern web apps. Not a key utility, a predictable plugin-first routing layer for keyboard shortcuts.
8
10
 
9
- Hotkey Router is not a key utility.
10
- It is a predictable, plugin-first routing layer for keyboard shortcuts.
11
+ ## Features
11
12
 
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.
13
+ - O(1) dispatch
14
+ - Plugin-safe lifecycle management
15
+ - Deterministic winner selection (priority + recency)
16
+ - Input-safe by default
17
+ - Testable via `trigger()`
18
+ - Optional opt-in browser/OS conflict warnings (tree-shakable)
19
+ - ~3 kB minified + gzipped (core); ~7.5 kB with conflict warnings
20
+ - Zero dependencies
32
21
 
33
22
  ---
34
23
 
35
24
  ## Install
36
25
 
37
- ```bash
38
- npm install hotkey-router
26
+ ```sh
27
+ pnpm add hotkey-router
39
28
  ```
40
29
 
41
- ### ESM
30
+ ESM:
42
31
 
43
32
  ```js
44
33
  import hotkeys from 'hotkey-router'
45
34
  ```
46
35
 
47
- ### CommonJS
36
+ CommonJS:
48
37
 
49
38
  ```js
50
39
  const hotkeys = require('hotkey-router')
51
40
  ```
52
41
 
53
- ### CDN (ESM)
42
+ CDN (ESM):
54
43
 
55
44
  ```js
56
45
  import hotkeys from 'https://cdn.jsdelivr.net/npm/hotkey-router/dist/hotkey-router.min.js'
@@ -58,7 +47,7 @@ import hotkeys from 'https://cdn.jsdelivr.net/npm/hotkey-router/dist/hotkey-rout
58
47
 
59
48
  ---
60
49
 
61
- # Basic Usage
50
+ ## Quick start
62
51
 
63
52
  ```js
64
53
  import hotkeys from 'hotkey-router'
@@ -82,8 +71,8 @@ hotkeys.bind('^k', () => {
82
71
  hotkeys.bind('alt', enterSelectMode) // fires on Alt keydown
83
72
  hotkeys.bind('alt up', exitSelectMode) // fires on Alt keyup
84
73
 
85
- // Layout-stable matching via KeyboardEvent.code (cross-platform Alt+letter
86
- // โ€” works on macOS where Option remaps Alt+X -> โ‰ˆ)
74
+ // Layout-stable matching via KeyboardEvent.code (cross-platform Alt+letter,
75
+ // works on macOS where Option remaps Alt+X to โ‰ˆ)
87
76
  hotkeys.bind('alt+code:KeyX', deleteHovered, null, { preventDefault: true })
88
77
 
89
78
  // Plugin grouping
@@ -93,155 +82,13 @@ hotkeys.registerPlugin('docs', {
93
82
  })
94
83
  ```
95
84
 
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
85
  ---
188
86
 
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.
87
+ ## API
203
88
 
204
- ## Code-based matching (v0.1.0+)
89
+ ### `bind(hotkey, handler, plugin?, options?)`
205
90
 
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.
91
+ Register a hotkey. Returns an `off()` function.
245
92
 
246
93
  ```js
247
94
  const off = hotkeys.bind('mod+k', openPalette)
@@ -250,7 +97,7 @@ const off = hotkeys.bind('mod+k', openPalette)
250
97
  off()
251
98
  ```
252
99
 
253
- ### Options
100
+ **Options**
254
101
 
255
102
  ```ts
256
103
  {
@@ -266,7 +113,7 @@ off()
266
113
  }
267
114
  ```
268
115
 
269
- ### Examples
116
+ **Examples**
270
117
 
271
118
  Ignore repeat keydown (default behavior):
272
119
 
@@ -297,9 +144,7 @@ Run once:
297
144
  hotkeys.bind('ctrl+s', saveDraft, null, { once: true })
298
145
  ```
299
146
 
300
- ---
301
-
302
- ## `unbind(hotkey, handler?)`
147
+ ### `unbind(hotkey, handler?)`
303
148
 
304
149
  Remove bindings.
305
150
 
@@ -308,9 +153,7 @@ hotkeys.unbind('ctrl+k')
308
153
  hotkeys.unbind('ctrl+k', openPalette)
309
154
  ```
310
155
 
311
- ---
312
-
313
- ## `registerPlugin(name, map)`
156
+ ### `registerPlugin(name, map)`
314
157
 
315
158
  Batch register hotkeys under a plugin namespace.
316
159
 
@@ -324,17 +167,13 @@ const unregister = hotkeys.registerPlugin('files', {
324
167
  unregister()
325
168
  ```
326
169
 
327
- Plugin cleanup is isolated โ€” removing one plugin never affects other bindings.
328
-
329
- ---
170
+ Plugin cleanup is isolated. Removing one plugin never affects other bindings.
330
171
 
331
- ## `unregisterPlugin(name)`
172
+ ### `unregisterPlugin(name)`
332
173
 
333
174
  Remove all hotkeys associated with a plugin.
334
175
 
335
- ---
336
-
337
- ## `onBind(hook)`
176
+ ### `onBind(hook)`
338
177
 
339
178
  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
179
 
@@ -345,31 +184,25 @@ const off = hotkeys.onBind(({ raw }) => {
345
184
  // off() unsubscribes.
346
185
  ```
347
186
 
348
- Hook errors are caught and logged via `console.error` โ€” a buggy hook can't break the bind.
349
-
350
- ---
187
+ Hook errors are caught and logged via `console.error`. A buggy hook can't break the bind.
351
188
 
352
- ## `pause()` / `resume()`
189
+ ### `pause()` / `resume()`
353
190
 
354
191
  Temporarily disable or re-enable all routing.
355
192
 
356
- ---
357
-
358
- ## `ignoreInput(boolean = true)`
193
+ ### `ignoreInput(boolean = true)`
359
194
 
360
195
  By default, hotkeys do **not** fire inside:
361
196
 
362
- * `<input>`
363
- * `<textarea>`
364
- * `<select>`
365
- * `[contenteditable]`
366
- * `role="textbox"`
197
+ - `<input>`
198
+ - `<textarea>`
199
+ - `<select>`
200
+ - `[contenteditable]`
201
+ - `role="textbox"`
367
202
 
368
203
  Override per-binding with `allowIn()`.
369
204
 
370
- ---
371
-
372
- ## `init(options?)`
205
+ ### `init(options?)`
373
206
 
374
207
  Manually attach listeners.
375
208
 
@@ -382,18 +215,13 @@ hotkeys.init({
382
215
 
383
216
  Auto-initializes on `window` by default (browser environments).
384
217
 
385
- ---
386
-
387
- ## `destroy()`
218
+ ### `destroy()`
388
219
 
389
220
  Removes all listeners and clears internal state.
390
221
 
391
- ---
392
-
393
- ## `trigger(hotkey, options?)`
222
+ ### `trigger(hotkey, options?)`
394
223
 
395
- Programmatically trigger a hotkey.
396
- Useful for testing.
224
+ Programmatically trigger a hotkey. Useful for testing.
397
225
 
398
226
  ```js
399
227
  hotkeys.trigger('ctrl+k')
@@ -403,77 +231,210 @@ Returns `true` if a handler ran.
403
231
 
404
232
  ---
405
233
 
406
- # Supported Syntax
234
+ ## Notes
407
235
 
408
- ## Standard
236
+ ### Routing model
409
237
 
410
- * `ctrl+k`
411
- * `shift+a`
412
- * `ctrl+k up`
413
- * `mod+s` (meta on macOS, ctrl elsewhere)
414
- * `ctrl++` or `ctrl+plus`
238
+ When multiple handlers match the same hotkey:
415
239
 
416
- ## AHK-Style Prefix Modifiers
240
+ 1. Highest `priority` wins.
241
+ 2. If equal priority, newest binding wins.
417
242
 
418
- * `^k` โ†’ `ctrl+k`
419
- * `!k` โ†’ `alt+k`
420
- * `+k` โ†’ `shift+k`
421
- * `#k` โ†’ `meta+k`
422
- * `^!k` โ†’ `ctrl+alt+k`
243
+ This makes modal overrides simple:
423
244
 
424
- Modifiers must appear before the base key.
245
+ ```js
246
+ hotkeys.bind('escape', closeModal, null, {
247
+ priority: 100,
248
+ preventDefault: true,
249
+ })
250
+ ```
251
+
252
+ ### Bare-modifier bindings (v0.1.0+)
425
253
 
426
- ## Aliases Supported
254
+ Some UX patterns are driven by a bare modifier rather than a chord. For example, "hold Alt to enter select mode, release Alt to exit."
427
255
 
428
- ### Modifiers
256
+ ```js
257
+ hotkeys.bind('alt', onAltDown) // fires on Alt keydown
258
+ hotkeys.bind('alt up', onAltUp) // fires on Alt keyup
259
+ hotkeys.bind('ctrl', onCtrlDown) // any single modifier supported
260
+ ```
429
261
 
430
- * `ctrl`, `control`, `โŒƒ`
431
- * `shift`, `โ‡ง`, `+`
432
- * `alt`, `option`, `โŒฅ`, `!`
433
- * `meta`, `cmd`, `command`, `win`, `โŒ˜`, `#`
434
- * `mod` (meta on macOS, ctrl elsewhere)
262
+ - Only **single** bare modifiers are supported. Multi-modifier bare bindings (`'ctrl+alt'`) throw, add a base key for those.
263
+ - Default `repeat: false` applies, so a held modifier fires only once on keydown.
264
+ - 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.
435
265
 
436
- ### Navigation / Special Keys
266
+ ### Code-based matching (v0.1.0+)
437
267
 
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`
268
+ `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:
269
+
270
+ ```js
271
+ hotkeys.bind('alt+code:KeyX', deleteHovered) // matches the physical X key
272
+ hotkeys.bind('ctrl+code:Digit1', goToTab1) // matches digit row, not numpad
273
+ hotkeys.bind('!code:KeyX', deleteHovered) // AHK shorthand also works
274
+ ```
275
+
276
+ 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. Both key-based and code-based bindings can coexist; the standard priority + recency rules pick the winner.
277
+
278
+ ### Conflict warnings (v0.2.0+)
279
+
280
+ 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.
281
+
282
+ 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.
283
+
284
+ **Two ways to opt in.** One-line ergonomic, use the `auto` entry. Same default export as `hotkey-router`, with warnings pre-installed:
285
+
286
+ ```js
287
+ import hotkeys from 'hotkey-router/auto'
288
+
289
+ hotkeys.bind('meta+shift+f', toggleFullscreen)
290
+ // Firefox on macOS:
291
+ // [hotkey-router] "meta+shift+f" reserved by firefox on macOS:
292
+ // "Toggle fullscreen" [hard], will not fire.
293
+ ```
294
+
295
+ Explicit install, keep using the tiny core, install warnings yourself:
296
+
297
+ ```js
298
+ import hotkeys from 'hotkey-router'
299
+ import { installReservationWarnings } from 'hotkey-router/reservations'
300
+
301
+ installReservationWarnings(hotkeys)
302
+ hotkeys.bind('meta+shift+f', toggleFullscreen)
303
+ ```
304
+
305
+ `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.
306
+
307
+ **Severity to log level:**
308
+
309
+ | Severity | Log level | Meaning |
310
+ | ----------------- | --------- | ------------------------------------------------------- |
311
+ | `hard` | `warn` | Browser intercepts before page world; combo won't fire. |
312
+ | `os` | `warn` | OS intercepts globally. |
313
+ | `menu-activation` | `warn` | Alt+letter activates the browser menu bar (Win/Linux). |
314
+ | `find-bar-only` | `info` | Reserved only when the find bar is focused. |
315
+ | `compose` | `info` | macOS Option+letter types a special char in inputs. |
316
+ | `system-text` | `info` | Mac Ctrl+letter cursor controls inside text inputs. |
317
+ | `devtools-open` | `info` | Only relevant when DevTools is already open. |
318
+
319
+ **Per-bind opt-out:**
320
+
321
+ ```js
322
+ hotkeys.bind('meta+shift+f', toggleFullscreen, null, { warnOnReserved: false })
323
+ ```
324
+
325
+ To turn warnings off globally, just don't install them (use the bare `hotkey-router` entry instead of `hotkey-router/auto`).
326
+
327
+ **Force a platform/browser (tests, SSR previews):**
328
+
329
+ ```js
330
+ installReservationWarnings(hotkeys, { platform: 'mac', browser: 'firefox' })
331
+ ```
332
+
333
+ Accepted platforms: `'mac' | 'windows' | 'linux'`. Browsers: `'firefox' | 'chrome' | 'safari' | 'edge'`.
334
+
335
+ **Caveats:**
336
+
337
+ - Reservations reflect **default** keybindings. Users with custom shortcuts (Edge 95+ rebinds, Firefox add-ons, OS-level customization) may not match.
338
+ - KDE/XFCE desktop reservations beyond GNOME are not yet catalogued. Linux coverage is conservative.
339
+ - 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.
340
+
341
+ **Programmatic lookup.** If you want to query the table yourself (e.g. building a cheatsheet that flags conflicts):
342
+
343
+ ```js
344
+ import { lookupReservation } from 'hotkey-router/reservations'
345
+
346
+ const r = lookupReservation(
347
+ { meta: true, shift: true, key: 'f' },
348
+ { platform: 'mac', browser: 'firefox' }
349
+ )
350
+ // โ†’ { source: 'browser', action: 'Toggle fullscreen', severity: 'hard', ... }
351
+ ```
352
+
353
+ **Extension hook (`onBind`).** 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:
354
+
355
+ ```js
356
+ const off = hotkeys.onBind(({ combo, raw, options, plugin, id }) => {
357
+ // combo: parsed combo { ctrl, meta, alt, shift, key, code, bareModifier }
358
+ // raw: original hotkey string
359
+ })
360
+ // off() unsubscribes.
361
+ ```
362
+
363
+ ### Supported syntax
364
+
365
+ **Standard:**
366
+
367
+ - `ctrl+k`
368
+ - `shift+a`
369
+ - `ctrl+k up`
370
+ - `mod+s` (meta on macOS, ctrl elsewhere)
371
+ - `ctrl++` or `ctrl+plus`
372
+
373
+ **AHK-style prefix modifiers:**
374
+
375
+ - `^k` โ†’ `ctrl+k`
376
+ - `!k` โ†’ `alt+k`
377
+ - `+k` โ†’ `shift+k`
378
+ - `#k` โ†’ `meta+k`
379
+ - `^!k` โ†’ `ctrl+alt+k`
380
+
381
+ Modifiers must appear before the base key.
382
+
383
+ **Modifier aliases:**
384
+
385
+ - `ctrl`, `control`, `โŒƒ`
386
+ - `shift`, `โ‡ง`, `+`
387
+ - `alt`, `option`, `โŒฅ`, `!`
388
+ - `meta`, `cmd`, `command`, `win`, `โŒ˜`, `#`
389
+ - `mod` (meta on macOS, ctrl elsewhere)
390
+
391
+ **Navigation / special key aliases:**
392
+
393
+ - `escape`, `esc`
394
+ - `enter`, `return`
395
+ - `space`
396
+ - `tab`
397
+ - `backspace`
398
+ - `delete`, `del`
399
+ - `home`, `end`
400
+ - `pageup`, `pgup`
401
+ - `pagedown`, `pgdn`
402
+ - `up`, `down`, `left`, `right`
403
+ - `f1`โ€“`f19`
449
404
 
450
405
  Keys are case-insensitive.
451
406
 
452
- ---
407
+ ### Philosophy
453
408
 
454
- # What This Is Not
409
+ hotkey-router follows three core rules:
455
410
 
456
- * Not a keycode polyfill
457
- * Not a legacy browser shim
458
- * Not a global scope manager
459
- * Not a VSCode-style sequence engine
411
+ 1. **Predictable routing.** Highest priority wins. Ties go to the most recently bound handler.
412
+ 2. **Safe composition.** Plugins can register and unregister without affecting others.
413
+ 3. **Modern only.** Built for modern browsers using `KeyboardEvent.key`.
460
414
 
461
- This is a small, deterministic routing layer for modern applications.
415
+ No keycodes. No legacy IE hacks. No hidden global scope state.
462
416
 
463
- ---
417
+ ### What this is not
418
+
419
+ - Not a keycode polyfill
420
+ - Not a legacy browser shim
421
+ - Not a global scope manager
422
+ - Not a VSCode-style sequence engine
423
+
424
+ This is a small, deterministic routing layer for modern applications.
464
425
 
465
- # Browser Support
426
+ ### Browser support
466
427
 
467
428
  Modern browsers supporting:
468
429
 
469
- * `KeyboardEvent.key`
470
- * `Map`
471
- * `addEventListener`
430
+ - `KeyboardEvent.key`
431
+ - `Map`
432
+ - `addEventListener`
472
433
 
473
434
  Chrome, Firefox, Safari, Edge.
474
435
 
475
436
  ---
476
437
 
477
- # License
438
+ ## License
478
439
 
479
- See LICENSE file for details.
440
+ Licensed under AGPL-3.0 with WATT3D Additional Terms. See [LICENSE](./LICENSE) and [ADDITIONAL_TERMS.md](./ADDITIONAL_TERMS.md). Commercial AI/model-training use requires compliance with those terms or a separate WATT3D license. ยฉ WATT3D.
package/dist/auto.js CHANGED
@@ -1,4 +1,4 @@
1
- // hotkey-router v0.2.0 | SEE LICENSE IN LICENSE
1
+ // hotkey-router v0.2.1 | SEE LICENSE IN LICENSE
2
2
 
3
3
 
4
4
  // hotkey-router.js