preact-missing-hooks 4.7.0 → 4.9.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/dist/react.js CHANGED
@@ -2161,7 +2161,346 @@ function usePoll(pollFn, options = {}) {
2161
2161
  return { data, done, error, pollCount, start, stop };
2162
2162
  }
2163
2163
 
2164
+ const UNKNOWN_BROWSER = { name: "Unknown", version: "" };
2165
+ const UNKNOWN_OS = { name: "Unknown", version: "" };
2166
+ const SSR_DEVICE_DATA = {
2167
+ userAgent: "",
2168
+ language: "en",
2169
+ languages: ["en"],
2170
+ platform: "",
2171
+ browser: UNKNOWN_BROWSER,
2172
+ os: UNKNOWN_OS,
2173
+ cookieEnabled: false,
2174
+ online: true,
2175
+ maxTouchPoints: 0,
2176
+ vendor: "",
2177
+ touch: false,
2178
+ screen: {
2179
+ width: 0,
2180
+ height: 0,
2181
+ availWidth: 0,
2182
+ availHeight: 0,
2183
+ colorDepth: 24,
2184
+ pixelRatio: 1,
2185
+ },
2186
+ viewport: { width: 0, height: 0 },
2187
+ reducedMotion: false,
2188
+ colorScheme: "no-preference",
2189
+ };
2190
+ const NOT_A_BRAND = /not.?a.?brand/i;
2191
+ /** Parse browser and OS from a user-agent string (sync fallback). */
2192
+ function parseUserAgent(userAgent) {
2193
+ var _a;
2194
+ const browser = Object.assign({}, UNKNOWN_BROWSER);
2195
+ const os = Object.assign({}, UNKNOWN_OS);
2196
+ if (!userAgent) {
2197
+ return { browser, os };
2198
+ }
2199
+ const win = userAgent.match(/Windows NT ([\d.]+)/);
2200
+ if (win) {
2201
+ os.name = "Windows";
2202
+ os.version = win[1];
2203
+ }
2204
+ else {
2205
+ const mac = userAgent.match(/Mac OS X ([\d._]+)/);
2206
+ if (mac) {
2207
+ os.name = "macOS";
2208
+ os.version = mac[1].replace(/_/g, ".");
2209
+ }
2210
+ else {
2211
+ const android = userAgent.match(/Android ([\d.]+)/);
2212
+ if (android) {
2213
+ os.name = "Android";
2214
+ os.version = android[1];
2215
+ }
2216
+ else {
2217
+ const ios = userAgent.match(/(?:iPhone OS|CPU OS) ([\d_]+)/);
2218
+ if (ios) {
2219
+ os.name = "iOS";
2220
+ os.version = ios[1].replace(/_/g, ".");
2221
+ }
2222
+ else if (/Linux/.test(userAgent)) {
2223
+ os.name = "Linux";
2224
+ const linux = userAgent.match(/Linux ([\d.]+)/);
2225
+ os.version = (_a = linux === null || linux === void 0 ? void 0 : linux[1]) !== null && _a !== void 0 ? _a : "";
2226
+ }
2227
+ }
2228
+ }
2229
+ }
2230
+ const edg = userAgent.match(/Edg(?:A|iOS)?\/([\d.]+)/);
2231
+ const opera = userAgent.match(/OPR\/([\d.]+)/);
2232
+ const firefox = userAgent.match(/Firefox\/([\d.]+)/);
2233
+ const safari = /Version\/([\d.]+)/.test(userAgent) &&
2234
+ /Safari/.test(userAgent) &&
2235
+ !/Chrome|Chromium|Edg|OPR/.test(userAgent)
2236
+ ? userAgent.match(/Version\/([\d.]+)/)
2237
+ : null;
2238
+ const chrome = userAgent.match(/Chrome\/([\d.]+)/);
2239
+ if (edg) {
2240
+ browser.name = "Edge";
2241
+ browser.version = edg[1];
2242
+ }
2243
+ else if (opera) {
2244
+ browser.name = "Opera";
2245
+ browser.version = opera[1];
2246
+ }
2247
+ else if (firefox) {
2248
+ browser.name = "Firefox";
2249
+ browser.version = firefox[1];
2250
+ }
2251
+ else if (safari) {
2252
+ browser.name = "Safari";
2253
+ browser.version = safari[1];
2254
+ }
2255
+ else if (chrome) {
2256
+ browser.name = "Chrome";
2257
+ browser.version = chrome[1];
2258
+ }
2259
+ return { browser, os };
2260
+ }
2261
+ function pickBrowserBrand(brands) {
2262
+ const meaningful = brands.find((b) => !NOT_A_BRAND.test(b.brand));
2263
+ const pick = meaningful !== null && meaningful !== void 0 ? meaningful : brands[0];
2264
+ if (!pick)
2265
+ return Object.assign({}, UNKNOWN_BROWSER);
2266
+ return { name: pick.brand, version: pick.version };
2267
+ }
2268
+ function mergeBrowserOsFromUaData(uaData, fallback) {
2269
+ var _a, _b;
2270
+ const browser = ((_a = uaData.brands) === null || _a === void 0 ? void 0 : _a.length)
2271
+ ? pickBrowserBrand(uaData.brands)
2272
+ : Object.assign({}, fallback.browser);
2273
+ const os = {
2274
+ name: ((_b = uaData.platform) === null || _b === void 0 ? void 0 : _b.trim()) || fallback.os.name,
2275
+ version: fallback.os.version,
2276
+ };
2277
+ return { browser, os };
2278
+ }
2279
+ function getColorScheme() {
2280
+ if (typeof window === "undefined" || !window.matchMedia) {
2281
+ return "no-preference";
2282
+ }
2283
+ if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
2284
+ return "dark";
2285
+ }
2286
+ if (window.matchMedia("(prefers-color-scheme: light)").matches) {
2287
+ return "light";
2288
+ }
2289
+ return "no-preference";
2290
+ }
2291
+ function getReducedMotion() {
2292
+ if (typeof window === "undefined" || !window.matchMedia) {
2293
+ return false;
2294
+ }
2295
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2296
+ }
2297
+ /** Reads synchronous device / browser data from Navigator, Screen, and matchMedia. */
2298
+ function getDeviceData() {
2299
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
2300
+ if (typeof navigator === "undefined") {
2301
+ return SSR_DEVICE_DATA;
2302
+ }
2303
+ const nav = navigator;
2304
+ const screen = typeof globalThis.screen !== "undefined" ? globalThis.screen : null;
2305
+ const win = typeof globalThis.window !== "undefined" ? globalThis.window : null;
2306
+ const ua = (_a = nav.userAgent) !== null && _a !== void 0 ? _a : "";
2307
+ const parsed = parseUserAgent(ua);
2308
+ const uaData = nav.userAgentData;
2309
+ const { browser, os } = uaData
2310
+ ? mergeBrowserOsFromUaData(uaData, parsed)
2311
+ : parsed;
2312
+ const data = {
2313
+ userAgent: ua,
2314
+ language: (_b = nav.language) !== null && _b !== void 0 ? _b : "",
2315
+ languages: nav.languages ? [...nav.languages] : [],
2316
+ platform: (_c = nav.platform) !== null && _c !== void 0 ? _c : "",
2317
+ browser,
2318
+ os,
2319
+ cookieEnabled: Boolean(nav.cookieEnabled),
2320
+ online: Boolean(nav.onLine),
2321
+ maxTouchPoints: (_d = nav.maxTouchPoints) !== null && _d !== void 0 ? _d : 0,
2322
+ vendor: (_e = nav.vendor) !== null && _e !== void 0 ? _e : "",
2323
+ touch: ((_f = nav.maxTouchPoints) !== null && _f !== void 0 ? _f : 0) > 0,
2324
+ screen: {
2325
+ width: (_g = screen === null || screen === void 0 ? void 0 : screen.width) !== null && _g !== void 0 ? _g : 0,
2326
+ height: (_h = screen === null || screen === void 0 ? void 0 : screen.height) !== null && _h !== void 0 ? _h : 0,
2327
+ availWidth: (_j = screen === null || screen === void 0 ? void 0 : screen.availWidth) !== null && _j !== void 0 ? _j : 0,
2328
+ availHeight: (_k = screen === null || screen === void 0 ? void 0 : screen.availHeight) !== null && _k !== void 0 ? _k : 0,
2329
+ colorDepth: (_l = screen === null || screen === void 0 ? void 0 : screen.colorDepth) !== null && _l !== void 0 ? _l : 24,
2330
+ pixelRatio: (_m = win === null || win === void 0 ? void 0 : win.devicePixelRatio) !== null && _m !== void 0 ? _m : 1,
2331
+ },
2332
+ viewport: {
2333
+ width: (_o = win === null || win === void 0 ? void 0 : win.innerWidth) !== null && _o !== void 0 ? _o : 0,
2334
+ height: (_p = win === null || win === void 0 ? void 0 : win.innerHeight) !== null && _p !== void 0 ? _p : 0,
2335
+ },
2336
+ reducedMotion: getReducedMotion(),
2337
+ colorScheme: getColorScheme(),
2338
+ };
2339
+ if (typeof nav.hardwareConcurrency === "number") {
2340
+ data.hardwareConcurrency = nav.hardwareConcurrency;
2341
+ }
2342
+ if (typeof nav.deviceMemory === "number") {
2343
+ data.deviceMemory = nav.deviceMemory;
2344
+ }
2345
+ if (uaData) {
2346
+ data.userAgentData = {
2347
+ mobile: Boolean(uaData.mobile),
2348
+ platform: (_q = uaData.platform) !== null && _q !== void 0 ? _q : "",
2349
+ brands: uaData.brands ? [...uaData.brands] : [],
2350
+ };
2351
+ }
2352
+ return data;
2353
+ }
2354
+ function readHighEntropyBrowserOs(uaData, current) {
2355
+ return __awaiter(this, void 0, void 0, function* () {
2356
+ var _a, _b;
2357
+ if (!uaData.getHighEntropyValues)
2358
+ return undefined;
2359
+ try {
2360
+ const hints = yield uaData.getHighEntropyValues([
2361
+ "platformVersion",
2362
+ "fullVersionList",
2363
+ ]);
2364
+ const browser = ((_a = hints.fullVersionList) === null || _a === void 0 ? void 0 : _a.length)
2365
+ ? pickBrowserBrand(hints.fullVersionList)
2366
+ : Object.assign({}, current.browser);
2367
+ const os = {
2368
+ name: current.os.name,
2369
+ version: ((_b = hints.platformVersion) === null || _b === void 0 ? void 0 : _b.trim()) || current.os.version,
2370
+ };
2371
+ return { browser, os };
2372
+ }
2373
+ catch (_c) {
2374
+ return undefined;
2375
+ }
2376
+ });
2377
+ }
2378
+ function readBattery() {
2379
+ return __awaiter(this, void 0, void 0, function* () {
2380
+ if (typeof navigator === "undefined")
2381
+ return undefined;
2382
+ const getBattery = navigator.getBattery;
2383
+ if (!getBattery)
2384
+ return undefined;
2385
+ try {
2386
+ const battery = yield getBattery.call(navigator);
2387
+ return { charging: battery.charging, level: battery.level };
2388
+ }
2389
+ catch (_a) {
2390
+ return undefined;
2391
+ }
2392
+ });
2393
+ }
2394
+ /**
2395
+ * Extracts device and browser data from native Navigator, Screen, window, and
2396
+ * matchMedia APIs. Updates on resize, orientation, online/offline, and
2397
+ * prefers-color-scheme / prefers-reduced-motion changes. Optionally polls the
2398
+ * Battery Status API when available.
2399
+ *
2400
+ * @param options - `includeBattery` (default true), `batteryPollIntervalMs` (default 60000)
2401
+ * @returns Current {@link DeviceData} snapshot
2402
+ *
2403
+ * @example
2404
+ * ```tsx
2405
+ * function DevicePanel() {
2406
+ * const device = useDeviceData();
2407
+ * return (
2408
+ * <dl>
2409
+ * <dt>Browser</dt><dd>{device.browser.name} {device.browser.version}</dd>
2410
+ * <dt>OS</dt><dd>{device.os.name} {device.os.version}</dd>
2411
+ * <dt>Language</dt><dd>{device.language}</dd>
2412
+ * <dt>Viewport</dt><dd>{device.viewport.width}×{device.viewport.height}</dd>
2413
+ * </dl>
2414
+ * );
2415
+ * }
2416
+ * ```
2417
+ */
2418
+ function useDeviceData(options = {}) {
2419
+ const { includeBattery = true, batteryPollIntervalMs = 60000, includeHighEntropy = true, } = options;
2420
+ const [data, setData] = react.useState(() => getDeviceData());
2421
+ const refresh = react.useCallback(() => {
2422
+ setData((prev) => {
2423
+ const next = getDeviceData();
2424
+ return Object.assign(Object.assign({}, next), (prev.battery ? { battery: prev.battery } : {}));
2425
+ });
2426
+ }, []);
2427
+ react.useEffect(() => {
2428
+ var _a, _b, _c, _d, _e, _f;
2429
+ if (typeof window === "undefined")
2430
+ return;
2431
+ const onResize = () => refresh();
2432
+ window.addEventListener("resize", onResize);
2433
+ window.addEventListener("orientationchange", onResize);
2434
+ window.addEventListener("online", refresh);
2435
+ window.addEventListener("offline", refresh);
2436
+ const reducedMotionMq = (_a = window.matchMedia) === null || _a === void 0 ? void 0 : _a.call(window, "(prefers-reduced-motion: reduce)");
2437
+ const darkMq = (_b = window.matchMedia) === null || _b === void 0 ? void 0 : _b.call(window, "(prefers-color-scheme: dark)");
2438
+ const lightMq = (_c = window.matchMedia) === null || _c === void 0 ? void 0 : _c.call(window, "(prefers-color-scheme: light)");
2439
+ const onMediaChange = () => refresh();
2440
+ (_d = reducedMotionMq === null || reducedMotionMq === void 0 ? void 0 : reducedMotionMq.addEventListener) === null || _d === void 0 ? void 0 : _d.call(reducedMotionMq, "change", onMediaChange);
2441
+ (_e = darkMq === null || darkMq === void 0 ? void 0 : darkMq.addEventListener) === null || _e === void 0 ? void 0 : _e.call(darkMq, "change", onMediaChange);
2442
+ (_f = lightMq === null || lightMq === void 0 ? void 0 : lightMq.addEventListener) === null || _f === void 0 ? void 0 : _f.call(lightMq, "change", onMediaChange);
2443
+ return () => {
2444
+ var _a, _b, _c;
2445
+ window.removeEventListener("resize", onResize);
2446
+ window.removeEventListener("orientationchange", onResize);
2447
+ window.removeEventListener("online", refresh);
2448
+ window.removeEventListener("offline", refresh);
2449
+ (_a = reducedMotionMq === null || reducedMotionMq === void 0 ? void 0 : reducedMotionMq.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(reducedMotionMq, "change", onMediaChange);
2450
+ (_b = darkMq === null || darkMq === void 0 ? void 0 : darkMq.removeEventListener) === null || _b === void 0 ? void 0 : _b.call(darkMq, "change", onMediaChange);
2451
+ (_c = lightMq === null || lightMq === void 0 ? void 0 : lightMq.removeEventListener) === null || _c === void 0 ? void 0 : _c.call(lightMq, "change", onMediaChange);
2452
+ };
2453
+ }, [refresh]);
2454
+ react.useEffect(() => {
2455
+ if (!includeHighEntropy || typeof navigator === "undefined")
2456
+ return;
2457
+ const uaData = navigator.userAgentData;
2458
+ if (!(uaData === null || uaData === void 0 ? void 0 : uaData.getHighEntropyValues))
2459
+ return;
2460
+ let cancelled = false;
2461
+ const updateHighEntropy = () => __awaiter(this, void 0, void 0, function* () {
2462
+ const base = getDeviceData();
2463
+ const enriched = yield readHighEntropyBrowserOs(uaData, {
2464
+ browser: base.browser,
2465
+ os: base.os,
2466
+ });
2467
+ if (cancelled || !enriched)
2468
+ return;
2469
+ setData((prev) => (Object.assign(Object.assign({}, prev), { browser: enriched.browser, os: enriched.os })));
2470
+ });
2471
+ void updateHighEntropy();
2472
+ return () => {
2473
+ cancelled = true;
2474
+ };
2475
+ }, [includeHighEntropy]);
2476
+ react.useEffect(() => {
2477
+ if (!includeBattery || typeof navigator === "undefined")
2478
+ return;
2479
+ let cancelled = false;
2480
+ let intervalId;
2481
+ const updateBattery = () => __awaiter(this, void 0, void 0, function* () {
2482
+ const battery = yield readBattery();
2483
+ if (cancelled || battery === undefined)
2484
+ return;
2485
+ setData((prev) => (Object.assign(Object.assign({}, prev), { battery })));
2486
+ });
2487
+ void updateBattery();
2488
+ if (batteryPollIntervalMs > 0) {
2489
+ intervalId = setInterval(() => void updateBattery(), batteryPollIntervalMs);
2490
+ }
2491
+ return () => {
2492
+ cancelled = true;
2493
+ if (intervalId !== undefined)
2494
+ clearInterval(intervalId);
2495
+ };
2496
+ }, [includeBattery, batteryPollIntervalMs]);
2497
+ return data;
2498
+ }
2499
+
2500
+ exports.getDeviceData = getDeviceData;
2501
+ exports.parseUserAgent = parseUserAgent;
2164
2502
  exports.useClipboard = useClipboard;
2503
+ exports.useDeviceData = useDeviceData;
2165
2504
  exports.useEventBus = useEventBus;
2166
2505
  exports.useIndexedDB = useIndexedDB;
2167
2506
  exports.useLLMMetadata = useLLMMetadata;
@@ -0,0 +1,106 @@
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
+ /** Detected browser name and version */
30
+ export interface DeviceBrowserInfo {
31
+ name: string;
32
+ version: string;
33
+ }
34
+ /** Detected operating system name and version */
35
+ export interface DeviceOsInfo {
36
+ name: string;
37
+ version: string;
38
+ }
39
+ /** Snapshot of device / browser data from native Navigator and related APIs */
40
+ export interface DeviceData {
41
+ userAgent: string;
42
+ language: string;
43
+ languages: readonly string[];
44
+ platform: string;
45
+ /** Detected browser (from Client Hints or user-agent parsing) */
46
+ browser: DeviceBrowserInfo;
47
+ /** Detected OS (from Client Hints or user-agent parsing) */
48
+ os: DeviceOsInfo;
49
+ cookieEnabled: boolean;
50
+ online: boolean;
51
+ hardwareConcurrency?: number;
52
+ /** Approximate device RAM in GB (Chrome / some browsers only) */
53
+ deviceMemory?: number;
54
+ maxTouchPoints: number;
55
+ vendor: string;
56
+ touch: boolean;
57
+ screen: DeviceScreenInfo;
58
+ viewport: DeviceViewportInfo;
59
+ userAgentData?: UserAgentDataInfo;
60
+ reducedMotion: boolean;
61
+ colorScheme: "light" | "dark" | "no-preference";
62
+ battery?: DeviceBatteryInfo;
63
+ }
64
+ export interface UseDeviceDataOptions {
65
+ /** Fetch battery info when the Battery Status API exists (default: true) */
66
+ includeBattery?: boolean;
67
+ /** Battery refresh interval in ms (default: 60000) */
68
+ batteryPollIntervalMs?: number;
69
+ /**
70
+ * Request high-entropy Client Hints (`platformVersion`, `fullVersionList`)
71
+ * when `navigator.userAgentData` supports it (default: true)
72
+ */
73
+ includeHighEntropy?: boolean;
74
+ }
75
+ /** Parse browser and OS from a user-agent string (sync fallback). */
76
+ export declare function parseUserAgent(userAgent: string): {
77
+ browser: DeviceBrowserInfo;
78
+ os: DeviceOsInfo;
79
+ };
80
+ /** Reads synchronous device / browser data from Navigator, Screen, and matchMedia. */
81
+ export declare function getDeviceData(): DeviceData;
82
+ /**
83
+ * Extracts device and browser data from native Navigator, Screen, window, and
84
+ * matchMedia APIs. Updates on resize, orientation, online/offline, and
85
+ * prefers-color-scheme / prefers-reduced-motion changes. Optionally polls the
86
+ * Battery Status API when available.
87
+ *
88
+ * @param options - `includeBattery` (default true), `batteryPollIntervalMs` (default 60000)
89
+ * @returns Current {@link DeviceData} snapshot
90
+ *
91
+ * @example
92
+ * ```tsx
93
+ * function DevicePanel() {
94
+ * const device = useDeviceData();
95
+ * return (
96
+ * <dl>
97
+ * <dt>Browser</dt><dd>{device.browser.name} {device.browser.version}</dd>
98
+ * <dt>OS</dt><dd>{device.os.name} {device.os.version}</dd>
99
+ * <dt>Language</dt><dd>{device.language}</dd>
100
+ * <dt>Viewport</dt><dd>{device.viewport.width}×{device.viewport.height}</dd>
101
+ * </dl>
102
+ * );
103
+ * }
104
+ * ```
105
+ */
106
+ export declare function useDeviceData(options?: UseDeviceDataOptions): DeviceData;
package/docs/README.md CHANGED
@@ -41,6 +41,7 @@ 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 browser/OS name & version (Client Hints + UA parse), language, CPUs, memory, viewport, screen, touch, theme prefs, UA brands, optional battery. |
44
45
  | **usePrefetch** | Hover or click to prefetch a URL (document or fetch); see prefetched status. |
45
46
  | **usePoll** | Poll until done (3 ticks); see poll count and result. Stop button to cancel. |
46
47
  | **useClipboard** | Copy and paste; see “Copied!” and pasted text. |
package/docs/main.js CHANGED
@@ -23,6 +23,7 @@ const {
23
23
  useRBAC,
24
24
  usePrefetch,
25
25
  usePoll,
26
+ useDeviceData,
26
27
  } = await import(
27
28
  isLocal ? '../dist/index.module.js' : 'https://unpkg.com/preact-missing-hooks/dist/index.module.js'
28
29
  );
@@ -86,6 +87,51 @@ function DemoNetworkState() {
86
87
  );
87
88
  }
88
89
 
90
+ function DemoDeviceData() {
91
+ const device = useDeviceData({ includeBattery: true, includeHighEntropy: true });
92
+ const formatBrowser = () =>
93
+ device.browser.name + (device.browser.version ? ' ' + device.browser.version : '');
94
+ const formatOs = () =>
95
+ device.os.name + (device.os.version ? ' ' + device.os.version : '');
96
+ const rows = [
97
+ ['Browser', formatBrowser()],
98
+ ['OS', formatOs()],
99
+ ['Language', device.language],
100
+ ['Platform (navigator)', device.platform || '—'],
101
+ ['CPUs', device.hardwareConcurrency != null ? String(device.hardwareConcurrency) : '—'],
102
+ ['Memory (GB)', device.deviceMemory != null ? String(device.deviceMemory) : '—'],
103
+ ['Viewport', device.viewport.width + '×' + device.viewport.height],
104
+ ['Screen', device.screen.width + '×' + device.screen.height + ' @' + device.screen.pixelRatio + 'x'],
105
+ ['Touch', device.touch ? 'yes' : 'no'],
106
+ ['Color scheme', device.colorScheme],
107
+ ['Reduced motion', device.reducedMotion ? 'yes' : 'no'],
108
+ ['Online', device.online ? 'yes' : 'no'],
109
+ ];
110
+ if (device.userAgentData?.mobile) {
111
+ rows.push(['Mobile (Client Hints)', 'yes']);
112
+ }
113
+ if (device.userAgentData?.brands?.length) {
114
+ rows.push([
115
+ 'UA brands',
116
+ device.userAgentData.brands.map((b) => b.brand + '/' + b.version).join(', '),
117
+ ]);
118
+ }
119
+ if (device.battery) {
120
+ rows.push([
121
+ 'Battery',
122
+ Math.round(device.battery.level * 100) + '%' + (device.battery.charging ? ' (charging)' : ''),
123
+ ]);
124
+ }
125
+ return h('div', { class: 'status', style: { fontSize: '0.85rem' } },
126
+ rows.map(([label, value]) =>
127
+ h('div', { key: label, style: { marginBottom: '0.25rem' } },
128
+ h('strong', {}, label + ': '),
129
+ value
130
+ )
131
+ )
132
+ );
133
+ }
134
+
89
135
  function DemoPrefetch() {
90
136
  const { prefetch, isPrefetched } = usePrefetch();
91
137
  const [lastUrl, setLastUrl] = useState('');
@@ -562,6 +608,13 @@ const HOOKS = [
562
608
  code: `const state = useNetworkState();\n// state.online, state.effectiveType, ...`,
563
609
  Live: DemoNetworkState,
564
610
  },
611
+ {
612
+ name: 'useDeviceData',
613
+ flow: 'Component → useDeviceData() → Client Hints + UA parse → browser/os → Screen + matchMedia (+ battery)',
614
+ summary: 'Device/browser snapshot: browser name & version, OS name & version (Client Hints + UA fallback), language, CPUs, memory, viewport, screen, touch, theme prefs, optional battery.',
615
+ code: `const device = useDeviceData({ includeHighEntropy: true });\n// device.browser.name, device.browser.version\n// device.os.name, device.os.version\n// device.viewport, device.colorScheme, device.battery`,
616
+ Live: DemoDeviceData,
617
+ },
565
618
  {
566
619
  name: 'usePrefetch',
567
620
  flow: 'Component → usePrefetch() → prefetch(url, options?) → link rel=prefetch or fetch()',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "preact-missing-hooks",
3
- "version": "4.7.0",
3
+ "version": "4.9.0",
4
4
  "description": "A lightweight, extendable collection of missing React-like hooks for Preact — plus fresh, powerful new ones designed specifically for modern Preact apps.",
5
5
  "author": "Prakhar Dubey",
6
6
  "license": "MIT",
@@ -102,6 +102,11 @@
102
102
  "import": "./dist/usePoll.module.js",
103
103
  "require": "./dist/usePoll.js"
104
104
  },
105
+ "./useDeviceData": {
106
+ "types": "./dist/useDeviceData.d.ts",
107
+ "import": "./dist/useDeviceData.module.js",
108
+ "require": "./dist/useDeviceData.js"
109
+ },
105
110
  "./react": {
106
111
  "types": "./dist/index.d.ts",
107
112
  "import": "./dist/react.module.js",
@@ -109,7 +114,7 @@
109
114
  }
110
115
  },
111
116
  "scripts": {
112
- "build": "microbundle --alias react=preact/compat && npm run build:react && node scripts/generate-entry.cjs",
117
+ "build": "node scripts/ensure-microbundle-patch.cjs && microbundle --alias react=preact/compat && npm run build:react && node scripts/generate-entry.cjs",
113
118
  "llm:generate": "hayagriva-llm generate --mode ai --include-src --verbose",
114
119
  "build:react": "rollup -c scripts/rollup.react.config.cjs",
115
120
  "dev": "microbundle watch",
@@ -122,7 +127,7 @@
122
127
  "lint": "eslint src",
123
128
  "format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
124
129
  "format:check": "prettier --check \"src/**/*.{ts,tsx,json,md}\"",
125
- "prepare": "husky",
130
+ "prepare": "husky && node scripts/ensure-microbundle-patch.cjs",
126
131
  "clean:cache": "node -e \"try{require('fs').rmSync('node_modules/.cache',{recursive:true,force:true});console.log('Cache cleared');}catch(e){}\"",
127
132
  "size": "npm run clean:cache && npm run build && size-limit"
128
133
  },
@@ -156,6 +161,7 @@
156
161
  "useClipboard",
157
162
  "usePrefetch",
158
163
  "usePoll",
164
+ "useDeviceData",
159
165
  "useRageClick",
160
166
  "rage click",
161
167
  "sentry",
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Patches microbundle to pass clean: true to rollup-plugin-typescript2.
3
+ * In rpt2, "clean" disables the RollingCache (noCache), avoiding EPERM rename
4
+ * failures on Windows during UMD/CJS/ESM builds.
5
+ */
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const cliPath = path.join(
10
+ __dirname,
11
+ "..",
12
+ "node_modules",
13
+ "microbundle",
14
+ "dist",
15
+ "cli.js",
16
+ );
17
+
18
+ if (!fs.existsSync(cliPath)) {
19
+ console.warn(
20
+ "ensure-microbundle-patch: microbundle not installed, skipping patch",
21
+ );
22
+ process.exit(0);
23
+ }
24
+
25
+ const src = fs.readFileSync(cliPath, "utf8");
26
+ const marker = "clean: true,";
27
+
28
+ if (src.includes(marker)) {
29
+ process.exit(0);
30
+ }
31
+
32
+ const needle =
33
+ "(useTypescript || emitDeclaration) && typescript__default['default']({";
34
+ const replacement = `${needle} clean: true,`;
35
+
36
+ if (!src.includes(needle)) {
37
+ console.error(
38
+ "ensure-microbundle-patch: microbundle cli.js format changed; update the patch script",
39
+ );
40
+ process.exit(1);
41
+ }
42
+
43
+ fs.writeFileSync(cliPath, src.replace(needle, replacement), "utf8");
44
+ console.log(
45
+ "Patched microbundle (rollup-plugin-typescript2 clean:true for Windows EPERM)",
46
+ );
package/src/index.ts CHANGED
@@ -16,3 +16,4 @@ export * from "./useRefPrint";
16
16
  export * from "./useRBAC";
17
17
  export * from "./usePrefetch";
18
18
  export * from "./usePoll";
19
+ export * from "./useDeviceData";