streetui 1.8.0 → 2.0.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
@@ -24,6 +24,7 @@ __export(server_exports, {
24
24
  ServerDOMAdapter: () => ServerDOMAdapter,
25
25
  VERSION: () => VERSION,
26
26
  readState: () => readState,
27
+ renderHead: () => renderHead,
27
28
  renderToString: () => renderToString,
28
29
  serializeState: () => serializeState
29
30
  });
@@ -531,6 +532,9 @@ var ServerDOMAdapter = class {
531
532
  body() {
532
533
  return null;
533
534
  }
535
+ head() {
536
+ return null;
537
+ }
534
538
  activeElement() {
535
539
  return null;
536
540
  }
@@ -644,18 +648,25 @@ function trapFocus(dom, container) {
644
648
  dom.addEventListener(container, "keydown", onKeydown);
645
649
  return () => dom.removeEventListener(container, "keydown", onKeydown);
646
650
  }
651
+ var containmentStack = [];
647
652
  function containFocus(dom, container) {
648
653
  const body = dom.body();
649
654
  if (body === null) return () => {
650
655
  };
656
+ containmentStack.push(container);
651
657
  const onFocusIn = (event) => {
658
+ if (containmentStack[containmentStack.length - 1] !== container) return;
652
659
  const target = event.target;
653
660
  if (target !== null && !dom.contains(container, target)) {
654
661
  focusFirst(dom, container);
655
662
  }
656
663
  };
657
664
  dom.addEventListener(body, "focusin", onFocusIn);
658
- return () => dom.removeEventListener(body, "focusin", onFocusIn);
665
+ return () => {
666
+ dom.removeEventListener(body, "focusin", onFocusIn);
667
+ const index = containmentStack.lastIndexOf(container);
668
+ if (index !== -1) containmentStack.splice(index, 1);
669
+ };
659
670
  }
660
671
  function onEscape(dom, target, handler) {
661
672
  const onKeydown = (event) => {
@@ -664,6 +675,33 @@ function onEscape(dom, target, handler) {
664
675
  dom.addEventListener(target, "keydown", onKeydown);
665
676
  return () => dom.removeEventListener(target, "keydown", onKeydown);
666
677
  }
678
+ function rovingMenu(dom, container, selector = FOCUSABLE_SELECTOR) {
679
+ const onKeydown = (event) => {
680
+ const key = event.key;
681
+ const isActivate = key === "Enter" || key === " " || key === "Spacebar";
682
+ const isMove = key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End";
683
+ if (!isActivate && !isMove) return;
684
+ const items = getFocusable(dom, container, selector);
685
+ if (items.length === 0) return;
686
+ const active = dom.activeElement();
687
+ const index = active === null ? -1 : items.indexOf(active);
688
+ if (isActivate) {
689
+ if (index < 0) return;
690
+ event.preventDefault();
691
+ items[index].click?.();
692
+ return;
693
+ }
694
+ event.preventDefault();
695
+ let next;
696
+ if (key === "Home") next = 0;
697
+ else if (key === "End") next = items.length - 1;
698
+ else if (key === "ArrowDown") next = index < 0 ? 0 : (index + 1) % items.length;
699
+ else next = index <= 0 ? items.length - 1 : index - 1;
700
+ dom.focus(items[next]);
701
+ };
702
+ dom.addEventListener(container, "keydown", onKeydown);
703
+ return () => dom.removeEventListener(container, "keydown", onKeydown);
704
+ }
667
705
 
668
706
  // ../renderer/src/tag-map.ts
669
707
  var TAG_MAP = {
@@ -688,7 +726,13 @@ var TAG_MAP = {
688
726
  // its children are relocated to a document.body container on the browser
689
727
  // (see the portal branch in mount.ts). On the server (no body) it renders
690
728
  // inline, so the anchor tag is what SSR/hydration positionally match on.
691
- portal: "div"
729
+ portal: "div",
730
+ // A `head()` node renders as a neutral, empty inline anchor <div> at its
731
+ // declaration site (like a portal anchor). Its actual contribution — title/
732
+ // meta/link/etc. — is applied to `document.head` by `wireHeadBehavior` on the
733
+ // browser, and emitted separately by `renderHead()` on the server. Keeping a
734
+ // one-node/one-element anchor preserves positional hydration.
735
+ head: "div"
692
736
  };
693
737
  function resolveTag(type) {
694
738
  return TAG_MAP[type] ?? "div";
@@ -736,7 +780,7 @@ function patchNode(ctx, graphNode, propKey, newValue) {
736
780
  }
737
781
 
738
782
  // ../renderer/src/reconciliation.ts
739
- function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
783
+ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn, hooks) {
740
784
  const oldByKey = /* @__PURE__ */ new Map();
741
785
  for (const inst of oldInstances) {
742
786
  const key = inst.graphNode.key ?? inst.graphNode.id;
@@ -757,28 +801,33 @@ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
757
801
  }
758
802
  newInstances.push(existing);
759
803
  } else {
760
- const inst = mountFn(newNode, parentDom);
761
- newInstances.push(inst);
804
+ const reclaimed = hooks?.takeLeaving(key);
805
+ if (reclaimed !== void 0) {
806
+ patchExistingInstance(ctx, reclaimed, newNode);
807
+ reconcileItemChildren(ctx, reclaimed, newNode, mountFn);
808
+ hooks?.onEnter(reclaimed);
809
+ newInstances.push(reclaimed);
810
+ } else {
811
+ const inst = mountFn(newNode, parentDom);
812
+ hooks?.onEnter(inst);
813
+ newInstances.push(inst);
814
+ }
762
815
  }
763
816
  }
764
817
  const removed = [];
765
818
  for (const inst of oldInstances) {
766
819
  const key = inst.graphNode.key ?? inst.graphNode.id;
767
- if (!usedKeys.has(key)) {
768
- removed.push(inst);
769
- }
770
- }
771
- for (const inst of removed) {
820
+ if (usedKeys.has(key)) continue;
821
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
772
822
  const parent = ctx.dom.parentNode(inst.domNode);
773
- if (parent !== null) {
774
- ctx.dom.removeChild(parent, inst.domNode);
775
- }
823
+ if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
776
824
  inst.dispose();
825
+ removed.push(inst);
777
826
  }
778
827
  reorderDom(ctx, parentDom, newInstances);
779
828
  return { instances: newInstances, removed };
780
829
  }
781
- function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
830
+ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn, hooks) {
782
831
  const oldByKey = /* @__PURE__ */ new Map();
783
832
  for (const inst of oldInstances) {
784
833
  oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
@@ -805,21 +854,34 @@ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
805
854
  }
806
855
  newInstances.push(existing);
807
856
  } else {
808
- const freshNode = entry.build();
809
- built.push(freshNode);
810
- const inst = mountFn(freshNode, parentDom);
811
- newInstances.push(inst);
857
+ const reclaimed = hooks?.takeLeaving(entry.key);
858
+ if (reclaimed !== void 0) {
859
+ const freshNode = entry.build();
860
+ built.push(freshNode);
861
+ patchExistingInstance(ctx, reclaimed, freshNode);
862
+ reconcileItemChildren(ctx, reclaimed, freshNode, mountFn);
863
+ reclaimed.graphNode.setProp("_sig", freshNode.getProp("_sig"));
864
+ reclaimed.graphNode.setProp("_item", entry.item);
865
+ hooks?.onEnter(reclaimed);
866
+ newInstances.push(reclaimed);
867
+ } else {
868
+ const freshNode = entry.build();
869
+ built.push(freshNode);
870
+ const inst = mountFn(freshNode, parentDom);
871
+ hooks?.onEnter(inst);
872
+ newInstances.push(inst);
873
+ }
812
874
  }
813
875
  }
814
876
  const removed = [];
815
877
  for (const inst of oldInstances) {
816
878
  const key = inst.graphNode.key ?? inst.graphNode.id;
817
- if (!usedKeys.has(key)) removed.push(inst);
818
- }
819
- for (const inst of removed) {
879
+ if (usedKeys.has(key)) continue;
880
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
820
881
  const parent = ctx.dom.parentNode(inst.domNode);
821
882
  if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
822
883
  inst.dispose();
884
+ removed.push(inst);
823
885
  }
824
886
  reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
825
887
  return { instances: newInstances, removed, built };
@@ -945,6 +1007,354 @@ function patchExistingInstance(ctx, instance, newNode) {
945
1007
  }
946
1008
  }
947
1009
 
1010
+ // ../renderer/src/transition.ts
1011
+ function getResolvedTransition(graph, nodeId) {
1012
+ const fn = graph.getHandler(`__transition__${nodeId}`);
1013
+ return fn === void 0 ? void 0 : fn();
1014
+ }
1015
+ function host() {
1016
+ return globalThis;
1017
+ }
1018
+ function nextFrame(cb) {
1019
+ const h = host();
1020
+ const raf = h.requestAnimationFrame;
1021
+ if (typeof raf === "function") {
1022
+ raf(() => raf(cb));
1023
+ } else {
1024
+ h.setTimeout(cb, 0);
1025
+ }
1026
+ }
1027
+ function splitClass(value) {
1028
+ if (value === null) return [];
1029
+ const out = [];
1030
+ for (const t of value.split(/\s+/)) if (t.length > 0) out.push(t);
1031
+ return out;
1032
+ }
1033
+ function addClasses(dom, el, classes) {
1034
+ if (classes.length === 0) return;
1035
+ const current = splitClass(dom.getAttribute(el, "class"));
1036
+ let changed = false;
1037
+ for (const c of classes) {
1038
+ if (!current.includes(c)) {
1039
+ current.push(c);
1040
+ changed = true;
1041
+ }
1042
+ }
1043
+ if (changed) dom.setAttribute(el, "class", current.join(" "));
1044
+ }
1045
+ function removeClasses(dom, el, classes) {
1046
+ if (classes.length === 0) return;
1047
+ const current = splitClass(dom.getAttribute(el, "class"));
1048
+ const next = current.filter((c) => !classes.includes(c));
1049
+ if (next.length !== current.length) {
1050
+ if (next.length === 0) dom.removeAttribute(el, "class");
1051
+ else dom.setAttribute(el, "class", next.join(" "));
1052
+ }
1053
+ }
1054
+ function startRun(dom, el, active, from, to, duration, onDone) {
1055
+ const h = host();
1056
+ let finished = false;
1057
+ let timer = null;
1058
+ const onEvent = (e) => {
1059
+ if (e.target !== el) return;
1060
+ finish();
1061
+ };
1062
+ const detach = () => {
1063
+ dom.removeEventListener(el, "transitionend", onEvent);
1064
+ dom.removeEventListener(el, "animationend", onEvent);
1065
+ if (timer !== null) {
1066
+ h.clearTimeout(timer);
1067
+ timer = null;
1068
+ }
1069
+ };
1070
+ const finish = () => {
1071
+ if (finished) return;
1072
+ finished = true;
1073
+ detach();
1074
+ removeClasses(dom, el, active);
1075
+ removeClasses(dom, el, to);
1076
+ removeClasses(dom, el, from);
1077
+ onDone();
1078
+ };
1079
+ addClasses(dom, el, from);
1080
+ addClasses(dom, el, active);
1081
+ dom.addEventListener(el, "transitionend", onEvent);
1082
+ dom.addEventListener(el, "animationend", onEvent);
1083
+ timer = h.setTimeout(finish, duration);
1084
+ nextFrame(() => {
1085
+ if (finished) return;
1086
+ removeClasses(dom, el, from);
1087
+ addClasses(dom, el, to);
1088
+ });
1089
+ return {
1090
+ cancel: () => {
1091
+ if (finished) return;
1092
+ finished = true;
1093
+ detach();
1094
+ removeClasses(dom, el, active);
1095
+ removeClasses(dom, el, to);
1096
+ removeClasses(dom, el, from);
1097
+ }
1098
+ };
1099
+ }
1100
+ var TransitionController = class {
1101
+ constructor(dom, graph, finalize) {
1102
+ this.dom = dom;
1103
+ this.graph = graph;
1104
+ this.finalize = finalize;
1105
+ }
1106
+ leaving = /* @__PURE__ */ new Map();
1107
+ /** True only in a real DOM environment (browser). */
1108
+ get browser() {
1109
+ return this.dom.body() !== null;
1110
+ }
1111
+ keyOf(inst) {
1112
+ return inst.graphNode.key ?? inst.graphNode.id;
1113
+ }
1114
+ resolved(node) {
1115
+ return getResolvedTransition(this.graph, node.id);
1116
+ }
1117
+ /** Run the enter animation for `inst` if it carries a transition (browser only). */
1118
+ enter(inst) {
1119
+ if (!this.browser) return;
1120
+ const rt = this.resolved(inst.graphNode);
1121
+ if (rt === void 0) return;
1122
+ const el = inst.domNode;
1123
+ if (!this.dom.isElement(el)) return;
1124
+ startRun(this.dom, el, rt.enterActive, rt.enterFrom, rt.enterTo, rt.duration, () => {
1125
+ });
1126
+ }
1127
+ /**
1128
+ * Play `appear` for any initial child that opted into it (fresh browser mount
1129
+ * only — hydration must never animate appear, §22, and this is called only on
1130
+ * the mount path).
1131
+ */
1132
+ appear(children) {
1133
+ if (!this.browser) return;
1134
+ for (const child of children) {
1135
+ const rt = this.resolved(child.graphNode);
1136
+ if (rt !== void 0 && rt.appear) this.enter(child);
1137
+ }
1138
+ }
1139
+ hooks() {
1140
+ return {
1141
+ takeLeaving: (key) => {
1142
+ const entry = this.leaving.get(key);
1143
+ if (entry === void 0) return void 0;
1144
+ entry.run.cancel();
1145
+ this.leaving.delete(key);
1146
+ return entry.inst;
1147
+ },
1148
+ beginLeave: (inst) => {
1149
+ if (!this.browser) return false;
1150
+ const rt = this.resolved(inst.graphNode);
1151
+ if (rt === void 0) return false;
1152
+ const el = inst.domNode;
1153
+ if (!this.dom.isElement(el)) return false;
1154
+ const key = this.keyOf(inst);
1155
+ const prior = this.leaving.get(key);
1156
+ if (prior !== void 0) prior.run.cancel();
1157
+ const run = startRun(
1158
+ this.dom,
1159
+ el,
1160
+ rt.leaveActive,
1161
+ rt.leaveFrom,
1162
+ rt.leaveTo,
1163
+ rt.duration,
1164
+ () => {
1165
+ const current = this.leaving.get(key);
1166
+ if (current !== void 0 && current.run === run) {
1167
+ this.leaving.delete(key);
1168
+ this.finalize(inst);
1169
+ }
1170
+ }
1171
+ );
1172
+ this.leaving.set(key, { inst, run });
1173
+ return true;
1174
+ },
1175
+ onEnter: (inst) => this.enter(inst)
1176
+ };
1177
+ }
1178
+ };
1179
+
1180
+ // ../renderer/src/head.ts
1181
+ var HEAD_MARKER = "data-streetui-head";
1182
+ var HEAD_KEY = "data-streetui-head-key";
1183
+ function isSignalLike(v) {
1184
+ return v !== null && typeof v === "object" && typeof v["subscribe"] === "function" && typeof v["peek"] === "function";
1185
+ }
1186
+ function readValue(v) {
1187
+ if (isSignalLike(v)) return String(v.peek() ?? "");
1188
+ return String(v ?? "");
1189
+ }
1190
+ function resolveEntry(entry) {
1191
+ const attrs = {};
1192
+ for (const key of Object.keys(entry.attrs)) {
1193
+ attrs[key] = readValue(entry.attrs[key]);
1194
+ }
1195
+ const resolved = { tag: entry.tag, attrs };
1196
+ if (entry.tag === "title") {
1197
+ return { ...resolved, text: readValue(entry.text) };
1198
+ }
1199
+ return resolved;
1200
+ }
1201
+ var HeadManager = class {
1202
+ _dom;
1203
+ _head;
1204
+ _contributions = /* @__PURE__ */ new Map();
1205
+ _applied = /* @__PURE__ */ new Map();
1206
+ _order = 0;
1207
+ _adopted = false;
1208
+ constructor(dom, head) {
1209
+ this._dom = dom;
1210
+ this._head = head;
1211
+ }
1212
+ /** Register (or replace) a node's contribution and re-apply the merged result. */
1213
+ register(nodeId, entries) {
1214
+ this._contributions.set(nodeId, { order: this._order++, entries });
1215
+ this.apply();
1216
+ }
1217
+ /** Withdraw a node's contribution (unmount / route change) and re-apply. */
1218
+ unregister(nodeId) {
1219
+ if (this._contributions.delete(nodeId)) this.apply();
1220
+ }
1221
+ /** Recompute the merged head and patch `document.head` to match. */
1222
+ apply() {
1223
+ if (!this._adopted) {
1224
+ this._adoptServerTags();
1225
+ this._adopted = true;
1226
+ }
1227
+ const ordered = [...this._contributions.values()].sort((a, b) => a.order - b.order);
1228
+ const merged = /* @__PURE__ */ new Map();
1229
+ for (const contribution of ordered) {
1230
+ for (const entry of contribution.entries) {
1231
+ merged.set(entry.dedupKey, resolveEntry(entry));
1232
+ }
1233
+ }
1234
+ for (const [key, desired] of merged) {
1235
+ const existing = this._applied.get(key);
1236
+ if (existing !== void 0 && existing.tag === desired.tag) {
1237
+ this._reconcileAttrs(existing, desired);
1238
+ } else {
1239
+ if (existing !== void 0) {
1240
+ this._dom.removeChild(this._head, existing.el);
1241
+ this._applied.delete(key);
1242
+ }
1243
+ const el = this._createTag(key, desired);
1244
+ this._dom.appendChild(this._head, el);
1245
+ this._applied.set(key, { el, attrKeys: new Set(Object.keys(desired.attrs)), tag: desired.tag });
1246
+ }
1247
+ }
1248
+ for (const [key, record] of [...this._applied]) {
1249
+ if (!merged.has(key)) {
1250
+ this._dom.removeChild(this._head, record.el);
1251
+ this._applied.delete(key);
1252
+ }
1253
+ }
1254
+ }
1255
+ _createTag(key, desired) {
1256
+ const el = this._dom.createElement(desired.tag);
1257
+ this._dom.setAttribute(el, HEAD_MARKER, "");
1258
+ this._dom.setAttribute(el, HEAD_KEY, key);
1259
+ for (const attr of Object.keys(desired.attrs)) {
1260
+ this._dom.setAttribute(el, attr, desired.attrs[attr]);
1261
+ }
1262
+ if (desired.tag === "title") this._dom.setTextContent(el, desired.text ?? "");
1263
+ return el;
1264
+ }
1265
+ _reconcileAttrs(record, desired) {
1266
+ const nextKeys = new Set(Object.keys(desired.attrs));
1267
+ for (const attr of record.attrKeys) {
1268
+ if (!nextKeys.has(attr)) this._dom.removeAttribute(record.el, attr);
1269
+ }
1270
+ for (const attr of nextKeys) {
1271
+ this._dom.setAttribute(record.el, attr, desired.attrs[attr]);
1272
+ }
1273
+ if (desired.tag === "title") this._dom.setTextContent(record.el, desired.text ?? "");
1274
+ record.attrKeys = nextKeys;
1275
+ }
1276
+ /**
1277
+ * Seed `_applied` from server-emitted `[data-streetui-head-key]` tags already
1278
+ * in `document.head`. The subsequent diff reuses these elements when the
1279
+ * client desires the same key (no duplicate), rewrites them if the value
1280
+ * changed, or removes them if the client graph no longer wants them.
1281
+ */
1282
+ _adoptServerTags() {
1283
+ for (const child of this._dom.childNodes(this._head)) {
1284
+ if (!this._dom.isElement(child)) continue;
1285
+ const el = child;
1286
+ const key = this._dom.getAttribute(el, HEAD_KEY);
1287
+ if (key === null) continue;
1288
+ this._applied.set(key, {
1289
+ el,
1290
+ attrKeys: new Set(this._attrNames(el)),
1291
+ tag: this._dom.tagName(el)
1292
+ });
1293
+ }
1294
+ }
1295
+ /** The framework-managed attribute names currently on a server tag. */
1296
+ _attrNames(el) {
1297
+ const names = [];
1298
+ if (this._dom.getAttribute(el, HEAD_MARKER) !== null) names.push(HEAD_MARKER);
1299
+ if (this._dom.getAttribute(el, HEAD_KEY) !== null) names.push(HEAD_KEY);
1300
+ return names;
1301
+ }
1302
+ };
1303
+ function getHeadManager(ctx) {
1304
+ const head = ctx.dom.head();
1305
+ if (head === null) return null;
1306
+ const mutable = ctx;
1307
+ if (mutable.head === void 0) mutable.head = new HeadManager(ctx.dom, head);
1308
+ return mutable.head;
1309
+ }
1310
+ function wireHeadBehavior(ctx, graphNode, instance) {
1311
+ const manager = getHeadManager(ctx);
1312
+ if (manager === null) return;
1313
+ const descFn = ctx.graph.getHandler(`__head__${graphNode.id}`);
1314
+ if (descFn === void 0) return;
1315
+ const contribution = descFn();
1316
+ const nodeId = graphNode.id;
1317
+ manager.register(nodeId, contribution.entries);
1318
+ for (const entry of contribution.entries) {
1319
+ for (const attrKey of Object.keys(entry.attrs)) {
1320
+ const v = entry.attrs[attrKey];
1321
+ if (isSignalLike(v)) {
1322
+ instance.trackCleanup(v.subscribe(() => manager.apply()));
1323
+ }
1324
+ }
1325
+ if (isSignalLike(entry.text)) {
1326
+ instance.trackCleanup(entry.text.subscribe(() => manager.apply()));
1327
+ }
1328
+ }
1329
+ instance.trackCleanup(() => manager.unregister(nodeId));
1330
+ }
1331
+ function renderHead(compiled) {
1332
+ const graph = compiled.graph;
1333
+ const merged = /* @__PURE__ */ new Map();
1334
+ graph.walk((node) => {
1335
+ if (node.type !== "head") return;
1336
+ const descFn = graph.getHandler(`__head__${node.id}`);
1337
+ if (descFn === void 0) return;
1338
+ for (const entry of descFn().entries) {
1339
+ merged.set(entry.dedupKey, resolveEntry(entry));
1340
+ }
1341
+ });
1342
+ if (merged.size === 0) return "";
1343
+ const dom = new ServerDOMAdapter();
1344
+ let out = "";
1345
+ for (const [key, desired] of merged) {
1346
+ const el = dom.createElement(desired.tag);
1347
+ dom.setAttribute(el, HEAD_MARKER, "");
1348
+ dom.setAttribute(el, HEAD_KEY, key);
1349
+ for (const attr of Object.keys(desired.attrs)) {
1350
+ dom.setAttribute(el, attr, desired.attrs[attr]);
1351
+ }
1352
+ if (desired.tag === "title") dom.setTextContent(el, desired.text ?? "");
1353
+ out += dom.serializeOuter(el);
1354
+ }
1355
+ return out;
1356
+ }
1357
+
948
1358
  // ../renderer/src/mount.ts
949
1359
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
950
1360
  "text",
@@ -1067,6 +1477,9 @@ function mountNode(ctx, graphNode, parentDom) {
1067
1477
  const instance2 = new NodeInstance(graphNode, el2);
1068
1478
  ctx.instances.set(graphNode.id, instance2);
1069
1479
  wireEvents(dom, graph, graphNode, el2, instance2);
1480
+ if (graphNode.stateRefs.length !== 0) {
1481
+ wireSignalBindings(ctx, graphNode, instance2, linkUpdate(dom, el2));
1482
+ }
1070
1483
  dom.appendChild(parentDom, el2);
1071
1484
  return instance2;
1072
1485
  }
@@ -1097,7 +1510,7 @@ function mountNode(ctx, graphNode, parentDom) {
1097
1510
  instance2.addChild(childInstance);
1098
1511
  }
1099
1512
  dom.appendChild(parentDom, el2);
1100
- wireReactiveList(ctx, graphNode, instance2, el2);
1513
+ wireReactiveList(ctx, graphNode, instance2, el2, true);
1101
1514
  return instance2;
1102
1515
  }
1103
1516
  if (graphNode.type === "portal") {
@@ -1122,6 +1535,16 @@ function mountNode(ctx, graphNode, parentDom) {
1122
1535
  wireOverlayBehavior(ctx, graphNode, instance2, target);
1123
1536
  return instance2;
1124
1537
  }
1538
+ if (graphNode.type === "head") {
1539
+ const anchor = dom.createElement(resolveTag("head"));
1540
+ dom.setAttribute(anchor, "data-streetui-head-anchor", "");
1541
+ applyNodeProps(ctx, graphNode, anchor);
1542
+ const instance2 = new NodeInstance(graphNode, anchor);
1543
+ ctx.instances.set(graphNode.id, instance2);
1544
+ dom.appendChild(parentDom, anchor);
1545
+ wireHeadBehavior(ctx, graphNode, instance2);
1546
+ return instance2;
1547
+ }
1125
1548
  const tag = resolveTag(graphNode.type);
1126
1549
  const el = dom.createElement(tag);
1127
1550
  applyNodeProps(ctx, graphNode, el);
@@ -1186,6 +1609,17 @@ function buttonUpdate(dom, el) {
1186
1609
  }
1187
1610
  };
1188
1611
  }
1612
+ function linkUpdate(dom, el) {
1613
+ return (propKey, value) => {
1614
+ if (propKey === "label") {
1615
+ dom.setTextContent(el, String(value ?? ""));
1616
+ } else if (propKey === "href") {
1617
+ dom.setAttribute(el, "href", String(value ?? ""));
1618
+ } else {
1619
+ applyProp(dom, el, propKey, value);
1620
+ }
1621
+ };
1622
+ }
1189
1623
  function applyNodeProps(ctx, graphNode, el) {
1190
1624
  const props = graphNode.props;
1191
1625
  for (const key in props) {
@@ -1206,32 +1640,42 @@ function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
1206
1640
  instance.trackCleanup(unsub);
1207
1641
  }
1208
1642
  }
1209
- function wireReactiveList(ctx, graphNode, instance, el) {
1643
+ function wireReactiveList(ctx, graphNode, instance, el, runAppear = false) {
1210
1644
  const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
1211
1645
  const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
1212
1646
  if (plan === void 0 && build === void 0) return;
1647
+ const controller = new TransitionController(ctx.dom, ctx.graph, (leaving) => {
1648
+ const parent = ctx.dom.parentNode(leaving.domNode);
1649
+ if (parent !== null) ctx.dom.removeChild(parent, leaving.domNode);
1650
+ leaving.dispose();
1651
+ forgetInstance(ctx, leaving);
1652
+ ctx.graph.detachNode(leaving.graphNode);
1653
+ });
1654
+ const hooks = controller.hooks();
1655
+ if (runAppear) controller.appear(instance.children);
1213
1656
  for (const stateRef of graphNode.stateRefs) {
1214
1657
  if (stateRef.propKey !== "items") continue;
1215
1658
  const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
1216
1659
  if (sig === void 0 || typeof sig.subscribe !== "function") continue;
1217
1660
  const unsub = sig.subscribe((value) => {
1218
1661
  if (plan !== void 0) {
1219
- reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
1662
+ reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value), hooks);
1220
1663
  } else {
1221
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
1664
+ reconcileReactiveList(ctx, graphNode, instance, el, build(value), hooks);
1222
1665
  }
1223
1666
  });
1224
1667
  instance.trackCleanup(unsub);
1225
1668
  }
1226
1669
  }
1227
- function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
1670
+ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan, hooks) {
1228
1671
  const oldInstances = [...listInstance.children];
1229
1672
  const result = reconcileChildrenByPlan(
1230
1673
  ctx,
1231
1674
  listEl,
1232
1675
  oldInstances,
1233
1676
  plan,
1234
- (node, parent) => mountNode(ctx, node, parent)
1677
+ (node, parent) => mountNode(ctx, node, parent),
1678
+ hooks
1235
1679
  );
1236
1680
  listInstance.children.length = 0;
1237
1681
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -1246,14 +1690,15 @@ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan)
1246
1690
  for (const child of [...listNode.children]) listNode.removeChild(child);
1247
1691
  for (const inst of result.instances) listNode.appendChild(inst.graphNode);
1248
1692
  }
1249
- function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
1693
+ function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes, hooks) {
1250
1694
  const oldInstances = [...listInstance.children];
1251
1695
  const result = reconcileChildren(
1252
1696
  ctx,
1253
1697
  listEl,
1254
1698
  oldInstances,
1255
1699
  newNodes,
1256
- (node, parent) => mountNode(ctx, node, parent)
1700
+ (node, parent) => mountNode(ctx, node, parent),
1701
+ hooks
1257
1702
  );
1258
1703
  listInstance.children.length = 0;
1259
1704
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -1289,11 +1734,14 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
1289
1734
  const onOpenChange = (isOpen) => {
1290
1735
  if (isOpen) {
1291
1736
  if (desc.restoreFocus) saved = saveFocus(dom);
1292
- if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
1293
1737
  if (desc.modal) {
1294
1738
  active.push(trapFocus(dom, target));
1295
1739
  active.push(containFocus(dom, target));
1296
1740
  }
1741
+ if (desc.menu) {
1742
+ active.push(rovingMenu(dom, target));
1743
+ }
1744
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
1297
1745
  if (desc.closeOnEscape && desc.onClose !== void 0) {
1298
1746
  active.push(onEscape(dom, target, desc.onClose));
1299
1747
  }
@@ -1383,7 +1831,8 @@ function analyzeGraph(graph) {
1383
1831
  const isConditional = node.type === "conditional";
1384
1832
  const isPortal = node.type === "portal";
1385
1833
  const isComponent = node.type === "component";
1386
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent;
1834
+ const isHead = node.type === "head";
1835
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent && !isHead;
1387
1836
  const isStaticSubtree = isStatic && allChildrenStatic;
1388
1837
  nodes.set(node.id, {
1389
1838
  isStatic,
@@ -1470,13 +1919,14 @@ function renderToString(compiled, options = {}) {
1470
1919
  }
1471
1920
 
1472
1921
  // src/version.ts
1473
- var VERSION = "1.8.0";
1922
+ var VERSION = "2.0.0";
1474
1923
  // Annotate the CommonJS export names for ESM import in node:
1475
1924
  0 && (module.exports = {
1476
1925
  STATE_MARKER_ATTR,
1477
1926
  ServerDOMAdapter,
1478
1927
  VERSION,
1479
1928
  readState,
1929
+ renderHead,
1480
1930
  renderToString,
1481
1931
  serializeState
1482
1932
  });