@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,214 @@
1
+ /**
2
+ * Exact Primitive Components
3
+ *
4
+ * This module exports the primitive components for building Exact UIs.
5
+ * Components are organized into two categories:
6
+ *
7
+ * 1. Web-style tags (lowercase): div, span, text, img, input, button
8
+ * - Use CSS flexbox defaults (flexDirection: 'row')
9
+ * - Use web-style props (src, alt, objectFit)
10
+ *
11
+ * 2. React Native-style components (PascalCase): View, Text, Image, ScrollView, Pressable
12
+ * - Use RN defaults (flexDirection: 'column')
13
+ * - Use RN-style props (source, resizeMode)
14
+ *
15
+ * Both styles render to the same underlying views. Choose based on your
16
+ * preferred API style and which defaults make sense for your UI.
17
+ *
18
+ * @example Web-style
19
+ * ```tsx
20
+ * <div style={{ flex: 1, backgroundColor: '#fff' }}>
21
+ * <text style={{ fontSize: 16 }}>Hello</text>
22
+ * <img src="https://example.com/image.png" style={{ width: 100, height: 100 }} />
23
+ * </div>
24
+ * ```
25
+ *
26
+ * @example React Native-style
27
+ * ```tsx
28
+ * <View style={{ flex: 1, backgroundColor: '#fff' }}>
29
+ * <Text style={{ fontSize: 16 }}>Hello</Text>
30
+ * <Image source={{ uri: 'https://example.com/image.png' }} style={{ width: 100, height: 100 }} />
31
+ * </View>
32
+ * ```
33
+ */
34
+
35
+ import { createElement } from 'react';
36
+
37
+ import type {
38
+ ContainerProps,
39
+ TextElementProps,
40
+ ImageElementProps,
41
+ PressableElementProps,
42
+ TextInputProps,
43
+ ToggleProps,
44
+ } from './types.js';
45
+ import {
46
+ button as platformButton,
47
+ div as platformDiv,
48
+ img as platformImg,
49
+ input as platformInput,
50
+ span as platformSpan,
51
+ text as platformText,
52
+ View as platformView,
53
+ } from './web-primitives.js';
54
+
55
+ // =============================================================================
56
+ // Web-Style Tags
57
+ // =============================================================================
58
+
59
+ /**
60
+ * Generic container element.
61
+ *
62
+ * Default flex direction: 'row' (CSS default)
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * <div style={{ flex: 1, flexDirection: 'column' }}>
67
+ * <div style={{ height: 100, backgroundColor: '#f00' }} />
68
+ * <div style={{ height: 100, backgroundColor: '#0f0' }} />
69
+ * </div>
70
+ * ```
71
+ */
72
+ /**
73
+ * Lowercase `div` now uses the same platform-aware bridge as the other
74
+ * primitives. On React DOM it renders a standard `<div>` with row defaults;
75
+ * on the custom reconciler path it still emits the raw `div` tag.
76
+ */
77
+ export const div = platformDiv;
78
+
79
+ /**
80
+ * Inline container element.
81
+ *
82
+ * Alias for div. Default flex direction: 'row' (CSS default)
83
+ *
84
+ * @example
85
+ * ```tsx
86
+ * <span style={{ padding: 8, backgroundColor: '#eee' }}>
87
+ * <text>Hello</text>
88
+ * </span>
89
+ * ```
90
+ */
91
+ /**
92
+ * Lowercase `span` follows the same bridge pattern as `div`, but preserves
93
+ * inline semantics on the React DOM path.
94
+ */
95
+ export const span = platformSpan;
96
+
97
+ /**
98
+ * Text content element.
99
+ *
100
+ * Use this to display text. Text content should be placed as children.
101
+ *
102
+ * @example
103
+ * ```tsx
104
+ * <text style={{ fontSize: 16, color: '#333' }}>
105
+ * Hello, world!
106
+ * </text>
107
+ * ```
108
+ */
109
+ export const text = platformText;
110
+
111
+ /**
112
+ * Image element (web-style).
113
+ *
114
+ * Uses web-style props: src, alt, objectFit.
115
+ *
116
+ * @example
117
+ * ```tsx
118
+ * <img
119
+ * src="https://example.com/image.png"
120
+ * alt="Description"
121
+ * style={{ width: 200, height: 200 }}
122
+ * objectFit="cover"
123
+ * />
124
+ * ```
125
+ */
126
+ /**
127
+ * Lowercase `img` is now platform-aware too. This keeps Facet's web-style
128
+ * image usage working on React DOM without giving up the custom-reconciler tag.
129
+ */
130
+ export const img = platformImg;
131
+
132
+ /**
133
+ * Text input element (web-style).
134
+ *
135
+ * @example
136
+ * ```tsx
137
+ * <input
138
+ * style={{ height: 40, borderWidth: 1, borderColor: '#ccc' }}
139
+ * placeholder="Enter text..."
140
+ * value={text}
141
+ * onChangeText={setText}
142
+ * />
143
+ * ```
144
+ */
145
+ /**
146
+ * Lowercase `input` now maps to the web text-input wrapper on React DOM so
147
+ * `onChangeText`, `multiline`, and the rest of Exact's input contract still
148
+ * work when apps render outside the custom reconciler.
149
+ */
150
+ export const input = platformInput;
151
+
152
+ /**
153
+ * Button element (pressable).
154
+ *
155
+ * A pressable container that responds to touch events.
156
+ * Default flex direction: 'row' (CSS default)
157
+ *
158
+ * @example
159
+ * ```tsx
160
+ * <button
161
+ * style={{ padding: 12, backgroundColor: '#007AFF', borderRadius: 8 }}
162
+ * onPress={() => console.log('Pressed!')}
163
+ * >
164
+ * <text style={{ color: '#fff' }}>Click me</text>
165
+ * </button>
166
+ * ```
167
+ */
168
+ export const button = platformButton;
169
+
170
+ /**
171
+ * Toggle/Switch element (web-style).
172
+ *
173
+ * A native iOS toggle with optional liquid glass effect.
174
+ *
175
+ * @example
176
+ * ```tsx
177
+ * <toggle
178
+ * value={isOn}
179
+ * onValueChange={setIsOn}
180
+ * glassEffect
181
+ * tintColor="#34d399"
182
+ * />
183
+ * ```
184
+ */
185
+ export const toggle = 'toggle' as unknown as React.FC<ToggleProps>;
186
+
187
+ // =============================================================================
188
+ // React Native-Style Components
189
+ //
190
+ // These are platform-aware: on web they render standard HTML elements with
191
+ // React Native-like defaults (flex column, onPress → onClick, etc.); on
192
+ // native they emit string tags for the Exact custom reconciler.
193
+ // =============================================================================
194
+
195
+ export {
196
+ View,
197
+ Text,
198
+ Image,
199
+ Svg,
200
+ List,
201
+ ScrollView,
202
+ TextInput,
203
+ Pressable,
204
+ Toggle,
205
+ Switch,
206
+ } from './web-primitives.js';
207
+
208
+ export const SelectionGroup: React.FC<ContainerProps> = function SelectionGroup(props) {
209
+ return createElement(platformView, {
210
+ ...props,
211
+ selectable: props.selectable ?? 'contain',
212
+ __exactComponentName: props.__exactComponentName ?? 'SelectionGroup',
213
+ });
214
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * CSS Custom Property context for native className resolution (RFC 0024 Phase 3).
3
+ *
4
+ * On native, CSS custom properties set via `style={{ '--brand': '#ff6b35' }}`
5
+ * are extracted from the style prop and provided to descendants via React context.
6
+ * The className resolver reads this context to resolve `var()` references at
7
+ * runtime (Layer 2).
8
+ *
9
+ * On web, `--`-prefixed keys are forwarded to the DOM `style` attribute and the
10
+ * browser handles `var()` resolution natively. This module is not used on web.
11
+ */
12
+ import { createContext, createElement, useContext, useMemo, type ReactNode } from 'react';
13
+
14
+ export type CSSVariableMap = Readonly<Record<string, string>>;
15
+
16
+ const EMPTY_MAP: CSSVariableMap = Object.freeze({});
17
+
18
+ const CSSVariableContext = createContext<CSSVariableMap>(EMPTY_MAP);
19
+
20
+ /**
21
+ * Read the inherited CSS variable map from context. Used by the className
22
+ * resolver to resolve runtime `var()` references.
23
+ */
24
+ export function useCSSVariables(): CSSVariableMap {
25
+ return useContext(CSSVariableContext);
26
+ }
27
+
28
+ /**
29
+ * Provide scoped CSS custom properties to descendants.
30
+ *
31
+ * When `declaredVars` is empty or undefined, the provider is a passthrough
32
+ * (returns the inherited context unchanged — zero cost via useMemo identity).
33
+ * This allows unconditional rendering without unmount/remount concerns.
34
+ */
35
+ export function CSSVariableProvider({
36
+ declaredVars,
37
+ children,
38
+ }: {
39
+ declaredVars: CSSVariableMap | undefined;
40
+ children: ReactNode;
41
+ }): ReactNode {
42
+ const inherited = useContext(CSSVariableContext);
43
+ const merged = useMemo(() => {
44
+ if (!declaredVars || Object.keys(declaredVars).length === 0) {
45
+ return inherited;
46
+ }
47
+ return { ...inherited, ...declaredVars };
48
+ }, [inherited, declaredVars]);
49
+
50
+ return createElement(CSSVariableContext.Provider, { value: merged }, children);
51
+ }
52
+
53
+ /**
54
+ * Extract `--`-prefixed keys from a style object, returning the extracted
55
+ * variables and the remaining style without them.
56
+ */
57
+ export function extractCSSVariables(
58
+ style: Record<string, unknown> | undefined,
59
+ ): { cssVars: CSSVariableMap | undefined; cleanStyle: Record<string, unknown> | undefined } {
60
+ if (!style) {
61
+ return { cssVars: undefined, cleanStyle: undefined };
62
+ }
63
+
64
+ let cssVars: Record<string, string> | undefined;
65
+ let cleanStyle: Record<string, unknown> | undefined;
66
+
67
+ for (const key of Object.keys(style)) {
68
+ if (key.startsWith('--')) {
69
+ if (!cssVars) cssVars = {};
70
+ cssVars[key] = String(style[key]);
71
+ } else {
72
+ if (!cleanStyle) cleanStyle = {};
73
+ cleanStyle[key] = style[key];
74
+ }
75
+ }
76
+
77
+ // If no -- keys found, return original style object to preserve identity
78
+ if (!cssVars) {
79
+ return { cssVars: undefined, cleanStyle: style };
80
+ }
81
+
82
+ return { cssVars, cleanStyle };
83
+ }
@@ -0,0 +1,160 @@
1
+ // @system @ref LLP 0201 W3 / LLP 0210 — Contract web host hydration adoption.
2
+
3
+ import type {
4
+ DomHydrationAdopter,
5
+ DomHydrationAdopterFactoryOptions,
6
+ DomHydrationManifestLike,
7
+ DomMirrorHostChild,
8
+ } from './dom-mirror.js';
9
+ import type { ElementNode, TextNode } from './nodes/node.js';
10
+ import { NodeKind } from './nodes/node.js';
11
+
12
+ type HydrationNode = NonNullable<DomHydrationManifestLike['nodes']>[number];
13
+
14
+ function buildHydrationAnchorMap(
15
+ hydrate: DomHydrationAdopterFactoryOptions['hydrate'],
16
+ ): Map<string, HydrationNode> {
17
+ const out = new Map<string, HydrationNode>();
18
+ for (const node of hydrate.manifest.nodes ?? []) {
19
+ if (typeof node.nodeId === 'string' && node.nodeId.length > 0) {
20
+ out.set(node.nodeId, node);
21
+ }
22
+ }
23
+ return out;
24
+ }
25
+
26
+ function resolveDomPath(root: ParentNode, path: readonly number[]): Node | null {
27
+ let current: ParentNode | Node = root;
28
+ for (const index of path) {
29
+ if (!Number.isInteger(index) || index < 0 || !('childNodes' in current)) {
30
+ return null;
31
+ }
32
+ const next = current.childNodes.item(index);
33
+ if (!next) {
34
+ return null;
35
+ }
36
+ current = next;
37
+ }
38
+ return current as Node;
39
+ }
40
+
41
+ function hydrationIdFor(child: DomMirrorHostChild): string | undefined {
42
+ if (child.kind === NodeKind.Text) {
43
+ return (child as TextNode & { __exactHydrationId?: string }).__exactHydrationId;
44
+ }
45
+ const value = ((child as ElementNode).originalProps ?? {})['data-exact-hydration-id'];
46
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
47
+ }
48
+
49
+ function createDomHydrationAdopter(
50
+ options: DomHydrationAdopterFactoryOptions,
51
+ ): DomHydrationAdopter | null {
52
+ const hydrationAnchors = buildHydrationAnchorMap(options.hydrate);
53
+ const hydrationResults = new Map<number, {
54
+ adoptedNodes: number;
55
+ recreatedNodes: number;
56
+ mismatches: string[];
57
+ rootId: number;
58
+ }>();
59
+ const reportedHydrationRoots = new Set<number>();
60
+
61
+ function resultFor(rootId: number) {
62
+ let result = hydrationResults.get(rootId);
63
+ if (!result) {
64
+ result = { rootId, adoptedNodes: 0, recreatedNodes: 0, mismatches: [] };
65
+ hydrationResults.set(rootId, result);
66
+ }
67
+ return result;
68
+ }
69
+
70
+ return {
71
+ adoptRootContainer(rootId, base, rootAttribute) {
72
+ if (options.hydrate.rootId !== rootId) {
73
+ return null;
74
+ }
75
+ if (base.getAttribute(rootAttribute) === String(rootId)) {
76
+ options.markMirrored(base);
77
+ return base;
78
+ }
79
+ const existing = base.querySelector(`[${rootAttribute}="${rootId}"]`);
80
+ if (existing && existing.nodeType === 1) {
81
+ options.markMirrored(existing);
82
+ return existing as HTMLElement;
83
+ }
84
+ return null;
85
+ },
86
+
87
+ adoptDom(child, rootId, rootContainer, desiredTag) {
88
+ if (options.hydrate.rootId !== rootId) {
89
+ return null;
90
+ }
91
+ const hydrationId = hydrationIdFor(child);
92
+ if (!hydrationId) {
93
+ return null;
94
+ }
95
+ const anchor = hydrationAnchors.get(hydrationId);
96
+ if (!anchor?.domPath) {
97
+ resultFor(rootId).mismatches.push(`${hydrationId}: missing hydration anchor`);
98
+ resultFor(rootId).recreatedNodes += 1;
99
+ return null;
100
+ }
101
+ const candidate = resolveDomPath(rootContainer, anchor.domPath);
102
+ if (!candidate) {
103
+ resultFor(rootId).mismatches.push(
104
+ `${hydrationId}: domPath ${anchor.domPath.join('.')} did not resolve`,
105
+ );
106
+ resultFor(rootId).recreatedNodes += 1;
107
+ return null;
108
+ }
109
+ if (child.kind === NodeKind.Text) {
110
+ if (candidate.nodeType !== 3) {
111
+ resultFor(rootId).mismatches.push(`${hydrationId}: expected text node`);
112
+ resultFor(rootId).recreatedNodes += 1;
113
+ return null;
114
+ }
115
+ if (anchor.textGuard !== undefined && candidate.nodeValue !== anchor.textGuard) {
116
+ resultFor(rootId).mismatches.push(`${hydrationId}: textGuard mismatch`);
117
+ resultFor(rootId).recreatedNodes += 1;
118
+ return null;
119
+ }
120
+ options.markMirrored(candidate);
121
+ resultFor(rootId).adoptedNodes += 1;
122
+ return candidate;
123
+ }
124
+ if (candidate.nodeType !== 1) {
125
+ resultFor(rootId).mismatches.push(`${hydrationId}: expected element node`);
126
+ resultFor(rootId).recreatedNodes += 1;
127
+ return null;
128
+ }
129
+ const elementCandidate = candidate as HTMLElement;
130
+ if (desiredTag && elementCandidate.localName !== desiredTag) {
131
+ resultFor(rootId).mismatches.push(
132
+ `${hydrationId}: expected <${desiredTag}>, got <${elementCandidate.localName}>`,
133
+ );
134
+ resultFor(rootId).recreatedNodes += 1;
135
+ return null;
136
+ }
137
+ const marker = elementCandidate.getAttribute('data-exact-hydration-id');
138
+ if (marker !== hydrationId) {
139
+ resultFor(rootId).mismatches.push(`${hydrationId}: marker mismatch`);
140
+ resultFor(rootId).recreatedNodes += 1;
141
+ return null;
142
+ }
143
+ options.markMirrored(elementCandidate);
144
+ resultFor(rootId).adoptedNodes += 1;
145
+ return elementCandidate;
146
+ },
147
+
148
+ report(rootId) {
149
+ if (options.hydrate.rootId !== rootId || reportedHydrationRoots.has(rootId)) {
150
+ return;
151
+ }
152
+ reportedHydrationRoots.add(rootId);
153
+ options.hydrate.onResult?.(resultFor(rootId));
154
+ },
155
+ };
156
+ }
157
+
158
+ export function installDomHydrationAdopterFactory(): void {
159
+ globalThis.__exactDomHydrationAdopterFactory = createDomHydrationAdopter;
160
+ }