Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit ab7e715

Browse files
authored
Merge pull request #9 from pyreon/docs/add-storage-hotkeys
docs: add storage, hotkeys, and useSubscription docs
2 parents f1473fa + ab142f6 commit ab7e715

3 files changed

Lines changed: 503 additions & 0 deletions

File tree

content/docs/hotkeys/index.mdx

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
---
2+
title: Hotkeys
3+
description: Reactive keyboard shortcut management for Pyreon — scope-aware, modifier keys, lifecycle-managed
4+
---
5+
6+
# @pyreon/hotkeys
7+
8+
Keyboard shortcut management with automatic lifecycle cleanup, scope-based activation, and modifier key support.
9+
10+
## Installation
11+
12+
```bash
13+
bun add @pyreon/hotkeys
14+
```
15+
16+
Peer dependencies: `@pyreon/core`, `@pyreon/reactivity`
17+
18+
## Quick Start
19+
20+
```tsx
21+
import { useHotkey } from '@pyreon/hotkeys'
22+
23+
function Editor() {
24+
useHotkey('mod+s', () => save(), { description: 'Save document' })
25+
useHotkey('mod+z', () => undo(), { description: 'Undo' })
26+
useHotkey('mod+shift+z', () => redo(), { description: 'Redo' })
27+
// Automatically unregistered when Editor unmounts
28+
}
29+
```
30+
31+
`mod` = ⌘ on Mac, Ctrl on Windows/Linux.
32+
33+
## Component Hook — `useHotkey()`
34+
35+
Registers a shortcut scoped to the component's lifecycle. Auto-unregisters on unmount.
36+
37+
```tsx
38+
import { useHotkey } from '@pyreon/hotkeys'
39+
40+
function App() {
41+
useHotkey('mod+k', () => openCommandPalette())
42+
useHotkey('escape', () => closeModal())
43+
useHotkey('ctrl+shift+p', () => openSettings(), {
44+
description: 'Open settings',
45+
preventDefault: true,
46+
})
47+
}
48+
```
49+
50+
### Options
51+
52+
| Option | Type | Default | Description |
53+
|--------|------|---------|-------------|
54+
| `scope` | `string` | `'global'` | Only fires when this scope is active |
55+
| `preventDefault` | `boolean` | `true` | Prevent default browser behavior |
56+
| `stopPropagation` | `boolean` | `false` | Stop event propagation |
57+
| `enableOnInputs` | `boolean` | `false` | Fire when input/textarea is focused |
58+
| `description` | `string` || For help dialogs |
59+
| `enabled` | `boolean \| () => boolean` | `true` | Dynamic enable/disable |
60+
61+
## Scopes — `useHotkeyScope()`
62+
63+
Scopes let you activate/deactivate groups of hotkeys based on UI context.
64+
65+
```tsx
66+
import { useHotkey, useHotkeyScope } from '@pyreon/hotkeys'
67+
68+
function Modal() {
69+
// Activate 'modal' scope while this component is mounted
70+
useHotkeyScope('modal')
71+
72+
// This only fires when the modal scope is active
73+
useHotkey('escape', () => closeModal(), { scope: 'modal' })
74+
useHotkey('enter', () => confirm(), { scope: 'modal' })
75+
}
76+
77+
function Editor() {
78+
useHotkeyScope('editor')
79+
80+
useHotkey('ctrl+s', () => save(), { scope: 'editor' })
81+
useHotkey('ctrl+z', () => undo(), { scope: 'editor' })
82+
}
83+
```
84+
85+
The `global` scope is always active. Custom scopes activate when `useHotkeyScope()` mounts and deactivate when it unmounts.
86+
87+
## Imperative API — `registerHotkey()`
88+
89+
For use outside components (e.g., in stores or at app init):
90+
91+
```tsx
92+
import { registerHotkey, enableScope, disableScope } from '@pyreon/hotkeys'
93+
94+
// Returns an unregister function
95+
const unregister = registerHotkey('ctrl+s', () => save(), {
96+
description: 'Save',
97+
})
98+
99+
// Manual scope management
100+
enableScope('editor')
101+
disableScope('editor')
102+
103+
// Later: cleanup
104+
unregister()
105+
```
106+
107+
## Modifier Keys
108+
109+
| Modifier | Keys |
110+
|----------|------|
111+
| `ctrl` | `ctrl`, `control` |
112+
| `shift` | `shift` |
113+
| `alt` | `alt` |
114+
| `meta` | `meta`, `cmd`, `command` |
115+
| `mod` | ⌘ on Mac, Ctrl elsewhere |
116+
117+
## Key Aliases
118+
119+
| Alias | Key |
120+
|-------|-----|
121+
| `esc` | `Escape` |
122+
| `return` | `Enter` |
123+
| `del` | `Delete` |
124+
| `ins` | `Insert` |
125+
| `space` | ` ` (space) |
126+
| `up/down/left/right` | Arrow keys |
127+
| `plus` | `+` |
128+
129+
## Input Filtering
130+
131+
By default, hotkeys are **ignored** when the user is typing in:
132+
- `<input>` elements
133+
- `<textarea>` elements
134+
- `<select>` elements
135+
- `contentEditable` elements
136+
137+
Override with `enableOnInputs: true`:
138+
139+
```tsx
140+
// This fires even when typing in an input
141+
useHotkey('escape', () => blur(), { enableOnInputs: true })
142+
```
143+
144+
## Dynamic Enable/Disable
145+
146+
```tsx
147+
const canSave = computed(() => hasChanges() && !isSaving())
148+
149+
useHotkey('mod+s', () => save(), {
150+
enabled: () => canSave(),
151+
description: 'Save (only when changes exist)',
152+
})
153+
```
154+
155+
## Help Dialogs
156+
157+
Build keyboard shortcut help screens with `getRegisteredHotkeys()`:
158+
159+
```tsx
160+
import { getRegisteredHotkeys, formatCombo, parseShortcut } from '@pyreon/hotkeys'
161+
162+
function ShortcutHelp() {
163+
const hotkeys = getRegisteredHotkeys()
164+
165+
return (
166+
<table>
167+
<thead><tr><th>Shortcut</th><th>Description</th></tr></thead>
168+
<tbody>
169+
{hotkeys
170+
.filter(h => h.description)
171+
.map(h => (
172+
<tr>
173+
<td><kbd>{formatCombo(parseShortcut(h.shortcut))}</kbd></td>
174+
<td>{h.description}</td>
175+
</tr>
176+
))}
177+
</tbody>
178+
</table>
179+
)
180+
}
181+
```
182+
183+
## Utilities
184+
185+
```tsx
186+
import { parseShortcut, formatCombo, matchesCombo } from '@pyreon/hotkeys'
187+
188+
// Parse a shortcut string into a KeyCombo
189+
const combo = parseShortcut('ctrl+shift+s')
190+
// { ctrl: true, shift: true, alt: false, meta: false, key: 's' }
191+
192+
// Format back to human-readable
193+
formatCombo(combo) // 'Ctrl+Shift+S'
194+
195+
// Check if a KeyboardEvent matches
196+
matchesCombo(event, combo) // true/false
197+
```

content/docs/query/index.mdx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,86 @@ Type re-exports include `QueryKey`, `QueryFilters`, `MutationFilters`, `Dehydrat
12941294
| `QueryErrorResetBoundary` | Error reset boundary component |
12951295
| `useQueryErrorResetBoundary` | Access error reset function |
12961296

1297+
## WebSocket Subscriptions — `useSubscription()`
1298+
1299+
Connect a WebSocket to the query cache for realtime data updates. Auto-reconnects with exponential backoff.
1300+
1301+
```tsx
1302+
import { useSubscription } from '@pyreon/query'
1303+
1304+
function OrdersDashboard() {
1305+
const sub = useSubscription({
1306+
url: 'wss://api.example.com/ws',
1307+
onMessage: (event, queryClient) => {
1308+
const data = JSON.parse(event.data)
1309+
if (data.type === 'order-updated') {
1310+
queryClient.invalidateQueries({ queryKey: ['orders'] })
1311+
}
1312+
if (data.type === 'order-created') {
1313+
queryClient.setQueryData(['orders', data.order.id], data.order)
1314+
}
1315+
},
1316+
})
1317+
1318+
return (
1319+
<div>
1320+
<p>Status: {() => sub.status()}</p>
1321+
{/* sub.status(): 'connecting' | 'connected' | 'disconnected' | 'error' */}
1322+
</div>
1323+
)
1324+
}
1325+
```
1326+
1327+
### Subscription Options
1328+
1329+
| Option | Type | Default | Description |
1330+
| --- | --- | --- | --- |
1331+
| `url` | `string \| () => string` | required | WebSocket URL (can be reactive) |
1332+
| `protocols` | `string \| string[]` || WebSocket sub-protocols |
1333+
| `onMessage` | `(event, queryClient) => void` | required | Message handler with query client access |
1334+
| `onOpen` | `(event) => void` || Connection opened callback |
1335+
| `onClose` | `(event) => void` || Connection closed callback |
1336+
| `onError` | `(event) => void` || Error callback |
1337+
| `reconnect` | `boolean` | `true` | Auto-reconnect on disconnect |
1338+
| `reconnectDelay` | `number` | `1000` | Initial reconnect delay (ms), doubles each retry |
1339+
| `maxReconnectAttempts` | `number` | `10` | Max reconnect attempts (0 = unlimited) |
1340+
| `enabled` | `boolean \| () => boolean` | `true` | Enable/disable the connection |
1341+
1342+
### Return Value
1343+
1344+
| Property | Type | Description |
1345+
| --- | --- | --- |
1346+
| `status` | `Signal<SubscriptionStatus>` | Current connection status |
1347+
| `send` | `(data) => void` | Send data through the WebSocket |
1348+
| `close` | `() => void` | Close the connection |
1349+
| `reconnect` | `() => void` | Manually reconnect |
1350+
1351+
### Reactive URL
1352+
1353+
```tsx
1354+
const channel = signal('orders')
1355+
1356+
useSubscription({
1357+
url: () => `wss://api.example.com/ws/${channel()}`,
1358+
onMessage: (event, qc) => { /* ... */ },
1359+
})
1360+
1361+
// Changing channel automatically reconnects to the new URL
1362+
channel.set('inventory')
1363+
```
1364+
1365+
### Conditional Connection
1366+
1367+
```tsx
1368+
const isAuthenticated = computed(() => !!token())
1369+
1370+
useSubscription({
1371+
url: 'wss://api.example.com/ws',
1372+
enabled: () => isAuthenticated(),
1373+
onMessage: (event, qc) => { /* ... */ },
1374+
})
1375+
```
1376+
12971377
## Type Exports
12981378

12991379
| Type | Description |
@@ -1307,3 +1387,6 @@ Type re-exports include `QueryKey`, `QueryFilters`, `MutationFilters`, `Dehydrat
13071387
| `QuerySuspenseProps` | Props for `QuerySuspense` |
13081388
| `QueryClientProviderProps` | Props for `QueryClientProvider` |
13091389
| `QueryErrorResetBoundaryProps` | Props for `QueryErrorResetBoundary` |
1390+
| `UseSubscriptionOptions` | Options for `useSubscription` |
1391+
| `UseSubscriptionResult` | Return type of `useSubscription` |
1392+
| `SubscriptionStatus` | `'connecting' \| 'connected' \| 'disconnected' \| 'error'` |

0 commit comments

Comments
 (0)