@jsenv/dom 0.8.1 → 0.8.2

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 (2) hide show
  1. package/dist/jsenv_dom.js +54 -1467
  2. package/package.json +2 -2
package/dist/jsenv_dom.js CHANGED
@@ -74,6 +74,13 @@ const getElementSignature = (element) => {
74
74
  if (!element) {
75
75
  return String(element);
76
76
  }
77
+ if (typeof element === "string") {
78
+ return element === ""
79
+ ? "empty string"
80
+ : element.length > 10
81
+ ? `${element.slice(0, 10)}...`
82
+ : element;
83
+ }
77
84
  if (typeof element === "function") {
78
85
  const functionName = element.name;
79
86
  const functionLabel = functionName
@@ -85,6 +92,9 @@ const getElementSignature = (element) => {
85
92
  }
86
93
  return `[${functionLabel}]`;
87
94
  }
95
+ if (element.nodeType === Node.TEXT_NODE) {
96
+ return `#text(${getElementSignature(element.nodeValue)})`;
97
+ }
88
98
  if (element.props) {
89
99
  const type = element.type;
90
100
  const id = element.props.id;
@@ -8385,9 +8395,11 @@ const createTransition = ({
8385
8395
  ...rest
8386
8396
  } = {}) => {
8387
8397
  const [updateCallbacks, executeUpdateCallbacks] = createCallbackController();
8398
+ const [cancelCallbacks, executeCancelCallbacks] = createCallbackController();
8388
8399
  const [finishCallbacks, executeFinishCallbacks] = createCallbackController();
8389
8400
  const channels = {
8390
8401
  update: updateCallbacks,
8402
+ cancel: cancelCallbacks,
8391
8403
  finish: finishCallbacks,
8392
8404
  };
8393
8405
  if (onUpdate) {
@@ -8498,6 +8510,7 @@ const createTransition = ({
8498
8510
  }
8499
8511
  resume = null;
8500
8512
  playState = "idle";
8513
+ executeCancelCallbacks();
8501
8514
  },
8502
8515
 
8503
8516
  finish: () => {
@@ -8725,9 +8738,10 @@ const createCallbackController = () => {
8725
8738
 
8726
8739
  const transitionStyleController = createStyleController("transition");
8727
8740
 
8728
- const createHeightTransition = (element, to, options) => {
8741
+ const createHeightTransition = (element, to, options = {}) => {
8742
+ const { setup, ...rest } = options;
8729
8743
  const heightTransition = createTimelineTransition({
8730
- ...options,
8744
+ ...rest,
8731
8745
  constructor: createHeightTransition,
8732
8746
  key: element,
8733
8747
  to,
@@ -8735,6 +8749,7 @@ const createHeightTransition = (element, to, options) => {
8735
8749
  minDiff: 10,
8736
8750
  lifecycle: {
8737
8751
  setup: () => {
8752
+ const teardown = setup?.();
8738
8753
  return {
8739
8754
  from: getHeight$1(element),
8740
8755
  update: ({ value }) => {
@@ -8742,6 +8757,7 @@ const createHeightTransition = (element, to, options) => {
8742
8757
  },
8743
8758
  teardown: () => {
8744
8759
  transitionStyleController.delete(element, "height");
8760
+ teardown?.();
8745
8761
  },
8746
8762
  };
8747
8763
  },
@@ -8749,9 +8765,10 @@ const createHeightTransition = (element, to, options) => {
8749
8765
  });
8750
8766
  return heightTransition;
8751
8767
  };
8752
- const createWidthTransition = (element, to, options) => {
8768
+ const createWidthTransition = (element, to, options = {}) => {
8769
+ const { setup, ...rest } = options;
8753
8770
  const widthTransition = createTimelineTransition({
8754
- ...options,
8771
+ ...rest,
8755
8772
  constructor: createWidthTransition,
8756
8773
  key: element,
8757
8774
  to,
@@ -8759,6 +8776,7 @@ const createWidthTransition = (element, to, options) => {
8759
8776
  isVisual: true,
8760
8777
  lifecycle: {
8761
8778
  setup: () => {
8779
+ const teardown = setup?.();
8762
8780
  return {
8763
8781
  from: getWidth$1(element),
8764
8782
  update: ({ value }) => {
@@ -8766,6 +8784,7 @@ const createWidthTransition = (element, to, options) => {
8766
8784
  },
8767
8785
  teardown: () => {
8768
8786
  transitionStyleController.delete(element, "width");
8787
+ teardown?.();
8769
8788
  },
8770
8789
  };
8771
8790
  },
@@ -8774,8 +8793,9 @@ const createWidthTransition = (element, to, options) => {
8774
8793
  return widthTransition;
8775
8794
  };
8776
8795
  const createOpacityTransition = (element, to, options = {}) => {
8796
+ const { setup, ...rest } = options;
8777
8797
  const opacityTransition = createTimelineTransition({
8778
- ...options,
8798
+ ...rest,
8779
8799
  constructor: createOpacityTransition,
8780
8800
  key: element,
8781
8801
  to,
@@ -8783,6 +8803,7 @@ const createOpacityTransition = (element, to, options = {}) => {
8783
8803
  isVisual: true,
8784
8804
  lifecycle: {
8785
8805
  setup: () => {
8806
+ const teardown = setup?.();
8786
8807
  return {
8787
8808
  from: getOpacity(element),
8788
8809
  update: ({ value }) => {
@@ -8790,6 +8811,7 @@ const createOpacityTransition = (element, to, options = {}) => {
8790
8811
  },
8791
8812
  teardown: () => {
8792
8813
  transitionStyleController.delete(element, "opacity");
8814
+ teardown?.();
8793
8815
  },
8794
8816
  };
8795
8817
  },
@@ -8797,7 +8819,6 @@ const createOpacityTransition = (element, to, options = {}) => {
8797
8819
  });
8798
8820
  return opacityTransition;
8799
8821
  };
8800
-
8801
8822
  const createTranslateXTransition = (element, to, options = {}) => {
8802
8823
  const { setup, ...rest } = options;
8803
8824
  const translateXTransition = createTimelineTransition({
@@ -8820,8 +8841,8 @@ const createTranslateXTransition = (element, to, options = {}) => {
8820
8841
  });
8821
8842
  },
8822
8843
  teardown: () => {
8823
- teardown?.();
8824
8844
  transitionStyleController.delete(element, "transform.translateX");
8845
+ teardown?.();
8825
8846
  },
8826
8847
  };
8827
8848
  },
@@ -8835,6 +8856,10 @@ const getOpacityWithoutTransition = (element) =>
8835
8856
  getOpacity(element, transitionStyleController);
8836
8857
  const getTranslateXWithoutTransition = (element) =>
8837
8858
  getTranslateX(element, transitionStyleController);
8859
+ const getWidthWithoutTransition = (element) =>
8860
+ getWidth$1(element, transitionStyleController);
8861
+ const getHeightWithoutTransition = (element) =>
8862
+ getHeight$1(element, transitionStyleController);
8838
8863
 
8839
8864
  // transition that manages multiple transitions
8840
8865
  const createGroupTransition = (transitionArray) => {
@@ -8970,7 +8995,7 @@ const createGroupTransitionController = () => {
8970
8995
  * @returns {Object} Playback controller with play(), pause(), cancel(), etc.
8971
8996
  */
8972
8997
  animate: (transitions, options = {}) => {
8973
- const { onChange, onFinish } = options;
8998
+ const { onChange, onCancel, onFinish } = options;
8974
8999
 
8975
9000
  if (transitions.length === 0) {
8976
9001
  // No transitions to animate, call onFinish immediately
@@ -8983,7 +9008,11 @@ const createGroupTransitionController = () => {
8983
9008
  cancel: () => {},
8984
9009
  finish: () => {},
8985
9010
  playState: "idle",
8986
- channels: { update: { add: () => {} }, finish: { add: () => {} } },
9011
+ channels: {
9012
+ update: { add: () => {} },
9013
+ cancel: { add: () => {} },
9014
+ finish: { add: () => {} },
9015
+ },
8987
9016
  };
8988
9017
  }
8989
9018
 
@@ -9037,6 +9066,7 @@ const createGroupTransitionController = () => {
9037
9066
  playState: "running", // All are already running
9038
9067
  channels: {
9039
9068
  update: { add: () => {} }, // Update tracking already set up
9069
+ cancel: { add: () => {} },
9040
9070
  finish: { add: () => {} },
9041
9071
  },
9042
9072
  };
@@ -9061,6 +9091,18 @@ const createGroupTransitionController = () => {
9061
9091
  });
9062
9092
  }
9063
9093
 
9094
+ if (onCancel) {
9095
+ groupTransition.channels.cancel.add(() => {
9096
+ const changeEntries = [...newTransitions, ...updatedTransitions].map(
9097
+ (transition) => ({
9098
+ transition,
9099
+ value: transition.value,
9100
+ }),
9101
+ );
9102
+ onCancel(changeEntries);
9103
+ });
9104
+ }
9105
+
9064
9106
  // Add finish tracking
9065
9107
  if (onFinish) {
9066
9108
  groupTransition.channels.finish.add(() => {
@@ -9204,7 +9246,7 @@ const HEIGHT_TRANSITION_DURATION = 300;
9204
9246
  const ANIMATE_TOGGLE = true;
9205
9247
  const ANIMATE_RESIZE_AFTER_MUTATION = true;
9206
9248
  const ANIMATION_THRESHOLD_PX = 10; // Don't animate changes smaller than this
9207
- const DEBUG$1 = false;
9249
+ const DEBUG = false;
9208
9250
 
9209
9251
  const initFlexDetailsSet = (
9210
9252
  container,
@@ -9213,7 +9255,7 @@ const initFlexDetailsSet = (
9213
9255
  onResizableDetailsChange,
9214
9256
  onMouseResizeEnd,
9215
9257
  onRequestedSizeChange,
9216
- debug = DEBUG$1,
9258
+ debug = DEBUG,
9217
9259
  } = {},
9218
9260
  ) => {
9219
9261
  const flexDetailsSet = {
@@ -10419,1459 +10461,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
10419
10461
  };
10420
10462
  };
10421
10463
 
10422
- installImportMetaCss(import.meta);
10423
- import.meta.css = /* css */ `
10424
- .ui_transition_container[data-transition-overflow] {
10425
- overflow: hidden;
10426
- }
10427
-
10428
- .ui_transition_container,
10429
- .ui_transition_outer_wrapper,
10430
- .ui_transition_measure_wrapper,
10431
- .ui_transition_slot,
10432
- .ui_transition_phase_overlay,
10433
- .ui_transition_content_overlay {
10434
- display: flex;
10435
- width: fit-content;
10436
- min-width: 100%;
10437
- height: fit-content;
10438
- min-height: 100%;
10439
- flex-direction: inherit;
10440
- align-items: inherit;
10441
- justify-content: inherit;
10442
- border-radius: inherit;
10443
- cursor: inherit;
10444
- }
10445
-
10446
- .ui_transition_measure_wrapper[data-transition-translate-x] {
10447
- overflow: hidden;
10448
- }
10449
-
10450
- .ui_transition_container,
10451
- .ui_transition_slot {
10452
- position: relative;
10453
- }
10454
-
10455
- .ui_transition_phase_overlay,
10456
- .ui_transition_content_overlay {
10457
- position: absolute;
10458
- inset: 0;
10459
- pointer-events: none;
10460
- }
10461
- `;
10462
-
10463
- const DEBUG = {
10464
- size: false,
10465
- transition: false,
10466
- transition_updates: false,
10467
- };
10468
-
10469
- // Utility function to format content key states consistently for debug logs
10470
- const formatContentKeyState = (
10471
- contentKey,
10472
- hasChildren,
10473
- hasTextNode = false,
10474
- ) => {
10475
- if (hasTextNode) {
10476
- return "[text]";
10477
- }
10478
- if (!hasChildren) {
10479
- return "[empty]";
10480
- }
10481
- if (contentKey === null || contentKey === undefined) {
10482
- return "[unkeyed]";
10483
- }
10484
- return `[data-content-key="${contentKey}"]`;
10485
- };
10486
-
10487
- const SIZE_TRANSITION_DURATION = 150; // Default size transition duration
10488
- const SIZE_DIFF_EPSILON = 0.5; // Ignore size transition when difference below this (px)
10489
- const CONTENT_TRANSITION = "cross-fade"; // Default content transition type
10490
- const CONTENT_TRANSITION_DURATION = 300; // Default content transition duration
10491
- const PHASE_TRANSITION = "cross-fade";
10492
- const PHASE_TRANSITION_DURATION = 300; // Default phase transition duration
10493
-
10494
- const initUITransition = (container) => {
10495
- const localDebug = {
10496
- ...DEBUG,
10497
- transition: container.hasAttribute("data-debug-transition"),
10498
- };
10499
- const debugClones = container.hasAttribute("data-debug-clones");
10500
-
10501
- const debug = (type, ...args) => {
10502
- if (localDebug[type]) {
10503
- console.debug(`[${type}]`, ...args);
10504
- }
10505
- };
10506
-
10507
- if (!container.classList.contains("ui_transition_container")) {
10508
- console.error("Element must have ui_transition_container class");
10509
- return { cleanup: () => {} };
10510
- }
10511
-
10512
- const outerWrapper = container.querySelector(".ui_transition_outer_wrapper");
10513
- const measureWrapper = container.querySelector(
10514
- ".ui_transition_measure_wrapper",
10515
- );
10516
- const slot = container.querySelector(".ui_transition_slot");
10517
- let phaseOverlay = measureWrapper.querySelector(
10518
- ".ui_transition_phase_overlay",
10519
- );
10520
- let contentOverlay = container.querySelector(
10521
- ".ui_transition_content_overlay",
10522
- );
10523
-
10524
- if (
10525
- !outerWrapper ||
10526
- !measureWrapper ||
10527
- !slot ||
10528
- !phaseOverlay ||
10529
- !contentOverlay
10530
- ) {
10531
- console.error("Missing required ui-transition structure");
10532
- return { cleanup: () => {} };
10533
- }
10534
-
10535
- const [teardown, addTeardown] = createPubSub();
10536
-
10537
- {
10538
- const transitionOverflowSet = new Set();
10539
- const updateTransitionOverflowAttribute = () => {
10540
- if (transitionOverflowSet.size > 0) {
10541
- container.setAttribute("data-transition-overflow", "");
10542
- } else {
10543
- container.removeAttribute("data-transition-overflow");
10544
- }
10545
- };
10546
- const onOverflowStart = (event) => {
10547
- transitionOverflowSet.add(event.detail.transitionId);
10548
- updateTransitionOverflowAttribute();
10549
- };
10550
- const onOverflowEnd = (event) => {
10551
- transitionOverflowSet.delete(event.detail.transitionId);
10552
- updateTransitionOverflowAttribute();
10553
- };
10554
- container.addEventListener("ui_transition_overflow_start", onOverflowStart);
10555
- container.addEventListener("ui_transition_overflow_end", onOverflowEnd);
10556
- addTeardown(() => {
10557
- container.removeEventListener(
10558
- "ui_transition_overflow_start",
10559
- onOverflowStart,
10560
- );
10561
- container.removeEventListener(
10562
- "ui_transition_overflow_end",
10563
- onOverflowEnd,
10564
- );
10565
- });
10566
- }
10567
-
10568
- const transitionController = createGroupTransitionController();
10569
-
10570
- // Transition state
10571
- let activeContentTransition = null;
10572
- let activeContentTransitionType = null;
10573
- let activePhaseTransition = null;
10574
- let activePhaseTransitionType = null;
10575
- let isPaused = false;
10576
-
10577
- // Size state
10578
- let naturalContentWidth = 0; // Natural size of actual content (not loading/error states)
10579
- let naturalContentHeight = 0;
10580
- let constrainedWidth = 0; // Current constrained dimensions (what outer wrapper is set to)
10581
- let constrainedHeight = 0;
10582
- let sizeTransition = null;
10583
- let resizeObserver = null;
10584
- let sizeHoldActive = false; // Hold previous dimensions during content transitions when size transitions are disabled
10585
-
10586
- // Prevent reacting to our own constrained size changes while animating
10587
- let suppressResizeObserver = false;
10588
- let pendingResizeSync = false; // ensure one measurement after suppression ends
10589
-
10590
- // Handle size updates based on content state
10591
- let hasSizeTransitions = container.hasAttribute("data-size-transition");
10592
- const initialTransitionEnabled = container.hasAttribute(
10593
- "data-initial-transition",
10594
- );
10595
- let hasPopulatedOnce = false; // track if we've already populated once (null → something)
10596
-
10597
- // Child state
10598
- let lastContentKey = null;
10599
- let previousChildNodes = [];
10600
- let isContentPhase = false; // Current state: true when showing content phase (loading/error)
10601
- let wasContentPhase = false; // Previous state for comparison
10602
-
10603
- const measureContentSize = () => {
10604
- return [getWidth(measureWrapper), getHeight(measureWrapper)];
10605
- };
10606
-
10607
- const updateContentDimensions = () => {
10608
- const [newWidth, newHeight] = measureContentSize();
10609
- debug("size", "Content size changed:", {
10610
- width: `${naturalContentWidth} → ${newWidth}`,
10611
- height: `${naturalContentHeight} → ${newHeight}`,
10612
- });
10613
-
10614
- updateNaturalContentSize(newWidth, newHeight);
10615
-
10616
- if (sizeTransition) {
10617
- debug("size", "Updating animation target:", newHeight);
10618
- updateToSize(newWidth, newHeight);
10619
- } else {
10620
- constrainedWidth = newWidth;
10621
- constrainedHeight = newHeight;
10622
- }
10623
- };
10624
-
10625
- const stopResizeObserver = () => {
10626
- if (resizeObserver) {
10627
- resizeObserver.disconnect();
10628
- resizeObserver = null;
10629
- }
10630
- };
10631
-
10632
- const startResizeObserver = () => {
10633
- resizeObserver = new ResizeObserver(() => {
10634
- if (!hasSizeTransitions) {
10635
- return;
10636
- }
10637
- if (suppressResizeObserver) {
10638
- pendingResizeSync = true;
10639
- debug("size", "Resize ignored (suppressed during size transition)");
10640
- return;
10641
- }
10642
- updateContentDimensions();
10643
- });
10644
- resizeObserver.observe(measureWrapper);
10645
- };
10646
-
10647
- const releaseConstraints = (reason) => {
10648
- debug("size", `Releasing constraints (${reason})`);
10649
- const [beforeWidth, beforeHeight] = measureContentSize();
10650
- outerWrapper.style.width = "";
10651
- outerWrapper.style.height = "";
10652
- outerWrapper.style.overflow = "";
10653
- const [afterWidth, afterHeight] = measureContentSize();
10654
- debug("size", "Size after release:", {
10655
- width: `${beforeWidth} → ${afterWidth}`,
10656
- height: `${beforeHeight} → ${afterHeight}`,
10657
- });
10658
- constrainedWidth = afterWidth;
10659
- constrainedHeight = afterHeight;
10660
- naturalContentWidth = afterWidth;
10661
- naturalContentHeight = afterHeight;
10662
- // Defer a sync if suppression just ended; actual dispatch will come from resize observer
10663
- if (!suppressResizeObserver && pendingResizeSync) {
10664
- pendingResizeSync = false;
10665
- updateContentDimensions();
10666
- }
10667
- };
10668
-
10669
- const updateToSize = (targetWidth, targetHeight) => {
10670
- if (
10671
- constrainedWidth === targetWidth &&
10672
- constrainedHeight === targetHeight
10673
- ) {
10674
- return;
10675
- }
10676
-
10677
- const shouldAnimate = container.hasAttribute("data-size-transition");
10678
- const widthDiff = Math.abs(targetWidth - constrainedWidth);
10679
- const heightDiff = Math.abs(targetHeight - constrainedHeight);
10680
-
10681
- if (widthDiff <= SIZE_DIFF_EPSILON && heightDiff <= SIZE_DIFF_EPSILON) {
10682
- // Both diffs negligible; just sync styles if changed and bail
10683
- if (widthDiff > 0) {
10684
- outerWrapper.style.width = `${targetWidth}px`;
10685
- constrainedWidth = targetWidth;
10686
- }
10687
- if (heightDiff > 0) {
10688
- outerWrapper.style.height = `${targetHeight}px`;
10689
- constrainedHeight = targetHeight;
10690
- }
10691
- debug(
10692
- "size",
10693
- `Skip size animation entirely (diffs width:${widthDiff.toFixed(4)}px height:${heightDiff.toFixed(4)}px)`,
10694
- );
10695
- return;
10696
- }
10697
-
10698
- if (!shouldAnimate) {
10699
- // No size transitions - just update dimensions instantly
10700
- debug("size", "Updating size instantly:", {
10701
- width: `${constrainedWidth} → ${targetWidth}`,
10702
- height: `${constrainedHeight} → ${targetHeight}`,
10703
- });
10704
- suppressResizeObserver = true;
10705
- outerWrapper.style.width = `${targetWidth}px`;
10706
- outerWrapper.style.height = `${targetHeight}px`;
10707
- constrainedWidth = targetWidth;
10708
- constrainedHeight = targetHeight;
10709
- // allow any resize notifications to settle then re-enable
10710
- requestAnimationFrame(() => {
10711
- suppressResizeObserver = false;
10712
- if (pendingResizeSync) {
10713
- pendingResizeSync = false;
10714
- updateContentDimensions();
10715
- }
10716
- });
10717
- return;
10718
- }
10719
-
10720
- // Animated size transition
10721
- debug("size", "Animating size:", {
10722
- width: `${constrainedWidth} → ${targetWidth}`,
10723
- height: `${constrainedHeight} → ${targetHeight}`,
10724
- });
10725
-
10726
- const duration = parseInt(
10727
- container.getAttribute("data-size-transition-duration") ||
10728
- SIZE_TRANSITION_DURATION,
10729
- );
10730
-
10731
- outerWrapper.style.overflow = "hidden";
10732
- const transitions = [];
10733
-
10734
- // heightDiff & widthDiff already computed earlier in updateToSize when deciding to skip entirely
10735
- if (heightDiff <= SIZE_DIFF_EPSILON) {
10736
- // Treat as identical
10737
- if (heightDiff > 0) {
10738
- debug(
10739
- "size",
10740
- `Skip height transition (negligible diff ${heightDiff.toFixed(4)}px)`,
10741
- );
10742
- }
10743
- outerWrapper.style.height = `${targetHeight}px`;
10744
- constrainedHeight = targetHeight;
10745
- } else if (targetHeight !== constrainedHeight) {
10746
- transitions.push(
10747
- createHeightTransition(outerWrapper, targetHeight, {
10748
- duration,
10749
- onUpdate: ({ value }) => {
10750
- constrainedHeight = value;
10751
- },
10752
- }),
10753
- );
10754
- }
10755
-
10756
- if (widthDiff <= SIZE_DIFF_EPSILON) {
10757
- if (widthDiff > 0) {
10758
- debug(
10759
- "size",
10760
- `Skip width transition (negligible diff ${widthDiff.toFixed(4)}px)`,
10761
- );
10762
- }
10763
- outerWrapper.style.width = `${targetWidth}px`;
10764
- constrainedWidth = targetWidth;
10765
- } else if (targetWidth !== constrainedWidth) {
10766
- transitions.push(
10767
- createWidthTransition(outerWrapper, targetWidth, {
10768
- duration,
10769
- onUpdate: ({ value }) => {
10770
- constrainedWidth = value;
10771
- },
10772
- }),
10773
- );
10774
- }
10775
-
10776
- if (transitions.length > 0) {
10777
- suppressResizeObserver = true;
10778
- sizeTransition = transitionController.animate(transitions, {
10779
- onFinish: () => {
10780
- releaseConstraints("animated size transition completed");
10781
- // End suppression next frame to avoid RO loop warnings
10782
- requestAnimationFrame(() => {
10783
- suppressResizeObserver = false;
10784
- if (pendingResizeSync) {
10785
- pendingResizeSync = false;
10786
- updateContentDimensions();
10787
- }
10788
- });
10789
- },
10790
- });
10791
- sizeTransition.play();
10792
- } else {
10793
- debug(
10794
- "size",
10795
- "No size transitions created (identical or negligible differences)",
10796
- );
10797
- }
10798
- };
10799
-
10800
- const applySizeConstraints = (targetWidth, targetHeight) => {
10801
- debug("size", "Applying size constraints:", {
10802
- width: `${constrainedWidth} → ${targetWidth}`,
10803
- height: `${constrainedHeight} → ${targetHeight}`,
10804
- });
10805
-
10806
- outerWrapper.style.width = `${targetWidth}px`;
10807
- outerWrapper.style.height = `${targetHeight}px`;
10808
- outerWrapper.style.overflow = "hidden";
10809
- constrainedWidth = targetWidth;
10810
- constrainedHeight = targetHeight;
10811
- };
10812
-
10813
- const updateNaturalContentSize = (newWidth, newHeight) => {
10814
- debug("size", "Updating natural content size:", {
10815
- width: `${naturalContentWidth} → ${newWidth}`,
10816
- height: `${naturalContentHeight} → ${newHeight}`,
10817
- });
10818
- naturalContentWidth = newWidth;
10819
- naturalContentHeight = newHeight;
10820
- };
10821
-
10822
- let isUpdating = false;
10823
-
10824
- // Shared transition setup function
10825
- const setupTransition = ({
10826
- isPhaseTransition = false,
10827
- overlay,
10828
- needsOldChildNodesClone,
10829
- previousChildNodes,
10830
- childNodes,
10831
- attributeToRemove = [],
10832
- }) => {
10833
- let cleanup = () => {};
10834
- let elementToImpact;
10835
-
10836
- if (overlay.childNodes.length > 0) {
10837
- elementToImpact = overlay;
10838
- cleanup = () => {
10839
- if (!debugClones) {
10840
- overlay.innerHTML = "";
10841
- }
10842
- };
10843
- debug(
10844
- "transition",
10845
- `Continuing from current ${isPhaseTransition ? "phase" : "content"} transition element`,
10846
- );
10847
- } else if (needsOldChildNodesClone) {
10848
- overlay.innerHTML = "";
10849
- for (const previousChildNode of previousChildNodes) {
10850
- const previousChildClone = previousChildNode.cloneNode(true);
10851
- if (previousChildClone.nodeType !== Node.TEXT_NODE) {
10852
- for (const attrToRemove of attributeToRemove) {
10853
- previousChildClone.removeAttribute(attrToRemove);
10854
- }
10855
- previousChildClone.setAttribute("data-ui-transition-clone", "");
10856
- }
10857
- overlay.appendChild(previousChildClone);
10858
- }
10859
- elementToImpact = overlay;
10860
- cleanup = () => {
10861
- if (!debugClones) {
10862
- overlay.innerHTML = "";
10863
- }
10864
- };
10865
- debug(
10866
- "transition",
10867
- `Cloned previous child for ${isPhaseTransition ? "phase" : "content"} transition:`,
10868
- getElementSignature(previousChildNodes),
10869
- );
10870
- } else {
10871
- overlay.innerHTML = "";
10872
- debug(
10873
- "transition",
10874
- `No old child to clone for ${isPhaseTransition ? "phase" : "content"} transition`,
10875
- );
10876
- }
10877
-
10878
- // Determine which elements to return based on transition type:
10879
- // - Phase transitions: operate on individual elements (cross-fade between specific elements)
10880
- // - Content transitions: operate at container level (slide entire containers, outlive content phases)
10881
- let oldElement;
10882
- let newElement;
10883
- if (isPhaseTransition) {
10884
- // Phase transitions work on individual elements
10885
- oldElement = elementToImpact;
10886
- newElement = slot;
10887
- } else {
10888
- // Content transitions work at container level and can outlive content phase changes
10889
- oldElement = previousChildNodes.length ? elementToImpact : null;
10890
- newElement = childNodes.length ? measureWrapper : null;
10891
- }
10892
-
10893
- return {
10894
- cleanup,
10895
- oldElement,
10896
- newElement,
10897
- };
10898
- };
10899
-
10900
- // Initialize with current size
10901
- [constrainedWidth, constrainedHeight] = measureContentSize();
10902
-
10903
- const handleChildSlotMutation = (reason = "mutation") => {
10904
- if (isUpdating) {
10905
- debug("transition", "Preventing recursive update");
10906
- return;
10907
- }
10908
-
10909
- hasSizeTransitions = container.hasAttribute("data-size-transition");
10910
-
10911
- try {
10912
- isUpdating = true;
10913
- const childNodes = Array.from(slot.childNodes);
10914
- if (localDebug.transition) {
10915
- const updateLabel =
10916
- childNodes.length === 0
10917
- ? "cleared/empty"
10918
- : childNodes.length === 1
10919
- ? getElementSignature(childNodes[0])
10920
- : getElementSignature(slot);
10921
- console.group(`UI Update: ${updateLabel} (reason: ${reason})`);
10922
- }
10923
-
10924
- // Determine transition scenarios early for early registration check
10925
- // Prepare phase info early so logging can be unified (even for early return)
10926
- wasContentPhase = isContentPhase;
10927
- const hadChild = previousChildNodes.length > 0;
10928
- const hasChild = childNodes.length > 0;
10929
-
10930
- // Prefer data-content-key on child, fallback to slot
10931
- let currentContentKey = null;
10932
- let slotContentKey = slot.getAttribute("data-content-key");
10933
- let childContentKey;
10934
-
10935
- if (childNodes.length === 0) {
10936
- childContentKey = null;
10937
- isContentPhase = true; // empty (no child) is treated as content phase
10938
- } else {
10939
- for (const childNode of childNodes) {
10940
- if (childNode.nodeType === Node.TEXT_NODE) {
10941
- } else if (childNode.hasAttribute("data-content-key")) {
10942
- childContentKey = childNode.getAttribute("data-content-key");
10943
- } else if (childNode.hasAttribute("data-content-phase")) {
10944
- isContentPhase = true;
10945
- }
10946
- }
10947
- }
10948
- if (childContentKey && slotContentKey) {
10949
- console.warn(
10950
- `Slot and slot child both have a [data-content-key]. Slot is ${slotContentKey} and child is ${childContentKey}, using the child.`,
10951
- );
10952
- }
10953
- currentContentKey = childContentKey || slotContentKey || null;
10954
- // Compute formatted content key states ONCE per mutation (requirement: max 2 calls)
10955
- const previousContentKeyState = formatContentKeyState(
10956
- lastContentKey,
10957
- hadChild,
10958
- );
10959
- const currentContentKeyState = formatContentKeyState(
10960
- currentContentKey,
10961
- hasChild,
10962
- );
10963
- // Track previous key before any potential early registration update
10964
- const prevKeyBeforeRegistration = lastContentKey;
10965
- const previousIsContentPhase = !hadChild || wasContentPhase;
10966
- const currentIsContentPhase = !hasChild || isContentPhase;
10967
-
10968
- // Early conceptual registration path: empty slot
10969
- const shouldGiveUpEarlyAndJustRegister = !hadChild && !hasChild;
10970
- let earlyAction = null;
10971
- if (shouldGiveUpEarlyAndJustRegister) {
10972
- const prevKey = prevKeyBeforeRegistration;
10973
- const keyChanged = prevKey !== currentContentKey;
10974
- if (!keyChanged) {
10975
- earlyAction = "unchanged";
10976
- } else if (prevKey === null && currentContentKey !== null) {
10977
- earlyAction = "registered";
10978
- } else if (prevKey !== null && currentContentKey === null) {
10979
- earlyAction = "cleared";
10980
- } else {
10981
- earlyAction = "changed";
10982
- }
10983
- // Will update lastContentKey after unified logging
10984
- }
10985
-
10986
- // Decide which representation to display for previous/current in early case
10987
- const conceptualPrevDisplay =
10988
- prevKeyBeforeRegistration === null
10989
- ? "[unkeyed]"
10990
- : `[data-content-key="${prevKeyBeforeRegistration}"]`;
10991
- const conceptualCurrentDisplay =
10992
- currentContentKey === null
10993
- ? "[unkeyed]"
10994
- : `[data-content-key="${currentContentKey}"]`;
10995
- const previousDisplay = shouldGiveUpEarlyAndJustRegister
10996
- ? conceptualPrevDisplay
10997
- : previousContentKeyState;
10998
- const currentDisplay = shouldGiveUpEarlyAndJustRegister
10999
- ? conceptualCurrentDisplay
11000
- : currentContentKeyState;
11001
-
11002
- // Build a simple descriptive sentence
11003
- let contentKeysSentence = `Content key: ${previousDisplay} → ${currentDisplay}`;
11004
- debug("transition", contentKeysSentence);
11005
-
11006
- if (shouldGiveUpEarlyAndJustRegister) {
11007
- // Log decision explicitly (was previously embedded)
11008
- debug("transition", `Decision: EARLY_RETURN (${earlyAction})`);
11009
- // Register new conceptual key & return early (skip rest of transition logic)
11010
- lastContentKey = currentContentKey;
11011
- if (localDebug.transition) {
11012
- console.groupEnd();
11013
- }
11014
- return;
11015
- }
11016
- debug(
11017
- "size",
11018
- `Update triggered, size: ${constrainedWidth}x${constrainedHeight}`,
11019
- );
11020
-
11021
- if (sizeTransition) {
11022
- sizeTransition.cancel();
11023
- }
11024
-
11025
- const [newWidth, newHeight] = measureContentSize();
11026
- debug("size", `Measured size: ${newWidth}x${newHeight}`);
11027
- outerWrapper.style.width = `${constrainedWidth}px`;
11028
- outerWrapper.style.height = `${constrainedHeight}px`;
11029
-
11030
- // Handle resize observation
11031
- stopResizeObserver();
11032
- if (hasChild && !isContentPhase) {
11033
- startResizeObserver();
11034
- debug("size", "Observing child resize");
11035
- }
11036
-
11037
- // Determine transition scenarios (hadChild/hasChild already computed above for logging)
11038
-
11039
- /**
11040
- * Content Phase Logic: Why empty slots are treated as content phases
11041
- *
11042
- * When there is no child element (React component returns null), it is considered
11043
- * that the component does not render anything temporarily. This might be because:
11044
- * - The component is loading but does not have a loading state
11045
- * - The component has an error but does not have an error state
11046
- * - The component is conceptually unloaded (underlying content was deleted/is not accessible)
11047
- *
11048
- * This represents a phase of the given content: having nothing to display.
11049
- *
11050
- * We support transitions between different contents via the ability to set
11051
- * [data-content-key] on the ".ui_transition_slot". This is also useful when you want
11052
- * all children of a React component to inherit the same data-content-key without
11053
- * explicitly setting the attribute on each child element.
11054
- */
11055
-
11056
- // Content key change when either slot or child has data-content-key and it changed
11057
- let shouldDoContentTransition = false;
11058
- if (currentContentKey && lastContentKey !== null) {
11059
- shouldDoContentTransition = currentContentKey !== lastContentKey;
11060
- }
11061
-
11062
- const becomesEmpty = hadChild && !hasChild;
11063
- const becomesPopulated = !hadChild && hasChild;
11064
- const isInitialPopulationWithoutTransition =
11065
- becomesPopulated && !hasPopulatedOnce && !initialTransitionEnabled;
11066
-
11067
- // Content phase change: any transition between content/content-phase/null except when slot key changes
11068
- // This includes: null→loading, loading→content, content→loading, loading→null, etc.
11069
- const shouldDoPhaseTransition =
11070
- !shouldDoContentTransition &&
11071
- (becomesPopulated ||
11072
- becomesEmpty ||
11073
- (hadChild &&
11074
- hasChild &&
11075
- (previousIsContentPhase !== currentIsContentPhase ||
11076
- (previousIsContentPhase && currentIsContentPhase))));
11077
-
11078
- const contentChange = hadChild && hasChild && shouldDoContentTransition;
11079
- const phaseChange = hadChild && hasChild && shouldDoPhaseTransition;
11080
-
11081
- // Determine if we only need to preserve an existing content transition (no new change)
11082
- const preserveOnlyContentTransition =
11083
- activeContentTransition !== null &&
11084
- !shouldDoContentTransition &&
11085
- !shouldDoPhaseTransition &&
11086
- !becomesPopulated &&
11087
- !becomesEmpty;
11088
-
11089
- // Include becomesPopulated in content transition only if it's not a phase transition
11090
- const shouldDoContentTransitionIncludingPopulation =
11091
- shouldDoContentTransition ||
11092
- (becomesPopulated && !shouldDoPhaseTransition);
11093
-
11094
- const decisions = [];
11095
- if (shouldDoContentTransition) decisions.push("CONTENT TRANSITION");
11096
- if (shouldDoPhaseTransition) decisions.push("PHASE TRANSITION");
11097
- if (preserveOnlyContentTransition)
11098
- decisions.push("PRESERVE CONTENT TRANSITION");
11099
- if (decisions.length === 0) decisions.push("NO TRANSITION");
11100
-
11101
- debug("transition", `Decision: ${decisions.join(" + ")}`);
11102
- if (preserveOnlyContentTransition) {
11103
- const progress = (activeContentTransition.progress * 100).toFixed(1);
11104
- debug(
11105
- "transition",
11106
- `Preserving existing content transition (progress ${progress}%)`,
11107
- );
11108
- }
11109
-
11110
- // Early return optimization: if no transition decision and we are not continuing
11111
- // an existing active content transition (animationProgress > 0), we can skip
11112
- // all transition setup logic below.
11113
- if (
11114
- decisions.length === 1 &&
11115
- decisions[0] === "NO TRANSITION" &&
11116
- activeContentTransition === null &&
11117
- activePhaseTransition === null
11118
- ) {
11119
- debug(
11120
- "transition",
11121
- `Early return: no transition or continuation required`,
11122
- );
11123
- // Still ensure size logic executes below (so do not return before size alignment)
11124
- }
11125
-
11126
- // Handle initial population skip (first null → something): no content or size animations
11127
- if (isInitialPopulationWithoutTransition) {
11128
- debug(
11129
- "transition",
11130
- "Initial population detected: skipping transitions (opt-in with data-initial-transition)",
11131
- );
11132
-
11133
- // Apply sizes instantly, no animation
11134
- if (isContentPhase) {
11135
- applySizeConstraints(newWidth, newHeight);
11136
- } else {
11137
- updateNaturalContentSize(newWidth, newHeight);
11138
- releaseConstraints("initial population - skip transitions");
11139
- }
11140
-
11141
- // Register state and mark initial population done
11142
- previousChildNodes = childNodes;
11143
- lastContentKey = currentContentKey;
11144
- hasPopulatedOnce = true;
11145
- if (localDebug.transition) {
11146
- console.groupEnd();
11147
- }
11148
- return;
11149
- }
11150
-
11151
- // Plan size transition upfront; execution will happen after content/phase transitions
11152
- let sizePlan = {
11153
- action: "none",
11154
- targetWidth: constrainedWidth,
11155
- targetHeight: constrainedHeight,
11156
- };
11157
-
11158
- size_transition: {
11159
- const getTargetDimensions = () => {
11160
- if (!isContentPhase) {
11161
- return [newWidth, newHeight];
11162
- }
11163
- const shouldUseNewDimensions =
11164
- naturalContentWidth === 0 && naturalContentHeight === 0;
11165
- const targetWidth = shouldUseNewDimensions
11166
- ? newWidth
11167
- : naturalContentWidth || newWidth;
11168
- const targetHeight = shouldUseNewDimensions
11169
- ? newHeight
11170
- : naturalContentHeight || newHeight;
11171
- return [targetWidth, targetHeight];
11172
- };
11173
-
11174
- const [targetWidth, targetHeight] = getTargetDimensions();
11175
- sizePlan.targetWidth = targetWidth;
11176
- sizePlan.targetHeight = targetHeight;
11177
-
11178
- if (
11179
- targetWidth === constrainedWidth &&
11180
- targetHeight === constrainedHeight
11181
- ) {
11182
- debug("size", "No size change required");
11183
- // We'll handle potential constraint release in final section (if not holding)
11184
- break size_transition;
11185
- }
11186
-
11187
- debug("size", "Size change needed:", {
11188
- width: `${constrainedWidth} → ${targetWidth}`,
11189
- height: `${constrainedHeight} → ${targetHeight}`,
11190
- });
11191
-
11192
- if (isContentPhase) {
11193
- // Content phases (loading/error) always use size constraints for consistent sizing
11194
- sizePlan.action = hasSizeTransitions ? "animate" : "applyConstraints";
11195
- } else {
11196
- // Actual content: update natural content dimensions for future content phases
11197
- updateNaturalContentSize(targetWidth, targetHeight);
11198
- sizePlan.action = hasSizeTransitions ? "animate" : "release";
11199
- }
11200
- }
11201
-
11202
- content_transition: {
11203
- // Handle content transitions (slide-left, cross-fade for content key changes)
11204
- if (
11205
- decisions.length === 1 &&
11206
- decisions[0] === "NO TRANSITION" &&
11207
- activeContentTransition === null &&
11208
- activePhaseTransition === null
11209
- ) {
11210
- // Skip creating any new transitions entirely
11211
- } else if (
11212
- shouldDoContentTransitionIncludingPopulation &&
11213
- !preserveOnlyContentTransition
11214
- ) {
11215
- const animationProgress = activeContentTransition?.progress || 0;
11216
- if (animationProgress > 0) {
11217
- debug(
11218
- "transition",
11219
- `Preserving content transition progress: ${(animationProgress * 100).toFixed(1)}%`,
11220
- );
11221
- }
11222
-
11223
- const newTransitionType =
11224
- container.getAttribute("data-content-transition") ||
11225
- CONTENT_TRANSITION;
11226
- const canContinueSmoothly =
11227
- activeContentTransitionType === newTransitionType &&
11228
- activeContentTransition;
11229
- if (canContinueSmoothly) {
11230
- debug(
11231
- "transition",
11232
- "Continuing with same content transition type (restarting due to actual change)",
11233
- );
11234
- activeContentTransition.cancel();
11235
- } else if (
11236
- activeContentTransition &&
11237
- activeContentTransitionType !== newTransitionType
11238
- ) {
11239
- debug(
11240
- "transition",
11241
- "Different content transition type, keeping both",
11242
- `${activeContentTransitionType} → ${newTransitionType}`,
11243
- );
11244
- } else if (activeContentTransition) {
11245
- debug("transition", "Cancelling current content transition");
11246
- activeContentTransition.cancel();
11247
- }
11248
-
11249
- const needsOldChildNodesClone =
11250
- (contentChange || becomesEmpty) && hadChild;
11251
- const duration = parseInt(
11252
- container.getAttribute("data-content-transition-duration") ||
11253
- CONTENT_TRANSITION_DURATION,
11254
- );
11255
- const type =
11256
- container.getAttribute("data-content-transition") ||
11257
- CONTENT_TRANSITION;
11258
-
11259
- const setupContentTransition = () =>
11260
- setupTransition({
11261
- isPhaseTransition: false,
11262
- overlay: contentOverlay,
11263
- needsOldChildNodesClone,
11264
- previousChildNodes,
11265
- childNodes,
11266
- attributeToRemove: ["data-content-key"],
11267
- });
11268
-
11269
- // If size transitions are disabled and the new content is smaller,
11270
- // hold the previous size to avoid cropping during the transition.
11271
- if (!hasSizeTransitions) {
11272
- const willShrinkWidth = constrainedWidth > newWidth;
11273
- const willShrinkHeight = constrainedHeight > newHeight;
11274
- sizeHoldActive = willShrinkWidth || willShrinkHeight;
11275
- if (sizeHoldActive) {
11276
- debug(
11277
- "size",
11278
- `Holding previous size during content transition: ${constrainedWidth}x${constrainedHeight}`,
11279
- );
11280
- applySizeConstraints(constrainedWidth, constrainedHeight);
11281
- }
11282
- }
11283
-
11284
- activeContentTransition = applyTransition(
11285
- transitionController,
11286
- setupContentTransition,
11287
- {
11288
- duration,
11289
- type,
11290
- animationProgress,
11291
- isPhaseTransition: false,
11292
- fromContentKeyState: previousContentKeyState,
11293
- toContentKeyState: currentContentKeyState,
11294
- onComplete: () => {
11295
- activeContentTransition = null;
11296
- activeContentTransitionType = null;
11297
- if (sizeHoldActive) {
11298
- // Release the hold after the content transition completes
11299
- releaseConstraints(
11300
- "content transition completed - release size hold",
11301
- );
11302
- sizeHoldActive = false;
11303
- }
11304
- },
11305
- debug,
11306
- },
11307
- );
11308
-
11309
- if (activeContentTransition) {
11310
- activeContentTransition.play();
11311
- }
11312
- activeContentTransitionType = type;
11313
- } else if (
11314
- !shouldDoContentTransition &&
11315
- !preserveOnlyContentTransition
11316
- ) {
11317
- // Clean up content overlay if no content transition needed and nothing to preserve
11318
- contentOverlay.innerHTML = "";
11319
- activeContentTransition = null;
11320
- activeContentTransitionType = null;
11321
- }
11322
-
11323
- // Handle phase transitions (cross-fade for content phase changes)
11324
- if (shouldDoPhaseTransition) {
11325
- const phaseTransitionType =
11326
- container.getAttribute("data-phase-transition") || PHASE_TRANSITION;
11327
- const phaseAnimationProgress = activePhaseTransition?.progress || 0;
11328
- if (phaseAnimationProgress > 0) {
11329
- debug(
11330
- "transition",
11331
- `Preserving phase transition progress: ${(phaseAnimationProgress * 100).toFixed(1)}%`,
11332
- );
11333
- }
11334
-
11335
- const canContinueSmoothly =
11336
- activePhaseTransitionType === phaseTransitionType &&
11337
- activePhaseTransition;
11338
-
11339
- if (canContinueSmoothly) {
11340
- debug("transition", "Continuing with same phase transition type");
11341
- activePhaseTransition.cancel();
11342
- } else if (
11343
- activePhaseTransition &&
11344
- activePhaseTransitionType !== phaseTransitionType
11345
- ) {
11346
- debug(
11347
- "transition",
11348
- "Different phase transition type, keeping both",
11349
- `${activePhaseTransitionType} → ${phaseTransitionType}`,
11350
- );
11351
- } else if (activePhaseTransition) {
11352
- debug("transition", "Cancelling current phase transition");
11353
- activePhaseTransition.cancel();
11354
- }
11355
-
11356
- const needsOldPhaseClone =
11357
- (becomesEmpty || becomesPopulated || phaseChange) && hadChild;
11358
- const phaseDuration = parseInt(
11359
- container.getAttribute("data-phase-transition-duration") ||
11360
- PHASE_TRANSITION_DURATION,
11361
- );
11362
-
11363
- const setupPhaseTransition = () =>
11364
- setupTransition({
11365
- isPhaseTransition: true,
11366
- overlay: phaseOverlay,
11367
- needsOldChildNodesClone: needsOldPhaseClone,
11368
- previousChildNodes,
11369
- childNodes,
11370
- attributeToRemove: ["data-content-key", "data-content-phase"],
11371
- });
11372
-
11373
- const fromPhase = !hadChild
11374
- ? "null"
11375
- : wasContentPhase
11376
- ? "content-phase"
11377
- : "content";
11378
- const toPhase = !hasChild
11379
- ? "null"
11380
- : isContentPhase
11381
- ? "content-phase"
11382
- : "content";
11383
-
11384
- debug(
11385
- "transition",
11386
- `Starting phase transition: ${fromPhase} → ${toPhase}`,
11387
- );
11388
-
11389
- activePhaseTransition = applyTransition(
11390
- transitionController,
11391
- setupPhaseTransition,
11392
- {
11393
- duration: phaseDuration,
11394
- type: phaseTransitionType,
11395
- animationProgress: phaseAnimationProgress,
11396
- isPhaseTransition: true,
11397
- fromContentKeyState: previousContentKeyState,
11398
- toContentKeyState: currentContentKeyState,
11399
- onComplete: () => {
11400
- activePhaseTransition = null;
11401
- activePhaseTransitionType = null;
11402
- debug("transition", "Phase transition complete");
11403
- },
11404
- debug,
11405
- },
11406
- );
11407
-
11408
- if (activePhaseTransition) {
11409
- activePhaseTransition.play();
11410
- }
11411
- activePhaseTransitionType = phaseTransitionType;
11412
- }
11413
- }
11414
-
11415
- // Store current child for next transition
11416
- previousChildNodes = childNodes;
11417
- lastContentKey = currentContentKey;
11418
- if (becomesPopulated) {
11419
- hasPopulatedOnce = true;
11420
- }
11421
-
11422
- // Execute planned size action, unless holding size during a content transition
11423
- if (!sizeHoldActive) {
11424
- if (
11425
- sizePlan.targetWidth === constrainedWidth &&
11426
- sizePlan.targetHeight === constrainedHeight
11427
- ) {
11428
- // no size changes planned; possibly release constraints
11429
- if (!isContentPhase) {
11430
- releaseConstraints("no size change needed");
11431
- }
11432
- } else if (sizePlan.action === "animate") {
11433
- updateToSize(sizePlan.targetWidth, sizePlan.targetHeight);
11434
- } else if (sizePlan.action === "applyConstraints") {
11435
- applySizeConstraints(sizePlan.targetWidth, sizePlan.targetHeight);
11436
- } else if (sizePlan.action === "release") {
11437
- releaseConstraints("actual content - no size transitions needed");
11438
- }
11439
- }
11440
- } finally {
11441
- isUpdating = false;
11442
- if (localDebug.transition) {
11443
- console.groupEnd();
11444
- }
11445
- }
11446
- };
11447
-
11448
- // Run once at init to process current slot content (warnings, sizing, transitions)
11449
- handleChildSlotMutation("init");
11450
-
11451
- // Watch for child changes and attribute changes on children
11452
- const mutationObserver = new MutationObserver((mutations) => {
11453
- let childListMutation = false;
11454
- const attributeMutationSet = new Set();
11455
-
11456
- for (const mutation of mutations) {
11457
- if (mutation.type === "childList") {
11458
- childListMutation = true;
11459
- continue;
11460
- }
11461
- if (mutation.type === "attributes") {
11462
- const { attributeName, target } = mutation;
11463
- if (
11464
- attributeName === "data-content-key" ||
11465
- attributeName === "data-content-phase"
11466
- ) {
11467
- attributeMutationSet.add(attributeName);
11468
- debug(
11469
- "transition",
11470
- `Attribute change detected: ${attributeName} on`,
11471
- getElementSignature(target),
11472
- );
11473
- }
11474
- }
11475
- }
11476
-
11477
- if (!childListMutation && attributeMutationSet.size === 0) {
11478
- return;
11479
- }
11480
- const reasonParts = [];
11481
- if (childListMutation) {
11482
- reasonParts.push("childList change");
11483
- }
11484
- if (attributeMutationSet.size) {
11485
- for (const attr of attributeMutationSet) {
11486
- reasonParts.push(`[${attr}] change`);
11487
- }
11488
- }
11489
- const reason = reasonParts.join("+");
11490
- handleChildSlotMutation(reason);
11491
- });
11492
-
11493
- mutationObserver.observe(slot, {
11494
- childList: true,
11495
- attributes: true,
11496
- attributeFilter: ["data-content-key", "data-content-phase"],
11497
- characterData: false,
11498
- });
11499
-
11500
- return {
11501
- slot,
11502
-
11503
- cleanup: () => {
11504
- teardown();
11505
- mutationObserver.disconnect();
11506
- stopResizeObserver();
11507
- if (sizeTransition) {
11508
- sizeTransition.cancel();
11509
- }
11510
- if (activeContentTransition) {
11511
- activeContentTransition.cancel();
11512
- }
11513
- if (activePhaseTransition) {
11514
- activePhaseTransition.cancel();
11515
- }
11516
- },
11517
- pause: () => {
11518
- if (activeContentTransition?.pause) {
11519
- activeContentTransition.pause();
11520
- isPaused = true;
11521
- }
11522
- if (activePhaseTransition?.pause) {
11523
- activePhaseTransition.pause();
11524
- isPaused = true;
11525
- }
11526
- },
11527
- resume: () => {
11528
- if (activeContentTransition?.play && isPaused) {
11529
- activeContentTransition.play();
11530
- isPaused = false;
11531
- }
11532
- if (activePhaseTransition?.play && isPaused) {
11533
- activePhaseTransition.play();
11534
- isPaused = false;
11535
- }
11536
- },
11537
- getState: () => ({
11538
- isPaused,
11539
- contentTransitionInProgress: activeContentTransition !== null,
11540
- phaseTransitionInProgress: activePhaseTransition !== null,
11541
- }),
11542
- };
11543
- };
11544
-
11545
- const applyTransition = (
11546
- transitionController,
11547
- setupTransition,
11548
- {
11549
- type,
11550
- duration,
11551
- animationProgress = 0,
11552
- isPhaseTransition,
11553
- onComplete,
11554
- fromContentKeyState,
11555
- toContentKeyState,
11556
- debug,
11557
- },
11558
- ) => {
11559
- let transitionType;
11560
- if (type === "cross-fade") {
11561
- transitionType = crossFade;
11562
- } else if (type === "slide-left") {
11563
- transitionType = slideLeft;
11564
- } else {
11565
- return null;
11566
- }
11567
-
11568
- const { cleanup, oldElement, newElement, onTeardown } = setupTransition();
11569
- // Use precomputed content key states (expected to be provided by caller)
11570
- const fromContentKey = fromContentKeyState;
11571
- const toContentKey = toContentKeyState;
11572
-
11573
- debug("transition", "Setting up animation:", {
11574
- type,
11575
- from: fromContentKey,
11576
- to: toContentKey,
11577
- progress: `${(animationProgress * 100).toFixed(1)}%`,
11578
- });
11579
-
11580
- const remainingDuration = Math.max(100, duration * (1 - animationProgress));
11581
- debug("transition", `Animation duration: ${remainingDuration}ms`);
11582
-
11583
- const transitions = transitionType.apply(oldElement, newElement, {
11584
- duration: remainingDuration,
11585
- startProgress: animationProgress,
11586
- isPhaseTransition,
11587
- debug,
11588
- });
11589
-
11590
- debug(
11591
- "transition",
11592
- `Created ${transitions.length} transition(s) for animation`,
11593
- );
11594
-
11595
- if (transitions.length === 0) {
11596
- debug("transition", "No transitions to animate, cleaning up immediately");
11597
- cleanup();
11598
- onTeardown?.();
11599
- onComplete?.();
11600
- return null;
11601
- }
11602
-
11603
- const groupTransition = transitionController.animate(transitions, {
11604
- onFinish: () => {
11605
- groupTransition.cancel();
11606
- cleanup();
11607
- onTeardown?.();
11608
- onComplete?.();
11609
- },
11610
- });
11611
-
11612
- return groupTransition;
11613
- };
11614
-
11615
- const slideLeft = {
11616
- name: "slide-left",
11617
- apply: (
11618
- oldElement,
11619
- newElement,
11620
- { duration, startProgress = 0, isPhaseTransition = false, debug },
11621
- ) => {
11622
- if (!oldElement && !newElement) {
11623
- return [];
11624
- }
11625
-
11626
- if (!newElement) {
11627
- // Content -> Empty (slide out left only)
11628
- const currentPosition = getTranslateX(oldElement);
11629
- const containerWidth = getInnerWidth(oldElement.parentElement);
11630
- const from = currentPosition;
11631
- const to = -containerWidth;
11632
- debug("transition", "Slide out to empty:", { from, to });
11633
-
11634
- return [
11635
- createTranslateXTransition(oldElement, to, {
11636
- setup: () =>
11637
- notifyTransitionOverflow(newElement, "slide_out_old_content"),
11638
- from,
11639
- duration,
11640
- startProgress,
11641
- onUpdate: ({ value, timing }) => {
11642
- debug("transition_updates", "Slide out progress:", value);
11643
- if (timing === "end") {
11644
- debug("transition", "Slide out complete");
11645
- }
11646
- },
11647
- }),
11648
- ];
11649
- }
11650
-
11651
- if (!oldElement) {
11652
- // Empty -> Content (slide in from right)
11653
- const containerWidth = getInnerWidth(newElement.parentElement);
11654
- const from = containerWidth; // Start from right edge for slide-in effect
11655
- const to = getTranslateXWithoutTransition(newElement);
11656
- debug("transition", "Slide in from empty:", { from, to });
11657
- return [
11658
- createTranslateXTransition(newElement, to, {
11659
- setup: () =>
11660
- notifyTransitionOverflow(newElement, "slice_in_new_content"),
11661
- from,
11662
- duration,
11663
- startProgress,
11664
- onUpdate: ({ value, timing }) => {
11665
- debug("transition_updates", "Slide in progress:", value);
11666
- if (timing === "end") {
11667
- debug("transition", "Slide in complete");
11668
- }
11669
- },
11670
- }),
11671
- ];
11672
- }
11673
-
11674
- // Content -> Content (slide left)
11675
- // The old content (oldElement) slides OUT to the left
11676
- // The new content (newElement) slides IN from the right
11677
-
11678
- // Get positions for the slide animation
11679
- const containerWidth = getInnerWidth(newElement.parentElement);
11680
- const oldContentPosition = getTranslateX(oldElement);
11681
- const currentNewPosition = getTranslateX(newElement);
11682
- const naturalNewPosition = getTranslateXWithoutTransition(newElement);
11683
-
11684
- // For smooth continuation: if newElement is mid-transition,
11685
- // calculate new position to maintain seamless sliding
11686
- let startNewPosition;
11687
- if (currentNewPosition !== 0 && naturalNewPosition === 0) {
11688
- startNewPosition = currentNewPosition + containerWidth;
11689
- debug(
11690
- "transition",
11691
- "Calculated seamless position:",
11692
- `${currentNewPosition} + ${containerWidth} = ${startNewPosition}`,
11693
- );
11694
- } else {
11695
- startNewPosition = naturalNewPosition || containerWidth;
11696
- }
11697
-
11698
- // For phase transitions, force new content to start from right edge for proper slide-in
11699
- const effectiveFromPosition = isPhaseTransition
11700
- ? containerWidth
11701
- : startNewPosition;
11702
-
11703
- debug("transition", "Slide transition:", {
11704
- oldContent: `${oldContentPosition} → ${-containerWidth}`,
11705
- newContent: `${effectiveFromPosition} → ${naturalNewPosition}`,
11706
- });
11707
-
11708
- const transitions = [];
11709
-
11710
- // Slide old content out
11711
- transitions.push(
11712
- createTranslateXTransition(oldElement, -containerWidth, {
11713
- setup: () =>
11714
- notifyTransitionOverflow(newElement, "slide_out_old_content"),
11715
- from: oldContentPosition,
11716
- duration,
11717
- startProgress,
11718
- onUpdate: ({ value }) => {
11719
- debug("transition_updates", "Old content slide out:", value);
11720
- },
11721
- }),
11722
- );
11723
-
11724
- // Slide new content in
11725
- transitions.push(
11726
- createTranslateXTransition(newElement, naturalNewPosition, {
11727
- setup: () =>
11728
- notifyTransitionOverflow(newElement, "slide_in_new_content"),
11729
- from: effectiveFromPosition,
11730
- duration,
11731
- startProgress,
11732
- onUpdate: ({ value, timing }) => {
11733
- debug("transition_updates", "New content slide in:", value);
11734
- if (timing === "end") {
11735
- debug("transition", "Slide complete");
11736
- }
11737
- },
11738
- }),
11739
- );
11740
-
11741
- return transitions;
11742
- },
11743
- };
11744
-
11745
- const crossFade = {
11746
- name: "cross-fade",
11747
- apply: (
11748
- oldElement,
11749
- newElement,
11750
- { duration, startProgress = 0, isPhaseTransition = false, debug },
11751
- ) => {
11752
- if (!oldElement && !newElement) {
11753
- return [];
11754
- }
11755
-
11756
- if (!newElement) {
11757
- // Content -> Empty (fade out only)
11758
- const from = getOpacity(oldElement);
11759
- const to = 0;
11760
- debug("transition", "Fade out to empty:", { from, to });
11761
- return [
11762
- createOpacityTransition(oldElement, to, {
11763
- from,
11764
- duration,
11765
- startProgress,
11766
- onUpdate: ({ value, timing }) => {
11767
- debug("transition_updates", "Content fade out:", value.toFixed(3));
11768
- if (timing === "end") {
11769
- debug("transition", "Fade out complete");
11770
- }
11771
- },
11772
- }),
11773
- ];
11774
- }
11775
-
11776
- if (!oldElement) {
11777
- // Empty -> Content (fade in only)
11778
- const from = 0;
11779
- const to = getOpacityWithoutTransition(newElement);
11780
- debug("transition", "Fade in from empty:", { from, to });
11781
- return [
11782
- createOpacityTransition(newElement, to, {
11783
- from,
11784
- duration,
11785
- startProgress,
11786
- onUpdate: ({ value, timing }) => {
11787
- debug("transition_updates", "Fade in progress:", value.toFixed(3));
11788
- if (timing === "end") {
11789
- debug("transition", "Fade in complete");
11790
- }
11791
- },
11792
- }),
11793
- ];
11794
- }
11795
-
11796
- // Content -> Content (cross-fade)
11797
- // Get current opacity for both elements
11798
- const oldOpacity = getOpacity(oldElement);
11799
- const newOpacity = getOpacity(newElement);
11800
- const newNaturalOpacity = getOpacityWithoutTransition(newElement);
11801
-
11802
- // For phase transitions, always start new content from 0 for clean visual transition
11803
- // For content transitions, check for ongoing transitions to continue smoothly
11804
- let effectiveFromOpacity;
11805
- if (isPhaseTransition) {
11806
- effectiveFromOpacity = 0; // Always start fresh for phase transitions (loading → content, etc.)
11807
- } else {
11808
- // For content transitions: if new element has ongoing opacity transition
11809
- // (indicated by non-zero opacity when natural opacity is different),
11810
- // start from current opacity to continue smoothly, otherwise start from 0
11811
- const hasOngoingTransition =
11812
- newOpacity !== newNaturalOpacity && newOpacity > 0;
11813
- effectiveFromOpacity = hasOngoingTransition ? newOpacity : 0;
11814
- }
11815
-
11816
- debug("transition", "Cross-fade transition:", {
11817
- oldOpacity: `${oldOpacity} → 0`,
11818
- newOpacity: `${effectiveFromOpacity} → ${newNaturalOpacity}`,
11819
- isPhaseTransition,
11820
- });
11821
-
11822
- return [
11823
- createOpacityTransition(oldElement, 0, {
11824
- from: oldOpacity,
11825
- duration,
11826
- startProgress,
11827
- onUpdate: ({ value }) => {
11828
- if (value > 0) {
11829
- debug(
11830
- "transition_updates",
11831
- "Old content fade out:",
11832
- value.toFixed(3),
11833
- );
11834
- }
11835
- },
11836
- }),
11837
- createOpacityTransition(newElement, newNaturalOpacity, {
11838
- from: effectiveFromOpacity,
11839
- duration,
11840
- startProgress: isPhaseTransition ? 0 : startProgress, // Phase transitions: new content always starts fresh
11841
- onUpdate: ({ value, timing }) => {
11842
- debug("transition_updates", "New content fade in:", value.toFixed(3));
11843
- if (timing === "end") {
11844
- debug("transition", "Cross-fade complete");
11845
- }
11846
- },
11847
- }),
11848
- ];
11849
- },
11850
- };
11851
-
11852
- const dispatchTransitionOverflowStartCustomEvent = (element, transitionId) => {
11853
- const customEvent = new CustomEvent("ui_transition_overflow_start", {
11854
- bubbles: true,
11855
- detail: {
11856
- transitionId,
11857
- },
11858
- });
11859
- element.dispatchEvent(customEvent);
11860
- };
11861
- const dispatchTransitionOverflowEndCustomEvent = (element, transitionId) => {
11862
- const customEvent = new CustomEvent("ui_transition_overflow_end", {
11863
- bubbles: true,
11864
- detail: {
11865
- transitionId,
11866
- },
11867
- });
11868
- element.dispatchEvent(customEvent);
11869
- };
11870
- const notifyTransitionOverflow = (element, transitionId) => {
11871
- dispatchTransitionOverflowStartCustomEvent(element, transitionId);
11872
- return () => {
11873
- dispatchTransitionOverflowEndCustomEvent(element, transitionId);
11874
- };
11875
- };
11876
-
11877
- 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, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateY, getVisuallyVisibleInfo, getWidth, initFlexDetailsSet, initFocusGroup, initPositionSticky, initUITransition, isScrollable, mergeTwoStyles, normalizeStyle, normalizeStyles, parseCSSColor, pickLightOrDark, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, resolveCSSColor, resolveCSSSize, resolveColorLuminance, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyCSSColor, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
10464
+ export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, addWillChange, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, createDragGestureController, createDragToMoveGestureController, createGroupTransitionController, 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, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, initFlexDetailsSet, initFocusGroup, initPositionSticky, isScrollable, mergeTwoStyles, normalizeStyle, normalizeStyles, parseCSSColor, pickLightOrDark, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, resolveCSSColor, resolveCSSSize, resolveColorLuminance, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyCSSColor, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };