@solid-primitives/sensors 1.0.0-next.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Solid Core Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,354 @@
1
+ <p>
2
+ <img width="100%" src="https://assets.solidjs.com/banner?type=Primitives&background=tiles&project=Sensors" alt="Solid Primitives Sensors">
3
+ </p>
4
+
5
+ # @solid-primitives/sensors
6
+
7
+ [![size](https://img.shields.io/badge/size-1.2_kB-blue?style=for-the-badge)](https://bundlephobia.com/package/@solid-primitives/sensors)
8
+ [![size](https://img.shields.io/npm/v/@solid-primitives/sensors?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/sensors)
9
+ [![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives#contribution-process)
10
+ [![tested with vitest](https://img.shields.io/badge/tested_with-vitest-6E9F18?style=for-the-badge&logo=vitest)](https://vitest.dev)
11
+
12
+ Reactive primitives for device motion, orientation, and hardware sensors using standard browser APIs.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @solid-primitives/sensors
18
+ # or
19
+ yarn add @solid-primitives/sensors
20
+ # or
21
+ pnpm add @solid-primitives/sensors
22
+ ```
23
+
24
+ ## Accelerometer
25
+
26
+ Uses the [`DeviceMotionEvent`](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent) API.
27
+
28
+ ### `makeAccelerometer`
29
+
30
+ Attaches a `devicemotion` event listener and calls `onChange` with the latest acceleration reading, throttled to at most once per `interval` milliseconds.
31
+
32
+ ```ts
33
+ import { makeAccelerometer } from "@solid-primitives/sensors";
34
+
35
+ const cleanup = makeAccelerometer(
36
+ acceleration => {
37
+ console.log(acceleration?.x, acceleration?.y, acceleration?.z);
38
+ },
39
+ { includeGravity: false, interval: 100 },
40
+ );
41
+
42
+ // Later, stop listening:
43
+ cleanup();
44
+ ```
45
+
46
+ **Options:**
47
+
48
+ | Option | Type | Default | Description |
49
+ | ---------------- | --------- | ------- | -------------------------------------------------------------------------- |
50
+ | `includeGravity` | `boolean` | `false` | When `true`, uses `accelerationIncludingGravity` instead of `acceleration` |
51
+ | `interval` | `number` | `100` | Minimum milliseconds between `onChange` calls |
52
+
53
+ **Returns:** `VoidFunction` — call to remove the event listener.
54
+
55
+ ### `createAccelerometer`
56
+
57
+ Reactive wrapper around `makeAccelerometer`. Returns a signal accessor that starts as `undefined` and updates to the latest `DeviceMotionEventAcceleration` reading on each throttled event.
58
+
59
+ ```ts
60
+ import { createAccelerometer } from "@solid-primitives/sensors";
61
+
62
+ function MyComponent() {
63
+ const acceleration = createAccelerometer();
64
+ // acceleration() is AccelerometerReading | undefined
65
+
66
+ return (
67
+ <div>
68
+ X: {acceleration()?.x ?? 0}, Y: {acceleration()?.y ?? 0}, Z: {acceleration()?.z ?? 0}
69
+ </div>
70
+ );
71
+ }
72
+ ```
73
+
74
+ **Signature:**
75
+
76
+ ```ts
77
+ function createAccelerometer(
78
+ includeGravity?: boolean, // default: false
79
+ interval?: number, // default: 100ms
80
+ ): Accessor<AccelerometerReading | undefined>;
81
+
82
+ type AccelerometerReading = DeviceMotionEventAcceleration | null;
83
+ ```
84
+
85
+ **SSR:** Returns `() => ({ x: 0, y: 0, z: 0 })` on the server — no event listeners are attached.
86
+
87
+ ## Gyroscope
88
+
89
+ Uses the [`DeviceOrientationEvent`](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent) API.
90
+
91
+ ### `makeGyroscope`
92
+
93
+ Attaches a `deviceorientation` event listener and calls `onChange` with the latest orientation reading, throttled to at most once per `interval` milliseconds. `null` orientation values (common on some platforms) are coerced to `0`.
94
+
95
+ ```ts
96
+ import { makeGyroscope } from "@solid-primitives/sensors";
97
+
98
+ const cleanup = makeGyroscope(
99
+ orientation => {
100
+ console.log(orientation.alpha, orientation.beta, orientation.gamma);
101
+ },
102
+ { interval: 100 },
103
+ );
104
+
105
+ // Later, stop listening:
106
+ cleanup();
107
+ ```
108
+
109
+ **Options:**
110
+
111
+ | Option | Type | Default | Description |
112
+ | ---------- | -------- | ------- | --------------------------------------------- |
113
+ | `interval` | `number` | `100` | Minimum milliseconds between `onChange` calls |
114
+
115
+ **Returns:** `VoidFunction` — call to remove the event listener.
116
+
117
+ ### `createGyroscope`
118
+
119
+ Reactive wrapper around `makeGyroscope`. Returns an object with reactive `alpha`, `beta`, and `gamma` getters that start at `0` and update on each throttled orientation event.
120
+
121
+ ```ts
122
+ import { createGyroscope } from "@solid-primitives/sensors";
123
+
124
+ function MyComponent() {
125
+ const orientation = createGyroscope();
126
+
127
+ return (
128
+ <div>
129
+ α: {orientation.alpha}°, β: {orientation.beta}°, γ: {orientation.gamma}°
130
+ </div>
131
+ );
132
+ }
133
+ ```
134
+
135
+ **Signature:**
136
+
137
+ ```ts
138
+ function createGyroscope(
139
+ interval?: number, // default: 100ms
140
+ ): GyroscopeReading;
141
+
142
+ type GyroscopeReading = { alpha: number; beta: number; gamma: number };
143
+ ```
144
+
145
+ **SSR:** Returns `{ alpha: 0, beta: 0, gamma: 0 }` — a plain non-reactive object.
146
+
147
+ ## Generic Sensor API
148
+
149
+ A factory pair for any [Generic Sensor API](https://developer.mozilla.org/en-US/docs/Web/API/Sensor_APIs) sensor (Chromium-based browsers). Covers `LinearAccelerationSensor`, `GravitySensor`, `AbsoluteOrientationSensor`, `RelativeOrientationSensor`, and others.
150
+
151
+ ### `makeSensor`
152
+
153
+ Sets up any Generic Sensor API sensor and calls `onChange` with the live sensor object on each reading. Returns `null` if the sensor constructor throws (API unsupported or permission denied).
154
+
155
+ ```ts
156
+ import { makeSensor } from "@solid-primitives/sensors";
157
+
158
+ const cleanup = makeSensor(
159
+ LinearAccelerationSensor,
160
+ sensor => console.log(sensor.x, sensor.y, sensor.z),
161
+ { frequency: 60 },
162
+ );
163
+
164
+ if (cleanup) {
165
+ // Later, stop:
166
+ cleanup();
167
+ }
168
+ ```
169
+
170
+ **Signature:**
171
+
172
+ ```ts
173
+ function makeSensor<T extends GenericSensor>(
174
+ SensorClass: { new (options?: any): T },
175
+ onChange: (sensor: T) => void,
176
+ options?: SensorOptions,
177
+ ): VoidFunction | null;
178
+
179
+ type SensorOptions = { frequency?: number };
180
+ ```
181
+
182
+ **Returns:** `VoidFunction` (cleanup) or `null` if unsupported.
183
+
184
+ ### `createSensor`
185
+
186
+ Reactive wrapper around `makeSensor`. Returns an accessor that updates on **every reading event** — even if the sensor object reference is the same — because the underlying signal uses `equals: false`. Returns `undefined` until the first reading or if the sensor is unavailable.
187
+
188
+ ```ts
189
+ import { createSensor } from "@solid-primitives/sensors";
190
+
191
+ function MyComponent() {
192
+ const sensor = createSensor(LinearAccelerationSensor, { frequency: 60 });
193
+
194
+ return (
195
+ <Show when={sensor()}>
196
+ {s => <div>X: {s().x ?? 0}</div>}
197
+ </Show>
198
+ );
199
+ }
200
+ ```
201
+
202
+ **Signature:**
203
+
204
+ ```ts
205
+ function createSensor<T extends GenericSensor>(
206
+ SensorClass: { new (options?: any): T },
207
+ options?: SensorOptions,
208
+ ): Accessor<T | undefined>;
209
+ ```
210
+
211
+ **SSR:** Returns `() => undefined`.
212
+
213
+ ## Compass
214
+
215
+ Uses `window.Magnetometer` from the [Generic Sensor API](https://developer.mozilla.org/en-US/docs/Web/API/Magnetometer). Chromium-based browsers only. Reports raw magnetic field strength in microteslas (µT) as `{ x, y, z }` components.
216
+
217
+ ### `makeCompass`
218
+
219
+ ```ts
220
+ import { makeCompass } from "@solid-primitives/sensors";
221
+
222
+ const cleanup = makeCompass(({ x, y, z }) => console.log(`Field: ${x}µT, ${y}µT, ${z}µT`), {
223
+ frequency: 10,
224
+ referenceFrame: "device",
225
+ });
226
+
227
+ if (cleanup) cleanup();
228
+ ```
229
+
230
+ **Options:**
231
+
232
+ | Option | Type | Default | Description |
233
+ | ---------------- | ---------------------- | ---------- | -------------------------- |
234
+ | `frequency` | `number` | — | Readings per second |
235
+ | `referenceFrame` | `"device" \| "screen"` | `"device"` | Coordinate reference frame |
236
+
237
+ **Returns:** `VoidFunction` or `null` if `window.Magnetometer` is unavailable.
238
+
239
+ ### `createCompass`
240
+
241
+ Returns an object with reactive `x`, `y`, `z` getters (in µT), all starting at `0`.
242
+
243
+ ```ts
244
+ import { createCompass } from "@solid-primitives/sensors";
245
+
246
+ function Compass() {
247
+ const mag = createCompass({ frequency: 10 });
248
+ const heading = () => Math.atan2(mag.y, mag.x) * (180 / Math.PI);
249
+ return <div>Heading: {heading()}°</div>;
250
+ }
251
+ ```
252
+
253
+ **Signature:**
254
+
255
+ ```ts
256
+ function createCompass(options?: CompassOptions): CompassReading;
257
+
258
+ type CompassOptions = { frequency?: number; referenceFrame?: "device" | "screen" };
259
+ type CompassReading = { x: number; y: number; z: number };
260
+ ```
261
+
262
+ **SSR:** Returns `{ x: 0, y: 0, z: 0 }` — a plain non-reactive object.
263
+
264
+ ## Battery
265
+
266
+ Uses the [Battery Status API](https://developer.mozilla.org/en-US/docs/Web/API/Battery_Status_API) (`navigator.getBattery()`). Supported in Chrome/Edge; not available in Firefox or Safari.
267
+
268
+ ### `makeBattery`
269
+
270
+ Subscribes to the Battery API and calls `onChange` immediately with the current reading, then again on every battery change event. Returns a synchronous cleanup function — safe to use with `onCleanup` even though the API initializes asynchronously.
271
+
272
+ ```ts
273
+ import { makeBattery } from "@solid-primitives/sensors";
274
+
275
+ const cleanup = makeBattery(({ level, charging }) => {
276
+ console.log(`${Math.round(level * 100)}% ${charging ? "charging" : "discharging"}`);
277
+ });
278
+
279
+ // Later:
280
+ cleanup();
281
+ ```
282
+
283
+ **Signature:**
284
+
285
+ ```ts
286
+ function makeBattery(onChange: (reading: BatteryReading) => void): VoidFunction;
287
+
288
+ type BatteryReading = {
289
+ charging: boolean;
290
+ chargingTime: number; // seconds until full; Infinity if not charging
291
+ dischargingTime: number; // seconds until empty; Infinity if charging
292
+ level: number; // 0.0–1.0
293
+ };
294
+ ```
295
+
296
+ **Returns:** `VoidFunction` — always (no-ops if the API is unavailable).
297
+
298
+ ### `createBattery`
299
+
300
+ Returns a reactive accessor for battery status. Starts as `undefined` until the Battery API resolves. Subscribe to any of the four properties to track specific changes.
301
+
302
+ ```ts
303
+ import { createBattery } from "@solid-primitives/sensors";
304
+
305
+ function BatteryIndicator() {
306
+ const battery = createBattery();
307
+
308
+ return (
309
+ <Show when={battery()} fallback={<span>Loading battery…</span>}>
310
+ {b => (
311
+ <span>
312
+ {Math.round(b().level * 100)}%{b().charging ? " ⚡" : ""}
313
+ </span>
314
+ )}
315
+ </Show>
316
+ );
317
+ }
318
+ ```
319
+
320
+ **Signature:**
321
+
322
+ ```ts
323
+ function createBattery(): Accessor<BatteryReading | undefined>;
324
+ ```
325
+
326
+ **SSR:** Returns `() => ({ charging: false, chargingTime: 0, dischargingTime: 0, level: 1 })`.
327
+
328
+ ## Throttling
329
+
330
+ Both `makeAccelerometer` and `makeGyroscope` throttle events using a **leading-edge** strategy: the first event in a burst fires `onChange` immediately; subsequent events within `interval` ms are dropped. The next event after the interval elapses fires again.
331
+
332
+ Set `interval: 0` to disable throttling (useful in tests).
333
+
334
+ Generic Sensor API primitives (`makeSensor`, `makeCompass`) use the sensor's built-in `frequency` option for rate control — no additional throttling is applied.
335
+
336
+ ## Types
337
+
338
+ ```ts
339
+ type AccelerometerReading = DeviceMotionEventAcceleration | null;
340
+ type GyroscopeReading = { alpha: number; beta: number; gamma: number };
341
+ type SensorOptions = { frequency?: number };
342
+ type CompassOptions = { frequency?: number; referenceFrame?: "device" | "screen" };
343
+ type CompassReading = { x: number; y: number; z: number };
344
+ type BatteryReading = {
345
+ charging: boolean;
346
+ chargingTime: number;
347
+ dischargingTime: number;
348
+ level: number;
349
+ };
350
+ ```
351
+
352
+ ## Changelog
353
+
354
+ See [CHANGELOG.md](./CHANGELOG.md)
@@ -0,0 +1,25 @@
1
+ import type { Accessor } from "solid-js";
2
+ export type AccelerometerReading = DeviceMotionEventAcceleration | null;
3
+ /**
4
+ * Sets up a raw `devicemotion` event listener and returns a cleanup function.
5
+ * No Solid lifecycle — suitable for use outside a reactive owner.
6
+ *
7
+ * @param onChange Called on each (throttled) motion event with the acceleration reading
8
+ * @param options.includeGravity Use `accelerationIncludingGravity` instead of `acceleration`
9
+ * @param options.interval Minimum milliseconds between onChange calls (default 100)
10
+ * @returns Cleanup function that removes the event listener
11
+ */
12
+ export declare const makeAccelerometer: (onChange: (acceleration: AccelerometerReading) => void, options?: {
13
+ includeGravity?: boolean;
14
+ interval?: number;
15
+ }) => VoidFunction;
16
+ /**
17
+ * Creates a reactive accessor for device acceleration data.
18
+ * Starts as `undefined` until the first motion event arrives.
19
+ * Registers cleanup with `onCleanup` when inside a reactive owner.
20
+ *
21
+ * @param includeGravity Use `accelerationIncludingGravity` instead of `acceleration` (default false)
22
+ * @param interval Minimum milliseconds between updates (default 100)
23
+ * @returns Accessor yielding the latest AccelerometerReading, or `undefined` before the first event
24
+ */
25
+ export declare const createAccelerometer: (includeGravity?: boolean, interval?: number) => Accessor<AccelerometerReading | undefined>;
@@ -0,0 +1,44 @@
1
+ import { createSignal, onCleanup } from "solid-js";
2
+ import { isServer } from "@solidjs/web";
3
+ const OWNED_WRITE = { ownedWrite: true };
4
+ /**
5
+ * Sets up a raw `devicemotion` event listener and returns a cleanup function.
6
+ * No Solid lifecycle — suitable for use outside a reactive owner.
7
+ *
8
+ * @param onChange Called on each (throttled) motion event with the acceleration reading
9
+ * @param options.includeGravity Use `accelerationIncludingGravity` instead of `acceleration`
10
+ * @param options.interval Minimum milliseconds between onChange calls (default 100)
11
+ * @returns Cleanup function that removes the event listener
12
+ */
13
+ export const makeAccelerometer = (onChange, options = {}) => {
14
+ const { includeGravity = false, interval = 100 } = options;
15
+ let throttled = false;
16
+ const handler = (e) => {
17
+ if (throttled)
18
+ return;
19
+ throttled = true;
20
+ setTimeout(() => {
21
+ throttled = false;
22
+ }, interval);
23
+ onChange(includeGravity ? e.accelerationIncludingGravity : e.acceleration);
24
+ };
25
+ addEventListener("devicemotion", handler);
26
+ return () => removeEventListener("devicemotion", handler);
27
+ };
28
+ /**
29
+ * Creates a reactive accessor for device acceleration data.
30
+ * Starts as `undefined` until the first motion event arrives.
31
+ * Registers cleanup with `onCleanup` when inside a reactive owner.
32
+ *
33
+ * @param includeGravity Use `accelerationIncludingGravity` instead of `acceleration` (default false)
34
+ * @param interval Minimum milliseconds between updates (default 100)
35
+ * @returns Accessor yielding the latest AccelerometerReading, or `undefined` before the first event
36
+ */
37
+ export const createAccelerometer = (includeGravity = false, interval = 100) => {
38
+ if (isServer)
39
+ return () => ({ x: 0, y: 0, z: 0 });
40
+ const [acceleration, setAcceleration] = createSignal(undefined, OWNED_WRITE);
41
+ const cleanup = makeAccelerometer(acc => setAcceleration(acc), { includeGravity, interval });
42
+ onCleanup(cleanup);
43
+ return acceleration;
44
+ };
@@ -0,0 +1,27 @@
1
+ import type { Accessor } from "solid-js";
2
+ export type BatteryReading = {
3
+ charging: boolean;
4
+ chargingTime: number;
5
+ dischargingTime: number;
6
+ level: number;
7
+ };
8
+ /**
9
+ * Subscribes to the Battery Status API and calls `onChange` with the current reading
10
+ * immediately after the API resolves, then again on every battery change event.
11
+ * Returns a synchronous cleanup function safe to pass to `onCleanup`.
12
+ * Silently no-ops if `navigator.getBattery` is unavailable.
13
+ *
14
+ * @param onChange Called with the current BatteryReading on subscribe and on each change
15
+ * @returns Synchronous cleanup function that removes all battery event listeners
16
+ */
17
+ export declare const makeBattery: (onChange: (reading: BatteryReading) => void) => VoidFunction;
18
+ /**
19
+ * Creates a reactive accessor for battery status.
20
+ * Starts as `undefined` until the Battery Status API resolves (async).
21
+ * Returns a static `() => ({ charging: false, chargingTime: 0, dischargingTime: 0, level: 1 })`
22
+ * on the server.
23
+ * Registers cleanup with `onCleanup` when inside a reactive owner.
24
+ *
25
+ * @returns Accessor yielding the current BatteryReading, or `undefined` before the API resolves
26
+ */
27
+ export declare const createBattery: () => Accessor<BatteryReading | undefined>;
@@ -0,0 +1,58 @@
1
+ import { createSignal, onCleanup } from "solid-js";
2
+ import { isServer } from "@solidjs/web";
3
+ const OWNED_WRITE = { ownedWrite: true };
4
+ /**
5
+ * Subscribes to the Battery Status API and calls `onChange` with the current reading
6
+ * immediately after the API resolves, then again on every battery change event.
7
+ * Returns a synchronous cleanup function safe to pass to `onCleanup`.
8
+ * Silently no-ops if `navigator.getBattery` is unavailable.
9
+ *
10
+ * @param onChange Called with the current BatteryReading on subscribe and on each change
11
+ * @returns Synchronous cleanup function that removes all battery event listeners
12
+ */
13
+ export const makeBattery = (onChange) => {
14
+ let disposed = false;
15
+ let removeFn;
16
+ if (typeof navigator !== "undefined" && "getBattery" in navigator) {
17
+ navigator.getBattery().then((battery) => {
18
+ if (disposed)
19
+ return;
20
+ const update = () => onChange({
21
+ charging: battery.charging,
22
+ chargingTime: battery.chargingTime,
23
+ dischargingTime: battery.dischargingTime,
24
+ level: battery.level,
25
+ });
26
+ update();
27
+ const events = [
28
+ "chargingchange",
29
+ "chargingtimechange",
30
+ "dischargingtimechange",
31
+ "levelchange",
32
+ ];
33
+ events.forEach(e => battery.addEventListener(e, update));
34
+ removeFn = () => events.forEach(e => battery.removeEventListener(e, update));
35
+ });
36
+ }
37
+ return () => {
38
+ disposed = true;
39
+ removeFn?.();
40
+ };
41
+ };
42
+ /**
43
+ * Creates a reactive accessor for battery status.
44
+ * Starts as `undefined` until the Battery Status API resolves (async).
45
+ * Returns a static `() => ({ charging: false, chargingTime: 0, dischargingTime: 0, level: 1 })`
46
+ * on the server.
47
+ * Registers cleanup with `onCleanup` when inside a reactive owner.
48
+ *
49
+ * @returns Accessor yielding the current BatteryReading, or `undefined` before the API resolves
50
+ */
51
+ export const createBattery = () => {
52
+ if (isServer)
53
+ return () => ({ charging: false, chargingTime: 0, dischargingTime: 0, level: 1 });
54
+ const [reading, setReading] = createSignal(undefined, OWNED_WRITE);
55
+ const cleanup = makeBattery(r => setReading(r));
56
+ onCleanup(cleanup);
57
+ return reading;
58
+ };
@@ -0,0 +1,31 @@
1
+ /** Options for `makeCompass` / `createCompass`. */
2
+ export type CompassOptions = {
3
+ frequency?: number;
4
+ referenceFrame?: "device" | "screen";
5
+ };
6
+ /** Raw magnetometer reading in microteslas (µT), as returned by `makeCompass` / `createCompass`. */
7
+ export type CompassReading = {
8
+ x: number;
9
+ y: number;
10
+ z: number;
11
+ };
12
+ /**
13
+ * Sets up a `Magnetometer` sensor and calls `onChange` with x/y/z readings in microteslas.
14
+ * Uses the Generic Sensor API (`window.Magnetometer`).
15
+ * Returns `null` if the Magnetometer API is unavailable or throws on construction.
16
+ *
17
+ * @param onChange Called on each reading with `{ x, y, z }` in microteslas (null coerced to 0)
18
+ * @param options Optional `{ frequency, referenceFrame }` passed to the Magnetometer constructor
19
+ * @returns Cleanup function that stops the sensor, or `null` if unsupported
20
+ */
21
+ export declare const makeCompass: (onChange: (reading: CompassReading) => void, options?: CompassOptions) => VoidFunction | null;
22
+ /**
23
+ * Creates a reactive object with `x`, `y`, `z` magnetometer readings in microteslas.
24
+ * All properties start at `0` and update on each sensor reading.
25
+ * Returns a non-reactive `{ x: 0, y: 0, z: 0 }` on the server.
26
+ * Registers cleanup with `onCleanup` when inside a reactive owner.
27
+ *
28
+ * @param options Optional `{ frequency, referenceFrame }` passed to the Magnetometer constructor
29
+ * @returns Reactive CompassReading object `{ x, y, z }`
30
+ */
31
+ export declare const createCompass: (options?: CompassOptions) => CompassReading;
@@ -0,0 +1,52 @@
1
+ import { createSignal, onCleanup } from "solid-js";
2
+ import { isServer } from "@solidjs/web";
3
+ import { makeSensor } from "./sensor.js";
4
+ const OWNED_WRITE = { ownedWrite: true };
5
+ /**
6
+ * Sets up a `Magnetometer` sensor and calls `onChange` with x/y/z readings in microteslas.
7
+ * Uses the Generic Sensor API (`window.Magnetometer`).
8
+ * Returns `null` if the Magnetometer API is unavailable or throws on construction.
9
+ *
10
+ * @param onChange Called on each reading with `{ x, y, z }` in microteslas (null coerced to 0)
11
+ * @param options Optional `{ frequency, referenceFrame }` passed to the Magnetometer constructor
12
+ * @returns Cleanup function that stops the sensor, or `null` if unsupported
13
+ */
14
+ export const makeCompass = (onChange, options) => {
15
+ if (!("Magnetometer" in globalThis))
16
+ return null;
17
+ return makeSensor(globalThis.Magnetometer, s => onChange({ x: s.x ?? 0, y: s.y ?? 0, z: s.z ?? 0 }), options);
18
+ };
19
+ /**
20
+ * Creates a reactive object with `x`, `y`, `z` magnetometer readings in microteslas.
21
+ * All properties start at `0` and update on each sensor reading.
22
+ * Returns a non-reactive `{ x: 0, y: 0, z: 0 }` on the server.
23
+ * Registers cleanup with `onCleanup` when inside a reactive owner.
24
+ *
25
+ * @param options Optional `{ frequency, referenceFrame }` passed to the Magnetometer constructor
26
+ * @returns Reactive CompassReading object `{ x, y, z }`
27
+ */
28
+ export const createCompass = (options) => {
29
+ if (isServer)
30
+ return { x: 0, y: 0, z: 0 };
31
+ const [x, setX] = createSignal(0, OWNED_WRITE);
32
+ const [y, setY] = createSignal(0, OWNED_WRITE);
33
+ const [z, setZ] = createSignal(0, OWNED_WRITE);
34
+ const cleanup = makeCompass(r => {
35
+ setX(r.x);
36
+ setY(r.y);
37
+ setZ(r.z);
38
+ }, options);
39
+ if (cleanup)
40
+ onCleanup(cleanup);
41
+ return {
42
+ get x() {
43
+ return x();
44
+ },
45
+ get y() {
46
+ return y();
47
+ },
48
+ get z() {
49
+ return z();
50
+ },
51
+ };
52
+ };
@@ -0,0 +1,25 @@
1
+ export type GyroscopeReading = {
2
+ alpha: number;
3
+ beta: number;
4
+ gamma: number;
5
+ };
6
+ /**
7
+ * Sets up a raw `deviceorientation` event listener and returns a cleanup function.
8
+ * No Solid lifecycle — suitable for use outside a reactive owner.
9
+ *
10
+ * @param onChange Called on each (throttled) orientation event with alpha/beta/gamma values
11
+ * @param options.interval Minimum milliseconds between onChange calls (default 100)
12
+ * @returns Cleanup function that removes the event listener
13
+ */
14
+ export declare const makeGyroscope: (onChange: (orientation: GyroscopeReading) => void, options?: {
15
+ interval?: number;
16
+ }) => VoidFunction;
17
+ /**
18
+ * Creates a reactive object tracking device orientation (gyroscope data).
19
+ * Returns an object with reactive `alpha`, `beta`, and `gamma` properties.
20
+ * Registers cleanup with `onCleanup` when inside a reactive owner.
21
+ *
22
+ * @param interval Minimum milliseconds between updates (default 100)
23
+ * @returns Reactive GyroscopeReading object `{ alpha, beta, gamma }`
24
+ */
25
+ export declare const createGyroscope: (interval?: number) => GyroscopeReading;
@@ -0,0 +1,62 @@
1
+ import { createSignal, onCleanup } from "solid-js";
2
+ import { isServer } from "@solidjs/web";
3
+ const OWNED_WRITE = { ownedWrite: true };
4
+ /**
5
+ * Sets up a raw `deviceorientation` event listener and returns a cleanup function.
6
+ * No Solid lifecycle — suitable for use outside a reactive owner.
7
+ *
8
+ * @param onChange Called on each (throttled) orientation event with alpha/beta/gamma values
9
+ * @param options.interval Minimum milliseconds between onChange calls (default 100)
10
+ * @returns Cleanup function that removes the event listener
11
+ */
12
+ export const makeGyroscope = (onChange, options = {}) => {
13
+ const { interval = 100 } = options;
14
+ let throttled = false;
15
+ const handler = (e) => {
16
+ if (throttled)
17
+ return;
18
+ throttled = true;
19
+ setTimeout(() => {
20
+ throttled = false;
21
+ }, interval);
22
+ onChange({
23
+ alpha: e.alpha ?? 0,
24
+ beta: e.beta ?? 0,
25
+ gamma: e.gamma ?? 0,
26
+ });
27
+ };
28
+ addEventListener("deviceorientation", handler);
29
+ return () => removeEventListener("deviceorientation", handler);
30
+ };
31
+ /**
32
+ * Creates a reactive object tracking device orientation (gyroscope data).
33
+ * Returns an object with reactive `alpha`, `beta`, and `gamma` properties.
34
+ * Registers cleanup with `onCleanup` when inside a reactive owner.
35
+ *
36
+ * @param interval Minimum milliseconds between updates (default 100)
37
+ * @returns Reactive GyroscopeReading object `{ alpha, beta, gamma }`
38
+ */
39
+ export const createGyroscope = (interval = 100) => {
40
+ if (isServer)
41
+ return { alpha: 0, beta: 0, gamma: 0 };
42
+ const [alpha, setAlpha] = createSignal(0, OWNED_WRITE);
43
+ const [beta, setBeta] = createSignal(0, OWNED_WRITE);
44
+ const [gamma, setGamma] = createSignal(0, OWNED_WRITE);
45
+ const cleanup = makeGyroscope(o => {
46
+ setAlpha(o.alpha);
47
+ setBeta(o.beta);
48
+ setGamma(o.gamma);
49
+ }, { interval });
50
+ onCleanup(cleanup);
51
+ return {
52
+ get alpha() {
53
+ return alpha();
54
+ },
55
+ get beta() {
56
+ return beta();
57
+ },
58
+ get gamma() {
59
+ return gamma();
60
+ },
61
+ };
62
+ };
@@ -0,0 +1,5 @@
1
+ export * from "./accelerometer.js";
2
+ export * from "./gyroscope.js";
3
+ export * from "./sensor.js";
4
+ export * from "./compass.js";
5
+ export * from "./battery.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./accelerometer.js";
2
+ export * from "./gyroscope.js";
3
+ export * from "./sensor.js";
4
+ export * from "./compass.js";
5
+ export * from "./battery.js";
@@ -0,0 +1,43 @@
1
+ import type { Accessor } from "solid-js";
2
+ /** Options shared by all Generic Sensor API primitives. */
3
+ export type SensorOptions = {
4
+ frequency?: number;
5
+ };
6
+ /**
7
+ * Minimal Generic Sensor API interface (not in the standard TypeScript DOM lib).
8
+ * Extend this to type specific sensors passed to `makeSensor` / `createSensor`.
9
+ */
10
+ export interface GenericSensor extends EventTarget {
11
+ readonly activated: boolean;
12
+ readonly hasReading: boolean;
13
+ readonly timestamp: DOMHighResTimeStamp | undefined;
14
+ start(): void;
15
+ stop(): void;
16
+ }
17
+ /**
18
+ * Sets up any Generic Sensor API sensor and calls `onChange` on each reading.
19
+ * Returns a cleanup function, or `null` if the sensor constructor throws
20
+ * (e.g. the API is unsupported or permission was denied).
21
+ *
22
+ * @param SensorClass Any Generic Sensor API constructor (Magnetometer, LinearAccelerationSensor, etc.)
23
+ * @param onChange Called with the live sensor object on each reading event
24
+ * @param options Optional `{ frequency }` passed to the sensor constructor
25
+ * @returns Cleanup function that stops and removes the sensor, or `null` on failure
26
+ */
27
+ export declare const makeSensor: <T extends GenericSensor>(SensorClass: {
28
+ new (options?: any): T;
29
+ }, onChange: (sensor: T) => void, options?: SensorOptions) => VoidFunction | null;
30
+ /**
31
+ * Creates a reactive accessor for any Generic Sensor API sensor.
32
+ * The accessor re-fires on every reading event (even if the sensor object reference
33
+ * is the same) because the underlying signal uses `equals: false`.
34
+ * Returns `undefined` until the first reading or if the sensor is unavailable.
35
+ * Returns a static `() => undefined` on the server.
36
+ *
37
+ * @param SensorClass Any Generic Sensor API constructor
38
+ * @param options Optional `{ frequency }` passed to the sensor constructor
39
+ * @returns Accessor yielding the live sensor object (updated on every reading), or `undefined`
40
+ */
41
+ export declare const createSensor: <T extends GenericSensor>(SensorClass: {
42
+ new (options?: any): T;
43
+ }, options?: SensorOptions) => Accessor<T | undefined>;
package/dist/sensor.js ADDED
@@ -0,0 +1,52 @@
1
+ import { createSignal, onCleanup } from "solid-js";
2
+ import { isServer } from "@solidjs/web";
3
+ const OWNED_WRITE = { ownedWrite: true };
4
+ /**
5
+ * Sets up any Generic Sensor API sensor and calls `onChange` on each reading.
6
+ * Returns a cleanup function, or `null` if the sensor constructor throws
7
+ * (e.g. the API is unsupported or permission was denied).
8
+ *
9
+ * @param SensorClass Any Generic Sensor API constructor (Magnetometer, LinearAccelerationSensor, etc.)
10
+ * @param onChange Called with the live sensor object on each reading event
11
+ * @param options Optional `{ frequency }` passed to the sensor constructor
12
+ * @returns Cleanup function that stops and removes the sensor, or `null` on failure
13
+ */
14
+ export const makeSensor = (SensorClass, onChange, options) => {
15
+ let sensor;
16
+ try {
17
+ sensor = new SensorClass(options);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ const handleReading = () => onChange(sensor);
23
+ sensor.addEventListener("reading", handleReading);
24
+ sensor.start();
25
+ return () => {
26
+ sensor.removeEventListener("reading", handleReading);
27
+ sensor.stop();
28
+ };
29
+ };
30
+ /**
31
+ * Creates a reactive accessor for any Generic Sensor API sensor.
32
+ * The accessor re-fires on every reading event (even if the sensor object reference
33
+ * is the same) because the underlying signal uses `equals: false`.
34
+ * Returns `undefined` until the first reading or if the sensor is unavailable.
35
+ * Returns a static `() => undefined` on the server.
36
+ *
37
+ * @param SensorClass Any Generic Sensor API constructor
38
+ * @param options Optional `{ frequency }` passed to the sensor constructor
39
+ * @returns Accessor yielding the live sensor object (updated on every reading), or `undefined`
40
+ */
41
+ export const createSensor = (SensorClass, options) => {
42
+ if (isServer)
43
+ return () => undefined;
44
+ const [sensor, setSensor] = createSignal(undefined, {
45
+ ...OWNED_WRITE,
46
+ equals: false,
47
+ });
48
+ const cleanup = makeSensor(SensorClass, s => setSensor(s), options);
49
+ if (cleanup)
50
+ onCleanup(cleanup);
51
+ return sensor;
52
+ };
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@solid-primitives/sensors",
3
+ "version": "1.0.0-next.0",
4
+ "description": "Primitives for device sensors: accelerometer, gyroscope, compass, battery, and generic Sensor API.",
5
+ "author": "David Di Biase <dave@solidjs.com>",
6
+ "contributors": [],
7
+ "license": "MIT",
8
+ "homepage": "https://primitives.solidjs.community/package/sensors",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/solidjs-community/solid-primitives.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/solidjs-community/solid-primitives/issues"
15
+ },
16
+ "primitive": {
17
+ "name": "sensors",
18
+ "stage": 3,
19
+ "list": [
20
+ "makeAccelerometer",
21
+ "createAccelerometer",
22
+ "makeGyroscope",
23
+ "createGyroscope",
24
+ "makeSensor",
25
+ "createSensor",
26
+ "makeCompass",
27
+ "createCompass",
28
+ "makeBattery",
29
+ "createBattery"
30
+ ],
31
+ "category": "Display & Media",
32
+ "gzip": 1195
33
+ },
34
+ "keywords": [
35
+ "accelerometer",
36
+ "gyroscope",
37
+ "sensors",
38
+ "solid",
39
+ "primitives"
40
+ ],
41
+ "private": false,
42
+ "sideEffects": false,
43
+ "files": [
44
+ "dist"
45
+ ],
46
+ "type": "module",
47
+ "module": "./dist/index.js",
48
+ "types": "./dist/index.d.ts",
49
+ "browser": {},
50
+ "exports": {
51
+ "import": {
52
+ "@solid-primitives/source": "./src/index.ts",
53
+ "types": "./dist/index.d.ts",
54
+ "default": "./dist/index.js"
55
+ }
56
+ },
57
+ "typesVersions": {},
58
+ "peerDependencies": {
59
+ "solid-js": "^2.0.0-beta.15"
60
+ },
61
+ "devDependencies": {
62
+ "@solidjs/web": "2.0.0-beta.15",
63
+ "solid-js": "2.0.0-beta.15"
64
+ },
65
+ "scripts": {
66
+ "dev": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/dev.ts",
67
+ "build": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/build.ts",
68
+ "vitest": "vitest -c ../../configs/vitest.config.ts",
69
+ "test": "pnpm run vitest",
70
+ "test:ssr": "pnpm run vitest --mode ssr"
71
+ }
72
+ }