streetui 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,11 +1,12 @@
1
1
  // ../renderer/src/render-context.ts
2
- function createRenderContext(dom, graph, container, hydrationDiagnostics) {
2
+ function createRenderContext(dom, graph, container, hydrationDiagnostics, staticHTML) {
3
3
  return {
4
4
  dom,
5
5
  graph,
6
6
  instances: /* @__PURE__ */ new Map(),
7
7
  container,
8
- ...hydrationDiagnostics !== void 0 ? { hydrationDiagnostics } : {}
8
+ ...hydrationDiagnostics !== void 0 ? { hydrationDiagnostics } : {},
9
+ ...staticHTML !== void 0 ? { staticHTML } : {}
9
10
  };
10
11
  }
11
12
 
@@ -153,948 +154,1130 @@ function wireEvents(dom, graph, node, element, instance) {
153
154
  }
154
155
  }
155
156
 
156
- // ../renderer/src/tag-map.ts
157
- var TAG_MAP = {
158
- application: "div",
159
- page: "div",
160
- section: "section",
161
- container: "div",
162
- heading: "h1",
163
- text: "span",
164
- button: "button",
165
- input: "input",
166
- form: "form",
167
- list: "ul",
168
- "list-item": "li",
169
- image: "img",
170
- link: "a",
171
- component: "div",
172
- slot: "div",
173
- fragment: "div",
174
- "reactive-list": "ul"
157
+ // ../dom/src/server-node.ts
158
+ var ServerStyle = class {
159
+ declarations = /* @__PURE__ */ new Map();
160
+ setProperty(name, value) {
161
+ this.declarations.set(name, value);
162
+ }
163
+ get isEmpty() {
164
+ return this.declarations.size === 0;
165
+ }
166
+ toCss() {
167
+ return [...this.declarations.entries()].map(([k, v]) => `${k}: ${v}`).join("; ");
168
+ }
175
169
  };
176
- function resolveTag(type) {
177
- return TAG_MAP[type] ?? "div";
178
- }
179
-
180
- // ../renderer/src/patch.ts
181
- function patchNode(ctx, graphNode, propKey, newValue) {
182
- const instance = ctx.instances.get(graphNode.id);
183
- if (instance === void 0) return;
184
- const domNode = instance.domNode;
185
- if (!ctx.dom.isElement(domNode)) return;
186
- const oldValue = graphNode.getProp(propKey);
187
- switch (propKey) {
188
- case "text":
189
- if (!Object.is(oldValue, newValue)) {
190
- ctx.dom.setTextContent(domNode, String(newValue ?? ""));
191
- graphNode.setProp("text", String(newValue ?? ""));
192
- }
193
- break;
194
- case "label":
195
- if (!Object.is(oldValue, newValue)) {
196
- ctx.dom.setTextContent(domNode, String(newValue ?? ""));
197
- graphNode.setProp("label", String(newValue ?? ""));
198
- }
199
- break;
200
- case "disabled":
201
- if (newValue === true) {
202
- ctx.dom.setAttribute(domNode, "disabled", "");
203
- } else {
204
- ctx.dom.removeAttribute(domNode, "disabled");
205
- }
206
- graphNode.setProp("disabled", Boolean(newValue));
207
- break;
208
- case "value":
209
- if (!Object.is(oldValue, newValue)) {
210
- ctx.dom.setProperty(domNode, "value", String(newValue ?? ""));
211
- graphNode.setProp("value", String(newValue ?? ""));
212
- }
213
- break;
214
- default:
215
- patchProp(ctx.dom, domNode, propKey, oldValue, newValue);
216
- graphNode.setProp(propKey, newValue);
217
- break;
170
+ var ServerText = class {
171
+ kind = "text";
172
+ parent = null;
173
+ data;
174
+ constructor(data) {
175
+ this.data = data;
218
176
  }
219
- }
220
-
221
- // ../renderer/src/reconciliation.ts
222
- function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
223
- const oldByKey = /* @__PURE__ */ new Map();
224
- for (const inst of oldInstances) {
225
- const key = inst.graphNode.key ?? inst.graphNode.id;
226
- oldByKey.set(key, inst);
177
+ };
178
+ var ServerComment = class {
179
+ kind = "comment";
180
+ parent = null;
181
+ data;
182
+ constructor(data) {
183
+ this.data = data;
227
184
  }
228
- const newInstances = [];
229
- const usedKeys = /* @__PURE__ */ new Set();
230
- for (const newNode of newNodes) {
231
- const key = newNode.key ?? newNode.id;
232
- const existing = oldByKey.get(key);
233
- if (existing !== void 0) {
234
- usedKeys.add(key);
235
- const oldSig = existing.graphNode.getProp("_sig");
236
- const newSig = newNode.getProp("_sig");
237
- patchExistingInstance(ctx, existing, newNode);
238
- if (!Object.is(oldSig, newSig)) {
239
- reconcileItemChildren(ctx, existing, newNode, mountFn);
240
- }
241
- newInstances.push(existing);
242
- } else {
243
- const inst = mountFn(newNode, parentDom);
244
- newInstances.push(inst);
245
- }
185
+ };
186
+ var ServerFragment = class {
187
+ kind = "fragment";
188
+ parent = null;
189
+ children = [];
190
+ };
191
+ var ServerRawHTML = class {
192
+ kind = "raw";
193
+ parent = null;
194
+ html;
195
+ constructor(html) {
196
+ this.html = html;
246
197
  }
247
- const removed = [];
248
- for (const inst of oldInstances) {
249
- const key = inst.graphNode.key ?? inst.graphNode.id;
250
- if (!usedKeys.has(key)) {
251
- removed.push(inst);
198
+ };
199
+ var ServerElement = class {
200
+ kind = "element";
201
+ parent = null;
202
+ tagName;
203
+ attributes = /* @__PURE__ */ new Map();
204
+ children = [];
205
+ // Lazily-allocated stores. On the 10k-row SSR corpus ~0% of elements carry JS
206
+ // properties or inline styles (measured, §5: 1 of 80,029 elements uses
207
+ // `properties`, 0 use `style`), so eagerly allocating a `properties` Map plus
208
+ // a `ServerStyle` (which itself holds a Map) per element wasted ~240k
209
+ // allocations per /users render — all in the dominant mount phase. These are
210
+ // created on first WRITE via the `properties`/`style` getters; the serializer
211
+ // reads the raw `_properties`/`_style` fields so a READ never forces an
212
+ // allocation. Output is byte-identical: an unset store previously serialized
213
+ // to nothing (empty `properties.has(...)` / `style.isEmpty`), and a null store
214
+ // is skipped the same way.
215
+ _properties = null;
216
+ _style = null;
217
+ constructor(tagName) {
218
+ this.tagName = tagName.toLowerCase();
219
+ }
220
+ /** JS properties set via `setProperty` (e.g. input `value`, `checked`). Allocated on first access. */
221
+ get properties() {
222
+ return this._properties ??= /* @__PURE__ */ new Map();
223
+ }
224
+ /** Inline-style holder mirroring `element.style`. Allocated on first access. */
225
+ get style() {
226
+ return this._style ??= new ServerStyle();
227
+ }
228
+ };
229
+ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
230
+ "area",
231
+ "base",
232
+ "br",
233
+ "col",
234
+ "embed",
235
+ "hr",
236
+ "img",
237
+ "input",
238
+ "link",
239
+ "meta",
240
+ "param",
241
+ "source",
242
+ "track",
243
+ "wbr"
244
+ ]);
245
+ var SERIALIZED_PROPERTIES = {
246
+ value: "attr",
247
+ checked: "boolean",
248
+ selected: "boolean"
249
+ };
250
+ var SERIALIZED_PROPERTY_ENTRIES = Object.entries(SERIALIZED_PROPERTIES);
251
+ var TEXT_SPECIAL = /[&<>]/;
252
+ var ATTR_SPECIAL = /[&<>"]/;
253
+ function escapeHtmlText(value) {
254
+ if (!TEXT_SPECIAL.test(value)) return value;
255
+ let out = "";
256
+ let last = 0;
257
+ for (let i = 0; i < value.length; i++) {
258
+ let esc;
259
+ switch (value.charCodeAt(i)) {
260
+ case 38:
261
+ esc = "&amp;";
262
+ break;
263
+ // &
264
+ case 60:
265
+ esc = "&lt;";
266
+ break;
267
+ // <
268
+ case 62:
269
+ esc = "&gt;";
270
+ break;
271
+ // >
272
+ default:
273
+ continue;
252
274
  }
275
+ out += value.slice(last, i) + esc;
276
+ last = i + 1;
253
277
  }
254
- for (const inst of removed) {
255
- const parent = ctx.dom.parentNode(inst.domNode);
256
- if (parent !== null) {
257
- ctx.dom.removeChild(parent, inst.domNode);
278
+ return out + value.slice(last);
279
+ }
280
+ function escapeHtmlAttr(value) {
281
+ if (!ATTR_SPECIAL.test(value)) return value;
282
+ let out = "";
283
+ let last = 0;
284
+ for (let i = 0; i < value.length; i++) {
285
+ let esc;
286
+ switch (value.charCodeAt(i)) {
287
+ case 38:
288
+ esc = "&amp;";
289
+ break;
290
+ // &
291
+ case 60:
292
+ esc = "&lt;";
293
+ break;
294
+ // <
295
+ case 62:
296
+ esc = "&gt;";
297
+ break;
298
+ // >
299
+ case 34:
300
+ esc = "&quot;";
301
+ break;
302
+ // "
303
+ default:
304
+ continue;
258
305
  }
259
- inst.dispose();
306
+ out += value.slice(last, i) + esc;
307
+ last = i + 1;
260
308
  }
261
- reorderDom(ctx, parentDom, newInstances);
262
- return { instances: newInstances, removed };
309
+ return out + value.slice(last);
263
310
  }
264
- function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
265
- const oldByKey = /* @__PURE__ */ new Map();
266
- for (const inst of oldInstances) {
267
- oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
311
+ function serializeAttributes(el) {
312
+ const parts = [];
313
+ for (const [name, value] of el.attributes) {
314
+ if (value === "") {
315
+ parts.push(` ${name}`);
316
+ } else {
317
+ parts.push(` ${name}="${escapeHtmlAttr(value)}"`);
318
+ }
268
319
  }
269
- const newInstances = [];
270
- const usedKeys = /* @__PURE__ */ new Set();
271
- const built = [];
272
- for (const entry of plan) {
273
- const existing = oldByKey.get(entry.key);
274
- if (existing !== void 0) {
275
- usedKeys.add(entry.key);
276
- const oldItem = existing.graphNode.getProp("_item");
277
- if (!Object.is(oldItem, entry.item)) {
278
- const newSig = entry.sig();
279
- const oldSig = existing.graphNode.getProp("_sig");
280
- if (!Object.is(oldSig, newSig)) {
281
- const freshNode = entry.build();
282
- built.push(freshNode);
283
- patchExistingInstance(ctx, existing, freshNode);
284
- reconcileItemChildren(ctx, existing, freshNode, mountFn);
285
- existing.graphNode.setProp("_sig", newSig);
320
+ const props = el._properties;
321
+ if (props !== null) {
322
+ for (const [name, kind] of SERIALIZED_PROPERTY_ENTRIES) {
323
+ if (!props.has(name)) continue;
324
+ if (el.attributes.has(name)) continue;
325
+ const raw = props.get(name);
326
+ if (kind === "boolean") {
327
+ if (raw === true) parts.push(` ${name}`);
328
+ } else {
329
+ if (raw !== void 0 && raw !== null) {
330
+ parts.push(` ${name}="${escapeHtmlAttr(String(raw))}"`);
286
331
  }
287
- existing.graphNode.setProp("_item", entry.item);
288
332
  }
289
- newInstances.push(existing);
290
- } else {
291
- const freshNode = entry.build();
292
- built.push(freshNode);
293
- const inst = mountFn(freshNode, parentDom);
294
- newInstances.push(inst);
295
333
  }
296
334
  }
297
- const removed = [];
298
- for (const inst of oldInstances) {
299
- const key = inst.graphNode.key ?? inst.graphNode.id;
300
- if (!usedKeys.has(key)) removed.push(inst);
301
- }
302
- for (const inst of removed) {
303
- const parent = ctx.dom.parentNode(inst.domNode);
304
- if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
305
- inst.dispose();
335
+ const style = el._style;
336
+ if (style !== null && !style.isEmpty && !el.attributes.has("style")) {
337
+ parts.push(` style="${escapeHtmlAttr(style.toCss())}"`);
306
338
  }
307
- reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
308
- return { instances: newInstances, removed, built };
339
+ return parts.join("");
309
340
  }
310
- function reorderDomMinimal(ctx, parentDom, oldInstances, newInstances) {
311
- const n = newInstances.length;
312
- if (n === 0) return;
313
- const oldIndexOf = /* @__PURE__ */ new Map();
314
- for (let i = 0; i < oldInstances.length; i++) oldIndexOf.set(oldInstances[i], i);
315
- const source = new Array(n);
316
- let moved = false;
317
- let lastSeen = -1;
318
- for (let i = 0; i < n; i++) {
319
- const oi = oldIndexOf.get(newInstances[i]);
320
- if (oi === void 0) {
321
- source[i] = -1;
322
- moved = true;
323
- } else {
324
- source[i] = oi;
325
- if (oi < lastSeen) moved = true;
326
- else lastSeen = oi;
327
- }
328
- }
329
- if (!moved) return;
330
- const keep = longestIncreasingSubsequence(source);
331
- let refNode = null;
332
- for (let i = n - 1; i >= 0; i--) {
333
- const domNode = newInstances[i].domNode;
334
- if (source[i] === -1 || !keep.has(i)) {
335
- if (ctx.dom.nextSibling(domNode) !== refNode) {
336
- ctx.dom.insertBefore(parentDom, domNode, refNode);
341
+ function serializeServerNode(node) {
342
+ switch (node.kind) {
343
+ case "text":
344
+ return escapeHtmlText(node.data);
345
+ case "comment":
346
+ return `<!--${node.data}-->`;
347
+ case "fragment":
348
+ return serializeChildren(node);
349
+ case "raw":
350
+ return node.html;
351
+ case "element": {
352
+ const el = node;
353
+ const tag = el.tagName;
354
+ const attrs = serializeAttributes(el);
355
+ if (VOID_ELEMENTS.has(tag)) {
356
+ return `<${tag}${attrs}>`;
337
357
  }
358
+ return `<${tag}${attrs}>${serializeChildren(el)}</${tag}>`;
338
359
  }
339
- refNode = domNode;
340
360
  }
341
361
  }
342
- function longestIncreasingSubsequence(source) {
343
- const keep = /* @__PURE__ */ new Set();
344
- const n = source.length;
345
- const tails = [];
346
- const prev = new Array(n).fill(-1);
347
- for (let i = 0; i < n; i++) {
348
- const v = source[i];
349
- if (v < 0) continue;
350
- let lo = 0;
351
- let hi = tails.length;
352
- while (lo < hi) {
353
- const mid = lo + hi >> 1;
354
- if (source[tails[mid]] < v) lo = mid + 1;
355
- else hi = mid;
362
+ function serializeChildren(node) {
363
+ let out = "";
364
+ for (const child of node.children) {
365
+ out += serializeServerNode(child);
366
+ }
367
+ return out;
368
+ }
369
+
370
+ // ../dom/src/server-adapter.ts
371
+ function asServer(node) {
372
+ return node;
373
+ }
374
+ function asParent(node) {
375
+ return node;
376
+ }
377
+ var ServerDOMAdapter = class {
378
+ createElement(tag, _ns) {
379
+ return new ServerElement(tag);
380
+ }
381
+ createTextNode(data) {
382
+ return new ServerText(data);
383
+ }
384
+ createComment(data) {
385
+ return new ServerComment(data);
386
+ }
387
+ createFragment() {
388
+ return new ServerFragment();
389
+ }
390
+ /**
391
+ * Create a verbatim pre-serialized HTML node (v1.7 static SSR plan, §6).
392
+ * Server-only: the browser adapter does not implement this, and the renderer
393
+ * fast path only invokes it when a static SSR plan is present (SSR). The
394
+ * stored HTML was produced by this same serializer, so it is emitted as-is.
395
+ */
396
+ createRawHTML(html) {
397
+ return new ServerRawHTML(html);
398
+ }
399
+ appendChild(parent, child) {
400
+ const p = asParent(parent);
401
+ const c = asServer(child);
402
+ this._detach(c);
403
+ c.parent = p;
404
+ p.children.push(c);
405
+ }
406
+ insertBefore(parent, child, reference) {
407
+ const p = asParent(parent);
408
+ const c = asServer(child);
409
+ this._detach(c);
410
+ c.parent = p;
411
+ if (reference === null) {
412
+ p.children.push(c);
413
+ return;
356
414
  }
357
- if (lo > 0) prev[i] = tails[lo - 1];
358
- tails[lo] = i;
415
+ const ref = asServer(reference);
416
+ const idx = p.children.indexOf(ref);
417
+ if (idx === -1) p.children.push(c);
418
+ else p.children.splice(idx, 0, c);
359
419
  }
360
- let idx = tails.length > 0 ? tails[tails.length - 1] : -1;
361
- while (idx >= 0) {
362
- keep.add(idx);
363
- idx = prev[idx];
420
+ removeChild(parent, child) {
421
+ const p = asParent(parent);
422
+ const c = asServer(child);
423
+ const idx = p.children.indexOf(c);
424
+ if (idx !== -1) {
425
+ p.children.splice(idx, 1);
426
+ c.parent = null;
427
+ }
364
428
  }
365
- return keep;
366
- }
367
- function reconcileItemChildren(ctx, itemInstance, newItemNode, mountFn) {
368
- const el = itemInstance.domNode;
369
- if (!ctx.dom.isElement(el)) return;
370
- const oldChildren = [...itemInstance.children];
371
- const newChildNodes = [...newItemNode.children];
372
- const nextChildren = [];
373
- const kept = /* @__PURE__ */ new Set();
374
- for (let i = 0; i < newChildNodes.length; i++) {
375
- const newChild = newChildNodes[i];
376
- const oldChild = oldChildren[i];
377
- if (oldChild !== void 0 && oldChild.graphNode.type === newChild.type) {
378
- patchExistingInstance(ctx, oldChild, newChild);
379
- reconcileItemChildren(ctx, oldChild, newChild, mountFn);
380
- nextChildren.push(oldChild);
381
- kept.add(oldChild);
382
- } else {
383
- itemInstance.graphNode.appendChild(newChild);
384
- const inst = mountFn(newChild, el);
385
- nextChildren.push(inst);
429
+ replaceChild(parent, newChild, oldChild) {
430
+ const p = asParent(parent);
431
+ const nc = asServer(newChild);
432
+ const oc = asServer(oldChild);
433
+ const idx = p.children.indexOf(oc);
434
+ if (idx === -1) return;
435
+ this._detach(nc);
436
+ nc.parent = p;
437
+ p.children.splice(idx, 1, nc);
438
+ oc.parent = null;
439
+ }
440
+ _detach(node) {
441
+ if (node.parent !== null) {
442
+ const siblings = node.parent.children;
443
+ const idx = siblings.indexOf(node);
444
+ if (idx !== -1) siblings.splice(idx, 1);
445
+ node.parent = null;
386
446
  }
387
447
  }
388
- for (const old of oldChildren) {
389
- if (kept.has(old)) continue;
390
- const parent = ctx.dom.parentNode(old.domNode);
391
- if (parent !== null) ctx.dom.removeChild(parent, old.domNode);
392
- old.dispose();
393
- forgetInstanceTree(ctx, old);
394
- ctx.graph.detachNode(old.graphNode);
448
+ setAttribute(element, name, value) {
449
+ element.attributes.set(name, value);
395
450
  }
396
- reorderDom(ctx, el, nextChildren);
397
- itemInstance.children.length = 0;
398
- for (const c of nextChildren) itemInstance.children.push(c);
399
- for (const c of [...itemInstance.graphNode.children]) {
400
- itemInstance.graphNode.removeChild(c);
451
+ removeAttribute(element, name) {
452
+ element.attributes.delete(name);
401
453
  }
402
- for (const c of nextChildren) itemInstance.graphNode.appendChild(c.graphNode);
403
- }
404
- function reorderDom(ctx, parentDom, instances) {
405
- let referenceNode = null;
406
- for (let i = instances.length - 1; i >= 0; i--) {
407
- const inst = instances[i];
408
- if (inst === void 0) continue;
409
- const domNode = inst.domNode;
410
- const currentNext = ctx.dom.nextSibling(domNode);
411
- if (currentNext !== referenceNode) {
412
- ctx.dom.insertBefore(parentDom, domNode, referenceNode);
413
- }
414
- referenceNode = domNode;
454
+ getAttribute(element, name) {
455
+ return element.attributes.get(name) ?? null;
415
456
  }
416
- }
417
- function forgetInstanceTree(ctx, instance) {
418
- ctx.instances.delete(instance.graphNode.id);
419
- for (const child of instance.children) forgetInstanceTree(ctx, child);
420
- }
421
- function patchExistingInstance(ctx, instance, newNode) {
422
- const oldNode = instance.graphNode;
423
- for (const [key, newVal] of Object.entries(newNode.props)) {
424
- const oldVal = oldNode.getProp(key);
425
- if (!Object.is(oldVal, newVal)) {
426
- patchNode(ctx, instance.graphNode, key, newVal);
427
- }
457
+ setProperty(element, name, value) {
458
+ element.properties.set(name, value);
428
459
  }
429
- }
430
-
431
- // ../renderer/src/mount.ts
432
- var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
433
- "text",
434
- "label",
435
- "level",
436
- "inputType",
437
- "src",
438
- "alt",
439
- "href",
440
- "external",
441
- "value",
442
- "placeholder",
443
- "disabled",
444
- "_renderKey",
445
- "key",
446
- "name"
447
- ]);
448
- function mountGraph(ctx) {
449
- return mountNode(ctx, ctx.graph.root, ctx.container);
450
- }
451
- function mountNode(ctx, graphNode, parentDom) {
452
- const { dom, graph } = ctx;
453
- if (graphNode.type === "application") {
454
- const instance2 = new NodeInstance(graphNode, parentDom);
455
- ctx.instances.set(graphNode.id, instance2);
456
- for (const child of graphNode.children) {
457
- const childInstance = mountNode(ctx, child, parentDom);
458
- instance2.addChild(childInstance);
460
+ setTextContent(node, text) {
461
+ const n = asServer(node);
462
+ if (n.kind === "element" || n.kind === "fragment") {
463
+ const el = n;
464
+ el.children.length = 0;
465
+ const t = new ServerText(text);
466
+ t.parent = el;
467
+ el.children.push(t);
468
+ } else if (n.kind === "text") {
469
+ n.data = text;
459
470
  }
460
- return instance2;
461
471
  }
462
- if (graphNode.type === "text") {
463
- const text = String(graphNode.getProp("text") ?? "");
464
- const el2 = dom.createElement("span");
465
- const textNode = dom.createTextNode(text);
466
- dom.appendChild(el2, textNode);
467
- applyNodeProps(ctx, graphNode, el2);
468
- const instance2 = new NodeInstance(graphNode, el2);
469
- ctx.instances.set(graphNode.id, instance2);
470
- wireEvents(dom, graph, graphNode, el2, instance2);
471
- if (graphNode.stateRefs.length !== 0) {
472
- wireSignalBindings(ctx, graphNode, instance2, textUpdate(dom, el2, textNode));
472
+ getTextContent(node) {
473
+ const n = asServer(node);
474
+ if (n.kind === "text") return n.data;
475
+ if (n.kind === "element" || n.kind === "fragment") {
476
+ let out = "";
477
+ for (const c of n.children) {
478
+ out += this.getTextContent(c) ?? "";
479
+ }
480
+ return out;
473
481
  }
474
- dom.appendChild(parentDom, el2);
475
- return instance2;
482
+ return null;
476
483
  }
477
- if (graphNode.type === "heading") {
478
- const level = graphNode.getProp("level") ?? 1;
479
- const tag2 = `h${level}`;
480
- const el2 = dom.createElement(tag2);
481
- const text = String(graphNode.getProp("text") ?? "");
482
- dom.setTextContent(el2, text);
483
- applyNodeProps(ctx, graphNode, el2);
484
- const instance2 = new NodeInstance(graphNode, el2);
485
- ctx.instances.set(graphNode.id, instance2);
486
- wireEvents(dom, graph, graphNode, el2, instance2);
487
- if (graphNode.stateRefs.length !== 0) {
488
- wireSignalBindings(ctx, graphNode, instance2, headingUpdate(dom, el2));
489
- }
490
- dom.appendChild(parentDom, el2);
491
- return instance2;
484
+ // Server nodes never dispatch events — listeners are a no-op on the server.
485
+ addEventListener() {
492
486
  }
493
- if (graphNode.type === "input") {
494
- const el2 = dom.createElement("input");
495
- const inputType = String(graphNode.getProp("inputType") ?? "text");
496
- dom.setAttribute(el2, "type", inputType);
497
- const placeholder = graphNode.getProp("placeholder");
498
- if (placeholder !== void 0) dom.setAttribute(el2, "placeholder", String(placeholder));
499
- const value = graphNode.getProp("value");
500
- if (value !== void 0) dom.setProperty(el2, "value", String(value));
501
- applyNodeProps(ctx, graphNode, el2);
502
- const instance2 = new NodeInstance(graphNode, el2);
503
- ctx.instances.set(graphNode.id, instance2);
504
- wireEvents(dom, graph, graphNode, el2, instance2);
505
- if (graphNode.stateRefs.length !== 0) {
506
- wireSignalBindings(ctx, graphNode, instance2, inputUpdate(dom, el2));
507
- }
508
- dom.appendChild(parentDom, el2);
509
- return instance2;
487
+ removeEventListener() {
510
488
  }
511
- if (graphNode.type === "image") {
512
- const el2 = dom.createElement("img");
513
- const src = graphNode.getProp("src");
514
- const alt = graphNode.getProp("alt");
515
- if (src !== void 0) dom.setAttribute(el2, "src", String(src));
516
- if (alt !== void 0) dom.setAttribute(el2, "alt", String(alt));
517
- const width = graphNode.getProp("width");
518
- const height = graphNode.getProp("height");
519
- if (width !== void 0) dom.setAttribute(el2, "width", String(width));
520
- if (height !== void 0) dom.setAttribute(el2, "height", String(height));
521
- applyNodeProps(ctx, graphNode, el2);
522
- const instance2 = new NodeInstance(graphNode, el2);
523
- ctx.instances.set(graphNode.id, instance2);
524
- dom.appendChild(parentDom, el2);
525
- return instance2;
489
+ querySelector() {
490
+ return null;
526
491
  }
527
- if (graphNode.type === "link") {
528
- const el2 = dom.createElement("a");
529
- const href = graphNode.getProp("href");
530
- const label = graphNode.getProp("label");
531
- const external = graphNode.getProp("external");
532
- if (href !== void 0) dom.setAttribute(el2, "href", String(href));
533
- if (label !== void 0) dom.setTextContent(el2, String(label));
534
- if (external === true) {
535
- dom.setAttribute(el2, "target", "_blank");
536
- dom.setAttribute(el2, "rel", "noopener noreferrer");
537
- }
538
- applyNodeProps(ctx, graphNode, el2);
539
- const instance2 = new NodeInstance(graphNode, el2);
540
- ctx.instances.set(graphNode.id, instance2);
541
- wireEvents(dom, graph, graphNode, el2, instance2);
542
- dom.appendChild(parentDom, el2);
543
- return instance2;
492
+ querySelectorAll() {
493
+ return [];
544
494
  }
545
- if (graphNode.type === "button") {
546
- const el2 = dom.createElement("button");
547
- const label = graphNode.getProp("label");
548
- if (label !== void 0) dom.setTextContent(el2, String(label));
549
- const disabled = graphNode.getProp("disabled");
550
- if (disabled === true) dom.setAttribute(el2, "disabled", "");
551
- applyNodeProps(ctx, graphNode, el2);
552
- const instance2 = new NodeInstance(graphNode, el2);
553
- ctx.instances.set(graphNode.id, instance2);
554
- wireEvents(dom, graph, graphNode, el2, instance2);
555
- if (graphNode.stateRefs.length !== 0) {
556
- wireSignalBindings(ctx, graphNode, instance2, buttonUpdate(dom, el2));
557
- }
558
- dom.appendChild(parentDom, el2);
559
- return instance2;
495
+ getElementById() {
496
+ return null;
560
497
  }
561
- if (graphNode.type === "reactive-list" || graphNode.type === "conditional") {
562
- const tag2 = resolveTag(graphNode.type);
563
- const el2 = dom.createElement(tag2);
564
- applyNodeProps(ctx, graphNode, el2);
565
- const instance2 = new NodeInstance(graphNode, el2);
566
- ctx.instances.set(graphNode.id, instance2);
567
- for (const child of graphNode.children) {
568
- const childInstance = mountNode(ctx, child, el2);
569
- instance2.addChild(childInstance);
498
+ focus() {
499
+ }
500
+ body() {
501
+ return null;
502
+ }
503
+ activeElement() {
504
+ return null;
505
+ }
506
+ contains(_ancestor, _node) {
507
+ return false;
508
+ }
509
+ matches(_element, _selector) {
510
+ return false;
511
+ }
512
+ isElement(node) {
513
+ return asServer(node).kind === "element";
514
+ }
515
+ isTextNode(node) {
516
+ return asServer(node).kind === "text";
517
+ }
518
+ tagName(element) {
519
+ return element.tagName;
520
+ }
521
+ parentNode(node) {
522
+ return asServer(node).parent ?? null;
523
+ }
524
+ nextSibling(node) {
525
+ const n = asServer(node);
526
+ const parent = n.parent;
527
+ if (parent === null) return null;
528
+ const idx = parent.children.indexOf(n);
529
+ if (idx === -1 || idx + 1 >= parent.children.length) return null;
530
+ return parent.children[idx + 1];
531
+ }
532
+ firstChild(node) {
533
+ const n = asServer(node);
534
+ if (n.kind === "element" || n.kind === "fragment") {
535
+ const el = n;
536
+ return el.children[0] ?? null;
570
537
  }
571
- dom.appendChild(parentDom, el2);
572
- wireReactiveList(ctx, graphNode, instance2, el2);
573
- return instance2;
538
+ return null;
574
539
  }
575
- const tag = resolveTag(graphNode.type);
576
- const el = dom.createElement(tag);
577
- applyNodeProps(ctx, graphNode, el);
578
- if (graphNode.type === "list-item") {
579
- const itemKey = graphNode.getProp("key");
580
- if (itemKey !== void 0) {
581
- dom.setAttribute(el, "data-streetui-key", String(itemKey));
540
+ childNodes(node) {
541
+ const n = asServer(node);
542
+ if (n.kind === "element" || n.kind === "fragment") {
543
+ return n.children;
582
544
  }
545
+ return [];
583
546
  }
584
- const instance = new NodeInstance(graphNode, el);
585
- ctx.instances.set(graphNode.id, instance);
586
- if (graphNode.type === "form") {
587
- wireEvents(dom, graph, graphNode, el, instance);
547
+ // ── Server-only ────────────────────────────────────────────────────────────
548
+ /** Serialize a node's children ("inner HTML") to an HTML string. */
549
+ serializeInner(node) {
550
+ const n = asServer(node);
551
+ if (n.kind === "element" || n.kind === "fragment") {
552
+ return serializeChildren(n);
553
+ }
554
+ return "";
588
555
  }
589
- for (const child of graphNode.children) {
590
- const childInstance = mountNode(ctx, child, el);
591
- instance.addChild(childInstance);
556
+ /** Serialize a node (including itself) to an HTML string. */
557
+ serializeOuter(node) {
558
+ return serializeServerNode(asServer(node));
592
559
  }
593
- dom.appendChild(parentDom, el);
594
- return instance;
560
+ };
561
+
562
+ // ../dom/src/focus.ts
563
+ var FOCUSABLE_SELECTOR = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
564
+ function focusById(dom, root, id) {
565
+ const el = dom.querySelector(root, `[id="${id}"]`);
566
+ if (el === null) return false;
567
+ dom.focus(el);
568
+ return true;
595
569
  }
596
- function textUpdate(dom, el, textNode) {
597
- return (propKey, value) => {
598
- if (propKey === "text") {
599
- dom.setTextContent(textNode, String(value ?? ""));
600
- } else {
601
- applyProp(dom, el, propKey, value);
602
- }
603
- };
570
+ function focusFirst(dom, container, selector = FOCUSABLE_SELECTOR) {
571
+ const el = dom.querySelector(container, selector);
572
+ if (el === null) return false;
573
+ dom.focus(el);
574
+ return true;
604
575
  }
605
- function headingUpdate(dom, el) {
606
- return (propKey, value) => {
607
- if (propKey === "text") {
608
- dom.setTextContent(el, String(value ?? ""));
609
- } else {
610
- applyProp(dom, el, propKey, value);
611
- }
612
- };
576
+ function getFocusable(dom, container, selector = FOCUSABLE_SELECTOR) {
577
+ return Array.from(dom.querySelectorAll(container, selector)).filter(
578
+ (el) => dom.matches(el, selector)
579
+ );
613
580
  }
614
- function inputUpdate(dom, el) {
615
- return (propKey, value) => {
616
- if (propKey === "value") {
617
- dom.setProperty(el, "value", String(value ?? ""));
618
- } else {
619
- applyProp(dom, el, propKey, value);
581
+ function saveFocus(dom) {
582
+ return dom.activeElement();
583
+ }
584
+ function restoreFocus(dom, saved) {
585
+ if (saved !== null) dom.focus(saved);
586
+ }
587
+ function focusInitial(dom, container, initialFocusId) {
588
+ if (initialFocusId !== void 0 && focusById(dom, container, initialFocusId)) return;
589
+ focusFirst(dom, container);
590
+ }
591
+ function trapFocus(dom, container) {
592
+ const onKeydown = (event) => {
593
+ if (event.key !== "Tab") return;
594
+ const items = getFocusable(dom, container);
595
+ if (items.length === 0) {
596
+ event.preventDefault();
597
+ return;
598
+ }
599
+ const first = items[0];
600
+ const last = items[items.length - 1];
601
+ const active = dom.activeElement();
602
+ if (active === null || !dom.contains(container, active)) {
603
+ event.preventDefault();
604
+ dom.focus(first);
605
+ } else if (event.shiftKey && active === first) {
606
+ event.preventDefault();
607
+ dom.focus(last);
608
+ } else if (!event.shiftKey && active === last) {
609
+ event.preventDefault();
610
+ dom.focus(first);
620
611
  }
621
612
  };
613
+ dom.addEventListener(container, "keydown", onKeydown);
614
+ return () => dom.removeEventListener(container, "keydown", onKeydown);
622
615
  }
623
- function buttonUpdate(dom, el) {
624
- return (propKey, value) => {
625
- if (propKey === "label") {
626
- dom.setTextContent(el, String(value ?? ""));
627
- } else if (propKey === "disabled") {
628
- if (value === true) {
629
- dom.setAttribute(el, "disabled", "");
630
- } else {
631
- dom.removeAttribute(el, "disabled");
632
- }
633
- } else {
634
- applyProp(dom, el, propKey, value);
616
+ function containFocus(dom, container) {
617
+ const body = dom.body();
618
+ if (body === null) return () => {
619
+ };
620
+ const onFocusIn = (event) => {
621
+ const target = event.target;
622
+ if (target !== null && !dom.contains(container, target)) {
623
+ focusFirst(dom, container);
635
624
  }
636
625
  };
626
+ dom.addEventListener(body, "focusin", onFocusIn);
627
+ return () => dom.removeEventListener(body, "focusin", onFocusIn);
637
628
  }
638
- function applyNodeProps(ctx, graphNode, el) {
639
- const props = graphNode.props;
640
- for (const key in props) {
641
- if (!Object.hasOwn(props, key)) continue;
642
- if (SKIP_PROP_KEYS.has(key)) continue;
643
- applyProp(ctx.dom, el, key, props[key]);
644
- }
629
+ function onEscape(dom, target, handler) {
630
+ const onKeydown = (event) => {
631
+ if (event.key === "Escape") handler();
632
+ };
633
+ dom.addEventListener(target, "keydown", onKeydown);
634
+ return () => dom.removeEventListener(target, "keydown", onKeydown);
645
635
  }
646
- function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
647
- if (graphNode.stateRefs.length === 0) return;
648
- for (const stateRef of graphNode.stateRefs) {
649
- const signalKey = `__signal__${stateRef.signalId}`;
650
- const maybeSig = ctx.graph.getHandler(signalKey);
651
- if (maybeSig === void 0 || typeof maybeSig.subscribe !== "function") continue;
652
- const unsub = maybeSig.subscribe((value) => {
653
- onUpdate(stateRef.propKey, value);
654
- });
655
- instance.trackCleanup(unsub);
656
- }
636
+
637
+ // ../renderer/src/tag-map.ts
638
+ var TAG_MAP = {
639
+ application: "div",
640
+ page: "div",
641
+ section: "section",
642
+ container: "div",
643
+ heading: "h1",
644
+ text: "span",
645
+ button: "button",
646
+ input: "input",
647
+ form: "form",
648
+ list: "ul",
649
+ "list-item": "li",
650
+ image: "img",
651
+ link: "a",
652
+ component: "div",
653
+ slot: "div",
654
+ fragment: "div",
655
+ "reactive-list": "ul",
656
+ // A portal renders as a neutral inline anchor <div> at its declaration site;
657
+ // its children are relocated to a document.body container on the browser
658
+ // (see the portal branch in mount.ts). On the server (no body) it renders
659
+ // inline, so the anchor tag is what SSR/hydration positionally match on.
660
+ portal: "div"
661
+ };
662
+ function resolveTag(type) {
663
+ return TAG_MAP[type] ?? "div";
657
664
  }
658
- function wireReactiveList(ctx, graphNode, instance, el) {
659
- const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
660
- const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
661
- if (plan === void 0 && build === void 0) return;
662
- for (const stateRef of graphNode.stateRefs) {
663
- if (stateRef.propKey !== "items") continue;
664
- const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
665
- if (sig === void 0 || typeof sig.subscribe !== "function") continue;
666
- const unsub = sig.subscribe((value) => {
667
- if (plan !== void 0) {
668
- reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
665
+
666
+ // ../renderer/src/patch.ts
667
+ function patchNode(ctx, graphNode, propKey, newValue) {
668
+ const instance = ctx.instances.get(graphNode.id);
669
+ if (instance === void 0) return;
670
+ const domNode = instance.domNode;
671
+ if (!ctx.dom.isElement(domNode)) return;
672
+ const oldValue = graphNode.getProp(propKey);
673
+ switch (propKey) {
674
+ case "text":
675
+ if (!Object.is(oldValue, newValue)) {
676
+ ctx.dom.setTextContent(domNode, String(newValue ?? ""));
677
+ graphNode.setProp("text", String(newValue ?? ""));
678
+ }
679
+ break;
680
+ case "label":
681
+ if (!Object.is(oldValue, newValue)) {
682
+ ctx.dom.setTextContent(domNode, String(newValue ?? ""));
683
+ graphNode.setProp("label", String(newValue ?? ""));
684
+ }
685
+ break;
686
+ case "disabled":
687
+ if (newValue === true) {
688
+ ctx.dom.setAttribute(domNode, "disabled", "");
669
689
  } else {
670
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
690
+ ctx.dom.removeAttribute(domNode, "disabled");
671
691
  }
672
- });
673
- instance.trackCleanup(unsub);
674
- }
675
- }
676
- function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
677
- const oldInstances = [...listInstance.children];
678
- const result = reconcileChildrenByPlan(
679
- ctx,
680
- listEl,
681
- oldInstances,
682
- plan,
683
- (node, parent) => mountNode(ctx, node, parent)
684
- );
685
- listInstance.children.length = 0;
686
- for (const inst of result.instances) listInstance.children.push(inst);
687
- for (const removed of result.removed) {
688
- forgetInstance(ctx, removed);
689
- ctx.graph.detachNode(removed.graphNode);
690
- }
691
- const adopted = new Set(result.instances.map((i) => i.graphNode));
692
- for (const node of result.built ?? []) {
693
- if (!adopted.has(node)) ctx.graph.detachNode(node);
694
- }
695
- for (const child of [...listNode.children]) listNode.removeChild(child);
696
- for (const inst of result.instances) listNode.appendChild(inst.graphNode);
697
- }
698
- function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
699
- const oldInstances = [...listInstance.children];
700
- const result = reconcileChildren(
701
- ctx,
702
- listEl,
703
- oldInstances,
704
- newNodes,
705
- (node, parent) => mountNode(ctx, node, parent)
706
- );
707
- listInstance.children.length = 0;
708
- for (const inst of result.instances) listInstance.children.push(inst);
709
- for (const removed of result.removed) {
710
- forgetInstance(ctx, removed);
711
- ctx.graph.detachNode(removed.graphNode);
712
- }
713
- const adopted = new Set(result.instances.map((i) => i.graphNode));
714
- for (const built of newNodes) {
715
- if (!adopted.has(built)) ctx.graph.detachNode(built);
716
- }
717
- for (const child of [...listNode.children]) listNode.removeChild(child);
718
- for (const inst of result.instances) listNode.appendChild(inst.graphNode);
719
- }
720
- function forgetInstance(ctx, instance) {
721
- ctx.instances.delete(instance.graphNode.id);
722
- for (const child of instance.children) forgetInstance(ctx, child);
723
- }
724
-
725
- // ../dom/src/server-node.ts
726
- var ServerStyle = class {
727
- declarations = /* @__PURE__ */ new Map();
728
- setProperty(name, value) {
729
- this.declarations.set(name, value);
730
- }
731
- get isEmpty() {
732
- return this.declarations.size === 0;
733
- }
734
- toCss() {
735
- return [...this.declarations.entries()].map(([k, v]) => `${k}: ${v}`).join("; ");
736
- }
737
- };
738
- var ServerText = class {
739
- kind = "text";
740
- parent = null;
741
- data;
742
- constructor(data) {
743
- this.data = data;
744
- }
745
- };
746
- var ServerComment = class {
747
- kind = "comment";
748
- parent = null;
749
- data;
750
- constructor(data) {
751
- this.data = data;
752
- }
753
- };
754
- var ServerFragment = class {
755
- kind = "fragment";
756
- parent = null;
757
- children = [];
758
- };
759
- var ServerElement = class {
760
- kind = "element";
761
- parent = null;
762
- tagName;
763
- attributes = /* @__PURE__ */ new Map();
764
- children = [];
765
- // Lazily-allocated stores. On the 10k-row SSR corpus ~0% of elements carry JS
766
- // properties or inline styles (measured, §5: 1 of 80,029 elements uses
767
- // `properties`, 0 use `style`), so eagerly allocating a `properties` Map plus
768
- // a `ServerStyle` (which itself holds a Map) per element wasted ~240k
769
- // allocations per /users render — all in the dominant mount phase. These are
770
- // created on first WRITE via the `properties`/`style` getters; the serializer
771
- // reads the raw `_properties`/`_style` fields so a READ never forces an
772
- // allocation. Output is byte-identical: an unset store previously serialized
773
- // to nothing (empty `properties.has(...)` / `style.isEmpty`), and a null store
774
- // is skipped the same way.
775
- _properties = null;
776
- _style = null;
777
- constructor(tagName) {
778
- this.tagName = tagName.toLowerCase();
692
+ graphNode.setProp("disabled", Boolean(newValue));
693
+ break;
694
+ case "value":
695
+ if (!Object.is(oldValue, newValue)) {
696
+ ctx.dom.setProperty(domNode, "value", String(newValue ?? ""));
697
+ graphNode.setProp("value", String(newValue ?? ""));
698
+ }
699
+ break;
700
+ default:
701
+ patchProp(ctx.dom, domNode, propKey, oldValue, newValue);
702
+ graphNode.setProp(propKey, newValue);
703
+ break;
779
704
  }
780
- /** JS properties set via `setProperty` (e.g. input `value`, `checked`). Allocated on first access. */
781
- get properties() {
782
- return this._properties ??= /* @__PURE__ */ new Map();
705
+ }
706
+
707
+ // ../renderer/src/reconciliation.ts
708
+ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
709
+ const oldByKey = /* @__PURE__ */ new Map();
710
+ for (const inst of oldInstances) {
711
+ const key = inst.graphNode.key ?? inst.graphNode.id;
712
+ oldByKey.set(key, inst);
783
713
  }
784
- /** Inline-style holder mirroring `element.style`. Allocated on first access. */
785
- get style() {
786
- return this._style ??= new ServerStyle();
714
+ const newInstances = [];
715
+ const usedKeys = /* @__PURE__ */ new Set();
716
+ for (const newNode of newNodes) {
717
+ const key = newNode.key ?? newNode.id;
718
+ const existing = oldByKey.get(key);
719
+ if (existing !== void 0) {
720
+ usedKeys.add(key);
721
+ const oldSig = existing.graphNode.getProp("_sig");
722
+ const newSig = newNode.getProp("_sig");
723
+ patchExistingInstance(ctx, existing, newNode);
724
+ if (!Object.is(oldSig, newSig)) {
725
+ reconcileItemChildren(ctx, existing, newNode, mountFn);
726
+ }
727
+ newInstances.push(existing);
728
+ } else {
729
+ const inst = mountFn(newNode, parentDom);
730
+ newInstances.push(inst);
731
+ }
787
732
  }
788
- };
789
- var VOID_ELEMENTS = /* @__PURE__ */ new Set([
790
- "area",
791
- "base",
792
- "br",
793
- "col",
794
- "embed",
795
- "hr",
796
- "img",
797
- "input",
798
- "link",
799
- "meta",
800
- "param",
801
- "source",
802
- "track",
803
- "wbr"
804
- ]);
805
- var SERIALIZED_PROPERTIES = {
806
- value: "attr",
807
- checked: "boolean",
808
- selected: "boolean"
809
- };
810
- var SERIALIZED_PROPERTY_ENTRIES = Object.entries(SERIALIZED_PROPERTIES);
811
- var TEXT_SPECIAL = /[&<>]/;
812
- var ATTR_SPECIAL = /[&<>"]/;
813
- function escapeHtmlText(value) {
814
- if (!TEXT_SPECIAL.test(value)) return value;
815
- let out = "";
816
- let last = 0;
817
- for (let i = 0; i < value.length; i++) {
818
- let esc;
819
- switch (value.charCodeAt(i)) {
820
- case 38:
821
- esc = "&amp;";
822
- break;
823
- // &
824
- case 60:
825
- esc = "&lt;";
826
- break;
827
- // <
828
- case 62:
829
- esc = "&gt;";
830
- break;
831
- // >
832
- default:
833
- continue;
733
+ const removed = [];
734
+ for (const inst of oldInstances) {
735
+ const key = inst.graphNode.key ?? inst.graphNode.id;
736
+ if (!usedKeys.has(key)) {
737
+ removed.push(inst);
834
738
  }
835
- out += value.slice(last, i) + esc;
836
- last = i + 1;
837
739
  }
838
- return out + value.slice(last);
740
+ for (const inst of removed) {
741
+ const parent = ctx.dom.parentNode(inst.domNode);
742
+ if (parent !== null) {
743
+ ctx.dom.removeChild(parent, inst.domNode);
744
+ }
745
+ inst.dispose();
746
+ }
747
+ reorderDom(ctx, parentDom, newInstances);
748
+ return { instances: newInstances, removed };
839
749
  }
840
- function escapeHtmlAttr(value) {
841
- if (!ATTR_SPECIAL.test(value)) return value;
842
- let out = "";
843
- let last = 0;
844
- for (let i = 0; i < value.length; i++) {
845
- let esc;
846
- switch (value.charCodeAt(i)) {
847
- case 38:
848
- esc = "&amp;";
849
- break;
850
- // &
851
- case 60:
852
- esc = "&lt;";
853
- break;
854
- // <
855
- case 62:
856
- esc = "&gt;";
857
- break;
858
- // >
859
- case 34:
860
- esc = "&quot;";
861
- break;
862
- // "
863
- default:
864
- continue;
750
+ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
751
+ const oldByKey = /* @__PURE__ */ new Map();
752
+ for (const inst of oldInstances) {
753
+ oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
754
+ }
755
+ const newInstances = [];
756
+ const usedKeys = /* @__PURE__ */ new Set();
757
+ const built = [];
758
+ for (const entry of plan) {
759
+ const existing = oldByKey.get(entry.key);
760
+ if (existing !== void 0) {
761
+ usedKeys.add(entry.key);
762
+ const oldItem = existing.graphNode.getProp("_item");
763
+ if (!Object.is(oldItem, entry.item)) {
764
+ const newSig = entry.sig();
765
+ const oldSig = existing.graphNode.getProp("_sig");
766
+ if (!Object.is(oldSig, newSig)) {
767
+ const freshNode = entry.build();
768
+ built.push(freshNode);
769
+ patchExistingInstance(ctx, existing, freshNode);
770
+ reconcileItemChildren(ctx, existing, freshNode, mountFn);
771
+ existing.graphNode.setProp("_sig", newSig);
772
+ }
773
+ existing.graphNode.setProp("_item", entry.item);
774
+ }
775
+ newInstances.push(existing);
776
+ } else {
777
+ const freshNode = entry.build();
778
+ built.push(freshNode);
779
+ const inst = mountFn(freshNode, parentDom);
780
+ newInstances.push(inst);
865
781
  }
866
- out += value.slice(last, i) + esc;
867
- last = i + 1;
868
782
  }
869
- return out + value.slice(last);
783
+ const removed = [];
784
+ for (const inst of oldInstances) {
785
+ const key = inst.graphNode.key ?? inst.graphNode.id;
786
+ if (!usedKeys.has(key)) removed.push(inst);
787
+ }
788
+ for (const inst of removed) {
789
+ const parent = ctx.dom.parentNode(inst.domNode);
790
+ if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
791
+ inst.dispose();
792
+ }
793
+ reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
794
+ return { instances: newInstances, removed, built };
870
795
  }
871
- function serializeAttributes(el) {
872
- const parts = [];
873
- for (const [name, value] of el.attributes) {
874
- if (value === "") {
875
- parts.push(` ${name}`);
796
+ function reorderDomMinimal(ctx, parentDom, oldInstances, newInstances) {
797
+ const n = newInstances.length;
798
+ if (n === 0) return;
799
+ const oldIndexOf = /* @__PURE__ */ new Map();
800
+ for (let i = 0; i < oldInstances.length; i++) oldIndexOf.set(oldInstances[i], i);
801
+ const source = new Array(n);
802
+ let moved = false;
803
+ let lastSeen = -1;
804
+ for (let i = 0; i < n; i++) {
805
+ const oi = oldIndexOf.get(newInstances[i]);
806
+ if (oi === void 0) {
807
+ source[i] = -1;
808
+ moved = true;
876
809
  } else {
877
- parts.push(` ${name}="${escapeHtmlAttr(value)}"`);
810
+ source[i] = oi;
811
+ if (oi < lastSeen) moved = true;
812
+ else lastSeen = oi;
878
813
  }
879
814
  }
880
- const props = el._properties;
881
- if (props !== null) {
882
- for (const [name, kind] of SERIALIZED_PROPERTY_ENTRIES) {
883
- if (!props.has(name)) continue;
884
- if (el.attributes.has(name)) continue;
885
- const raw = props.get(name);
886
- if (kind === "boolean") {
887
- if (raw === true) parts.push(` ${name}`);
888
- } else {
889
- if (raw !== void 0 && raw !== null) {
890
- parts.push(` ${name}="${escapeHtmlAttr(String(raw))}"`);
891
- }
815
+ if (!moved) return;
816
+ const keep = longestIncreasingSubsequence(source);
817
+ let refNode = null;
818
+ for (let i = n - 1; i >= 0; i--) {
819
+ const domNode = newInstances[i].domNode;
820
+ if (source[i] === -1 || !keep.has(i)) {
821
+ if (ctx.dom.nextSibling(domNode) !== refNode) {
822
+ ctx.dom.insertBefore(parentDom, domNode, refNode);
892
823
  }
893
824
  }
825
+ refNode = domNode;
826
+ }
827
+ }
828
+ function longestIncreasingSubsequence(source) {
829
+ const keep = /* @__PURE__ */ new Set();
830
+ const n = source.length;
831
+ const tails = [];
832
+ const prev = new Array(n).fill(-1);
833
+ for (let i = 0; i < n; i++) {
834
+ const v = source[i];
835
+ if (v < 0) continue;
836
+ let lo = 0;
837
+ let hi = tails.length;
838
+ while (lo < hi) {
839
+ const mid = lo + hi >> 1;
840
+ if (source[tails[mid]] < v) lo = mid + 1;
841
+ else hi = mid;
842
+ }
843
+ if (lo > 0) prev[i] = tails[lo - 1];
844
+ tails[lo] = i;
845
+ }
846
+ let idx = tails.length > 0 ? tails[tails.length - 1] : -1;
847
+ while (idx >= 0) {
848
+ keep.add(idx);
849
+ idx = prev[idx];
850
+ }
851
+ return keep;
852
+ }
853
+ function reconcileItemChildren(ctx, itemInstance, newItemNode, mountFn) {
854
+ const el = itemInstance.domNode;
855
+ if (!ctx.dom.isElement(el)) return;
856
+ const oldChildren = [...itemInstance.children];
857
+ const newChildNodes = [...newItemNode.children];
858
+ const nextChildren = [];
859
+ const kept = /* @__PURE__ */ new Set();
860
+ for (let i = 0; i < newChildNodes.length; i++) {
861
+ const newChild = newChildNodes[i];
862
+ const oldChild = oldChildren[i];
863
+ if (oldChild !== void 0 && oldChild.graphNode.type === newChild.type) {
864
+ patchExistingInstance(ctx, oldChild, newChild);
865
+ reconcileItemChildren(ctx, oldChild, newChild, mountFn);
866
+ nextChildren.push(oldChild);
867
+ kept.add(oldChild);
868
+ } else {
869
+ itemInstance.graphNode.appendChild(newChild);
870
+ const inst = mountFn(newChild, el);
871
+ nextChildren.push(inst);
872
+ }
894
873
  }
895
- const style = el._style;
896
- if (style !== null && !style.isEmpty && !el.attributes.has("style")) {
897
- parts.push(` style="${escapeHtmlAttr(style.toCss())}"`);
874
+ for (const old of oldChildren) {
875
+ if (kept.has(old)) continue;
876
+ const parent = ctx.dom.parentNode(old.domNode);
877
+ if (parent !== null) ctx.dom.removeChild(parent, old.domNode);
878
+ old.dispose();
879
+ forgetInstanceTree(ctx, old);
880
+ ctx.graph.detachNode(old.graphNode);
898
881
  }
899
- return parts.join("");
882
+ reorderDom(ctx, el, nextChildren);
883
+ itemInstance.children.length = 0;
884
+ for (const c of nextChildren) itemInstance.children.push(c);
885
+ for (const c of [...itemInstance.graphNode.children]) {
886
+ itemInstance.graphNode.removeChild(c);
887
+ }
888
+ for (const c of nextChildren) itemInstance.graphNode.appendChild(c.graphNode);
900
889
  }
901
- function serializeServerNode(node) {
902
- switch (node.kind) {
903
- case "text":
904
- return escapeHtmlText(node.data);
905
- case "comment":
906
- return `<!--${node.data}-->`;
907
- case "fragment":
908
- return serializeChildren(node);
909
- case "element": {
910
- const el = node;
911
- const tag = el.tagName;
912
- const attrs = serializeAttributes(el);
913
- if (VOID_ELEMENTS.has(tag)) {
914
- return `<${tag}${attrs}>`;
915
- }
916
- return `<${tag}${attrs}>${serializeChildren(el)}</${tag}>`;
890
+ function reorderDom(ctx, parentDom, instances) {
891
+ let referenceNode = null;
892
+ for (let i = instances.length - 1; i >= 0; i--) {
893
+ const inst = instances[i];
894
+ if (inst === void 0) continue;
895
+ const domNode = inst.domNode;
896
+ const currentNext = ctx.dom.nextSibling(domNode);
897
+ if (currentNext !== referenceNode) {
898
+ ctx.dom.insertBefore(parentDom, domNode, referenceNode);
917
899
  }
900
+ referenceNode = domNode;
918
901
  }
919
902
  }
920
- function serializeChildren(node) {
921
- let out = "";
922
- for (const child of node.children) {
923
- out += serializeServerNode(child);
903
+ function forgetInstanceTree(ctx, instance) {
904
+ ctx.instances.delete(instance.graphNode.id);
905
+ for (const child of instance.children) forgetInstanceTree(ctx, child);
906
+ }
907
+ function patchExistingInstance(ctx, instance, newNode) {
908
+ const oldNode = instance.graphNode;
909
+ for (const [key, newVal] of Object.entries(newNode.props)) {
910
+ const oldVal = oldNode.getProp(key);
911
+ if (!Object.is(oldVal, newVal)) {
912
+ patchNode(ctx, instance.graphNode, key, newVal);
913
+ }
924
914
  }
925
- return out;
926
915
  }
927
916
 
928
- // ../dom/src/server-adapter.ts
929
- function asServer(node) {
930
- return node;
931
- }
932
- function asParent(node) {
933
- return node;
917
+ // ../renderer/src/mount.ts
918
+ var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
919
+ "text",
920
+ "label",
921
+ "level",
922
+ "inputType",
923
+ "src",
924
+ "alt",
925
+ "href",
926
+ "external",
927
+ "value",
928
+ "placeholder",
929
+ "disabled",
930
+ "_renderKey",
931
+ "key",
932
+ "name"
933
+ ]);
934
+ function mountGraph(ctx) {
935
+ return mountNode(ctx, ctx.graph.root, ctx.container);
934
936
  }
935
- var ServerDOMAdapter = class {
936
- createElement(tag, _ns) {
937
- return new ServerElement(tag);
938
- }
939
- createTextNode(data) {
940
- return new ServerText(data);
941
- }
942
- createComment(data) {
943
- return new ServerComment(data);
944
- }
945
- createFragment() {
946
- return new ServerFragment();
947
- }
948
- appendChild(parent, child) {
949
- const p = asParent(parent);
950
- const c = asServer(child);
951
- this._detach(c);
952
- c.parent = p;
953
- p.children.push(c);
954
- }
955
- insertBefore(parent, child, reference) {
956
- const p = asParent(parent);
957
- const c = asServer(child);
958
- this._detach(c);
959
- c.parent = p;
960
- if (reference === null) {
961
- p.children.push(c);
962
- return;
937
+ function mountNode(ctx, graphNode, parentDom) {
938
+ const { dom, graph } = ctx;
939
+ const staticHTML = ctx.staticHTML;
940
+ if (staticHTML !== void 0 && dom.createRawHTML !== void 0) {
941
+ const precomputed = staticHTML.get(graphNode.id);
942
+ if (precomputed !== void 0) {
943
+ const raw = dom.createRawHTML(precomputed);
944
+ dom.appendChild(parentDom, raw);
945
+ const instance2 = new NodeInstance(graphNode, raw);
946
+ ctx.instances.set(graphNode.id, instance2);
947
+ return instance2;
963
948
  }
964
- const ref = asServer(reference);
965
- const idx = p.children.indexOf(ref);
966
- if (idx === -1) p.children.push(c);
967
- else p.children.splice(idx, 0, c);
968
949
  }
969
- removeChild(parent, child) {
970
- const p = asParent(parent);
971
- const c = asServer(child);
972
- const idx = p.children.indexOf(c);
973
- if (idx !== -1) {
974
- p.children.splice(idx, 1);
975
- c.parent = null;
950
+ if (graphNode.type === "application") {
951
+ const instance2 = new NodeInstance(graphNode, parentDom);
952
+ ctx.instances.set(graphNode.id, instance2);
953
+ for (const child of graphNode.children) {
954
+ const childInstance = mountNode(ctx, child, parentDom);
955
+ instance2.addChild(childInstance);
976
956
  }
957
+ return instance2;
977
958
  }
978
- replaceChild(parent, newChild, oldChild) {
979
- const p = asParent(parent);
980
- const nc = asServer(newChild);
981
- const oc = asServer(oldChild);
982
- const idx = p.children.indexOf(oc);
983
- if (idx === -1) return;
984
- this._detach(nc);
985
- nc.parent = p;
986
- p.children.splice(idx, 1, nc);
987
- oc.parent = null;
988
- }
989
- _detach(node) {
990
- if (node.parent !== null) {
991
- const siblings = node.parent.children;
992
- const idx = siblings.indexOf(node);
993
- if (idx !== -1) siblings.splice(idx, 1);
994
- node.parent = null;
959
+ if (graphNode.type === "text") {
960
+ const text = String(graphNode.getProp("text") ?? "");
961
+ const el2 = dom.createElement("span");
962
+ const textNode = dom.createTextNode(text);
963
+ dom.appendChild(el2, textNode);
964
+ applyNodeProps(ctx, graphNode, el2);
965
+ const instance2 = new NodeInstance(graphNode, el2);
966
+ ctx.instances.set(graphNode.id, instance2);
967
+ wireEvents(dom, graph, graphNode, el2, instance2);
968
+ if (graphNode.stateRefs.length !== 0) {
969
+ wireSignalBindings(ctx, graphNode, instance2, textUpdate(dom, el2, textNode));
995
970
  }
971
+ dom.appendChild(parentDom, el2);
972
+ return instance2;
996
973
  }
997
- setAttribute(element, name, value) {
998
- element.attributes.set(name, value);
974
+ if (graphNode.type === "heading") {
975
+ const level = graphNode.getProp("level") ?? 1;
976
+ const tag2 = `h${level}`;
977
+ const el2 = dom.createElement(tag2);
978
+ const text = String(graphNode.getProp("text") ?? "");
979
+ dom.setTextContent(el2, text);
980
+ applyNodeProps(ctx, graphNode, el2);
981
+ const instance2 = new NodeInstance(graphNode, el2);
982
+ ctx.instances.set(graphNode.id, instance2);
983
+ wireEvents(dom, graph, graphNode, el2, instance2);
984
+ if (graphNode.stateRefs.length !== 0) {
985
+ wireSignalBindings(ctx, graphNode, instance2, headingUpdate(dom, el2));
986
+ }
987
+ dom.appendChild(parentDom, el2);
988
+ return instance2;
999
989
  }
1000
- removeAttribute(element, name) {
1001
- element.attributes.delete(name);
990
+ if (graphNode.type === "input") {
991
+ const el2 = dom.createElement("input");
992
+ const inputType = String(graphNode.getProp("inputType") ?? "text");
993
+ dom.setAttribute(el2, "type", inputType);
994
+ const placeholder = graphNode.getProp("placeholder");
995
+ if (placeholder !== void 0) dom.setAttribute(el2, "placeholder", String(placeholder));
996
+ const value = graphNode.getProp("value");
997
+ if (value !== void 0) dom.setProperty(el2, "value", String(value));
998
+ applyNodeProps(ctx, graphNode, el2);
999
+ const instance2 = new NodeInstance(graphNode, el2);
1000
+ ctx.instances.set(graphNode.id, instance2);
1001
+ wireEvents(dom, graph, graphNode, el2, instance2);
1002
+ if (graphNode.stateRefs.length !== 0) {
1003
+ wireSignalBindings(ctx, graphNode, instance2, inputUpdate(dom, el2));
1004
+ }
1005
+ dom.appendChild(parentDom, el2);
1006
+ return instance2;
1002
1007
  }
1003
- getAttribute(element, name) {
1004
- return element.attributes.get(name) ?? null;
1008
+ if (graphNode.type === "image") {
1009
+ const el2 = dom.createElement("img");
1010
+ const src = graphNode.getProp("src");
1011
+ const alt = graphNode.getProp("alt");
1012
+ if (src !== void 0) dom.setAttribute(el2, "src", String(src));
1013
+ if (alt !== void 0) dom.setAttribute(el2, "alt", String(alt));
1014
+ const width = graphNode.getProp("width");
1015
+ const height = graphNode.getProp("height");
1016
+ if (width !== void 0) dom.setAttribute(el2, "width", String(width));
1017
+ if (height !== void 0) dom.setAttribute(el2, "height", String(height));
1018
+ applyNodeProps(ctx, graphNode, el2);
1019
+ const instance2 = new NodeInstance(graphNode, el2);
1020
+ ctx.instances.set(graphNode.id, instance2);
1021
+ dom.appendChild(parentDom, el2);
1022
+ return instance2;
1005
1023
  }
1006
- setProperty(element, name, value) {
1007
- element.properties.set(name, value);
1024
+ if (graphNode.type === "link") {
1025
+ const el2 = dom.createElement("a");
1026
+ const href = graphNode.getProp("href");
1027
+ const label = graphNode.getProp("label");
1028
+ const external = graphNode.getProp("external");
1029
+ if (href !== void 0) dom.setAttribute(el2, "href", String(href));
1030
+ if (label !== void 0) dom.setTextContent(el2, String(label));
1031
+ if (external === true) {
1032
+ dom.setAttribute(el2, "target", "_blank");
1033
+ dom.setAttribute(el2, "rel", "noopener noreferrer");
1034
+ }
1035
+ applyNodeProps(ctx, graphNode, el2);
1036
+ const instance2 = new NodeInstance(graphNode, el2);
1037
+ ctx.instances.set(graphNode.id, instance2);
1038
+ wireEvents(dom, graph, graphNode, el2, instance2);
1039
+ dom.appendChild(parentDom, el2);
1040
+ return instance2;
1008
1041
  }
1009
- setTextContent(node, text) {
1010
- const n = asServer(node);
1011
- if (n.kind === "element" || n.kind === "fragment") {
1012
- const el = n;
1013
- el.children.length = 0;
1014
- const t = new ServerText(text);
1015
- t.parent = el;
1016
- el.children.push(t);
1017
- } else if (n.kind === "text") {
1018
- n.data = text;
1042
+ if (graphNode.type === "button") {
1043
+ const el2 = dom.createElement("button");
1044
+ const label = graphNode.getProp("label");
1045
+ if (label !== void 0) dom.setTextContent(el2, String(label));
1046
+ const disabled = graphNode.getProp("disabled");
1047
+ if (disabled === true) dom.setAttribute(el2, "disabled", "");
1048
+ applyNodeProps(ctx, graphNode, el2);
1049
+ const instance2 = new NodeInstance(graphNode, el2);
1050
+ ctx.instances.set(graphNode.id, instance2);
1051
+ wireEvents(dom, graph, graphNode, el2, instance2);
1052
+ if (graphNode.stateRefs.length !== 0) {
1053
+ wireSignalBindings(ctx, graphNode, instance2, buttonUpdate(dom, el2));
1019
1054
  }
1055
+ dom.appendChild(parentDom, el2);
1056
+ return instance2;
1020
1057
  }
1021
- getTextContent(node) {
1022
- const n = asServer(node);
1023
- if (n.kind === "text") return n.data;
1024
- if (n.kind === "element" || n.kind === "fragment") {
1025
- let out = "";
1026
- for (const c of n.children) {
1027
- out += this.getTextContent(c) ?? "";
1028
- }
1029
- return out;
1058
+ if (graphNode.type === "reactive-list" || graphNode.type === "conditional") {
1059
+ const tag2 = resolveTag(graphNode.type);
1060
+ const el2 = dom.createElement(tag2);
1061
+ applyNodeProps(ctx, graphNode, el2);
1062
+ const instance2 = new NodeInstance(graphNode, el2);
1063
+ ctx.instances.set(graphNode.id, instance2);
1064
+ for (const child of graphNode.children) {
1065
+ const childInstance = mountNode(ctx, child, el2);
1066
+ instance2.addChild(childInstance);
1030
1067
  }
1031
- return null;
1032
- }
1033
- // Server nodes never dispatch events — listeners are a no-op on the server.
1034
- addEventListener() {
1035
- }
1036
- removeEventListener() {
1068
+ dom.appendChild(parentDom, el2);
1069
+ wireReactiveList(ctx, graphNode, instance2, el2);
1070
+ return instance2;
1037
1071
  }
1038
- querySelector() {
1039
- return null;
1072
+ if (graphNode.type === "portal") {
1073
+ const anchor = dom.createElement(resolveTag("portal"));
1074
+ dom.setAttribute(anchor, "data-streetui-portal", "");
1075
+ applyNodeProps(ctx, graphNode, anchor);
1076
+ const instance2 = new NodeInstance(graphNode, anchor);
1077
+ ctx.instances.set(graphNode.id, instance2);
1078
+ const body = dom.body();
1079
+ let target = anchor;
1080
+ if (body !== null) {
1081
+ const portalContainer = dom.createElement("div");
1082
+ dom.setAttribute(portalContainer, "data-streetui-portal-container", "");
1083
+ dom.appendChild(body, portalContainer);
1084
+ instance2.trackCleanup(() => dom.removeChild(body, portalContainer));
1085
+ target = portalContainer;
1086
+ }
1087
+ for (const child of graphNode.children) {
1088
+ instance2.addChild(mountNode(ctx, child, target));
1089
+ }
1090
+ dom.appendChild(parentDom, anchor);
1091
+ wireOverlayBehavior(ctx, graphNode, instance2, target);
1092
+ return instance2;
1040
1093
  }
1041
- querySelectorAll() {
1042
- return [];
1094
+ const tag = resolveTag(graphNode.type);
1095
+ const el = dom.createElement(tag);
1096
+ applyNodeProps(ctx, graphNode, el);
1097
+ if (graphNode.type === "list-item") {
1098
+ const itemKey = graphNode.getProp("key");
1099
+ if (itemKey !== void 0) {
1100
+ dom.setAttribute(el, "data-streetui-key", String(itemKey));
1101
+ }
1043
1102
  }
1044
- getElementById() {
1045
- return null;
1103
+ const instance = new NodeInstance(graphNode, el);
1104
+ ctx.instances.set(graphNode.id, instance);
1105
+ if (graphNode.type === "form") {
1106
+ wireEvents(dom, graph, graphNode, el, instance);
1046
1107
  }
1047
- focus() {
1108
+ for (const child of graphNode.children) {
1109
+ const childInstance = mountNode(ctx, child, el);
1110
+ instance.addChild(childInstance);
1048
1111
  }
1049
- isElement(node) {
1050
- return asServer(node).kind === "element";
1112
+ dom.appendChild(parentDom, el);
1113
+ return instance;
1114
+ }
1115
+ function textUpdate(dom, el, textNode) {
1116
+ return (propKey, value) => {
1117
+ if (propKey === "text") {
1118
+ dom.setTextContent(textNode, String(value ?? ""));
1119
+ } else {
1120
+ applyProp(dom, el, propKey, value);
1121
+ }
1122
+ };
1123
+ }
1124
+ function headingUpdate(dom, el) {
1125
+ return (propKey, value) => {
1126
+ if (propKey === "text") {
1127
+ dom.setTextContent(el, String(value ?? ""));
1128
+ } else {
1129
+ applyProp(dom, el, propKey, value);
1130
+ }
1131
+ };
1132
+ }
1133
+ function inputUpdate(dom, el) {
1134
+ return (propKey, value) => {
1135
+ if (propKey === "value") {
1136
+ dom.setProperty(el, "value", String(value ?? ""));
1137
+ } else {
1138
+ applyProp(dom, el, propKey, value);
1139
+ }
1140
+ };
1141
+ }
1142
+ function buttonUpdate(dom, el) {
1143
+ return (propKey, value) => {
1144
+ if (propKey === "label") {
1145
+ dom.setTextContent(el, String(value ?? ""));
1146
+ } else if (propKey === "disabled") {
1147
+ if (value === true) {
1148
+ dom.setAttribute(el, "disabled", "");
1149
+ } else {
1150
+ dom.removeAttribute(el, "disabled");
1151
+ }
1152
+ } else {
1153
+ applyProp(dom, el, propKey, value);
1154
+ }
1155
+ };
1156
+ }
1157
+ function applyNodeProps(ctx, graphNode, el) {
1158
+ const props = graphNode.props;
1159
+ for (const key in props) {
1160
+ if (!Object.hasOwn(props, key)) continue;
1161
+ if (SKIP_PROP_KEYS.has(key)) continue;
1162
+ applyProp(ctx.dom, el, key, props[key]);
1051
1163
  }
1052
- isTextNode(node) {
1053
- return asServer(node).kind === "text";
1164
+ }
1165
+ function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
1166
+ if (graphNode.stateRefs.length === 0) return;
1167
+ for (const stateRef of graphNode.stateRefs) {
1168
+ const signalKey = `__signal__${stateRef.signalId}`;
1169
+ const maybeSig = ctx.graph.getHandler(signalKey);
1170
+ if (maybeSig === void 0 || typeof maybeSig.subscribe !== "function") continue;
1171
+ const unsub = maybeSig.subscribe((value) => {
1172
+ onUpdate(stateRef.propKey, value);
1173
+ });
1174
+ instance.trackCleanup(unsub);
1054
1175
  }
1055
- tagName(element) {
1056
- return element.tagName;
1176
+ }
1177
+ function wireReactiveList(ctx, graphNode, instance, el) {
1178
+ const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
1179
+ const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
1180
+ if (plan === void 0 && build === void 0) return;
1181
+ for (const stateRef of graphNode.stateRefs) {
1182
+ if (stateRef.propKey !== "items") continue;
1183
+ const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
1184
+ if (sig === void 0 || typeof sig.subscribe !== "function") continue;
1185
+ const unsub = sig.subscribe((value) => {
1186
+ if (plan !== void 0) {
1187
+ reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
1188
+ } else {
1189
+ reconcileReactiveList(ctx, graphNode, instance, el, build(value));
1190
+ }
1191
+ });
1192
+ instance.trackCleanup(unsub);
1057
1193
  }
1058
- parentNode(node) {
1059
- return asServer(node).parent ?? null;
1194
+ }
1195
+ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
1196
+ const oldInstances = [...listInstance.children];
1197
+ const result = reconcileChildrenByPlan(
1198
+ ctx,
1199
+ listEl,
1200
+ oldInstances,
1201
+ plan,
1202
+ (node, parent) => mountNode(ctx, node, parent)
1203
+ );
1204
+ listInstance.children.length = 0;
1205
+ for (const inst of result.instances) listInstance.children.push(inst);
1206
+ for (const removed of result.removed) {
1207
+ forgetInstance(ctx, removed);
1208
+ ctx.graph.detachNode(removed.graphNode);
1060
1209
  }
1061
- nextSibling(node) {
1062
- const n = asServer(node);
1063
- const parent = n.parent;
1064
- if (parent === null) return null;
1065
- const idx = parent.children.indexOf(n);
1066
- if (idx === -1 || idx + 1 >= parent.children.length) return null;
1067
- return parent.children[idx + 1];
1210
+ const adopted = new Set(result.instances.map((i) => i.graphNode));
1211
+ for (const node of result.built ?? []) {
1212
+ if (!adopted.has(node)) ctx.graph.detachNode(node);
1068
1213
  }
1069
- firstChild(node) {
1070
- const n = asServer(node);
1071
- if (n.kind === "element" || n.kind === "fragment") {
1072
- const el = n;
1073
- return el.children[0] ?? null;
1074
- }
1075
- return null;
1214
+ for (const child of [...listNode.children]) listNode.removeChild(child);
1215
+ for (const inst of result.instances) listNode.appendChild(inst.graphNode);
1216
+ }
1217
+ function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
1218
+ const oldInstances = [...listInstance.children];
1219
+ const result = reconcileChildren(
1220
+ ctx,
1221
+ listEl,
1222
+ oldInstances,
1223
+ newNodes,
1224
+ (node, parent) => mountNode(ctx, node, parent)
1225
+ );
1226
+ listInstance.children.length = 0;
1227
+ for (const inst of result.instances) listInstance.children.push(inst);
1228
+ for (const removed of result.removed) {
1229
+ forgetInstance(ctx, removed);
1230
+ ctx.graph.detachNode(removed.graphNode);
1076
1231
  }
1077
- childNodes(node) {
1078
- const n = asServer(node);
1079
- if (n.kind === "element" || n.kind === "fragment") {
1080
- return n.children;
1081
- }
1082
- return [];
1232
+ const adopted = new Set(result.instances.map((i) => i.graphNode));
1233
+ for (const built of newNodes) {
1234
+ if (!adopted.has(built)) ctx.graph.detachNode(built);
1083
1235
  }
1084
- // ── Server-only ────────────────────────────────────────────────────────────
1085
- /** Serialize a node's children ("inner HTML") to an HTML string. */
1086
- serializeInner(node) {
1087
- const n = asServer(node);
1088
- if (n.kind === "element" || n.kind === "fragment") {
1089
- return serializeChildren(n);
1236
+ for (const child of [...listNode.children]) listNode.removeChild(child);
1237
+ for (const inst of result.instances) listNode.appendChild(inst.graphNode);
1238
+ }
1239
+ function forgetInstance(ctx, instance) {
1240
+ ctx.instances.delete(instance.graphNode.id);
1241
+ for (const child of instance.children) forgetInstance(ctx, child);
1242
+ }
1243
+ function wireOverlayBehavior(ctx, graphNode, instance, target) {
1244
+ const { dom, graph } = ctx;
1245
+ if (dom.body() === null) return;
1246
+ const descFn = graph.getHandler(`__overlay__${graphNode.id}`);
1247
+ if (descFn === void 0) return;
1248
+ const desc = descFn();
1249
+ const openSig = desc.open;
1250
+ if (openSig === void 0 || typeof openSig.subscribe !== "function") return;
1251
+ let active = [];
1252
+ let saved = null;
1253
+ const teardown = () => {
1254
+ for (const fn of active) fn();
1255
+ active = [];
1256
+ };
1257
+ const onOpenChange = (isOpen) => {
1258
+ if (isOpen) {
1259
+ if (desc.restoreFocus) saved = saveFocus(dom);
1260
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
1261
+ if (desc.modal) {
1262
+ active.push(trapFocus(dom, target));
1263
+ active.push(containFocus(dom, target));
1264
+ }
1265
+ if (desc.closeOnEscape && desc.onClose !== void 0) {
1266
+ active.push(onEscape(dom, target, desc.onClose));
1267
+ }
1268
+ } else {
1269
+ teardown();
1270
+ if (desc.restoreFocus && saved !== null) {
1271
+ restoreFocus(dom, saved);
1272
+ saved = null;
1273
+ }
1090
1274
  }
1091
- return "";
1092
- }
1093
- /** Serialize a node (including itself) to an HTML string. */
1094
- serializeOuter(node) {
1095
- return serializeServerNode(asServer(node));
1096
- }
1097
- };
1275
+ };
1276
+ const unsub = openSig.subscribe(onOpenChange);
1277
+ instance.trackCleanup(unsub);
1278
+ instance.trackCleanup(teardown);
1279
+ if (openSig.peek() === true) onOpenChange(true);
1280
+ }
1098
1281
 
1099
1282
  // ../renderer/src/dehydrate.ts
1100
1283
  var STATE_MARKER_ATTR = "data-streetui-state";
@@ -1132,11 +1315,115 @@ function readState(dom, root) {
1132
1315
  }
1133
1316
  }
1134
1317
 
1318
+ // ../compiler/dist/diagnostics.js
1319
+ var TEXT_PROP_KEYS = /* @__PURE__ */ new Set(["text", "label", "value"]);
1320
+ function analyzeGraph(graph) {
1321
+ const nodes = /* @__PURE__ */ new Map();
1322
+ const summary = {
1323
+ totalNodes: 0,
1324
+ staticNodes: 0,
1325
+ staticSubtrees: 0,
1326
+ dynamicTextNodes: 0,
1327
+ dynamicAttrNodes: 0,
1328
+ eventNodes: 0,
1329
+ lists: 0,
1330
+ conditionals: 0
1331
+ };
1332
+ const visit = (node) => {
1333
+ let allChildrenStatic = true;
1334
+ for (const child of node.children) {
1335
+ const childSubtreeStatic = visit(child);
1336
+ if (!childSubtreeStatic) allChildrenStatic = false;
1337
+ }
1338
+ let hasDynamicText = false;
1339
+ let hasDynamicAttr = false;
1340
+ for (const ref of node.stateRefs) {
1341
+ if (TEXT_PROP_KEYS.has(ref.propKey)) hasDynamicText = true;
1342
+ else hasDynamicAttr = true;
1343
+ }
1344
+ const hasEvents = node.events.length > 0;
1345
+ const isList = node.type === "reactive-list";
1346
+ const isConditional = node.type === "conditional";
1347
+ const isPortal = node.type === "portal";
1348
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal;
1349
+ const isStaticSubtree = isStatic && allChildrenStatic;
1350
+ nodes.set(node.id, {
1351
+ isStatic,
1352
+ isStaticSubtree,
1353
+ hasDynamicText,
1354
+ hasDynamicAttr,
1355
+ hasEvents,
1356
+ isList,
1357
+ isConditional
1358
+ });
1359
+ summary.totalNodes += 1;
1360
+ if (isStatic) summary.staticNodes += 1;
1361
+ if (isStaticSubtree) summary.staticSubtrees += 1;
1362
+ if (hasDynamicText) summary.dynamicTextNodes += 1;
1363
+ if (hasDynamicAttr) summary.dynamicAttrNodes += 1;
1364
+ if (hasEvents) summary.eventNodes += 1;
1365
+ if (isList) summary.lists += 1;
1366
+ if (isConditional) summary.conditionals += 1;
1367
+ return isStaticSubtree;
1368
+ };
1369
+ visit(graph.root);
1370
+ return { nodes, summary };
1371
+ }
1372
+
1373
+ // ../renderer/src/static-ssr-plan.ts
1374
+ function collectMaximalStaticRoots(graph) {
1375
+ const analysis = analyzeGraph(graph);
1376
+ const roots = [];
1377
+ const walk = (node) => {
1378
+ if (node.type !== "application") {
1379
+ const a = analysis.nodes.get(node.id);
1380
+ if (a !== void 0 && a.isStaticSubtree) {
1381
+ roots.push(node);
1382
+ return;
1383
+ }
1384
+ }
1385
+ for (const child of node.children) walk(child);
1386
+ };
1387
+ walk(graph.root);
1388
+ return roots;
1389
+ }
1390
+ function serializeStaticSubtree(dom, graph, root) {
1391
+ const container = dom.createElement("div");
1392
+ const ctx = createRenderContext(dom, graph, container);
1393
+ const instance = mountNode(ctx, root, container);
1394
+ const html = dom.serializeInner(container);
1395
+ instance.dispose();
1396
+ ctx.instances.clear();
1397
+ return html;
1398
+ }
1399
+ function buildStaticSSRPlan(compiled) {
1400
+ const graph = compiled.graph;
1401
+ const roots = collectMaximalStaticRoots(graph);
1402
+ const plan = /* @__PURE__ */ new Map();
1403
+ if (roots.length === 0) return plan;
1404
+ const dom = new ServerDOMAdapter();
1405
+ for (const root of roots) {
1406
+ plan.set(root.id, serializeStaticSubtree(dom, graph, root));
1407
+ }
1408
+ return plan;
1409
+ }
1410
+ var PLAN_CACHE = /* @__PURE__ */ new WeakMap();
1411
+ function getStaticSSRPlan(compiled) {
1412
+ let plan = PLAN_CACHE.get(compiled);
1413
+ if (plan === void 0) {
1414
+ plan = buildStaticSSRPlan(compiled);
1415
+ PLAN_CACHE.set(compiled, plan);
1416
+ }
1417
+ return plan;
1418
+ }
1419
+
1135
1420
  // ../renderer/src/ssr.ts
1136
1421
  function renderToString(compiled, options = {}) {
1137
1422
  const dom = options.domAdapter ?? new ServerDOMAdapter();
1423
+ const plan = options.staticPlan === null ? void 0 : options.staticPlan ?? getStaticSSRPlan(compiled);
1424
+ const staticHTML = plan !== void 0 && plan.size > 0 ? plan : void 0;
1138
1425
  const container = dom.createElement("div");
1139
- const ctx = createRenderContext(dom, compiled.graph, container);
1426
+ const ctx = createRenderContext(dom, compiled.graph, container, void 0, staticHTML);
1140
1427
  const rootInstance = mountGraph(ctx);
1141
1428
  const html = dom.serializeInner(container);
1142
1429
  rootInstance.dispose();
@@ -1145,7 +1432,7 @@ function renderToString(compiled, options = {}) {
1145
1432
  }
1146
1433
 
1147
1434
  // src/version.ts
1148
- var VERSION = "1.6.0";
1435
+ var VERSION = "1.7.0";
1149
1436
  export {
1150
1437
  STATE_MARKER_ATTR,
1151
1438
  ServerDOMAdapter,