@flighthq/device 0.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/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # @flighthq/device
2
+
3
+ Device identity, hardware, and safe-area insets over a swappable web/native backend.
4
+
5
+ `device` is a host-identity leaf in the platform-integration suite. Every public function is a free function that delegates to the active `DeviceBackend`. A web/DOM backend is lazily available, so every function works on the web; a native host (Electron, Tauri, Capacitor, a C/C++ shell) replaces it via `setDeviceBackend`. Snapshot reads fill an `out` value and return it. Unknown or unavailable fields resolve to sentinels (`''` / `-1` / `false`), never throwing.
6
+
7
+ Battery state is **not** here — it is a live, event-bearing concern owned by `@flighthq/power`. `DeviceInfo` is a static identity snapshot. Live multi-display enumeration and work-area geometry belong to `@flighthq/screen`; `DeviceDisplayMetrics` describes only the built-in display.
8
+
9
+ ## Functions
10
+
11
+ | Function | Purpose |
12
+ | --- | --- |
13
+ | `createDeviceCapabilities()` | Allocate a zeroed `DeviceCapabilities` (all `false`) to pass as `out`. |
14
+ | `createDeviceDisplayMetrics()` | Allocate a zeroed `DeviceDisplayMetrics` (all `-1`) to pass as `out`. |
15
+ | `createDeviceInfo()` | Allocate a zeroed `DeviceInfo` (strings `''`, booleans `false`, arrays `[]`, numbers `-1`) to pass as `out`. |
16
+ | `createSafeAreaInsets()` | Allocate a zeroed `SafeAreaInsets` (all edges `0`) to pass as `out`. |
17
+ | `createWebDeviceBackend()` | Build the default web backend. |
18
+ | `enableWebSafeAreaInsets()` | Mount a CSS `env(safe-area-inset-*)` probe; returns a dispose function. Opt-in (touches the DOM). |
19
+ | `getDeviceBackend()` | Return the active backend, lazily creating the web default. There is always a backend. |
20
+ | `getDeviceCapabilities(out)` | Fill `out` with input/hardware capability flags. |
21
+ | `getDeviceDisplayMetrics(out)` | Fill `out` with the built-in display metrics. |
22
+ | `getDeviceId()` | Return a stable install identifier (resettable, not a hardware serial). |
23
+ | `getDeviceInfo(out)` | Fill `out` with the device's identity snapshot. |
24
+ | `getSafeAreaInsets(out)` | Fill `out` with safe-area insets, in CSS pixels. |
25
+ | `refreshDeviceInfo()` | Invalidate a backend snapshot cache so the next read is fresh. No-op on the stateless web default. |
26
+ | `setDeviceBackend(backend)` | Install a native host backend; pass `null` to fall back to the web default. |
27
+
28
+ ## `DeviceInfo` fields
29
+
30
+ | Field | Type | Unit | Sentinel | Web backend |
31
+ | --- | --- | --- | --- | --- |
32
+ | `arch` | `string` | CPU architecture token (e.g. `x86_64`, `arm64`) | `''` | Parsed from the UA / `userAgentData.platform`. Best-effort. |
33
+ | `availableMemory` | `number` | bytes of currently-available RAM | `-1` | Not exposed by browsers — always `-1`. |
34
+ | `boardName` | `string` | hardware board / mainboard name | `''` | Not exposed — always `''`. |
35
+ | `colorGamut` | `string` | display color gamut token | `''` | Not exposed — always `''`. |
36
+ | `cpuCores` | `number` | logical CPU core count | `-1` | `navigator.hardwareConcurrency` when present. |
37
+ | `fontScale` | `number` | OS font-scale multiplier (`1.0` = default) | `-1` | Not exposed — always `-1`. |
38
+ | `formFactor` | `DeviceFormFactor` | physical device class (open string-kind: `Desktop`, `Phone`, `Tablet`, `TV`, `Watch`, `Car`, `Unknown`, or vendor-prefixed) | `'Unknown'` | Parsed from the UA + `maxTouchPoints`. |
39
+ | `gpuRenderer` | `string` | GPU renderer string | `''` | `WEBGL_debug_renderer_info`; may be masked/randomized by browser privacy budget. |
40
+ | `gpuVendor` | `string` | GPU vendor string | `''` | `WEBGL_debug_renderer_info`; same caveat. |
41
+ | `isHdr` | `boolean` | display is HDR-capable | `false` | Not exposed — always `false`. |
42
+ | `isJailbroken` | `boolean` | iOS jailbreak detected | `false` | No web detection — always `false`. |
43
+ | `isLowEndDevice` | `boolean` | heuristically a low-end device | `false` | Heuristic: `≤ 1 GiB` RAM or `≤ 2` cores. Unknown inputs → `false`. |
44
+ | `isRooted` | `boolean` | Android root detected | `false` | No web detection — always `false`. |
45
+ | `isVirtual` | `boolean` | running on an emulator / VM | `false` | No web detection — always `false`. |
46
+ | `manufacturer` | `string` | hardware manufacturer | `''` | Not exposed — always `''`. |
47
+ | `marketingName` | `string` | consumer marketing name | `''` | Not exposed — always `''`. |
48
+ | `model` | `string` | hardware model identifier | `''` | Not exposed — always `''`. |
49
+ | `osBuild` | `string` | OS build number | `''` | Not exposed — always `''`. |
50
+ | `osName` | `string` | OS name | `''` | Parsed from the UA. |
51
+ | `osVersion` | `string` | OS version string | `''` | Parsed from the UA. |
52
+ | `platformString` | `string` | raw platform identifier | `''` | The full UA string. |
53
+ | `productName` | `string` | product / device codename | `''` | Not exposed — always `''`. |
54
+ | `supportedAbis` | `readonly string[]` | supported native ABIs | `[]` | Not exposed — always `[]`. |
55
+ | `totalMemory` | `number` | bytes of total RAM | `-1` | `navigator.deviceMemory` (GiB) converted to bytes; coarse, privacy-clamped. |
56
+ | `webViewVersion` | `string` | host WebView version | `''` | Not applicable (we are the browser) — always `''`. |
57
+
58
+ ## `DeviceCapabilities` fields
59
+
60
+ | Field | Type | Sentinel | Web backend |
61
+ | --- | --- | --- | --- |
62
+ | `hasKeyboard` | `boolean` | `false` | Heuristic: desktop UA likely has a physical keyboard. Cannot distinguish hybrid (Surface, iPad + keyboard). |
63
+ | `hasMouse` | `boolean` | `false` | Heuristic: `maxTouchPoints === 0` is a strong pointer-device signal. Cannot confirm a physical mouse. |
64
+ | `hasStylus` | `boolean` | `false` | No reliable browser signal — always `false`. |
65
+
66
+ Richer pointer/keyboard event handling lives in `@flighthq/input` and `@flighthq/interaction`; `DeviceCapabilities` only surfaces capabilities with no dedicated package owner.
67
+
68
+ ## `DeviceDisplayMetrics` fields
69
+
70
+ | Field | Type | Unit | Sentinel | Web backend |
71
+ | --- | --- | --- | --- | --- |
72
+ | `colorDepth` | `number` | bits per pixel | `-1` | `screen.colorDepth`. |
73
+ | `densityDpi` | `number` | dots per inch | `-1` | Not exposed by browsers — always `-1`. |
74
+ | `logicalHeight` | `number` | CSS pixels | `-1` | `screen.height`. |
75
+ | `logicalWidth` | `number` | CSS pixels | `-1` | `screen.width`. |
76
+ | `physicalHeight` | `number` | device pixels | `-1` | `screen.height × devicePixelRatio` when the ratio is known. |
77
+ | `physicalWidth` | `number` | device pixels | `-1` | `screen.width × devicePixelRatio` when the ratio is known. |
78
+ | `pixelRatio` | `number` | device pixels per CSS pixel | `-1` | `window.devicePixelRatio`. |
79
+
80
+ ## `SafeAreaInsets` fields
81
+
82
+ | Field | Type | Unit | Sentinel | Web backend |
83
+ | --- | --- | --- | --- | --- |
84
+ | `top` | `number` | CSS pixels | `0` | `0` by default; real `env(safe-area-inset-top)` after `enableWebSafeAreaInsets()`. |
85
+ | `right` | `number` | CSS pixels | `0` | `0` by default; real `env(safe-area-inset-right)` after `enableWebSafeAreaInsets()`. |
86
+ | `bottom` | `number` | CSS pixels | `0` | `0` by default; real `env(safe-area-inset-bottom)` after `enableWebSafeAreaInsets()`. |
87
+ | `left` | `number` | CSS pixels | `0` | `0` by default; real `env(safe-area-inset-left)` after `enableWebSafeAreaInsets()`. |
88
+
89
+ Edge insets keep content clear of notches, rounded corners, and system bars. The web backend returns zeros until `enableWebSafeAreaInsets()` mounts a live CSS-var probe (one hidden element, no polling).
90
+
91
+ ## `getDeviceId`
92
+
93
+ The web default generates a `crypto.randomUUID()` and persists it to `localStorage` as a stable install id. It returns `''` when no stable id can be formed (SSR, blocked storage, privacy mode). This is an **install** id — it resets when storage is cleared — not a hardware serial.
94
+
95
+ ## Usage
96
+
97
+ ```ts
98
+ import { createDeviceInfo, getDeviceInfo, getDeviceId } from '@flighthq/device';
99
+
100
+ const info = getDeviceInfo(createDeviceInfo());
101
+ console.log(info.formFactor, info.osName, info.cpuCores);
102
+
103
+ const installId = getDeviceId();
104
+ ```
@@ -0,0 +1,16 @@
1
+ import type { DeviceBackend, DeviceCapabilities, DeviceDisplayMetrics, DeviceInfo, SafeAreaInsets } from '@flighthq/types';
2
+ export declare function createDeviceCapabilities(): DeviceCapabilities;
3
+ export declare function createDeviceDisplayMetrics(): DeviceDisplayMetrics;
4
+ export declare function createDeviceInfo(): DeviceInfo;
5
+ export declare function createSafeAreaInsets(): SafeAreaInsets;
6
+ export declare function createWebDeviceBackend(): DeviceBackend;
7
+ export declare function enableWebSafeAreaInsets(): () => void;
8
+ export declare function getDeviceBackend(): DeviceBackend;
9
+ export declare function getDeviceCapabilities(out: DeviceCapabilities): DeviceCapabilities;
10
+ export declare function getDeviceDisplayMetrics(out: DeviceDisplayMetrics): DeviceDisplayMetrics;
11
+ export declare function getDeviceId(): string;
12
+ export declare function getDeviceInfo(out: DeviceInfo): DeviceInfo;
13
+ export declare function getSafeAreaInsets(out: SafeAreaInsets): SafeAreaInsets;
14
+ export declare function refreshDeviceInfo(): void;
15
+ export declare function setDeviceBackend(backend: DeviceBackend | null): void;
16
+ //# sourceMappingURL=device.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,UAAU,EACV,cAAc,EACf,MAAM,iBAAiB,CAAC;AAWzB,wBAAgB,wBAAwB,IAAI,kBAAkB,CAM7D;AAID,wBAAgB,0BAA0B,IAAI,oBAAoB,CAUjE;AAID,wBAAgB,gBAAgB,IAAI,UAAU,CA4B7C;AAGD,wBAAgB,oBAAoB,IAAI,cAAc,CAErD;AAID,wBAAgB,sBAAsB,IAAI,aAAa,CA4GtD;AAOD,wBAAgB,uBAAuB,IAAI,MAAM,IAAI,CA+BpD;AAGD,wBAAgB,gBAAgB,IAAI,aAAa,CAGhD;AAID,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,kBAAkB,GAAG,kBAAkB,CAEjF;AAID,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,oBAAoB,GAAG,oBAAoB,CAEvF;AAMD,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAGD,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAEzD;AAID,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,cAAc,CAErE;AAOD,wBAAgB,iBAAiB,IAAI,IAAI,CAMxC;AAGD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,GAAG,IAAI,CAEpE"}
package/dist/device.js ADDED
@@ -0,0 +1,296 @@
1
+ import { DeviceFormFactorUnknown } from '@flighthq/types';
2
+ import { parseUserAgentArch, parseUserAgentFormFactor, parseUserAgentOsName, parseUserAgentOsVersion, } from '@flighthq/useragent';
3
+ // Allocates a zeroed DeviceCapabilities; use as the `out` for getDeviceCapabilities.
4
+ // All boolean fields default to false (unknown).
5
+ export function createDeviceCapabilities() {
6
+ return {
7
+ hasKeyboard: false,
8
+ hasMouse: false,
9
+ hasStylus: false,
10
+ };
11
+ }
12
+ // Allocates a zeroed DeviceDisplayMetrics; use as the `out` for getDeviceDisplayMetrics.
13
+ // All numeric fields default to -1 when unknown.
14
+ export function createDeviceDisplayMetrics() {
15
+ return {
16
+ colorDepth: -1,
17
+ densityDpi: -1,
18
+ logicalHeight: -1,
19
+ logicalWidth: -1,
20
+ physicalHeight: -1,
21
+ physicalWidth: -1,
22
+ pixelRatio: -1,
23
+ };
24
+ }
25
+ // Allocates a zeroed DeviceInfo; use as the `out` for getDeviceInfo or when building a backend.
26
+ // Strings default to '', booleans to false, arrays to [], and unknown-numeric fields to -1.
27
+ export function createDeviceInfo() {
28
+ return {
29
+ arch: '',
30
+ availableMemory: -1,
31
+ boardName: '',
32
+ colorGamut: '',
33
+ cpuCores: -1,
34
+ fontScale: -1,
35
+ formFactor: DeviceFormFactorUnknown,
36
+ gpuRenderer: '',
37
+ gpuVendor: '',
38
+ isHdr: false,
39
+ isJailbroken: false,
40
+ isLowEndDevice: false,
41
+ isRooted: false,
42
+ isVirtual: false,
43
+ manufacturer: '',
44
+ marketingName: '',
45
+ model: '',
46
+ osBuild: '',
47
+ osName: '',
48
+ osVersion: '',
49
+ platformString: '',
50
+ productName: '',
51
+ supportedAbis: [],
52
+ totalMemory: -1,
53
+ webViewVersion: '',
54
+ };
55
+ }
56
+ // Allocates a zeroed SafeAreaInsets (all edges 0); use as the `out` for getSafeAreaInsets.
57
+ export function createSafeAreaInsets() {
58
+ return { bottom: 0, left: 0, right: 0, top: 0 };
59
+ }
60
+ // Builds the default web backend. Reads what the browser exposes and returns sentinels for
61
+ // everything a web page cannot know (model/manufacturer, OS version, safe area, DPI).
62
+ export function createWebDeviceBackend() {
63
+ return {
64
+ getCapabilities(out) {
65
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
66
+ // hasMouse: weak heuristic — no touch points is a strong desktop / pointer-device signal.
67
+ // This is best-effort; the browser cannot confirm whether a physical mouse is attached.
68
+ const maxTouch = nav !== null && 'maxTouchPoints' in nav ? nav.maxTouchPoints : -1;
69
+ out.hasMouse = maxTouch === 0;
70
+ // hasKeyboard: desktop UAs very likely have a physical keyboard; mobile UAs likely do not.
71
+ // Cannot distinguish virtual + physical keyboard on hybrid devices (Surface, iPad with keyboard).
72
+ const ua = nav?.userAgent ?? '';
73
+ out.hasKeyboard = detectDesktopUa(ua);
74
+ // hasStylus: no reliable UA or API signal in browsers — always false.
75
+ out.hasStylus = false;
76
+ return out;
77
+ },
78
+ getDisplayMetrics(out) {
79
+ const win = typeof window !== 'undefined' ? window : null;
80
+ const scr = typeof screen !== 'undefined' ? screen : null;
81
+ out.colorDepth = scr !== null ? scr.colorDepth : -1;
82
+ // DPI is not exposed by browsers — always sentinel.
83
+ out.densityDpi = -1;
84
+ out.logicalHeight = scr !== null ? scr.height : -1;
85
+ out.logicalWidth = scr !== null ? scr.width : -1;
86
+ const pixelRatio = win !== null ? win.devicePixelRatio : -1;
87
+ out.pixelRatio = pixelRatio;
88
+ out.physicalWidth = scr !== null && pixelRatio > 0 ? Math.round(scr.width * pixelRatio) : -1;
89
+ out.physicalHeight = scr !== null && pixelRatio > 0 ? Math.round(scr.height * pixelRatio) : -1;
90
+ return out;
91
+ },
92
+ getId() {
93
+ // Web: crypto.randomUUID() persisted to localStorage as a stable install id.
94
+ // Returns '' when storage is unavailable (SSR, private browsing with blocked storage).
95
+ // This is an install id — it resets if localStorage is cleared. Not a hardware serial.
96
+ // For a durable cross-storage id, use @flighthq/storage as the backend's persistence layer.
97
+ try {
98
+ const key = '__flighthq_device_id';
99
+ const existing = typeof localStorage !== 'undefined' ? localStorage.getItem(key) : null;
100
+ if (existing !== null)
101
+ return existing;
102
+ if (typeof crypto === 'undefined' || typeof crypto.randomUUID !== 'function')
103
+ return '';
104
+ const id = crypto.randomUUID();
105
+ if (typeof localStorage !== 'undefined')
106
+ localStorage.setItem(key, id);
107
+ return id;
108
+ }
109
+ catch {
110
+ return '';
111
+ }
112
+ },
113
+ getInfo(out) {
114
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
115
+ const ua = nav?.userAgent ?? '';
116
+ const uadPlatform = nav?.userAgentData
117
+ ?.platform;
118
+ out.arch = parseUserAgentArch(ua, uadPlatform);
119
+ // availableMemory is not exposed by browsers — always -1.
120
+ out.availableMemory = -1;
121
+ // boardName, marketingName, productName, supportedAbis — not exposed by browsers.
122
+ out.boardName = '';
123
+ // colorGamut, fontScale, isHdr — not exposed by browsers.
124
+ out.colorGamut = '';
125
+ const cores = nav !== null && 'hardwareConcurrency' in nav ? (nav.hardwareConcurrency ?? -1) : -1;
126
+ out.cpuCores = cores;
127
+ out.fontScale = -1;
128
+ out.formFactor = parseUserAgentFormFactor(ua, nav !== null && 'maxTouchPoints' in nav ? nav.maxTouchPoints : -1);
129
+ const gpuInfo = readWebGpuInfo();
130
+ out.gpuRenderer = gpuInfo.renderer;
131
+ out.gpuVendor = gpuInfo.vendor;
132
+ out.isHdr = false;
133
+ // isJailbroken and isRooted are always false on web — no detection available.
134
+ out.isJailbroken = false;
135
+ const devMem = nav !== null && 'deviceMemory' in nav ? (nav.deviceMemory ?? -1) : -1;
136
+ out.isLowEndDevice = detectLowEndDevice(devMem, cores);
137
+ out.isRooted = false;
138
+ out.isVirtual = false;
139
+ out.manufacturer = '';
140
+ out.marketingName = '';
141
+ out.model = '';
142
+ out.osBuild = '';
143
+ out.osName = parseUserAgentOsName(ua);
144
+ out.osVersion = parseUserAgentOsVersion(ua);
145
+ out.platformString = ua;
146
+ out.productName = '';
147
+ out.supportedAbis = [];
148
+ // totalMemory: navigator.deviceMemory is in GiB; convert to bytes. -1 when absent.
149
+ out.totalMemory = devMem >= 0 ? devMem * 1024 * 1024 * 1024 : -1;
150
+ // webViewVersion is not exposed by browsers (we are the browser).
151
+ out.webViewVersion = '';
152
+ return out;
153
+ },
154
+ getSafeAreaInsets(out) {
155
+ // Reading CSS env(safe-area-inset-*) requires a probe element in the DOM. The web backend
156
+ // returns zero insets by default. Call enableWebSafeAreaInsets() to mount a live CSS-var probe
157
+ // that updates this when the device reports real insets (notched PWAs).
158
+ const insets = _safeAreaInsets;
159
+ if (insets !== null) {
160
+ out.bottom = insets.bottom;
161
+ out.left = insets.left;
162
+ out.right = insets.right;
163
+ out.top = insets.top;
164
+ }
165
+ else {
166
+ out.bottom = 0;
167
+ out.left = 0;
168
+ out.right = 0;
169
+ out.top = 0;
170
+ }
171
+ return out;
172
+ },
173
+ };
174
+ }
175
+ // Mounts a CSS env(safe-area-inset-*) probe element in the DOM and keeps the active web backend's
176
+ // safe-area values live. Call once after the page is ready; unmount by calling the returned dispose
177
+ // function. Has no effect when called outside of a browser context (SSR, workers). The probe is
178
+ // cheap: one hidden element, no polling — a ResizeObserver detects viewport changes.
179
+ // Returns a dispose function that removes the probe element and stops the observer.
180
+ export function enableWebSafeAreaInsets() {
181
+ if (typeof document === 'undefined')
182
+ return () => { };
183
+ const el = document.createElement('div');
184
+ el.style.cssText =
185
+ 'position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);' +
186
+ 'bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);' +
187
+ 'pointer-events:none;visibility:hidden;';
188
+ document.body.appendChild(el);
189
+ function readInsets() {
190
+ const style = getComputedStyle(el);
191
+ _safeAreaInsets = {
192
+ bottom: parseFloat(style.bottom) || 0,
193
+ left: parseFloat(style.left) || 0,
194
+ right: parseFloat(style.right) || 0,
195
+ top: parseFloat(style.top) || 0,
196
+ };
197
+ }
198
+ readInsets();
199
+ // ResizeObserver may be absent in older browsers or test environments; fall back to a no-op.
200
+ const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(readInsets) : null;
201
+ if (observer !== null)
202
+ observer.observe(document.documentElement);
203
+ return () => {
204
+ if (observer !== null)
205
+ observer.disconnect();
206
+ el.parentNode?.removeChild(el);
207
+ _safeAreaInsets = null;
208
+ };
209
+ }
210
+ // Returns the active device backend, or a lazily-created web default. There is always a backend.
211
+ export function getDeviceBackend() {
212
+ if (_backend === null)
213
+ _backend = createWebDeviceBackend();
214
+ return _backend;
215
+ }
216
+ // Fills `out` with the device's input/hardware capability flags and returns it.
217
+ // Only surfaces capabilities with no dedicated package owner; see DeviceCapabilities for cross-refs.
218
+ export function getDeviceCapabilities(out) {
219
+ return getDeviceBackend().getCapabilities(out);
220
+ }
221
+ // Fills `out` with the device's built-in display metrics and returns it. Reads the active backend.
222
+ // For live multi-display enumeration and work-area geometry, use @flighthq/screen.
223
+ export function getDeviceDisplayMetrics(out) {
224
+ return getDeviceBackend().getDisplayMetrics(out);
225
+ }
226
+ // Returns a stable install identifier for this device/app install. Backed by DeviceBackend.getId().
227
+ // Web default: crypto.randomUUID() persisted to localStorage. Returns '' when no stable id can be
228
+ // formed (SSR, blocked storage, privacy mode). This is an _install_ id — resettable by clearing
229
+ // storage — not a hardware serial.
230
+ export function getDeviceId() {
231
+ return getDeviceBackend().getId();
232
+ }
233
+ // Fills `out` with the running device's identity and returns it. Reads the active backend.
234
+ export function getDeviceInfo(out) {
235
+ return getDeviceBackend().getInfo(out);
236
+ }
237
+ // Fills `out` with the device's safe-area insets, in CSS pixels, and returns it.
238
+ // Web: returns zeros by default; call enableWebSafeAreaInsets() for real CSS env() values.
239
+ export function getSafeAreaInsets(out) {
240
+ return getDeviceBackend().getSafeAreaInsets(out);
241
+ }
242
+ // Invalidates any snapshot cached in the active backend and triggers a fresh read on the next call
243
+ // to getDeviceInfo / getSafeAreaInsets / getDeviceDisplayMetrics. Useful for orientation-affected
244
+ // safe-area insets and available-memory reads that may change at runtime.
245
+ // The web default backend is stateless (no cache), so this is a no-op there; native backends that
246
+ // cache a snapshot must listen for this signal and invalidate their cache.
247
+ export function refreshDeviceInfo() {
248
+ const backend = getDeviceBackend();
249
+ const maybeRefreshable = backend;
250
+ if (typeof maybeRefreshable.refresh === 'function') {
251
+ maybeRefreshable.refresh();
252
+ }
253
+ }
254
+ // Installs a native host device backend; pass null to fall back to the web default.
255
+ export function setDeviceBackend(backend) {
256
+ _backend = backend;
257
+ }
258
+ let _backend = null;
259
+ let _safeAreaInsets = null;
260
+ // ---------------------------------------------------------------------------
261
+ // Web-backend detection helpers (private)
262
+ // ---------------------------------------------------------------------------
263
+ function detectDesktopUa(ua) {
264
+ return /win(?:dows)?nt|macintosh|mac os x|linux(?!.*android)|cros|x11/i.test(ua);
265
+ }
266
+ function detectLowEndDevice(deviceMemoryGib, cores) {
267
+ // Low-end heuristic: <= 1 GiB RAM or <= 2 cores. Both sentinels (-1) = unknown = false.
268
+ if (deviceMemoryGib > 0 && deviceMemoryGib <= 1)
269
+ return true;
270
+ if (cores > 0 && cores <= 2)
271
+ return true;
272
+ return false;
273
+ }
274
+ function readWebGpuInfo() {
275
+ // Reads WEBGL_debug_renderer_info from a transient WebGL context. Returns '' when unavailable or
276
+ // blocked by browser privacy budget. This is best-effort — modern browsers may mask or randomize.
277
+ try {
278
+ if (typeof document === 'undefined')
279
+ return { renderer: '', vendor: '' };
280
+ const canvas = document.createElement('canvas');
281
+ const gl = canvas.getContext('webgl') ??
282
+ canvas.getContext('experimental-webgl');
283
+ if (gl === null)
284
+ return { renderer: '', vendor: '' };
285
+ const ext = gl.getExtension('WEBGL_debug_renderer_info');
286
+ if (ext === null)
287
+ return { renderer: '', vendor: '' };
288
+ const vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) ?? '';
289
+ const renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) ?? '';
290
+ return { renderer, vendor };
291
+ }
292
+ catch {
293
+ return { renderer: '', vendor: '' };
294
+ }
295
+ }
296
+ //# sourceMappingURL=device.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.js","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAE7B,qFAAqF;AACrF,iDAAiD;AACjD,MAAM,UAAU,wBAAwB;IACtC,OAAO;QACL,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE,KAAK;QACf,SAAS,EAAE,KAAK;KACjB,CAAC;AACJ,CAAC;AAED,yFAAyF;AACzF,iDAAiD;AACjD,MAAM,UAAU,0BAA0B;IACxC,OAAO;QACL,UAAU,EAAE,CAAC,CAAC;QACd,UAAU,EAAE,CAAC,CAAC;QACd,aAAa,EAAE,CAAC,CAAC;QACjB,YAAY,EAAE,CAAC,CAAC;QAChB,cAAc,EAAE,CAAC,CAAC;QAClB,aAAa,EAAE,CAAC,CAAC;QACjB,UAAU,EAAE,CAAC,CAAC;KACf,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,4FAA4F;AAC5F,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,IAAI,EAAE,EAAE;QACR,eAAe,EAAE,CAAC,CAAC;QACnB,SAAS,EAAE,EAAE;QACb,UAAU,EAAE,EAAE;QACd,QAAQ,EAAE,CAAC,CAAC;QACZ,SAAS,EAAE,CAAC,CAAC;QACb,UAAU,EAAE,uBAAuB;QACnC,WAAW,EAAE,EAAE;QACf,SAAS,EAAE,EAAE;QACb,KAAK,EAAE,KAAK;QACZ,YAAY,EAAE,KAAK;QACnB,cAAc,EAAE,KAAK;QACrB,QAAQ,EAAE,KAAK;QACf,SAAS,EAAE,KAAK;QAChB,YAAY,EAAE,EAAE;QAChB,aAAa,EAAE,EAAE;QACjB,KAAK,EAAE,EAAE;QACT,OAAO,EAAE,EAAE;QACX,MAAM,EAAE,EAAE;QACV,SAAS,EAAE,EAAE;QACb,cAAc,EAAE,EAAE;QAClB,WAAW,EAAE,EAAE;QACf,aAAa,EAAE,EAAE;QACjB,WAAW,EAAE,CAAC,CAAC;QACf,cAAc,EAAE,EAAE;KACnB,CAAC;AACJ,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,oBAAoB;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;AAClD,CAAC;AAED,2FAA2F;AAC3F,sFAAsF;AACtF,MAAM,UAAU,sBAAsB;IACpC,OAAO;QACL,eAAe,CAAC,GAAuB;YACrC,MAAM,GAAG,GAAG,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;YAChE,0FAA0F;YAC1F,wFAAwF;YACxF,MAAM,QAAQ,GAAG,GAAG,KAAK,IAAI,IAAI,gBAAgB,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACnF,GAAG,CAAC,QAAQ,GAAG,QAAQ,KAAK,CAAC,CAAC;YAC9B,2FAA2F;YAC3F,kGAAkG;YAClG,MAAM,EAAE,GAAG,GAAG,EAAE,SAAS,IAAI,EAAE,CAAC;YAChC,GAAG,CAAC,WAAW,GAAG,eAAe,CAAC,EAAE,CAAC,CAAC;YACtC,sEAAsE;YACtE,GAAG,CAAC,SAAS,GAAG,KAAK,CAAC;YACtB,OAAO,GAAG,CAAC;QACb,CAAC;QACD,iBAAiB,CAAC,GAAyB;YACzC,MAAM,GAAG,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;YAC1D,MAAM,GAAG,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;YAC1D,GAAG,CAAC,UAAU,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACpD,oDAAoD;YACpD,GAAG,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;YACpB,GAAG,CAAC,aAAa,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACnD,GAAG,CAAC,YAAY,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACjD,MAAM,UAAU,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5D,GAAG,CAAC,UAAU,GAAG,UAAU,CAAC;YAC5B,GAAG,CAAC,aAAa,GAAG,GAAG,KAAK,IAAI,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7F,GAAG,CAAC,cAAc,GAAG,GAAG,KAAK,IAAI,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC/F,OAAO,GAAG,CAAC;QACb,CAAC;QACD,KAAK;YACH,6EAA6E;YAC7E,uFAAuF;YACvF,uFAAuF;YACvF,4FAA4F;YAC5F,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,sBAAsB,CAAC;gBACnC,MAAM,QAAQ,GAAG,OAAO,YAAY,KAAK,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBACxF,IAAI,QAAQ,KAAK,IAAI;oBAAE,OAAO,QAAQ,CAAC;gBACvC,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU;oBAAE,OAAO,EAAE,CAAC;gBACxF,MAAM,EAAE,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;gBAC/B,IAAI,OAAO,YAAY,KAAK,WAAW;oBAAE,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBACvE,OAAO,EAAE,CAAC;YACZ,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QACD,OAAO,CAAC,GAAe;YACrB,MAAM,GAAG,GAAG,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;YAChE,MAAM,EAAE,GAAG,GAAG,EAAE,SAAS,IAAI,EAAE,CAAC;YAChC,MAAM,WAAW,GAAwB,GAAwD,EAAE,aAAa;gBAC9G,EAAE,QAAQ,CAAC;YACb,GAAG,CAAC,IAAI,GAAG,kBAAkB,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;YAC/C,0DAA0D;YAC1D,GAAG,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC;YACzB,kFAAkF;YAClF,GAAG,CAAC,SAAS,GAAG,EAAE,CAAC;YACnB,0DAA0D;YAC1D,GAAG,CAAC,UAAU,GAAG,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,GAAG,KAAK,IAAI,IAAI,qBAAqB,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,mBAAmB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAClG,GAAG,CAAC,QAAQ,GAAG,KAAK,CAAC;YACrB,GAAG,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;YACnB,GAAG,CAAC,UAAU,GAAG,wBAAwB,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,IAAI,gBAAgB,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACjH,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC;YACjC,GAAG,CAAC,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;YACnC,GAAG,CAAC,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;YAC/B,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC;YAClB,8EAA8E;YAC9E,GAAG,CAAC,YAAY,GAAG,KAAK,CAAC;YACzB,MAAM,MAAM,GACV,GAAG,KAAK,IAAI,IAAI,cAAc,IAAI,GAAG,CAAC,CAAC,CAAC,CAAE,GAAiC,CAAC,YAAY,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACvG,GAAG,CAAC,cAAc,GAAG,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YACvD,GAAG,CAAC,QAAQ,GAAG,KAAK,CAAC;YACrB,GAAG,CAAC,SAAS,GAAG,KAAK,CAAC;YACtB,GAAG,CAAC,YAAY,GAAG,EAAE,CAAC;YACtB,GAAG,CAAC,aAAa,GAAG,EAAE,CAAC;YACvB,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC;YACf,GAAG,CAAC,OAAO,GAAG,EAAE,CAAC;YACjB,GAAG,CAAC,MAAM,GAAG,oBAAoB,CAAC,EAAE,CAAC,CAAC;YACtC,GAAG,CAAC,SAAS,GAAG,uBAAuB,CAAC,EAAE,CAAC,CAAC;YAC5C,GAAG,CAAC,cAAc,GAAG,EAAE,CAAC;YACxB,GAAG,CAAC,WAAW,GAAG,EAAE,CAAC;YACrB,GAAG,CAAC,aAAa,GAAG,EAAE,CAAC;YACvB,mFAAmF;YACnF,GAAG,CAAC,WAAW,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACjE,kEAAkE;YAClE,GAAG,CAAC,cAAc,GAAG,EAAE,CAAC;YACxB,OAAO,GAAG,CAAC;QACb,CAAC;QACD,iBAAiB,CAAC,GAAmB;YACnC,0FAA0F;YAC1F,+FAA+F;YAC/F,wEAAwE;YACxE,MAAM,MAAM,GAAG,eAAe,CAAC;YAC/B,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACpB,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;gBAC3B,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;gBACvB,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;gBACzB,GAAG,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;YACvB,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;gBACf,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC;gBACb,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC;gBACd,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;YACd,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC;KACF,CAAC;AACJ,CAAC;AAED,kGAAkG;AAClG,oGAAoG;AACpG,gGAAgG;AAChG,qFAAqF;AACrF,oFAAoF;AACpF,MAAM,UAAU,uBAAuB;IACrC,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IAErD,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACzC,EAAE,CAAC,KAAK,CAAC,OAAO;QACd,uFAAuF;YACvF,4EAA4E;YAC5E,wCAAwC,CAAC;IAC3C,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAE9B,SAAS,UAAU;QACjB,MAAM,KAAK,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAC;QACnC,eAAe,GAAG;YAChB,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC;YACrC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;YACjC,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC;YACnC,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;SAChC,CAAC;IACJ,CAAC;IAED,UAAU,EAAE,CAAC;IAEb,6FAA6F;IAC7F,MAAM,QAAQ,GAAG,OAAO,cAAc,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/F,IAAI,QAAQ,KAAK,IAAI;QAAE,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IAElE,OAAO,GAAG,EAAE;QACV,IAAI,QAAQ,KAAK,IAAI;YAAE,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC7C,EAAE,CAAC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;QAC/B,eAAe,GAAG,IAAI,CAAC;IACzB,CAAC,CAAC;AACJ,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,gBAAgB;IAC9B,IAAI,QAAQ,KAAK,IAAI;QAAE,QAAQ,GAAG,sBAAsB,EAAE,CAAC;IAC3D,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,gFAAgF;AAChF,qGAAqG;AACrG,MAAM,UAAU,qBAAqB,CAAC,GAAuB;IAC3D,OAAO,gBAAgB,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;AACjD,CAAC;AAED,mGAAmG;AACnG,mFAAmF;AACnF,MAAM,UAAU,uBAAuB,CAAC,GAAyB;IAC/D,OAAO,gBAAgB,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;AACnD,CAAC;AAED,oGAAoG;AACpG,kGAAkG;AAClG,gGAAgG;AAChG,mCAAmC;AACnC,MAAM,UAAU,WAAW;IACzB,OAAO,gBAAgB,EAAE,CAAC,KAAK,EAAE,CAAC;AACpC,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,aAAa,CAAC,GAAe;IAC3C,OAAO,gBAAgB,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AACzC,CAAC;AAED,iFAAiF;AACjF,2FAA2F;AAC3F,MAAM,UAAU,iBAAiB,CAAC,GAAmB;IACnD,OAAO,gBAAgB,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;AACnD,CAAC;AAED,mGAAmG;AACnG,kGAAkG;AAClG,0EAA0E;AAC1E,kGAAkG;AAClG,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB;IAC/B,MAAM,OAAO,GAAG,gBAAgB,EAAE,CAAC;IACnC,MAAM,gBAAgB,GAAG,OAA8C,CAAC;IACxE,IAAI,OAAO,gBAAgB,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QACnD,gBAAgB,CAAC,OAAO,EAAE,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,gBAAgB,CAAC,OAA6B;IAC5D,QAAQ,GAAG,OAAO,CAAC;AACrB,CAAC;AAED,IAAI,QAAQ,GAAyB,IAAI,CAAC;AAC1C,IAAI,eAAe,GAA0B,IAAI,CAAC;AAElD,8EAA8E;AAC9E,0CAA0C;AAC1C,8EAA8E;AAE9E,SAAS,eAAe,CAAC,EAAU;IACjC,OAAO,gEAAgE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACnF,CAAC;AAED,SAAS,kBAAkB,CAAC,eAAuB,EAAE,KAAa;IAChE,wFAAwF;IACxF,IAAI,eAAe,GAAG,CAAC,IAAI,eAAe,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,cAAc;IACrB,iGAAiG;IACjG,kGAAkG;IAClG,IAAI,CAAC;QACH,IAAI,OAAO,QAAQ,KAAK,WAAW;YAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;QACzE,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,EAAE,GACL,MAAM,CAAC,UAAU,CAAC,OAAO,CAAkC;YAC3D,MAAM,CAAC,UAAU,CAAC,oBAAoB,CAAkC,CAAC;QAC5E,IAAI,EAAE,KAAK,IAAI;YAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;QACrD,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,2BAA2B,CAAC,CAAC;QACzD,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;QACtD,MAAM,MAAM,GAAI,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,qBAAqB,CAAY,IAAI,EAAE,CAAC;QAC5E,MAAM,QAAQ,GAAI,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,uBAAuB,CAAY,IAAI,EAAE,CAAC;QAChF,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACtC,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './device';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './device';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/device",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/types": "0.1.0",
31
+ "@flighthq/useragent": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "Device identity, hardware, and safe-area insets over a swappable web/native backend",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,384 @@
1
+ import type {
2
+ DeviceBackend,
3
+ DeviceCapabilities,
4
+ DeviceDisplayMetrics,
5
+ DeviceInfo,
6
+ SafeAreaInsets,
7
+ } from '@flighthq/types';
8
+ import { DeviceFormFactorDesktop, DeviceFormFactorPhone, DeviceFormFactorUnknown } from '@flighthq/types';
9
+
10
+ import {
11
+ createDeviceCapabilities,
12
+ createDeviceDisplayMetrics,
13
+ createDeviceInfo,
14
+ createSafeAreaInsets,
15
+ createWebDeviceBackend,
16
+ enableWebSafeAreaInsets,
17
+ getDeviceBackend,
18
+ getDeviceCapabilities,
19
+ getDeviceDisplayMetrics,
20
+ getDeviceId,
21
+ getDeviceInfo,
22
+ getSafeAreaInsets,
23
+ refreshDeviceInfo,
24
+ setDeviceBackend,
25
+ } from './device';
26
+
27
+ function fakeBackend(): DeviceBackend {
28
+ return {
29
+ getCapabilities(out: DeviceCapabilities): DeviceCapabilities {
30
+ out.hasKeyboard = true;
31
+ out.hasMouse = true;
32
+ out.hasStylus = false;
33
+ return out;
34
+ },
35
+ getDisplayMetrics(out: DeviceDisplayMetrics): DeviceDisplayMetrics {
36
+ out.colorDepth = 8;
37
+ out.densityDpi = 440;
38
+ out.logicalHeight = 800;
39
+ out.logicalWidth = 360;
40
+ out.physicalHeight = 1600;
41
+ out.physicalWidth = 720;
42
+ out.pixelRatio = 2;
43
+ return out;
44
+ },
45
+ getId(): string {
46
+ return 'test-device-id';
47
+ },
48
+ getInfo(out: DeviceInfo): DeviceInfo {
49
+ out.arch = 'arm64';
50
+ out.availableMemory = 3_000_000_000;
51
+ out.boardName = 'msm8998';
52
+ out.colorGamut = 'display-p3';
53
+ out.cpuCores = 8;
54
+ out.fontScale = 1.2;
55
+ out.formFactor = 'Phone';
56
+ out.gpuRenderer = 'Adreno 650';
57
+ out.gpuVendor = 'Qualcomm';
58
+ out.isHdr = true;
59
+ out.isJailbroken = false;
60
+ out.isLowEndDevice = false;
61
+ out.isRooted = false;
62
+ out.isVirtual = true;
63
+ out.manufacturer = 'Google';
64
+ out.marketingName = 'Pixel 8 Pro';
65
+ out.model = 'Pixel';
66
+ out.osBuild = 'TP1A.220624.014';
67
+ out.osName = 'Android';
68
+ out.osVersion = '14';
69
+ out.platformString = 'Linux armv8l';
70
+ out.productName = 'husky';
71
+ out.supportedAbis = ['arm64-v8a', 'armeabi-v7a'];
72
+ out.totalMemory = 8_000_000_000;
73
+ out.webViewVersion = '120.0.6099.230';
74
+ return out;
75
+ },
76
+ getSafeAreaInsets(out: SafeAreaInsets): SafeAreaInsets {
77
+ out.bottom = 16;
78
+ out.left = 0;
79
+ out.right = 0;
80
+ out.top = 24;
81
+ return out;
82
+ },
83
+ };
84
+ }
85
+
86
+ afterEach(() => setDeviceBackend(null));
87
+
88
+ describe('createDeviceCapabilities', () => {
89
+ it('allocates a zeroed snapshot with all false capability flags', () => {
90
+ const caps = createDeviceCapabilities();
91
+ expect(caps.hasKeyboard).toBe(false);
92
+ expect(caps.hasMouse).toBe(false);
93
+ expect(caps.hasStylus).toBe(false);
94
+ });
95
+ });
96
+
97
+ describe('createDeviceDisplayMetrics', () => {
98
+ it('allocates a zeroed snapshot with -1 numeric sentinels', () => {
99
+ const metrics = createDeviceDisplayMetrics();
100
+ expect(metrics.colorDepth).toBe(-1);
101
+ expect(metrics.densityDpi).toBe(-1);
102
+ expect(metrics.logicalHeight).toBe(-1);
103
+ expect(metrics.logicalWidth).toBe(-1);
104
+ expect(metrics.physicalHeight).toBe(-1);
105
+ expect(metrics.physicalWidth).toBe(-1);
106
+ expect(metrics.pixelRatio).toBe(-1);
107
+ });
108
+ });
109
+
110
+ describe('createDeviceInfo', () => {
111
+ it('allocates zeroed snapshot with string, boolean, and -1 numeric sentinels', () => {
112
+ const info = createDeviceInfo();
113
+ expect(info.arch).toBe('');
114
+ expect(info.availableMemory).toBe(-1);
115
+ expect(info.boardName).toBe('');
116
+ expect(info.colorGamut).toBe('');
117
+ expect(info.cpuCores).toBe(-1);
118
+ expect(info.fontScale).toBe(-1);
119
+ expect(info.formFactor).toBe(DeviceFormFactorUnknown);
120
+ expect(info.gpuRenderer).toBe('');
121
+ expect(info.gpuVendor).toBe('');
122
+ expect(info.isHdr).toBe(false);
123
+ expect(info.isJailbroken).toBe(false);
124
+ expect(info.isLowEndDevice).toBe(false);
125
+ expect(info.isRooted).toBe(false);
126
+ expect(info.isVirtual).toBe(false);
127
+ expect(info.manufacturer).toBe('');
128
+ expect(info.marketingName).toBe('');
129
+ expect(info.model).toBe('');
130
+ expect(info.osBuild).toBe('');
131
+ expect(info.osName).toBe('');
132
+ expect(info.osVersion).toBe('');
133
+ expect(info.platformString).toBe('');
134
+ expect(info.productName).toBe('');
135
+ expect(info.supportedAbis).toEqual([]);
136
+ expect(info.totalMemory).toBe(-1);
137
+ expect(info.webViewVersion).toBe('');
138
+ });
139
+ });
140
+
141
+ describe('createSafeAreaInsets', () => {
142
+ it('allocates zeroed edges', () => {
143
+ expect(createSafeAreaInsets()).toEqual({ bottom: 0, left: 0, right: 0, top: 0 });
144
+ });
145
+ });
146
+
147
+ describe('createWebDeviceBackend', () => {
148
+ it('fills the snapshot with sentinels without throwing (jsdom)', () => {
149
+ const backend = createWebDeviceBackend();
150
+ const info = backend.getInfo(createDeviceInfo());
151
+ expect(info.model).toBe('');
152
+ expect(info.manufacturer).toBe('');
153
+ expect(info.marketingName).toBe('');
154
+ expect(info.productName).toBe('');
155
+ expect(info.boardName).toBe('');
156
+ expect(info.webViewVersion).toBe('');
157
+ expect(info.colorGamut).toBe('');
158
+ expect(info.fontScale).toBe(-1);
159
+ expect(info.isHdr).toBe(false);
160
+ expect(typeof info.totalMemory).toBe('number');
161
+ expect(info.availableMemory).toBe(-1);
162
+ expect(typeof info.cpuCores).toBe('number');
163
+ expect(info.isJailbroken).toBe(false);
164
+ expect(info.isRooted).toBe(false);
165
+ expect(info.osBuild).toBe('');
166
+ expect(typeof info.formFactor).toBe('string');
167
+ expect(typeof info.arch).toBe('string');
168
+ expect(info.platformString).toBe(navigator.userAgent);
169
+ expect(Array.isArray(info.supportedAbis)).toBe(true);
170
+ expect(info.supportedAbis.length).toBe(0);
171
+ });
172
+
173
+ it('returns zero safe-area insets on plain web (no CSS probe)', () => {
174
+ const backend = createWebDeviceBackend();
175
+ expect(backend.getSafeAreaInsets(createSafeAreaInsets())).toEqual({ bottom: 0, left: 0, right: 0, top: 0 });
176
+ });
177
+
178
+ it('getId returns a string (empty or a UUID) without throwing', () => {
179
+ const backend = createWebDeviceBackend();
180
+ const id = backend.getId();
181
+ expect(typeof id).toBe('string');
182
+ });
183
+
184
+ it('getId returns the same value on repeated calls (stable install id)', () => {
185
+ const backend = createWebDeviceBackend();
186
+ const id1 = backend.getId();
187
+ const id2 = backend.getId();
188
+ if (id1 !== '') {
189
+ expect(id1).toBe(id2);
190
+ }
191
+ });
192
+
193
+ it('returns display metrics with a valid pixel ratio from jsdom', () => {
194
+ const backend = createWebDeviceBackend();
195
+ const metrics = backend.getDisplayMetrics(createDeviceDisplayMetrics());
196
+ // jsdom exposes window.devicePixelRatio = 1 and screen.width/height
197
+ expect(typeof metrics.pixelRatio).toBe('number');
198
+ expect(typeof metrics.logicalWidth).toBe('number');
199
+ });
200
+
201
+ it('totalMemory converts deviceMemory GiB to bytes', () => {
202
+ // Patch navigator.deviceMemory to a known value
203
+ const nav = navigator as unknown as Record<string, unknown>;
204
+ const original = nav['deviceMemory'];
205
+ try {
206
+ Object.defineProperty(navigator, 'deviceMemory', { configurable: true, value: 4 });
207
+ const backend = createWebDeviceBackend();
208
+ const info = backend.getInfo(createDeviceInfo());
209
+ expect(info.totalMemory).toBe(4 * 1024 * 1024 * 1024);
210
+ } finally {
211
+ if (original !== undefined) {
212
+ Object.defineProperty(navigator, 'deviceMemory', { configurable: true, value: original });
213
+ } else {
214
+ // jsdom has no deviceMemory; delete the injected one so it does not leak into a shared worker.
215
+ delete (navigator as unknown as Record<string, unknown>)['deviceMemory'];
216
+ }
217
+ }
218
+ });
219
+
220
+ it('osVersion parses Android version from UA', () => {
221
+ const backend = createWebDeviceBackend();
222
+ const result = backend.getInfo(createDeviceInfo());
223
+ // jsdom UA results in a string; actual parsing is covered by useragent unit tests
224
+ expect(typeof result.osVersion).toBe('string');
225
+ });
226
+
227
+ it('getCapabilities returns a capability snapshot without throwing', () => {
228
+ const backend = createWebDeviceBackend();
229
+ const caps = backend.getCapabilities(createDeviceCapabilities());
230
+ expect(typeof caps.hasKeyboard).toBe('boolean');
231
+ expect(typeof caps.hasMouse).toBe('boolean');
232
+ expect(caps.hasStylus).toBe(false);
233
+ });
234
+ });
235
+
236
+ describe('enableWebSafeAreaInsets', () => {
237
+ it('returns a dispose function and does not throw in jsdom', () => {
238
+ const dispose = enableWebSafeAreaInsets();
239
+ expect(typeof dispose).toBe('function');
240
+ dispose();
241
+ });
242
+ });
243
+
244
+ describe('getDeviceBackend', () => {
245
+ it('falls back to a web backend when none is set', () => {
246
+ expect(getDeviceBackend()).not.toBeNull();
247
+ });
248
+
249
+ it('returns the registered backend', () => {
250
+ const backend = fakeBackend();
251
+ setDeviceBackend(backend);
252
+ expect(getDeviceBackend()).toBe(backend);
253
+ });
254
+ });
255
+
256
+ describe('getDeviceCapabilities', () => {
257
+ it('fills and returns out via the active backend', () => {
258
+ setDeviceBackend(fakeBackend());
259
+ const out = createDeviceCapabilities();
260
+ const result = getDeviceCapabilities(out);
261
+ expect(result).toBe(out);
262
+ expect(out.hasKeyboard).toBe(true);
263
+ expect(out.hasMouse).toBe(true);
264
+ expect(out.hasStylus).toBe(false);
265
+ });
266
+ });
267
+
268
+ describe('getDeviceDisplayMetrics', () => {
269
+ it('fills and returns out via the active backend', () => {
270
+ setDeviceBackend(fakeBackend());
271
+ const out = createDeviceDisplayMetrics();
272
+ const result = getDeviceDisplayMetrics(out);
273
+ expect(result).toBe(out);
274
+ expect(out.colorDepth).toBe(8);
275
+ expect(out.densityDpi).toBe(440);
276
+ expect(out.logicalHeight).toBe(800);
277
+ expect(out.logicalWidth).toBe(360);
278
+ expect(out.physicalHeight).toBe(1600);
279
+ expect(out.physicalWidth).toBe(720);
280
+ expect(out.pixelRatio).toBe(2);
281
+ });
282
+ });
283
+
284
+ describe('getDeviceId', () => {
285
+ it('returns a string without throwing', () => {
286
+ expect(typeof getDeviceId()).toBe('string');
287
+ });
288
+
289
+ it('returns the value from a registered backend', () => {
290
+ setDeviceBackend(fakeBackend());
291
+ expect(getDeviceId()).toBe('test-device-id');
292
+ });
293
+
294
+ it('returns a stable value across two reads (web fallback)', () => {
295
+ const id1 = getDeviceId();
296
+ const id2 = getDeviceId();
297
+ if (id1 !== '') {
298
+ expect(id1).toBe(id2);
299
+ }
300
+ });
301
+ });
302
+
303
+ describe('getDeviceInfo', () => {
304
+ it('fills and returns out via the active backend', () => {
305
+ setDeviceBackend(fakeBackend());
306
+ const out = createDeviceInfo();
307
+ const result = getDeviceInfo(out);
308
+ expect(result).toBe(out);
309
+ expect(out.arch).toBe('arm64');
310
+ expect(out.availableMemory).toBe(3_000_000_000);
311
+ expect(out.boardName).toBe('msm8998');
312
+ expect(out.colorGamut).toBe('display-p3');
313
+ expect(out.cpuCores).toBe(8);
314
+ expect(out.fontScale).toBe(1.2);
315
+ expect(out.formFactor).toBe(DeviceFormFactorPhone);
316
+ expect(out.gpuRenderer).toBe('Adreno 650');
317
+ expect(out.gpuVendor).toBe('Qualcomm');
318
+ expect(out.isHdr).toBe(true);
319
+ expect(out.isJailbroken).toBe(false);
320
+ expect(out.isLowEndDevice).toBe(false);
321
+ expect(out.isRooted).toBe(false);
322
+ expect(out.isVirtual).toBe(true);
323
+ expect(out.manufacturer).toBe('Google');
324
+ expect(out.marketingName).toBe('Pixel 8 Pro');
325
+ expect(out.model).toBe('Pixel');
326
+ expect(out.osBuild).toBe('TP1A.220624.014');
327
+ expect(out.osName).toBe('Android');
328
+ expect(out.osVersion).toBe('14');
329
+ expect(out.platformString).toBe('Linux armv8l');
330
+ expect(out.productName).toBe('husky');
331
+ expect(out.supportedAbis).toEqual(['arm64-v8a', 'armeabi-v7a']);
332
+ expect(out.totalMemory).toBe(8_000_000_000);
333
+ expect(out.webViewVersion).toBe('120.0.6099.230');
334
+ });
335
+
336
+ it('web backend returns Desktop formFactor for desktop UA', () => {
337
+ const backend = createWebDeviceBackend();
338
+ const info = backend.getInfo(createDeviceInfo());
339
+ // jsdom uses a desktop UA, so formFactor should be Desktop or Unknown
340
+ expect([DeviceFormFactorDesktop, DeviceFormFactorUnknown]).toContain(info.formFactor);
341
+ });
342
+ });
343
+
344
+ describe('getSafeAreaInsets', () => {
345
+ it('fills and returns out via the active backend', () => {
346
+ setDeviceBackend(fakeBackend());
347
+ const out = createSafeAreaInsets();
348
+ const result = getSafeAreaInsets(out);
349
+ expect(result).toBe(out);
350
+ expect(out.top).toBe(24);
351
+ expect(out.bottom).toBe(16);
352
+ });
353
+ });
354
+
355
+ describe('refreshDeviceInfo', () => {
356
+ it('does not throw on the default web backend', () => {
357
+ expect(() => refreshDeviceInfo()).not.toThrow();
358
+ });
359
+
360
+ it('calls refresh() on backends that expose it', () => {
361
+ let refreshed = false;
362
+ const backend = {
363
+ ...fakeBackend(),
364
+ refresh() {
365
+ refreshed = true;
366
+ },
367
+ };
368
+ setDeviceBackend(backend);
369
+ refreshDeviceInfo();
370
+ expect(refreshed).toBe(true);
371
+ });
372
+ });
373
+
374
+ describe('setDeviceBackend', () => {
375
+ it('clears back to the web fallback when passed null', () => {
376
+ setDeviceBackend(fakeBackend());
377
+ setDeviceBackend(null);
378
+ expect(getDeviceBackend()).not.toBeNull();
379
+ // After reset, should be the web backend (not the fake)
380
+ const out = createDeviceInfo();
381
+ getDeviceInfo(out);
382
+ expect(out.model).toBe('');
383
+ });
384
+ });