@inklu/keys 1.3.0 → 1.4.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 CHANGED
@@ -15,16 +15,16 @@ A command is an addressable action. It has an id, title, and a handler.
15
15
  import { createCommands, seq } from "@inklu/keys";
16
16
 
17
17
  export const commands = createCommands({
18
- "palette.open": {
19
- title: "Open command palette",
20
- keys: "Mod+K",
21
- run: () => setPaletteOpen(true),
22
- },
23
- "nav.inbox": {
24
- title: "Go to inbox",
25
- keys: seq("G", "I"), // press G, then I
26
- run: () => navigate("/inbox"),
27
- },
18
+ "palette.open": {
19
+ title: "Open command palette",
20
+ keys: "Mod+K",
21
+ run: () => setPaletteOpen(true),
22
+ },
23
+ "nav.inbox": {
24
+ title: "Go to inbox",
25
+ keys: seq("G", "I"), // press G, then I
26
+ run: () => navigate("/inbox"),
27
+ },
28
28
  });
29
29
  ```
30
30
 
@@ -37,24 +37,26 @@ import { useCommands, useMountCommands, useShortcut } from "@inklu/keys/react";
37
37
  import { commands } from "./commands";
38
38
 
39
39
  function App() {
40
- // Starts listening in an effect
41
- useMountCommands(commands);
42
- return <ShortcutButton />;
40
+ // Starts listening in an effect
41
+ useMountCommands(commands);
42
+ return <ShortcutButton />;
43
43
  }
44
44
 
45
45
  function ShortcutButton() {
46
- // Get live shortcut state
47
- const shortcut = useShortcut(commands, "palette.open");
48
-
49
- return (
50
- <button
51
- onClick={() => commands.run("palette.open")}
52
- aria-keyshortcuts={shortcut.aria || undefined}
53
- >
54
- Open Palette
55
- {shortcut.tokens[0]?.map((token, i) => <kbd key={i}>{token}</kbd>)}
56
- </button>
57
- );
46
+ // Get live shortcut state
47
+ const shortcut = useShortcut(commands, "palette.open");
48
+
49
+ return (
50
+ <button
51
+ onClick={() => commands.run("palette.open")}
52
+ aria-keyshortcuts={shortcut.aria || undefined}
53
+ >
54
+ Open Palette
55
+ {shortcut.tokens[0]?.map((token, i) => (
56
+ <kbd key={i}>{token}</kbd>
57
+ ))}
58
+ </button>
59
+ );
58
60
  }
59
61
  ```
60
62
 
@@ -66,10 +68,10 @@ Use scopes to isolate commands when modals or dialogs are open.
66
68
  import { useScope } from "@inklu/keys/react";
67
69
 
68
70
  function Dialog({ isOpen }) {
69
- // When exclusive is true, commands outside this scope are silenced
70
- useScope(commands, "dialog", isOpen, { exclusive: true });
71
+ // When exclusive is true, commands outside this scope are silenced
72
+ useScope(commands, "dialog", isOpen, { exclusive: true });
71
73
 
72
- return isOpen ? <div className="dialog">...</div> : null;
74
+ return isOpen ? <div className="dialog">...</div> : null;
73
75
  }
74
76
  ```
75
77
 
@@ -82,27 +84,27 @@ Exactly one command runs per keypress. The winner is:
82
84
  1. The command in a named scope, over a global one. `"*"` counts as global here — it means "survives barriers", not "belongs to a region of the UI".
83
85
  2. Failing that, the one registered later. Registration order tracks mount order, so the most recently opened surface claims the key.
84
86
 
85
- A winner whose `when()` currently returns `false` steps aside rather than swallowing the keypress. This is how `Escape` closes the dialog when one is open and the panel otherwise, without making the dialog `exclusive`. Two commands sharing a chord *in the same scope* can never separate, so that case warns in development.
87
+ A winner whose `when()` currently returns `false` steps aside rather than swallowing the keypress. This is how `Escape` closes the dialog when one is open and the panel otherwise, without making the dialog `exclusive`. Two commands sharing a chord _in the same scope_ can never separate, so that case warns in development.
86
88
 
87
89
  ## 4. Rebinding
88
90
 
89
91
  Overrides are layered over defaults.
90
92
 
91
93
  ```ts
92
- commands.keymap.set("palette.open", "Mod+P"); // remap
94
+ commands.keymap.set("palette.open", "Mod+P"); // remap
93
95
  commands.keymap.add("palette.open", "Control+P"); // an additional key
94
- commands.keymap.set("palette.open", null); // unbind
95
- commands.keymap.reset("palette.open"); // reset to default
96
+ commands.keymap.set("palette.open", null); // unbind
97
+ commands.keymap.reset("palette.open"); // reset to default
96
98
  ```
97
99
 
98
100
  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`:
99
101
 
100
102
  ```ts
101
103
  createCommands(commands, {
102
- bindings: {
103
- "palette.open": "Mod+P", // replace the shipped default
104
- "doc.publish": null, // ship it unbound
105
- },
104
+ bindings: {
105
+ "palette.open": "Mod+P", // replace the shipped default
106
+ "doc.publish": null, // ship it unbound
107
+ },
106
108
  });
107
109
  ```
108
110
 
@@ -113,9 +115,7 @@ In React, use the `useRebind` hook for a capture flow:
113
115
  ```tsx
114
116
  const rebind = useRebind(commands, "palette.open");
115
117
 
116
- <button onClick={rebind.start}>
117
- {rebind.recording ? "Press keys…" : rebind.display}
118
- </button>;
118
+ <button onClick={rebind.start}>{rebind.recording ? "Press keys…" : rebind.display}</button>;
119
119
  ```
120
120
 
121
121
  ## API Summary
@@ -37,7 +37,7 @@ interface RebindState<Id extends string = string> {
37
37
  interface RebindOptions<Id extends string = string> {
38
38
  /** Capture mode. Defaults to `"chord"`. */
39
39
  mode?: RebindMode;
40
- /** Whether to apply the recorded binding to the keymap. Defaults to `true`. */
40
+ /** Whether to write the recorded (or cleared) binding into the keymap. Defaults to `true`. */
41
41
  apply?: boolean;
42
42
  /** Whether to append rather than replace existing bindings. Defaults to `false`. */
43
43
  add?: boolean;
@@ -51,7 +51,7 @@ interface RebindOptions<Id extends string = string> {
51
51
  onRecord?: (binding: Binding, conflicts: ReadonlyArray<Conflict<Id>>) => void;
52
52
  /** Callback fired when recording is cancelled. */
53
53
  onCancel?: () => void;
54
- /** Callback fired when bindings are cleared. */
54
+ /** Callback fired when Backspace or Delete clears the binding while recording. */
55
55
  onClear?: () => void;
56
56
  }
57
57
  /** Controller instance for managing a rebinding session. */
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { A as Platform, C as CommandDefinition, D as HotkeySequence, E as Hotkey, M as SequenceBinding, N as SerializedBinding, O as KeymapPersistence, S as CommandContext, T as Conflict, _ as formatBinding, a as Commands, b as Chord, c as PendingSequence, d as createCommands, f as CommandKeymap, g as bindingTokens, h as ariaKeyShortcuts, i as CommandId, j as ScopeOptions, k as KeymapStorage, l as RunBlockReason, m as DisplayOptions, n as observe, o as CommandsOptions, p as SerializedKeymap, r as CommandHandle, s as ConflictOptions, t as ObserveOptions, u as RunOptions, v as Binding, w as CommandSource, x as Command, y as BindingInput } from "./attributes-O6bXNKT9.js";
2
- import { a as Rebinder, c as seq, i as RebindState, l as validateBinding, n as RebindMode, o as createRebinder, r as RebindOptions, s as BindingValidation, t as ALWAYS_SCOPE } from "./index-nIJJ9qsY.js";
2
+ import { a as Rebinder, c as seq, i as RebindState, l as validateBinding, n as RebindMode, o as createRebinder, r as RebindOptions, s as BindingValidation, t as ALWAYS_SCOPE } from "./index-B5iiRwCy.js";
3
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 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, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, observe, seq, validateBinding };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { observe } from "./attributes.js";
2
- import { a as bindingTokens, c as seq, i as ariaKeyShortcuts, n as ALWAYS_SCOPE, o as formatBinding, r as createRebinder, t as createCommands, u as validateBinding } from "./src-DifrdRcK.js";
2
+ import { a as bindingTokens, c as seq, i as ariaKeyShortcuts, n as ALWAYS_SCOPE, o as formatBinding, r as createRebinder, t as createCommands, u as validateBinding } from "./src-DWmUPZSL.js";
3
3
  export { ALWAYS_SCOPE, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, observe, seq, validateBinding };
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { A as Platform, C as CommandDefinition, D as HotkeySequence, E as Hotkey, M as SequenceBinding, N as SerializedBinding, O as KeymapPersistence, S as CommandContext, T as Conflict, _ as formatBinding, a as Commands, b as Chord, c as PendingSequence, d as createCommands, f as CommandKeymap, g as bindingTokens, h as ariaKeyShortcuts, i as CommandId, j as ScopeOptions, k as KeymapStorage, l as RunBlockReason, m as DisplayOptions, n as observe, o as CommandsOptions, p as SerializedKeymap, r as CommandHandle, s as ConflictOptions, t as ObserveOptions, u as RunOptions, v as Binding, w as CommandSource, x as Command, y as BindingInput } from "./attributes-O6bXNKT9.js";
2
- import { a as Rebinder, c as seq, i as RebindState, l as validateBinding, n as RebindMode, o as createRebinder, r as RebindOptions, s as BindingValidation, t as ALWAYS_SCOPE } from "./index-nIJJ9qsY.js";
2
+ import { a as Rebinder, c as seq, i as RebindState, l as validateBinding, n as RebindMode, o as createRebinder, r as RebindOptions, s as BindingValidation, t as ALWAYS_SCOPE } from "./index-B5iiRwCy.js";
3
3
  //#region src/react/hooks.d.ts
4
4
  /**
5
5
  * Mounts a commands registry for the lifecycle of the component.
package/dist/react.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { observe } from "./attributes.js";
3
- import { a as bindingTokens, c as seq, i as ariaKeyShortcuts, l as toBindings, n as ALWAYS_SCOPE, o as formatBinding, r as createRebinder, s as bindingSignature, t as createCommands, u as validateBinding } from "./src-DifrdRcK.js";
3
+ import { a as bindingTokens, c as seq, i as ariaKeyShortcuts, l as toBindings, n as ALWAYS_SCOPE, o as formatBinding, r as createRebinder, s as bindingSignature, t as createCommands, u as validateBinding } from "./src-DWmUPZSL.js";
4
4
  import { getKeyStateTracker } from "@tanstack/hotkeys";
5
5
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
6
  //#region src/react/hooks.ts
@@ -192,6 +192,7 @@ function createRebinder(registry, id, options = {}) {
192
192
  };
193
193
  const cleared = () => {
194
194
  releaseScope();
195
+ if (options.apply !== false) registry.keymap.set(id, null);
195
196
  notify({
196
197
  recording: false,
197
198
  binding: null,
@@ -212,7 +213,9 @@ function createRebinder(registry, id, options = {}) {
212
213
  onClear: cleared
213
214
  }) : new HotkeyRecorder({
214
215
  ignoreInputs: options.ignoreInputs ?? true,
215
- onRecord: (hotkey) => record(hotkey),
216
+ onRecord: (hotkey) => {
217
+ if (hotkey) record(hotkey);
218
+ },
216
219
  onCancel: cancelled,
217
220
  onClear: cleared
218
221
  });
@@ -468,8 +471,10 @@ var Keymap = class {
468
471
  const raw = storage.getItem(this.#storageKey());
469
472
  if (!raw) return;
470
473
  this.#read(JSON.parse(raw));
471
- for (const listener of this.#listeners) listener();
472
- } catch {}
474
+ } catch {
475
+ return;
476
+ }
477
+ for (const listener of this.#listeners) listener();
473
478
  }
474
479
  /** Subscribes to keymap changes. Returns an unsubscribe function. */
475
480
  subscribe(listener) {
@@ -666,9 +671,14 @@ var CommandRegistry = class {
666
671
  if (this.#mounts > 0) this.#binder.sync(id);
667
672
  this.#invalidate();
668
673
  this.#auditAdded(id);
674
+ let disposed = false;
669
675
  return {
670
- run: (options) => this.run(id, options),
671
- dispose: () => this.#pop(id, entry)
676
+ run: (options) => !disposed && this.run(id, options),
677
+ dispose: () => {
678
+ if (disposed) return;
679
+ disposed = true;
680
+ this.#pop(id, entry);
681
+ }
672
682
  };
673
683
  }
674
684
  remove(loose) {
package/package.json CHANGED
@@ -1,22 +1,37 @@
1
1
  {
2
2
  "name": "@inklu/keys",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
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
- "keyboard",
7
- "shortcuts",
8
- "hotkeys",
9
- "keybindings",
6
+ "accessibility",
10
7
  "command-palette",
11
8
  "commands",
9
+ "hotkeys",
10
+ "keybindings",
11
+ "keyboard",
12
+ "react",
12
13
  "scopes",
13
14
  "sequences",
14
- "accessibility",
15
- "react"
15
+ "shortcuts"
16
16
  ],
17
+ "homepage": "https://github.com/nkurunziza-saddy/inklu#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/nkurunziza-saddy/inklu/issues"
20
+ },
17
21
  "license": "MIT",
18
22
  "author": "nkurunziza-saddy",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/nkurunziza-saddy/inklu.git",
26
+ "directory": "packages/keys"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
19
33
  "type": "module",
34
+ "sideEffects": false,
20
35
  "main": "./dist/index.js",
21
36
  "module": "./dist/index.js",
22
37
  "types": "./dist/index.d.ts",
@@ -38,61 +53,47 @@
38
53
  },
39
54
  "./package.json": "./package.json"
40
55
  },
41
- "sideEffects": false,
42
- "files": [
43
- "dist",
44
- "README.md",
45
- "LICENSE"
46
- ],
47
- "engines": {
48
- "node": ">=18"
56
+ "publishConfig": {
57
+ "access": "public"
49
58
  },
50
59
  "dependencies": {
51
60
  "@tanstack/hotkeys": "0.8.0"
52
61
  },
53
- "peerDependencies": {
54
- "react": "^18.0.0 || ^19.0.0"
55
- },
56
- "peerDependenciesMeta": {
57
- "react": {
58
- "optional": true
59
- }
60
- },
61
62
  "devDependencies": {
63
+ "@inklu/tsconfig": "1.0.0",
62
64
  "@testing-library/react": "^16.3.2",
63
65
  "@types/react": "^19",
64
- "@vitest/coverage-v8": "^4.1.10",
66
+ "@vitest/coverage-v8": "4.1.11",
65
67
  "jsdom": "^29.1.1",
66
68
  "react": "19.2.8",
67
69
  "react-dom": "19.2.8",
68
- "tsdown": "^0.22.14",
69
70
  "typescript": "^5.8.2",
70
- "vitest": "^4.1.10",
71
- "@inklu/tsconfig": "1.0.0"
71
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.3.0",
72
+ "vite-plus": "0.3.0",
73
+ "vitest": "4.1.11"
72
74
  },
73
- "repository": {
74
- "type": "git",
75
- "url": "git+https://github.com/nkurunziza-saddy/inklu.git",
76
- "directory": "packages/keys"
75
+ "peerDependencies": {
76
+ "react": "^18.0.0 || ^19.0.0"
77
77
  },
78
- "homepage": "https://github.com/nkurunziza-saddy/inklu#readme",
79
- "bugs": {
80
- "url": "https://github.com/nkurunziza-saddy/inklu/issues"
78
+ "peerDependenciesMeta": {
79
+ "react": {
80
+ "optional": true
81
+ }
81
82
  },
82
- "publishConfig": {
83
- "access": "public"
83
+ "engines": {
84
+ "node": ">=18"
84
85
  },
85
86
  "scripts": {
86
- "build": "tsdown",
87
+ "build": "vp pack",
87
88
  "lint:exports": "publint",
88
- "dev": "tsdown --watch",
89
- "lint": "biome lint",
90
- "check": "biome check",
91
- "format": "biome format --write",
89
+ "dev": "vp pack --watch",
90
+ "lint": "vp lint",
91
+ "check": "vp check",
92
+ "format": "vp fmt --write",
92
93
  "check-types": "tsc --noEmit",
93
94
  "typecheck": "tsc --noEmit",
94
- "test": "vitest run",
95
- "test:watch": "vitest",
96
- "test:coverage": "vitest run --coverage"
95
+ "test": "vp test run",
96
+ "test:watch": "vp test",
97
+ "test:coverage": "vp test run --coverage"
97
98
  }
98
99
  }