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/ADDITIONAL_TERMS.md +122 -0
- package/LICENSE +694 -285
- package/README.md +224 -263
- package/dist/auto.js +1 -1
- package/dist/auto.min.js +1 -1
- package/dist/hotkey-router.cjs +1 -1
- package/dist/hotkey-router.js +1 -1
- package/dist/hotkey-router.min.js +1 -1
- package/dist/reservations.js +1 -1
- package/package.json +5 -3
- package/robots.txt +79 -0
package/README.md
CHANGED
|
@@ -1,56 +1,45 @@
|
|
|
1
1
|
# hotkey-router
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/hotkey-router)
|
|
4
|
+
[](https://www.npmjs.com/package/hotkey-router)
|
|
4
5
|
[](https://bundlephobia.com/package/hotkey-router)
|
|
5
|
-
[](https://github.com/iWhatty/hotkey-router-js/blob/main/LICENSE)
|
|
7
|
+
[](https://github.com/iWhatty/hotkey-router-js)
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
It is a predictable, plugin-first routing layer for keyboard shortcuts.
|
|
11
|
+
## Features
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
```
|
|
38
|
-
|
|
26
|
+
```sh
|
|
27
|
+
pnpm add hotkey-router
|
|
39
28
|
```
|
|
40
29
|
|
|
41
|
-
|
|
30
|
+
ESM:
|
|
42
31
|
|
|
43
32
|
```js
|
|
44
33
|
import hotkeys from 'hotkey-router'
|
|
45
34
|
```
|
|
46
35
|
|
|
47
|
-
|
|
36
|
+
CommonJS:
|
|
48
37
|
|
|
49
38
|
```js
|
|
50
39
|
const hotkeys = require('hotkey-router')
|
|
51
40
|
```
|
|
52
41
|
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
##
|
|
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
|
-
|
|
89
|
+
### `bind(hotkey, handler, plugin?, options?)`
|
|
205
90
|
|
|
206
|
-
|
|
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
|
-
|
|
100
|
+
**Options**
|
|
254
101
|
|
|
255
102
|
```ts
|
|
256
103
|
{
|
|
@@ -266,7 +113,7 @@ off()
|
|
|
266
113
|
}
|
|
267
114
|
```
|
|
268
115
|
|
|
269
|
-
|
|
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
|
|
328
|
-
|
|
329
|
-
---
|
|
170
|
+
Plugin cleanup is isolated. Removing one plugin never affects other bindings.
|
|
330
171
|
|
|
331
|
-
|
|
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
|
|
349
|
-
|
|
350
|
-
---
|
|
187
|
+
Hook errors are caught and logged via `console.error`. A buggy hook can't break the bind.
|
|
351
188
|
|
|
352
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
234
|
+
## Notes
|
|
407
235
|
|
|
408
|
-
|
|
236
|
+
### Routing model
|
|
409
237
|
|
|
410
|
-
|
|
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
|
-
|
|
240
|
+
1. Highest `priority` wins.
|
|
241
|
+
2. If equal priority, newest binding wins.
|
|
417
242
|
|
|
418
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
###
|
|
266
|
+
### Code-based matching (v0.1.0+)
|
|
437
267
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
409
|
+
hotkey-router follows three core rules:
|
|
455
410
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
+
### Browser support
|
|
466
427
|
|
|
467
428
|
Modern browsers supporting:
|
|
468
429
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
430
|
+
- `KeyboardEvent.key`
|
|
431
|
+
- `Map`
|
|
432
|
+
- `addEventListener`
|
|
472
433
|
|
|
473
434
|
Chrome, Firefox, Safari, Edge.
|
|
474
435
|
|
|
475
436
|
---
|
|
476
437
|
|
|
477
|
-
|
|
438
|
+
## License
|
|
478
439
|
|
|
479
|
-
See LICENSE
|
|
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