streetui 1.8.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.8.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
  }
@@ -804,6 +807,7 @@ var ApplicationGraph = class {
804
807
  this.handlers.delete(`__listplan__${node.id}`);
805
808
  this.handlers.delete(`__overlay__${node.id}`);
806
809
  this.handlers.delete(`__component__${node.id}`);
810
+ this.handlers.delete(`__transition__${node.id}`);
807
811
  }
808
812
  // ── Handler registry ──────────────────────────────────────────────────────
809
813
  registerHandler(key, fn) {
@@ -892,6 +896,47 @@ var ApplicationGraph = class {
892
896
  }
893
897
  };
894
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
+
895
940
  // ../dsl/src/builders.ts
896
941
  function isSignal(v) {
897
942
  return v !== null && typeof v === "object" && typeof v["get"] === "function" && typeof v["subscribe"] === "function";
@@ -919,6 +964,18 @@ function applyA11yProps(props, options) {
919
964
  if (options.ariaInvalid !== void 0) props["aria-invalid"] = String(options.ariaInvalid);
920
965
  if (options.ariaRequired !== void 0) props["aria-required"] = String(options.ariaRequired);
921
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
+ );
922
979
  }
923
980
  function containerProps(options) {
924
981
  const props = {};
@@ -979,6 +1036,7 @@ var OVERLAY_KINDS = {
979
1036
  role: "menu",
980
1037
  modal: false,
981
1038
  takesFocus: true,
1039
+ menu: true,
982
1040
  ariaModal: false,
983
1041
  defaultCloseOnEscape: true,
984
1042
  defaultRestoreFocus: true
@@ -1111,6 +1169,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1111
1169
  parent: this._node,
1112
1170
  props: containerProps(options)
1113
1171
  });
1172
+ registerTransition(this._graph, node, options.transition);
1114
1173
  builder(new SectionBuilderImpl(node, this._graph));
1115
1174
  }
1116
1175
  container(key, builder, options = {}) {
@@ -1119,6 +1178,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1119
1178
  parent: this._node,
1120
1179
  props: containerProps(options)
1121
1180
  });
1181
+ registerTransition(this._graph, node, options.transition);
1122
1182
  builder(new ContainerBuilderImpl(node, this._graph));
1123
1183
  }
1124
1184
  list(key, builder, options = {}) {
@@ -1127,6 +1187,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1127
1187
  parent: this._node,
1128
1188
  props: containerProps(options)
1129
1189
  });
1190
+ registerTransition(this._graph, node, options.transition);
1130
1191
  builder(new ListBuilderImpl(node, this._graph));
1131
1192
  }
1132
1193
  listOf(key, items, renderItem, options = {}) {
@@ -1136,6 +1197,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1136
1197
  parent: this._node,
1137
1198
  props: containerProps(options)
1138
1199
  });
1200
+ registerTransition(graph, node, options.transition);
1139
1201
  const signalId = `${node.id}:items`;
1140
1202
  node.stateRefs.push({ signalId, propKey: "items" });
1141
1203
  graph.registerHandler(`__signal__${signalId}`, items);
@@ -1157,6 +1219,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1157
1219
  }
1158
1220
  });
1159
1221
  renderItem(item, index, new ContainerBuilderImpl(itemNode, graph));
1222
+ registerTransition(graph, itemNode, options.itemTransition);
1160
1223
  return itemNode;
1161
1224
  };
1162
1225
  const buildPlan = (raw) => {
@@ -1188,6 +1251,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1188
1251
  parent: this._node,
1189
1252
  props
1190
1253
  });
1254
+ registerTransition(this._graph, node, options.transition);
1191
1255
  if (options.onSubmit !== void 0) {
1192
1256
  const handlerKey = `submit:${node.id}`;
1193
1257
  this._graph.registerHandler(handlerKey, options.onSubmit);
@@ -1195,18 +1259,20 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1195
1259
  }
1196
1260
  builder(new FormBuilderImpl(node, this._graph));
1197
1261
  }
1198
- when(condition, builder, elseBuilder) {
1262
+ when(condition, builder, elseBuilder, options = {}) {
1199
1263
  const graph = this._graph;
1200
1264
  const node = graph.createNode("conditional", {
1201
1265
  parent: this._node,
1202
1266
  props: containerProps({})
1203
1267
  });
1268
+ const branchTransition = options.transition !== void 0 && options.appear === true ? { ...options.transition, appear: true } : options.transition;
1204
1269
  const buildBranch = (build, tag) => {
1205
1270
  const branchKey = `when-${tag}:${node.id}`;
1206
1271
  const branch = graph.createNode("container", {
1207
1272
  key: branchKey,
1208
1273
  props: { key: branchKey }
1209
1274
  });
1275
+ registerTransition(graph, branch, branchTransition);
1210
1276
  build(new ContainerBuilderImpl(branch, graph));
1211
1277
  return branch;
1212
1278
  };
@@ -1301,13 +1367,24 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1301
1367
  ...options.ariaDescribedBy !== void 0 ? { ariaDescribedBy: options.ariaDescribedBy } : {}
1302
1368
  };
1303
1369
  const portalBuilder = new ContainerBuilderImpl(portalNode, graph);
1304
- portalBuilder.when(openSignal, (panelHost) => {
1305
- panelHost.container(`${key}__panel`, builder, panelOptions);
1306
- });
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
+ );
1307
1383
  const descriptor = {
1308
1384
  open: openSignal,
1309
1385
  modal: kind.modal,
1310
1386
  takesFocus: kind.takesFocus,
1387
+ menu: kind.menu ?? false,
1311
1388
  closeOnEscape: options.closeOnEscape ?? kind.defaultCloseOnEscape,
1312
1389
  restoreFocus: options.restoreFocus ?? kind.defaultRestoreFocus,
1313
1390
  ...options.initialFocusId !== void 0 ? { initialFocusId: options.initialFocusId } : {},
@@ -1404,6 +1481,7 @@ var ListBuilderImpl = class extends ContentBuilderBase {
1404
1481
  parent: this._node,
1405
1482
  props: containerProps(options)
1406
1483
  });
1484
+ registerTransition(this._graph, node, options.transition);
1407
1485
  builder(new ContainerBuilderImpl(node, this._graph));
1408
1486
  }
1409
1487
  };
@@ -2498,18 +2576,25 @@ function trapFocus(dom, container) {
2498
2576
  dom.addEventListener(container, "keydown", onKeydown);
2499
2577
  return () => dom.removeEventListener(container, "keydown", onKeydown);
2500
2578
  }
2579
+ var containmentStack = [];
2501
2580
  function containFocus(dom, container) {
2502
2581
  const body = dom.body();
2503
2582
  if (body === null) return () => {
2504
2583
  };
2584
+ containmentStack.push(container);
2505
2585
  const onFocusIn = (event) => {
2586
+ if (containmentStack[containmentStack.length - 1] !== container) return;
2506
2587
  const target = event.target;
2507
2588
  if (target !== null && !dom.contains(container, target)) {
2508
2589
  focusFirst(dom, container);
2509
2590
  }
2510
2591
  };
2511
2592
  dom.addEventListener(body, "focusin", onFocusIn);
2512
- 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
+ };
2513
2598
  }
2514
2599
  function onEscape(dom, target, handler) {
2515
2600
  const onKeydown = (event) => {
@@ -2518,6 +2603,84 @@ function onEscape(dom, target, handler) {
2518
2603
  dom.addEventListener(target, "keydown", onKeydown);
2519
2604
  return () => dom.removeEventListener(target, "keydown", onKeydown);
2520
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
+ }
2521
2684
 
2522
2685
  // ../renderer/src/render-context.ts
2523
2686
  function createRenderContext(dom, graph, container, hydrationDiagnostics, staticHTML) {
@@ -2729,7 +2892,7 @@ function patchNode(ctx, graphNode, propKey, newValue) {
2729
2892
  }
2730
2893
 
2731
2894
  // ../renderer/src/reconciliation.ts
2732
- function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
2895
+ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn, hooks) {
2733
2896
  const oldByKey = /* @__PURE__ */ new Map();
2734
2897
  for (const inst of oldInstances) {
2735
2898
  const key = inst.graphNode.key ?? inst.graphNode.id;
@@ -2750,28 +2913,33 @@ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
2750
2913
  }
2751
2914
  newInstances.push(existing);
2752
2915
  } else {
2753
- const inst = mountFn(newNode, parentDom);
2754
- 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
+ }
2755
2927
  }
2756
2928
  }
2757
2929
  const removed = [];
2758
2930
  for (const inst of oldInstances) {
2759
2931
  const key = inst.graphNode.key ?? inst.graphNode.id;
2760
- if (!usedKeys.has(key)) {
2761
- removed.push(inst);
2762
- }
2763
- }
2764
- for (const inst of removed) {
2932
+ if (usedKeys.has(key)) continue;
2933
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
2765
2934
  const parent = ctx.dom.parentNode(inst.domNode);
2766
- if (parent !== null) {
2767
- ctx.dom.removeChild(parent, inst.domNode);
2768
- }
2935
+ if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
2769
2936
  inst.dispose();
2937
+ removed.push(inst);
2770
2938
  }
2771
2939
  reorderDom(ctx, parentDom, newInstances);
2772
2940
  return { instances: newInstances, removed };
2773
2941
  }
2774
- function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
2942
+ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn, hooks) {
2775
2943
  const oldByKey = /* @__PURE__ */ new Map();
2776
2944
  for (const inst of oldInstances) {
2777
2945
  oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
@@ -2798,21 +2966,34 @@ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
2798
2966
  }
2799
2967
  newInstances.push(existing);
2800
2968
  } else {
2801
- const freshNode = entry.build();
2802
- built.push(freshNode);
2803
- const inst = mountFn(freshNode, parentDom);
2804
- 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
+ }
2805
2986
  }
2806
2987
  }
2807
2988
  const removed = [];
2808
2989
  for (const inst of oldInstances) {
2809
2990
  const key = inst.graphNode.key ?? inst.graphNode.id;
2810
- if (!usedKeys.has(key)) removed.push(inst);
2811
- }
2812
- for (const inst of removed) {
2991
+ if (usedKeys.has(key)) continue;
2992
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
2813
2993
  const parent = ctx.dom.parentNode(inst.domNode);
2814
2994
  if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
2815
2995
  inst.dispose();
2996
+ removed.push(inst);
2816
2997
  }
2817
2998
  reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
2818
2999
  return { instances: newInstances, removed, built };
@@ -2938,6 +3119,187 @@ function patchExistingInstance(ctx, instance, newNode) {
2938
3119
  }
2939
3120
  }
2940
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
+
2941
3303
  // ../renderer/src/mount.ts
2942
3304
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
2943
3305
  "text",
@@ -3090,7 +3452,7 @@ function mountNode(ctx, graphNode, parentDom) {
3090
3452
  instance2.addChild(childInstance);
3091
3453
  }
3092
3454
  dom.appendChild(parentDom, el2);
3093
- wireReactiveList(ctx, graphNode, instance2, el2);
3455
+ wireReactiveList(ctx, graphNode, instance2, el2, true);
3094
3456
  return instance2;
3095
3457
  }
3096
3458
  if (graphNode.type === "portal") {
@@ -3199,32 +3561,42 @@ function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
3199
3561
  instance.trackCleanup(unsub);
3200
3562
  }
3201
3563
  }
3202
- function wireReactiveList(ctx, graphNode, instance, el) {
3564
+ function wireReactiveList(ctx, graphNode, instance, el, runAppear = false) {
3203
3565
  const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
3204
3566
  const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
3205
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);
3206
3577
  for (const stateRef of graphNode.stateRefs) {
3207
3578
  if (stateRef.propKey !== "items") continue;
3208
3579
  const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
3209
3580
  if (sig === void 0 || typeof sig.subscribe !== "function") continue;
3210
3581
  const unsub = sig.subscribe((value) => {
3211
3582
  if (plan !== void 0) {
3212
- reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
3583
+ reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value), hooks);
3213
3584
  } else {
3214
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
3585
+ reconcileReactiveList(ctx, graphNode, instance, el, build(value), hooks);
3215
3586
  }
3216
3587
  });
3217
3588
  instance.trackCleanup(unsub);
3218
3589
  }
3219
3590
  }
3220
- function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
3591
+ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan, hooks) {
3221
3592
  const oldInstances = [...listInstance.children];
3222
3593
  const result = reconcileChildrenByPlan(
3223
3594
  ctx,
3224
3595
  listEl,
3225
3596
  oldInstances,
3226
3597
  plan,
3227
- (node, parent) => mountNode(ctx, node, parent)
3598
+ (node, parent) => mountNode(ctx, node, parent),
3599
+ hooks
3228
3600
  );
3229
3601
  listInstance.children.length = 0;
3230
3602
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -3239,14 +3611,15 @@ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan)
3239
3611
  for (const child of [...listNode.children]) listNode.removeChild(child);
3240
3612
  for (const inst of result.instances) listNode.appendChild(inst.graphNode);
3241
3613
  }
3242
- function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
3614
+ function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes, hooks) {
3243
3615
  const oldInstances = [...listInstance.children];
3244
3616
  const result = reconcileChildren(
3245
3617
  ctx,
3246
3618
  listEl,
3247
3619
  oldInstances,
3248
3620
  newNodes,
3249
- (node, parent) => mountNode(ctx, node, parent)
3621
+ (node, parent) => mountNode(ctx, node, parent),
3622
+ hooks
3250
3623
  );
3251
3624
  listInstance.children.length = 0;
3252
3625
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -3282,11 +3655,14 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
3282
3655
  const onOpenChange = (isOpen) => {
3283
3656
  if (isOpen) {
3284
3657
  if (desc.restoreFocus) saved = saveFocus(dom);
3285
- if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
3286
3658
  if (desc.modal) {
3287
3659
  active.push(trapFocus(dom, target));
3288
3660
  active.push(containFocus(dom, target));
3289
3661
  }
3662
+ if (desc.menu) {
3663
+ active.push(rovingMenu(dom, target));
3664
+ }
3665
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
3290
3666
  if (desc.closeOnEscape && desc.onClose !== void 0) {
3291
3667
  active.push(onEscape(dom, target, desc.onClose));
3292
3668
  }
@@ -4021,16 +4397,38 @@ function mountRouter(router, options) {
4021
4397
  } else {
4022
4398
  outlet = container;
4023
4399
  }
4400
+ const routeTransition = options.transition !== void 0 ? resolveTransition(options.transition) : void 0;
4401
+ const txDom = routeTransition !== void 0 ? new BrowserDOMAdapter() : void 0;
4024
4402
  let active = null;
4403
+ const pendingLeaves = /* @__PURE__ */ new Map();
4025
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
+ };
4026
4413
  const disposeActive = () => {
4027
4414
  if (active === null) return;
4028
- active.registry.run();
4029
- active.mounted.unmount();
4415
+ teardownRoute(active);
4030
4416
  active = null;
4031
4417
  };
4032
- const renderRoute = (match) => {
4033
- 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) => {
4034
4432
  const registry = new CleanupRegistry();
4035
4433
  const ctx = {
4036
4434
  path: match.path,
@@ -4043,9 +4441,53 @@ function mountRouter(router, options) {
4043
4441
  routeApp.page("route", (page) => match.route.builder(page, ctx));
4044
4442
  const runtime = createRuntime({ renderer });
4045
4443
  const routeCompiled = compile(routeApp);
4046
- 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
+ }
4047
4468
  firstRender = false;
4048
- 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
+ }
4049
4491
  };
4050
4492
  renderRoute(router.currentRoute.peek());
4051
4493
  const stopRouteSub = router.currentRoute.subscribe((match) => renderRoute(match));
@@ -4073,6 +4515,11 @@ function mountRouter(router, options) {
4073
4515
  unmount() {
4074
4516
  if (interceptLinks) container.removeEventListener("click", onClick);
4075
4517
  stopRouteSub();
4518
+ for (const [route, run] of pendingLeaves) {
4519
+ run.cancel();
4520
+ teardownRoute(route);
4521
+ }
4522
+ pendingLeaves.clear();
4076
4523
  disposeActive();
4077
4524
  shellMounted?.unmount();
4078
4525
  router.destroy();
@@ -4397,6 +4844,40 @@ function inspectComponents(graph) {
4397
4844
  });
4398
4845
  return out;
4399
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
+ }
4400
4881
 
4401
4882
  // ../devtools/src/application.ts
4402
4883
  function collectPerf(node, distinctSignals, acc) {
@@ -4770,6 +5251,7 @@ export {
4770
5251
  StreetFrameworkError,
4771
5252
  StreetRenderHandle,
4772
5253
  StreetRendererImpl,
5254
+ TransitionController,
4773
5255
  VERSION,
4774
5256
  a11yIds,
4775
5257
  applyNodeProps,
@@ -4784,6 +5266,7 @@ export {
4784
5266
  consoleDiagnosticSink,
4785
5267
  consoleHydrationDiagnosticSink,
4786
5268
  containFocus,
5269
+ createAnnouncer,
4787
5270
  createApplication,
4788
5271
  createBrowserHistory,
4789
5272
  createContext,
@@ -4818,6 +5301,7 @@ export {
4818
5301
  generateApplicationId,
4819
5302
  generateNodeId,
4820
5303
  getFocusable,
5304
+ getResolvedTransition,
4821
5305
  globalEventBus,
4822
5306
  headingUpdate,
4823
5307
  hydrateGraph,
@@ -4828,12 +5312,14 @@ export {
4828
5312
  inspectForm,
4829
5313
  inspectGraph,
4830
5314
  inspectI18n,
5315
+ inspectInteractions,
4831
5316
  inspectResource,
4832
5317
  inspectRouter,
4833
5318
  inspectSignal,
4834
5319
  interpolate,
4835
5320
  isBatching,
4836
5321
  isComponentDefinition,
5322
+ isTransitionConfig,
4837
5323
  matchPattern,
4838
5324
  matchRoutes,
4839
5325
  maxLength,
@@ -4862,9 +5348,12 @@ export {
4862
5348
  required,
4863
5349
  resetIdCounter,
4864
5350
  resolveTag,
5351
+ resolveTransition,
4865
5352
  resource,
4866
5353
  restoreFocus,
4867
5354
  routerOutlet,
5355
+ rovingMenu,
5356
+ runElementTransition,
4868
5357
  runValidators,
4869
5358
  saveFocus,
4870
5359
  scheduleImmediate,