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.
Files changed (50) hide show
  1. package/dist/lib/compiler.cjs +3 -0
  2. package/dist/lib/compiler.cjs.map +1 -0
  3. package/dist/lib/compiler.js +54 -0
  4. package/dist/lib/compiler.js.map +1 -0
  5. package/dist/lib/components.cjs +2 -0
  6. package/dist/lib/components.cjs.map +1 -0
  7. package/dist/lib/components.js +80 -0
  8. package/dist/lib/components.js.map +1 -0
  9. package/dist/lib/hooks.cjs +2 -0
  10. package/dist/lib/hooks.cjs.map +1 -0
  11. package/dist/lib/hooks.js +99 -0
  12. package/dist/lib/hooks.js.map +1 -0
  13. package/dist/lib/index.cjs +2 -0
  14. package/dist/lib/index.cjs.map +1 -0
  15. package/dist/lib/index.js +585 -0
  16. package/dist/lib/index.js.map +1 -0
  17. package/dist/lib/platform.cjs +2 -0
  18. package/dist/lib/platform.cjs.map +1 -0
  19. package/dist/lib/platform.js +52 -0
  20. package/dist/lib/platform.js.map +1 -0
  21. package/dist/lib/plugin.cjs +60 -0
  22. package/dist/lib/plugin.cjs.map +1 -0
  23. package/dist/lib/plugin.js +102 -0
  24. package/dist/lib/plugin.js.map +1 -0
  25. package/dist/lib/src/compiler/index.d.ts +40 -0
  26. package/dist/lib/src/components/index.d.ts +129 -0
  27. package/dist/lib/src/hooks/index.d.ts +75 -0
  28. package/dist/lib/src/index.d.ts +36 -0
  29. package/dist/lib/src/pebble-dom-shim.d.ts +45 -0
  30. package/dist/lib/src/pebble-dom.d.ts +59 -0
  31. package/dist/lib/src/pebble-output.d.ts +44 -0
  32. package/dist/lib/src/pebble-reconciler.d.ts +16 -0
  33. package/dist/lib/src/pebble-render.d.ts +31 -0
  34. package/dist/lib/src/platform.d.ts +30 -0
  35. package/dist/lib/src/plugin/index.d.ts +20 -0
  36. package/package.json +90 -0
  37. package/scripts/compile-to-piu.ts +1794 -0
  38. package/scripts/deploy.sh +46 -0
  39. package/src/compiler/index.ts +114 -0
  40. package/src/components/index.tsx +280 -0
  41. package/src/hooks/index.ts +311 -0
  42. package/src/index.ts +126 -0
  43. package/src/pebble-dom-shim.ts +266 -0
  44. package/src/pebble-dom.ts +190 -0
  45. package/src/pebble-output.ts +310 -0
  46. package/src/pebble-reconciler.ts +54 -0
  47. package/src/pebble-render.ts +311 -0
  48. package/src/platform.ts +50 -0
  49. package/src/plugin/index.ts +274 -0
  50. 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
+ }