preact-missing-hooks 4.7.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/dist/react.js CHANGED
@@ -2161,7 +2161,199 @@ function usePoll(pollFn, options = {}) {
2161
2161
  return { data, done, error, pollCount, start, stop };
2162
2162
  }
2163
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;
2164
2355
  exports.useClipboard = useClipboard;
2356
+ exports.useDeviceData = useDeviceData;
2165
2357
  exports.useEventBus = useEventBus;
2166
2358
  exports.useIndexedDB = useIndexedDB;
2167
2359
  exports.useLLMMetadata = useLLMMetadata;
@@ -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;
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 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. |
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,33 @@ function DemoNetworkState() {
86
87
  );
87
88
  }
88
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
+
89
117
  function DemoPrefetch() {
90
118
  const { prefetch, isPrefetched } = usePrefetch();
91
119
  const [lastUrl, setLastUrl] = useState('');
@@ -562,6 +590,13 @@ const HOOKS = [
562
590
  code: `const state = useNetworkState();\n// state.online, state.effectiveType, ...`,
563
591
  Live: DemoNetworkState,
564
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
+ },
565
600
  {
566
601
  name: 'usePrefetch',
567
602
  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.8.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";