preact-missing-hooks 2.1.0 → 3.0.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 CHANGED
@@ -32,6 +32,7 @@ A lightweight, extendable collection of React-like hooks for Preact, including u
32
32
  - **`useThreadedWorker`** — Run async work in a queue with **sequential** (single worker, priority-ordered) or **parallel** (worker pool) mode. Optional priority (1 = highest); FIFO within same priority.
33
33
  - **`useIndexedDB`** — IndexedDB abstraction with database/table init, insert, update, delete, exists, query (cursor + filter), upsert, bulk insert, clear, count, and full transaction support. Singleton connection, Promise-based API, optional `onSuccess`/`onError` callbacks.
34
34
  - **`useWebRTCIP`** — Detects client IP addresses using WebRTC ICE candidates and a STUN server (frontend-only). **Not highly reliable**; use as a first-priority hint and fall back to a public IP API (e.g. [ipapi.co](https://ipapi.co), [ipify](https://www.ipify.org), [ip-api.com](https://ip-api.com)) when it fails or returns empty.
35
+ - **`useWasmCompute`** — Runs WebAssembly computation off the main thread via a Web Worker. Flow: Preact Component → useWasmCompute() → Web Worker → WASM Module → return result. Validates environment (browser, Worker, WebAssembly) and returns `compute(input)`, `result`, `loading`, `error`, `ready`.
35
36
  - Fully TypeScript compatible
36
37
  - Bundled with Microbundle
37
38
  - Zero dependencies (except `preact`)
@@ -57,8 +58,9 @@ npm install preact-missing-hooks
57
58
  import { useThreadedWorker } from 'preact-missing-hooks/useThreadedWorker'
58
59
  import { useClipboard } from 'preact-missing-hooks/useClipboard'
59
60
  import { useWebRTCIP } from 'preact-missing-hooks/useWebRTCIP'
61
+ import { useWasmCompute } from 'preact-missing-hooks/useWasmCompute'
60
62
  ```
61
- All hooks are available: `useTransition`, `useMutationObserver`, `useEventBus`, `useWrappedChildren`, `usePreferredTheme`, `useNetworkState`, `useClipboard`, `useRageClick`, `useThreadedWorker`, `useIndexedDB`, `useWebRTCIP`.
63
+ All hooks are available: `useTransition`, `useMutationObserver`, `useEventBus`, `useWrappedChildren`, `usePreferredTheme`, `useNetworkState`, `useClipboard`, `useRageClick`, `useThreadedWorker`, `useIndexedDB`, `useWebRTCIP`, `useWasmCompute`.
62
64
 
63
65
  ---
64
66
 
@@ -415,6 +417,43 @@ function ClientIP() {
415
417
 
416
418
  ---
417
419
 
420
+ ### `useWasmCompute`
421
+
422
+ Runs WebAssembly computation in a Web Worker so the main thread stays responsive. Flow: **Preact Component → useWasmCompute() → Web Worker → WASM Module → return result.** The hook checks that the environment supports `window`, `Worker`, and `WebAssembly`; in SSR or unsupported environments it sets `error` and leaves `ready` false.
423
+
424
+ Returns `{ compute, result, loading, error, ready }`. Options: `wasmUrl` (required), `exportName` (default `'compute'`), optional `workerUrl` (custom worker script), optional `importObject` (must be serializable for the default worker).
425
+
426
+ ```tsx
427
+ import { useWasmCompute } from 'preact-missing-hooks'
428
+
429
+ function AddWithWasm() {
430
+ const { compute, result, loading, error, ready } = useWasmCompute<
431
+ number,
432
+ number
433
+ >({
434
+ wasmUrl: '/add.wasm',
435
+ exportName: 'add',
436
+ })
437
+
438
+ const handleClick = () => {
439
+ if (ready) compute(2).then(() => {})
440
+ }
441
+
442
+ if (error) return <p>WASM unavailable: {error}</p>
443
+ if (!ready) return <p>Loading WASM…</p>
444
+ return (
445
+ <div>
446
+ <button onClick={handleClick} disabled={loading}>
447
+ Add 2
448
+ </button>
449
+ {result != null && <p>Result: {result}</p>}
450
+ </div>
451
+ )
452
+ }
453
+ ```
454
+
455
+ ---
456
+
418
457
  ## Built With
419
458
 
420
459
  - [Preact](https://preactjs.com)
package/demo/add.wasm ADDED
Binary file
@@ -0,0 +1,242 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Preact Missing Hooks — Demo</title>
7
+ <script type="importmap">
8
+ {
9
+ "imports": {
10
+ "preact": "https://esm.sh/preact@10",
11
+ "preact/hooks": "https://esm.sh/preact@10/hooks"
12
+ }
13
+ }
14
+ </script>
15
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
16
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
17
+ <link
18
+ href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap"
19
+ rel="stylesheet"
20
+ />
21
+ <style>
22
+ :root {
23
+ --bg: #0f0f12;
24
+ --surface: #18181c;
25
+ --surface2: #222228;
26
+ --border: #2e2e36;
27
+ --text: #e4e4e7;
28
+ --textMuted: #a1a1aa;
29
+ --accent: #7c3aed;
30
+ --accentDim: rgba(124, 58, 237, 0.15);
31
+ --green: #22c55e;
32
+ --greenDim: rgba(34, 197, 94, 0.12);
33
+ --amber: #f59e0b;
34
+ --red: #ef4444;
35
+ --radius: 10px;
36
+ --radiusSm: 6px;
37
+ }
38
+ * {
39
+ box-sizing: border-box;
40
+ }
41
+ body {
42
+ margin: 0;
43
+ font-family: 'DM Sans', system-ui, sans-serif;
44
+ background: var(--bg);
45
+ color: var(--text);
46
+ line-height: 1.5;
47
+ min-height: 100vh;
48
+ }
49
+ .page-header {
50
+ padding: 2rem 1.5rem;
51
+ text-align: center;
52
+ border-bottom: 1px solid var(--border);
53
+ }
54
+ .page-header h1 {
55
+ margin: 0;
56
+ font-size: 1.75rem;
57
+ font-weight: 700;
58
+ letter-spacing: -0.02em;
59
+ }
60
+ .page-header p {
61
+ margin: 0.5rem 0 0;
62
+ color: var(--textMuted);
63
+ font-size: 0.95rem;
64
+ }
65
+ .container {
66
+ max-width: 1200px;
67
+ margin: 0 auto;
68
+ padding: 1.5rem;
69
+ }
70
+ .hook-section {
71
+ margin-bottom: 3rem;
72
+ }
73
+ .hook-section h2 {
74
+ margin: 0 0 0.5rem;
75
+ font-size: 1.35rem;
76
+ font-weight: 600;
77
+ color: var(--text);
78
+ }
79
+ .flow {
80
+ display: inline-block;
81
+ margin-bottom: 0.5rem;
82
+ padding: 0.25rem 0.6rem;
83
+ font-size: 0.75rem;
84
+ font-family: 'JetBrains Mono', monospace;
85
+ color: var(--accent);
86
+ background: var(--accentDim);
87
+ border-radius: var(--radiusSm);
88
+ }
89
+ .summary {
90
+ margin: 0 0 1rem;
91
+ font-size: 0.9rem;
92
+ color: var(--textMuted);
93
+ max-width: 60ch;
94
+ }
95
+ .cards {
96
+ display: grid;
97
+ grid-template-columns: 1fr 1fr;
98
+ gap: 1rem;
99
+ }
100
+ @media (max-width: 800px) {
101
+ .cards {
102
+ grid-template-columns: 1fr;
103
+ }
104
+ }
105
+ .card {
106
+ background: var(--surface);
107
+ border: 1px solid var(--border);
108
+ border-radius: var(--radius);
109
+ overflow: hidden;
110
+ }
111
+ .card-title {
112
+ padding: 0.5rem 0.75rem;
113
+ font-size: 0.75rem;
114
+ font-weight: 600;
115
+ text-transform: uppercase;
116
+ letter-spacing: 0.05em;
117
+ color: var(--textMuted);
118
+ border-bottom: 1px solid var(--border);
119
+ }
120
+ .card-code {
121
+ padding: 0.75rem;
122
+ font-family: 'JetBrains Mono', monospace;
123
+ font-size: 0.8rem;
124
+ line-height: 1.45;
125
+ color: var(--textMuted);
126
+ background: #0d0d0f;
127
+ overflow-x: auto;
128
+ white-space: pre;
129
+ }
130
+ .card-live {
131
+ padding: 1rem;
132
+ min-height: 80px;
133
+ }
134
+ .card-live .status {
135
+ font-size: 0.85rem;
136
+ }
137
+ .card-live button {
138
+ padding: 0.4rem 0.75rem;
139
+ margin: 0.2rem 0.2rem 0 0;
140
+ font-family: inherit;
141
+ font-size: 0.85rem;
142
+ background: var(--surface2);
143
+ border: 1px solid var(--border);
144
+ border-radius: var(--radiusSm);
145
+ color: var(--text);
146
+ cursor: pointer;
147
+ }
148
+ .card-live button:hover {
149
+ background: var(--border);
150
+ }
151
+ .card-live button:disabled {
152
+ opacity: 0.5;
153
+ cursor: not-allowed;
154
+ }
155
+ .badge {
156
+ display: inline-block;
157
+ padding: 0.2rem 0.5rem;
158
+ border-radius: 4px;
159
+ font-size: 0.8rem;
160
+ }
161
+ .badge.green {
162
+ background: var(--greenDim);
163
+ color: var(--green);
164
+ }
165
+ .badge.amber {
166
+ color: var(--amber);
167
+ }
168
+ /* IndexedDB demo: reactive table + action buttons */
169
+ .idb-actions {
170
+ display: flex;
171
+ flex-wrap: wrap;
172
+ gap: 0.35rem;
173
+ margin-bottom: 0.75rem;
174
+ }
175
+ .idb-table-viz {
176
+ background: var(--surface2);
177
+ border-radius: var(--radiusSm);
178
+ padding: 0.5rem;
179
+ font-size: 0.8rem;
180
+ max-height: 180px;
181
+ overflow-y: auto;
182
+ }
183
+ .idb-row {
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: space-between;
187
+ padding: 0.35rem 0.5rem;
188
+ border-radius: 4px;
189
+ margin-bottom: 2px;
190
+ }
191
+ .idb-row:nth-child(odd) {
192
+ background: rgba(255, 255, 255, 0.03);
193
+ }
194
+ .idb-row-actions {
195
+ display: flex;
196
+ gap: 0.25rem;
197
+ }
198
+ .idb-count {
199
+ margin-bottom: 0.5rem;
200
+ font-weight: 600;
201
+ color: var(--textMuted);
202
+ }
203
+ /* useWasmCompute flow diagram */
204
+ .wasm-flow {
205
+ display: flex;
206
+ align-items: center;
207
+ justify-content: center;
208
+ flex-wrap: wrap;
209
+ gap: 0.25rem;
210
+ padding: 0.75rem;
211
+ margin-bottom: 0.75rem;
212
+ background: var(--surface2);
213
+ border-radius: var(--radiusSm);
214
+ }
215
+ .wasm-flow-node {
216
+ padding: 0.3rem 0.5rem;
217
+ font-size: 0.7rem;
218
+ font-family: 'JetBrains Mono', monospace;
219
+ border-radius: 4px;
220
+ border: 1px solid var(--border);
221
+ color: var(--textMuted);
222
+ transition: background 0.2s, border-color 0.2s;
223
+ }
224
+ .wasm-flow-node.active {
225
+ border-color: var(--accent);
226
+ background: var(--accentDim);
227
+ color: var(--accent);
228
+ }
229
+ .wasm-flow-arrow {
230
+ color: var(--textMuted);
231
+ font-size: 0.75rem;
232
+ }
233
+ </style>
234
+ </head>
235
+ <body>
236
+ <header class="page-header">
237
+ <h1>Preact Missing Hooks</h1>
238
+ </header>
239
+ <main class="container" id="root"></main>
240
+ <script type="module" src="./main.js"></script>
241
+ </body>
242
+ </html>
package/demo/main.js ADDED
@@ -0,0 +1,312 @@
1
+ import { h, render } from 'https://esm.sh/preact@10';
2
+ import { useState, useEffect, useRef } from 'https://esm.sh/preact@10/hooks';
3
+ import {
4
+ useTransition,
5
+ useMutationObserver,
6
+ useEventBus,
7
+ useWrappedChildren,
8
+ usePreferredTheme,
9
+ useNetworkState,
10
+ useClipboard,
11
+ useRageClick,
12
+ useThreadedWorker,
13
+ useIndexedDB,
14
+ useWebRTCIP,
15
+ useWasmCompute,
16
+ } from 'https://unpkg.com/preact-missing-hooks/dist/index.module.js';
17
+
18
+ // ——— Hook demo components ———
19
+
20
+ function DemoTransition() {
21
+ const [startTransition, isPending] = useTransition();
22
+ const [count, setCount] = useState(0);
23
+ return h('div', {},
24
+ h('button', { onClick: () => startTransition(() => setCount((c) => c + 1)) }, 'Increment'),
25
+ ' ',
26
+ isPending ? h('span', { class: 'badge amber' }, 'Pending…') : null,
27
+ h('span', { style: { marginLeft: '0.5rem' } }, 'Count: ' + count)
28
+ );
29
+ }
30
+
31
+ function DemoMutationObserver() {
32
+ const ref = useRef(null);
33
+ const [log, setLog] = useState([]);
34
+ useMutationObserver(ref, (mutations) => {
35
+ setLog((prev) => [...prev.slice(-2), mutations.length + ' mutation(s)']);
36
+ }, { childList: true, subtree: true });
37
+ return h('div', {},
38
+ h('div', { ref, style: { padding: '0.5rem', background: 'var(--surface2)', borderRadius: '6px', marginBottom: '0.5rem' } },
39
+ h('span', {}, 'Watch this area → '),
40
+ h('button', { onClick: () => { const el = ref.current; if (el) { const s = document.createElement('span'); s.textContent = ' +new'; el.appendChild(s); } } }, 'Add node')
41
+ ),
42
+ h('div', { class: 'status' }, log.length ? log.join(' · ') : 'No mutations yet')
43
+ );
44
+ }
45
+
46
+ function DemoEventBus() {
47
+ const { emit, on } = useEventBus();
48
+ const [msg, setMsg] = useState('');
49
+ useEffect(() => {
50
+ return on('greet', setMsg);
51
+ }, [on]);
52
+ return h('div', {},
53
+ h('button', { onClick: () => emit('greet', 'Hello from bus!') }, 'Emit greet'),
54
+ msg ? h('span', { style: { marginLeft: '0.5rem' }, class: 'badge green' }, msg) : null
55
+ );
56
+ }
57
+
58
+ function DemoWrappedChildren() {
59
+ const children = [h('button', {}, 'A'), h('button', {}, 'B')];
60
+ const wrapped = useWrappedChildren(children, { style: { marginRight: '0.25rem' } });
61
+ return h('div', {}, wrapped);
62
+ }
63
+
64
+ function DemoPreferredTheme() {
65
+ const theme = usePreferredTheme();
66
+ return h('div', {}, h('span', { class: 'badge ' + (theme === 'dark' ? 'green' : 'amber') }, theme));
67
+ }
68
+
69
+ function DemoNetworkState() {
70
+ const state = useNetworkState();
71
+ return h('div', { class: 'status' },
72
+ state.online ? h('span', { class: 'badge green' }, 'Online') : h('span', { class: 'badge', style: { background: 'var(--red)', color: '#fff' } }, 'Offline'),
73
+ state.effectiveType ? ' ' + state.effectiveType : ''
74
+ );
75
+ }
76
+
77
+ function DemoClipboard() {
78
+ const { copy, paste, copied, error } = useClipboard();
79
+ const [pasted, setPasted] = useState('');
80
+ return h('div', {},
81
+ h('button', { onClick: () => copy('Hello from useClipboard!') }, copied ? 'Copied!' : 'Copy'),
82
+ ' ',
83
+ h('button', { onClick: () => paste().then(setPasted) }, 'Paste'),
84
+ pasted ? h('div', { style: { marginTop: '0.5rem', fontSize: '0.85rem' } }, 'Pasted: ' + pasted) : null,
85
+ error ? h('div', { style: { color: 'var(--red)', fontSize: '0.8rem' } }, error.message) : null
86
+ );
87
+ }
88
+
89
+ function DemoRageClick() {
90
+ const ref = useRef(null);
91
+ const [count, setCount] = useState(0);
92
+ useRageClick(ref, { onRageClick: () => setCount((c) => c + 1), threshold: 3 });
93
+ return h('div', {},
94
+ h('div', { ref, style: { padding: '1rem', background: 'var(--surface2)', borderRadius: '6px', cursor: 'pointer', userSelect: 'none' } }, 'Click here 3+ times fast (rage click)'),
95
+ count ? h('div', { class: 'badge amber', style: { marginTop: '0.5rem' } }, 'Rage clicks: ' + count) : null
96
+ );
97
+ }
98
+
99
+ function DemoThreadedWorker() {
100
+ const { run, loading, result } = useThreadedWorker(
101
+ (x) => new Promise((r) => setTimeout(() => r('Result: ' + x), 500)),
102
+ { mode: 'sequential' }
103
+ );
104
+ return h('div', {},
105
+ h('button', { onClick: () => run('task'), disabled: loading }, loading ? 'Running…' : 'Run task'),
106
+ result ? h('span', { style: { marginLeft: '0.5rem' }, class: 'badge green' }, result) : null
107
+ );
108
+ }
109
+
110
+ function DemoIndexedDB() {
111
+ const { db, isReady, error } = useIndexedDB({ name: 'demo-db', version: 1, tables: { items: { keyPath: 'id' } } });
112
+ const [items, setItems] = useState([]);
113
+ const [count, setCount] = useState(null);
114
+
115
+ const refresh = () => {
116
+ if (!db) return;
117
+ db.table('items').query(() => true).then(setItems);
118
+ db.table('items').count().then(setCount);
119
+ };
120
+
121
+ useEffect(() => {
122
+ if (!db || !isReady) return;
123
+ refresh();
124
+ }, [db, isReady]);
125
+
126
+ const insert = () => db && db.table('items').insert({ id: Date.now(), label: 'Item ' + (items.length + 1), created: Date.now() }).then(refresh);
127
+ const bulkInsert = () => db && db.table('items').bulkInsert([
128
+ { id: Date.now() + 1, label: 'Bulk A', created: Date.now() },
129
+ { id: Date.now() + 2, label: 'Bulk B', created: Date.now() },
130
+ ]).then(refresh);
131
+ const update = (id, label) => db && db.table('items').update(id, { label }).then(refresh);
132
+ const remove = (id) => db && db.table('items').delete(id).then(refresh);
133
+ const clear = () => db && db.table('items').clear().then(refresh);
134
+
135
+ if (error) return h('div', { style: { color: 'var(--red)' } }, error.message);
136
+ if (!isReady) return h('div', { class: 'status' }, 'Opening DB…');
137
+
138
+ return h('div', {},
139
+ h('div', { class: 'idb-actions' }, [
140
+ h('button', { onClick: insert }, 'Insert'),
141
+ h('button', { onClick: bulkInsert }, 'Bulk insert'),
142
+ h('button', { onClick: refresh }, 'Query all'),
143
+ h('button', { onClick: clear }, 'Clear'),
144
+ ]),
145
+ h('div', { class: 'idb-count' }, 'Count: ' + (count ?? '—')),
146
+ h('div', { class: 'idb-table-viz' },
147
+ items.length === 0
148
+ ? h('div', { style: { color: 'var(--textMuted)', padding: '0.5rem' } }, 'No rows. Use Insert or Bulk insert.')
149
+ : items.map((row) =>
150
+ h('div', { key: row.id, class: 'idb-row' }, [
151
+ h('span', {}, row.label || 'id:' + row.id),
152
+ h('div', { class: 'idb-row-actions' }, [
153
+ h('button', { onClick: () => update(row.id, (row.label || '') + '✓') }, 'Update'),
154
+ h('button', { onClick: () => remove(row.id) }, 'Delete'),
155
+ ])
156
+ ])
157
+ )
158
+ )
159
+ );
160
+ }
161
+
162
+ function DemoWebRTCIP() {
163
+ const { ips, loading, error } = useWebRTCIP({ timeout: 4000 });
164
+ if (loading) return h('div', { class: 'status' }, 'Detecting IP…');
165
+ if (error) return h('div', { style: { color: 'var(--red)', fontSize: '0.85rem' } }, error);
166
+ return h('div', {}, ips.length ? h('span', { class: 'badge green' }, ips.join(', ')) : 'No IPs');
167
+ }
168
+
169
+ function DemoWasmCompute() {
170
+ const { compute, result, loading, error, ready } = useWasmCompute({
171
+ wasmUrl: new URL('./add.wasm', import.meta.url).href,
172
+ exportName: 'compute',
173
+ });
174
+ const flowSteps = [
175
+ { id: 'component', label: 'Component' },
176
+ { id: 'hook', label: 'useWasmCompute()' },
177
+ { id: 'worker', label: 'Web Worker' },
178
+ { id: 'wasm', label: 'WASM' },
179
+ { id: 'result', label: 'result' },
180
+ ];
181
+ const activeId = result != null ? 'result' : ready ? (loading ? 'wasm' : 'wasm') : 'worker';
182
+
183
+ if (error) return h('div', { style: { color: 'var(--red)' } }, error);
184
+ return h('div', {},
185
+ h('div', { class: 'wasm-flow' },
186
+ flowSteps.map((s, i) => [
187
+ i > 0 && h('span', { class: 'wasm-flow-arrow' }, '→'),
188
+ h('span', { class: 'wasm-flow-node' + (s.id === activeId ? ' active' : '') }, s.label),
189
+ ]).flat()
190
+ ),
191
+ !ready && h('div', { class: 'status' }, 'Loading WASM in worker…'),
192
+ ready && h('div', { style: { marginBottom: '0.5rem' } }, [
193
+ h('button', { onClick: () => compute(41), disabled: loading }, loading ? 'Computing…' : 'compute(41)'),
194
+ h('button', { onClick: () => compute(100), disabled: loading }, 'compute(100)'),
195
+ h('button', { onClick: () => compute(0), disabled: loading }, 'compute(0)'),
196
+ ]),
197
+ result != null && h('div', { class: 'badge green', style: { marginTop: '0.5rem' } }, 'Result: ' + result)
198
+ );
199
+ }
200
+
201
+ // ——— Page data: heading, flow, summary, code, LiveComponent ———
202
+
203
+ const HOOKS = [
204
+ {
205
+ name: 'useTransition',
206
+ flow: 'Component → useTransition() → startTransition(cb) → deferred state update',
207
+ summary: 'Defers state updates so the UI stays responsive. Returns [startTransition, isPending].',
208
+ code: `const [startTransition, isPending] = useTransition();\nstartTransition(() => setCount(c => c + 1));`,
209
+ Live: DemoTransition,
210
+ },
211
+ {
212
+ name: 'useMutationObserver',
213
+ flow: 'Component → ref + useMutationObserver(ref, callback, options) → DOM changes trigger callback',
214
+ summary: 'Observes DOM mutations (childList, attributes, etc.) on the element attached to ref.',
215
+ code: `const ref = useRef(null);\nuseMutationObserver(ref, (mutations) => { ... }, { childList: true });`,
216
+ Live: DemoMutationObserver,
217
+ },
218
+ {
219
+ name: 'useEventBus',
220
+ flow: 'Components → useEventBus() → emit(name, ...args) / on(name, fn) → cross-component events',
221
+ summary: 'Publish/subscribe event bus so components can talk without prop drilling.',
222
+ code: `const { emit, on } = useEventBus();\non('greet', setMsg);\nemit('greet', 'Hello');`,
223
+ Live: DemoEventBus,
224
+ },
225
+ {
226
+ name: 'useWrappedChildren',
227
+ flow: 'Parent → useWrappedChildren(children, props) → children with merged props',
228
+ summary: 'Injects props into every child (e.g. style, className) with preserve or override strategy.',
229
+ code: `const wrapped = useWrappedChildren(children, { style: { marginRight: 8 } });`,
230
+ Live: DemoWrappedChildren,
231
+ },
232
+ {
233
+ name: 'usePreferredTheme',
234
+ flow: 'Component → usePreferredTheme() → matchMedia(prefers-color-scheme) → theme',
235
+ summary: 'Returns the user’s preferred color scheme: light, dark, or no-preference.',
236
+ code: `const theme = usePreferredTheme(); // 'light' | 'dark' | 'no-preference'`,
237
+ Live: DemoPreferredTheme,
238
+ },
239
+ {
240
+ name: 'useNetworkState',
241
+ flow: 'Component → useNetworkState() → online + connection (effectiveType, etc.)',
242
+ summary: 'Tracks online/offline and connection type (when the Network Information API is available).',
243
+ code: `const state = useNetworkState();\n// state.online, state.effectiveType, ...`,
244
+ Live: DemoNetworkState,
245
+ },
246
+ {
247
+ name: 'useClipboard',
248
+ flow: 'Component → useClipboard() → copy(text) / paste() → Clipboard API',
249
+ summary: 'Copy and paste text with the Clipboard API; returns copied and error state.',
250
+ code: `const { copy, copied, paste } = useClipboard();\ncopy('Hello');`,
251
+ Live: DemoClipboard,
252
+ },
253
+ {
254
+ name: 'useRageClick',
255
+ flow: 'ref + useRageClick(ref, { onRageClick }) → N fast clicks in same spot → callback',
256
+ summary: 'Detects rage clicks (e.g. for Sentry) when the user clicks repeatedly in the same area.',
257
+ code: `useRageClick(ref, { onRageClick: (p) => report(p.count), threshold: 5 });`,
258
+ Live: DemoRageClick,
259
+ },
260
+ {
261
+ name: 'useThreadedWorker',
262
+ flow: 'useThreadedWorker(fn, { mode }) → run(data) → queue → fn runs → result',
263
+ summary: 'Runs async work in a queue; sequential or parallel mode with optional priority.',
264
+ code: `const { run, loading, result } = useThreadedWorker(fn, { mode: 'sequential' });\nrun(data);`,
265
+ Live: DemoThreadedWorker,
266
+ },
267
+ {
268
+ name: 'useIndexedDB',
269
+ flow: 'useIndexedDB({ name, version, tables }) → db.table(name).insert/query/...',
270
+ summary: 'IndexedDB with tables, insert, update, delete, query, count, and transactions.',
271
+ code: `const { db, isReady } = useIndexedDB({ name: 'my-db', tables: { items: { keyPath: 'id' } } });\ndb.table('items').insert({ id: 1 });`,
272
+ Live: DemoIndexedDB,
273
+ },
274
+ {
275
+ name: 'useWebRTCIP',
276
+ flow: 'useWebRTCIP() → STUN + ICE candidates → extract IPv4 → ips[]',
277
+ summary: 'Tries to detect client IP via WebRTC; use as a hint and fall back to an IP API if needed.',
278
+ code: `const { ips, loading, error } = useWebRTCIP({ timeout: 3000 });`,
279
+ Live: DemoWebRTCIP,
280
+ },
281
+ {
282
+ name: 'useWasmCompute',
283
+ flow: 'Component → useWasmCompute({ wasmUrl }) → Web Worker → WASM → compute(input) → result',
284
+ summary: 'Runs WebAssembly in a worker; returns compute(input), result, loading, ready.',
285
+ code: `const { compute, result, ready } = useWasmCompute({ wasmUrl: '/add.wasm' });\ncompute(41); // → 42`,
286
+ Live: DemoWasmCompute,
287
+ },
288
+ ];
289
+
290
+ function App() {
291
+ return h('div', {},
292
+ HOOKS.map((hook) =>
293
+ h('section', { key: hook.name, class: 'hook-section' }, [
294
+ h('h2', {}, hook.name),
295
+ h('div', { class: 'flow' }, hook.flow),
296
+ h('p', { class: 'summary' }, hook.summary),
297
+ h('div', { class: 'cards' }, [
298
+ h('div', { class: 'card' }, [
299
+ h('div', { class: 'card-title' }, 'Example'),
300
+ h('pre', { class: 'card-code' }, hook.code),
301
+ ]),
302
+ h('div', { class: 'card' }, [
303
+ h('div', { class: 'card-title' }, 'Live'),
304
+ h('div', { class: 'card-live' }, h(hook.Live)),
305
+ ]),
306
+ ]),
307
+ ])
308
+ )
309
+ );
310
+ }
311
+
312
+ render(h(App), document.getElementById('root'));
package/dist/index.d.ts CHANGED
@@ -9,3 +9,4 @@ export * from './useRageClick';
9
9
  export * from './useThreadedWorker';
10
10
  export * from './useIndexedDB';
11
11
  export * from './useWebRTCIP';
12
+ export * from './useWasmCompute';