@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 +39 -39
- package/dist/{index-nIJJ9qsY.d.ts → index-B5iiRwCy.d.ts} +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{src-DifrdRcK.js → src-DWmUPZSL.js} +15 -5
- package/package.json +45 -44
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
// Starts listening in an effect
|
|
41
|
+
useMountCommands(commands);
|
|
42
|
+
return <ShortcutButton />;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function ShortcutButton() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
+
// When exclusive is true, commands outside this scope are silenced
|
|
72
|
+
useScope(commands, "dialog", isOpen, { exclusive: true });
|
|
71
73
|
|
|
72
|
-
|
|
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
|
|
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");
|
|
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);
|
|
95
|
-
commands.keymap.reset("palette.open");
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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) =>
|
|
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
|
-
|
|
472
|
-
|
|
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: () =>
|
|
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
|
+
"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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
42
|
-
|
|
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": "
|
|
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
|
-
"
|
|
71
|
-
"
|
|
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
|
-
"
|
|
74
|
-
"
|
|
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
|
-
"
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
"peerDependenciesMeta": {
|
|
79
|
+
"react": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
81
82
|
},
|
|
82
|
-
"
|
|
83
|
-
"
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": ">=18"
|
|
84
85
|
},
|
|
85
86
|
"scripts": {
|
|
86
|
-
"build": "
|
|
87
|
+
"build": "vp pack",
|
|
87
88
|
"lint:exports": "publint",
|
|
88
|
-
"dev": "
|
|
89
|
-
"lint": "
|
|
90
|
-
"check": "
|
|
91
|
-
"format": "
|
|
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": "
|
|
95
|
-
"test:watch": "
|
|
96
|
-
"test:coverage": "
|
|
95
|
+
"test": "vp test run",
|
|
96
|
+
"test:watch": "vp test",
|
|
97
|
+
"test:coverage": "vp test run --coverage"
|
|
97
98
|
}
|
|
98
99
|
}
|