electrobun 1.18.4-beta.18 → 1.18.4-beta.21

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 (55) hide show
  1. package/README.md +9 -0
  2. package/bin/electrobun.cjs +165 -0
  3. package/dist/api/browser/ui/__tests__/dom.test.ts +473 -0
  4. package/dist/api/browser/ui/__tests__/domStub.ts +218 -0
  5. package/dist/api/browser/ui/dom.ts +490 -0
  6. package/dist/api/browser/ui/index.ts +44 -0
  7. package/dist/api/browser/ui/jsx-dev-runtime.ts +16 -0
  8. package/dist/api/browser/ui/jsx-runtime.ts +56 -0
  9. package/dist/api/config/ElectrobunConfig.ts +33 -0
  10. package/dist/api/preload/.generated/compiled.ts +1 -1
  11. package/dist/api/preload/index.ts +2 -0
  12. package/dist/api/preload/uiTag.ts +45 -0
  13. package/dist/api/sdks/main/__tests__/utils-quit-exit-code.test.ts +44 -0
  14. package/dist/api/sdks/main/core/GpuWindow.ts +19 -0
  15. package/dist/api/sdks/main/core/Utils.ts +52 -4
  16. package/dist/api/sdks/main/core/WGPUView.ts +9 -0
  17. package/dist/api/sdks/main/entries/ui.ts +1 -0
  18. package/dist/api/sdks/main/proc/native.ts +187 -0
  19. package/dist/api/sdks/main/ui/__tests__/font.test.ts +49 -0
  20. package/dist/api/sdks/main/ui/__tests__/hit.test.ts +64 -0
  21. package/dist/api/sdks/main/ui/__tests__/jsx.test.ts +252 -0
  22. package/dist/api/sdks/main/ui/__tests__/layout.test.ts +159 -0
  23. package/dist/api/sdks/main/ui/__tests__/paint.test.ts +115 -0
  24. package/dist/api/sdks/main/ui/__tests__/reactive.test.ts +456 -0
  25. package/dist/api/sdks/main/ui/__tests__/scroll-focus-input.test.ts +298 -0
  26. package/dist/api/sdks/main/ui/__tests__/tree.test.ts +96 -0
  27. package/dist/api/sdks/main/ui/__tests__/ui.test.ts +170 -0
  28. package/dist/api/sdks/main/ui/elements.ts +135 -0
  29. package/dist/api/sdks/main/ui/font.ts +168 -0
  30. package/dist/api/sdks/main/ui/hit.ts +46 -0
  31. package/dist/api/sdks/main/ui/index.ts +71 -0
  32. package/dist/api/sdks/main/ui/input.ts +268 -0
  33. package/dist/api/sdks/main/ui/jsx-dev-runtime.ts +16 -0
  34. package/dist/api/sdks/main/ui/jsx-runtime.ts +136 -0
  35. package/dist/api/sdks/main/ui/keymap.ts +147 -0
  36. package/dist/api/sdks/main/ui/layout.ts +178 -0
  37. package/dist/api/sdks/main/ui/paint.ts +196 -0
  38. package/dist/api/sdks/main/ui/reactive.ts +4 -0
  39. package/dist/api/sdks/main/ui/renderer.ts +278 -0
  40. package/dist/api/sdks/main/ui/text.ts +175 -0
  41. package/dist/api/sdks/main/ui/textInput.ts +121 -0
  42. package/dist/api/sdks/main/ui/tree.ts +276 -0
  43. package/dist/api/sdks/main/ui/ui.ts +457 -0
  44. package/dist/api/sdks/main/ui/uiTagHost.ts +56 -0
  45. package/dist/api/sdks/main/ui/uiwindow.ts +330 -0
  46. package/dist/api/shared/build-dependencies.test.ts +1 -1
  47. package/dist/api/shared/build-dependencies.ts +4 -4
  48. package/dist/api/shared/linux-webkit-automation.test.ts +1 -1
  49. package/dist/api/shared/warren/jsx.ts +279 -0
  50. package/dist/api/shared/warren/reactive.ts +638 -0
  51. package/dist/api/shared/windows-unicode-ui.test.ts +4 -4
  52. package/dist/preload-full.js +35 -0
  53. package/dist/zig-sdk/electrobun.zig +197 -162
  54. package/{dash.config.ts → hutch.config.ts} +4 -3
  55. package/package.json +14 -2
@@ -0,0 +1,330 @@
1
+ // Mounting a UI tree onto a Dawn target. Two shapes:
2
+ //
3
+ // - createUIWindow: a GpuWindow whose whole content is the retained UI tree —
4
+ // app chrome without a webview.
5
+ // - createUIView: a UI tree rendered into an existing WGPUView composited
6
+ // inside any window (e.g. a view created by an <electrobun-wgpu>-style tag
7
+ // over a webview) — UI layered on top of web content.
8
+ //
9
+ // Both are invalidation-driven: the frame tick is a cheap input poll unless
10
+ // something marked the tree dirty.
11
+
12
+ import { GpuWindow } from "../core/GpuWindow";
13
+ import type { WGPUView } from "../core/WGPUView";
14
+ import { batch, createRoot, inert } from "./reactive";
15
+ import { Prop } from "./tree";
16
+ import { hitChain, scrollTargetAt } from "./hit";
17
+ import { computeLayout } from "./layout";
18
+ import { paint } from "./paint";
19
+ import { createUiRenderer } from "./renderer";
20
+ import { attachInput } from "./input";
21
+ import { tryEnableNativeText } from "./text";
22
+ import { isUIElement, type UIElement } from "./jsx-runtime";
23
+ import { nativeText } from "../proc/native";
24
+ import {
25
+ createUiContext,
26
+ withUiContext,
27
+ type AnchorRect,
28
+ type KeyEventInfo,
29
+ type PointerEventInfo,
30
+ type UiContext,
31
+ } from "./ui";
32
+ import electrobunEventEmitter from "../events/eventEmitter";
33
+
34
+ export interface UIMountOptions {
35
+ /** Painted every frame behind the tree. */
36
+ background?: string;
37
+ /** Frame tick in ms: input poll always, render only when dirty. */
38
+ tickMs?: number;
39
+ }
40
+
41
+ export interface UIWindowOptions extends UIMountOptions {
42
+ title: string;
43
+ width: number;
44
+ height: number;
45
+ titleBarStyle?: "hidden" | "hiddenInset" | "default";
46
+ /**
47
+ * Transparent window: pair with an alpha background (e.g. "#00000000")
48
+ * and a rounded root box for a floating-panel look.
49
+ */
50
+ transparent?: boolean;
51
+ alwaysOnTop?: boolean;
52
+ }
53
+
54
+ export interface UIMount {
55
+ context: UiContext;
56
+ dispose(): void;
57
+ }
58
+
59
+ export interface UIWindow extends UIMount {
60
+ window: GpuWindow;
61
+ }
62
+
63
+ export interface UIView extends UIMount {
64
+ view: WGPUView;
65
+ }
66
+
67
+ interface MountTarget {
68
+ renderTarget: GpuWindow | WGPUView;
69
+ /** WGPUView id whose native pointer events drive this mount. */
70
+ viewId: number;
71
+ windowId: number;
72
+ getSize(): { width: number; height: number };
73
+ /** Offset of the render target inside the window's content area. */
74
+ viewOffset(): { x: number; y: number };
75
+ /** False once the underlying native target is gone; ticks become no-ops. */
76
+ isAlive?(): boolean;
77
+ /** False while hidden: skip input polling and rendering until shown. */
78
+ isVisible?(): boolean;
79
+ /** Whether windowDrag nodes may move the host window (window mounts). */
80
+ allowWindowDrag?: boolean;
81
+ }
82
+
83
+ export type UIApp = () => void | UIElement;
84
+
85
+ async function mount(
86
+ target: MountTarget,
87
+ options: UIMountOptions,
88
+ app: UIApp,
89
+ ): Promise<{ context: UiContext; stop: () => void }> {
90
+ const background = options.background ?? "#141420";
91
+ // System-font text when the native wrapper provides it (macOS); the
92
+ // built-in bitmap font otherwise.
93
+ tryEnableNativeText(nativeText.available() ? nativeText : null);
94
+ const renderer = await createUiRenderer(
95
+ target.renderTarget,
96
+ background,
97
+ target.getSize(),
98
+ );
99
+ const ctx = createUiContext();
100
+ ctx.windowId = target.windowId;
101
+ const { tree } = ctx;
102
+
103
+ let disposeRoot = () => {};
104
+ createRoot((dispose) => {
105
+ disposeRoot = dispose;
106
+ withUiContext(ctx, () => {
107
+ const result = app();
108
+ // JSX apps return a lazy element; the builder API returns nothing.
109
+ if (isUIElement(result)) result.create();
110
+ });
111
+ });
112
+
113
+ const pointerHandler = (
114
+ type: "click" | "down" | "up" | "enter" | "leave",
115
+ targetId: number,
116
+ x: number,
117
+ y: number,
118
+ ) => {
119
+ if (type === "down") {
120
+ // Click-to-focus: innermost focusable ancestor of the hit, or blur
121
+ // (targetId 0 = background click).
122
+ let id = targetId;
123
+ while (id !== 0 && tree.has(id)) {
124
+ if (tree.getProp(id, Prop.Focusable) === 1) break;
125
+ id = tree.parentOf(id);
126
+ }
127
+ ctx.setFocused(id);
128
+ } else if (type === "enter") {
129
+ ctx.setHovered(targetId);
130
+ } else if (type === "leave") {
131
+ if (inert(ctx.hoveredId) === targetId) ctx.setHovered(0);
132
+ }
133
+ const handlers = ctx.handlers.get(targetId);
134
+ if (!handlers) return;
135
+ const event: PointerEventInfo = { x, y, target: targetId };
136
+ const fn =
137
+ type === "click"
138
+ ? handlers.onClick
139
+ : type === "down"
140
+ ? handlers.onPointerDown
141
+ : type === "up"
142
+ ? handlers.onPointerUp
143
+ : type === "enter"
144
+ ? handlers.onPointerEnter
145
+ : handlers.onPointerLeave;
146
+ if (fn) batch(() => fn(event));
147
+ };
148
+
149
+ const input = attachInput(target.windowId, target.viewId, target.viewOffset, {
150
+ hitChain: (x, y) => hitChain(tree, x, y),
151
+ dispatchWheel: (x, y, dx, dy) => {
152
+ const targetId = scrollTargetAt(tree, x, y);
153
+ if (targetId === 0) return;
154
+ const node = tree.get(targetId);
155
+ const column = tree.getProp(targetId, Prop.Dir) === 1;
156
+ const delta = column ? dy : dx;
157
+ const viewport = column ? node.h : node.w;
158
+ const max = Math.max(0, node.contentMain - viewport);
159
+ const current = tree.getProp(targetId, Prop.Scroll);
160
+ // Natural scrolling: positive delta scrolls content down/right.
161
+ const next = Math.max(0, Math.min(max, current - delta));
162
+ tree.setProp(targetId, Prop.Scroll, next);
163
+ },
164
+ isDragHandle: (id) => {
165
+ if (!target.allowWindowDrag) return false;
166
+ for (let n = id; n !== 0 && tree.has(n); n = tree.parentOf(n)) {
167
+ if (tree.getProp(n, Prop.WindowDrag) === 1) return true;
168
+ }
169
+ return false;
170
+ },
171
+ dispatchPointer: pointerHandler,
172
+ dispatchKey: (e: KeyEventInfo) => {
173
+ batch(() => {
174
+ // Focused node first, bubbling through ancestors; a handler
175
+ // returning true stops propagation to the window-level handlers.
176
+ let id = inert(ctx.focusedId);
177
+ while (id !== 0 && tree.has(id)) {
178
+ const handler = ctx.handlers.get(id)?.onKeyDown;
179
+ if (handler && handler(e) === true) return;
180
+ id = tree.parentOf(id);
181
+ }
182
+ for (const handler of ctx.keyHandlers) handler(e);
183
+ });
184
+ },
185
+ });
186
+
187
+ let lastWidth = 0;
188
+ let lastHeight = 0;
189
+ const lastAnchorRects = new Map<number, AnchorRect>();
190
+ const syncAnchors = () => {
191
+ for (const [id, onFrame] of ctx.anchors) {
192
+ if (!tree.has(id)) {
193
+ lastAnchorRects.delete(id);
194
+ continue;
195
+ }
196
+ const node = tree.get(id);
197
+ const rect: AnchorRect = {
198
+ x: node.x,
199
+ y: node.y,
200
+ width: node.w,
201
+ height: node.h,
202
+ };
203
+ const last = lastAnchorRects.get(id);
204
+ if (
205
+ !last ||
206
+ last.x !== rect.x ||
207
+ last.y !== rect.y ||
208
+ last.width !== rect.width ||
209
+ last.height !== rect.height
210
+ ) {
211
+ lastAnchorRects.set(id, rect);
212
+ onFrame(rect);
213
+ }
214
+ }
215
+ };
216
+
217
+ const renderFrame = (width: number, height: number) => {
218
+ if (width <= 0 || height <= 0) return;
219
+ renderer.resize(width, height);
220
+ computeLayout(tree, width, height);
221
+ syncAnchors();
222
+ renderer.render(paint(tree), width, height);
223
+ };
224
+
225
+ const timer = setInterval(() => {
226
+ if (target.isAlive && !target.isAlive()) return;
227
+ if (target.isVisible && !target.isVisible()) return;
228
+ input.poll();
229
+ const { width, height } = target.getSize();
230
+ const sizeChanged = width !== lastWidth || height !== lastHeight;
231
+ if (tree.takeDirty() || sizeChanged) {
232
+ lastWidth = width;
233
+ lastHeight = height;
234
+ renderFrame(width, height);
235
+ }
236
+ }, options.tickMs ?? 8);
237
+
238
+ return {
239
+ context: ctx,
240
+ stop() {
241
+ clearInterval(timer);
242
+ input.dispose();
243
+ disposeRoot();
244
+ },
245
+ };
246
+ }
247
+
248
+ export async function createUIWindow(
249
+ options: UIWindowOptions,
250
+ app: UIApp,
251
+ ): Promise<UIWindow> {
252
+ const win = new GpuWindow({
253
+ title: options.title,
254
+ frame: { width: options.width, height: options.height },
255
+ // hiddenInset keeps the window frame equal to the content area, so
256
+ // cursor-to-local math needs no title-bar offset.
257
+ titleBarStyle: options.titleBarStyle ?? "hiddenInset",
258
+ transparent: options.transparent ?? false,
259
+ });
260
+ if (options.alwaysOnTop) {
261
+ win.setAlwaysOnTop(true);
262
+ }
263
+
264
+ const mounted = await mount(
265
+ {
266
+ renderTarget: win,
267
+ viewId: win.wgpuViewId,
268
+ windowId: win.id,
269
+ getSize: () => win.getSize(),
270
+ viewOffset: () => ({ x: 0, y: 0 }),
271
+ // Hidden windows skip layout/paint/GPU entirely (e.g. a focused
272
+ // textInput's caret blink must not keep a hidden palette rendering).
273
+ isVisible: () => win.isVisible(),
274
+ allowWindowDrag: true,
275
+ },
276
+ options,
277
+ app,
278
+ );
279
+
280
+ const onClose = () => mounted.stop();
281
+ electrobunEventEmitter.on(`close-${win.id}`, onClose);
282
+
283
+ return {
284
+ window: win,
285
+ context: mounted.context,
286
+ dispose() {
287
+ electrobunEventEmitter.off(`close-${win.id}`, onClose);
288
+ mounted.stop();
289
+ win.close();
290
+ },
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Mount a UI tree into an existing WGPUView. The view's frame (tracked via
296
+ * the JS wrapper) supplies size and pointer offset; pair it with an
297
+ * <electrobun-wgpu>-style tag to layer reactive native UI over a webview.
298
+ * Note: frames moved natively without updating the JS wrapper (tag overlay
299
+ * sync) are not observed yet — production work alongside native pointer
300
+ * events.
301
+ */
302
+ export async function createUIView(
303
+ view: WGPUView,
304
+ options: UIMountOptions,
305
+ app: UIApp,
306
+ ): Promise<UIView> {
307
+ const mounted = await mount(
308
+ {
309
+ renderTarget: view,
310
+ viewId: view.id,
311
+ windowId: view.windowId,
312
+ getSize: () => ({
313
+ width: view.frame.width,
314
+ height: view.frame.height,
315
+ }),
316
+ viewOffset: () => ({ x: view.frame.x, y: view.frame.y }),
317
+ isAlive: () => !view.isRemoved,
318
+ },
319
+ options,
320
+ app,
321
+ );
322
+
323
+ return {
324
+ view,
325
+ context: mounted.context,
326
+ dispose() {
327
+ mounted.stop();
328
+ },
329
+ };
330
+ }
@@ -42,7 +42,7 @@ describe("owned build dependency artifacts", () => {
42
42
  );
43
43
 
44
44
  expect(artifact.url).toBe(
45
- "https://artifacts.example.test/zig-bsdiff/releases/0.1.21/zig-bsdiff-linux-x64.tar.gz",
45
+ "https://artifacts.example.test/zig-bsdiff/releases/0.1.22/zig-bsdiff-linux-x64.tar.gz",
46
46
  );
47
47
  });
48
48
  });
@@ -1,12 +1,12 @@
1
1
  export const BUILD_DEPENDENCIES_PUBLIC_BASE_URL =
2
2
  "https://electrobun-artifacts.blackboard.sh";
3
3
 
4
- export const ZIG_VERSION = "0.13.0";
4
+ export const ZIG_VERSION = "0.16.0";
5
5
 
6
6
  export const OWNED_BUILD_DEPENDENCY_VERSIONS = {
7
- "zig-bsdiff": "0.1.21",
8
- "zig-zstd": "0.1.6",
9
- "zig-asar": "0.2.5",
7
+ "zig-bsdiff": "0.1.22",
8
+ "zig-zstd": "0.1.7",
9
+ "zig-asar": "0.2.7",
10
10
  "electrobun-dawn": "0.2.5",
11
11
  } as const;
12
12
 
@@ -30,7 +30,7 @@ describe("Linux WebKitGTK automation contract", () => {
30
30
  "automation.private_inspector_server_environment_variable",
31
31
  );
32
32
  expect(launcher).toContain(
33
- "env_map.remove(automation.inspector_server_environment_variable)",
33
+ "env_map.swapRemove(automation.inspector_server_environment_variable)",
34
34
  );
35
35
  expect(launcher).not.toContain("argv = launcher_args");
36
36
  });
@@ -0,0 +1,279 @@
1
+ // Warren JSX core — renderer-agnostic.
2
+ //
3
+ // Everything a renderer target shares: lazy elements, child mounting,
4
+ // component invocation, and the control-flow components (Show / For /
5
+ // Switch / Match) with their live-vs-frozen-snapshot semantics. A renderer
6
+ // (GPU retained tree, browser DOM) supplies five primitives and gets the
7
+ // whole component model:
8
+ //
9
+ // text(value) create a text leaf at the insertion point
10
+ // dynamic(build) reactive region: build re-runs on change
11
+ // each(items, key, render) keyed list region with row reconciliation
12
+ // intrinsic(type, props) create one intrinsic element (tag vocabulary
13
+ // is renderer-owned; children arrive in props)
14
+ // escape(fn) a bare function child — renderer escape hatch
15
+ //
16
+ // Each package binds a renderer once via createJsxRuntime() and re-exports
17
+ // the result as its jsx-runtime module; JSX.IntrinsicElements typing stays
18
+ // per-package (types only, Solid-style — the runtime never enumerates tags).
19
+
20
+ import { inert, isLive, memo, type Accessor, type LiveBinding } from "./reactive";
21
+
22
+ export interface UIElement {
23
+ readonly __electrobunElement: true;
24
+ /** Create this element's nodes under the current build parent. */
25
+ create(): void;
26
+ }
27
+
28
+ export function isUIElement(value: unknown): value is UIElement {
29
+ return (
30
+ typeof value === "object" &&
31
+ value !== null &&
32
+ (value as any).__electrobunElement === true
33
+ );
34
+ }
35
+
36
+ export type UIChild =
37
+ | UIElement
38
+ | string
39
+ | number
40
+ | boolean
41
+ | null
42
+ | undefined
43
+ | LiveBinding<string | number>
44
+ | (() => void)
45
+ | UIChild[];
46
+
47
+ export function element(create: () => void): UIElement {
48
+ return { __electrobunElement: true, create };
49
+ }
50
+
51
+ export interface WarrenRenderer {
52
+ text(value: string | number | LiveBinding<string | number>): void;
53
+ /**
54
+ * A claimed live() child. Text-only renderers may bind it as reactive
55
+ * text; DOM-style renderers should evaluate inside a region so element
56
+ * results ({live(() => open() && <div/>)}) remount on change.
57
+ */
58
+ liveChild(binding: LiveBinding<unknown>): void;
59
+ dynamic(build: () => void): void;
60
+ each<T>(
61
+ items: () => readonly T[],
62
+ key: (item: T, index: number) => string | number,
63
+ render: (item: T, index: Accessor<number>) => void,
64
+ ): void;
65
+ intrinsic(type: string, props: Record<string, unknown>): void;
66
+ escape(fn: () => unknown): void;
67
+ }
68
+
69
+ // --- Match markers (structural brand: survives mixed module realms) -------
70
+
71
+ const MATCH_BRAND = "__warrenMatch";
72
+
73
+ export interface MatchProps {
74
+ when: unknown;
75
+ children?: UIChild;
76
+ }
77
+
78
+ interface MatchMarker {
79
+ [MATCH_BRAND]: true;
80
+ when: unknown;
81
+ children?: UIChild;
82
+ }
83
+
84
+ export function Match(props: MatchProps): MatchMarker {
85
+ return { [MATCH_BRAND]: true, when: props.when, children: props.children };
86
+ }
87
+
88
+ function isMatch(value: unknown): value is MatchMarker {
89
+ return (
90
+ typeof value === "object" &&
91
+ value !== null &&
92
+ (value as any)[MATCH_BRAND] === true
93
+ );
94
+ }
95
+
96
+ // --- Control-flow prop shapes ---------------------------------------------
97
+
98
+ export interface ShowProps {
99
+ when: unknown;
100
+ fallback?: UIChild;
101
+ children?: UIChild;
102
+ }
103
+
104
+ export interface ForProps<T> {
105
+ each: readonly T[] | LiveBinding<readonly T[]>;
106
+ /** Row identity for reconciliation; defaults to item identity. */
107
+ key?: (item: T, index: number) => string | number;
108
+ fallback?: UIChild;
109
+ children?: (item: T, index: Accessor<number>) => UIChild;
110
+ }
111
+
112
+ export interface SwitchProps {
113
+ fallback?: UIChild;
114
+ children?: unknown;
115
+ }
116
+
117
+ type ComponentFn = (props: Record<string, unknown>) => UIElement | UIChild;
118
+
119
+ export interface WarrenJsxRuntime {
120
+ jsx(
121
+ type: string | ComponentFn,
122
+ props: Record<string, unknown> | null,
123
+ key?: unknown,
124
+ ): UIElement;
125
+ jsxs(
126
+ type: string | ComponentFn,
127
+ props: Record<string, unknown> | null,
128
+ key?: unknown,
129
+ ): UIElement;
130
+ Fragment(props: { children?: UIChild }): UIElement;
131
+ mountChild(child: UIChild): void;
132
+ Show(props: ShowProps): UIElement;
133
+ For<T>(props: ForProps<T>): UIElement;
134
+ Switch(props: SwitchProps): UIElement;
135
+ Match: typeof Match;
136
+ }
137
+
138
+ export function createJsxRuntime(renderer: WarrenRenderer): WarrenJsxRuntime {
139
+ function mountChild(child: UIChild): void {
140
+ if (child == null || typeof child === "boolean") return;
141
+ if (Array.isArray(child)) {
142
+ for (const c of child) mountChild(c);
143
+ return;
144
+ }
145
+ if (isUIElement(child)) {
146
+ child.create();
147
+ return;
148
+ }
149
+ if (isLive(child)) {
150
+ const binding = child as LiveBinding<unknown>;
151
+ binding.claimed = true;
152
+ renderer.liveChild(binding);
153
+ return;
154
+ }
155
+ if (typeof child === "function") {
156
+ renderer.escape(child as () => unknown);
157
+ return;
158
+ }
159
+ renderer.text(String(child));
160
+ }
161
+
162
+ function jsx(
163
+ type: string | ComponentFn,
164
+ props: Record<string, unknown> | null,
165
+ key?: unknown,
166
+ ): UIElement {
167
+ let resolved = props ?? {};
168
+ if (typeof type === "function") {
169
+ // The automatic JSX transform lifts `key` out of props into the
170
+ // third argument; components (For) receive it back as a prop.
171
+ if (key !== undefined && resolved["key"] === undefined) {
172
+ resolved = { ...resolved, key };
173
+ }
174
+ // Component bodies are inert (the one-line rule) — including when
175
+ // the component is created lazily inside a live region's tracked
176
+ // build. Props were already evaluated at the call site, so region
177
+ // reactivity on prop expressions is unaffected; statement lives
178
+ // declared in the body stay deferred instead of running (and
179
+ // tracking) synchronously into the enclosing region.
180
+ const result = inert(() => type(resolved));
181
+ if (isUIElement(result)) return result;
182
+ // Match markers pass through raw so Switch can collect them.
183
+ if (isMatch(result)) return result as unknown as UIElement;
184
+ // Components may return any child shape (fragment arrays, strings...).
185
+ return element(() => mountChild(result as UIChild));
186
+ }
187
+ return element(() => renderer.intrinsic(type, resolved));
188
+ }
189
+
190
+ function Fragment(props: { children?: UIChild }): UIElement {
191
+ return element(() => mountChild(props.children));
192
+ }
193
+
194
+ // --- Control flow: a live() prop reconciles on change; a plain value is
195
+ // a snapshot that renders once and never updates. ------------------------
196
+
197
+ function Show(props: ShowProps): UIElement {
198
+ const { when } = props;
199
+ if (isLive(when)) {
200
+ when.claimed = true;
201
+ return element(() => {
202
+ renderer.dynamic(() => {
203
+ mountChild(when.fn() ? props.children : props.fallback);
204
+ });
205
+ });
206
+ }
207
+ return element(() => mountChild(when ? props.children : props.fallback));
208
+ }
209
+
210
+ function For<T>(props: ForProps<T>): UIElement {
211
+ const render = props.children;
212
+ if (typeof render !== "function") {
213
+ throw new Error(
214
+ "Warren: <For> takes a function child: (item, index) => ...",
215
+ );
216
+ }
217
+ const keyOf =
218
+ props.key ?? ((item: T) => item as unknown as string | number);
219
+ const { each } = props;
220
+ if (isLive(each)) {
221
+ each.claimed = true;
222
+ const items = () => (each.fn() ?? []) as readonly T[];
223
+ return element(() => {
224
+ // The region depends only on emptiness (equality-cut memo), so
225
+ // list changes reconcile rows in the keyed each instead of
226
+ // rebuilding the whole region; only empty<->non-empty flips it.
227
+ const empty = memo(() => items().length === 0);
228
+ renderer.dynamic(() => {
229
+ if (empty()) {
230
+ mountChild(props.fallback);
231
+ return;
232
+ }
233
+ renderer.each(items, keyOf, (item, index) => {
234
+ mountChild(render(item, index));
235
+ });
236
+ });
237
+ });
238
+ }
239
+ // Snapshot: rendered once, no reconciliation.
240
+ const list = (each ?? []) as readonly T[];
241
+ return element(() => {
242
+ if (list.length === 0) {
243
+ mountChild(props.fallback);
244
+ return;
245
+ }
246
+ list.forEach((item, i) => mountChild(render(item, () => i)));
247
+ });
248
+ }
249
+
250
+ function Switch(props: SwitchProps): UIElement {
251
+ const kids = Array.isArray(props.children)
252
+ ? props.children
253
+ : [props.children];
254
+ const matches = kids.filter(isMatch);
255
+ const anyLive = matches.some((m) => isLive(m.when));
256
+ for (const m of matches) {
257
+ if (isLive(m.when)) (m.when as LiveBinding<unknown>).claimed = true;
258
+ }
259
+ const pick = (): UIChild | undefined => {
260
+ for (const m of matches) {
261
+ const truthy = isLive(m.when)
262
+ ? (m.when as LiveBinding<unknown>).fn()
263
+ : m.when;
264
+ if (truthy) return m.children;
265
+ }
266
+ return props.fallback;
267
+ };
268
+ if (anyLive) {
269
+ return element(() => {
270
+ renderer.dynamic(() => {
271
+ mountChild(pick());
272
+ });
273
+ });
274
+ }
275
+ return element(() => mountChild(pick()));
276
+ }
277
+
278
+ return { jsx, jsxs: jsx, Fragment, mountChild, Show, For, Switch, Match };
279
+ }