@inklu/keys 2.1.0 → 2.2.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 +138 -21
- package/dist/attributes.d.mts +1 -1
- package/dist/attributes.mjs +15 -18
- package/dist/{index-3d9f54ln.d.mts → index-DRXMJmbc.d.mts} +53 -36
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/react.d.mts +17 -34
- package/dist/react.mjs +36 -74
- package/dist/{src-B76YvObL.mjs → src-TaZw4V61.mjs} +624 -514
- package/dist/{types-QEXldPnL.d.mts → types-D-hUKGoN.d.mts} +101 -142
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@ Three ideas, learned as you need them:
|
|
|
8
8
|
- **Handler**: what the command does, attached from the component that owns the state.
|
|
9
9
|
- **Keymap**: the keys, kept apart from the definition so users can rebind them.
|
|
10
10
|
|
|
11
|
-
It has no dependencies, and listens with one `keydown` listener however many commands exist.
|
|
11
|
+
It has no dependencies, and listens with one `keydown` listener however many commands exist. What an app does not use, it does not ship: persistence, recording a shortcut and palette search are imports of their own, and the registry does not pull them in.
|
|
12
12
|
|
|
13
|
-
Docs: [inklu.saddy.me/keys](https://inklu.saddy.me/keys) · For agents: [llms.txt](https://inklu.saddy.me/keys/llms.txt) · Agent
|
|
13
|
+
Docs: [inklu.saddy.me/keys](https://inklu.saddy.me/keys) · For agents: [llms.txt](https://inklu.saddy.me/keys/llms.txt) · Agent skills: `npx skills add nkurunziza-saddy/inklu --skill inklu-keys` for the API, and `--skill inklu-keys-practices` for choosing and showing shortcuts well
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
pnpm add @inklu/keys
|
|
@@ -38,6 +38,43 @@ export const commands = createCommands({
|
|
|
38
38
|
});
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
### Keys that differ by platform
|
|
42
|
+
|
|
43
|
+
`Mod` covers most shortcuts, since it is Command on a Mac and Control everywhere else. Where the platforms' own conventions differ, give each its keys: `mac` is for Apple keyboards (macOS, iPadOS, iOS), `other` for the rest. A side left out is unbound there.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
createCommands({
|
|
47
|
+
"edit.redo": { title: "Redo", keys: { mac: "Mod+Shift+Z", other: ["Mod+Y", "Mod+Shift+Z"] } },
|
|
48
|
+
"nav.back": { title: "Back", keys: { mac: "Mod+[", other: "Alt+ArrowLeft" } },
|
|
49
|
+
"doc.new": { title: "New document", keys: { mac: "Mod+Alt+N", other: "Alt+N" } },
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Some keys are not an app's to bind, and the registry warns about them in development:
|
|
54
|
+
|
|
55
|
+
- **Reserved by the browser or the operating system.** `Mod+N`, `Mod+T`, `Mod+W` and their `Shift` forms, `Control+Tab`, `Mod+Q` on a Mac, `Alt+F4` and anything holding `Meta` (the Windows or Super key) elsewhere. The page never receives most of these, so `preventDefault()` cannot help. That is why `doc.new` above is not `Mod+N`.
|
|
56
|
+
- **`Control+Alt` with a character key, on Windows and Linux.** Many keyboard layouts report AltGr as `Control+Alt`, and there the keys type a character (`@`, `€`, `{`) instead. `Mod+Alt+N` is fine on a Mac and not elsewhere, which is the other reason for `{ mac, other }`.
|
|
57
|
+
- **`Control+Alt` on a Mac.** It is the VoiceOver modifier.
|
|
58
|
+
|
|
59
|
+
Browser features a page _can_ take over (`Mod+F`, `Mod+P`, `Mod+L`, `Mod+R`) are left to your judgement: do it only where your version does the same job better.
|
|
60
|
+
|
|
61
|
+
### Commands generated from data
|
|
62
|
+
|
|
63
|
+
Ids come from the object's keys, so generated commands are typed when their keys are. `Object.fromEntries` returns plain string keys; give the result its key type where you build it:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const pages = Object.fromEntries(
|
|
67
|
+
docs.map((doc) => [`go.${doc.slug}`, { title: doc.title, run: () => router.navigate(doc.path) }]),
|
|
68
|
+
) as Record<`go.${Slug}`, CommandDefinition>;
|
|
69
|
+
|
|
70
|
+
export const commands = createCommands({
|
|
71
|
+
"palette.open": { title: "Open command palette", keys: "Mod+K" },
|
|
72
|
+
...pages,
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Spread from a `Record<string, CommandDefinition>` instead, the commands still exist at runtime but the types only know the literal keys. To have the compiler check that every id is present, name the ids yourself: `createCommands<AppCommandId>({ … })`.
|
|
77
|
+
|
|
41
78
|
## 2. React Setup
|
|
42
79
|
|
|
43
80
|
Pass the registry explicitly to all hooks. There is no Context provider.
|
|
@@ -89,6 +126,7 @@ useCommandHandler(commands, "doc.save", save, dirty);
|
|
|
89
126
|
- A command with no handler cannot run and leaves its key alone, so unmounting a feature switches its shortcuts off with it.
|
|
90
127
|
- 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
128
|
- Outside React, `commands.handle(id, run)` does the same and returns a function that detaches.
|
|
129
|
+
- A handler is attached when its component mounts. Keep it with its state in code that is already loaded, and lazy-load only what it shows: a handler inside a lazily loaded component leaves the key to the browser until its chunk arrives.
|
|
92
130
|
|
|
93
131
|
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
132
|
|
|
@@ -165,6 +203,7 @@ import { createRebinder } from "@inklu/keys";
|
|
|
165
203
|
const rebinder = createRebinder(commands, "palette.open", {
|
|
166
204
|
mode: "chord", // or "sequence": Enter or `commit()` finishes it
|
|
167
205
|
rejectConflicts: true, // don't apply a binding another command uses
|
|
206
|
+
// A binding the browser or OS reserves is never applied, whatever this says.
|
|
168
207
|
onRecord: (binding, conflicts) => {},
|
|
169
208
|
});
|
|
170
209
|
rebinder.subscribe(() => render(rebinder.state)); // { recording, binding, steps, conflicts }
|
|
@@ -175,39 +214,51 @@ While recording, every command is silenced — including `"*"` ones — so the k
|
|
|
175
214
|
|
|
176
215
|
## 5. Conflicts
|
|
177
216
|
|
|
178
|
-
`commands.conflicts(binding, { scope, ignore })` lists
|
|
217
|
+
`commands.conflicts(binding, { scope, ignore })` lists what stands in the way of `binding`. Each result has a `kind`:
|
|
179
218
|
|
|
180
|
-
- `"
|
|
219
|
+
- `"reserved"` — the browser or operating system keeps these keys on this platform (see [Keys that differ by platform](#keys-that-differ-by-platform)). Listed first. A rebinder reports it and never applies the binding.
|
|
220
|
+
- `"exact"` — a command in an overlapping scope has the same keys. Only one command runs (see above).
|
|
181
221
|
- `"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.
|
|
182
222
|
|
|
183
|
-
|
|
223
|
+
Only a conflict with a command has an `id`, so narrow on `kind` before reading it:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
rebind.conflicts.map((conflict) =>
|
|
227
|
+
conflict.kind === "reserved"
|
|
228
|
+
? `${conflict.display} belongs to the browser`
|
|
229
|
+
: `Also used by ${commands.get(conflict.id)?.title}`,
|
|
230
|
+
);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
In development, all three are also reported with `console.warn` when the registry is created, for the defaults on both kinds of platform. The check is dropped from production builds.
|
|
184
234
|
|
|
185
235
|
## 6. Persistence and SSR
|
|
186
236
|
|
|
187
237
|
```ts
|
|
188
|
-
createCommands
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
key: "my-app.keys",
|
|
192
|
-
},
|
|
193
|
-
});
|
|
238
|
+
import { createCommands, webStorage } from "@inklu/keys";
|
|
239
|
+
|
|
240
|
+
createCommands(definitions, { persist: webStorage("my-app.keys") });
|
|
194
241
|
```
|
|
195
242
|
|
|
196
|
-
|
|
243
|
+
`webStorage(key, storage?)` keeps the user's overrides in `localStorage`. It is an import, so a registry created without it carries no storage code. Pass a second argument to keep them elsewhere: a function returning the storage, as in `webStorage("my-app.keys", () => sessionStorage)`. Any object with `getItem`, `setItem` and `removeItem` will do, and the function is only called in the browser.
|
|
244
|
+
|
|
245
|
+
Only user overrides are stored — never defaults. To move them between devices yourself, `exportKeymap(commands)` returns them as JSON-safe data and `importKeymap(commands, data)` puts them back and saves them. The data carries a `version`, and data in a version this release does not know is ignored instead of misread.
|
|
246
|
+
|
|
247
|
+
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. Each mount reads what is stored, so a registry that was unmounted for a while has caught up by the time it listens again.
|
|
197
248
|
|
|
198
|
-
|
|
249
|
+
For a store that is not Web Storage, `persist` takes any object shaped like `KeymapStore`: `read()`, `write(overrides)` and `watch(onChange)`, all synchronous, with `Override` the type of one command's entry. For something asynchronous, such as a user's account, use `exportKeymap` and `importKeymap` instead.
|
|
199
250
|
|
|
200
251
|
The registry is safe to create at module scope and render on the server:
|
|
201
252
|
|
|
202
253
|
- 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.
|
|
203
|
-
- Before mount, shortcuts render in the non-Apple form (`Ctrl+K`); after mount they switch to the detected platform (`⌘
|
|
254
|
+
- Before mount, shortcuts render in the non-Apple form (`Ctrl+K`); after mount they switch to the detected platform (`⌘K`), and `{ mac, other }` keys to its side. Pass `platform` to pin it.
|
|
204
255
|
- Nothing listens for keys until `mount()` (or `useMountCommands`).
|
|
205
256
|
|
|
206
257
|
## 7. Inputs and events
|
|
207
258
|
|
|
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.
|
|
259
|
+
`ignoreInputs` on a command decides whether it fires while focus is in a text input, textarea, select or contenteditable, or in a custom widget with a text-entry role (`textbox`, `searchbox`, `combobox`, `spinbutton`, `slider`). 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. Editing is not hijacked either. Select all, copy, cut, paste, undo and redo (`Mod+A`, `C`, `X`, `V`, `Z`, `Shift+Z`, `Shift+V`, and `Mod+Y` off the Mac) are ignored in a field too, and so is `Mod` with `Backspace`, `Delete`, an arrow, `Home` or `End`, which move and delete by word or line. An app's own undo, select-all or delete-item leaves the field's alone. Set `ignoreInputs: false` on a command that should take one of them anyway. Keydowns that are part of an IME composition never trigger commands.
|
|
209
260
|
|
|
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.
|
|
261
|
+
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, which suits stepping and undo; give a toggle, or anything that should happen once per press, `repeat: false`. It still claims its key for as long as the key is held.
|
|
211
262
|
|
|
212
263
|
`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.
|
|
213
264
|
|
|
@@ -261,20 +312,84 @@ const stop = observe(commands); // or observe(commands, { root: element })
|
|
|
261
312
|
|
|
262
313
|
Clicking an element with `data-command` runs that command (`source: "dom"`), unless the element or an ancestor has `aria-disabled="true"` or another handler already called `preventDefault()`. Elements with `data-command-keys` get the shortcut as text, and `data-command` elements get `aria-keyshortcuts`; both stay current across rebinds and DOM changes.
|
|
263
314
|
|
|
315
|
+
## 11. Recipes
|
|
316
|
+
|
|
317
|
+
Patterns the pieces above already cover, so none of them needs an API of its own.
|
|
318
|
+
|
|
319
|
+
### Act on the selection, else on the highlighted item
|
|
320
|
+
|
|
321
|
+
Which item a shortcut applies to is your state. Compute the target once, and let it switch the handler on and off:
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
const targets = selection.size > 0 ? [...selection] : highlighted ? [highlighted] : [];
|
|
325
|
+
|
|
326
|
+
useCommandHandler(commands, "issue.setStatus", () => openStatusPicker(targets), targets.length > 0);
|
|
327
|
+
useCommandHandler(commands, "issue.selectAll", () => setSelection(new Set(visibleIds)));
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
With nothing targeted the command has no handler, so its key falls through, `reachable` is false for its button, and a palette that lists what `canRun` leaves it out: the palette follows the selection without being told. `issue.selectAll` can be `Mod+A`: inside a text field that stays the field's own select-all.
|
|
331
|
+
|
|
332
|
+
### Hold a key to peek
|
|
333
|
+
|
|
334
|
+
`repeat: false` runs a command once per press. Letting go is a `keyup` away:
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
// "issue.peek": { title: "Peek", keys: "Space", repeat: false }
|
|
338
|
+
const [peeking, setPeeking] = useState(false);
|
|
339
|
+
useCommandHandler(commands, "issue.peek", () => setPeeking(true), highlighted !== null);
|
|
340
|
+
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
if (!peeking) return;
|
|
343
|
+
const stop = () => setPeeking(false);
|
|
344
|
+
window.addEventListener("keyup", stop);
|
|
345
|
+
window.addEventListener("blur", stop);
|
|
346
|
+
return () => {
|
|
347
|
+
window.removeEventListener("keyup", stop);
|
|
348
|
+
window.removeEventListener("blur", stop);
|
|
349
|
+
};
|
|
350
|
+
}, [peeking]);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Bind a hold to a bare key. While Command is down, macOS sends no `keyup` for the other keys, so a held `Mod+…` chord cannot tell when it was let go.
|
|
354
|
+
|
|
355
|
+
### A shortcut sheet on `?` and `Mod+/`
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
"help.shortcuts": { title: "Keyboard shortcuts", keys: ["?", "Mod+/"], scope: "*" },
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Build the sheet from `useCommands(commands)`, grouped by `group`, and search it with `matchCommands`, which also matches the displayed shortcut. When a user turns single-key shortcuts off, `?` goes and `Mod+/` stays, so the sheet that explains the setting is still reachable.
|
|
362
|
+
|
|
363
|
+
### A way out of a text field
|
|
364
|
+
|
|
365
|
+
Bare keys are ignored while the user types, so give them a key that leaves the field. `Escape` is the one bare key that fires inside an input:
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
// "field.leave": { title: "Leave field", keys: "Escape", hidden: true }
|
|
369
|
+
const [editing, setEditing] = useState(false);
|
|
370
|
+
useCommandHandler(commands, "field.leave", () => rowRef.current?.focus(), editing);
|
|
371
|
+
|
|
372
|
+
<input onFocus={() => setEditing(true)} onBlur={() => setEditing(false)} />;
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Move focus to the item the field belongs to, not to nothing, so the next key has a target.
|
|
376
|
+
|
|
264
377
|
## API Summary
|
|
265
378
|
|
|
266
379
|
- `createCommands(definitions, options)` - creates the registry
|
|
267
|
-
- `options`: `{ bindings, platform, target, persist, display, singleKeyShortcuts, onRun, onError }`
|
|
380
|
+
- `options`: `{ bindings, platform, target, persist, display, singleKeyShortcuts, onRun, onError }`; `platform` is `"mac"` or `"other"`
|
|
268
381
|
- `commands.run(id)`, `canRun(id)`, `whyNotRun(id)`, `handle(id, run)`
|
|
269
382
|
- `commands.get(id)`, `list()`, `subscribe(listener)`: snapshots with `display`, `tokens`, `aria`, `spoken`, `reachable`, `overridden`
|
|
270
383
|
- `commands.pushScope(name, { exclusive })`, `capture(onKey)`, `conflicts(binding, options)`, `mount()`, `destroy()`
|
|
271
384
|
- `commands.serverList()`: the list as the server rendered it, for a framework's hydration snapshot
|
|
272
|
-
- `commands.keymap`: `get`, `set`, `add`, `reset
|
|
385
|
+
- `commands.keymap`: `get`, `set`, `add`, `reset`
|
|
273
386
|
- `commands.setSingleKeyShortcuts(enabled)`, `commands.singleKeyShortcuts`
|
|
387
|
+
- `webStorage(key, storage?)` - the `persist` option: Web Storage, followed across tabs
|
|
388
|
+
- `exportKeymap(commands)`, `importKeymap(commands, data)` - user overrides as JSON-safe data
|
|
274
389
|
- `createRebinder(commands, id, options)` - framework-agnostic shortcut recording
|
|
275
390
|
- `observe(commands, { root })` from `@inklu/keys/attributes` - declarative DOM wiring
|
|
276
391
|
- `matchCommands(list, query)` - palette filtering over `title`, `description`, `group`, `keywords`, id and displayed shortcut
|
|
277
|
-
- `formatBinding`, `bindingTokens`, `ariaKeyShortcuts`, `spokenBinding`, `
|
|
392
|
+
- `formatBinding`, `bindingTokens`, `ariaKeyShortcuts`, `spokenBinding`, `isSingleKeyBinding`, `seq`
|
|
278
393
|
- React (`@inklu/keys/react`):
|
|
279
394
|
- `useMountCommands(commands)` - starts listening
|
|
280
395
|
- `useCommands(commands)`, `useCommand(commands, id)` - live command snapshots
|
|
@@ -283,13 +398,15 @@ Clicking an element with `data-command` runs that command (`source: "dom"`), unl
|
|
|
283
398
|
- `useCommandHandler(commands, id, run, enabled)` - what a command does while a component is mounted
|
|
284
399
|
- `useRebind(commands, id, options)` - records user shortcuts
|
|
285
400
|
- `usePendingSequences(commands)` - half-typed sequences
|
|
286
|
-
- `useHeldKeys()
|
|
401
|
+
- `useHeldKeys()` - the keys held down right now, by name
|
|
287
402
|
|
|
288
403
|
Inside a handler, the context's `commands` is typed with the registry's own ids, so `commands.run("typo")` is a compile error.
|
|
289
404
|
|
|
290
405
|
## Accessibility
|
|
291
406
|
|
|
292
|
-
|
|
407
|
+
Shortcuts are written the way each platform writes them: `⇧⌘S` on a Mac, symbols run together in Apple's order, and `Ctrl+Shift+S` elsewhere. `tokens` holds the same keys one at a time, for a `<kbd>` each.
|
|
408
|
+
|
|
409
|
+
Every command carries an `aria` string for the `aria-keyshortcuts` attribute. It lists chords only: the attribute separates alternatives with spaces, so it cannot express a sequence. Every command also carries `spoken` — `"Command K"` on a Mac and `"Control K"` elsewhere, or `"G then I"` for a sequence — for visually hidden text or `aria-description`, which is how a sequence gets announced:
|
|
293
410
|
|
|
294
411
|
```tsx
|
|
295
412
|
const { aria, spoken } = useShortcut(commands, "nav.inbox");
|
package/dist/attributes.d.mts
CHANGED
package/dist/attributes.mjs
CHANGED
|
@@ -52,28 +52,25 @@ function observe(registry, options = {}) {
|
|
|
52
52
|
lastPainted = next;
|
|
53
53
|
paintWithin(root);
|
|
54
54
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
paintElement(mutation.target);
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
for (const node of mutation.addedNodes) if (node instanceof Element) paintWithin(node);
|
|
55
|
+
const observer = new MutationObserver((mutations) => {
|
|
56
|
+
for (const mutation of mutations) {
|
|
57
|
+
if (mutation.type === "attributes" && mutation.target instanceof Element) {
|
|
58
|
+
paintElement(mutation.target);
|
|
59
|
+
continue;
|
|
64
60
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
for (const node of mutation.addedNodes) if (node instanceof Element) paintWithin(node);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
observer.observe(root instanceof Document ? root.documentElement : root, {
|
|
65
|
+
childList: true,
|
|
66
|
+
subtree: true,
|
|
67
|
+
attributes: true,
|
|
68
|
+
attributeFilter: [COMMAND_ATTRIBUTE, KEYS_ATTRIBUTE]
|
|
69
|
+
});
|
|
73
70
|
return () => {
|
|
74
71
|
root.removeEventListener("click", onClick);
|
|
75
72
|
unsubscribe();
|
|
76
|
-
observer
|
|
73
|
+
observer.disconnect();
|
|
77
74
|
};
|
|
78
75
|
}
|
|
79
76
|
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
1
|
+
import { C as SequenceBinding, D as Chord, T as SerializedKeymap, a as CommandDefinition, d as Conflict, g as KeymapStore, k as Platform, l as Commands, m as Json, p as DisplayOptions, r as Command, t as Binding, u as CommandsOptions } from "./types-D-hUKGoN.mjs";
|
|
2
2
|
//#region src/bindings.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Creates a sequence binding from ordered chords.
|
|
@@ -10,35 +10,54 @@ import { C as SequenceBinding, D as Chord, a as CommandDefinition, d as Conflict
|
|
|
10
10
|
*/
|
|
11
11
|
declare function seq(...steps: Array<Chord>): SequenceBinding;
|
|
12
12
|
/**
|
|
13
|
-
* Whether every step
|
|
14
|
-
*
|
|
15
|
-
* turn off, since speech input and stray keypresses trigger them by accident.
|
|
13
|
+
* Whether every step is a printable character with no Control, Alt or Meta:
|
|
14
|
+
* the "character key shortcuts" WCAG 2.1.4 lets users turn off.
|
|
16
15
|
*/
|
|
17
16
|
declare function isSingleKeyBinding(binding: Binding): boolean;
|
|
18
|
-
/** Validation result for a hotkey binding. */
|
|
19
|
-
interface BindingValidation {
|
|
20
|
-
readonly valid: boolean;
|
|
21
|
-
readonly errors: ReadonlyArray<string>;
|
|
22
|
-
readonly warnings: ReadonlyArray<string>;
|
|
23
|
-
}
|
|
24
|
-
/** Validates a binding's syntax without logging warnings or errors. */
|
|
25
|
-
declare function validateBinding(binding: Binding): BindingValidation;
|
|
26
17
|
//#endregion
|
|
27
18
|
//#region src/display.d.ts
|
|
28
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Formats a binding the way its platform writes shortcuts: `⇧⌘S` on a Mac,
|
|
21
|
+
* `Ctrl+Shift+S` elsewhere. The steps of a sequence are set apart by a space.
|
|
22
|
+
*/
|
|
29
23
|
declare function formatBinding(binding: Binding, options?: DisplayOptions): string;
|
|
30
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Splits a binding into display tokens for rendering `<kbd>` elements: one
|
|
26
|
+
* per key of a chord, one per step of a sequence.
|
|
27
|
+
*/
|
|
31
28
|
declare function bindingTokens(binding: Binding, options?: DisplayOptions): Array<string>;
|
|
32
29
|
/**
|
|
33
|
-
* Phrases a binding for a screen reader: `"
|
|
34
|
-
* `"G then I"` for 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`.
|
|
30
|
+
* Phrases a binding for a screen reader: `"Command K"` for a chord,
|
|
31
|
+
* `"G then I"` for a sequence, which `aria-keyshortcuts` cannot express.
|
|
37
32
|
*/
|
|
38
33
|
declare function spokenBinding(binding: Binding, platform?: Platform): string;
|
|
39
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The `aria-keyshortcuts` value for a chord, as in `"Meta+K"`. Empty for a
|
|
36
|
+
* sequence and for a chord that cannot be read.
|
|
37
|
+
*/
|
|
40
38
|
declare function ariaKeyShortcuts(binding: Binding, platform?: Platform): string;
|
|
41
39
|
//#endregion
|
|
40
|
+
//#region src/persist.d.ts
|
|
41
|
+
type WebStorage = Pick<Storage, "getItem" | "setItem" | "removeItem">;
|
|
42
|
+
/**
|
|
43
|
+
* Keeps user overrides in Web Storage under `key`, and follows edits made in
|
|
44
|
+
* other tabs. `storage` is only called in the browser; return
|
|
45
|
+
* `sessionStorage` or an object of your own to keep them somewhere else.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* createCommands(definitions, { persist: webStorage("my-app.keys") });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function webStorage(key?: string, storage?: () => WebStorage | null): KeymapStore;
|
|
53
|
+
/** A registry's user overrides as JSON-safe data, for your own storage or another device. */
|
|
54
|
+
declare function exportKeymap<Id extends string>(commands: Pick<Commands<Id>, "keymap">): SerializedKeymap;
|
|
55
|
+
/**
|
|
56
|
+
* Replaces a registry's user overrides with exported data, and saves them.
|
|
57
|
+
* Data in another format changes nothing.
|
|
58
|
+
*/
|
|
59
|
+
declare function importKeymap<Id extends string>(commands: Pick<Commands<Id>, "keymap">, data: Json): void;
|
|
60
|
+
//#endregion
|
|
42
61
|
//#region src/rebind.d.ts
|
|
43
62
|
/** Hotkey capture mode: single `"chord"` or multi-step `"sequence"`. */
|
|
44
63
|
type RebindMode = "chord" | "sequence";
|
|
@@ -50,7 +69,7 @@ interface RebindState<Id extends string = string> {
|
|
|
50
69
|
readonly binding: Binding | null;
|
|
51
70
|
/** Captured steps in the current sequence session. */
|
|
52
71
|
readonly steps: ReadonlyArray<Chord>;
|
|
53
|
-
/**
|
|
72
|
+
/** What stands in the captured binding's way: other commands using it, or the platform reserving it. */
|
|
54
73
|
readonly conflicts: ReadonlyArray<Conflict<Id>>;
|
|
55
74
|
}
|
|
56
75
|
/** Options for creating a rebinder. */
|
|
@@ -61,7 +80,10 @@ interface RebindOptions<Id extends string = string> {
|
|
|
61
80
|
apply?: boolean;
|
|
62
81
|
/** Whether to append rather than replace existing bindings. Defaults to `false`. */
|
|
63
82
|
add?: boolean;
|
|
64
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Prevent applying bindings that conflict with other commands. Defaults to
|
|
85
|
+
* `false`. A binding the browser or operating system reserves is never applied.
|
|
86
|
+
*/
|
|
65
87
|
rejectConflicts?: boolean;
|
|
66
88
|
/** Ignore key events from inputs while recording. Defaults to `true`. */
|
|
67
89
|
ignoreInputs?: boolean;
|
|
@@ -92,17 +114,14 @@ interface Rebinder<Id extends string = string> {
|
|
|
92
114
|
destroy: () => void;
|
|
93
115
|
}
|
|
94
116
|
/**
|
|
95
|
-
* Creates a rebinder
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* last step of a sequence), and Enter finishes a sequence.
|
|
117
|
+
* Creates a rebinder for recording a command's keys. While it records, no
|
|
118
|
+
* command runs. Escape cancels, Backspace removes the shortcut (or the last
|
|
119
|
+
* step of a sequence), and Enter finishes a sequence.
|
|
99
120
|
*
|
|
100
121
|
* @example
|
|
101
122
|
* ```ts
|
|
102
123
|
* const rebinder = createRebinder(commands, "palette.open", {
|
|
103
|
-
* onRecord: (binding, conflicts) =>
|
|
104
|
-
* if (conflicts.length) console.warn("Conflict with", conflicts[0].id);
|
|
105
|
-
* },
|
|
124
|
+
* onRecord: (binding, conflicts) => showConflicts(conflicts),
|
|
106
125
|
* });
|
|
107
126
|
* rebinder.start();
|
|
108
127
|
* ```
|
|
@@ -125,7 +144,7 @@ declare function createRebinder<Id extends string>(registry: Commands<Id>, id: I
|
|
|
125
144
|
* });
|
|
126
145
|
* ```
|
|
127
146
|
*/
|
|
128
|
-
declare function createCommands<Id extends string>(definitions: { [K in Id]: CommandDefinition<Id
|
|
147
|
+
declare function createCommands<Id extends string>(definitions: { [K in Id]: CommandDefinition<NoInfer<Id>>; }, options?: CommandsOptions<NoInfer<Id>>): Commands<Id>;
|
|
129
148
|
/**
|
|
130
149
|
* Extracts command ID union from a Commands registry type.
|
|
131
150
|
*
|
|
@@ -139,12 +158,10 @@ type CommandId<C> = C extends Commands<infer Id> ? Id : never;
|
|
|
139
158
|
//#endregion
|
|
140
159
|
//#region src/search.d.ts
|
|
141
160
|
/**
|
|
142
|
-
* Filters commands for a search query,
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* then titles containing every word, then the rest; each tier keeps the
|
|
147
|
-
* list's own order. An empty query returns every visible command.
|
|
161
|
+
* Filters commands for a search query, leaving out `hidden` ones. Every word
|
|
162
|
+
* must appear in the title, description, group, keywords, id or displayed
|
|
163
|
+
* shortcut. Titles starting with the query come first, then titles containing
|
|
164
|
+
* every word, then the rest. An empty query returns every visible command.
|
|
148
165
|
*
|
|
149
166
|
* @example
|
|
150
167
|
* ```ts
|
|
@@ -153,4 +170,4 @@ type CommandId<C> = C extends Commands<infer Id> ? Id : never;
|
|
|
153
170
|
*/
|
|
154
171
|
declare function matchCommands<Id extends string>(commands: ReadonlyArray<Command<Id>>, query: string): Array<Command<Id>>;
|
|
155
172
|
//#endregion
|
|
156
|
-
export { RebindOptions as a, createRebinder as c,
|
|
173
|
+
export { seq as _, RebindOptions as a, createRebinder as c, webStorage as d, ariaKeyShortcuts as f, isSingleKeyBinding as g, spokenBinding as h, RebindMode as i, exportKeymap as l, formatBinding as m, CommandId as n, RebindState as o, bindingTokens as p, createCommands as r, Rebinder as s, matchCommands as t, importKeymap as u };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { C as SequenceBinding, D as Chord, E as SerializedOverride, O as Key, S as ScopeOptions, T as SerializedKeymap, _ as
|
|
2
|
-
import { a as RebindOptions, c as createRebinder, d as
|
|
3
|
-
export { type Binding, type BindingInput, type
|
|
1
|
+
import { C as SequenceBinding, D as Chord, E as SerializedOverride, O as Key, S as ScopeOptions, T as SerializedKeymap, _ as Override, a as CommandDefinition, b as RunBlockReason, c as CommandSource, d as Conflict, f as ConflictOptions, g as KeymapStore, h as KeyCapture, i as CommandContext, k as Platform, l as Commands, m as Json, n as BindingInput, o as CommandHandler, p as DisplayOptions, r as Command, s as CommandKeymap, t as Binding, u as CommandsOptions, v as PendingSequence, w as SerializedBinding, x as RunOptions, y as PlatformBindings } from "./types-D-hUKGoN.mjs";
|
|
2
|
+
import { _ as seq, a as RebindOptions, c as createRebinder, d as webStorage, f as ariaKeyShortcuts, g as isSingleKeyBinding, h as spokenBinding, i as RebindMode, l as exportKeymap, m as formatBinding, n as CommandId, o as RebindState, p as bindingTokens, r as createCommands, s as Rebinder, t as matchCommands, u as importKeymap } from "./index-DRXMJmbc.mjs";
|
|
3
|
+
export { type Binding, type BindingInput, type Chord, type Command, type CommandContext, type CommandDefinition, type CommandHandler, type CommandId, type CommandKeymap, type CommandSource, type Commands, type CommandsOptions, type Conflict, type ConflictOptions, type DisplayOptions, type Json, type Key, type KeyCapture, type KeymapStore, type Override, type PendingSequence, type Platform, type PlatformBindings, 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, exportKeymap, formatBinding, importKeymap, isSingleKeyBinding, matchCommands, seq, spokenBinding, webStorage };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, isSingleKeyBinding, matchCommands, seq, spokenBinding,
|
|
1
|
+
import { a as importKeymap, c as bindingTokens, d as isSingleKeyBinding, f as seq, i as exportKeymap, l as formatBinding, n as createCommands, o as webStorage, r as createRebinder, s as ariaKeyShortcuts, t as matchCommands, u as spokenBinding } from "./src-TaZw4V61.mjs";
|
|
2
|
+
export { ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, exportKeymap, formatBinding, importKeymap, isSingleKeyBinding, matchCommands, seq, spokenBinding, webStorage };
|
package/dist/react.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as SequenceBinding, D as Chord, E as SerializedOverride, O as Key, S as ScopeOptions, T as SerializedKeymap, _ as
|
|
2
|
-
import { a as RebindOptions, c as createRebinder, d as
|
|
1
|
+
import { C as SequenceBinding, D as Chord, E as SerializedOverride, O as Key, S as ScopeOptions, T as SerializedKeymap, _ as Override, a as CommandDefinition, b as RunBlockReason, c as CommandSource, d as Conflict, f as ConflictOptions, g as KeymapStore, h as KeyCapture, i as CommandContext, k as Platform, l as Commands, m as Json, n as BindingInput, o as CommandHandler, p as DisplayOptions, r as Command, s as CommandKeymap, t as Binding, u as CommandsOptions, v as PendingSequence, w as SerializedBinding, x as RunOptions, y as PlatformBindings } from "./types-D-hUKGoN.mjs";
|
|
2
|
+
import { _ as seq, a as RebindOptions, c as createRebinder, d as webStorage, f as ariaKeyShortcuts, g as isSingleKeyBinding, h as spokenBinding, i as RebindMode, l as exportKeymap, m as formatBinding, n as CommandId, o as RebindState, p as bindingTokens, r as createCommands, s as Rebinder, t as matchCommands, u as importKeymap } from "./index-DRXMJmbc.mjs";
|
|
3
3
|
//#region src/react/hooks.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Mounts a commands registry for the lifecycle of the component.
|
|
@@ -13,9 +13,13 @@ import { a as RebindOptions, c as createRebinder, d as formatBinding, f as spoke
|
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
15
|
export declare function useMountCommands<Id extends string>(registry: Commands<Id>): void;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Returns all registered commands, re-rendering when the registry changes.
|
|
18
|
+
* Hydrates against `serverList()`, so a subtree that hydrates late still
|
|
19
|
+
* matches the server's HTML.
|
|
20
|
+
*/
|
|
17
21
|
export declare function useCommands<Id extends string>(registry: Commands<Id>): ReadonlyArray<Command<Id>>;
|
|
18
|
-
/** Returns
|
|
22
|
+
/** Returns one command's snapshot, re-rendering only when that command changes. */
|
|
19
23
|
export declare function useCommand<Id extends string>(registry: Commands<Id>, id: NoInfer<Id>): Command<Id> | undefined;
|
|
20
24
|
/**
|
|
21
25
|
* Returns display and accessibility shortcuts for a command's primary
|
|
@@ -30,9 +34,8 @@ export declare function useShortcut<Id extends string>(registry: Commands<Id>, i
|
|
|
30
34
|
};
|
|
31
35
|
/**
|
|
32
36
|
* Activates a scope for the lifecycle of the component. Scopes rank in the
|
|
33
|
-
* order
|
|
34
|
-
*
|
|
35
|
-
* the same render the child is pushed first.
|
|
37
|
+
* order React runs effects in: of a parent and a child opening in the same
|
|
38
|
+
* render, the child is pushed first.
|
|
36
39
|
*
|
|
37
40
|
* @example
|
|
38
41
|
* ```tsx
|
|
@@ -41,12 +44,10 @@ export declare function useShortcut<Id extends string>(registry: Commands<Id>, i
|
|
|
41
44
|
*/
|
|
42
45
|
export declare function useScope<Id extends string>(registry: Commands<Id>, name: string, active?: boolean, options?: ScopeOptions): void;
|
|
43
46
|
/**
|
|
44
|
-
* Attaches what a command does for the lifecycle of the component
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* The handler attached last wins, and React attaches a parent's after its
|
|
49
|
-
* children's when they mount together.
|
|
47
|
+
* Attaches what a command does for the lifecycle of the component. The latest
|
|
48
|
+
* `run` is always the one called, and the handler attached last wins. While
|
|
49
|
+
* `enabled` is `false` the command falls to the handler attached before it,
|
|
50
|
+
* or cannot run if there is none.
|
|
50
51
|
*
|
|
51
52
|
* @example
|
|
52
53
|
* ```tsx
|
|
@@ -72,27 +73,9 @@ interface UseRebindResult<Id extends string = string> extends RebindState<Id> {
|
|
|
72
73
|
}
|
|
73
74
|
/** Hook for recording and configuring a command's keybindings. */
|
|
74
75
|
export declare function useRebind<Id extends string>(registry: Commands<Id>, id: NoInfer<Id>, options?: RebindOptions<Id>): UseRebindResult<Id>;
|
|
75
|
-
/** Returns
|
|
76
|
+
/** Returns the keys held down right now, by the name `KeyboardEvent.key` gives each. */
|
|
76
77
|
export declare function useHeldKeys(): ReadonlyArray<string>;
|
|
77
|
-
/**
|
|
78
|
-
* Checks whether a specific key is currently held down.
|
|
79
|
-
*
|
|
80
|
-
* @example
|
|
81
|
-
* ```tsx
|
|
82
|
-
* const shift = useKeyHold("Shift");
|
|
83
|
-
* ```
|
|
84
|
-
*/
|
|
85
|
-
export declare function useKeyHold(key: string): boolean;
|
|
86
|
-
/** Returns a map of held key identifiers to physical event codes. */
|
|
87
|
-
export declare function useHeldKeyCodes(): Readonly<Record<string, string>>;
|
|
88
|
-
/**
|
|
89
|
-
* Returns currently active partially-typed sequences.
|
|
90
|
-
*
|
|
91
|
-
* @example
|
|
92
|
-
* ```tsx
|
|
93
|
-
* const pending = usePendingSequences(commands);
|
|
94
|
-
* ```
|
|
95
|
-
*/
|
|
78
|
+
/** Returns the half-typed sequences, for a hint such as "G …". */
|
|
96
79
|
export declare function usePendingSequences<Id extends string>(registry: Commands<Id>): ReadonlyArray<PendingSequence<Id>>;
|
|
97
80
|
//#endregion
|
|
98
|
-
export { type Binding, type BindingInput, type
|
|
81
|
+
export { type Binding, type BindingInput, type Chord, type Command, type CommandContext, type CommandDefinition, type CommandHandler, type CommandId, type CommandKeymap, type CommandSource, type Commands, type CommandsOptions, type Conflict, type ConflictOptions, type DisplayOptions, type Json, type Key, type KeyCapture, type KeymapStore, type Override, type PendingSequence, type Platform, type PlatformBindings, 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, exportKeymap, formatBinding, importKeymap, isSingleKeyBinding, matchCommands, seq, spokenBinding, webStorage };
|