@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.
- package/README.md +95 -27
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,63 +1,131 @@
|
|
|
1
1
|
# @pyreon/hotkeys
|
|
2
2
|
|
|
3
|
-
Reactive keyboard shortcut management
|
|
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
|
|
13
|
+
## Quick start
|
|
12
14
|
|
|
13
15
|
```tsx
|
|
14
16
|
import { useHotkey, useHotkeyScope } from '@pyreon/hotkeys'
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
useHotkey('mod+
|
|
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
|
|
21
|
-
useHotkeyScope('editor')
|
|
22
|
-
useHotkey('
|
|
23
|
-
useHotkey('
|
|
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` =
|
|
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
|
-
##
|
|
32
|
+
## Hooks
|
|
29
33
|
|
|
30
34
|
### `useHotkey(shortcut, handler, options?)`
|
|
31
35
|
|
|
32
|
-
Component-
|
|
36
|
+
Component-lifecycle shortcut. Registers on mount, unregisters on unmount.
|
|
33
37
|
|
|
34
|
-
|
|
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.
|
|
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
|
-
|
|
90
|
+
For building custom UIs (settings panels, help overlays):
|
|
41
91
|
|
|
42
|
-
|
|
92
|
+
```ts
|
|
93
|
+
import { parseShortcut, formatCombo, matchesCombo } from '@pyreon/hotkeys'
|
|
43
94
|
|
|
44
|
-
|
|
95
|
+
const combo = parseShortcut('mod+shift+s')
|
|
96
|
+
// { ctrl: false, shift: true, alt: false, meta: true, key: 's' } (on Mac)
|
|
45
97
|
|
|
46
|
-
|
|
98
|
+
formatCombo(combo)
|
|
99
|
+
// '⌘+Shift+S' (on Mac) or 'Ctrl+Shift+S' (elsewhere)
|
|
47
100
|
|
|
48
|
-
|
|
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
|
-
|
|
115
|
+
Clears every registered hotkey and active scope. Underscore-prefixed because it's only meant for test environments.
|
|
51
116
|
|
|
52
|
-
|
|
117
|
+
## Gotchas
|
|
53
118
|
|
|
54
|
-
|
|
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
|
-
|
|
126
|
+
## Documentation
|
|
57
127
|
|
|
58
|
-
|
|
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.
|
|
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.
|
|
46
|
+
"@pyreon/core": "^0.23.0",
|
|
47
47
|
"@pyreon/manifest": "0.13.1",
|
|
48
|
-
"@pyreon/reactivity": "^0.
|
|
49
|
-
"@vitus-labs/tools-lint": "^2.
|
|
48
|
+
"@pyreon/reactivity": "^0.23.0",
|
|
49
|
+
"@vitus-labs/tools-lint": "^2.4.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@pyreon/core": "^0.
|
|
53
|
-
"@pyreon/reactivity": "^0.
|
|
52
|
+
"@pyreon/core": "^0.23.0",
|
|
53
|
+
"@pyreon/reactivity": "^0.23.0"
|
|
54
54
|
}
|
|
55
55
|
}
|