@tanstack/svelte-hotkeys 0.15.1 → 0.17.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.
@@ -5,9 +5,10 @@ export interface SvelteHotkeyHint {
5
5
  }
6
6
  /**
7
7
  * Returns an object with a reactive `visible` getter indicating whether held
8
- * modifiers reveal this shortcut. Pass getters to track changing hotkeys and
9
- * options. A sequence reveals its hint from its first step, and an app hotkey
10
- * uses its effective hotkey (false while unbound). This helper does not
11
- * register a shortcut or check whether its target is focused.
8
+ * modifiers reveal this shortcut. The hint shows only while the hotkey can
9
+ * fire: some registration of it is enabled and in an active scope. Pass
10
+ * getters to track changing hotkeys and options. A sequence reveals its hint
11
+ * from its first step, and an app hotkey matches its registrations by id and
12
+ * uses its effective hotkey (false while unbound). This helper does not register a shortcut. A hotkey on an element counts only while focus is inside it.
12
13
  */
13
14
  export declare function getHotkeyHint(hotkey: MaybeGetter<RegisterableHotkey | AppHotkey>, options?: MaybeGetter<HeldModifierOptions>): SvelteHotkeyHint;
@@ -1,16 +1,18 @@
1
1
  import { createSubscriber } from 'svelte/reactivity';
2
- import { getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey, } from '@tanstack/hotkeys';
2
+ import { getHotkeyManager, getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey, } from '@tanstack/hotkeys';
3
3
  import { getHotkeysContext } from './HotkeysCtx';
4
4
  import { resolveMaybeGetter } from './internal.svelte';
5
5
  /**
6
6
  * Returns an object with a reactive `visible` getter indicating whether held
7
- * modifiers reveal this shortcut. Pass getters to track changing hotkeys and
8
- * options. A sequence reveals its hint from its first step, and an app hotkey
9
- * uses its effective hotkey (false while unbound). This helper does not
10
- * register a shortcut or check whether its target is focused.
7
+ * modifiers reveal this shortcut. The hint shows only while the hotkey can
8
+ * fire: some registration of it is enabled and in an active scope. Pass
9
+ * getters to track changing hotkeys and options. A sequence reveals its hint
10
+ * from its first step, and an app hotkey matches its registrations by id and
11
+ * uses its effective hotkey (false while unbound). This helper does not register a shortcut. A hotkey on an element counts only while focus is inside it.
11
12
  */
12
13
  export function getHotkeyHint(hotkey, options = {}) {
13
14
  const tracker = getKeyStateTracker();
15
+ const manager = getHotkeyManager();
14
16
  const store = getHotkeysContext()?.appHotkeysStore ?? null;
15
17
  // Resolve current reactive arguments without caching a stale hotkey.
16
18
  const read = () => {
@@ -19,7 +21,8 @@ export function getHotkeyHint(hotkey, options = {}) {
19
21
  ? resolveAppHotkey(value, store).hotkey
20
22
  : value;
21
23
  return (resolved !== null &&
22
- matchesHeldModifiers(resolved, tracker.store.state.heldKeys, resolveMaybeGetter(options)));
24
+ matchesHeldModifiers(resolved, tracker.store.state.heldKeys, resolveMaybeGetter(options)) &&
25
+ manager.hasActiveRegistration(value));
23
26
  };
24
27
  let previous = false;
25
28
  // Notify Svelte only when visibility changes, not for every unrelated held key.
@@ -33,9 +36,11 @@ export function getHotkeyHint(hotkey, options = {}) {
33
36
  }
34
37
  };
35
38
  const keys = tracker.store.subscribe(check);
39
+ const registrations = manager.registrations.subscribe(check);
36
40
  const custom = store?.store.subscribe(check);
37
41
  return () => {
38
42
  keys.unsubscribe();
43
+ registrations.unsubscribe();
39
44
  custom?.unsubscribe();
40
45
  };
41
46
  });
@@ -0,0 +1,24 @@
1
+ import type { HotkeyScopesState } from '@tanstack/hotkeys';
2
+ export interface SvelteHotkeyScopesState {
3
+ readonly state: HotkeyScopesState;
4
+ }
5
+ /**
6
+ * Svelte function that returns reactive access to the hotkey manager's scope
7
+ * state: the active scopes, the exclusive scope that owns the keyboard, and
8
+ * the scopes it allows or suspends. Updates when scopes activate or release.
9
+ *
10
+ * @returns Object with a reactive `state` property
11
+ *
12
+ * @example
13
+ * ```svelte
14
+ * <script>
15
+ * import { getHotkeyScopesState } from '@tanstack/svelte-hotkeys'
16
+ *
17
+ * const scopes = getHotkeyScopesState()
18
+ * </script>
19
+ * {#if scopes.state.exclusive}
20
+ * <p>Shortcuts belong to {scopes.state.exclusive}</p>
21
+ * {/if}
22
+ * ```
23
+ */
24
+ export declare function getHotkeyScopesState(): SvelteHotkeyScopesState;
@@ -0,0 +1,32 @@
1
+ import { getHotkeyManager } from '@tanstack/hotkeys';
2
+ import { createStoreSubscriber } from './internal.svelte';
3
+ class HotkeyScopesStateReader {
4
+ #store = getHotkeyManager().scopes;
5
+ #subscribe = createStoreSubscriber(this.#store);
6
+ get state() {
7
+ this.#subscribe();
8
+ return this.#store.state;
9
+ }
10
+ }
11
+ /**
12
+ * Svelte function that returns reactive access to the hotkey manager's scope
13
+ * state: the active scopes, the exclusive scope that owns the keyboard, and
14
+ * the scopes it allows or suspends. Updates when scopes activate or release.
15
+ *
16
+ * @returns Object with a reactive `state` property
17
+ *
18
+ * @example
19
+ * ```svelte
20
+ * <script>
21
+ * import { getHotkeyScopesState } from '@tanstack/svelte-hotkeys'
22
+ *
23
+ * const scopes = getHotkeyScopesState()
24
+ * </script>
25
+ * {#if scopes.state.exclusive}
26
+ * <p>Shortcuts belong to {scopes.state.exclusive}</p>
27
+ * {/if}
28
+ * ```
29
+ */
30
+ export function getHotkeyScopesState() {
31
+ return new HotkeyScopesStateReader();
32
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from './createHotkeys.svelte';
4
4
  export * from './createHotkeyRecorder.svelte';
5
5
  export * from './getHeldKeys.svelte';
6
6
  export * from './getHotkeyRegistrations.svelte';
7
+ export * from './getHotkeyScopesState.svelte';
7
8
  export * from './getHeldKeyCodesMap.svelte';
8
9
  export * from './getIsKeyHeld.svelte';
9
10
  export * from './getAppHotkeys.svelte';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export * from './createHotkeys.svelte';
5
5
  export * from './createHotkeyRecorder.svelte';
6
6
  export * from './getHeldKeys.svelte';
7
7
  export * from './getHotkeyRegistrations.svelte';
8
+ export * from './getHotkeyScopesState.svelte';
8
9
  export * from './getHeldKeyCodesMap.svelte';
9
10
  export * from './getIsKeyHeld.svelte';
10
11
  export * from './getAppHotkeys.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/svelte-hotkeys",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "description": "Svelte adapter for TanStack Hotkeys",
5
5
  "author": "Kunal Rao",
6
6
  "license": "MIT",
@@ -20,7 +20,8 @@
20
20
  "keys",
21
21
  "hotkeys",
22
22
  "keyboard",
23
- "shortcuts"
23
+ "shortcuts",
24
+ "tanstack-intent"
24
25
  ],
25
26
  "type": "module",
26
27
  "types": "./dist/index.d.ts",
@@ -36,10 +37,11 @@
36
37
  },
37
38
  "sideEffects": false,
38
39
  "files": [
39
- "dist"
40
+ "dist",
41
+ "skills"
40
42
  ],
41
43
  "dependencies": {
42
- "@tanstack/hotkeys": "0.16.1"
44
+ "@tanstack/hotkeys": "0.18.0"
43
45
  },
44
46
  "devDependencies": {
45
47
  "@sveltejs/package": "^3.0.0",
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: svelte-hotkeys
3
+ description: >-
4
+ Use when adding or fixing keyboard shortcuts in a Svelte 5 app with
5
+ @tanstack/svelte-hotkeys: createHotkey and createHotkeys, getter options,
6
+ createHotkeyAttachment for element hotkeys, HotkeysProvider and
7
+ setHotkeysContext scopes, app hotkey getters, recorders, held-key and hint
8
+ getters, or replacing svelte:window keydown handlers.
9
+ metadata:
10
+ purpose: >-
11
+ Svelte 5 bindings for TanStack Hotkeys: getters, attachments, providers,
12
+ and the Svelte-specific reactivity mistakes to avoid.
13
+ type: framework
14
+ framework: svelte
15
+ library: '@tanstack/svelte-hotkeys'
16
+ library_version: '0.15.0'
17
+ sources:
18
+ - src/createHotkey.svelte.ts
19
+ - src/createHotkeys.svelte.ts
20
+ - src/HotkeysCtx.ts
21
+ - src/HotkeysProvider.svelte
22
+ - src/getAppHotkeys.svelte.ts
23
+ - src/createHotkeyRecorder.svelte.ts
24
+ - TanStack/hotkeys:docs/framework/svelte/quick-start.md
25
+ - TanStack/hotkeys:docs/framework/svelte/guides/hotkeys-provider-and-scopes.md
26
+ requires:
27
+ - '@tanstack/hotkeys#hotkeys-core'
28
+ ---
29
+
30
+ # TanStack Hotkeys in Svelte
31
+
32
+ Before starting, load the `@tanstack/hotkeys#hotkeys-core` skill (`npx @tanstack/intent@latest load @tanstack/hotkeys#hotkeys-core`).
33
+
34
+ ```svelte
35
+ <script lang="ts">
36
+ import {
37
+ createHotkey,
38
+ createHotkeyAttachment,
39
+ } from '@tanstack/svelte-hotkeys'
40
+
41
+ let isDirty = $state(false)
42
+
43
+ createHotkey(
44
+ 'Mod+S',
45
+ () => save(),
46
+ () => ({ enabled: isDirty }),
47
+ )
48
+ const closeOnEscape = createHotkeyAttachment('Escape', () => closePanel())
49
+ </script>
50
+
51
+ <div tabindex="0" {@attach closeOnEscape} oninput={() => (isDirty = true)}>
52
+ Panel
53
+ </div>
54
+ ```
55
+
56
+ ## API
57
+
58
+ | API | What it's for |
59
+ | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
+ | `createHotkey(hotkey, callback, options?)` | Register one chord, sequence, or app hotkey on the document while the component is mounted; pass getters for values that change. |
61
+ | `createHotkeys(definitions, commonOptions?)` | Register several hotkeys on the document, or a getter of a list built from data. |
62
+ | `createHotkeyAttachment(hotkey, callback, options?)` | Create an attachment for `{@attach}` that listens on one element. |
63
+ | `createHotkeysAttachment(definitions, commonOptions?)` | Create an attachment that registers several hotkeys on one element. |
64
+ | `<HotkeysProvider>` | Set default options, a scope (`exclusive` for dialogs), `enabled`, and app hotkeys for the components inside it. |
65
+ | `setHotkeysContext(options)` | Set the same options from a component's script instead of rendering `HotkeysProvider`; they apply to that component too. |
66
+ | `getHotkeysContext()` | Read the nearest context's merged defaults and enabled state, or `null` outside one. |
67
+ | `getDefaultHotkeysOptions()` | Read the default options merged from every context above. |
68
+ | `getAppHotkeys()` | Read `.current`, the declared app hotkeys with the user's custom hotkeys applied. |
69
+ | `getAppHotkeyList()` | Read `.current`, every app hotkey with its status, for a cheat sheet or settings screen. |
70
+ | `getCustomHotkeys()` | Read the user's custom hotkeys from `.current`, change or reset them, and check a new one for conflicts. |
71
+ | `createHotkeyRecorder(options)` | Record a chord or sequence the user presses, for a settings screen. |
72
+ | `getHotkeyRegistrations()` | Read `.hotkeys`, every registered hotkey with its options, `meta`, and `status` (whether it's enabled and in an active scope). |
73
+ | `getHotkeyScopesState()` | Read `.state`, the active scopes and the exclusive scope that owns the keyboard. |
74
+ | `getHotkeyHint(hotkey, options?)` | Read `.visible`, whether held modifiers should reveal a shortcut's hint. False unless a registration of the hotkey can fire: enabled, in an active scope, and with focus inside its element target. |
75
+ | `getIsKeyHeld(key)` | Read `.held`, whether one key is held. |
76
+ | `getHeldKeys()` | Read `.keys`, every key that's held right now. |
77
+ | `getHeldKeyCodesMap()` | Read `.codes`, which maps each held key to its physical `event.code`. |
78
+
79
+ The package also re-exports `@tanstack/hotkeys`, such as `formatForDisplay`, `matchesKeyboardEvent`, and `defineAppHotkeys`.
80
+
81
+ No provider is required. Call these at the top level of a component's `<script>`.
82
+
83
+ ## Pitfalls
84
+
85
+ - **Pass options as a getter.** `{ enabled: isDirty }` is read once, during initialization, so the hotkey never follows the state. Use `() => ({ enabled: isDirty })`. The hotkey and `createHotkeys`' definitions also accept getters.
86
+ - **Don't destructure the returned objects.** `const { held } = getIsKeyHeld('Shift')` copies the value once. Keep the object and read `shift.held`, `recorder.isRecording`, or `appHotkeys.current` where you use it.
87
+ - **Use attachments for element hotkeys.** The `target` option takes only `document` or `window`.
88
+ - **Call `setHotkeysContext` before creating the component's hotkeys.** It applies to the component itself, so a hotkey created first lands in the outer scope, which an exclusive context then suspends. It reads its options once; for `enabled` or `exclusive` that change, wrap the component in `<HotkeysProvider>`. Give each open dialog its own scope name.
89
+ - **The callback isn't tracked.** Read state and props inside it.
90
+ - **Call the functions directly.** A `createShortcut(keys: string)` wrapper has to cast strings to hotkeys, which turns off type checking and drops sequences, getter options, and app hotkeys.
91
+
92
+ When declaring app hotkeys, building a cheat sheet, or letting users rebind shortcuts, load `@tanstack/hotkeys#app-hotkeys` and read [app hotkeys in Svelte](references/app-hotkeys.md).
93
+
94
+ ## API discovery and docs
95
+
96
+ Signatures: `node_modules/@tanstack/svelte-hotkeys/dist/*.d.ts`. Guides: `https://tanstack.com/hotkeys/latest/docs/framework/svelte/guides/<page>.md`, such as `hotkeys`, `hotkeys-provider-and-scopes`, or `app-hotkeys`. Devtools: `hotkeysDevtoolsPlugin()` from `@tanstack/svelte-hotkeys-devtools` in `@tanstack/svelte-devtools`.
@@ -0,0 +1,79 @@
1
+ # App hotkeys in Svelte
2
+
3
+ The `@tanstack/hotkeys#app-hotkeys` skill owns the rules. This is the Svelte wiring.
4
+
5
+ ```ts
6
+ // app-hotkeys.ts
7
+ import { defineAppHotkeys } from '@tanstack/svelte-hotkeys'
8
+
9
+ export const appHotkeys = defineAppHotkeys({
10
+ 'doc.save': { hotkey: 'Mod+S', meta: { name: 'Save' } },
11
+ 'help.shortcuts': { hotkey: 'Mod+/', meta: { name: 'Keyboard shortcuts' } },
12
+ })
13
+
14
+ declare module '@tanstack/svelte-hotkeys' {
15
+ interface Register {
16
+ appHotkeys: typeof appHotkeys
17
+ }
18
+ }
19
+ ```
20
+
21
+ ```svelte
22
+ <!-- App.svelte -->
23
+ <script lang="ts">
24
+ import { HotkeysProvider } from '@tanstack/svelte-hotkeys'
25
+ import type { CustomHotkeys } from '@tanstack/svelte-hotkeys'
26
+ import { appHotkeys } from './app-hotkeys'
27
+ import AppContent from './AppContent.svelte'
28
+
29
+ const saved: CustomHotkeys = JSON.parse(
30
+ localStorage.getItem('hotkeys') ?? '{}',
31
+ )
32
+ </script>
33
+
34
+ <HotkeysProvider
35
+ {appHotkeys}
36
+ customHotkeys={saved}
37
+ onCustomHotkeysChange={(custom) =>
38
+ localStorage.setItem('hotkeys', JSON.stringify(custom))}
39
+ >
40
+ <AppContent />
41
+ </HotkeysProvider>
42
+ ```
43
+
44
+ Read app hotkeys inside a getter so registrations follow rebinding: `createHotkey(() => appHotkeys.current['doc.save'], save)`, with `const appHotkeys = getAppHotkeys()`. Build a cheat sheet from `getAppHotkeyList().current`.
45
+
46
+ A settings row records with `createHotkeyRecorder` and saves through `getCustomHotkeys`:
47
+
48
+ ```svelte
49
+ <script lang="ts">
50
+ import {
51
+ createHotkeyRecorder,
52
+ formatForDisplay,
53
+ getAppHotkeys,
54
+ getCustomHotkeys,
55
+ } from '@tanstack/svelte-hotkeys'
56
+ import type { AppHotkeyId } from '@tanstack/svelte-hotkeys'
57
+
58
+ let { id }: { id: AppHotkeyId } = $props()
59
+ const appHotkeys = getAppHotkeys()
60
+ const custom = getCustomHotkeys()
61
+ const appHotkey = $derived(appHotkeys.current[id])
62
+
63
+ const recorder = createHotkeyRecorder(() => ({
64
+ maxSteps: Infinity,
65
+ validate: (hotkey) => {
66
+ const [conflict] = custom.findConflicts(id, hotkey)
67
+ return conflict ? `Used by ${conflict.meta?.name ?? conflict.id}` : true
68
+ },
69
+ onRecord: (hotkey) => custom.setCustomHotkey(id, hotkey),
70
+ onClear: () => custom.setCustomHotkey(id, null),
71
+ }))
72
+ </script>
73
+
74
+ {appHotkey.meta?.name}: {appHotkey.hotkey
75
+ ? formatForDisplay(appHotkey.hotkey)
76
+ : 'Unassigned'}
77
+ <button onclick={recorder.startRecording}>Change</button>
78
+ <button onclick={() => custom.resetCustomHotkey(id)}>Reset</button>
79
+ ```