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/testing.cjs CHANGED
@@ -22,22 +22,29 @@ var testing_exports = {};
22
22
  __export(testing_exports, {
23
23
  VERSION: () => VERSION,
24
24
  analyzeGraph: () => analyzeGraph,
25
+ blur: () => blur,
26
+ clickOutside: () => clickOutside,
27
+ closeOverlay: () => closeOverlay,
25
28
  findAllByRole: () => findAllByRole,
26
29
  findAllComponents: () => findAllComponents,
27
30
  findByRole: () => findByRole,
28
31
  findByText: () => findByText,
29
32
  findComponent: () => findComponent,
30
33
  flushUpdates: () => flushUpdates,
34
+ focus: () => focus,
31
35
  formatInspection: () => formatInspection,
32
36
  getComponentName: () => getComponentName,
33
37
  hydrateComponent: () => hydrateComponent,
34
38
  inspectCompilation: () => inspectCompilation,
39
+ openOverlay: () => openOverlay,
40
+ pressKey: () => pressKey,
35
41
  render: () => render,
36
42
  renderComponent: () => renderComponent,
37
43
  renderOnce: () => renderOnce,
38
44
  renderServerThenHydrate: () => renderServerThenHydrate,
39
45
  trigger: () => trigger,
40
- waitFor: () => waitFor
46
+ waitFor: () => waitFor,
47
+ waitForTransition: () => waitForTransition
41
48
  });
42
49
  module.exports = __toCommonJS(testing_exports);
43
50
 
@@ -308,6 +315,9 @@ var BrowserDOMAdapter = class {
308
315
  body() {
309
316
  return document.body ?? null;
310
317
  }
318
+ head() {
319
+ return document.head ?? null;
320
+ }
311
321
  activeElement() {
312
322
  return document.activeElement ?? null;
313
323
  }
@@ -686,6 +696,9 @@ var ServerDOMAdapter = class {
686
696
  body() {
687
697
  return null;
688
698
  }
699
+ head() {
700
+ return null;
701
+ }
689
702
  activeElement() {
690
703
  return null;
691
704
  }
@@ -799,18 +812,25 @@ function trapFocus(dom, container) {
799
812
  dom.addEventListener(container, "keydown", onKeydown);
800
813
  return () => dom.removeEventListener(container, "keydown", onKeydown);
801
814
  }
815
+ var containmentStack = [];
802
816
  function containFocus(dom, container) {
803
817
  const body = dom.body();
804
818
  if (body === null) return () => {
805
819
  };
820
+ containmentStack.push(container);
806
821
  const onFocusIn = (event) => {
822
+ if (containmentStack[containmentStack.length - 1] !== container) return;
807
823
  const target = event.target;
808
824
  if (target !== null && !dom.contains(container, target)) {
809
825
  focusFirst(dom, container);
810
826
  }
811
827
  };
812
828
  dom.addEventListener(body, "focusin", onFocusIn);
813
- return () => dom.removeEventListener(body, "focusin", onFocusIn);
829
+ return () => {
830
+ dom.removeEventListener(body, "focusin", onFocusIn);
831
+ const index = containmentStack.lastIndexOf(container);
832
+ if (index !== -1) containmentStack.splice(index, 1);
833
+ };
814
834
  }
815
835
  function onEscape(dom, target, handler) {
816
836
  const onKeydown = (event) => {
@@ -819,6 +839,33 @@ function onEscape(dom, target, handler) {
819
839
  dom.addEventListener(target, "keydown", onKeydown);
820
840
  return () => dom.removeEventListener(target, "keydown", onKeydown);
821
841
  }
842
+ function rovingMenu(dom, container, selector = FOCUSABLE_SELECTOR) {
843
+ const onKeydown = (event) => {
844
+ const key = event.key;
845
+ const isActivate = key === "Enter" || key === " " || key === "Spacebar";
846
+ const isMove = key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End";
847
+ if (!isActivate && !isMove) return;
848
+ const items = getFocusable(dom, container, selector);
849
+ if (items.length === 0) return;
850
+ const active = dom.activeElement();
851
+ const index = active === null ? -1 : items.indexOf(active);
852
+ if (isActivate) {
853
+ if (index < 0) return;
854
+ event.preventDefault();
855
+ items[index].click?.();
856
+ return;
857
+ }
858
+ event.preventDefault();
859
+ let next;
860
+ if (key === "Home") next = 0;
861
+ else if (key === "End") next = items.length - 1;
862
+ else if (key === "ArrowDown") next = index < 0 ? 0 : (index + 1) % items.length;
863
+ else next = index <= 0 ? items.length - 1 : index - 1;
864
+ dom.focus(items[next]);
865
+ };
866
+ dom.addEventListener(container, "keydown", onKeydown);
867
+ return () => dom.removeEventListener(container, "keydown", onKeydown);
868
+ }
822
869
 
823
870
  // ../renderer/src/render-context.ts
824
871
  function createRenderContext(dom, graph, container, hydrationDiagnostics, staticHTML) {
@@ -982,7 +1029,13 @@ var TAG_MAP = {
982
1029
  // its children are relocated to a document.body container on the browser
983
1030
  // (see the portal branch in mount.ts). On the server (no body) it renders
984
1031
  // inline, so the anchor tag is what SSR/hydration positionally match on.
985
- portal: "div"
1032
+ portal: "div",
1033
+ // A `head()` node renders as a neutral, empty inline anchor <div> at its
1034
+ // declaration site (like a portal anchor). Its actual contribution — title/
1035
+ // meta/link/etc. — is applied to `document.head` by `wireHeadBehavior` on the
1036
+ // browser, and emitted separately by `renderHead()` on the server. Keeping a
1037
+ // one-node/one-element anchor preserves positional hydration.
1038
+ head: "div"
986
1039
  };
987
1040
  function resolveTag(type) {
988
1041
  return TAG_MAP[type] ?? "div";
@@ -1030,7 +1083,7 @@ function patchNode(ctx, graphNode, propKey, newValue) {
1030
1083
  }
1031
1084
 
1032
1085
  // ../renderer/src/reconciliation.ts
1033
- function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
1086
+ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn, hooks) {
1034
1087
  const oldByKey = /* @__PURE__ */ new Map();
1035
1088
  for (const inst of oldInstances) {
1036
1089
  const key = inst.graphNode.key ?? inst.graphNode.id;
@@ -1051,28 +1104,33 @@ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
1051
1104
  }
1052
1105
  newInstances.push(existing);
1053
1106
  } else {
1054
- const inst = mountFn(newNode, parentDom);
1055
- newInstances.push(inst);
1107
+ const reclaimed = hooks?.takeLeaving(key);
1108
+ if (reclaimed !== void 0) {
1109
+ patchExistingInstance(ctx, reclaimed, newNode);
1110
+ reconcileItemChildren(ctx, reclaimed, newNode, mountFn);
1111
+ hooks?.onEnter(reclaimed);
1112
+ newInstances.push(reclaimed);
1113
+ } else {
1114
+ const inst = mountFn(newNode, parentDom);
1115
+ hooks?.onEnter(inst);
1116
+ newInstances.push(inst);
1117
+ }
1056
1118
  }
1057
1119
  }
1058
1120
  const removed = [];
1059
1121
  for (const inst of oldInstances) {
1060
1122
  const key = inst.graphNode.key ?? inst.graphNode.id;
1061
- if (!usedKeys.has(key)) {
1062
- removed.push(inst);
1063
- }
1064
- }
1065
- for (const inst of removed) {
1123
+ if (usedKeys.has(key)) continue;
1124
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
1066
1125
  const parent = ctx.dom.parentNode(inst.domNode);
1067
- if (parent !== null) {
1068
- ctx.dom.removeChild(parent, inst.domNode);
1069
- }
1126
+ if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
1070
1127
  inst.dispose();
1128
+ removed.push(inst);
1071
1129
  }
1072
1130
  reorderDom(ctx, parentDom, newInstances);
1073
1131
  return { instances: newInstances, removed };
1074
1132
  }
1075
- function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
1133
+ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn, hooks) {
1076
1134
  const oldByKey = /* @__PURE__ */ new Map();
1077
1135
  for (const inst of oldInstances) {
1078
1136
  oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
@@ -1099,21 +1157,34 @@ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
1099
1157
  }
1100
1158
  newInstances.push(existing);
1101
1159
  } else {
1102
- const freshNode = entry.build();
1103
- built.push(freshNode);
1104
- const inst = mountFn(freshNode, parentDom);
1105
- newInstances.push(inst);
1160
+ const reclaimed = hooks?.takeLeaving(entry.key);
1161
+ if (reclaimed !== void 0) {
1162
+ const freshNode = entry.build();
1163
+ built.push(freshNode);
1164
+ patchExistingInstance(ctx, reclaimed, freshNode);
1165
+ reconcileItemChildren(ctx, reclaimed, freshNode, mountFn);
1166
+ reclaimed.graphNode.setProp("_sig", freshNode.getProp("_sig"));
1167
+ reclaimed.graphNode.setProp("_item", entry.item);
1168
+ hooks?.onEnter(reclaimed);
1169
+ newInstances.push(reclaimed);
1170
+ } else {
1171
+ const freshNode = entry.build();
1172
+ built.push(freshNode);
1173
+ const inst = mountFn(freshNode, parentDom);
1174
+ hooks?.onEnter(inst);
1175
+ newInstances.push(inst);
1176
+ }
1106
1177
  }
1107
1178
  }
1108
1179
  const removed = [];
1109
1180
  for (const inst of oldInstances) {
1110
1181
  const key = inst.graphNode.key ?? inst.graphNode.id;
1111
- if (!usedKeys.has(key)) removed.push(inst);
1112
- }
1113
- for (const inst of removed) {
1182
+ if (usedKeys.has(key)) continue;
1183
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
1114
1184
  const parent = ctx.dom.parentNode(inst.domNode);
1115
1185
  if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
1116
1186
  inst.dispose();
1187
+ removed.push(inst);
1117
1188
  }
1118
1189
  reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
1119
1190
  return { instances: newInstances, removed, built };
@@ -1239,6 +1310,328 @@ function patchExistingInstance(ctx, instance, newNode) {
1239
1310
  }
1240
1311
  }
1241
1312
 
1313
+ // ../renderer/src/transition.ts
1314
+ function getResolvedTransition(graph, nodeId) {
1315
+ const fn = graph.getHandler(`__transition__${nodeId}`);
1316
+ return fn === void 0 ? void 0 : fn();
1317
+ }
1318
+ function host() {
1319
+ return globalThis;
1320
+ }
1321
+ function nextFrame(cb) {
1322
+ const h = host();
1323
+ const raf = h.requestAnimationFrame;
1324
+ if (typeof raf === "function") {
1325
+ raf(() => raf(cb));
1326
+ } else {
1327
+ h.setTimeout(cb, 0);
1328
+ }
1329
+ }
1330
+ function splitClass(value) {
1331
+ if (value === null) return [];
1332
+ const out = [];
1333
+ for (const t of value.split(/\s+/)) if (t.length > 0) out.push(t);
1334
+ return out;
1335
+ }
1336
+ function addClasses(dom, el, classes2) {
1337
+ if (classes2.length === 0) return;
1338
+ const current = splitClass(dom.getAttribute(el, "class"));
1339
+ let changed = false;
1340
+ for (const c of classes2) {
1341
+ if (!current.includes(c)) {
1342
+ current.push(c);
1343
+ changed = true;
1344
+ }
1345
+ }
1346
+ if (changed) dom.setAttribute(el, "class", current.join(" "));
1347
+ }
1348
+ function removeClasses(dom, el, classes2) {
1349
+ if (classes2.length === 0) return;
1350
+ const current = splitClass(dom.getAttribute(el, "class"));
1351
+ const next = current.filter((c) => !classes2.includes(c));
1352
+ if (next.length !== current.length) {
1353
+ if (next.length === 0) dom.removeAttribute(el, "class");
1354
+ else dom.setAttribute(el, "class", next.join(" "));
1355
+ }
1356
+ }
1357
+ function startRun(dom, el, active, from, to, duration, onDone) {
1358
+ const h = host();
1359
+ let finished = false;
1360
+ let timer = null;
1361
+ const onEvent = (e) => {
1362
+ if (e.target !== el) return;
1363
+ finish();
1364
+ };
1365
+ const detach = () => {
1366
+ dom.removeEventListener(el, "transitionend", onEvent);
1367
+ dom.removeEventListener(el, "animationend", onEvent);
1368
+ if (timer !== null) {
1369
+ h.clearTimeout(timer);
1370
+ timer = null;
1371
+ }
1372
+ };
1373
+ const finish = () => {
1374
+ if (finished) return;
1375
+ finished = true;
1376
+ detach();
1377
+ removeClasses(dom, el, active);
1378
+ removeClasses(dom, el, to);
1379
+ removeClasses(dom, el, from);
1380
+ onDone();
1381
+ };
1382
+ addClasses(dom, el, from);
1383
+ addClasses(dom, el, active);
1384
+ dom.addEventListener(el, "transitionend", onEvent);
1385
+ dom.addEventListener(el, "animationend", onEvent);
1386
+ timer = h.setTimeout(finish, duration);
1387
+ nextFrame(() => {
1388
+ if (finished) return;
1389
+ removeClasses(dom, el, from);
1390
+ addClasses(dom, el, to);
1391
+ });
1392
+ return {
1393
+ cancel: () => {
1394
+ if (finished) return;
1395
+ finished = true;
1396
+ detach();
1397
+ removeClasses(dom, el, active);
1398
+ removeClasses(dom, el, to);
1399
+ removeClasses(dom, el, from);
1400
+ }
1401
+ };
1402
+ }
1403
+ var TransitionController = class {
1404
+ constructor(dom, graph, finalize) {
1405
+ this.dom = dom;
1406
+ this.graph = graph;
1407
+ this.finalize = finalize;
1408
+ }
1409
+ leaving = /* @__PURE__ */ new Map();
1410
+ /** True only in a real DOM environment (browser). */
1411
+ get browser() {
1412
+ return this.dom.body() !== null;
1413
+ }
1414
+ keyOf(inst) {
1415
+ return inst.graphNode.key ?? inst.graphNode.id;
1416
+ }
1417
+ resolved(node) {
1418
+ return getResolvedTransition(this.graph, node.id);
1419
+ }
1420
+ /** Run the enter animation for `inst` if it carries a transition (browser only). */
1421
+ enter(inst) {
1422
+ if (!this.browser) return;
1423
+ const rt = this.resolved(inst.graphNode);
1424
+ if (rt === void 0) return;
1425
+ const el = inst.domNode;
1426
+ if (!this.dom.isElement(el)) return;
1427
+ startRun(this.dom, el, rt.enterActive, rt.enterFrom, rt.enterTo, rt.duration, () => {
1428
+ });
1429
+ }
1430
+ /**
1431
+ * Play `appear` for any initial child that opted into it (fresh browser mount
1432
+ * only — hydration must never animate appear, §22, and this is called only on
1433
+ * the mount path).
1434
+ */
1435
+ appear(children) {
1436
+ if (!this.browser) return;
1437
+ for (const child of children) {
1438
+ const rt = this.resolved(child.graphNode);
1439
+ if (rt !== void 0 && rt.appear) this.enter(child);
1440
+ }
1441
+ }
1442
+ hooks() {
1443
+ return {
1444
+ takeLeaving: (key) => {
1445
+ const entry = this.leaving.get(key);
1446
+ if (entry === void 0) return void 0;
1447
+ entry.run.cancel();
1448
+ this.leaving.delete(key);
1449
+ return entry.inst;
1450
+ },
1451
+ beginLeave: (inst) => {
1452
+ if (!this.browser) return false;
1453
+ const rt = this.resolved(inst.graphNode);
1454
+ if (rt === void 0) return false;
1455
+ const el = inst.domNode;
1456
+ if (!this.dom.isElement(el)) return false;
1457
+ const key = this.keyOf(inst);
1458
+ const prior = this.leaving.get(key);
1459
+ if (prior !== void 0) prior.run.cancel();
1460
+ const run = startRun(
1461
+ this.dom,
1462
+ el,
1463
+ rt.leaveActive,
1464
+ rt.leaveFrom,
1465
+ rt.leaveTo,
1466
+ rt.duration,
1467
+ () => {
1468
+ const current = this.leaving.get(key);
1469
+ if (current !== void 0 && current.run === run) {
1470
+ this.leaving.delete(key);
1471
+ this.finalize(inst);
1472
+ }
1473
+ }
1474
+ );
1475
+ this.leaving.set(key, { inst, run });
1476
+ return true;
1477
+ },
1478
+ onEnter: (inst) => this.enter(inst)
1479
+ };
1480
+ }
1481
+ };
1482
+
1483
+ // ../renderer/src/head.ts
1484
+ var HEAD_MARKER = "data-streetui-head";
1485
+ var HEAD_KEY = "data-streetui-head-key";
1486
+ function isSignalLike(v) {
1487
+ return v !== null && typeof v === "object" && typeof v["subscribe"] === "function" && typeof v["peek"] === "function";
1488
+ }
1489
+ function readValue(v) {
1490
+ if (isSignalLike(v)) return String(v.peek() ?? "");
1491
+ return String(v ?? "");
1492
+ }
1493
+ function resolveEntry(entry) {
1494
+ const attrs = {};
1495
+ for (const key of Object.keys(entry.attrs)) {
1496
+ attrs[key] = readValue(entry.attrs[key]);
1497
+ }
1498
+ const resolved = { tag: entry.tag, attrs };
1499
+ if (entry.tag === "title") {
1500
+ return { ...resolved, text: readValue(entry.text) };
1501
+ }
1502
+ return resolved;
1503
+ }
1504
+ var HeadManager = class {
1505
+ _dom;
1506
+ _head;
1507
+ _contributions = /* @__PURE__ */ new Map();
1508
+ _applied = /* @__PURE__ */ new Map();
1509
+ _order = 0;
1510
+ _adopted = false;
1511
+ constructor(dom, head) {
1512
+ this._dom = dom;
1513
+ this._head = head;
1514
+ }
1515
+ /** Register (or replace) a node's contribution and re-apply the merged result. */
1516
+ register(nodeId, entries) {
1517
+ this._contributions.set(nodeId, { order: this._order++, entries });
1518
+ this.apply();
1519
+ }
1520
+ /** Withdraw a node's contribution (unmount / route change) and re-apply. */
1521
+ unregister(nodeId) {
1522
+ if (this._contributions.delete(nodeId)) this.apply();
1523
+ }
1524
+ /** Recompute the merged head and patch `document.head` to match. */
1525
+ apply() {
1526
+ if (!this._adopted) {
1527
+ this._adoptServerTags();
1528
+ this._adopted = true;
1529
+ }
1530
+ const ordered = [...this._contributions.values()].sort((a, b) => a.order - b.order);
1531
+ const merged = /* @__PURE__ */ new Map();
1532
+ for (const contribution of ordered) {
1533
+ for (const entry of contribution.entries) {
1534
+ merged.set(entry.dedupKey, resolveEntry(entry));
1535
+ }
1536
+ }
1537
+ for (const [key, desired] of merged) {
1538
+ const existing = this._applied.get(key);
1539
+ if (existing !== void 0 && existing.tag === desired.tag) {
1540
+ this._reconcileAttrs(existing, desired);
1541
+ } else {
1542
+ if (existing !== void 0) {
1543
+ this._dom.removeChild(this._head, existing.el);
1544
+ this._applied.delete(key);
1545
+ }
1546
+ const el = this._createTag(key, desired);
1547
+ this._dom.appendChild(this._head, el);
1548
+ this._applied.set(key, { el, attrKeys: new Set(Object.keys(desired.attrs)), tag: desired.tag });
1549
+ }
1550
+ }
1551
+ for (const [key, record] of [...this._applied]) {
1552
+ if (!merged.has(key)) {
1553
+ this._dom.removeChild(this._head, record.el);
1554
+ this._applied.delete(key);
1555
+ }
1556
+ }
1557
+ }
1558
+ _createTag(key, desired) {
1559
+ const el = this._dom.createElement(desired.tag);
1560
+ this._dom.setAttribute(el, HEAD_MARKER, "");
1561
+ this._dom.setAttribute(el, HEAD_KEY, key);
1562
+ for (const attr of Object.keys(desired.attrs)) {
1563
+ this._dom.setAttribute(el, attr, desired.attrs[attr]);
1564
+ }
1565
+ if (desired.tag === "title") this._dom.setTextContent(el, desired.text ?? "");
1566
+ return el;
1567
+ }
1568
+ _reconcileAttrs(record, desired) {
1569
+ const nextKeys = new Set(Object.keys(desired.attrs));
1570
+ for (const attr of record.attrKeys) {
1571
+ if (!nextKeys.has(attr)) this._dom.removeAttribute(record.el, attr);
1572
+ }
1573
+ for (const attr of nextKeys) {
1574
+ this._dom.setAttribute(record.el, attr, desired.attrs[attr]);
1575
+ }
1576
+ if (desired.tag === "title") this._dom.setTextContent(record.el, desired.text ?? "");
1577
+ record.attrKeys = nextKeys;
1578
+ }
1579
+ /**
1580
+ * Seed `_applied` from server-emitted `[data-streetui-head-key]` tags already
1581
+ * in `document.head`. The subsequent diff reuses these elements when the
1582
+ * client desires the same key (no duplicate), rewrites them if the value
1583
+ * changed, or removes them if the client graph no longer wants them.
1584
+ */
1585
+ _adoptServerTags() {
1586
+ for (const child of this._dom.childNodes(this._head)) {
1587
+ if (!this._dom.isElement(child)) continue;
1588
+ const el = child;
1589
+ const key = this._dom.getAttribute(el, HEAD_KEY);
1590
+ if (key === null) continue;
1591
+ this._applied.set(key, {
1592
+ el,
1593
+ attrKeys: new Set(this._attrNames(el)),
1594
+ tag: this._dom.tagName(el)
1595
+ });
1596
+ }
1597
+ }
1598
+ /** The framework-managed attribute names currently on a server tag. */
1599
+ _attrNames(el) {
1600
+ const names = [];
1601
+ if (this._dom.getAttribute(el, HEAD_MARKER) !== null) names.push(HEAD_MARKER);
1602
+ if (this._dom.getAttribute(el, HEAD_KEY) !== null) names.push(HEAD_KEY);
1603
+ return names;
1604
+ }
1605
+ };
1606
+ function getHeadManager(ctx) {
1607
+ const head = ctx.dom.head();
1608
+ if (head === null) return null;
1609
+ const mutable = ctx;
1610
+ if (mutable.head === void 0) mutable.head = new HeadManager(ctx.dom, head);
1611
+ return mutable.head;
1612
+ }
1613
+ function wireHeadBehavior(ctx, graphNode, instance) {
1614
+ const manager = getHeadManager(ctx);
1615
+ if (manager === null) return;
1616
+ const descFn = ctx.graph.getHandler(`__head__${graphNode.id}`);
1617
+ if (descFn === void 0) return;
1618
+ const contribution = descFn();
1619
+ const nodeId = graphNode.id;
1620
+ manager.register(nodeId, contribution.entries);
1621
+ for (const entry of contribution.entries) {
1622
+ for (const attrKey of Object.keys(entry.attrs)) {
1623
+ const v = entry.attrs[attrKey];
1624
+ if (isSignalLike(v)) {
1625
+ instance.trackCleanup(v.subscribe(() => manager.apply()));
1626
+ }
1627
+ }
1628
+ if (isSignalLike(entry.text)) {
1629
+ instance.trackCleanup(entry.text.subscribe(() => manager.apply()));
1630
+ }
1631
+ }
1632
+ instance.trackCleanup(() => manager.unregister(nodeId));
1633
+ }
1634
+
1242
1635
  // ../renderer/src/mount.ts
1243
1636
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
1244
1637
  "text",
@@ -1361,6 +1754,9 @@ function mountNode(ctx, graphNode, parentDom) {
1361
1754
  const instance2 = new NodeInstance(graphNode, el2);
1362
1755
  ctx.instances.set(graphNode.id, instance2);
1363
1756
  wireEvents(dom, graph, graphNode, el2, instance2);
1757
+ if (graphNode.stateRefs.length !== 0) {
1758
+ wireSignalBindings(ctx, graphNode, instance2, linkUpdate(dom, el2));
1759
+ }
1364
1760
  dom.appendChild(parentDom, el2);
1365
1761
  return instance2;
1366
1762
  }
@@ -1391,7 +1787,7 @@ function mountNode(ctx, graphNode, parentDom) {
1391
1787
  instance2.addChild(childInstance);
1392
1788
  }
1393
1789
  dom.appendChild(parentDom, el2);
1394
- wireReactiveList(ctx, graphNode, instance2, el2);
1790
+ wireReactiveList(ctx, graphNode, instance2, el2, true);
1395
1791
  return instance2;
1396
1792
  }
1397
1793
  if (graphNode.type === "portal") {
@@ -1416,6 +1812,16 @@ function mountNode(ctx, graphNode, parentDom) {
1416
1812
  wireOverlayBehavior(ctx, graphNode, instance2, target);
1417
1813
  return instance2;
1418
1814
  }
1815
+ if (graphNode.type === "head") {
1816
+ const anchor = dom.createElement(resolveTag("head"));
1817
+ dom.setAttribute(anchor, "data-streetui-head-anchor", "");
1818
+ applyNodeProps(ctx, graphNode, anchor);
1819
+ const instance2 = new NodeInstance(graphNode, anchor);
1820
+ ctx.instances.set(graphNode.id, instance2);
1821
+ dom.appendChild(parentDom, anchor);
1822
+ wireHeadBehavior(ctx, graphNode, instance2);
1823
+ return instance2;
1824
+ }
1419
1825
  const tag = resolveTag(graphNode.type);
1420
1826
  const el = dom.createElement(tag);
1421
1827
  applyNodeProps(ctx, graphNode, el);
@@ -1480,6 +1886,17 @@ function buttonUpdate(dom, el) {
1480
1886
  }
1481
1887
  };
1482
1888
  }
1889
+ function linkUpdate(dom, el) {
1890
+ return (propKey, value) => {
1891
+ if (propKey === "label") {
1892
+ dom.setTextContent(el, String(value ?? ""));
1893
+ } else if (propKey === "href") {
1894
+ dom.setAttribute(el, "href", String(value ?? ""));
1895
+ } else {
1896
+ applyProp(dom, el, propKey, value);
1897
+ }
1898
+ };
1899
+ }
1483
1900
  function applyNodeProps(ctx, graphNode, el) {
1484
1901
  const props = graphNode.props;
1485
1902
  for (const key in props) {
@@ -1500,32 +1917,42 @@ function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
1500
1917
  instance.trackCleanup(unsub);
1501
1918
  }
1502
1919
  }
1503
- function wireReactiveList(ctx, graphNode, instance, el) {
1920
+ function wireReactiveList(ctx, graphNode, instance, el, runAppear = false) {
1504
1921
  const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
1505
1922
  const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
1506
1923
  if (plan === void 0 && build === void 0) return;
1924
+ const controller = new TransitionController(ctx.dom, ctx.graph, (leaving) => {
1925
+ const parent = ctx.dom.parentNode(leaving.domNode);
1926
+ if (parent !== null) ctx.dom.removeChild(parent, leaving.domNode);
1927
+ leaving.dispose();
1928
+ forgetInstance(ctx, leaving);
1929
+ ctx.graph.detachNode(leaving.graphNode);
1930
+ });
1931
+ const hooks = controller.hooks();
1932
+ if (runAppear) controller.appear(instance.children);
1507
1933
  for (const stateRef of graphNode.stateRefs) {
1508
1934
  if (stateRef.propKey !== "items") continue;
1509
1935
  const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
1510
1936
  if (sig === void 0 || typeof sig.subscribe !== "function") continue;
1511
1937
  const unsub = sig.subscribe((value) => {
1512
1938
  if (plan !== void 0) {
1513
- reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
1939
+ reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value), hooks);
1514
1940
  } else {
1515
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
1941
+ reconcileReactiveList(ctx, graphNode, instance, el, build(value), hooks);
1516
1942
  }
1517
1943
  });
1518
1944
  instance.trackCleanup(unsub);
1519
1945
  }
1520
1946
  }
1521
- function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
1947
+ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan, hooks) {
1522
1948
  const oldInstances = [...listInstance.children];
1523
1949
  const result = reconcileChildrenByPlan(
1524
1950
  ctx,
1525
1951
  listEl,
1526
1952
  oldInstances,
1527
1953
  plan,
1528
- (node, parent) => mountNode(ctx, node, parent)
1954
+ (node, parent) => mountNode(ctx, node, parent),
1955
+ hooks
1529
1956
  );
1530
1957
  listInstance.children.length = 0;
1531
1958
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -1540,14 +1967,15 @@ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan)
1540
1967
  for (const child of [...listNode.children]) listNode.removeChild(child);
1541
1968
  for (const inst of result.instances) listNode.appendChild(inst.graphNode);
1542
1969
  }
1543
- function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
1970
+ function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes, hooks) {
1544
1971
  const oldInstances = [...listInstance.children];
1545
1972
  const result = reconcileChildren(
1546
1973
  ctx,
1547
1974
  listEl,
1548
1975
  oldInstances,
1549
1976
  newNodes,
1550
- (node, parent) => mountNode(ctx, node, parent)
1977
+ (node, parent) => mountNode(ctx, node, parent),
1978
+ hooks
1551
1979
  );
1552
1980
  listInstance.children.length = 0;
1553
1981
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -1583,11 +2011,14 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
1583
2011
  const onOpenChange = (isOpen) => {
1584
2012
  if (isOpen) {
1585
2013
  if (desc.restoreFocus) saved = saveFocus(dom);
1586
- if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
1587
2014
  if (desc.modal) {
1588
2015
  active.push(trapFocus(dom, target));
1589
2016
  active.push(containFocus(dom, target));
1590
2017
  }
2018
+ if (desc.menu) {
2019
+ active.push(rovingMenu(dom, target));
2020
+ }
2021
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
1591
2022
  if (desc.closeOnEscape && desc.onClose !== void 0) {
1592
2023
  active.push(onEscape(dom, target, desc.onClose));
1593
2024
  }
@@ -1693,7 +2124,12 @@ function hydrateNode(ctx, graphNode, domNode, path) {
1693
2124
  case "link": {
1694
2125
  const instance = new NodeInstance(graphNode, domNode);
1695
2126
  ctx.instances.set(graphNode.id, instance);
1696
- if (graphNode.type === "link") wireEvents(dom, graph, graphNode, domNode, instance);
2127
+ if (graphNode.type === "link") {
2128
+ wireEvents(dom, graph, graphNode, domNode, instance);
2129
+ if (graphNode.stateRefs.length !== 0) {
2130
+ wireSignalBindings(ctx, graphNode, instance, linkUpdate(dom, domNode));
2131
+ }
2132
+ }
1697
2133
  return instance;
1698
2134
  }
1699
2135
  case "reactive-list":
@@ -1723,6 +2159,12 @@ function hydrateNode(ctx, graphNode, domNode, path) {
1723
2159
  wireOverlayBehavior(ctx, graphNode, instance, target);
1724
2160
  return instance;
1725
2161
  }
2162
+ case "head": {
2163
+ const instance = new NodeInstance(graphNode, domNode);
2164
+ ctx.instances.set(graphNode.id, instance);
2165
+ wireHeadBehavior(ctx, graphNode, instance);
2166
+ return instance;
2167
+ }
1726
2168
  default: {
1727
2169
  const instance = new NodeInstance(graphNode, domNode);
1728
2170
  ctx.instances.set(graphNode.id, instance);
@@ -1933,7 +2375,8 @@ function analyzeGraph(graph) {
1933
2375
  const isConditional = node.type === "conditional";
1934
2376
  const isPortal = node.type === "portal";
1935
2377
  const isComponent = node.type === "component";
1936
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent;
2378
+ const isHead = node.type === "head";
2379
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent && !isHead;
1937
2380
  const isStaticSubtree = isStatic && allChildrenStatic;
1938
2381
  nodes.set(node.id, {
1939
2382
  isStatic,
@@ -2264,6 +2707,37 @@ async function waitFor(check, options = {}) {
2264
2707
  await new Promise((resolve) => setTimeout(resolve, interval));
2265
2708
  }
2266
2709
  }
2710
+ function focus(el) {
2711
+ el.focus?.();
2712
+ }
2713
+ function blur(el) {
2714
+ el.blur?.();
2715
+ }
2716
+ function pressKey(key, el = document.activeElement, init = {}) {
2717
+ if (el === null) return;
2718
+ el.dispatchEvent(
2719
+ new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true, ...init })
2720
+ );
2721
+ }
2722
+ function clickOutside(container, target = document.body) {
2723
+ if (container.contains(target)) {
2724
+ throw new Error("[StreetUI Testing] clickOutside: target is inside the container");
2725
+ }
2726
+ target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
2727
+ target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
2728
+ }
2729
+ async function openOverlay(open) {
2730
+ open.set(true);
2731
+ await flushUpdates();
2732
+ }
2733
+ async function closeOverlay(open) {
2734
+ open.set(false);
2735
+ await flushUpdates();
2736
+ }
2737
+ async function waitForTransition(el) {
2738
+ el.dispatchEvent(new Event("transitionend", { bubbles: true }));
2739
+ await flushUpdates();
2740
+ }
2267
2741
  function findByText(container, text) {
2268
2742
  const match = Array.from(container.querySelectorAll("*")).find(
2269
2743
  (el) => el.children.length === 0 && (el.textContent?.includes(text) ?? false)
@@ -2358,6 +2832,154 @@ function renderServerThenHydrate(build, options = {}) {
2358
2832
  };
2359
2833
  }
2360
2834
 
2835
+ // ../dsl/src/transition.ts
2836
+ function classes(value) {
2837
+ if (value === void 0) return [];
2838
+ const out = [];
2839
+ for (const token of value.split(/\s+/)) {
2840
+ if (token.length > 0 && !out.includes(token)) out.push(token);
2841
+ }
2842
+ return out;
2843
+ }
2844
+ function merge(a, b) {
2845
+ const out = [...a];
2846
+ for (const token of b) if (!out.includes(token)) out.push(token);
2847
+ return out;
2848
+ }
2849
+ function resolveTransition(config) {
2850
+ const n = config.name;
2851
+ const enterActive = merge(
2852
+ classes(config.enter),
2853
+ classes(config.enterActive ?? (n !== void 0 ? `${n}-enter-active` : void 0))
2854
+ );
2855
+ const leaveActive = merge(
2856
+ classes(config.leave),
2857
+ classes(config.leaveActive ?? (n !== void 0 ? `${n}-leave-active` : void 0))
2858
+ );
2859
+ return {
2860
+ enterActive,
2861
+ enterFrom: classes(config.enterFrom ?? (n !== void 0 ? `${n}-enter-from` : void 0)),
2862
+ enterTo: classes(config.enterTo ?? (n !== void 0 ? `${n}-enter-to` : void 0)),
2863
+ leaveActive,
2864
+ leaveFrom: classes(config.leaveFrom ?? (n !== void 0 ? `${n}-leave-from` : void 0)),
2865
+ leaveTo: classes(config.leaveTo ?? (n !== void 0 ? `${n}-leave-to` : void 0)),
2866
+ appear: config.appear ?? false,
2867
+ duration: config.duration ?? 1e3
2868
+ };
2869
+ }
2870
+
2871
+ // ../dsl/src/head.ts
2872
+ function metaDedupKey(m) {
2873
+ if (m.charset !== void 0) return "meta:charset";
2874
+ if (m.name !== void 0) return `meta:name=${m.name}`;
2875
+ if (m.property !== void 0) return `meta:property=${m.property}`;
2876
+ if (m.httpEquiv !== void 0) return `meta:http-equiv=${m.httpEquiv}`;
2877
+ return void 0;
2878
+ }
2879
+ function metaAttrs(m) {
2880
+ const attrs = {};
2881
+ if (m.charset !== void 0) attrs["charset"] = m.charset;
2882
+ if (m.name !== void 0) attrs["name"] = m.name;
2883
+ if (m.property !== void 0) attrs["property"] = m.property;
2884
+ if (m.httpEquiv !== void 0) attrs["http-equiv"] = m.httpEquiv;
2885
+ if (m.content !== void 0) attrs["content"] = m.content;
2886
+ return attrs;
2887
+ }
2888
+ function linkAttrs(l) {
2889
+ const attrs = { rel: l.rel, href: l.href };
2890
+ if (l.sizes !== void 0) attrs["sizes"] = l.sizes;
2891
+ if (l.type !== void 0) attrs["type"] = l.type;
2892
+ if (l.media !== void 0) attrs["media"] = l.media;
2893
+ if (l.as !== void 0) attrs["as"] = l.as;
2894
+ if (l.crossorigin !== void 0) attrs["crossorigin"] = l.crossorigin;
2895
+ if (l.hreflang !== void 0) attrs["hreflang"] = l.hreflang;
2896
+ return attrs;
2897
+ }
2898
+ function resolveHead(config) {
2899
+ const entries = [];
2900
+ if (config.charset !== void 0) {
2901
+ entries.push({ tag: "meta", dedupKey: "meta:charset", attrs: { charset: config.charset } });
2902
+ }
2903
+ if (config.base !== void 0) {
2904
+ entries.push({ tag: "base", dedupKey: "base", attrs: { href: config.base } });
2905
+ }
2906
+ if (config.title !== void 0) {
2907
+ entries.push({ tag: "title", dedupKey: "title", attrs: {}, text: config.title });
2908
+ }
2909
+ if (config.description !== void 0) {
2910
+ entries.push({
2911
+ tag: "meta",
2912
+ dedupKey: "meta:name=description",
2913
+ attrs: { name: "description", content: config.description }
2914
+ });
2915
+ }
2916
+ if (config.canonical !== void 0) {
2917
+ entries.push({
2918
+ tag: "link",
2919
+ dedupKey: "link:rel=canonical",
2920
+ attrs: { rel: "canonical", href: config.canonical }
2921
+ });
2922
+ }
2923
+ if (config.robots !== void 0) {
2924
+ entries.push({
2925
+ tag: "meta",
2926
+ dedupKey: "meta:name=robots",
2927
+ attrs: { name: "robots", content: config.robots }
2928
+ });
2929
+ }
2930
+ if (config.themeColor !== void 0) {
2931
+ entries.push({
2932
+ tag: "meta",
2933
+ dedupKey: "meta:name=theme-color",
2934
+ attrs: { name: "theme-color", content: config.themeColor }
2935
+ });
2936
+ }
2937
+ if (config.viewport !== void 0) {
2938
+ entries.push({
2939
+ tag: "meta",
2940
+ dedupKey: "meta:name=viewport",
2941
+ attrs: { name: "viewport", content: config.viewport }
2942
+ });
2943
+ }
2944
+ if (config.favicon !== void 0) {
2945
+ const l = typeof config.favicon === "string" ? { rel: "icon", href: config.favicon } : config.favicon;
2946
+ entries.push({ tag: "link", dedupKey: `link:rel=${l.rel}`, attrs: linkAttrs(l) });
2947
+ }
2948
+ if (config.openGraph !== void 0) {
2949
+ for (const key of Object.keys(config.openGraph)) {
2950
+ const property = `og:${key}`;
2951
+ entries.push({
2952
+ tag: "meta",
2953
+ dedupKey: `meta:property=${property}`,
2954
+ attrs: { property, content: config.openGraph[key] }
2955
+ });
2956
+ }
2957
+ }
2958
+ if (config.twitter !== void 0) {
2959
+ for (const key of Object.keys(config.twitter)) {
2960
+ const name = `twitter:${key}`;
2961
+ entries.push({
2962
+ tag: "meta",
2963
+ dedupKey: `meta:name=${name}`,
2964
+ attrs: { name, content: config.twitter[key] }
2965
+ });
2966
+ }
2967
+ }
2968
+ if (config.meta !== void 0) {
2969
+ for (const m of config.meta) {
2970
+ const key = metaDedupKey(m);
2971
+ if (key === void 0) continue;
2972
+ entries.push({ tag: "meta", dedupKey: key, attrs: metaAttrs(m) });
2973
+ }
2974
+ }
2975
+ if (config.link !== void 0) {
2976
+ for (const l of config.link) {
2977
+ entries.push({ tag: "link", dedupKey: `link:rel=${l.rel}:href=${l.href}`, attrs: linkAttrs(l) });
2978
+ }
2979
+ }
2980
+ return { entries };
2981
+ }
2982
+
2361
2983
  // ../state/src/signal.ts
2362
2984
  var _activeConsumer = null;
2363
2985
  function withConsumer(consumer, fn) {
@@ -2567,6 +3189,18 @@ function applyA11yProps(props, options) {
2567
3189
  if (options.ariaInvalid !== void 0) props["aria-invalid"] = String(options.ariaInvalid);
2568
3190
  if (options.ariaRequired !== void 0) props["aria-required"] = String(options.ariaRequired);
2569
3191
  if (options.ariaModal !== void 0) props["aria-modal"] = String(options.ariaModal);
3192
+ if (options.ariaOwns !== void 0) props["aria-owns"] = options.ariaOwns;
3193
+ if (options.ariaActiveDescendant !== void 0) props["aria-activedescendant"] = options.ariaActiveDescendant;
3194
+ if (options.ariaHasPopup !== void 0) props["aria-haspopup"] = String(options.ariaHasPopup);
3195
+ if (options.ariaSelected !== void 0) props["aria-selected"] = String(options.ariaSelected);
3196
+ }
3197
+ function registerTransition(graph, node, config) {
3198
+ if (config === void 0) return;
3199
+ const resolved = resolveTransition(config);
3200
+ graph.registerHandler(
3201
+ `__transition__${node.id}`,
3202
+ () => resolved
3203
+ );
2570
3204
  }
2571
3205
  function containerProps(options) {
2572
3206
  const props = {};
@@ -2627,6 +3261,7 @@ var OVERLAY_KINDS = {
2627
3261
  role: "menu",
2628
3262
  modal: false,
2629
3263
  takesFocus: true,
3264
+ menu: true,
2630
3265
  ariaModal: false,
2631
3266
  defaultCloseOnEscape: true,
2632
3267
  defaultRestoreFocus: true
@@ -2759,6 +3394,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
2759
3394
  parent: this._node,
2760
3395
  props: containerProps(options)
2761
3396
  });
3397
+ registerTransition(this._graph, node, options.transition);
2762
3398
  builder(new SectionBuilderImpl(node, this._graph));
2763
3399
  }
2764
3400
  container(key, builder, options = {}) {
@@ -2767,6 +3403,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
2767
3403
  parent: this._node,
2768
3404
  props: containerProps(options)
2769
3405
  });
3406
+ registerTransition(this._graph, node, options.transition);
2770
3407
  builder(new ContainerBuilderImpl(node, this._graph));
2771
3408
  }
2772
3409
  list(key, builder, options = {}) {
@@ -2775,6 +3412,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
2775
3412
  parent: this._node,
2776
3413
  props: containerProps(options)
2777
3414
  });
3415
+ registerTransition(this._graph, node, options.transition);
2778
3416
  builder(new ListBuilderImpl(node, this._graph));
2779
3417
  }
2780
3418
  listOf(key, items, renderItem, options = {}) {
@@ -2784,6 +3422,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
2784
3422
  parent: this._node,
2785
3423
  props: containerProps(options)
2786
3424
  });
3425
+ registerTransition(graph, node, options.transition);
2787
3426
  const signalId = `${node.id}:items`;
2788
3427
  node.stateRefs.push({ signalId, propKey: "items" });
2789
3428
  graph.registerHandler(`__signal__${signalId}`, items);
@@ -2805,6 +3444,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
2805
3444
  }
2806
3445
  });
2807
3446
  renderItem(item, index, new ContainerBuilderImpl(itemNode, graph));
3447
+ registerTransition(graph, itemNode, options.itemTransition);
2808
3448
  return itemNode;
2809
3449
  };
2810
3450
  const buildPlan = (raw) => {
@@ -2836,6 +3476,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
2836
3476
  parent: this._node,
2837
3477
  props
2838
3478
  });
3479
+ registerTransition(this._graph, node, options.transition);
2839
3480
  if (options.onSubmit !== void 0) {
2840
3481
  const handlerKey = `submit:${node.id}`;
2841
3482
  this._graph.registerHandler(handlerKey, options.onSubmit);
@@ -2843,18 +3484,20 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
2843
3484
  }
2844
3485
  builder(new FormBuilderImpl(node, this._graph));
2845
3486
  }
2846
- when(condition, builder, elseBuilder) {
3487
+ when(condition, builder, elseBuilder, options = {}) {
2847
3488
  const graph = this._graph;
2848
3489
  const node = graph.createNode("conditional", {
2849
3490
  parent: this._node,
2850
3491
  props: containerProps({})
2851
3492
  });
3493
+ const branchTransition = options.transition !== void 0 && options.appear === true ? { ...options.transition, appear: true } : options.transition;
2852
3494
  const buildBranch = (build, tag) => {
2853
3495
  const branchKey = `when-${tag}:${node.id}`;
2854
3496
  const branch = graph.createNode("container", {
2855
3497
  key: branchKey,
2856
3498
  props: { key: branchKey }
2857
3499
  });
3500
+ registerTransition(graph, branch, branchTransition);
2858
3501
  build(new ContainerBuilderImpl(branch, graph));
2859
3502
  return branch;
2860
3503
  };
@@ -2900,8 +3543,14 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
2900
3543
  this.container(id, (c) => {
2901
3544
  c.when(
2902
3545
  hasError,
2903
- // Error state → fallback.
2904
- (fb) => options.fallback(fb, readError(), retry),
3546
+ // Error state → fallback. The fallback branch mounts exactly when the
3547
+ // boundary enters its error state, so this is also where the optional
3548
+ // `onError` reporting hook fires (observe-only; §6/§7).
3549
+ (fb) => {
3550
+ const currentError = readError();
3551
+ options.onError?.(currentError);
3552
+ options.fallback(fb, currentError, retry);
3553
+ },
2905
3554
  // Healthy state → body, guarded against synchronous build throws.
2906
3555
  (body) => {
2907
3556
  try {
@@ -2913,6 +3562,24 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
2913
3562
  );
2914
3563
  }, { id });
2915
3564
  }
3565
+ asyncBoundary(key, res, branches) {
3566
+ const isError = derived(() => res.status.get() === "error");
3567
+ const showSuccess = derived(
3568
+ () => res.status.get() !== "error" && res.data.get() !== void 0
3569
+ );
3570
+ const showLoading = derived(
3571
+ () => res.status.get() !== "error" && res.data.get() === void 0
3572
+ );
3573
+ const dataSignal = derived(() => res.data.get());
3574
+ const retry = () => {
3575
+ void res.refetch();
3576
+ };
3577
+ this.container(key, (c) => {
3578
+ c.when(isError, (fb) => branches.error?.(fb, res.error.peek(), retry));
3579
+ c.when(showSuccess, (sb) => branches.success(sb, dataSignal));
3580
+ c.when(showLoading, (lb) => branches.loading?.(lb));
3581
+ }, { key });
3582
+ }
2916
3583
  // ── Portals & overlays ──────────────────────────────────────────────────────
2917
3584
  portal(key, builder, options = {}) {
2918
3585
  const node = this._graph.createNode("portal", {
@@ -2922,6 +3589,31 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
2922
3589
  });
2923
3590
  builder(new ContainerBuilderImpl(node, this._graph));
2924
3591
  }
3592
+ /**
3593
+ * Declare document metadata (2.0 §1–§3). Creates a `'head'` node — a neutral,
3594
+ * empty inline anchor at this position (one node / one element, so positional
3595
+ * hydration is preserved) — and registers a `__head__<id>` descriptor holding
3596
+ * this call's normalized, dedup-keyed {@link resolveHead} contribution. The
3597
+ * renderer applies it to `document.head` on the browser (adopting server tags
3598
+ * on hydration, cleaning up on unmount / route change) and emits the active
3599
+ * graph's merged metadata as a string on the server (`renderHead`).
3600
+ *
3601
+ * Multiple `head()` nodes may be live at once (app default + route + component)
3602
+ * — the renderer merges them and, per dedup key, the last in document order
3603
+ * wins (see head.ts). No new render path: this reuses the same graph-node +
3604
+ * handler-registry convention as overlays/transitions/components.
3605
+ */
3606
+ head(metadata) {
3607
+ const node = this._graph.createNode("head", {
3608
+ parent: this._node,
3609
+ props: { "data-streetui-head-anchor": "" }
3610
+ });
3611
+ const contribution = resolveHead(metadata);
3612
+ this._graph.registerHandler(
3613
+ `__head__${node.id}`,
3614
+ () => contribution
3615
+ );
3616
+ }
2925
3617
  /**
2926
3618
  * Shared assembly for every overlay kind: a `portal` node whose single child
2927
3619
  * is a `when(open, panel)` conditional. The panel container carries the
@@ -2949,13 +3641,24 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
2949
3641
  ...options.ariaDescribedBy !== void 0 ? { ariaDescribedBy: options.ariaDescribedBy } : {}
2950
3642
  };
2951
3643
  const portalBuilder = new ContainerBuilderImpl(portalNode, graph);
2952
- portalBuilder.when(openSignal, (panelHost) => {
2953
- panelHost.container(`${key}__panel`, builder, panelOptions);
2954
- });
3644
+ portalBuilder.when(
3645
+ openSignal,
3646
+ (panelHost) => {
3647
+ panelHost.container(`${key}__panel`, builder, panelOptions);
3648
+ },
3649
+ void 0,
3650
+ // Overlay open/close rides the panel's `when`; a transition animates the
3651
+ // panel in on open and — via the reconciler's deferred-leave — plays the
3652
+ // leave before the panel is removed (§10). Focus is restored at close-
3653
+ // request time (see wireOverlayBehavior), so it never stays trapped inside
3654
+ // a panel that is animating away.
3655
+ options.transition !== void 0 ? { transition: options.transition } : {}
3656
+ );
2955
3657
  const descriptor = {
2956
3658
  open: openSignal,
2957
3659
  modal: kind.modal,
2958
3660
  takesFocus: kind.takesFocus,
3661
+ menu: kind.menu ?? false,
2959
3662
  closeOnEscape: options.closeOnEscape ?? kind.defaultCloseOnEscape,
2960
3663
  restoreFocus: options.restoreFocus ?? kind.defaultRestoreFocus,
2961
3664
  ...options.initialFocusId !== void 0 ? { initialFocusId: options.initialFocusId } : {},
@@ -3052,6 +3755,7 @@ var ListBuilderImpl = class extends ContentBuilderBase {
3052
3755
  parent: this._node,
3053
3756
  props: containerProps(options)
3054
3757
  });
3758
+ registerTransition(this._graph, node, options.transition);
3055
3759
  builder(new ContainerBuilderImpl(node, this._graph));
3056
3760
  }
3057
3761
  };
@@ -3239,6 +3943,8 @@ var ApplicationGraph = class {
3239
3943
  this.handlers.delete(`__listplan__${node.id}`);
3240
3944
  this.handlers.delete(`__overlay__${node.id}`);
3241
3945
  this.handlers.delete(`__component__${node.id}`);
3946
+ this.handlers.delete(`__transition__${node.id}`);
3947
+ this.handlers.delete(`__head__${node.id}`);
3242
3948
  }
3243
3949
  // ── Handler registry ──────────────────────────────────────────────────────
3244
3950
  registerHandler(key, fn) {
@@ -3416,26 +4122,33 @@ function trigger(el, type, init = {}) {
3416
4122
  }
3417
4123
 
3418
4124
  // src/version.ts
3419
- var VERSION = "1.8.0";
4125
+ var VERSION = "2.0.0";
3420
4126
  // Annotate the CommonJS export names for ESM import in node:
3421
4127
  0 && (module.exports = {
3422
4128
  VERSION,
3423
4129
  analyzeGraph,
4130
+ blur,
4131
+ clickOutside,
4132
+ closeOverlay,
3424
4133
  findAllByRole,
3425
4134
  findAllComponents,
3426
4135
  findByRole,
3427
4136
  findByText,
3428
4137
  findComponent,
3429
4138
  flushUpdates,
4139
+ focus,
3430
4140
  formatInspection,
3431
4141
  getComponentName,
3432
4142
  hydrateComponent,
3433
4143
  inspectCompilation,
4144
+ openOverlay,
4145
+ pressKey,
3434
4146
  render,
3435
4147
  renderComponent,
3436
4148
  renderOnce,
3437
4149
  renderServerThenHydrate,
3438
4150
  trigger,
3439
- waitFor
4151
+ waitFor,
4152
+ waitForTransition
3440
4153
  });
3441
4154
  //# sourceMappingURL=testing.cjs.map