@inklu/keys 1.0.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.
@@ -0,0 +1,93 @@
1
+ import { M as SequenceBinding, T as Conflict, a as Commands, b as Chord, v as Binding } from "./attributes-lCPeoWII.js";
2
+ import { Hotkey } from "@tanstack/hotkeys";
3
+ //#region src/bindings.d.ts
4
+ /**
5
+ * Creates a sequence binding from ordered hotkey steps.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * keys: seq("G", "I")
10
+ * ```
11
+ */
12
+ declare function seq(...steps: Array<Hotkey>): SequenceBinding;
13
+ /** Validation result for a hotkey binding. */
14
+ interface BindingValidation {
15
+ readonly valid: boolean;
16
+ readonly errors: ReadonlyArray<string>;
17
+ readonly warnings: ReadonlyArray<string>;
18
+ }
19
+ /** Validates a binding's syntax without logging warnings or errors. */
20
+ declare function validateBinding(binding: Binding): BindingValidation;
21
+ //#endregion
22
+ //#region src/rebind.d.ts
23
+ /** Hotkey capture mode: single `"chord"` or multi-step `"sequence"`. */
24
+ type RebindMode = "chord" | "sequence";
25
+ /** Current state of a rebinding session. */
26
+ interface RebindState<Id extends string = string> {
27
+ /** Whether keys are actively being recorded. */
28
+ readonly recording: boolean;
29
+ /** Most recently captured binding. */
30
+ readonly binding: Binding | null;
31
+ /** Captured steps in the current sequence session. */
32
+ readonly steps: ReadonlyArray<Chord>;
33
+ /** Conflicting commands for the captured binding. */
34
+ readonly conflicts: ReadonlyArray<Conflict<Id>>;
35
+ }
36
+ /** Options for creating a rebinder. */
37
+ interface RebindOptions<Id extends string = string> {
38
+ /** Capture mode. Defaults to `"chord"`. */
39
+ mode?: RebindMode;
40
+ /** Whether to apply the recorded binding to the keymap. Defaults to `true`. */
41
+ apply?: boolean;
42
+ /** Whether to append rather than replace existing bindings. Defaults to `false`. */
43
+ add?: boolean;
44
+ /** Prevent applying bindings that conflict with other commands. Defaults to `false`. */
45
+ rejectConflicts?: boolean;
46
+ /** Ignore key events from inputs while recording. Defaults to `true`. */
47
+ ignoreInputs?: boolean;
48
+ /** Inactivity timeout in milliseconds before a sequence commits. */
49
+ idleTimeout?: number;
50
+ /** Callback fired when a binding is recorded. */
51
+ onRecord?: (binding: Binding, conflicts: ReadonlyArray<Conflict<Id>>) => void;
52
+ /** Callback fired when recording is cancelled. */
53
+ onCancel?: () => void;
54
+ /** Callback fired when bindings are cleared. */
55
+ onClear?: () => void;
56
+ }
57
+ /** Controller instance for managing a rebinding session. */
58
+ interface Rebinder<Id extends string = string> {
59
+ /** Begins capturing keys. */
60
+ start: () => void;
61
+ /** Stops capturing keys without recording. */
62
+ stop: () => void;
63
+ /** Stops capturing keys and reports cancellation. */
64
+ cancel: () => void;
65
+ /** Commits the currently captured sequence steps. */
66
+ commit: () => void;
67
+ /** Current rebinder state. */
68
+ readonly state: RebindState<Id>;
69
+ /** Subscribes to rebinder state changes. Returns an unsubscribe function. */
70
+ subscribe: (listener: () => void) => () => void;
71
+ /** Destroys the rebinder and cleans up listeners. */
72
+ destroy: () => void;
73
+ }
74
+ /**
75
+ * Creates a rebinder instance for capturing and remapping a command's keys.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * const rebinder = createRebinder(commands, "palette.open", {
80
+ * onRecord: (binding, conflicts) => {
81
+ * if (conflicts.length) console.warn("Conflict with", conflicts[0].id);
82
+ * },
83
+ * });
84
+ * rebinder.start();
85
+ * ```
86
+ */
87
+ declare function createRebinder<Id extends string>(registry: Commands<Id>, id: Id, options?: RebindOptions<Id>): Rebinder<Id>;
88
+ //#endregion
89
+ //#region src/scopes.d.ts
90
+ /** Wildcard scope identifier for commands that remain active across exclusive scopes. */
91
+ declare const ALWAYS_SCOPE = "*";
92
+ //#endregion
93
+ export { Rebinder as a, seq as c, RebindState as i, validateBinding as l, RebindMode as n, createRebinder as o, RebindOptions as r, BindingValidation as s, ALWAYS_SCOPE as t };
@@ -0,0 +1,3 @@
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-lCPeoWII.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-WDNLK0dX.js";
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 ADDED
@@ -0,0 +1,3 @@
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-B305icXC.js";
3
+ export { ALWAYS_SCOPE, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, observe, seq, validateBinding };
@@ -0,0 +1,77 @@
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-lCPeoWII.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-WDNLK0dX.js";
3
+ //#region src/react/hooks.d.ts
4
+ /**
5
+ * Mounts a commands registry for the lifecycle of the component.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * function App() {
10
+ * useMountCommands(commands);
11
+ * return <Shortcuts />;
12
+ * }
13
+ * ```
14
+ */
15
+ declare function useMountCommands<Id extends string>(registry: Commands<Id>): void;
16
+ /** Returns all registered commands, re-rendering when the registry changes. */
17
+ declare function useCommands<Id extends string>(registry: Commands<Id>): ReadonlyArray<Command<Id>>;
18
+ /** Returns a specific command snapshot by ID. */
19
+ declare function useCommand<Id extends string>(registry: Commands<Id>, id: NoInfer<Id>): Command<Id> | undefined;
20
+ /** Returns display and accessibility shortcuts for a command. */
21
+ declare function useShortcut<Id extends string>(registry: Commands<Id>, id: NoInfer<Id>): {
22
+ display: string;
23
+ tokens: ReadonlyArray<string>;
24
+ aria: string;
25
+ };
26
+ /**
27
+ * Activates a scope for the lifecycle of the component.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * useScope(commands, "dialog", isOpen, { exclusive: true });
32
+ * ```
33
+ */
34
+ declare function useScope<Id extends string>(registry: Commands<Id>, name: string, active?: boolean, options?: ScopeOptions): void;
35
+ /** Registers a dynamic command for the lifecycle of the component. */
36
+ declare function useRegisterCommand<Id extends string>(registry: Commands<Id>, id: NoInfer<Id> | (string & {}), definition: CommandDefinition): void;
37
+ /** State and actions for rebinding a command. */
38
+ interface UseRebindResult<Id extends string = string> extends RebindState<Id> {
39
+ /** Active display string for the shortcut. */
40
+ display: string;
41
+ /** Display tokens for rendering `<kbd>` elements. */
42
+ tokens: ReadonlyArray<string>;
43
+ /** Whether the shortcut has been customized by the user. */
44
+ overridden: boolean;
45
+ start: () => void;
46
+ stop: () => void;
47
+ cancel: () => void;
48
+ commit: () => void;
49
+ clear: () => void;
50
+ reset: () => void;
51
+ }
52
+ /** Hook for recording and configuring a command's keybindings. */
53
+ declare function useRebind<Id extends string>(registry: Commands<Id>, id: NoInfer<Id>, options?: RebindOptions<Id>): UseRebindResult<Id>;
54
+ /** Returns an array of keys currently held down. */
55
+ declare function useHeldKeys(): ReadonlyArray<string>;
56
+ /**
57
+ * Checks whether a specific key is currently held down.
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * const shift = useKeyHold("Shift");
62
+ * ```
63
+ */
64
+ declare function useKeyHold(key: string): boolean;
65
+ /** Returns a map of held key identifiers to physical event codes. */
66
+ declare function useHeldKeyCodes(): Readonly<Record<string, string>>;
67
+ /**
68
+ * Returns currently active partially-typed sequences.
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * const pending = usePendingSequences(commands);
73
+ * ```
74
+ */
75
+ declare function usePendingSequences<Id extends string>(registry: Commands<Id>): ReadonlyArray<PendingSequence<Id>>;
76
+ //#endregion
77
+ 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, type UseRebindResult, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, observe, seq, useCommand, useCommands, useHeldKeyCodes, useHeldKeys, useKeyHold, useMountCommands, usePendingSequences, useRebind, useRegisterCommand, useScope, useShortcut, validateBinding };
package/dist/react.js ADDED
@@ -0,0 +1,214 @@
1
+ "use client";
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-B305icXC.js";
4
+ import { getKeyStateTracker } from "@tanstack/hotkeys";
5
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
+ //#region src/react/hooks.ts
7
+ const EMPTY_TOKENS = [];
8
+ const EMPTY_CODES = {};
9
+ const EMPTY_PENDING = [];
10
+ const getEmptyTokens = () => EMPTY_TOKENS;
11
+ const getEmptyCodes = () => EMPTY_CODES;
12
+ const getEmptyPending = () => EMPTY_PENDING;
13
+ const getFalse = () => false;
14
+ /**
15
+ * Mounts a commands registry for the lifecycle of the component.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * function App() {
20
+ * useMountCommands(commands);
21
+ * return <Shortcuts />;
22
+ * }
23
+ * ```
24
+ */
25
+ function useMountCommands(registry) {
26
+ useEffect(() => registry.mount(), [registry]);
27
+ }
28
+ function useCommandList(registry) {
29
+ const subscribe = useCallback((onChange) => registry.subscribe(onChange), [registry]);
30
+ const getSnapshot = useCallback(() => registry.list(), [registry]);
31
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
32
+ }
33
+ /** Returns all registered commands, re-rendering when the registry changes. */
34
+ function useCommands(registry) {
35
+ return useCommandList(registry);
36
+ }
37
+ /** Returns a specific command snapshot by ID. */
38
+ function useCommand(registry, id) {
39
+ const commands = useCommandList(registry);
40
+ return useMemo(() => registry.get(id), [
41
+ registry,
42
+ commands,
43
+ id
44
+ ]);
45
+ }
46
+ /** Returns display and accessibility shortcuts for a command. */
47
+ function useShortcut(registry, id) {
48
+ const command = useCommand(registry, id);
49
+ return useMemo(() => ({
50
+ display: command?.display[0] ?? "",
51
+ tokens: command?.tokens[0] ?? EMPTY_TOKENS,
52
+ aria: command?.aria ?? ""
53
+ }), [command]);
54
+ }
55
+ /**
56
+ * Activates a scope for the lifecycle of the component.
57
+ *
58
+ * @example
59
+ * ```tsx
60
+ * useScope(commands, "dialog", isOpen, { exclusive: true });
61
+ * ```
62
+ */
63
+ function useScope(registry, name, active = true, options = {}) {
64
+ const exclusive = options.exclusive === true;
65
+ useEffect(() => {
66
+ if (!active) return;
67
+ return registry.pushScope(name, { exclusive });
68
+ }, [
69
+ registry,
70
+ name,
71
+ active,
72
+ exclusive
73
+ ]);
74
+ }
75
+ /** Registers a dynamic command for the lifecycle of the component. */
76
+ function useRegisterCommand(registry, id, definition) {
77
+ const latest = useRef(definition);
78
+ latest.current = definition;
79
+ const signature = useMemo(() => definitionSignature(definition), [definition]);
80
+ useEffect(() => {
81
+ const handle = registry.add(id, {
82
+ ...latest.current,
83
+ run: (context) => latest.current.run(context),
84
+ when: () => latest.current.when?.() !== false
85
+ });
86
+ return () => handle.dispose();
87
+ }, [
88
+ registry,
89
+ id,
90
+ signature
91
+ ]);
92
+ }
93
+ function definitionSignature(definition) {
94
+ return [
95
+ toBindings(definition.keys).map((binding) => bindingSignature(binding)).join("|"),
96
+ definition.scope ?? "",
97
+ definition.title,
98
+ definition.enabled === false ? "0" : "1",
99
+ definition.preventDefault === false ? "0" : "1",
100
+ definition.ignoreInputs === void 0 ? "" : String(definition.ignoreInputs),
101
+ definition.requireReset === true ? "1" : "0",
102
+ definition.sequenceTimeout === void 0 ? "" : String(definition.sequenceTimeout)
103
+ ].join(" ");
104
+ }
105
+ const IDLE_REBIND = {
106
+ recording: false,
107
+ binding: null,
108
+ steps: [],
109
+ conflicts: []
110
+ };
111
+ /** Hook for recording and configuring a command's keybindings. */
112
+ function useRebind(registry, id, options = {}) {
113
+ const latest = useRef(options);
114
+ latest.current = options;
115
+ const command = useCommand(registry, id);
116
+ const [rebinder, setRebinder] = useState(null);
117
+ const [state, setState] = useState(IDLE_REBIND);
118
+ const mode = options.mode ?? "chord";
119
+ const idleTimeout = options.idleTimeout;
120
+ useEffect(() => {
121
+ const instance = createRebinder(registry, id, {
122
+ mode,
123
+ idleTimeout,
124
+ get apply() {
125
+ return latest.current.apply;
126
+ },
127
+ get add() {
128
+ return latest.current.add;
129
+ },
130
+ get rejectConflicts() {
131
+ return latest.current.rejectConflicts;
132
+ },
133
+ get ignoreInputs() {
134
+ return latest.current.ignoreInputs;
135
+ },
136
+ onRecord: (binding, conflicts) => latest.current.onRecord?.(binding, conflicts),
137
+ onCancel: () => latest.current.onCancel?.(),
138
+ onClear: () => latest.current.onClear?.()
139
+ });
140
+ const unsubscribe = instance.subscribe(() => setState(instance.state));
141
+ setRebinder(instance);
142
+ return () => {
143
+ unsubscribe();
144
+ instance.destroy();
145
+ setRebinder(null);
146
+ setState(IDLE_REBIND);
147
+ };
148
+ }, [
149
+ registry,
150
+ id,
151
+ mode,
152
+ idleTimeout
153
+ ]);
154
+ return useMemo(() => ({
155
+ ...state,
156
+ display: command?.display[0] ?? "",
157
+ tokens: command?.tokens[0] ?? EMPTY_TOKENS,
158
+ overridden: command?.overridden ?? false,
159
+ start: () => rebinder?.start(),
160
+ stop: () => rebinder?.stop(),
161
+ cancel: () => rebinder?.cancel(),
162
+ commit: () => rebinder?.commit(),
163
+ clear: () => registry.keymap.set(id, null),
164
+ reset: () => registry.keymap.reset(id)
165
+ }), [
166
+ state,
167
+ command,
168
+ rebinder,
169
+ registry,
170
+ id
171
+ ]);
172
+ }
173
+ function useKeyState() {
174
+ return useCallback((onChange) => {
175
+ const subscription = getKeyStateTracker().store.subscribe(onChange);
176
+ return () => subscription.unsubscribe();
177
+ }, []);
178
+ }
179
+ /** Returns an array of keys currently held down. */
180
+ function useHeldKeys() {
181
+ return useSyncExternalStore(useKeyState(), () => getKeyStateTracker().store.state.heldKeys, getEmptyTokens);
182
+ }
183
+ /**
184
+ * Checks whether a specific key is currently held down.
185
+ *
186
+ * @example
187
+ * ```tsx
188
+ * const shift = useKeyHold("Shift");
189
+ * ```
190
+ */
191
+ function useKeyHold(key) {
192
+ const subscribe = useKeyState();
193
+ const getSnapshot = useCallback(() => getKeyStateTracker().isKeyHeld(key), [key]);
194
+ return useSyncExternalStore(subscribe, getSnapshot, getFalse);
195
+ }
196
+ /** Returns a map of held key identifiers to physical event codes. */
197
+ function useHeldKeyCodes() {
198
+ return useSyncExternalStore(useKeyState(), () => getKeyStateTracker().store.state.heldCodes, getEmptyCodes);
199
+ }
200
+ /**
201
+ * Returns currently active partially-typed sequences.
202
+ *
203
+ * @example
204
+ * ```tsx
205
+ * const pending = usePendingSequences(commands);
206
+ * ```
207
+ */
208
+ function usePendingSequences(registry) {
209
+ const subscribe = useCallback((onChange) => registry.subscribePending(onChange), [registry]);
210
+ const getSnapshot = useCallback(() => registry.pending(), [registry]);
211
+ return useSyncExternalStore(subscribe, getSnapshot, getEmptyPending);
212
+ }
213
+ //#endregion
214
+ export { ALWAYS_SCOPE, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, observe, seq, useCommand, useCommands, useHeldKeyCodes, useHeldKeys, useKeyHold, useMountCommands, usePendingSequences, useRebind, useRegisterCommand, useScope, useShortcut, validateBinding };