@inklu/keys 1.4.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -8
- package/dist/{attributes-O6bXNKT9.d.ts → attributes-DotLr2MW.d.mts} +142 -23
- package/dist/{attributes.d.ts → attributes.d.mts} +1 -1
- package/dist/{attributes.js → attributes.mjs} +12 -5
- package/dist/{index-B5iiRwCy.d.ts → index-BTgscYdI.d.mts} +25 -2
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/react.d.mts +82 -0
- package/dist/{react.js → react.mjs} +24 -13
- package/dist/src-DgYjA1oS.mjs +1697 -0
- package/package.json +15 -42
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -3
- package/dist/react.d.ts +0 -77
- package/dist/src-DWmUPZSL.js +0 -1078
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Keyboard shortcut management for the web. Built on TanStack Hotkeys, `@inklu/keys` provides a registry, scopes, conflict detection, and screen-reader-ready shortcuts.
|
|
4
4
|
|
|
5
|
+
Docs: [inklu-keys.vercel.app](https://inklu-keys.vercel.app) · For agents: [llms.txt](https://inklu-keys.vercel.app/llms.txt) · Agent skill: `npx skills add nkurunziza-saddy/inklu --skill inklu-keys`
|
|
6
|
+
|
|
5
7
|
```bash
|
|
6
8
|
pnpm add @inklu/keys
|
|
7
9
|
```
|
|
@@ -43,7 +45,7 @@ function App() {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
function ShortcutButton() {
|
|
46
|
-
//
|
|
48
|
+
// Live state for the primary binding: `tokens` is one string per key
|
|
47
49
|
const shortcut = useShortcut(commands, "palette.open");
|
|
48
50
|
|
|
49
51
|
return (
|
|
@@ -52,7 +54,7 @@ function ShortcutButton() {
|
|
|
52
54
|
aria-keyshortcuts={shortcut.aria || undefined}
|
|
53
55
|
>
|
|
54
56
|
Open Palette
|
|
55
|
-
{shortcut.tokens
|
|
57
|
+
{shortcut.tokens.map((token, i) => (
|
|
56
58
|
<kbd key={i}>{token}</kbd>
|
|
57
59
|
))}
|
|
58
60
|
</button>
|
|
@@ -97,6 +99,8 @@ commands.keymap.set("palette.open", null); // unbind
|
|
|
97
99
|
commands.keymap.reset("palette.open"); // reset to default
|
|
98
100
|
```
|
|
99
101
|
|
|
102
|
+
`set` replaces the command's keys outright; setting exactly the default forgets the override instead. `add` layers on top of the default rather than copying it, so if a later release of your app changes the default, the user gets the new one and keeps the keys they added.
|
|
103
|
+
|
|
100
104
|
To rebind a command you did not define — one a package shipped — pass `bindings` when creating the registry. An unknown id is a compile error, and `reset` returns here rather than to the definition's `keys`:
|
|
101
105
|
|
|
102
106
|
```ts
|
|
@@ -118,18 +122,167 @@ const rebind = useRebind(commands, "palette.open");
|
|
|
118
122
|
<button onClick={rebind.start}>{rebind.recording ? "Press keys…" : rebind.display}</button>;
|
|
119
123
|
```
|
|
120
124
|
|
|
125
|
+
To build your own capture UI without React, use `createRebinder`:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { createRebinder } from "@inklu/keys";
|
|
129
|
+
|
|
130
|
+
const rebinder = createRebinder(commands, "palette.open", {
|
|
131
|
+
mode: "chord", // or "sequence": Enter or `commit()` finishes it
|
|
132
|
+
rejectConflicts: true, // don't apply a binding another command uses
|
|
133
|
+
onRecord: (binding, conflicts) => {},
|
|
134
|
+
});
|
|
135
|
+
rebinder.subscribe(() => render(rebinder.state)); // { recording, binding, steps, conflicts }
|
|
136
|
+
rebinder.start(); // Escape cancels, Backspace unbinds
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
While recording, every other command is silenced — including `"*"` ones — so the keys being recorded never run anything.
|
|
140
|
+
|
|
141
|
+
## 5. Conflicts
|
|
142
|
+
|
|
143
|
+
`commands.conflicts(binding, { scope, ignore })` lists commands in overlapping scopes whose bindings collide with `binding`. Each result has a `kind`:
|
|
144
|
+
|
|
145
|
+
- `"exact"` — the same keys. Only one command runs (see above).
|
|
146
|
+
- `"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
|
+
|
|
148
|
+
In development, both kinds are also reported with `console.warn` when commands are defined or added.
|
|
149
|
+
|
|
150
|
+
## 6. Persistence and SSR
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
createCommands(definitions, {
|
|
154
|
+
persist: {
|
|
155
|
+
storage: () => (typeof window === "undefined" ? null : localStorage),
|
|
156
|
+
key: "my-app.keys",
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
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 leaves it alone instead of misreading it. Data from releases before the version field still loads.
|
|
162
|
+
|
|
163
|
+
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
|
+
|
|
165
|
+
The registry is safe to create at module scope and render on the server:
|
|
166
|
+
|
|
167
|
+
- 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.
|
|
169
|
+
- Nothing listens for keys until `mount()` (or `useMountCommands`).
|
|
170
|
+
|
|
171
|
+
## 7. Inputs and events
|
|
172
|
+
|
|
173
|
+
`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
|
+
|
|
175
|
+
A command calls `preventDefault()` only when it actually runs; one that is disabled, out of scope, guarded or outranked leaves the event alone. Opt out per command with `preventDefault: false`. Propagation is never stopped. `run` may be async: nothing awaits it, but a rejection reaches `onError`.
|
|
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
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
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
|
+
```
|
|
194
|
+
|
|
195
|
+
## 9. Sequence progress
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
const pending = usePendingSequences(commands); // [{ id, matched, total, remaining }]
|
|
199
|
+
// e.g. show "G → …" while the user is halfway through `G then I`
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`commands.pending()` / `commands.subscribePending()` are the non-React equivalents.
|
|
203
|
+
|
|
204
|
+
## 10. Command palette
|
|
205
|
+
|
|
206
|
+
Definitions can carry what a palette needs to list and find them, and `matchCommands` does the filtering:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
createCommands({
|
|
210
|
+
"doc.find": {
|
|
211
|
+
title: "Find in document",
|
|
212
|
+
description: "Search the open document",
|
|
213
|
+
group: "Document", // heading to list it under
|
|
214
|
+
keywords: ["search", "lookup"], // extra search terms
|
|
215
|
+
keys: "Mod+F",
|
|
216
|
+
run: openFind,
|
|
217
|
+
},
|
|
218
|
+
"palette.close": { title: "Close palette", hidden: true, keys: "Escape", run: close },
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
import { matchCommands } from "@inklu/keys";
|
|
224
|
+
|
|
225
|
+
const results = matchCommands(useCommands(commands), query);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
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
|
+
|
|
230
|
+
## 11. Without React: data attributes
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { observe } from "@inklu/keys/attributes";
|
|
234
|
+
|
|
235
|
+
const stop = observe(commands); // or observe(commands, { root: element })
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```html
|
|
239
|
+
<button data-command="palette.open">Search <kbd data-command-keys="palette.open"></kbd></button>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
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.
|
|
243
|
+
|
|
121
244
|
## API Summary
|
|
122
245
|
|
|
123
246
|
- `createCommands(definitions, options)` - creates the registry
|
|
124
|
-
- `options`: `{ bindings, platform, target, persist, display, onRun, onError }`
|
|
125
|
-
- `
|
|
126
|
-
- `
|
|
127
|
-
- `
|
|
128
|
-
- `
|
|
247
|
+
- `options`: `{ bindings, platform, target, persist, display, singleKeyShortcuts, onRun, onError }`
|
|
248
|
+
- `commands.run(id)`, `canRun(id)`, `whyNotRun(id)`, `setEnabled(id, enabled)`, `add(id, definition)`, `remove(id)`
|
|
249
|
+
- `commands.pushScope(name, { exclusive, strict })`, `conflicts(binding, options)`, `display(id)`, `tokens(id)`
|
|
250
|
+
- `commands.keymap`: `get`, `set`, `add`, `reset`, `export`, `import`
|
|
251
|
+
- `commands.setSingleKeyShortcuts(enabled)`, `commands.singleKeyShortcuts`
|
|
252
|
+
- `createRebinder(commands, id, options)` - framework-agnostic shortcut recording
|
|
253
|
+
- `observe(commands, { root })` from `@inklu/keys/attributes` - declarative DOM wiring
|
|
254
|
+
- `matchCommands(list, query)` - palette filtering over `title`, `description`, `group`, `keywords`
|
|
255
|
+
- `formatBinding`, `bindingTokens`, `ariaKeyShortcuts`, `spokenBinding`, `validateBinding`, `isSingleKeyBinding`, `seq`
|
|
256
|
+
- React (`@inklu/keys/react`):
|
|
257
|
+
- `useMountCommands(commands)` - starts listening
|
|
258
|
+
- `useCommands(commands)`, `useCommand(commands, id)` - live command snapshots
|
|
259
|
+
- `useShortcut(commands, id)` - `{ display, tokens, aria, spoken }` for the primary binding
|
|
260
|
+
- `useScope(commands, name, active, { exclusive, strict })` - isolates command execution
|
|
261
|
+
- `useRegisterCommand(commands, id, definition)` - component-scoped commands
|
|
262
|
+
- `useRebind(commands, id, options)` - records user shortcuts
|
|
263
|
+
- `usePendingSequences(commands)` - half-typed sequences
|
|
264
|
+
- `useHeldKeys()`, `useHeldKeyCodes()`, `useKeyHold(key)` - live key state
|
|
265
|
+
|
|
266
|
+
Inside `run`, the context's `commands` is typed with the registry's own ids, so `commands.run("typo")` is a compile error.
|
|
129
267
|
|
|
130
268
|
## Accessibility
|
|
131
269
|
|
|
132
|
-
Every command carries an `aria` string
|
|
270
|
+
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` — `"Control+K"`, or `"G then I"` for a sequence — for visually hidden text or `aria-description`, which is how a sequence gets announced:
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
const { aria, spoken } = useShortcut(commands, "nav.inbox");
|
|
274
|
+
<button aria-keyshortcuts={aria || undefined} aria-description={spoken || undefined}>
|
|
275
|
+
Inbox
|
|
276
|
+
</button>;
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
WCAG 2.1.4 (Character Key Shortcuts) requires that users can turn off shortcuts made of printable characters without a modifier — `?`, `G then I` — because speech input and stray keypresses trigger them. Expose a setting wired to:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
commands.setSingleKeyShortcuts(false); // or createCommands(defs, { singleKeyShortcuts: false })
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
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`.
|
|
133
286
|
|
|
134
287
|
## License
|
|
135
288
|
|
|
@@ -41,10 +41,28 @@ interface CommandContext<Id extends string = string> {
|
|
|
41
41
|
*/
|
|
42
42
|
readonly commands: Commands<Id>;
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Definition of a command and its default configuration.
|
|
46
|
+
*
|
|
47
|
+
* `Id` is the registry's id union. `createCommands` fills it in, so
|
|
48
|
+
* `commands.run(...)` inside a handler is checked against the registry's own
|
|
49
|
+
* ids; a definition written on its own defaults to plain `string`.
|
|
50
|
+
*/
|
|
51
|
+
interface CommandDefinition<Id extends string = string> {
|
|
46
52
|
/** Human-readable title. */
|
|
47
53
|
title: string;
|
|
54
|
+
/** Longer explanation, such as a palette's secondary line or a tooltip. */
|
|
55
|
+
description?: string;
|
|
56
|
+
/** Heading to list the command under in a palette or shortcut sheet, e.g. `"Navigate"`. */
|
|
57
|
+
group?: string;
|
|
58
|
+
/** Extra search terms `matchCommands` checks besides the title, e.g. `["find", "search"]`. */
|
|
59
|
+
keywords?: ReadonlyArray<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Leave the command out of `matchCommands` — for commands that only make
|
|
62
|
+
* sense from their key, like a palette's own Close. It still runs and
|
|
63
|
+
* still appears in `list()`. Defaults to `false`.
|
|
64
|
+
*/
|
|
65
|
+
hidden?: boolean;
|
|
48
66
|
/** Default key binding(s). */
|
|
49
67
|
keys?: BindingInput;
|
|
50
68
|
/** Scope required for this command to fire. Omit for global, or `"*"` to bypass exclusive scopes. */
|
|
@@ -53,21 +71,38 @@ interface CommandDefinition {
|
|
|
53
71
|
when?: () => boolean;
|
|
54
72
|
/** Whether the command is enabled. Defaults to `true`. */
|
|
55
73
|
enabled?: boolean;
|
|
56
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Prevent default event behavior when run. Defaults to `true`. For a
|
|
76
|
+
* sequence this also covers each step as it matches, so a first step such
|
|
77
|
+
* as `Mod+K` does not trigger the browser's own shortcut on the way.
|
|
78
|
+
*/
|
|
57
79
|
preventDefault?: boolean;
|
|
58
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Whether to ignore key events from text inputs, textareas and
|
|
82
|
+
* contenteditable elements. When omitted, TanStack Hotkeys decides per
|
|
83
|
+
* binding: chords holding Control or Meta (so `Mod+…`) and bare `Escape`
|
|
84
|
+
* still fire inside inputs, while everything else — bare keys, Shift/Alt
|
|
85
|
+
* chords, and sequences starting with one — is ignored there, so typing is
|
|
86
|
+
* never hijacked.
|
|
87
|
+
*/
|
|
59
88
|
ignoreInputs?: boolean;
|
|
60
89
|
/** Require key release before firing again (prevents auto-repeat). Defaults to `false`. */
|
|
61
90
|
requireReset?: boolean;
|
|
62
91
|
/** Timeout in milliseconds between sequence steps. Defaults to `1000`. */
|
|
63
92
|
sequenceTimeout?: number;
|
|
64
93
|
/** Handler executed when the command runs. */
|
|
65
|
-
run: (context: CommandContext) => void | Promise<void>;
|
|
94
|
+
run: (context: CommandContext<Id>) => void | Promise<void>;
|
|
66
95
|
}
|
|
67
96
|
/** Resolved command snapshot including effective bindings, display tokens, and reachability. */
|
|
68
97
|
interface Command<Id extends string = string> {
|
|
69
98
|
readonly id: Id;
|
|
70
99
|
readonly title: string;
|
|
100
|
+
readonly description?: string;
|
|
101
|
+
readonly group?: string;
|
|
102
|
+
/** Extra search terms; empty when the definition gave none. */
|
|
103
|
+
readonly keywords: ReadonlyArray<string>;
|
|
104
|
+
/** Whether `matchCommands` leaves the command out. */
|
|
105
|
+
readonly hidden: boolean;
|
|
71
106
|
readonly scope?: string;
|
|
72
107
|
/** Active bindings after user overrides. */
|
|
73
108
|
readonly bindings: ReadonlyArray<Binding>;
|
|
@@ -75,8 +110,18 @@ interface Command<Id extends string = string> {
|
|
|
75
110
|
readonly display: ReadonlyArray<string>;
|
|
76
111
|
/** Formatted tokens per binding for rendering `<kbd>` elements. */
|
|
77
112
|
readonly tokens: ReadonlyArray<ReadonlyArray<string>>;
|
|
78
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Value for the `aria-keyshortcuts` attribute. Only chords appear here: the
|
|
115
|
+
* attribute separates alternatives with spaces, so it cannot express a
|
|
116
|
+
* sequence. Use `spoken` to announce those.
|
|
117
|
+
*/
|
|
79
118
|
readonly aria: string;
|
|
119
|
+
/**
|
|
120
|
+
* Every binding phrased for a screen reader, alternatives joined by
|
|
121
|
+
* `" or "` — e.g. `"Control+K"` or `"G then I"`. Suited to visually hidden
|
|
122
|
+
* text or `aria-description`, and the only accessible form of a sequence.
|
|
123
|
+
*/
|
|
124
|
+
readonly spoken: string;
|
|
80
125
|
/** Whether the command is enabled. */
|
|
81
126
|
readonly enabled: boolean;
|
|
82
127
|
/** Whether the command is enabled and in an active scope. */
|
|
@@ -99,12 +144,20 @@ interface KeymapPersistence {
|
|
|
99
144
|
/** Getter returning the storage implementation lazily. */
|
|
100
145
|
storage: () => KeymapStorage | null;
|
|
101
146
|
}
|
|
102
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* How two bindings collide: `"exact"` when they are the same keys, `"prefix"`
|
|
149
|
+
* when one is the start of the other — chord `G` against `G then I` — so the
|
|
150
|
+
* shorter one fires on the way to the longer one.
|
|
151
|
+
*/
|
|
152
|
+
type ConflictKind = "exact" | "prefix";
|
|
153
|
+
/** Details of a conflict between commands whose bindings collide in overlapping scopes. */
|
|
103
154
|
interface Conflict<Id extends string = string> {
|
|
104
155
|
/** ID of the conflicting command. */
|
|
105
156
|
readonly id: Id;
|
|
106
|
-
/**
|
|
157
|
+
/** The conflicting command's binding. */
|
|
107
158
|
readonly binding: Binding;
|
|
159
|
+
/** Whether the bindings are identical or one is a prefix of the other. */
|
|
160
|
+
readonly kind: ConflictKind;
|
|
108
161
|
/** Formatted display representation of the binding. */
|
|
109
162
|
readonly display: string;
|
|
110
163
|
/** Overlapping scope where conflict occurs, if any. */
|
|
@@ -114,34 +167,66 @@ interface Conflict<Id extends string = string> {
|
|
|
114
167
|
interface ScopeOptions {
|
|
115
168
|
/** Shadow commands in lower scopes, including global commands. */
|
|
116
169
|
exclusive?: boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Also shadow `"*"` commands, which otherwise survive every barrier.
|
|
172
|
+
* Implies `exclusive`. Meant for surfaces that must own the whole keyboard,
|
|
173
|
+
* such as recording a new shortcut.
|
|
174
|
+
*/
|
|
175
|
+
strict?: boolean;
|
|
117
176
|
}
|
|
118
177
|
//#endregion
|
|
119
178
|
//#region src/display.d.ts
|
|
120
179
|
/** Display options for formatting hotkeys. */
|
|
121
|
-
type DisplayOptions = FormatDisplayOptions
|
|
180
|
+
type DisplayOptions = Omit<FormatDisplayOptions, "parts">;
|
|
122
181
|
/** Formats a binding for display on the target platform (e.g. `⌘K` or `Ctrl+K`). */
|
|
123
182
|
declare function formatBinding(binding: Binding, options?: DisplayOptions): string;
|
|
124
183
|
/** Splits a binding into display tokens for rendering `<kbd>` elements. */
|
|
125
184
|
declare function bindingTokens(binding: Binding, options?: DisplayOptions): Array<string>;
|
|
185
|
+
/**
|
|
186
|
+
* Phrases a binding for a screen reader: `"Control+K"` for a chord,
|
|
187
|
+
* `"G then I"` for a sequence. `aria-keyshortcuts` cannot express a sequence
|
|
188
|
+
* (a space there means "or"), so this is how a sequence gets announced — put it
|
|
189
|
+
* in visually hidden text or `aria-description`.
|
|
190
|
+
*/
|
|
191
|
+
declare function spokenBinding(binding: Binding, platform?: Platform): string;
|
|
126
192
|
/** Generates an `aria-keyshortcuts` attribute string for a binding. */
|
|
127
193
|
declare function ariaKeyShortcuts(binding: Binding, platform?: Platform): string;
|
|
128
194
|
//#endregion
|
|
129
195
|
//#region src/keymap.d.ts
|
|
130
|
-
/**
|
|
131
|
-
type
|
|
196
|
+
/** One command's stored override: an array replaces the default, `{ add }` extends it. */
|
|
197
|
+
type SerializedOverride = Array<SerializedBinding> | {
|
|
198
|
+
add: Array<SerializedBinding>;
|
|
199
|
+
};
|
|
200
|
+
/** Persisted user overrides, keyed by command id. */
|
|
201
|
+
interface SerializedKeymap {
|
|
202
|
+
/** Format version, so a release can skip a format it does not know instead of misreading it. */
|
|
203
|
+
version: 1;
|
|
204
|
+
overrides: Record<string, SerializedOverride>;
|
|
205
|
+
}
|
|
132
206
|
/** Keymap interface for querying, modifying, and persisting keybindings. */
|
|
133
207
|
interface CommandKeymap<Id extends string = string> {
|
|
134
|
-
/** Active bindings in effect
|
|
208
|
+
/** Active bindings in effect: the default, with any user override applied. */
|
|
135
209
|
get(id: Id | (string & {})): ReadonlyArray<Binding>;
|
|
136
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* Replaces a command's bindings. Passing `null` unbinds it without
|
|
212
|
+
* restoring the default; passing exactly the default forgets the override,
|
|
213
|
+
* so the command follows the default again if the app changes it.
|
|
214
|
+
*/
|
|
137
215
|
set(id: Id | (string & {}), input: BindingInput | null): void;
|
|
138
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* Appends an additional binding. On a command the user has not replaced,
|
|
218
|
+
* it is stored on top of the default rather than as a copy of it, so a
|
|
219
|
+
* default the app changes later still applies alongside the added keys.
|
|
220
|
+
*/
|
|
139
221
|
add(id: Id | (string & {}), input: BindingInput): void;
|
|
140
222
|
/** Resets user overrides back to defaults for a specific command or all commands. */
|
|
141
223
|
reset(id?: Id | (string & {})): void;
|
|
142
224
|
/** Exports user overrides as JSON-serializable data. */
|
|
143
225
|
export(): SerializedKeymap;
|
|
144
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* Imports user overrides, replacing current overrides and saving to
|
|
228
|
+
* storage. Accepts `export()` output from this and earlier releases.
|
|
229
|
+
*/
|
|
145
230
|
import(data: Json): void;
|
|
146
231
|
}
|
|
147
232
|
//#endregion
|
|
@@ -170,8 +255,21 @@ interface CommandsOptions<Id extends string = string> {
|
|
|
170
255
|
platform?: Platform;
|
|
171
256
|
/** Target element for key events. Defaults to `document`. */
|
|
172
257
|
target?: HTMLElement | Document | Window | null;
|
|
173
|
-
/**
|
|
258
|
+
/**
|
|
259
|
+
* Persistence configuration for user keymap overrides. Overrides are read
|
|
260
|
+
* on the first `mount()` rather than at creation, so the server render and
|
|
261
|
+
* the client's first render agree and hydration never mismatches; the
|
|
262
|
+
* registry notifies subscribers once they load.
|
|
263
|
+
*/
|
|
174
264
|
persist?: KeymapPersistence;
|
|
265
|
+
/**
|
|
266
|
+
* Whether single-key shortcuts — bindings where every step is a printable
|
|
267
|
+
* character with no Control, Alt or Meta, such as `?` or `G then I` — are
|
|
268
|
+
* active. Defaults to `true`. WCAG 2.1.4 requires a way to turn these off
|
|
269
|
+
* for speech-input and motor-impaired users; wire a setting to
|
|
270
|
+
* `commands.setSingleKeyShortcuts()`.
|
|
271
|
+
*/
|
|
272
|
+
singleKeyShortcuts?: boolean;
|
|
175
273
|
/** Display options for rendering shortcuts. */
|
|
176
274
|
display?: DisplayOptions;
|
|
177
275
|
/**
|
|
@@ -184,9 +282,12 @@ interface CommandsOptions<Id extends string = string> {
|
|
|
184
282
|
* });
|
|
185
283
|
* ```
|
|
186
284
|
*/
|
|
187
|
-
onRun?: (context: CommandContext) => void;
|
|
188
|
-
/**
|
|
189
|
-
|
|
285
|
+
onRun?: (context: CommandContext<Id>) => void;
|
|
286
|
+
/**
|
|
287
|
+
* Error handler called when a command, its `when()` guard, or `onRun`
|
|
288
|
+
* throws. Defaults to `console.error`.
|
|
289
|
+
*/
|
|
290
|
+
onError?: (error: Error, context: CommandContext<Id>) => void;
|
|
190
291
|
}
|
|
191
292
|
/** Options passed when manually running a command. */
|
|
192
293
|
interface RunOptions {
|
|
@@ -232,6 +333,16 @@ interface Commands<Id extends string = string> {
|
|
|
232
333
|
get(id: Id | (string & {})): Command<Id> | undefined;
|
|
233
334
|
/** Returns all registered commands in definition order. */
|
|
234
335
|
list(): ReadonlyArray<Command<Id>>;
|
|
336
|
+
/**
|
|
337
|
+
* The command list as the server renders it: the commands passed to
|
|
338
|
+
* `createCommands` with their default bindings (after `options.bindings`),
|
|
339
|
+
* formatted for the server platform — no runtime `add()`, scope, user
|
|
340
|
+
* override or `setEnabled` applied. Referentially stable. This is the
|
|
341
|
+
* `getServerSnapshot` the React hooks hand `useSyncExternalStore`, so a
|
|
342
|
+
* subtree that hydrates after the client state moved on still matches the
|
|
343
|
+
* server's HTML.
|
|
344
|
+
*/
|
|
345
|
+
serverList(): ReadonlyArray<Command<Id>>;
|
|
235
346
|
/** Runs a command by ID. Returns `true` if executed. */
|
|
236
347
|
run(id: Id, options?: RunOptions): boolean;
|
|
237
348
|
/** Checks whether a command can run currently. */
|
|
@@ -241,12 +352,16 @@ interface Commands<Id extends string = string> {
|
|
|
241
352
|
/** Enables or disables a command. */
|
|
242
353
|
setEnabled(id: Id | (string & {}), enabled: boolean): void;
|
|
243
354
|
/** Registers a command at runtime. */
|
|
244
|
-
add(id: Id | (string & {}), definition: CommandDefinition): CommandHandle;
|
|
355
|
+
add(id: Id | (string & {}), definition: CommandDefinition<Id>): CommandHandle;
|
|
245
356
|
/** Removes a command registration. */
|
|
246
357
|
remove(id: Id | (string & {})): void;
|
|
247
358
|
/** Activates a scope until the returned disposal function is called. */
|
|
248
359
|
pushScope(name: string, options?: ScopeOptions): () => void;
|
|
249
|
-
/**
|
|
360
|
+
/**
|
|
361
|
+
* Finds commands in overlapping scopes whose bindings collide with the given
|
|
362
|
+
* one — the same keys (`kind: "exact"`), or one a prefix of the other, like
|
|
363
|
+
* chord `G` against `G then I` (`kind: "prefix"`).
|
|
364
|
+
*/
|
|
250
365
|
conflicts(binding: BindingInput, options?: ConflictOptions<Id>): Array<Conflict<Id>>;
|
|
251
366
|
/** Formats a command's primary binding for display. */
|
|
252
367
|
display(id: Id | (string & {})): string;
|
|
@@ -260,6 +375,10 @@ interface Commands<Id extends string = string> {
|
|
|
260
375
|
mount(): () => void;
|
|
261
376
|
/** Subscribes to registry changes. Returns an unsubscribe function. */
|
|
262
377
|
subscribe(listener: () => void): () => void;
|
|
378
|
+
/** Whether single-key shortcuts are active. See `CommandsOptions.singleKeyShortcuts`. */
|
|
379
|
+
readonly singleKeyShortcuts: boolean;
|
|
380
|
+
/** Turns single-key shortcuts on or off for every command (WCAG 2.1.4). */
|
|
381
|
+
setSingleKeyShortcuts(enabled: boolean): void;
|
|
263
382
|
/** Tears down listeners and clears all command registrations. */
|
|
264
383
|
destroy(): void;
|
|
265
384
|
}
|
|
@@ -282,7 +401,7 @@ interface Commands<Id extends string = string> {
|
|
|
282
401
|
* });
|
|
283
402
|
* ```
|
|
284
403
|
*/
|
|
285
|
-
declare function createCommands<
|
|
404
|
+
declare function createCommands<Id extends string>(definitions: { [K in Id]: CommandDefinition<Id>; }, options?: CommandsOptions<NoInfer<Id>>): Commands<Id>;
|
|
286
405
|
/**
|
|
287
406
|
* Extracts command ID union from a Commands registry type.
|
|
288
407
|
*
|
|
@@ -304,4 +423,4 @@ interface ObserveOptions {
|
|
|
304
423
|
*/
|
|
305
424
|
declare function observe<Id extends string>(registry: Commands<Id>, options?: ObserveOptions): () => void;
|
|
306
425
|
//#endregion
|
|
307
|
-
export {
|
|
426
|
+
export { HotkeySequence as A, Command as C, Conflict as D, CommandSource as E, SequenceBinding as F, SerializedBinding as I, KeymapStorage as M, Platform as N, ConflictKind as O, ScopeOptions as P, Chord as S, CommandDefinition as T, bindingTokens as _, Commands as a, Binding as b, PendingSequence as c, createCommands as d, CommandKeymap as f, ariaKeyShortcuts as g, DisplayOptions as h, CommandId as i, KeymapPersistence as j, Hotkey$1 as k, RunBlockReason as l, SerializedOverride as m, observe as n, CommandsOptions as o, SerializedKeymap as p, CommandHandle as r, ConflictOptions as s, ObserveOptions as t, RunOptions as u, formatBinding as v, CommandContext as w, BindingInput as x, spokenBinding as y };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as observe, t as ObserveOptions } from "./attributes-
|
|
1
|
+
import { n as observe, t as ObserveOptions } from "./attributes-DotLr2MW.mjs";
|
|
2
2
|
export { ObserveOptions, observe };
|
|
@@ -8,15 +8,19 @@ function observe(registry, options = {}) {
|
|
|
8
8
|
if (!("document" in globalThis)) return () => {};
|
|
9
9
|
const root = options.root ?? document;
|
|
10
10
|
const onClick = (event) => {
|
|
11
|
+
if (event.defaultPrevented) return;
|
|
11
12
|
const target = event.target;
|
|
12
13
|
if (!(target instanceof Element)) return;
|
|
13
|
-
const
|
|
14
|
+
const element = target.closest(`[${COMMAND_ATTRIBUTE}]`);
|
|
15
|
+
const id = element?.getAttribute(COMMAND_ATTRIBUTE);
|
|
14
16
|
if (!id) return;
|
|
17
|
+
if (element?.closest("[aria-disabled=\"true\"]")) return;
|
|
15
18
|
registry.run(id, {
|
|
16
19
|
source: "dom",
|
|
17
20
|
event
|
|
18
21
|
});
|
|
19
22
|
};
|
|
23
|
+
const labelled = /* @__PURE__ */ new WeakSet();
|
|
20
24
|
const paintElement = (element) => {
|
|
21
25
|
const keysId = element.getAttribute(KEYS_ATTRIBUTE);
|
|
22
26
|
if (keysId) {
|
|
@@ -24,10 +28,13 @@ function observe(registry, options = {}) {
|
|
|
24
28
|
if (element.textContent !== display) element.textContent = display;
|
|
25
29
|
}
|
|
26
30
|
const commandId = element.getAttribute(COMMAND_ATTRIBUTE);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (aria) element.setAttribute("aria-keyshortcuts", aria);
|
|
30
|
-
|
|
31
|
+
const aria = commandId ? registry.get(commandId)?.aria ?? "" : "";
|
|
32
|
+
if (aria) {
|
|
33
|
+
if (element.getAttribute("aria-keyshortcuts") !== aria) element.setAttribute("aria-keyshortcuts", aria);
|
|
34
|
+
labelled.add(element);
|
|
35
|
+
} else if (commandId || labelled.has(element)) {
|
|
36
|
+
element.removeAttribute("aria-keyshortcuts");
|
|
37
|
+
labelled.delete(element);
|
|
31
38
|
}
|
|
32
39
|
};
|
|
33
40
|
const paintWithin = (scope) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as Command, D as Conflict, F as SequenceBinding, N as Platform, S as Chord, a as Commands, b as Binding } from "./attributes-DotLr2MW.mjs";
|
|
2
2
|
import { Hotkey } from "@tanstack/hotkeys";
|
|
3
3
|
//#region src/bindings.d.ts
|
|
4
4
|
/**
|
|
@@ -10,6 +10,13 @@ import { Hotkey } from "@tanstack/hotkeys";
|
|
|
10
10
|
* ```
|
|
11
11
|
*/
|
|
12
12
|
declare function seq(...steps: Array<Hotkey>): SequenceBinding;
|
|
13
|
+
/**
|
|
14
|
+
* Whether every step of a binding is a single printable character with no
|
|
15
|
+
* Control, Alt or Meta — the "character key shortcuts" WCAG 2.1.4 lets users
|
|
16
|
+
* 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
|
+
*/
|
|
19
|
+
declare function isSingleKeyBinding(binding: Binding, platform?: Platform): boolean;
|
|
13
20
|
/** Validation result for a hotkey binding. */
|
|
14
21
|
interface BindingValidation {
|
|
15
22
|
readonly valid: boolean;
|
|
@@ -90,4 +97,20 @@ declare function createRebinder<Id extends string>(registry: Commands<Id>, id: I
|
|
|
90
97
|
/** Wildcard scope identifier for commands that remain active across exclusive scopes. */
|
|
91
98
|
declare const ALWAYS_SCOPE = "*";
|
|
92
99
|
//#endregion
|
|
93
|
-
|
|
100
|
+
//#region src/search.d.ts
|
|
101
|
+
/**
|
|
102
|
+
* Filters commands for a search query, as a command palette does, leaving
|
|
103
|
+
* out `hidden` ones. Every word of the query must appear somewhere in the
|
|
104
|
+
* command — title, description, group, keywords, id or displayed shortcut —
|
|
105
|
+
* in any order. Title matches come first: titles starting with the query,
|
|
106
|
+
* then titles containing every word, then the rest; each tier keeps the
|
|
107
|
+
* list's own order. An empty query returns every visible command.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* const results = matchCommands(commands.list(), "go inbox");
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
declare function matchCommands<Id extends string>(commands: ReadonlyArray<Command<Id>>, query: string): Array<Command<Id>>;
|
|
115
|
+
//#endregion
|
|
116
|
+
export { RebindState as a, BindingValidation as c, validateBinding as d, RebindOptions as i, isSingleKeyBinding as l, ALWAYS_SCOPE as n, Rebinder as o, RebindMode as r, createRebinder as s, matchCommands as t, seq as u };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { A as HotkeySequence, C as Command, D as Conflict, E as CommandSource, F as SequenceBinding, I as SerializedBinding, M as KeymapStorage, N as Platform, O as ConflictKind, P as ScopeOptions, S as Chord, T as CommandDefinition, _ as bindingTokens, a as Commands, b as Binding, c as PendingSequence, d as createCommands, f as CommandKeymap, g as ariaKeyShortcuts, h as DisplayOptions, i as CommandId, j as KeymapPersistence, k as Hotkey, l as RunBlockReason, m as SerializedOverride, n as observe, o as CommandsOptions, p as SerializedKeymap, r as CommandHandle, s as ConflictOptions, t as ObserveOptions, u as RunOptions, v as formatBinding, w as CommandContext, x as BindingInput, y as spokenBinding } from "./attributes-DotLr2MW.mjs";
|
|
2
|
+
import { a as RebindState, c as BindingValidation, d as validateBinding, i as RebindOptions, l as isSingleKeyBinding, n as ALWAYS_SCOPE, o as Rebinder, r as RebindMode, s as createRebinder, t as matchCommands, u as seq } from "./index-BTgscYdI.mjs";
|
|
3
|
+
export { ALWAYS_SCOPE, type Binding, type BindingInput, type BindingValidation, type Chord, type Command, type CommandContext, type CommandDefinition, type CommandHandle, type CommandId, type CommandKeymap, type CommandSource, type Commands, type CommandsOptions, type Conflict, type ConflictKind, type ConflictOptions, type DisplayOptions, type Hotkey, type HotkeySequence, type KeymapPersistence, type KeymapStorage, type ObserveOptions, 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, observe, seq, spokenBinding, validateBinding };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { observe } from "./attributes.mjs";
|
|
2
|
+
import { a as ariaKeyShortcuts, c as spokenBinding, d as seq, i as createRebinder, n as createCommands, o as bindingTokens, p as validateBinding, r as ALWAYS_SCOPE, s as formatBinding, t as matchCommands, u as isSingleKeyBinding } from "./src-DgYjA1oS.mjs";
|
|
3
|
+
export { ALWAYS_SCOPE, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, isSingleKeyBinding, matchCommands, observe, seq, spokenBinding, validateBinding };
|