@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.
@@ -1,15 +1,11 @@
1
1
  //#region src/chord.d.ts
2
+ /** Chords: reading one from text or a keypress, matching the two, and writing one back. */
2
3
  /**
3
- * One chord: its spelling, reading it from a string or a keypress, comparing
4
- * the two, and writing it back. Everything else in the package handles chords
5
- * through here, so there is one grammar and one idea of which key was pressed.
6
- */
7
- /** Supported platforms for hotkey resolution and display formatting. */
8
- type Platform = "mac" | "windows" | "linux";
9
- /**
10
- * Named keys with a place in the `Key` type, as `KeyboardEvent.key` spells
11
- * them. Any other name still works as a key; `validateChord` only remarks on it.
4
+ * The two kinds of keyboard: `"mac"` for Apple's (macOS, iPadOS, iOS), whose
5
+ * primary modifier is Command, and `"other"` for the rest, where it is Control.
12
6
  */
7
+ type Platform = "mac" | "other";
8
+ /** Named keys in the `Key` type, as `KeyboardEvent.key` spells them. Other names still work. */
13
9
  declare const NAMED_KEYS: readonly ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown", "Enter", "Escape", "Space", "Tab", "Backspace", "Delete", "Insert", "CapsLock", "ContextMenu", "PrintScreen", "ScrollLock", "Pause", "NumLock", "Help", "Clear", "MediaPlayPause", "MediaStop", "MediaTrackNext", "MediaTrackPrevious", "AudioVolumeUp", "AudioVolumeDown", "AudioVolumeMute", "BrowserBack", "BrowserForward", "BrowserRefresh", "BrowserSearch", "BrowserHome"];
14
10
  type Letter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z";
15
11
  type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
@@ -38,6 +34,19 @@ interface SequenceBinding {
38
34
  type Binding = Chord | SequenceBinding;
39
35
  /** One binding or an array of alternative bindings. */
40
36
  type BindingInput = Binding | ReadonlyArray<Binding>;
37
+ /**
38
+ * Keys that differ by platform: `mac` on Apple keyboards (macOS, iPadOS and
39
+ * iOS), `other` everywhere else. A side left out is unbound there.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * keys: { mac: "Mod+Shift+Z", other: ["Mod+Y", "Mod+Shift+Z"] }
44
+ * ```
45
+ */
46
+ interface PlatformBindings {
47
+ readonly mac?: BindingInput;
48
+ readonly other?: BindingInput;
49
+ }
41
50
  /** JSON-serializable form of a binding. */
42
51
  type SerializedBinding = string | Array<string>;
43
52
  /** An arbitrary JSON-compatible value, e.g. from `JSON.parse` or persisted storage. */
@@ -48,7 +57,7 @@ type Json = string | number | boolean | null | ReadonlyArray<Json> | {
48
57
  interface DisplayOptions {
49
58
  /** Platform to write for. Defaults to the one detected. */
50
59
  platform?: Platform;
51
- /** Symbols such as `⌘` and `↵` rather than names such as `Cmd`. Defaults to `true`. */
60
+ /** Symbols such as `⌘` and `↩` rather than names such as `Cmd`. Defaults to `true`. */
52
61
  useSymbols?: boolean;
53
62
  }
54
63
  /** Trigger source for a command. */
@@ -63,26 +72,14 @@ interface CommandContext<Id extends string = string> {
63
72
  readonly event?: Event;
64
73
  /** Specific binding that triggered the command. */
65
74
  readonly binding?: Binding;
66
- /**
67
- * The commands registry instance.
68
- *
69
- * @example
70
- * ```ts
71
- * run: ({ commands }) => {
72
- * commands.run("doc.save");
73
- * }
74
- * ```
75
- */
75
+ /** The registry, typed with its own ids. */
76
76
  readonly commands: Commands<Id>;
77
77
  }
78
78
  /** What a command does when it runs. */
79
79
  type CommandHandler<Id extends string = string> = (context: CommandContext<Id>) => void | Promise<void>;
80
80
  /**
81
81
  * A command: what it is called, which keys reach it, and where it is live.
82
- *
83
- * `Id` is the registry's id union. `createCommands` fills it in, so
84
- * `commands.run(...)` inside a handler is checked against the registry's own
85
- * ids; a definition written on its own defaults to plain `string`.
82
+ * `Id` is the registry's id union, which `createCommands` fills in.
86
83
  */
87
84
  interface CommandDefinition<Id extends string = string> {
88
85
  /** Human-readable title. */
@@ -94,37 +91,42 @@ interface CommandDefinition<Id extends string = string> {
94
91
  /** Extra search terms `matchCommands` checks besides the title, e.g. `["find", "search"]`. */
95
92
  keywords?: ReadonlyArray<string>;
96
93
  /**
97
- * Leave the command out of `matchCommands` — for commands that only make
98
- * sense from their key, like a palette's own Close. It still runs and
99
- * still appears in `list()`. Defaults to `false`.
94
+ * Leave the command out of `matchCommands`. It still runs and still appears
95
+ * in `list()`. Defaults to `false`.
100
96
  */
101
97
  hidden?: boolean;
102
- /** Default key binding(s). */
103
- keys?: BindingInput;
98
+ /**
99
+ * Default key binding(s). Write `{ mac, other }` where the platforms'
100
+ * conventions differ, as they do for redo and for back and forward.
101
+ */
102
+ keys?: BindingInput | PlatformBindings;
104
103
  /** Scope required for this command to fire. Omit for global, or `"*"` to bypass exclusive scopes. */
105
104
  scope?: string;
106
105
  /**
107
- * Prevent default event behavior when run. Defaults to `true`. For a
108
- * sequence this also covers each step as it matches, so a first step such
109
- * as `Mod+K` does not trigger the browser's own shortcut on the way.
106
+ * Prevent default event behavior when run, and for each step of a sequence as
107
+ * it matches. Defaults to `true`.
110
108
  */
111
109
  preventDefault?: boolean;
112
110
  /**
113
- * Whether to ignore key events from text inputs, textareas, selects and
114
- * contenteditable elements. When omitted it is decided per binding: chords
115
- * holding Control or Meta (so `Mod+…`) and bare `Escape` still fire inside
116
- * inputs, while everything else — bare keys, Shift/Alt chords, and
117
- * sequences starting with one — is ignored there, so typing is never
118
- * hijacked.
111
+ * Whether to ignore key events from text inputs, selects, contenteditable
112
+ * elements and widgets with a text-entry ARIA role. When omitted, chords
113
+ * holding Control or Meta and bare `Escape` still fire there, except the
114
+ * field's own: select all, copy, cut, paste, undo, redo, and moving or
115
+ * deleting by word or line.
119
116
  */
120
117
  ignoreInputs?: boolean;
121
118
  /** Timeout in milliseconds between sequence steps. Defaults to `1000`. */
122
119
  sequenceTimeout?: number;
123
120
  /**
124
- * What the command does, when that needs nothing a component owns. Leave
125
- * it out and attach the behaviour where the state lives, with
126
- * `commands.handle()` or `useCommandHandler`: a command with no handler
127
- * cannot run and leaves its key alone.
121
+ * Whether holding the keys down runs the command again on each auto-repeat.
122
+ * Defaults to `true`. With `false` it runs once per press and still claims
123
+ * its key while held.
124
+ */
125
+ repeat?: boolean;
126
+ /**
127
+ * What the command does, when that needs nothing a component owns. Otherwise
128
+ * attach it with `commands.handle()` or `useCommandHandler`. A command with
129
+ * no handler cannot run and leaves its key alone.
128
130
  */
129
131
  run?: CommandHandler<Id>;
130
132
  }
@@ -145,16 +147,11 @@ interface Command<Id extends string = string> {
145
147
  readonly display: ReadonlyArray<string>;
146
148
  /** Formatted tokens per binding for rendering `<kbd>` elements. */
147
149
  readonly tokens: ReadonlyArray<ReadonlyArray<string>>;
148
- /**
149
- * Value for the `aria-keyshortcuts` attribute. Only chords appear here: the
150
- * attribute separates alternatives with spaces, so it cannot express a
151
- * sequence. Use `spoken` to announce those.
152
- */
150
+ /** Value for the `aria-keyshortcuts` attribute. Chords only: it cannot express a sequence. */
153
151
  readonly aria: string;
154
152
  /**
155
- * Every binding phrased for a screen reader, alternatives joined by
156
- * `" or "` — e.g. `"Control+K"` or `"G then I"`. Suited to visually hidden
157
- * text or `aria-description`, and the only accessible form of a sequence.
153
+ * Every binding phrased for a screen reader, joined by `" or "`: `"Command K"`,
154
+ * `"G then I"`. The only accessible form of a sequence.
158
155
  */
159
156
  readonly spoken: string;
160
157
  /** Whether its keys work right now: it has a handler and its scope is active. */
@@ -162,76 +159,63 @@ interface Command<Id extends string = string> {
162
159
  /** Whether the command has a user keymap override. */
163
160
  readonly overridden: boolean;
164
161
  }
165
- /** Storage interface for persisting keymap overrides. */
166
- interface KeymapStorage {
167
- getItem: (key: string) => string | null;
168
- setItem: (key: string, value: string) => void;
169
- removeItem: (key: string) => void;
162
+ /** A user's change to one command: `"replace"` the default (`[]` unbinds), or `"add"` to it. */
163
+ interface Override {
164
+ readonly kind: "replace" | "add";
165
+ readonly bindings: ReadonlyArray<Binding>;
170
166
  }
171
- /** Configuration for persisting keymap overrides. */
172
- interface KeymapPersistence {
173
- /** Storage key name. Defaults to `"inklu.keys"`. */
174
- key?: string;
175
- /** Getter returning the storage implementation lazily. */
176
- storage: () => KeymapStorage | null;
167
+ /** Where user overrides are kept between visits. `webStorage()` makes one. */
168
+ interface KeymapStore {
169
+ /** The stored overrides: empty when there are none, `null` when they cannot be read. */
170
+ read(): Map<string, Override> | null;
171
+ write(overrides: ReadonlyMap<string, Override>): void;
172
+ /** Calls `onChange` when another tab writes. Returns a function that stops. */
173
+ watch(onChange: () => void): () => void;
177
174
  }
178
175
  /** One command's stored override: an array replaces the default, `{ add }` extends it. */
179
176
  type SerializedOverride = Array<SerializedBinding> | {
180
177
  add: Array<SerializedBinding>;
181
178
  };
182
- /**
183
- * Persisted user overrides, keyed by command id. `version` lets a release
184
- * skip a format it does not know instead of misreading it.
185
- */
179
+ /** User overrides as JSON-safe data, keyed by command id. What `exportKeymap()` returns. */
186
180
  type SerializedKeymap = {
187
181
  version: 1;
188
182
  overrides: Record<string, SerializedOverride>;
189
183
  };
190
- /** Keymap interface for querying, modifying, and persisting keybindings. */
184
+ /** A registry's keymap: the keys in effect, and the user's changes to them. */
191
185
  interface CommandKeymap<Id extends string = string> {
192
186
  /** Active bindings in effect: the default, with any user override applied. */
193
187
  get(id: Id): ReadonlyArray<Binding>;
194
- /**
195
- * Replaces a command's bindings. Passing `null` unbinds it without
196
- * restoring the default; passing exactly the default forgets the override,
197
- * so the command follows the default again if the app changes it.
198
- */
188
+ /** Replaces a command's bindings. `null` unbinds it; exactly the default forgets the override. */
199
189
  set(id: Id, input: BindingInput | null): void;
200
- /**
201
- * Appends an additional binding. On a command the user has not replaced,
202
- * it is stored on top of the default rather than as a copy of it, so a
203
- * default the app changes later still applies alongside the added keys.
204
- */
190
+ /** Appends a binding, stored on top of the default so a default that changes still applies. */
205
191
  add(id: Id, input: BindingInput): void;
206
192
  /** Resets user overrides back to defaults for a specific command or all commands. */
207
193
  reset(id?: Id): void;
208
- /** Exports user overrides as JSON-serializable data. */
209
- export(): SerializedKeymap;
210
- /**
211
- * Imports user overrides, replacing current overrides and saving to
212
- * storage. Accepts `export()` output from this and earlier releases.
213
- */
214
- import(data: Json): void;
215
194
  }
216
195
  /**
217
- * How two bindings collide: `"exact"` when they are the same keys, `"prefix"`
218
- * when one is the start of the other — chord `G` against `G then I` — so the
219
- * shorter one fires on the way to the longer one.
196
+ * One thing in a binding's way: a command in an overlapping scope with the
197
+ * same keys (`"exact"`) or with one binding starting the other (`"prefix"`),
198
+ * or the browser or operating system keeping the keys (`"reserved"`, which
199
+ * has no `id`).
220
200
  */
221
- type ConflictKind = "exact" | "prefix";
222
- /** Details of a conflict between commands whose bindings collide in overlapping scopes. */
223
- interface Conflict<Id extends string = string> {
201
+ type Conflict<Id extends string = string> = {
202
+ /** Whether the bindings are identical or one is a prefix of the other. */
203
+ readonly kind: "exact" | "prefix";
224
204
  /** ID of the conflicting command. */
225
205
  readonly id: Id;
226
206
  /** The conflicting command's binding. */
227
207
  readonly binding: Binding;
228
- /** Whether the bindings are identical or one is a prefix of the other. */
229
- readonly kind: ConflictKind;
230
- /** Formatted display representation of the binding. */
208
+ /** Formatted display representation of that binding. */
231
209
  readonly display: string;
232
- /** Overlapping scope where conflict occurs, if any. */
210
+ /** The conflicting command's scope, if it has one. */
233
211
  readonly scope?: string;
234
- }
212
+ } | {
213
+ readonly kind: "reserved";
214
+ /** The binding that was asked about. */
215
+ readonly binding: Binding;
216
+ /** Formatted display representation of that binding. */
217
+ readonly display: string;
218
+ };
235
219
  /** Options for conflict detection. */
236
220
  interface ConflictOptions<Id extends string = string> {
237
221
  /** Command ID to ignore during conflict checking. */
@@ -246,8 +230,7 @@ interface ScopeOptions {
246
230
  }
247
231
  /**
248
232
  * Receives each keypress while the keyboard is captured: the event, and the
249
- * chord it expresses (`null` for a modifier on its own). It decides what the
250
- * key means, including whether to `preventDefault()` it.
233
+ * chord it expresses (`null` for a modifier on its own).
251
234
  */
252
235
  type KeyCapture = (event: KeyboardEvent, chord: Chord | null) => void;
253
236
  /** Options passed when manually running a command. */
@@ -278,14 +261,9 @@ interface PendingSequence<Id extends string = string> {
278
261
  /** Configuration options for a commands registry. */
279
262
  interface CommandsOptions<Id extends string = string> {
280
263
  /**
281
- * Replaces the default `keys` a command was defined with, keyed by command
282
- * id. Lets an app rebind commands it did not define — a package ships the
283
- * behaviour, the app decides which key reaches it — and an unknown id is a
284
- * compile error rather than a silent no-op. `null` ships the command with
285
- * no default binding at all.
286
- *
287
- * These are defaults, applied before persisted overrides load, so a user's
288
- * own rebinding still wins.
264
+ * Replaces the default `keys` of commands, by id, for rebinding commands the
265
+ * app did not define. `null` ships a command unbound. A user's own rebinding
266
+ * still wins.
289
267
  *
290
268
  * @example
291
269
  * ```ts
@@ -294,38 +272,26 @@ interface CommandsOptions<Id extends string = string> {
294
272
  * });
295
273
  * ```
296
274
  */
297
- bindings?: Partial<Record<Id, BindingInput | null>>;
275
+ bindings?: Partial<Record<Id, BindingInput | PlatformBindings | null>>;
298
276
  /** Platform for modifier resolution and display formatting. Defaults to auto-detection. */
299
277
  platform?: Platform;
300
278
  /** Target element for key events. Defaults to `document`. */
301
279
  target?: HTMLElement | Document | Window | null;
302
280
  /**
303
- * Persistence configuration for user keymap overrides. Overrides are read
304
- * on the first `mount()` rather than at creation, so the server render and
305
- * the client's first render agree and hydration never mismatches; the
306
- * registry notifies subscribers once they load.
281
+ * Where to keep user keymap overrides between visits, such as
282
+ * `webStorage("my-app.keys")`. They load on the first `mount()`, so the
283
+ * server render and the client's first render agree.
307
284
  */
308
- persist?: KeymapPersistence;
285
+ persist?: KeymapStore;
309
286
  /**
310
- * Whether single-key shortcuts — bindings where every step is a printable
311
- * character with no Control, Alt or Meta, such as `?` or `G then I` — are
312
- * active. Defaults to `true`. WCAG 2.1.4 requires a way to turn these off
313
- * for speech-input and motor-impaired users; wire a setting to
314
- * `commands.setSingleKeyShortcuts()`.
287
+ * Whether single-key shortcuts (every step a printable character with no
288
+ * Control, Alt or Meta, such as `?` or `G then I`) are active. Defaults to
289
+ * `true`. WCAG 2.1.4 requires a way to turn them off.
315
290
  */
316
291
  singleKeyShortcuts?: boolean;
317
292
  /** Display options for rendering shortcuts. */
318
293
  display?: DisplayOptions;
319
- /**
320
- * Callback fired whenever a command runs.
321
- *
322
- * @example
323
- * ```ts
324
- * createCommands(definitions, {
325
- * onRun: ({ id }) => logRun(id),
326
- * });
327
- * ```
328
- */
294
+ /** Called whenever a command runs. */
329
295
  onRun?: (context: CommandContext<Id>) => void;
330
296
  /**
331
297
  * Error handler called when a handler or `onRun` throws. Defaults to
@@ -342,17 +308,13 @@ interface Commands<Id extends string = string> {
342
308
  /** Returns all commands in definition order. */
343
309
  list(): ReadonlyArray<Command<Id>>;
344
310
  /**
345
- * The list as the server rendered it: default bindings in the
346
- * platform-neutral form, no scope, user override or attached handler
347
- * applied. Referentially stable. Hand it to a framework as the server
348
- * snapshot (`useSyncExternalStore`'s third argument), so a subtree that
349
- * hydrates after the client state moved on still matches the server's HTML.
311
+ * The list as the server rendered it: defaults only, and referentially
312
+ * stable. For a framework's server snapshot.
350
313
  */
351
314
  serverList(): ReadonlyArray<Command<Id>>;
352
315
  /**
353
- * Runs a command by ID. Returns `true` if executed. A direct call is not
354
- * a keypress, so it passes `exclusive` barriers: a palette can run what it
355
- * lists while its own scope silences the keyboard.
316
+ * Runs a command by ID. Returns `true` if executed. Passes `exclusive`
317
+ * barriers, which silence keys, not commands.
356
318
  */
357
319
  run(id: Id, options?: RunOptions): boolean;
358
320
  /** Checks whether `run` would execute the command right now. */
@@ -360,23 +322,20 @@ interface Commands<Id extends string = string> {
360
322
  /** Returns why `run` would refuse the command, or `null` if it would run. */
361
323
  whyNotRun(id: Id): RunBlockReason | null;
362
324
  /**
363
- * Attaches what a command does until the returned function is called.
364
- * The most recently attached handler runs; disposing it hands the command
365
- * back to the one before, or to the definition's own `run`.
325
+ * Attaches what a command does until the returned function is called. The
326
+ * most recently attached handler runs.
366
327
  */
367
328
  handle(id: Id, run: CommandHandler<Id>): () => void;
368
329
  /** Activates a scope until the returned disposal function is called. */
369
330
  pushScope(name: string, options?: ScopeOptions): () => void;
370
331
  /**
371
332
  * Takes the whole keyboard until the returned function is called: every
372
- * keypress goes to `onKey` first and no command's keys are live, `"*"`
373
- * ones included. This is what recording a new shortcut is built on.
333
+ * keypress goes to `onKey` and no command runs.
374
334
  */
375
335
  capture(onKey: KeyCapture): () => void;
376
336
  /**
377
- * Finds commands in overlapping scopes whose bindings collide with the given
378
- * one — the same keys (`kind: "exact"`), or one a prefix of the other, like
379
- * chord `G` against `G then I` (`kind: "prefix"`).
337
+ * Finds what stands in a binding's way: the platform reserving it (listed
338
+ * first), then commands in overlapping scopes with colliding keys.
380
339
  */
381
340
  conflicts(binding: BindingInput, options?: ConflictOptions<Id>): Array<Conflict<Id>>;
382
341
  /** Returns the partially-typed sequences. Tracked only while something is subscribed. */
@@ -395,4 +354,4 @@ interface Commands<Id extends string = string> {
395
354
  destroy(): void;
396
355
  }
397
356
  //#endregion
398
- export { SequenceBinding as C, Chord as D, SerializedOverride as E, Key as O, ScopeOptions as S, SerializedKeymap as T, KeymapPersistence as _, CommandDefinition as a, RunBlockReason as b, CommandSource as c, Conflict as d, ConflictKind as f, KeyCapture as g, Json as h, CommandContext as i, Platform as k, Commands as l, DisplayOptions as m, BindingInput as n, CommandHandler as o, ConflictOptions as p, Command as r, CommandKeymap as s, Binding as t, CommandsOptions as u, KeymapStorage as v, SerializedBinding as w, RunOptions as x, PendingSequence as y };
357
+ export { SequenceBinding as C, Chord as D, SerializedOverride as E, Key as O, ScopeOptions as S, SerializedKeymap as T, Override as _, CommandDefinition as a, RunBlockReason as b, CommandSource as c, Conflict as d, ConflictOptions as f, KeymapStore as g, KeyCapture as h, CommandContext as i, Platform as k, Commands as l, Json as m, BindingInput as n, CommandHandler as o, DisplayOptions as p, Command as r, CommandKeymap as s, Binding as t, CommandsOptions as u, PendingSequence as v, SerializedBinding as w, RunOptions as x, PlatformBindings as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inklu/keys",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "A command system for the web — typed commands, portable keybindings, scopes, sequences, conflict detection, rebinding, and platform-aware display.",
5
5
  "keywords": [
6
6
  "accessibility",