@pyreon/hotkeys 0.22.0 → 0.23.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.
Files changed (2) hide show
  1. package/README.md +95 -27
  2. package/package.json +6 -6
package/README.md CHANGED
@@ -1,63 +1,131 @@
1
1
  # @pyreon/hotkeys
2
2
 
3
- Reactive keyboard shortcut management for Pyreon. Scope-aware, modifier keys, conflict detection, automatic lifecycle cleanup.
3
+ Reactive keyboard shortcut management scope-aware, platform-aware, lifecycle-cleaned.
4
+
5
+ Register global or component-scoped keyboard shortcuts with automatic unregistration on unmount. Supports modifier keys (`ctrl` / `shift` / `alt` / `meta` / `cmd` / `command` / `mod`), scope-based activation (only fire while a scope is active), input filtering (off by default in form fields), and the `mod` modifier that maps to `⌘` on Mac and `Ctrl` everywhere else. Pairs naturally with `@pyreon/router` (per-route scopes), `@pyreon/dialog` (modal scope), and command-palette UIs.
4
6
 
5
7
  ## Install
6
8
 
7
9
  ```bash
8
- bun add @pyreon/hotkeys
10
+ bun add @pyreon/hotkeys @pyreon/core @pyreon/reactivity
9
11
  ```
10
12
 
11
- ## Quick Start
13
+ ## Quick start
12
14
 
13
15
  ```tsx
14
16
  import { useHotkey, useHotkeyScope } from '@pyreon/hotkeys'
15
17
 
16
- // Global shortcut — auto-unregisters on unmount
17
- useHotkey('mod+s', () => save(), { description: 'Save document' })
18
- useHotkey('mod+k', () => openCommandPalette())
18
+ function Editor() {
19
+ // Global shortcut auto-unregisters on unmount
20
+ useHotkey('mod+s', () => save(), { description: 'Save document' })
21
+ useHotkey('mod+k', () => openCommandPalette())
19
22
 
20
- // Scoped shortcuts — only active when scope is enabled
21
- useHotkeyScope('editor')
22
- useHotkey('ctrl+z', () => undo(), { scope: 'editor' })
23
- useHotkey('escape', () => closeModal(), { scope: 'modal' })
23
+ // Scoped shortcuts — only fire when the `editor` scope is active
24
+ useHotkeyScope('editor')
25
+ useHotkey('mod+z', () => undo(), { scope: 'editor' })
26
+ useHotkey('mod+shift+z', () => redo(), { scope: 'editor' })
27
+ }
24
28
  ```
25
29
 
26
- `mod` = Command on Mac, Ctrl elsewhere. Shortcuts are ignored in input elements by default.
30
+ `mod` = `⌘` on Mac, `Ctrl` everywhere else. Shortcuts are ignored when typing in `<input>` / `<textarea>` / `contenteditable` by default (`enableOnInputs: true` to opt in).
27
31
 
28
- ## API
32
+ ## Hooks
29
33
 
30
34
  ### `useHotkey(shortcut, handler, options?)`
31
35
 
32
- Component-scoped keyboard shortcut. Automatically unregisters on unmount.
36
+ Component-lifecycle shortcut. Registers on mount, unregisters on unmount.
33
37
 
34
- Options: `scope`, `description`, `preventDefault` (default: true), `enableInInputs` (default: false).
38
+ ```ts
39
+ useHotkey('escape', () => setOpen(false), { scope: 'modal', preventDefault: true })
40
+ ```
35
41
 
36
42
  ### `useHotkeyScope(scope)`
37
43
 
38
- Activate a scope for the component's lifetime. Deactivates on unmount.
44
+ Activate a scope for the component's lifetime. Multiple scopes can be active concurrently; a hotkey fires when ITS scope is active.
45
+
46
+ ```tsx
47
+ function Modal() {
48
+ useHotkeyScope('modal')
49
+ useHotkey('escape', close, { scope: 'modal' })
50
+ useHotkey('tab', focusNext, { scope: 'modal' })
51
+ }
52
+ ```
53
+
54
+ ## Imperative API
55
+
56
+ For non-component use (one-off registration, dynamic shortcuts read from settings, etc.):
57
+
58
+ | Function | Notes |
59
+ |---|---|
60
+ | `registerHotkey(shortcut, handler, options?)` | Returns an `unregister()` function — call manually |
61
+ | `enableScope(scope)` / `disableScope(scope)` | Imperative scope control |
62
+ | `getActiveScopes()` | Currently active scope names |
63
+ | `getRegisteredHotkeys(): HotkeyEntry[]` | All registered shortcuts — useful for help dialogs |
64
+
65
+ ## Options
66
+
67
+ ```ts
68
+ interface HotkeyOptions {
69
+ scope?: string // default: 'global'
70
+ preventDefault?: boolean // default: true
71
+ stopPropagation?: boolean // default: false
72
+ enableOnInputs?: boolean // default: false
73
+ description?: string // for help dialogs
74
+ enabled?: boolean | (() => boolean) // reactive — re-evaluated each fire
75
+ }
76
+ ```
77
+
78
+ The `enabled` accessor lets the hotkey gate on any reactive condition (`enabled: () => !isLoading()`) without re-registering on every change.
79
+
80
+ ## Shortcut syntax
81
+
82
+ Plus-separated, case-insensitive: `ctrl+shift+s`, `mod+k`, `alt+enter`.
83
+
84
+ Modifiers: `ctrl` / `control`, `shift`, `alt`, `meta` / `cmd` / `command`, `mod` (cross-platform).
85
+
86
+ Key aliases: `esc` → `escape`, `return` → `enter`, `del` → `delete`, `ins` → `insert`, `space` / `spacebar` → ` `, `up` / `down` / `left` / `right` → `arrowup` / `arrowdown` / …, `plus` → `+`.
87
+
88
+ ## Parsing utilities
39
89
 
40
- ### `registerHotkey(shortcut, handler, options?)`
90
+ For building custom UIs (settings panels, help overlays):
41
91
 
42
- Imperative registration. Returns an unregister function.
92
+ ```ts
93
+ import { parseShortcut, formatCombo, matchesCombo } from '@pyreon/hotkeys'
43
94
 
44
- ### `enableScope(scope)` / `disableScope(scope)`
95
+ const combo = parseShortcut('mod+shift+s')
96
+ // { ctrl: false, shift: true, alt: false, meta: true, key: 's' } (on Mac)
45
97
 
46
- Manually control which scopes are active.
98
+ formatCombo(combo)
99
+ // '⌘+Shift+S' (on Mac) or 'Ctrl+Shift+S' (elsewhere)
47
100
 
48
- ### `getRegisteredHotkeys()`
101
+ window.addEventListener('keydown', (e) => {
102
+ if (matchesCombo(e, combo)) save()
103
+ })
104
+ ```
105
+
106
+ ## Testing
107
+
108
+ ```ts
109
+ import { _resetHotkeys } from '@pyreon/hotkeys'
110
+ import { afterEach } from 'vitest'
111
+
112
+ afterEach(_resetHotkeys)
113
+ ```
49
114
 
50
- List all registered hotkeys useful for help dialogs.
115
+ Clears every registered hotkey and active scope. Underscore-prefixed because it's only meant for test environments.
51
116
 
52
- ### `getActiveScopes()`
117
+ ## Gotchas
53
118
 
54
- List currently active scopes.
119
+ - **Scopes are NOT hierarchical** — activating `'editor'` does not implicitly activate `'editor/code'`. A hotkey fires only when its exact scope string is active.
120
+ - **`'global'` is the default scope** and is always active. A hotkey with no `scope` option fires whenever the global scope is active (which is always, unless you disable it).
121
+ - **Multiple scopes can fire simultaneously** — if both `'modal'` and `'global'` are active and both have a `'mod+s'` binding, both handlers fire. Use `stopPropagation: true` or different scopes to disambiguate.
122
+ - **`enableOnInputs: true` is required** to let users trigger shortcuts while typing — by default the listener checks `document.activeElement` and bails on form fields / `contenteditable`.
123
+ - **`enabled` is re-evaluated on every dispatch** — pass a function for reactive gating; a static `false` is equivalent to never registering.
124
+ - **The hotkey listener attaches to `window`** at first registration and detaches when the last hotkey is removed (`_resetHotkeys` or every `unregister()` called).
55
125
 
56
- ### Utilities
126
+ ## Documentation
57
127
 
58
- - `parseShortcut(str)` parse shortcut string into `KeyCombo`
59
- - `formatCombo(combo)` — format `KeyCombo` as display string
60
- - `matchesCombo(event, combo)` — check if a keyboard event matches a combo
128
+ Full docs: [docs.pyreon.dev/docs/hotkeys](https://docs.pyreon.dev/docs/hotkeys) (or `docs/docs/hotkeys.md` in this repo).
61
129
 
62
130
  ## License
63
131
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/hotkeys",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Reactive keyboard shortcut management for Pyreon — scope-aware, conflict detection",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/hotkeys#readme",
6
6
  "bugs": {
@@ -43,13 +43,13 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@happy-dom/global-registrator": "^20.8.9",
46
- "@pyreon/core": "^0.22.0",
46
+ "@pyreon/core": "^0.23.0",
47
47
  "@pyreon/manifest": "0.13.1",
48
- "@pyreon/reactivity": "^0.22.0",
49
- "@vitus-labs/tools-lint": "^2.3.0"
48
+ "@pyreon/reactivity": "^0.23.0",
49
+ "@vitus-labs/tools-lint": "^2.4.0"
50
50
  },
51
51
  "dependencies": {
52
- "@pyreon/core": "^0.22.0",
53
- "@pyreon/reactivity": "^0.22.0"
52
+ "@pyreon/core": "^0.23.0",
53
+ "@pyreon/reactivity": "^0.23.0"
54
54
  }
55
55
  }