@ccheever/exact-renderer 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 (80) hide show
  1. package/package.json +118 -0
  2. package/src/__tests__/adapter-window-state.test.tsx +190 -0
  3. package/src/__tests__/attrs.test.ts +157 -0
  4. package/src/__tests__/classname.test.ts +332 -0
  5. package/src/__tests__/color.test.ts +169 -0
  6. package/src/__tests__/dom-mirror.test.ts +682 -0
  7. package/src/__tests__/dom-shim.test.ts +274 -0
  8. package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
  9. package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
  10. package/src/__tests__/host-config.test.ts +51 -0
  11. package/src/__tests__/host-ops.test.ts +2234 -0
  12. package/src/__tests__/image-source.test.ts +135 -0
  13. package/src/__tests__/liquid-glass.test.ts +72 -0
  14. package/src/__tests__/multi-root.test.ts +118 -0
  15. package/src/__tests__/native-view-events.test.ts +102 -0
  16. package/src/__tests__/nodes.test.ts +399 -0
  17. package/src/__tests__/normalize.test.ts +576 -0
  18. package/src/__tests__/paragraph-lowering.test.tsx +144 -0
  19. package/src/__tests__/props.test.ts +518 -0
  20. package/src/__tests__/protocol-encoder.test.ts +732 -0
  21. package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
  22. package/src/__tests__/reconciler.test.tsx +241 -0
  23. package/src/__tests__/svelte-adapter.test.ts +166 -0
  24. package/src/__tests__/svg-source.test.ts +71 -0
  25. package/src/__tests__/tags.test.ts +354 -0
  26. package/src/__tests__/toggle.test.ts +441 -0
  27. package/src/__tests__/transitions.test.ts +106 -0
  28. package/src/__tests__/web-primitives.test.tsx +454 -0
  29. package/src/__tests__/window-hooks.test.tsx +447 -0
  30. package/src/adapter-contract.ts +68 -0
  31. package/src/attrs.ts +596 -0
  32. package/src/classname-contract.ts +87 -0
  33. package/src/classname-resolve.ts +553 -0
  34. package/src/classname-runtime.ts +29 -0
  35. package/src/components.ts +214 -0
  36. package/src/css-variable-context.ts +83 -0
  37. package/src/dom-hydration.ts +160 -0
  38. package/src/dom-mirror.ts +1459 -0
  39. package/src/dom-shim.ts +1736 -0
  40. package/src/group-context.ts +69 -0
  41. package/src/host-config.ts +431 -0
  42. package/src/host-ops.ts +3167 -0
  43. package/src/image-source.native.ts +703 -0
  44. package/src/image-source.ts +554 -0
  45. package/src/index.ts +278 -0
  46. package/src/inspector-runtime.ts +244 -0
  47. package/src/inspector.ts +3570 -0
  48. package/src/jsx-augmentations.ts +54 -0
  49. package/src/keyboard-avoidance.ts +217 -0
  50. package/src/native-primitives.ts +43 -0
  51. package/src/native-view-events.ts +322 -0
  52. package/src/native-view.ts +60 -0
  53. package/src/nodes/index.ts +41 -0
  54. package/src/nodes/node.ts +531 -0
  55. package/src/peer-context.ts +100 -0
  56. package/src/primitives.native.ts +8 -0
  57. package/src/primitives.ts +8 -0
  58. package/src/props/index.ts +14 -0
  59. package/src/props/normalize.ts +816 -0
  60. package/src/protocol/encoder.ts +940 -0
  61. package/src/protocol/index.ts +33 -0
  62. package/src/reconciler.ts +581 -0
  63. package/src/runtime.ts +11 -0
  64. package/src/safe-area.ts +543 -0
  65. package/src/solid.ts +490 -0
  66. package/src/style/color.js +1 -0
  67. package/src/style/color.ts +15 -0
  68. package/src/style/index.js +1 -0
  69. package/src/style/index.ts +22 -0
  70. package/src/style/normalize.js +1 -0
  71. package/src/style/normalize.ts +1426 -0
  72. package/src/svelte.ts +349 -0
  73. package/src/svg-source.ts +222 -0
  74. package/src/tags/index.ts +21 -0
  75. package/src/tags/tag-map.ts +289 -0
  76. package/src/text/paragraph-lowering.ts +310 -0
  77. package/src/types.ts +1175 -0
  78. package/src/vue.ts +535 -0
  79. package/src/web-host.ts +19 -0
  80. package/src/web-primitives.ts +1654 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Host Node Model - Public Exports
3
+ *
4
+ * This module re-exports the host node types and factory functions.
5
+ */
6
+
7
+ export {
8
+ // Types
9
+ NodeKind,
10
+ DirtyFlags,
11
+ type BaseNode,
12
+ type RootNode,
13
+ type ElementNode,
14
+ type TextNode,
15
+ type HostNode,
16
+
17
+ // Factory functions
18
+ createRootNode,
19
+ createElementNode,
20
+ createTextNode,
21
+
22
+ // Relationship helpers
23
+ setParent,
24
+ appendChild,
25
+ insertBefore,
26
+ removeChild,
27
+ clearChildren,
28
+ getChildIds,
29
+
30
+ // Dirty flag helpers
31
+ markStyleDirty,
32
+ markPropsDirty,
33
+ markEventsDirty,
34
+ clearDirtyFlags,
35
+ isDirty,
36
+ hasDirtyFlag,
37
+
38
+ // Internal (for testing)
39
+ _resetNodeIdCounter,
40
+ _peekNextNodeId,
41
+ } from './node.js';
@@ -0,0 +1,531 @@
1
+ /**
2
+ * Base Node Interface
3
+ *
4
+ * This module defines the core node interface used by the reconciler
5
+ * to represent elements in the host tree. All node types implement
6
+ * this interface.
7
+ *
8
+ * Design Principles:
9
+ * - Immutable IDs for stable identity
10
+ * - Clear parent-child relationships
11
+ * - Explicit dirty flags for efficient updates
12
+ * - Separation between structural and content mutations
13
+ */
14
+
15
+ import type { EventType } from '@exact/core/protocol/opcodes';
16
+ import type { ParagraphSpec } from '@exact/text';
17
+ import type {
18
+ CanonicalTagType,
19
+ CanonicalStyle,
20
+ CanonicalProps,
21
+ CanonicalTransitionMap,
22
+ EventBinding,
23
+ ResolvedDirection,
24
+ ResolvedSafeAreaState,
25
+ SafeAreaPropagationState,
26
+ WritingMode,
27
+ } from '../types.js';
28
+
29
+ /**
30
+ * Node type discriminator for type-safe branching.
31
+ */
32
+ export const NodeKind = {
33
+ Root: 0,
34
+ Element: 1,
35
+ Text: 2,
36
+ } as const;
37
+
38
+ export type NodeKind = (typeof NodeKind)[keyof typeof NodeKind];
39
+
40
+ /**
41
+ * Dirty flags for tracking what needs to be synchronized.
42
+ * These are bit flags that can be combined.
43
+ */
44
+ export const DirtyFlags = {
45
+ None: 0,
46
+ /** Style properties have changed */
47
+ Style: 1 << 0,
48
+ /** Props have changed (text content, image source, etc.) */
49
+ Props: 1 << 1,
50
+ /** Children have changed (added, removed, reordered) */
51
+ Children: 1 << 2,
52
+ /** Events have changed (handlers added/removed) */
53
+ Events: 1 << 3,
54
+ /** Node is newly created and needs full initialization */
55
+ Created: 1 << 4,
56
+ /** All flags set - full sync needed */
57
+ All: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4),
58
+ } as const;
59
+
60
+ export type DirtyFlags = (typeof DirtyFlags)[keyof typeof DirtyFlags];
61
+
62
+ /**
63
+ * Base interface for all host nodes.
64
+ *
65
+ * The node hierarchy is:
66
+ * - RootNode: The container node (ID 0)
67
+ * - ElementNode: View, Text, Image, etc.
68
+ * - TextNode: Raw text content (children of Text elements)
69
+ */
70
+ export interface BaseNode {
71
+ /**
72
+ * Discriminator for type-safe branching.
73
+ */
74
+ readonly kind: NodeKind;
75
+
76
+ /**
77
+ * Unique numeric ID for this node.
78
+ * IDs are assigned at creation and never change.
79
+ * ID 0 is reserved for the root container.
80
+ */
81
+ readonly id: number;
82
+
83
+ /**
84
+ * Parent node, or null for the root.
85
+ */
86
+ parent: BaseNode | null;
87
+
88
+ /**
89
+ * Dirty flags indicating what needs to be synchronized.
90
+ */
91
+ dirtyFlags: DirtyFlags;
92
+ }
93
+
94
+ /**
95
+ * Root node - the container for the entire tree.
96
+ * For single-root apps, id=0. For multi-root, each root gets a unique id.
97
+ */
98
+ export interface RootNode extends BaseNode {
99
+ readonly kind: typeof NodeKind.Root;
100
+ readonly id: number;
101
+ parent: null;
102
+
103
+ /**
104
+ * The root ID for multi-root support (0 = default single root).
105
+ */
106
+ readonly rootId: number;
107
+
108
+ /**
109
+ * Children of the root node.
110
+ */
111
+ children: ElementNode[];
112
+ }
113
+
114
+ /**
115
+ * Element node - represents a view, text, image, etc.
116
+ */
117
+ export interface ElementNode extends BaseNode {
118
+ readonly kind: typeof NodeKind.Element;
119
+
120
+ /**
121
+ * The canonical tag type for this element.
122
+ */
123
+ readonly tagType: CanonicalTagType;
124
+
125
+ /**
126
+ * The original tag name as specified in JSX.
127
+ * Used for debugging and dev tools.
128
+ */
129
+ readonly originalTag: string;
130
+
131
+ /**
132
+ * Whether this element uses RN semantics (column default, etc.)
133
+ */
134
+ readonly isRNStyle: boolean;
135
+
136
+ /**
137
+ * Current normalized style.
138
+ */
139
+ style: CanonicalStyle;
140
+
141
+ /**
142
+ * Resolved inherited direction for this element.
143
+ */
144
+ resolvedDirection: ResolvedDirection;
145
+
146
+ /**
147
+ * Resolved inherited writing mode for this element.
148
+ */
149
+ resolvedWritingMode: WritingMode;
150
+
151
+ /**
152
+ * Resolved inherited language tag for this element.
153
+ */
154
+ resolvedLang: string;
155
+
156
+ /**
157
+ * Current normalized props.
158
+ */
159
+ props: CanonicalProps;
160
+
161
+ /**
162
+ * Canonical paragraph lowering for text-like elements.
163
+ * Non-text elements keep this null.
164
+ */
165
+ paragraphSpec: ParagraphSpec | null;
166
+
167
+ /**
168
+ * Stable cache key for the lowered paragraph.
169
+ * Non-text elements keep this null.
170
+ */
171
+ paragraphKey: string | null;
172
+
173
+ /**
174
+ * Current normalized transition metadata.
175
+ */
176
+ transitions: CanonicalTransitionMap;
177
+
178
+ /**
179
+ * Safe-area work derived by the renderer. This stays local to JS so
180
+ * diagnostics and auto-scroll logic can inspect the resolved behavior.
181
+ */
182
+ safeAreaState: ResolvedSafeAreaState | null;
183
+
184
+ /**
185
+ * Safe-area context that descendants inherit from this node after any
186
+ * consumption performed here.
187
+ */
188
+ propagatedSafeArea: SafeAreaPropagationState | null;
189
+
190
+ /**
191
+ * Child element nodes.
192
+ */
193
+ children: (ElementNode | TextNode)[];
194
+
195
+ /**
196
+ * Event bindings for this element.
197
+ */
198
+ events: Map<EventType, EventBinding>;
199
+
200
+ /**
201
+ * Per-node escape hatch for adapters that need press semantics on a tag that
202
+ * is normally not press-capable. RFC 0043 uses this for DOM-shim click
203
+ * auto-promotion without mutating the global tag map.
204
+ */
205
+ pressOverride?: boolean;
206
+
207
+ /**
208
+ * Original props as passed from React (for diffing).
209
+ */
210
+ originalProps: Record<string, unknown>;
211
+ }
212
+
213
+ /**
214
+ * Text node - raw text content inside a Text element.
215
+ */
216
+ export interface TextNode extends BaseNode {
217
+ readonly kind: typeof NodeKind.Text;
218
+
219
+ /**
220
+ * The text content.
221
+ */
222
+ text: string;
223
+ }
224
+
225
+ /**
226
+ * Union type for all node types.
227
+ */
228
+ export type HostNode = RootNode | ElementNode | TextNode;
229
+
230
+ // =============================================================================
231
+ // Node Factory Functions
232
+ // =============================================================================
233
+
234
+ /**
235
+ * Global node ID counter. Starts at 1 because 0 is reserved for root.
236
+ */
237
+ let nextNodeId = 1;
238
+
239
+ /**
240
+ * Reset the node ID counter. Only for testing.
241
+ * @internal
242
+ */
243
+ export function _resetNodeIdCounter(): void {
244
+ nextNodeId = 1;
245
+ }
246
+
247
+ /**
248
+ * Get the next node ID without incrementing.
249
+ * @internal
250
+ */
251
+ export function _peekNextNodeId(): number {
252
+ return nextNodeId;
253
+ }
254
+
255
+ /**
256
+ * Create a new root node.
257
+ *
258
+ * @param rootId - Root ID for multi-root support (default 0 = single root).
259
+ * When rootId=0, the node ID is 0 (backwards compatible).
260
+ * When rootId>0, a new unique node ID is allocated.
261
+ */
262
+ export function createRootNode(rootId: number = 0): RootNode {
263
+ const id = rootId === 0 ? 0 : nextNodeId++;
264
+ return {
265
+ kind: NodeKind.Root,
266
+ id,
267
+ rootId,
268
+ parent: null,
269
+ children: [],
270
+ dirtyFlags: DirtyFlags.Created,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Create a new element node.
276
+ *
277
+ * @param tagType - The canonical tag type
278
+ * @param originalTag - The original tag name from JSX
279
+ * @param isRNStyle - Whether to use RN semantics
280
+ * @param props - The original props from React
281
+ */
282
+ export function createElementNode(
283
+ tagType: CanonicalTagType,
284
+ originalTag: string,
285
+ isRNStyle: boolean,
286
+ props: Record<string, unknown>
287
+ ): ElementNode {
288
+ const id = nextNodeId++;
289
+ return {
290
+ kind: NodeKind.Element,
291
+ id,
292
+ tagType,
293
+ originalTag,
294
+ isRNStyle,
295
+ parent: null,
296
+ style: {},
297
+ resolvedDirection: 'ltr',
298
+ resolvedWritingMode: 'horizontal-tb',
299
+ resolvedLang: '',
300
+ props: {},
301
+ paragraphSpec: null,
302
+ paragraphKey: null,
303
+ transitions: {},
304
+ safeAreaState: null,
305
+ propagatedSafeArea: null,
306
+ children: [],
307
+ events: new Map(),
308
+ pressOverride: undefined,
309
+ originalProps: props,
310
+ dirtyFlags: DirtyFlags.Created,
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Create a new text node.
316
+ *
317
+ * @param text - The text content
318
+ */
319
+ export function createTextNode(text: string): TextNode {
320
+ const id = nextNodeId++;
321
+ return {
322
+ kind: NodeKind.Text,
323
+ id,
324
+ parent: null,
325
+ text,
326
+ dirtyFlags: DirtyFlags.Created,
327
+ };
328
+ }
329
+
330
+ // =============================================================================
331
+ // Node Relationship Helpers
332
+ // =============================================================================
333
+
334
+ /**
335
+ * Set a node's parent and mark children as dirty on the parent.
336
+ */
337
+ export function setParent(node: ElementNode | TextNode, parent: BaseNode | null): void {
338
+ node.parent = parent;
339
+ }
340
+
341
+ /**
342
+ * Child lookups use plain Array#indexOf below this size (allocation-free).
343
+ * Above it, a per-parent index map is built lazily so appending n fresh
344
+ * children is O(n) instead of O(n^2).
345
+ */
346
+ const CHILD_INDEX_CACHE_THRESHOLD = 64;
347
+
348
+ /**
349
+ * Lazily built child -> index caches for large parents. Kept out of the node
350
+ * shape (WeakMap keyed by parent) so node objects stay plain data and the
351
+ * cache disappears with the parent. Any mid-array mutation invalidates the
352
+ * parent's cache; appends of new children extend it in place.
353
+ */
354
+ const childIndexCaches = new WeakMap<RootNode | ElementNode, Map<ElementNode | TextNode, number>>();
355
+
356
+ function indexOfChild(parent: RootNode | ElementNode, child: ElementNode | TextNode): number {
357
+ const children = parent.children;
358
+ if (children.length < CHILD_INDEX_CACHE_THRESHOLD) {
359
+ return children.indexOf(child as any);
360
+ }
361
+
362
+ let cache = childIndexCaches.get(parent);
363
+ if (!cache) {
364
+ cache = new Map();
365
+ for (let index = 0; index < children.length; index++) {
366
+ cache.set(children[index]!, index);
367
+ }
368
+ childIndexCaches.set(parent, cache);
369
+ }
370
+ return cache.get(child) ?? -1;
371
+ }
372
+
373
+ function invalidateChildIndexCache(parent: RootNode | ElementNode): void {
374
+ childIndexCaches.delete(parent);
375
+ }
376
+
377
+ function extendChildIndexCache(parent: RootNode | ElementNode, child: ElementNode | TextNode): void {
378
+ const cache = childIndexCaches.get(parent);
379
+ if (cache) {
380
+ cache.set(child, parent.children.length - 1);
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Add a child to a parent node.
386
+ * Returns true if the child was added or moved, false if it was already last.
387
+ */
388
+ export function appendChild(
389
+ parent: RootNode | ElementNode,
390
+ child: ElementNode | TextNode
391
+ ): boolean {
392
+ const existingIndex = indexOfChild(parent, child);
393
+ if (existingIndex !== -1) {
394
+ if (existingIndex === parent.children.length - 1) {
395
+ return false;
396
+ }
397
+ parent.children.splice(existingIndex, 1);
398
+ parent.children.push(child as any);
399
+ invalidateChildIndexCache(parent);
400
+ setParent(child, parent);
401
+ parent.dirtyFlags |= DirtyFlags.Children;
402
+ return true;
403
+ }
404
+
405
+ // Add the child
406
+ parent.children.push(child as any);
407
+ extendChildIndexCache(parent, child);
408
+ setParent(child, parent);
409
+ parent.dirtyFlags |= DirtyFlags.Children;
410
+
411
+ return true;
412
+ }
413
+
414
+ /**
415
+ * Insert a child before another child.
416
+ * Returns true if inserted, false if before child not found.
417
+ */
418
+ export function insertBefore(
419
+ parent: RootNode | ElementNode,
420
+ child: ElementNode | TextNode,
421
+ beforeChild: ElementNode | TextNode
422
+ ): boolean {
423
+ const beforeIndex = indexOfChild(parent, beforeChild);
424
+ if (beforeIndex === -1) {
425
+ // Before child not found, append instead
426
+ return appendChild(parent, child);
427
+ }
428
+
429
+ // Remove if already present elsewhere
430
+ const existingIndex = indexOfChild(parent, child);
431
+ if (existingIndex !== -1) {
432
+ parent.children.splice(existingIndex, 1);
433
+ }
434
+
435
+ // Insert before
436
+ const adjustedIndex = existingIndex !== -1 && existingIndex < beforeIndex
437
+ ? beforeIndex - 1
438
+ : beforeIndex;
439
+ parent.children.splice(adjustedIndex, 0, child as any);
440
+ invalidateChildIndexCache(parent);
441
+ setParent(child, parent);
442
+ parent.dirtyFlags |= DirtyFlags.Children;
443
+
444
+ return true;
445
+ }
446
+
447
+ /**
448
+ * Remove a child from a parent node.
449
+ * Returns true if removed, false if not found.
450
+ */
451
+ export function removeChild(
452
+ parent: RootNode | ElementNode,
453
+ child: ElementNode | TextNode
454
+ ): boolean {
455
+ const index = indexOfChild(parent, child);
456
+ if (index === -1) {
457
+ return false;
458
+ }
459
+
460
+ parent.children.splice(index, 1);
461
+ invalidateChildIndexCache(parent);
462
+ setParent(child, null);
463
+ parent.dirtyFlags |= DirtyFlags.Children;
464
+
465
+ return true;
466
+ }
467
+
468
+ /**
469
+ * Clear all children from a node.
470
+ */
471
+ export function clearChildren(parent: RootNode | ElementNode): void {
472
+ for (const child of parent.children) {
473
+ setParent(child, null);
474
+ }
475
+ parent.children = [];
476
+ invalidateChildIndexCache(parent);
477
+ parent.dirtyFlags |= DirtyFlags.Children;
478
+ }
479
+
480
+ /**
481
+ * Get the child IDs for a node.
482
+ */
483
+ export function getChildIds(parent: RootNode | ElementNode): number[] {
484
+ return parent.children.map(child => child.id);
485
+ }
486
+
487
+ // =============================================================================
488
+ // Node Dirty Flag Helpers
489
+ // =============================================================================
490
+
491
+ /**
492
+ * Mark a node as having dirty style.
493
+ */
494
+ export function markStyleDirty(node: ElementNode): void {
495
+ node.dirtyFlags |= DirtyFlags.Style;
496
+ }
497
+
498
+ /**
499
+ * Mark a node as having dirty props.
500
+ */
501
+ export function markPropsDirty(node: ElementNode | TextNode): void {
502
+ node.dirtyFlags |= DirtyFlags.Props;
503
+ }
504
+
505
+ /**
506
+ * Mark a node as having dirty events.
507
+ */
508
+ export function markEventsDirty(node: ElementNode): void {
509
+ node.dirtyFlags |= DirtyFlags.Events;
510
+ }
511
+
512
+ /**
513
+ * Clear all dirty flags on a node.
514
+ */
515
+ export function clearDirtyFlags(node: BaseNode): void {
516
+ node.dirtyFlags = DirtyFlags.None;
517
+ }
518
+
519
+ /**
520
+ * Check if a node has any dirty flags.
521
+ */
522
+ export function isDirty(node: BaseNode): boolean {
523
+ return node.dirtyFlags !== DirtyFlags.None;
524
+ }
525
+
526
+ /**
527
+ * Check if a node has a specific dirty flag.
528
+ */
529
+ export function hasDirtyFlag(node: BaseNode, flag: DirtyFlags): boolean {
530
+ return (node.dirtyFlags & flag) !== 0;
531
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Peer state store for Tailwind peer-* modifiers (RFC 0024 Phase 4).
3
+ *
4
+ * A view with `className="peer"` writes its interaction state to a ref-based
5
+ * sibling store. Subsequent sibling views with `peer-hover:*` classes subscribe
6
+ * to this store via useSyncExternalStore.
7
+ *
8
+ * Named peers (`peer/email`) get separate store entries so multiple peers in
9
+ * the same parent don't conflict.
10
+ */
11
+ import { useCallback, useRef, useSyncExternalStore } from 'react';
12
+ import type { InteractionState } from './classname-contract.js';
13
+
14
+ const DEFAULT_INTERACTION: InteractionState = Object.freeze({
15
+ hovered: false,
16
+ focused: false,
17
+ active: false,
18
+ focusVisible: false,
19
+ });
20
+
21
+ export interface PeerStore {
22
+ getState(name: string | null): InteractionState | undefined;
23
+ setState(name: string | null, state: InteractionState): void;
24
+ subscribe(callback: () => void): () => void;
25
+ }
26
+
27
+ /**
28
+ * Create a PeerStore. Typically held as a ref on the parent component.
29
+ */
30
+ export function createPeerStore(): PeerStore {
31
+ const states = new Map<string | null, InteractionState>();
32
+ const listeners = new Set<() => void>();
33
+
34
+ return {
35
+ getState(name) {
36
+ return states.get(name);
37
+ },
38
+ setState(name, state) {
39
+ const current = states.get(name);
40
+ if (
41
+ current &&
42
+ current.hovered === state.hovered &&
43
+ current.focused === state.focused &&
44
+ current.active === state.active &&
45
+ current.focusVisible === state.focusVisible
46
+ ) {
47
+ return;
48
+ }
49
+ states.set(name, state);
50
+ for (const listener of listeners) {
51
+ listener();
52
+ }
53
+ },
54
+ subscribe(callback) {
55
+ listeners.add(callback);
56
+ return () => { listeners.delete(callback); };
57
+ },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Hook to create and maintain a PeerStore for a parent component.
63
+ */
64
+ export function usePeerStore(): PeerStore {
65
+ const ref = useRef<PeerStore | null>(null);
66
+ if (!ref.current) {
67
+ ref.current = createPeerStore();
68
+ }
69
+ return ref.current;
70
+ }
71
+
72
+ /**
73
+ * Hook for a subsequent sibling to subscribe to peer state changes.
74
+ */
75
+ export function usePeerState(store: PeerStore, name: string | null): InteractionState {
76
+ const subscribe = useCallback(
77
+ (callback: () => void) => store.subscribe(callback),
78
+ [store],
79
+ );
80
+ const getSnapshot = useCallback(
81
+ () => store.getState(name) ?? DEFAULT_INTERACTION,
82
+ [store, name],
83
+ );
84
+ return useSyncExternalStore(subscribe, getSnapshot);
85
+ }
86
+
87
+ /**
88
+ * Check if a className string includes "peer" or "peer/{name}".
89
+ * Returns the peer name (null for unnamed) or undefined if not a peer.
90
+ */
91
+ const WS_RE = /\s+/;
92
+
93
+ export function extractPeerName(className: string): string | null | undefined {
94
+ const tokens = className.split(WS_RE);
95
+ for (const token of tokens) {
96
+ if (token === 'peer') return null;
97
+ if (token.startsWith('peer/')) return token.slice(5);
98
+ }
99
+ return undefined;
100
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ Pressable,
3
+ List,
4
+ ScrollView,
5
+ SelectionGroup,
6
+ Text,
7
+ View,
8
+ } from './native-primitives.js';
@@ -0,0 +1,8 @@
1
+ export {
2
+ Pressable,
3
+ List,
4
+ ScrollView,
5
+ SelectionGroup,
6
+ Text,
7
+ View,
8
+ } from './components.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Prop Normalization - Public Exports
3
+ *
4
+ * This module re-exports the prop normalization utilities.
5
+ */
6
+
7
+ export {
8
+ extractTextContent,
9
+ extractWebImageSource,
10
+ extractRNImageSource,
11
+ extractResizeMode,
12
+ normalizeProps,
13
+ propsEqual,
14
+ } from './normalize.js';