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.js CHANGED
@@ -500,6 +500,9 @@ var ServerDOMAdapter = class {
500
500
  body() {
501
501
  return null;
502
502
  }
503
+ head() {
504
+ return null;
505
+ }
503
506
  activeElement() {
504
507
  return null;
505
508
  }
@@ -613,18 +616,25 @@ function trapFocus(dom, container) {
613
616
  dom.addEventListener(container, "keydown", onKeydown);
614
617
  return () => dom.removeEventListener(container, "keydown", onKeydown);
615
618
  }
619
+ var containmentStack = [];
616
620
  function containFocus(dom, container) {
617
621
  const body = dom.body();
618
622
  if (body === null) return () => {
619
623
  };
624
+ containmentStack.push(container);
620
625
  const onFocusIn = (event) => {
626
+ if (containmentStack[containmentStack.length - 1] !== container) return;
621
627
  const target = event.target;
622
628
  if (target !== null && !dom.contains(container, target)) {
623
629
  focusFirst(dom, container);
624
630
  }
625
631
  };
626
632
  dom.addEventListener(body, "focusin", onFocusIn);
627
- return () => dom.removeEventListener(body, "focusin", onFocusIn);
633
+ return () => {
634
+ dom.removeEventListener(body, "focusin", onFocusIn);
635
+ const index = containmentStack.lastIndexOf(container);
636
+ if (index !== -1) containmentStack.splice(index, 1);
637
+ };
628
638
  }
629
639
  function onEscape(dom, target, handler) {
630
640
  const onKeydown = (event) => {
@@ -633,6 +643,33 @@ function onEscape(dom, target, handler) {
633
643
  dom.addEventListener(target, "keydown", onKeydown);
634
644
  return () => dom.removeEventListener(target, "keydown", onKeydown);
635
645
  }
646
+ function rovingMenu(dom, container, selector = FOCUSABLE_SELECTOR) {
647
+ const onKeydown = (event) => {
648
+ const key = event.key;
649
+ const isActivate = key === "Enter" || key === " " || key === "Spacebar";
650
+ const isMove = key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End";
651
+ if (!isActivate && !isMove) return;
652
+ const items = getFocusable(dom, container, selector);
653
+ if (items.length === 0) return;
654
+ const active = dom.activeElement();
655
+ const index = active === null ? -1 : items.indexOf(active);
656
+ if (isActivate) {
657
+ if (index < 0) return;
658
+ event.preventDefault();
659
+ items[index].click?.();
660
+ return;
661
+ }
662
+ event.preventDefault();
663
+ let next;
664
+ if (key === "Home") next = 0;
665
+ else if (key === "End") next = items.length - 1;
666
+ else if (key === "ArrowDown") next = index < 0 ? 0 : (index + 1) % items.length;
667
+ else next = index <= 0 ? items.length - 1 : index - 1;
668
+ dom.focus(items[next]);
669
+ };
670
+ dom.addEventListener(container, "keydown", onKeydown);
671
+ return () => dom.removeEventListener(container, "keydown", onKeydown);
672
+ }
636
673
 
637
674
  // ../renderer/src/tag-map.ts
638
675
  var TAG_MAP = {
@@ -657,7 +694,13 @@ var TAG_MAP = {
657
694
  // its children are relocated to a document.body container on the browser
658
695
  // (see the portal branch in mount.ts). On the server (no body) it renders
659
696
  // inline, so the anchor tag is what SSR/hydration positionally match on.
660
- portal: "div"
697
+ portal: "div",
698
+ // A `head()` node renders as a neutral, empty inline anchor <div> at its
699
+ // declaration site (like a portal anchor). Its actual contribution — title/
700
+ // meta/link/etc. — is applied to `document.head` by `wireHeadBehavior` on the
701
+ // browser, and emitted separately by `renderHead()` on the server. Keeping a
702
+ // one-node/one-element anchor preserves positional hydration.
703
+ head: "div"
661
704
  };
662
705
  function resolveTag(type) {
663
706
  return TAG_MAP[type] ?? "div";
@@ -705,7 +748,7 @@ function patchNode(ctx, graphNode, propKey, newValue) {
705
748
  }
706
749
 
707
750
  // ../renderer/src/reconciliation.ts
708
- function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
751
+ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn, hooks) {
709
752
  const oldByKey = /* @__PURE__ */ new Map();
710
753
  for (const inst of oldInstances) {
711
754
  const key = inst.graphNode.key ?? inst.graphNode.id;
@@ -726,28 +769,33 @@ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
726
769
  }
727
770
  newInstances.push(existing);
728
771
  } else {
729
- const inst = mountFn(newNode, parentDom);
730
- newInstances.push(inst);
772
+ const reclaimed = hooks?.takeLeaving(key);
773
+ if (reclaimed !== void 0) {
774
+ patchExistingInstance(ctx, reclaimed, newNode);
775
+ reconcileItemChildren(ctx, reclaimed, newNode, mountFn);
776
+ hooks?.onEnter(reclaimed);
777
+ newInstances.push(reclaimed);
778
+ } else {
779
+ const inst = mountFn(newNode, parentDom);
780
+ hooks?.onEnter(inst);
781
+ newInstances.push(inst);
782
+ }
731
783
  }
732
784
  }
733
785
  const removed = [];
734
786
  for (const inst of oldInstances) {
735
787
  const key = inst.graphNode.key ?? inst.graphNode.id;
736
- if (!usedKeys.has(key)) {
737
- removed.push(inst);
738
- }
739
- }
740
- for (const inst of removed) {
788
+ if (usedKeys.has(key)) continue;
789
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
741
790
  const parent = ctx.dom.parentNode(inst.domNode);
742
- if (parent !== null) {
743
- ctx.dom.removeChild(parent, inst.domNode);
744
- }
791
+ if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
745
792
  inst.dispose();
793
+ removed.push(inst);
746
794
  }
747
795
  reorderDom(ctx, parentDom, newInstances);
748
796
  return { instances: newInstances, removed };
749
797
  }
750
- function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
798
+ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn, hooks) {
751
799
  const oldByKey = /* @__PURE__ */ new Map();
752
800
  for (const inst of oldInstances) {
753
801
  oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
@@ -774,21 +822,34 @@ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
774
822
  }
775
823
  newInstances.push(existing);
776
824
  } else {
777
- const freshNode = entry.build();
778
- built.push(freshNode);
779
- const inst = mountFn(freshNode, parentDom);
780
- newInstances.push(inst);
825
+ const reclaimed = hooks?.takeLeaving(entry.key);
826
+ if (reclaimed !== void 0) {
827
+ const freshNode = entry.build();
828
+ built.push(freshNode);
829
+ patchExistingInstance(ctx, reclaimed, freshNode);
830
+ reconcileItemChildren(ctx, reclaimed, freshNode, mountFn);
831
+ reclaimed.graphNode.setProp("_sig", freshNode.getProp("_sig"));
832
+ reclaimed.graphNode.setProp("_item", entry.item);
833
+ hooks?.onEnter(reclaimed);
834
+ newInstances.push(reclaimed);
835
+ } else {
836
+ const freshNode = entry.build();
837
+ built.push(freshNode);
838
+ const inst = mountFn(freshNode, parentDom);
839
+ hooks?.onEnter(inst);
840
+ newInstances.push(inst);
841
+ }
781
842
  }
782
843
  }
783
844
  const removed = [];
784
845
  for (const inst of oldInstances) {
785
846
  const key = inst.graphNode.key ?? inst.graphNode.id;
786
- if (!usedKeys.has(key)) removed.push(inst);
787
- }
788
- for (const inst of removed) {
847
+ if (usedKeys.has(key)) continue;
848
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
789
849
  const parent = ctx.dom.parentNode(inst.domNode);
790
850
  if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
791
851
  inst.dispose();
852
+ removed.push(inst);
792
853
  }
793
854
  reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
794
855
  return { instances: newInstances, removed, built };
@@ -914,6 +975,354 @@ function patchExistingInstance(ctx, instance, newNode) {
914
975
  }
915
976
  }
916
977
 
978
+ // ../renderer/src/transition.ts
979
+ function getResolvedTransition(graph, nodeId) {
980
+ const fn = graph.getHandler(`__transition__${nodeId}`);
981
+ return fn === void 0 ? void 0 : fn();
982
+ }
983
+ function host() {
984
+ return globalThis;
985
+ }
986
+ function nextFrame(cb) {
987
+ const h = host();
988
+ const raf = h.requestAnimationFrame;
989
+ if (typeof raf === "function") {
990
+ raf(() => raf(cb));
991
+ } else {
992
+ h.setTimeout(cb, 0);
993
+ }
994
+ }
995
+ function splitClass(value) {
996
+ if (value === null) return [];
997
+ const out = [];
998
+ for (const t of value.split(/\s+/)) if (t.length > 0) out.push(t);
999
+ return out;
1000
+ }
1001
+ function addClasses(dom, el, classes) {
1002
+ if (classes.length === 0) return;
1003
+ const current = splitClass(dom.getAttribute(el, "class"));
1004
+ let changed = false;
1005
+ for (const c of classes) {
1006
+ if (!current.includes(c)) {
1007
+ current.push(c);
1008
+ changed = true;
1009
+ }
1010
+ }
1011
+ if (changed) dom.setAttribute(el, "class", current.join(" "));
1012
+ }
1013
+ function removeClasses(dom, el, classes) {
1014
+ if (classes.length === 0) return;
1015
+ const current = splitClass(dom.getAttribute(el, "class"));
1016
+ const next = current.filter((c) => !classes.includes(c));
1017
+ if (next.length !== current.length) {
1018
+ if (next.length === 0) dom.removeAttribute(el, "class");
1019
+ else dom.setAttribute(el, "class", next.join(" "));
1020
+ }
1021
+ }
1022
+ function startRun(dom, el, active, from, to, duration, onDone) {
1023
+ const h = host();
1024
+ let finished = false;
1025
+ let timer = null;
1026
+ const onEvent = (e) => {
1027
+ if (e.target !== el) return;
1028
+ finish();
1029
+ };
1030
+ const detach = () => {
1031
+ dom.removeEventListener(el, "transitionend", onEvent);
1032
+ dom.removeEventListener(el, "animationend", onEvent);
1033
+ if (timer !== null) {
1034
+ h.clearTimeout(timer);
1035
+ timer = null;
1036
+ }
1037
+ };
1038
+ const finish = () => {
1039
+ if (finished) return;
1040
+ finished = true;
1041
+ detach();
1042
+ removeClasses(dom, el, active);
1043
+ removeClasses(dom, el, to);
1044
+ removeClasses(dom, el, from);
1045
+ onDone();
1046
+ };
1047
+ addClasses(dom, el, from);
1048
+ addClasses(dom, el, active);
1049
+ dom.addEventListener(el, "transitionend", onEvent);
1050
+ dom.addEventListener(el, "animationend", onEvent);
1051
+ timer = h.setTimeout(finish, duration);
1052
+ nextFrame(() => {
1053
+ if (finished) return;
1054
+ removeClasses(dom, el, from);
1055
+ addClasses(dom, el, to);
1056
+ });
1057
+ return {
1058
+ cancel: () => {
1059
+ if (finished) return;
1060
+ finished = true;
1061
+ detach();
1062
+ removeClasses(dom, el, active);
1063
+ removeClasses(dom, el, to);
1064
+ removeClasses(dom, el, from);
1065
+ }
1066
+ };
1067
+ }
1068
+ var TransitionController = class {
1069
+ constructor(dom, graph, finalize) {
1070
+ this.dom = dom;
1071
+ this.graph = graph;
1072
+ this.finalize = finalize;
1073
+ }
1074
+ leaving = /* @__PURE__ */ new Map();
1075
+ /** True only in a real DOM environment (browser). */
1076
+ get browser() {
1077
+ return this.dom.body() !== null;
1078
+ }
1079
+ keyOf(inst) {
1080
+ return inst.graphNode.key ?? inst.graphNode.id;
1081
+ }
1082
+ resolved(node) {
1083
+ return getResolvedTransition(this.graph, node.id);
1084
+ }
1085
+ /** Run the enter animation for `inst` if it carries a transition (browser only). */
1086
+ enter(inst) {
1087
+ if (!this.browser) return;
1088
+ const rt = this.resolved(inst.graphNode);
1089
+ if (rt === void 0) return;
1090
+ const el = inst.domNode;
1091
+ if (!this.dom.isElement(el)) return;
1092
+ startRun(this.dom, el, rt.enterActive, rt.enterFrom, rt.enterTo, rt.duration, () => {
1093
+ });
1094
+ }
1095
+ /**
1096
+ * Play `appear` for any initial child that opted into it (fresh browser mount
1097
+ * only — hydration must never animate appear, §22, and this is called only on
1098
+ * the mount path).
1099
+ */
1100
+ appear(children) {
1101
+ if (!this.browser) return;
1102
+ for (const child of children) {
1103
+ const rt = this.resolved(child.graphNode);
1104
+ if (rt !== void 0 && rt.appear) this.enter(child);
1105
+ }
1106
+ }
1107
+ hooks() {
1108
+ return {
1109
+ takeLeaving: (key) => {
1110
+ const entry = this.leaving.get(key);
1111
+ if (entry === void 0) return void 0;
1112
+ entry.run.cancel();
1113
+ this.leaving.delete(key);
1114
+ return entry.inst;
1115
+ },
1116
+ beginLeave: (inst) => {
1117
+ if (!this.browser) return false;
1118
+ const rt = this.resolved(inst.graphNode);
1119
+ if (rt === void 0) return false;
1120
+ const el = inst.domNode;
1121
+ if (!this.dom.isElement(el)) return false;
1122
+ const key = this.keyOf(inst);
1123
+ const prior = this.leaving.get(key);
1124
+ if (prior !== void 0) prior.run.cancel();
1125
+ const run = startRun(
1126
+ this.dom,
1127
+ el,
1128
+ rt.leaveActive,
1129
+ rt.leaveFrom,
1130
+ rt.leaveTo,
1131
+ rt.duration,
1132
+ () => {
1133
+ const current = this.leaving.get(key);
1134
+ if (current !== void 0 && current.run === run) {
1135
+ this.leaving.delete(key);
1136
+ this.finalize(inst);
1137
+ }
1138
+ }
1139
+ );
1140
+ this.leaving.set(key, { inst, run });
1141
+ return true;
1142
+ },
1143
+ onEnter: (inst) => this.enter(inst)
1144
+ };
1145
+ }
1146
+ };
1147
+
1148
+ // ../renderer/src/head.ts
1149
+ var HEAD_MARKER = "data-streetui-head";
1150
+ var HEAD_KEY = "data-streetui-head-key";
1151
+ function isSignalLike(v) {
1152
+ return v !== null && typeof v === "object" && typeof v["subscribe"] === "function" && typeof v["peek"] === "function";
1153
+ }
1154
+ function readValue(v) {
1155
+ if (isSignalLike(v)) return String(v.peek() ?? "");
1156
+ return String(v ?? "");
1157
+ }
1158
+ function resolveEntry(entry) {
1159
+ const attrs = {};
1160
+ for (const key of Object.keys(entry.attrs)) {
1161
+ attrs[key] = readValue(entry.attrs[key]);
1162
+ }
1163
+ const resolved = { tag: entry.tag, attrs };
1164
+ if (entry.tag === "title") {
1165
+ return { ...resolved, text: readValue(entry.text) };
1166
+ }
1167
+ return resolved;
1168
+ }
1169
+ var HeadManager = class {
1170
+ _dom;
1171
+ _head;
1172
+ _contributions = /* @__PURE__ */ new Map();
1173
+ _applied = /* @__PURE__ */ new Map();
1174
+ _order = 0;
1175
+ _adopted = false;
1176
+ constructor(dom, head) {
1177
+ this._dom = dom;
1178
+ this._head = head;
1179
+ }
1180
+ /** Register (or replace) a node's contribution and re-apply the merged result. */
1181
+ register(nodeId, entries) {
1182
+ this._contributions.set(nodeId, { order: this._order++, entries });
1183
+ this.apply();
1184
+ }
1185
+ /** Withdraw a node's contribution (unmount / route change) and re-apply. */
1186
+ unregister(nodeId) {
1187
+ if (this._contributions.delete(nodeId)) this.apply();
1188
+ }
1189
+ /** Recompute the merged head and patch `document.head` to match. */
1190
+ apply() {
1191
+ if (!this._adopted) {
1192
+ this._adoptServerTags();
1193
+ this._adopted = true;
1194
+ }
1195
+ const ordered = [...this._contributions.values()].sort((a, b) => a.order - b.order);
1196
+ const merged = /* @__PURE__ */ new Map();
1197
+ for (const contribution of ordered) {
1198
+ for (const entry of contribution.entries) {
1199
+ merged.set(entry.dedupKey, resolveEntry(entry));
1200
+ }
1201
+ }
1202
+ for (const [key, desired] of merged) {
1203
+ const existing = this._applied.get(key);
1204
+ if (existing !== void 0 && existing.tag === desired.tag) {
1205
+ this._reconcileAttrs(existing, desired);
1206
+ } else {
1207
+ if (existing !== void 0) {
1208
+ this._dom.removeChild(this._head, existing.el);
1209
+ this._applied.delete(key);
1210
+ }
1211
+ const el = this._createTag(key, desired);
1212
+ this._dom.appendChild(this._head, el);
1213
+ this._applied.set(key, { el, attrKeys: new Set(Object.keys(desired.attrs)), tag: desired.tag });
1214
+ }
1215
+ }
1216
+ for (const [key, record] of [...this._applied]) {
1217
+ if (!merged.has(key)) {
1218
+ this._dom.removeChild(this._head, record.el);
1219
+ this._applied.delete(key);
1220
+ }
1221
+ }
1222
+ }
1223
+ _createTag(key, desired) {
1224
+ const el = this._dom.createElement(desired.tag);
1225
+ this._dom.setAttribute(el, HEAD_MARKER, "");
1226
+ this._dom.setAttribute(el, HEAD_KEY, key);
1227
+ for (const attr of Object.keys(desired.attrs)) {
1228
+ this._dom.setAttribute(el, attr, desired.attrs[attr]);
1229
+ }
1230
+ if (desired.tag === "title") this._dom.setTextContent(el, desired.text ?? "");
1231
+ return el;
1232
+ }
1233
+ _reconcileAttrs(record, desired) {
1234
+ const nextKeys = new Set(Object.keys(desired.attrs));
1235
+ for (const attr of record.attrKeys) {
1236
+ if (!nextKeys.has(attr)) this._dom.removeAttribute(record.el, attr);
1237
+ }
1238
+ for (const attr of nextKeys) {
1239
+ this._dom.setAttribute(record.el, attr, desired.attrs[attr]);
1240
+ }
1241
+ if (desired.tag === "title") this._dom.setTextContent(record.el, desired.text ?? "");
1242
+ record.attrKeys = nextKeys;
1243
+ }
1244
+ /**
1245
+ * Seed `_applied` from server-emitted `[data-streetui-head-key]` tags already
1246
+ * in `document.head`. The subsequent diff reuses these elements when the
1247
+ * client desires the same key (no duplicate), rewrites them if the value
1248
+ * changed, or removes them if the client graph no longer wants them.
1249
+ */
1250
+ _adoptServerTags() {
1251
+ for (const child of this._dom.childNodes(this._head)) {
1252
+ if (!this._dom.isElement(child)) continue;
1253
+ const el = child;
1254
+ const key = this._dom.getAttribute(el, HEAD_KEY);
1255
+ if (key === null) continue;
1256
+ this._applied.set(key, {
1257
+ el,
1258
+ attrKeys: new Set(this._attrNames(el)),
1259
+ tag: this._dom.tagName(el)
1260
+ });
1261
+ }
1262
+ }
1263
+ /** The framework-managed attribute names currently on a server tag. */
1264
+ _attrNames(el) {
1265
+ const names = [];
1266
+ if (this._dom.getAttribute(el, HEAD_MARKER) !== null) names.push(HEAD_MARKER);
1267
+ if (this._dom.getAttribute(el, HEAD_KEY) !== null) names.push(HEAD_KEY);
1268
+ return names;
1269
+ }
1270
+ };
1271
+ function getHeadManager(ctx) {
1272
+ const head = ctx.dom.head();
1273
+ if (head === null) return null;
1274
+ const mutable = ctx;
1275
+ if (mutable.head === void 0) mutable.head = new HeadManager(ctx.dom, head);
1276
+ return mutable.head;
1277
+ }
1278
+ function wireHeadBehavior(ctx, graphNode, instance) {
1279
+ const manager = getHeadManager(ctx);
1280
+ if (manager === null) return;
1281
+ const descFn = ctx.graph.getHandler(`__head__${graphNode.id}`);
1282
+ if (descFn === void 0) return;
1283
+ const contribution = descFn();
1284
+ const nodeId = graphNode.id;
1285
+ manager.register(nodeId, contribution.entries);
1286
+ for (const entry of contribution.entries) {
1287
+ for (const attrKey of Object.keys(entry.attrs)) {
1288
+ const v = entry.attrs[attrKey];
1289
+ if (isSignalLike(v)) {
1290
+ instance.trackCleanup(v.subscribe(() => manager.apply()));
1291
+ }
1292
+ }
1293
+ if (isSignalLike(entry.text)) {
1294
+ instance.trackCleanup(entry.text.subscribe(() => manager.apply()));
1295
+ }
1296
+ }
1297
+ instance.trackCleanup(() => manager.unregister(nodeId));
1298
+ }
1299
+ function renderHead(compiled) {
1300
+ const graph = compiled.graph;
1301
+ const merged = /* @__PURE__ */ new Map();
1302
+ graph.walk((node) => {
1303
+ if (node.type !== "head") return;
1304
+ const descFn = graph.getHandler(`__head__${node.id}`);
1305
+ if (descFn === void 0) return;
1306
+ for (const entry of descFn().entries) {
1307
+ merged.set(entry.dedupKey, resolveEntry(entry));
1308
+ }
1309
+ });
1310
+ if (merged.size === 0) return "";
1311
+ const dom = new ServerDOMAdapter();
1312
+ let out = "";
1313
+ for (const [key, desired] of merged) {
1314
+ const el = dom.createElement(desired.tag);
1315
+ dom.setAttribute(el, HEAD_MARKER, "");
1316
+ dom.setAttribute(el, HEAD_KEY, key);
1317
+ for (const attr of Object.keys(desired.attrs)) {
1318
+ dom.setAttribute(el, attr, desired.attrs[attr]);
1319
+ }
1320
+ if (desired.tag === "title") dom.setTextContent(el, desired.text ?? "");
1321
+ out += dom.serializeOuter(el);
1322
+ }
1323
+ return out;
1324
+ }
1325
+
917
1326
  // ../renderer/src/mount.ts
918
1327
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
919
1328
  "text",
@@ -1036,6 +1445,9 @@ function mountNode(ctx, graphNode, parentDom) {
1036
1445
  const instance2 = new NodeInstance(graphNode, el2);
1037
1446
  ctx.instances.set(graphNode.id, instance2);
1038
1447
  wireEvents(dom, graph, graphNode, el2, instance2);
1448
+ if (graphNode.stateRefs.length !== 0) {
1449
+ wireSignalBindings(ctx, graphNode, instance2, linkUpdate(dom, el2));
1450
+ }
1039
1451
  dom.appendChild(parentDom, el2);
1040
1452
  return instance2;
1041
1453
  }
@@ -1066,7 +1478,7 @@ function mountNode(ctx, graphNode, parentDom) {
1066
1478
  instance2.addChild(childInstance);
1067
1479
  }
1068
1480
  dom.appendChild(parentDom, el2);
1069
- wireReactiveList(ctx, graphNode, instance2, el2);
1481
+ wireReactiveList(ctx, graphNode, instance2, el2, true);
1070
1482
  return instance2;
1071
1483
  }
1072
1484
  if (graphNode.type === "portal") {
@@ -1091,6 +1503,16 @@ function mountNode(ctx, graphNode, parentDom) {
1091
1503
  wireOverlayBehavior(ctx, graphNode, instance2, target);
1092
1504
  return instance2;
1093
1505
  }
1506
+ if (graphNode.type === "head") {
1507
+ const anchor = dom.createElement(resolveTag("head"));
1508
+ dom.setAttribute(anchor, "data-streetui-head-anchor", "");
1509
+ applyNodeProps(ctx, graphNode, anchor);
1510
+ const instance2 = new NodeInstance(graphNode, anchor);
1511
+ ctx.instances.set(graphNode.id, instance2);
1512
+ dom.appendChild(parentDom, anchor);
1513
+ wireHeadBehavior(ctx, graphNode, instance2);
1514
+ return instance2;
1515
+ }
1094
1516
  const tag = resolveTag(graphNode.type);
1095
1517
  const el = dom.createElement(tag);
1096
1518
  applyNodeProps(ctx, graphNode, el);
@@ -1155,6 +1577,17 @@ function buttonUpdate(dom, el) {
1155
1577
  }
1156
1578
  };
1157
1579
  }
1580
+ function linkUpdate(dom, el) {
1581
+ return (propKey, value) => {
1582
+ if (propKey === "label") {
1583
+ dom.setTextContent(el, String(value ?? ""));
1584
+ } else if (propKey === "href") {
1585
+ dom.setAttribute(el, "href", String(value ?? ""));
1586
+ } else {
1587
+ applyProp(dom, el, propKey, value);
1588
+ }
1589
+ };
1590
+ }
1158
1591
  function applyNodeProps(ctx, graphNode, el) {
1159
1592
  const props = graphNode.props;
1160
1593
  for (const key in props) {
@@ -1175,32 +1608,42 @@ function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
1175
1608
  instance.trackCleanup(unsub);
1176
1609
  }
1177
1610
  }
1178
- function wireReactiveList(ctx, graphNode, instance, el) {
1611
+ function wireReactiveList(ctx, graphNode, instance, el, runAppear = false) {
1179
1612
  const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
1180
1613
  const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
1181
1614
  if (plan === void 0 && build === void 0) return;
1615
+ const controller = new TransitionController(ctx.dom, ctx.graph, (leaving) => {
1616
+ const parent = ctx.dom.parentNode(leaving.domNode);
1617
+ if (parent !== null) ctx.dom.removeChild(parent, leaving.domNode);
1618
+ leaving.dispose();
1619
+ forgetInstance(ctx, leaving);
1620
+ ctx.graph.detachNode(leaving.graphNode);
1621
+ });
1622
+ const hooks = controller.hooks();
1623
+ if (runAppear) controller.appear(instance.children);
1182
1624
  for (const stateRef of graphNode.stateRefs) {
1183
1625
  if (stateRef.propKey !== "items") continue;
1184
1626
  const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
1185
1627
  if (sig === void 0 || typeof sig.subscribe !== "function") continue;
1186
1628
  const unsub = sig.subscribe((value) => {
1187
1629
  if (plan !== void 0) {
1188
- reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
1630
+ reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value), hooks);
1189
1631
  } else {
1190
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
1632
+ reconcileReactiveList(ctx, graphNode, instance, el, build(value), hooks);
1191
1633
  }
1192
1634
  });
1193
1635
  instance.trackCleanup(unsub);
1194
1636
  }
1195
1637
  }
1196
- function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
1638
+ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan, hooks) {
1197
1639
  const oldInstances = [...listInstance.children];
1198
1640
  const result = reconcileChildrenByPlan(
1199
1641
  ctx,
1200
1642
  listEl,
1201
1643
  oldInstances,
1202
1644
  plan,
1203
- (node, parent) => mountNode(ctx, node, parent)
1645
+ (node, parent) => mountNode(ctx, node, parent),
1646
+ hooks
1204
1647
  );
1205
1648
  listInstance.children.length = 0;
1206
1649
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -1215,14 +1658,15 @@ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan)
1215
1658
  for (const child of [...listNode.children]) listNode.removeChild(child);
1216
1659
  for (const inst of result.instances) listNode.appendChild(inst.graphNode);
1217
1660
  }
1218
- function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
1661
+ function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes, hooks) {
1219
1662
  const oldInstances = [...listInstance.children];
1220
1663
  const result = reconcileChildren(
1221
1664
  ctx,
1222
1665
  listEl,
1223
1666
  oldInstances,
1224
1667
  newNodes,
1225
- (node, parent) => mountNode(ctx, node, parent)
1668
+ (node, parent) => mountNode(ctx, node, parent),
1669
+ hooks
1226
1670
  );
1227
1671
  listInstance.children.length = 0;
1228
1672
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -1258,11 +1702,14 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
1258
1702
  const onOpenChange = (isOpen) => {
1259
1703
  if (isOpen) {
1260
1704
  if (desc.restoreFocus) saved = saveFocus(dom);
1261
- if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
1262
1705
  if (desc.modal) {
1263
1706
  active.push(trapFocus(dom, target));
1264
1707
  active.push(containFocus(dom, target));
1265
1708
  }
1709
+ if (desc.menu) {
1710
+ active.push(rovingMenu(dom, target));
1711
+ }
1712
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
1266
1713
  if (desc.closeOnEscape && desc.onClose !== void 0) {
1267
1714
  active.push(onEscape(dom, target, desc.onClose));
1268
1715
  }
@@ -1352,7 +1799,8 @@ function analyzeGraph(graph) {
1352
1799
  const isConditional = node.type === "conditional";
1353
1800
  const isPortal = node.type === "portal";
1354
1801
  const isComponent = node.type === "component";
1355
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent;
1802
+ const isHead = node.type === "head";
1803
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent && !isHead;
1356
1804
  const isStaticSubtree = isStatic && allChildrenStatic;
1357
1805
  nodes.set(node.id, {
1358
1806
  isStatic,
@@ -1439,12 +1887,13 @@ function renderToString(compiled, options = {}) {
1439
1887
  }
1440
1888
 
1441
1889
  // src/version.ts
1442
- var VERSION = "1.8.0";
1890
+ var VERSION = "2.0.0";
1443
1891
  export {
1444
1892
  STATE_MARKER_ATTR,
1445
1893
  ServerDOMAdapter,
1446
1894
  VERSION,
1447
1895
  readState,
1896
+ renderHead,
1448
1897
  renderToString,
1449
1898
  serializeState
1450
1899
  };