@inklu/keys 2.0.2 → 2.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/LICENSE +1 -1
- package/README.md +77 -51
- package/dist/attributes.d.mts +11 -2
- package/dist/attributes.mjs +7 -6
- package/dist/{index-BTgscYdI.d.mts → index-3d9f54ln.d.mts} +50 -10
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -3
- package/dist/react.d.mts +22 -6
- package/dist/react.mjs +130 -97
- package/dist/src-B76YvObL.mjs +1687 -0
- package/dist/{attributes-DotLr2MW.d.mts → types-QEXldPnL.d.mts} +163 -191
- package/package.json +5 -5
- package/dist/src-DgYjA1oS.mjs +0 -1697
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# @inklu/keys
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@inklu/keys` organises an app's keys and respects its boundaries. Every shortcut is a named command declared in one place, and exactly one command runs per keypress: the right one for where the user is.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Three ideas, learned as you need them:
|
|
6
|
+
|
|
7
|
+
- **Command**: something the app can do, with a title, default keys and the scope it is live in.
|
|
8
|
+
- **Handler**: what the command does, attached from the component that owns the state.
|
|
9
|
+
- **Keymap**: the keys, kept apart from the definition so users can rebind them.
|
|
10
|
+
|
|
11
|
+
It has no dependencies, and listens with one `keydown` listener however many commands exist.
|
|
12
|
+
|
|
13
|
+
Docs: [inklu.saddy.me/keys](https://inklu.saddy.me/keys) · For agents: [llms.txt](https://inklu.saddy.me/keys/llms.txt) · Agent skill: `npx skills add nkurunziza-saddy/inklu --skill inklu-keys`
|
|
6
14
|
|
|
7
15
|
```bash
|
|
8
16
|
pnpm add @inklu/keys
|
|
@@ -10,22 +18,22 @@ pnpm add @inklu/keys
|
|
|
10
18
|
|
|
11
19
|
## 1. Define Commands
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
Declare every command in one module. Ids come from the object keys, so `commands.run("doc.save")` autocompletes and a typo is a type error.
|
|
22
|
+
|
|
23
|
+
A chord is written modifiers first, then one key: `"Mod+Shift+S"`. The modifiers are `Mod`, `Control`, `Alt`, `Shift` and `Meta`, in that order, and `Mod` is Command on macOS and Control elsewhere. The key is the character or the name `KeyboardEvent.key` gives it (`"K"`, `"?"`, `"Escape"`, `"ArrowUp"`), or a physical key in brackets (`"[KeyK]"`) that stays put whatever the layout. Chords are typed, so a misspelling is a compile error. An array of chords means alternatives; a sequence is `seq("G", "I")`.
|
|
14
24
|
|
|
15
25
|
```ts
|
|
16
26
|
// commands.ts
|
|
17
27
|
import { createCommands, seq } from "@inklu/keys";
|
|
18
28
|
|
|
19
29
|
export const commands = createCommands({
|
|
20
|
-
"palette.open": {
|
|
21
|
-
|
|
22
|
-
keys: "Mod+K",
|
|
23
|
-
run: () => setPaletteOpen(true),
|
|
24
|
-
},
|
|
30
|
+
"palette.open": { title: "Open command palette", keys: "Mod+K" },
|
|
31
|
+
"doc.save": { title: "Save document", keys: "Mod+S" },
|
|
25
32
|
"nav.inbox": {
|
|
26
33
|
title: "Go to inbox",
|
|
27
34
|
keys: seq("G", "I"), // press G, then I
|
|
28
|
-
|
|
35
|
+
// Needs nothing a component owns, so it can live here.
|
|
36
|
+
run: () => router.navigate("/inbox"),
|
|
29
37
|
},
|
|
30
38
|
});
|
|
31
39
|
```
|
|
@@ -35,7 +43,7 @@ export const commands = createCommands({
|
|
|
35
43
|
Pass the registry explicitly to all hooks. There is no Context provider.
|
|
36
44
|
|
|
37
45
|
```tsx
|
|
38
|
-
import {
|
|
46
|
+
import { useCommandHandler, useMountCommands, useShortcut } from "@inklu/keys/react";
|
|
39
47
|
import { commands } from "./commands";
|
|
40
48
|
|
|
41
49
|
function App() {
|
|
@@ -44,6 +52,13 @@ function App() {
|
|
|
44
52
|
return <ShortcutButton />;
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
function Palette() {
|
|
56
|
+
const [open, setOpen] = useState(false);
|
|
57
|
+
// What the command does, attached where the state lives.
|
|
58
|
+
useCommandHandler(commands, "palette.open", () => setOpen(true));
|
|
59
|
+
return open ? <dialog open>…</dialog> : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
function ShortcutButton() {
|
|
48
63
|
// Live state for the primary binding: `tokens` is one string per key
|
|
49
64
|
const shortcut = useShortcut(commands, "palette.open");
|
|
@@ -62,6 +77,21 @@ function ShortcutButton() {
|
|
|
62
77
|
}
|
|
63
78
|
```
|
|
64
79
|
|
|
80
|
+
### Handlers
|
|
81
|
+
|
|
82
|
+
A command runs when its scope is open and it has a handler. A definition's own `run` is one; `useCommandHandler` attaches another for as long as a component is mounted, and the latest closure is always the one called.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// The last argument switches the handler off: with nothing to save, Mod+S is not ours.
|
|
86
|
+
useCommandHandler(commands, "doc.save", save, dirty);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- A command with no handler cannot run and leaves its key alone, so unmounting a feature switches its shortcuts off with it.
|
|
90
|
+
- The most recently mounted handler wins. Unmounting hands the command back to the one before it, or to the definition's `run`. "Most recently" is the order React runs effects in, so of a parent and a child mounting in the same render, the parent's comes last and wins.
|
|
91
|
+
- Outside React, `commands.handle(id, run)` does the same and returns a function that detaches.
|
|
92
|
+
|
|
93
|
+
Switch a command off by its handler (`enabled`, or detaching) rather than with an early `return` inside it: only then does the key fall through to another command or the browser.
|
|
94
|
+
|
|
65
95
|
## 3. Scopes
|
|
66
96
|
|
|
67
97
|
Use scopes to isolate commands when modals or dialogs are open.
|
|
@@ -70,23 +100,28 @@ Use scopes to isolate commands when modals or dialogs are open.
|
|
|
70
100
|
import { useScope } from "@inklu/keys/react";
|
|
71
101
|
|
|
72
102
|
function Dialog({ isOpen }) {
|
|
73
|
-
// When exclusive is true, commands outside this scope are silenced
|
|
103
|
+
// When exclusive is true, the keys of commands outside this scope are silenced
|
|
74
104
|
useScope(commands, "dialog", isOpen, { exclusive: true });
|
|
75
105
|
|
|
76
106
|
return isOpen ? <div className="dialog">...</div> : null;
|
|
77
107
|
}
|
|
78
108
|
```
|
|
79
109
|
|
|
80
|
-
`exclusive` is a barrier: it silences
|
|
110
|
+
`exclusive` is a barrier: it silences every key beneath it. To claim only a few keys, drop it and give those commands a `scope` instead — see below.
|
|
111
|
+
|
|
112
|
+
A barrier claims the keyboard, not the command: `commands.run(id)` still works beneath one, so a palette can run what it lists while its own scope is open. A direct run only needs the command's own scope to be open. To make a button obey the same gate as its key, read `reachable` from `useCommand(commands, id)`.
|
|
81
113
|
|
|
82
114
|
### When two commands share a key
|
|
83
115
|
|
|
84
|
-
Exactly one command runs per keypress.
|
|
116
|
+
Exactly one command runs per keypress. Among the commands that could take it, the winner is:
|
|
85
117
|
|
|
86
|
-
1.
|
|
87
|
-
2.
|
|
118
|
+
1. A sequence being finished, over a chord on the same key.
|
|
119
|
+
2. The command whose scope was opened most recently. Scopes track the surfaces on screen, so the dialog on top claims `Escape` before the panel beneath it, and any scoped command beats a global one. `"*"` counts as global here — it means "survives barriers", not "belongs to a region of the UI".
|
|
120
|
+
3. Failing that, the one defined later.
|
|
88
121
|
|
|
89
|
-
|
|
122
|
+
Scopes rank in the order they were pushed. With `useScope` that is the order React runs effects in: a surface opened later ranks higher, but of a parent and a child that open in the same render, the child is pushed first. Open the inner surface after the outer one when their order matters.
|
|
123
|
+
|
|
124
|
+
A command with no handler is never a candidate, so it steps aside rather than swallowing the keypress. This is how `Escape` closes the dialog when one is open and the panel otherwise, without making the dialog `exclusive`. Two commands sharing a chord _in the same scope_ can only be told apart by which has a handler, so that case warns in development.
|
|
90
125
|
|
|
91
126
|
## 4. Rebinding
|
|
92
127
|
|
|
@@ -136,7 +171,7 @@ rebinder.subscribe(() => render(rebinder.state)); // { recording, binding, steps
|
|
|
136
171
|
rebinder.start(); // Escape cancels, Backspace unbinds
|
|
137
172
|
```
|
|
138
173
|
|
|
139
|
-
While recording, every
|
|
174
|
+
While recording, every command is silenced — including `"*"` ones — so the keys being recorded never run anything. The rebinder is built on `commands.capture(onKey)`, which hands every keypress to `onKey(event, chord)` until the function it returns is called; use it directly for a recorder of your own.
|
|
140
175
|
|
|
141
176
|
## 5. Conflicts
|
|
142
177
|
|
|
@@ -145,7 +180,7 @@ While recording, every other command is silenced — including `"*"` ones — so
|
|
|
145
180
|
- `"exact"` — the same keys. Only one command runs (see above).
|
|
146
181
|
- `"prefix"` — one binding starts the other, like `G` against `seq("G", "I")`. These cannot be arbitrated: pressing `G` runs the chord, and continuing with `I` runs the sequence too.
|
|
147
182
|
|
|
148
|
-
In development, both kinds are also reported with `console.warn` when
|
|
183
|
+
In development, both kinds are also reported with `console.warn` when the registry is created. The check is dropped from production builds.
|
|
149
184
|
|
|
150
185
|
## 6. Persistence and SSR
|
|
151
186
|
|
|
@@ -158,50 +193,34 @@ createCommands(definitions, {
|
|
|
158
193
|
});
|
|
159
194
|
```
|
|
160
195
|
|
|
161
|
-
Only user overrides are stored — never defaults. `keymap.export()` / `keymap.import(json)` move them between devices. The stored data carries a `version`, and a release that doesn't recognise a version
|
|
196
|
+
Only user overrides are stored — never defaults. `keymap.export()` / `keymap.import(json)` move them between devices. The stored data carries a `version`, and a release that doesn't recognise a version ignores it instead of misreading it; the user's next change then replaces it. Data from releases before the version field still loads.
|
|
162
197
|
|
|
163
198
|
While mounted, the registry follows edits other tabs make to the same storage key, so a shortcut rebound in one tab applies in every open tab, and a stale tab can't overwrite it on its next save.
|
|
164
199
|
|
|
165
200
|
The registry is safe to create at module scope and render on the server:
|
|
166
201
|
|
|
167
202
|
- Persisted overrides load on the first `mount()`, not at creation, so the server render and the client's first render match. Subscribers (and every hook) are notified when they arrive. Calling `keymap.set/add/reset` before mount loads them first, so an early edit never overwrites stored ones.
|
|
168
|
-
- Before mount, shortcuts render in the non-Apple form (`Ctrl+K`); after mount they switch to the detected platform (`⌘K`). Pass `platform` to pin it.
|
|
203
|
+
- Before mount, shortcuts render in the non-Apple form (`Ctrl+K`); after mount they switch to the detected platform (`⌘ K`). Pass `platform` to pin it.
|
|
169
204
|
- Nothing listens for keys until `mount()` (or `useMountCommands`).
|
|
170
205
|
|
|
171
206
|
## 7. Inputs and events
|
|
172
207
|
|
|
173
208
|
`ignoreInputs` on a command decides whether it fires while focus is in a text input, textarea, select or contenteditable. When omitted, chords holding `Control` or `Meta` (so any `Mod+…`) and bare `Escape` still fire there; bare keys, `Shift`/`Alt` chords and sequences starting with one are ignored, so typing is never hijacked. Keydowns that are part of an IME composition never trigger commands.
|
|
174
209
|
|
|
175
|
-
A command calls `preventDefault()` only when it actually runs; one that is
|
|
176
|
-
|
|
177
|
-
`commands.whyNotRun(id)` explains a shortcut that did not fire: `"disabled"`, `"out-of-scope"`, `"guarded"`, `"unknown"`, or `null` when it would run.
|
|
178
|
-
|
|
179
|
-
## 8. Runtime commands
|
|
210
|
+
A command calls `preventDefault()` only when it actually runs, or when the key is a step on the way through one of its sequences; one that is unhandled, out of scope or outranked leaves the event alone. Opt out per command with `preventDefault: false`. Propagation is never stopped. A handler may be async: nothing awaits it, but a rejection reaches `onError`. Holding a chord down runs its command again on each auto-repeat.
|
|
180
211
|
|
|
181
|
-
|
|
182
|
-
// Registered while the component is mounted; the latest `run` and `when` are always used.
|
|
183
|
-
useRegisterCommand(commands, "editor.format", {
|
|
184
|
-
title: "Format document",
|
|
185
|
-
keys: "Shift+Alt+F",
|
|
186
|
-
scope: "editor",
|
|
187
|
-
run: () => format(),
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// Outside React: `add` returns a handle. Adding an existing id stacks over it until disposed.
|
|
191
|
-
const handle = commands.add("editor.format", { title: "Format", run: format });
|
|
192
|
-
handle.dispose();
|
|
193
|
-
```
|
|
212
|
+
`commands.whyNotRun(id)` explains a command that would not run: `"unhandled"` (nothing has attached a handler), `"out-of-scope"` (its scope is not open), `"unknown"`, or `null` when it would run.
|
|
194
213
|
|
|
195
|
-
##
|
|
214
|
+
## 8. Sequence progress
|
|
196
215
|
|
|
197
216
|
```tsx
|
|
198
217
|
const pending = usePendingSequences(commands); // [{ id, matched, total, remaining }]
|
|
199
218
|
// e.g. show "G → …" while the user is halfway through `G then I`
|
|
200
219
|
```
|
|
201
220
|
|
|
202
|
-
`commands.
|
|
221
|
+
`commands.subscribePending()` / `commands.pending()` are the non-React equivalents. Progress is tracked only while something is subscribed, so `pending()` is empty otherwise.
|
|
203
222
|
|
|
204
|
-
##
|
|
223
|
+
## 9. Command palette
|
|
205
224
|
|
|
206
225
|
Definitions can carry what a palette needs to list and find them, and `matchCommands` does the filtering:
|
|
207
226
|
|
|
@@ -213,21 +232,22 @@ createCommands({
|
|
|
213
232
|
group: "Document", // heading to list it under
|
|
214
233
|
keywords: ["search", "lookup"], // extra search terms
|
|
215
234
|
keys: "Mod+F",
|
|
216
|
-
run: openFind,
|
|
217
235
|
},
|
|
218
|
-
"palette.close": { title: "Close palette", hidden: true,
|
|
236
|
+
"palette.close": { title: "Close palette", hidden: true, scope: "palette", keys: "Escape" },
|
|
219
237
|
});
|
|
220
238
|
```
|
|
221
239
|
|
|
222
240
|
```tsx
|
|
223
241
|
import { matchCommands } from "@inklu/keys";
|
|
224
242
|
|
|
225
|
-
|
|
243
|
+
// List what can run right now: it has a handler and its scope is open.
|
|
244
|
+
const runnable = useCommands(commands).filter((command) => commands.canRun(command.id));
|
|
245
|
+
const results = matchCommands(runnable, query);
|
|
226
246
|
```
|
|
227
247
|
|
|
228
248
|
Every word of the query must match the title, description, group, keywords, id or displayed shortcut. Titles starting with the query come first, then titles containing it, then everything else, each in list order. `hidden` commands never match; they still run and still appear in `list()`.
|
|
229
249
|
|
|
230
|
-
##
|
|
250
|
+
## 10. Without React: data attributes
|
|
231
251
|
|
|
232
252
|
```ts
|
|
233
253
|
import { observe } from "@inklu/keys/attributes";
|
|
@@ -245,25 +265,27 @@ Clicking an element with `data-command` runs that command (`source: "dom"`), unl
|
|
|
245
265
|
|
|
246
266
|
- `createCommands(definitions, options)` - creates the registry
|
|
247
267
|
- `options`: `{ bindings, platform, target, persist, display, singleKeyShortcuts, onRun, onError }`
|
|
248
|
-
- `commands.run(id)`, `canRun(id)`, `whyNotRun(id)`, `
|
|
249
|
-
- `commands.
|
|
268
|
+
- `commands.run(id)`, `canRun(id)`, `whyNotRun(id)`, `handle(id, run)`
|
|
269
|
+
- `commands.get(id)`, `list()`, `subscribe(listener)`: snapshots with `display`, `tokens`, `aria`, `spoken`, `reachable`, `overridden`
|
|
270
|
+
- `commands.pushScope(name, { exclusive })`, `capture(onKey)`, `conflicts(binding, options)`, `mount()`, `destroy()`
|
|
271
|
+
- `commands.serverList()`: the list as the server rendered it, for a framework's hydration snapshot
|
|
250
272
|
- `commands.keymap`: `get`, `set`, `add`, `reset`, `export`, `import`
|
|
251
273
|
- `commands.setSingleKeyShortcuts(enabled)`, `commands.singleKeyShortcuts`
|
|
252
274
|
- `createRebinder(commands, id, options)` - framework-agnostic shortcut recording
|
|
253
275
|
- `observe(commands, { root })` from `@inklu/keys/attributes` - declarative DOM wiring
|
|
254
|
-
- `matchCommands(list, query)` - palette filtering over `title`, `description`, `group`, `keywords
|
|
276
|
+
- `matchCommands(list, query)` - palette filtering over `title`, `description`, `group`, `keywords`, id and displayed shortcut
|
|
255
277
|
- `formatBinding`, `bindingTokens`, `ariaKeyShortcuts`, `spokenBinding`, `validateBinding`, `isSingleKeyBinding`, `seq`
|
|
256
278
|
- React (`@inklu/keys/react`):
|
|
257
279
|
- `useMountCommands(commands)` - starts listening
|
|
258
280
|
- `useCommands(commands)`, `useCommand(commands, id)` - live command snapshots
|
|
259
281
|
- `useShortcut(commands, id)` - `{ display, tokens, aria, spoken }` for the primary binding
|
|
260
|
-
- `useScope(commands, name, active, { exclusive
|
|
261
|
-
- `
|
|
282
|
+
- `useScope(commands, name, active, { exclusive })` - opens a scope while a component needs it
|
|
283
|
+
- `useCommandHandler(commands, id, run, enabled)` - what a command does while a component is mounted
|
|
262
284
|
- `useRebind(commands, id, options)` - records user shortcuts
|
|
263
285
|
- `usePendingSequences(commands)` - half-typed sequences
|
|
264
286
|
- `useHeldKeys()`, `useHeldKeyCodes()`, `useKeyHold(key)` - live key state
|
|
265
287
|
|
|
266
|
-
Inside
|
|
288
|
+
Inside a handler, the context's `commands` is typed with the registry's own ids, so `commands.run("typo")` is a compile error.
|
|
267
289
|
|
|
268
290
|
## Accessibility
|
|
269
291
|
|
|
@@ -284,6 +306,10 @@ commands.setSingleKeyShortcuts(false); // or createCommands(defs, { singleKeySho
|
|
|
284
306
|
|
|
285
307
|
While off, those bindings neither fire nor appear in `display`, `tokens`, `aria` or `spoken`; chords with `Control`, `Alt` or `Meta` and keys like `Escape` or `F1` keep working. Users can also remap any shortcut with `useRebind`.
|
|
286
308
|
|
|
309
|
+
## Acknowledgements
|
|
310
|
+
|
|
311
|
+
The rules for matching a keypress across keyboard layouts follow the matcher in [TanStack Hotkeys](https://tanstack.com/hotkeys) (MIT), which earlier releases depended on.
|
|
312
|
+
|
|
287
313
|
## License
|
|
288
314
|
|
|
289
315
|
MIT © nkurunziza-saddy
|
package/dist/attributes.d.mts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { l as Commands } from "./types-QEXldPnL.mjs";
|
|
2
|
+
//#region src/attributes.d.ts
|
|
3
|
+
export interface ObserveOptions {
|
|
4
|
+
/** Root document or element to delegate from. Defaults to `document`. */
|
|
5
|
+
root?: Document | Element;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Wires a commands registry to DOM data attributes. Returns a cleanup function.
|
|
9
|
+
*/
|
|
10
|
+
export declare function observe<Id extends string>(registry: Commands<Id>, options?: ObserveOptions): () => void;
|
|
11
|
+
//#endregion
|
package/dist/attributes.mjs
CHANGED
|
@@ -7,15 +7,16 @@ const KEYS_ATTRIBUTE = "data-command-keys";
|
|
|
7
7
|
function observe(registry, options = {}) {
|
|
8
8
|
if (!("document" in globalThis)) return () => {};
|
|
9
9
|
const root = options.root ?? document;
|
|
10
|
+
const find = (id) => id ? registry.list().find((command) => command.id === id) : void 0;
|
|
10
11
|
const onClick = (event) => {
|
|
11
12
|
if (event.defaultPrevented) return;
|
|
12
13
|
const target = event.target;
|
|
13
14
|
if (!(target instanceof Element)) return;
|
|
14
15
|
const element = target.closest(`[${COMMAND_ATTRIBUTE}]`);
|
|
15
|
-
const
|
|
16
|
-
if (!
|
|
16
|
+
const command = find(element?.getAttribute(COMMAND_ATTRIBUTE) ?? null);
|
|
17
|
+
if (!command) return;
|
|
17
18
|
if (element?.closest("[aria-disabled=\"true\"]")) return;
|
|
18
|
-
registry.run(id, {
|
|
19
|
+
registry.run(command.id, {
|
|
19
20
|
source: "dom",
|
|
20
21
|
event
|
|
21
22
|
});
|
|
@@ -24,15 +25,15 @@ function observe(registry, options = {}) {
|
|
|
24
25
|
const paintElement = (element) => {
|
|
25
26
|
const keysId = element.getAttribute(KEYS_ATTRIBUTE);
|
|
26
27
|
if (keysId) {
|
|
27
|
-
const display =
|
|
28
|
+
const display = find(keysId)?.display[0] ?? "";
|
|
28
29
|
if (element.textContent !== display) element.textContent = display;
|
|
29
30
|
}
|
|
30
31
|
const commandId = element.getAttribute(COMMAND_ATTRIBUTE);
|
|
31
|
-
const aria =
|
|
32
|
+
const aria = find(commandId)?.aria ?? "";
|
|
32
33
|
if (aria) {
|
|
33
34
|
if (element.getAttribute("aria-keyshortcuts") !== aria) element.setAttribute("aria-keyshortcuts", aria);
|
|
34
35
|
labelled.add(element);
|
|
35
|
-
} else if (
|
|
36
|
+
} else if (labelled.has(element)) {
|
|
36
37
|
element.removeAttribute("aria-keyshortcuts");
|
|
37
38
|
labelled.delete(element);
|
|
38
39
|
}
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import { Hotkey } from "@tanstack/hotkeys";
|
|
1
|
+
import { C as SequenceBinding, D as Chord, a as CommandDefinition, d as Conflict, k as Platform, l as Commands, m as DisplayOptions, r as Command, t as Binding, u as CommandsOptions } from "./types-QEXldPnL.mjs";
|
|
3
2
|
//#region src/bindings.d.ts
|
|
4
3
|
/**
|
|
5
|
-
* Creates a sequence binding from ordered
|
|
4
|
+
* Creates a sequence binding from ordered chords.
|
|
6
5
|
*
|
|
7
6
|
* @example
|
|
8
7
|
* ```ts
|
|
9
8
|
* keys: seq("G", "I")
|
|
10
9
|
* ```
|
|
11
10
|
*/
|
|
12
|
-
declare function seq(...steps: Array<
|
|
11
|
+
declare function seq(...steps: Array<Chord>): SequenceBinding;
|
|
13
12
|
/**
|
|
14
13
|
* Whether every step of a binding is a single printable character with no
|
|
15
14
|
* Control, Alt or Meta — the "character key shortcuts" WCAG 2.1.4 lets users
|
|
16
15
|
* turn off, since speech input and stray keypresses trigger them by accident.
|
|
17
|
-
* Shift does not count as a modifier here: `Shift+A` still types a character.
|
|
18
16
|
*/
|
|
19
|
-
declare function isSingleKeyBinding(binding: Binding
|
|
17
|
+
declare function isSingleKeyBinding(binding: Binding): boolean;
|
|
20
18
|
/** Validation result for a hotkey binding. */
|
|
21
19
|
interface BindingValidation {
|
|
22
20
|
readonly valid: boolean;
|
|
@@ -26,6 +24,21 @@ interface BindingValidation {
|
|
|
26
24
|
/** Validates a binding's syntax without logging warnings or errors. */
|
|
27
25
|
declare function validateBinding(binding: Binding): BindingValidation;
|
|
28
26
|
//#endregion
|
|
27
|
+
//#region src/display.d.ts
|
|
28
|
+
/** Formats a binding for display on the target platform (e.g. `⌘ K` or `Ctrl+K`). */
|
|
29
|
+
declare function formatBinding(binding: Binding, options?: DisplayOptions): string;
|
|
30
|
+
/** Splits a binding into display tokens for rendering `<kbd>` elements. */
|
|
31
|
+
declare function bindingTokens(binding: Binding, options?: DisplayOptions): Array<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Phrases a binding for a screen reader: `"Control+K"` for a chord,
|
|
34
|
+
* `"G then I"` for a sequence. `aria-keyshortcuts` cannot express a sequence
|
|
35
|
+
* (a space there means "or"), so this is how a sequence gets announced — put it
|
|
36
|
+
* in visually hidden text or `aria-description`.
|
|
37
|
+
*/
|
|
38
|
+
declare function spokenBinding(binding: Binding, platform?: Platform): string;
|
|
39
|
+
/** Generates an `aria-keyshortcuts` attribute string for a binding. */
|
|
40
|
+
declare function ariaKeyShortcuts(binding: Binding, platform?: Platform): string;
|
|
41
|
+
//#endregion
|
|
29
42
|
//#region src/rebind.d.ts
|
|
30
43
|
/** Hotkey capture mode: single `"chord"` or multi-step `"sequence"`. */
|
|
31
44
|
type RebindMode = "chord" | "sequence";
|
|
@@ -80,6 +93,9 @@ interface Rebinder<Id extends string = string> {
|
|
|
80
93
|
}
|
|
81
94
|
/**
|
|
82
95
|
* Creates a rebinder instance for capturing and remapping a command's keys.
|
|
96
|
+
* While it records it holds the whole keyboard, so no command runs on the
|
|
97
|
+
* keys being pressed. Escape cancels, Backspace removes the shortcut (or the
|
|
98
|
+
* last step of a sequence), and Enter finishes a sequence.
|
|
83
99
|
*
|
|
84
100
|
* @example
|
|
85
101
|
* ```ts
|
|
@@ -93,9 +109,33 @@ interface Rebinder<Id extends string = string> {
|
|
|
93
109
|
*/
|
|
94
110
|
declare function createRebinder<Id extends string>(registry: Commands<Id>, id: Id, options?: RebindOptions<Id>): Rebinder<Id>;
|
|
95
111
|
//#endregion
|
|
96
|
-
//#region src/
|
|
97
|
-
/**
|
|
98
|
-
|
|
112
|
+
//#region src/registry.d.ts
|
|
113
|
+
/**
|
|
114
|
+
* Creates a command registry.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* export const commands = createCommands({
|
|
119
|
+
* "palette.open": { title: "Open command palette", keys: "Mod+K" },
|
|
120
|
+
* "nav.inbox": {
|
|
121
|
+
* title: "Go to inbox",
|
|
122
|
+
* keys: seq("G", "I"),
|
|
123
|
+
* run: () => router.navigate("/inbox"),
|
|
124
|
+
* },
|
|
125
|
+
* });
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
declare function createCommands<Id extends string>(definitions: { [K in Id]: CommandDefinition<Id>; }, options?: CommandsOptions<NoInfer<Id>>): Commands<Id>;
|
|
129
|
+
/**
|
|
130
|
+
* Extracts command ID union from a Commands registry type.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* export const commands = createCommands({ … });
|
|
135
|
+
* export type AppCommandId = CommandId<typeof commands>;
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
type CommandId<C> = C extends Commands<infer Id> ? Id : never;
|
|
99
139
|
//#endregion
|
|
100
140
|
//#region src/search.d.ts
|
|
101
141
|
/**
|
|
@@ -113,4 +153,4 @@ declare const ALWAYS_SCOPE = "*";
|
|
|
113
153
|
*/
|
|
114
154
|
declare function matchCommands<Id extends string>(commands: ReadonlyArray<Command<Id>>, query: string): Array<Command<Id>>;
|
|
115
155
|
//#endregion
|
|
116
|
-
export {
|
|
156
|
+
export { RebindOptions as a, createRebinder as c, formatBinding as d, spokenBinding as f, validateBinding as g, seq as h, RebindMode as i, ariaKeyShortcuts as l, isSingleKeyBinding as m, CommandId as n, RebindState as o, BindingValidation as p, createCommands as r, Rebinder as s, matchCommands as t, bindingTokens as u };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
export {
|
|
1
|
+
import { C as SequenceBinding, D as Chord, E as SerializedOverride, O as Key, S as ScopeOptions, T as SerializedKeymap, _ as KeymapPersistence, a as CommandDefinition, b as RunBlockReason, c as CommandSource, d as Conflict, f as ConflictKind, g as KeyCapture, h as Json, i as CommandContext, k as Platform, l as Commands, m as DisplayOptions, n as BindingInput, o as CommandHandler, p as ConflictOptions, r as Command, s as CommandKeymap, t as Binding, u as CommandsOptions, v as KeymapStorage, w as SerializedBinding, x as RunOptions, y as PendingSequence } from "./types-QEXldPnL.mjs";
|
|
2
|
+
import { a as RebindOptions, c as createRebinder, d as formatBinding, f as spokenBinding, g as validateBinding, h as seq, i as RebindMode, l as ariaKeyShortcuts, m as isSingleKeyBinding, n as CommandId, o as RebindState, p as BindingValidation, r as createCommands, s as Rebinder, t as matchCommands, u as bindingTokens } from "./index-3d9f54ln.mjs";
|
|
3
|
+
export { type Binding, type BindingInput, type BindingValidation, type Chord, type Command, type CommandContext, type CommandDefinition, type CommandHandler, type CommandId, type CommandKeymap, type CommandSource, type Commands, type CommandsOptions, type Conflict, type ConflictKind, type ConflictOptions, type DisplayOptions, type Json, type Key, type KeyCapture, type KeymapPersistence, type KeymapStorage, type PendingSequence, type Platform, type RebindMode, type RebindOptions, type RebindState, type Rebinder, type RunBlockReason, type RunOptions, type ScopeOptions, type SequenceBinding, type SerializedBinding, type SerializedKeymap, type SerializedOverride, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, isSingleKeyBinding, matchCommands, seq, spokenBinding, validateBinding };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export { ALWAYS_SCOPE, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, isSingleKeyBinding, matchCommands, observe, seq, spokenBinding, validateBinding };
|
|
1
|
+
import { a as bindingTokens, c as isSingleKeyBinding, i as ariaKeyShortcuts, l as seq, n as createCommands, o as formatBinding, r as createRebinder, s as spokenBinding, t as matchCommands, u as validateBinding } from "./src-B76YvObL.mjs";
|
|
2
|
+
export { ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, isSingleKeyBinding, matchCommands, seq, spokenBinding, validateBinding };
|
package/dist/react.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
1
|
+
import { C as SequenceBinding, D as Chord, E as SerializedOverride, O as Key, S as ScopeOptions, T as SerializedKeymap, _ as KeymapPersistence, a as CommandDefinition, b as RunBlockReason, c as CommandSource, d as Conflict, f as ConflictKind, g as KeyCapture, h as Json, i as CommandContext, k as Platform, l as Commands, m as DisplayOptions, n as BindingInput, o as CommandHandler, p as ConflictOptions, r as Command, s as CommandKeymap, t as Binding, u as CommandsOptions, v as KeymapStorage, w as SerializedBinding, x as RunOptions, y as PendingSequence } from "./types-QEXldPnL.mjs";
|
|
2
|
+
import { a as RebindOptions, c as createRebinder, d as formatBinding, f as spokenBinding, g as validateBinding, h as seq, i as RebindMode, l as ariaKeyShortcuts, m as isSingleKeyBinding, n as CommandId, o as RebindState, p as BindingValidation, r as createCommands, s as Rebinder, t as matchCommands, u as bindingTokens } from "./index-3d9f54ln.mjs";
|
|
3
3
|
//#region src/react/hooks.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Mounts a commands registry for the lifecycle of the component.
|
|
@@ -29,7 +29,10 @@ export declare function useShortcut<Id extends string>(registry: Commands<Id>, i
|
|
|
29
29
|
spoken: string;
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
|
-
* Activates a scope for the lifecycle of the component.
|
|
32
|
+
* Activates a scope for the lifecycle of the component. Scopes rank in the
|
|
33
|
+
* order they are pushed, which here is the order React runs effects in: a
|
|
34
|
+
* surface opened later ranks higher, but of a parent and a child opening in
|
|
35
|
+
* the same render the child is pushed first.
|
|
33
36
|
*
|
|
34
37
|
* @example
|
|
35
38
|
* ```tsx
|
|
@@ -37,8 +40,21 @@ export declare function useShortcut<Id extends string>(registry: Commands<Id>, i
|
|
|
37
40
|
* ```
|
|
38
41
|
*/
|
|
39
42
|
export declare function useScope<Id extends string>(registry: Commands<Id>, name: string, active?: boolean, options?: ScopeOptions): void;
|
|
40
|
-
/**
|
|
41
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Attaches what a command does for the lifecycle of the component, so the
|
|
45
|
+
* behaviour lives where its state does. The latest `run` is always the one
|
|
46
|
+
* called. While `enabled` is `false` the component steps back: the command
|
|
47
|
+
* falls to the handler attached before it, or cannot run if there is none.
|
|
48
|
+
* The handler attached last wins, and React attaches a parent's after its
|
|
49
|
+
* children's when they mount together.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* useCommandHandler(commands, "palette.open", () => setOpen(true));
|
|
54
|
+
* useCommandHandler(commands, "doc.save", save, dirty);
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function useCommandHandler<Id extends string>(registry: Commands<Id>, id: NoInfer<Id>, run: CommandHandler<NoInfer<Id>>, enabled?: boolean): void;
|
|
42
58
|
/** State and actions for rebinding a command. */
|
|
43
59
|
interface UseRebindResult<Id extends string = string> extends RebindState<Id> {
|
|
44
60
|
/** Active display string for the shortcut. */
|
|
@@ -79,4 +95,4 @@ export declare function useHeldKeyCodes(): Readonly<Record<string, string>>;
|
|
|
79
95
|
*/
|
|
80
96
|
export declare function usePendingSequences<Id extends string>(registry: Commands<Id>): ReadonlyArray<PendingSequence<Id>>;
|
|
81
97
|
//#endregion
|
|
82
|
-
export {
|
|
98
|
+
export { type Binding, type BindingInput, type BindingValidation, type Chord, type Command, type CommandContext, type CommandDefinition, type CommandHandler, type CommandId, type CommandKeymap, type CommandSource, type Commands, type CommandsOptions, type Conflict, type ConflictKind, type ConflictOptions, type DisplayOptions, type Json, type Key, type KeyCapture, type KeymapPersistence, type KeymapStorage, type PendingSequence, type Platform, type RebindMode, type RebindOptions, type RebindState, type Rebinder, type RunBlockReason, type RunOptions, type ScopeOptions, type SequenceBinding, type SerializedBinding, type SerializedKeymap, type SerializedOverride, type UseRebindResult, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, isSingleKeyBinding, matchCommands, seq, spokenBinding, validateBinding };
|