react-pebble 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/dist/lib/compiler.cjs +3 -0
- package/dist/lib/compiler.cjs.map +1 -0
- package/dist/lib/compiler.js +54 -0
- package/dist/lib/compiler.js.map +1 -0
- package/dist/lib/components.cjs +2 -0
- package/dist/lib/components.cjs.map +1 -0
- package/dist/lib/components.js +80 -0
- package/dist/lib/components.js.map +1 -0
- package/dist/lib/hooks.cjs +2 -0
- package/dist/lib/hooks.cjs.map +1 -0
- package/dist/lib/hooks.js +99 -0
- package/dist/lib/hooks.js.map +1 -0
- package/dist/lib/index.cjs +2 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.js +585 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/platform.cjs +2 -0
- package/dist/lib/platform.cjs.map +1 -0
- package/dist/lib/platform.js +52 -0
- package/dist/lib/platform.js.map +1 -0
- package/dist/lib/plugin.cjs +60 -0
- package/dist/lib/plugin.cjs.map +1 -0
- package/dist/lib/plugin.js +102 -0
- package/dist/lib/plugin.js.map +1 -0
- package/dist/lib/src/compiler/index.d.ts +40 -0
- package/dist/lib/src/components/index.d.ts +129 -0
- package/dist/lib/src/hooks/index.d.ts +75 -0
- package/dist/lib/src/index.d.ts +36 -0
- package/dist/lib/src/pebble-dom-shim.d.ts +45 -0
- package/dist/lib/src/pebble-dom.d.ts +59 -0
- package/dist/lib/src/pebble-output.d.ts +44 -0
- package/dist/lib/src/pebble-reconciler.d.ts +16 -0
- package/dist/lib/src/pebble-render.d.ts +31 -0
- package/dist/lib/src/platform.d.ts +30 -0
- package/dist/lib/src/plugin/index.d.ts +20 -0
- package/package.json +90 -0
- package/scripts/compile-to-piu.ts +1794 -0
- package/scripts/deploy.sh +46 -0
- package/src/compiler/index.ts +114 -0
- package/src/components/index.tsx +280 -0
- package/src/hooks/index.ts +311 -0
- package/src/index.ts +126 -0
- package/src/pebble-dom-shim.ts +266 -0
- package/src/pebble-dom.ts +190 -0
- package/src/pebble-output.ts +310 -0
- package/src/pebble-reconciler.ts +54 -0
- package/src/pebble-render.ts +311 -0
- package/src/platform.ts +50 -0
- package/src/plugin/index.ts +274 -0
- package/src/types/moddable.d.ts +156 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hooks/index.ts — React hooks for react-pebble.
|
|
3
|
+
*
|
|
4
|
+
* This set intentionally covers only what the Alloy runtime can support
|
|
5
|
+
* today. Sensor / connectivity hooks (battery, BT connection, accelerometer,
|
|
6
|
+
* phone↔watch messaging) used to live here based on a fictional `Pebble`
|
|
7
|
+
* global; they've been removed until we've identified the real Moddable
|
|
8
|
+
* module shape for each one.
|
|
9
|
+
*
|
|
10
|
+
* See `pebble-render.ts` for the runtime wiring that lets `useTime` and
|
|
11
|
+
* `useButton` actually fire on-device — hooks publish to registries that the
|
|
12
|
+
* renderer connects to Moddable's `watch` event source.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createContext } from 'preact';
|
|
16
|
+
import {
|
|
17
|
+
useCallback,
|
|
18
|
+
useContext,
|
|
19
|
+
useEffect,
|
|
20
|
+
useRef,
|
|
21
|
+
useState as _preactUseState,
|
|
22
|
+
} from 'preact/hooks';
|
|
23
|
+
|
|
24
|
+
// Wrap useState so the compiler can swap the implementation at compile time.
|
|
25
|
+
// Direct ESM re-exports are sealed (getter-only), so we use a mutable
|
|
26
|
+
// internal reference that the compiler patches via _setUseStateImpl().
|
|
27
|
+
type UseStateFn = <T>(init: T | (() => T)) => [T, (v: T | ((p: T) => T)) => void];
|
|
28
|
+
let _useStateImpl: UseStateFn = _preactUseState;
|
|
29
|
+
|
|
30
|
+
export function useState<T>(init: T | (() => T)): [T, (v: T | ((p: T) => T)) => void] {
|
|
31
|
+
return _useStateImpl(init);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @internal — called by the compiler to intercept useState calls. */
|
|
35
|
+
export function _setUseStateImpl(impl: UseStateFn): void {
|
|
36
|
+
_useStateImpl = impl;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @internal — restore original useState after compilation. */
|
|
40
|
+
export function _restoreUseState(): void {
|
|
41
|
+
_useStateImpl = _preactUseState;
|
|
42
|
+
}
|
|
43
|
+
import type { PebbleApp } from '../pebble-render.js';
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Button types
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export type PebbleButton = 'up' | 'down' | 'select' | 'back';
|
|
50
|
+
export type PebbleButtonHandler = () => void;
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// App context — provides access to the render app from nested components.
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
export const PebbleAppContext = createContext<PebbleApp | null>(null);
|
|
57
|
+
|
|
58
|
+
export function useApp(): PebbleApp {
|
|
59
|
+
const app = useContext(PebbleAppContext);
|
|
60
|
+
if (!app) {
|
|
61
|
+
throw new Error('useApp must be used inside a react-pebble render tree');
|
|
62
|
+
}
|
|
63
|
+
return app;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Button registry
|
|
68
|
+
//
|
|
69
|
+
// Buttons are delivered to react-pebble two ways:
|
|
70
|
+
// (a) via props on elements (onUp/onDown/onSelect/onBack), collected and
|
|
71
|
+
// subscribed to Moddable's `watch` in pebble-render.ts; or
|
|
72
|
+
// (b) via this hook registry, which the renderer pumps from `watch` events.
|
|
73
|
+
//
|
|
74
|
+
// The registry is the substrate for both `useButton` and `useLongButton`.
|
|
75
|
+
// The exact `watch` event names for buttons are not yet confirmed — the
|
|
76
|
+
// renderer normalizes them into our four logical buttons before emitting.
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
type ButtonRegistryKey = PebbleButton | `long_${PebbleButton}`;
|
|
80
|
+
|
|
81
|
+
interface ButtonRegistryShape {
|
|
82
|
+
_listeners: Map<ButtonRegistryKey, Set<PebbleButtonHandler>>;
|
|
83
|
+
subscribe(button: ButtonRegistryKey, fn: PebbleButtonHandler): void;
|
|
84
|
+
unsubscribe(button: ButtonRegistryKey, fn: PebbleButtonHandler): void;
|
|
85
|
+
emit(button: ButtonRegistryKey): void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const ButtonRegistry: ButtonRegistryShape = {
|
|
89
|
+
_listeners: new Map<ButtonRegistryKey, Set<PebbleButtonHandler>>(),
|
|
90
|
+
|
|
91
|
+
subscribe(button, fn) {
|
|
92
|
+
let set = this._listeners.get(button);
|
|
93
|
+
if (!set) {
|
|
94
|
+
set = new Set();
|
|
95
|
+
this._listeners.set(button, set);
|
|
96
|
+
}
|
|
97
|
+
set.add(fn);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
unsubscribe(button, fn) {
|
|
101
|
+
const set = this._listeners.get(button);
|
|
102
|
+
if (set) {
|
|
103
|
+
set.delete(fn);
|
|
104
|
+
if (set.size === 0) this._listeners.delete(button);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
emit(button) {
|
|
109
|
+
const set = this._listeners.get(button);
|
|
110
|
+
if (set) {
|
|
111
|
+
for (const fn of set) fn();
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export function useButton(button: PebbleButton, handler: PebbleButtonHandler): void {
|
|
117
|
+
const handlerRef = useRef(handler);
|
|
118
|
+
handlerRef.current = handler;
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const listener: PebbleButtonHandler = () => handlerRef.current();
|
|
122
|
+
ButtonRegistry.subscribe(button, listener);
|
|
123
|
+
return () => {
|
|
124
|
+
ButtonRegistry.unsubscribe(button, listener);
|
|
125
|
+
};
|
|
126
|
+
}, [button]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function useLongButton(button: PebbleButton, handler: PebbleButtonHandler): void {
|
|
130
|
+
const handlerRef = useRef(handler);
|
|
131
|
+
handlerRef.current = handler;
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const listener: PebbleButtonHandler = () => handlerRef.current();
|
|
135
|
+
const key: ButtonRegistryKey = `long_${button}`;
|
|
136
|
+
ButtonRegistry.subscribe(key, listener);
|
|
137
|
+
return () => ButtonRegistry.unsubscribe(key, listener);
|
|
138
|
+
}, [button]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Time hooks
|
|
143
|
+
//
|
|
144
|
+
// On Alloy, `watch.addEventListener('secondchange'|'minutechange', fn)` is
|
|
145
|
+
// the canonical tick source. In Node mock mode we fall back to setInterval.
|
|
146
|
+
// Both paths are abstracted away by the renderer, which pumps the tick into
|
|
147
|
+
// the hook via React state — here we just use setInterval directly, which
|
|
148
|
+
// works in both XS and Node. (`watch` is used for redraws, not hook state.)
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
export function useTime(intervalMs = 1000): Date {
|
|
152
|
+
const [time, setTime] = useState<Date>(() => new Date());
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const tick = () => setTime(new Date());
|
|
156
|
+
const id = setInterval(tick, intervalMs);
|
|
157
|
+
return () => clearInterval(id);
|
|
158
|
+
}, [intervalMs]);
|
|
159
|
+
|
|
160
|
+
return time;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function useFormattedTime(format = 'HH:mm'): string {
|
|
164
|
+
const time = useTime(format.includes('ss') ? 1000 : 60000);
|
|
165
|
+
|
|
166
|
+
const hours24 = time.getHours();
|
|
167
|
+
const hours12 = hours24 % 12 || 12;
|
|
168
|
+
const minutes = time.getMinutes().toString().padStart(2, '0');
|
|
169
|
+
const seconds = time.getSeconds().toString().padStart(2, '0');
|
|
170
|
+
const ampm = hours24 < 12 ? 'AM' : 'PM';
|
|
171
|
+
|
|
172
|
+
let result = format;
|
|
173
|
+
result = result.replace('HH', hours24.toString().padStart(2, '0'));
|
|
174
|
+
result = result.replace('hh', hours12.toString().padStart(2, '0'));
|
|
175
|
+
result = result.replace('mm', minutes);
|
|
176
|
+
result = result.replace('ss', seconds);
|
|
177
|
+
result = result.replace('a', ampm);
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// useInterval
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
export function useInterval(callback: () => void, delay: number | null): void {
|
|
187
|
+
const savedCallback = useRef(callback);
|
|
188
|
+
savedCallback.current = callback;
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (delay === null) return;
|
|
192
|
+
|
|
193
|
+
const id = setInterval(() => savedCallback.current(), delay);
|
|
194
|
+
return () => clearInterval(id);
|
|
195
|
+
}, [delay]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// List navigation
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
export interface ListNavigationOptions {
|
|
203
|
+
wrap?: boolean;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ListNavigationResult<T> {
|
|
207
|
+
index: number;
|
|
208
|
+
item: T | undefined;
|
|
209
|
+
next: () => void;
|
|
210
|
+
prev: () => void;
|
|
211
|
+
setIndex: (index: number) => void;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function useListNavigation<T>(
|
|
215
|
+
items: readonly T[],
|
|
216
|
+
options: ListNavigationOptions = {},
|
|
217
|
+
): ListNavigationResult<T> {
|
|
218
|
+
const { wrap = false } = options;
|
|
219
|
+
const [index, setIndex] = useState(0);
|
|
220
|
+
|
|
221
|
+
const next = useCallback(() => {
|
|
222
|
+
setIndex((i) => {
|
|
223
|
+
if (i >= items.length - 1) return wrap ? 0 : i;
|
|
224
|
+
return i + 1;
|
|
225
|
+
});
|
|
226
|
+
}, [items.length, wrap]);
|
|
227
|
+
|
|
228
|
+
const prev = useCallback(() => {
|
|
229
|
+
setIndex((i) => {
|
|
230
|
+
if (i <= 0) return wrap ? items.length - 1 : i;
|
|
231
|
+
return i - 1;
|
|
232
|
+
});
|
|
233
|
+
}, [items.length, wrap]);
|
|
234
|
+
|
|
235
|
+
useButton('down', next);
|
|
236
|
+
useButton('up', prev);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
index,
|
|
240
|
+
item: items[index],
|
|
241
|
+
next,
|
|
242
|
+
prev,
|
|
243
|
+
setIndex,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// useMessage — runtime data loading via phone→watch messaging
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
export interface UseMessageOptions<T> {
|
|
252
|
+
/** Message key name (must match PebbleKit JS sendAppMessage key) */
|
|
253
|
+
key: string;
|
|
254
|
+
/** Mock data returned at compile time so the compiler can render the loaded state */
|
|
255
|
+
mockData: T;
|
|
256
|
+
/** Delay in ms before mock data appears (for SETTLE_MS) */
|
|
257
|
+
mockDelay?: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface UseMessageResult<T> {
|
|
261
|
+
data: T | null;
|
|
262
|
+
loading: boolean;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Load data from the phone at runtime via Pebble's Message API.
|
|
267
|
+
*
|
|
268
|
+
* At compile time (Node mock mode): returns mockData after mockDelay ms.
|
|
269
|
+
* At runtime (Alloy): the compiler emits a Message subscription that
|
|
270
|
+
* populates data when the phone sends it.
|
|
271
|
+
*
|
|
272
|
+
* Usage:
|
|
273
|
+
* const { data, loading } = useMessage({
|
|
274
|
+
* key: 'items',
|
|
275
|
+
* mockData: [{ title: 'Fix bug', status: 'Open' }],
|
|
276
|
+
* });
|
|
277
|
+
*/
|
|
278
|
+
export function useMessage<T>(options: UseMessageOptions<T>): UseMessageResult<T> {
|
|
279
|
+
const [data, setData] = useState<T | null>(null);
|
|
280
|
+
const [loading, setLoading] = useState(true);
|
|
281
|
+
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
// In mock mode (compile time), simulate async data arrival
|
|
284
|
+
const timer = setTimeout(() => {
|
|
285
|
+
setData(options.mockData);
|
|
286
|
+
setLoading(false);
|
|
287
|
+
}, options.mockDelay ?? 100);
|
|
288
|
+
return () => clearTimeout(timer);
|
|
289
|
+
}, []);
|
|
290
|
+
|
|
291
|
+
return { data, loading };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// REMOVED HOOKS — reference for future reimplementation
|
|
296
|
+
//
|
|
297
|
+
// - useBattery — previously read Pebble.battery (fictional global).
|
|
298
|
+
// Alloy equivalent: probably a Moddable power
|
|
299
|
+
// module; not yet explored.
|
|
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.
|
|
311
|
+
// ---------------------------------------------------------------------------
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* react-pebble — A React renderer for Pebble Alloy (Moddable XS / Poco).
|
|
3
|
+
*
|
|
4
|
+
* Usage on Alloy:
|
|
5
|
+
* import Poco from 'commodetto/Poco';
|
|
6
|
+
* import React from 'react';
|
|
7
|
+
* import { render, Group, Rect, Text } from 'react-pebble';
|
|
8
|
+
* import { useTime } from 'react-pebble/hooks';
|
|
9
|
+
*
|
|
10
|
+
* function WatchFace() {
|
|
11
|
+
* const time = useTime();
|
|
12
|
+
* return (
|
|
13
|
+
* <Group>
|
|
14
|
+
* <Rect x={0} y={0} w={200} h={228} fill="black" />
|
|
15
|
+
* <Text x={0} y={90} w={200} font="bitham42Bold" color="white" align="center">
|
|
16
|
+
* {time.getHours()}:{time.getMinutes().toString().padStart(2, '0')}
|
|
17
|
+
* </Text>
|
|
18
|
+
* </Group>
|
|
19
|
+
* );
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* render(<WatchFace />, { poco: Poco });
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Platform / screen dimensions
|
|
26
|
+
export { SCREEN, PLATFORMS, _setPlatform } from './platform.js';
|
|
27
|
+
export type { PebblePlatform } from './platform.js';
|
|
28
|
+
|
|
29
|
+
// Core render API
|
|
30
|
+
export { render } from './pebble-render.js';
|
|
31
|
+
export type {
|
|
32
|
+
PebbleApp,
|
|
33
|
+
PebblePlatformInfo,
|
|
34
|
+
RenderOptions,
|
|
35
|
+
RenderOptionsExt,
|
|
36
|
+
DrawCall,
|
|
37
|
+
} from './pebble-render.js';
|
|
38
|
+
|
|
39
|
+
// Component wrappers
|
|
40
|
+
export {
|
|
41
|
+
Window,
|
|
42
|
+
Rect,
|
|
43
|
+
Circle,
|
|
44
|
+
Text,
|
|
45
|
+
Line,
|
|
46
|
+
Image,
|
|
47
|
+
Group,
|
|
48
|
+
StatusBar,
|
|
49
|
+
ActionBar,
|
|
50
|
+
Card,
|
|
51
|
+
Badge,
|
|
52
|
+
} from './components/index.js';
|
|
53
|
+
export type {
|
|
54
|
+
WindowProps,
|
|
55
|
+
RectProps,
|
|
56
|
+
CircleProps,
|
|
57
|
+
TextProps,
|
|
58
|
+
LineProps,
|
|
59
|
+
ImageProps,
|
|
60
|
+
GroupProps,
|
|
61
|
+
StatusBarProps,
|
|
62
|
+
ActionBarProps,
|
|
63
|
+
CardProps,
|
|
64
|
+
BadgeProps,
|
|
65
|
+
PositionProps,
|
|
66
|
+
SizeProps,
|
|
67
|
+
ButtonHandlerProps,
|
|
68
|
+
ColorName,
|
|
69
|
+
FontName,
|
|
70
|
+
Alignment,
|
|
71
|
+
} from './components/index.js';
|
|
72
|
+
|
|
73
|
+
// Hooks
|
|
74
|
+
export {
|
|
75
|
+
useApp,
|
|
76
|
+
useButton,
|
|
77
|
+
useLongButton,
|
|
78
|
+
useTime,
|
|
79
|
+
useFormattedTime,
|
|
80
|
+
useInterval,
|
|
81
|
+
useListNavigation,
|
|
82
|
+
ButtonRegistry,
|
|
83
|
+
PebbleAppContext,
|
|
84
|
+
} from './hooks/index.js';
|
|
85
|
+
export type {
|
|
86
|
+
PebbleButton,
|
|
87
|
+
PebbleButtonHandler,
|
|
88
|
+
ListNavigationOptions,
|
|
89
|
+
ListNavigationResult,
|
|
90
|
+
} from './hooks/index.js';
|
|
91
|
+
|
|
92
|
+
// Low-level access (for advanced usage / custom renderers / tests)
|
|
93
|
+
export { default as reconciler } from './pebble-reconciler.js';
|
|
94
|
+
export {
|
|
95
|
+
ELEMENT_TYPES,
|
|
96
|
+
appendChildNode,
|
|
97
|
+
createNode,
|
|
98
|
+
createTextNode,
|
|
99
|
+
findRoot,
|
|
100
|
+
getTextContent,
|
|
101
|
+
insertBeforeNode,
|
|
102
|
+
removeChildNode,
|
|
103
|
+
setAttribute,
|
|
104
|
+
setTextNodeValue,
|
|
105
|
+
walkTree,
|
|
106
|
+
} from './pebble-dom.js';
|
|
107
|
+
export type {
|
|
108
|
+
AnyNode,
|
|
109
|
+
DOMElement,
|
|
110
|
+
ElementType,
|
|
111
|
+
NodeProps,
|
|
112
|
+
TextNode,
|
|
113
|
+
Visitor,
|
|
114
|
+
} from './pebble-dom.js';
|
|
115
|
+
export {
|
|
116
|
+
PocoRenderer,
|
|
117
|
+
COLOR_PALETTE,
|
|
118
|
+
FONT_PALETTE,
|
|
119
|
+
resolveColorName,
|
|
120
|
+
resolveFontName,
|
|
121
|
+
} from './pebble-output.js';
|
|
122
|
+
export type {
|
|
123
|
+
RGB,
|
|
124
|
+
FontSpec,
|
|
125
|
+
RenderOptions as PocoRenderOptions,
|
|
126
|
+
} from './pebble-output.js';
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pebble-dom-shim.ts — A minimal DOM-like adapter over `pebble-dom` so
|
|
3
|
+
* Preact's `render(vnode, parentDom)` can drive it.
|
|
4
|
+
*
|
|
5
|
+
* Preact's diff loop mutates a tree of DOM-shaped nodes by calling:
|
|
6
|
+
* - document.createElement(tag)
|
|
7
|
+
* - document.createTextNode(text)
|
|
8
|
+
* - parent.appendChild(child), .insertBefore(child, ref), .removeChild(child)
|
|
9
|
+
* - element.setAttribute(name, value), .removeAttribute(name)
|
|
10
|
+
* - element.addEventListener(name, handler), .removeEventListener(...)
|
|
11
|
+
* - element.nodeType, .nodeName, .parentNode, .childNodes, .firstChild,
|
|
12
|
+
* .nextSibling
|
|
13
|
+
*
|
|
14
|
+
* This file implements the minimum surface Preact actually touches when
|
|
15
|
+
* rendering into a headless tree. Each "element" carries a reference to the
|
|
16
|
+
* underlying pebble-dom node so the renderer can walk both views
|
|
17
|
+
* interchangeably.
|
|
18
|
+
*
|
|
19
|
+
* The shim is deliberately NOT a full undom — it only does what Preact 10
|
|
20
|
+
* needs. If we trip on a missing method, add it here rather than including
|
|
21
|
+
* undom (which would push the bundle back up).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { AnyNode, DOMElement, ElementType, TextNode } from './pebble-dom.js';
|
|
25
|
+
import {
|
|
26
|
+
ELEMENT_TYPES,
|
|
27
|
+
appendChildNode,
|
|
28
|
+
createNode,
|
|
29
|
+
createTextNode,
|
|
30
|
+
insertBeforeNode,
|
|
31
|
+
removeChildNode,
|
|
32
|
+
setAttribute,
|
|
33
|
+
setTextNodeValue,
|
|
34
|
+
} from './pebble-dom.js';
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// DOM node interfaces the shim exposes to Preact
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
interface ShimNodeBase {
|
|
41
|
+
readonly nodeType: number;
|
|
42
|
+
nodeName: string;
|
|
43
|
+
parentNode: ShimElement | null;
|
|
44
|
+
childNodes: ShimNode[];
|
|
45
|
+
firstChild: ShimNode | null;
|
|
46
|
+
nextSibling: ShimNode | null;
|
|
47
|
+
/** The backing pebble-dom node. */
|
|
48
|
+
readonly _pbl: AnyNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ShimElement extends ShimNodeBase {
|
|
52
|
+
readonly nodeType: 1;
|
|
53
|
+
readonly localName: string;
|
|
54
|
+
readonly tagName: string;
|
|
55
|
+
readonly _pbl: DOMElement;
|
|
56
|
+
attributes: Record<string, unknown>;
|
|
57
|
+
|
|
58
|
+
appendChild<T extends ShimNode>(child: T): T;
|
|
59
|
+
insertBefore<T extends ShimNode>(child: T, ref: ShimNode | null): T;
|
|
60
|
+
removeChild<T extends ShimNode>(child: T): T;
|
|
61
|
+
remove(): void;
|
|
62
|
+
|
|
63
|
+
setAttribute(name: string, value: unknown): void;
|
|
64
|
+
removeAttribute(name: string): void;
|
|
65
|
+
getAttribute(name: string): unknown;
|
|
66
|
+
|
|
67
|
+
addEventListener(name: string, handler: (...args: unknown[]) => unknown): void;
|
|
68
|
+
removeEventListener(name: string, handler: (...args: unknown[]) => unknown): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ShimText extends ShimNodeBase {
|
|
72
|
+
readonly nodeType: 3;
|
|
73
|
+
readonly _pbl: TextNode;
|
|
74
|
+
data: string;
|
|
75
|
+
nodeValue: string;
|
|
76
|
+
textContent: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ShimNode = ShimElement | ShimText;
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Shim factories
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
function linkSiblings(parent: ShimElement): void {
|
|
86
|
+
const kids = parent.childNodes;
|
|
87
|
+
parent.firstChild = kids[0] ?? null;
|
|
88
|
+
for (let i = 0; i < kids.length; i++) {
|
|
89
|
+
const node = kids[i]!;
|
|
90
|
+
node.parentNode = parent;
|
|
91
|
+
node.nextSibling = kids[i + 1] ?? null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createShimElement(tag: string): ShimElement {
|
|
96
|
+
// Accept both 'pbl-rect' and friendly aliases from components. Unknown
|
|
97
|
+
// tags are rejected loudly so typos surface at render time.
|
|
98
|
+
const pblType = tag as ElementType;
|
|
99
|
+
if (!ELEMENT_TYPES.has(pblType)) {
|
|
100
|
+
throw new Error(`react-pebble: unknown element tag "${tag}"`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const pbl = createNode(pblType);
|
|
104
|
+
|
|
105
|
+
const el: ShimElement = {
|
|
106
|
+
nodeType: 1,
|
|
107
|
+
nodeName: tag.toUpperCase(),
|
|
108
|
+
localName: tag,
|
|
109
|
+
tagName: tag.toUpperCase(),
|
|
110
|
+
_pbl: pbl,
|
|
111
|
+
attributes: {},
|
|
112
|
+
parentNode: null,
|
|
113
|
+
childNodes: [],
|
|
114
|
+
firstChild: null,
|
|
115
|
+
nextSibling: null,
|
|
116
|
+
|
|
117
|
+
appendChild(child) {
|
|
118
|
+
// Detach from any prior parent in the shim view
|
|
119
|
+
if (child.parentNode) {
|
|
120
|
+
child.parentNode.removeChild(child);
|
|
121
|
+
}
|
|
122
|
+
this.childNodes.push(child);
|
|
123
|
+
appendChildNode(pbl, child._pbl);
|
|
124
|
+
linkSiblings(this);
|
|
125
|
+
return child;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
insertBefore(child, ref) {
|
|
129
|
+
if (child.parentNode) {
|
|
130
|
+
child.parentNode.removeChild(child);
|
|
131
|
+
}
|
|
132
|
+
if (ref === null) {
|
|
133
|
+
return this.appendChild(child);
|
|
134
|
+
}
|
|
135
|
+
const idx = this.childNodes.indexOf(ref);
|
|
136
|
+
if (idx < 0) {
|
|
137
|
+
return this.appendChild(child);
|
|
138
|
+
}
|
|
139
|
+
this.childNodes.splice(idx, 0, child);
|
|
140
|
+
insertBeforeNode(pbl, child._pbl, ref._pbl);
|
|
141
|
+
linkSiblings(this);
|
|
142
|
+
return child;
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
removeChild(child) {
|
|
146
|
+
const idx = this.childNodes.indexOf(child);
|
|
147
|
+
if (idx >= 0) {
|
|
148
|
+
this.childNodes.splice(idx, 1);
|
|
149
|
+
}
|
|
150
|
+
removeChildNode(pbl, child._pbl);
|
|
151
|
+
child.parentNode = null;
|
|
152
|
+
child.nextSibling = null;
|
|
153
|
+
linkSiblings(this);
|
|
154
|
+
return child;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
remove() {
|
|
158
|
+
if (this.parentNode) {
|
|
159
|
+
this.parentNode.removeChild(this);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
setAttribute(name, value) {
|
|
164
|
+
this.attributes[name] = value;
|
|
165
|
+
setAttribute(pbl, name, value);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
removeAttribute(name) {
|
|
169
|
+
delete this.attributes[name];
|
|
170
|
+
setAttribute(pbl, name, undefined);
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
getAttribute(name) {
|
|
174
|
+
return this.attributes[name];
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
addEventListener(name, handler) {
|
|
178
|
+
// Event handlers are stored in props (on* style) so the button wiring
|
|
179
|
+
// in pebble-render.ts can find them the same way React props do.
|
|
180
|
+
const key = `on${name[0]?.toUpperCase()}${name.slice(1)}`;
|
|
181
|
+
this.attributes[key] = handler;
|
|
182
|
+
setAttribute(pbl, key, handler);
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
removeEventListener(name, _handler) {
|
|
186
|
+
const key = `on${name[0]?.toUpperCase()}${name.slice(1)}`;
|
|
187
|
+
delete this.attributes[key];
|
|
188
|
+
setAttribute(pbl, key, undefined);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return el;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function createShimText(data: string): ShimText {
|
|
196
|
+
const pbl = createTextNode(data);
|
|
197
|
+
|
|
198
|
+
const node: ShimText = {
|
|
199
|
+
nodeType: 3,
|
|
200
|
+
nodeName: '#text',
|
|
201
|
+
_pbl: pbl,
|
|
202
|
+
data,
|
|
203
|
+
nodeValue: data,
|
|
204
|
+
textContent: data,
|
|
205
|
+
parentNode: null,
|
|
206
|
+
childNodes: [],
|
|
207
|
+
firstChild: null,
|
|
208
|
+
nextSibling: null,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Keep pebble-dom in sync whenever data/nodeValue/textContent is assigned.
|
|
212
|
+
// Preact mutates `.data` directly on text updates.
|
|
213
|
+
Object.defineProperty(node, 'data', {
|
|
214
|
+
get() {
|
|
215
|
+
return pbl.value;
|
|
216
|
+
},
|
|
217
|
+
set(next: string) {
|
|
218
|
+
setTextNodeValue(pbl, next);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
Object.defineProperty(node, 'nodeValue', {
|
|
222
|
+
get() {
|
|
223
|
+
return pbl.value;
|
|
224
|
+
},
|
|
225
|
+
set(next: string) {
|
|
226
|
+
setTextNodeValue(pbl, next);
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
Object.defineProperty(node, 'textContent', {
|
|
230
|
+
get() {
|
|
231
|
+
return pbl.value;
|
|
232
|
+
},
|
|
233
|
+
set(next: string) {
|
|
234
|
+
setTextNodeValue(pbl, next);
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return node;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Shim "document" — what Preact reaches via `parentDom.ownerDocument` etc.
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
export interface ShimDocument {
|
|
246
|
+
createElement(tag: string): ShimElement;
|
|
247
|
+
createElementNS(ns: string | null, tag: string): ShimElement;
|
|
248
|
+
createTextNode(data: string): ShimText;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const shimDocument: ShimDocument = {
|
|
252
|
+
createElement: createShimElement,
|
|
253
|
+
createElementNS: (_ns, tag) => createShimElement(tag),
|
|
254
|
+
createTextNode: createShimText,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// Root container — the "parent DOM" handed to preact.render().
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
export function createShimRoot(): ShimElement {
|
|
262
|
+
const root = createShimElement('pbl-root');
|
|
263
|
+
// Preact reads `ownerDocument` off the parent.
|
|
264
|
+
(root as unknown as { ownerDocument: ShimDocument }).ownerDocument = shimDocument;
|
|
265
|
+
return root;
|
|
266
|
+
}
|