@remcostoeten/use-shortcut 1.3.0 → 2.0.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/CHANGELOG.md +8 -0
- package/README.md +33 -313
- package/dist/cli/index.mjs +88 -216
- package/dist/index.d.mts +39 -68
- package/dist/index.d.ts +39 -68
- package/dist/index.js +350 -361
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +351 -354
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- package/src/__tests__/features.test.ts +43 -12
- package/src/builder.ts +37 -476
- package/src/constants.ts +59 -13
- package/src/formatter.ts +37 -22
- package/src/hook.ts +150 -31
- package/src/index.ts +1 -12
- package/src/parser.ts +6 -3
- package/src/runtime/binding.ts +136 -0
- package/src/runtime/conflicts.ts +43 -0
- package/src/runtime/debug.ts +6 -0
- package/src/runtime/guards.ts +82 -0
- package/src/runtime/keys.ts +63 -0
- package/src/runtime/listener.ts +142 -0
- package/src/runtime/recording.ts +39 -0
- package/src/runtime/types.ts +48 -0
- package/src/types.ts +16 -19
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [2.0.0] - 2026-03-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Removed non-React public entry points (`createShortcut`, `createShortcutMap`) to focus package API on React-first usage.
|
|
15
|
+
|
|
8
16
|
## [1.3.0] - 2026-02-28
|
|
9
17
|
|
|
10
18
|
### Added
|
package/README.md
CHANGED
|
@@ -1,331 +1,51 @@
|
|
|
1
1
|
# @remcostoeten/use-shortcut
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://www.typescriptlang.org/)
|
|
3
|
+
WIP keyboard shortcut library for React with a chainable API.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
## Status
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
- Focus right now: runtime architecture and DX refinement
|
|
8
|
+
- Documentation scope: feature/status overview only (full API docs will be expanded later)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
const $ = useShortcut()
|
|
10
|
+
## Implemented Features
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
- **Recording mode** - Capture the next key combo for custom keybind UIs
|
|
28
|
-
- **Priorities** - Deterministic ordering for overlapping shortcuts
|
|
29
|
-
- **Groups** - Register multiple shortcuts and unbind them together
|
|
30
|
-
- **Event filtering** - Global event guard via `eventFilter`
|
|
31
|
-
- **Perfect TypeScript** - Intellisense at every step
|
|
32
|
-
- **Cross-platform** - `mod` = ⌘ on Mac, Ctrl on Windows/Linux
|
|
33
|
-
- **Context-aware** - Skip shortcuts in inputs with `.except()`
|
|
34
|
-
- **Zero dependencies** - Only React as peer dependency
|
|
35
|
-
- **Tiny** - ~3KB gzipped
|
|
12
|
+
- Chainable shortcut builder: `$.mod.key("k").on(handler)`
|
|
13
|
+
- Modifier support: `ctrl`, `shift`, `alt`, `cmd`, `mod`
|
|
14
|
+
- Sequence support: `$.key("g").then("d")`
|
|
15
|
+
- Scope-aware shortcuts:
|
|
16
|
+
- Register with `.in("editor")`
|
|
17
|
+
- Runtime controls: `setScopes`, `enableScope`, `disableScope`, `getScopes`, `isScopeActive`
|
|
18
|
+
- Exception predicates/presets with `.except(...)`
|
|
19
|
+
- Recording mode: `$.record({ timeoutMs })`
|
|
20
|
+
- Conflict detection (`exact`, `sequence-prefix`)
|
|
21
|
+
- Priority ordering and `stopOnMatch`
|
|
22
|
+
- Global guard/filter support via `eventFilter`
|
|
23
|
+
- React entry point:
|
|
24
|
+
- `useShortcut`
|
|
36
25
|
|
|
37
|
-
##
|
|
38
|
-
|
|
39
|
-
### npm/pnpm/bun
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
npm install @remcostoeten/use-shortcut
|
|
43
|
-
pnpm add @remcostoeten/use-shortcut
|
|
44
|
-
bun add @remcostoeten/use-shortcut
|
|
45
|
-
```
|
|
26
|
+
## API Intention (Consumer-Facing)
|
|
46
27
|
|
|
47
|
-
|
|
28
|
+
- `useShortcut(options?)`
|
|
29
|
+
- Main React hook. Use this for the chainable API (`$.mod.key("s").on(...)`).
|
|
48
30
|
|
|
49
|
-
|
|
50
|
-
npx @remcostoeten/use-shortcut init
|
|
51
|
-
# or
|
|
52
|
-
bunx @remcostoeten/use-shortcut init
|
|
53
|
-
```
|
|
31
|
+
Internal helpers follow underscore naming (for example `_createShortcutBuilder`, `_canonicalizeParsed`) and are not re-exported from `src/index.ts`.
|
|
54
32
|
|
|
55
|
-
|
|
33
|
+
## Architecture Notes
|
|
56
34
|
|
|
57
|
-
|
|
35
|
+
- Core runtime lives in `src/builder.ts`
|
|
36
|
+
- Parsing/formatting are isolated in `src/parser.ts` and `src/formatter.ts`
|
|
37
|
+
- React bindings and map helpers live in `src/hook.ts`
|
|
38
|
+
- Type contracts live in `src/types.ts`
|
|
39
|
+
- CLI scaffold/copy commands live under `cli/`
|
|
58
40
|
|
|
59
|
-
|
|
41
|
+
## Development
|
|
60
42
|
|
|
61
43
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# React scaffold
|
|
66
|
-
npx @remcostoeten/use-shortcut scaffold --framework react
|
|
67
|
-
|
|
68
|
-
# Custom location
|
|
69
|
-
npx @remcostoeten/use-shortcut scaffold --target src --dir shortcuts
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Generated structure:
|
|
73
|
-
|
|
74
|
-
```txt
|
|
75
|
-
src/shortcuts/
|
|
76
|
-
index.ts
|
|
77
|
-
provider.tsx
|
|
78
|
-
registry.ts
|
|
79
|
-
runtime.ts
|
|
80
|
-
scopes.ts
|
|
81
|
-
storage.ts
|
|
82
|
-
types.ts
|
|
83
|
-
README.md
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Architecture docs:
|
|
87
|
-
- Human guide: [`docs/app-architecture.md`](./docs/app-architecture.md)
|
|
88
|
-
- LLM guide: [`docs/app-architecture.llm.md`](./docs/app-architecture.llm.md)
|
|
89
|
-
|
|
90
|
-
## Quick Start
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
93
|
-
"use client"
|
|
94
|
-
|
|
95
|
-
import { useShortcut } from "@remcostoeten/use-shortcut"
|
|
96
|
-
|
|
97
|
-
export function App() {
|
|
98
|
-
const $ = useShortcut()
|
|
99
|
-
|
|
100
|
-
$.cmd.key("s").on(() => {
|
|
101
|
-
console.log("Save!")
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
$.mod.key("k").on(() => {
|
|
105
|
-
console.log("Search!")
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
return <div>Press ⌘+S or ⌘+K</div>
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## API
|
|
113
|
-
|
|
114
|
-
### Modifiers
|
|
115
|
-
|
|
116
|
-
Chain modifiers before calling `.key()`:
|
|
117
|
-
|
|
118
|
-
```tsx
|
|
119
|
-
$.ctrl.key("s") // Ctrl+S
|
|
120
|
-
$.shift.key("enter") // Shift+Enter
|
|
121
|
-
$.alt.key("n") // Alt+N
|
|
122
|
-
$.cmd.key("k") // ⌘+K (Mac) or Ctrl+K (Windows)
|
|
123
|
-
$.mod.key("k") // Cross-platform: ⌘ on Mac, Ctrl on Windows/Linux
|
|
124
|
-
|
|
125
|
-
// Multiple modifiers
|
|
126
|
-
$.ctrl.shift.key("p") // Ctrl+Shift+P
|
|
127
|
-
$.cmd.shift.alt.key("a") // ⌘+Shift+Alt+A
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Keys
|
|
131
|
-
|
|
132
|
-
Supports all standard keys:
|
|
133
|
-
|
|
134
|
-
```tsx
|
|
135
|
-
// Letters
|
|
136
|
-
$.mod.key("s") // a-z
|
|
137
|
-
|
|
138
|
-
// Numbers
|
|
139
|
-
$.mod.key("1") // 0-9
|
|
140
|
-
|
|
141
|
-
// Function keys
|
|
142
|
-
$.key("f1") // f1-f12
|
|
143
|
-
|
|
144
|
-
// Special keys
|
|
145
|
-
$.key("escape") // escape, enter, space, tab
|
|
146
|
-
$.key("backspace") // backspace, delete
|
|
147
|
-
$.mod.key("up") // up, down, left, right
|
|
148
|
-
$.key("home") // home, end, pageup, pagedown
|
|
149
|
-
|
|
150
|
-
// Symbols
|
|
151
|
-
$.mod.key("slash") // slash, backslash, comma, period
|
|
152
|
-
$.mod.key("/") // Also works with actual symbol
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Exception Handling
|
|
156
|
-
|
|
157
|
-
Skip shortcuts in certain contexts:
|
|
158
|
-
|
|
159
|
-
```tsx
|
|
160
|
-
// Built-in presets
|
|
161
|
-
$.key("/").except("input").on(handler) // Skip in <input>, <textarea>, <select>
|
|
162
|
-
$.key("/").except("editable").on(handler) // Skip in contenteditable
|
|
163
|
-
$.key("/").except("typing").on(handler) // Skip in any text input
|
|
164
|
-
$.key("escape").except("modal").on(handler) // Skip when modal is open
|
|
165
|
-
$.key("enter").except("disabled").on(handler) // Skip on disabled elements
|
|
166
|
-
|
|
167
|
-
// Multiple presets
|
|
168
|
-
$.key("/").except(["input", "modal"]).on(handler)
|
|
169
|
-
|
|
170
|
-
// Custom predicate
|
|
171
|
-
$.key("k").except((e) => {
|
|
172
|
-
return e.target.classList.contains("no-shortcuts")
|
|
173
|
-
}).on(handler)
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### Handler Options
|
|
177
|
-
|
|
178
|
-
```tsx
|
|
179
|
-
$.mod.key("s").on(save, {
|
|
180
|
-
preventDefault: true, // Prevent browser default (default: true)
|
|
181
|
-
stopPropagation: false, // Stop event bubbling (default: false)
|
|
182
|
-
delay: 100, // Delay before firing (ms)
|
|
183
|
-
description: "Save doc", // For accessibility
|
|
184
|
-
disabled: false, // Temporarily disable
|
|
185
|
-
})
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### Result Object
|
|
189
|
-
|
|
190
|
-
`.on()` returns a result object:
|
|
191
|
-
|
|
192
|
-
```tsx
|
|
193
|
-
const save = $.mod.key("s").on(handleSave)
|
|
194
|
-
|
|
195
|
-
save.display // "⌘S" on Mac, "Ctrl+S" on Windows
|
|
196
|
-
save.combo // "cmd+s"
|
|
197
|
-
save.isEnabled // true/false
|
|
198
|
-
save.enable() // Enable the shortcut
|
|
199
|
-
save.disable() // Disable the shortcut
|
|
200
|
-
save.unbind() // Remove the shortcut
|
|
201
|
-
save.trigger() // Programmatically trigger
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### Sequences / Chords
|
|
205
|
-
|
|
206
|
-
```tsx
|
|
207
|
-
// GitHub-style: press g, then d
|
|
208
|
-
$.key("g").then("d").on(() => goToDashboard())
|
|
209
|
-
|
|
210
|
-
// Steps can include modifiers too
|
|
211
|
-
$.key("g").then("shift+d").on(() => openDebug())
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Named Scopes
|
|
215
|
-
|
|
216
|
-
```tsx
|
|
217
|
-
const $ = useShortcut({ activeScopes: "navigation" })
|
|
218
|
-
|
|
219
|
-
$.in("navigation").key("g").then("d").on(() => goToDashboard())
|
|
220
|
-
$.in("editor").mod.key("s").on(() => saveFile())
|
|
221
|
-
|
|
222
|
-
$.setScopes("editor") // enable editor scope only
|
|
223
|
-
$.enableScope("navigation") // add a second active scope
|
|
224
|
-
$.disableScope("editor") // remove one scope
|
|
225
|
-
$.getScopes() // ["navigation"]
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### Shortcut Maps
|
|
229
|
-
|
|
230
|
-
```tsx
|
|
231
|
-
import { useShortcutMap } from "@remcostoeten/use-shortcut"
|
|
232
|
-
|
|
233
|
-
useShortcutMap({
|
|
234
|
-
save: { keys: "mod+s", handler: () => save() },
|
|
235
|
-
undo: { keys: "mod+z", handler: () => undo() },
|
|
236
|
-
dashboard: { keys: ["g", "d"], handler: () => goToDashboard() },
|
|
237
|
-
})
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### Recording Mode
|
|
241
|
-
|
|
242
|
-
```tsx
|
|
243
|
-
const combo = await $.record({ timeoutMs: 5000 })
|
|
244
|
-
// e.g. "ctrl+k" or "cmd+k"
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
### Shortcut Groups
|
|
248
|
-
|
|
249
|
-
```tsx
|
|
250
|
-
import { createShortcutGroup } from "@remcostoeten/use-shortcut"
|
|
251
|
-
|
|
252
|
-
const group = createShortcutGroup()
|
|
253
|
-
|
|
254
|
-
group.add($.mod.key("k").on(openPalette))
|
|
255
|
-
group.add($.mod.key("p").on(openSearch))
|
|
256
|
-
|
|
257
|
-
// Later, cleanup all at once
|
|
258
|
-
group.unbindAll()
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### Hook Options
|
|
262
|
-
|
|
263
|
-
```tsx
|
|
264
|
-
const $ = useShortcut({
|
|
265
|
-
debug: true, // Log all shortcuts to console
|
|
266
|
-
delay: 0, // Global delay for all shortcuts
|
|
267
|
-
ignoreInputs: true, // Ignore in form elements (default: true)
|
|
268
|
-
disabled: false, // Disable all shortcuts
|
|
269
|
-
eventType: "keydown", // or "keyup"
|
|
270
|
-
target: window, // Custom event target
|
|
271
|
-
activeScopes: "editor", // Active named scopes
|
|
272
|
-
sequenceTimeout: 800, // ms to complete sequence
|
|
273
|
-
conflictWarnings: true, // Warn on overlaps
|
|
274
|
-
onConflict: (conflict) => {
|
|
275
|
-
console.warn(conflict)
|
|
276
|
-
},
|
|
277
|
-
eventFilter: (event) => !event.isComposing,
|
|
278
|
-
})
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### Handler Options (Advanced)
|
|
282
|
-
|
|
283
|
-
```tsx
|
|
284
|
-
$.mod.key("k").on(openPalette, {
|
|
285
|
-
priority: 10, // higher runs first
|
|
286
|
-
stopOnMatch: true, // prevent lower priority handlers from running
|
|
287
|
-
})
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
## Vanilla JS (Non-React)
|
|
291
|
-
|
|
292
|
-
```tsx
|
|
293
|
-
import { createShortcut } from "@remcostoeten/use-shortcut"
|
|
294
|
-
|
|
295
|
-
const $ = createShortcut()
|
|
296
|
-
|
|
297
|
-
const save = $.mod.key("s").on(() => {
|
|
298
|
-
console.log("Saved!")
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
// Clean up when done
|
|
302
|
-
save.unbind()
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
## Display Formatting
|
|
306
|
-
|
|
307
|
-
```tsx
|
|
308
|
-
import { formatShortcut } from "@remcostoeten/use-shortcut"
|
|
309
|
-
|
|
310
|
-
formatShortcut("cmd+s") // "⌘S" on Mac, "Ctrl+S" on Windows
|
|
311
|
-
formatShortcut("ctrl+shift+p") // "⌃⇧P" on Mac, "Ctrl+Shift+P" on Windows
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
## TypeScript
|
|
315
|
-
|
|
316
|
-
Full type definitions with intellisense:
|
|
317
|
-
|
|
318
|
-
```tsx
|
|
319
|
-
import type {
|
|
320
|
-
ShortcutBuilder,
|
|
321
|
-
ShortcutResult,
|
|
322
|
-
ShortcutHandler,
|
|
323
|
-
HandlerOptions,
|
|
324
|
-
ActionKey,
|
|
325
|
-
ModifierName,
|
|
326
|
-
} from "@remcostoeten/use-shortcut"
|
|
44
|
+
bun run typecheck
|
|
45
|
+
bun run test
|
|
46
|
+
bun run build
|
|
327
47
|
```
|
|
328
48
|
|
|
329
49
|
## License
|
|
330
50
|
|
|
331
|
-
MIT
|
|
51
|
+
MIT
|