hotkey-router 0.0.2 โ†’ 0.1.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,331 +1,370 @@
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
- * ๐Ÿ“ฆ Zero dependencies
18
-
19
- ---
20
-
21
- ## Philosophy
22
-
23
- Hotkey Router follows three core rules:
24
-
25
- 1. **Predictable routing** โ€” Highest priority wins. Ties go to the most recently bound handler.
26
- 2. **Safe composition** โ€” Plugins can register and unregister without affecting others.
27
- 3. **Modern only** โ€” Built for modern browsers using `KeyboardEvent.key`.
28
-
29
- No keycodes. No legacy IE hacks. No hidden global scope state.
30
-
31
- ---
32
-
33
- ## Install
34
-
35
- ```bash
36
- npm install hotkey-router
37
- ```
38
-
39
- ### ESM
40
-
41
- ```js
42
- import hotkeys from 'hotkey-router'
43
- ```
44
-
45
- ### CommonJS
46
-
47
- ```js
48
- const hotkeys = require('hotkey-router')
49
- ```
50
-
51
- ### CDN (ESM)
52
-
53
- ```js
54
- import hotkeys from 'https://cdn.jsdelivr.net/npm/hotkey-router/dist/hotkey-router.min.js'
55
- ```
56
-
57
- ---
58
-
59
- # Basic Usage
60
-
61
- ```js
62
- import hotkeys from 'hotkey-router'
63
-
64
- // Simple keydown
65
- hotkeys.bind('ctrl+k', () => {
66
- openCommandPalette()
67
- })
68
-
69
- // Keyup using " up" suffix
70
- hotkeys.bind('ctrl+p up', () => {
71
- console.log('Released CTRL+P')
72
- })
73
-
74
- // AHK-style modifiers also supported
75
- hotkeys.bind('^k', () => {
76
- openCommandPalette() // ctrl+k
77
- })
78
-
79
- // Plugin grouping
80
- hotkeys.registerPlugin('docs', {
81
- 'ctrl+f': openSearch,
82
- 'escape': closeSearch,
83
- })
84
- ```
85
-
86
- ---
87
-
88
- # Routing Model
89
-
90
- When multiple handlers match the same hotkey:
91
-
92
- 1. Highest `priority` wins
93
- 2. If equal priority โ†’ newest binding wins
94
-
95
- This makes modal overrides simple:
96
-
97
- ```js
98
- hotkeys.bind('escape', closeModal, null, {
99
- priority: 100,
100
- preventDefault: true,
101
- })
102
- ```
103
-
104
- ---
105
-
106
- # API
107
-
108
- ## `bind(hotkey, handler, plugin?, options?)`
109
-
110
- Register a hotkey.
111
-
112
- Returns an `off()` function.
113
-
114
- ```js
115
- const off = hotkeys.bind('mod+k', openPalette)
116
-
117
- // Later
118
- off()
119
- ```
120
-
121
- ### Options
122
-
123
- ```ts
124
- {
125
- preventDefault?: boolean
126
- stopPropagation?: boolean
127
- stopImmediatePropagation?: boolean
128
- repeat?: boolean // default false on keydown
129
- once?: boolean
130
- when?: (event) => boolean
131
- allowIn?: (event) => boolean
132
- priority?: number // default 0
133
- }
134
- ```
135
-
136
- ### Examples
137
-
138
- Ignore repeat keydown (default behavior):
139
-
140
- ```js
141
- hotkeys.bind('j', nextItem)
142
- ```
143
-
144
- Allow inside inputs:
145
-
146
- ```js
147
- hotkeys.bind('mod+k', openPalette, null, {
148
- allowIn: () => true,
149
- preventDefault: true,
150
- })
151
- ```
152
-
153
- Conditional binding:
154
-
155
- ```js
156
- hotkeys.bind('delete', deleteItem, null, {
157
- when: () => selectionCount() > 0,
158
- })
159
- ```
160
-
161
- Run once:
162
-
163
- ```js
164
- hotkeys.bind('ctrl+s', saveDraft, null, { once: true })
165
- ```
166
-
167
- ---
168
-
169
- ## `unbind(hotkey, handler?)`
170
-
171
- Remove bindings.
172
-
173
- ```js
174
- hotkeys.unbind('ctrl+k')
175
- hotkeys.unbind('ctrl+k', openPalette)
176
- ```
177
-
178
- ---
179
-
180
- ## `registerPlugin(name, map)`
181
-
182
- Batch register hotkeys under a plugin namespace.
183
-
184
- ```js
185
- const unregister = hotkeys.registerPlugin('files', {
186
- 'mod+o': openFile,
187
- 'delete': deleteFile,
188
- })
189
-
190
- // Later
191
- unregister()
192
- ```
193
-
194
- Plugin cleanup is isolated โ€” removing one plugin never affects other bindings.
195
-
196
- ---
197
-
198
- ## `unregisterPlugin(name)`
199
-
200
- Remove all hotkeys associated with a plugin.
201
-
202
- ---
203
-
204
- ## `pause()` / `resume()`
205
-
206
- Temporarily disable or re-enable all routing.
207
-
208
- ---
209
-
210
- ## `ignoreInput(boolean = true)`
211
-
212
- By default, hotkeys do **not** fire inside:
213
-
214
- * `<input>`
215
- * `<textarea>`
216
- * `<select>`
217
- * `[contenteditable]`
218
- * `role="textbox"`
219
-
220
- Override per-binding with `allowIn()`.
221
-
222
- ---
223
-
224
- ## `init(options?)`
225
-
226
- Manually attach listeners.
227
-
228
- ```js
229
- hotkeys.init({
230
- target: window,
231
- capture: false,
232
- })
233
- ```
234
-
235
- Auto-initializes on `window` by default (browser environments).
236
-
237
- ---
238
-
239
- ## `destroy()`
240
-
241
- Removes all listeners and clears internal state.
242
-
243
- ---
244
-
245
- ## `trigger(hotkey, options?)`
246
-
247
- Programmatically trigger a hotkey.
248
- Useful for testing.
249
-
250
- ```js
251
- hotkeys.trigger('ctrl+k')
252
- ```
253
-
254
- Returns `true` if a handler ran.
255
-
256
- ---
257
-
258
- # Supported Syntax
259
-
260
- ## Standard
261
-
262
- * `ctrl+k`
263
- * `shift+a`
264
- * `ctrl+k up`
265
- * `mod+s` (meta on macOS, ctrl elsewhere)
266
- * `ctrl++` or `ctrl+plus`
267
-
268
- ## AHK-Style Prefix Modifiers
269
-
270
- * `^k` โ†’ `ctrl+k`
271
- * `!k` โ†’ `alt+k`
272
- * `+k` โ†’ `shift+k`
273
- * `#k` โ†’ `meta+k`
274
- * `^!k` โ†’ `ctrl+alt+k`
275
-
276
- Modifiers must appear before the base key.
277
-
278
- ## Aliases Supported
279
-
280
- ### Modifiers
281
-
282
- * `ctrl`, `control`, `โŒƒ`
283
- * `shift`, `โ‡ง`, `+`
284
- * `alt`, `option`, `โŒฅ`, `!`
285
- * `meta`, `cmd`, `command`, `win`, `โŒ˜`, `#`
286
- * `mod` (meta on macOS, ctrl elsewhere)
287
-
288
- ### Navigation / Special Keys
289
-
290
- * `escape`, `esc`
291
- * `enter`, `return`
292
- * `space`
293
- * `tab`
294
- * `backspace`
295
- * `delete`, `del`
296
- * `home`, `end`
297
- * `pageup`, `pgup`
298
- * `pagedown`, `pgdn`
299
- * `up`, `down`, `left`, `right`
300
- * `f1`โ€“`f19`
301
-
302
- Keys are case-insensitive.
303
-
304
- ---
305
-
306
- # What This Is Not
307
-
308
- * Not a keycode polyfill
309
- * Not a legacy browser shim
310
- * Not a global scope manager
311
- * Not a VSCode-style sequence engine
312
-
313
- This is a small, deterministic routing layer for modern applications.
314
-
315
- ---
316
-
317
- # Browser Support
318
-
319
- Modern browsers supporting:
320
-
321
- * `KeyboardEvent.key`
322
- * `Map`
323
- * `addEventListener`
324
-
325
- Chrome, Firefox, Safari, Edge.
326
-
327
- ---
328
-
329
- # License
330
-
331
- 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
+ * ๐Ÿ“ฆ 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
+ // 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
+ ## Bare-modifier bindings (v0.1.0+)
97
+
98
+ 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."
99
+
100
+ ```js
101
+ hotkeys.bind('alt', onAltDown) // fires on Alt keydown
102
+ hotkeys.bind('alt up', onAltUp) // fires on Alt keyup
103
+ hotkeys.bind('ctrl', onCtrlDown) // any single modifier supported
104
+ ```
105
+
106
+ Notes:
107
+ - Only **single** bare modifiers are supported. Multi-modifier bare bindings (`'ctrl+alt'`) throw โ€” add a base key for those.
108
+ - Default `repeat: false` applies, so a held modifier fires only once on keydown.
109
+ - 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.
110
+
111
+ ## Code-based matching (v0.1.0+)
112
+
113
+ `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:
114
+
115
+ ```js
116
+ hotkeys.bind('alt+code:KeyX', deleteHovered) // matches the physical X key
117
+ hotkeys.bind('ctrl+code:Digit1', goToTab1) // matches digit row, not numpad
118
+ hotkeys.bind('!code:KeyX', deleteHovered) // AHK shorthand also works
119
+ ```
120
+
121
+ 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.
122
+
123
+ Both key-based and code-based bindings can coexist; the standard priority + recency rules pick the winner.
124
+
125
+ ---
126
+
127
+ # Routing Model
128
+
129
+ When multiple handlers match the same hotkey:
130
+
131
+ 1. Highest `priority` wins
132
+ 2. If equal priority โ†’ newest binding wins
133
+
134
+ This makes modal overrides simple:
135
+
136
+ ```js
137
+ hotkeys.bind('escape', closeModal, null, {
138
+ priority: 100,
139
+ preventDefault: true,
140
+ })
141
+ ```
142
+
143
+ ---
144
+
145
+ # API
146
+
147
+ ## `bind(hotkey, handler, plugin?, options?)`
148
+
149
+ Register a hotkey.
150
+
151
+ Returns an `off()` function.
152
+
153
+ ```js
154
+ const off = hotkeys.bind('mod+k', openPalette)
155
+
156
+ // Later
157
+ off()
158
+ ```
159
+
160
+ ### Options
161
+
162
+ ```ts
163
+ {
164
+ preventDefault?: boolean
165
+ stopPropagation?: boolean
166
+ stopImmediatePropagation?: boolean
167
+ repeat?: boolean // default false on keydown
168
+ once?: boolean
169
+ when?: (event) => boolean
170
+ allowIn?: (event) => boolean
171
+ priority?: number // default 0
172
+ }
173
+ ```
174
+
175
+ ### Examples
176
+
177
+ Ignore repeat keydown (default behavior):
178
+
179
+ ```js
180
+ hotkeys.bind('j', nextItem)
181
+ ```
182
+
183
+ Allow inside inputs:
184
+
185
+ ```js
186
+ hotkeys.bind('mod+k', openPalette, null, {
187
+ allowIn: () => true,
188
+ preventDefault: true,
189
+ })
190
+ ```
191
+
192
+ Conditional binding:
193
+
194
+ ```js
195
+ hotkeys.bind('delete', deleteItem, null, {
196
+ when: () => selectionCount() > 0,
197
+ })
198
+ ```
199
+
200
+ Run once:
201
+
202
+ ```js
203
+ hotkeys.bind('ctrl+s', saveDraft, null, { once: true })
204
+ ```
205
+
206
+ ---
207
+
208
+ ## `unbind(hotkey, handler?)`
209
+
210
+ Remove bindings.
211
+
212
+ ```js
213
+ hotkeys.unbind('ctrl+k')
214
+ hotkeys.unbind('ctrl+k', openPalette)
215
+ ```
216
+
217
+ ---
218
+
219
+ ## `registerPlugin(name, map)`
220
+
221
+ Batch register hotkeys under a plugin namespace.
222
+
223
+ ```js
224
+ const unregister = hotkeys.registerPlugin('files', {
225
+ 'mod+o': openFile,
226
+ 'delete': deleteFile,
227
+ })
228
+
229
+ // Later
230
+ unregister()
231
+ ```
232
+
233
+ Plugin cleanup is isolated โ€” removing one plugin never affects other bindings.
234
+
235
+ ---
236
+
237
+ ## `unregisterPlugin(name)`
238
+
239
+ Remove all hotkeys associated with a plugin.
240
+
241
+ ---
242
+
243
+ ## `pause()` / `resume()`
244
+
245
+ Temporarily disable or re-enable all routing.
246
+
247
+ ---
248
+
249
+ ## `ignoreInput(boolean = true)`
250
+
251
+ By default, hotkeys do **not** fire inside:
252
+
253
+ * `<input>`
254
+ * `<textarea>`
255
+ * `<select>`
256
+ * `[contenteditable]`
257
+ * `role="textbox"`
258
+
259
+ Override per-binding with `allowIn()`.
260
+
261
+ ---
262
+
263
+ ## `init(options?)`
264
+
265
+ Manually attach listeners.
266
+
267
+ ```js
268
+ hotkeys.init({
269
+ target: window,
270
+ capture: false,
271
+ })
272
+ ```
273
+
274
+ Auto-initializes on `window` by default (browser environments).
275
+
276
+ ---
277
+
278
+ ## `destroy()`
279
+
280
+ Removes all listeners and clears internal state.
281
+
282
+ ---
283
+
284
+ ## `trigger(hotkey, options?)`
285
+
286
+ Programmatically trigger a hotkey.
287
+ Useful for testing.
288
+
289
+ ```js
290
+ hotkeys.trigger('ctrl+k')
291
+ ```
292
+
293
+ Returns `true` if a handler ran.
294
+
295
+ ---
296
+
297
+ # Supported Syntax
298
+
299
+ ## Standard
300
+
301
+ * `ctrl+k`
302
+ * `shift+a`
303
+ * `ctrl+k up`
304
+ * `mod+s` (meta on macOS, ctrl elsewhere)
305
+ * `ctrl++` or `ctrl+plus`
306
+
307
+ ## AHK-Style Prefix Modifiers
308
+
309
+ * `^k` โ†’ `ctrl+k`
310
+ * `!k` โ†’ `alt+k`
311
+ * `+k` โ†’ `shift+k`
312
+ * `#k` โ†’ `meta+k`
313
+ * `^!k` โ†’ `ctrl+alt+k`
314
+
315
+ Modifiers must appear before the base key.
316
+
317
+ ## Aliases Supported
318
+
319
+ ### Modifiers
320
+
321
+ * `ctrl`, `control`, `โŒƒ`
322
+ * `shift`, `โ‡ง`, `+`
323
+ * `alt`, `option`, `โŒฅ`, `!`
324
+ * `meta`, `cmd`, `command`, `win`, `โŒ˜`, `#`
325
+ * `mod` (meta on macOS, ctrl elsewhere)
326
+
327
+ ### Navigation / Special Keys
328
+
329
+ * `escape`, `esc`
330
+ * `enter`, `return`
331
+ * `space`
332
+ * `tab`
333
+ * `backspace`
334
+ * `delete`, `del`
335
+ * `home`, `end`
336
+ * `pageup`, `pgup`
337
+ * `pagedown`, `pgdn`
338
+ * `up`, `down`, `left`, `right`
339
+ * `f1`โ€“`f19`
340
+
341
+ Keys are case-insensitive.
342
+
343
+ ---
344
+
345
+ # What This Is Not
346
+
347
+ * Not a keycode polyfill
348
+ * Not a legacy browser shim
349
+ * Not a global scope manager
350
+ * Not a VSCode-style sequence engine
351
+
352
+ This is a small, deterministic routing layer for modern applications.
353
+
354
+ ---
355
+
356
+ # Browser Support
357
+
358
+ Modern browsers supporting:
359
+
360
+ * `KeyboardEvent.key`
361
+ * `Map`
362
+ * `addEventListener`
363
+
364
+ Chrome, Firefox, Safari, Edge.
365
+
366
+ ---
367
+
368
+ # License
369
+
370
+ See LICENSE file for details.