streetui 1.7.0 → 1.9.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.js CHANGED
@@ -1,8 +1,17 @@
1
1
  // ../core/src/identity.ts
2
2
  var _counter = 0;
3
+ function nextId() {
4
+ return ++_counter;
5
+ }
3
6
  function resetIdCounter() {
4
7
  _counter = 0;
5
8
  }
9
+ function createNodeId(value) {
10
+ return value;
11
+ }
12
+ function generateNodeId(prefix = "node") {
13
+ return createNodeId(`${prefix}:${nextId()}`);
14
+ }
6
15
 
7
16
  // ../core/src/lifecycle.ts
8
17
  var CleanupRegistry = class {
@@ -747,18 +756,25 @@ function trapFocus(dom, container) {
747
756
  dom.addEventListener(container, "keydown", onKeydown);
748
757
  return () => dom.removeEventListener(container, "keydown", onKeydown);
749
758
  }
759
+ var containmentStack = [];
750
760
  function containFocus(dom, container) {
751
761
  const body = dom.body();
752
762
  if (body === null) return () => {
753
763
  };
764
+ containmentStack.push(container);
754
765
  const onFocusIn = (event) => {
766
+ if (containmentStack[containmentStack.length - 1] !== container) return;
755
767
  const target = event.target;
756
768
  if (target !== null && !dom.contains(container, target)) {
757
769
  focusFirst(dom, container);
758
770
  }
759
771
  };
760
772
  dom.addEventListener(body, "focusin", onFocusIn);
761
- return () => dom.removeEventListener(body, "focusin", onFocusIn);
773
+ return () => {
774
+ dom.removeEventListener(body, "focusin", onFocusIn);
775
+ const index = containmentStack.lastIndexOf(container);
776
+ if (index !== -1) containmentStack.splice(index, 1);
777
+ };
762
778
  }
763
779
  function onEscape(dom, target, handler) {
764
780
  const onKeydown = (event) => {
@@ -767,6 +783,33 @@ function onEscape(dom, target, handler) {
767
783
  dom.addEventListener(target, "keydown", onKeydown);
768
784
  return () => dom.removeEventListener(target, "keydown", onKeydown);
769
785
  }
786
+ function rovingMenu(dom, container, selector = FOCUSABLE_SELECTOR) {
787
+ const onKeydown = (event) => {
788
+ const key = event.key;
789
+ const isActivate = key === "Enter" || key === " " || key === "Spacebar";
790
+ const isMove = key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End";
791
+ if (!isActivate && !isMove) return;
792
+ const items = getFocusable(dom, container, selector);
793
+ if (items.length === 0) return;
794
+ const active = dom.activeElement();
795
+ const index = active === null ? -1 : items.indexOf(active);
796
+ if (isActivate) {
797
+ if (index < 0) return;
798
+ event.preventDefault();
799
+ items[index].click?.();
800
+ return;
801
+ }
802
+ event.preventDefault();
803
+ let next;
804
+ if (key === "Home") next = 0;
805
+ else if (key === "End") next = items.length - 1;
806
+ else if (key === "ArrowDown") next = index < 0 ? 0 : (index + 1) % items.length;
807
+ else next = index <= 0 ? items.length - 1 : index - 1;
808
+ dom.focus(items[next]);
809
+ };
810
+ dom.addEventListener(container, "keydown", onKeydown);
811
+ return () => dom.removeEventListener(container, "keydown", onKeydown);
812
+ }
770
813
 
771
814
  // ../renderer/src/render-context.ts
772
815
  function createRenderContext(dom, graph, container, hydrationDiagnostics, staticHTML) {
@@ -978,7 +1021,7 @@ function patchNode(ctx, graphNode, propKey, newValue) {
978
1021
  }
979
1022
 
980
1023
  // ../renderer/src/reconciliation.ts
981
- function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
1024
+ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn, hooks) {
982
1025
  const oldByKey = /* @__PURE__ */ new Map();
983
1026
  for (const inst of oldInstances) {
984
1027
  const key = inst.graphNode.key ?? inst.graphNode.id;
@@ -999,28 +1042,33 @@ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
999
1042
  }
1000
1043
  newInstances.push(existing);
1001
1044
  } else {
1002
- const inst = mountFn(newNode, parentDom);
1003
- newInstances.push(inst);
1045
+ const reclaimed = hooks?.takeLeaving(key);
1046
+ if (reclaimed !== void 0) {
1047
+ patchExistingInstance(ctx, reclaimed, newNode);
1048
+ reconcileItemChildren(ctx, reclaimed, newNode, mountFn);
1049
+ hooks?.onEnter(reclaimed);
1050
+ newInstances.push(reclaimed);
1051
+ } else {
1052
+ const inst = mountFn(newNode, parentDom);
1053
+ hooks?.onEnter(inst);
1054
+ newInstances.push(inst);
1055
+ }
1004
1056
  }
1005
1057
  }
1006
1058
  const removed = [];
1007
1059
  for (const inst of oldInstances) {
1008
1060
  const key = inst.graphNode.key ?? inst.graphNode.id;
1009
- if (!usedKeys.has(key)) {
1010
- removed.push(inst);
1011
- }
1012
- }
1013
- for (const inst of removed) {
1061
+ if (usedKeys.has(key)) continue;
1062
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
1014
1063
  const parent = ctx.dom.parentNode(inst.domNode);
1015
- if (parent !== null) {
1016
- ctx.dom.removeChild(parent, inst.domNode);
1017
- }
1064
+ if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
1018
1065
  inst.dispose();
1066
+ removed.push(inst);
1019
1067
  }
1020
1068
  reorderDom(ctx, parentDom, newInstances);
1021
1069
  return { instances: newInstances, removed };
1022
1070
  }
1023
- function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
1071
+ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn, hooks) {
1024
1072
  const oldByKey = /* @__PURE__ */ new Map();
1025
1073
  for (const inst of oldInstances) {
1026
1074
  oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
@@ -1047,21 +1095,34 @@ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
1047
1095
  }
1048
1096
  newInstances.push(existing);
1049
1097
  } else {
1050
- const freshNode = entry.build();
1051
- built.push(freshNode);
1052
- const inst = mountFn(freshNode, parentDom);
1053
- newInstances.push(inst);
1098
+ const reclaimed = hooks?.takeLeaving(entry.key);
1099
+ if (reclaimed !== void 0) {
1100
+ const freshNode = entry.build();
1101
+ built.push(freshNode);
1102
+ patchExistingInstance(ctx, reclaimed, freshNode);
1103
+ reconcileItemChildren(ctx, reclaimed, freshNode, mountFn);
1104
+ reclaimed.graphNode.setProp("_sig", freshNode.getProp("_sig"));
1105
+ reclaimed.graphNode.setProp("_item", entry.item);
1106
+ hooks?.onEnter(reclaimed);
1107
+ newInstances.push(reclaimed);
1108
+ } else {
1109
+ const freshNode = entry.build();
1110
+ built.push(freshNode);
1111
+ const inst = mountFn(freshNode, parentDom);
1112
+ hooks?.onEnter(inst);
1113
+ newInstances.push(inst);
1114
+ }
1054
1115
  }
1055
1116
  }
1056
1117
  const removed = [];
1057
1118
  for (const inst of oldInstances) {
1058
1119
  const key = inst.graphNode.key ?? inst.graphNode.id;
1059
- if (!usedKeys.has(key)) removed.push(inst);
1060
- }
1061
- for (const inst of removed) {
1120
+ if (usedKeys.has(key)) continue;
1121
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
1062
1122
  const parent = ctx.dom.parentNode(inst.domNode);
1063
1123
  if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
1064
1124
  inst.dispose();
1125
+ removed.push(inst);
1065
1126
  }
1066
1127
  reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
1067
1128
  return { instances: newInstances, removed, built };
@@ -1187,6 +1248,176 @@ function patchExistingInstance(ctx, instance, newNode) {
1187
1248
  }
1188
1249
  }
1189
1250
 
1251
+ // ../renderer/src/transition.ts
1252
+ function getResolvedTransition(graph, nodeId) {
1253
+ const fn = graph.getHandler(`__transition__${nodeId}`);
1254
+ return fn === void 0 ? void 0 : fn();
1255
+ }
1256
+ function host() {
1257
+ return globalThis;
1258
+ }
1259
+ function nextFrame(cb) {
1260
+ const h = host();
1261
+ const raf = h.requestAnimationFrame;
1262
+ if (typeof raf === "function") {
1263
+ raf(() => raf(cb));
1264
+ } else {
1265
+ h.setTimeout(cb, 0);
1266
+ }
1267
+ }
1268
+ function splitClass(value) {
1269
+ if (value === null) return [];
1270
+ const out = [];
1271
+ for (const t of value.split(/\s+/)) if (t.length > 0) out.push(t);
1272
+ return out;
1273
+ }
1274
+ function addClasses(dom, el, classes2) {
1275
+ if (classes2.length === 0) return;
1276
+ const current = splitClass(dom.getAttribute(el, "class"));
1277
+ let changed = false;
1278
+ for (const c of classes2) {
1279
+ if (!current.includes(c)) {
1280
+ current.push(c);
1281
+ changed = true;
1282
+ }
1283
+ }
1284
+ if (changed) dom.setAttribute(el, "class", current.join(" "));
1285
+ }
1286
+ function removeClasses(dom, el, classes2) {
1287
+ if (classes2.length === 0) return;
1288
+ const current = splitClass(dom.getAttribute(el, "class"));
1289
+ const next = current.filter((c) => !classes2.includes(c));
1290
+ if (next.length !== current.length) {
1291
+ if (next.length === 0) dom.removeAttribute(el, "class");
1292
+ else dom.setAttribute(el, "class", next.join(" "));
1293
+ }
1294
+ }
1295
+ function startRun(dom, el, active, from, to, duration, onDone) {
1296
+ const h = host();
1297
+ let finished = false;
1298
+ let timer = null;
1299
+ const onEvent = (e) => {
1300
+ if (e.target !== el) return;
1301
+ finish();
1302
+ };
1303
+ const detach = () => {
1304
+ dom.removeEventListener(el, "transitionend", onEvent);
1305
+ dom.removeEventListener(el, "animationend", onEvent);
1306
+ if (timer !== null) {
1307
+ h.clearTimeout(timer);
1308
+ timer = null;
1309
+ }
1310
+ };
1311
+ const finish = () => {
1312
+ if (finished) return;
1313
+ finished = true;
1314
+ detach();
1315
+ removeClasses(dom, el, active);
1316
+ removeClasses(dom, el, to);
1317
+ removeClasses(dom, el, from);
1318
+ onDone();
1319
+ };
1320
+ addClasses(dom, el, from);
1321
+ addClasses(dom, el, active);
1322
+ dom.addEventListener(el, "transitionend", onEvent);
1323
+ dom.addEventListener(el, "animationend", onEvent);
1324
+ timer = h.setTimeout(finish, duration);
1325
+ nextFrame(() => {
1326
+ if (finished) return;
1327
+ removeClasses(dom, el, from);
1328
+ addClasses(dom, el, to);
1329
+ });
1330
+ return {
1331
+ cancel: () => {
1332
+ if (finished) return;
1333
+ finished = true;
1334
+ detach();
1335
+ removeClasses(dom, el, active);
1336
+ removeClasses(dom, el, to);
1337
+ removeClasses(dom, el, from);
1338
+ }
1339
+ };
1340
+ }
1341
+ var TransitionController = class {
1342
+ constructor(dom, graph, finalize) {
1343
+ this.dom = dom;
1344
+ this.graph = graph;
1345
+ this.finalize = finalize;
1346
+ }
1347
+ leaving = /* @__PURE__ */ new Map();
1348
+ /** True only in a real DOM environment (browser). */
1349
+ get browser() {
1350
+ return this.dom.body() !== null;
1351
+ }
1352
+ keyOf(inst) {
1353
+ return inst.graphNode.key ?? inst.graphNode.id;
1354
+ }
1355
+ resolved(node) {
1356
+ return getResolvedTransition(this.graph, node.id);
1357
+ }
1358
+ /** Run the enter animation for `inst` if it carries a transition (browser only). */
1359
+ enter(inst) {
1360
+ if (!this.browser) return;
1361
+ const rt = this.resolved(inst.graphNode);
1362
+ if (rt === void 0) return;
1363
+ const el = inst.domNode;
1364
+ if (!this.dom.isElement(el)) return;
1365
+ startRun(this.dom, el, rt.enterActive, rt.enterFrom, rt.enterTo, rt.duration, () => {
1366
+ });
1367
+ }
1368
+ /**
1369
+ * Play `appear` for any initial child that opted into it (fresh browser mount
1370
+ * only — hydration must never animate appear, §22, and this is called only on
1371
+ * the mount path).
1372
+ */
1373
+ appear(children) {
1374
+ if (!this.browser) return;
1375
+ for (const child of children) {
1376
+ const rt = this.resolved(child.graphNode);
1377
+ if (rt !== void 0 && rt.appear) this.enter(child);
1378
+ }
1379
+ }
1380
+ hooks() {
1381
+ return {
1382
+ takeLeaving: (key) => {
1383
+ const entry = this.leaving.get(key);
1384
+ if (entry === void 0) return void 0;
1385
+ entry.run.cancel();
1386
+ this.leaving.delete(key);
1387
+ return entry.inst;
1388
+ },
1389
+ beginLeave: (inst) => {
1390
+ if (!this.browser) return false;
1391
+ const rt = this.resolved(inst.graphNode);
1392
+ if (rt === void 0) return false;
1393
+ const el = inst.domNode;
1394
+ if (!this.dom.isElement(el)) return false;
1395
+ const key = this.keyOf(inst);
1396
+ const prior = this.leaving.get(key);
1397
+ if (prior !== void 0) prior.run.cancel();
1398
+ const run = startRun(
1399
+ this.dom,
1400
+ el,
1401
+ rt.leaveActive,
1402
+ rt.leaveFrom,
1403
+ rt.leaveTo,
1404
+ rt.duration,
1405
+ () => {
1406
+ const current = this.leaving.get(key);
1407
+ if (current !== void 0 && current.run === run) {
1408
+ this.leaving.delete(key);
1409
+ this.finalize(inst);
1410
+ }
1411
+ }
1412
+ );
1413
+ this.leaving.set(key, { inst, run });
1414
+ return true;
1415
+ },
1416
+ onEnter: (inst) => this.enter(inst)
1417
+ };
1418
+ }
1419
+ };
1420
+
1190
1421
  // ../renderer/src/mount.ts
1191
1422
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
1192
1423
  "text",
@@ -1339,7 +1570,7 @@ function mountNode(ctx, graphNode, parentDom) {
1339
1570
  instance2.addChild(childInstance);
1340
1571
  }
1341
1572
  dom.appendChild(parentDom, el2);
1342
- wireReactiveList(ctx, graphNode, instance2, el2);
1573
+ wireReactiveList(ctx, graphNode, instance2, el2, true);
1343
1574
  return instance2;
1344
1575
  }
1345
1576
  if (graphNode.type === "portal") {
@@ -1383,6 +1614,7 @@ function mountNode(ctx, graphNode, parentDom) {
1383
1614
  instance.addChild(childInstance);
1384
1615
  }
1385
1616
  dom.appendChild(parentDom, el);
1617
+ if (graphNode.type === "component") wireComponentBehavior(ctx, graphNode, instance);
1386
1618
  return instance;
1387
1619
  }
1388
1620
  function textUpdate(dom, el, textNode) {
@@ -1447,32 +1679,42 @@ function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
1447
1679
  instance.trackCleanup(unsub);
1448
1680
  }
1449
1681
  }
1450
- function wireReactiveList(ctx, graphNode, instance, el) {
1682
+ function wireReactiveList(ctx, graphNode, instance, el, runAppear = false) {
1451
1683
  const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
1452
1684
  const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
1453
1685
  if (plan === void 0 && build === void 0) return;
1686
+ const controller = new TransitionController(ctx.dom, ctx.graph, (leaving) => {
1687
+ const parent = ctx.dom.parentNode(leaving.domNode);
1688
+ if (parent !== null) ctx.dom.removeChild(parent, leaving.domNode);
1689
+ leaving.dispose();
1690
+ forgetInstance(ctx, leaving);
1691
+ ctx.graph.detachNode(leaving.graphNode);
1692
+ });
1693
+ const hooks = controller.hooks();
1694
+ if (runAppear) controller.appear(instance.children);
1454
1695
  for (const stateRef of graphNode.stateRefs) {
1455
1696
  if (stateRef.propKey !== "items") continue;
1456
1697
  const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
1457
1698
  if (sig === void 0 || typeof sig.subscribe !== "function") continue;
1458
1699
  const unsub = sig.subscribe((value) => {
1459
1700
  if (plan !== void 0) {
1460
- reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
1701
+ reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value), hooks);
1461
1702
  } else {
1462
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
1703
+ reconcileReactiveList(ctx, graphNode, instance, el, build(value), hooks);
1463
1704
  }
1464
1705
  });
1465
1706
  instance.trackCleanup(unsub);
1466
1707
  }
1467
1708
  }
1468
- function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
1709
+ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan, hooks) {
1469
1710
  const oldInstances = [...listInstance.children];
1470
1711
  const result = reconcileChildrenByPlan(
1471
1712
  ctx,
1472
1713
  listEl,
1473
1714
  oldInstances,
1474
1715
  plan,
1475
- (node, parent) => mountNode(ctx, node, parent)
1716
+ (node, parent) => mountNode(ctx, node, parent),
1717
+ hooks
1476
1718
  );
1477
1719
  listInstance.children.length = 0;
1478
1720
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -1487,14 +1729,15 @@ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan)
1487
1729
  for (const child of [...listNode.children]) listNode.removeChild(child);
1488
1730
  for (const inst of result.instances) listNode.appendChild(inst.graphNode);
1489
1731
  }
1490
- function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
1732
+ function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes, hooks) {
1491
1733
  const oldInstances = [...listInstance.children];
1492
1734
  const result = reconcileChildren(
1493
1735
  ctx,
1494
1736
  listEl,
1495
1737
  oldInstances,
1496
1738
  newNodes,
1497
- (node, parent) => mountNode(ctx, node, parent)
1739
+ (node, parent) => mountNode(ctx, node, parent),
1740
+ hooks
1498
1741
  );
1499
1742
  listInstance.children.length = 0;
1500
1743
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -1530,11 +1773,14 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
1530
1773
  const onOpenChange = (isOpen) => {
1531
1774
  if (isOpen) {
1532
1775
  if (desc.restoreFocus) saved = saveFocus(dom);
1533
- if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
1534
1776
  if (desc.modal) {
1535
1777
  active.push(trapFocus(dom, target));
1536
1778
  active.push(containFocus(dom, target));
1537
1779
  }
1780
+ if (desc.menu) {
1781
+ active.push(rovingMenu(dom, target));
1782
+ }
1783
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
1538
1784
  if (desc.closeOnEscape && desc.onClose !== void 0) {
1539
1785
  active.push(onEscape(dom, target, desc.onClose));
1540
1786
  }
@@ -1551,6 +1797,11 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
1551
1797
  instance.trackCleanup(teardown);
1552
1798
  if (openSig.peek() === true) onOpenChange(true);
1553
1799
  }
1800
+ function wireComponentBehavior(ctx, graphNode, instance) {
1801
+ const fn = ctx.graph.getHandler(`__component__${graphNode.id}`);
1802
+ if (fn === void 0) return;
1803
+ for (const cleanup of fn()) instance.trackCleanup(cleanup);
1804
+ }
1554
1805
 
1555
1806
  // ../renderer/src/hydration-diagnostics.ts
1556
1807
  function formatHydrationDiagnostic(d) {
@@ -1675,6 +1926,7 @@ function hydrateNode(ctx, graphNode, domNode, path) {
1675
1926
  return instance;
1676
1927
  }
1677
1928
  hydrateChildren(ctx, graphNode, instance, domNode, path);
1929
+ if (graphNode.type === "component") wireComponentBehavior(ctx, graphNode, instance);
1678
1930
  return instance;
1679
1931
  }
1680
1932
  }
@@ -1873,7 +2125,8 @@ function analyzeGraph(graph) {
1873
2125
  const isList = node.type === "reactive-list";
1874
2126
  const isConditional = node.type === "conditional";
1875
2127
  const isPortal = node.type === "portal";
1876
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal;
2128
+ const isComponent = node.type === "component";
2129
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent;
1877
2130
  const isStaticSubtree = isStatic && allChildrenStatic;
1878
2131
  nodes.set(node.id, {
1879
2132
  isStatic,
@@ -2204,6 +2457,37 @@ async function waitFor(check, options = {}) {
2204
2457
  await new Promise((resolve) => setTimeout(resolve, interval));
2205
2458
  }
2206
2459
  }
2460
+ function focus(el) {
2461
+ el.focus?.();
2462
+ }
2463
+ function blur(el) {
2464
+ el.blur?.();
2465
+ }
2466
+ function pressKey(key, el = document.activeElement, init = {}) {
2467
+ if (el === null) return;
2468
+ el.dispatchEvent(
2469
+ new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true, ...init })
2470
+ );
2471
+ }
2472
+ function clickOutside(container, target = document.body) {
2473
+ if (container.contains(target)) {
2474
+ throw new Error("[StreetUI Testing] clickOutside: target is inside the container");
2475
+ }
2476
+ target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
2477
+ target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
2478
+ }
2479
+ async function openOverlay(open) {
2480
+ open.set(true);
2481
+ await flushUpdates();
2482
+ }
2483
+ async function closeOverlay(open) {
2484
+ open.set(false);
2485
+ await flushUpdates();
2486
+ }
2487
+ async function waitForTransition(el) {
2488
+ el.dispatchEvent(new Event("transitionend", { bubbles: true }));
2489
+ await flushUpdates();
2490
+ }
2207
2491
  function findByText(container, text) {
2208
2492
  const match = Array.from(container.querySelectorAll("*")).find(
2209
2493
  (el) => el.children.length === 0 && (el.textContent?.includes(text) ?? false)
@@ -2298,20 +2582,1160 @@ function renderServerThenHydrate(build, options = {}) {
2298
2582
  };
2299
2583
  }
2300
2584
 
2585
+ // ../dsl/src/transition.ts
2586
+ function classes(value) {
2587
+ if (value === void 0) return [];
2588
+ const out = [];
2589
+ for (const token of value.split(/\s+/)) {
2590
+ if (token.length > 0 && !out.includes(token)) out.push(token);
2591
+ }
2592
+ return out;
2593
+ }
2594
+ function merge(a, b) {
2595
+ const out = [...a];
2596
+ for (const token of b) if (!out.includes(token)) out.push(token);
2597
+ return out;
2598
+ }
2599
+ function resolveTransition(config) {
2600
+ const n = config.name;
2601
+ const enterActive = merge(
2602
+ classes(config.enter),
2603
+ classes(config.enterActive ?? (n !== void 0 ? `${n}-enter-active` : void 0))
2604
+ );
2605
+ const leaveActive = merge(
2606
+ classes(config.leave),
2607
+ classes(config.leaveActive ?? (n !== void 0 ? `${n}-leave-active` : void 0))
2608
+ );
2609
+ return {
2610
+ enterActive,
2611
+ enterFrom: classes(config.enterFrom ?? (n !== void 0 ? `${n}-enter-from` : void 0)),
2612
+ enterTo: classes(config.enterTo ?? (n !== void 0 ? `${n}-enter-to` : void 0)),
2613
+ leaveActive,
2614
+ leaveFrom: classes(config.leaveFrom ?? (n !== void 0 ? `${n}-leave-from` : void 0)),
2615
+ leaveTo: classes(config.leaveTo ?? (n !== void 0 ? `${n}-leave-to` : void 0)),
2616
+ appear: config.appear ?? false,
2617
+ duration: config.duration ?? 1e3
2618
+ };
2619
+ }
2620
+
2621
+ // ../state/src/signal.ts
2622
+ var _activeConsumer = null;
2623
+ function withConsumer(consumer, fn) {
2624
+ const prev = _activeConsumer;
2625
+ _activeConsumer = consumer;
2626
+ try {
2627
+ return fn();
2628
+ } finally {
2629
+ _activeConsumer = prev;
2630
+ }
2631
+ }
2632
+ var _batchDepth = 0;
2633
+ var _pendingFlushes = /* @__PURE__ */ new Map();
2634
+ function _enqueueBatchFlush(sig, value) {
2635
+ _pendingFlushes.set(sig, { signal: sig, value });
2636
+ }
2637
+ var Signal = class {
2638
+ _value;
2639
+ _subscribers = /* @__PURE__ */ new Set();
2640
+ _consumers = /* @__PURE__ */ new Set();
2641
+ constructor(initial) {
2642
+ this._value = initial;
2643
+ }
2644
+ get() {
2645
+ if (_activeConsumer !== null) {
2646
+ this._consumers.add(_activeConsumer);
2647
+ _activeConsumer._addSource(this);
2648
+ }
2649
+ return this._value;
2650
+ }
2651
+ peek() {
2652
+ return this._value;
2653
+ }
2654
+ set(value) {
2655
+ if (Object.is(this._value, value)) return;
2656
+ this._value = value;
2657
+ if (_batchDepth > 0) {
2658
+ _enqueueBatchFlush(this, value);
2659
+ } else {
2660
+ this._flush(value);
2661
+ }
2662
+ }
2663
+ update(fn) {
2664
+ this.set(fn(this._value));
2665
+ }
2666
+ subscribe(fn) {
2667
+ this._subscribers.add(fn);
2668
+ return () => {
2669
+ this._subscribers.delete(fn);
2670
+ };
2671
+ }
2672
+ _removeConsumer(consumer) {
2673
+ this._consumers.delete(consumer);
2674
+ }
2675
+ /**
2676
+ * Called by the batch machinery after the batch has completed.
2677
+ * Notifies subscribers with the final coalesced value.
2678
+ */
2679
+ _flushBatch(value) {
2680
+ this._flush(value);
2681
+ }
2682
+ _flush(value) {
2683
+ for (const sub of [...this._subscribers]) sub(value);
2684
+ for (const consumer of [...this._consumers]) consumer._invalidate();
2685
+ }
2686
+ /**
2687
+ * @internal DevTools inspection only. The number of live observers
2688
+ * (direct subscribers plus derived/effect consumers). Read-only; never
2689
+ * mutates reactive state.
2690
+ */
2691
+ _observerCount() {
2692
+ return this._subscribers.size + this._consumers.size;
2693
+ }
2694
+ };
2695
+ var DerivedSignal = class {
2696
+ _value = void 0;
2697
+ _dirty = true;
2698
+ _disposed = false;
2699
+ _fn;
2700
+ _subscribers = /* @__PURE__ */ new Set();
2701
+ /** All upstream sources this derived currently reads from. */
2702
+ _sources = /* @__PURE__ */ new Set();
2703
+ /** Downstream consumers that depend on this derived. */
2704
+ _consumers = /* @__PURE__ */ new Set();
2705
+ constructor(fn) {
2706
+ this._fn = fn;
2707
+ }
2708
+ get() {
2709
+ if (_activeConsumer !== null) {
2710
+ this._consumers.add(_activeConsumer);
2711
+ _activeConsumer._addSource(this);
2712
+ }
2713
+ if (this._dirty) this._recompute();
2714
+ return this._value;
2715
+ }
2716
+ peek() {
2717
+ if (this._dirty) this._recompute();
2718
+ return this._value;
2719
+ }
2720
+ subscribe(fn) {
2721
+ if (this._dirty) this._recompute();
2722
+ this._subscribers.add(fn);
2723
+ return () => {
2724
+ this._subscribers.delete(fn);
2725
+ };
2726
+ }
2727
+ _addSource(src) {
2728
+ this._sources.add(src);
2729
+ }
2730
+ _removeConsumer(consumer) {
2731
+ this._consumers.delete(consumer);
2732
+ }
2733
+ _invalidate() {
2734
+ if (this._disposed) return;
2735
+ this._dirty = true;
2736
+ const newVal = this.peek();
2737
+ for (const sub of [...this._subscribers]) sub(newVal);
2738
+ for (const consumer of [...this._consumers]) consumer._invalidate();
2739
+ }
2740
+ _recompute() {
2741
+ for (const src of this._sources) src._removeConsumer(this);
2742
+ this._sources.clear();
2743
+ this._value = withConsumer(this, this._fn);
2744
+ this._dirty = false;
2745
+ }
2746
+ dispose() {
2747
+ this._disposed = true;
2748
+ for (const src of this._sources) src._removeConsumer(this);
2749
+ this._sources.clear();
2750
+ this._subscribers.clear();
2751
+ this._consumers.clear();
2752
+ }
2753
+ /**
2754
+ * @internal DevTools inspection only. Live observers (subscribers plus
2755
+ * downstream consumers). Read-only.
2756
+ */
2757
+ _observerCount() {
2758
+ return this._subscribers.size + this._consumers.size;
2759
+ }
2760
+ };
2761
+ var Effect = class {
2762
+ _fn;
2763
+ _cleanup = void 0;
2764
+ _disposed = false;
2765
+ _sources = /* @__PURE__ */ new Set();
2766
+ constructor(fn) {
2767
+ this._fn = fn;
2768
+ this._run();
2769
+ }
2770
+ _addSource(src) {
2771
+ this._sources.add(src);
2772
+ }
2773
+ _invalidate() {
2774
+ if (this._disposed) return;
2775
+ this._run();
2776
+ }
2777
+ _run() {
2778
+ for (const src of this._sources) src._removeConsumer(this);
2779
+ this._sources.clear();
2780
+ if (typeof this._cleanup === "function") this._cleanup();
2781
+ const result = withConsumer(this, this._fn);
2782
+ this._cleanup = typeof result === "function" ? result : void 0;
2783
+ }
2784
+ dispose() {
2785
+ this._disposed = true;
2786
+ for (const src of this._sources) src._removeConsumer(this);
2787
+ this._sources.clear();
2788
+ if (typeof this._cleanup === "function") this._cleanup();
2789
+ this._cleanup = void 0;
2790
+ }
2791
+ };
2792
+ function signal(initial) {
2793
+ return new Signal(initial);
2794
+ }
2795
+ function derived(fn) {
2796
+ return new DerivedSignal(fn);
2797
+ }
2798
+ function effect(fn) {
2799
+ const e = new Effect(fn);
2800
+ return () => e.dispose();
2801
+ }
2802
+
2803
+ // ../dsl/src/builders.ts
2804
+ function isSignal(v) {
2805
+ return v !== null && typeof v === "object" && typeof v["get"] === "function" && typeof v["subscribe"] === "function";
2806
+ }
2807
+ function bindValue(graph, node, propKey, value) {
2808
+ if (isSignal(value)) {
2809
+ const signalId = `${node.id}:${propKey}`;
2810
+ node.stateRefs.push({ signalId, propKey });
2811
+ graph.registerHandler(`__signal__${signalId}`, value);
2812
+ return value.peek();
2813
+ }
2814
+ return value;
2815
+ }
2816
+ function applyA11yProps(props, options) {
2817
+ if (options.role !== void 0) props["role"] = options.role;
2818
+ if (options.tabIndex !== void 0) props["tabindex"] = String(options.tabIndex);
2819
+ if (options.ariaLabel !== void 0) props["aria-label"] = options.ariaLabel;
2820
+ if (options.ariaLabelledBy !== void 0) props["aria-labelledby"] = options.ariaLabelledBy;
2821
+ if (options.ariaDescribedBy !== void 0) props["aria-describedby"] = options.ariaDescribedBy;
2822
+ if (options.ariaExpanded !== void 0) props["aria-expanded"] = String(options.ariaExpanded);
2823
+ if (options.ariaControls !== void 0) props["aria-controls"] = options.ariaControls;
2824
+ if (options.ariaHidden !== void 0) props["aria-hidden"] = String(options.ariaHidden);
2825
+ if (options.ariaLive !== void 0) props["aria-live"] = options.ariaLive;
2826
+ if (options.ariaCurrent !== void 0) props["aria-current"] = String(options.ariaCurrent);
2827
+ if (options.ariaInvalid !== void 0) props["aria-invalid"] = String(options.ariaInvalid);
2828
+ if (options.ariaRequired !== void 0) props["aria-required"] = String(options.ariaRequired);
2829
+ if (options.ariaModal !== void 0) props["aria-modal"] = String(options.ariaModal);
2830
+ if (options.ariaOwns !== void 0) props["aria-owns"] = options.ariaOwns;
2831
+ if (options.ariaActiveDescendant !== void 0) props["aria-activedescendant"] = options.ariaActiveDescendant;
2832
+ if (options.ariaHasPopup !== void 0) props["aria-haspopup"] = String(options.ariaHasPopup);
2833
+ if (options.ariaSelected !== void 0) props["aria-selected"] = String(options.ariaSelected);
2834
+ }
2835
+ function registerTransition(graph, node, config) {
2836
+ if (config === void 0) return;
2837
+ const resolved = resolveTransition(config);
2838
+ graph.registerHandler(
2839
+ `__transition__${node.id}`,
2840
+ () => resolved
2841
+ );
2842
+ }
2843
+ function containerProps(options) {
2844
+ const props = {};
2845
+ if (options.class !== void 0) props["class"] = options.class;
2846
+ if (options.id !== void 0) props["id"] = options.id;
2847
+ if (options.key !== void 0) props["key"] = options.key;
2848
+ applyA11yProps(props, options);
2849
+ return props;
2850
+ }
2851
+ function itemIdentity(item, index) {
2852
+ if (item !== null && typeof item === "object") {
2853
+ const obj = item;
2854
+ if ("id" in obj) return `id:${String(obj["id"])}`;
2855
+ if ("key" in obj) return `key:${String(obj["key"])}`;
2856
+ return `idx:${index}`;
2857
+ }
2858
+ return `val:${String(item)}`;
2859
+ }
2860
+ function itemValueSignature(item) {
2861
+ try {
2862
+ return JSON.stringify(item) ?? String(item);
2863
+ } catch {
2864
+ return String(item);
2865
+ }
2866
+ }
2867
+ function reactiveListItemSignature(item) {
2868
+ return itemValueSignature(item);
2869
+ }
2870
+ function reactiveListItemKey(item, index) {
2871
+ return itemIdentity(item, index);
2872
+ }
2873
+ var OVERLAY_KINDS = {
2874
+ dialog: {
2875
+ role: "dialog",
2876
+ modal: true,
2877
+ takesFocus: true,
2878
+ ariaModal: true,
2879
+ defaultCloseOnEscape: true,
2880
+ defaultRestoreFocus: true
2881
+ },
2882
+ popover: {
2883
+ role: "dialog",
2884
+ modal: false,
2885
+ takesFocus: true,
2886
+ ariaModal: false,
2887
+ defaultCloseOnEscape: true,
2888
+ defaultRestoreFocus: true
2889
+ },
2890
+ tooltip: {
2891
+ role: "tooltip",
2892
+ modal: false,
2893
+ takesFocus: false,
2894
+ ariaModal: false,
2895
+ defaultCloseOnEscape: false,
2896
+ defaultRestoreFocus: false
2897
+ },
2898
+ dropdown: {
2899
+ role: "menu",
2900
+ modal: false,
2901
+ takesFocus: true,
2902
+ menu: true,
2903
+ ariaModal: false,
2904
+ defaultCloseOnEscape: true,
2905
+ defaultRestoreFocus: true
2906
+ },
2907
+ toast: {
2908
+ role: "status",
2909
+ modal: false,
2910
+ takesFocus: false,
2911
+ ariaModal: false,
2912
+ ariaLive: "polite",
2913
+ defaultCloseOnEscape: false,
2914
+ defaultRestoreFocus: false
2915
+ }
2916
+ };
2917
+ var ContentBuilderBase = class {
2918
+ constructor(_node, _graph) {
2919
+ this._node = _node;
2920
+ this._graph = _graph;
2921
+ }
2922
+ heading(text, options = {}) {
2923
+ const props = { level: options.level ?? 1 };
2924
+ if (options.class !== void 0) props["class"] = options.class;
2925
+ if (options.id !== void 0) props["id"] = options.id;
2926
+ applyA11yProps(props, options);
2927
+ const node = this._graph.createNode("heading", { parent: this._node, props });
2928
+ const resolved = bindValue(this._graph, node, "text", text);
2929
+ node.setProp("text", resolved);
2930
+ }
2931
+ text(content, options = {}) {
2932
+ const props = {};
2933
+ if (options.class !== void 0) props["class"] = options.class;
2934
+ if (options.id !== void 0) props["id"] = options.id;
2935
+ applyA11yProps(props, options);
2936
+ const node = this._graph.createNode("text", { parent: this._node, props });
2937
+ const resolved = bindValue(this._graph, node, "text", content);
2938
+ node.setProp("text", resolved);
2939
+ }
2940
+ button(label, options = {}) {
2941
+ const props = {};
2942
+ if (options.class !== void 0) props["class"] = options.class;
2943
+ if (options.id !== void 0) props["id"] = options.id;
2944
+ applyA11yProps(props, options);
2945
+ const node = this._graph.createNode("button", { parent: this._node, props });
2946
+ const resolved = bindValue(this._graph, node, "label", label);
2947
+ node.setProp("label", resolved);
2948
+ if (options.disabled !== void 0) {
2949
+ const resolvedDisabled = bindValue(this._graph, node, "disabled", options.disabled);
2950
+ node.setProp("disabled", resolvedDisabled);
2951
+ }
2952
+ if (options.onClick !== void 0) {
2953
+ const handlerKey = `click:${node.id}`;
2954
+ this._graph.registerHandler(handlerKey, options.onClick);
2955
+ node.addEvent({ type: "click", handlerKey });
2956
+ }
2957
+ }
2958
+ input(options = {}) {
2959
+ const props = {};
2960
+ props["inputType"] = options.type ?? "text";
2961
+ if (options.placeholder !== void 0) props["placeholder"] = options.placeholder;
2962
+ if (options.class !== void 0) props["class"] = options.class;
2963
+ if (options.id !== void 0) props["id"] = options.id;
2964
+ applyA11yProps(props, options);
2965
+ const nodeOpts = {
2966
+ props,
2967
+ parent: this._node
2968
+ };
2969
+ if (options.id !== void 0) nodeOpts.key = options.id;
2970
+ const node = this._graph.createNode("input", nodeOpts);
2971
+ const bindSignal = options.bind;
2972
+ const valueBindable = bindSignal !== void 0 ? bindSignal : options.value;
2973
+ const inputHandler = bindSignal !== void 0 ? (v) => bindSignal.set(v) : options.onInput;
2974
+ if (valueBindable !== void 0) {
2975
+ const resolved = bindValue(this._graph, node, "value", valueBindable);
2976
+ node.setProp("value", resolved);
2977
+ }
2978
+ if (options.disabled !== void 0) {
2979
+ const resolved = bindValue(this._graph, node, "disabled", options.disabled);
2980
+ node.setProp("disabled", resolved);
2981
+ }
2982
+ if (inputHandler !== void 0) {
2983
+ const handlerKey = `input:${node.id}`;
2984
+ this._graph.registerHandler(handlerKey, inputHandler);
2985
+ node.addEvent({ type: "input", handlerKey });
2986
+ }
2987
+ if (options.onChange !== void 0) {
2988
+ const handlerKey = `change:${node.id}`;
2989
+ this._graph.registerHandler(handlerKey, options.onChange);
2990
+ node.addEvent({ type: "change", handlerKey });
2991
+ }
2992
+ }
2993
+ image(options) {
2994
+ const props = {
2995
+ src: options.src,
2996
+ alt: options.alt
2997
+ };
2998
+ if (options.width !== void 0) props["width"] = options.width;
2999
+ if (options.height !== void 0) props["height"] = options.height;
3000
+ if (options.class !== void 0) props["class"] = options.class;
3001
+ if (options.id !== void 0) props["id"] = options.id;
3002
+ applyA11yProps(props, options);
3003
+ const nodeOpts = {
3004
+ props,
3005
+ parent: this._node
3006
+ };
3007
+ if (options.id !== void 0) nodeOpts.key = options.id;
3008
+ this._graph.createNode("image", nodeOpts);
3009
+ }
3010
+ link(label, options) {
3011
+ const props = {
3012
+ href: options.href,
3013
+ external: options.external ?? false
3014
+ };
3015
+ if (options.class !== void 0) props["class"] = options.class;
3016
+ if (options.id !== void 0) props["id"] = options.id;
3017
+ applyA11yProps(props, options);
3018
+ const node = this._graph.createNode("link", { parent: this._node, props });
3019
+ const resolved = bindValue(this._graph, node, "label", label);
3020
+ node.setProp("label", resolved);
3021
+ if (options.onClick !== void 0) {
3022
+ const handlerKey = `click:${node.id}`;
3023
+ this._graph.registerHandler(handlerKey, options.onClick);
3024
+ node.addEvent({ type: "click", handlerKey });
3025
+ }
3026
+ }
3027
+ };
3028
+ var ContainerBuilderBase = class extends ContentBuilderBase {
3029
+ section(key, builder, options = {}) {
3030
+ const node = this._graph.createNode("section", {
3031
+ key,
3032
+ parent: this._node,
3033
+ props: containerProps(options)
3034
+ });
3035
+ registerTransition(this._graph, node, options.transition);
3036
+ builder(new SectionBuilderImpl(node, this._graph));
3037
+ }
3038
+ container(key, builder, options = {}) {
3039
+ const node = this._graph.createNode("container", {
3040
+ key,
3041
+ parent: this._node,
3042
+ props: containerProps(options)
3043
+ });
3044
+ registerTransition(this._graph, node, options.transition);
3045
+ builder(new ContainerBuilderImpl(node, this._graph));
3046
+ }
3047
+ list(key, builder, options = {}) {
3048
+ const node = this._graph.createNode("list", {
3049
+ key,
3050
+ parent: this._node,
3051
+ props: containerProps(options)
3052
+ });
3053
+ registerTransition(this._graph, node, options.transition);
3054
+ builder(new ListBuilderImpl(node, this._graph));
3055
+ }
3056
+ listOf(key, items, renderItem, options = {}) {
3057
+ const graph = this._graph;
3058
+ const node = graph.createNode("reactive-list", {
3059
+ key,
3060
+ parent: this._node,
3061
+ props: containerProps(options)
3062
+ });
3063
+ registerTransition(graph, node, options.transition);
3064
+ const signalId = `${node.id}:items`;
3065
+ node.stateRefs.push({ signalId, propKey: "items" });
3066
+ graph.registerHandler(`__signal__${signalId}`, items);
3067
+ const buildItem = (item, index) => {
3068
+ const itemKey = reactiveListItemKey(item, index);
3069
+ const itemNode = graph.createNode("list-item", {
3070
+ key: itemKey,
3071
+ // `_item` records the source item *reference* so the reconciler can
3072
+ // short-circuit unchanged rows by identity (no signature hashing); `_sig`
3073
+ // is the content signature used to detect an in-place data change when the
3074
+ // reference differs. Both are internal metadata (leading `_`) and never
3075
+ // reach the DOM.
3076
+ props: {
3077
+ key: itemKey,
3078
+ _sig: reactiveListItemSignature(item),
3079
+ // The item reference is stored as opaque internal metadata (never
3080
+ // rendered); cast through `unknown` since `T` is not a `PropValue`.
3081
+ _item: item
3082
+ }
3083
+ });
3084
+ renderItem(item, index, new ContainerBuilderImpl(itemNode, graph));
3085
+ registerTransition(graph, itemNode, options.itemTransition);
3086
+ return itemNode;
3087
+ };
3088
+ const buildPlan = (raw) => {
3089
+ const arr = Array.isArray(raw) ? raw : [];
3090
+ const plan = new Array(arr.length);
3091
+ for (let i = 0; i < arr.length; i++) {
3092
+ const item = arr[i];
3093
+ const index = i;
3094
+ plan[i] = {
3095
+ key: reactiveListItemKey(item, index),
3096
+ item,
3097
+ sig: () => reactiveListItemSignature(item),
3098
+ build: () => buildItem(item, index)
3099
+ };
3100
+ }
3101
+ return plan;
3102
+ };
3103
+ graph.registerHandler(`__listplan__${node.id}`, buildPlan);
3104
+ const current = isSignal(items) ? items.peek() : items;
3105
+ const initial = Array.isArray(current) ? current : [];
3106
+ initial.forEach((item, i) => {
3107
+ node.appendChild(buildItem(item, i));
3108
+ });
3109
+ }
3110
+ form(key, builder, options = {}) {
3111
+ const props = containerProps(options);
3112
+ const node = this._graph.createNode("form", {
3113
+ key,
3114
+ parent: this._node,
3115
+ props
3116
+ });
3117
+ registerTransition(this._graph, node, options.transition);
3118
+ if (options.onSubmit !== void 0) {
3119
+ const handlerKey = `submit:${node.id}`;
3120
+ this._graph.registerHandler(handlerKey, options.onSubmit);
3121
+ node.addEvent({ type: "submit", handlerKey });
3122
+ }
3123
+ builder(new FormBuilderImpl(node, this._graph));
3124
+ }
3125
+ when(condition, builder, elseBuilder, options = {}) {
3126
+ const graph = this._graph;
3127
+ const node = graph.createNode("conditional", {
3128
+ parent: this._node,
3129
+ props: containerProps({})
3130
+ });
3131
+ const branchTransition = options.transition !== void 0 && options.appear === true ? { ...options.transition, appear: true } : options.transition;
3132
+ const buildBranch = (build, tag) => {
3133
+ const branchKey = `when-${tag}:${node.id}`;
3134
+ const branch = graph.createNode("container", {
3135
+ key: branchKey,
3136
+ props: { key: branchKey }
3137
+ });
3138
+ registerTransition(graph, branch, branchTransition);
3139
+ build(new ContainerBuilderImpl(branch, graph));
3140
+ return branch;
3141
+ };
3142
+ const buildAll = (raw) => {
3143
+ if (raw) return [buildBranch(builder, "then")];
3144
+ return elseBuilder !== void 0 ? [buildBranch(elseBuilder, "else")] : [];
3145
+ };
3146
+ if (isSignal(condition)) {
3147
+ const signalId = `${node.id}:items`;
3148
+ node.stateRefs.push({ signalId, propKey: "items" });
3149
+ graph.registerHandler(`__signal__${signalId}`, condition);
3150
+ graph.registerHandler(`__listbuild__${node.id}`, buildAll);
3151
+ const current = condition.peek();
3152
+ for (const child of buildAll(current)) node.appendChild(child);
3153
+ } else {
3154
+ for (const child of buildAll(condition)) node.appendChild(child);
3155
+ }
3156
+ }
3157
+ errorBoundary(id, builder, options) {
3158
+ const sources = options.source === void 0 ? [] : Array.isArray(options.source) ? [...options.source] : [options.source];
3159
+ const localError = signal(void 0);
3160
+ const retryNonce = signal(0);
3161
+ const readError = () => {
3162
+ const local = localError.peek();
3163
+ if (local !== void 0 && local !== null) return local;
3164
+ for (const s of sources) {
3165
+ const e = s.peek();
3166
+ if (e !== void 0 && e !== null) return e;
3167
+ }
3168
+ return void 0;
3169
+ };
3170
+ const hasError = derived(() => {
3171
+ retryNonce.get();
3172
+ localError.get();
3173
+ for (const s of sources) s.get();
3174
+ return readError() !== void 0;
3175
+ });
3176
+ const retry = () => {
3177
+ localError.set(void 0);
3178
+ options.onRetry?.();
3179
+ retryNonce.update((n) => n + 1);
3180
+ };
3181
+ this.container(id, (c) => {
3182
+ c.when(
3183
+ hasError,
3184
+ // Error state → fallback.
3185
+ (fb) => options.fallback(fb, readError(), retry),
3186
+ // Healthy state → body, guarded against synchronous build throws.
3187
+ (body) => {
3188
+ try {
3189
+ builder(body);
3190
+ } catch (err) {
3191
+ queueMicrotask(() => localError.set(err));
3192
+ }
3193
+ }
3194
+ );
3195
+ }, { id });
3196
+ }
3197
+ // ── Portals & overlays ──────────────────────────────────────────────────────
3198
+ portal(key, builder, options = {}) {
3199
+ const node = this._graph.createNode("portal", {
3200
+ key,
3201
+ parent: this._node,
3202
+ props: containerProps(options)
3203
+ });
3204
+ builder(new ContainerBuilderImpl(node, this._graph));
3205
+ }
3206
+ /**
3207
+ * Shared assembly for every overlay kind: a `portal` node whose single child
3208
+ * is a `when(open, panel)` conditional. The panel container carries the
3209
+ * kind's ARIA semantics; `builder` fills it. An `__overlay__<portalId>`
3210
+ * descriptor is registered so the renderer wires focus/keyboard behavior to
3211
+ * the same `open` signal that drives the panel. Reuses existing primitives
3212
+ * (portal + when + container) — no new render path.
3213
+ */
3214
+ _overlay(kind, key, options, builder) {
3215
+ const graph = this._graph;
3216
+ const portalNode = graph.createNode("portal", {
3217
+ key,
3218
+ parent: this._node,
3219
+ props: { key }
3220
+ });
3221
+ const openBindable = options.open;
3222
+ const openSignal = isSignal(openBindable) ? openBindable : signal(openBindable);
3223
+ const panelOptions = {
3224
+ role: options.role ?? kind.role,
3225
+ ...kind.ariaModal ? { ariaModal: true } : {},
3226
+ ...kind.ariaLive !== void 0 ? { ariaLive: kind.ariaLive } : {},
3227
+ ...options.class !== void 0 ? { class: options.class } : {},
3228
+ ...options.ariaLabel !== void 0 ? { ariaLabel: options.ariaLabel } : {},
3229
+ ...options.ariaLabelledBy !== void 0 ? { ariaLabelledBy: options.ariaLabelledBy } : {},
3230
+ ...options.ariaDescribedBy !== void 0 ? { ariaDescribedBy: options.ariaDescribedBy } : {}
3231
+ };
3232
+ const portalBuilder = new ContainerBuilderImpl(portalNode, graph);
3233
+ portalBuilder.when(
3234
+ openSignal,
3235
+ (panelHost) => {
3236
+ panelHost.container(`${key}__panel`, builder, panelOptions);
3237
+ },
3238
+ void 0,
3239
+ // Overlay open/close rides the panel's `when`; a transition animates the
3240
+ // panel in on open and — via the reconciler's deferred-leave — plays the
3241
+ // leave before the panel is removed (§10). Focus is restored at close-
3242
+ // request time (see wireOverlayBehavior), so it never stays trapped inside
3243
+ // a panel that is animating away.
3244
+ options.transition !== void 0 ? { transition: options.transition } : {}
3245
+ );
3246
+ const descriptor = {
3247
+ open: openSignal,
3248
+ modal: kind.modal,
3249
+ takesFocus: kind.takesFocus,
3250
+ menu: kind.menu ?? false,
3251
+ closeOnEscape: options.closeOnEscape ?? kind.defaultCloseOnEscape,
3252
+ restoreFocus: options.restoreFocus ?? kind.defaultRestoreFocus,
3253
+ ...options.initialFocusId !== void 0 ? { initialFocusId: options.initialFocusId } : {},
3254
+ ...options.onClose !== void 0 ? { onClose: options.onClose } : {}
3255
+ };
3256
+ graph.registerHandler(
3257
+ `__overlay__${portalNode.id}`,
3258
+ () => descriptor
3259
+ );
3260
+ }
3261
+ dialog(key, options, builder) {
3262
+ this._overlay(OVERLAY_KINDS.dialog, key, options, builder);
3263
+ }
3264
+ popover(key, options, builder) {
3265
+ this._overlay(OVERLAY_KINDS.popover, key, options, builder);
3266
+ }
3267
+ tooltip(key, options, builder) {
3268
+ this._overlay(OVERLAY_KINDS.tooltip, key, options, builder);
3269
+ }
3270
+ dropdown(key, options, builder) {
3271
+ this._overlay(OVERLAY_KINDS.dropdown, key, options, builder);
3272
+ }
3273
+ toast(key, options, builder) {
3274
+ this._overlay(OVERLAY_KINDS.toast, key, options, builder);
3275
+ }
3276
+ // ── Components ────────────────────────────────────────────────────────────
3277
+ /**
3278
+ * Instantiate a reusable component (§3–§9). Creates a `'component'` node
3279
+ * (rendered as a `<div>` wrapper — preserves the one-node/one-element
3280
+ * positional-hydration invariant), then runs `def.setup(props, ctx)`
3281
+ * synchronously to obtain the render function and fills the component's own
3282
+ * container scope with it — structurally identical to `container`/
3283
+ * `errorBoundary`. Cleanups the setup registers via `ctx.effect`/
3284
+ * `ctx.onCleanup` are collected into a closure and exposed to the renderer
3285
+ * through a `__component__<id>` handler (mirroring `__overlay__`); the mount/
3286
+ * hydrate paths read it and route each into `NodeInstance.trackCleanup`, so
3287
+ * teardown runs (children-first) when the component leaves the graph.
3288
+ *
3289
+ * `setup` runs once per instance here at build time. When this component sits
3290
+ * inside a keyed list / conditional, a rebuild disposes the old instance
3291
+ * (running its cleanups + pruning its `__component__` entry) and re-runs this
3292
+ * method for the new node — so re-invocation is safe and leak-free.
3293
+ */
3294
+ component(key, def, props, children) {
3295
+ const graph = this._graph;
3296
+ const node = graph.createNode("component", {
3297
+ key,
3298
+ parent: this._node,
3299
+ // `data-streetui-component` is a non-underscore prop, so it reaches the
3300
+ // DOM as an attribute and is visible to DevTools (§21) — unlike the
3301
+ // internal `_`-prefixed metadata the renderer hides.
3302
+ props: { key, "data-streetui-component": def.name }
3303
+ });
3304
+ const cleanups = [];
3305
+ const ctx = {
3306
+ key,
3307
+ onCleanup(fn) {
3308
+ cleanups.push(fn);
3309
+ },
3310
+ effect(fn) {
3311
+ cleanups.push(effect(fn));
3312
+ },
3313
+ renderChildren(content) {
3314
+ if (children !== void 0) children(content);
3315
+ }
3316
+ };
3317
+ let render2;
3318
+ try {
3319
+ render2 = def.setup(props, ctx);
3320
+ render2(new ContainerBuilderImpl(node, graph));
3321
+ } catch (err) {
3322
+ queueMicrotask(() => {
3323
+ throw err;
3324
+ });
3325
+ }
3326
+ if (cleanups.length > 0) {
3327
+ graph.registerHandler(
3328
+ `__component__${node.id}`,
3329
+ () => cleanups
3330
+ );
3331
+ }
3332
+ }
3333
+ };
3334
+ var SectionBuilderImpl = class extends ContainerBuilderBase {
3335
+ };
3336
+ var ContainerBuilderImpl = class extends ContainerBuilderBase {
3337
+ };
3338
+ var FormBuilderImpl = class extends ContainerBuilderBase {
3339
+ };
3340
+ var ListBuilderImpl = class extends ContentBuilderBase {
3341
+ item(key, builder, options = {}) {
3342
+ const node = this._graph.createNode("list-item", {
3343
+ key,
3344
+ parent: this._node,
3345
+ props: containerProps(options)
3346
+ });
3347
+ registerTransition(this._graph, node, options.transition);
3348
+ builder(new ContainerBuilderImpl(node, this._graph));
3349
+ }
3350
+ };
3351
+ var PageBuilderImpl = class extends ContainerBuilderBase {
3352
+ };
3353
+ var AppBuilder = class {
3354
+ constructor(_graph) {
3355
+ this._graph = _graph;
3356
+ }
3357
+ page(key, builder) {
3358
+ const node = this._graph.createNode("page", {
3359
+ key,
3360
+ parent: this._graph.root,
3361
+ props: { key }
3362
+ });
3363
+ builder(new PageBuilderImpl(node, this._graph));
3364
+ }
3365
+ };
3366
+
3367
+ // ../graph/src/graph-node.ts
3368
+ var GraphNode = class _GraphNode {
3369
+ id;
3370
+ type;
3371
+ key;
3372
+ props;
3373
+ events;
3374
+ stateRefs;
3375
+ children;
3376
+ parent;
3377
+ constructor(type, options = {}) {
3378
+ this.type = type;
3379
+ this.id = options.id ?? generateNodeId(type);
3380
+ this.key = options.key;
3381
+ this.props = options.props ?? {};
3382
+ this.events = options.events ?? [];
3383
+ this.stateRefs = options.stateRefs ?? [];
3384
+ this.children = [];
3385
+ this.parent = null;
3386
+ }
3387
+ // ── Child management ────────────────────────────────────────────────────────
3388
+ appendChild(child) {
3389
+ if (child.parent !== null) {
3390
+ child.parent.removeChild(child);
3391
+ }
3392
+ child.parent = this;
3393
+ this.children.push(child);
3394
+ }
3395
+ insertBefore(child, reference) {
3396
+ const idx = this.children.indexOf(reference);
3397
+ if (idx === -1) {
3398
+ this.appendChild(child);
3399
+ return;
3400
+ }
3401
+ if (child.parent !== null) {
3402
+ child.parent.removeChild(child);
3403
+ }
3404
+ child.parent = this;
3405
+ this.children.splice(idx, 0, child);
3406
+ }
3407
+ removeChild(child) {
3408
+ const idx = this.children.indexOf(child);
3409
+ if (idx === -1) return;
3410
+ this.children.splice(idx, 1);
3411
+ child.parent = null;
3412
+ }
3413
+ replaceChild(newChild, oldChild) {
3414
+ const idx = this.children.indexOf(oldChild);
3415
+ if (idx === -1) {
3416
+ throw new Error(`GraphNode.replaceChild: oldChild is not a child of this node`);
3417
+ }
3418
+ if (newChild.parent !== null) {
3419
+ newChild.parent.removeChild(newChild);
3420
+ }
3421
+ oldChild.parent = null;
3422
+ newChild.parent = this;
3423
+ this.children.splice(idx, 1, newChild);
3424
+ }
3425
+ // ── Prop helpers ────────────────────────────────────────────────────────────
3426
+ setProp(key, value) {
3427
+ this.props = { ...this.props, [key]: value };
3428
+ }
3429
+ getProp(key) {
3430
+ return this.props[key];
3431
+ }
3432
+ // ── Event helpers ───────────────────────────────────────────────────────────
3433
+ addEvent(descriptor) {
3434
+ this.events.push(descriptor);
3435
+ }
3436
+ removeEvent(type) {
3437
+ this.events = this.events.filter((e) => e.type !== type);
3438
+ }
3439
+ // ── Queries ─────────────────────────────────────────────────────────────────
3440
+ get isLeaf() {
3441
+ return this.children.length === 0;
3442
+ }
3443
+ get depth() {
3444
+ let d = 0;
3445
+ let node = this.parent;
3446
+ while (node !== null) {
3447
+ d++;
3448
+ node = node.parent;
3449
+ }
3450
+ return d;
3451
+ }
3452
+ get root() {
3453
+ let node = this;
3454
+ while (node.parent !== null) {
3455
+ node = node.parent;
3456
+ }
3457
+ return node;
3458
+ }
3459
+ /** Shallow clone — does not clone children. */
3460
+ shallowClone() {
3461
+ const opts = {
3462
+ props: { ...this.props },
3463
+ events: [...this.events],
3464
+ stateRefs: [...this.stateRefs]
3465
+ };
3466
+ if (this.key !== void 0) opts.key = this.key;
3467
+ return new _GraphNode(this.type, opts);
3468
+ }
3469
+ };
3470
+
3471
+ // ../graph/src/graph.ts
3472
+ var ApplicationGraph = class {
3473
+ root;
3474
+ name;
3475
+ version;
3476
+ _nodeIndex = /* @__PURE__ */ new Map();
3477
+ /** Handler registry — maps handlerKey → actual function */
3478
+ handlers = /* @__PURE__ */ new Map();
3479
+ constructor(options) {
3480
+ this.name = options.name;
3481
+ this.version = options.version ?? "0.0.1";
3482
+ this.root = new GraphNode("application", { props: { name: options.name } });
3483
+ this._nodeIndex.set(this.root.id, this.root);
3484
+ }
3485
+ // ── Node creation & attachment ────────────────────────────────────────────
3486
+ createNode(type, options = {}) {
3487
+ const nodeOpts = {};
3488
+ if (options.key !== void 0) nodeOpts.key = options.key;
3489
+ if (options.props !== void 0) nodeOpts.props = options.props;
3490
+ const node = new GraphNode(type, nodeOpts);
3491
+ this._nodeIndex.set(node.id, node);
3492
+ if (options.parent !== void 0) {
3493
+ options.parent.appendChild(node);
3494
+ }
3495
+ return node;
3496
+ }
3497
+ attachNode(node, parent) {
3498
+ this._nodeIndex.set(node.id, node);
3499
+ parent.appendChild(node);
3500
+ }
3501
+ detachNode(node) {
3502
+ if (node.parent !== null) {
3503
+ node.parent.removeChild(node);
3504
+ }
3505
+ this._removeFromIndex(node);
3506
+ }
3507
+ _removeFromIndex(node) {
3508
+ this._nodeIndex.delete(node.id);
3509
+ this._unregisterNodeHandlers(node);
3510
+ for (const child of node.children) {
3511
+ this._removeFromIndex(child);
3512
+ }
3513
+ }
3514
+ /**
3515
+ * Remove every handler-registry entry owned by a single node. A node owns:
3516
+ * - one entry per event descriptor (its `handlerKey`),
3517
+ * - one `__signal__<signalId>` entry per state ref (signalIds are namespaced
3518
+ * by node id, so they are never shared between nodes), and
3519
+ * - a `__listbuild__<id>` entry if it is a reactive-list.
3520
+ * Called for every node in a detached subtree so removing list items (or
3521
+ * discarding freshly-built-but-unadopted item subtrees) leaves no stale
3522
+ * registrations behind.
3523
+ */
3524
+ _unregisterNodeHandlers(node) {
3525
+ for (const event of node.events) {
3526
+ this.handlers.delete(event.handlerKey);
3527
+ }
3528
+ for (const ref of node.stateRefs) {
3529
+ this.handlers.delete(`__signal__${ref.signalId}`);
3530
+ }
3531
+ this.handlers.delete(`__listbuild__${node.id}`);
3532
+ this.handlers.delete(`__listplan__${node.id}`);
3533
+ this.handlers.delete(`__overlay__${node.id}`);
3534
+ this.handlers.delete(`__component__${node.id}`);
3535
+ this.handlers.delete(`__transition__${node.id}`);
3536
+ }
3537
+ // ── Handler registry ──────────────────────────────────────────────────────
3538
+ registerHandler(key, fn) {
3539
+ this.handlers.set(key, fn);
3540
+ }
3541
+ getHandler(key) {
3542
+ return this.handlers.get(key);
3543
+ }
3544
+ /** True if a handler is currently registered under `key`. Inspection helper. */
3545
+ hasHandler(key) {
3546
+ return this.handlers.has(key);
3547
+ }
3548
+ /** Number of currently-registered handlers. Inspection helper. */
3549
+ get handlerCount() {
3550
+ return this.handlers.size;
3551
+ }
3552
+ // ── Lookup ────────────────────────────────────────────────────────────────
3553
+ findById(id) {
3554
+ return this._nodeIndex.get(id);
3555
+ }
3556
+ findAll(predicate) {
3557
+ const results = [];
3558
+ this._walk(this.root, (node) => {
3559
+ if (predicate(node)) results.push(node);
3560
+ });
3561
+ return results;
3562
+ }
3563
+ findByType(type) {
3564
+ return this.findAll((n) => n.type === type);
3565
+ }
3566
+ // ── Traversal ─────────────────────────────────────────────────────────────
3567
+ walk(visitor) {
3568
+ this._walk(this.root, visitor, 0);
3569
+ }
3570
+ _walk(node, visitor, depth = 0) {
3571
+ visitor(node, depth);
3572
+ for (const child of node.children) {
3573
+ this._walk(child, visitor, depth + 1);
3574
+ }
3575
+ }
3576
+ get nodeCount() {
3577
+ return this._nodeIndex.size;
3578
+ }
3579
+ // ── Validation ────────────────────────────────────────────────────────────
3580
+ validate() {
3581
+ const dc = new DiagnosticCollector();
3582
+ this.walk((node) => {
3583
+ for (const event of node.events) {
3584
+ if (!this.handlers.has(event.handlerKey)) {
3585
+ dc.warn(
3586
+ "GRAPH_MISSING_HANDLER",
3587
+ `Node "${node.id}" references handler "${event.handlerKey}" which is not registered`,
3588
+ { nodeId: node.id }
3589
+ );
3590
+ }
3591
+ }
3592
+ if (node.type === "page" && node.parent?.type !== "application") {
3593
+ dc.error(
3594
+ "GRAPH_PAGE_DEPTH",
3595
+ `Page node "${node.id}" must be a direct child of the application root`,
3596
+ { nodeId: node.id }
3597
+ );
3598
+ }
3599
+ });
3600
+ return dc;
3601
+ }
3602
+ // ── Serialization ─────────────────────────────────────────────────────────
3603
+ serialize() {
3604
+ return {
3605
+ name: this.name,
3606
+ version: this.version,
3607
+ root: this._serializeNode(this.root)
3608
+ };
3609
+ }
3610
+ _serializeNode(node) {
3611
+ const result = {
3612
+ id: node.id,
3613
+ type: node.type,
3614
+ key: node.key,
3615
+ props: node.props,
3616
+ events: node.events,
3617
+ stateRefs: node.stateRefs,
3618
+ children: node.children.map((c) => this._serializeNode(c))
3619
+ };
3620
+ return result;
3621
+ }
3622
+ };
3623
+
3624
+ // ../dsl/src/dsl.ts
3625
+ var StreetApp = class {
3626
+ _graph;
3627
+ _builder;
3628
+ constructor(options) {
3629
+ const graphOpts = { name: options.name };
3630
+ if (options.version !== void 0) graphOpts.version = options.version;
3631
+ this._graph = new ApplicationGraph(graphOpts);
3632
+ this._builder = new AppBuilder(this._graph);
3633
+ }
3634
+ page(key, builder) {
3635
+ this._builder.page(key, builder);
3636
+ return this;
3637
+ }
3638
+ /** Compile to ApplicationGraph — validates and returns the graph. */
3639
+ build() {
3640
+ const dc = this._graph.validate();
3641
+ dc.throwIfErrors();
3642
+ return this._graph;
3643
+ }
3644
+ /** Access graph before building (useful for inspection). */
3645
+ get graph() {
3646
+ return this._graph;
3647
+ }
3648
+ };
3649
+ var streetui = {
3650
+ app(options) {
3651
+ return new StreetApp(options);
3652
+ }
3653
+ };
3654
+
3655
+ // ../testing/src/component.ts
3656
+ var COMPONENT_ATTR = "data-streetui-component";
3657
+ function renderComponent(def, props, children) {
3658
+ const app = streetui.app({ name: `test:${def.name}` });
3659
+ app.page("host", (page) => {
3660
+ page.component("root", def, props, children);
3661
+ });
3662
+ const result = render(app);
3663
+ const component = result.container.querySelector(`[${COMPONENT_ATTR}]`);
3664
+ if (component === null) {
3665
+ throw new Error(`[StreetUI Testing] renderComponent: no component element rendered for "${def.name}"`);
3666
+ }
3667
+ return { ...result, component };
3668
+ }
3669
+ function hydrateComponent(build, options = {}) {
3670
+ return renderServerThenHydrate(() => {
3671
+ const { def, props, children } = build();
3672
+ const app = streetui.app({ name: `test:${def.name}` });
3673
+ app.page("host", (page) => page.component("root", def, props, children));
3674
+ return app;
3675
+ }, options);
3676
+ }
3677
+ function findAllComponents(container, name) {
3678
+ const selector = name === void 0 ? `[${COMPONENT_ATTR}]` : `[${COMPONENT_ATTR}="${name}"]`;
3679
+ return Array.from(container.querySelectorAll(selector));
3680
+ }
3681
+ function findComponent(container, name) {
3682
+ const matches = findAllComponents(container, name);
3683
+ const named = name !== void 0 ? ` named "${name}"` : "";
3684
+ if (matches.length === 0) {
3685
+ throw new Error(`[StreetUI Testing] No component${named} found`);
3686
+ }
3687
+ if (matches.length > 1) {
3688
+ throw new Error(
3689
+ `[StreetUI Testing] Found ${matches.length} components${named} \u2014 pass a name to disambiguate`
3690
+ );
3691
+ }
3692
+ return matches[0];
3693
+ }
3694
+ function getComponentName(el) {
3695
+ return el.getAttribute(COMPONENT_ATTR);
3696
+ }
3697
+ function trigger(el, type, init = {}) {
3698
+ const base = { bubbles: true, cancelable: true, ...init };
3699
+ let event;
3700
+ if (type.startsWith("key")) {
3701
+ event = new KeyboardEvent(type, base);
3702
+ } else if (type.startsWith("mouse") || type === "click" || type === "dblclick") {
3703
+ event = new MouseEvent(type, base);
3704
+ } else if (type === "input" || type === "change") {
3705
+ event = new Event(type, base);
3706
+ } else {
3707
+ event = new Event(type, base);
3708
+ }
3709
+ el.dispatchEvent(event);
3710
+ }
3711
+
2301
3712
  // src/version.ts
2302
- var VERSION = "1.7.0";
3713
+ var VERSION = "1.9.0";
2303
3714
  export {
2304
3715
  VERSION,
2305
3716
  analyzeGraph,
3717
+ blur,
3718
+ clickOutside,
3719
+ closeOverlay,
2306
3720
  findAllByRole,
3721
+ findAllComponents,
2307
3722
  findByRole,
2308
3723
  findByText,
3724
+ findComponent,
2309
3725
  flushUpdates,
3726
+ focus,
2310
3727
  formatInspection,
3728
+ getComponentName,
3729
+ hydrateComponent,
2311
3730
  inspectCompilation,
3731
+ openOverlay,
3732
+ pressKey,
2312
3733
  render,
3734
+ renderComponent,
2313
3735
  renderOnce,
2314
3736
  renderServerThenHydrate,
2315
- waitFor
3737
+ trigger,
3738
+ waitFor,
3739
+ waitForTransition
2316
3740
  };
2317
3741
  //# sourceMappingURL=testing.js.map