@jsenv/navi 0.12.6 → 0.12.8

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.
@@ -1,5 +1,5 @@
1
1
  import { installImportMetaCss } from "./jsenv_navi_side_effects.js";
2
- import { createIterableWeakSet, createPubSub, createValueEffect, createStyleController, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, activeElementSignal, canInterceptKeys, initUITransition, getElementSignature, normalizeStyle, mergeTwoStyles, appendStyles, normalizeStyles, resolveCSSSize, findBefore, findAfter, initFocusGroup, elementIsFocusable, pickLightOrDark, resolveColorLuminance, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
2
+ import { createIterableWeakSet, createPubSub, createValueEffect, createStyleController, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, activeElementSignal, canInterceptKeys, createGroupTransitionController, getWidthWithoutTransition, getHeightWithoutTransition, createWidthTransition, createHeightTransition, getElementSignature, getTranslateX, getInnerWidth, createTranslateXTransition, getTranslateXWithoutTransition, getOpacity, createOpacityTransition, getOpacityWithoutTransition, normalizeStyle, mergeTwoStyles, appendStyles, normalizeStyles, resolveCSSSize, findBefore, findAfter, initFocusGroup, elementIsFocusable, pickLightOrDark, resolveColorLuminance, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
3
3
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
4
4
  import { effect, signal, computed, batch, useSignal } from "@preact/signals";
5
5
  import { useEffect, useRef, useCallback, useContext, useState, useLayoutEffect, useMemo, useErrorBoundary, useImperativeHandle, useId } from "preact/hooks";
@@ -614,9 +614,9 @@ const weakEffect = (values, callback) => {
614
614
  return dispose;
615
615
  };
616
616
 
617
- let DEBUG$2 = false;
617
+ let DEBUG$3 = false;
618
618
  const enableDebugActions = () => {
619
- DEBUG$2 = true;
619
+ DEBUG$3 = true;
620
620
  };
621
621
 
622
622
  let dispatchActions = (params) => {
@@ -680,7 +680,7 @@ const prerunProtectionRegistry = (() => {
680
680
  if (protection) {
681
681
  clearTimeout(protection.timeoutId);
682
682
  protectedActionMap.delete(action);
683
- if (DEBUG$2) {
683
+ if (DEBUG$3) {
684
684
  const elapsed = Date.now() - protection.timestamp;
685
685
  console.debug(`"${action}": GC protection removed after ${elapsed}ms`);
686
686
  }
@@ -698,7 +698,7 @@ const prerunProtectionRegistry = (() => {
698
698
  const timestamp = Date.now();
699
699
  const timeoutId = setTimeout(() => {
700
700
  unprotect(action);
701
- if (DEBUG$2) {
701
+ if (DEBUG$3) {
702
702
  console.debug(
703
703
  `"${action}": prerun protection expired after ${PROTECTION_DURATION}ms`,
704
704
  );
@@ -707,7 +707,7 @@ const prerunProtectionRegistry = (() => {
707
707
 
708
708
  protectedActionMap.set(action, { timeoutId, timestamp });
709
709
 
710
- if (DEBUG$2) {
710
+ if (DEBUG$3) {
711
711
  console.debug(
712
712
  `"${action}": protected from GC for ${PROTECTION_DURATION}ms`,
713
713
  );
@@ -818,7 +818,7 @@ const updateActions = ({
818
818
 
819
819
  const { runningSet, settledSet } = getActivationInfo();
820
820
 
821
- if (DEBUG$2) {
821
+ if (DEBUG$3) {
822
822
  let argSource = `reason: \`${reason}\``;
823
823
  if (isReplace) {
824
824
  argSource += `, isReplace: true`;
@@ -942,7 +942,7 @@ ${lines.join("\n")}`,
942
942
  }
943
943
  }
944
944
  }
945
- if (DEBUG$2) {
945
+ if (DEBUG$3) {
946
946
  const lines = [
947
947
  ...(willResetSet.size
948
948
  ? [formatActionSet(willResetSet, "- will reset:")]
@@ -1038,7 +1038,7 @@ ${lines.join("\n")}`);
1038
1038
  actionToPromotePrivateProperties.isPrerunSignal.value = false;
1039
1039
  }
1040
1040
  }
1041
- if (DEBUG$2) {
1041
+ if (DEBUG$3) {
1042
1042
  console.groupEnd();
1043
1043
  }
1044
1044
 
@@ -1149,7 +1149,7 @@ const createAction = (callback, rootOptions = {}) => {
1149
1149
  if (!actionAbort) {
1150
1150
  return false;
1151
1151
  }
1152
- if (DEBUG$2) {
1152
+ if (DEBUG$3) {
1153
1153
  console.log(`"${action}": aborting (reason: ${reason})`);
1154
1154
  }
1155
1155
  actionAbort(reason);
@@ -1421,7 +1421,7 @@ const createAction = (callback, rootOptions = {}) => {
1421
1421
  if (isPrerun && (globalAbortSignal.aborted || abortSignal.aborted)) {
1422
1422
  prerunProtectionRegistry.unprotect(action);
1423
1423
  }
1424
- if (DEBUG$2) {
1424
+ if (DEBUG$3) {
1425
1425
  console.log(`"${action}": aborted (reason: ${abortReason})`);
1426
1426
  }
1427
1427
  };
@@ -1494,7 +1494,7 @@ const createAction = (callback, rootOptions = {}) => {
1494
1494
  onComplete?.(computedDataSignal.peek(), action);
1495
1495
  completeSideEffect?.(action);
1496
1496
  });
1497
- if (DEBUG$2) {
1497
+ if (DEBUG$3) {
1498
1498
  console.log(`"${action}": completed`);
1499
1499
  }
1500
1500
  return computedDataSignal.peek();
@@ -1521,7 +1521,7 @@ const createAction = (callback, rootOptions = {}) => {
1521
1521
  "never supposed to happen, abort error should be handled by the abort signal",
1522
1522
  );
1523
1523
  }
1524
- if (DEBUG$2) {
1524
+ if (DEBUG$3) {
1525
1525
  console.log(
1526
1526
  `"${action}": failed (error: ${e}, handled by ui: ${ui.hasRenderers})`,
1527
1527
  );
@@ -1591,7 +1591,7 @@ const createAction = (callback, rootOptions = {}) => {
1591
1591
 
1592
1592
  const performStop = ({ reason }) => {
1593
1593
  abort(reason);
1594
- if (DEBUG$2) {
1594
+ if (DEBUG$3) {
1595
1595
  console.log(`"${action}": stopping (reason: ${reason})`);
1596
1596
  }
1597
1597
 
@@ -2089,10 +2089,11 @@ const openCallout = (
2089
2089
  });
2090
2090
 
2091
2091
  addLevelEffect(() => {
2092
- anchorElement.style.setProperty(
2093
- "--callout-color",
2094
- `var(--navi-${level}-color)`,
2092
+ const levelColor = resolveCSSColor(
2093
+ `var(--${level}-color)`,
2094
+ calloutElement,
2095
2095
  );
2096
+ anchorElement.style.setProperty("--callout-color", levelColor);
2096
2097
  return () => {
2097
2098
  anchorElement.style.removeProperty("--callout-color");
2098
2099
  };
@@ -2200,7 +2201,13 @@ const ARROW_SPACING = 8;
2200
2201
  import.meta.css = /* css */ `
2201
2202
  @layer navi {
2202
2203
  .navi_callout {
2204
+ --success-color: #4caf50;
2205
+ --info-color: #2196f3;
2206
+ --warning-color: #ff9800;
2207
+ --error-color: #f44336;
2208
+
2203
2209
  --background-color: white;
2210
+ --icon-color: black;
2204
2211
  --padding: 8px;
2205
2212
  }
2206
2213
  }
@@ -2218,8 +2225,8 @@ import.meta.css = /* css */ `
2218
2225
  overflow: visible;
2219
2226
 
2220
2227
  --x-border-color: var(--x-level-color);
2228
+ --x-background-color: var(--x-level-color);
2221
2229
  --x-icon-color: var(--x-level-color);
2222
- --x-background-color: var(--background-color);
2223
2230
  }
2224
2231
 
2225
2232
  .navi_callout_frame {
@@ -2230,16 +2237,6 @@ import.meta.css = /* css */ `
2230
2237
  .navi_callout .navi_callout_border {
2231
2238
  fill: var(--x-border-color);
2232
2239
  }
2233
- .navi_callout[data-level="info"] {
2234
- --x-level-color: var(--navi-info-color);
2235
- }
2236
- .navi_callout[data-level="warning"] {
2237
- --x-level-color: var(--navi-warning-color);
2238
- }
2239
- .navi_callout[data-level="error"] {
2240
- --x-level-color: var(--navi-error-color);
2241
- }
2242
-
2243
2240
  .navi_callout_frame svg {
2244
2241
  position: absolute;
2245
2242
  inset: 0;
@@ -2248,7 +2245,6 @@ import.meta.css = /* css */ `
2248
2245
  .navi_callout_background {
2249
2246
  fill: var(--x-background-color);
2250
2247
  }
2251
-
2252
2248
  .navi_callout_box {
2253
2249
  position: relative;
2254
2250
  border-style: solid;
@@ -2316,6 +2312,19 @@ import.meta.css = /* css */ `
2316
2312
  max-height: 200px;
2317
2313
  overflow: auto;
2318
2314
  }
2315
+
2316
+ .navi_callout[data-level="success"] {
2317
+ --x-level-color: var(--success-color);
2318
+ }
2319
+ .navi_callout[data-level="info"] {
2320
+ --x-level-color: var(--info-color);
2321
+ }
2322
+ .navi_callout[data-level="warning"] {
2323
+ --x-level-color: var(--warning-color);
2324
+ }
2325
+ .navi_callout[data-level="error"] {
2326
+ --x-level-color: var(--error-color);
2327
+ }
2319
2328
  `;
2320
2329
 
2321
2330
  // HTML template for the callout
@@ -8004,9 +8013,9 @@ const executeWithCleanup = (fn, cleanup) => {
8004
8013
  }
8005
8014
  };
8006
8015
 
8007
- let DEBUG$1 = false;
8016
+ let DEBUG$2 = false;
8008
8017
  const enableDebugOnDocumentLoading = () => {
8009
- DEBUG$1 = true;
8018
+ DEBUG$2 = true;
8010
8019
  };
8011
8020
 
8012
8021
  const windowIsLoadingSignal = signal(true);
@@ -8026,13 +8035,13 @@ const [
8026
8035
  removeFromDocumentLoadingRouteArraySignal,
8027
8036
  ] = arraySignal([]);
8028
8037
  const routingWhile = (fn, routeNames = []) => {
8029
- if (DEBUG$1 && routeNames.length > 0) {
8038
+ if (DEBUG$2 && routeNames.length > 0) {
8030
8039
  console.debug(`routingWhile: Adding routes to loading state:`, routeNames);
8031
8040
  }
8032
8041
  addToDocumentLoadingRouteArraySignal(...routeNames);
8033
8042
  return executeWithCleanup(fn, () => {
8034
8043
  removeFromDocumentLoadingRouteArraySignal(...routeNames);
8035
- if (DEBUG$1 && routeNames.length > 0) {
8044
+ if (DEBUG$2 && routeNames.length > 0) {
8036
8045
  console.debug(
8037
8046
  `routingWhile: Removed routes from loading state:`,
8038
8047
  routeNames,
@@ -8049,7 +8058,7 @@ const [
8049
8058
  removeFromDocumentLoadingActionArraySignal,
8050
8059
  ] = arraySignal([]);
8051
8060
  const workingWhile = (fn, actionNames = []) => {
8052
- if (DEBUG$1 && actionNames.length > 0) {
8061
+ if (DEBUG$2 && actionNames.length > 0) {
8053
8062
  console.debug(
8054
8063
  `workingWhile: Adding actions to loading state:`,
8055
8064
  actionNames,
@@ -8058,7 +8067,7 @@ const workingWhile = (fn, actionNames = []) => {
8058
8067
  addToDocumentLoadingActionArraySignal(...actionNames);
8059
8068
  return executeWithCleanup(fn, () => {
8060
8069
  removeFromDocumentLoadingActionArraySignal(...actionNames);
8061
- if (DEBUG$1 && actionNames.length > 0) {
8070
+ if (DEBUG$2 && actionNames.length > 0) {
8062
8071
  console.debug(
8063
8072
  `routingWhile: Removed action from loading state:`,
8064
8073
  actionNames,
@@ -8555,6 +8564,1557 @@ const useNavStateBasic = (id, initialValue, { debug } = {}) => {
8555
8564
 
8556
8565
  const useNavState = useNavStateBasic;
8557
8566
 
8567
+ const NEVER_SET = {};
8568
+ const useUrlSearchParam = (paramName) => {
8569
+ const documentUrl = documentUrlSignal.value;
8570
+ const searchParam = new URL(documentUrl).searchParams.get(paramName);
8571
+ const valueRef = useRef(NEVER_SET);
8572
+ const [value, setValue] = useState();
8573
+ if (valueRef.current !== searchParam) {
8574
+ valueRef.current = searchParam;
8575
+ setValue(searchParam);
8576
+ }
8577
+
8578
+ const setSearchParamValue = (newValue, { replace = true } = {}) => {
8579
+ const newUrlObject = new URL(window.location.href);
8580
+ newUrlObject.searchParams.set(paramName, newValue);
8581
+ const newUrl = newUrlObject.href;
8582
+ goTo(newUrl, { replace });
8583
+ };
8584
+
8585
+ return [value, setSearchParamValue];
8586
+ };
8587
+
8588
+ installImportMetaCss(import.meta);
8589
+ import.meta.css = /* css */ `
8590
+ .ui_transition_container[data-transition-running] {
8591
+ /* When transition are running we need to put overflow: hidden */
8592
+ /* Either because the transition slides */
8593
+ /* Or when size transition are disabled because we need to immediatly crop old content when it's bigger than new content */
8594
+ overflow: hidden;
8595
+ }
8596
+
8597
+ .ui_transition_container,
8598
+ .ui_transition_outer_wrapper,
8599
+ .ui_transition_slot,
8600
+ .ui_transition_phase_overlay,
8601
+ .ui_transition_content_overlay {
8602
+ display: flex;
8603
+ width: fit-content;
8604
+ min-width: 100%;
8605
+ height: fit-content;
8606
+ min-height: 100%;
8607
+ flex-direction: inherit;
8608
+ align-items: inherit;
8609
+ justify-content: inherit;
8610
+ border-radius: inherit;
8611
+ cursor: inherit;
8612
+ }
8613
+
8614
+ .ui_transition_slot {
8615
+ width: 100%;
8616
+ min-width: fit-content;
8617
+ height: 100%;
8618
+ min-height: fit-content;
8619
+ flex-direction: column;
8620
+ }
8621
+
8622
+ .ui_transition_container,
8623
+ .ui_transition_slot {
8624
+ position: relative;
8625
+ }
8626
+
8627
+ .ui_transition_phase_overlay,
8628
+ .ui_transition_content_overlay {
8629
+ position: absolute;
8630
+ inset: 0;
8631
+ pointer-events: none;
8632
+ }
8633
+ `;
8634
+
8635
+ const DEBUG$1 = {
8636
+ detection: false,
8637
+ size: false,
8638
+ content: false,
8639
+ transition_updates: false,
8640
+ };
8641
+
8642
+ const SIZE_TRANSITION_DURATION = 150; // Default size transition duration
8643
+ const SIZE_DIFF_EPSILON = 0.5; // Ignore size transition when difference below this (px)
8644
+ const CONTENT_TRANSITION = "cross-fade"; // Default content transition type
8645
+ const CONTENT_TRANSITION_DURATION = 300; // Default content transition duration
8646
+ const PHASE_TRANSITION = "cross-fade"; // Default phase transition type (only cross-fade supported)
8647
+ const PHASE_TRANSITION_DURATION = 300; // Default phase transition duration
8648
+
8649
+ const initUITransition = (container) => {
8650
+ if (!container.classList.contains("ui_transition_container")) {
8651
+ console.error("Element must have ui_transition_container class");
8652
+ return { cleanup: () => {} };
8653
+ }
8654
+
8655
+ const localDebug = {
8656
+ ...DEBUG$1,
8657
+ detection: container.hasAttribute("data-debug-detection"),
8658
+ size: container.hasAttribute("data-debug-size"),
8659
+ content: container.hasAttribute("data-debug-content"),
8660
+ };
8661
+ const hasSomeDebugLogs =
8662
+ localDebug.detection || localDebug.size || localDebug.content;
8663
+ const debugClones = container.hasAttribute("data-debug-clones");
8664
+ const debugBreakAfterClone = container.getAttribute(
8665
+ "data-debug-break-after-clone",
8666
+ );
8667
+ const debug = (type, ...args) => {
8668
+ if (localDebug[type]) {
8669
+ console.debug(`[${type}]`, ...args);
8670
+ }
8671
+ };
8672
+
8673
+ const outerWrapper = container.querySelector(".ui_transition_outer_wrapper");
8674
+ const slot = container.querySelector(".ui_transition_slot");
8675
+ const phaseOverlay = outerWrapper.querySelector(
8676
+ ".ui_transition_phase_overlay",
8677
+ );
8678
+ const contentOverlay = container.querySelector(
8679
+ ".ui_transition_content_overlay",
8680
+ );
8681
+ if (!outerWrapper || !slot || !phaseOverlay || !contentOverlay) {
8682
+ console.error("Missing required ui-transition structure");
8683
+ return { cleanup: () => {} };
8684
+ }
8685
+
8686
+ const state = {
8687
+ isPaused: false,
8688
+ };
8689
+ const initialTransitionEnabled = container.hasAttribute(
8690
+ "data-initial-transition",
8691
+ );
8692
+ const transitionController = createGroupTransitionController();
8693
+ const setupTransition = ({
8694
+ isPhaseTransition = false,
8695
+ overlay,
8696
+ needsOldChildNodesClone,
8697
+ previousChildNodes,
8698
+ childNodes,
8699
+ slotInfo,
8700
+ attributeToRemove = [],
8701
+ }) => {
8702
+ let cleanup = () => {};
8703
+ let elementToImpact;
8704
+
8705
+ if (overlay.childNodes.length > 0) {
8706
+ elementToImpact = overlay;
8707
+ cleanup = () => {
8708
+ if (!debugClones) {
8709
+ overlay.innerHTML = "";
8710
+ }
8711
+ };
8712
+ debug(
8713
+ "content",
8714
+ `Continuing from current ${isPhaseTransition ? "phase" : "content"} transition element`,
8715
+ );
8716
+ } else if (needsOldChildNodesClone) {
8717
+ overlay.innerHTML = "";
8718
+ for (const previousChildNode of previousChildNodes) {
8719
+ const previousChildClone = previousChildNode.cloneNode(true);
8720
+ if (previousChildClone.nodeType !== Node.TEXT_NODE) {
8721
+ for (const attrToRemove of attributeToRemove) {
8722
+ previousChildClone.removeAttribute(attrToRemove);
8723
+ }
8724
+ previousChildClone.setAttribute("data-ui-transition-clone", "");
8725
+ }
8726
+ overlay.appendChild(previousChildClone);
8727
+ }
8728
+ elementToImpact = overlay;
8729
+ cleanup = () => {
8730
+ if (!debugClones) {
8731
+ overlay.innerHTML = "";
8732
+ }
8733
+ };
8734
+ debug(
8735
+ "content",
8736
+ `Cloned previous child for ${isPhaseTransition ? "phase" : "content"} transition:`,
8737
+ getElementSignature(previousChildNodes),
8738
+ );
8739
+ if (debugBreakAfterClone === slotInfo.contentKey) {
8740
+ debugger;
8741
+ }
8742
+ } else {
8743
+ overlay.innerHTML = "";
8744
+ debug(
8745
+ "content",
8746
+ `No old child to clone for ${isPhaseTransition ? "phase" : "content"} transition`,
8747
+ );
8748
+ }
8749
+
8750
+ // Determine which elements to return based on transition type:
8751
+ // - Phase transitions: operate on individual elements (cross-fade between specific elements)
8752
+ // - Content transitions: operate at container level (slide entire containers, outlive content phases)
8753
+ let oldElement;
8754
+ let newElement;
8755
+ if (isPhaseTransition) {
8756
+ // Phase transitions work on individual elements
8757
+ oldElement = elementToImpact;
8758
+ newElement = slot;
8759
+ } else {
8760
+ // Content transitions work at container level and can outlive content phase changes
8761
+ oldElement = previousChildNodes.length ? elementToImpact : null;
8762
+ newElement = childNodes.length ? slot : null;
8763
+ }
8764
+
8765
+ return {
8766
+ cleanup,
8767
+ oldElement,
8768
+ newElement,
8769
+ };
8770
+ };
8771
+ const [teardown, addTeardown] = createPubSub();
8772
+ const [publishPause, addPauseCallback] = createPubSub();
8773
+ const [publishResume, addResumeCallback] = createPubSub();
8774
+
8775
+ const [publishChange, subscribeChange] = createPubSub();
8776
+ let triggerChildSlotMutation;
8777
+ let previousSlotInfo;
8778
+ let slotInfo;
8779
+ let changeInfo;
8780
+ {
8781
+ const createSlotInfo = (childNodes, { contentKey, contentPhase }) => {
8782
+ const hasChild = childNodes.length > 0;
8783
+ let contentKeyFormatted;
8784
+ let contentName;
8785
+ if (hasChild) {
8786
+ if (contentKey) {
8787
+ contentKeyFormatted = `[data-content-key="${contentKey}"]`;
8788
+ } else {
8789
+ let onlyTextNodes = true;
8790
+ for (const child of childNodes) {
8791
+ if (child.nodeType !== Node.TEXT_NODE) {
8792
+ onlyTextNodes = false;
8793
+ break;
8794
+ }
8795
+ }
8796
+ contentKeyFormatted = onlyTextNodes ? "[text]" : "[unkeyed]";
8797
+ }
8798
+ contentName = contentPhase ? "content-phase" : "content";
8799
+ } else {
8800
+ contentKeyFormatted = "[empty]";
8801
+ contentName = "null";
8802
+ }
8803
+
8804
+ return {
8805
+ childNodes,
8806
+ contentKey,
8807
+ contentPhase,
8808
+
8809
+ hasChild: childNodes.length > 0,
8810
+ contentKeyFormatted,
8811
+ isContentPhase: Boolean(contentPhase),
8812
+ contentName,
8813
+ };
8814
+ };
8815
+ previousSlotInfo = createSlotInfo([], {
8816
+ contentKey: undefined,
8817
+ contentPhase: undefined,
8818
+ });
8819
+ slotInfo = previousSlotInfo;
8820
+ let isUpdating = false;
8821
+ triggerChildSlotMutation = (reason) => {
8822
+ if (isUpdating) {
8823
+ debug("detection", "Preventing recursive update");
8824
+ return;
8825
+ }
8826
+ try {
8827
+ const childNodes = Array.from(slot.childNodes);
8828
+ if (hasSomeDebugLogs) {
8829
+ const updateLabel =
8830
+ childNodes.length === 0
8831
+ ? "cleared/empty"
8832
+ : childNodes.length === 1
8833
+ ? getElementSignature(childNodes[0])
8834
+ : getElementSignature(slot);
8835
+ console.group(`UI Update: ${updateLabel} (reason: ${reason})`);
8836
+ }
8837
+ updateSlotChangeInfo(childNodes, reason);
8838
+ if (changeInfo.isStateChangeOnly) {
8839
+ } else {
8840
+ publishChange();
8841
+ previousSlotInfo = slotInfo;
8842
+ if (
8843
+ changeInfo.isInitialPopulationWithoutTransition ||
8844
+ changeInfo.becomesPopulated
8845
+ ) {
8846
+ hasPopulatedOnce = true;
8847
+ }
8848
+ }
8849
+ } finally {
8850
+ isUpdating = false;
8851
+ if (hasSomeDebugLogs) {
8852
+ console.groupEnd();
8853
+ }
8854
+ }
8855
+ };
8856
+
8857
+ let hasPopulatedOnce = false; // track if we've already populated once (null → something)
8858
+ const updateSlotChangeInfo = (currentChildNodes, reason = "mutation") => {
8859
+ let childContentKey;
8860
+ let contentPhase;
8861
+ if (currentChildNodes.length === 0) {
8862
+ contentPhase = true; // empty treated as phase
8863
+ } else {
8864
+ for (const childNode of currentChildNodes) {
8865
+ if (childNode.nodeType === Node.TEXT_NODE) {
8866
+ continue;
8867
+ }
8868
+ if (childNode.hasAttribute("data-content-phase")) {
8869
+ const contentPhaseAttr =
8870
+ childNode.getAttribute("data-content-phase");
8871
+ contentPhase = contentPhaseAttr || true;
8872
+ }
8873
+ if (childNode.hasAttribute("data-content-key")) {
8874
+ childContentKey = childNode.getAttribute("data-content-key");
8875
+ }
8876
+ }
8877
+ }
8878
+ const slotContentKey = slot.getAttribute("data-content-key");
8879
+ if (childContentKey && slotContentKey) {
8880
+ console.warn(
8881
+ `Slot and slot child both have a [data-content-key]. Slot is ${slotContentKey} and child is ${childContentKey}, using the child.`,
8882
+ );
8883
+ }
8884
+ const contentKey = childContentKey || slotContentKey || undefined;
8885
+ slotInfo = createSlotInfo(currentChildNodes, {
8886
+ contentKey,
8887
+ contentPhase,
8888
+ });
8889
+
8890
+ const hadChild = previousSlotInfo.hasChild;
8891
+ const hasChild = currentChildNodes.length > 0;
8892
+ const becomesEmpty = hadChild && !hasChild;
8893
+ const becomesPopulated = !hadChild && hasChild;
8894
+ const isInitialPopulationWithoutTransition =
8895
+ becomesPopulated && !hasPopulatedOnce && !initialTransitionEnabled;
8896
+ const shouldDoContentTransition =
8897
+ contentKey &&
8898
+ previousSlotInfo.contentKey &&
8899
+ contentKey !== previousSlotInfo.contentKey;
8900
+ const previousIsContentPhase = !hadChild || previousSlotInfo.contentPhase;
8901
+ const currentIsContentPhase = !hasChild || contentPhase;
8902
+ const shouldDoPhaseTransition =
8903
+ !shouldDoContentTransition &&
8904
+ (becomesPopulated ||
8905
+ becomesEmpty ||
8906
+ (hadChild &&
8907
+ hasChild &&
8908
+ (previousIsContentPhase !== currentIsContentPhase ||
8909
+ (previousIsContentPhase && currentIsContentPhase))));
8910
+ const contentChange = hadChild && hasChild && shouldDoContentTransition;
8911
+ const phaseChange = hadChild && hasChild && shouldDoPhaseTransition;
8912
+ const isTransitionLess =
8913
+ !shouldDoContentTransition &&
8914
+ !shouldDoPhaseTransition &&
8915
+ !becomesPopulated &&
8916
+ !becomesEmpty;
8917
+ const shouldDoContentTransitionIncludingPopulation =
8918
+ shouldDoContentTransition ||
8919
+ (becomesPopulated && !shouldDoPhaseTransition);
8920
+ // nothing to transition if no previous and no current child
8921
+ // (Either it's the initial call or just content-key changes but there is no child yet)
8922
+ const isStateChangeOnly = !hadChild && !hasChild;
8923
+ if (isStateChangeOnly) {
8924
+ const prevKey = previousSlotInfo.contentKey;
8925
+ const keyIsTheSame = prevKey === contentKey;
8926
+ if (keyIsTheSame) {
8927
+ debug(
8928
+ "detection",
8929
+ `Childless change: no changes found -> do nothing and skip transitions`,
8930
+ );
8931
+ } else if (!prevKey && contentKey) {
8932
+ debug(
8933
+ "detection",
8934
+ `Childless change: ${contentKey} added -> registering it and skip transitions`,
8935
+ );
8936
+ } else if (prevKey && !contentKey) {
8937
+ debug(
8938
+ "detection",
8939
+ `Childless change: ${contentKey} removed -> registering it and skip transitions`,
8940
+ );
8941
+ } else {
8942
+ debug(
8943
+ "detection",
8944
+ `Childless change: content key updated from ${prevKey} to ${contentKey} -> registering it and skip transitions`,
8945
+ );
8946
+ }
8947
+ } else if (isInitialPopulationWithoutTransition) {
8948
+ debug(
8949
+ "detection",
8950
+ "Initial population detected -> skipping transitions (opt-in with [data-initial-transition])",
8951
+ );
8952
+ } else if (previousSlotInfo.contentKey !== slotInfo.contentKey) {
8953
+ let contentKeysSentence = `Content key: ${previousSlotInfo.contentKeyFormatted} → ${slotInfo.contentKeyFormatted}`;
8954
+ debug("detection", contentKeysSentence);
8955
+ } else if (previousSlotInfo.contentPhase !== slotInfo.contentPhase) {
8956
+ let contentPhasesSentence =
8957
+ slotInfo.contentPhase && previousSlotInfo.contentPhase
8958
+ ? `Content phase: ${previousSlotInfo.contentPhase} → ${slotInfo.contentPhase}`
8959
+ : previousSlotInfo.contentPhase
8960
+ ? `becomes content (content phase becomes undefined)`
8961
+ : `content phase becomes ${slotInfo.contentPhase}`;
8962
+ debug("detection", contentPhasesSentence);
8963
+ }
8964
+
8965
+ changeInfo = {
8966
+ reason,
8967
+ previousSlotInfo,
8968
+ becomesEmpty,
8969
+ becomesPopulated,
8970
+ isInitialPopulationWithoutTransition,
8971
+ shouldDoContentTransition,
8972
+ shouldDoPhaseTransition,
8973
+ contentChange,
8974
+ phaseChange,
8975
+ isTransitionLess,
8976
+ shouldDoContentTransitionIncludingPopulation,
8977
+ isStateChangeOnly,
8978
+ };
8979
+ };
8980
+ }
8981
+
8982
+ let onContentTransitionComplete;
8983
+ let hasSizeTransitions = container.hasAttribute("data-size-transition");
8984
+ {
8985
+ let naturalContentWidth = 0; // Natural size of actual content (not loading/error states)
8986
+ let naturalContentHeight = 0;
8987
+ let constrainedWidth = 0; // Current constrained dimensions (what outer wrapper is set to)
8988
+ let constrainedHeight = 0;
8989
+ let sizeTransition = null;
8990
+
8991
+ let pauseResizeObserver;
8992
+ {
8993
+ let resizeObserver = null;
8994
+ let isWithinResizeObserverTick = false;
8995
+ const pauseReasonSet = new Set();
8996
+ let state = "disconnected"; // "disconnected" | "paused" | "observing"
8997
+ let pendingResizeCount = 0;
8998
+ let resumeAnimationFrame;
8999
+
9000
+ pauseResizeObserver = (reason = "pause_requested") => {
9001
+ cancelAnimationFrame(resumeAnimationFrame);
9002
+ pauseReasonSet.add(reason);
9003
+ if (isWithinResizeObserverTick) {
9004
+ if (resizeObserver) {
9005
+ debug("size", `[resize observer] stop while "${reason}"`);
9006
+ stopResizeObserver();
9007
+ }
9008
+ } else {
9009
+ debug("size", `[resize observer] pause while "${reason}"`);
9010
+ // we keep the resize observer alive because we are not in a resize tick
9011
+ state = "paused";
9012
+ }
9013
+ const resume = () => {
9014
+ pauseReasonSet.delete(reason);
9015
+ if (pauseReasonSet.size > 0) {
9016
+ return;
9017
+ }
9018
+ resumeAnimationFrame = requestAnimationFrame(() => {
9019
+ debug("size", `[resize observer] resume after "${reason}"`);
9020
+ if (pendingResizeCount) {
9021
+ debug(
9022
+ "size",
9023
+ `[resize observer] was called while paused -> syncContentDimensions()`,
9024
+ );
9025
+ pendingResizeCount = 0;
9026
+ syncContentDimensions();
9027
+ state = "observing";
9028
+ }
9029
+ if (state === "disconnected") {
9030
+ debug(
9031
+ "size",
9032
+ `[resize observer] was disconnected -> reconnect it`,
9033
+ );
9034
+ startResizeObserver();
9035
+ }
9036
+ });
9037
+ };
9038
+ return resume;
9039
+ };
9040
+ const stopResizeObserver = () => {
9041
+ state = "disconnected";
9042
+ if (!resizeObserver) return;
9043
+ resizeObserver.disconnect();
9044
+ resizeObserver = null;
9045
+ };
9046
+ const startResizeObserver = () => {
9047
+ state = "observing";
9048
+ resizeObserver = new ResizeObserver(() => {
9049
+ if (!hasSizeTransitions) {
9050
+ return;
9051
+ }
9052
+ if (!slotInfo.hasChild || slotInfo.isContentPhase) {
9053
+ debug(
9054
+ "size",
9055
+ "[resize observer] size change ignored (no child or content-phase)",
9056
+ );
9057
+ return;
9058
+ }
9059
+ if (state === "paused") {
9060
+ pendingResizeCount++;
9061
+ const pauseReason =
9062
+ Array.from(pauseReasonSet).join(", ") ||
9063
+ "wait next frame to resume";
9064
+ debug(
9065
+ "size",
9066
+ `[resize observer] size change ignore (${pauseReason})`,
9067
+ );
9068
+ return;
9069
+ }
9070
+ if (localDebug.size) {
9071
+ console.group("[resize observer] size change detected");
9072
+ }
9073
+ isWithinResizeObserverTick = true;
9074
+ syncContentDimensions();
9075
+ if (localDebug.size) {
9076
+ console.groupEnd();
9077
+ }
9078
+ requestAnimationFrame(() => {
9079
+ isWithinResizeObserverTick = false;
9080
+ });
9081
+ });
9082
+ resizeObserver.observe(slot);
9083
+ };
9084
+ startResizeObserver();
9085
+ addTeardown(() => {
9086
+ stopResizeObserver();
9087
+ });
9088
+ }
9089
+
9090
+ const measureSlotSize = () => {
9091
+ return [
9092
+ getWidthWithoutTransition(slot),
9093
+ getHeightWithoutTransition(slot),
9094
+ ];
9095
+ };
9096
+ const syncContentDimensions = () => {
9097
+ // check content dimensions to see if they changed and sync them
9098
+ const [currentWidth, currentHeight] = measureSlotSize();
9099
+ if (!slotInfo.isContentPhase) {
9100
+ updateNaturalContentSize(currentWidth, currentHeight);
9101
+ }
9102
+ if (sizeTransition) {
9103
+ updateToSize(currentWidth, currentHeight);
9104
+ } else {
9105
+ constrainedWidth = currentWidth;
9106
+ constrainedHeight = currentHeight;
9107
+ }
9108
+ };
9109
+ const applySizeConstraintsUntil = (width, height, reason) => {
9110
+ // we want to pause either because we have a diff and don't want to trigger the resize observer
9111
+ // or if we have no diff because we're about to do something that would trigger it (transition)
9112
+ const resumeResizeObserver = pauseResizeObserver(reason);
9113
+ debug("size", `Applying size constraints (${reason})`, {
9114
+ width: `${constrainedWidth} → ${width}`,
9115
+ height: `${constrainedHeight} → ${height}`,
9116
+ });
9117
+ outerWrapper.style.width = `${width}px`;
9118
+ outerWrapper.style.height = `${height}px`;
9119
+ constrainedWidth = width;
9120
+ constrainedHeight = height;
9121
+ // force content overlay to take the right size
9122
+ // (this way the content clone is not distorted by the new content size)
9123
+ contentOverlay.style.width = `${width}px`;
9124
+ contentOverlay.style.height = `${height}px`;
9125
+ const release = (reason) => {
9126
+ releaseSizeConstraints(reason);
9127
+ resumeResizeObserver(reason);
9128
+ };
9129
+ release.releaseResizeObserver = () => {
9130
+ resumeResizeObserver(reason);
9131
+ };
9132
+ return release;
9133
+ };
9134
+ const applySizeConstraints = (width, height, reason) => {
9135
+ applySizeConstraintsUntil(width, height, reason);
9136
+ };
9137
+ const releaseSizeConstraints = (reason) => {
9138
+ if (slotInfo.isContentPhase) {
9139
+ return;
9140
+ }
9141
+ debug("size", `Releasing constraints (${reason})`);
9142
+ const [beforeWidth, beforeHeight] = measureSlotSize();
9143
+ outerWrapper.style.width = "";
9144
+ outerWrapper.style.height = "";
9145
+ const [afterWidth, afterHeight] = measureSlotSize();
9146
+ debug("size", "Size after release:", {
9147
+ width: `${beforeWidth} → ${afterWidth}`,
9148
+ height: `${beforeHeight} → ${afterHeight}`,
9149
+ });
9150
+ updateNaturalContentSize(afterWidth, afterHeight);
9151
+ constrainedWidth = afterWidth;
9152
+ constrainedHeight = afterHeight;
9153
+ contentOverlay.style.width = ``;
9154
+ contentOverlay.style.height = ``;
9155
+ };
9156
+ const updateToSize = (targetWidth, targetHeight) => {
9157
+ if (
9158
+ constrainedWidth === targetWidth &&
9159
+ constrainedHeight === targetHeight
9160
+ ) {
9161
+ return;
9162
+ }
9163
+ if (!hasSizeTransitions) {
9164
+ applySizeConstraints(
9165
+ targetWidth,
9166
+ targetHeight,
9167
+ "size update without transition",
9168
+ );
9169
+ return;
9170
+ }
9171
+ const widthDiff = Math.abs(targetWidth - constrainedWidth);
9172
+ const heightDiff = Math.abs(targetHeight - constrainedHeight);
9173
+ if (widthDiff <= SIZE_DIFF_EPSILON && heightDiff <= SIZE_DIFF_EPSILON) {
9174
+ applySizeConstraints(
9175
+ targetWidth,
9176
+ targetHeight,
9177
+ "skip transition (negligible diff)",
9178
+ );
9179
+ return;
9180
+ }
9181
+ const duration = parseInt(
9182
+ container.getAttribute("data-size-transition-duration") ||
9183
+ SIZE_TRANSITION_DURATION,
9184
+ );
9185
+ debug("size", "prepare transition:", {
9186
+ width: `${constrainedWidth} → ${targetWidth}`,
9187
+ height: `${constrainedHeight} → ${targetHeight}`,
9188
+ duration,
9189
+ });
9190
+
9191
+ const transitions = [];
9192
+ if (widthDiff === 0) ; else if (widthDiff <= SIZE_DIFF_EPSILON) {
9193
+ debug(
9194
+ "size",
9195
+ `Skip width transition (negligible diff ${widthDiff.toFixed(4)}px)`,
9196
+ );
9197
+ } else {
9198
+ transitions.push(
9199
+ createWidthTransition(outerWrapper, targetWidth, {
9200
+ setup: () =>
9201
+ notifyTransition(outerWrapper, {
9202
+ modelId: "ui_transition_width",
9203
+ canOverflow: true,
9204
+ id:
9205
+ targetWidth > constrainedWidth
9206
+ ? "grow_to_new_width"
9207
+ : "shrink_to_new_width",
9208
+ }),
9209
+ duration,
9210
+ onUpdate: ({ value }) => {
9211
+ constrainedWidth = value;
9212
+ },
9213
+ }),
9214
+ );
9215
+ }
9216
+ if (heightDiff === 0) ; else if (heightDiff <= SIZE_DIFF_EPSILON) {
9217
+ debug(
9218
+ "size",
9219
+ `Skip height transition (negligible diff ${heightDiff.toFixed(4)}px)`,
9220
+ );
9221
+ } else {
9222
+ transitions.push(
9223
+ createHeightTransition(outerWrapper, targetHeight, {
9224
+ setup: () =>
9225
+ notifyTransition(outerWrapper, {
9226
+ modelId: "ui_transition_height",
9227
+ canOverflow: true,
9228
+ id:
9229
+ targetHeight > constrainedHeight
9230
+ ? "grow_to_new_height"
9231
+ : "shrink_to_new_height",
9232
+ }),
9233
+ duration,
9234
+ onUpdate: ({ value }) => {
9235
+ constrainedHeight = value;
9236
+ },
9237
+ }),
9238
+ );
9239
+ }
9240
+ const release = applySizeConstraintsUntil(
9241
+ constrainedWidth,
9242
+ constrainedHeight,
9243
+ "size transitioning",
9244
+ );
9245
+ sizeTransition = transitionController.animate(transitions, {
9246
+ onCancel: () => {
9247
+ release.releaseResizeObserver("size transition cancelled");
9248
+ },
9249
+ onFinish: () => {
9250
+ release("size transition finished");
9251
+ },
9252
+ });
9253
+ sizeTransition.play();
9254
+ };
9255
+ const updateNaturalContentSize = (width, height) => {
9256
+ if (width === naturalContentWidth && height === naturalContentHeight) {
9257
+ return;
9258
+ }
9259
+ debug("size", "Updating natural content size:", {
9260
+ width: `${naturalContentWidth} → ${width}`,
9261
+ height: `${naturalContentHeight} → ${height}`,
9262
+ });
9263
+ naturalContentWidth = width;
9264
+ naturalContentHeight = height;
9265
+ };
9266
+
9267
+ // Initialize with current size
9268
+ [constrainedWidth, constrainedHeight] = measureSlotSize();
9269
+
9270
+ const updateSizeTransition = () => {
9271
+ hasSizeTransitions = container.hasAttribute("data-size-transition");
9272
+ const { isContentPhase } = slotInfo;
9273
+ const { isInitialPopulationWithoutTransition } = changeInfo;
9274
+ debug(
9275
+ "size",
9276
+ `updateSizeTransition(), current constrained size: ${constrainedWidth.toFixed(2)}x${constrainedHeight.toFixed(2)}`,
9277
+ );
9278
+ sizeTransition?.cancel();
9279
+
9280
+ // Initial population skip (first null → something): no content or size animations
9281
+ if (isInitialPopulationWithoutTransition) {
9282
+ const [newWidth, newHeight] = measureSlotSize();
9283
+ debug("size", `content size measured to: ${newWidth}x${newHeight}`);
9284
+ if (isContentPhase) {
9285
+ applySizeConstraints(
9286
+ newWidth,
9287
+ newHeight,
9288
+ "content phase initial population",
9289
+ );
9290
+ } else {
9291
+ updateNaturalContentSize(newWidth, newHeight);
9292
+ releaseSizeConstraints("initial population - skip transitions");
9293
+ }
9294
+ return;
9295
+ }
9296
+
9297
+ let targetWidth;
9298
+ let targetHeight;
9299
+ if (isContentPhase) {
9300
+ const shouldUseNewDimensions =
9301
+ naturalContentWidth === 0 && naturalContentHeight === 0;
9302
+ if (shouldUseNewDimensions) {
9303
+ // we don't have any natural content dimensions yet, we can use the content phase dimensions for now
9304
+ [targetWidth, targetHeight] = measureSlotSize();
9305
+ debug(
9306
+ "size",
9307
+ `content phase dimension measured to: ${targetWidth}x${targetHeight}`,
9308
+ );
9309
+ } else {
9310
+ // we don't care about the content phase dimension.
9311
+ // the content dimensions prevails
9312
+ targetWidth = naturalContentWidth;
9313
+ targetHeight = naturalContentHeight;
9314
+ debug(
9315
+ "size",
9316
+ `content phase using natural content size: ${naturalContentWidth}x${naturalContentHeight}`,
9317
+ );
9318
+ }
9319
+ } else {
9320
+ outerWrapper.style.width = "";
9321
+ outerWrapper.style.height = "";
9322
+ const [slotNaturalWidth, slotNaturalHeight] = measureSlotSize();
9323
+ outerWrapper.style.width = `${constrainedWidth}px`;
9324
+ outerWrapper.style.height = `${constrainedHeight}px`;
9325
+ updateNaturalContentSize(slotNaturalWidth, slotNaturalHeight);
9326
+ targetWidth = slotNaturalWidth;
9327
+ targetHeight = slotNaturalHeight;
9328
+ debug(
9329
+ "size",
9330
+ `content size measured to: ${slotNaturalWidth}x${slotNaturalHeight}`,
9331
+ );
9332
+ }
9333
+
9334
+ // If size transitions are disabled hold the previous size to avoid cropping during the content transition.
9335
+ if (!hasSizeTransitions) {
9336
+ debug(
9337
+ "size",
9338
+ `Holding previous size during content transition: ${constrainedWidth}x${constrainedHeight}`,
9339
+ );
9340
+ applySizeConstraints(
9341
+ constrainedWidth,
9342
+ constrainedHeight,
9343
+ "hold size for content transition",
9344
+ );
9345
+ sizeTransition?.cancel();
9346
+ onContentTransitionComplete = () => {
9347
+ onContentTransitionComplete = null;
9348
+ releaseSizeConstraints(
9349
+ "content transition completed - release size hold",
9350
+ );
9351
+ };
9352
+ return;
9353
+ }
9354
+
9355
+ if (
9356
+ targetWidth === constrainedWidth &&
9357
+ targetHeight === constrainedHeight
9358
+ ) {
9359
+ sizeTransition?.cancel();
9360
+ debug("size", "No size change required");
9361
+ releaseSizeConstraints("no size change needed");
9362
+ return;
9363
+ }
9364
+ debug("size", "Size change needed:", {
9365
+ width: `${constrainedWidth} → ${targetWidth}`,
9366
+ height: `${constrainedHeight} → ${targetHeight}`,
9367
+ });
9368
+ updateToSize(targetWidth, targetHeight);
9369
+ };
9370
+ subscribeChange(updateSizeTransition);
9371
+
9372
+ addPauseCallback(() => {
9373
+ sizeTransition?.pause();
9374
+ });
9375
+ addResumeCallback(() => {
9376
+ sizeTransition?.play();
9377
+ });
9378
+ addTeardown(() => {
9379
+ sizeTransition?.cancel();
9380
+ });
9381
+ }
9382
+
9383
+ {
9384
+ let activeContentTransition = null;
9385
+ let activeContentTransitionType = null;
9386
+ let activePhaseTransition = null;
9387
+ let activePhaseTransitionType = null;
9388
+
9389
+ const updateContentTransitions = () => {
9390
+ const { childNodes, contentName: fromContentName } = slotInfo;
9391
+ const {
9392
+ previousSlotInfo,
9393
+ becomesEmpty,
9394
+ becomesPopulated,
9395
+ shouldDoContentTransition,
9396
+ shouldDoPhaseTransition,
9397
+ contentChange,
9398
+ phaseChange,
9399
+ isTransitionLess,
9400
+ shouldDoContentTransitionIncludingPopulation,
9401
+ } = changeInfo;
9402
+ const { hasChild: hadChild, contentName: toContentName } =
9403
+ previousSlotInfo;
9404
+
9405
+ const preserveOnlyContentTransition =
9406
+ isTransitionLess && activeContentTransition !== null;
9407
+ const previousChildNodes = previousSlotInfo.childNodes;
9408
+
9409
+ // Determine transition scenarios (hadChild/hasChild already computed above for logging)
9410
+
9411
+ /**
9412
+ * Content Phase Logic: Why empty slots are treated as content phases
9413
+ *
9414
+ * When there is no child element (React component returns null), it is considered
9415
+ * that the component does not render anything temporarily. This might be because:
9416
+ * - The component is loading but does not have a loading state
9417
+ * - The component has an error but does not have an error state
9418
+ * - The component is conceptually unloaded (underlying content was deleted/is not accessible)
9419
+ *
9420
+ * This represents a phase of the given content: having nothing to display.
9421
+ *
9422
+ * We support transitions between different contents via the ability to set
9423
+ * [data-content-key] on the ".ui_transition_slot". This is also useful when you want
9424
+ * all children of a React component to inherit the same data-content-key without
9425
+ * explicitly setting the attribute on each child element.
9426
+ */
9427
+
9428
+ // Content key change when either slot or child has data-content-key and it changed
9429
+ // Content key change detection already computed in getSlotChangeInfo.
9430
+ // We rely on the shouldDoContentTransition value coming from changeInfo.
9431
+
9432
+ const decisions = [];
9433
+ if (shouldDoContentTransition) {
9434
+ decisions.push("CONTENT TRANSITION");
9435
+ }
9436
+ if (shouldDoPhaseTransition) {
9437
+ decisions.push("PHASE TRANSITION");
9438
+ }
9439
+ if (preserveOnlyContentTransition) {
9440
+ decisions.push("PRESERVE CONTENT TRANSITION");
9441
+ }
9442
+ if (decisions.length === 0) {
9443
+ decisions.push("NO TRANSITION");
9444
+ }
9445
+
9446
+ debug("content", `Decision: ${decisions.join(" + ")}`);
9447
+ if (preserveOnlyContentTransition) {
9448
+ const progress = (activeContentTransition.progress * 100).toFixed(1);
9449
+ debug(
9450
+ "content",
9451
+ `Preserving existing content transition (progress ${progress}%)`,
9452
+ );
9453
+ }
9454
+
9455
+ if (changeInfo.isInitialPopulationWithoutTransition) {
9456
+ return;
9457
+ }
9458
+
9459
+ // Handle content transitions (slide-left, cross-fade for content key changes)
9460
+ if (
9461
+ decisions.length === 1 &&
9462
+ decisions[0] === "NO TRANSITION" &&
9463
+ activeContentTransition === null &&
9464
+ activePhaseTransition === null
9465
+ ) {
9466
+ // Skip creating any new transitions entirely
9467
+ onContentTransitionComplete?.();
9468
+ } else if (
9469
+ shouldDoContentTransitionIncludingPopulation &&
9470
+ !preserveOnlyContentTransition
9471
+ ) {
9472
+ const animationProgress = activeContentTransition?.progress || 0;
9473
+ if (animationProgress > 0) {
9474
+ debug(
9475
+ "content",
9476
+ `Preserving content transition progress: ${(animationProgress * 100).toFixed(1)}%`,
9477
+ );
9478
+ }
9479
+
9480
+ const newTransitionType =
9481
+ container.getAttribute("data-content-transition") ||
9482
+ CONTENT_TRANSITION;
9483
+ const canContinueSmoothly =
9484
+ activeContentTransitionType === newTransitionType &&
9485
+ activeContentTransition;
9486
+ if (canContinueSmoothly) {
9487
+ debug(
9488
+ "content",
9489
+ "Continuing with same content transition type (restarting due to actual change)",
9490
+ );
9491
+ activeContentTransition.cancel();
9492
+ } else if (
9493
+ activeContentTransition &&
9494
+ activeContentTransitionType !== newTransitionType
9495
+ ) {
9496
+ debug(
9497
+ "content",
9498
+ "Different content transition type, keeping both",
9499
+ `${activeContentTransitionType} → ${newTransitionType}`,
9500
+ );
9501
+ } else if (activeContentTransition) {
9502
+ debug("content", "Cancelling current content transition");
9503
+ activeContentTransition.cancel();
9504
+ }
9505
+
9506
+ const needsOldChildNodesClone =
9507
+ (contentChange || becomesEmpty) && hadChild;
9508
+ const duration = parseInt(
9509
+ container.getAttribute("data-content-transition-duration") ||
9510
+ CONTENT_TRANSITION_DURATION,
9511
+ );
9512
+ const type =
9513
+ container.getAttribute("data-content-transition") ||
9514
+ CONTENT_TRANSITION;
9515
+
9516
+ const setupContentTransition = () =>
9517
+ setupTransition({
9518
+ isPhaseTransition: false,
9519
+ overlay: contentOverlay,
9520
+ needsOldChildNodesClone,
9521
+ previousChildNodes,
9522
+ childNodes,
9523
+ slotInfo,
9524
+ attributeToRemove: ["data-content-key"],
9525
+ });
9526
+
9527
+ activeContentTransition = applyTransition(
9528
+ transitionController,
9529
+ setupContentTransition,
9530
+ {
9531
+ duration,
9532
+ type,
9533
+ animationProgress,
9534
+ isPhaseTransition: false,
9535
+ previousSlotInfo,
9536
+ slotInfo,
9537
+ onComplete: () => {
9538
+ activeContentTransition = null;
9539
+ activeContentTransitionType = null;
9540
+ onContentTransitionComplete?.();
9541
+ },
9542
+ debug,
9543
+ },
9544
+ );
9545
+
9546
+ if (activeContentTransition) {
9547
+ activeContentTransition.play();
9548
+ }
9549
+ activeContentTransitionType = type;
9550
+ } else if (!shouldDoContentTransition && !preserveOnlyContentTransition) {
9551
+ // Clean up content overlay if no content transition needed and nothing to preserve
9552
+ contentOverlay.innerHTML = "";
9553
+ activeContentTransition = null;
9554
+ activeContentTransitionType = null;
9555
+ }
9556
+
9557
+ // Handle phase transitions (cross-fade for content phase changes)
9558
+ if (shouldDoPhaseTransition) {
9559
+ const phaseTransitionType =
9560
+ container.getAttribute("data-phase-transition") || PHASE_TRANSITION;
9561
+ const phaseAnimationProgress = activePhaseTransition?.progress || 0;
9562
+ if (phaseAnimationProgress > 0) {
9563
+ debug(
9564
+ "content",
9565
+ `Preserving phase transition progress: ${(phaseAnimationProgress * 100).toFixed(1)}%`,
9566
+ );
9567
+ }
9568
+
9569
+ const canContinueSmoothly =
9570
+ activePhaseTransitionType === phaseTransitionType &&
9571
+ activePhaseTransition;
9572
+
9573
+ if (canContinueSmoothly) {
9574
+ debug("content", "Continuing with same phase transition type");
9575
+ activePhaseTransition.cancel();
9576
+ } else if (
9577
+ activePhaseTransition &&
9578
+ activePhaseTransitionType !== phaseTransitionType
9579
+ ) {
9580
+ debug(
9581
+ "content",
9582
+ "Different phase transition type, keeping both",
9583
+ `${activePhaseTransitionType} → ${phaseTransitionType}`,
9584
+ );
9585
+ } else if (activePhaseTransition) {
9586
+ debug("content", "Cancelling current phase transition");
9587
+ activePhaseTransition.cancel();
9588
+ }
9589
+
9590
+ const needsOldPhaseClone =
9591
+ (becomesEmpty || becomesPopulated || phaseChange) && hadChild;
9592
+ const phaseDuration = parseInt(
9593
+ container.getAttribute("data-phase-transition-duration") ||
9594
+ PHASE_TRANSITION_DURATION,
9595
+ );
9596
+
9597
+ const setupPhaseTransition = () =>
9598
+ setupTransition({
9599
+ isPhaseTransition: true,
9600
+ overlay: phaseOverlay,
9601
+ needsOldChildNodesClone: needsOldPhaseClone,
9602
+ previousChildNodes,
9603
+ childNodes,
9604
+ slotInfo,
9605
+ attributeToRemove: ["data-content-key", "data-content-phase"],
9606
+ });
9607
+
9608
+ debug(
9609
+ "content",
9610
+ `Starting transition: ${fromContentName} → ${toContentName}`,
9611
+ );
9612
+
9613
+ activePhaseTransition = applyTransition(
9614
+ transitionController,
9615
+ setupPhaseTransition,
9616
+ {
9617
+ duration: phaseDuration,
9618
+ type: phaseTransitionType,
9619
+ animationProgress: phaseAnimationProgress,
9620
+ isPhaseTransition: true,
9621
+ previousSlotInfo,
9622
+ slotInfo,
9623
+ onComplete: () => {
9624
+ activePhaseTransition = null;
9625
+ activePhaseTransitionType = null;
9626
+ onContentTransitionComplete?.();
9627
+ debug("content", "Phase transition complete");
9628
+ },
9629
+ debug,
9630
+ },
9631
+ );
9632
+
9633
+ if (activePhaseTransition) {
9634
+ activePhaseTransition.play();
9635
+ }
9636
+ activePhaseTransitionType = phaseTransitionType;
9637
+ }
9638
+ };
9639
+ subscribeChange(updateContentTransitions);
9640
+
9641
+ addPauseCallback(() => {
9642
+ activeContentTransition?.pause();
9643
+ activePhaseTransition?.pause();
9644
+ });
9645
+ addResumeCallback(() => {
9646
+ activeContentTransition?.play();
9647
+ activePhaseTransition?.play();
9648
+ });
9649
+ addTeardown(() => {
9650
+ activeContentTransition?.cancel();
9651
+ activePhaseTransition?.cancel();
9652
+ });
9653
+ }
9654
+
9655
+ {
9656
+ const transitionSet = new Set();
9657
+ const updateTransitionOverflowAttribute = () => {
9658
+ if (transitionSet.size > 0) {
9659
+ container.setAttribute("data-transition-running", "");
9660
+ } else {
9661
+ container.removeAttribute("data-transition-running");
9662
+ }
9663
+ };
9664
+ const onTransitionStart = (event) => {
9665
+ transitionSet.add(event.detail.id);
9666
+ updateTransitionOverflowAttribute();
9667
+ };
9668
+ const onTransitionEnd = (event) => {
9669
+ transitionSet.delete(event.detail.id);
9670
+ updateTransitionOverflowAttribute();
9671
+ };
9672
+ container.addEventListener("ui_transition_start", onTransitionStart);
9673
+ container.addEventListener("ui_transition_end", onTransitionEnd);
9674
+ addTeardown(() => {
9675
+ container.removeEventListener("ui_transition_start", onTransitionStart);
9676
+ container.removeEventListener("ui_transition_end", onTransitionEnd);
9677
+ });
9678
+ }
9679
+
9680
+ // Run once at init to process current slot content
9681
+ triggerChildSlotMutation("init");
9682
+ {
9683
+ const mutationObserver = new MutationObserver((mutations) => {
9684
+ const reasonParts = [];
9685
+
9686
+ for (const mutation of mutations) {
9687
+ if (mutation.type === "childList") {
9688
+ const added = mutation.addedNodes.length;
9689
+ const removed = mutation.removedNodes.length;
9690
+ if (added && removed) {
9691
+ reasonParts.push(`addedNodes(${added}) removedNodes(${removed})`);
9692
+ } else if (added) {
9693
+ reasonParts.push(`addedNodes(${added})`);
9694
+ } else {
9695
+ reasonParts.push(`removedNodes(${removed})`);
9696
+ }
9697
+ continue;
9698
+ }
9699
+ if (mutation.type === "attributes") {
9700
+ const { attributeName } = mutation;
9701
+ if (
9702
+ attributeName === "data-content-key" ||
9703
+ attributeName === "data-content-phase"
9704
+ ) {
9705
+ reasonParts.push(`[${attributeName}] change`);
9706
+ }
9707
+ }
9708
+ }
9709
+
9710
+ if (reasonParts.length === 0) {
9711
+ return;
9712
+ }
9713
+ const reason = reasonParts.join("+");
9714
+ triggerChildSlotMutation(reason);
9715
+ });
9716
+ mutationObserver.observe(slot, {
9717
+ childList: true,
9718
+ attributes: true,
9719
+ attributeFilter: ["data-content-key", "data-content-phase"],
9720
+ characterData: false,
9721
+ });
9722
+ addTeardown(() => {
9723
+ mutationObserver.disconnect();
9724
+ });
9725
+ }
9726
+
9727
+ return {
9728
+ slot,
9729
+
9730
+ cleanup: () => {
9731
+ teardown();
9732
+ },
9733
+ pause: () => {
9734
+ if (state.isPaused) {
9735
+ return;
9736
+ }
9737
+ publishPause();
9738
+ state.isPaused = true;
9739
+ },
9740
+ resume: () => {
9741
+ if (!state.isPaused) {
9742
+ return;
9743
+ }
9744
+ state.isPaused = false;
9745
+ publishResume();
9746
+ },
9747
+ getState: () => state,
9748
+ };
9749
+ };
9750
+
9751
+ const applyTransition = (
9752
+ transitionController,
9753
+ setupTransition,
9754
+ {
9755
+ type,
9756
+ duration,
9757
+ animationProgress = 0,
9758
+ isPhaseTransition,
9759
+ onComplete,
9760
+ previousSlotInfo,
9761
+ slotInfo,
9762
+ debug,
9763
+ },
9764
+ ) => {
9765
+ let transitionType;
9766
+ if (type === "cross-fade") {
9767
+ transitionType = crossFade;
9768
+ } else if (type === "slide-left") {
9769
+ transitionType = slideLeft;
9770
+ } else {
9771
+ return null;
9772
+ }
9773
+
9774
+ const { cleanup, oldElement, newElement, onTeardown } = setupTransition();
9775
+ // Use precomputed content key states (expected to be provided by caller)
9776
+ const fromContentKey = previousSlotInfo.contentKeyFormatted;
9777
+ const toContentKey = slotInfo.contentKeyFormatted;
9778
+
9779
+ debug("content", "Setting up animation:", {
9780
+ type,
9781
+ from: fromContentKey,
9782
+ to: toContentKey,
9783
+ progress: `${(animationProgress * 100).toFixed(1)}%`,
9784
+ });
9785
+
9786
+ const remainingDuration = Math.max(100, duration * (1 - animationProgress));
9787
+ debug("content", `Animation duration: ${remainingDuration}ms`);
9788
+
9789
+ const transitions = transitionType.apply(oldElement, newElement, {
9790
+ duration: remainingDuration,
9791
+ startProgress: animationProgress,
9792
+ isPhaseTransition,
9793
+ debug,
9794
+ });
9795
+
9796
+ debug("content", `Created ${transitions.length} transition(s) for animation`);
9797
+
9798
+ if (transitions.length === 0) {
9799
+ debug("content", "No transitions to animate, cleaning up immediately");
9800
+ cleanup();
9801
+ onTeardown?.();
9802
+ onComplete?.();
9803
+ return null;
9804
+ }
9805
+
9806
+ const groupTransition = transitionController.animate(transitions, {
9807
+ onFinish: () => {
9808
+ groupTransition.cancel();
9809
+ cleanup();
9810
+ onTeardown?.();
9811
+ onComplete?.();
9812
+ },
9813
+ });
9814
+
9815
+ return groupTransition;
9816
+ };
9817
+
9818
+ const slideLeft = {
9819
+ id: "ui_transition_slide_left",
9820
+ name: "slide-left",
9821
+ apply: (
9822
+ oldElement,
9823
+ newElement,
9824
+ { duration, startProgress = 0, isPhaseTransition = false, debug },
9825
+ ) => {
9826
+ if (!oldElement && !newElement) {
9827
+ return [];
9828
+ }
9829
+
9830
+ if (!newElement) {
9831
+ // Content -> Empty (slide out left only)
9832
+ const currentPosition = getTranslateX(oldElement);
9833
+ const containerWidth = getInnerWidth(oldElement.parentElement);
9834
+ const from = currentPosition;
9835
+ const to = -containerWidth;
9836
+ debug("content", "Slide out to empty:", { from, to });
9837
+
9838
+ return [
9839
+ createTranslateXTransition(oldElement, to, {
9840
+ setup: () =>
9841
+ notifyTransition(newElement, {
9842
+ modelId: slideLeft.id,
9843
+ canOverflow: true,
9844
+ id: "slide_out_old_content",
9845
+ }),
9846
+ from,
9847
+ duration,
9848
+ startProgress,
9849
+ onUpdate: ({ value, timing }) => {
9850
+ debug("transition_updates", "Slide out progress:", value);
9851
+ if (timing === "end") {
9852
+ debug("content", "Slide out complete");
9853
+ }
9854
+ },
9855
+ }),
9856
+ ];
9857
+ }
9858
+
9859
+ if (!oldElement) {
9860
+ // Empty -> Content (slide in from right)
9861
+ const containerWidth = getInnerWidth(newElement.parentElement);
9862
+ const from = containerWidth; // Start from right edge for slide-in effect
9863
+ const to = getTranslateXWithoutTransition(newElement);
9864
+ debug("content", "Slide in from empty:", { from, to });
9865
+ return [
9866
+ createTranslateXTransition(newElement, to, {
9867
+ setup: () =>
9868
+ notifyTransition(newElement, {
9869
+ modelId: slideLeft.id,
9870
+ canOverflow: true,
9871
+ id: "slide_in_new_content",
9872
+ }),
9873
+ from,
9874
+ duration,
9875
+ startProgress,
9876
+ onUpdate: ({ value, timing }) => {
9877
+ debug("transition_updates", "Slide in progress:", value);
9878
+ if (timing === "end") {
9879
+ debug("content", "Slide in complete");
9880
+ }
9881
+ },
9882
+ }),
9883
+ ];
9884
+ }
9885
+
9886
+ // Content -> Content (slide left)
9887
+ // The old content (oldElement) slides OUT to the left
9888
+ // The new content (newElement) slides IN from the right
9889
+
9890
+ // Get positions for the slide animation
9891
+ const containerWidth = getInnerWidth(newElement.parentElement);
9892
+ const oldContentPosition = getTranslateX(oldElement);
9893
+ const currentNewPosition = getTranslateX(newElement);
9894
+ const naturalNewPosition = getTranslateXWithoutTransition(newElement);
9895
+
9896
+ // For smooth continuation: if newElement is mid-transition,
9897
+ // calculate new position to maintain seamless sliding
9898
+ let startNewPosition;
9899
+ if (currentNewPosition !== 0 && naturalNewPosition === 0) {
9900
+ startNewPosition = currentNewPosition + containerWidth;
9901
+ debug(
9902
+ "content",
9903
+ "Calculated seamless position:",
9904
+ `${currentNewPosition} + ${containerWidth} = ${startNewPosition}`,
9905
+ );
9906
+ } else {
9907
+ startNewPosition = naturalNewPosition || containerWidth;
9908
+ }
9909
+
9910
+ // For phase transitions, force new content to start from right edge for proper slide-in
9911
+ const effectiveFromPosition = isPhaseTransition
9912
+ ? containerWidth
9913
+ : startNewPosition;
9914
+
9915
+ debug("content", "Slide transition:", {
9916
+ oldContent: `${oldContentPosition} → ${-containerWidth}`,
9917
+ newContent: `${effectiveFromPosition} → ${naturalNewPosition}`,
9918
+ });
9919
+
9920
+ const transitions = [];
9921
+
9922
+ // Slide old content out
9923
+ transitions.push(
9924
+ createTranslateXTransition(oldElement, -containerWidth, {
9925
+ setup: () =>
9926
+ notifyTransition(newElement, {
9927
+ modelId: slideLeft.id,
9928
+ canOverflow: true,
9929
+ id: "slide_out_old_content",
9930
+ }),
9931
+ from: oldContentPosition,
9932
+ duration,
9933
+ startProgress,
9934
+ onUpdate: ({ value }) => {
9935
+ debug("transition_updates", "Old content slide out:", value);
9936
+ },
9937
+ }),
9938
+ );
9939
+
9940
+ // Slide new content in
9941
+ transitions.push(
9942
+ createTranslateXTransition(newElement, naturalNewPosition, {
9943
+ setup: () =>
9944
+ notifyTransition(newElement, {
9945
+ modelId: slideLeft.id,
9946
+ canOverflow: true,
9947
+ id: "slide_in_new_content",
9948
+ }),
9949
+ from: effectiveFromPosition,
9950
+ duration,
9951
+ startProgress,
9952
+ onUpdate: ({ value, timing }) => {
9953
+ debug("transition_updates", "New content slide in:", value);
9954
+ if (timing === "end") {
9955
+ debug("content", "Slide complete");
9956
+ }
9957
+ },
9958
+ }),
9959
+ );
9960
+
9961
+ return transitions;
9962
+ },
9963
+ };
9964
+
9965
+ const crossFade = {
9966
+ id: "ui_transition_cross_fade",
9967
+ name: "cross-fade",
9968
+ apply: (
9969
+ oldElement,
9970
+ newElement,
9971
+ { duration, startProgress = 0, isPhaseTransition = false, debug },
9972
+ ) => {
9973
+ if (!oldElement && !newElement) {
9974
+ return [];
9975
+ }
9976
+
9977
+ if (!newElement) {
9978
+ // Content -> Empty (fade out only)
9979
+ const from = getOpacity(oldElement);
9980
+ const to = 0;
9981
+ debug("content", "Fade out to empty:", { from, to });
9982
+ return [
9983
+ createOpacityTransition(oldElement, to, {
9984
+ setup: () =>
9985
+ notifyTransition(newElement, {
9986
+ modelId: crossFade.id,
9987
+ canOverflow: true,
9988
+ id: "fade_out_old_content",
9989
+ }),
9990
+ from,
9991
+ duration,
9992
+ startProgress,
9993
+ onUpdate: ({ value, timing }) => {
9994
+ debug("transition_updates", "Content fade out:", value.toFixed(3));
9995
+ if (timing === "end") {
9996
+ debug("content", "Fade out complete");
9997
+ }
9998
+ },
9999
+ }),
10000
+ ];
10001
+ }
10002
+
10003
+ if (!oldElement) {
10004
+ // Empty -> Content (fade in only)
10005
+ const from = 0;
10006
+ const to = getOpacityWithoutTransition(newElement);
10007
+ debug("content", "Fade in from empty:", { from, to });
10008
+ return [
10009
+ createOpacityTransition(newElement, to, {
10010
+ setup: () =>
10011
+ notifyTransition(newElement, {
10012
+ modelId: crossFade.id,
10013
+ canOverflow: true,
10014
+ id: "fade_in_new_content",
10015
+ }),
10016
+ from,
10017
+ duration,
10018
+ startProgress,
10019
+ onUpdate: ({ value, timing }) => {
10020
+ debug("transition_updates", "Fade in progress:", value.toFixed(3));
10021
+ if (timing === "end") {
10022
+ debug("content", "Fade in complete");
10023
+ }
10024
+ },
10025
+ }),
10026
+ ];
10027
+ }
10028
+
10029
+ // Content -> Content (cross-fade)
10030
+ // Get current opacity for both elements
10031
+ const oldOpacity = getOpacity(oldElement);
10032
+ const newOpacity = getOpacity(newElement);
10033
+ const newNaturalOpacity = getOpacityWithoutTransition(newElement);
10034
+
10035
+ // For phase transitions, always start new content from 0 for clean visual transition
10036
+ // For content transitions, check for ongoing transitions to continue smoothly
10037
+ let effectiveFromOpacity;
10038
+ if (isPhaseTransition) {
10039
+ effectiveFromOpacity = 0; // Always start fresh for phase transitions (loading → content, etc.)
10040
+ } else {
10041
+ // For content transitions: if new element has ongoing opacity transition
10042
+ // (indicated by non-zero opacity when natural opacity is different),
10043
+ // start from current opacity to continue smoothly, otherwise start from 0
10044
+ const hasOngoingTransition =
10045
+ newOpacity !== newNaturalOpacity && newOpacity > 0;
10046
+ effectiveFromOpacity = hasOngoingTransition ? newOpacity : 0;
10047
+ }
10048
+
10049
+ debug("content", "Cross-fade transition:", {
10050
+ oldOpacity: `${oldOpacity} → 0`,
10051
+ newOpacity: `${effectiveFromOpacity} → ${newNaturalOpacity}`,
10052
+ isPhaseTransition,
10053
+ });
10054
+
10055
+ return [
10056
+ createOpacityTransition(oldElement, 0, {
10057
+ setup: () =>
10058
+ notifyTransition(newElement, {
10059
+ modelId: crossFade.id,
10060
+ canOverflow: true,
10061
+ id: "fade_out_old_content",
10062
+ }),
10063
+ from: oldOpacity,
10064
+ duration,
10065
+ startProgress,
10066
+ onUpdate: ({ value }) => {
10067
+ if (value > 0) {
10068
+ debug(
10069
+ "transition_updates",
10070
+ "Old content fade out:",
10071
+ value.toFixed(3),
10072
+ );
10073
+ }
10074
+ },
10075
+ }),
10076
+ createOpacityTransition(newElement, newNaturalOpacity, {
10077
+ setup: () =>
10078
+ notifyTransition(newElement, {
10079
+ modelId: crossFade.id,
10080
+ canOverflow: true,
10081
+ id: "fade_in_new_content",
10082
+ }),
10083
+ from: effectiveFromOpacity,
10084
+ duration,
10085
+ startProgress: isPhaseTransition ? 0 : startProgress, // Phase transitions: new content always starts fresh
10086
+ onUpdate: ({ value, timing }) => {
10087
+ debug("transition_updates", "New content fade in:", value.toFixed(3));
10088
+ if (timing === "end") {
10089
+ debug("content", "Cross-fade complete");
10090
+ }
10091
+ },
10092
+ }),
10093
+ ];
10094
+ },
10095
+ };
10096
+
10097
+ const notifyTransition = (element, detail) => {
10098
+ dispatchUITransitionStartCustomEvent(element, detail);
10099
+ return () => {
10100
+ dispatchUITransitionEndCustomEvent(element, detail);
10101
+ };
10102
+ };
10103
+ const dispatchUITransitionStartCustomEvent = (element, detail) => {
10104
+ const customEvent = new CustomEvent("ui_transition_start", {
10105
+ bubbles: true,
10106
+ detail,
10107
+ });
10108
+ element.dispatchEvent(customEvent);
10109
+ };
10110
+ const dispatchUITransitionEndCustomEvent = (element, detail) => {
10111
+ const customEvent = new CustomEvent("ui_transition_end", {
10112
+ bubbles: true,
10113
+ detail,
10114
+ });
10115
+ element.dispatchEvent(customEvent);
10116
+ };
10117
+
8558
10118
  /**
8559
10119
  * UITransition
8560
10120
  *
@@ -8595,13 +10155,15 @@ const ContentKeyContext = createContext();
8595
10155
  const UITransition = ({
8596
10156
  children,
8597
10157
  contentKey,
8598
- sizeTransition,
10158
+ sizeTransition = true,
8599
10159
  sizeTransitionDuration,
8600
10160
  transitionType,
8601
10161
  transitionDuration,
8602
10162
  phaseTransitionType,
8603
10163
  phaseTransitionDuration,
8604
- debugTransition,
10164
+ debugDetection,
10165
+ debugSize,
10166
+ debugBreakAfterClone,
8605
10167
  ...props
8606
10168
  }) => {
8607
10169
  const [contentKeyFromContext, setContentKeyFromContext] = useState();
@@ -8665,19 +10227,18 @@ const UITransition = ({
8665
10227
  "data-content-transition-duration": transitionDuration ? transitionDuration : undefined,
8666
10228
  "data-phase-transition": phaseTransitionType ? phaseTransitionType : undefined,
8667
10229
  "data-phase-transition-duration": phaseTransitionDuration ? phaseTransitionDuration : undefined,
8668
- "data-debug-transition": debugTransition ? "" : undefined,
8669
- children: [jsx("div", {
10230
+ "data-debug-detection": debugDetection ? "" : undefined,
10231
+ "data-debug-size": debugSize ? "" : undefined,
10232
+ "data-debug-break-after-clone": debugBreakAfterClone,
10233
+ children: [jsxs("div", {
8670
10234
  className: "ui_transition_outer_wrapper",
8671
- children: jsxs("div", {
8672
- className: "ui_transition_measure_wrapper",
8673
- children: [jsx("div", {
8674
- className: "ui_transition_slot",
8675
- "data-content-key": effectiveContentKey ? effectiveContentKey : undefined,
8676
- children: children
8677
- }), jsx("div", {
8678
- className: "ui_transition_phase_overlay"
8679
- })]
8680
- })
10235
+ children: [jsx("div", {
10236
+ className: "ui_transition_slot",
10237
+ "data-content-key": effectiveContentKey ? effectiveContentKey : undefined,
10238
+ children: children
10239
+ }), jsx("div", {
10240
+ className: "ui_transition_phase_overlay"
10241
+ })]
8681
10242
  }), jsx("div", {
8682
10243
  className: "ui_transition_content_overlay"
8683
10244
  })]
@@ -12605,6 +14166,8 @@ const applyContentSpacingOnTextChildren = (children, contentSpacing) => {
12605
14166
  installImportMetaCss(import.meta);import.meta.css = /* css */`
12606
14167
  @layer navi {
12607
14168
  .navi_link {
14169
+ --border-radius: 2px;
14170
+ --outline-color: var(--navi-focus-outline-color);
12608
14171
  --color: rgb(0, 0, 238);
12609
14172
  --color-visited: light-dark(#6a1b9a, #ab47bc);
12610
14173
  --color-active: red;
@@ -12615,9 +14178,6 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
12615
14178
  }
12616
14179
 
12617
14180
  .navi_link {
12618
- position: relative;
12619
- border-radius: 2px;
12620
-
12621
14181
  --x-color: var(--color);
12622
14182
  --x-color-hover: var(--color-hover, var(--color));
12623
14183
  --x-color-visited: var(--color-visited);
@@ -12626,8 +14186,11 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
12626
14186
  --x-text-decoration-hover: var(--text-decoration-hover,);
12627
14187
  --x-cursor: var(--cursor);
12628
14188
 
14189
+ position: relative;
12629
14190
  color: var(--x-color);
12630
14191
  text-decoration: var(--x-text-decoration);
14192
+ border-radius: var(--border-radius);
14193
+ outline-color: var(--outline-color);
12631
14194
  cursor: var(--x-cursor);
12632
14195
  }
12633
14196
  /* Hover */
@@ -12640,6 +14203,10 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
12640
14203
  position: relative;
12641
14204
  z-index: 1; /* Ensure focus outline is above other elements */
12642
14205
  }
14206
+ .navi_link[data-focus-visible] {
14207
+ outline-width: 2px;
14208
+ outline-style: solid;
14209
+ }
12643
14210
  /* Visited */
12644
14211
  .navi_link[data-visited] {
12645
14212
  --x-color: var(--x-color-visited);
@@ -12673,9 +14240,18 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
12673
14240
  }
12674
14241
  `;
12675
14242
  const LinkManagedByCSSVars = {
12676
- color: "--color",
12677
- cursor: "--cursor",
12678
- textDecoration: "--text-decoration"
14243
+ "outlineColor": "--outline-color",
14244
+ "borderRadius": "--border-radius",
14245
+ "color": "--color",
14246
+ "cursor": "--cursor",
14247
+ "textDecoration": "--text-decoration",
14248
+ ":hover": {
14249
+ color: "--color-hover",
14250
+ textDecoration: "--text-decoration-hover"
14251
+ },
14252
+ ":active": {
14253
+ color: "--color-active"
14254
+ }
12679
14255
  };
12680
14256
  const LinkPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":visited", ":-navi-loading", ":-navi-internal-link", ":-navi-external-link", ":-navi-anchor-link", ":-navi-current-link"];
12681
14257
  const LinkPseudoElements = ["::-navi-loader"];
@@ -14054,7 +15630,8 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
14054
15630
  --width: 13px;
14055
15631
  --height: 13px;
14056
15632
 
14057
- --outline-color: light-dark(#4476ff, #3b82f6);
15633
+ --outline-color: var(--navi-focus-outline-color);
15634
+ --loader-color: var(--navi-loader-color);
14058
15635
  --border-color: light-dark(#767676, #8e8e93);
14059
15636
  --background-color: white;
14060
15637
  --color: light-dark(#4476ff, #3b82f6);
@@ -14352,7 +15929,7 @@ const InputCheckboxBasic = props => {
14352
15929
  children: [jsx(LoaderBackground, {
14353
15930
  loading: innerLoading,
14354
15931
  inset: -1,
14355
- color: "var(--navi-loader-color)"
15932
+ color: "var(--loader-color)"
14356
15933
  }), renderCheckboxMemoized, jsx("div", {
14357
15934
  className: "navi_checkbox_field",
14358
15935
  children: jsx("svg", {
@@ -14443,7 +16020,8 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
14443
16020
  --width: 13px;
14444
16021
  --height: 13px;
14445
16022
 
14446
- --outline-color: light-dark(#4476ff, #3b82f6);
16023
+ --outline-color: var(--navi-focus-outline-color);
16024
+ --loader-color: var(--navi-loader-color);
14447
16025
  --border-color: light-dark(#767676, #8e8e93);
14448
16026
  --background-color: white;
14449
16027
  --color: light-dark(#4476ff, #3b82f6);
@@ -14785,7 +16363,7 @@ const InputRadioBasic = props => {
14785
16363
  loading: innerLoading,
14786
16364
  inset: -1,
14787
16365
  targetSelector: ".navi_radio_field",
14788
- color: "var(--navi-loader-color)"
16366
+ color: "var(--loader-color)"
14789
16367
  }), renderRadioMemoized, jsx("span", {
14790
16368
  className: "navi_radio_field",
14791
16369
  children: jsxs("svg", {
@@ -14830,7 +16408,8 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
14830
16408
  --outer-width: calc(var(--border-width) + var(--outline-width));
14831
16409
 
14832
16410
  /* Default */
14833
- --outline-color: light-dark(#4476ff, #3b82f6);
16411
+ --outline-color: var(--navi-focus-outline-color);
16412
+ --loader-color: var(--navi-loader-color);
14834
16413
  --border-color: light-dark(#767676, #8e8e93);
14835
16414
  --background-color: white;
14836
16415
  --color: currentColor;
@@ -15072,7 +16651,7 @@ const InputTextualBasic = props => {
15072
16651
  ...rest,
15073
16652
  children: [jsx(LoaderBackground, {
15074
16653
  loading: innerLoading,
15075
- color: "var(--navi-loader-color)",
16654
+ color: "var(--loader-color)",
15076
16655
  inset: -1
15077
16656
  }), renderInputMemoized]
15078
16657
  });
@@ -15433,7 +17012,8 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
15433
17012
  --border-width: 1px;
15434
17013
  --border-radius: 2px;
15435
17014
  /* default */
15436
- --outline-color: light-dark(#4476ff, #3b82f6);
17015
+ --outline-color: var(--navi-focus-outline-color);
17016
+ --loader-color: var(--navi-loader-color);
15437
17017
  --border-color: light-dark(#767676, #8e8e93);
15438
17018
  --background-color: light-dark(#f3f4f6, #2d3748);
15439
17019
  --color: currentColor;
@@ -15694,7 +17274,7 @@ const ButtonBasic = props => {
15694
17274
  children: [jsx(LoaderBackground, {
15695
17275
  loading: innerLoading,
15696
17276
  inset: -1,
15697
- color: "var(--navi-loader-color)"
17277
+ color: "var(--loader-color)"
15698
17278
  }), renderButtonContentMemoized]
15699
17279
  });
15700
17280
  };
@@ -20161,53 +21741,9 @@ const useSignalSync = (value, initialValue = value) => {
20161
21741
  return signal;
20162
21742
  };
20163
21743
 
20164
- installImportMetaCss(import.meta);import.meta.css = /* css */`
20165
- .navi_font_sized_svg {
20166
- display: flex;
20167
- width: 1em;
20168
- height: 1em;
20169
- flex-shrink: 0;
20170
- align-items: center;
20171
- justify-self: center;
20172
- line-height: 1em;
20173
- }
20174
- `;
20175
- const FontSizedSvg = ({
20176
- children,
20177
- ...rest
20178
- }) => {
20179
- return jsx(Box, {
20180
- ...rest,
20181
- as: "span",
20182
- baseClassName: "navi_font_sized_svg",
20183
- children: children
20184
- });
20185
- };
21744
+ const FontSizedSvg = () => {};
20186
21745
 
20187
- const IconAndText = ({
20188
- icon,
20189
- children,
20190
- ...rest
20191
- }) => {
20192
- if (typeof icon === "function") icon = icon({});
20193
- return jsxs("span", {
20194
- className: "icon_and_text",
20195
- ...rest,
20196
- style: {
20197
- display: "flex",
20198
- alignItems: "center",
20199
- gap: "0.1em",
20200
- ...rest.style
20201
- },
20202
- children: [jsx(FontSizedSvg, {
20203
- className: "icon",
20204
- children: icon
20205
- }), jsx("span", {
20206
- className: "text",
20207
- children: children
20208
- })]
20209
- });
20210
- };
21746
+ const IconAndText = () => {};
20211
21747
 
20212
21748
  installImportMetaCss(import.meta);import.meta.css = /* css */`
20213
21749
  .svg_mask_content * {
@@ -20550,5 +22086,5 @@ const useDependenciesDiff = (inputs) => {
20550
22086
  return diffRef.current;
20551
22087
  };
20552
22088
 
20553
- export { ActionRenderer, ActiveKeyboardShortcuts, Box, Button, CharSlot, Checkbox, CheckboxList, Code, Col, Colgroup, Count, Details, Editable, ErrorBoundaryContext, FontSizedSvg, Form, Icon, IconAndText, Image, Input, Label, Layout, Link, LinkWithIcon, MessageBox, Paragraph, Radio, RadioList, Route, RouteLink, Routes, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, Select, SelectionContext, SummaryMarker, Svg, Tab, TabList, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, actionIntegratedVia, addCustomMessage, createAction, createSelectionKeyboardShortcuts, createUniqueValueConstraint, enableDebugActions, enableDebugOnDocumentLoading, forwardActionRequested, goBack, goForward, goTo, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, openCallout, rawUrlPart, reload, removeCustomMessage, rerunActions, resource, setBaseUrl, setupRoutes, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useCellsAndColumns, useDependenciesDiff, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, valueInLocalStorage };
22089
+ export { ActionRenderer, ActiveKeyboardShortcuts, Box, Button, CharSlot, Checkbox, CheckboxList, Code, Col, Colgroup, Count, Details, Editable, ErrorBoundaryContext, FontSizedSvg, Form, Icon, IconAndText, Image, Input, Label, Layout, Link, LinkWithIcon, MessageBox, Paragraph, Radio, RadioList, Route, RouteLink, Routes, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, Select, SelectionContext, SummaryMarker, Svg, Tab, TabList, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, actionIntegratedVia, addCustomMessage, createAction, createSelectionKeyboardShortcuts, createUniqueValueConstraint, enableDebugActions, enableDebugOnDocumentLoading, forwardActionRequested, goBack, goForward, goTo, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, openCallout, rawUrlPart, reload, removeCustomMessage, rerunActions, resource, setBaseUrl, setupRoutes, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useCellsAndColumns, useDependenciesDiff, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, useUrlSearchParam, valueInLocalStorage };
20554
22090
  //# sourceMappingURL=jsenv_navi.js.map