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.cjs CHANGED
@@ -60,6 +60,7 @@ __export(src_exports, {
60
60
  StreetFrameworkError: () => StreetFrameworkError,
61
61
  StreetRenderHandle: () => StreetRenderHandle,
62
62
  StreetRendererImpl: () => StreetRendererImpl,
63
+ TransitionController: () => TransitionController,
63
64
  VERSION: () => VERSION,
64
65
  a11yIds: () => a11yIds,
65
66
  applyNodeProps: () => applyNodeProps,
@@ -74,6 +75,7 @@ __export(src_exports, {
74
75
  consoleDiagnosticSink: () => consoleDiagnosticSink,
75
76
  consoleHydrationDiagnosticSink: () => consoleHydrationDiagnosticSink,
76
77
  containFocus: () => containFocus,
78
+ createAnnouncer: () => createAnnouncer,
77
79
  createApplication: () => createApplication,
78
80
  createBrowserHistory: () => createBrowserHistory,
79
81
  createContext: () => createContext,
@@ -108,6 +110,7 @@ __export(src_exports, {
108
110
  generateApplicationId: () => generateApplicationId,
109
111
  generateNodeId: () => generateNodeId,
110
112
  getFocusable: () => getFocusable,
113
+ getResolvedTransition: () => getResolvedTransition,
111
114
  globalEventBus: () => globalEventBus,
112
115
  headingUpdate: () => headingUpdate,
113
116
  hydrateGraph: () => hydrateGraph,
@@ -118,12 +121,14 @@ __export(src_exports, {
118
121
  inspectForm: () => inspectForm,
119
122
  inspectGraph: () => inspectGraph,
120
123
  inspectI18n: () => inspectI18n,
124
+ inspectInteractions: () => inspectInteractions,
121
125
  inspectResource: () => inspectResource,
122
126
  inspectRouter: () => inspectRouter,
123
127
  inspectSignal: () => inspectSignal,
124
128
  interpolate: () => interpolate,
125
129
  isBatching: () => isBatching,
126
130
  isComponentDefinition: () => isComponentDefinition,
131
+ isTransitionConfig: () => isTransitionConfig,
127
132
  matchPattern: () => matchPattern,
128
133
  matchRoutes: () => matchRoutes,
129
134
  maxLength: () => maxLength,
@@ -152,9 +157,12 @@ __export(src_exports, {
152
157
  required: () => required,
153
158
  resetIdCounter: () => resetIdCounter,
154
159
  resolveTag: () => resolveTag,
160
+ resolveTransition: () => resolveTransition,
155
161
  resource: () => resource,
156
162
  restoreFocus: () => restoreFocus,
157
163
  routerOutlet: () => routerOutlet,
164
+ rovingMenu: () => rovingMenu,
165
+ runElementTransition: () => runElementTransition,
158
166
  runValidators: () => runValidators,
159
167
  saveFocus: () => saveFocus,
160
168
  scheduleImmediate: () => scheduleImmediate,
@@ -182,7 +190,7 @@ __export(src_exports, {
182
190
  module.exports = __toCommonJS(src_exports);
183
191
 
184
192
  // src/version.ts
185
- var VERSION = "1.8.0";
193
+ var VERSION = "1.9.0";
186
194
 
187
195
  // ../state/src/signal.ts
188
196
  var _activeConsumer = null;
@@ -537,6 +545,9 @@ function a11yIds(base) {
537
545
  description: `${token}-description`,
538
546
  error: `${token}-error`,
539
547
  title: `${token}-title`,
548
+ trigger: `${token}-trigger`,
549
+ controls: `${token}-controls`,
550
+ owns: `${token}-owns`,
540
551
  id: (suffix) => `${token}-${toIdToken(suffix)}`
541
552
  };
542
553
  }
@@ -987,6 +998,7 @@ var ApplicationGraph = class {
987
998
  this.handlers.delete(`__listplan__${node.id}`);
988
999
  this.handlers.delete(`__overlay__${node.id}`);
989
1000
  this.handlers.delete(`__component__${node.id}`);
1001
+ this.handlers.delete(`__transition__${node.id}`);
990
1002
  }
991
1003
  // ── Handler registry ──────────────────────────────────────────────────────
992
1004
  registerHandler(key, fn) {
@@ -1075,6 +1087,47 @@ var ApplicationGraph = class {
1075
1087
  }
1076
1088
  };
1077
1089
 
1090
+ // ../dsl/src/transition.ts
1091
+ function classes(value) {
1092
+ if (value === void 0) return [];
1093
+ const out = [];
1094
+ for (const token of value.split(/\s+/)) {
1095
+ if (token.length > 0 && !out.includes(token)) out.push(token);
1096
+ }
1097
+ return out;
1098
+ }
1099
+ function merge(a, b) {
1100
+ const out = [...a];
1101
+ for (const token of b) if (!out.includes(token)) out.push(token);
1102
+ return out;
1103
+ }
1104
+ function resolveTransition(config) {
1105
+ const n = config.name;
1106
+ const enterActive = merge(
1107
+ classes(config.enter),
1108
+ classes(config.enterActive ?? (n !== void 0 ? `${n}-enter-active` : void 0))
1109
+ );
1110
+ const leaveActive = merge(
1111
+ classes(config.leave),
1112
+ classes(config.leaveActive ?? (n !== void 0 ? `${n}-leave-active` : void 0))
1113
+ );
1114
+ return {
1115
+ enterActive,
1116
+ enterFrom: classes(config.enterFrom ?? (n !== void 0 ? `${n}-enter-from` : void 0)),
1117
+ enterTo: classes(config.enterTo ?? (n !== void 0 ? `${n}-enter-to` : void 0)),
1118
+ leaveActive,
1119
+ leaveFrom: classes(config.leaveFrom ?? (n !== void 0 ? `${n}-leave-from` : void 0)),
1120
+ leaveTo: classes(config.leaveTo ?? (n !== void 0 ? `${n}-leave-to` : void 0)),
1121
+ appear: config.appear ?? false,
1122
+ duration: config.duration ?? 1e3
1123
+ };
1124
+ }
1125
+ function isTransitionConfig(value) {
1126
+ if (value === null || typeof value !== "object") return false;
1127
+ const o = value;
1128
+ 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";
1129
+ }
1130
+
1078
1131
  // ../dsl/src/builders.ts
1079
1132
  function isSignal(v) {
1080
1133
  return v !== null && typeof v === "object" && typeof v["get"] === "function" && typeof v["subscribe"] === "function";
@@ -1102,6 +1155,18 @@ function applyA11yProps(props, options) {
1102
1155
  if (options.ariaInvalid !== void 0) props["aria-invalid"] = String(options.ariaInvalid);
1103
1156
  if (options.ariaRequired !== void 0) props["aria-required"] = String(options.ariaRequired);
1104
1157
  if (options.ariaModal !== void 0) props["aria-modal"] = String(options.ariaModal);
1158
+ if (options.ariaOwns !== void 0) props["aria-owns"] = options.ariaOwns;
1159
+ if (options.ariaActiveDescendant !== void 0) props["aria-activedescendant"] = options.ariaActiveDescendant;
1160
+ if (options.ariaHasPopup !== void 0) props["aria-haspopup"] = String(options.ariaHasPopup);
1161
+ if (options.ariaSelected !== void 0) props["aria-selected"] = String(options.ariaSelected);
1162
+ }
1163
+ function registerTransition(graph, node, config) {
1164
+ if (config === void 0) return;
1165
+ const resolved = resolveTransition(config);
1166
+ graph.registerHandler(
1167
+ `__transition__${node.id}`,
1168
+ () => resolved
1169
+ );
1105
1170
  }
1106
1171
  function containerProps(options) {
1107
1172
  const props = {};
@@ -1162,6 +1227,7 @@ var OVERLAY_KINDS = {
1162
1227
  role: "menu",
1163
1228
  modal: false,
1164
1229
  takesFocus: true,
1230
+ menu: true,
1165
1231
  ariaModal: false,
1166
1232
  defaultCloseOnEscape: true,
1167
1233
  defaultRestoreFocus: true
@@ -1294,6 +1360,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1294
1360
  parent: this._node,
1295
1361
  props: containerProps(options)
1296
1362
  });
1363
+ registerTransition(this._graph, node, options.transition);
1297
1364
  builder(new SectionBuilderImpl(node, this._graph));
1298
1365
  }
1299
1366
  container(key, builder, options = {}) {
@@ -1302,6 +1369,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1302
1369
  parent: this._node,
1303
1370
  props: containerProps(options)
1304
1371
  });
1372
+ registerTransition(this._graph, node, options.transition);
1305
1373
  builder(new ContainerBuilderImpl(node, this._graph));
1306
1374
  }
1307
1375
  list(key, builder, options = {}) {
@@ -1310,6 +1378,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1310
1378
  parent: this._node,
1311
1379
  props: containerProps(options)
1312
1380
  });
1381
+ registerTransition(this._graph, node, options.transition);
1313
1382
  builder(new ListBuilderImpl(node, this._graph));
1314
1383
  }
1315
1384
  listOf(key, items, renderItem, options = {}) {
@@ -1319,6 +1388,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1319
1388
  parent: this._node,
1320
1389
  props: containerProps(options)
1321
1390
  });
1391
+ registerTransition(graph, node, options.transition);
1322
1392
  const signalId = `${node.id}:items`;
1323
1393
  node.stateRefs.push({ signalId, propKey: "items" });
1324
1394
  graph.registerHandler(`__signal__${signalId}`, items);
@@ -1340,6 +1410,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1340
1410
  }
1341
1411
  });
1342
1412
  renderItem(item, index, new ContainerBuilderImpl(itemNode, graph));
1413
+ registerTransition(graph, itemNode, options.itemTransition);
1343
1414
  return itemNode;
1344
1415
  };
1345
1416
  const buildPlan = (raw) => {
@@ -1371,6 +1442,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1371
1442
  parent: this._node,
1372
1443
  props
1373
1444
  });
1445
+ registerTransition(this._graph, node, options.transition);
1374
1446
  if (options.onSubmit !== void 0) {
1375
1447
  const handlerKey = `submit:${node.id}`;
1376
1448
  this._graph.registerHandler(handlerKey, options.onSubmit);
@@ -1378,18 +1450,20 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1378
1450
  }
1379
1451
  builder(new FormBuilderImpl(node, this._graph));
1380
1452
  }
1381
- when(condition, builder, elseBuilder) {
1453
+ when(condition, builder, elseBuilder, options = {}) {
1382
1454
  const graph = this._graph;
1383
1455
  const node = graph.createNode("conditional", {
1384
1456
  parent: this._node,
1385
1457
  props: containerProps({})
1386
1458
  });
1459
+ const branchTransition = options.transition !== void 0 && options.appear === true ? { ...options.transition, appear: true } : options.transition;
1387
1460
  const buildBranch = (build, tag) => {
1388
1461
  const branchKey = `when-${tag}:${node.id}`;
1389
1462
  const branch = graph.createNode("container", {
1390
1463
  key: branchKey,
1391
1464
  props: { key: branchKey }
1392
1465
  });
1466
+ registerTransition(graph, branch, branchTransition);
1393
1467
  build(new ContainerBuilderImpl(branch, graph));
1394
1468
  return branch;
1395
1469
  };
@@ -1484,13 +1558,24 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1484
1558
  ...options.ariaDescribedBy !== void 0 ? { ariaDescribedBy: options.ariaDescribedBy } : {}
1485
1559
  };
1486
1560
  const portalBuilder = new ContainerBuilderImpl(portalNode, graph);
1487
- portalBuilder.when(openSignal, (panelHost) => {
1488
- panelHost.container(`${key}__panel`, builder, panelOptions);
1489
- });
1561
+ portalBuilder.when(
1562
+ openSignal,
1563
+ (panelHost) => {
1564
+ panelHost.container(`${key}__panel`, builder, panelOptions);
1565
+ },
1566
+ void 0,
1567
+ // Overlay open/close rides the panel's `when`; a transition animates the
1568
+ // panel in on open and — via the reconciler's deferred-leave — plays the
1569
+ // leave before the panel is removed (§10). Focus is restored at close-
1570
+ // request time (see wireOverlayBehavior), so it never stays trapped inside
1571
+ // a panel that is animating away.
1572
+ options.transition !== void 0 ? { transition: options.transition } : {}
1573
+ );
1490
1574
  const descriptor = {
1491
1575
  open: openSignal,
1492
1576
  modal: kind.modal,
1493
1577
  takesFocus: kind.takesFocus,
1578
+ menu: kind.menu ?? false,
1494
1579
  closeOnEscape: options.closeOnEscape ?? kind.defaultCloseOnEscape,
1495
1580
  restoreFocus: options.restoreFocus ?? kind.defaultRestoreFocus,
1496
1581
  ...options.initialFocusId !== void 0 ? { initialFocusId: options.initialFocusId } : {},
@@ -1587,6 +1672,7 @@ var ListBuilderImpl = class extends ContentBuilderBase {
1587
1672
  parent: this._node,
1588
1673
  props: containerProps(options)
1589
1674
  });
1675
+ registerTransition(this._graph, node, options.transition);
1590
1676
  builder(new ContainerBuilderImpl(node, this._graph));
1591
1677
  }
1592
1678
  };
@@ -2681,18 +2767,25 @@ function trapFocus(dom, container) {
2681
2767
  dom.addEventListener(container, "keydown", onKeydown);
2682
2768
  return () => dom.removeEventListener(container, "keydown", onKeydown);
2683
2769
  }
2770
+ var containmentStack = [];
2684
2771
  function containFocus(dom, container) {
2685
2772
  const body = dom.body();
2686
2773
  if (body === null) return () => {
2687
2774
  };
2775
+ containmentStack.push(container);
2688
2776
  const onFocusIn = (event) => {
2777
+ if (containmentStack[containmentStack.length - 1] !== container) return;
2689
2778
  const target = event.target;
2690
2779
  if (target !== null && !dom.contains(container, target)) {
2691
2780
  focusFirst(dom, container);
2692
2781
  }
2693
2782
  };
2694
2783
  dom.addEventListener(body, "focusin", onFocusIn);
2695
- return () => dom.removeEventListener(body, "focusin", onFocusIn);
2784
+ return () => {
2785
+ dom.removeEventListener(body, "focusin", onFocusIn);
2786
+ const index = containmentStack.lastIndexOf(container);
2787
+ if (index !== -1) containmentStack.splice(index, 1);
2788
+ };
2696
2789
  }
2697
2790
  function onEscape(dom, target, handler) {
2698
2791
  const onKeydown = (event) => {
@@ -2701,6 +2794,84 @@ function onEscape(dom, target, handler) {
2701
2794
  dom.addEventListener(target, "keydown", onKeydown);
2702
2795
  return () => dom.removeEventListener(target, "keydown", onKeydown);
2703
2796
  }
2797
+ function rovingMenu(dom, container, selector = FOCUSABLE_SELECTOR) {
2798
+ const onKeydown = (event) => {
2799
+ const key = event.key;
2800
+ const isActivate = key === "Enter" || key === " " || key === "Spacebar";
2801
+ const isMove = key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End";
2802
+ if (!isActivate && !isMove) return;
2803
+ const items = getFocusable(dom, container, selector);
2804
+ if (items.length === 0) return;
2805
+ const active = dom.activeElement();
2806
+ const index = active === null ? -1 : items.indexOf(active);
2807
+ if (isActivate) {
2808
+ if (index < 0) return;
2809
+ event.preventDefault();
2810
+ items[index].click?.();
2811
+ return;
2812
+ }
2813
+ event.preventDefault();
2814
+ let next;
2815
+ if (key === "Home") next = 0;
2816
+ else if (key === "End") next = items.length - 1;
2817
+ else if (key === "ArrowDown") next = index < 0 ? 0 : (index + 1) % items.length;
2818
+ else next = index <= 0 ? items.length - 1 : index - 1;
2819
+ dom.focus(items[next]);
2820
+ };
2821
+ dom.addEventListener(container, "keydown", onKeydown);
2822
+ return () => dom.removeEventListener(container, "keydown", onKeydown);
2823
+ }
2824
+
2825
+ // ../dom/src/live-region.ts
2826
+ 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;";
2827
+ function makeRegion(dom, politeness) {
2828
+ const el = dom.createElement("div");
2829
+ dom.setAttribute(el, "aria-live", politeness);
2830
+ dom.setAttribute(el, "aria-atomic", "true");
2831
+ dom.setAttribute(el, "role", politeness === "assertive" ? "alert" : "status");
2832
+ dom.setAttribute(el, "data-streetui-live", politeness);
2833
+ dom.setAttribute(el, "style", VISUALLY_HIDDEN);
2834
+ return el;
2835
+ }
2836
+ function createAnnouncer(dom) {
2837
+ const body = dom.body();
2838
+ if (body === null) {
2839
+ return { announce() {
2840
+ }, clear() {
2841
+ }, destroy() {
2842
+ } };
2843
+ }
2844
+ const polite = makeRegion(dom, "polite");
2845
+ const assertive = makeRegion(dom, "assertive");
2846
+ dom.appendChild(body, polite);
2847
+ dom.appendChild(body, assertive);
2848
+ let destroyed = false;
2849
+ const write = (region, message) => {
2850
+ dom.setTextContent(region, "");
2851
+ void Promise.resolve().then(() => {
2852
+ if (!destroyed) dom.setTextContent(region, message);
2853
+ });
2854
+ };
2855
+ return {
2856
+ announce(message, options) {
2857
+ if (destroyed) return;
2858
+ write(options?.assertive === true ? assertive : polite, message);
2859
+ },
2860
+ clear() {
2861
+ if (destroyed) return;
2862
+ dom.setTextContent(polite, "");
2863
+ dom.setTextContent(assertive, "");
2864
+ },
2865
+ destroy() {
2866
+ if (destroyed) return;
2867
+ destroyed = true;
2868
+ for (const region of [polite, assertive]) {
2869
+ const parent = dom.parentNode(region);
2870
+ if (parent !== null) dom.removeChild(parent, region);
2871
+ }
2872
+ }
2873
+ };
2874
+ }
2704
2875
 
2705
2876
  // ../renderer/src/render-context.ts
2706
2877
  function createRenderContext(dom, graph, container, hydrationDiagnostics, staticHTML) {
@@ -2912,7 +3083,7 @@ function patchNode(ctx, graphNode, propKey, newValue) {
2912
3083
  }
2913
3084
 
2914
3085
  // ../renderer/src/reconciliation.ts
2915
- function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
3086
+ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn, hooks) {
2916
3087
  const oldByKey = /* @__PURE__ */ new Map();
2917
3088
  for (const inst of oldInstances) {
2918
3089
  const key = inst.graphNode.key ?? inst.graphNode.id;
@@ -2933,28 +3104,33 @@ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
2933
3104
  }
2934
3105
  newInstances.push(existing);
2935
3106
  } else {
2936
- const inst = mountFn(newNode, parentDom);
2937
- newInstances.push(inst);
3107
+ const reclaimed = hooks?.takeLeaving(key);
3108
+ if (reclaimed !== void 0) {
3109
+ patchExistingInstance(ctx, reclaimed, newNode);
3110
+ reconcileItemChildren(ctx, reclaimed, newNode, mountFn);
3111
+ hooks?.onEnter(reclaimed);
3112
+ newInstances.push(reclaimed);
3113
+ } else {
3114
+ const inst = mountFn(newNode, parentDom);
3115
+ hooks?.onEnter(inst);
3116
+ newInstances.push(inst);
3117
+ }
2938
3118
  }
2939
3119
  }
2940
3120
  const removed = [];
2941
3121
  for (const inst of oldInstances) {
2942
3122
  const key = inst.graphNode.key ?? inst.graphNode.id;
2943
- if (!usedKeys.has(key)) {
2944
- removed.push(inst);
2945
- }
2946
- }
2947
- for (const inst of removed) {
3123
+ if (usedKeys.has(key)) continue;
3124
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
2948
3125
  const parent = ctx.dom.parentNode(inst.domNode);
2949
- if (parent !== null) {
2950
- ctx.dom.removeChild(parent, inst.domNode);
2951
- }
3126
+ if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
2952
3127
  inst.dispose();
3128
+ removed.push(inst);
2953
3129
  }
2954
3130
  reorderDom(ctx, parentDom, newInstances);
2955
3131
  return { instances: newInstances, removed };
2956
3132
  }
2957
- function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
3133
+ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn, hooks) {
2958
3134
  const oldByKey = /* @__PURE__ */ new Map();
2959
3135
  for (const inst of oldInstances) {
2960
3136
  oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
@@ -2981,21 +3157,34 @@ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
2981
3157
  }
2982
3158
  newInstances.push(existing);
2983
3159
  } else {
2984
- const freshNode = entry.build();
2985
- built.push(freshNode);
2986
- const inst = mountFn(freshNode, parentDom);
2987
- newInstances.push(inst);
3160
+ const reclaimed = hooks?.takeLeaving(entry.key);
3161
+ if (reclaimed !== void 0) {
3162
+ const freshNode = entry.build();
3163
+ built.push(freshNode);
3164
+ patchExistingInstance(ctx, reclaimed, freshNode);
3165
+ reconcileItemChildren(ctx, reclaimed, freshNode, mountFn);
3166
+ reclaimed.graphNode.setProp("_sig", freshNode.getProp("_sig"));
3167
+ reclaimed.graphNode.setProp("_item", entry.item);
3168
+ hooks?.onEnter(reclaimed);
3169
+ newInstances.push(reclaimed);
3170
+ } else {
3171
+ const freshNode = entry.build();
3172
+ built.push(freshNode);
3173
+ const inst = mountFn(freshNode, parentDom);
3174
+ hooks?.onEnter(inst);
3175
+ newInstances.push(inst);
3176
+ }
2988
3177
  }
2989
3178
  }
2990
3179
  const removed = [];
2991
3180
  for (const inst of oldInstances) {
2992
3181
  const key = inst.graphNode.key ?? inst.graphNode.id;
2993
- if (!usedKeys.has(key)) removed.push(inst);
2994
- }
2995
- for (const inst of removed) {
3182
+ if (usedKeys.has(key)) continue;
3183
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
2996
3184
  const parent = ctx.dom.parentNode(inst.domNode);
2997
3185
  if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
2998
3186
  inst.dispose();
3187
+ removed.push(inst);
2999
3188
  }
3000
3189
  reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
3001
3190
  return { instances: newInstances, removed, built };
@@ -3121,6 +3310,187 @@ function patchExistingInstance(ctx, instance, newNode) {
3121
3310
  }
3122
3311
  }
3123
3312
 
3313
+ // ../renderer/src/transition.ts
3314
+ function getResolvedTransition(graph, nodeId) {
3315
+ const fn = graph.getHandler(`__transition__${nodeId}`);
3316
+ return fn === void 0 ? void 0 : fn();
3317
+ }
3318
+ function host() {
3319
+ return globalThis;
3320
+ }
3321
+ function nextFrame(cb) {
3322
+ const h = host();
3323
+ const raf = h.requestAnimationFrame;
3324
+ if (typeof raf === "function") {
3325
+ raf(() => raf(cb));
3326
+ } else {
3327
+ h.setTimeout(cb, 0);
3328
+ }
3329
+ }
3330
+ function splitClass(value) {
3331
+ if (value === null) return [];
3332
+ const out = [];
3333
+ for (const t of value.split(/\s+/)) if (t.length > 0) out.push(t);
3334
+ return out;
3335
+ }
3336
+ function addClasses(dom, el, classes2) {
3337
+ if (classes2.length === 0) return;
3338
+ const current = splitClass(dom.getAttribute(el, "class"));
3339
+ let changed = false;
3340
+ for (const c of classes2) {
3341
+ if (!current.includes(c)) {
3342
+ current.push(c);
3343
+ changed = true;
3344
+ }
3345
+ }
3346
+ if (changed) dom.setAttribute(el, "class", current.join(" "));
3347
+ }
3348
+ function removeClasses(dom, el, classes2) {
3349
+ if (classes2.length === 0) return;
3350
+ const current = splitClass(dom.getAttribute(el, "class"));
3351
+ const next = current.filter((c) => !classes2.includes(c));
3352
+ if (next.length !== current.length) {
3353
+ if (next.length === 0) dom.removeAttribute(el, "class");
3354
+ else dom.setAttribute(el, "class", next.join(" "));
3355
+ }
3356
+ }
3357
+ function startRun(dom, el, active, from, to, duration, onDone) {
3358
+ const h = host();
3359
+ let finished = false;
3360
+ let timer = null;
3361
+ const onEvent = (e) => {
3362
+ if (e.target !== el) return;
3363
+ finish();
3364
+ };
3365
+ const detach = () => {
3366
+ dom.removeEventListener(el, "transitionend", onEvent);
3367
+ dom.removeEventListener(el, "animationend", onEvent);
3368
+ if (timer !== null) {
3369
+ h.clearTimeout(timer);
3370
+ timer = null;
3371
+ }
3372
+ };
3373
+ const finish = () => {
3374
+ if (finished) return;
3375
+ finished = true;
3376
+ detach();
3377
+ removeClasses(dom, el, active);
3378
+ removeClasses(dom, el, to);
3379
+ removeClasses(dom, el, from);
3380
+ onDone();
3381
+ };
3382
+ addClasses(dom, el, from);
3383
+ addClasses(dom, el, active);
3384
+ dom.addEventListener(el, "transitionend", onEvent);
3385
+ dom.addEventListener(el, "animationend", onEvent);
3386
+ timer = h.setTimeout(finish, duration);
3387
+ nextFrame(() => {
3388
+ if (finished) return;
3389
+ removeClasses(dom, el, from);
3390
+ addClasses(dom, el, to);
3391
+ });
3392
+ return {
3393
+ cancel: () => {
3394
+ if (finished) return;
3395
+ finished = true;
3396
+ detach();
3397
+ removeClasses(dom, el, active);
3398
+ removeClasses(dom, el, to);
3399
+ removeClasses(dom, el, from);
3400
+ }
3401
+ };
3402
+ }
3403
+ function runElementTransition(dom, el, rt, phase, onDone) {
3404
+ if (dom.body() === null || !dom.isElement(el)) {
3405
+ onDone();
3406
+ return { cancel: () => {
3407
+ } };
3408
+ }
3409
+ const active = phase === "enter" ? rt.enterActive : rt.leaveActive;
3410
+ const from = phase === "enter" ? rt.enterFrom : rt.leaveFrom;
3411
+ const to = phase === "enter" ? rt.enterTo : rt.leaveTo;
3412
+ return startRun(dom, el, active, from, to, rt.duration, onDone);
3413
+ }
3414
+ var TransitionController = class {
3415
+ constructor(dom, graph, finalize) {
3416
+ this.dom = dom;
3417
+ this.graph = graph;
3418
+ this.finalize = finalize;
3419
+ }
3420
+ leaving = /* @__PURE__ */ new Map();
3421
+ /** True only in a real DOM environment (browser). */
3422
+ get browser() {
3423
+ return this.dom.body() !== null;
3424
+ }
3425
+ keyOf(inst) {
3426
+ return inst.graphNode.key ?? inst.graphNode.id;
3427
+ }
3428
+ resolved(node) {
3429
+ return getResolvedTransition(this.graph, node.id);
3430
+ }
3431
+ /** Run the enter animation for `inst` if it carries a transition (browser only). */
3432
+ enter(inst) {
3433
+ if (!this.browser) return;
3434
+ const rt = this.resolved(inst.graphNode);
3435
+ if (rt === void 0) return;
3436
+ const el = inst.domNode;
3437
+ if (!this.dom.isElement(el)) return;
3438
+ startRun(this.dom, el, rt.enterActive, rt.enterFrom, rt.enterTo, rt.duration, () => {
3439
+ });
3440
+ }
3441
+ /**
3442
+ * Play `appear` for any initial child that opted into it (fresh browser mount
3443
+ * only — hydration must never animate appear, §22, and this is called only on
3444
+ * the mount path).
3445
+ */
3446
+ appear(children) {
3447
+ if (!this.browser) return;
3448
+ for (const child of children) {
3449
+ const rt = this.resolved(child.graphNode);
3450
+ if (rt !== void 0 && rt.appear) this.enter(child);
3451
+ }
3452
+ }
3453
+ hooks() {
3454
+ return {
3455
+ takeLeaving: (key) => {
3456
+ const entry = this.leaving.get(key);
3457
+ if (entry === void 0) return void 0;
3458
+ entry.run.cancel();
3459
+ this.leaving.delete(key);
3460
+ return entry.inst;
3461
+ },
3462
+ beginLeave: (inst) => {
3463
+ if (!this.browser) return false;
3464
+ const rt = this.resolved(inst.graphNode);
3465
+ if (rt === void 0) return false;
3466
+ const el = inst.domNode;
3467
+ if (!this.dom.isElement(el)) return false;
3468
+ const key = this.keyOf(inst);
3469
+ const prior = this.leaving.get(key);
3470
+ if (prior !== void 0) prior.run.cancel();
3471
+ const run = startRun(
3472
+ this.dom,
3473
+ el,
3474
+ rt.leaveActive,
3475
+ rt.leaveFrom,
3476
+ rt.leaveTo,
3477
+ rt.duration,
3478
+ () => {
3479
+ const current = this.leaving.get(key);
3480
+ if (current !== void 0 && current.run === run) {
3481
+ this.leaving.delete(key);
3482
+ this.finalize(inst);
3483
+ }
3484
+ }
3485
+ );
3486
+ this.leaving.set(key, { inst, run });
3487
+ return true;
3488
+ },
3489
+ onEnter: (inst) => this.enter(inst)
3490
+ };
3491
+ }
3492
+ };
3493
+
3124
3494
  // ../renderer/src/mount.ts
3125
3495
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
3126
3496
  "text",
@@ -3273,7 +3643,7 @@ function mountNode(ctx, graphNode, parentDom) {
3273
3643
  instance2.addChild(childInstance);
3274
3644
  }
3275
3645
  dom.appendChild(parentDom, el2);
3276
- wireReactiveList(ctx, graphNode, instance2, el2);
3646
+ wireReactiveList(ctx, graphNode, instance2, el2, true);
3277
3647
  return instance2;
3278
3648
  }
3279
3649
  if (graphNode.type === "portal") {
@@ -3382,32 +3752,42 @@ function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
3382
3752
  instance.trackCleanup(unsub);
3383
3753
  }
3384
3754
  }
3385
- function wireReactiveList(ctx, graphNode, instance, el) {
3755
+ function wireReactiveList(ctx, graphNode, instance, el, runAppear = false) {
3386
3756
  const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
3387
3757
  const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
3388
3758
  if (plan === void 0 && build === void 0) return;
3759
+ const controller = new TransitionController(ctx.dom, ctx.graph, (leaving) => {
3760
+ const parent = ctx.dom.parentNode(leaving.domNode);
3761
+ if (parent !== null) ctx.dom.removeChild(parent, leaving.domNode);
3762
+ leaving.dispose();
3763
+ forgetInstance(ctx, leaving);
3764
+ ctx.graph.detachNode(leaving.graphNode);
3765
+ });
3766
+ const hooks = controller.hooks();
3767
+ if (runAppear) controller.appear(instance.children);
3389
3768
  for (const stateRef of graphNode.stateRefs) {
3390
3769
  if (stateRef.propKey !== "items") continue;
3391
3770
  const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
3392
3771
  if (sig === void 0 || typeof sig.subscribe !== "function") continue;
3393
3772
  const unsub = sig.subscribe((value) => {
3394
3773
  if (plan !== void 0) {
3395
- reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
3774
+ reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value), hooks);
3396
3775
  } else {
3397
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
3776
+ reconcileReactiveList(ctx, graphNode, instance, el, build(value), hooks);
3398
3777
  }
3399
3778
  });
3400
3779
  instance.trackCleanup(unsub);
3401
3780
  }
3402
3781
  }
3403
- function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
3782
+ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan, hooks) {
3404
3783
  const oldInstances = [...listInstance.children];
3405
3784
  const result = reconcileChildrenByPlan(
3406
3785
  ctx,
3407
3786
  listEl,
3408
3787
  oldInstances,
3409
3788
  plan,
3410
- (node, parent) => mountNode(ctx, node, parent)
3789
+ (node, parent) => mountNode(ctx, node, parent),
3790
+ hooks
3411
3791
  );
3412
3792
  listInstance.children.length = 0;
3413
3793
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -3422,14 +3802,15 @@ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan)
3422
3802
  for (const child of [...listNode.children]) listNode.removeChild(child);
3423
3803
  for (const inst of result.instances) listNode.appendChild(inst.graphNode);
3424
3804
  }
3425
- function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
3805
+ function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes, hooks) {
3426
3806
  const oldInstances = [...listInstance.children];
3427
3807
  const result = reconcileChildren(
3428
3808
  ctx,
3429
3809
  listEl,
3430
3810
  oldInstances,
3431
3811
  newNodes,
3432
- (node, parent) => mountNode(ctx, node, parent)
3812
+ (node, parent) => mountNode(ctx, node, parent),
3813
+ hooks
3433
3814
  );
3434
3815
  listInstance.children.length = 0;
3435
3816
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -3465,11 +3846,14 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
3465
3846
  const onOpenChange = (isOpen) => {
3466
3847
  if (isOpen) {
3467
3848
  if (desc.restoreFocus) saved = saveFocus(dom);
3468
- if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
3469
3849
  if (desc.modal) {
3470
3850
  active.push(trapFocus(dom, target));
3471
3851
  active.push(containFocus(dom, target));
3472
3852
  }
3853
+ if (desc.menu) {
3854
+ active.push(rovingMenu(dom, target));
3855
+ }
3856
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
3473
3857
  if (desc.closeOnEscape && desc.onClose !== void 0) {
3474
3858
  active.push(onEscape(dom, target, desc.onClose));
3475
3859
  }
@@ -4204,16 +4588,38 @@ function mountRouter(router, options) {
4204
4588
  } else {
4205
4589
  outlet = container;
4206
4590
  }
4591
+ const routeTransition = options.transition !== void 0 ? resolveTransition(options.transition) : void 0;
4592
+ const txDom = routeTransition !== void 0 ? new BrowserDOMAdapter() : void 0;
4207
4593
  let active = null;
4594
+ const pendingLeaves = /* @__PURE__ */ new Map();
4208
4595
  let firstRender = hydrateMode;
4596
+ const teardownRoute = (route) => {
4597
+ route.registry.run();
4598
+ route.mounted.unmount();
4599
+ if (route.host !== null && txDom !== void 0) {
4600
+ const parent = txDom.parentNode(route.host);
4601
+ if (parent !== null) txDom.removeChild(parent, route.host);
4602
+ }
4603
+ };
4209
4604
  const disposeActive = () => {
4210
4605
  if (active === null) return;
4211
- active.registry.run();
4212
- active.mounted.unmount();
4606
+ teardownRoute(active);
4213
4607
  active = null;
4214
4608
  };
4215
- const renderRoute = (match) => {
4216
- disposeActive();
4609
+ const wrapOutletChildren = (dom) => {
4610
+ const host2 = dom.createElement("div");
4611
+ dom.setAttribute(host2, "data-streetui-route", "");
4612
+ const moved = [];
4613
+ let child = dom.firstChild(outlet);
4614
+ while (child !== null) {
4615
+ moved.push(child);
4616
+ child = dom.nextSibling(child);
4617
+ }
4618
+ for (const n of moved) dom.appendChild(host2, n);
4619
+ dom.appendChild(outlet, host2);
4620
+ return host2;
4621
+ };
4622
+ const buildRoute = (match, target, hydrate) => {
4217
4623
  const registry = new CleanupRegistry();
4218
4624
  const ctx = {
4219
4625
  path: match.path,
@@ -4226,9 +4632,53 @@ function mountRouter(router, options) {
4226
4632
  routeApp.page("route", (page) => match.route.builder(page, ctx));
4227
4633
  const runtime = createRuntime({ renderer });
4228
4634
  const routeCompiled = compile(routeApp);
4229
- const mounted = firstRender ? runtime.hydrate(routeCompiled, outlet) : runtime.mount(routeCompiled, outlet);
4635
+ const mounted = hydrate ? runtime.hydrate(routeCompiled, target) : runtime.mount(routeCompiled, target);
4636
+ return { registry, mounted, host: null, enterRun: null };
4637
+ };
4638
+ const renderRoute = (match) => {
4639
+ if (routeTransition === void 0 || txDom === void 0) {
4640
+ disposeActive();
4641
+ active = buildRoute(match, outlet, firstRender);
4642
+ firstRender = false;
4643
+ return;
4644
+ }
4645
+ if (active === null) {
4646
+ if (firstRender) {
4647
+ active = buildRoute(match, outlet, true);
4648
+ active.host = wrapOutletChildren(txDom);
4649
+ } else {
4650
+ const host2 = txDom.createElement("div");
4651
+ txDom.setAttribute(host2, "data-streetui-route", "");
4652
+ txDom.appendChild(outlet, host2);
4653
+ active = buildRoute(match, host2, false);
4654
+ active.host = host2;
4655
+ }
4656
+ firstRender = false;
4657
+ return;
4658
+ }
4230
4659
  firstRender = false;
4231
- active = { registry, mounted };
4660
+ const leaving = active;
4661
+ leaving.enterRun?.cancel();
4662
+ leaving.enterRun = null;
4663
+ const leaveHost = leaving.host;
4664
+ const enterHost = txDom.createElement("div");
4665
+ txDom.setAttribute(enterHost, "data-streetui-route", "");
4666
+ txDom.appendChild(outlet, enterHost);
4667
+ const next = buildRoute(match, enterHost, false);
4668
+ next.host = enterHost;
4669
+ next.enterRun = runElementTransition(txDom, enterHost, routeTransition, "enter", () => {
4670
+ next.enterRun = null;
4671
+ });
4672
+ active = next;
4673
+ if (leaveHost !== null) {
4674
+ const run = runElementTransition(txDom, leaveHost, routeTransition, "leave", () => {
4675
+ pendingLeaves.delete(leaving);
4676
+ teardownRoute(leaving);
4677
+ });
4678
+ pendingLeaves.set(leaving, run);
4679
+ } else {
4680
+ teardownRoute(leaving);
4681
+ }
4232
4682
  };
4233
4683
  renderRoute(router.currentRoute.peek());
4234
4684
  const stopRouteSub = router.currentRoute.subscribe((match) => renderRoute(match));
@@ -4256,6 +4706,11 @@ function mountRouter(router, options) {
4256
4706
  unmount() {
4257
4707
  if (interceptLinks) container.removeEventListener("click", onClick);
4258
4708
  stopRouteSub();
4709
+ for (const [route, run] of pendingLeaves) {
4710
+ run.cancel();
4711
+ teardownRoute(route);
4712
+ }
4713
+ pendingLeaves.clear();
4259
4714
  disposeActive();
4260
4715
  shellMounted?.unmount();
4261
4716
  router.destroy();
@@ -4580,6 +5035,40 @@ function inspectComponents(graph) {
4580
5035
  });
4581
5036
  return out;
4582
5037
  }
5038
+ function inspectInteractions(graph) {
5039
+ const overlays = [];
5040
+ const transitions = [];
5041
+ graph.walk((node, depth) => {
5042
+ const overlayFn = graph.getHandler(`__overlay__${node.id}`);
5043
+ if (overlayFn !== void 0) {
5044
+ const d = overlayFn();
5045
+ overlays.push({
5046
+ id: node.id,
5047
+ key: node.key,
5048
+ open: d.open.peek(),
5049
+ modal: d.modal,
5050
+ takesFocus: d.takesFocus,
5051
+ menu: d.menu,
5052
+ closeOnEscape: d.closeOnEscape,
5053
+ restoreFocus: d.restoreFocus,
5054
+ depth
5055
+ });
5056
+ }
5057
+ const transitionFn = graph.getHandler(`__transition__${node.id}`);
5058
+ if (transitionFn !== void 0) {
5059
+ const t = transitionFn();
5060
+ transitions.push({
5061
+ id: node.id,
5062
+ key: node.key,
5063
+ nodeType: node.type,
5064
+ duration: t.duration,
5065
+ appear: t.appear,
5066
+ depth
5067
+ });
5068
+ }
5069
+ });
5070
+ return { overlays, transitions };
5071
+ }
4583
5072
 
4584
5073
  // ../devtools/src/application.ts
4585
5074
  function collectPerf(node, distinctSignals, acc) {
@@ -4954,6 +5443,7 @@ function defineConfig(config) {
4954
5443
  StreetFrameworkError,
4955
5444
  StreetRenderHandle,
4956
5445
  StreetRendererImpl,
5446
+ TransitionController,
4957
5447
  VERSION,
4958
5448
  a11yIds,
4959
5449
  applyNodeProps,
@@ -4968,6 +5458,7 @@ function defineConfig(config) {
4968
5458
  consoleDiagnosticSink,
4969
5459
  consoleHydrationDiagnosticSink,
4970
5460
  containFocus,
5461
+ createAnnouncer,
4971
5462
  createApplication,
4972
5463
  createBrowserHistory,
4973
5464
  createContext,
@@ -5002,6 +5493,7 @@ function defineConfig(config) {
5002
5493
  generateApplicationId,
5003
5494
  generateNodeId,
5004
5495
  getFocusable,
5496
+ getResolvedTransition,
5005
5497
  globalEventBus,
5006
5498
  headingUpdate,
5007
5499
  hydrateGraph,
@@ -5012,12 +5504,14 @@ function defineConfig(config) {
5012
5504
  inspectForm,
5013
5505
  inspectGraph,
5014
5506
  inspectI18n,
5507
+ inspectInteractions,
5015
5508
  inspectResource,
5016
5509
  inspectRouter,
5017
5510
  inspectSignal,
5018
5511
  interpolate,
5019
5512
  isBatching,
5020
5513
  isComponentDefinition,
5514
+ isTransitionConfig,
5021
5515
  matchPattern,
5022
5516
  matchRoutes,
5023
5517
  maxLength,
@@ -5046,9 +5540,12 @@ function defineConfig(config) {
5046
5540
  required,
5047
5541
  resetIdCounter,
5048
5542
  resolveTag,
5543
+ resolveTransition,
5049
5544
  resource,
5050
5545
  restoreFocus,
5051
5546
  routerOutlet,
5547
+ rovingMenu,
5548
+ runElementTransition,
5052
5549
  runValidators,
5053
5550
  saveFocus,
5054
5551
  scheduleImmediate,