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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "1.7.0";
2
+ var VERSION = "1.9.0";
3
3
 
4
4
  // ../state/src/signal.ts
5
5
  var _activeConsumer = null;
@@ -354,6 +354,9 @@ function a11yIds(base) {
354
354
  description: `${token}-description`,
355
355
  error: `${token}-error`,
356
356
  title: `${token}-title`,
357
+ trigger: `${token}-trigger`,
358
+ controls: `${token}-controls`,
359
+ owns: `${token}-owns`,
357
360
  id: (suffix) => `${token}-${toIdToken(suffix)}`
358
361
  };
359
362
  }
@@ -803,6 +806,8 @@ var ApplicationGraph = class {
803
806
  this.handlers.delete(`__listbuild__${node.id}`);
804
807
  this.handlers.delete(`__listplan__${node.id}`);
805
808
  this.handlers.delete(`__overlay__${node.id}`);
809
+ this.handlers.delete(`__component__${node.id}`);
810
+ this.handlers.delete(`__transition__${node.id}`);
806
811
  }
807
812
  // ── Handler registry ──────────────────────────────────────────────────────
808
813
  registerHandler(key, fn) {
@@ -891,6 +896,47 @@ var ApplicationGraph = class {
891
896
  }
892
897
  };
893
898
 
899
+ // ../dsl/src/transition.ts
900
+ function classes(value) {
901
+ if (value === void 0) return [];
902
+ const out = [];
903
+ for (const token of value.split(/\s+/)) {
904
+ if (token.length > 0 && !out.includes(token)) out.push(token);
905
+ }
906
+ return out;
907
+ }
908
+ function merge(a, b) {
909
+ const out = [...a];
910
+ for (const token of b) if (!out.includes(token)) out.push(token);
911
+ return out;
912
+ }
913
+ function resolveTransition(config) {
914
+ const n = config.name;
915
+ const enterActive = merge(
916
+ classes(config.enter),
917
+ classes(config.enterActive ?? (n !== void 0 ? `${n}-enter-active` : void 0))
918
+ );
919
+ const leaveActive = merge(
920
+ classes(config.leave),
921
+ classes(config.leaveActive ?? (n !== void 0 ? `${n}-leave-active` : void 0))
922
+ );
923
+ return {
924
+ enterActive,
925
+ enterFrom: classes(config.enterFrom ?? (n !== void 0 ? `${n}-enter-from` : void 0)),
926
+ enterTo: classes(config.enterTo ?? (n !== void 0 ? `${n}-enter-to` : void 0)),
927
+ leaveActive,
928
+ leaveFrom: classes(config.leaveFrom ?? (n !== void 0 ? `${n}-leave-from` : void 0)),
929
+ leaveTo: classes(config.leaveTo ?? (n !== void 0 ? `${n}-leave-to` : void 0)),
930
+ appear: config.appear ?? false,
931
+ duration: config.duration ?? 1e3
932
+ };
933
+ }
934
+ function isTransitionConfig(value) {
935
+ if (value === null || typeof value !== "object") return false;
936
+ const o = value;
937
+ return typeof o["name"] === "string" || typeof o["enter"] === "string" || typeof o["enterActive"] === "string" || typeof o["enterFrom"] === "string" || typeof o["leave"] === "string" || typeof o["leaveActive"] === "string" || typeof o["leaveFrom"] === "string";
938
+ }
939
+
894
940
  // ../dsl/src/builders.ts
895
941
  function isSignal(v) {
896
942
  return v !== null && typeof v === "object" && typeof v["get"] === "function" && typeof v["subscribe"] === "function";
@@ -918,6 +964,18 @@ function applyA11yProps(props, options) {
918
964
  if (options.ariaInvalid !== void 0) props["aria-invalid"] = String(options.ariaInvalid);
919
965
  if (options.ariaRequired !== void 0) props["aria-required"] = String(options.ariaRequired);
920
966
  if (options.ariaModal !== void 0) props["aria-modal"] = String(options.ariaModal);
967
+ if (options.ariaOwns !== void 0) props["aria-owns"] = options.ariaOwns;
968
+ if (options.ariaActiveDescendant !== void 0) props["aria-activedescendant"] = options.ariaActiveDescendant;
969
+ if (options.ariaHasPopup !== void 0) props["aria-haspopup"] = String(options.ariaHasPopup);
970
+ if (options.ariaSelected !== void 0) props["aria-selected"] = String(options.ariaSelected);
971
+ }
972
+ function registerTransition(graph, node, config) {
973
+ if (config === void 0) return;
974
+ const resolved = resolveTransition(config);
975
+ graph.registerHandler(
976
+ `__transition__${node.id}`,
977
+ () => resolved
978
+ );
921
979
  }
922
980
  function containerProps(options) {
923
981
  const props = {};
@@ -978,6 +1036,7 @@ var OVERLAY_KINDS = {
978
1036
  role: "menu",
979
1037
  modal: false,
980
1038
  takesFocus: true,
1039
+ menu: true,
981
1040
  ariaModal: false,
982
1041
  defaultCloseOnEscape: true,
983
1042
  defaultRestoreFocus: true
@@ -1110,6 +1169,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1110
1169
  parent: this._node,
1111
1170
  props: containerProps(options)
1112
1171
  });
1172
+ registerTransition(this._graph, node, options.transition);
1113
1173
  builder(new SectionBuilderImpl(node, this._graph));
1114
1174
  }
1115
1175
  container(key, builder, options = {}) {
@@ -1118,6 +1178,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1118
1178
  parent: this._node,
1119
1179
  props: containerProps(options)
1120
1180
  });
1181
+ registerTransition(this._graph, node, options.transition);
1121
1182
  builder(new ContainerBuilderImpl(node, this._graph));
1122
1183
  }
1123
1184
  list(key, builder, options = {}) {
@@ -1126,6 +1187,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1126
1187
  parent: this._node,
1127
1188
  props: containerProps(options)
1128
1189
  });
1190
+ registerTransition(this._graph, node, options.transition);
1129
1191
  builder(new ListBuilderImpl(node, this._graph));
1130
1192
  }
1131
1193
  listOf(key, items, renderItem, options = {}) {
@@ -1135,6 +1197,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1135
1197
  parent: this._node,
1136
1198
  props: containerProps(options)
1137
1199
  });
1200
+ registerTransition(graph, node, options.transition);
1138
1201
  const signalId = `${node.id}:items`;
1139
1202
  node.stateRefs.push({ signalId, propKey: "items" });
1140
1203
  graph.registerHandler(`__signal__${signalId}`, items);
@@ -1156,6 +1219,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1156
1219
  }
1157
1220
  });
1158
1221
  renderItem(item, index, new ContainerBuilderImpl(itemNode, graph));
1222
+ registerTransition(graph, itemNode, options.itemTransition);
1159
1223
  return itemNode;
1160
1224
  };
1161
1225
  const buildPlan = (raw) => {
@@ -1187,6 +1251,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1187
1251
  parent: this._node,
1188
1252
  props
1189
1253
  });
1254
+ registerTransition(this._graph, node, options.transition);
1190
1255
  if (options.onSubmit !== void 0) {
1191
1256
  const handlerKey = `submit:${node.id}`;
1192
1257
  this._graph.registerHandler(handlerKey, options.onSubmit);
@@ -1194,18 +1259,20 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1194
1259
  }
1195
1260
  builder(new FormBuilderImpl(node, this._graph));
1196
1261
  }
1197
- when(condition, builder, elseBuilder) {
1262
+ when(condition, builder, elseBuilder, options = {}) {
1198
1263
  const graph = this._graph;
1199
1264
  const node = graph.createNode("conditional", {
1200
1265
  parent: this._node,
1201
1266
  props: containerProps({})
1202
1267
  });
1268
+ const branchTransition = options.transition !== void 0 && options.appear === true ? { ...options.transition, appear: true } : options.transition;
1203
1269
  const buildBranch = (build, tag) => {
1204
1270
  const branchKey = `when-${tag}:${node.id}`;
1205
1271
  const branch = graph.createNode("container", {
1206
1272
  key: branchKey,
1207
1273
  props: { key: branchKey }
1208
1274
  });
1275
+ registerTransition(graph, branch, branchTransition);
1209
1276
  build(new ContainerBuilderImpl(branch, graph));
1210
1277
  return branch;
1211
1278
  };
@@ -1300,13 +1367,24 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1300
1367
  ...options.ariaDescribedBy !== void 0 ? { ariaDescribedBy: options.ariaDescribedBy } : {}
1301
1368
  };
1302
1369
  const portalBuilder = new ContainerBuilderImpl(portalNode, graph);
1303
- portalBuilder.when(openSignal, (panelHost) => {
1304
- panelHost.container(`${key}__panel`, builder, panelOptions);
1305
- });
1370
+ portalBuilder.when(
1371
+ openSignal,
1372
+ (panelHost) => {
1373
+ panelHost.container(`${key}__panel`, builder, panelOptions);
1374
+ },
1375
+ void 0,
1376
+ // Overlay open/close rides the panel's `when`; a transition animates the
1377
+ // panel in on open and — via the reconciler's deferred-leave — plays the
1378
+ // leave before the panel is removed (§10). Focus is restored at close-
1379
+ // request time (see wireOverlayBehavior), so it never stays trapped inside
1380
+ // a panel that is animating away.
1381
+ options.transition !== void 0 ? { transition: options.transition } : {}
1382
+ );
1306
1383
  const descriptor = {
1307
1384
  open: openSignal,
1308
1385
  modal: kind.modal,
1309
1386
  takesFocus: kind.takesFocus,
1387
+ menu: kind.menu ?? false,
1310
1388
  closeOnEscape: options.closeOnEscape ?? kind.defaultCloseOnEscape,
1311
1389
  restoreFocus: options.restoreFocus ?? kind.defaultRestoreFocus,
1312
1390
  ...options.initialFocusId !== void 0 ? { initialFocusId: options.initialFocusId } : {},
@@ -1332,6 +1410,63 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1332
1410
  toast(key, options, builder) {
1333
1411
  this._overlay(OVERLAY_KINDS.toast, key, options, builder);
1334
1412
  }
1413
+ // ── Components ────────────────────────────────────────────────────────────
1414
+ /**
1415
+ * Instantiate a reusable component (§3–§9). Creates a `'component'` node
1416
+ * (rendered as a `<div>` wrapper — preserves the one-node/one-element
1417
+ * positional-hydration invariant), then runs `def.setup(props, ctx)`
1418
+ * synchronously to obtain the render function and fills the component's own
1419
+ * container scope with it — structurally identical to `container`/
1420
+ * `errorBoundary`. Cleanups the setup registers via `ctx.effect`/
1421
+ * `ctx.onCleanup` are collected into a closure and exposed to the renderer
1422
+ * through a `__component__<id>` handler (mirroring `__overlay__`); the mount/
1423
+ * hydrate paths read it and route each into `NodeInstance.trackCleanup`, so
1424
+ * teardown runs (children-first) when the component leaves the graph.
1425
+ *
1426
+ * `setup` runs once per instance here at build time. When this component sits
1427
+ * inside a keyed list / conditional, a rebuild disposes the old instance
1428
+ * (running its cleanups + pruning its `__component__` entry) and re-runs this
1429
+ * method for the new node — so re-invocation is safe and leak-free.
1430
+ */
1431
+ component(key, def, props, children) {
1432
+ const graph = this._graph;
1433
+ const node = graph.createNode("component", {
1434
+ key,
1435
+ parent: this._node,
1436
+ // `data-streetui-component` is a non-underscore prop, so it reaches the
1437
+ // DOM as an attribute and is visible to DevTools (§21) — unlike the
1438
+ // internal `_`-prefixed metadata the renderer hides.
1439
+ props: { key, "data-streetui-component": def.name }
1440
+ });
1441
+ const cleanups = [];
1442
+ const ctx = {
1443
+ key,
1444
+ onCleanup(fn) {
1445
+ cleanups.push(fn);
1446
+ },
1447
+ effect(fn) {
1448
+ cleanups.push(effect(fn));
1449
+ },
1450
+ renderChildren(content) {
1451
+ if (children !== void 0) children(content);
1452
+ }
1453
+ };
1454
+ let render;
1455
+ try {
1456
+ render = def.setup(props, ctx);
1457
+ render(new ContainerBuilderImpl(node, graph));
1458
+ } catch (err) {
1459
+ queueMicrotask(() => {
1460
+ throw err;
1461
+ });
1462
+ }
1463
+ if (cleanups.length > 0) {
1464
+ graph.registerHandler(
1465
+ `__component__${node.id}`,
1466
+ () => cleanups
1467
+ );
1468
+ }
1469
+ }
1335
1470
  };
1336
1471
  var SectionBuilderImpl = class extends ContainerBuilderBase {
1337
1472
  };
@@ -1346,6 +1481,7 @@ var ListBuilderImpl = class extends ContentBuilderBase {
1346
1481
  parent: this._node,
1347
1482
  props: containerProps(options)
1348
1483
  });
1484
+ registerTransition(this._graph, node, options.transition);
1349
1485
  builder(new ContainerBuilderImpl(node, this._graph));
1350
1486
  }
1351
1487
  };
@@ -1365,6 +1501,18 @@ var AppBuilder = class {
1365
1501
  }
1366
1502
  };
1367
1503
 
1504
+ // ../dsl/src/component.ts
1505
+ function component(setup, options = {}) {
1506
+ return {
1507
+ __streetui_component: true,
1508
+ name: options.name ?? setup.name ?? "Component",
1509
+ setup
1510
+ };
1511
+ }
1512
+ function isComponentDefinition(value) {
1513
+ return value !== null && typeof value === "object" && value.__streetui_component === true;
1514
+ }
1515
+
1368
1516
  // ../dsl/src/dsl.ts
1369
1517
  var StreetApp = class {
1370
1518
  _graph;
@@ -2428,18 +2576,25 @@ function trapFocus(dom, container) {
2428
2576
  dom.addEventListener(container, "keydown", onKeydown);
2429
2577
  return () => dom.removeEventListener(container, "keydown", onKeydown);
2430
2578
  }
2579
+ var containmentStack = [];
2431
2580
  function containFocus(dom, container) {
2432
2581
  const body = dom.body();
2433
2582
  if (body === null) return () => {
2434
2583
  };
2584
+ containmentStack.push(container);
2435
2585
  const onFocusIn = (event) => {
2586
+ if (containmentStack[containmentStack.length - 1] !== container) return;
2436
2587
  const target = event.target;
2437
2588
  if (target !== null && !dom.contains(container, target)) {
2438
2589
  focusFirst(dom, container);
2439
2590
  }
2440
2591
  };
2441
2592
  dom.addEventListener(body, "focusin", onFocusIn);
2442
- return () => dom.removeEventListener(body, "focusin", onFocusIn);
2593
+ return () => {
2594
+ dom.removeEventListener(body, "focusin", onFocusIn);
2595
+ const index = containmentStack.lastIndexOf(container);
2596
+ if (index !== -1) containmentStack.splice(index, 1);
2597
+ };
2443
2598
  }
2444
2599
  function onEscape(dom, target, handler) {
2445
2600
  const onKeydown = (event) => {
@@ -2448,6 +2603,84 @@ function onEscape(dom, target, handler) {
2448
2603
  dom.addEventListener(target, "keydown", onKeydown);
2449
2604
  return () => dom.removeEventListener(target, "keydown", onKeydown);
2450
2605
  }
2606
+ function rovingMenu(dom, container, selector = FOCUSABLE_SELECTOR) {
2607
+ const onKeydown = (event) => {
2608
+ const key = event.key;
2609
+ const isActivate = key === "Enter" || key === " " || key === "Spacebar";
2610
+ const isMove = key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End";
2611
+ if (!isActivate && !isMove) return;
2612
+ const items = getFocusable(dom, container, selector);
2613
+ if (items.length === 0) return;
2614
+ const active = dom.activeElement();
2615
+ const index = active === null ? -1 : items.indexOf(active);
2616
+ if (isActivate) {
2617
+ if (index < 0) return;
2618
+ event.preventDefault();
2619
+ items[index].click?.();
2620
+ return;
2621
+ }
2622
+ event.preventDefault();
2623
+ let next;
2624
+ if (key === "Home") next = 0;
2625
+ else if (key === "End") next = items.length - 1;
2626
+ else if (key === "ArrowDown") next = index < 0 ? 0 : (index + 1) % items.length;
2627
+ else next = index <= 0 ? items.length - 1 : index - 1;
2628
+ dom.focus(items[next]);
2629
+ };
2630
+ dom.addEventListener(container, "keydown", onKeydown);
2631
+ return () => dom.removeEventListener(container, "keydown", onKeydown);
2632
+ }
2633
+
2634
+ // ../dom/src/live-region.ts
2635
+ var VISUALLY_HIDDEN = "position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;";
2636
+ function makeRegion(dom, politeness) {
2637
+ const el = dom.createElement("div");
2638
+ dom.setAttribute(el, "aria-live", politeness);
2639
+ dom.setAttribute(el, "aria-atomic", "true");
2640
+ dom.setAttribute(el, "role", politeness === "assertive" ? "alert" : "status");
2641
+ dom.setAttribute(el, "data-streetui-live", politeness);
2642
+ dom.setAttribute(el, "style", VISUALLY_HIDDEN);
2643
+ return el;
2644
+ }
2645
+ function createAnnouncer(dom) {
2646
+ const body = dom.body();
2647
+ if (body === null) {
2648
+ return { announce() {
2649
+ }, clear() {
2650
+ }, destroy() {
2651
+ } };
2652
+ }
2653
+ const polite = makeRegion(dom, "polite");
2654
+ const assertive = makeRegion(dom, "assertive");
2655
+ dom.appendChild(body, polite);
2656
+ dom.appendChild(body, assertive);
2657
+ let destroyed = false;
2658
+ const write = (region, message) => {
2659
+ dom.setTextContent(region, "");
2660
+ void Promise.resolve().then(() => {
2661
+ if (!destroyed) dom.setTextContent(region, message);
2662
+ });
2663
+ };
2664
+ return {
2665
+ announce(message, options) {
2666
+ if (destroyed) return;
2667
+ write(options?.assertive === true ? assertive : polite, message);
2668
+ },
2669
+ clear() {
2670
+ if (destroyed) return;
2671
+ dom.setTextContent(polite, "");
2672
+ dom.setTextContent(assertive, "");
2673
+ },
2674
+ destroy() {
2675
+ if (destroyed) return;
2676
+ destroyed = true;
2677
+ for (const region of [polite, assertive]) {
2678
+ const parent = dom.parentNode(region);
2679
+ if (parent !== null) dom.removeChild(parent, region);
2680
+ }
2681
+ }
2682
+ };
2683
+ }
2451
2684
 
2452
2685
  // ../renderer/src/render-context.ts
2453
2686
  function createRenderContext(dom, graph, container, hydrationDiagnostics, staticHTML) {
@@ -2659,7 +2892,7 @@ function patchNode(ctx, graphNode, propKey, newValue) {
2659
2892
  }
2660
2893
 
2661
2894
  // ../renderer/src/reconciliation.ts
2662
- function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
2895
+ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn, hooks) {
2663
2896
  const oldByKey = /* @__PURE__ */ new Map();
2664
2897
  for (const inst of oldInstances) {
2665
2898
  const key = inst.graphNode.key ?? inst.graphNode.id;
@@ -2680,28 +2913,33 @@ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
2680
2913
  }
2681
2914
  newInstances.push(existing);
2682
2915
  } else {
2683
- const inst = mountFn(newNode, parentDom);
2684
- newInstances.push(inst);
2916
+ const reclaimed = hooks?.takeLeaving(key);
2917
+ if (reclaimed !== void 0) {
2918
+ patchExistingInstance(ctx, reclaimed, newNode);
2919
+ reconcileItemChildren(ctx, reclaimed, newNode, mountFn);
2920
+ hooks?.onEnter(reclaimed);
2921
+ newInstances.push(reclaimed);
2922
+ } else {
2923
+ const inst = mountFn(newNode, parentDom);
2924
+ hooks?.onEnter(inst);
2925
+ newInstances.push(inst);
2926
+ }
2685
2927
  }
2686
2928
  }
2687
2929
  const removed = [];
2688
2930
  for (const inst of oldInstances) {
2689
2931
  const key = inst.graphNode.key ?? inst.graphNode.id;
2690
- if (!usedKeys.has(key)) {
2691
- removed.push(inst);
2692
- }
2693
- }
2694
- for (const inst of removed) {
2932
+ if (usedKeys.has(key)) continue;
2933
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
2695
2934
  const parent = ctx.dom.parentNode(inst.domNode);
2696
- if (parent !== null) {
2697
- ctx.dom.removeChild(parent, inst.domNode);
2698
- }
2935
+ if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
2699
2936
  inst.dispose();
2937
+ removed.push(inst);
2700
2938
  }
2701
2939
  reorderDom(ctx, parentDom, newInstances);
2702
2940
  return { instances: newInstances, removed };
2703
2941
  }
2704
- function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
2942
+ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn, hooks) {
2705
2943
  const oldByKey = /* @__PURE__ */ new Map();
2706
2944
  for (const inst of oldInstances) {
2707
2945
  oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
@@ -2728,21 +2966,34 @@ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
2728
2966
  }
2729
2967
  newInstances.push(existing);
2730
2968
  } else {
2731
- const freshNode = entry.build();
2732
- built.push(freshNode);
2733
- const inst = mountFn(freshNode, parentDom);
2734
- newInstances.push(inst);
2969
+ const reclaimed = hooks?.takeLeaving(entry.key);
2970
+ if (reclaimed !== void 0) {
2971
+ const freshNode = entry.build();
2972
+ built.push(freshNode);
2973
+ patchExistingInstance(ctx, reclaimed, freshNode);
2974
+ reconcileItemChildren(ctx, reclaimed, freshNode, mountFn);
2975
+ reclaimed.graphNode.setProp("_sig", freshNode.getProp("_sig"));
2976
+ reclaimed.graphNode.setProp("_item", entry.item);
2977
+ hooks?.onEnter(reclaimed);
2978
+ newInstances.push(reclaimed);
2979
+ } else {
2980
+ const freshNode = entry.build();
2981
+ built.push(freshNode);
2982
+ const inst = mountFn(freshNode, parentDom);
2983
+ hooks?.onEnter(inst);
2984
+ newInstances.push(inst);
2985
+ }
2735
2986
  }
2736
2987
  }
2737
2988
  const removed = [];
2738
2989
  for (const inst of oldInstances) {
2739
2990
  const key = inst.graphNode.key ?? inst.graphNode.id;
2740
- if (!usedKeys.has(key)) removed.push(inst);
2741
- }
2742
- for (const inst of removed) {
2991
+ if (usedKeys.has(key)) continue;
2992
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
2743
2993
  const parent = ctx.dom.parentNode(inst.domNode);
2744
2994
  if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
2745
2995
  inst.dispose();
2996
+ removed.push(inst);
2746
2997
  }
2747
2998
  reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
2748
2999
  return { instances: newInstances, removed, built };
@@ -2868,6 +3119,187 @@ function patchExistingInstance(ctx, instance, newNode) {
2868
3119
  }
2869
3120
  }
2870
3121
 
3122
+ // ../renderer/src/transition.ts
3123
+ function getResolvedTransition(graph, nodeId) {
3124
+ const fn = graph.getHandler(`__transition__${nodeId}`);
3125
+ return fn === void 0 ? void 0 : fn();
3126
+ }
3127
+ function host() {
3128
+ return globalThis;
3129
+ }
3130
+ function nextFrame(cb) {
3131
+ const h = host();
3132
+ const raf = h.requestAnimationFrame;
3133
+ if (typeof raf === "function") {
3134
+ raf(() => raf(cb));
3135
+ } else {
3136
+ h.setTimeout(cb, 0);
3137
+ }
3138
+ }
3139
+ function splitClass(value) {
3140
+ if (value === null) return [];
3141
+ const out = [];
3142
+ for (const t of value.split(/\s+/)) if (t.length > 0) out.push(t);
3143
+ return out;
3144
+ }
3145
+ function addClasses(dom, el, classes2) {
3146
+ if (classes2.length === 0) return;
3147
+ const current = splitClass(dom.getAttribute(el, "class"));
3148
+ let changed = false;
3149
+ for (const c of classes2) {
3150
+ if (!current.includes(c)) {
3151
+ current.push(c);
3152
+ changed = true;
3153
+ }
3154
+ }
3155
+ if (changed) dom.setAttribute(el, "class", current.join(" "));
3156
+ }
3157
+ function removeClasses(dom, el, classes2) {
3158
+ if (classes2.length === 0) return;
3159
+ const current = splitClass(dom.getAttribute(el, "class"));
3160
+ const next = current.filter((c) => !classes2.includes(c));
3161
+ if (next.length !== current.length) {
3162
+ if (next.length === 0) dom.removeAttribute(el, "class");
3163
+ else dom.setAttribute(el, "class", next.join(" "));
3164
+ }
3165
+ }
3166
+ function startRun(dom, el, active, from, to, duration, onDone) {
3167
+ const h = host();
3168
+ let finished = false;
3169
+ let timer = null;
3170
+ const onEvent = (e) => {
3171
+ if (e.target !== el) return;
3172
+ finish();
3173
+ };
3174
+ const detach = () => {
3175
+ dom.removeEventListener(el, "transitionend", onEvent);
3176
+ dom.removeEventListener(el, "animationend", onEvent);
3177
+ if (timer !== null) {
3178
+ h.clearTimeout(timer);
3179
+ timer = null;
3180
+ }
3181
+ };
3182
+ const finish = () => {
3183
+ if (finished) return;
3184
+ finished = true;
3185
+ detach();
3186
+ removeClasses(dom, el, active);
3187
+ removeClasses(dom, el, to);
3188
+ removeClasses(dom, el, from);
3189
+ onDone();
3190
+ };
3191
+ addClasses(dom, el, from);
3192
+ addClasses(dom, el, active);
3193
+ dom.addEventListener(el, "transitionend", onEvent);
3194
+ dom.addEventListener(el, "animationend", onEvent);
3195
+ timer = h.setTimeout(finish, duration);
3196
+ nextFrame(() => {
3197
+ if (finished) return;
3198
+ removeClasses(dom, el, from);
3199
+ addClasses(dom, el, to);
3200
+ });
3201
+ return {
3202
+ cancel: () => {
3203
+ if (finished) return;
3204
+ finished = true;
3205
+ detach();
3206
+ removeClasses(dom, el, active);
3207
+ removeClasses(dom, el, to);
3208
+ removeClasses(dom, el, from);
3209
+ }
3210
+ };
3211
+ }
3212
+ function runElementTransition(dom, el, rt, phase, onDone) {
3213
+ if (dom.body() === null || !dom.isElement(el)) {
3214
+ onDone();
3215
+ return { cancel: () => {
3216
+ } };
3217
+ }
3218
+ const active = phase === "enter" ? rt.enterActive : rt.leaveActive;
3219
+ const from = phase === "enter" ? rt.enterFrom : rt.leaveFrom;
3220
+ const to = phase === "enter" ? rt.enterTo : rt.leaveTo;
3221
+ return startRun(dom, el, active, from, to, rt.duration, onDone);
3222
+ }
3223
+ var TransitionController = class {
3224
+ constructor(dom, graph, finalize) {
3225
+ this.dom = dom;
3226
+ this.graph = graph;
3227
+ this.finalize = finalize;
3228
+ }
3229
+ leaving = /* @__PURE__ */ new Map();
3230
+ /** True only in a real DOM environment (browser). */
3231
+ get browser() {
3232
+ return this.dom.body() !== null;
3233
+ }
3234
+ keyOf(inst) {
3235
+ return inst.graphNode.key ?? inst.graphNode.id;
3236
+ }
3237
+ resolved(node) {
3238
+ return getResolvedTransition(this.graph, node.id);
3239
+ }
3240
+ /** Run the enter animation for `inst` if it carries a transition (browser only). */
3241
+ enter(inst) {
3242
+ if (!this.browser) return;
3243
+ const rt = this.resolved(inst.graphNode);
3244
+ if (rt === void 0) return;
3245
+ const el = inst.domNode;
3246
+ if (!this.dom.isElement(el)) return;
3247
+ startRun(this.dom, el, rt.enterActive, rt.enterFrom, rt.enterTo, rt.duration, () => {
3248
+ });
3249
+ }
3250
+ /**
3251
+ * Play `appear` for any initial child that opted into it (fresh browser mount
3252
+ * only — hydration must never animate appear, §22, and this is called only on
3253
+ * the mount path).
3254
+ */
3255
+ appear(children) {
3256
+ if (!this.browser) return;
3257
+ for (const child of children) {
3258
+ const rt = this.resolved(child.graphNode);
3259
+ if (rt !== void 0 && rt.appear) this.enter(child);
3260
+ }
3261
+ }
3262
+ hooks() {
3263
+ return {
3264
+ takeLeaving: (key) => {
3265
+ const entry = this.leaving.get(key);
3266
+ if (entry === void 0) return void 0;
3267
+ entry.run.cancel();
3268
+ this.leaving.delete(key);
3269
+ return entry.inst;
3270
+ },
3271
+ beginLeave: (inst) => {
3272
+ if (!this.browser) return false;
3273
+ const rt = this.resolved(inst.graphNode);
3274
+ if (rt === void 0) return false;
3275
+ const el = inst.domNode;
3276
+ if (!this.dom.isElement(el)) return false;
3277
+ const key = this.keyOf(inst);
3278
+ const prior = this.leaving.get(key);
3279
+ if (prior !== void 0) prior.run.cancel();
3280
+ const run = startRun(
3281
+ this.dom,
3282
+ el,
3283
+ rt.leaveActive,
3284
+ rt.leaveFrom,
3285
+ rt.leaveTo,
3286
+ rt.duration,
3287
+ () => {
3288
+ const current = this.leaving.get(key);
3289
+ if (current !== void 0 && current.run === run) {
3290
+ this.leaving.delete(key);
3291
+ this.finalize(inst);
3292
+ }
3293
+ }
3294
+ );
3295
+ this.leaving.set(key, { inst, run });
3296
+ return true;
3297
+ },
3298
+ onEnter: (inst) => this.enter(inst)
3299
+ };
3300
+ }
3301
+ };
3302
+
2871
3303
  // ../renderer/src/mount.ts
2872
3304
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
2873
3305
  "text",
@@ -3020,7 +3452,7 @@ function mountNode(ctx, graphNode, parentDom) {
3020
3452
  instance2.addChild(childInstance);
3021
3453
  }
3022
3454
  dom.appendChild(parentDom, el2);
3023
- wireReactiveList(ctx, graphNode, instance2, el2);
3455
+ wireReactiveList(ctx, graphNode, instance2, el2, true);
3024
3456
  return instance2;
3025
3457
  }
3026
3458
  if (graphNode.type === "portal") {
@@ -3064,6 +3496,7 @@ function mountNode(ctx, graphNode, parentDom) {
3064
3496
  instance.addChild(childInstance);
3065
3497
  }
3066
3498
  dom.appendChild(parentDom, el);
3499
+ if (graphNode.type === "component") wireComponentBehavior(ctx, graphNode, instance);
3067
3500
  return instance;
3068
3501
  }
3069
3502
  function textUpdate(dom, el, textNode) {
@@ -3128,32 +3561,42 @@ function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
3128
3561
  instance.trackCleanup(unsub);
3129
3562
  }
3130
3563
  }
3131
- function wireReactiveList(ctx, graphNode, instance, el) {
3564
+ function wireReactiveList(ctx, graphNode, instance, el, runAppear = false) {
3132
3565
  const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
3133
3566
  const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
3134
3567
  if (plan === void 0 && build === void 0) return;
3568
+ const controller = new TransitionController(ctx.dom, ctx.graph, (leaving) => {
3569
+ const parent = ctx.dom.parentNode(leaving.domNode);
3570
+ if (parent !== null) ctx.dom.removeChild(parent, leaving.domNode);
3571
+ leaving.dispose();
3572
+ forgetInstance(ctx, leaving);
3573
+ ctx.graph.detachNode(leaving.graphNode);
3574
+ });
3575
+ const hooks = controller.hooks();
3576
+ if (runAppear) controller.appear(instance.children);
3135
3577
  for (const stateRef of graphNode.stateRefs) {
3136
3578
  if (stateRef.propKey !== "items") continue;
3137
3579
  const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
3138
3580
  if (sig === void 0 || typeof sig.subscribe !== "function") continue;
3139
3581
  const unsub = sig.subscribe((value) => {
3140
3582
  if (plan !== void 0) {
3141
- reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
3583
+ reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value), hooks);
3142
3584
  } else {
3143
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
3585
+ reconcileReactiveList(ctx, graphNode, instance, el, build(value), hooks);
3144
3586
  }
3145
3587
  });
3146
3588
  instance.trackCleanup(unsub);
3147
3589
  }
3148
3590
  }
3149
- function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
3591
+ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan, hooks) {
3150
3592
  const oldInstances = [...listInstance.children];
3151
3593
  const result = reconcileChildrenByPlan(
3152
3594
  ctx,
3153
3595
  listEl,
3154
3596
  oldInstances,
3155
3597
  plan,
3156
- (node, parent) => mountNode(ctx, node, parent)
3598
+ (node, parent) => mountNode(ctx, node, parent),
3599
+ hooks
3157
3600
  );
3158
3601
  listInstance.children.length = 0;
3159
3602
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -3168,14 +3611,15 @@ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan)
3168
3611
  for (const child of [...listNode.children]) listNode.removeChild(child);
3169
3612
  for (const inst of result.instances) listNode.appendChild(inst.graphNode);
3170
3613
  }
3171
- function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
3614
+ function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes, hooks) {
3172
3615
  const oldInstances = [...listInstance.children];
3173
3616
  const result = reconcileChildren(
3174
3617
  ctx,
3175
3618
  listEl,
3176
3619
  oldInstances,
3177
3620
  newNodes,
3178
- (node, parent) => mountNode(ctx, node, parent)
3621
+ (node, parent) => mountNode(ctx, node, parent),
3622
+ hooks
3179
3623
  );
3180
3624
  listInstance.children.length = 0;
3181
3625
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -3211,11 +3655,14 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
3211
3655
  const onOpenChange = (isOpen) => {
3212
3656
  if (isOpen) {
3213
3657
  if (desc.restoreFocus) saved = saveFocus(dom);
3214
- if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
3215
3658
  if (desc.modal) {
3216
3659
  active.push(trapFocus(dom, target));
3217
3660
  active.push(containFocus(dom, target));
3218
3661
  }
3662
+ if (desc.menu) {
3663
+ active.push(rovingMenu(dom, target));
3664
+ }
3665
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
3219
3666
  if (desc.closeOnEscape && desc.onClose !== void 0) {
3220
3667
  active.push(onEscape(dom, target, desc.onClose));
3221
3668
  }
@@ -3232,6 +3679,11 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
3232
3679
  instance.trackCleanup(teardown);
3233
3680
  if (openSig.peek() === true) onOpenChange(true);
3234
3681
  }
3682
+ function wireComponentBehavior(ctx, graphNode, instance) {
3683
+ const fn = ctx.graph.getHandler(`__component__${graphNode.id}`);
3684
+ if (fn === void 0) return;
3685
+ for (const cleanup of fn()) instance.trackCleanup(cleanup);
3686
+ }
3235
3687
 
3236
3688
  // ../renderer/src/hydration-diagnostics.ts
3237
3689
  function formatHydrationDiagnostic(d) {
@@ -3363,6 +3815,7 @@ function hydrateNode(ctx, graphNode, domNode, path) {
3363
3815
  return instance;
3364
3816
  }
3365
3817
  hydrateChildren(ctx, graphNode, instance, domNode, path);
3818
+ if (graphNode.type === "component") wireComponentBehavior(ctx, graphNode, instance);
3366
3819
  return instance;
3367
3820
  }
3368
3821
  }
@@ -3597,7 +4050,8 @@ function analyzeGraph(graph) {
3597
4050
  const isList = node.type === "reactive-list";
3598
4051
  const isConditional = node.type === "conditional";
3599
4052
  const isPortal = node.type === "portal";
3600
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal;
4053
+ const isComponent = node.type === "component";
4054
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent;
3601
4055
  const isStaticSubtree = isStatic && allChildrenStatic;
3602
4056
  nodes.set(node.id, {
3603
4057
  isStatic,
@@ -3943,16 +4397,38 @@ function mountRouter(router, options) {
3943
4397
  } else {
3944
4398
  outlet = container;
3945
4399
  }
4400
+ const routeTransition = options.transition !== void 0 ? resolveTransition(options.transition) : void 0;
4401
+ const txDom = routeTransition !== void 0 ? new BrowserDOMAdapter() : void 0;
3946
4402
  let active = null;
4403
+ const pendingLeaves = /* @__PURE__ */ new Map();
3947
4404
  let firstRender = hydrateMode;
4405
+ const teardownRoute = (route) => {
4406
+ route.registry.run();
4407
+ route.mounted.unmount();
4408
+ if (route.host !== null && txDom !== void 0) {
4409
+ const parent = txDom.parentNode(route.host);
4410
+ if (parent !== null) txDom.removeChild(parent, route.host);
4411
+ }
4412
+ };
3948
4413
  const disposeActive = () => {
3949
4414
  if (active === null) return;
3950
- active.registry.run();
3951
- active.mounted.unmount();
4415
+ teardownRoute(active);
3952
4416
  active = null;
3953
4417
  };
3954
- const renderRoute = (match) => {
3955
- disposeActive();
4418
+ const wrapOutletChildren = (dom) => {
4419
+ const host2 = dom.createElement("div");
4420
+ dom.setAttribute(host2, "data-streetui-route", "");
4421
+ const moved = [];
4422
+ let child = dom.firstChild(outlet);
4423
+ while (child !== null) {
4424
+ moved.push(child);
4425
+ child = dom.nextSibling(child);
4426
+ }
4427
+ for (const n of moved) dom.appendChild(host2, n);
4428
+ dom.appendChild(outlet, host2);
4429
+ return host2;
4430
+ };
4431
+ const buildRoute = (match, target, hydrate) => {
3956
4432
  const registry = new CleanupRegistry();
3957
4433
  const ctx = {
3958
4434
  path: match.path,
@@ -3965,9 +4441,53 @@ function mountRouter(router, options) {
3965
4441
  routeApp.page("route", (page) => match.route.builder(page, ctx));
3966
4442
  const runtime = createRuntime({ renderer });
3967
4443
  const routeCompiled = compile(routeApp);
3968
- const mounted = firstRender ? runtime.hydrate(routeCompiled, outlet) : runtime.mount(routeCompiled, outlet);
4444
+ const mounted = hydrate ? runtime.hydrate(routeCompiled, target) : runtime.mount(routeCompiled, target);
4445
+ return { registry, mounted, host: null, enterRun: null };
4446
+ };
4447
+ const renderRoute = (match) => {
4448
+ if (routeTransition === void 0 || txDom === void 0) {
4449
+ disposeActive();
4450
+ active = buildRoute(match, outlet, firstRender);
4451
+ firstRender = false;
4452
+ return;
4453
+ }
4454
+ if (active === null) {
4455
+ if (firstRender) {
4456
+ active = buildRoute(match, outlet, true);
4457
+ active.host = wrapOutletChildren(txDom);
4458
+ } else {
4459
+ const host2 = txDom.createElement("div");
4460
+ txDom.setAttribute(host2, "data-streetui-route", "");
4461
+ txDom.appendChild(outlet, host2);
4462
+ active = buildRoute(match, host2, false);
4463
+ active.host = host2;
4464
+ }
4465
+ firstRender = false;
4466
+ return;
4467
+ }
3969
4468
  firstRender = false;
3970
- active = { registry, mounted };
4469
+ const leaving = active;
4470
+ leaving.enterRun?.cancel();
4471
+ leaving.enterRun = null;
4472
+ const leaveHost = leaving.host;
4473
+ const enterHost = txDom.createElement("div");
4474
+ txDom.setAttribute(enterHost, "data-streetui-route", "");
4475
+ txDom.appendChild(outlet, enterHost);
4476
+ const next = buildRoute(match, enterHost, false);
4477
+ next.host = enterHost;
4478
+ next.enterRun = runElementTransition(txDom, enterHost, routeTransition, "enter", () => {
4479
+ next.enterRun = null;
4480
+ });
4481
+ active = next;
4482
+ if (leaveHost !== null) {
4483
+ const run = runElementTransition(txDom, leaveHost, routeTransition, "leave", () => {
4484
+ pendingLeaves.delete(leaving);
4485
+ teardownRoute(leaving);
4486
+ });
4487
+ pendingLeaves.set(leaving, run);
4488
+ } else {
4489
+ teardownRoute(leaving);
4490
+ }
3971
4491
  };
3972
4492
  renderRoute(router.currentRoute.peek());
3973
4493
  const stopRouteSub = router.currentRoute.subscribe((match) => renderRoute(match));
@@ -3995,6 +4515,11 @@ function mountRouter(router, options) {
3995
4515
  unmount() {
3996
4516
  if (interceptLinks) container.removeEventListener("click", onClick);
3997
4517
  stopRouteSub();
4518
+ for (const [route, run] of pendingLeaves) {
4519
+ run.cancel();
4520
+ teardownRoute(route);
4521
+ }
4522
+ pendingLeaves.clear();
3998
4523
  disposeActive();
3999
4524
  shellMounted?.unmount();
4000
4525
  router.destroy();
@@ -4304,6 +4829,55 @@ function nodeTypeStats(graph) {
4304
4829
  });
4305
4830
  return counts;
4306
4831
  }
4832
+ function inspectComponents(graph) {
4833
+ const out = [];
4834
+ graph.walk((node, depth) => {
4835
+ if (node.type !== "component") return;
4836
+ const name = node.props["data-streetui-component"];
4837
+ out.push({
4838
+ id: node.id,
4839
+ key: node.key,
4840
+ name: typeof name === "string" ? name : "Component",
4841
+ depth,
4842
+ childCount: node.children.length
4843
+ });
4844
+ });
4845
+ return out;
4846
+ }
4847
+ function inspectInteractions(graph) {
4848
+ const overlays = [];
4849
+ const transitions = [];
4850
+ graph.walk((node, depth) => {
4851
+ const overlayFn = graph.getHandler(`__overlay__${node.id}`);
4852
+ if (overlayFn !== void 0) {
4853
+ const d = overlayFn();
4854
+ overlays.push({
4855
+ id: node.id,
4856
+ key: node.key,
4857
+ open: d.open.peek(),
4858
+ modal: d.modal,
4859
+ takesFocus: d.takesFocus,
4860
+ menu: d.menu,
4861
+ closeOnEscape: d.closeOnEscape,
4862
+ restoreFocus: d.restoreFocus,
4863
+ depth
4864
+ });
4865
+ }
4866
+ const transitionFn = graph.getHandler(`__transition__${node.id}`);
4867
+ if (transitionFn !== void 0) {
4868
+ const t = transitionFn();
4869
+ transitions.push({
4870
+ id: node.id,
4871
+ key: node.key,
4872
+ nodeType: node.type,
4873
+ duration: t.duration,
4874
+ appear: t.appear,
4875
+ depth
4876
+ });
4877
+ }
4878
+ });
4879
+ return { overlays, transitions };
4880
+ }
4307
4881
 
4308
4882
  // ../devtools/src/application.ts
4309
4883
  function collectPerf(node, distinctSignals, acc) {
@@ -4677,6 +5251,7 @@ export {
4677
5251
  StreetFrameworkError,
4678
5252
  StreetRenderHandle,
4679
5253
  StreetRendererImpl,
5254
+ TransitionController,
4680
5255
  VERSION,
4681
5256
  a11yIds,
4682
5257
  applyNodeProps,
@@ -4687,9 +5262,11 @@ export {
4687
5262
  buttonUpdate,
4688
5263
  compile,
4689
5264
  compileGraph,
5265
+ component,
4690
5266
  consoleDiagnosticSink,
4691
5267
  consoleHydrationDiagnosticSink,
4692
5268
  containFocus,
5269
+ createAnnouncer,
4693
5270
  createApplication,
4694
5271
  createBrowserHistory,
4695
5272
  createContext,
@@ -4724,20 +5301,25 @@ export {
4724
5301
  generateApplicationId,
4725
5302
  generateNodeId,
4726
5303
  getFocusable,
5304
+ getResolvedTransition,
4727
5305
  globalEventBus,
4728
5306
  headingUpdate,
4729
5307
  hydrateGraph,
4730
5308
  inputUpdate,
4731
5309
  inspectApplication,
5310
+ inspectComponents,
4732
5311
  inspectContext,
4733
5312
  inspectForm,
4734
5313
  inspectGraph,
4735
5314
  inspectI18n,
5315
+ inspectInteractions,
4736
5316
  inspectResource,
4737
5317
  inspectRouter,
4738
5318
  inspectSignal,
4739
5319
  interpolate,
4740
5320
  isBatching,
5321
+ isComponentDefinition,
5322
+ isTransitionConfig,
4741
5323
  matchPattern,
4742
5324
  matchRoutes,
4743
5325
  maxLength,
@@ -4766,9 +5348,12 @@ export {
4766
5348
  required,
4767
5349
  resetIdCounter,
4768
5350
  resolveTag,
5351
+ resolveTransition,
4769
5352
  resource,
4770
5353
  restoreFocus,
4771
5354
  routerOutlet,
5355
+ rovingMenu,
5356
+ runElementTransition,
4772
5357
  runValidators,
4773
5358
  saveFocus,
4774
5359
  scheduleImmediate,
@@ -4787,6 +5372,7 @@ export {
4787
5372
  transformGraph,
4788
5373
  trapFocus,
4789
5374
  validateGraph,
5375
+ wireComponentBehavior,
4790
5376
  wireEvents,
4791
5377
  wireOverlayBehavior,
4792
5378
  wireReactiveList,