@tanstack/solid-hotkeys 0.12.1 → 0.14.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.
package/README.md CHANGED
@@ -67,6 +67,9 @@ Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objec
67
67
  > [!NOTE]
68
68
  > You may know **TanStack Hotkeys** by our adapter names, too!
69
69
  >
70
+ > - [**Alpine Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/alpine/quick-start)
71
+ > - [**Ember Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/ember/quick-start)
72
+ > - [**Octane Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/octane/quick-start)
70
73
  > - [**React Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/react/react-hotkeys)
71
74
  > - [**Preact Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/preact/preact-hotkeys)
72
75
  > - [**Solid Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/solid/reference)
@@ -1,21 +1,52 @@
1
- import { CreateHotkeyOptions } from "./createHotkey.js";
2
- import { CreateHotkeySequenceOptions } from "./createHotkeySequence.js";
3
- import { HotkeyRecorderOptions, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
1
+ import { CreateHotkeyCommonOptions } from "./createHotkey.js";
2
+ import { AppHotkeys, AppHotkeysStore, CustomHotkeys, HotkeyChordRecorderOptions, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
4
3
  import { JSX, ParentComponent } from "solid-js";
5
4
  //#region src/HotkeysProvider.d.ts
6
5
  export interface HotkeysProviderOptions {
7
- hotkey?: Partial<CreateHotkeyOptions>;
8
- hotkeyRecorder?: Partial<HotkeyRecorderOptions>;
9
- hotkeySequenceRecorder?: Partial<HotkeySequenceRecorderOptions>;
10
- hotkeySequence?: Partial<CreateHotkeySequenceOptions>;
6
+ /**
7
+ * Defaults for every hotkey registered by `createHotkey` and `createHotkeys`.
8
+ * `requireReset` applies to chords and `timeout` applies to sequences.
9
+ */
10
+ hotkey?: CreateHotkeyCommonOptions;
11
+ /** Defaults for every `createHotkeyRecorder`. */
12
+ hotkeyRecorder?: Partial<Omit<HotkeyChordRecorderOptions & HotkeySequenceRecorderOptions, 'maxSteps' | 'onRecord' | 'validate'>>;
11
13
  }
12
14
  interface HotkeysContextValue {
13
- defaultOptions: HotkeysProviderOptions;
15
+ readonly appHotkeysStore: AppHotkeysStore | null;
16
+ readonly defaultOptions: HotkeysProviderOptions;
14
17
  }
15
18
  export interface HotkeysProviderProps {
16
19
  children: JSX.Element;
20
+ /**
21
+ * Every app hotkey, from `defineAppHotkeys`. Read when the provider is
22
+ * created; listed by `createAppHotkeyList` whether or not they are registered.
23
+ */
24
+ appHotkeys?: AppHotkeys;
25
+ /**
26
+ * Starting custom hotkeys, such as saved data. Read once; the provider owns
27
+ * the state afterwards. Invalid entries are ignored with a warning.
28
+ */
29
+ customHotkeys?: CustomHotkeys | null;
30
+ /** Called with the custom hotkeys after every change, ready to save. */
31
+ onCustomHotkeysChange?: (customHotkeys: CustomHotkeys) => void;
17
32
  defaultOptions?: HotkeysProviderOptions;
18
33
  }
34
+ /**
35
+ * Provides default options, app hotkeys, and the user's custom hotkeys.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * <HotkeysProvider
40
+ * appHotkeys={appHotkeys}
41
+ * customHotkeys={JSON.parse(localStorage.getItem('hotkeys') ?? '{}')}
42
+ * onCustomHotkeysChange={(custom) =>
43
+ * localStorage.setItem('hotkeys', JSON.stringify(custom))
44
+ * }
45
+ * >
46
+ * <App />
47
+ * </HotkeysProvider>
48
+ * ```
49
+ */
19
50
  export declare const HotkeysProvider: ParentComponent<HotkeysProviderProps>;
20
51
  export declare function useHotkeysContext(): HotkeysContextValue | null;
21
52
  export declare function useDefaultHotkeysOptions(): HotkeysProviderOptions;
@@ -1,11 +1,43 @@
1
+ import { AppHotkeysStore, publishAppHotkeysStore } from "@tanstack/hotkeys";
1
2
  import { createComponent } from "solid-js/web";
2
- import { createContext, useContext } from "solid-js";
3
+ import { createContext, createEffect, onCleanup, onMount, useContext } from "solid-js";
3
4
 
4
5
  //#region src/HotkeysProvider.tsx
5
6
  const HotkeysContext = createContext(null);
6
7
  const DEFAULT_OPTIONS = {};
8
+ /**
9
+ * Provides default options, app hotkeys, and the user's custom hotkeys.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * <HotkeysProvider
14
+ * appHotkeys={appHotkeys}
15
+ * customHotkeys={JSON.parse(localStorage.getItem('hotkeys') ?? '{}')}
16
+ * onCustomHotkeysChange={(custom) =>
17
+ * localStorage.setItem('hotkeys', JSON.stringify(custom))
18
+ * }
19
+ * >
20
+ * <App />
21
+ * </HotkeysProvider>
22
+ * ```
23
+ */
7
24
  const HotkeysProvider = (props) => {
8
- const contextValue = { defaultOptions: props.defaultOptions ?? DEFAULT_OPTIONS };
25
+ const store = props.appHotkeys ? new AppHotkeysStore(props.appHotkeys, {
26
+ customHotkeys: props.customHotkeys,
27
+ onCustomHotkeysChange: (custom) => props.onCustomHotkeysChange?.(custom)
28
+ }) : null;
29
+ createEffect(() => {
30
+ if (props.appHotkeys) store?.setAppHotkeys(props.appHotkeys);
31
+ });
32
+ onMount(() => {
33
+ if (store) onCleanup(publishAppHotkeysStore(store));
34
+ });
35
+ const contextValue = {
36
+ appHotkeysStore: store,
37
+ get defaultOptions() {
38
+ return props.defaultOptions ?? DEFAULT_OPTIONS;
39
+ }
40
+ };
9
41
  return createComponent(HotkeysContext.Provider, {
10
42
  value: contextValue,
11
43
  get children() {
@@ -17,7 +49,7 @@ function useHotkeysContext() {
17
49
  return useContext(HotkeysContext);
18
50
  }
19
51
  function useDefaultHotkeysOptions() {
20
- return useContext(HotkeysContext)?.defaultOptions ?? {};
52
+ return useContext(HotkeysContext)?.defaultOptions ?? DEFAULT_OPTIONS;
21
53
  }
22
54
 
23
55
  //#endregion
@@ -0,0 +1,42 @@
1
+ import { AppHotkey, AppHotkeyListItem, CustomHotkeys, Hotkey, RegisteredAppHotkeys } from "@tanstack/hotkeys";
2
+ //#region src/appHotkeys.d.ts
3
+ /**
4
+ * Returns an accessor for the effective app hotkeys from `HotkeysProvider`,
5
+ * typed by `Register`: each declaration with the user's custom hotkey applied.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const hotkeys = useAppHotkeys()
10
+ * createHotkey(() => hotkeys()['doc.save'], () => save())
11
+ * ```
12
+ */
13
+ export declare function useAppHotkeys(): () => RegisteredAppHotkeys;
14
+ /**
15
+ * Lists every app hotkey with its live status, in declaration order, for
16
+ * cheat sheets and settings screens. App hotkeys whose components have not
17
+ * mounted are `'inactive'`; those without a hotkey are `'unbound'`.
18
+ */
19
+ export declare function createAppHotkeyList(): () => Array<AppHotkeyListItem<RegisteredAppHotkeys[keyof RegisteredAppHotkeys]>>;
20
+ /** Custom hotkey state and actions returned by `useCustomHotkeys`. */
21
+ export interface UseCustomHotkeysResult {
22
+ /** The user's hotkeys that differ from the defaults. */
23
+ customHotkeys: () => CustomHotkeys;
24
+ /**
25
+ * Sets the user's hotkey for an app hotkey. `null` unbinds it.
26
+ * Returns conflicting app hotkeys; when any exist, nothing changes.
27
+ */
28
+ setCustomHotkey: (id: keyof RegisteredAppHotkeys & string, hotkey: Hotkey | null) => Array<AppHotkey>;
29
+ /** Restores the default hotkey for an app hotkey. */
30
+ resetCustomHotkey: (id: keyof RegisteredAppHotkeys & string) => void;
31
+ /** Restores every default hotkey. */
32
+ resetCustomHotkeys: () => void;
33
+ /** Other app hotkeys that would collide if `id` used `hotkey`. */
34
+ findConflicts: (id: keyof RegisteredAppHotkeys & string, hotkey: Hotkey) => Array<AppHotkey>;
35
+ }
36
+ /**
37
+ * Reads and changes the user's custom hotkeys. Changes apply to every
38
+ * registered app hotkey immediately and are passed to the provider's
39
+ * `onCustomHotkeysChange`, ready to save.
40
+ */
41
+ export declare function useCustomHotkeys(): UseCustomHotkeysResult;
42
+ //#endregion
@@ -0,0 +1,64 @@
1
+ import { useAppHotkeysStoreSource } from "./defaultOptions.js";
2
+ import { createHotkeyRegistrations } from "./createHotkeyRegistrations.js";
3
+ import { createMemo } from "solid-js";
4
+
5
+ //#region src/appHotkeys.ts
6
+ function useRequiredStore(name) {
7
+ const source = useAppHotkeysStoreSource();
8
+ if (!source.store) throw new Error(`${name} requires a HotkeysProvider with an appHotkeys prop`);
9
+ return {
10
+ store: source.store,
11
+ state: source.state
12
+ };
13
+ }
14
+ /**
15
+ * Returns an accessor for the effective app hotkeys from `HotkeysProvider`,
16
+ * typed by `Register`: each declaration with the user's custom hotkey applied.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * const hotkeys = useAppHotkeys()
21
+ * createHotkey(() => hotkeys()['doc.save'], () => save())
22
+ * ```
23
+ */
24
+ function useAppHotkeys() {
25
+ const { store, state } = useRequiredStore("useAppHotkeys");
26
+ return createMemo(() => {
27
+ state();
28
+ return store.appHotkeys;
29
+ });
30
+ }
31
+ /**
32
+ * Lists every app hotkey with its live status, in declaration order, for
33
+ * cheat sheets and settings screens. App hotkeys whose components have not
34
+ * mounted are `'inactive'`; those without a hotkey are `'unbound'`.
35
+ */
36
+ function createAppHotkeyList() {
37
+ const { store, state } = useRequiredStore("createAppHotkeyList");
38
+ const registrations = createHotkeyRegistrations();
39
+ return createMemo(() => {
40
+ state();
41
+ return store.getList(registrations());
42
+ });
43
+ }
44
+ /**
45
+ * Reads and changes the user's custom hotkeys. Changes apply to every
46
+ * registered app hotkey immediately and are passed to the provider's
47
+ * `onCustomHotkeysChange`, ready to save.
48
+ */
49
+ function useCustomHotkeys() {
50
+ const { store, state } = useRequiredStore("useCustomHotkeys");
51
+ return {
52
+ customHotkeys: createMemo(() => {
53
+ state();
54
+ return store.customHotkeys;
55
+ }),
56
+ setCustomHotkey: (id, hotkey) => store.setCustomHotkey(id, hotkey),
57
+ resetCustomHotkey: (id) => store.resetCustomHotkey(id),
58
+ resetCustomHotkeys: () => store.resetCustomHotkeys(),
59
+ findConflicts: (id, hotkey) => store.findConflicts(id, hotkey)
60
+ };
61
+ }
62
+
63
+ //#endregion
64
+ export { createAppHotkeyList, useAppHotkeys, useCustomHotkeys };
@@ -1,69 +1,42 @@
1
- import { HotkeyCallback, HotkeyOptions, RegisterableHotkey } from "@tanstack/hotkeys";
1
+ import { AppHotkey, HotkeyCallback, RegisterableHotkey, Target } from "@tanstack/hotkeys";
2
+ import { AdapterAppHotkeyOptions, AdapterChordOptions, AdapterCommonOptions, AdapterHotkeyOptions, AdapterOptionsFor, AdapterSequenceOptions } from "@tanstack/hotkeys/adapter";
2
3
  //#region src/createHotkey.d.ts
3
- export interface CreateHotkeyOptions extends Omit<HotkeyOptions, 'target'> {
4
- /**
5
- * The DOM element to attach the event listener to.
6
- * Can be a direct DOM element, an accessor (for reactive targets that become
7
- * available after mount), or null. Defaults to document.
8
- * When using scoped targets, pass an accessor: () => ({ target: elementSignal() })
9
- * so the hotkey waits for the element to be attached before registering.
10
- */
11
- target?: HTMLElement | Document | Window | null;
12
- }
13
4
  /**
14
- * SolidJS primitive for registering a keyboard hotkey.
15
- *
16
- * Uses the singleton HotkeyManager for efficient event handling.
17
- * The callback receives both the keyboard event and a context object
18
- * containing the hotkey string and parsed hotkey.
19
- *
20
- * This primitive automatically tracks reactive dependencies and updates
21
- * the registration when options or the callback change.
22
- *
23
- * @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)
24
- * @param callback - The function to call when the hotkey is pressed
25
- * @param options - Options for the hotkey behavior
26
- *
27
- * @example
28
- * ```tsx
29
- * function SaveButton() {
30
- * const [count, setCount] = createSignal(0)
31
- *
32
- * // Callback always has access to latest count value
33
- * createHotkey('Mod+S', (event, { hotkey }) => {
34
- * console.log(`Save triggered, count is ${count()}`)
35
- * handleSave()
36
- * })
37
- *
38
- * return <button onClick={() => setCount(c => c + 1)}>Count: {count()}</button>
39
- * }
40
- * ```
41
- *
42
- * @example
43
- * ```tsx
44
- * function Modal(props) {
45
- * // enabled option is reactive
46
- * createHotkey('Escape', () => {
47
- * props.onClose()
48
- * }, () => ({ enabled: props.isOpen }))
5
+ * The element to listen on. A `null` target skips registration, so pass an
6
+ * accessor such as `() => ({ target: element() })` for elements that attach
7
+ * after mount.
8
+ */
9
+ export type CreateHotkeyTarget = Target | null;
10
+ /** Options shared by chords and sequences. */
11
+ export type CreateHotkeyOptions = AdapterHotkeyOptions<CreateHotkeyTarget>;
12
+ /** Options for a chord such as `'Mod+S'`. */
13
+ export type CreateHotkeyChordOptions = AdapterChordOptions<CreateHotkeyTarget>;
14
+ /** Options for a sequence such as `['G', 'G']`. */
15
+ export type CreateHotkeySequenceOptions = AdapterSequenceOptions<CreateHotkeyTarget>;
16
+ /** Options applied to every hotkey, such as provider defaults. Each applies to the kinds that support it. */
17
+ export type CreateHotkeyCommonOptions = AdapterCommonOptions<CreateHotkeyTarget>;
18
+ /** The options type for a hotkey: chord options for chords, sequence options for sequences. */
19
+ export type CreateHotkeyOptionsFor<T extends RegisterableHotkey> = AdapterOptionsFor<T, CreateHotkeyTarget>;
20
+ /** Call-site options for an app hotkey, typed by its declared default kind. */
21
+ export type CreateAppHotkeyOptions<T extends AppHotkey = AppHotkey> = AdapterAppHotkeyOptions<T, CreateHotkeyTarget>;
22
+ type MaybeAccessor<T> = T | (() => T);
23
+ /**
24
+ * Solid primitive for registering a hotkey: a chord such as `'Mod+S'`, a
25
+ * sequence such as `['G', 'G']`, or an app hotkey from `defineAppHotkeys`.
26
+ * The hotkey and options may be accessors.
49
27
  *
50
- * return <Show when={props.isOpen}>
51
- * <div class="modal">...</div>
52
- * </Show>
53
- * }
54
- * ```
28
+ * Chords accept `requireReset`; sequences accept `timeout`. A one-step
29
+ * sequence registers as its chord. An app hotkey uses its effective hotkey
30
+ * from `HotkeysProvider` (including the user's custom hotkey) and its
31
+ * declared meta and options, and registers nothing while unbound.
55
32
  *
56
33
  * @example
57
34
  * ```tsx
58
- * function Editor() {
59
- * const [editorRef, setEditorRef] = createSignal<HTMLDivElement | null>(null)
60
- *
61
- * // Scoped to a specific element - use accessor so hotkey waits for ref
62
- * createHotkey('Mod+S', save, () => ({ target: editorRef() }))
63
- *
64
- * return <div ref={setEditorRef}>...</div>
65
- * }
35
+ * createHotkey('Mod+S', () => save())
36
+ * createHotkey(['G', 'G'], () => scrollToTop(), { timeout: 500 })
37
+ * createHotkey('Escape', () => close(), () => ({ enabled: isOpen() }))
66
38
  * ```
67
39
  */
68
- export declare function createHotkey(hotkey: RegisterableHotkey | (() => RegisterableHotkey), callback: HotkeyCallback, options?: CreateHotkeyOptions | (() => CreateHotkeyOptions)): void;
40
+ export declare function createHotkey<T extends AppHotkey>(hotkey: MaybeAccessor<T>, callback: HotkeyCallback, options?: MaybeAccessor<CreateAppHotkeyOptions<T>>): void;
41
+ export declare function createHotkey<T extends RegisterableHotkey>(hotkey: MaybeAccessor<T>, callback: HotkeyCallback, options?: MaybeAccessor<CreateHotkeyOptionsFor<T>>): void;
69
42
  //#endregion
@@ -1,119 +1,12 @@
1
- import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
2
- import { detectPlatform, getHotkeyManager, normalizeRegisterableHotkey } from "@tanstack/hotkeys";
3
- import { createEffect, onCleanup, untrack } from "solid-js";
1
+ import { createHotkeys } from "./createHotkeys.js";
4
2
 
5
3
  //#region src/createHotkey.ts
6
- /**
7
- * SolidJS primitive for registering a keyboard hotkey.
8
- *
9
- * Uses the singleton HotkeyManager for efficient event handling.
10
- * The callback receives both the keyboard event and a context object
11
- * containing the hotkey string and parsed hotkey.
12
- *
13
- * This primitive automatically tracks reactive dependencies and updates
14
- * the registration when options or the callback change.
15
- *
16
- * @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)
17
- * @param callback - The function to call when the hotkey is pressed
18
- * @param options - Options for the hotkey behavior
19
- *
20
- * @example
21
- * ```tsx
22
- * function SaveButton() {
23
- * const [count, setCount] = createSignal(0)
24
- *
25
- * // Callback always has access to latest count value
26
- * createHotkey('Mod+S', (event, { hotkey }) => {
27
- * console.log(`Save triggered, count is ${count()}`)
28
- * handleSave()
29
- * })
30
- *
31
- * return <button onClick={() => setCount(c => c + 1)}>Count: {count()}</button>
32
- * }
33
- * ```
34
- *
35
- * @example
36
- * ```tsx
37
- * function Modal(props) {
38
- * // enabled option is reactive
39
- * createHotkey('Escape', () => {
40
- * props.onClose()
41
- * }, () => ({ enabled: props.isOpen }))
42
- *
43
- * return <Show when={props.isOpen}>
44
- * <div class="modal">...</div>
45
- * </Show>
46
- * }
47
- * ```
48
- *
49
- * @example
50
- * ```tsx
51
- * function Editor() {
52
- * const [editorRef, setEditorRef] = createSignal<HTMLDivElement | null>(null)
53
- *
54
- * // Scoped to a specific element - use accessor so hotkey waits for ref
55
- * createHotkey('Mod+S', save, () => ({ target: editorRef() }))
56
- *
57
- * return <div ref={setEditorRef}>...</div>
58
- * }
59
- * ```
60
- */
61
4
  function createHotkey(hotkey, callback, options = {}) {
62
- const defaultOptions = useDefaultHotkeysOptions();
63
- const manager = getHotkeyManager();
64
- let registration = null;
65
- let lastHotkeyString = null;
66
- let lastTarget = null;
67
- onCleanup(() => {
68
- if (registration?.isActive) {
69
- registration.unregister();
70
- registration = null;
71
- }
72
- lastHotkeyString = null;
73
- lastTarget = null;
74
- });
75
- createEffect(() => {
76
- const resolvedHotkey = typeof hotkey === "function" ? hotkey() : hotkey;
77
- const resolvedOptions = typeof options === "function" ? options() : options;
78
- const mergedOptions = {
79
- ...defaultOptions.hotkey,
80
- ...resolvedOptions
81
- };
82
- const platform = mergedOptions.platform ?? detectPlatform();
83
- const hotkeyString = normalizeRegisterableHotkey(resolvedHotkey, platform);
84
- const resolvedTarget = "target" in mergedOptions ? mergedOptions.target ?? null : typeof document !== "undefined" ? document : null;
85
- if (!resolvedTarget) {
86
- if (registration?.isActive) {
87
- registration.unregister();
88
- registration = null;
89
- }
90
- lastHotkeyString = null;
91
- lastTarget = null;
92
- return;
93
- }
94
- const { target: _target, ...optionsWithoutTarget } = mergedOptions;
95
- untrack(() => {
96
- if (registration?.isActive && lastHotkeyString === hotkeyString && lastTarget === resolvedTarget) {
97
- registration.callback = callback;
98
- registration.setOptions(optionsWithoutTarget);
99
- return;
100
- }
101
- if (registration?.isActive) {
102
- registration.unregister();
103
- registration = null;
104
- }
105
- registration = manager.register(hotkeyString, callback, {
106
- ...optionsWithoutTarget,
107
- target: resolvedTarget
108
- });
109
- if (registration.isActive) {
110
- registration.callback = callback;
111
- registration.setOptions(optionsWithoutTarget);
112
- }
113
- lastHotkeyString = hotkeyString;
114
- lastTarget = resolvedTarget;
115
- });
116
- });
5
+ createHotkeys(() => [{
6
+ hotkey: typeof hotkey === "function" ? hotkey() : hotkey,
7
+ callback,
8
+ options: typeof options === "function" ? options() : options
9
+ }]);
117
10
  }
118
11
 
119
12
  //#endregion
@@ -1,11 +1,10 @@
1
- import { HeldModifierOptions, RegisterableHotkey } from "@tanstack/hotkeys";
1
+ import { AppHotkey, HeldModifierOptions, RegisterableHotkey } from "@tanstack/hotkeys";
2
2
  //#region src/createHotkeyHint.d.ts
3
3
  /**
4
- * Returns a boolean accessor indicating whether held modifiers reveal this shortcut.
5
- * Pass accessors to track changing bindings and options.
6
- * Uses the nonempty-subset/AltGr rules of `matchesHeldModifiers`; `exact` requires
7
- * every modifier. Combine with the action's enabled state before showing a badge.
8
- * This helper does not register a shortcut or check whether its target is focused.
4
+ * Returns an accessor indicating whether held modifiers reveal this shortcut.
5
+ * A sequence reveals its hint from its first step, and an app hotkey uses its
6
+ * effective hotkey (false while unbound). This helper does not register a
7
+ * shortcut or check whether its target is focused.
9
8
  */
10
- export declare function createHotkeyHint(hotkey: RegisterableHotkey | (() => RegisterableHotkey), options?: HeldModifierOptions | (() => HeldModifierOptions)): () => boolean;
9
+ export declare function createHotkeyHint(hotkey: RegisterableHotkey | AppHotkey | (() => RegisterableHotkey | AppHotkey), options?: HeldModifierOptions | (() => HeldModifierOptions)): () => boolean;
11
10
  //#endregion
@@ -1,18 +1,24 @@
1
- import { getKeyStateTracker, matchesHeldModifiers } from "@tanstack/hotkeys";
1
+ import { useAppHotkeysStoreSource } from "./defaultOptions.js";
2
+ import { getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey } from "@tanstack/hotkeys";
2
3
  import { createMemo } from "solid-js";
3
4
  import { useSelector } from "@tanstack/solid-store";
4
5
 
5
6
  //#region src/createHotkeyHint.ts
6
7
  /**
7
- * Returns a boolean accessor indicating whether held modifiers reveal this shortcut.
8
- * Pass accessors to track changing bindings and options.
9
- * Uses the nonempty-subset/AltGr rules of `matchesHeldModifiers`; `exact` requires
10
- * every modifier. Combine with the action's enabled state before showing a badge.
11
- * This helper does not register a shortcut or check whether its target is focused.
8
+ * Returns an accessor indicating whether held modifiers reveal this shortcut.
9
+ * A sequence reveals its hint from its first step, and an app hotkey uses its
10
+ * effective hotkey (false while unbound). This helper does not register a
11
+ * shortcut or check whether its target is focused.
12
12
  */
13
13
  function createHotkeyHint(hotkey, options = {}) {
14
+ const appHotkeys = useAppHotkeysStoreSource();
14
15
  const held = useSelector(getKeyStateTracker().store, (state) => state.heldKeys);
15
- return createMemo(() => matchesHeldModifiers(typeof hotkey === "function" ? hotkey() : hotkey, held(), typeof options === "function" ? options() : options));
16
+ return createMemo(() => {
17
+ appHotkeys.state();
18
+ const value = typeof hotkey === "function" ? hotkey() : hotkey;
19
+ const resolved = isAppHotkey(value) ? resolveAppHotkey(value, appHotkeys.store).hotkey : value;
20
+ return resolved !== null && matchesHeldModifiers(resolved, held(), typeof options === "function" ? options() : options);
21
+ });
16
22
  }
17
23
 
18
24
  //#endregion
@@ -1,57 +1,38 @@
1
- import { Hotkey, HotkeyRecorderOptions } from "@tanstack/hotkeys";
1
+ import { Hotkey, HotkeyChord, HotkeyChordRecorderOptions, HotkeyRecorderOptions, HotkeySequence, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
2
2
  //#region src/createHotkeyRecorder.d.ts
3
- export interface SolidHotkeyRecorder {
3
+ export interface SolidHotkeyRecorder<THotkey extends Hotkey = Hotkey> {
4
4
  /** Whether recording is currently active */
5
5
  isRecording: () => boolean;
6
- /** The currently recorded hotkey (for live preview) */
7
- recordedHotkey: () => Hotkey | null;
6
+ /** Chords recorded so far in a multi-step recording (for live preview) */
7
+ steps: () => HotkeySequence;
8
+ /** The last committed hotkey */
9
+ recordedHotkey: () => THotkey | null;
8
10
  /** Start recording a new hotkey */
9
11
  startRecording: () => void;
10
- /** Stop recording (same as cancel) */
12
+ /** Stop recording without calling onCancel */
11
13
  stopRecording: () => void;
12
- /** Cancel recording without saving */
14
+ /** Cancel recording and call onCancel */
13
15
  cancelRecording: () => void;
16
+ /** Commit the recorded steps now (multi-step recordings) */
17
+ commitRecording: () => void;
14
18
  }
15
19
  /**
16
- * SolidJS primitive for recording keyboard shortcuts.
20
+ * Solid primitive for recording keyboard shortcuts. Options may be an accessor.
17
21
  *
18
- * This primitive provides a thin wrapper around the framework-agnostic `HotkeyRecorder`
19
- * class, managing all the complexity of capturing keyboard events, converting them
20
- * to hotkey strings, and handling edge cases like Escape to cancel or Backspace/Delete
21
- * to clear.
22
- *
23
- * This primitive uses `useSelector` from `@tanstack/solid-store` to subscribe
24
- * to the recorder's store state (same pattern as useHotkeyRecorder in React).
25
- *
26
- * @param options - Configuration options for the recorder (or accessor function)
27
- * @returns An object with recording state signals and control functions
22
+ * With the default `maxSteps: 1`, the first valid chord is recorded. With a
23
+ * larger `maxSteps`, each chord adds a step; the recording commits on Enter
24
+ * (unless `commitKeys: 'none'`), after `idleTimeoutMs`, or at `maxSteps`, and
25
+ * `onRecord` receives a chord (one step) or a sequence.
28
26
  *
29
27
  * @example
30
28
  * ```tsx
31
- * function ShortcutSettings() {
32
- * const [shortcut, setShortcut] = createSignal<Hotkey>('Mod+S')
33
- *
34
- * const recorder = createHotkeyRecorder({
35
- * onRecord: (hotkey) => {
36
- * setShortcut(hotkey)
37
- * },
38
- * onCancel: () => {
39
- * console.log('Recording cancelled')
40
- * },
41
- * })
42
- *
43
- * return (
44
- * <div>
45
- * <button onClick={recorder.startRecording}>
46
- * {recorder.isRecording() ? 'Recording...' : 'Edit Shortcut'}
47
- * </button>
48
- * <Show when={recorder.recordedHotkey()}>
49
- * <div>Recording: {recorder.recordedHotkey()}</div>
50
- * </Show>
51
- * </div>
52
- * )
53
- * }
29
+ * const recorder = createHotkeyRecorder({ onRecord: setShortcut })
30
+ * const sequenceRecorder = createHotkeyRecorder({ maxSteps: 3, onRecord: setShortcut })
54
31
  * ```
55
32
  */
56
- export declare function createHotkeyRecorder(options: HotkeyRecorderOptions | (() => HotkeyRecorderOptions)): SolidHotkeyRecorder;
33
+ export declare function createHotkeyRecorder(options: HotkeyChordRecorderOptions): SolidHotkeyRecorder<HotkeyChord>;
34
+ export declare function createHotkeyRecorder(options: HotkeySequenceRecorderOptions): SolidHotkeyRecorder;
35
+ export declare function createHotkeyRecorder(options: () => HotkeySequenceRecorderOptions): SolidHotkeyRecorder;
36
+ export declare function createHotkeyRecorder(options: () => HotkeyChordRecorderOptions): SolidHotkeyRecorder<HotkeyChord>;
37
+ export declare function createHotkeyRecorder(options: () => HotkeyRecorderOptions): SolidHotkeyRecorder;
57
38
  //#endregion