preact-missing-hooks 3.1.0 → 4.1.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/.husky/pre-commit +1 -0
- package/.husky/pre-push +1 -0
- package/.prettierignore +3 -0
- package/.prettierrc +6 -0
- package/Readme.md +333 -137
- package/dist/entry.cjs +21 -0
- package/dist/entry.js +2 -0
- package/dist/entry.js.map +1 -0
- package/dist/entry.modern.mjs +2 -0
- package/dist/entry.modern.mjs.map +1 -0
- package/dist/entry.module.js +2 -0
- package/dist/entry.module.js.map +1 -0
- package/dist/entry.umd.js +2 -0
- package/dist/entry.umd.js.map +1 -0
- package/dist/index.d.ts +14 -13
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.modern.mjs +2 -0
- package/dist/index.modern.mjs.map +1 -0
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/indexedDB/dbController.d.ts +2 -2
- package/dist/indexedDB/index.d.ts +6 -6
- package/dist/indexedDB/openDB.d.ts +1 -1
- package/dist/indexedDB/tableController.d.ts +1 -1
- package/dist/indexedDB/types.d.ts +1 -2
- package/dist/react.js +1 -0
- package/dist/react.modern.mjs +1 -0
- package/dist/react.module.js +1 -0
- package/dist/react.umd.js +1 -0
- package/dist/useEventBus.d.ts +1 -1
- package/dist/useIndexedDB.d.ts +3 -3
- package/dist/useLLMMetadata.d.ts +71 -0
- package/dist/useMutationObserver.d.ts +1 -1
- package/dist/useNetworkState.d.ts +3 -3
- package/dist/usePreferredTheme.d.ts +1 -1
- package/dist/useRageClick.d.ts +1 -1
- package/dist/useThreadedWorker.d.ts +1 -1
- package/dist/useTransition.d.ts +4 -1
- package/dist/useWorkerNotifications.d.ts +1 -1
- package/dist/useWrappedChildren.d.ts +3 -3
- package/docs/README.md +111 -0
- package/docs/index.html +58 -20
- package/docs/main.js +49 -0
- package/eslint.config.mjs +10 -0
- package/package.json +60 -6
- package/scripts/generate-entry.cjs +34 -0
- package/src/index.ts +14 -13
- package/src/indexedDB/dbController.ts +101 -92
- package/src/indexedDB/index.ts +16 -11
- package/src/indexedDB/openDB.ts +49 -49
- package/src/indexedDB/requestToPromise.ts +17 -16
- package/src/indexedDB/tableController.ts +331 -257
- package/src/indexedDB/types.ts +35 -35
- package/src/useClipboard.ts +99 -97
- package/src/useEventBus.ts +39 -36
- package/src/useIndexedDB.ts +111 -111
- package/src/useLLMMetadata.ts +418 -0
- package/src/useMutationObserver.ts +26 -26
- package/src/useNetworkState.ts +124 -122
- package/src/usePreferredTheme.ts +68 -68
- package/src/useRageClick.ts +103 -103
- package/src/useThreadedWorker.ts +165 -165
- package/src/useTransition.ts +22 -19
- package/src/useWasmCompute.ts +209 -204
- package/src/useWebRTCIP.ts +181 -176
- package/src/useWorkerNotifications.ts +28 -20
- package/src/useWrappedChildren.ts +72 -58
- package/tests/preact-as-react.ts +5 -0
- package/tests/react-adapter.tsx +12 -0
- package/tests/setup-react.ts +4 -0
- package/tests/useClipboard.test.tsx +4 -2
- package/tests/useLLMMetadata.test.tsx +149 -0
- package/tests/useThreadedWorker.test.tsx +3 -1
- package/tests/useWasmCompute.test.tsx +1 -1
- package/tests/useWebRTCIP.test.tsx +3 -1
- package/vite.config.ts +11 -4
- package/vitest.config.preact.ts +21 -0
- package/vitest.config.react.ts +36 -0
- package/vitest.workspace.ts +6 -0
package/Readme.md
CHANGED
|
@@ -34,9 +34,10 @@ A lightweight, extendable collection of React-like hooks for Preact, including u
|
|
|
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
35
|
- **`useWasmCompute`** — Runs WebAssembly computation off the main thread via a Web Worker. Validates environment (browser, Worker, WebAssembly) and returns `compute(input)`, `result`, `loading`, `error`, `ready`.
|
|
36
36
|
- **`useWorkerNotifications`** — Listens to a Worker's messages and maintains state: running tasks, completed/failed counts, event history, average task duration, throughput per second, and queue size. Worker posts `task_start` / `task_end` / `task_fail` / `queue_size`; returns `progress` (default view of all active worker data) plus individual stats.
|
|
37
|
+
- **`useLLMMetadata`** — Injects an AI-readable metadata block into the document head on route change. Works in React 18+ and Preact 10+. Supports **manual** (title, description, tags) and **auto-extract** (from `document.title`, visible `h1`/`h2`, first 3 `p`). Cacheable, SSR-safe, no router dependency.
|
|
37
38
|
- Fully TypeScript compatible
|
|
38
39
|
- Bundled with Microbundle
|
|
39
|
-
- Zero dependencies (
|
|
40
|
+
- Zero dependencies (peer: `preact` or `react` — use `/react` for React)
|
|
40
41
|
|
|
41
42
|
---
|
|
42
43
|
|
|
@@ -46,23 +47,103 @@ A lightweight, extendable collection of React-like hooks for Preact, including u
|
|
|
46
47
|
npm install preact-missing-hooks
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
Ensure your app has either **preact** or **react** installed (the package uses whichever is present).
|
|
51
|
+
|
|
49
52
|
---
|
|
50
53
|
|
|
51
54
|
## Import options
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
Use the same import in Preact and React projects:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { useThreadedWorker, useClipboard } from "preact-missing-hooks";
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- **How it picks Preact vs React**
|
|
63
|
+
- **CommonJS / Node:** The package detects which of `preact` or `react` is installed and uses that build automatically.
|
|
64
|
+
- **ESM (Vite, Webpack, etc.):** Default is the Preact build. In a **React** app, add the `react` condition so the package resolves to the React build:
|
|
65
|
+
- **Vite:** `vite.config.ts` → `resolve: { conditions: ['react'] }`
|
|
66
|
+
- **Webpack:** `resolve.conditionNames` (or similar) to include `'react'`
|
|
67
|
+
- **Or** in React projects you can always import from the explicit entry: `preact-missing-hooks/react`.
|
|
68
|
+
|
|
57
69
|
- **Subpath exports (tree-shakeable)** — Import a single hook:
|
|
70
|
+
|
|
58
71
|
```ts
|
|
59
|
-
import { useThreadedWorker } from
|
|
60
|
-
import { useClipboard } from
|
|
61
|
-
import { useWebRTCIP } from
|
|
62
|
-
import { useWasmCompute } from
|
|
63
|
-
import { useWorkerNotifications } from
|
|
72
|
+
import { useThreadedWorker } from "preact-missing-hooks/useThreadedWorker";
|
|
73
|
+
import { useClipboard } from "preact-missing-hooks/useClipboard";
|
|
74
|
+
import { useWebRTCIP } from "preact-missing-hooks/useWebRTCIP";
|
|
75
|
+
import { useWasmCompute } from "preact-missing-hooks/useWasmCompute";
|
|
76
|
+
import { useWorkerNotifications } from "preact-missing-hooks/useWorkerNotifications";
|
|
64
77
|
```
|
|
65
|
-
|
|
78
|
+
|
|
79
|
+
All hooks are available: `useTransition`, `useMutationObserver`, `useEventBus`, `useWrappedChildren`, `usePreferredTheme`, `useNetworkState`, `useClipboard`, `useRageClick`, `useThreadedWorker`, `useIndexedDB`, `useWebRTCIP`, `useWasmCompute`, `useWorkerNotifications`, `useLLMMetadata`.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quick start
|
|
84
|
+
|
|
85
|
+
Minimal example (Preact or React):
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import {
|
|
89
|
+
useTransition,
|
|
90
|
+
useClipboard,
|
|
91
|
+
usePreferredTheme,
|
|
92
|
+
} from "preact-missing-hooks";
|
|
93
|
+
|
|
94
|
+
function App() {
|
|
95
|
+
const [startTransition, isPending] = useTransition();
|
|
96
|
+
const { copy, copied } = useClipboard();
|
|
97
|
+
const theme = usePreferredTheme();
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div>
|
|
101
|
+
<button
|
|
102
|
+
onClick={() =>
|
|
103
|
+
startTransition(() => {
|
|
104
|
+
/* heavy update */
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
disabled={isPending}
|
|
108
|
+
>
|
|
109
|
+
{isPending ? "Loading…" : "Update"}
|
|
110
|
+
</button>
|
|
111
|
+
<button onClick={() => copy("Hello!")}>
|
|
112
|
+
{copied ? "Copied!" : "Copy"}
|
|
113
|
+
</button>
|
|
114
|
+
<span>Theme: {theme}</span>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Live demo:** Run the docs demo locally to try every hook with live examples:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm run build && npx serve -l 5000
|
|
124
|
+
# Open http://localhost:5000/docs/
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Or open `docs/index.html` after building (see [docs/README.md](docs/README.md) for details).
|
|
128
|
+
|
|
129
|
+
**Usage at a glance:**
|
|
130
|
+
|
|
131
|
+
| Hook | One-liner |
|
|
132
|
+
| ------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
133
|
+
| [useTransition](#usetransition) | `const [startTransition, isPending] = useTransition();` |
|
|
134
|
+
| [useMutationObserver](#usemutationobserver) | `useMutationObserver(ref, callback, { childList: true });` |
|
|
135
|
+
| [useEventBus](#useeventbus) | `const { emit, on } = useEventBus();` |
|
|
136
|
+
| [useWrappedChildren](#usewrappedchildren) | `const wrapped = useWrappedChildren(children, { className: 'x' });` |
|
|
137
|
+
| [usePreferredTheme](#usepreferredtheme) | `const theme = usePreferredTheme(); // 'light' \| 'dark' \| 'no-preference'` |
|
|
138
|
+
| [useNetworkState](#usenetworkstate) | `const { online, effectiveType } = useNetworkState();` |
|
|
139
|
+
| [useClipboard](#useclipboard) | `const { copy, paste, copied } = useClipboard();` |
|
|
140
|
+
| [useRageClick](#userageclick) | `useRageClick(ref, { onRageClick, threshold: 5 });` |
|
|
141
|
+
| [useThreadedWorker](#usethreadedworker) | `const { run, loading, result } = useThreadedWorker(fn, { mode: 'sequential' });` |
|
|
142
|
+
| [useIndexedDB](#useindexeddb) | `const { db, isReady } = useIndexedDB({ name, version, tables });` |
|
|
143
|
+
| [useWebRTCIP](#usewebrtcip) | `const { ips, loading, error } = useWebRTCIP({ timeout: 3000 });` |
|
|
144
|
+
| [useWasmCompute](#usewasmcompute) | `const { compute, result, ready } = useWasmCompute({ wasmUrl });` |
|
|
145
|
+
| [useWorkerNotifications](#useworkernotifications) | `const { progress, eventHistory } = useWorkerNotifications(worker);` |
|
|
146
|
+
| [useLLMMetadata](#usellmmetadata) | `useLLMMetadata({ route: pathname, mode: 'auto-extract' });` |
|
|
66
147
|
|
|
67
148
|
---
|
|
68
149
|
|
|
@@ -71,22 +152,22 @@ npm install preact-missing-hooks
|
|
|
71
152
|
### `useTransition`
|
|
72
153
|
|
|
73
154
|
```tsx
|
|
74
|
-
import { useTransition } from
|
|
155
|
+
import { useTransition } from "preact-missing-hooks";
|
|
75
156
|
|
|
76
157
|
function ExampleTransition() {
|
|
77
|
-
const [startTransition, isPending] = useTransition()
|
|
158
|
+
const [startTransition, isPending] = useTransition();
|
|
78
159
|
|
|
79
160
|
const handleClick = () => {
|
|
80
161
|
startTransition(() => {
|
|
81
162
|
// perform an expensive update here
|
|
82
|
-
})
|
|
83
|
-
}
|
|
163
|
+
});
|
|
164
|
+
};
|
|
84
165
|
|
|
85
166
|
return (
|
|
86
167
|
<button onClick={handleClick} disabled={isPending}>
|
|
87
|
-
{isPending ?
|
|
168
|
+
{isPending ? "Loading..." : "Click Me"}
|
|
88
169
|
</button>
|
|
89
|
-
)
|
|
170
|
+
);
|
|
90
171
|
}
|
|
91
172
|
```
|
|
92
173
|
|
|
@@ -95,21 +176,21 @@ function ExampleTransition() {
|
|
|
95
176
|
### `useMutationObserver`
|
|
96
177
|
|
|
97
178
|
```tsx
|
|
98
|
-
import { useRef } from
|
|
99
|
-
import { useMutationObserver } from
|
|
179
|
+
import { useRef } from "preact/hooks";
|
|
180
|
+
import { useMutationObserver } from "preact-missing-hooks";
|
|
100
181
|
|
|
101
182
|
function ExampleMutation() {
|
|
102
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
183
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
103
184
|
|
|
104
185
|
useMutationObserver(
|
|
105
186
|
ref,
|
|
106
187
|
(mutations) => {
|
|
107
|
-
console.log(
|
|
188
|
+
console.log("Detected mutations:", mutations);
|
|
108
189
|
},
|
|
109
|
-
{ childList: true, subtree: true }
|
|
110
|
-
)
|
|
190
|
+
{ childList: true, subtree: true }
|
|
191
|
+
);
|
|
111
192
|
|
|
112
|
-
return <div ref={ref}>Observe this content</div
|
|
193
|
+
return <div ref={ref}>Observe this content</div>;
|
|
113
194
|
}
|
|
114
195
|
```
|
|
115
196
|
|
|
@@ -120,33 +201,33 @@ function ExampleMutation() {
|
|
|
120
201
|
```tsx
|
|
121
202
|
// types.ts
|
|
122
203
|
export type Events = {
|
|
123
|
-
notify: (message: string) => void
|
|
124
|
-
}
|
|
204
|
+
notify: (message: string) => void;
|
|
205
|
+
};
|
|
125
206
|
|
|
126
207
|
// Sender.tsx
|
|
127
|
-
import { useEventBus } from
|
|
128
|
-
import type { Events } from
|
|
208
|
+
import { useEventBus } from "preact-missing-hooks";
|
|
209
|
+
import type { Events } from "./types";
|
|
129
210
|
|
|
130
211
|
function Sender() {
|
|
131
|
-
const { emit } = useEventBus<Events>()
|
|
132
|
-
return <button onClick={() => emit(
|
|
212
|
+
const { emit } = useEventBus<Events>();
|
|
213
|
+
return <button onClick={() => emit("notify", "Hello World!")}>Send</button>;
|
|
133
214
|
}
|
|
134
215
|
|
|
135
216
|
// Receiver.tsx
|
|
136
|
-
import { useEventBus } from
|
|
137
|
-
import { useState, useEffect } from
|
|
138
|
-
import type { Events } from
|
|
217
|
+
import { useEventBus } from "preact-missing-hooks";
|
|
218
|
+
import { useState, useEffect } from "preact/hooks";
|
|
219
|
+
import type { Events } from "./types";
|
|
139
220
|
|
|
140
221
|
function Receiver() {
|
|
141
|
-
const [msg, setMsg] = useState<string>(
|
|
142
|
-
const { on } = useEventBus<Events>()
|
|
222
|
+
const [msg, setMsg] = useState<string>("");
|
|
223
|
+
const { on } = useEventBus<Events>();
|
|
143
224
|
|
|
144
225
|
useEffect(() => {
|
|
145
|
-
const unsubscribe = on(
|
|
146
|
-
return unsubscribe
|
|
147
|
-
}, [])
|
|
226
|
+
const unsubscribe = on("notify", setMsg);
|
|
227
|
+
return unsubscribe;
|
|
228
|
+
}, []);
|
|
148
229
|
|
|
149
|
-
return <div>Message: {msg}</div
|
|
230
|
+
return <div>Message: {msg}</div>;
|
|
150
231
|
}
|
|
151
232
|
```
|
|
152
233
|
|
|
@@ -155,19 +236,19 @@ function Receiver() {
|
|
|
155
236
|
### `useWrappedChildren`
|
|
156
237
|
|
|
157
238
|
```tsx
|
|
158
|
-
import { useWrappedChildren } from
|
|
239
|
+
import { useWrappedChildren } from "preact-missing-hooks";
|
|
159
240
|
|
|
160
241
|
function ParentComponent({ children }) {
|
|
161
242
|
// Inject common props into all children
|
|
162
243
|
const injectProps = {
|
|
163
|
-
className:
|
|
164
|
-
onClick: () => console.log(
|
|
165
|
-
style: { border:
|
|
166
|
-
}
|
|
244
|
+
className: "enhanced-child",
|
|
245
|
+
onClick: () => console.log("Child clicked!"),
|
|
246
|
+
style: { border: "1px solid #ccc" },
|
|
247
|
+
};
|
|
167
248
|
|
|
168
|
-
const wrappedChildren = useWrappedChildren(children, injectProps)
|
|
249
|
+
const wrappedChildren = useWrappedChildren(children, injectProps);
|
|
169
250
|
|
|
170
|
-
return <div className="parent">{wrappedChildren}</div
|
|
251
|
+
return <div className="parent">{wrappedChildren}</div>;
|
|
171
252
|
}
|
|
172
253
|
|
|
173
254
|
// Usage with preserve strategy (default - existing props are preserved)
|
|
@@ -175,21 +256,21 @@ function PreserveExample() {
|
|
|
175
256
|
return (
|
|
176
257
|
<ParentComponent>
|
|
177
258
|
<button className="btn">Existing class preserved</button>
|
|
178
|
-
<span style={{ color:
|
|
259
|
+
<span style={{ color: "red" }}>Both styles applied</span>
|
|
179
260
|
</ParentComponent>
|
|
180
|
-
)
|
|
261
|
+
);
|
|
181
262
|
}
|
|
182
263
|
|
|
183
264
|
// Usage with override strategy (injected props override existing ones)
|
|
184
265
|
function OverrideExample() {
|
|
185
|
-
const injectProps = { className:
|
|
266
|
+
const injectProps = { className: "new-class" };
|
|
186
267
|
const children = (
|
|
187
268
|
<button className="old-class">Class will be overridden</button>
|
|
188
|
-
)
|
|
269
|
+
);
|
|
189
270
|
|
|
190
|
-
const wrappedChildren = useWrappedChildren(children, injectProps,
|
|
271
|
+
const wrappedChildren = useWrappedChildren(children, injectProps, "override");
|
|
191
272
|
|
|
192
|
-
return <div>{wrappedChildren}</div
|
|
273
|
+
return <div>{wrappedChildren}</div>;
|
|
193
274
|
}
|
|
194
275
|
```
|
|
195
276
|
|
|
@@ -198,12 +279,12 @@ function OverrideExample() {
|
|
|
198
279
|
### `usePreferredTheme`
|
|
199
280
|
|
|
200
281
|
```tsx
|
|
201
|
-
import { usePreferredTheme } from
|
|
282
|
+
import { usePreferredTheme } from "preact-missing-hooks";
|
|
202
283
|
|
|
203
284
|
function ThemeAwareComponent() {
|
|
204
|
-
const theme = usePreferredTheme() // 'light' | 'dark' | 'no-preference'
|
|
285
|
+
const theme = usePreferredTheme(); // 'light' | 'dark' | 'no-preference'
|
|
205
286
|
|
|
206
|
-
return <div data-theme={theme}>Your system prefers: {theme}</div
|
|
287
|
+
return <div data-theme={theme}>Your system prefers: {theme}</div>;
|
|
207
288
|
}
|
|
208
289
|
```
|
|
209
290
|
|
|
@@ -212,18 +293,18 @@ function ThemeAwareComponent() {
|
|
|
212
293
|
### `useNetworkState`
|
|
213
294
|
|
|
214
295
|
```tsx
|
|
215
|
-
import { useNetworkState } from
|
|
296
|
+
import { useNetworkState } from "preact-missing-hooks";
|
|
216
297
|
|
|
217
298
|
function NetworkStatus() {
|
|
218
|
-
const { online, effectiveType, saveData } = useNetworkState()
|
|
299
|
+
const { online, effectiveType, saveData } = useNetworkState();
|
|
219
300
|
|
|
220
301
|
return (
|
|
221
302
|
<div>
|
|
222
|
-
Status: {online ?
|
|
303
|
+
Status: {online ? "Online" : "Offline"}
|
|
223
304
|
{effectiveType && ` (${effectiveType})`}
|
|
224
|
-
{saveData &&
|
|
305
|
+
{saveData && " — Reduced data mode enabled"}
|
|
225
306
|
</div>
|
|
226
|
-
)
|
|
307
|
+
);
|
|
227
308
|
}
|
|
228
309
|
```
|
|
229
310
|
|
|
@@ -232,34 +313,34 @@ function NetworkStatus() {
|
|
|
232
313
|
### `useClipboard`
|
|
233
314
|
|
|
234
315
|
```tsx
|
|
235
|
-
import { useState } from
|
|
236
|
-
import { useClipboard } from
|
|
316
|
+
import { useState } from "preact/hooks";
|
|
317
|
+
import { useClipboard } from "preact-missing-hooks";
|
|
237
318
|
|
|
238
319
|
function CopyButton() {
|
|
239
|
-
const { copy, copied, error } = useClipboard({ resetDelay: 2000 })
|
|
320
|
+
const { copy, copied, error } = useClipboard({ resetDelay: 2000 });
|
|
240
321
|
|
|
241
322
|
return (
|
|
242
|
-
<button onClick={() => copy(
|
|
243
|
-
{copied ?
|
|
323
|
+
<button onClick={() => copy("Hello, World!")}>
|
|
324
|
+
{copied ? "Copied!" : "Copy"}
|
|
244
325
|
</button>
|
|
245
|
-
)
|
|
326
|
+
);
|
|
246
327
|
}
|
|
247
328
|
|
|
248
329
|
function PasteInput() {
|
|
249
|
-
const [text, setText] = useState(
|
|
250
|
-
const { paste } = useClipboard()
|
|
330
|
+
const [text, setText] = useState("");
|
|
331
|
+
const { paste } = useClipboard();
|
|
251
332
|
|
|
252
333
|
const handlePaste = async () => {
|
|
253
|
-
const content = await paste()
|
|
254
|
-
setText(content)
|
|
255
|
-
}
|
|
334
|
+
const content = await paste();
|
|
335
|
+
setText(content);
|
|
336
|
+
};
|
|
256
337
|
|
|
257
338
|
return (
|
|
258
339
|
<div>
|
|
259
340
|
<input value={text} onChange={(e) => setText(e.target.value)} />
|
|
260
341
|
<button onClick={handlePaste}>Paste</button>
|
|
261
342
|
</div>
|
|
262
|
-
)
|
|
343
|
+
);
|
|
263
344
|
}
|
|
264
345
|
```
|
|
265
346
|
|
|
@@ -270,26 +351,26 @@ function PasteInput() {
|
|
|
270
351
|
Detects rage clicks (multiple rapid clicks in the same area), e.g. when the UI is unresponsive. Report them to [Sentry](https://docs.sentry.io/product/issues/issue-details/replay-issues/rage-clicks/) or your error tracker to surface rage-click issues and lower rage-click-related support.
|
|
271
352
|
|
|
272
353
|
```tsx
|
|
273
|
-
import { useRef } from
|
|
274
|
-
import { useRageClick } from
|
|
354
|
+
import { useRef } from "preact/hooks";
|
|
355
|
+
import { useRageClick } from "preact-missing-hooks";
|
|
275
356
|
|
|
276
357
|
function SubmitButton() {
|
|
277
|
-
const ref = useRef<HTMLButtonElement>(null)
|
|
358
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
278
359
|
|
|
279
360
|
useRageClick(ref, {
|
|
280
361
|
onRageClick: ({ count, event }) => {
|
|
281
362
|
// Report to Sentry (or your error tracker) to create rage-click issues
|
|
282
|
-
Sentry.captureMessage(
|
|
283
|
-
level:
|
|
284
|
-
extra: { count, target: event.target, tag:
|
|
285
|
-
})
|
|
363
|
+
Sentry.captureMessage("Rage click detected", {
|
|
364
|
+
level: "warning",
|
|
365
|
+
extra: { count, target: event.target, tag: "rage_click" },
|
|
366
|
+
});
|
|
286
367
|
},
|
|
287
368
|
threshold: 5, // min clicks (default 5, Sentry-style)
|
|
288
369
|
timeWindow: 1000, // ms (default 1000)
|
|
289
370
|
distanceThreshold: 30, // px (default 30)
|
|
290
|
-
})
|
|
371
|
+
});
|
|
291
372
|
|
|
292
|
-
return <button ref={ref}>Submit</button
|
|
373
|
+
return <button ref={ref}>Submit</button>;
|
|
293
374
|
}
|
|
294
375
|
```
|
|
295
376
|
|
|
@@ -300,16 +381,16 @@ function SubmitButton() {
|
|
|
300
381
|
Runs async work in a queue with **sequential** (one task at a time, by priority) or **parallel** (worker pool) execution. Lower priority number = higher priority; same priority is FIFO.
|
|
301
382
|
|
|
302
383
|
```tsx
|
|
303
|
-
import { useThreadedWorker } from
|
|
384
|
+
import { useThreadedWorker } from "preact-missing-hooks";
|
|
304
385
|
|
|
305
386
|
// Sequential: one task at a time, sorted by priority
|
|
306
|
-
const sequential = useThreadedWorker(fetchUser, { mode:
|
|
387
|
+
const sequential = useThreadedWorker(fetchUser, { mode: "sequential" });
|
|
307
388
|
|
|
308
389
|
// Parallel: up to N tasks at once
|
|
309
390
|
const parallel = useThreadedWorker(processItem, {
|
|
310
|
-
mode:
|
|
391
|
+
mode: "parallel",
|
|
311
392
|
concurrency: 4,
|
|
312
|
-
})
|
|
393
|
+
});
|
|
313
394
|
|
|
314
395
|
// API (same for both modes)
|
|
315
396
|
const {
|
|
@@ -320,11 +401,11 @@ const {
|
|
|
320
401
|
queueSize, // tasks queued + running
|
|
321
402
|
clearQueue, // clear pending tasks (running continue)
|
|
322
403
|
terminate, // clear queue and reject new run()
|
|
323
|
-
} = sequential
|
|
404
|
+
} = sequential;
|
|
324
405
|
|
|
325
406
|
// Run with priority (1 = highest)
|
|
326
|
-
await run({ userId: 1 }, { priority: 1 })
|
|
327
|
-
await run({ userId: 2 }, { priority: 3 })
|
|
407
|
+
await run({ userId: 1 }, { priority: 1 });
|
|
408
|
+
await run({ userId: 2 }, { priority: 3 });
|
|
328
409
|
```
|
|
329
410
|
|
|
330
411
|
---
|
|
@@ -340,40 +421,40 @@ Production-ready IndexedDB hook: database initialization, table creation (with k
|
|
|
340
421
|
**Database API:** `db.table(name)`, `db.hasTable(name)`, `db.transaction(storeNames, mode, callback, options?)`.
|
|
341
422
|
|
|
342
423
|
```tsx
|
|
343
|
-
import { useIndexedDB } from
|
|
424
|
+
import { useIndexedDB } from "preact-missing-hooks";
|
|
344
425
|
|
|
345
426
|
function App() {
|
|
346
427
|
const { db, isReady, error } = useIndexedDB({
|
|
347
|
-
name:
|
|
428
|
+
name: "my-app-db",
|
|
348
429
|
version: 1,
|
|
349
430
|
tables: {
|
|
350
|
-
users: { keyPath:
|
|
351
|
-
settings: { keyPath:
|
|
431
|
+
users: { keyPath: "id", autoIncrement: true, indexes: ["email"] },
|
|
432
|
+
settings: { keyPath: "key" },
|
|
352
433
|
},
|
|
353
|
-
})
|
|
434
|
+
});
|
|
354
435
|
|
|
355
|
-
if (error) return <div>Failed to open database</div
|
|
356
|
-
if (!isReady || !db) return <div>Loading...</div
|
|
436
|
+
if (error) return <div>Failed to open database</div>;
|
|
437
|
+
if (!isReady || !db) return <div>Loading...</div>;
|
|
357
438
|
|
|
358
|
-
const users = db.table(
|
|
439
|
+
const users = db.table("users");
|
|
359
440
|
|
|
360
441
|
// All operations return Promises and accept optional { onSuccess, onError }
|
|
361
|
-
await users.insert({ email:
|
|
362
|
-
await users.update(1, { name:
|
|
363
|
-
const found = await users.query((u) => u.email.startsWith(
|
|
364
|
-
const n = await users.count()
|
|
365
|
-
await users.delete(1)
|
|
366
|
-
await users.upsert({ id: 2, email:
|
|
367
|
-
await users.bulkInsert([{ email:
|
|
368
|
-
await users.clear()
|
|
442
|
+
await users.insert({ email: "a@b.com", name: "Alice" });
|
|
443
|
+
await users.update(1, { name: "Alice Smith" });
|
|
444
|
+
const found = await users.query((u) => u.email.startsWith("a@"));
|
|
445
|
+
const n = await users.count();
|
|
446
|
+
await users.delete(1);
|
|
447
|
+
await users.upsert({ id: 2, email: "b@b.com" });
|
|
448
|
+
await users.bulkInsert([{ email: "c@b.com" }, { email: "d@b.com" }]);
|
|
449
|
+
await users.clear();
|
|
369
450
|
|
|
370
451
|
// Full transaction support
|
|
371
|
-
await db.transaction([
|
|
372
|
-
await tx.table(
|
|
373
|
-
await tx.table(
|
|
374
|
-
})
|
|
452
|
+
await db.transaction(["users", "settings"], "readwrite", async (tx) => {
|
|
453
|
+
await tx.table("users").insert({ email: "e@b.com" });
|
|
454
|
+
await tx.table("settings").upsert({ key: "theme", value: "dark" });
|
|
455
|
+
});
|
|
375
456
|
|
|
376
|
-
return <div>DB ready. Tables: {db.hasTable(
|
|
457
|
+
return <div>DB ready. Tables: {db.hasTable("users") ? "users" : ""}</div>;
|
|
377
458
|
}
|
|
378
459
|
```
|
|
379
460
|
|
|
@@ -386,8 +467,8 @@ Detects client IP addresses using WebRTC ICE candidates and a STUN server (**fro
|
|
|
386
467
|
Returns `{ ips: string[], loading: boolean, error: string | null }`. Options: `stunServers`, `timeout` (ms), `onDetect(ip)`.
|
|
387
468
|
|
|
388
469
|
```tsx
|
|
389
|
-
import { useWebRTCIP } from
|
|
390
|
-
import { useState, useEffect } from
|
|
470
|
+
import { useWebRTCIP } from "preact-missing-hooks";
|
|
471
|
+
import { useState, useEffect } from "preact/hooks";
|
|
391
472
|
|
|
392
473
|
function ClientIP() {
|
|
393
474
|
const { ips, loading, error } = useWebRTCIP({
|
|
@@ -395,25 +476,25 @@ function ClientIP() {
|
|
|
395
476
|
onDetect: (ip) => {
|
|
396
477
|
/* optional: e.g. analytics */
|
|
397
478
|
},
|
|
398
|
-
})
|
|
399
|
-
const [fallbackIP, setFallbackIP] = useState<string | null>(null)
|
|
479
|
+
});
|
|
480
|
+
const [fallbackIP, setFallbackIP] = useState<string | null>(null);
|
|
400
481
|
|
|
401
482
|
// Fallback to public IP API when WebRTC fails or returns empty
|
|
402
483
|
useEffect(() => {
|
|
403
|
-
if (loading || ips.length > 0) return
|
|
484
|
+
if (loading || ips.length > 0) return;
|
|
404
485
|
if (error) {
|
|
405
|
-
fetch(
|
|
486
|
+
fetch("https://api.ipify.org?format=json")
|
|
406
487
|
.then((r) => r.json())
|
|
407
488
|
.then((d) => setFallbackIP(d.ip))
|
|
408
|
-
.catch(() => {})
|
|
489
|
+
.catch(() => {});
|
|
409
490
|
}
|
|
410
|
-
}, [loading, ips.length, error])
|
|
491
|
+
}, [loading, ips.length, error]);
|
|
411
492
|
|
|
412
|
-
if (loading) return <p>Detecting IP…</p
|
|
413
|
-
if (ips.length > 0) return <p>IPs (WebRTC): {ips.join(
|
|
414
|
-
if (fallbackIP) return <p>IP (fallback API): {fallbackIP}</p
|
|
415
|
-
if (error) return <p>WebRTC failed. Try fallback API.</p
|
|
416
|
-
return null
|
|
493
|
+
if (loading) return <p>Detecting IP…</p>;
|
|
494
|
+
if (ips.length > 0) return <p>IPs (WebRTC): {ips.join(", ")}</p>;
|
|
495
|
+
if (fallbackIP) return <p>IP (fallback API): {fallbackIP}</p>;
|
|
496
|
+
if (error) return <p>WebRTC failed. Try fallback API.</p>;
|
|
497
|
+
return null;
|
|
417
498
|
}
|
|
418
499
|
```
|
|
419
500
|
|
|
@@ -426,23 +507,23 @@ Runs WebAssembly computation in a Web Worker so the main thread stays responsive
|
|
|
426
507
|
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).
|
|
427
508
|
|
|
428
509
|
```tsx
|
|
429
|
-
import { useWasmCompute } from
|
|
510
|
+
import { useWasmCompute } from "preact-missing-hooks";
|
|
430
511
|
|
|
431
512
|
function AddWithWasm() {
|
|
432
513
|
const { compute, result, loading, error, ready } = useWasmCompute<
|
|
433
514
|
number,
|
|
434
515
|
number
|
|
435
516
|
>({
|
|
436
|
-
wasmUrl:
|
|
437
|
-
exportName:
|
|
438
|
-
})
|
|
517
|
+
wasmUrl: "/add.wasm",
|
|
518
|
+
exportName: "add",
|
|
519
|
+
});
|
|
439
520
|
|
|
440
521
|
const handleClick = () => {
|
|
441
|
-
if (ready) compute(2).then(() => {})
|
|
442
|
-
}
|
|
522
|
+
if (ready) compute(2).then(() => {});
|
|
523
|
+
};
|
|
443
524
|
|
|
444
|
-
if (error) return <p>WASM unavailable: {error}</p
|
|
445
|
-
if (!ready) return <p>Loading WASM…</p
|
|
525
|
+
if (error) return <p>WASM unavailable: {error}</p>;
|
|
526
|
+
if (!ready) return <p>Loading WASM…</p>;
|
|
446
527
|
return (
|
|
447
528
|
<div>
|
|
448
529
|
<button onClick={handleClick} disabled={loading}>
|
|
@@ -450,7 +531,7 @@ function AddWithWasm() {
|
|
|
450
531
|
</button>
|
|
451
532
|
{result != null && <p>Result: {result}</p>}
|
|
452
533
|
</div>
|
|
453
|
-
)
|
|
534
|
+
);
|
|
454
535
|
}
|
|
455
536
|
```
|
|
456
537
|
|
|
@@ -463,27 +544,142 @@ Listens to a Worker's `message` events and maintains state and derived stats. Yo
|
|
|
463
544
|
Returns `runningTasks`, `completedCount`, `failedCount`, `eventHistory`, `averageDurationMs`, `throughputPerSecond`, `currentQueueSize`, and **`progress`** — a single object with all active worker data (running, completed, failed, totalProcessed, avg duration, throughput/s, queue). Options: `maxHistory` (default 100), `throughputWindowMs` (default 1000).
|
|
464
545
|
|
|
465
546
|
```tsx
|
|
466
|
-
import { useWorkerNotifications } from
|
|
547
|
+
import { useWorkerNotifications } from "preact-missing-hooks";
|
|
467
548
|
|
|
468
549
|
function WorkerDashboard({ worker }) {
|
|
469
550
|
const { progress, eventHistory } = useWorkerNotifications(worker, {
|
|
470
551
|
maxHistory: 50,
|
|
471
|
-
})
|
|
552
|
+
});
|
|
472
553
|
|
|
473
554
|
return (
|
|
474
555
|
<div>
|
|
475
556
|
<p>
|
|
476
|
-
Running: {progress.runningTasks.length} | Done:{
|
|
557
|
+
Running: {progress.runningTasks.length} | Done:{" "}
|
|
477
558
|
{progress.completedCount} | Failed: {progress.failedCount}
|
|
478
559
|
</p>
|
|
479
560
|
<p>
|
|
480
|
-
Avg: {progress.averageDurationMs.toFixed(0)}ms | Throughput:{
|
|
481
|
-
{progress.throughputPerSecond.toFixed(2)}/s | Queue:{
|
|
561
|
+
Avg: {progress.averageDurationMs.toFixed(0)}ms | Throughput:{" "}
|
|
562
|
+
{progress.throughputPerSecond.toFixed(2)}/s | Queue:{" "}
|
|
482
563
|
{progress.currentQueueSize}
|
|
483
564
|
</p>
|
|
484
565
|
<small>Events: {eventHistory.length}</small>
|
|
485
566
|
</div>
|
|
486
|
-
)
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
### `useLLMMetadata`
|
|
574
|
+
|
|
575
|
+
Injects an AI-readable metadata block into the document head when the route changes. Works in **React 18+** and **Preact 10+** (framework-agnostic). No router dependency — you pass the current `route` string and the hook updates the script when it changes.
|
|
576
|
+
|
|
577
|
+
**Safe usage:** The hook **never throws**. It accepts `config` or `null`/`undefined`. When `config` is `null` or `undefined`, it injects a minimal payload with `route: "/"` and `generatedAt`. Invalid or missing values are normalized; all strings are length-limited and URLs validated; DOM access is wrapped in try/catch. Safe for SSR (no-op when `window` is undefined).
|
|
578
|
+
|
|
579
|
+
**API:**
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
type OGType =
|
|
583
|
+
| "website"
|
|
584
|
+
| "article"
|
|
585
|
+
| "profile"
|
|
586
|
+
| "video.other"
|
|
587
|
+
| "product"
|
|
588
|
+
| "music.song"
|
|
589
|
+
| "book";
|
|
590
|
+
|
|
591
|
+
interface LLMConfig {
|
|
592
|
+
route: string;
|
|
593
|
+
mode?: "manual" | "auto-extract";
|
|
594
|
+
title?: string;
|
|
595
|
+
description?: string;
|
|
596
|
+
tags?: string[];
|
|
597
|
+
canonicalUrl?: string; // absolute URL
|
|
598
|
+
language?: string; // e.g. "en", "en-US"
|
|
599
|
+
ogType?: OGType; // Open Graph type
|
|
600
|
+
ogImage?: string; // absolute image URL
|
|
601
|
+
ogImageAlt?: string;
|
|
602
|
+
siteName?: string;
|
|
603
|
+
author?: string;
|
|
604
|
+
publishedTime?: string; // ISO date
|
|
605
|
+
modifiedTime?: string; // ISO date
|
|
606
|
+
robots?: string; // e.g. "index, follow"
|
|
607
|
+
extra?: Record<string, string | number | boolean | string[]>;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function useLLMMetadata(config: LLMConfig | null | undefined): void;
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
**Behavior:**
|
|
614
|
+
|
|
615
|
+
- When `config` is `null` or `undefined`: injects a minimal payload with `route: "/"` and `generatedAt` (no throw).
|
|
616
|
+
- When `config.route` (or other deps) change: removes any existing `<script data-llm="true">`, then injects a new one.
|
|
617
|
+
- Script tag: `<script type="application/llm+json" data-llm="true">` with JSON payload. Only defined, safe fields are included.
|
|
618
|
+
- **Cacheable:** If the generated payload is unchanged, the script is not replaced.
|
|
619
|
+
- **SSR-safe:** No-op when `typeof window === "undefined"`.
|
|
620
|
+
- Cleans up on unmount (removes the script).
|
|
621
|
+
|
|
622
|
+
**Modes:**
|
|
623
|
+
|
|
624
|
+
- **`manual`** (default): Uses `title`, `description`, `tags`, and any other config fields you pass.
|
|
625
|
+
- **`auto-extract`**: Fills `title`, `description`, and `outline` from the DOM (`document.title`, visible `<h1>`/`<h2>`, first 3 visible `<p>`). You can still override with config. Ignores content inside `nav`, `footer`, `script`, `style`.
|
|
626
|
+
|
|
627
|
+
**Example payload (rich):**
|
|
628
|
+
|
|
629
|
+
```json
|
|
630
|
+
{
|
|
631
|
+
"route": "/blog/ai-hooks",
|
|
632
|
+
"title": "AI Hooks in Preact",
|
|
633
|
+
"description": "A short summary...",
|
|
634
|
+
"tags": ["preact", "react", "hooks"],
|
|
635
|
+
"outline": ["Intro", "Problem", "Solution"],
|
|
636
|
+
"canonicalUrl": "https://example.com/blog/ai-hooks",
|
|
637
|
+
"language": "en",
|
|
638
|
+
"ogType": "article",
|
|
639
|
+
"ogImage": "https://example.com/og.png",
|
|
640
|
+
"siteName": "My Blog",
|
|
641
|
+
"author": "Jane Doe",
|
|
642
|
+
"publishedTime": "2025-02-14T10:00:00.000Z",
|
|
643
|
+
"modifiedTime": "2025-02-14T12:00:00.000Z",
|
|
644
|
+
"robots": "index, follow",
|
|
645
|
+
"generatedAt": "2025-02-14T12:00:00.000Z"
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
**Example: React Router**
|
|
650
|
+
|
|
651
|
+
```tsx
|
|
652
|
+
import { useLocation } from "react-router-dom";
|
|
653
|
+
import { useLLMMetadata } from "preact-missing-hooks"; // or "preact-missing-hooks/react"
|
|
654
|
+
|
|
655
|
+
function App() {
|
|
656
|
+
const { pathname } = useLocation();
|
|
657
|
+
useLLMMetadata({
|
|
658
|
+
route: pathname,
|
|
659
|
+
mode: "auto-extract",
|
|
660
|
+
title: document.title,
|
|
661
|
+
tags: ["my-app"],
|
|
662
|
+
});
|
|
663
|
+
return <Outlet />;
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Example: Preact Router**
|
|
668
|
+
|
|
669
|
+
```tsx
|
|
670
|
+
import { useLocation } from "preact-router";
|
|
671
|
+
import { useLLMMetadata } from "preact-missing-hooks";
|
|
672
|
+
|
|
673
|
+
function App() {
|
|
674
|
+
const [pathname] = useLocation();
|
|
675
|
+
useLLMMetadata({
|
|
676
|
+
route: pathname ?? "/",
|
|
677
|
+
mode: "manual",
|
|
678
|
+
title: "My Page",
|
|
679
|
+
description: "Page description",
|
|
680
|
+
tags: ["preact", "hooks"],
|
|
681
|
+
});
|
|
682
|
+
return <div>{/* your routes / children */}</div>;
|
|
487
683
|
}
|
|
488
684
|
```
|
|
489
685
|
|