streetui 1.6.1 → 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
@@ -154,978 +154,1130 @@ function wireEvents(dom, graph, node, element, instance) {
154
154
  }
155
155
  }
156
156
 
157
- // ../renderer/src/tag-map.ts
158
- var TAG_MAP = {
159
- application: "div",
160
- page: "div",
161
- section: "section",
162
- container: "div",
163
- heading: "h1",
164
- text: "span",
165
- button: "button",
166
- input: "input",
167
- form: "form",
168
- list: "ul",
169
- "list-item": "li",
170
- image: "img",
171
- link: "a",
172
- component: "div",
173
- slot: "div",
174
- fragment: "div",
175
- "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
+ }
176
169
  };
177
- function resolveTag(type) {
178
- return TAG_MAP[type] ?? "div";
179
- }
180
-
181
- // ../renderer/src/patch.ts
182
- function patchNode(ctx, graphNode, propKey, newValue) {
183
- const instance = ctx.instances.get(graphNode.id);
184
- if (instance === void 0) return;
185
- const domNode = instance.domNode;
186
- if (!ctx.dom.isElement(domNode)) return;
187
- const oldValue = graphNode.getProp(propKey);
188
- switch (propKey) {
189
- case "text":
190
- if (!Object.is(oldValue, newValue)) {
191
- ctx.dom.setTextContent(domNode, String(newValue ?? ""));
192
- graphNode.setProp("text", String(newValue ?? ""));
193
- }
194
- break;
195
- case "label":
196
- if (!Object.is(oldValue, newValue)) {
197
- ctx.dom.setTextContent(domNode, String(newValue ?? ""));
198
- graphNode.setProp("label", String(newValue ?? ""));
199
- }
200
- break;
201
- case "disabled":
202
- if (newValue === true) {
203
- ctx.dom.setAttribute(domNode, "disabled", "");
204
- } else {
205
- ctx.dom.removeAttribute(domNode, "disabled");
206
- }
207
- graphNode.setProp("disabled", Boolean(newValue));
208
- break;
209
- case "value":
210
- if (!Object.is(oldValue, newValue)) {
211
- ctx.dom.setProperty(domNode, "value", String(newValue ?? ""));
212
- graphNode.setProp("value", String(newValue ?? ""));
213
- }
214
- break;
215
- default:
216
- patchProp(ctx.dom, domNode, propKey, oldValue, newValue);
217
- graphNode.setProp(propKey, newValue);
218
- break;
170
+ var ServerText = class {
171
+ kind = "text";
172
+ parent = null;
173
+ data;
174
+ constructor(data) {
175
+ this.data = data;
219
176
  }
220
- }
221
-
222
- // ../renderer/src/reconciliation.ts
223
- function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
224
- const oldByKey = /* @__PURE__ */ new Map();
225
- for (const inst of oldInstances) {
226
- const key = inst.graphNode.key ?? inst.graphNode.id;
227
- oldByKey.set(key, inst);
177
+ };
178
+ var ServerComment = class {
179
+ kind = "comment";
180
+ parent = null;
181
+ data;
182
+ constructor(data) {
183
+ this.data = data;
228
184
  }
229
- const newInstances = [];
230
- const usedKeys = /* @__PURE__ */ new Set();
231
- for (const newNode of newNodes) {
232
- const key = newNode.key ?? newNode.id;
233
- const existing = oldByKey.get(key);
234
- if (existing !== void 0) {
235
- usedKeys.add(key);
236
- const oldSig = existing.graphNode.getProp("_sig");
237
- const newSig = newNode.getProp("_sig");
238
- patchExistingInstance(ctx, existing, newNode);
239
- if (!Object.is(oldSig, newSig)) {
240
- reconcileItemChildren(ctx, existing, newNode, mountFn);
241
- }
242
- newInstances.push(existing);
243
- } else {
244
- const inst = mountFn(newNode, parentDom);
245
- newInstances.push(inst);
246
- }
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;
247
197
  }
248
- const removed = [];
249
- for (const inst of oldInstances) {
250
- const key = inst.graphNode.key ?? inst.graphNode.id;
251
- if (!usedKeys.has(key)) {
252
- 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;
253
274
  }
275
+ out += value.slice(last, i) + esc;
276
+ last = i + 1;
254
277
  }
255
- for (const inst of removed) {
256
- const parent = ctx.dom.parentNode(inst.domNode);
257
- if (parent !== null) {
258
- 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;
259
305
  }
260
- inst.dispose();
306
+ out += value.slice(last, i) + esc;
307
+ last = i + 1;
261
308
  }
262
- reorderDom(ctx, parentDom, newInstances);
263
- return { instances: newInstances, removed };
309
+ return out + value.slice(last);
264
310
  }
265
- function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
266
- const oldByKey = /* @__PURE__ */ new Map();
267
- for (const inst of oldInstances) {
268
- 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
+ }
269
319
  }
270
- const newInstances = [];
271
- const usedKeys = /* @__PURE__ */ new Set();
272
- const built = [];
273
- for (const entry of plan) {
274
- const existing = oldByKey.get(entry.key);
275
- if (existing !== void 0) {
276
- usedKeys.add(entry.key);
277
- const oldItem = existing.graphNode.getProp("_item");
278
- if (!Object.is(oldItem, entry.item)) {
279
- const newSig = entry.sig();
280
- const oldSig = existing.graphNode.getProp("_sig");
281
- if (!Object.is(oldSig, newSig)) {
282
- const freshNode = entry.build();
283
- built.push(freshNode);
284
- patchExistingInstance(ctx, existing, freshNode);
285
- reconcileItemChildren(ctx, existing, freshNode, mountFn);
286
- 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))}"`);
287
331
  }
288
- existing.graphNode.setProp("_item", entry.item);
289
332
  }
290
- newInstances.push(existing);
291
- } else {
292
- const freshNode = entry.build();
293
- built.push(freshNode);
294
- const inst = mountFn(freshNode, parentDom);
295
- newInstances.push(inst);
296
333
  }
297
334
  }
298
- const removed = [];
299
- for (const inst of oldInstances) {
300
- const key = inst.graphNode.key ?? inst.graphNode.id;
301
- if (!usedKeys.has(key)) removed.push(inst);
302
- }
303
- for (const inst of removed) {
304
- const parent = ctx.dom.parentNode(inst.domNode);
305
- if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
306
- inst.dispose();
335
+ const style = el._style;
336
+ if (style !== null && !style.isEmpty && !el.attributes.has("style")) {
337
+ parts.push(` style="${escapeHtmlAttr(style.toCss())}"`);
307
338
  }
308
- reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
309
- return { instances: newInstances, removed, built };
339
+ return parts.join("");
310
340
  }
311
- function reorderDomMinimal(ctx, parentDom, oldInstances, newInstances) {
312
- const n = newInstances.length;
313
- if (n === 0) return;
314
- const oldIndexOf = /* @__PURE__ */ new Map();
315
- for (let i = 0; i < oldInstances.length; i++) oldIndexOf.set(oldInstances[i], i);
316
- const source = new Array(n);
317
- let moved = false;
318
- let lastSeen = -1;
319
- for (let i = 0; i < n; i++) {
320
- const oi = oldIndexOf.get(newInstances[i]);
321
- if (oi === void 0) {
322
- source[i] = -1;
323
- moved = true;
324
- } else {
325
- source[i] = oi;
326
- if (oi < lastSeen) moved = true;
327
- else lastSeen = oi;
328
- }
329
- }
330
- if (!moved) return;
331
- const keep = longestIncreasingSubsequence(source);
332
- let refNode = null;
333
- for (let i = n - 1; i >= 0; i--) {
334
- const domNode = newInstances[i].domNode;
335
- if (source[i] === -1 || !keep.has(i)) {
336
- if (ctx.dom.nextSibling(domNode) !== refNode) {
337
- 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}>`;
338
357
  }
358
+ return `<${tag}${attrs}>${serializeChildren(el)}</${tag}>`;
339
359
  }
340
- refNode = domNode;
341
360
  }
342
361
  }
343
- function longestIncreasingSubsequence(source) {
344
- const keep = /* @__PURE__ */ new Set();
345
- const n = source.length;
346
- const tails = [];
347
- const prev = new Array(n).fill(-1);
348
- for (let i = 0; i < n; i++) {
349
- const v = source[i];
350
- if (v < 0) continue;
351
- let lo = 0;
352
- let hi = tails.length;
353
- while (lo < hi) {
354
- const mid = lo + hi >> 1;
355
- if (source[tails[mid]] < v) lo = mid + 1;
356
- 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;
357
414
  }
358
- if (lo > 0) prev[i] = tails[lo - 1];
359
- 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);
360
419
  }
361
- let idx = tails.length > 0 ? tails[tails.length - 1] : -1;
362
- while (idx >= 0) {
363
- keep.add(idx);
364
- 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
+ }
365
428
  }
366
- return keep;
367
- }
368
- function reconcileItemChildren(ctx, itemInstance, newItemNode, mountFn) {
369
- const el = itemInstance.domNode;
370
- if (!ctx.dom.isElement(el)) return;
371
- const oldChildren = [...itemInstance.children];
372
- const newChildNodes = [...newItemNode.children];
373
- const nextChildren = [];
374
- const kept = /* @__PURE__ */ new Set();
375
- for (let i = 0; i < newChildNodes.length; i++) {
376
- const newChild = newChildNodes[i];
377
- const oldChild = oldChildren[i];
378
- if (oldChild !== void 0 && oldChild.graphNode.type === newChild.type) {
379
- patchExistingInstance(ctx, oldChild, newChild);
380
- reconcileItemChildren(ctx, oldChild, newChild, mountFn);
381
- nextChildren.push(oldChild);
382
- kept.add(oldChild);
383
- } else {
384
- itemInstance.graphNode.appendChild(newChild);
385
- const inst = mountFn(newChild, el);
386
- 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;
387
446
  }
388
447
  }
389
- for (const old of oldChildren) {
390
- if (kept.has(old)) continue;
391
- const parent = ctx.dom.parentNode(old.domNode);
392
- if (parent !== null) ctx.dom.removeChild(parent, old.domNode);
393
- old.dispose();
394
- forgetInstanceTree(ctx, old);
395
- ctx.graph.detachNode(old.graphNode);
448
+ setAttribute(element, name, value) {
449
+ element.attributes.set(name, value);
450
+ }
451
+ removeAttribute(element, name) {
452
+ element.attributes.delete(name);
453
+ }
454
+ getAttribute(element, name) {
455
+ return element.attributes.get(name) ?? null;
396
456
  }
397
- reorderDom(ctx, el, nextChildren);
398
- itemInstance.children.length = 0;
399
- for (const c of nextChildren) itemInstance.children.push(c);
400
- for (const c of [...itemInstance.graphNode.children]) {
401
- itemInstance.graphNode.removeChild(c);
457
+ setProperty(element, name, value) {
458
+ element.properties.set(name, value);
402
459
  }
403
- for (const c of nextChildren) itemInstance.graphNode.appendChild(c.graphNode);
404
- }
405
- function reorderDom(ctx, parentDom, instances) {
406
- let referenceNode = null;
407
- for (let i = instances.length - 1; i >= 0; i--) {
408
- const inst = instances[i];
409
- if (inst === void 0) continue;
410
- const domNode = inst.domNode;
411
- const currentNext = ctx.dom.nextSibling(domNode);
412
- if (currentNext !== referenceNode) {
413
- ctx.dom.insertBefore(parentDom, domNode, referenceNode);
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;
414
470
  }
415
- referenceNode = domNode;
416
471
  }
417
- }
418
- function forgetInstanceTree(ctx, instance) {
419
- ctx.instances.delete(instance.graphNode.id);
420
- for (const child of instance.children) forgetInstanceTree(ctx, child);
421
- }
422
- function patchExistingInstance(ctx, instance, newNode) {
423
- const oldNode = instance.graphNode;
424
- for (const [key, newVal] of Object.entries(newNode.props)) {
425
- const oldVal = oldNode.getProp(key);
426
- if (!Object.is(oldVal, newVal)) {
427
- patchNode(ctx, instance.graphNode, key, newVal);
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;
428
481
  }
482
+ return null;
429
483
  }
430
- }
431
-
432
- // ../renderer/src/mount.ts
433
- var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
434
- "text",
435
- "label",
436
- "level",
437
- "inputType",
438
- "src",
439
- "alt",
440
- "href",
441
- "external",
442
- "value",
443
- "placeholder",
444
- "disabled",
445
- "_renderKey",
446
- "key",
447
- "name"
448
- ]);
449
- function mountGraph(ctx) {
450
- return mountNode(ctx, ctx.graph.root, ctx.container);
451
- }
452
- function mountNode(ctx, graphNode, parentDom) {
453
- const { dom, graph } = ctx;
454
- const staticHTML = ctx.staticHTML;
455
- if (staticHTML !== void 0 && dom.createRawHTML !== void 0) {
456
- const precomputed = staticHTML.get(graphNode.id);
457
- if (precomputed !== void 0) {
458
- const raw = dom.createRawHTML(precomputed);
459
- dom.appendChild(parentDom, raw);
460
- const instance2 = new NodeInstance(graphNode, raw);
461
- ctx.instances.set(graphNode.id, instance2);
462
- return instance2;
463
- }
484
+ // Server nodes never dispatch events — listeners are a no-op on the server.
485
+ addEventListener() {
464
486
  }
465
- if (graphNode.type === "application") {
466
- const instance2 = new NodeInstance(graphNode, parentDom);
467
- ctx.instances.set(graphNode.id, instance2);
468
- for (const child of graphNode.children) {
469
- const childInstance = mountNode(ctx, child, parentDom);
470
- instance2.addChild(childInstance);
471
- }
472
- return instance2;
487
+ removeEventListener() {
473
488
  }
474
- if (graphNode.type === "text") {
475
- const text = String(graphNode.getProp("text") ?? "");
476
- const el2 = dom.createElement("span");
477
- const textNode = dom.createTextNode(text);
478
- dom.appendChild(el2, textNode);
479
- applyNodeProps(ctx, graphNode, el2);
480
- const instance2 = new NodeInstance(graphNode, el2);
481
- ctx.instances.set(graphNode.id, instance2);
482
- wireEvents(dom, graph, graphNode, el2, instance2);
483
- if (graphNode.stateRefs.length !== 0) {
484
- wireSignalBindings(ctx, graphNode, instance2, textUpdate(dom, el2, textNode));
485
- }
486
- dom.appendChild(parentDom, el2);
487
- return instance2;
489
+ querySelector() {
490
+ return null;
488
491
  }
489
- if (graphNode.type === "heading") {
490
- const level = graphNode.getProp("level") ?? 1;
491
- const tag2 = `h${level}`;
492
- const el2 = dom.createElement(tag2);
493
- const text = String(graphNode.getProp("text") ?? "");
494
- dom.setTextContent(el2, text);
495
- applyNodeProps(ctx, graphNode, el2);
496
- const instance2 = new NodeInstance(graphNode, el2);
497
- ctx.instances.set(graphNode.id, instance2);
498
- wireEvents(dom, graph, graphNode, el2, instance2);
499
- if (graphNode.stateRefs.length !== 0) {
500
- wireSignalBindings(ctx, graphNode, instance2, headingUpdate(dom, el2));
501
- }
502
- dom.appendChild(parentDom, el2);
503
- return instance2;
492
+ querySelectorAll() {
493
+ return [];
504
494
  }
505
- if (graphNode.type === "input") {
506
- const el2 = dom.createElement("input");
507
- const inputType = String(graphNode.getProp("inputType") ?? "text");
508
- dom.setAttribute(el2, "type", inputType);
509
- const placeholder = graphNode.getProp("placeholder");
510
- if (placeholder !== void 0) dom.setAttribute(el2, "placeholder", String(placeholder));
511
- const value = graphNode.getProp("value");
512
- if (value !== void 0) dom.setProperty(el2, "value", String(value));
513
- applyNodeProps(ctx, graphNode, el2);
514
- const instance2 = new NodeInstance(graphNode, el2);
515
- ctx.instances.set(graphNode.id, instance2);
516
- wireEvents(dom, graph, graphNode, el2, instance2);
517
- if (graphNode.stateRefs.length !== 0) {
518
- wireSignalBindings(ctx, graphNode, instance2, inputUpdate(dom, el2));
519
- }
520
- dom.appendChild(parentDom, el2);
521
- return instance2;
495
+ getElementById() {
496
+ return null;
522
497
  }
523
- if (graphNode.type === "image") {
524
- const el2 = dom.createElement("img");
525
- const src = graphNode.getProp("src");
526
- const alt = graphNode.getProp("alt");
527
- if (src !== void 0) dom.setAttribute(el2, "src", String(src));
528
- if (alt !== void 0) dom.setAttribute(el2, "alt", String(alt));
529
- const width = graphNode.getProp("width");
530
- const height = graphNode.getProp("height");
531
- if (width !== void 0) dom.setAttribute(el2, "width", String(width));
532
- if (height !== void 0) dom.setAttribute(el2, "height", String(height));
533
- applyNodeProps(ctx, graphNode, el2);
534
- const instance2 = new NodeInstance(graphNode, el2);
535
- ctx.instances.set(graphNode.id, instance2);
536
- dom.appendChild(parentDom, el2);
537
- return instance2;
498
+ focus() {
538
499
  }
539
- if (graphNode.type === "link") {
540
- const el2 = dom.createElement("a");
541
- const href = graphNode.getProp("href");
542
- const label = graphNode.getProp("label");
543
- const external = graphNode.getProp("external");
544
- if (href !== void 0) dom.setAttribute(el2, "href", String(href));
545
- if (label !== void 0) dom.setTextContent(el2, String(label));
546
- if (external === true) {
547
- dom.setAttribute(el2, "target", "_blank");
548
- dom.setAttribute(el2, "rel", "noopener noreferrer");
549
- }
550
- applyNodeProps(ctx, graphNode, el2);
551
- const instance2 = new NodeInstance(graphNode, el2);
552
- ctx.instances.set(graphNode.id, instance2);
553
- wireEvents(dom, graph, graphNode, el2, instance2);
554
- dom.appendChild(parentDom, el2);
555
- return instance2;
500
+ body() {
501
+ return null;
556
502
  }
557
- if (graphNode.type === "button") {
558
- const el2 = dom.createElement("button");
559
- const label = graphNode.getProp("label");
560
- if (label !== void 0) dom.setTextContent(el2, String(label));
561
- const disabled = graphNode.getProp("disabled");
562
- if (disabled === true) dom.setAttribute(el2, "disabled", "");
563
- applyNodeProps(ctx, graphNode, el2);
564
- const instance2 = new NodeInstance(graphNode, el2);
565
- ctx.instances.set(graphNode.id, instance2);
566
- wireEvents(dom, graph, graphNode, el2, instance2);
567
- if (graphNode.stateRefs.length !== 0) {
568
- wireSignalBindings(ctx, graphNode, instance2, buttonUpdate(dom, el2));
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;
569
537
  }
570
- dom.appendChild(parentDom, el2);
571
- return instance2;
538
+ return null;
572
539
  }
573
- if (graphNode.type === "reactive-list" || graphNode.type === "conditional") {
574
- const tag2 = resolveTag(graphNode.type);
575
- const el2 = dom.createElement(tag2);
576
- applyNodeProps(ctx, graphNode, el2);
577
- const instance2 = new NodeInstance(graphNode, el2);
578
- ctx.instances.set(graphNode.id, instance2);
579
- for (const child of graphNode.children) {
580
- const childInstance = mountNode(ctx, child, el2);
581
- instance2.addChild(childInstance);
540
+ childNodes(node) {
541
+ const n = asServer(node);
542
+ if (n.kind === "element" || n.kind === "fragment") {
543
+ return n.children;
582
544
  }
583
- dom.appendChild(parentDom, el2);
584
- wireReactiveList(ctx, graphNode, instance2, el2);
585
- return instance2;
545
+ return [];
586
546
  }
587
- const tag = resolveTag(graphNode.type);
588
- const el = dom.createElement(tag);
589
- applyNodeProps(ctx, graphNode, el);
590
- if (graphNode.type === "list-item") {
591
- const itemKey = graphNode.getProp("key");
592
- if (itemKey !== void 0) {
593
- dom.setAttribute(el, "data-streetui-key", String(itemKey));
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);
594
553
  }
554
+ return "";
595
555
  }
596
- const instance = new NodeInstance(graphNode, el);
597
- ctx.instances.set(graphNode.id, instance);
598
- if (graphNode.type === "form") {
599
- wireEvents(dom, graph, graphNode, el, instance);
600
- }
601
- for (const child of graphNode.children) {
602
- const childInstance = mountNode(ctx, child, el);
603
- instance.addChild(childInstance);
556
+ /** Serialize a node (including itself) to an HTML string. */
557
+ serializeOuter(node) {
558
+ return serializeServerNode(asServer(node));
604
559
  }
605
- dom.appendChild(parentDom, el);
606
- 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;
607
569
  }
608
- function textUpdate(dom, el, textNode) {
609
- return (propKey, value) => {
610
- if (propKey === "text") {
611
- dom.setTextContent(textNode, String(value ?? ""));
612
- } else {
613
- applyProp(dom, el, propKey, value);
614
- }
615
- };
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;
616
575
  }
617
- function headingUpdate(dom, el) {
618
- return (propKey, value) => {
619
- if (propKey === "text") {
620
- dom.setTextContent(el, String(value ?? ""));
621
- } else {
622
- applyProp(dom, el, propKey, value);
623
- }
624
- };
576
+ function getFocusable(dom, container, selector = FOCUSABLE_SELECTOR) {
577
+ return Array.from(dom.querySelectorAll(container, selector)).filter(
578
+ (el) => dom.matches(el, selector)
579
+ );
625
580
  }
626
- function inputUpdate(dom, el) {
627
- return (propKey, value) => {
628
- if (propKey === "value") {
629
- dom.setProperty(el, "value", String(value ?? ""));
630
- } else {
631
- 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);
632
611
  }
633
612
  };
613
+ dom.addEventListener(container, "keydown", onKeydown);
614
+ return () => dom.removeEventListener(container, "keydown", onKeydown);
634
615
  }
635
- function buttonUpdate(dom, el) {
636
- return (propKey, value) => {
637
- if (propKey === "label") {
638
- dom.setTextContent(el, String(value ?? ""));
639
- } else if (propKey === "disabled") {
640
- if (value === true) {
641
- dom.setAttribute(el, "disabled", "");
642
- } else {
643
- dom.removeAttribute(el, "disabled");
644
- }
645
- } else {
646
- 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);
647
624
  }
648
625
  };
626
+ dom.addEventListener(body, "focusin", onFocusIn);
627
+ return () => dom.removeEventListener(body, "focusin", onFocusIn);
649
628
  }
650
- function applyNodeProps(ctx, graphNode, el) {
651
- const props = graphNode.props;
652
- for (const key in props) {
653
- if (!Object.hasOwn(props, key)) continue;
654
- if (SKIP_PROP_KEYS.has(key)) continue;
655
- applyProp(ctx.dom, el, key, props[key]);
656
- }
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);
657
635
  }
658
- function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
659
- if (graphNode.stateRefs.length === 0) return;
660
- for (const stateRef of graphNode.stateRefs) {
661
- const signalKey = `__signal__${stateRef.signalId}`;
662
- const maybeSig = ctx.graph.getHandler(signalKey);
663
- if (maybeSig === void 0 || typeof maybeSig.subscribe !== "function") continue;
664
- const unsub = maybeSig.subscribe((value) => {
665
- onUpdate(stateRef.propKey, value);
666
- });
667
- instance.trackCleanup(unsub);
668
- }
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";
669
664
  }
670
- function wireReactiveList(ctx, graphNode, instance, el) {
671
- const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
672
- const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
673
- if (plan === void 0 && build === void 0) return;
674
- for (const stateRef of graphNode.stateRefs) {
675
- if (stateRef.propKey !== "items") continue;
676
- const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
677
- if (sig === void 0 || typeof sig.subscribe !== "function") continue;
678
- const unsub = sig.subscribe((value) => {
679
- if (plan !== void 0) {
680
- 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", "");
681
689
  } else {
682
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
690
+ ctx.dom.removeAttribute(domNode, "disabled");
683
691
  }
684
- });
685
- instance.trackCleanup(unsub);
686
- }
687
- }
688
- function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
689
- const oldInstances = [...listInstance.children];
690
- const result = reconcileChildrenByPlan(
691
- ctx,
692
- listEl,
693
- oldInstances,
694
- plan,
695
- (node, parent) => mountNode(ctx, node, parent)
696
- );
697
- listInstance.children.length = 0;
698
- for (const inst of result.instances) listInstance.children.push(inst);
699
- for (const removed of result.removed) {
700
- forgetInstance(ctx, removed);
701
- ctx.graph.detachNode(removed.graphNode);
702
- }
703
- const adopted = new Set(result.instances.map((i) => i.graphNode));
704
- for (const node of result.built ?? []) {
705
- if (!adopted.has(node)) ctx.graph.detachNode(node);
706
- }
707
- for (const child of [...listNode.children]) listNode.removeChild(child);
708
- for (const inst of result.instances) listNode.appendChild(inst.graphNode);
709
- }
710
- function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
711
- const oldInstances = [...listInstance.children];
712
- const result = reconcileChildren(
713
- ctx,
714
- listEl,
715
- oldInstances,
716
- newNodes,
717
- (node, parent) => mountNode(ctx, node, parent)
718
- );
719
- listInstance.children.length = 0;
720
- for (const inst of result.instances) listInstance.children.push(inst);
721
- for (const removed of result.removed) {
722
- forgetInstance(ctx, removed);
723
- ctx.graph.detachNode(removed.graphNode);
724
- }
725
- const adopted = new Set(result.instances.map((i) => i.graphNode));
726
- for (const built of newNodes) {
727
- if (!adopted.has(built)) ctx.graph.detachNode(built);
728
- }
729
- for (const child of [...listNode.children]) listNode.removeChild(child);
730
- for (const inst of result.instances) listNode.appendChild(inst.graphNode);
731
- }
732
- function forgetInstance(ctx, instance) {
733
- ctx.instances.delete(instance.graphNode.id);
734
- for (const child of instance.children) forgetInstance(ctx, child);
735
- }
736
-
737
- // ../dom/src/server-node.ts
738
- var ServerStyle = class {
739
- declarations = /* @__PURE__ */ new Map();
740
- setProperty(name, value) {
741
- this.declarations.set(name, value);
742
- }
743
- get isEmpty() {
744
- return this.declarations.size === 0;
745
- }
746
- toCss() {
747
- return [...this.declarations.entries()].map(([k, v]) => `${k}: ${v}`).join("; ");
748
- }
749
- };
750
- var ServerText = class {
751
- kind = "text";
752
- parent = null;
753
- data;
754
- constructor(data) {
755
- this.data = data;
756
- }
757
- };
758
- var ServerComment = class {
759
- kind = "comment";
760
- parent = null;
761
- data;
762
- constructor(data) {
763
- this.data = data;
764
- }
765
- };
766
- var ServerFragment = class {
767
- kind = "fragment";
768
- parent = null;
769
- children = [];
770
- };
771
- var ServerRawHTML = class {
772
- kind = "raw";
773
- parent = null;
774
- html;
775
- constructor(html) {
776
- this.html = html;
777
- }
778
- };
779
- var ServerElement = class {
780
- kind = "element";
781
- parent = null;
782
- tagName;
783
- attributes = /* @__PURE__ */ new Map();
784
- children = [];
785
- // Lazily-allocated stores. On the 10k-row SSR corpus ~0% of elements carry JS
786
- // properties or inline styles (measured, §5: 1 of 80,029 elements uses
787
- // `properties`, 0 use `style`), so eagerly allocating a `properties` Map plus
788
- // a `ServerStyle` (which itself holds a Map) per element wasted ~240k
789
- // allocations per /users render — all in the dominant mount phase. These are
790
- // created on first WRITE via the `properties`/`style` getters; the serializer
791
- // reads the raw `_properties`/`_style` fields so a READ never forces an
792
- // allocation. Output is byte-identical: an unset store previously serialized
793
- // to nothing (empty `properties.has(...)` / `style.isEmpty`), and a null store
794
- // is skipped the same way.
795
- _properties = null;
796
- _style = null;
797
- constructor(tagName) {
798
- 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;
799
704
  }
800
- /** JS properties set via `setProperty` (e.g. input `value`, `checked`). Allocated on first access. */
801
- get properties() {
802
- 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);
803
713
  }
804
- /** Inline-style holder mirroring `element.style`. Allocated on first access. */
805
- get style() {
806
- 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
+ }
807
732
  }
808
- };
809
- var VOID_ELEMENTS = /* @__PURE__ */ new Set([
810
- "area",
811
- "base",
812
- "br",
813
- "col",
814
- "embed",
815
- "hr",
816
- "img",
817
- "input",
818
- "link",
819
- "meta",
820
- "param",
821
- "source",
822
- "track",
823
- "wbr"
824
- ]);
825
- var SERIALIZED_PROPERTIES = {
826
- value: "attr",
827
- checked: "boolean",
828
- selected: "boolean"
829
- };
830
- var SERIALIZED_PROPERTY_ENTRIES = Object.entries(SERIALIZED_PROPERTIES);
831
- var TEXT_SPECIAL = /[&<>]/;
832
- var ATTR_SPECIAL = /[&<>"]/;
833
- function escapeHtmlText(value) {
834
- if (!TEXT_SPECIAL.test(value)) return value;
835
- let out = "";
836
- let last = 0;
837
- for (let i = 0; i < value.length; i++) {
838
- let esc;
839
- switch (value.charCodeAt(i)) {
840
- case 38:
841
- esc = "&amp;";
842
- break;
843
- // &
844
- case 60:
845
- esc = "&lt;";
846
- break;
847
- // <
848
- case 62:
849
- esc = "&gt;";
850
- break;
851
- // >
852
- default:
853
- 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);
854
738
  }
855
- out += value.slice(last, i) + esc;
856
- last = i + 1;
857
739
  }
858
- 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 };
859
749
  }
860
- function escapeHtmlAttr(value) {
861
- if (!ATTR_SPECIAL.test(value)) return value;
862
- let out = "";
863
- let last = 0;
864
- for (let i = 0; i < value.length; i++) {
865
- let esc;
866
- switch (value.charCodeAt(i)) {
867
- case 38:
868
- esc = "&amp;";
869
- break;
870
- // &
871
- case 60:
872
- esc = "&lt;";
873
- break;
874
- // <
875
- case 62:
876
- esc = "&gt;";
877
- break;
878
- // >
879
- case 34:
880
- esc = "&quot;";
881
- break;
882
- // "
883
- default:
884
- 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);
885
781
  }
886
- out += value.slice(last, i) + esc;
887
- last = i + 1;
888
782
  }
889
- 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 };
890
795
  }
891
- function serializeAttributes(el) {
892
- const parts = [];
893
- for (const [name, value] of el.attributes) {
894
- if (value === "") {
895
- 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;
896
809
  } else {
897
- parts.push(` ${name}="${escapeHtmlAttr(value)}"`);
810
+ source[i] = oi;
811
+ if (oi < lastSeen) moved = true;
812
+ else lastSeen = oi;
898
813
  }
899
814
  }
900
- const props = el._properties;
901
- if (props !== null) {
902
- for (const [name, kind] of SERIALIZED_PROPERTY_ENTRIES) {
903
- if (!props.has(name)) continue;
904
- if (el.attributes.has(name)) continue;
905
- const raw = props.get(name);
906
- if (kind === "boolean") {
907
- if (raw === true) parts.push(` ${name}`);
908
- } else {
909
- if (raw !== void 0 && raw !== null) {
910
- parts.push(` ${name}="${escapeHtmlAttr(String(raw))}"`);
911
- }
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);
912
823
  }
913
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
+ }
914
873
  }
915
- const style = el._style;
916
- if (style !== null && !style.isEmpty && !el.attributes.has("style")) {
917
- 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);
918
881
  }
919
- 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);
920
889
  }
921
- function serializeServerNode(node) {
922
- switch (node.kind) {
923
- case "text":
924
- return escapeHtmlText(node.data);
925
- case "comment":
926
- return `<!--${node.data}-->`;
927
- case "fragment":
928
- return serializeChildren(node);
929
- case "raw":
930
- return node.html;
931
- case "element": {
932
- const el = node;
933
- const tag = el.tagName;
934
- const attrs = serializeAttributes(el);
935
- if (VOID_ELEMENTS.has(tag)) {
936
- return `<${tag}${attrs}>`;
937
- }
938
- 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);
939
899
  }
900
+ referenceNode = domNode;
940
901
  }
941
902
  }
942
- function serializeChildren(node) {
943
- let out = "";
944
- for (const child of node.children) {
945
- 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
+ }
946
914
  }
947
- return out;
948
915
  }
949
916
 
950
- // ../dom/src/server-adapter.ts
951
- function asServer(node) {
952
- return node;
953
- }
954
- function asParent(node) {
955
- 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);
956
936
  }
957
- var ServerDOMAdapter = class {
958
- createElement(tag, _ns) {
959
- return new ServerElement(tag);
960
- }
961
- createTextNode(data) {
962
- return new ServerText(data);
963
- }
964
- createComment(data) {
965
- return new ServerComment(data);
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;
948
+ }
966
949
  }
967
- createFragment() {
968
- return new ServerFragment();
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);
956
+ }
957
+ return instance2;
969
958
  }
970
- /**
971
- * Create a verbatim pre-serialized HTML node (v1.7 static SSR plan, §6).
972
- * Server-only: the browser adapter does not implement this, and the renderer
973
- * fast path only invokes it when a static SSR plan is present (SSR). The
974
- * stored HTML was produced by this same serializer, so it is emitted as-is.
975
- */
976
- createRawHTML(html) {
977
- return new ServerRawHTML(html);
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));
970
+ }
971
+ dom.appendChild(parentDom, el2);
972
+ return instance2;
978
973
  }
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);
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;
985
989
  }
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;
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));
994
1004
  }
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);
1005
+ dom.appendChild(parentDom, el2);
1006
+ return instance2;
999
1007
  }
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;
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;
1023
+ }
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");
1007
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
- 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;
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));
1054
+ }
1055
+ dom.appendChild(parentDom, el2);
1056
+ return instance2;
1019
1057
  }
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;
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);
1026
1067
  }
1068
+ dom.appendChild(parentDom, el2);
1069
+ wireReactiveList(ctx, graphNode, instance2, el2);
1070
+ return instance2;
1027
1071
  }
1028
- setAttribute(element, name, value) {
1029
- element.attributes.set(name, value);
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;
1030
1093
  }
1031
- removeAttribute(element, name) {
1032
- element.attributes.delete(name);
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
+ }
1033
1102
  }
1034
- getAttribute(element, name) {
1035
- return element.attributes.get(name) ?? 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);
1036
1107
  }
1037
- setProperty(element, name, value) {
1038
- element.properties.set(name, value);
1108
+ for (const child of graphNode.children) {
1109
+ const childInstance = mountNode(ctx, child, el);
1110
+ instance.addChild(childInstance);
1039
1111
  }
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;
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);
1050
1121
  }
1051
- }
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) ?? "";
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");
1059
1151
  }
1060
- return out;
1152
+ } else {
1153
+ applyProp(dom, el, propKey, value);
1061
1154
  }
1062
- return null;
1063
- }
1064
- // Server nodes never dispatch events — listeners are a no-op on the server.
1065
- addEventListener() {
1066
- }
1067
- removeEventListener() {
1068
- }
1069
- querySelector() {
1070
- return null;
1071
- }
1072
- querySelectorAll() {
1073
- return [];
1074
- }
1075
- getElementById() {
1076
- return null;
1077
- }
1078
- focus() {
1079
- }
1080
- isElement(node) {
1081
- return asServer(node).kind === "element";
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]);
1082
1163
  }
1083
- isTextNode(node) {
1084
- 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);
1085
1175
  }
1086
- tagName(element) {
1087
- 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);
1088
1193
  }
1089
- parentNode(node) {
1090
- 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);
1091
1209
  }
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];
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);
1099
1213
  }
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;
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);
1107
1231
  }
1108
- childNodes(node) {
1109
- const n = asServer(node);
1110
- if (n.kind === "element" || n.kind === "fragment") {
1111
- return n.children;
1112
- }
1113
- 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);
1114
1235
  }
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);
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
+ }
1121
1274
  }
1122
- return "";
1123
- }
1124
- /** Serialize a node (including itself) to an HTML string. */
1125
- serializeOuter(node) {
1126
- return serializeServerNode(asServer(node));
1127
- }
1128
- };
1275
+ };
1276
+ const unsub = openSig.subscribe(onOpenChange);
1277
+ instance.trackCleanup(unsub);
1278
+ instance.trackCleanup(teardown);
1279
+ if (openSig.peek() === true) onOpenChange(true);
1280
+ }
1129
1281
 
1130
1282
  // ../renderer/src/dehydrate.ts
1131
1283
  var STATE_MARKER_ATTR = "data-streetui-state";
@@ -1192,7 +1344,8 @@ function analyzeGraph(graph) {
1192
1344
  const hasEvents = node.events.length > 0;
1193
1345
  const isList = node.type === "reactive-list";
1194
1346
  const isConditional = node.type === "conditional";
1195
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional;
1347
+ const isPortal = node.type === "portal";
1348
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal;
1196
1349
  const isStaticSubtree = isStatic && allChildrenStatic;
1197
1350
  nodes.set(node.id, {
1198
1351
  isStatic,
@@ -1279,7 +1432,7 @@ function renderToString(compiled, options = {}) {
1279
1432
  }
1280
1433
 
1281
1434
  // src/version.ts
1282
- var VERSION = "1.6.0";
1435
+ var VERSION = "1.7.0";
1283
1436
  export {
1284
1437
  STATE_MARKER_ATTR,
1285
1438
  ServerDOMAdapter,