preact-missing-hooks 4.6.0 → 4.8.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 +84 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.modern.mjs +1 -1
- package/dist/index.modern.mjs.map +1 -1
- 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/react.js +270 -0
- package/dist/useDeviceData.d.ts +82 -0
- package/dist/usePoll.d.ts +47 -0
- package/docs/README.md +2 -0
- package/docs/main.js +66 -0
- package/llm.package.json +57 -72
- package/llm.package.txt +21 -19
- package/package.json +15 -3
- package/scripts/ensure-microbundle-patch.cjs +46 -0
- package/src/index.ts +2 -0
- package/src/useDeviceData.ts +285 -0
- package/src/usePoll.ts +110 -0
- package/tests/useDeviceData.test.tsx +213 -0
- package/tests/usePoll.test.tsx +110 -0
package/dist/react.js
CHANGED
|
@@ -2084,12 +2084,282 @@ function usePrefetch() {
|
|
|
2084
2084
|
return { prefetch, isPrefetched };
|
|
2085
2085
|
}
|
|
2086
2086
|
|
|
2087
|
+
/**
|
|
2088
|
+
* Polls an async function at a fixed interval until it returns { done: true, data? }.
|
|
2089
|
+
* Useful for waiting on a backend job, readiness checks, or until a condition is met.
|
|
2090
|
+
*
|
|
2091
|
+
* @param pollFn - Async function called each tick. Return { done: true, data? } to stop and set result.
|
|
2092
|
+
* @param options - intervalMs, immediate, enabled
|
|
2093
|
+
* @returns { data, done, error, pollCount, start, stop }
|
|
2094
|
+
*
|
|
2095
|
+
* @example
|
|
2096
|
+
* ```tsx
|
|
2097
|
+
* const { data, done, pollCount } = usePoll(
|
|
2098
|
+
* async () => {
|
|
2099
|
+
* const res = await fetch('/api/status');
|
|
2100
|
+
* const json = await res.json();
|
|
2101
|
+
* return json.ready ? { done: true, data: json } : { done: false };
|
|
2102
|
+
* },
|
|
2103
|
+
* { intervalMs: 500, immediate: true }
|
|
2104
|
+
* );
|
|
2105
|
+
* return done ? <div>Ready: {JSON.stringify(data)}</div> : <div>Polling… ({pollCount})</div>;
|
|
2106
|
+
* ```
|
|
2107
|
+
*/
|
|
2108
|
+
function usePoll(pollFn, options = {}) {
|
|
2109
|
+
const { intervalMs = 1000, immediate = true, enabled = true } = options;
|
|
2110
|
+
const [data, setData] = react.useState(null);
|
|
2111
|
+
const [done, setDone] = react.useState(false);
|
|
2112
|
+
const [error, setError] = react.useState(null);
|
|
2113
|
+
const [pollCount, setPollCount] = react.useState(0);
|
|
2114
|
+
const [running, setRunning] = react.useState(false);
|
|
2115
|
+
const intervalRef = react.useRef(null);
|
|
2116
|
+
const pollFnRef = react.useRef(pollFn);
|
|
2117
|
+
pollFnRef.current = pollFn;
|
|
2118
|
+
const stop = react.useCallback(() => {
|
|
2119
|
+
if (intervalRef.current != null) {
|
|
2120
|
+
clearInterval(intervalRef.current);
|
|
2121
|
+
intervalRef.current = null;
|
|
2122
|
+
}
|
|
2123
|
+
setRunning(false);
|
|
2124
|
+
}, []);
|
|
2125
|
+
const tick = react.useCallback(() => __awaiter(this, void 0, void 0, function* () {
|
|
2126
|
+
try {
|
|
2127
|
+
setError(null);
|
|
2128
|
+
const result = yield pollFnRef.current();
|
|
2129
|
+
setPollCount((n) => n + 1);
|
|
2130
|
+
if (result.done) {
|
|
2131
|
+
stop();
|
|
2132
|
+
setDone(true);
|
|
2133
|
+
if (result.data !== undefined) {
|
|
2134
|
+
setData(result.data);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
catch (e) {
|
|
2139
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
2140
|
+
setError(err);
|
|
2141
|
+
stop();
|
|
2142
|
+
}
|
|
2143
|
+
}), [stop]);
|
|
2144
|
+
const start = react.useCallback(() => {
|
|
2145
|
+
if (!enabled || running)
|
|
2146
|
+
return;
|
|
2147
|
+
setRunning(true);
|
|
2148
|
+
if (immediate) {
|
|
2149
|
+
tick();
|
|
2150
|
+
}
|
|
2151
|
+
intervalRef.current = setInterval(tick, intervalMs);
|
|
2152
|
+
}, [enabled, immediate, intervalMs, running, tick]);
|
|
2153
|
+
react.useEffect(() => {
|
|
2154
|
+
if (enabled) {
|
|
2155
|
+
start();
|
|
2156
|
+
}
|
|
2157
|
+
return () => {
|
|
2158
|
+
stop();
|
|
2159
|
+
};
|
|
2160
|
+
}, [enabled]);
|
|
2161
|
+
return { data, done, error, pollCount, start, stop };
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
const SSR_DEVICE_DATA = {
|
|
2165
|
+
userAgent: "",
|
|
2166
|
+
language: "en",
|
|
2167
|
+
languages: ["en"],
|
|
2168
|
+
platform: "",
|
|
2169
|
+
cookieEnabled: false,
|
|
2170
|
+
online: true,
|
|
2171
|
+
maxTouchPoints: 0,
|
|
2172
|
+
vendor: "",
|
|
2173
|
+
touch: false,
|
|
2174
|
+
screen: {
|
|
2175
|
+
width: 0,
|
|
2176
|
+
height: 0,
|
|
2177
|
+
availWidth: 0,
|
|
2178
|
+
availHeight: 0,
|
|
2179
|
+
colorDepth: 24,
|
|
2180
|
+
pixelRatio: 1,
|
|
2181
|
+
},
|
|
2182
|
+
viewport: { width: 0, height: 0 },
|
|
2183
|
+
reducedMotion: false,
|
|
2184
|
+
colorScheme: "no-preference",
|
|
2185
|
+
};
|
|
2186
|
+
function getColorScheme() {
|
|
2187
|
+
if (typeof window === "undefined" || !window.matchMedia) {
|
|
2188
|
+
return "no-preference";
|
|
2189
|
+
}
|
|
2190
|
+
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
2191
|
+
return "dark";
|
|
2192
|
+
}
|
|
2193
|
+
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
2194
|
+
return "light";
|
|
2195
|
+
}
|
|
2196
|
+
return "no-preference";
|
|
2197
|
+
}
|
|
2198
|
+
function getReducedMotion() {
|
|
2199
|
+
if (typeof window === "undefined" || !window.matchMedia) {
|
|
2200
|
+
return false;
|
|
2201
|
+
}
|
|
2202
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2203
|
+
}
|
|
2204
|
+
/** Reads synchronous device / browser data from Navigator, Screen, and matchMedia. */
|
|
2205
|
+
function getDeviceData() {
|
|
2206
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
2207
|
+
if (typeof navigator === "undefined") {
|
|
2208
|
+
return SSR_DEVICE_DATA;
|
|
2209
|
+
}
|
|
2210
|
+
const nav = navigator;
|
|
2211
|
+
const screen = typeof globalThis.screen !== "undefined" ? globalThis.screen : null;
|
|
2212
|
+
const win = typeof globalThis.window !== "undefined" ? globalThis.window : null;
|
|
2213
|
+
const data = {
|
|
2214
|
+
userAgent: (_a = nav.userAgent) !== null && _a !== void 0 ? _a : "",
|
|
2215
|
+
language: (_b = nav.language) !== null && _b !== void 0 ? _b : "",
|
|
2216
|
+
languages: nav.languages ? [...nav.languages] : [],
|
|
2217
|
+
platform: (_c = nav.platform) !== null && _c !== void 0 ? _c : "",
|
|
2218
|
+
cookieEnabled: Boolean(nav.cookieEnabled),
|
|
2219
|
+
online: Boolean(nav.onLine),
|
|
2220
|
+
maxTouchPoints: (_d = nav.maxTouchPoints) !== null && _d !== void 0 ? _d : 0,
|
|
2221
|
+
vendor: (_e = nav.vendor) !== null && _e !== void 0 ? _e : "",
|
|
2222
|
+
touch: ((_f = nav.maxTouchPoints) !== null && _f !== void 0 ? _f : 0) > 0,
|
|
2223
|
+
screen: {
|
|
2224
|
+
width: (_g = screen === null || screen === void 0 ? void 0 : screen.width) !== null && _g !== void 0 ? _g : 0,
|
|
2225
|
+
height: (_h = screen === null || screen === void 0 ? void 0 : screen.height) !== null && _h !== void 0 ? _h : 0,
|
|
2226
|
+
availWidth: (_j = screen === null || screen === void 0 ? void 0 : screen.availWidth) !== null && _j !== void 0 ? _j : 0,
|
|
2227
|
+
availHeight: (_k = screen === null || screen === void 0 ? void 0 : screen.availHeight) !== null && _k !== void 0 ? _k : 0,
|
|
2228
|
+
colorDepth: (_l = screen === null || screen === void 0 ? void 0 : screen.colorDepth) !== null && _l !== void 0 ? _l : 24,
|
|
2229
|
+
pixelRatio: (_m = win === null || win === void 0 ? void 0 : win.devicePixelRatio) !== null && _m !== void 0 ? _m : 1,
|
|
2230
|
+
},
|
|
2231
|
+
viewport: {
|
|
2232
|
+
width: (_o = win === null || win === void 0 ? void 0 : win.innerWidth) !== null && _o !== void 0 ? _o : 0,
|
|
2233
|
+
height: (_p = win === null || win === void 0 ? void 0 : win.innerHeight) !== null && _p !== void 0 ? _p : 0,
|
|
2234
|
+
},
|
|
2235
|
+
reducedMotion: getReducedMotion(),
|
|
2236
|
+
colorScheme: getColorScheme(),
|
|
2237
|
+
};
|
|
2238
|
+
if (typeof nav.hardwareConcurrency === "number") {
|
|
2239
|
+
data.hardwareConcurrency = nav.hardwareConcurrency;
|
|
2240
|
+
}
|
|
2241
|
+
if (typeof nav.deviceMemory === "number") {
|
|
2242
|
+
data.deviceMemory = nav.deviceMemory;
|
|
2243
|
+
}
|
|
2244
|
+
const uaData = nav.userAgentData;
|
|
2245
|
+
if (uaData) {
|
|
2246
|
+
data.userAgentData = {
|
|
2247
|
+
mobile: Boolean(uaData.mobile),
|
|
2248
|
+
platform: (_q = uaData.platform) !== null && _q !== void 0 ? _q : "",
|
|
2249
|
+
brands: uaData.brands ? [...uaData.brands] : [],
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
return data;
|
|
2253
|
+
}
|
|
2254
|
+
function readBattery() {
|
|
2255
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2256
|
+
if (typeof navigator === "undefined")
|
|
2257
|
+
return undefined;
|
|
2258
|
+
const getBattery = navigator.getBattery;
|
|
2259
|
+
if (!getBattery)
|
|
2260
|
+
return undefined;
|
|
2261
|
+
try {
|
|
2262
|
+
const battery = yield getBattery.call(navigator);
|
|
2263
|
+
return { charging: battery.charging, level: battery.level };
|
|
2264
|
+
}
|
|
2265
|
+
catch (_a) {
|
|
2266
|
+
return undefined;
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
2271
|
+
* Extracts device and browser data from native Navigator, Screen, window, and
|
|
2272
|
+
* matchMedia APIs. Updates on resize, orientation, online/offline, and
|
|
2273
|
+
* prefers-color-scheme / prefers-reduced-motion changes. Optionally polls the
|
|
2274
|
+
* Battery Status API when available.
|
|
2275
|
+
*
|
|
2276
|
+
* @param options - `includeBattery` (default true), `batteryPollIntervalMs` (default 60000)
|
|
2277
|
+
* @returns Current {@link DeviceData} snapshot
|
|
2278
|
+
*
|
|
2279
|
+
* @example
|
|
2280
|
+
* ```tsx
|
|
2281
|
+
* function DevicePanel() {
|
|
2282
|
+
* const device = useDeviceData();
|
|
2283
|
+
* return (
|
|
2284
|
+
* <dl>
|
|
2285
|
+
* <dt>Language</dt><dd>{device.language}</dd>
|
|
2286
|
+
* <dt>CPUs</dt><dd>{device.hardwareConcurrency ?? '—'}</dd>
|
|
2287
|
+
* <dt>Viewport</dt><dd>{device.viewport.width}×{device.viewport.height}</dd>
|
|
2288
|
+
* <dt>Theme</dt><dd>{device.colorScheme}</dd>
|
|
2289
|
+
* </dl>
|
|
2290
|
+
* );
|
|
2291
|
+
* }
|
|
2292
|
+
* ```
|
|
2293
|
+
*/
|
|
2294
|
+
function useDeviceData(options = {}) {
|
|
2295
|
+
const { includeBattery = true, batteryPollIntervalMs = 60000 } = options;
|
|
2296
|
+
const [data, setData] = react.useState(() => getDeviceData());
|
|
2297
|
+
const refresh = react.useCallback(() => {
|
|
2298
|
+
setData((prev) => {
|
|
2299
|
+
const next = getDeviceData();
|
|
2300
|
+
return prev.battery ? Object.assign(Object.assign({}, next), { battery: prev.battery }) : next;
|
|
2301
|
+
});
|
|
2302
|
+
}, []);
|
|
2303
|
+
react.useEffect(() => {
|
|
2304
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2305
|
+
if (typeof window === "undefined")
|
|
2306
|
+
return;
|
|
2307
|
+
const onResize = () => refresh();
|
|
2308
|
+
window.addEventListener("resize", onResize);
|
|
2309
|
+
window.addEventListener("orientationchange", onResize);
|
|
2310
|
+
window.addEventListener("online", refresh);
|
|
2311
|
+
window.addEventListener("offline", refresh);
|
|
2312
|
+
const reducedMotionMq = (_a = window.matchMedia) === null || _a === void 0 ? void 0 : _a.call(window, "(prefers-reduced-motion: reduce)");
|
|
2313
|
+
const darkMq = (_b = window.matchMedia) === null || _b === void 0 ? void 0 : _b.call(window, "(prefers-color-scheme: dark)");
|
|
2314
|
+
const lightMq = (_c = window.matchMedia) === null || _c === void 0 ? void 0 : _c.call(window, "(prefers-color-scheme: light)");
|
|
2315
|
+
const onMediaChange = () => refresh();
|
|
2316
|
+
(_d = reducedMotionMq === null || reducedMotionMq === void 0 ? void 0 : reducedMotionMq.addEventListener) === null || _d === void 0 ? void 0 : _d.call(reducedMotionMq, "change", onMediaChange);
|
|
2317
|
+
(_e = darkMq === null || darkMq === void 0 ? void 0 : darkMq.addEventListener) === null || _e === void 0 ? void 0 : _e.call(darkMq, "change", onMediaChange);
|
|
2318
|
+
(_f = lightMq === null || lightMq === void 0 ? void 0 : lightMq.addEventListener) === null || _f === void 0 ? void 0 : _f.call(lightMq, "change", onMediaChange);
|
|
2319
|
+
return () => {
|
|
2320
|
+
var _a, _b, _c;
|
|
2321
|
+
window.removeEventListener("resize", onResize);
|
|
2322
|
+
window.removeEventListener("orientationchange", onResize);
|
|
2323
|
+
window.removeEventListener("online", refresh);
|
|
2324
|
+
window.removeEventListener("offline", refresh);
|
|
2325
|
+
(_a = reducedMotionMq === null || reducedMotionMq === void 0 ? void 0 : reducedMotionMq.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(reducedMotionMq, "change", onMediaChange);
|
|
2326
|
+
(_b = darkMq === null || darkMq === void 0 ? void 0 : darkMq.removeEventListener) === null || _b === void 0 ? void 0 : _b.call(darkMq, "change", onMediaChange);
|
|
2327
|
+
(_c = lightMq === null || lightMq === void 0 ? void 0 : lightMq.removeEventListener) === null || _c === void 0 ? void 0 : _c.call(lightMq, "change", onMediaChange);
|
|
2328
|
+
};
|
|
2329
|
+
}, [refresh]);
|
|
2330
|
+
react.useEffect(() => {
|
|
2331
|
+
if (!includeBattery || typeof navigator === "undefined")
|
|
2332
|
+
return;
|
|
2333
|
+
let cancelled = false;
|
|
2334
|
+
let intervalId;
|
|
2335
|
+
const updateBattery = () => __awaiter(this, void 0, void 0, function* () {
|
|
2336
|
+
const battery = yield readBattery();
|
|
2337
|
+
if (cancelled || battery === undefined)
|
|
2338
|
+
return;
|
|
2339
|
+
setData((prev) => (Object.assign(Object.assign({}, prev), { battery })));
|
|
2340
|
+
});
|
|
2341
|
+
void updateBattery();
|
|
2342
|
+
if (batteryPollIntervalMs > 0) {
|
|
2343
|
+
intervalId = setInterval(() => void updateBattery(), batteryPollIntervalMs);
|
|
2344
|
+
}
|
|
2345
|
+
return () => {
|
|
2346
|
+
cancelled = true;
|
|
2347
|
+
if (intervalId !== undefined)
|
|
2348
|
+
clearInterval(intervalId);
|
|
2349
|
+
};
|
|
2350
|
+
}, [includeBattery, batteryPollIntervalMs]);
|
|
2351
|
+
return data;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
exports.getDeviceData = getDeviceData;
|
|
2087
2355
|
exports.useClipboard = useClipboard;
|
|
2356
|
+
exports.useDeviceData = useDeviceData;
|
|
2088
2357
|
exports.useEventBus = useEventBus;
|
|
2089
2358
|
exports.useIndexedDB = useIndexedDB;
|
|
2090
2359
|
exports.useLLMMetadata = useLLMMetadata;
|
|
2091
2360
|
exports.useMutationObserver = useMutationObserver;
|
|
2092
2361
|
exports.useNetworkState = useNetworkState;
|
|
2362
|
+
exports.usePoll = usePoll;
|
|
2093
2363
|
exports.usePreferredTheme = usePreferredTheme;
|
|
2094
2364
|
exports.usePrefetch = usePrefetch;
|
|
2095
2365
|
exports.useRBAC = useRBAC;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Parsed Client Hints from `navigator.userAgentData` when available */
|
|
2
|
+
export interface UserAgentDataInfo {
|
|
3
|
+
mobile: boolean;
|
|
4
|
+
platform: string;
|
|
5
|
+
brands: ReadonlyArray<{
|
|
6
|
+
brand: string;
|
|
7
|
+
version: string;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
/** Screen and display metrics from `screen` and `window` */
|
|
11
|
+
export interface DeviceScreenInfo {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
availWidth: number;
|
|
15
|
+
availHeight: number;
|
|
16
|
+
colorDepth: number;
|
|
17
|
+
pixelRatio: number;
|
|
18
|
+
}
|
|
19
|
+
/** Viewport size (`window.innerWidth` / `innerHeight`) */
|
|
20
|
+
export interface DeviceViewportInfo {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}
|
|
24
|
+
/** Battery status from the Battery Status API when available */
|
|
25
|
+
export interface DeviceBatteryInfo {
|
|
26
|
+
charging: boolean;
|
|
27
|
+
level: number;
|
|
28
|
+
}
|
|
29
|
+
/** Snapshot of device / browser data from native Navigator and related APIs */
|
|
30
|
+
export interface DeviceData {
|
|
31
|
+
userAgent: string;
|
|
32
|
+
language: string;
|
|
33
|
+
languages: readonly string[];
|
|
34
|
+
platform: string;
|
|
35
|
+
cookieEnabled: boolean;
|
|
36
|
+
online: boolean;
|
|
37
|
+
hardwareConcurrency?: number;
|
|
38
|
+
/** Approximate device RAM in GB (Chrome / some browsers only) */
|
|
39
|
+
deviceMemory?: number;
|
|
40
|
+
maxTouchPoints: number;
|
|
41
|
+
vendor: string;
|
|
42
|
+
touch: boolean;
|
|
43
|
+
screen: DeviceScreenInfo;
|
|
44
|
+
viewport: DeviceViewportInfo;
|
|
45
|
+
userAgentData?: UserAgentDataInfo;
|
|
46
|
+
reducedMotion: boolean;
|
|
47
|
+
colorScheme: "light" | "dark" | "no-preference";
|
|
48
|
+
battery?: DeviceBatteryInfo;
|
|
49
|
+
}
|
|
50
|
+
export interface UseDeviceDataOptions {
|
|
51
|
+
/** Fetch battery info when the Battery Status API exists (default: true) */
|
|
52
|
+
includeBattery?: boolean;
|
|
53
|
+
/** Battery refresh interval in ms (default: 60000) */
|
|
54
|
+
batteryPollIntervalMs?: number;
|
|
55
|
+
}
|
|
56
|
+
/** Reads synchronous device / browser data from Navigator, Screen, and matchMedia. */
|
|
57
|
+
export declare function getDeviceData(): DeviceData;
|
|
58
|
+
/**
|
|
59
|
+
* Extracts device and browser data from native Navigator, Screen, window, and
|
|
60
|
+
* matchMedia APIs. Updates on resize, orientation, online/offline, and
|
|
61
|
+
* prefers-color-scheme / prefers-reduced-motion changes. Optionally polls the
|
|
62
|
+
* Battery Status API when available.
|
|
63
|
+
*
|
|
64
|
+
* @param options - `includeBattery` (default true), `batteryPollIntervalMs` (default 60000)
|
|
65
|
+
* @returns Current {@link DeviceData} snapshot
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* function DevicePanel() {
|
|
70
|
+
* const device = useDeviceData();
|
|
71
|
+
* return (
|
|
72
|
+
* <dl>
|
|
73
|
+
* <dt>Language</dt><dd>{device.language}</dd>
|
|
74
|
+
* <dt>CPUs</dt><dd>{device.hardwareConcurrency ?? '—'}</dd>
|
|
75
|
+
* <dt>Viewport</dt><dd>{device.viewport.width}×{device.viewport.height}</dd>
|
|
76
|
+
* <dt>Theme</dt><dd>{device.colorScheme}</dd>
|
|
77
|
+
* </dl>
|
|
78
|
+
* );
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function useDeviceData(options?: UseDeviceDataOptions): DeviceData;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface UsePollOptions {
|
|
2
|
+
/** Polling interval in milliseconds. Default: 1000 */
|
|
3
|
+
intervalMs?: number;
|
|
4
|
+
/** Run the poll function immediately when the hook mounts. Default: true */
|
|
5
|
+
immediate?: boolean;
|
|
6
|
+
/** When false, do not start or continue polling. Default: true */
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface UsePollResult<T> {
|
|
10
|
+
/** Last resolved data when poll returned done: true */
|
|
11
|
+
data: T | null;
|
|
12
|
+
/** True once the poll function returned { done: true } */
|
|
13
|
+
done: boolean;
|
|
14
|
+
/** Error from the last failed poll call */
|
|
15
|
+
error: Error | null;
|
|
16
|
+
/** Number of times the poll function has been invoked */
|
|
17
|
+
pollCount: number;
|
|
18
|
+
/** Manually start polling (e.g. after reset). Only has effect when not already polling. */
|
|
19
|
+
start: () => void;
|
|
20
|
+
/** Stop polling. */
|
|
21
|
+
stop: () => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Polls an async function at a fixed interval until it returns { done: true, data? }.
|
|
25
|
+
* Useful for waiting on a backend job, readiness checks, or until a condition is met.
|
|
26
|
+
*
|
|
27
|
+
* @param pollFn - Async function called each tick. Return { done: true, data? } to stop and set result.
|
|
28
|
+
* @param options - intervalMs, immediate, enabled
|
|
29
|
+
* @returns { data, done, error, pollCount, start, stop }
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const { data, done, pollCount } = usePoll(
|
|
34
|
+
* async () => {
|
|
35
|
+
* const res = await fetch('/api/status');
|
|
36
|
+
* const json = await res.json();
|
|
37
|
+
* return json.ready ? { done: true, data: json } : { done: false };
|
|
38
|
+
* },
|
|
39
|
+
* { intervalMs: 500, immediate: true }
|
|
40
|
+
* );
|
|
41
|
+
* return done ? <div>Ready: {JSON.stringify(data)}</div> : <div>Polling… ({pollCount})</div>;
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function usePoll<T>(pollFn: () => Promise<{
|
|
45
|
+
done: boolean;
|
|
46
|
+
data?: T;
|
|
47
|
+
}>, options?: UsePollOptions): UsePollResult<T>;
|
package/docs/README.md
CHANGED
|
@@ -41,7 +41,9 @@ Open `docs/index.html` in a browser that supports ES modules and import maps. Th
|
|
|
41
41
|
| **useWrappedChildren** | Children buttons get injected styles. |
|
|
42
42
|
| **usePreferredTheme** | Shows light / dark / no-preference from system. |
|
|
43
43
|
| **useNetworkState** | Online/offline and connection type. |
|
|
44
|
+
| **useDeviceData** | Live device/browser snapshot: language, CPUs, memory, viewport, screen, touch, color scheme, reduced motion, Client Hints. |
|
|
44
45
|
| **usePrefetch** | Hover or click to prefetch a URL (document or fetch); see prefetched status. |
|
|
46
|
+
| **usePoll** | Poll until done (3 ticks); see poll count and result. Stop button to cancel. |
|
|
45
47
|
| **useClipboard** | Copy and paste; see “Copied!” and pasted text. |
|
|
46
48
|
| **useRageClick** | Click the area 3+ times quickly; rage click count. |
|
|
47
49
|
| **useThreadedWorker** | Run a task; see loading and result. |
|
package/docs/main.js
CHANGED
|
@@ -22,6 +22,8 @@ const {
|
|
|
22
22
|
useRefPrint,
|
|
23
23
|
useRBAC,
|
|
24
24
|
usePrefetch,
|
|
25
|
+
usePoll,
|
|
26
|
+
useDeviceData,
|
|
25
27
|
} = await import(
|
|
26
28
|
isLocal ? '../dist/index.module.js' : 'https://unpkg.com/preact-missing-hooks/dist/index.module.js'
|
|
27
29
|
);
|
|
@@ -85,6 +87,33 @@ function DemoNetworkState() {
|
|
|
85
87
|
);
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
function DemoDeviceData() {
|
|
91
|
+
const device = useDeviceData({ includeBattery: false });
|
|
92
|
+
const rows = [
|
|
93
|
+
['Language', device.language],
|
|
94
|
+
['Platform', device.platform || device.userAgentData?.platform || '—'],
|
|
95
|
+
['CPUs', device.hardwareConcurrency != null ? String(device.hardwareConcurrency) : '—'],
|
|
96
|
+
['Memory (GB)', device.deviceMemory != null ? String(device.deviceMemory) : '—'],
|
|
97
|
+
['Viewport', device.viewport.width + '×' + device.viewport.height],
|
|
98
|
+
['Screen', device.screen.width + '×' + device.screen.height],
|
|
99
|
+
['Touch', device.touch ? 'yes' : 'no'],
|
|
100
|
+
['Color scheme', device.colorScheme],
|
|
101
|
+
['Reduced motion', device.reducedMotion ? 'yes' : 'no'],
|
|
102
|
+
['Online', device.online ? 'yes' : 'no'],
|
|
103
|
+
];
|
|
104
|
+
if (device.userAgentData?.mobile) {
|
|
105
|
+
rows.push(['Mobile (UA-CH)', 'yes']);
|
|
106
|
+
}
|
|
107
|
+
return h('div', { class: 'status', style: { fontSize: '0.85rem' } },
|
|
108
|
+
rows.map(([label, value]) =>
|
|
109
|
+
h('div', { key: label, style: { marginBottom: '0.25rem' } },
|
|
110
|
+
h('strong', {}, label + ': '),
|
|
111
|
+
value
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
88
117
|
function DemoPrefetch() {
|
|
89
118
|
const { prefetch, isPrefetched } = usePrefetch();
|
|
90
119
|
const [lastUrl, setLastUrl] = useState('');
|
|
@@ -111,6 +140,29 @@ function DemoPrefetch() {
|
|
|
111
140
|
);
|
|
112
141
|
}
|
|
113
142
|
|
|
143
|
+
function DemoPoll() {
|
|
144
|
+
const countRef = { current: 0 };
|
|
145
|
+
const { data, done, error, pollCount, start, stop } = usePoll(
|
|
146
|
+
async () => {
|
|
147
|
+
countRef.current += 1;
|
|
148
|
+
if (countRef.current >= 3) return { done: true, data: { message: 'Ready after ' + countRef.current + ' polls' } };
|
|
149
|
+
return { done: false };
|
|
150
|
+
},
|
|
151
|
+
{ intervalMs: 700, immediate: true }
|
|
152
|
+
);
|
|
153
|
+
return h('div', {},
|
|
154
|
+
h('div', { style: { marginBottom: '0.5rem', fontSize: '0.85rem' } }, [
|
|
155
|
+
h('button', { onClick: start }, 'Start'),
|
|
156
|
+
' ',
|
|
157
|
+
h('button', { onClick: stop }, 'Stop'),
|
|
158
|
+
]),
|
|
159
|
+
error ? h('span', { class: 'badge', style: { background: 'var(--red)', color: '#fff' } }, error.message) : null,
|
|
160
|
+
done
|
|
161
|
+
? h('span', { class: 'badge green', style: { marginLeft: '0.35rem' } }, data?.message ?? 'Done')
|
|
162
|
+
: h('span', { class: 'status' }, 'Polling… (' + pollCount + ' calls)')
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
114
166
|
function DemoClipboard() {
|
|
115
167
|
const { copy, paste, copied, error } = useClipboard();
|
|
116
168
|
const [pasted, setPasted] = useState('');
|
|
@@ -538,6 +590,13 @@ const HOOKS = [
|
|
|
538
590
|
code: `const state = useNetworkState();\n// state.online, state.effectiveType, ...`,
|
|
539
591
|
Live: DemoNetworkState,
|
|
540
592
|
},
|
|
593
|
+
{
|
|
594
|
+
name: 'useDeviceData',
|
|
595
|
+
flow: 'Component → useDeviceData() → Navigator + Screen + matchMedia (+ optional Battery API)',
|
|
596
|
+
summary: 'Extracts device/browser data: language, platform, CPUs, memory, screen, viewport, touch, color scheme, reduced motion, Client Hints, and optional battery.',
|
|
597
|
+
code: `const device = useDeviceData();\n// device.language, device.hardwareConcurrency, device.viewport, device.colorScheme`,
|
|
598
|
+
Live: DemoDeviceData,
|
|
599
|
+
},
|
|
541
600
|
{
|
|
542
601
|
name: 'usePrefetch',
|
|
543
602
|
flow: 'Component → usePrefetch() → prefetch(url, options?) → link rel=prefetch or fetch()',
|
|
@@ -545,6 +604,13 @@ const HOOKS = [
|
|
|
545
604
|
code: `const { prefetch, isPrefetched } = usePrefetch();\n<a onMouseEnter={() => prefetch(href)} href={href}>Link</a>\n// or prefetch(url, { as: 'fetch' }) for API`,
|
|
546
605
|
Live: DemoPrefetch,
|
|
547
606
|
},
|
|
607
|
+
{
|
|
608
|
+
name: 'usePoll',
|
|
609
|
+
flow: 'Component → usePoll(pollFn, { intervalMs, immediate }) → poll until done: true → data, done, pollCount',
|
|
610
|
+
summary: 'Polls an async function at a fixed interval until it returns { done: true, data? }. Stops on error. Good for readiness checks or waiting on a backend job.',
|
|
611
|
+
code: `const { data, done, error, pollCount, stop } = usePoll(\n async () => (await fetch('/api/status')).ok ? { done: true, data } : { done: false },\n { intervalMs: 1000, immediate: true }\n);`,
|
|
612
|
+
Live: DemoPoll,
|
|
613
|
+
},
|
|
548
614
|
{
|
|
549
615
|
name: 'useClipboard',
|
|
550
616
|
flow: 'Component → useClipboard() → copy(text) / paste() → Clipboard API',
|