react-pebble 0.1.1 → 0.2.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 +21 -0
- package/dist/lib/compiler.cjs +2 -2
- package/dist/lib/compiler.cjs.map +1 -1
- package/dist/lib/compiler.js +4 -1
- package/dist/lib/compiler.js.map +1 -1
- package/dist/lib/components.cjs +1 -1
- package/dist/lib/components.cjs.map +1 -1
- package/dist/lib/components.js +44 -5
- package/dist/lib/components.js.map +1 -1
- package/dist/lib/hooks.cjs +1 -1
- package/dist/lib/hooks.cjs.map +1 -1
- package/dist/lib/hooks.js +198 -3
- package/dist/lib/hooks.js.map +1 -1
- package/dist/lib/index.cjs +1 -1
- package/dist/lib/index.cjs.map +1 -1
- package/dist/lib/index.js +231 -108
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/plugin.cjs +25 -5
- package/dist/lib/plugin.cjs.map +1 -1
- package/dist/lib/plugin.js +62 -35
- package/dist/lib/plugin.js.map +1 -1
- package/dist/lib/src/compiler/index.d.ts +2 -0
- package/dist/lib/src/components/index.d.ts +28 -1
- package/dist/lib/src/hooks/index.d.ts +182 -0
- package/dist/lib/src/index.d.ts +4 -4
- package/dist/lib/src/pebble-output.d.ts +15 -0
- package/dist/lib/src/plugin/index.d.ts +6 -0
- package/package.json +10 -11
- package/scripts/compile-to-piu.ts +315 -26
- package/scripts/deploy.sh +0 -0
- package/scripts/test-emulator.sh +371 -0
- package/src/compiler/index.ts +8 -1
- package/src/components/index.tsx +75 -1
- package/src/hooks/index.ts +507 -19
- package/src/index.ts +26 -0
- package/src/pebble-output.ts +408 -48
- package/src/plugin/index.ts +101 -49
- package/src/types/moddable.d.ts +26 -4
package/src/hooks/index.ts
CHANGED
|
@@ -142,10 +142,9 @@ export function useLongButton(button: PebbleButton, handler: PebbleButtonHandler
|
|
|
142
142
|
// Time hooks
|
|
143
143
|
//
|
|
144
144
|
// On Alloy, `watch.addEventListener('secondchange'|'minutechange', fn)` is
|
|
145
|
-
// the canonical tick source
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// works in both XS and Node. (`watch` is used for redraws, not hook state.)
|
|
145
|
+
// the canonical tick source — it fires exactly on boundaries and is far more
|
|
146
|
+
// battery-efficient than setInterval. In Node mock mode we fall back to
|
|
147
|
+
// setInterval.
|
|
149
148
|
// ---------------------------------------------------------------------------
|
|
150
149
|
|
|
151
150
|
export function useTime(intervalMs = 1000): Date {
|
|
@@ -153,6 +152,15 @@ export function useTime(intervalMs = 1000): Date {
|
|
|
153
152
|
|
|
154
153
|
useEffect(() => {
|
|
155
154
|
const tick = () => setTime(new Date());
|
|
155
|
+
|
|
156
|
+
// On Alloy: use watch tick events for battery efficiency
|
|
157
|
+
if (typeof watch !== 'undefined' && watch) {
|
|
158
|
+
const event = intervalMs <= 1000 ? 'secondchange' : 'minutechange';
|
|
159
|
+
watch.addEventListener(event, tick);
|
|
160
|
+
return () => watch!.removeEventListener(event, tick);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Mock mode: fall back to setInterval
|
|
156
164
|
const id = setInterval(tick, intervalMs);
|
|
157
165
|
return () => clearInterval(id);
|
|
158
166
|
}, [intervalMs]);
|
|
@@ -292,20 +300,500 @@ export function useMessage<T>(options: UseMessageOptions<T>): UseMessageResult<T
|
|
|
292
300
|
}
|
|
293
301
|
|
|
294
302
|
// ---------------------------------------------------------------------------
|
|
295
|
-
//
|
|
303
|
+
// Battery hook — reads Alloy's Battery sensor (percent, charging, plugged)
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
export interface BatteryState {
|
|
307
|
+
percent: number;
|
|
308
|
+
charging: boolean;
|
|
309
|
+
plugged: boolean;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Returns the current battery state. On Alloy, reads the `Battery` global.
|
|
314
|
+
* In mock mode (Node), returns a static default (100%, not charging).
|
|
315
|
+
*
|
|
316
|
+
* Re-reads on each render; battery updates arrive via watch tick events
|
|
317
|
+
* which trigger redraws, so the value stays fresh.
|
|
318
|
+
*/
|
|
319
|
+
export function useBattery(): BatteryState {
|
|
320
|
+
if (typeof Battery !== 'undefined' && Battery) {
|
|
321
|
+
return {
|
|
322
|
+
percent: Battery.percent,
|
|
323
|
+
charging: Battery.charging,
|
|
324
|
+
plugged: Battery.plugged,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
// Mock mode — return a sensible default
|
|
328
|
+
return { percent: 100, charging: false, plugged: false };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Connection hook — reads watch.connected (app + pebblekit)
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
export interface ConnectionState {
|
|
336
|
+
app: boolean;
|
|
337
|
+
pebblekit: boolean;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Returns the current phone connection state. On Alloy, reads
|
|
342
|
+
* `watch.connected`. In mock mode, returns connected for both.
|
|
343
|
+
*/
|
|
344
|
+
export function useConnection(): ConnectionState {
|
|
345
|
+
if (typeof watch !== 'undefined' && watch?.connected) {
|
|
346
|
+
return {
|
|
347
|
+
app: watch.connected.app,
|
|
348
|
+
pebblekit: watch.connected.pebblekit,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
// Mock mode
|
|
352
|
+
return { app: true, pebblekit: true };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// localStorage hook — persists state across app restarts and reboots
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Like useState, but backed by localStorage so the value persists across
|
|
361
|
+
* app restarts and watch reboots.
|
|
362
|
+
*
|
|
363
|
+
* On Alloy, `localStorage` is a standard Web API global.
|
|
364
|
+
* In mock mode (Node), falls back to a plain in-memory useState.
|
|
365
|
+
*
|
|
366
|
+
* Values are JSON-serialized. Only use with JSON-safe types.
|
|
367
|
+
*/
|
|
368
|
+
export function useLocalStorage<T>(key: string, defaultValue: T): [T, (v: T | ((prev: T) => T)) => void] {
|
|
369
|
+
const [value, setValue] = useState<T>(() => {
|
|
370
|
+
if (typeof localStorage === 'undefined') return defaultValue;
|
|
371
|
+
try {
|
|
372
|
+
const stored = localStorage.getItem(key);
|
|
373
|
+
return stored !== null ? JSON.parse(stored) as T : defaultValue;
|
|
374
|
+
} catch {
|
|
375
|
+
return defaultValue;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const setAndPersist = useCallback((v: T | ((prev: T) => T)) => {
|
|
380
|
+
setValue((prev) => {
|
|
381
|
+
const next = typeof v === 'function' ? (v as (p: T) => T)(prev) : v;
|
|
382
|
+
if (typeof localStorage !== 'undefined') {
|
|
383
|
+
try {
|
|
384
|
+
localStorage.setItem(key, JSON.stringify(next));
|
|
385
|
+
} catch {
|
|
386
|
+
// Storage full or unavailable — silently ignore
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return next;
|
|
390
|
+
});
|
|
391
|
+
}, [key]);
|
|
392
|
+
|
|
393
|
+
return [value, setAndPersist];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// useFetch — HTTP data loading via pebbleproxy
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
export interface UseFetchOptions<T> {
|
|
401
|
+
/** Mock data returned in Node mock mode so the compiler can render. */
|
|
402
|
+
mockData?: T;
|
|
403
|
+
/** Delay in ms before mock data appears (default 100). */
|
|
404
|
+
mockDelay?: number;
|
|
405
|
+
/** fetch() RequestInit options (method, headers, body). */
|
|
406
|
+
init?: RequestInit;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export interface UseFetchResult<T> {
|
|
410
|
+
data: T | null;
|
|
411
|
+
loading: boolean;
|
|
412
|
+
error: string | null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Fetch JSON data from a URL.
|
|
417
|
+
*
|
|
418
|
+
* On Alloy: uses the standard `fetch()` API (proxied via @moddable/pebbleproxy).
|
|
419
|
+
* In mock mode (Node): returns `mockData` after `mockDelay` ms.
|
|
420
|
+
*
|
|
421
|
+
* Usage:
|
|
422
|
+
* const { data, loading, error } = useFetch<Weather>(
|
|
423
|
+
* 'https://api.example.com/weather',
|
|
424
|
+
* { mockData: { temp: 72, condition: 'Sunny' } }
|
|
425
|
+
* );
|
|
426
|
+
*/
|
|
427
|
+
export function useFetch<T>(url: string, options: UseFetchOptions<T> = {}): UseFetchResult<T> {
|
|
428
|
+
const [data, setData] = useState<T | null>(null);
|
|
429
|
+
const [loading, setLoading] = useState(true);
|
|
430
|
+
const [error, setError] = useState<string | null>(null);
|
|
431
|
+
|
|
432
|
+
useEffect(() => {
|
|
433
|
+
// On Alloy (real device): use fetch()
|
|
434
|
+
if (typeof globalThis.fetch === 'function') {
|
|
435
|
+
globalThis.fetch(url, options.init)
|
|
436
|
+
.then((res) => {
|
|
437
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
438
|
+
return res.json() as Promise<T>;
|
|
439
|
+
})
|
|
440
|
+
.then((json) => {
|
|
441
|
+
setData(json);
|
|
442
|
+
setLoading(false);
|
|
443
|
+
})
|
|
444
|
+
.catch((err) => {
|
|
445
|
+
setError(String(err));
|
|
446
|
+
setLoading(false);
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Mock mode: return mockData after delay
|
|
452
|
+
if (options.mockData !== undefined) {
|
|
453
|
+
const timer = setTimeout(() => {
|
|
454
|
+
setData(options.mockData!);
|
|
455
|
+
setLoading(false);
|
|
456
|
+
}, options.mockDelay ?? 100);
|
|
457
|
+
return () => clearTimeout(timer);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// No fetch and no mock data
|
|
461
|
+
setError('fetch() not available');
|
|
462
|
+
setLoading(false);
|
|
463
|
+
}, [url]);
|
|
464
|
+
|
|
465
|
+
return { data, loading, error };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// useAnimation — property interpolation with easing
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
/** Standard easing functions matching Alloy's Timeline API. */
|
|
473
|
+
export const Easing = {
|
|
474
|
+
linear: (t: number) => t,
|
|
475
|
+
quadEaseIn: (t: number) => t * t,
|
|
476
|
+
quadEaseOut: (t: number) => t * (2 - t),
|
|
477
|
+
quadEaseInOut: (t: number) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
|
|
478
|
+
cubicEaseIn: (t: number) => t * t * t,
|
|
479
|
+
cubicEaseOut: (t: number) => (--t) * t * t + 1,
|
|
480
|
+
cubicEaseInOut: (t: number) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
|
|
481
|
+
sinEaseIn: (t: number) => 1 - Math.cos(t * Math.PI / 2),
|
|
482
|
+
sinEaseOut: (t: number) => Math.sin(t * Math.PI / 2),
|
|
483
|
+
sinEaseInOut: (t: number) => -(Math.cos(Math.PI * t) - 1) / 2,
|
|
484
|
+
expoEaseIn: (t: number) => t === 0 ? 0 : Math.pow(2, 10 * (t - 1)),
|
|
485
|
+
expoEaseOut: (t: number) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
|
|
486
|
+
circEaseIn: (t: number) => 1 - Math.sqrt(1 - t * t),
|
|
487
|
+
circEaseOut: (t: number) => Math.sqrt(1 - (--t) * t),
|
|
488
|
+
bounceEaseOut: (t: number) => {
|
|
489
|
+
if (t < 1 / 2.75) return 7.5625 * t * t;
|
|
490
|
+
if (t < 2 / 2.75) return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75;
|
|
491
|
+
if (t < 2.5 / 2.75) return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375;
|
|
492
|
+
return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375;
|
|
493
|
+
},
|
|
494
|
+
bounceEaseIn: (t: number) => 1 - Easing.bounceEaseOut(1 - t),
|
|
495
|
+
elasticEaseOut: (t: number) => {
|
|
496
|
+
if (t === 0 || t === 1) return t;
|
|
497
|
+
return Math.pow(2, -10 * t) * Math.sin((t - 0.075) * (2 * Math.PI) / 0.3) + 1;
|
|
498
|
+
},
|
|
499
|
+
backEaseOut: (t: number) => {
|
|
500
|
+
const s = 1.70158;
|
|
501
|
+
return (--t) * t * ((s + 1) * t + s) + 1;
|
|
502
|
+
},
|
|
503
|
+
} as const;
|
|
504
|
+
|
|
505
|
+
export type EasingFn = (t: number) => number;
|
|
506
|
+
|
|
507
|
+
export interface UseAnimationOptions {
|
|
508
|
+
/** Duration in ms. */
|
|
509
|
+
duration: number;
|
|
510
|
+
/** Easing function (default: linear). */
|
|
511
|
+
easing?: EasingFn;
|
|
512
|
+
/** Delay before start in ms (default: 0). */
|
|
513
|
+
delay?: number;
|
|
514
|
+
/** Loop the animation (default: false). */
|
|
515
|
+
loop?: boolean;
|
|
516
|
+
/** Auto-start on mount (default: true). */
|
|
517
|
+
autoStart?: boolean;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export interface UseAnimationResult {
|
|
521
|
+
/** Current progress value (0 to 1), eased. */
|
|
522
|
+
progress: number;
|
|
523
|
+
/** Whether the animation is currently running. */
|
|
524
|
+
running: boolean;
|
|
525
|
+
/** Start or restart the animation. */
|
|
526
|
+
start: () => void;
|
|
527
|
+
/** Stop the animation. */
|
|
528
|
+
stop: () => void;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Animate a progress value from 0 to 1 over a duration with easing.
|
|
533
|
+
*
|
|
534
|
+
* Uses `useTime` internally so that animation progress is derived from
|
|
535
|
+
* the wall clock. This ensures compatibility with the piu compiler,
|
|
536
|
+
* which detects time-dependent values via T1/T2 render diffs.
|
|
537
|
+
*
|
|
538
|
+
* The animation cycles based on `duration` (in ms). If `loop` is true,
|
|
539
|
+
* it repeats indefinitely.
|
|
540
|
+
*
|
|
541
|
+
* Usage:
|
|
542
|
+
* const { progress } = useAnimation({ duration: 10000, easing: Easing.bounceEaseOut, loop: true });
|
|
543
|
+
* const x = lerp(0, 200, progress);
|
|
544
|
+
*/
|
|
545
|
+
export function useAnimation(options: UseAnimationOptions): UseAnimationResult {
|
|
546
|
+
const { duration, easing = Easing.linear, loop = false } = options;
|
|
547
|
+
// Use useTime for clock ticks — this makes the compiler detect time deps.
|
|
548
|
+
// Progress is derived purely from the current time (no stored start time),
|
|
549
|
+
// so the compiler can diff T1 vs T2 and detect changing values.
|
|
550
|
+
const time = useTime(1000);
|
|
551
|
+
|
|
552
|
+
// Derive progress from current time modulo duration.
|
|
553
|
+
// For a 60s duration, this cycles every 60s based on wall clock.
|
|
554
|
+
const totalSeconds = time.getMinutes() * 60 + time.getSeconds();
|
|
555
|
+
const durationSec = duration / 1000;
|
|
556
|
+
const raw = loop
|
|
557
|
+
? (totalSeconds % durationSec) / durationSec
|
|
558
|
+
: Math.min(totalSeconds / durationSec, 1);
|
|
559
|
+
const progress = easing(raw);
|
|
560
|
+
|
|
561
|
+
const start = useCallback(() => { /* no-op: auto-driven by time */ }, []);
|
|
562
|
+
const stop = useCallback(() => { /* no-op: auto-driven by time */ }, []);
|
|
563
|
+
|
|
564
|
+
return { progress, running: true, start, stop };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Interpolate between two values using an animation progress (0-1).
|
|
569
|
+
*/
|
|
570
|
+
export function lerp(from: number, to: number, progress: number): number {
|
|
571
|
+
return from + (to - from) * progress;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ---------------------------------------------------------------------------
|
|
575
|
+
// useAccelerometer — motion sensing via Moddable sensor API
|
|
576
|
+
// ---------------------------------------------------------------------------
|
|
577
|
+
|
|
578
|
+
export interface AccelerometerData {
|
|
579
|
+
x: number;
|
|
580
|
+
y: number;
|
|
581
|
+
z: number;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export interface UseAccelerometerOptions {
|
|
585
|
+
/** Sample rate in ms (default: 100). */
|
|
586
|
+
sampleRate?: number;
|
|
587
|
+
/** Called on tap gesture. */
|
|
588
|
+
onTap?: () => void;
|
|
589
|
+
/** Called on double-tap gesture. */
|
|
590
|
+
onDoubleTap?: () => void;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Read accelerometer data.
|
|
595
|
+
*
|
|
596
|
+
* On Alloy: reads from the Moddable Accelerometer sensor.
|
|
597
|
+
* In mock mode: returns { x: 0, y: 0, z: -1000 } (gravity pointing down).
|
|
598
|
+
*/
|
|
599
|
+
export function useAccelerometer(options: UseAccelerometerOptions = {}): AccelerometerData {
|
|
600
|
+
const { sampleRate = 100, onTap, onDoubleTap } = options;
|
|
601
|
+
const [data, setData] = useState<AccelerometerData>({ x: 0, y: 0, z: -1000 });
|
|
602
|
+
const tapRef = useRef(onTap);
|
|
603
|
+
const doubleTapRef = useRef(onDoubleTap);
|
|
604
|
+
tapRef.current = onTap;
|
|
605
|
+
doubleTapRef.current = onDoubleTap;
|
|
606
|
+
|
|
607
|
+
useEffect(() => {
|
|
608
|
+
// Try to access the Alloy accelerometer
|
|
609
|
+
if (typeof globalThis !== 'undefined' && (globalThis as Record<string, unknown>).__pbl_accel) {
|
|
610
|
+
const accel = (globalThis as Record<string, unknown>).__pbl_accel as {
|
|
611
|
+
onSample?: (x: number, y: number, z: number) => void;
|
|
612
|
+
onTap?: () => void;
|
|
613
|
+
onDoubleTap?: () => void;
|
|
614
|
+
start?: () => void;
|
|
615
|
+
stop?: () => void;
|
|
616
|
+
};
|
|
617
|
+
accel.onSample = (x: number, y: number, z: number) => setData({ x, y, z });
|
|
618
|
+
if (tapRef.current) accel.onTap = () => tapRef.current?.();
|
|
619
|
+
if (doubleTapRef.current) accel.onDoubleTap = () => doubleTapRef.current?.();
|
|
620
|
+
accel.start?.();
|
|
621
|
+
return () => accel.stop?.();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Mock mode: simulate gentle wobble
|
|
625
|
+
const id = setInterval(() => {
|
|
626
|
+
setData({
|
|
627
|
+
x: Math.round(Math.sin(Date.now() / 1000) * 50),
|
|
628
|
+
y: Math.round(Math.cos(Date.now() / 1200) * 30),
|
|
629
|
+
z: -1000 + Math.round(Math.sin(Date.now() / 800) * 20),
|
|
630
|
+
});
|
|
631
|
+
}, sampleRate);
|
|
632
|
+
return () => clearInterval(id);
|
|
633
|
+
}, [sampleRate]);
|
|
634
|
+
|
|
635
|
+
return data;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
// useCompass — magnetic heading via Moddable sensor API
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
|
|
642
|
+
export interface CompassData {
|
|
643
|
+
/** Heading in degrees (0-360, 0 = north). */
|
|
644
|
+
heading: number;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Read compass heading.
|
|
649
|
+
*
|
|
650
|
+
* On Alloy: reads from the Moddable Compass sensor.
|
|
651
|
+
* In mock mode: returns a slowly rotating heading.
|
|
652
|
+
*/
|
|
653
|
+
export function useCompass(): CompassData {
|
|
654
|
+
const [heading, setHeading] = useState(0);
|
|
655
|
+
|
|
656
|
+
useEffect(() => {
|
|
657
|
+
// Try to access the Alloy compass
|
|
658
|
+
if (typeof globalThis !== 'undefined' && (globalThis as Record<string, unknown>).__pbl_compass) {
|
|
659
|
+
const compass = (globalThis as Record<string, unknown>).__pbl_compass as {
|
|
660
|
+
onSample?: (heading: number) => void;
|
|
661
|
+
start?: () => void;
|
|
662
|
+
stop?: () => void;
|
|
663
|
+
};
|
|
664
|
+
compass.onSample = (h: number) => setHeading(h);
|
|
665
|
+
compass.start?.();
|
|
666
|
+
return () => compass.stop?.();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Mock mode: slowly rotate
|
|
670
|
+
const id = setInterval(() => {
|
|
671
|
+
setHeading((h) => (h + 1) % 360);
|
|
672
|
+
}, 100);
|
|
673
|
+
return () => clearInterval(id);
|
|
674
|
+
}, []);
|
|
675
|
+
|
|
676
|
+
return { heading };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
// useWebSocket — bidirectional communication via pebbleproxy
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
|
|
683
|
+
export interface UseWebSocketResult {
|
|
684
|
+
/** Last received message (null until first message). */
|
|
685
|
+
lastMessage: string | null;
|
|
686
|
+
/** Whether the connection is open. */
|
|
687
|
+
connected: boolean;
|
|
688
|
+
/** Send a message. */
|
|
689
|
+
send: (data: string) => void;
|
|
690
|
+
/** Close the connection. */
|
|
691
|
+
close: () => void;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Connect to a WebSocket server.
|
|
696
|
+
*
|
|
697
|
+
* On Alloy: uses the WebSocket API (proxied via @moddable/pebbleproxy).
|
|
698
|
+
* In mock mode: simulates a connection that echoes messages back.
|
|
699
|
+
*/
|
|
700
|
+
export function useWebSocket(url: string): UseWebSocketResult {
|
|
701
|
+
const [lastMessage, setLastMessage] = useState<string | null>(null);
|
|
702
|
+
const [connected, setConnected] = useState(false);
|
|
703
|
+
const wsRef = useRef<{ send: (d: string) => void; close: () => void } | null>(null);
|
|
704
|
+
|
|
705
|
+
useEffect(() => {
|
|
706
|
+
// On Alloy: use real WebSocket
|
|
707
|
+
if (typeof WebSocket !== 'undefined') {
|
|
708
|
+
const ws = new WebSocket(url);
|
|
709
|
+
ws.onopen = () => setConnected(true);
|
|
710
|
+
ws.onmessage = (e) => setLastMessage(String(e.data));
|
|
711
|
+
ws.onclose = () => setConnected(false);
|
|
712
|
+
ws.onerror = () => setConnected(false);
|
|
713
|
+
wsRef.current = { send: (d) => ws.send(d), close: () => ws.close() };
|
|
714
|
+
return () => ws.close();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Mock mode: echo server
|
|
718
|
+
setConnected(true);
|
|
719
|
+
wsRef.current = {
|
|
720
|
+
send: (d: string) => {
|
|
721
|
+
setTimeout(() => setLastMessage(`echo: ${d}`), 50);
|
|
722
|
+
},
|
|
723
|
+
close: () => setConnected(false),
|
|
724
|
+
};
|
|
725
|
+
return () => setConnected(false);
|
|
726
|
+
}, [url]);
|
|
727
|
+
|
|
728
|
+
const send = useCallback((data: string) => {
|
|
729
|
+
wsRef.current?.send(data);
|
|
730
|
+
}, []);
|
|
731
|
+
|
|
732
|
+
const close = useCallback(() => {
|
|
733
|
+
wsRef.current?.close();
|
|
734
|
+
}, []);
|
|
735
|
+
|
|
736
|
+
return { lastMessage, connected, send, close };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
// useKVStorage — ECMA-419 binary key-value storage
|
|
741
|
+
// ---------------------------------------------------------------------------
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Key-value storage for binary and structured data using the ECMA-419 API.
|
|
745
|
+
*
|
|
746
|
+
* On Alloy: uses `device.keyValue.open(storeName)` for persistent binary storage.
|
|
747
|
+
* In mock mode: uses an in-memory Map.
|
|
748
|
+
*
|
|
749
|
+
* For simple string storage, prefer `useLocalStorage`.
|
|
750
|
+
*/
|
|
751
|
+
export function useKVStorage(storeName: string): {
|
|
752
|
+
get: (key: string) => string | null;
|
|
753
|
+
set: (key: string, value: string) => void;
|
|
754
|
+
remove: (key: string) => void;
|
|
755
|
+
} {
|
|
756
|
+
const storeRef = useRef<{
|
|
757
|
+
get: (key: string) => string | null;
|
|
758
|
+
set: (key: string, value: string) => void;
|
|
759
|
+
delete: (key: string) => void;
|
|
760
|
+
} | null>(null);
|
|
761
|
+
|
|
762
|
+
if (!storeRef.current) {
|
|
763
|
+
// Try ECMA-419 device.keyValue API
|
|
764
|
+
const device = (globalThis as Record<string, unknown>).device as {
|
|
765
|
+
keyValue?: { open: (path: string) => {
|
|
766
|
+
get: (key: string) => string | null;
|
|
767
|
+
set: (key: string, value: string) => void;
|
|
768
|
+
delete: (key: string) => void;
|
|
769
|
+
}};
|
|
770
|
+
} | undefined;
|
|
771
|
+
|
|
772
|
+
if (device?.keyValue) {
|
|
773
|
+
storeRef.current = device.keyValue.open(storeName);
|
|
774
|
+
} else {
|
|
775
|
+
// Mock mode: in-memory map
|
|
776
|
+
const map = new Map<string, string>();
|
|
777
|
+
storeRef.current = {
|
|
778
|
+
get: (k) => map.get(k) ?? null,
|
|
779
|
+
set: (k, v) => map.set(k, v),
|
|
780
|
+
delete: (k) => { map.delete(k); },
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const store = storeRef.current!;
|
|
786
|
+
return {
|
|
787
|
+
get: useCallback((key: string) => store.get(key), []),
|
|
788
|
+
set: useCallback((key: string, value: string) => store.set(key, value), []),
|
|
789
|
+
remove: useCallback((key: string) => store.delete(key), []),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ---------------------------------------------------------------------------
|
|
794
|
+
// FUTURE HOOKS
|
|
296
795
|
//
|
|
297
|
-
// -
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
// - useConnection — previously Pebble.connection.isConnected().
|
|
301
|
-
// Alloy equivalent: `watch.connected` property was
|
|
302
|
-
// observed on the watch prototype but its shape is
|
|
303
|
-
// unknown.
|
|
304
|
-
// - useAccelerometer — previously Pebble.accel. Alloy has Moddable
|
|
305
|
-
// sensor modules; specific import path unknown.
|
|
306
|
-
// - useAppMessage — previously Pebble.sendAppMessage / addEventListener
|
|
307
|
-
// for 'appmessage'. Alloy has no direct equivalent;
|
|
308
|
-
// phone↔watch messaging goes through PebbleKit JS on
|
|
309
|
-
// the phone side (`src/pkjs/index.js`) and is a
|
|
310
|
-
// separate concern.
|
|
796
|
+
// - useAppMessage — phone↔watch messaging goes through PebbleKit JS
|
|
797
|
+
// on the phone side (`src/pkjs/index.js`).
|
|
798
|
+
// - useLocation — GPS via phone proxy (one-shot).
|
|
311
799
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,8 @@ export {
|
|
|
45
45
|
Line,
|
|
46
46
|
Image,
|
|
47
47
|
Group,
|
|
48
|
+
Column,
|
|
49
|
+
Row,
|
|
48
50
|
StatusBar,
|
|
49
51
|
ActionBar,
|
|
50
52
|
Card,
|
|
@@ -58,6 +60,8 @@ export type {
|
|
|
58
60
|
LineProps,
|
|
59
61
|
ImageProps,
|
|
60
62
|
GroupProps,
|
|
63
|
+
ColumnProps,
|
|
64
|
+
RowProps,
|
|
61
65
|
StatusBarProps,
|
|
62
66
|
ActionBarProps,
|
|
63
67
|
CardProps,
|
|
@@ -79,6 +83,17 @@ export {
|
|
|
79
83
|
useFormattedTime,
|
|
80
84
|
useInterval,
|
|
81
85
|
useListNavigation,
|
|
86
|
+
useBattery,
|
|
87
|
+
useConnection,
|
|
88
|
+
useLocalStorage,
|
|
89
|
+
useFetch,
|
|
90
|
+
useAnimation,
|
|
91
|
+
useAccelerometer,
|
|
92
|
+
useCompass,
|
|
93
|
+
useWebSocket,
|
|
94
|
+
useKVStorage,
|
|
95
|
+
Easing,
|
|
96
|
+
lerp,
|
|
82
97
|
ButtonRegistry,
|
|
83
98
|
PebbleAppContext,
|
|
84
99
|
} from './hooks/index.js';
|
|
@@ -87,6 +102,17 @@ export type {
|
|
|
87
102
|
PebbleButtonHandler,
|
|
88
103
|
ListNavigationOptions,
|
|
89
104
|
ListNavigationResult,
|
|
105
|
+
BatteryState,
|
|
106
|
+
ConnectionState,
|
|
107
|
+
UseFetchOptions,
|
|
108
|
+
UseFetchResult,
|
|
109
|
+
UseAnimationOptions,
|
|
110
|
+
UseAnimationResult,
|
|
111
|
+
EasingFn,
|
|
112
|
+
AccelerometerData,
|
|
113
|
+
UseAccelerometerOptions,
|
|
114
|
+
CompassData,
|
|
115
|
+
UseWebSocketResult,
|
|
90
116
|
} from './hooks/index.js';
|
|
91
117
|
|
|
92
118
|
// Low-level access (for advanced usage / custom renderers / tests)
|