@tanstack/alpine-hotkeys 0.4.1 → 0.6.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.
@@ -1,12 +1,14 @@
1
1
  import { read } from "./scope.js";
2
- import { getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey } from "@tanstack/hotkeys";
2
+ import { getHotkeyManager, getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey } from "@tanstack/hotkeys";
3
3
 
4
4
  //#region src/createHotkeyHint.ts
5
5
  function createHotkeyHint(scope, hotkey, options = {}) {
6
+ const manager = getHotkeyManager();
7
+ const registrations = scope.select(manager.registrations, (state) => state);
6
8
  return scope.select(getKeyStateTracker().store, (state) => {
7
9
  const value = read(hotkey);
8
10
  const resolved = isAppHotkey(value) ? resolveAppHotkey(value, scope.appHotkeys()).hotkey : value;
9
- return resolved !== null && matchesHeldModifiers(resolved, state.heldKeys, read(options));
11
+ return resolved !== null && matchesHeldModifiers(resolved, state.heldKeys, read(options)) && registrations.value.size > 0 && manager.hasActiveRegistration(value);
10
12
  });
11
13
  }
12
14
 
@@ -0,0 +1,9 @@
1
+ import { getHotkeyManager } from "@tanstack/hotkeys";
2
+
3
+ //#region src/createHotkeyScopesState.ts
4
+ function createHotkeyScopesState(scope) {
5
+ return scope.select(getHotkeyManager().scopes, (state) => state);
6
+ }
7
+
8
+ //#endregion
9
+ export { createHotkeyScopesState };
@@ -6,6 +6,7 @@ import { createHeldKeyCodes } from "./createHeldKeyCodes.js";
6
6
  import { createKeyHold } from "./createKeyHold.js";
7
7
  import { createHotkeyHint } from "./createHotkeyHint.js";
8
8
  import { createHotkeyRegistrations } from "./createHotkeyRegistrations.js";
9
+ import { createHotkeyScopesState } from "./createHotkeyScopesState.js";
9
10
  import { createHotkeyRecorder } from "./createHotkeyRecorder.js";
10
11
  import { createAppHotkeyList, createAppHotkeys, createCustomHotkeys } from "./createAppHotkeys.js";
11
12
 
@@ -25,6 +26,7 @@ function createHotkeysScope(defaultOptions = {}) {
25
26
  createKeyHold: (...args) => createKeyHold(scope, ...args),
26
27
  createHotkeyHint: (...args) => createHotkeyHint(scope, ...args),
27
28
  createHotkeyRegistrations: (...args) => createHotkeyRegistrations(scope, ...args),
29
+ createHotkeyScopesState: () => createHotkeyScopesState(scope),
28
30
  createHotkeyRecorder: ((options) => createHotkeyRecorder(scope, options)),
29
31
  createAppHotkeys: () => createAppHotkeys(scope),
30
32
  createAppHotkeyList: () => createAppHotkeyList(scope),
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AppHotkey, AppHotkeyListItem, CustomHotkeys, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContextFor, HotkeyChord, HotkeyChordRecorderOptions, HotkeyRecorderOptions, HotkeyRegistrationView, HotkeySequence, HotkeySequenceRecorderOptions, IndividualKey, RegisterableHotkey, RegisteredAppHotkeys, Target } from "@tanstack/hotkeys";
1
+ import { AppHotkey, AppHotkeyListItem, CustomHotkeys, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContextFor, HotkeyChord, HotkeyChordRecorderOptions, HotkeyRecorderOptions, HotkeyRegistrationView, HotkeyScopesState, HotkeySequence, HotkeySequenceRecorderOptions, IndividualKey, RegisterableHotkey, RegisteredAppHotkeys, Target } from "@tanstack/hotkeys";
2
2
  import { AdapterAppHotkeyOptions, AdapterCommonOptions, AdapterHotkeyDefinition, AdapterOptionsFor } from "@tanstack/hotkeys/adapter";
3
3
  //#region src/types.d.ts
4
4
  /** A reactive value. Read `.value` where the framework tracks dependencies. */
@@ -87,6 +87,8 @@ export interface AlpineHotkeys {
87
87
  createHotkeyHint: (hotkey: MaybeGetter<RegisterableHotkey | AppHotkey>, options?: MaybeGetter<HeldModifierOptions>) => AlpineHotkeyState<boolean>;
88
88
  /** Read live registrations, including disabled entries. */
89
89
  createHotkeyRegistrations: () => HotkeyRegistrationsResult;
90
+ /** Read the active, exclusive, allowed, and suspended scopes through `.value`. */
91
+ createHotkeyScopesState: () => AlpineHotkeyState<HotkeyScopesState>;
90
92
  /** Create a recorder whose options can follow Alpine state. */
91
93
  createHotkeyRecorder: AlpineCreateHotkeyRecorder;
92
94
  /** Read the effective app hotkeys from `provideAppHotkeys` through `.value`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/alpine-hotkeys",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Alpine adapter for TanStack Hotkeys",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -17,7 +17,8 @@
17
17
  "keywords": [
18
18
  "alpine",
19
19
  "tanstack",
20
- "hotkeys"
20
+ "hotkeys",
21
+ "tanstack-intent"
21
22
  ],
22
23
  "type": "module",
23
24
  "types": "./dist/index.d.ts",
@@ -30,10 +31,11 @@
30
31
  "node": ">=20"
31
32
  },
32
33
  "files": [
33
- "dist"
34
+ "dist",
35
+ "skills"
34
36
  ],
35
37
  "dependencies": {
36
- "@tanstack/hotkeys": "0.16.1",
38
+ "@tanstack/hotkeys": "0.18.0",
37
39
  "@tanstack/store": "^0.11.2"
38
40
  },
39
41
  "devDependencies": {
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: alpine-hotkeys
3
+ description: >-
4
+ Use when adding or fixing keyboard shortcuts in an Alpine.js app with
5
+ @tanstack/alpine-hotkeys: createHotkeysScope owners, the $hotkeys magic and
6
+ hotkeysPlugin, getter options, element targets with $refs, scopes activated
7
+ from the hotkey manager, app hotkeys, recorders, and held keys, or replacing
8
+ @keydown.window handlers.
9
+ metadata:
10
+ purpose: >-
11
+ Alpine bindings for TanStack Hotkeys: per-component owners, the $hotkeys
12
+ magic, getter options, manager-activated scopes, and the Alpine-specific
13
+ mistakes to avoid.
14
+ type: framework
15
+ framework: alpine
16
+ library: '@tanstack/alpine-hotkeys'
17
+ library_version: '0.4.0'
18
+ sources:
19
+ - src/createHotkeysScope.ts
20
+ - src/hotkeysPlugin.ts
21
+ - src/scope.ts
22
+ - src/createHotkey.ts
23
+ - src/createAppHotkeys.ts
24
+ - TanStack/hotkeys:docs/framework/alpine/quick-start.md
25
+ - TanStack/hotkeys:docs/framework/alpine/guides/app-hotkeys.md
26
+ requires:
27
+ - '@tanstack/hotkeys#hotkeys-core'
28
+ ---
29
+
30
+ # TanStack Hotkeys in Alpine
31
+
32
+ Before starting, load the `@tanstack/hotkeys#hotkeys-core` skill (`npx @tanstack/intent@latest load @tanstack/hotkeys#hotkeys-core`).
33
+
34
+ ```ts no-check
35
+ import Alpine from 'alpinejs'
36
+ import { createHotkeysScope } from '@tanstack/alpine-hotkeys'
37
+
38
+ Alpine.data('panel', () => ({
39
+ dirty: false,
40
+ hotkeys: createHotkeysScope(),
41
+ init() {
42
+ this.hotkeys.createHotkey(
43
+ 'Mod+S',
44
+ () => save(),
45
+ () => ({ enabled: this.dirty }),
46
+ )
47
+ this.hotkeys.createHotkey(
48
+ 'Escape',
49
+ () => closePanel(),
50
+ () => ({ target: this.$refs.panel }),
51
+ )
52
+ },
53
+ destroy() {
54
+ this.hotkeys.destroy()
55
+ },
56
+ }))
57
+ ```
58
+
59
+ For markup-only components, install `Alpine.plugin(hotkeysPlugin)` before `Alpine.start()` and use the `$hotkeys` magic, which is destroyed with its element: `x-init="$hotkeys.createHotkey('Mod+S', () => save())"`.
60
+
61
+ ## API
62
+
63
+ | API | What it's for |
64
+ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
65
+ | `Alpine.plugin(hotkeysPlugin)` | Install the `$hotkeys` magic, an owner that's destroyed with its element. |
66
+ | `createHotkeysScope(defaults?)` | Create an owner for one `Alpine.data` component, with optional default options. |
67
+ | `provideAppHotkeys(appHotkeys, options?)` | Declare app hotkeys and load the user's custom hotkeys once at startup, since Alpine has no provider. |
68
+ | `getHotkeyManager().activateScope(scope, options?)` | Turn on a scope, such as an exclusive one for a dialog; it returns the function that turns the scope off. |
69
+
70
+ Each owner has these methods. Read their state through `.value`, or `.hotkeys` for registrations.
71
+
72
+ | Method | What it's for |
73
+ | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
74
+ | `createHotkey(hotkey, callback, options?)` | Register one chord, sequence, or app hotkey until the owner is destroyed; options can be a getter. |
75
+ | `createHotkeys(definitions, commonOptions?)` | Register several hotkeys, or a getter of a list built from data, in one call. |
76
+ | `createHotkeyRecorder(options)` | Record a chord or sequence the user presses, for a settings screen. |
77
+ | `createAppHotkeys()` | Get the declared app hotkeys with the user's custom hotkeys applied. |
78
+ | `createAppHotkeyList()` | List every app hotkey with its status, for a cheat sheet or settings screen. |
79
+ | `createCustomHotkeys()` | Read, change, and reset the user's custom hotkeys, and check a new one for conflicts. |
80
+ | `createHotkeyRegistrations()` | List every registered hotkey with its options, `meta`, and `status` (whether it's enabled and in an active scope). |
81
+ | `createHotkeyScopesState()` | Get the active scopes and the exclusive scope that owns the keyboard. |
82
+ | `createHotkeyHint(hotkey, options?)` | Get 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. |
83
+ | `createKeyHold(key)` | Get whether one key is held. |
84
+ | `createHeldKeys()` | Get every key that's held right now. |
85
+ | `createHeldKeyCodes()` | Map each held key to its physical `event.code`. |
86
+ | `destroy()` | Release every hotkey, subscription, and recorder the owner created. |
87
+
88
+ The package also re-exports `@tanstack/hotkeys`, such as `formatForDisplay`, `matchesKeyboardEvent`, and `defineAppHotkeys`.
89
+
90
+ ## Pitfalls
91
+
92
+ - **Destroy each owner.** Owners aren't tied to Alpine's lifecycle, so without `this.hotkeys.destroy()` in `destroy()`, a removed component's hotkeys keep firing. Create one owner per component instance, inside the `Alpine.data` factory.
93
+ - **`createHotkeysScope` isn't a hotkey scope.** It only owns registrations and sets defaults. For a dialog, activate a real scope in `init()` with `release = getHotkeyManager().activateScope(name, { exclusive: true })`, pass `{ scope: name }` to the dialog's hotkeys, and call `release()` in `destroy()`. Give each open dialog its own name.
94
+ - **Pass options as a getter.** `{ enabled: this.dirty }` is read once. Use `() => ({ enabled: this.dirty })`.
95
+ - **Use either `$hotkeys` or your own owner for a hotkey, not both.** Otherwise it registers twice.
96
+ - With server rendering, load the adapter only in the browser.
97
+
98
+ When declaring app hotkeys, building a cheat sheet, or letting users rebind shortcuts, load `@tanstack/hotkeys#app-hotkeys` and read [app hotkeys in Alpine](references/app-hotkeys.md).
99
+
100
+ ## API discovery and docs
101
+
102
+ Signatures: `node_modules/@tanstack/alpine-hotkeys/dist/types.d.ts`. Guides: `https://tanstack.com/hotkeys/latest/docs/framework/alpine/guides/<page>.md`, such as `hotkeys` or `app-hotkeys`.
@@ -0,0 +1,74 @@
1
+ # App hotkeys in Alpine
2
+
3
+ The `@tanstack/hotkeys#app-hotkeys` skill owns the rules. This is the Alpine wiring.
4
+
5
+ ```ts
6
+ // app-hotkeys.ts
7
+ import { defineAppHotkeys } from '@tanstack/alpine-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/alpine-hotkeys' {
15
+ interface Register {
16
+ appHotkeys: typeof appHotkeys
17
+ }
18
+ }
19
+ ```
20
+
21
+ ```ts
22
+ // main.ts, before Alpine.start()
23
+ import { provideAppHotkeys } from '@tanstack/alpine-hotkeys'
24
+ import { appHotkeys } from './app-hotkeys'
25
+
26
+ provideAppHotkeys(appHotkeys, {
27
+ customHotkeys: JSON.parse(localStorage.getItem('hotkeys') ?? '{}'),
28
+ onCustomHotkeysChange: (custom) =>
29
+ localStorage.setItem('hotkeys', JSON.stringify(custom)),
30
+ })
31
+ ```
32
+
33
+ The app hotkey methods throw until `provideAppHotkeys` has run. When saved shortcuts come from a server, await them first. Register inside a getter so registrations follow rebinding: `this.hotkeys.createHotkey(() => appHotkeys.value['doc.save'], save)`, with `const appHotkeys = this.hotkeys.createAppHotkeys()`. Build a cheat sheet from `createAppHotkeyList().value`.
34
+
35
+ A settings row records with the owner's `createHotkeyRecorder`:
36
+
37
+ ```ts
38
+ import Alpine from 'alpinejs'
39
+ import { createHotkeysScope, formatForDisplay } from '@tanstack/alpine-hotkeys'
40
+ import type { AppHotkeyId } from '@tanstack/alpine-hotkeys'
41
+
42
+ Alpine.data('shortcutRow', (id: AppHotkeyId) => {
43
+ const hotkeys = createHotkeysScope()
44
+ const appHotkeys = hotkeys.createAppHotkeys()
45
+ const custom = hotkeys.createCustomHotkeys()
46
+ const recorder = hotkeys.createHotkeyRecorder({
47
+ maxSteps: Infinity,
48
+ validate: (hotkey) => {
49
+ const [conflict] = custom.findConflicts(id, hotkey)
50
+ return conflict ? `Used by ${conflict.meta?.name ?? conflict.id}` : true
51
+ },
52
+ onRecord: (hotkey) => custom.setCustomHotkey(id, hotkey),
53
+ onClear: () => custom.setCustomHotkey(id, null),
54
+ })
55
+
56
+ return {
57
+ recorder,
58
+ get label() {
59
+ const { meta, hotkey } = appHotkeys.value[id]
60
+ return `${meta?.name}: ${hotkey ? formatForDisplay(hotkey) : 'Unassigned'}`
61
+ },
62
+ reset: () => custom.resetCustomHotkey(id),
63
+ destroy: () => hotkeys.destroy(),
64
+ }
65
+ })
66
+ ```
67
+
68
+ ```html
69
+ <div x-data="shortcutRow('doc.save')">
70
+ <span x-text="label"></span>
71
+ <button @click="recorder.startRecording()">Change</button>
72
+ <button @click="reset()">Reset</button>
73
+ </div>
74
+ ```