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