@jsenv/dom 0.6.1 → 0.7.1

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.
Files changed (109) hide show
  1. package/dist/jsenv_dom.js +339 -327
  2. package/package.json +2 -4
  3. package/index.js +0 -124
  4. package/src/attr/add_attribute_effect.js +0 -93
  5. package/src/attr/attributes.js +0 -32
  6. package/src/color/color_constrast.js +0 -69
  7. package/src/color/color_parsing.js +0 -319
  8. package/src/color/color_scheme.js +0 -28
  9. package/src/color/pick_light_or_dark.js +0 -34
  10. package/src/color/resolve_css_color.js +0 -60
  11. package/src/demos/3_columns_resize_demo.html +0 -84
  12. package/src/demos/3_rows_resize_demo.html +0 -89
  13. package/src/demos/aside_and_main_demo.html +0 -93
  14. package/src/demos/coordinates_demo.html +0 -450
  15. package/src/demos/document_autoscroll_demo.html +0 -517
  16. package/src/demos/drag_gesture_constraints_demo.html +0 -701
  17. package/src/demos/drag_gesture_demo.html +0 -1047
  18. package/src/demos/drag_gesture_element_to_impact_demo.html +0 -445
  19. package/src/demos/drag_reference_element_demo.html +0 -480
  20. package/src/demos/flex_details_set_demo.html +0 -302
  21. package/src/demos/flex_details_set_demo_2.html +0 -315
  22. package/src/demos/visible_rect_demo.html +0 -525
  23. package/src/element_signature.js +0 -100
  24. package/src/interaction/drag/constraint_feedback_line.js +0 -92
  25. package/src/interaction/drag/drag_constraint.js +0 -659
  26. package/src/interaction/drag/drag_debug_markers.js +0 -635
  27. package/src/interaction/drag/drag_element_positioner.js +0 -382
  28. package/src/interaction/drag/drag_gesture.js +0 -566
  29. package/src/interaction/drag/drag_resize_demo.html +0 -571
  30. package/src/interaction/drag/drag_to_move.js +0 -301
  31. package/src/interaction/drag/drag_to_resize_gesture.js +0 -68
  32. package/src/interaction/drag/drop_target_detection.js +0 -148
  33. package/src/interaction/drag/sticky_frontiers.js +0 -160
  34. package/src/interaction/event_marker.js +0 -14
  35. package/src/interaction/focus/active_element.js +0 -33
  36. package/src/interaction/focus/arrow_navigation.js +0 -599
  37. package/src/interaction/focus/element_is_focusable.js +0 -57
  38. package/src/interaction/focus/element_visibility.js +0 -111
  39. package/src/interaction/focus/find_focusable.js +0 -21
  40. package/src/interaction/focus/focus_group.js +0 -91
  41. package/src/interaction/focus/focus_group_registry.js +0 -12
  42. package/src/interaction/focus/focus_nav.js +0 -12
  43. package/src/interaction/focus/focus_nav_event_marker.js +0 -14
  44. package/src/interaction/focus/focus_trap.js +0 -105
  45. package/src/interaction/focus/tab_navigation.js +0 -128
  46. package/src/interaction/focus/tests/focus_group_skip_tab_test.html +0 -206
  47. package/src/interaction/focus/tests/tree_focus_test.html +0 -304
  48. package/src/interaction/focus/tests/tree_focus_test.jsx +0 -261
  49. package/src/interaction/focus/tests/tree_focus_test_preact.html +0 -13
  50. package/src/interaction/isolate_interactions.js +0 -161
  51. package/src/interaction/keyboard.js +0 -26
  52. package/src/interaction/scroll/capture_scroll.js +0 -47
  53. package/src/interaction/scroll/is_scrollable.js +0 -159
  54. package/src/interaction/scroll/scroll_container.js +0 -110
  55. package/src/interaction/scroll/scroll_trap.js +0 -44
  56. package/src/interaction/scroll/scrollbar_size.js +0 -20
  57. package/src/interaction/scroll/wheel_through.js +0 -138
  58. package/src/iterable_weak_set.js +0 -66
  59. package/src/position/dom_coords.js +0 -340
  60. package/src/position/offset_parent.js +0 -15
  61. package/src/position/position_fixed.js +0 -15
  62. package/src/position/position_sticky.js +0 -213
  63. package/src/position/sticky_rect.js +0 -79
  64. package/src/position/visible_rect.js +0 -486
  65. package/src/pub_sub.js +0 -31
  66. package/src/size/can_take_size.js +0 -11
  67. package/src/size/details_content_full_height.js +0 -63
  68. package/src/size/flex_details_set.js +0 -974
  69. package/src/size/get_available_height.js +0 -22
  70. package/src/size/get_available_width.js +0 -22
  71. package/src/size/get_border_sizes.js +0 -14
  72. package/src/size/get_height.js +0 -4
  73. package/src/size/get_inner_height.js +0 -15
  74. package/src/size/get_inner_width.js +0 -15
  75. package/src/size/get_margin_sizes.js +0 -10
  76. package/src/size/get_max_height.js +0 -57
  77. package/src/size/get_max_width.js +0 -47
  78. package/src/size/get_min_height.js +0 -14
  79. package/src/size/get_min_width.js +0 -14
  80. package/src/size/get_padding_sizes.js +0 -10
  81. package/src/size/get_width.js +0 -4
  82. package/src/size/hooks/use_available_height.js +0 -27
  83. package/src/size/hooks/use_available_width.js +0 -27
  84. package/src/size/hooks/use_max_height.js +0 -10
  85. package/src/size/hooks/use_max_width.js +0 -10
  86. package/src/size/hooks/use_resize_status.js +0 -62
  87. package/src/size/resize.js +0 -695
  88. package/src/size/resolve_css_size.js +0 -32
  89. package/src/style/dom_styles.js +0 -97
  90. package/src/style/style_composition.js +0 -121
  91. package/src/style/style_controller.js +0 -345
  92. package/src/style/style_default.js +0 -153
  93. package/src/style/style_default_demo.html +0 -128
  94. package/src/style/style_parsing.js +0 -375
  95. package/src/transition/demos/animation_resumption_test.xhtml +0 -500
  96. package/src/transition/demos/height_toggle_test.xhtml +0 -515
  97. package/src/transition/dom_transition.js +0 -254
  98. package/src/transition/easing.js +0 -48
  99. package/src/transition/group_transition.js +0 -261
  100. package/src/transition/transform_style_parser.js +0 -32
  101. package/src/transition/transition_playback.js +0 -366
  102. package/src/transition/transition_timeline.js +0 -79
  103. package/src/traversal.js +0 -247
  104. package/src/ui_transition/demos/content_states_transition_demo.html +0 -628
  105. package/src/ui_transition/demos/smooth_height_transition_demo.html +0 -149
  106. package/src/ui_transition/demos/transition_testing.html +0 -354
  107. package/src/ui_transition/ui_transition.js +0 -1470
  108. package/src/utils.js +0 -69
  109. package/src/value_effect.js +0 -35
package/dist/jsenv_dom.js CHANGED
@@ -61,6 +61,16 @@ import { useState, useLayoutEffect } from "preact/hooks";
61
61
  * getElementSignature(null) // Returns: "null"
62
62
  */
63
63
  const getElementSignature = (element) => {
64
+ if (Array.isArray(element)) {
65
+ if (element.length === 0) {
66
+ return "empty";
67
+ }
68
+ if (element.length === 1) {
69
+ return getElementSignature(element[0]);
70
+ }
71
+ const parent = element[0].parentNode;
72
+ return `${getElementSignature(parent)} children`;
73
+ }
64
74
  if (!element) {
65
75
  return String(element);
66
76
  }
@@ -306,11 +316,13 @@ const getAssociatedElements = (element) => {
306
316
  return null;
307
317
  };
308
318
 
309
- const getComputedStyle$1 = (element) =>
310
- elementToOwnerWindow(element).getComputedStyle(element);
319
+ const getComputedStyle$1 = (element) => {
320
+ return elementToOwnerWindow(element).getComputedStyle(element);
321
+ };
311
322
 
312
- const getStyle = (element, name) =>
313
- getComputedStyle$1(element).getPropertyValue(name);
323
+ const getStyle = (element, name) => {
324
+ return getComputedStyle$1(element).getPropertyValue(name);
325
+ };
314
326
  const setStyle = (element, name, value) => {
315
327
 
316
328
  const prevValue = element.style[name];
@@ -487,7 +499,11 @@ const normalizeStyle = (value, propertyName, context = "js") => {
487
499
  // If value is a CSS transform string, parse it first to extract the specific property
488
500
  if (typeof value === "string") {
489
501
  if (value === "none") {
490
- return undefined;
502
+ if (transformProperty.startsWith("scale")) {
503
+ return 1;
504
+ }
505
+ // translate, rotate, skew
506
+ return 0;
491
507
  }
492
508
  const parsedTransform = parseCSSTransform(value);
493
509
  return parsedTransform?.[transformProperty];
@@ -501,38 +517,55 @@ const normalizeStyle = (value, propertyName, context = "js") => {
501
517
  }
502
518
 
503
519
  if (pxProperties.includes(propertyName)) {
504
- return normalizeNumber(value, context, "px", propertyName);
520
+ return normalizeNumber(value, {
521
+ propertyName,
522
+ unit: "px",
523
+ preferedType: context === "js" ? "number" : "string",
524
+ });
505
525
  }
506
526
  if (degProperties.includes(propertyName)) {
507
- return normalizeNumber(value, context, "deg", propertyName);
527
+ return normalizeNumber(value, {
528
+ propertyName,
529
+ unit: "deg",
530
+ preferedType: "string",
531
+ });
508
532
  }
509
533
  if (unitlessProperties.includes(propertyName)) {
510
- return normalizeNumber(value, context, "", propertyName);
534
+ return normalizeNumber(value, {
535
+ propertyName,
536
+ unit: "",
537
+ preferedType: context === "js" ? "number" : "string",
538
+ });
511
539
  }
512
540
 
513
541
  return value;
514
542
  };
515
- const normalizeNumber = (value, context, unit, propertyName) => {
516
- if (context === "css") {
517
- if (typeof value === "number") {
518
- if (isNaN(value)) {
519
- console.warn(`NaN found for "${propertyName}"`);
520
- }
521
- return `${value}${unit}`;
522
- }
523
- return value;
524
- }
543
+ const normalizeNumber = (value, { unit, propertyName, preferedType }) => {
525
544
  if (typeof value === "string") {
526
- // For js context, only convert px values to numbers
527
- if (unit === "px" && value.endsWith("px")) {
545
+ // Keep strings as-is (including %, em, rem, auto, none, etc.)
546
+ if (preferedType === "string") {
547
+ return value;
548
+ }
549
+ // convert to number if possible (font-size: "12px" -> fontSize:12, opacity: "0.5" -> opacity: 0.5)
550
+ if (!unit || value.endsWith(unit)) {
528
551
  const numericValue = parseFloat(value);
529
552
  if (!isNaN(numericValue)) {
530
553
  return numericValue;
531
554
  }
532
555
  }
533
- // Keep all other strings as-is (including %, em, rem, auto, none, etc.)
534
556
  return value;
535
557
  }
558
+ if (typeof value === "number") {
559
+ if (isNaN(value)) {
560
+ console.warn(`NaN found for "${propertyName}"`);
561
+ }
562
+ if (preferedType === "number") {
563
+ return value;
564
+ }
565
+ // convert to string with unit
566
+ return `${value}${unit}`;
567
+ }
568
+
536
569
  return value;
537
570
  };
538
571
 
@@ -958,6 +991,21 @@ const onElementControllerRemoved = (element, controller) => {
958
991
  }
959
992
  };
960
993
 
994
+ /**
995
+ * Creates a style controller that can safely manage CSS styles on DOM elements.
996
+ *
997
+ * Uses Web Animations API to override styles without touching inline styles,
998
+ * allowing multiple controllers to work together and providing intelligent transform composition.
999
+ *
1000
+ * @param {string} [name="anonymous"] - Debug name for the controller
1001
+ * @returns {Object} Controller with methods: set, get, delete, getUnderlyingValue, commit, clear, clearAll
1002
+ *
1003
+ * @example
1004
+ * const controller = createStyleController("myFeature");
1005
+ * controller.set(element, { opacity: 0.5, transform: { translateX: 100 } });
1006
+ * controller.getUnderlyingValue(element, "opacity"); // Read value without controller influence
1007
+ * controller.clearAll(); // Cleanup
1008
+ */
961
1009
  const createStyleController = (name = "anonymous") => {
962
1010
  // Store element data for this controller: element -> { styles, animation }
963
1011
  const elementWeakMap = new WeakMap();
@@ -1014,11 +1062,27 @@ const createStyleController = (name = "anonymous") => {
1014
1062
  return;
1015
1063
  }
1016
1064
  const { styles, animation } = elementData;
1017
- const hasStyle = Object.hasOwn(styles, propertyName);
1018
- if (!hasStyle) {
1019
- return;
1065
+ if (propertyName.startsWith("transform.")) {
1066
+ const transformProp = propertyName.slice("transform.".length);
1067
+ const transformObject = styles.transform;
1068
+ if (!transformObject) {
1069
+ return;
1070
+ }
1071
+ const hasTransformProp = Object.hasOwn(transformObject, transformProp);
1072
+ if (!hasTransformProp) {
1073
+ return;
1074
+ }
1075
+ delete transformObject[transformProp];
1076
+ if (Object.keys(transformObject).length === 0) {
1077
+ delete styles.transform;
1078
+ }
1079
+ } else {
1080
+ const hasStyle = Object.hasOwn(styles, propertyName);
1081
+ if (!hasStyle) {
1082
+ return;
1083
+ }
1084
+ delete styles[propertyName];
1020
1085
  }
1021
- delete styles[propertyName];
1022
1086
  const isEmpty = Object.keys(styles).length === 0;
1023
1087
  // Clean up empty controller
1024
1088
  if (isEmpty) {
@@ -1220,6 +1284,44 @@ const updateAnimationStyles = (animation, styles) => {
1220
1284
  animation.pause();
1221
1285
  };
1222
1286
 
1287
+ const dormantStyleController = createStyleController("dormant");
1288
+ const getOpacity = (
1289
+ element,
1290
+ styleControllerToIgnore = dormantStyleController,
1291
+ ) => {
1292
+ return styleControllerToIgnore.getUnderlyingValue(element, "opacity");
1293
+ };
1294
+ const getTranslateX = (
1295
+ element,
1296
+ styleControllerToIgnore = dormantStyleController,
1297
+ ) => {
1298
+ return styleControllerToIgnore.getUnderlyingValue(
1299
+ element,
1300
+ "transform.translateX",
1301
+ );
1302
+ };
1303
+ const getTranslateY = (
1304
+ element,
1305
+ styleControllerToIgnore = dormantStyleController,
1306
+ ) => {
1307
+ return styleControllerToIgnore.getUnderlyingValue(
1308
+ element,
1309
+ "transform.translateY",
1310
+ );
1311
+ };
1312
+ const getWidth$1 = (
1313
+ element,
1314
+ styleControllerToIgnore = dormantStyleController,
1315
+ ) => {
1316
+ return styleControllerToIgnore.getUnderlyingValue(element, "rect.width");
1317
+ };
1318
+ const getHeight$1 = (
1319
+ element,
1320
+ styleControllerToIgnore = dormantStyleController,
1321
+ ) => {
1322
+ return styleControllerToIgnore.getUnderlyingValue(element, "rect.height");
1323
+ };
1324
+
1223
1325
  // Register the style isolator custom element once
1224
1326
  let persistentStyleIsolator = null;
1225
1327
  const getNaviStyleIsolator = () => {
@@ -7990,32 +8092,6 @@ const pickPositionRelativeTo = (
7990
8092
  };
7991
8093
  };
7992
8094
 
7993
- const parseTransform = (transform) => {
7994
- if (!transform || transform === "none") return new Map();
7995
- const transformMap = new Map();
7996
-
7997
- if (transform.startsWith("matrix(")) {
7998
- // matrix(a, b, c, d, e, f) where e is translateX and f is translateY
7999
- const values = transform
8000
- .match(/matrix\((.*?)\)/)?.[1]
8001
- .split(",")
8002
- .map(Number);
8003
- if (values) {
8004
- const translateX = values[4]; // e value from matrix
8005
- transformMap.set("translateX", { value: translateX, unit: "px" });
8006
- return transformMap;
8007
- }
8008
- }
8009
-
8010
- // For direct transform functions (when set via style.transform)
8011
- const matches = transform.matchAll(/(\w+)\(([-\d.]+)(%|px|deg)?\)/g);
8012
- for (const match of matches) {
8013
- const [, func, value, unit = ""] = match;
8014
- transformMap.set(func, { value: parseFloat(value), unit });
8015
- }
8016
- return transformMap;
8017
- };
8018
-
8019
8095
  const EASING = {
8020
8096
  LINEAR: (x) => x,
8021
8097
  EASE: (x) => {
@@ -8241,7 +8317,7 @@ const createTransition = ({
8241
8317
  transition.value = value;
8242
8318
  transition.timing = isLast ? "end" : isFirstUpdate ? "start" : "progress";
8243
8319
  isFirstUpdate = false;
8244
- executionLifecycle.update(transition);
8320
+ executionLifecycle.update?.(transition);
8245
8321
  executeUpdateCallbacks(transition);
8246
8322
  },
8247
8323
 
@@ -8263,8 +8339,8 @@ const createTransition = ({
8263
8339
  cancel: () => {
8264
8340
  if (executionLifecycle) {
8265
8341
  lifecycle.cancel(transition);
8266
- executionLifecycle.teardown();
8267
- executionLifecycle.restore();
8342
+ executionLifecycle.teardown?.();
8343
+ executionLifecycle.restore?.();
8268
8344
  }
8269
8345
  resume = null;
8270
8346
  playState = "idle";
@@ -8281,7 +8357,7 @@ const createTransition = ({
8281
8357
  }
8282
8358
  // "running" or "paused"
8283
8359
  lifecycle.finish(transition);
8284
- executionLifecycle.teardown();
8360
+ executionLifecycle.teardown?.();
8285
8361
  resume = null;
8286
8362
  playState = "finished";
8287
8363
  executeFinishCallbacks();
@@ -8493,25 +8569,7 @@ const createCallbackController = () => {
8493
8569
  return [callbacks, execute];
8494
8570
  };
8495
8571
 
8496
- installImportMetaCss(import.meta);
8497
- import.meta.css = /* css */ `
8498
- /* Transition data attributes override inline styles using CSS custom properties */
8499
- *[data-transition-opacity] {
8500
- opacity: var(--ui-transition-opacity) !important;
8501
- }
8502
-
8503
- *[data-transition-translate-x] {
8504
- transform: translateX(var(--ui-transition-translate-x)) !important;
8505
- }
8506
-
8507
- *[data-transition-width] {
8508
- width: var(--ui-transition-width) !important;
8509
- }
8510
-
8511
- *[data-transition-height] {
8512
- height: var(--ui-transition-height) !important;
8513
- }
8514
- `;
8572
+ const transitionStyleController = createStyleController("transition");
8515
8573
 
8516
8574
  const createHeightTransition = (element, to, options) => {
8517
8575
  const heightTransition = createTimelineTransition({
@@ -8523,22 +8581,13 @@ const createHeightTransition = (element, to, options) => {
8523
8581
  minDiff: 10,
8524
8582
  lifecycle: {
8525
8583
  setup: () => {
8526
- const restoreWillChange = addWillChange(element, "height");
8527
8584
  return {
8528
- from: getHeight(element),
8585
+ from: getHeight$1(element),
8529
8586
  update: ({ value }) => {
8530
- const valueWithUnit = `${value}px`;
8531
- element.setAttribute("data-transition-height", valueWithUnit);
8532
- element.style.setProperty("--ui-transition-height", valueWithUnit);
8587
+ transitionStyleController.set(element, { height: value });
8533
8588
  },
8534
8589
  teardown: () => {
8535
- element.removeAttribute("data-transition-height");
8536
- element.style.removeProperty("--ui-transition-height");
8537
- restoreWillChange();
8538
- },
8539
- restore: () => {
8540
- element.removeAttribute("data-transition-height");
8541
- element.style.removeProperty("--ui-transition-height");
8590
+ transitionStyleController.delete(element, "height");
8542
8591
  },
8543
8592
  };
8544
8593
  },
@@ -8556,22 +8605,13 @@ const createWidthTransition = (element, to, options) => {
8556
8605
  isVisual: true,
8557
8606
  lifecycle: {
8558
8607
  setup: () => {
8559
- const restoreWillChange = addWillChange(element, "width");
8560
8608
  return {
8561
- from: getWidth(element),
8609
+ from: getWidth$1(element),
8562
8610
  update: ({ value }) => {
8563
- const valueWithUnit = `${value}px`;
8564
- element.setAttribute("data-transition-width", valueWithUnit);
8565
- element.style.setProperty("--ui-transition-width", valueWithUnit);
8611
+ transitionStyleController.set(element, { width: value });
8566
8612
  },
8567
8613
  teardown: () => {
8568
- element.removeAttribute("data-transition-width");
8569
- element.style.removeProperty("--ui-transition-width");
8570
- restoreWillChange();
8571
- },
8572
- restore: () => {
8573
- element.removeAttribute("data-transition-width");
8574
- element.style.removeProperty("--ui-transition-width");
8614
+ transitionStyleController.delete(element, "width");
8575
8615
  },
8576
8616
  };
8577
8617
  },
@@ -8589,21 +8629,13 @@ const createOpacityTransition = (element, to, options = {}) => {
8589
8629
  isVisual: true,
8590
8630
  lifecycle: {
8591
8631
  setup: () => {
8592
- const restoreWillChange = addWillChange(element, "opacity");
8593
8632
  return {
8594
8633
  from: getOpacity(element),
8595
8634
  update: ({ value }) => {
8596
- element.setAttribute("data-transition-opacity", value);
8597
- element.style.setProperty("--ui-transition-opacity", value);
8635
+ transitionStyleController.set(element, { opacity: value });
8598
8636
  },
8599
8637
  teardown: () => {
8600
- element.removeAttribute("data-transition-opacity");
8601
- element.style.removeProperty("--ui-transition-opacity");
8602
- restoreWillChange();
8603
- },
8604
- restore: () => {
8605
- element.removeAttribute("data-transition-opacity");
8606
- element.style.removeProperty("--ui-transition-opacity");
8638
+ transitionStyleController.delete(element, "opacity");
8607
8639
  },
8608
8640
  };
8609
8641
  },
@@ -8611,21 +8643,11 @@ const createOpacityTransition = (element, to, options = {}) => {
8611
8643
  });
8612
8644
  return opacityTransition;
8613
8645
  };
8614
- const getOpacity = (element) => {
8615
- return parseFloat(getComputedStyle(element).opacity) || 0;
8616
- };
8617
-
8618
- const createTranslateXTransition = (element, to, options) => {
8619
- let unit = "px";
8620
- if (typeof to === "string") {
8621
- if (to.endsWith("%")) {
8622
- unit = "%";
8623
- }
8624
- to = parseFloat(to);
8625
- }
8626
8646
 
8647
+ const createTranslateXTransition = (element, to, options = {}) => {
8648
+ const { setup, ...rest } = options;
8627
8649
  const translateXTransition = createTimelineTransition({
8628
- ...options,
8650
+ ...rest,
8629
8651
  constructor: createTranslateXTransition,
8630
8652
  key: element,
8631
8653
  to,
@@ -8633,25 +8655,19 @@ const createTranslateXTransition = (element, to, options) => {
8633
8655
  isVisual: true,
8634
8656
  lifecycle: {
8635
8657
  setup: () => {
8636
- const restoreWillChange = addWillChange(element, "transform");
8658
+ const teardown = setup?.();
8637
8659
  return {
8638
8660
  from: getTranslateX(element),
8639
8661
  update: ({ value }) => {
8640
- const valueWithUnit = `${value}${unit}`;
8641
- element.setAttribute("data-transition-translate-x", valueWithUnit);
8642
- element.style.setProperty(
8643
- "--ui-transition-translate-x",
8644
- valueWithUnit,
8645
- );
8662
+ transitionStyleController.set(element, {
8663
+ transform: {
8664
+ translateX: value,
8665
+ },
8666
+ });
8646
8667
  },
8647
8668
  teardown: () => {
8648
- restoreWillChange();
8649
- element.removeAttribute("data-transition-translate-x");
8650
- element.style.removeProperty("--ui-transition-translate-x");
8651
- },
8652
- restore: () => {
8653
- element.removeAttribute("data-transition-translate-x");
8654
- element.style.removeProperty("--ui-transition-translate-x");
8669
+ teardown?.();
8670
+ transitionStyleController.delete(element, "transform.translateX");
8655
8671
  },
8656
8672
  };
8657
8673
  },
@@ -8659,48 +8675,12 @@ const createTranslateXTransition = (element, to, options) => {
8659
8675
  });
8660
8676
  return translateXTransition;
8661
8677
  };
8662
- const getTranslateX = (element) => {
8663
- const transform = getComputedStyle(element).transform;
8664
- const transformMap = parseTransform(transform);
8665
- return transformMap.get("translateX")?.value || 0;
8666
- };
8667
-
8668
- // Helper functions for getting natural (non-transition) values
8669
- const getOpacityWithoutTransition = (element) => {
8670
- const transitionOpacity = element.getAttribute("data-transition-opacity");
8671
-
8672
- // Temporarily remove transition attribute
8673
- element.removeAttribute("data-transition-opacity");
8674
-
8675
- const naturalValue = parseFloat(getComputedStyle(element).opacity) || 0;
8676
-
8677
- // Restore transition attribute if it existed
8678
- if (transitionOpacity !== null) {
8679
- element.setAttribute("data-transition-opacity", transitionOpacity);
8680
- }
8681
-
8682
- return naturalValue;
8683
- };
8684
-
8685
- const getTranslateXWithoutTransition = (element) => {
8686
- const transitionTranslateX = element.getAttribute(
8687
- "data-transition-translate-x",
8688
- );
8689
8678
 
8690
- // Temporarily remove transition attribute
8691
- element.removeAttribute("data-transition-translate-x");
8692
-
8693
- const transform = getComputedStyle(element).transform;
8694
- const transformMap = parseTransform(transform);
8695
- const naturalValue = transformMap.get("translateX")?.value || 0;
8696
-
8697
- // Restore transition attribute if it existed
8698
- if (transitionTranslateX !== null) {
8699
- element.setAttribute("data-transition-translate-x", transitionTranslateX);
8700
- }
8701
-
8702
- return naturalValue;
8703
- };
8679
+ // Helper functions for getting natural values
8680
+ const getOpacityWithoutTransition = (element) =>
8681
+ getOpacity(element, transitionStyleController);
8682
+ const getTranslateXWithoutTransition = (element) =>
8683
+ getTranslateX(element, transitionStyleController);
8704
8684
 
8705
8685
  // transition that manages multiple transitions
8706
8686
  const createGroupTransition = (transitionArray) => {
@@ -10287,13 +10267,28 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
10287
10267
 
10288
10268
  installImportMetaCss(import.meta);
10289
10269
  import.meta.css = /* css */ `
10270
+ .ui_transition_container[data-transition-overflow] {
10271
+ overflow: hidden;
10272
+ }
10273
+
10290
10274
  .ui_transition_container,
10291
10275
  .ui_transition_outer_wrapper,
10292
10276
  .ui_transition_measure_wrapper,
10293
- .ui_transition_slot {
10294
- display: inline-flex;
10277
+ .ui_transition_slot,
10278
+ .ui_transition_phase_overlay,
10279
+ .ui_transition_content_overlay {
10280
+ display: flex;
10295
10281
  width: fit-content;
10282
+ min-width: 100%;
10296
10283
  height: fit-content;
10284
+ min-height: 100%;
10285
+ flex-direction: inherit;
10286
+ align-items: inherit;
10287
+ justify-content: inherit;
10288
+ }
10289
+
10290
+ .ui_transition_measure_wrapper[data-transition-translate-x] {
10291
+ overflow: hidden;
10297
10292
  }
10298
10293
 
10299
10294
  .ui_transition_container,
@@ -10316,11 +10311,15 @@ const DEBUG = {
10316
10311
  };
10317
10312
 
10318
10313
  // Utility function to format content key states consistently for debug logs
10319
- const formatContentKeyState = (contentKey, hasChild, hasTextNode = false) => {
10314
+ const formatContentKeyState = (
10315
+ contentKey,
10316
+ hasChildren,
10317
+ hasTextNode = false,
10318
+ ) => {
10320
10319
  if (hasTextNode) {
10321
10320
  return "[text]";
10322
10321
  }
10323
- if (!hasChild) {
10322
+ if (!hasChildren) {
10324
10323
  return "[empty]";
10325
10324
  }
10326
10325
  if (contentKey === null || contentKey === undefined) {
@@ -10341,6 +10340,7 @@ const initUITransition = (container) => {
10341
10340
  ...DEBUG,
10342
10341
  transition: container.hasAttribute("data-debug-transition"),
10343
10342
  };
10343
+ const debugClones = container.hasAttribute("data-debug-clones");
10344
10344
 
10345
10345
  const debug = (type, ...args) => {
10346
10346
  if (localDebug[type]) {
@@ -10365,17 +10365,6 @@ const initUITransition = (container) => {
10365
10365
  ".ui_transition_content_overlay",
10366
10366
  );
10367
10367
 
10368
- if (!phaseOverlay) {
10369
- phaseOverlay = document.createElement("div");
10370
- phaseOverlay.className = "ui_transition_phase_overlay";
10371
- measureWrapper.appendChild(phaseOverlay);
10372
- }
10373
- if (!contentOverlay) {
10374
- contentOverlay = document.createElement("div");
10375
- contentOverlay.className = "ui_transition_content_overlay";
10376
- container.appendChild(contentOverlay);
10377
- }
10378
-
10379
10368
  if (
10380
10369
  !outerWrapper ||
10381
10370
  !measureWrapper ||
@@ -10387,6 +10376,39 @@ const initUITransition = (container) => {
10387
10376
  return { cleanup: () => {} };
10388
10377
  }
10389
10378
 
10379
+ const [teardown, addTeardown] = createPubSub();
10380
+
10381
+ {
10382
+ const transitionOverflowSet = new Set();
10383
+ const updateTransitionOverflowAttribute = () => {
10384
+ if (transitionOverflowSet.size > 0) {
10385
+ container.setAttribute("data-transition-overflow", "");
10386
+ } else {
10387
+ container.removeAttribute("data-transition-overflow");
10388
+ }
10389
+ };
10390
+ const onOverflowStart = (event) => {
10391
+ transitionOverflowSet.add(event.detail.transitionId);
10392
+ updateTransitionOverflowAttribute();
10393
+ };
10394
+ const onOverflowEnd = (event) => {
10395
+ transitionOverflowSet.delete(event.detail.transitionId);
10396
+ updateTransitionOverflowAttribute();
10397
+ };
10398
+ container.addEventListener("ui_transition_overflow_start", onOverflowStart);
10399
+ container.addEventListener("ui_transition_overflow_end", onOverflowEnd);
10400
+ addTeardown(() => {
10401
+ container.removeEventListener(
10402
+ "ui_transition_overflow_start",
10403
+ onOverflowStart,
10404
+ );
10405
+ container.removeEventListener(
10406
+ "ui_transition_overflow_end",
10407
+ onOverflowEnd,
10408
+ );
10409
+ });
10410
+ }
10411
+
10390
10412
  const transitionController = createGroupTransitionController();
10391
10413
 
10392
10414
  // Transition state
@@ -10418,7 +10440,7 @@ const initUITransition = (container) => {
10418
10440
 
10419
10441
  // Child state
10420
10442
  let lastContentKey = null;
10421
- let previousChild = null;
10443
+ let previousChildNodes = [];
10422
10444
  let isContentPhase = false; // Current state: true when showing content phase (loading/error)
10423
10445
  let wasContentPhase = false; // Previous state for comparison
10424
10446
 
@@ -10647,40 +10669,48 @@ const initUITransition = (container) => {
10647
10669
  const setupTransition = ({
10648
10670
  isPhaseTransition = false,
10649
10671
  overlay,
10650
- existingOldContents,
10651
- needsOldChildClone,
10652
- previousChild,
10653
- firstChild,
10672
+ needsOldChildNodesClone,
10673
+ previousChildNodes,
10674
+ childNodes,
10654
10675
  attributeToRemove = [],
10655
10676
  }) => {
10656
- let oldChild = null;
10657
10677
  let cleanup = () => {};
10658
- const currentTransitionElement = existingOldContents[0];
10678
+ let elementToImpact;
10659
10679
 
10660
- if (currentTransitionElement) {
10661
- oldChild = currentTransitionElement;
10680
+ if (overlay.childNodes.length > 0) {
10681
+ elementToImpact = overlay;
10682
+ cleanup = () => {
10683
+ if (!debugClones) {
10684
+ overlay.innerHTML = "";
10685
+ }
10686
+ };
10662
10687
  debug(
10663
10688
  "transition",
10664
10689
  `Continuing from current ${isPhaseTransition ? "phase" : "content"} transition element`,
10665
10690
  );
10666
- cleanup = () => oldChild.remove();
10667
- } else if (needsOldChildClone) {
10691
+ } else if (needsOldChildNodesClone) {
10668
10692
  overlay.innerHTML = "";
10669
-
10670
- // Clone the individual element for the transition
10671
- oldChild = previousChild.cloneNode(true);
10672
-
10673
- // Remove specified attributes
10674
- attributeToRemove.forEach((attr) => oldChild.removeAttribute(attr));
10675
-
10676
- oldChild.setAttribute("data-ui-transition-old", "");
10677
- overlay.appendChild(oldChild);
10693
+ for (const previousChildNode of previousChildNodes) {
10694
+ const previousChildClone = previousChildNode.cloneNode(true);
10695
+ if (previousChildClone.nodeType !== Node.TEXT_NODE) {
10696
+ for (const attrToRemove of attributeToRemove) {
10697
+ previousChildClone.removeAttribute(attrToRemove);
10698
+ }
10699
+ previousChildClone.setAttribute("data-ui-transition-clone", "");
10700
+ }
10701
+ overlay.appendChild(previousChildClone);
10702
+ }
10703
+ elementToImpact = overlay;
10704
+ cleanup = () => {
10705
+ if (!debugClones) {
10706
+ overlay.innerHTML = "";
10707
+ }
10708
+ };
10678
10709
  debug(
10679
10710
  "transition",
10680
10711
  `Cloned previous child for ${isPhaseTransition ? "phase" : "content"} transition:`,
10681
- getElementSignature(previousChild),
10712
+ getElementSignature(previousChildNodes),
10682
10713
  );
10683
- cleanup = () => oldChild.remove();
10684
10714
  } else {
10685
10715
  overlay.innerHTML = "";
10686
10716
  debug(
@@ -10696,16 +10726,15 @@ const initUITransition = (container) => {
10696
10726
  let newElement;
10697
10727
  if (isPhaseTransition) {
10698
10728
  // Phase transitions work on individual elements
10699
- oldElement = oldChild;
10700
- newElement = firstChild;
10729
+ oldElement = elementToImpact;
10730
+ newElement = slot;
10701
10731
  } else {
10702
10732
  // Content transitions work at container level and can outlive content phase changes
10703
- oldElement = oldChild ? overlay : null;
10704
- newElement = firstChild ? measureWrapper : null;
10733
+ oldElement = previousChildNodes.length ? elementToImpact : null;
10734
+ newElement = childNodes.length ? measureWrapper : null;
10705
10735
  }
10706
10736
 
10707
10737
  return {
10708
- oldChild,
10709
10738
  cleanup,
10710
10739
  oldElement,
10711
10740
  newElement,
@@ -10725,102 +10754,75 @@ const initUITransition = (container) => {
10725
10754
 
10726
10755
  try {
10727
10756
  isUpdating = true;
10728
- const firstChild = slot.children[0] || null;
10729
- const childUIName = firstChild?.getAttribute("data-ui-name");
10757
+ const childNodes = Array.from(slot.childNodes);
10730
10758
  if (localDebug.transition) {
10731
10759
  const updateLabel =
10732
- childUIName ||
10733
- (firstChild ? getElementSignature(firstChild) : "cleared/empty");
10760
+ childNodes.length === 0
10761
+ ? "cleared/empty"
10762
+ : childNodes.length === 1
10763
+ ? getElementSignature(childNodes[0])
10764
+ : getElementSignature(slot);
10734
10765
  console.group(`UI Update: ${updateLabel} (reason: ${reason})`);
10735
10766
  }
10736
10767
 
10737
- // Check for text nodes in the slot (not supported)
10738
- const hasTextNode = Array.from(slot.childNodes).some(
10739
- (node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim(),
10740
- );
10741
- if (hasTextNode) {
10742
- console.warn(
10743
- "UI Transition: Text nodes in transition slots are not supported. Please wrap text content in an element.",
10744
- { slot, textContent: slot.textContent.trim() },
10745
- );
10746
- }
10747
-
10748
- // Check for multiple elements in the slot (not supported yet)
10749
- const hasMultipleElements = slot.children.length > 1;
10750
- if (hasMultipleElements) {
10751
- console.warn(
10752
- "UI Transition: Multiple elements in transition slots are not supported yet. Please use a single container element.",
10753
- { slot, elementCount: slot.children.length },
10754
- );
10755
- }
10768
+ // Determine transition scenarios early for early registration check
10769
+ // Prepare phase info early so logging can be unified (even for early return)
10770
+ wasContentPhase = isContentPhase;
10771
+ const hadChild = previousChildNodes.length > 0;
10772
+ const hasChild = childNodes.length > 0;
10756
10773
 
10757
10774
  // Prefer data-content-key on child, fallback to slot
10758
10775
  let currentContentKey = null;
10759
10776
  let slotContentKey = slot.getAttribute("data-content-key");
10760
- let childContentKey = firstChild?.getAttribute("data-content-key");
10777
+ let childContentKey;
10778
+
10779
+ if (childNodes.length === 0) {
10780
+ childContentKey = null;
10781
+ isContentPhase = true; // empty (no child) is treated as content phase
10782
+ } else {
10783
+ for (const childNode of childNodes) {
10784
+ if (childNode.nodeType === Node.TEXT_NODE) {
10785
+ } else if (childNode.hasAttribute("data-content-key")) {
10786
+ childContentKey = childNode.getAttribute("data-content-key");
10787
+ } else if (childNode.hasAttribute("data-content-phase")) {
10788
+ isContentPhase = true;
10789
+ }
10790
+ }
10791
+ }
10761
10792
  if (childContentKey && slotContentKey) {
10762
10793
  console.warn(
10763
- "Both data-content-key found on child and ui_transition_slot. Using child value.",
10764
- { childContentKey, slotContentKey },
10794
+ `Slot and slot child both have a [data-content-key]. Slot is ${slotContentKey} and child is ${childContentKey}, using the child.`,
10765
10795
  );
10766
10796
  }
10767
10797
  currentContentKey = childContentKey || slotContentKey || null;
10768
-
10769
- // Determine transition scenarios early for early registration check
10770
- const hadChild = previousChild !== null;
10771
- const hasChild = firstChild !== null;
10772
-
10773
- // Check for text nodes in previous state (reconstruct from previousChild)
10774
- const hadTextNode =
10775
- previousChild && previousChild.nodeType === Node.TEXT_NODE;
10776
-
10777
10798
  // Compute formatted content key states ONCE per mutation (requirement: max 2 calls)
10778
10799
  const previousContentKeyState = formatContentKeyState(
10779
10800
  lastContentKey,
10780
10801
  hadChild,
10781
- hadTextNode,
10782
10802
  );
10783
10803
  const currentContentKeyState = formatContentKeyState(
10784
10804
  currentContentKey,
10785
10805
  hasChild,
10786
- hasTextNode,
10787
10806
  );
10788
-
10789
10807
  // Track previous key before any potential early registration update
10790
10808
  const prevKeyBeforeRegistration = lastContentKey;
10791
-
10792
- // Prepare phase info early so logging can be unified (even for early return)
10793
- wasContentPhase = isContentPhase;
10794
- isContentPhase = firstChild
10795
- ? firstChild.hasAttribute("data-content-phase")
10796
- : true; // empty (no child) is treated as content phase
10797
-
10798
10809
  const previousIsContentPhase = !hadChild || wasContentPhase;
10799
10810
  const currentIsContentPhase = !hasChild || isContentPhase;
10800
10811
 
10801
- // Early conceptual registration path: empty slot, text nodes, or multiple elements (no visual transition)
10802
- const shouldGiveUpEarlyAndJustRegister =
10803
- (!hadChild && !hasChild && !hasTextNode) ||
10804
- hasTextNode ||
10805
- hasMultipleElements;
10812
+ // Early conceptual registration path: empty slot
10813
+ const shouldGiveUpEarlyAndJustRegister = !hadChild && !hasChild;
10806
10814
  let earlyAction = null;
10807
10815
  if (shouldGiveUpEarlyAndJustRegister) {
10808
- if (hasTextNode) {
10809
- earlyAction = "text_nodes_unsupported";
10810
- } else if (hasMultipleElements) {
10811
- earlyAction = "multiple_elements_unsupported";
10816
+ const prevKey = prevKeyBeforeRegistration;
10817
+ const keyChanged = prevKey !== currentContentKey;
10818
+ if (!keyChanged) {
10819
+ earlyAction = "unchanged";
10820
+ } else if (prevKey === null && currentContentKey !== null) {
10821
+ earlyAction = "registered";
10822
+ } else if (prevKey !== null && currentContentKey === null) {
10823
+ earlyAction = "cleared";
10812
10824
  } else {
10813
- const prevKey = prevKeyBeforeRegistration;
10814
- const keyChanged = prevKey !== currentContentKey;
10815
- if (!keyChanged) {
10816
- earlyAction = "unchanged";
10817
- } else if (prevKey === null && currentContentKey !== null) {
10818
- earlyAction = "registered";
10819
- } else if (prevKey !== null && currentContentKey === null) {
10820
- earlyAction = "cleared";
10821
- } else {
10822
- earlyAction = "changed";
10823
- }
10825
+ earlyAction = "changed";
10824
10826
  }
10825
10827
  // Will update lastContentKey after unified logging
10826
10828
  }
@@ -10871,7 +10873,7 @@ const initUITransition = (container) => {
10871
10873
 
10872
10874
  // Handle resize observation
10873
10875
  stopResizeObserver();
10874
- if (firstChild && !isContentPhase) {
10876
+ if (hasChild && !isContentPhase) {
10875
10877
  startResizeObserver();
10876
10878
  debug("size", "Observing child resize");
10877
10879
  }
@@ -10897,11 +10899,7 @@ const initUITransition = (container) => {
10897
10899
 
10898
10900
  // Content key change when either slot or child has data-content-key and it changed
10899
10901
  let shouldDoContentTransition = false;
10900
- if (
10901
- (slot.getAttribute("data-content-key") ||
10902
- firstChild?.getAttribute("data-content-key")) &&
10903
- lastContentKey !== null
10904
- ) {
10902
+ if (currentContentKey && lastContentKey !== null) {
10905
10903
  shouldDoContentTransition = currentContentKey !== lastContentKey;
10906
10904
  }
10907
10905
 
@@ -10985,7 +10983,7 @@ const initUITransition = (container) => {
10985
10983
  }
10986
10984
 
10987
10985
  // Register state and mark initial population done
10988
- previousChild = firstChild;
10986
+ previousChildNodes = childNodes;
10989
10987
  lastContentKey = currentContentKey;
10990
10988
  hasPopulatedOnce = true;
10991
10989
  if (localDebug.transition) {
@@ -11058,11 +11056,7 @@ const initUITransition = (container) => {
11058
11056
  shouldDoContentTransitionIncludingPopulation &&
11059
11057
  !preserveOnlyContentTransition
11060
11058
  ) {
11061
- const existingOldContents = contentOverlay.querySelectorAll(
11062
- "[data-ui-transition-old]",
11063
- );
11064
11059
  const animationProgress = activeContentTransition?.progress || 0;
11065
-
11066
11060
  if (animationProgress > 0) {
11067
11061
  debug(
11068
11062
  "transition",
@@ -11076,7 +11070,6 @@ const initUITransition = (container) => {
11076
11070
  const canContinueSmoothly =
11077
11071
  activeContentTransitionType === newTransitionType &&
11078
11072
  activeContentTransition;
11079
-
11080
11073
  if (canContinueSmoothly) {
11081
11074
  debug(
11082
11075
  "transition",
@@ -11097,11 +11090,8 @@ const initUITransition = (container) => {
11097
11090
  activeContentTransition.cancel();
11098
11091
  }
11099
11092
 
11100
- const needsOldChildClone =
11101
- (contentChange || becomesEmpty) &&
11102
- previousChild &&
11103
- !existingOldContents[0];
11104
-
11093
+ const needsOldChildNodesClone =
11094
+ (contentChange || becomesEmpty) && hadChild;
11105
11095
  const duration = parseInt(
11106
11096
  container.getAttribute("data-content-transition-duration") ||
11107
11097
  CONTENT_TRANSITION_DURATION,
@@ -11114,10 +11104,9 @@ const initUITransition = (container) => {
11114
11104
  setupTransition({
11115
11105
  isPhaseTransition: false,
11116
11106
  overlay: contentOverlay,
11117
- existingOldContents,
11118
- needsOldChildClone,
11119
- previousChild,
11120
- firstChild,
11107
+ needsOldChildNodesClone,
11108
+ previousChildNodes,
11109
+ childNodes,
11121
11110
  attributeToRemove: ["data-content-key"],
11122
11111
  });
11123
11112
 
@@ -11136,9 +11125,8 @@ const initUITransition = (container) => {
11136
11125
  }
11137
11126
  }
11138
11127
 
11139
- activeContentTransition = animateTransition(
11128
+ activeContentTransition = applyTransition(
11140
11129
  transitionController,
11141
- firstChild,
11142
11130
  setupContentTransition,
11143
11131
  {
11144
11132
  duration,
@@ -11180,12 +11168,7 @@ const initUITransition = (container) => {
11180
11168
  if (shouldDoPhaseTransition) {
11181
11169
  const phaseTransitionType =
11182
11170
  container.getAttribute("data-phase-transition") || PHASE_TRANSITION;
11183
-
11184
- const existingOldPhaseContents = phaseOverlay.querySelectorAll(
11185
- "[data-ui-transition-old]",
11186
- );
11187
11171
  const phaseAnimationProgress = activePhaseTransition?.progress || 0;
11188
-
11189
11172
  if (phaseAnimationProgress > 0) {
11190
11173
  debug(
11191
11174
  "transition",
@@ -11215,10 +11198,7 @@ const initUITransition = (container) => {
11215
11198
  }
11216
11199
 
11217
11200
  const needsOldPhaseClone =
11218
- (becomesEmpty || becomesPopulated || phaseChange) &&
11219
- previousChild &&
11220
- !existingOldPhaseContents[0];
11221
-
11201
+ (becomesEmpty || becomesPopulated || phaseChange) && hadChild;
11222
11202
  const phaseDuration = parseInt(
11223
11203
  container.getAttribute("data-phase-transition-duration") ||
11224
11204
  PHASE_TRANSITION_DURATION,
@@ -11228,10 +11208,9 @@ const initUITransition = (container) => {
11228
11208
  setupTransition({
11229
11209
  isPhaseTransition: true,
11230
11210
  overlay: phaseOverlay,
11231
- existingOldContents: existingOldPhaseContents,
11232
- needsOldChildClone: needsOldPhaseClone,
11233
- previousChild,
11234
- firstChild,
11211
+ needsOldChildNodesClone: needsOldPhaseClone,
11212
+ previousChildNodes,
11213
+ childNodes,
11235
11214
  attributeToRemove: ["data-content-key", "data-content-phase"],
11236
11215
  });
11237
11216
 
@@ -11251,9 +11230,8 @@ const initUITransition = (container) => {
11251
11230
  `Starting phase transition: ${fromPhase} → ${toPhase}`,
11252
11231
  );
11253
11232
 
11254
- activePhaseTransition = animateTransition(
11233
+ activePhaseTransition = applyTransition(
11255
11234
  transitionController,
11256
- firstChild,
11257
11235
  setupPhaseTransition,
11258
11236
  {
11259
11237
  duration: phaseDuration,
@@ -11279,7 +11257,7 @@ const initUITransition = (container) => {
11279
11257
  }
11280
11258
 
11281
11259
  // Store current child for next transition
11282
- previousChild = firstChild;
11260
+ previousChildNodes = childNodes;
11283
11261
  lastContentKey = currentContentKey;
11284
11262
  if (becomesPopulated) {
11285
11263
  hasPopulatedOnce = true;
@@ -11363,11 +11341,11 @@ const initUITransition = (container) => {
11363
11341
  characterData: false,
11364
11342
  });
11365
11343
 
11366
- // Return API
11367
11344
  return {
11368
11345
  slot,
11369
11346
 
11370
11347
  cleanup: () => {
11348
+ teardown();
11371
11349
  mutationObserver.disconnect();
11372
11350
  stopResizeObserver();
11373
11351
  if (sizeTransition) {
@@ -11408,9 +11386,8 @@ const initUITransition = (container) => {
11408
11386
  };
11409
11387
  };
11410
11388
 
11411
- const animateTransition = (
11389
+ const applyTransition = (
11412
11390
  transitionController,
11413
- newChild,
11414
11391
  setupTransition,
11415
11392
  {
11416
11393
  type,
@@ -11432,7 +11409,7 @@ const animateTransition = (
11432
11409
  return null;
11433
11410
  }
11434
11411
 
11435
- const { cleanup, oldElement, newElement } = setupTransition();
11412
+ const { cleanup, oldElement, newElement, onTeardown } = setupTransition();
11436
11413
  // Use precomputed content key states (expected to be provided by caller)
11437
11414
  const fromContentKey = fromContentKeyState;
11438
11415
  const toContentKey = toContentKeyState;
@@ -11462,6 +11439,7 @@ const animateTransition = (
11462
11439
  if (transitions.length === 0) {
11463
11440
  debug("transition", "No transitions to animate, cleaning up immediately");
11464
11441
  cleanup();
11442
+ onTeardown?.();
11465
11443
  onComplete?.();
11466
11444
  return null;
11467
11445
  }
@@ -11470,6 +11448,7 @@ const animateTransition = (
11470
11448
  onFinish: () => {
11471
11449
  groupTransition.cancel();
11472
11450
  cleanup();
11451
+ onTeardown?.();
11473
11452
  onComplete?.();
11474
11453
  },
11475
11454
  });
@@ -11498,6 +11477,8 @@ const slideLeft = {
11498
11477
 
11499
11478
  return [
11500
11479
  createTranslateXTransition(oldElement, to, {
11480
+ setup: () =>
11481
+ notifyTransitionOverflow(newElement, "slide_out_old_content"),
11501
11482
  from,
11502
11483
  duration,
11503
11484
  startProgress,
@@ -11519,6 +11500,8 @@ const slideLeft = {
11519
11500
  debug("transition", "Slide in from empty:", { from, to });
11520
11501
  return [
11521
11502
  createTranslateXTransition(newElement, to, {
11503
+ setup: () =>
11504
+ notifyTransitionOverflow(newElement, "slice_in_new_content"),
11522
11505
  from,
11523
11506
  duration,
11524
11507
  startProgress,
@@ -11571,6 +11554,8 @@ const slideLeft = {
11571
11554
  // Slide old content out
11572
11555
  transitions.push(
11573
11556
  createTranslateXTransition(oldElement, -containerWidth, {
11557
+ setup: () =>
11558
+ notifyTransitionOverflow(newElement, "slide_out_old_content"),
11574
11559
  from: oldContentPosition,
11575
11560
  duration,
11576
11561
  startProgress,
@@ -11583,6 +11568,8 @@ const slideLeft = {
11583
11568
  // Slide new content in
11584
11569
  transitions.push(
11585
11570
  createTranslateXTransition(newElement, naturalNewPosition, {
11571
+ setup: () =>
11572
+ notifyTransitionOverflow(newElement, "slide_in_new_content"),
11586
11573
  from: effectiveFromPosition,
11587
11574
  duration,
11588
11575
  startProgress,
@@ -11706,4 +11693,29 @@ const crossFade = {
11706
11693
  },
11707
11694
  };
11708
11695
 
11709
- export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, addWillChange, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, createDragGestureController, createDragToMoveGestureController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getInnerHeight, getInnerWidth, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getVisuallyVisibleInfo, getWidth, initFlexDetailsSet, initFocusGroup, initPositionSticky, initUITransition, isScrollable, mergeStyles, normalizeStyles, parseCSSColor, pickLightOrDark, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, resolveCSSColor, resolveCSSSize, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyCSSColor, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
11696
+ const dispatchTransitionOverflowStartCustomEvent = (element, transitionId) => {
11697
+ const customEvent = new CustomEvent("ui_transition_overflow_start", {
11698
+ bubbles: true,
11699
+ detail: {
11700
+ transitionId,
11701
+ },
11702
+ });
11703
+ element.dispatchEvent(customEvent);
11704
+ };
11705
+ const dispatchTransitionOverflowEndCustomEvent = (element, transitionId) => {
11706
+ const customEvent = new CustomEvent("ui_transition_overflow_end", {
11707
+ bubbles: true,
11708
+ detail: {
11709
+ transitionId,
11710
+ },
11711
+ });
11712
+ element.dispatchEvent(customEvent);
11713
+ };
11714
+ const notifyTransitionOverflow = (element, transitionId) => {
11715
+ dispatchTransitionOverflowStartCustomEvent(element, transitionId);
11716
+ return () => {
11717
+ dispatchTransitionOverflowEndCustomEvent(element, transitionId);
11718
+ };
11719
+ };
11720
+
11721
+ export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, addWillChange, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, createDragGestureController, createDragToMoveGestureController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getInnerHeight, getInnerWidth, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateY, getVisuallyVisibleInfo, getWidth, initFlexDetailsSet, initFocusGroup, initPositionSticky, initUITransition, isScrollable, mergeStyles, normalizeStyles, parseCSSColor, pickLightOrDark, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, resolveCSSColor, resolveCSSSize, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyCSSColor, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };