@jsenv/navi 0.12.8 → 0.12.10

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,10 +1,10 @@
1
1
  import { installImportMetaCss } from "./jsenv_navi_side_effects.js";
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";
2
+ import { createIterableWeakSet, createPubSub, createValueEffect, createStyleController, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, activeElementSignal, canInterceptKeys, createGroupTransitionController, getElementSignature, getBorderRadius, getBackground, preventIntermediateScrollbar, createOpacityTransition, stringifyStyle, mergeOneStyle, 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";
6
6
  import { createContext, toChildArray, createRef, cloneElement } from "preact";
7
- import { jsx, jsxs, Fragment } from "preact/jsx-runtime";
7
+ import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
8
8
  import { createPortal, forwardRef } from "preact/compat";
9
9
 
10
10
  const actionPrivatePropertiesWeakMap = new WeakMap();
@@ -614,9 +614,9 @@ const weakEffect = (values, callback) => {
614
614
  return dispose;
615
615
  };
616
616
 
617
- let DEBUG$3 = false;
617
+ let DEBUG$2 = false;
618
618
  const enableDebugActions = () => {
619
- DEBUG$3 = true;
619
+ DEBUG$2 = 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$3) {
683
+ if (DEBUG$2) {
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$3) {
701
+ if (DEBUG$2) {
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$3) {
710
+ if (DEBUG$2) {
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$3) {
821
+ if (DEBUG$2) {
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$3) {
945
+ if (DEBUG$2) {
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$3) {
1041
+ if (DEBUG$2) {
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$3) {
1152
+ if (DEBUG$2) {
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$3) {
1424
+ if (DEBUG$2) {
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$3) {
1497
+ if (DEBUG$2) {
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$3) {
1524
+ if (DEBUG$2) {
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$3) {
1594
+ if (DEBUG$2) {
1595
1595
  console.log(`"${action}": stopping (reason: ${reason})`);
1596
1596
  }
1597
1597
 
@@ -5212,11 +5212,11 @@ const useExternalValueSync = (
5212
5212
  }
5213
5213
  };
5214
5214
 
5215
- const UNSET = {};
5215
+ const UNSET$1 = {};
5216
5216
  const useInitialValue = (compute) => {
5217
- const initialValueRef = useRef(UNSET);
5217
+ const initialValueRef = useRef(UNSET$1);
5218
5218
  let initialValue = initialValueRef.current;
5219
- if (initialValue !== UNSET) {
5219
+ if (initialValue !== UNSET$1) {
5220
5220
  return initialValue;
5221
5221
  }
5222
5222
 
@@ -8013,9 +8013,9 @@ const executeWithCleanup = (fn, cleanup) => {
8013
8013
  }
8014
8014
  };
8015
8015
 
8016
- let DEBUG$2 = false;
8016
+ let DEBUG$1 = false;
8017
8017
  const enableDebugOnDocumentLoading = () => {
8018
- DEBUG$2 = true;
8018
+ DEBUG$1 = true;
8019
8019
  };
8020
8020
 
8021
8021
  const windowIsLoadingSignal = signal(true);
@@ -8035,13 +8035,13 @@ const [
8035
8035
  removeFromDocumentLoadingRouteArraySignal,
8036
8036
  ] = arraySignal([]);
8037
8037
  const routingWhile = (fn, routeNames = []) => {
8038
- if (DEBUG$2 && routeNames.length > 0) {
8038
+ if (DEBUG$1 && routeNames.length > 0) {
8039
8039
  console.debug(`routingWhile: Adding routes to loading state:`, routeNames);
8040
8040
  }
8041
8041
  addToDocumentLoadingRouteArraySignal(...routeNames);
8042
8042
  return executeWithCleanup(fn, () => {
8043
8043
  removeFromDocumentLoadingRouteArraySignal(...routeNames);
8044
- if (DEBUG$2 && routeNames.length > 0) {
8044
+ if (DEBUG$1 && routeNames.length > 0) {
8045
8045
  console.debug(
8046
8046
  `routingWhile: Removed routes from loading state:`,
8047
8047
  routeNames,
@@ -8058,7 +8058,7 @@ const [
8058
8058
  removeFromDocumentLoadingActionArraySignal,
8059
8059
  ] = arraySignal([]);
8060
8060
  const workingWhile = (fn, actionNames = []) => {
8061
- if (DEBUG$2 && actionNames.length > 0) {
8061
+ if (DEBUG$1 && actionNames.length > 0) {
8062
8062
  console.debug(
8063
8063
  `workingWhile: Adding actions to loading state:`,
8064
8064
  actionNames,
@@ -8067,7 +8067,7 @@ const workingWhile = (fn, actionNames = []) => {
8067
8067
  addToDocumentLoadingActionArraySignal(...actionNames);
8068
8068
  return executeWithCleanup(fn, () => {
8069
8069
  removeFromDocumentLoadingActionArraySignal(...actionNames);
8070
- if (DEBUG$2 && actionNames.length > 0) {
8070
+ if (DEBUG$1 && actionNames.length > 0) {
8071
8071
  console.debug(
8072
8072
  `routingWhile: Removed action from loading state:`,
8073
8073
  actionNames,
@@ -8585,1104 +8585,807 @@ const useUrlSearchParam = (paramName) => {
8585
8585
  return [value, setSearchParamValue];
8586
8586
  };
8587
8587
 
8588
+ /**
8589
+ * Fix alignment behavior for flex/grid containers that use `height: 100%`.
8590
+ *
8591
+ * Context:
8592
+ * - When a flex/grid container has `height: 100%`, it is "height-locked".
8593
+ * - If its content becomes taller than the container, alignment rules like
8594
+ * `align-items: center` will cause content to be partially clipped.
8595
+ *
8596
+ * Goal:
8597
+ * - Center content only when it fits.
8598
+ * - Align content at start when it overflows.
8599
+ *
8600
+ * How:
8601
+ * - Temporarily remove height-constraint (`height:auto`) to measure natural height.
8602
+ * - Compare natural height to container height.
8603
+ * - Add/remove an attribute so CSS can adapt alignment.
8604
+ *
8605
+ * Usage:
8606
+ * monitorItemsOverflow(containerElement);
8607
+ *
8608
+ * CSS example:
8609
+ * .container { align-items: center; }
8610
+ * .container[data-items-height-overflow] { align-items: flex-start; }
8611
+ */
8612
+
8613
+
8614
+ const WIDTH_ATTRIBUTE_NAME = "data-items-width-overflow";
8615
+ const HEIGHT_ATTRIBUTE_NAME = "data-items-height-overflow";
8616
+ const monitorItemsOverflow = (container) => {
8617
+ let widthIsOverflowing;
8618
+ let heightIsOverflowing;
8619
+ const onItemsWidthOverflowChange = () => {
8620
+ if (widthIsOverflowing) {
8621
+ container.setAttribute(WIDTH_ATTRIBUTE_NAME, "");
8622
+ } else {
8623
+ container.removeAttribute(WIDTH_ATTRIBUTE_NAME);
8624
+ }
8625
+ };
8626
+ const onItemsHeightOverflowChange = () => {
8627
+ if (heightIsOverflowing) {
8628
+ container.setAttribute(HEIGHT_ATTRIBUTE_NAME, "");
8629
+ } else {
8630
+ container.removeAttribute(HEIGHT_ATTRIBUTE_NAME);
8631
+ }
8632
+ };
8633
+
8634
+ const update = () => {
8635
+ // Save current manual height constraint
8636
+ const prevWidth = container.style.width;
8637
+ const prevHeight = container.style.height;
8638
+ // Remove constraint → get true content dimension
8639
+ container.style.width = "auto";
8640
+ container.style.height = "auto";
8641
+ const naturalWidth = container.scrollWidth;
8642
+ const naturalHeight = container.scrollHeight;
8643
+ if (prevWidth) {
8644
+ container.style.width = prevWidth;
8645
+ } else {
8646
+ container.style.removeProperty("width");
8647
+ }
8648
+ if (prevHeight) {
8649
+ container.style.height = prevHeight;
8650
+ } else {
8651
+ container.style.removeProperty("height");
8652
+ }
8653
+
8654
+ const lockedWidth = container.clientWidth;
8655
+ const lockedHeight = container.clientHeight;
8656
+ const currentWidthIsOverflowing = naturalWidth > lockedWidth;
8657
+ const currentHeightIsOverflowing = naturalHeight > lockedHeight;
8658
+ if (currentWidthIsOverflowing !== widthIsOverflowing) {
8659
+ widthIsOverflowing = currentWidthIsOverflowing;
8660
+ onItemsWidthOverflowChange();
8661
+ }
8662
+ if (currentHeightIsOverflowing !== heightIsOverflowing) {
8663
+ heightIsOverflowing = currentHeightIsOverflowing;
8664
+ onItemsHeightOverflowChange();
8665
+ }
8666
+ };
8667
+
8668
+ const [teardown, addTeardown] = createPubSub();
8669
+
8670
+ update();
8671
+
8672
+ // mutation observer
8673
+ const mutationObserver = new MutationObserver(() => {
8674
+ update();
8675
+ });
8676
+ mutationObserver.observe(container, {
8677
+ childList: true,
8678
+ characterData: true,
8679
+ });
8680
+ addTeardown(() => {
8681
+ mutationObserver.disconnect();
8682
+ });
8683
+
8684
+ // resize observer
8685
+ const resizeObserver = new ResizeObserver(update);
8686
+ resizeObserver.observe(container);
8687
+ addTeardown(() => {
8688
+ resizeObserver.disconnect();
8689
+ });
8690
+
8691
+ const destroy = () => {
8692
+ teardown();
8693
+ container.removeAttribute(WIDTH_ATTRIBUTE_NAME);
8694
+ container.removeAttribute(HEIGHT_ATTRIBUTE_NAME);
8695
+ };
8696
+ return destroy;
8697
+ };
8698
+
8588
8699
  installImportMetaCss(import.meta);
8589
8700
  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;
8701
+ * {
8702
+ box-sizing: border-box;
8595
8703
  }
8596
8704
 
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;
8705
+ .ui_transition {
8706
+ --transition-duration: 300ms;
8707
+ --justify-content: center;
8708
+ --align-items: center;
8709
+
8710
+ --x-transition-duration: var(--transition-duration);
8711
+ --x-justify-content: var(--justify-content);
8712
+ --x-align-items: var(--align-items);
8713
+
8714
+ position: relative;
8715
+ }
8716
+ /* Alignment controls */
8717
+ .ui_transition[data-align-x="start"] {
8718
+ --x-justify-content: flex-start;
8719
+ }
8720
+ .ui_transition[data-align-x="center"] {
8721
+ --x-justify-content: center;
8722
+ }
8723
+ .ui_transition[data-align-x="end"] {
8724
+ --x-justify-content: flex-end;
8725
+ }
8726
+ .ui_transition[data-align-y="start"] {
8727
+ --x-align-items: flex-start;
8728
+ }
8729
+ .ui_transition[data-align-y="center"] {
8730
+ --x-align-items: center;
8731
+ }
8732
+ .ui_transition[data-align-y="end"] {
8733
+ --x-align-items: flex-end;
8612
8734
  }
8613
8735
 
8614
- .ui_transition_slot {
8736
+ .ui_transition,
8737
+ .ui_transition_active_group,
8738
+ .ui_transition_previous_group,
8739
+ .ui_transition_target_slot,
8740
+ .ui_transition_previous_target_slot,
8741
+ .ui_transition_outgoing_slot,
8742
+ .ui_transition_previous_outgoing_slot {
8615
8743
  width: 100%;
8616
- min-width: fit-content;
8617
8744
  height: 100%;
8618
- min-height: fit-content;
8619
- flex-direction: column;
8620
8745
  }
8621
8746
 
8622
- .ui_transition_container,
8623
- .ui_transition_slot {
8624
- position: relative;
8747
+ .ui_transition_target_slot,
8748
+ .ui_transition_outgoing_slot,
8749
+ .ui_transition_previous_target_slot,
8750
+ .ui_transition_previous_outgoing_slot {
8751
+ display: flex;
8752
+ align-items: var(--x-align-items);
8753
+ justify-content: var(--x-justify-content);
8754
+ }
8755
+ .ui_transition_target_slot[data-items-width-overflow],
8756
+ .ui_transition_previous_target_slot[data-items-width-overflow],
8757
+ .ui_transition_previous_target_slot[data-items-width-overflow],
8758
+ .ui_transition_previous_outgoing_slot[data-items-width-overflow] {
8759
+ --x-justify-content: flex-start;
8760
+ }
8761
+ .ui_transition_target_slot[data-items-height-overflow],
8762
+ .ui_transition_previous_slot[data-items-height-overflow],
8763
+ .ui_transition_previous_target_slot[data-items-height-overflow],
8764
+ .ui_transition_previous_outgoing_slot[data-items-height-overflow] {
8765
+ --x-align-items: flex-start;
8625
8766
  }
8626
8767
 
8627
- .ui_transition_phase_overlay,
8628
- .ui_transition_content_overlay {
8768
+ .ui_transition_active_group {
8769
+ position: relative;
8770
+ }
8771
+ .ui_transition_target_slot {
8772
+ position: relative;
8773
+ }
8774
+ .ui_transition_outgoing_slot,
8775
+ .ui_transition_previous_outgoing_slot {
8776
+ position: absolute;
8777
+ top: 0;
8778
+ left: 0;
8779
+ }
8780
+ .ui_transition_previous_group {
8629
8781
  position: absolute;
8630
8782
  inset: 0;
8783
+ }
8784
+ .ui_transition[data-only-previous-group] .ui_transition_previous_group {
8785
+ position: relative;
8786
+ }
8787
+
8788
+ .ui_transition_target_slot_background {
8789
+ position: absolute;
8790
+ top: 0;
8791
+ left: 0;
8792
+ z-index: -1;
8793
+ display: none;
8794
+ width: var(--target-slot-width, 100%);
8795
+ height: var(--target-slot-height, 100%);
8796
+ background: var(--target-slot-background, transparent);
8631
8797
  pointer-events: none;
8632
8798
  }
8799
+ .ui_transition[data-transitioning] .ui_transition_target_slot_background {
8800
+ display: block;
8801
+ }
8633
8802
  `;
8634
8803
 
8635
- const DEBUG$1 = {
8636
- detection: false,
8637
- size: false,
8638
- content: false,
8639
- transition_updates: false,
8640
- };
8804
+ const CONTENT_ID_ATTRIBUTE = "data-content-id";
8805
+ const CONTENT_PHASE_ATTRIBUTE = "data-content-phase";
8806
+ const UNSET = {
8807
+ domNodes: [],
8808
+ domNodesClone: [],
8809
+ isEmpty: true,
8641
8810
 
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
8811
+ type: "unset",
8812
+ contentId: "unset",
8813
+ contentPhase: undefined,
8814
+ isContentPhase: false,
8815
+ isContent: false,
8816
+ toString: () => "unset",
8817
+ };
8648
8818
 
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
- }
8819
+ const isSameConfiguration = (configA, configB) => {
8820
+ return configA.toString() === configB.toString();
8821
+ };
8654
8822
 
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"),
8823
+ const createUITransitionController = (
8824
+ root,
8825
+ {
8826
+ duration = 300,
8827
+ alignX = "center",
8828
+ alignY = "center",
8829
+ onStateChange = () => {},
8830
+ pauseBreakpoints = [],
8831
+ } = {},
8832
+ ) => {
8833
+ const debugConfig = {
8834
+ detection: root.hasAttribute("data-debug-detection"),
8835
+ size: root.hasAttribute("data-debug-size"),
8660
8836
  };
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
- }
8837
+ const hasDebugLogs = debugConfig.size;
8838
+ const debugDetection = (message) => {
8839
+ if (!debugConfig.detection) return;
8840
+ console.debug(`[detection]`, message);
8841
+ };
8842
+ const debugSize = (message) => {
8843
+ if (!debugConfig.size) return;
8844
+ console.debug(`[size]`, message);
8671
8845
  };
8672
8846
 
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",
8847
+ const activeGroup = root.querySelector(".ui_transition_active_group");
8848
+ const targetSlot = root.querySelector(".ui_transition_target_slot");
8849
+ const outgoingSlot = root.querySelector(".ui_transition_outgoing_slot");
8850
+ const previousGroup = root.querySelector(".ui_transition_previous_group");
8851
+ const previousTargetSlot = previousGroup?.querySelector(
8852
+ ".ui_transition_previous_target_slot",
8677
8853
  );
8678
- const contentOverlay = container.querySelector(
8679
- ".ui_transition_content_overlay",
8854
+ const previousOutgoingSlot = previousGroup?.querySelector(
8855
+ ".ui_transition_previous_outgoing_slot",
8680
8856
  );
8681
- if (!outerWrapper || !slot || !phaseOverlay || !contentOverlay) {
8682
- console.error("Missing required ui-transition structure");
8683
- return { cleanup: () => {} };
8857
+
8858
+ if (
8859
+ !root ||
8860
+ !activeGroup ||
8861
+ !targetSlot ||
8862
+ !outgoingSlot ||
8863
+ !previousGroup ||
8864
+ !previousTargetSlot ||
8865
+ !previousOutgoingSlot
8866
+ ) {
8867
+ throw new Error(
8868
+ "createUITransitionController requires element with .active_group, .target_slot, .outgoing_slot, .previous_group, .previous_target_slot, and .previous_outgoing_slot elements",
8869
+ );
8684
8870
  }
8685
8871
 
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;
8872
+ // we maintain a background copy behind target slot to avoid showing
8873
+ // the body flashing during the fade-in
8874
+ const targetSlotBackground = document.createElement("div");
8875
+ targetSlotBackground.className = "ui_transition_target_slot_background";
8876
+ activeGroup.insertBefore(targetSlotBackground, targetSlot);
8704
8877
 
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
- }
8878
+ root.style.setProperty("--x-transition-duration", `${duration}ms`);
8879
+ outgoingSlot.setAttribute("inert", "");
8880
+ previousGroup.setAttribute("inert", "");
8749
8881
 
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;
8882
+ const detectConfiguration = (slot, { contentId, contentPhase } = {}) => {
8883
+ const domNodes = Array.from(slot.childNodes);
8884
+ if (!domNodes) {
8885
+ return UNSET;
8763
8886
  }
8764
8887
 
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;
8888
+ const isEmpty = domNodes.length === 0;
8889
+ let textNodeCount = 0;
8890
+ let elementNodeCount = 0;
8891
+ let firstElementNode;
8892
+ const domNodesClone = [];
8893
+ if (isEmpty) {
8894
+ if (contentPhase === undefined) {
8895
+ contentPhase = "empty";
8825
8896
  }
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) {
8897
+ } else {
8898
+ const contentIdSlotAttr = slot.getAttribute(CONTENT_ID_ATTRIBUTE);
8899
+ let contentIdChildAttr;
8900
+ for (const domNode of domNodes) {
8901
+ if (domNode.nodeType === Node.TEXT_NODE) {
8902
+ textNodeCount++;
8839
8903
  } else {
8840
- publishChange();
8841
- previousSlotInfo = slotInfo;
8842
- if (
8843
- changeInfo.isInitialPopulationWithoutTransition ||
8844
- changeInfo.becomesPopulated
8845
- ) {
8846
- hasPopulatedOnce = true;
8904
+ if (!firstElementNode) {
8905
+ firstElementNode = domNode;
8847
8906
  }
8848
- }
8849
- } finally {
8850
- isUpdating = false;
8851
- if (hasSomeDebugLogs) {
8852
- console.groupEnd();
8853
- }
8854
- }
8855
- };
8907
+ elementNodeCount++;
8856
8908
 
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;
8909
+ if (domNode.hasAttribute("data-content-phase")) {
8910
+ const contentPhaseAttr = domNode.getAttribute("data-content-phase");
8911
+ contentPhase = contentPhaseAttr || "attr";
8872
8912
  }
8873
- if (childNode.hasAttribute("data-content-key")) {
8874
- childContentKey = childNode.getAttribute("data-content-key");
8913
+ if (domNode.hasAttribute("data-content-key")) {
8914
+ contentIdChildAttr = domNode.getAttribute("data-content-key");
8875
8915
  }
8876
8916
  }
8917
+ const domNodeClone = domNode.cloneNode(true);
8918
+ domNodesClone.push(domNodeClone);
8877
8919
  }
8878
- const slotContentKey = slot.getAttribute("data-content-key");
8879
- if (childContentKey && slotContentKey) {
8920
+
8921
+ if (contentIdSlotAttr && contentIdChildAttr) {
8880
8922
  console.warn(
8881
- `Slot and slot child both have a [data-content-key]. Slot is ${slotContentKey} and child is ${childContentKey}, using the child.`,
8923
+ `Slot and slot child both have a [${CONTENT_ID_ATTRIBUTE}]. Slot is ${contentIdSlotAttr} and child is ${contentIdChildAttr}, using the child.`,
8882
8924
  );
8883
8925
  }
8884
- const contentKey = childContentKey || slotContentKey || undefined;
8885
- slotInfo = createSlotInfo(currentChildNodes, {
8886
- contentKey,
8887
- contentPhase,
8888
- });
8926
+ if (contentId === undefined) {
8927
+ contentId = contentIdChildAttr || contentIdSlotAttr || undefined;
8928
+ }
8929
+ }
8930
+ const isOnlyTextNodes = elementNodeCount === 0 && textNodeCount > 1;
8931
+ const singleElementNode = elementNodeCount === 1 ? firstElementNode : null;
8932
+
8933
+ contentId = contentId || getElementSignature(domNodes[0]);
8934
+ if (!contentPhase && isEmpty) {
8935
+ // Imagine code rendering null while switching to a new content
8936
+ // or even while staying on the same content.
8937
+ // In the UI we want to consider this as an "empty" phase.
8938
+ // meaning the ui will keep the same size until something else happens
8939
+ // This prevent layout shifts of code not properly handling
8940
+ // intermediate states.
8941
+ contentPhase = "empty";
8942
+ }
8943
+
8944
+ let width;
8945
+ let height;
8946
+ let borderRadius;
8947
+ let border;
8948
+ let background;
8949
+
8950
+ if (isEmpty) {
8951
+ debugSize(`measureSlot(".${slot.className}") -> it is empty`);
8952
+ } else if (singleElementNode) {
8953
+ const rect = singleElementNode.getBoundingClientRect();
8954
+ width = rect.width;
8955
+ height = rect.height;
8956
+ debugSize(`measureSlot(".${slot.className}") -> [${width}x${height}]`);
8957
+ borderRadius = getBorderRadius(singleElementNode);
8958
+ border = getComputedStyle(singleElementNode).border;
8959
+ background = getBackground(singleElementNode);
8960
+ } else {
8961
+ // text, multiple elements
8962
+ const rect = slot.getBoundingClientRect();
8963
+ width = rect.width;
8964
+ height = rect.height;
8965
+ debugSize(`measureSlot(".${slot.className}") -> [${width}x${height}]`);
8966
+ }
8889
8967
 
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,
8968
+ const commonProperties = {
8969
+ domNodes,
8970
+ domNodesClone,
8971
+ isEmpty,
8972
+ isOnlyTextNodes,
8973
+ singleElementNode,
8974
+
8975
+ width,
8976
+ height,
8977
+ borderRadius,
8978
+ border,
8979
+ background,
8980
+
8981
+ contentId,
8982
+ };
8983
+
8984
+ if (contentPhase) {
8985
+ return {
8986
+ ...commonProperties,
8987
+ type: "content_phase",
8988
+ contentPhase,
8989
+ isContentPhase: true,
8990
+ isContent: false,
8991
+ toString: () => `content(${contentId}).phase(${contentPhase})`,
8978
8992
  };
8993
+ }
8994
+ return {
8995
+ ...commonProperties,
8996
+ type: "content",
8997
+ contentPhase: undefined,
8998
+ isContentPhase: false,
8999
+ isContent: true,
9000
+ toString: () => `content(${contentId})`,
8979
9001
  };
8980
- }
9002
+ };
8981
9003
 
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;
9004
+ const targetSlotInitialConfiguration = detectConfiguration(targetSlot);
9005
+ const outgoingSlotInitialConfiguration = detectConfiguration(outgoingSlot, {
9006
+ contentPhase: "true",
9007
+ });
9008
+ let targetSlotConfiguration = targetSlotInitialConfiguration;
9009
+ let outgoingSlotConfiguration = outgoingSlotInitialConfiguration;
9010
+ let previousTargetSlotConfiguration = UNSET;
8990
9011
 
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
- });
9012
+ const updateSlotAttributes = () => {
9013
+ if (targetSlotConfiguration.isEmpty && outgoingSlotConfiguration.isEmpty) {
9014
+ root.setAttribute("data-only-previous-group", "");
9015
+ } else {
9016
+ root.removeAttribute("data-only-previous-group");
9017
+ }
9018
+ };
9019
+ const updateAlignment = () => {
9020
+ // Set data attributes for CSS-based alignment
9021
+ root.setAttribute("data-align-x", alignX);
9022
+ root.setAttribute("data-align-y", alignY);
9023
+ };
9024
+
9025
+ const moveConfigurationIntoSlot = (configuration, slot) => {
9026
+ slot.innerHTML = "";
9027
+ for (const domNode of configuration.domNodesClone) {
9028
+ slot.appendChild(domNode);
9029
+ }
9030
+ // in case border or stuff like that have changed we re-detect the config
9031
+ const updatedConfig = detectConfiguration(slot);
9032
+ if (slot === targetSlot) {
9033
+ targetSlotConfiguration = updatedConfig;
9034
+ } else if (slot === outgoingSlot) {
9035
+ outgoingSlotConfiguration = updatedConfig;
9036
+ } else if (slot === previousTargetSlot) {
9037
+ previousTargetSlotConfiguration = updatedConfig;
9038
+ } else if (slot === previousOutgoingSlot) ; else {
9039
+ throw new Error("Unknown slot for applyConfiguration");
9040
+ }
9041
+ };
9042
+
9043
+ updateAlignment();
9044
+
9045
+ let transitionType = "none";
9046
+ const groupTransitionOptions = {
9047
+ // debugBreakpoints: [0.25],
9048
+ pauseBreakpoints,
9049
+ lifecycle: {
9050
+ setup: () => {
9051
+ updateSlotAttributes();
9052
+ root.setAttribute("data-transitioning", "");
9053
+ onStateChange({ isTransitioning: true });
9054
+ return {
9055
+ teardown: () => {
9056
+ root.removeAttribute("data-transitioning");
9057
+ updateSlotAttributes(); // Update positioning after transition
9058
+ onStateChange({ isTransitioning: false });
9059
+ },
9037
9060
  };
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)",
9061
+ },
9062
+ },
9063
+ };
9064
+ const transitionController = createGroupTransitionController(
9065
+ groupTransitionOptions,
9066
+ );
9067
+
9068
+ const elementToClip = root;
9069
+ const morphContainerIntoTarget = () => {
9070
+ const morphTransitions = [];
9071
+ {
9072
+ // TODO: ideally when scrollContainer is document AND we transition
9073
+ // from a layout with scrollbar to a layout without
9074
+ // we have clip path detecting we go from a given width/height to a new width/height
9075
+ // that might just be the result of scrollbar appearing/disappearing
9076
+ // we should detect when this happens to avoid clipping what correspond to the scrollbar presence toggling
9077
+ const fromWidth = previousTargetSlotConfiguration.width || 0;
9078
+ const fromHeight = previousTargetSlotConfiguration.height || 0;
9079
+ const toWidth = targetSlotConfiguration.width || 0;
9080
+ const toHeight = targetSlotConfiguration.height || 0;
9081
+ debugSize(
9082
+ `transition from [${fromWidth}x${fromHeight}] to [${toWidth}x${toHeight}]`,
9083
+ );
9084
+ const restoreOverflow = preventIntermediateScrollbar(root, {
9085
+ fromWidth,
9086
+ fromHeight,
9087
+ toWidth,
9088
+ toHeight,
9089
+ onPrevent: ({ x, y, scrollContainer }) => {
9090
+ if (x) {
9091
+ debugSize(
9092
+ `Temporarily hiding horizontal overflow during transition on ${getElementSignature(scrollContainer)}`,
9056
9093
  );
9057
- return;
9058
9094
  }
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})`,
9095
+ if (y) {
9096
+ debugSize(
9097
+ `Temporarily hiding vertical overflow during transition on ${getElementSignature(scrollContainer)}`,
9067
9098
  );
9068
- return;
9069
- }
9070
- if (localDebug.size) {
9071
- console.group("[resize observer] size change detected");
9072
9099
  }
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();
9100
+ },
9101
+ onRestore: () => {
9102
+ debugSize(`Restored overflow after transition`);
9103
+ },
9087
9104
  });
9088
- }
9089
9105
 
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);
9106
+ const onSizeTransitionFinished = () => {
9107
+ // Restore overflow when transition is complete
9108
+ restoreOverflow();
9128
9109
  };
9129
- release.releaseResizeObserver = () => {
9130
- resumeResizeObserver(reason);
9110
+
9111
+ // https://emilkowal.ski/ui/the-magic-of-clip-path
9112
+ const elementToClipRect = elementToClip.getBoundingClientRect();
9113
+ const elementToClipWidth = elementToClipRect.width;
9114
+ const elementToClipHeight = elementToClipRect.height;
9115
+ // Calculate where content is positioned within the large container
9116
+ const getAlignedPosition = (containerSize, contentSize, align) => {
9117
+ switch (align) {
9118
+ case "start":
9119
+ return 0;
9120
+ case "end":
9121
+ return containerSize - contentSize;
9122
+ case "center":
9123
+ default:
9124
+ return (containerSize - contentSize) / 2;
9125
+ }
9131
9126
  };
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,
9127
+ // Position of "from" content within large container
9128
+ const fromLeft = getAlignedPosition(
9129
+ elementToClipWidth,
9130
+ fromWidth,
9131
+ alignX,
9184
9132
  );
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",
9133
+ const fromTop = getAlignedPosition(
9134
+ elementToClipHeight,
9135
+ fromHeight,
9136
+ alignY,
9244
9137
  );
9245
- sizeTransition = transitionController.animate(transitions, {
9246
- onCancel: () => {
9247
- release.releaseResizeObserver("size transition cancelled");
9248
- },
9249
- onFinish: () => {
9250
- release("size transition finished");
9138
+ // Position of target content within large container
9139
+ const targetLeft = getAlignedPosition(
9140
+ elementToClipWidth,
9141
+ toWidth,
9142
+ alignX,
9143
+ );
9144
+ const targetTop = getAlignedPosition(
9145
+ elementToClipHeight,
9146
+ toHeight,
9147
+ alignY,
9148
+ );
9149
+ debugSize(
9150
+ `Positions in container: from [${fromLeft},${fromTop}] ${fromWidth}x${fromHeight} to [${targetLeft},${targetTop}] ${toWidth}x${toHeight}`,
9151
+ );
9152
+ // Get border-radius values
9153
+ const fromBorderRadius =
9154
+ previousTargetSlotConfiguration.borderRadius || 0;
9155
+ const toBorderRadius = targetSlotConfiguration.borderRadius || 0;
9156
+ const startInsetTop = fromTop;
9157
+ const startInsetRight = elementToClipWidth - (fromLeft + fromWidth);
9158
+ const startInsetBottom = elementToClipHeight - (fromTop + fromHeight);
9159
+ const startInsetLeft = fromLeft;
9160
+
9161
+ const endInsetTop = targetTop;
9162
+ const endInsetRight = elementToClipWidth - (targetLeft + toWidth);
9163
+ const endInsetBottom = elementToClipHeight - (targetTop + toHeight);
9164
+ const endInsetLeft = targetLeft;
9165
+
9166
+ const startClipPath = `inset(${startInsetTop}px ${startInsetRight}px ${startInsetBottom}px ${startInsetLeft}px round ${fromBorderRadius}px)`;
9167
+ const endClipPath = `inset(${endInsetTop}px ${endInsetRight}px ${endInsetBottom}px ${endInsetLeft}px round ${toBorderRadius}px)`;
9168
+ // Create clip-path animation using Web Animations API
9169
+ const clipAnimation = elementToClip.animate(
9170
+ [{ clipPath: startClipPath }, { clipPath: endClipPath }],
9171
+ {
9172
+ duration,
9173
+ easing: "ease",
9174
+ fill: "forwards",
9251
9175
  },
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
9176
  );
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
9177
 
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);
9178
+ // Handle finish
9179
+ clipAnimation.finished
9180
+ .then(() => {
9181
+ // Clear clip-path to restore normal behavior
9182
+ elementToClip.style.clipPath = "";
9183
+ clipAnimation.cancel();
9184
+ onSizeTransitionFinished();
9185
+ })
9186
+ .catch(() => {
9187
+ // Animation was cancelled
9188
+ });
9189
+ clipAnimation.play();
9190
+ }
9371
9191
 
9372
- addPauseCallback(() => {
9373
- sizeTransition?.pause();
9192
+ return morphTransitions;
9193
+ };
9194
+ const fadeInTargetSlot = () => {
9195
+ targetSlotBackground.style.setProperty(
9196
+ "--target-slot-background",
9197
+ stringifyStyle(targetSlotConfiguration.background, "background"),
9198
+ );
9199
+ targetSlotBackground.style.setProperty(
9200
+ "--target-slot-width",
9201
+ `${targetSlotConfiguration.width || 0}px`,
9202
+ );
9203
+ targetSlotBackground.style.setProperty(
9204
+ "--target-slot-height",
9205
+ `${targetSlotConfiguration.height || 0}px`,
9206
+ );
9207
+ return createOpacityTransition(targetSlot, 1, {
9208
+ from: 0,
9209
+ duration,
9210
+ styleSynchronizer: "inline_style",
9211
+ onFinish: (targetSlotOpacityTransition) => {
9212
+ targetSlotOpacityTransition.cancel();
9213
+ },
9374
9214
  });
9375
- addResumeCallback(() => {
9376
- sizeTransition?.play();
9215
+ };
9216
+ const fadeOutPreviousGroup = () => {
9217
+ return createOpacityTransition(previousGroup, 0, {
9218
+ from: 1,
9219
+ duration,
9220
+ styleSynchronizer: "inline_style",
9221
+ onFinish: (previousGroupOpacityTransition) => {
9222
+ previousGroupOpacityTransition.cancel();
9223
+ previousGroup.style.opacity = "0"; // keep previous group visually hidden
9224
+ },
9377
9225
  });
9378
- addTeardown(() => {
9379
- sizeTransition?.cancel();
9226
+ };
9227
+ const fadeOutOutgoingSlot = () => {
9228
+ return createOpacityTransition(outgoingSlot, 0, {
9229
+ duration,
9230
+ from: 1,
9231
+ styleSynchronizer: "inline_style",
9232
+ onFinish: (outgoingSlotOpacityTransition) => {
9233
+ outgoingSlotOpacityTransition.cancel();
9234
+ outgoingSlot.style.opacity = "0"; // keep outgoing slot visually hidden
9235
+ },
9380
9236
  });
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;
9237
+ };
9572
9238
 
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();
9239
+ // content_to_content transition (uses previous_group)
9240
+ const applyContentToContentTransition = (toConfiguration) => {
9241
+ // 1. move target slot to previous
9242
+ moveConfigurationIntoSlot(targetSlotConfiguration, previousTargetSlot);
9243
+ targetSlotConfiguration = toConfiguration;
9244
+ // 2. move outgoing slot to previous
9245
+ moveConfigurationIntoSlot(outgoingSlotConfiguration, previousOutgoingSlot);
9246
+ moveConfigurationIntoSlot(UNSET, outgoingSlot);
9247
+
9248
+ const transitions = [
9249
+ ...morphContainerIntoTarget(),
9250
+ fadeInTargetSlot(),
9251
+ fadeOutPreviousGroup(),
9252
+ ];
9253
+ const transition = transitionController.update(transitions, {
9254
+ onFinish: () => {
9255
+ moveConfigurationIntoSlot(UNSET, previousTargetSlot);
9256
+ moveConfigurationIntoSlot(UNSET, previousOutgoingSlot);
9257
+ if (hasDebugLogs) {
9258
+ console.groupEnd();
9588
9259
  }
9260
+ },
9261
+ });
9262
+ transition.play();
9263
+ };
9264
+ // content_phase_to_content_phase transition (uses outgoing_slot)
9265
+ const applyContentPhaseToContentPhaseTransition = (toConfiguration) => {
9266
+ // 1. Move target slot to outgoing
9267
+ moveConfigurationIntoSlot(targetSlotConfiguration, outgoingSlot);
9268
+ targetSlotConfiguration = toConfiguration;
9269
+
9270
+ const transitions = [
9271
+ ...morphContainerIntoTarget(),
9272
+ fadeInTargetSlot(),
9273
+ fadeOutOutgoingSlot(),
9274
+ ];
9275
+ const transition = transitionController.update(transitions, {
9276
+ onFinish: () => {
9277
+ moveConfigurationIntoSlot(UNSET, outgoingSlot);
9589
9278
 
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();
9279
+ if (hasDebugLogs) {
9280
+ console.groupEnd();
9635
9281
  }
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();
9282
+ },
9648
9283
  });
9649
- addTeardown(() => {
9650
- activeContentTransition?.cancel();
9651
- activePhaseTransition?.cancel();
9284
+ transition.play();
9285
+ };
9286
+ // any_to_empty transition
9287
+ const applyToEmptyTransition = () => {
9288
+ // 1. move target slot to previous
9289
+ moveConfigurationIntoSlot(targetSlotConfiguration, previousTargetSlot);
9290
+ targetSlotConfiguration = UNSET;
9291
+ // 2. move outgoing slot to previous
9292
+ moveConfigurationIntoSlot(outgoingSlotConfiguration, previousOutgoingSlot);
9293
+ outgoingSlotConfiguration = UNSET;
9294
+
9295
+ const transitions = [...morphContainerIntoTarget(), fadeOutPreviousGroup()];
9296
+ const transition = transitionController.update(transitions, {
9297
+ onFinish: () => {
9298
+ moveConfigurationIntoSlot(UNSET, previousTargetSlot);
9299
+ moveConfigurationIntoSlot(UNSET, previousOutgoingSlot);
9300
+ if (hasDebugLogs) {
9301
+ console.groupEnd();
9302
+ }
9303
+ },
9652
9304
  });
9653
- }
9305
+ transition.play();
9306
+ };
9307
+ // Main transition method
9308
+ const transitionTo = (
9309
+ newContentElement,
9310
+ { contentPhase, contentId } = {},
9311
+ ) => {
9312
+ if (contentId) {
9313
+ targetSlot.setAttribute(CONTENT_ID_ATTRIBUTE, contentId);
9314
+ } else {
9315
+ targetSlot.removeAttribute(CONTENT_ID_ATTRIBUTE);
9316
+ }
9317
+ if (contentPhase) {
9318
+ targetSlot.setAttribute(CONTENT_PHASE_ATTRIBUTE, contentPhase);
9319
+ } else {
9320
+ targetSlot.removeAttribute(CONTENT_PHASE_ATTRIBUTE);
9321
+ }
9322
+ if (newContentElement) {
9323
+ targetSlot.innerHTML = "";
9324
+ targetSlot.appendChild(newContentElement);
9325
+ } else {
9326
+ targetSlot.innerHTML = "";
9327
+ }
9328
+ };
9329
+ // Reset to initial content
9330
+ const resetContent = () => {
9331
+ transitionController.cancel();
9332
+ moveConfigurationIntoSlot(targetSlotInitialConfiguration, targetSlot);
9333
+ moveConfigurationIntoSlot(outgoingSlotInitialConfiguration, outgoingSlot);
9334
+ moveConfigurationIntoSlot(UNSET, previousTargetSlot);
9335
+ moveConfigurationIntoSlot(UNSET, previousOutgoingSlot);
9336
+ };
9654
9337
 
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");
9338
+ const targetSlotEffect = (reasons) => {
9339
+ const fromConfiguration = targetSlotConfiguration;
9340
+ const toConfiguration = detectConfiguration(targetSlot);
9341
+ if (hasDebugLogs) {
9342
+ console.group(`targetSlotEffect()`);
9343
+ console.debug(`reasons:`);
9344
+ console.debug(`- ${reasons.join("\n- ")}`);
9345
+ }
9346
+ if (isSameConfiguration(fromConfiguration, toConfiguration)) {
9347
+ debugDetection(
9348
+ `already in desired state (${toConfiguration}) -> early return`,
9349
+ );
9350
+ if (hasDebugLogs) {
9351
+ console.groupEnd();
9662
9352
  }
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
- }
9353
+ return;
9354
+ }
9355
+ const fromConfigType = fromConfiguration.type;
9356
+ const toConfigType = toConfiguration.type;
9357
+ transitionType = `${fromConfigType}_to_${toConfigType}`;
9358
+ debugDetection(
9359
+ `Prepare "${transitionType}" transition (${fromConfiguration} -> ${toConfiguration})`,
9360
+ );
9361
+ // content_to_empty / content_phase_to_empty
9362
+ if (toConfiguration.isEmpty) {
9363
+ applyToEmptyTransition();
9364
+ return;
9365
+ }
9366
+ // content_phase_to_content_phase
9367
+ if (fromConfiguration.isContentPhase && toConfiguration.isContentPhase) {
9368
+ applyContentPhaseToContentPhaseTransition(toConfiguration);
9369
+ return;
9370
+ }
9371
+ // content_phase_to_content
9372
+ if (fromConfiguration.isContentPhase && toConfiguration.isContent) {
9373
+ applyContentPhaseToContentPhaseTransition(toConfiguration);
9374
+ return;
9375
+ }
9376
+ // content_to_content_phase
9377
+ if (fromConfiguration.isContent && toConfiguration.isContentPhase) {
9378
+ applyContentPhaseToContentPhaseTransition(toConfiguration);
9379
+ return;
9380
+ }
9381
+ // content_to_content (default case)
9382
+ applyContentToContentTransition(toConfiguration);
9383
+ };
9679
9384
 
9680
- // Run once at init to process current slot content
9681
- triggerChildSlotMutation("init");
9385
+ const [teardown, addTeardown] = createPubSub();
9682
9386
  {
9683
9387
  const mutationObserver = new MutationObserver((mutations) => {
9684
9388
  const reasonParts = [];
9685
-
9686
9389
  for (const mutation of mutations) {
9687
9390
  if (mutation.type === "childList") {
9688
9391
  const added = mutation.addedNodes.length;
@@ -9699,10 +9402,27 @@ const initUITransition = (container) => {
9699
9402
  if (mutation.type === "attributes") {
9700
9403
  const { attributeName } = mutation;
9701
9404
  if (
9702
- attributeName === "data-content-key" ||
9703
- attributeName === "data-content-phase"
9405
+ attributeName === CONTENT_ID_ATTRIBUTE ||
9406
+ attributeName === CONTENT_PHASE_ATTRIBUTE
9704
9407
  ) {
9705
- reasonParts.push(`[${attributeName}] change`);
9408
+ const { oldValue } = mutation;
9409
+ if (oldValue === null) {
9410
+ const value = targetSlot.getAttribute(attributeName);
9411
+ reasonParts.push(
9412
+ value
9413
+ ? `added [${attributeName}=${value}]`
9414
+ : `added [${attributeName}]`,
9415
+ );
9416
+ } else if (targetSlot.hasAttribute(attributeName)) {
9417
+ const value = targetSlot.getAttribute(attributeName);
9418
+ reasonParts.push(`[${attributeName}] ${oldValue} -> ${value}`);
9419
+ } else {
9420
+ reasonParts.push(
9421
+ oldValue
9422
+ ? `removed [${attributeName}=${oldValue}]`
9423
+ : `removed [${attributeName}]`,
9424
+ );
9425
+ }
9706
9426
  }
9707
9427
  }
9708
9428
  }
@@ -9710,410 +9430,63 @@ const initUITransition = (container) => {
9710
9430
  if (reasonParts.length === 0) {
9711
9431
  return;
9712
9432
  }
9713
- const reason = reasonParts.join("+");
9714
- triggerChildSlotMutation(reason);
9433
+ targetSlotEffect(reasonParts);
9715
9434
  });
9716
- mutationObserver.observe(slot, {
9435
+ mutationObserver.observe(targetSlot, {
9717
9436
  childList: true,
9718
9437
  attributes: true,
9719
- attributeFilter: ["data-content-key", "data-content-phase"],
9438
+ attributeFilter: [CONTENT_ID_ATTRIBUTE, CONTENT_PHASE_ATTRIBUTE],
9720
9439
  characterData: false,
9721
9440
  });
9722
9441
  addTeardown(() => {
9723
9442
  mutationObserver.disconnect();
9724
9443
  });
9725
9444
  }
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
9445
  {
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;
9446
+ const slots = [
9447
+ targetSlot,
9448
+ outgoingSlot,
9449
+ previousTargetSlot,
9450
+ previousOutgoingSlot,
9451
+ ];
9452
+ for (const slot of slots) {
9453
+ addTeardown(monitorItemsOverflow(slot));
9454
+ }
9772
9455
  }
9773
9456
 
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
- }
9457
+ const setDuration = (newDuration) => {
9458
+ duration = newDuration;
9459
+ // Update CSS variable immediately
9460
+ root.style.setProperty("--x-transition-duration", `${duration}ms`);
9461
+ };
9462
+ const setAlignment = (newAlignX, newAlignY) => {
9463
+ alignX = newAlignX;
9464
+ alignY = newAlignY;
9465
+ updateAlignment();
9466
+ };
9805
9467
 
9806
- const groupTransition = transitionController.animate(transitions, {
9807
- onFinish: () => {
9808
- groupTransition.cancel();
9809
- cleanup();
9810
- onTeardown?.();
9811
- onComplete?.();
9468
+ return {
9469
+ updateContentId: (value) => {
9470
+ if (value) {
9471
+ targetSlot.setAttribute(CONTENT_ID_ATTRIBUTE, value);
9472
+ } else {
9473
+ targetSlot.removeAttribute(CONTENT_ID_ATTRIBUTE);
9474
+ }
9812
9475
  },
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
9476
 
10097
- const notifyTransition = (element, detail) => {
10098
- dispatchUITransitionStartCustomEvent(element, detail);
10099
- return () => {
10100
- dispatchUITransitionEndCustomEvent(element, detail);
9477
+ transitionTo,
9478
+ resetContent,
9479
+ setDuration,
9480
+ setAlignment,
9481
+ updateAlignment,
9482
+ setPauseBreakpoints: (value) => {
9483
+ groupTransitionOptions.pauseBreakpoints = value;
9484
+ },
9485
+ cleanup: () => {
9486
+ teardown();
9487
+ },
10101
9488
  };
10102
9489
  };
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
9490
 
10118
9491
  /**
10119
9492
  * UITransition
@@ -10130,76 +9503,81 @@ const dispatchUITransitionEndCustomEvent = (element, detail) => {
10130
9503
  *
10131
9504
  * Usage:
10132
9505
  * - Wrap dynamic content in <UITransition> to animate between states
10133
- * - Set a unique `data-content-key` on your rendered content to identify each content variant
9506
+ * - Set a unique `data-content-id` on your rendered content to identify each content variant
10134
9507
  * - Use `data-content-phase` to mark loading/error states for phase transitions
10135
9508
  * - Configure transition types and durations for both content and phase changes
10136
9509
  *
10137
9510
  * Example:
10138
9511
  *
10139
- * <UITransition
10140
- * transitionType="slide-left"
10141
- * transitionDuration={400}
10142
- * phaseTransitionType="cross-fade"
10143
- * phaseTransitionDuration={300}
10144
- * >
9512
+ * <UITransition>
10145
9513
  * {isLoading
10146
9514
  * ? <Spinner data-content-key={userId} data-content-phase />
10147
9515
  * : <UserProfile user={user} data-content-key={userId} />}
10148
9516
  * </UITransition>
10149
9517
  *
10150
- * When `data-content-key` changes, UITransition animates content transitions.
9518
+ * When `data-content-id` changes, UITransition animates content transitions.
10151
9519
  * When `data-content-phase` changes for the same key, it animates phase transitions.
10152
9520
  */
10153
9521
 
10154
- const ContentKeyContext = createContext();
9522
+ const UITransitionContentIdContext = createContext();
10155
9523
  const UITransition = ({
10156
9524
  children,
10157
- contentKey,
10158
- sizeTransition = true,
10159
- sizeTransitionDuration,
10160
- transitionType,
10161
- transitionDuration,
10162
- phaseTransitionType,
10163
- phaseTransitionDuration,
9525
+ contentId,
9526
+ type,
9527
+ duration,
10164
9528
  debugDetection,
9529
+ debugContent,
10165
9530
  debugSize,
10166
- debugBreakAfterClone,
9531
+ disabled,
9532
+ uiTransitionRef,
9533
+ alignX,
9534
+ alignY,
10167
9535
  ...props
10168
9536
  }) => {
10169
- const [contentKeyFromContext, setContentKeyFromContext] = useState();
10170
- const contentKeyContextValue = useMemo(() => {
10171
- const keySet = new Set();
10172
- const onKeySetChange = () => {
10173
- setContentKeyFromContext(Array.from(keySet).join("|"));
9537
+ const contentIdRef = useRef(contentId);
9538
+ const updateContentId = () => {
9539
+ const uiTransition = uiTransitionRef.current;
9540
+ if (!uiTransition) {
9541
+ return;
9542
+ }
9543
+ const value = contentIdRef.current;
9544
+ uiTransition.updateContentId(value);
9545
+ };
9546
+ const uiTransitionContentIdContextValue = useMemo(() => {
9547
+ const set = new Set();
9548
+ const onSetChange = () => {
9549
+ const value = Array.from(set).join("|");
9550
+ contentIdRef.current = value;
9551
+ updateContentId();
10174
9552
  };
10175
- const update = (key, newKey) => {
10176
- if (!keySet.has(key)) {
10177
- console.warn(`UITransition: trying to update a key that does not exist: ${key}`);
9553
+ const update = (part, newPart) => {
9554
+ if (!set.has(part)) {
9555
+ console.warn(`UITransition: trying to update an id that does not exist: ${part}`);
10178
9556
  return;
10179
9557
  }
10180
- keySet.delete(key);
10181
- keySet.add(newKey);
10182
- onKeySetChange();
9558
+ set.delete(part);
9559
+ set.add(newPart);
9560
+ onSetChange();
10183
9561
  };
10184
- const add = key => {
10185
- if (!key) {
9562
+ const add = part => {
9563
+ if (!part) {
10186
9564
  return;
10187
9565
  }
10188
- if (keySet.has(key)) {
9566
+ if (set.has(part)) {
10189
9567
  return;
10190
9568
  }
10191
- keySet.add(key);
10192
- onKeySetChange();
9569
+ set.add(part);
9570
+ onSetChange();
10193
9571
  };
10194
- const remove = key => {
10195
- if (!key) {
9572
+ const remove = part => {
9573
+ if (!part) {
10196
9574
  return;
10197
9575
  }
10198
- if (!keySet.has(key)) {
9576
+ if (!set.has(part)) {
10199
9577
  return;
10200
9578
  }
10201
- keySet.delete(key);
10202
- onKeySetChange();
9579
+ set.delete(part);
9580
+ onSetChange();
10203
9581
  };
10204
9582
  return {
10205
9583
  add,
@@ -10207,42 +9585,54 @@ const UITransition = ({
10207
9585
  remove
10208
9586
  };
10209
9587
  }, []);
10210
- const effectiveContentKey = contentKey || contentKeyFromContext;
10211
9588
  const ref = useRef();
9589
+ const uiTransitionRefDefault = useRef();
9590
+ uiTransitionRef = uiTransitionRef || uiTransitionRefDefault;
10212
9591
  useLayoutEffect(() => {
10213
- const uiTransition = initUITransition(ref.current);
9592
+ if (disabled) {
9593
+ return null;
9594
+ }
9595
+ const uiTransition = createUITransitionController(ref.current, {
9596
+ alignX,
9597
+ alignY
9598
+ });
9599
+ uiTransitionRef.current = uiTransition;
10214
9600
  return () => {
10215
9601
  uiTransition.cleanup();
10216
9602
  };
10217
- }, []);
10218
- return jsx(ContentKeyContext.Provider, {
10219
- value: contentKeyContextValue,
10220
- children: jsxs("div", {
10221
- ref: ref,
10222
- ...props,
10223
- className: "ui_transition_container",
10224
- "data-size-transition": sizeTransition ? "" : undefined,
10225
- "data-size-transition-duration": sizeTransitionDuration ? sizeTransitionDuration : undefined,
10226
- "data-content-transition": transitionType ? transitionType : undefined,
10227
- "data-content-transition-duration": transitionDuration ? transitionDuration : undefined,
10228
- "data-phase-transition": phaseTransitionType ? phaseTransitionType : undefined,
10229
- "data-phase-transition-duration": phaseTransitionDuration ? phaseTransitionDuration : undefined,
10230
- "data-debug-detection": debugDetection ? "" : undefined,
10231
- "data-debug-size": debugSize ? "" : undefined,
10232
- "data-debug-break-after-clone": debugBreakAfterClone,
10233
- children: [jsxs("div", {
10234
- className: "ui_transition_outer_wrapper",
10235
- children: [jsx("div", {
10236
- className: "ui_transition_slot",
10237
- "data-content-key": effectiveContentKey ? effectiveContentKey : undefined,
9603
+ }, [disabled, alignX, alignY]);
9604
+ if (disabled) {
9605
+ return children;
9606
+ }
9607
+ return jsxs("div", {
9608
+ ref: ref,
9609
+ ...props,
9610
+ className: "ui_transition",
9611
+ "data-transition-type": type,
9612
+ "data-transition-duration": duration,
9613
+ "data-debug-detection": debugDetection ? "" : undefined,
9614
+ "data-debug-size": debugSize ? "" : undefined,
9615
+ "data-debug-content": debugContent ? "" : undefined,
9616
+ children: [jsxs("div", {
9617
+ className: "ui_transition_active_group",
9618
+ children: [jsx("div", {
9619
+ className: "ui_transition_target_slot",
9620
+ "data-content-id": contentIdRef.current ? contentIdRef.current : undefined,
9621
+ children: jsx(UITransitionContentIdContext.Provider, {
9622
+ value: uiTransitionContentIdContextValue,
10238
9623
  children: children
10239
- }), jsx("div", {
10240
- className: "ui_transition_phase_overlay"
10241
- })]
9624
+ })
10242
9625
  }), jsx("div", {
10243
- className: "ui_transition_content_overlay"
9626
+ className: "ui_transition_outgoing_slot"
10244
9627
  })]
10245
- })
9628
+ }), jsxs("div", {
9629
+ className: "ui_transition_previous_group",
9630
+ children: [jsx("div", {
9631
+ className: "ui_transition_previous_target_slot"
9632
+ }), jsx("div", {
9633
+ className: "ui_transition_previous_outgoing_slot"
9634
+ })]
9635
+ })]
10246
9636
  });
10247
9637
  };
10248
9638
 
@@ -10254,28 +9644,28 @@ const UITransition = ({
10254
9644
  * as changed even if the component is still the same
10255
9645
  *
10256
9646
  * This is used by <Route> to set the content key to the route path
10257
- * When the route becomes inactive it will call useContentKey(undefined)
10258
- * And if a sibling route becones active it will call useContentKey with its own path
9647
+ * When the route becomes inactive it will call useUITransitionContentId(undefined)
9648
+ * And if a sibling route becones active it will call useUITransitionContentId with its own path
10259
9649
  *
10260
9650
  */
10261
- const useContentKey = key => {
10262
- const contentKey = useContext(ContentKeyContext);
10263
- const keyRef = useRef();
10264
- if (keyRef.current !== key && contentKey) {
10265
- const previousKey = keyRef.current;
10266
- keyRef.current = key;
10267
- if (previousKey) {
10268
- contentKey.update(previousKey, key);
9651
+ const useUITransitionContentId = value => {
9652
+ const contentId = useContext(UITransitionContentIdContext);
9653
+ const valueRef = useRef();
9654
+ if (contentId !== undefined && valueRef.current !== value) {
9655
+ const previousValue = valueRef.current;
9656
+ valueRef.current = value;
9657
+ if (previousValue === undefined) {
9658
+ contentId.add(value);
10269
9659
  } else {
10270
- contentKey.add(key);
9660
+ contentId.update(previousValue, value);
10271
9661
  }
10272
9662
  }
10273
9663
  useLayoutEffect(() => {
10274
- if (!contentKey) {
9664
+ if (contentId === undefined) {
10275
9665
  return null;
10276
9666
  }
10277
9667
  return () => {
10278
- contentKey.remove(keyRef.current);
9668
+ contentId.remove(valueRef.current);
10279
9669
  };
10280
9670
  }, []);
10281
9671
  };
@@ -10331,10 +9721,13 @@ const Routes = ({
10331
9721
  });
10332
9722
  };
10333
9723
  const SlotContext = createContext(null);
9724
+ const RouteInfoContext = createContext(null);
9725
+ const useActiveRouteInfo = () => useContext(RouteInfoContext);
10334
9726
  const Route = ({
10335
9727
  element,
10336
9728
  route,
10337
9729
  fallback,
9730
+ meta,
10338
9731
  children
10339
9732
  }) => {
10340
9733
  const forceRender = useForceRender();
@@ -10345,6 +9738,7 @@ const Route = ({
10345
9738
  element: element,
10346
9739
  route: route,
10347
9740
  fallback: fallback,
9741
+ meta: meta,
10348
9742
  onActiveInfoChange: activeInfo => {
10349
9743
  hasDiscoveredRef.current = true;
10350
9744
  activeInfoRef.current = activeInfo;
@@ -10371,18 +9765,23 @@ const ActiveRouteManager = ({
10371
9765
  element,
10372
9766
  route,
10373
9767
  fallback,
9768
+ meta,
10374
9769
  onActiveInfoChange,
10375
9770
  children
10376
9771
  }) => {
9772
+ if (route && fallback) {
9773
+ throw new Error("Route cannot have both route and fallback props");
9774
+ }
10377
9775
  const registerChildRouteFromContext = useContext(RegisterChildRouteContext);
10378
9776
  getElementSignature(element);
10379
9777
  const candidateSet = new Set();
10380
- const registerChildRoute = (ChildActiveElement, childRoute, childFallback) => {
9778
+ const registerChildRoute = (ChildActiveElement, childRoute, childFallback, childMeta) => {
10381
9779
  getElementSignature(ChildActiveElement);
10382
9780
  candidateSet.add({
10383
9781
  ActiveElement: ChildActiveElement,
10384
9782
  route: childRoute,
10385
- fallback: childFallback
9783
+ fallback: childFallback,
9784
+ meta: childMeta
10386
9785
  });
10387
9786
  };
10388
9787
  useLayoutEffect(() => {
@@ -10390,6 +9789,7 @@ const ActiveRouteManager = ({
10390
9789
  element,
10391
9790
  route,
10392
9791
  fallback,
9792
+ meta,
10393
9793
  candidateSet,
10394
9794
  onActiveInfoChange,
10395
9795
  registerChildRouteFromContext
@@ -10404,6 +9804,7 @@ const initRouteObserver = ({
10404
9804
  element,
10405
9805
  route,
10406
9806
  fallback,
9807
+ meta,
10407
9808
  candidateSet,
10408
9809
  onActiveInfoChange,
10409
9810
  registerChildRouteFromContext
@@ -10424,19 +9825,13 @@ const initRouteObserver = ({
10424
9825
  let fallbackInfo = null;
10425
9826
  for (const candidate of candidateSet) {
10426
9827
  if (candidate.route?.active) {
10427
- return {
10428
- ChildActiveElement: candidate.ActiveElement,
10429
- route: candidate.route
10430
- };
9828
+ return candidate;
10431
9829
  }
10432
9830
  // fallback without route can match when no other route matches.
10433
9831
  // This is useful solely for "catch all" fallback used on the <Routes>
10434
9832
  // otherwise a fallback would always match and make the parent route always active
10435
9833
  if (candidate.fallback && !candidate.route.routeFromProps) {
10436
- fallbackInfo = {
10437
- ChildActiveElement: candidate.ActiveElement,
10438
- route: candidate.route
10439
- };
9834
+ fallbackInfo = candidate;
10440
9835
  }
10441
9836
  }
10442
9837
  return fallbackInfo;
@@ -10453,8 +9848,9 @@ const initRouteObserver = ({
10453
9848
  return activeChildInfo;
10454
9849
  }
10455
9850
  return {
10456
- ChildActiveElement: null,
10457
- route
9851
+ ActiveElement: null,
9852
+ route,
9853
+ meta
10458
9854
  };
10459
9855
  } : () => {
10460
9856
  // we don't have a route, do we have an active child?
@@ -10464,22 +9860,22 @@ const initRouteObserver = ({
10464
9860
  }
10465
9861
  return null;
10466
9862
  };
10467
- const activeRouteSignal = signal();
9863
+ const activeRouteInfoSignal = signal();
10468
9864
  const SlotActiveElementSignal = signal();
10469
9865
  const ActiveElement = () => {
10470
- const activeRoute = activeRouteSignal.value;
10471
- useContentKey(activeRoute?.urlPattern);
9866
+ const activeRouteInfo = activeRouteInfoSignal.value;
9867
+ useUITransitionContentId(activeRouteInfo ? activeRouteInfo.route.urlPattern : fallback ? "fallback" : undefined);
10472
9868
  const SlotActiveElement = SlotActiveElementSignal.value;
10473
9869
  if (typeof element === "function") {
10474
9870
  const Element = element;
10475
- return jsx(SlotContext.Provider, {
10476
- value: SlotActiveElement,
10477
- children: jsx(Element, {})
10478
- });
9871
+ element = jsx(Element, {});
10479
9872
  }
10480
- return jsx(SlotContext.Provider, {
10481
- value: SlotActiveElement,
10482
- children: element
9873
+ return jsx(RouteInfoContext.Provider, {
9874
+ value: activeRouteInfo,
9875
+ children: jsx(SlotContext.Provider, {
9876
+ value: SlotActiveElement,
9877
+ children: element
9878
+ })
10483
9879
  });
10484
9880
  };
10485
9881
  ActiveElement.underlyingElementId = candidateSet.size === 0 ? `${getElementSignature(element)} without slot` : `[${getElementSignature(element)} with slot one of ${candidateElementIds}]`;
@@ -10487,20 +9883,17 @@ const initRouteObserver = ({
10487
9883
  const newActiveInfo = getActiveInfo();
10488
9884
  if (newActiveInfo) {
10489
9885
  compositeRoute.active = true;
10490
- const {
10491
- route,
10492
- ChildActiveElement
10493
- } = newActiveInfo;
10494
- activeRouteSignal.value = route;
10495
- SlotActiveElementSignal.value = ChildActiveElement;
9886
+ activeRouteInfoSignal.value = newActiveInfo;
9887
+ SlotActiveElementSignal.value = newActiveInfo.ActiveElement;
10496
9888
  onActiveInfoChange({
10497
- route: newActiveInfo.route,
10498
9889
  ActiveElement,
10499
- SlotActiveElement: ChildActiveElement
9890
+ SlotActiveElement: newActiveInfo.ActiveElement,
9891
+ route: newActiveInfo.route,
9892
+ meta: newActiveInfo.meta
10500
9893
  });
10501
9894
  } else {
10502
9895
  compositeRoute.active = false;
10503
- activeRouteSignal.value = null;
9896
+ activeRouteInfoSignal.value = null;
10504
9897
  SlotActiveElementSignal.value = null;
10505
9898
  onActiveInfoChange(null);
10506
9899
  }
@@ -10516,7 +9909,7 @@ const initRouteObserver = ({
10516
9909
  candidate.route.subscribeStatus(onChange);
10517
9910
  }
10518
9911
  if (registerChildRouteFromContext) {
10519
- registerChildRouteFromContext(ActiveElement, compositeRoute, fallback);
9912
+ registerChildRouteFromContext(ActiveElement, compositeRoute, fallback, meta);
10520
9913
  }
10521
9914
  updateActiveInfo();
10522
9915
  };
@@ -10718,14 +10111,11 @@ const renderActionableComponent = (props, {
10718
10111
 
10719
10112
  const normalizeSpacingStyle = (value, property = "padding") => {
10720
10113
  const cssSize = sizeSpacingScale[value];
10721
- return cssSize || normalizeStyle(value, property, "css");
10114
+ return cssSize || stringifyStyle(value, property);
10722
10115
  };
10723
10116
  const normalizeTypoStyle = (value, property = "fontSize") => {
10724
10117
  const cssSize = sizeTypoScale[value];
10725
- return cssSize || normalizeStyle(value, property, "css");
10726
- };
10727
- const normalizeCssStyle = (value, property) => {
10728
- return normalizeStyle(value, property, "css");
10118
+ return cssSize || stringifyStyle(value, property);
10729
10119
  };
10730
10120
 
10731
10121
  const PASS_THROUGH = { name: "pass_through" };
@@ -10776,6 +10166,13 @@ const applyOnTwoProps = (propA, propB) => {
10776
10166
  };
10777
10167
  };
10778
10168
 
10169
+ const LAYOUT_PROPS = {
10170
+ // all are handled by data-attributes
10171
+ inline: () => {},
10172
+ box: () => {},
10173
+ row: () => {},
10174
+ column: () => {},
10175
+ };
10779
10176
  const OUTER_SPACING_PROPS = {
10780
10177
  margin: PASS_THROUGH,
10781
10178
  marginLeft: PASS_THROUGH,
@@ -10812,21 +10209,21 @@ const DIMENSION_PROPS = {
10812
10209
  return { flexGrow: 1 }; // Grow horizontally in row
10813
10210
  }
10814
10211
  if (parentLayout === "row") {
10815
- return { minWidth: "100%" }; // Take full width in column
10212
+ return { minWidth: "100%", width: "auto" }; // Take full width in column
10816
10213
  }
10817
- return { minWidth: "100%" }; // Take full width outside flex
10214
+ return { minWidth: "100%", width: "auto" }; // Take full width outside flex
10818
10215
  },
10819
10216
  expandY: (value, { parentLayout }) => {
10820
10217
  if (!value) {
10821
10218
  return null;
10822
10219
  }
10823
10220
  if (parentLayout === "column") {
10824
- return { minHeight: "100%" }; // Make column full height
10221
+ return { minHeight: "100%", height: "auto" }; // Make column full height
10825
10222
  }
10826
10223
  if (parentLayout === "row" || parentLayout === "inline-row") {
10827
10224
  return { flexGrow: 1 }; // Make row full height
10828
10225
  }
10829
- return { minHeight: "100%" }; // Take full height outside flex
10226
+ return { minHeight: "100%", height: "auto" }; // Take full height outside flex
10830
10227
  },
10831
10228
  shrinkX: (value, { parentLayout }) => {
10832
10229
  if (!value) {
@@ -10846,6 +10243,23 @@ const DIMENSION_PROPS = {
10846
10243
  }
10847
10244
  return { maxHeight: "100%" };
10848
10245
  },
10246
+
10247
+ scaleX: (value) => {
10248
+ return { transform: `scaleX(${stringifyStyle(value, "scaleX")})` };
10249
+ },
10250
+ scaleY: (value) => {
10251
+ return { transform: `scaleY(${value})` };
10252
+ },
10253
+ scale: (value) => {
10254
+ if (Array.isArray(value)) {
10255
+ const [x, y] = value;
10256
+ return { transform: `scale(${x}, ${y})` };
10257
+ }
10258
+ return { transform: `scale(${value})` };
10259
+ },
10260
+ scaleZ: (value) => {
10261
+ return { transform: `scaleZ(${value})` };
10262
+ },
10849
10263
  };
10850
10264
  const POSITION_PROPS = {
10851
10265
  // For row, alignX uses auto margins for positioning
@@ -10886,7 +10300,7 @@ const POSITION_PROPS = {
10886
10300
 
10887
10301
  if (value === "start") {
10888
10302
  if (inlineColumnLayout) {
10889
- return undefined; // this is the default
10303
+ return { alignSelf: "start" };
10890
10304
  }
10891
10305
  return { marginBottom: "auto" };
10892
10306
  }
@@ -10906,8 +10320,49 @@ const POSITION_PROPS = {
10906
10320
  },
10907
10321
  left: PASS_THROUGH,
10908
10322
  top: PASS_THROUGH,
10323
+
10324
+ translateX: (value) => {
10325
+ return { transform: `translateX(${value})` };
10326
+ },
10327
+ translateY: (value) => {
10328
+ return { transform: `translateY(${value})` };
10329
+ },
10330
+ translate: (value) => {
10331
+ if (Array.isArray(value)) {
10332
+ const [x, y] = value;
10333
+ return { transform: `translate(${x}, ${y})` };
10334
+ }
10335
+ return { transform: `translate(${stringifyStyle(value, "translateX")})` };
10336
+ },
10337
+ rotateX: (value) => {
10338
+ return { transform: `rotateX(${value})` };
10339
+ },
10340
+ rotateY: (value) => {
10341
+ return { transform: `rotateY(${value})` };
10342
+ },
10343
+ rotateZ: (value) => {
10344
+ return { transform: `rotateZ(${value})` };
10345
+ },
10346
+ rotate: (value) => {
10347
+ return { transform: `rotate(${value})` };
10348
+ },
10349
+ skewX: (value) => {
10350
+ return { transform: `skewX(${value})` };
10351
+ },
10352
+ skewY: (value) => {
10353
+ return { transform: `skewY(${value})` };
10354
+ },
10355
+ skew: (value) => {
10356
+ if (Array.isArray(value)) {
10357
+ const [x, y] = value;
10358
+ return { transform: `skew(${x}, ${y})` };
10359
+ }
10360
+ return { transform: `skew(${value})` };
10361
+ },
10909
10362
  };
10910
10363
  const TYPO_PROPS = {
10364
+ font: PASS_THROUGH,
10365
+ fontFamily: PASS_THROUGH,
10911
10366
  size: applyOnCSSProp("fontSize"),
10912
10367
  fontSize: PASS_THROUGH,
10913
10368
  bold: applyToCssPropWhenTruthy("fontWeight", "bold", "normal"),
@@ -10924,6 +10379,11 @@ const TYPO_PROPS = {
10924
10379
  preWrap: applyToCssPropWhenTruthy("whiteSpace", "pre-wrap", "normal"),
10925
10380
  };
10926
10381
  const VISUAL_PROPS = {
10382
+ outline: PASS_THROUGH,
10383
+ outlineStyle: PASS_THROUGH,
10384
+ outlineColor: PASS_THROUGH,
10385
+ outlineWidth: PASS_THROUGH,
10386
+ boxDecorationBreak: PASS_THROUGH,
10927
10387
  boxShadow: PASS_THROUGH,
10928
10388
  background: PASS_THROUGH,
10929
10389
  backgroundColor: PASS_THROUGH,
@@ -11003,6 +10463,7 @@ const CONTENT_PROPS = {
11003
10463
  },
11004
10464
  };
11005
10465
  const All_PROPS = {
10466
+ ...LAYOUT_PROPS,
11006
10467
  ...OUTER_SPACING_PROPS,
11007
10468
  ...INNER_SPACING_PROPS,
11008
10469
  ...DIMENSION_PROPS,
@@ -11011,6 +10472,7 @@ const All_PROPS = {
11011
10472
  ...VISUAL_PROPS,
11012
10473
  ...CONTENT_PROPS,
11013
10474
  };
10475
+ const LAYOUT_PROP_NAME_SET = new Set(Object.keys(LAYOUT_PROPS));
11014
10476
  const OUTER_SPACING_PROP_NAME_SET = new Set(Object.keys(OUTER_SPACING_PROPS));
11015
10477
  const INNER_SPACING_PROP_NAME_SET = new Set(Object.keys(INNER_SPACING_PROPS));
11016
10478
  const DIMENSION_PROP_NAME_SET = new Set(Object.keys(DIMENSION_PROPS));
@@ -11026,6 +10488,7 @@ const HANDLED_BY_VISUAL_CHILD_PROP_SET = new Set([
11026
10488
  ...CONTENT_PROP_NAME_SET,
11027
10489
  ]);
11028
10490
  const COPIED_ON_VISUAL_CHILD_PROP_SET = new Set([
10491
+ ...LAYOUT_PROP_NAME_SET,
11029
10492
  "expand",
11030
10493
  "shrink",
11031
10494
  "expandX",
@@ -11037,6 +10500,9 @@ const COPIED_ON_VISUAL_CHILD_PROP_SET = new Set([
11037
10500
  const isStyleProp = (name) => STYLE_PROP_NAME_SET.has(name);
11038
10501
 
11039
10502
  const getStylePropGroup = (name) => {
10503
+ if (LAYOUT_PROP_NAME_SET.has(name)) {
10504
+ return "layout";
10505
+ }
11040
10506
  if (OUTER_SPACING_PROP_NAME_SET.has(name)) {
11041
10507
  return "margin";
11042
10508
  }
@@ -11068,7 +10534,7 @@ const getNormalizer = (key) => {
11068
10534
  if (group === "typo") {
11069
10535
  return normalizeTypoStyle;
11070
10536
  }
11071
- return normalizeCssStyle;
10537
+ return stringifyStyle;
11072
10538
  };
11073
10539
 
11074
10540
  const assignStyle = (
@@ -11076,7 +10542,7 @@ const assignStyle = (
11076
10542
  propValue,
11077
10543
  propName,
11078
10544
  styleContext,
11079
- normalizer = getNormalizer(propName),
10545
+ context = "js",
11080
10546
  ) => {
11081
10547
  if (propValue === undefined) {
11082
10548
  return;
@@ -11085,6 +10551,7 @@ const assignStyle = (
11085
10551
  if (!managedByCSSVars) {
11086
10552
  throw new Error("managedByCSSVars is required in styleContext");
11087
10553
  }
10554
+ const normalizer = getNormalizer(propName);
11088
10555
  const getStyle = All_PROPS[propName];
11089
10556
  if (
11090
10557
  getStyle === PASS_THROUGH ||
@@ -11093,10 +10560,16 @@ const assignStyle = (
11093
10560
  ) {
11094
10561
  const cssValue = normalizer(propValue, propName);
11095
10562
  const cssVar = managedByCSSVars[propName];
10563
+ const mergedValue = mergeOneStyle(
10564
+ styleObject[propName],
10565
+ cssValue,
10566
+ propName,
10567
+ context,
10568
+ );
11096
10569
  if (cssVar) {
11097
- styleObject[cssVar] = cssValue;
10570
+ styleObject[cssVar] = mergedValue;
11098
10571
  } else {
11099
- styleObject[propName] = cssValue;
10572
+ styleObject[propName] = mergedValue;
11100
10573
  }
11101
10574
  return;
11102
10575
  }
@@ -11108,10 +10581,11 @@ const assignStyle = (
11108
10581
  const value = values[key];
11109
10582
  const cssValue = normalizer(value, key);
11110
10583
  const cssVar = managedByCSSVars[key];
10584
+ const mergedValue = mergeOneStyle(styleObject[key], cssValue, key, context);
11111
10585
  if (cssVar) {
11112
- styleObject[cssVar] = cssValue;
10586
+ styleObject[cssVar] = mergedValue;
11113
10587
  } else {
11114
- styleObject[key] = cssValue;
10588
+ styleObject[key] = mergedValue;
11115
10589
  }
11116
10590
  }
11117
10591
  };
@@ -11128,12 +10602,8 @@ const sizeSpacingScale = {
11128
10602
  xl: "2em", // 2 = 32px at 16px base
11129
10603
  xxl: "3em", // 3 = 48px at 16px base
11130
10604
  };
11131
- const resolveSpacingSize = (
11132
- size,
11133
- property = "padding",
11134
- context = "css",
11135
- ) => {
11136
- return normalizeStyle(sizeSpacingScale[size] || size, property, context);
10605
+ const resolveSpacingSize = (size, property = "padding") => {
10606
+ return stringifyStyle(sizeSpacingScale[size] || size, property);
11137
10607
  };
11138
10608
 
11139
10609
  const sizeTypoScale = {
@@ -11511,7 +10981,7 @@ const initPseudoStyles = (
11511
10981
  }
11512
10982
  currentState[pseudoClass] = currentValue;
11513
10983
  const oldValue = state ? state[pseudoClass] : undefined;
11514
- if (oldValue !== currentValue) {
10984
+ if (oldValue !== currentValue || !state) {
11515
10985
  someChange = true;
11516
10986
  const { attribute, add, remove } = pseudoClassDefinition;
11517
10987
  if (currentValue) {
@@ -11566,8 +11036,15 @@ const applyStyle = (element, style, pseudoState, pseudoNamedStyles) => {
11566
11036
  updateStyle(element, getStyleToApply(style, pseudoState, pseudoNamedStyles));
11567
11037
  };
11568
11038
 
11039
+ const PSEUDO_STATE_DEFAULT = {};
11040
+ const PSEUDO_NAMED_STYLES_DEFAULT = {};
11569
11041
  const getStyleToApply = (styles, pseudoState, pseudoNamedStyles) => {
11570
- if (!pseudoState || !pseudoNamedStyles) {
11042
+ if (
11043
+ !pseudoState ||
11044
+ pseudoState === PSEUDO_STATE_DEFAULT ||
11045
+ !pseudoNamedStyles ||
11046
+ pseudoNamedStyles === PSEUDO_NAMED_STYLES_DEFAULT
11047
+ ) {
11571
11048
  return styles;
11572
11049
  }
11573
11050
 
@@ -11634,7 +11111,13 @@ const updateStyle = (element, style) => {
11634
11111
  }
11635
11112
  }
11636
11113
  for (const toDeleteKey of toDeleteKeySet) {
11637
- element.style.removeProperty(toDeleteKey);
11114
+ if (toDeleteKey.startsWith("--")) {
11115
+ element.style.removeProperty(toDeleteKey);
11116
+ } else {
11117
+ // we can't use removeProperty because "toDeleteKey" is in camelCase
11118
+ // e.g., backgroundColor (and it's safer to just let the browser do the conversion)
11119
+ element.style[toDeleteKey] = "";
11120
+ }
11638
11121
  }
11639
11122
  styleKeySetWeakMap.set(element, styleKeySet);
11640
11123
  return;
@@ -11693,8 +11176,10 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
11693
11176
  flex-direction: row;
11694
11177
  }
11695
11178
 
11696
- [data-layout-row] > *,
11697
- [data-layout-column] > * {
11179
+ [data-layout-row] > [data-layout-row],
11180
+ [data-layout-row] > [data-layout-column],
11181
+ [data-layout-column] > [data-layout-column],
11182
+ [data-layout-column] > [data-layout-row] {
11698
11183
  flex-shrink: 0;
11699
11184
  }
11700
11185
 
@@ -11709,9 +11194,6 @@ const MANAGED_BY_CSS_VARS_DEFAULT = {};
11709
11194
  const Box = props => {
11710
11195
  const {
11711
11196
  as = "div",
11712
- layoutRow,
11713
- layoutColumn,
11714
- layoutInline,
11715
11197
  baseClassName,
11716
11198
  className,
11717
11199
  baseStyle,
@@ -11741,30 +11223,78 @@ const Box = props => {
11741
11223
  const defaultRef = useRef();
11742
11224
  const ref = props.ref || defaultRef;
11743
11225
  const TagName = as;
11226
+ const {
11227
+ box,
11228
+ inline = box,
11229
+ row,
11230
+ column = box
11231
+ } = rest;
11744
11232
  let layout;
11745
- if (layoutInline) {
11746
- if (layoutRow) {
11233
+ if (inline) {
11234
+ if (row) {
11747
11235
  layout = "inline-row";
11748
- } else if (layoutColumn) {
11236
+ } else if (column) {
11749
11237
  layout = "inline-column";
11750
11238
  } else {
11751
11239
  layout = "inline";
11752
11240
  }
11753
- } else if (layoutRow) {
11241
+ } else if (row) {
11754
11242
  layout = "row";
11755
- } else if (layoutColumn) {
11243
+ } else if (column) {
11756
11244
  layout = "column";
11757
11245
  } else {
11758
11246
  layout = getDefaultDisplay(TagName);
11759
11247
  }
11760
11248
  const innerClassName = withPropsClassName(baseClassName, className);
11761
11249
  const remainingProps = {};
11250
+ const propsToForward = {};
11251
+ const shouldForwardAllToChild = visualSelector && pseudoStateSelector;
11762
11252
  {
11763
11253
  const parentLayout = useContext(BoxLayoutContext);
11764
- const innerPseudoState = basePseudoState && pseudoState ? {
11765
- ...basePseudoState,
11766
- ...pseudoState
11767
- } : basePseudoState;
11254
+ const styleDeps = [
11255
+ // Layout and alignment props
11256
+ parentLayout, layout,
11257
+ // Style context dependencies
11258
+ managedByCSSVars, pseudoClasses, pseudoElements,
11259
+ // Selectors
11260
+ visualSelector, pseudoStateSelector];
11261
+ let innerPseudoState;
11262
+ if (basePseudoState && pseudoState) {
11263
+ innerPseudoState = {};
11264
+ const baseStateKeys = Object.keys(basePseudoState);
11265
+ const pseudoStateKeySet = new Set(Object.keys(pseudoState));
11266
+ for (const key of baseStateKeys) {
11267
+ if (pseudoStateKeySet.has(key)) {
11268
+ pseudoStateKeySet.delete(key);
11269
+ const value = pseudoState[key];
11270
+ styleDeps.push(value);
11271
+ innerPseudoState[key] = value;
11272
+ } else {
11273
+ const value = basePseudoState[key];
11274
+ styleDeps.push(value);
11275
+ innerPseudoState[key] = value;
11276
+ }
11277
+ }
11278
+ for (const key of pseudoStateKeySet) {
11279
+ const value = pseudoState[key];
11280
+ styleDeps.push(value);
11281
+ innerPseudoState[key] = value;
11282
+ }
11283
+ } else if (basePseudoState) {
11284
+ innerPseudoState = basePseudoState;
11285
+ for (const key of Object.keys(basePseudoState)) {
11286
+ const value = basePseudoState[key];
11287
+ styleDeps.push(value);
11288
+ }
11289
+ } else if (pseudoState) {
11290
+ innerPseudoState = pseudoState;
11291
+ for (const key of Object.keys(pseudoState)) {
11292
+ const value = pseudoState[key];
11293
+ styleDeps.push(value);
11294
+ }
11295
+ } else {
11296
+ innerPseudoState = PSEUDO_STATE_DEFAULT;
11297
+ }
11768
11298
  const styleContext = {
11769
11299
  parentLayout,
11770
11300
  layout,
@@ -11773,13 +11303,6 @@ const Box = props => {
11773
11303
  pseudoClasses,
11774
11304
  pseudoElements
11775
11305
  };
11776
- const styleDeps = [
11777
- // Layout and alignment props
11778
- parentLayout, layout,
11779
- // Style context dependencies
11780
- managedByCSSVars, pseudoClasses, pseudoElements,
11781
- // Selectors
11782
- visualSelector, pseudoStateSelector];
11783
11306
  const boxStyles = {};
11784
11307
  if (baseStyle) {
11785
11308
  for (const key of baseStyle) {
@@ -11789,22 +11312,6 @@ const Box = props => {
11789
11312
  }
11790
11313
  }
11791
11314
  const stylingKeyCandidateArray = Object.keys(rest);
11792
- const assignStyleFromProp = (propValue, propName, stylesTarget, context) => {
11793
- const propEffect = getPropEffect(propName);
11794
- if (propEffect === "ignore") {
11795
- return;
11796
- }
11797
- if (propEffect === "forward" || propEffect === "style_and_forward") {
11798
- if (stylesTarget === boxStyles) {
11799
- remainingProps[propName] = propValue;
11800
- }
11801
- if (propEffect === "forward") {
11802
- return;
11803
- }
11804
- }
11805
- styleDeps.push(propValue);
11806
- assignStyle(stylesTarget, propValue, propName, context);
11807
- };
11808
11315
  const getPropEffect = propName => {
11809
11316
  if (visualSelector) {
11810
11317
  if (HANDLED_BY_VISUAL_CHILD_PROP_SET.has(propName)) {
@@ -11814,7 +11321,34 @@ const Box = props => {
11814
11321
  return "style_and_forward";
11815
11322
  }
11816
11323
  }
11817
- return isStyleProp(propName) ? "style" : "forward";
11324
+ if (isStyleProp(propName)) {
11325
+ return "style";
11326
+ }
11327
+ if (propName.startsWith("data-")) {
11328
+ return "use";
11329
+ }
11330
+ return "forward";
11331
+ };
11332
+ const assignStyleFromProp = (propValue, propName, stylesTarget, styleContext) => {
11333
+ const propEffect = getPropEffect(propName);
11334
+ if (propEffect === "ignore") {
11335
+ return;
11336
+ }
11337
+ const useToStyle = propEffect === "style" || propEffect === "style_and_forward";
11338
+ const shouldForward = propEffect === "forward" || propEffect === "style_and_forward";
11339
+ if (useToStyle) {
11340
+ styleDeps.push(propValue);
11341
+ assignStyle(stylesTarget, propValue, propName, styleContext, "css");
11342
+ }
11343
+ if (stylesTarget === boxStyles) {
11344
+ if (!shouldForwardAllToChild && !useToStyle) {
11345
+ // we'll put these props on ourselves
11346
+ remainingProps[propName] = propValue;
11347
+ }
11348
+ if (shouldForward) {
11349
+ propsToForward[propName] = propValue;
11350
+ }
11351
+ }
11818
11352
  };
11819
11353
  for (const key of stylingKeyCandidateArray) {
11820
11354
  if (key === "ref") {
@@ -11825,46 +11359,49 @@ const Box = props => {
11825
11359
  const value = rest[key];
11826
11360
  assignStyleFromProp(value, key, boxStyles, styleContext);
11827
11361
  }
11828
- const pseudoNamedStyles = {};
11362
+ let pseudoNamedStyles = PSEUDO_NAMED_STYLES_DEFAULT;
11829
11363
  if (pseudoStyle) {
11830
- for (const key of Object.keys(pseudoStyle)) {
11831
- const pseudoStyleContext = {
11832
- ...styleContext,
11833
- managedByCSSVars: {
11834
- ...managedByCSSVars,
11835
- ...managedByCSSVars[key]
11836
- },
11837
- pseudoName: key
11838
- };
11364
+ const pseudoStyleKeys = Object.keys(pseudoStyle);
11365
+ if (pseudoStyleKeys.length) {
11366
+ pseudoNamedStyles = {};
11367
+ for (const key of pseudoStyleKeys) {
11368
+ const pseudoStyleContext = {
11369
+ ...styleContext,
11370
+ managedByCSSVars: {
11371
+ ...managedByCSSVars,
11372
+ ...managedByCSSVars[key]
11373
+ },
11374
+ pseudoName: key
11375
+ };
11839
11376
 
11840
- // pseudo class
11841
- if (key.startsWith(":")) {
11842
- styleDeps.push(key);
11843
- const pseudoClassStyles = {};
11844
- const pseudoClassStyle = pseudoStyle[key];
11845
- for (const pseudoClassStyleKey of Object.keys(pseudoClassStyle)) {
11846
- const pseudoClassStyleValue = pseudoClassStyle[pseudoClassStyleKey];
11847
- assignStyleFromProp(pseudoClassStyleValue, pseudoClassStyleKey, pseudoClassStyles, pseudoStyleContext);
11377
+ // pseudo class
11378
+ if (key.startsWith(":")) {
11379
+ styleDeps.push(key);
11380
+ const pseudoClassStyles = {};
11381
+ const pseudoClassStyle = pseudoStyle[key];
11382
+ for (const pseudoClassStyleKey of Object.keys(pseudoClassStyle)) {
11383
+ const pseudoClassStyleValue = pseudoClassStyle[pseudoClassStyleKey];
11384
+ assignStyleFromProp(pseudoClassStyleValue, pseudoClassStyleKey, pseudoClassStyles, pseudoStyleContext);
11385
+ }
11386
+ pseudoNamedStyles[key] = pseudoClassStyles;
11387
+ continue;
11848
11388
  }
11849
- pseudoNamedStyles[key] = pseudoClassStyles;
11850
- continue;
11851
- }
11852
- // pseudo element
11853
- if (key.startsWith("::")) {
11854
- styleDeps.push(key);
11855
- const pseudoElementStyles = {};
11856
- const pseudoElementStyle = pseudoStyle[key];
11857
- for (const pseudoElementStyleKey of Object.keys(pseudoElementStyle)) {
11858
- const pseudoElementStyleValue = pseudoElementStyle[pseudoElementStyleKey];
11859
- assignStyleFromProp(pseudoElementStyleValue, pseudoElementStyleKey, pseudoElementStyles, pseudoStyleContext);
11389
+ // pseudo element
11390
+ if (key.startsWith("::")) {
11391
+ styleDeps.push(key);
11392
+ const pseudoElementStyles = {};
11393
+ const pseudoElementStyle = pseudoStyle[key];
11394
+ for (const pseudoElementStyleKey of Object.keys(pseudoElementStyle)) {
11395
+ const pseudoElementStyleValue = pseudoElementStyle[pseudoElementStyleKey];
11396
+ assignStyleFromProp(pseudoElementStyleValue, pseudoElementStyleKey, pseudoElementStyles, pseudoStyleContext);
11397
+ }
11398
+ pseudoNamedStyles[key] = pseudoElementStyles;
11399
+ continue;
11860
11400
  }
11861
- pseudoNamedStyles[key] = pseudoElementStyles;
11862
- continue;
11401
+ console.warn(`unsupported pseudo style key "${key}"`);
11863
11402
  }
11864
- console.warn(`unsupported pseudo style key "${key}"`);
11865
11403
  }
11866
11404
  remainingProps.pseudoStyle = pseudoStyle;
11867
- // TODO: we should also pass pseudoState right?
11868
11405
  }
11869
11406
  if (typeof style === "string") {
11870
11407
  appendStyles(boxStyles, normalizeStyles(style, "css"), "css");
@@ -11872,7 +11409,7 @@ const Box = props => {
11872
11409
  } else if (style && typeof style === "object") {
11873
11410
  for (const key of Object.keys(style)) {
11874
11411
  const stylePropValue = style[key];
11875
- assignStyle(boxStyles, stylePropValue, key, styleContext);
11412
+ assignStyle(boxStyles, stylePropValue, key, styleContext, "css");
11876
11413
  styleDeps.push(stylePropValue); // impact box style -> add to deps
11877
11414
  }
11878
11415
  }
@@ -11924,49 +11461,31 @@ const Box = props => {
11924
11461
  let innerChildren;
11925
11462
  if (hasChildFunction) {
11926
11463
  if (Array.isArray(children)) {
11927
- innerChildren = children.map(child => typeof child === "function" ? child(remainingProps) : child);
11464
+ innerChildren = children.map(child => typeof child === "function" ? child(propsToForward) : child);
11928
11465
  } else if (typeof children === "function") {
11929
- innerChildren = children(remainingProps);
11466
+ innerChildren = children(propsToForward);
11930
11467
  } else {
11931
11468
  innerChildren = children;
11932
11469
  }
11933
11470
  } else {
11934
11471
  innerChildren = children;
11935
11472
  }
11936
- const shouldForwardAllToChild = visualSelector && pseudoStateSelector;
11937
11473
  return jsx(TagName, {
11938
11474
  ref: ref,
11939
11475
  className: innerClassName,
11940
- "data-layout-inline": layoutInline ? "" : undefined,
11941
- "data-layout-row": layoutRow ? "" : undefined,
11942
- "data-layout-column": layoutColumn ? "" : undefined,
11943
- ...(shouldForwardAllToChild ? undefined : remainingProps),
11476
+ "data-layout-inline": inline ? "" : undefined,
11477
+ "data-layout-row": row ? "" : undefined,
11478
+ "data-layout-column": column ? "" : undefined,
11479
+ ...remainingProps,
11944
11480
  children: jsx(BoxLayoutContext.Provider, {
11945
11481
  value: layout,
11946
11482
  children: innerChildren
11947
11483
  })
11948
11484
  });
11949
11485
  };
11950
- const Layout = props => {
11951
- const {
11952
- row,
11953
- column,
11954
- ...rest
11955
- } = props;
11956
- if (row) {
11957
- return jsx(Box, {
11958
- layoutRow: true,
11959
- ...rest
11960
- });
11961
- }
11962
- if (column) {
11963
- return jsx(Box, {
11964
- layoutColumn: true,
11965
- ...rest
11966
- });
11967
- }
11486
+ const Layout = props => {
11968
11487
  return jsx(Box, {
11969
- ...rest
11488
+ ...props
11970
11489
  });
11971
11490
  };
11972
11491
 
@@ -13907,31 +13426,6 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
13907
13426
  color: inherit;
13908
13427
  }
13909
13428
 
13910
- .navi_char_slot_invisible {
13911
- opacity: 0;
13912
- }
13913
-
13914
- .navi_icon {
13915
- display: flex;
13916
- aspect-ratio: 1 / 1;
13917
- height: 100%;
13918
- max-height: 1em;
13919
- align-items: center;
13920
- justify-content: center;
13921
- }
13922
-
13923
- .navi_text[data-has-foreground] {
13924
- display: inline-block;
13925
- }
13926
-
13927
- .navi_text_foreground {
13928
- position: absolute;
13929
- inset: 0;
13930
- display: inline-flex;
13931
- align-items: center;
13932
- justify-content: center;
13933
- }
13934
-
13935
13429
  .navi_text_overflow_wrapper {
13936
13430
  display: flex;
13937
13431
  width: 0;
@@ -14016,91 +13510,18 @@ const TextOverflowPinned = ({
14016
13510
  };
14017
13511
  const TextBasic = ({
14018
13512
  as = "span",
14019
- foregroundColor,
14020
- foregroundElement,
14021
13513
  contentSpacing = " ",
14022
- box,
14023
13514
  children,
14024
13515
  ...rest
14025
13516
  }) => {
14026
- const hasForeground = Boolean(foregroundElement || foregroundColor);
14027
- const text = jsxs(Box, {
13517
+ const text = jsx(Box, {
14028
13518
  ...rest,
14029
13519
  baseClassName: "navi_text",
14030
13520
  as: as,
14031
- layoutInline: true,
14032
- layoutColumn: box ? true : undefined,
14033
- "data-has-foreground": hasForeground ? "" : undefined,
14034
- children: [applyContentSpacingOnTextChildren(children, contentSpacing), hasForeground && jsx("span", {
14035
- className: "navi_text_foreground",
14036
- style: {
14037
- backgroundColor: foregroundColor
14038
- },
14039
- children: foregroundElement
14040
- })]
13521
+ children: applyContentSpacingOnTextChildren(children, contentSpacing)
14041
13522
  });
14042
13523
  return text;
14043
13524
  };
14044
- const CharSlot = ({
14045
- charWidth = 1,
14046
- // 0 (zéro) is the real char width
14047
- // but 2 zéros gives too big icons
14048
- // while 1 "W" gives a nice result
14049
- baseChar = "W",
14050
- "aria-label": ariaLabel,
14051
- role,
14052
- decorative = false,
14053
- children,
14054
- ...rest
14055
- }) => {
14056
- const invisibleText = baseChar.repeat(charWidth);
14057
- const ariaProps = decorative ? {
14058
- "aria-hidden": "true"
14059
- } : {
14060
- role,
14061
- "aria-label": ariaLabel
14062
- };
14063
- return jsx(Text, {
14064
- ...rest,
14065
- ...ariaProps,
14066
- foregroundElement: children,
14067
- children: jsx("span", {
14068
- className: "navi_char_slot_invisible",
14069
- "aria-hidden": "true",
14070
- children: invisibleText
14071
- })
14072
- });
14073
- };
14074
- const Icon = ({
14075
- box,
14076
- href,
14077
- children,
14078
- ...props
14079
- }) => {
14080
- const innerChildren = href ? jsx("svg", {
14081
- width: "100%",
14082
- height: "100%",
14083
- children: jsx("use", {
14084
- href: href
14085
- })
14086
- }) : children;
14087
- if (box) {
14088
- return jsx(Box, {
14089
- layoutInline: true,
14090
- layoutColumn: true,
14091
- ...props,
14092
- children: innerChildren
14093
- });
14094
- }
14095
- return jsx(CharSlot, {
14096
- decorative: true,
14097
- ...props,
14098
- children: jsx("span", {
14099
- className: "navi_icon",
14100
- children: innerChildren
14101
- })
14102
- });
14103
- };
14104
13525
  const Paragraph = ({
14105
13526
  contentSpacing = " ",
14106
13527
  marginTop = "md",
@@ -14163,6 +13584,114 @@ const applyContentSpacingOnTextChildren = (children, contentSpacing) => {
14163
13584
  return childrenWithGap;
14164
13585
  };
14165
13586
 
13587
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
13588
+ .navi_icon {
13589
+ display: inline-block;
13590
+ box-sizing: border-box;
13591
+ }
13592
+
13593
+ .navi_icon_char_slot {
13594
+ opacity: 0;
13595
+ }
13596
+ .navi_icon_foreground {
13597
+ position: absolute;
13598
+ inset: 0;
13599
+ display: inline-flex;
13600
+ box-sizing: border-box;
13601
+ align-items: center;
13602
+ justify-content: start;
13603
+ }
13604
+ .navi_icon_foreground > .navi_text {
13605
+ display: flex;
13606
+ aspect-ratio: 1 / 1;
13607
+ height: 100%;
13608
+ max-height: 1em;
13609
+ align-items: center;
13610
+ justify-content: center;
13611
+ }
13612
+
13613
+ .navi_icon > svg,
13614
+ .navi_icon > img {
13615
+ width: 100%;
13616
+ height: 100%;
13617
+ }
13618
+ .navi_icon[data-width] > svg,
13619
+ .navi_icon[data-width] > img {
13620
+ width: 100%;
13621
+ height: auto;
13622
+ }
13623
+ .navi_icon[data-height] > svg,
13624
+ .navi_icon[data-height] > img {
13625
+ width: auto;
13626
+ height: 100%;
13627
+ }
13628
+ `;
13629
+ const Icon = ({
13630
+ href,
13631
+ children,
13632
+ className,
13633
+ charWidth = 1,
13634
+ // 0 (zéro) is the real char width
13635
+ // but 2 zéros gives too big icons
13636
+ // while 1 "W" gives a nice result
13637
+ baseChar = "W",
13638
+ "aria-label": ariaLabel,
13639
+ role,
13640
+ decorative = false,
13641
+ ...props
13642
+ }) => {
13643
+ const innerChildren = href ? jsx("svg", {
13644
+ width: "100%",
13645
+ height: "100%",
13646
+ children: jsx("use", {
13647
+ href: href
13648
+ })
13649
+ }) : children;
13650
+ let {
13651
+ box,
13652
+ width,
13653
+ height
13654
+ } = props;
13655
+ if (width !== undefined || height !== undefined) {
13656
+ box = true;
13657
+ }
13658
+ if (box) {
13659
+ return jsx(Box, {
13660
+ ...props,
13661
+ baseClassName: "navi_icon",
13662
+ "data-width": width,
13663
+ "data-height": height,
13664
+ children: innerChildren
13665
+ });
13666
+ }
13667
+ const invisibleText = baseChar.repeat(charWidth);
13668
+ const ariaProps = decorative ? {
13669
+ "aria-hidden": "true"
13670
+ } : {
13671
+ role,
13672
+ "aria-label": ariaLabel
13673
+ };
13674
+ return jsxs(Text, {
13675
+ ...props,
13676
+ ...ariaProps,
13677
+ box: box,
13678
+ className: withPropsClassName("navi_icon", className),
13679
+ "data-icon-char": "",
13680
+ "data-width": width,
13681
+ "data-height": height,
13682
+ children: [jsx("span", {
13683
+ className: "navi_icon_char_slot",
13684
+ "aria-hidden": "true",
13685
+ children: invisibleText
13686
+ }), jsx("span", {
13687
+ className: "navi_icon_foreground",
13688
+ children: jsx(Text, {
13689
+ children: innerChildren
13690
+ })
13691
+ })]
13692
+ });
13693
+ };
13694
+
14166
13695
  installImportMetaCss(import.meta);import.meta.css = /* css */`
14167
13696
  @layer navi {
14168
13697
  .navi_link {
@@ -14172,7 +13701,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
14172
13701
  --color-visited: light-dark(#6a1b9a, #ab47bc);
14173
13702
  --color-active: red;
14174
13703
  --text-decoration: underline;
14175
- --text-decoration-hover: underline;
13704
+ --text-decoration-hover: var(--text-decoration);
14176
13705
  --cursor: pointer;
14177
13706
  }
14178
13707
  }
@@ -14183,7 +13712,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
14183
13712
  --x-color-visited: var(--color-visited);
14184
13713
  --x-color-active: var(--color-active);
14185
13714
  --x-text-decoration: var(--text-decoration);
14186
- --x-text-decoration-hover: var(--text-decoration-hover,);
13715
+ --x-text-decoration-hover: var(--text-decoration-hover);
14187
13716
  --x-cursor: var(--cursor);
14188
13717
 
14189
13718
  position: relative;
@@ -14301,7 +13830,6 @@ const LinkPlain = props => {
14301
13830
  rel,
14302
13831
  preventDefault,
14303
13832
  // visual
14304
- box,
14305
13833
  blankTargetIcon,
14306
13834
  anchorIcon,
14307
13835
  icon,
@@ -14361,8 +13889,6 @@ const LinkPlain = props => {
14361
13889
  // Visual
14362
13890
  ,
14363
13891
  baseClassName: "navi_link",
14364
- layoutInline: true,
14365
- layoutColumn: box ? true : undefined,
14366
13892
  managedByCSSVars: LinkManagedByCSSVars,
14367
13893
  pseudoClasses: LinkPseudoClasses,
14368
13894
  pseudoElements: LinkPseudoElements,
@@ -14408,8 +13934,6 @@ const LinkPlain = props => {
14408
13934
  const BlankTargetLinkSvg = () => {
14409
13935
  return jsx("svg", {
14410
13936
  viewBox: "0 0 24 24",
14411
- width: "100%",
14412
- height: "100%",
14413
13937
  xmlns: "http://www.w3.org/2000/svg",
14414
13938
  children: jsx("path", {
14415
13939
  d: "M10.0002 5H8.2002C7.08009 5 6.51962 5 6.0918 5.21799C5.71547 5.40973 5.40973 5.71547 5.21799 6.0918C5 6.51962 5 7.08009 5 8.2002V15.8002C5 16.9203 5 17.4801 5.21799 17.9079C5.40973 18.2842 5.71547 18.5905 6.0918 18.7822C6.5192 19 7.07899 19 8.19691 19H15.8031C16.921 19 17.48 19 17.9074 18.7822C18.2837 18.5905 18.5905 18.2839 18.7822 17.9076C19 17.4802 19 16.921 19 15.8031V14M20 9V4M20 4H15M20 4L13 11",
@@ -14424,8 +13948,6 @@ const BlankTargetLinkSvg = () => {
14424
13948
  const AnchorLinkSvg = () => {
14425
13949
  return jsxs("svg", {
14426
13950
  viewBox: "0 0 24 24",
14427
- width: "100%",
14428
- height: "100%",
14429
13951
  xmlns: "http://www.w3.org/2000/svg",
14430
13952
  children: [jsx("path", {
14431
13953
  d: "M13.2218 3.32234C15.3697 1.17445 18.8521 1.17445 21 3.32234C23.1479 5.47022 23.1479 8.95263 21 11.1005L17.4645 14.636C15.3166 16.7839 11.8342 16.7839 9.6863 14.636C9.48752 14.4373 9.30713 14.2271 9.14514 14.0075C8.90318 13.6796 8.97098 13.2301 9.25914 12.9419C9.73221 12.4688 10.5662 12.6561 11.0245 13.1435C11.0494 13.1699 11.0747 13.196 11.1005 13.2218C12.4673 14.5887 14.6834 14.5887 16.0503 13.2218L19.5858 9.6863C20.9526 8.31947 20.9526 6.10339 19.5858 4.73655C18.219 3.36972 16.0029 3.36972 14.636 4.73655L13.5754 5.79721C13.1849 6.18774 12.5517 6.18774 12.1612 5.79721C11.7706 5.40669 11.7706 4.77352 12.1612 4.383L13.2218 3.32234Z",
@@ -14439,8 +13961,6 @@ const AnchorLinkSvg = () => {
14439
13961
  const PhoneSvg = () => {
14440
13962
  return jsx("svg", {
14441
13963
  viewBox: "0 0 24 24",
14442
- width: "100%",
14443
- height: "100%",
14444
13964
  xmlns: "http://www.w3.org/2000/svg",
14445
13965
  children: jsx("path", {
14446
13966
  d: "M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z",
@@ -14451,8 +13971,6 @@ const PhoneSvg = () => {
14451
13971
  const SmsSvg = () => {
14452
13972
  return jsx("svg", {
14453
13973
  viewBox: "0 0 24 24",
14454
- width: "100%",
14455
- height: "100%",
14456
13974
  xmlns: "http://www.w3.org/2000/svg",
14457
13975
  children: jsx("path", {
14458
13976
  d: "M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM18 14H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z",
@@ -14463,8 +13981,6 @@ const SmsSvg = () => {
14463
13981
  const EmailSvg = () => {
14464
13982
  return jsxs("svg", {
14465
13983
  viewBox: "0 0 24 24",
14466
- width: "100%",
14467
- height: "100%",
14468
13984
  xmlns: "http://www.w3.org/2000/svg",
14469
13985
  children: [jsx("path", {
14470
13986
  d: "M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z",
@@ -14484,8 +14000,6 @@ const EmailSvg = () => {
14484
14000
  const GithubSvg = () => {
14485
14001
  return jsx("svg", {
14486
14002
  viewBox: "0 0 24 24",
14487
- width: "100%",
14488
- height: "100%",
14489
14003
  xmlns: "http://www.w3.org/2000/svg",
14490
14004
  children: jsx("path", {
14491
14005
  d: "M12 2C6.48 2 2 6.48 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.87 1.52 2.34 1.07 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02.79-.22 1.65-.33 2.5-.33.85 0 1.71.11 2.5.33 1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0012 2z",
@@ -17056,13 +16570,10 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
17056
16570
  --x-color: var(--color);
17057
16571
 
17058
16572
  position: relative;
16573
+ display: inline-flex;
17059
16574
  box-sizing: border-box;
17060
- width: fit-content;
17061
- height: fit-content;
17062
16575
  padding: 0;
17063
16576
  flex-direction: inherit;
17064
- align-items: inherit;
17065
- justify-content: inherit;
17066
16577
  background: none;
17067
16578
  border: none;
17068
16579
  border-radius: inherit;
@@ -17071,6 +16582,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
17071
16582
  }
17072
16583
  .navi_button_content {
17073
16584
  position: relative;
16585
+ box-sizing: border-box;
17074
16586
  padding-top: var(--padding-top, var(--padding-y, var(--padding, 1px)));
17075
16587
  padding-right: var(--padding-right, var(--padding-x, var(--padding, 6px)));
17076
16588
  padding-bottom: var(
@@ -17241,7 +16753,6 @@ const ButtonBasic = props => {
17241
16753
  ...buttonProps,
17242
16754
  as: "span",
17243
16755
  baseClassName: "navi_button_content",
17244
- layoutInline: true,
17245
16756
  children: [innerChildren, jsx("span", {
17246
16757
  className: "navi_button_shadow"
17247
16758
  })]
@@ -17259,8 +16770,6 @@ const ButtonBasic = props => {
17259
16770
  // style management
17260
16771
  ,
17261
16772
  baseClassName: "navi_button",
17262
- layoutInline: true,
17263
- layoutColumn: true,
17264
16773
  managedByCSSVars: ButtonManagedByCSSVars,
17265
16774
  pseudoClasses: ButtonPseudoClasses,
17266
16775
  pseudoElements: ButtonPseudoElements,
@@ -21811,29 +21320,97 @@ const SVGMaskOverlay = ({
21811
21320
  });
21812
21321
  };
21813
21322
 
21323
+ const CSS_VAR_NAME = "--color-contrasting";
21324
+
21325
+ const useContrastingColor = (ref) => {
21326
+ useLayoutEffect(() => {
21327
+ const el = ref.current;
21328
+ if (!el) {
21329
+ return;
21330
+ }
21331
+ const lightColor = "var(--navi-color-light)";
21332
+ const darkColor = "var(--navi-color-dark)";
21333
+ const backgroundColor = getComputedStyle(el).backgroundColor;
21334
+ if (!backgroundColor) {
21335
+ el.style.removeProperty(CSS_VAR_NAME);
21336
+ return;
21337
+ }
21338
+ const colorPicked = pickLightOrDark(
21339
+ backgroundColor,
21340
+ lightColor,
21341
+ darkColor,
21342
+ el,
21343
+ );
21344
+ el.style.setProperty(CSS_VAR_NAME, colorPicked);
21345
+ }, []);
21346
+ };
21347
+
21814
21348
  installImportMetaCss(import.meta);import.meta.css = /* css */`
21815
- .navi_count {
21816
- position: relative;
21817
- top: -1px;
21818
- color: rgba(28, 43, 52, 0.4);
21349
+ @layer navi {
21350
+ .navi_badge {
21351
+ --border-radius: 1em;
21352
+ }
21353
+ }
21354
+ .navi_badge {
21355
+ display: inline-block;
21356
+ box-sizing: border-box;
21357
+ min-width: 1.5em;
21358
+ height: 1.5em;
21359
+ max-height: 1.5em;
21360
+ padding-right: var(
21361
+ --padding-right,
21362
+ var(--padding-x, var(--padding, 0.4em))
21363
+ );
21364
+ padding-left: var(--padding-left, var(--padding-x, var(--padding, 0.4em)));
21365
+ color: var(--color, var(--color-contrasting));
21366
+ text-align: center;
21367
+ line-height: 1.5em;
21368
+ vertical-align: middle;
21369
+ border-radius: var(--border-radius, 1em);
21819
21370
  }
21820
21371
  `;
21821
- const Count = ({
21372
+ const BadgeManagedByCSSVars = {
21373
+ borderWidth: "--border-width",
21374
+ borderRadius: "--border-radius",
21375
+ paddingRight: "--padding-right",
21376
+ paddingLeft: "--padding-left",
21377
+ backgroundColor: "--background-color",
21378
+ borderColor: "--border-color",
21379
+ color: "--color"
21380
+ };
21381
+ const BadgeCount = ({
21822
21382
  children,
21823
- ...rest
21383
+ bold = true,
21384
+ max,
21385
+ ...props
21824
21386
  }) => {
21825
- return jsxs(Box, {
21826
- as: "span",
21827
- baseClassName: ".navi_count",
21828
- ...rest,
21829
- children: ["(", children, ")"]
21830
- });
21831
- };
21387
+ const defaultRef = useRef();
21388
+ const ref = props.ref || defaultRef;
21832
21389
 
21833
- const Image = props => {
21834
- return jsx(Box, {
21390
+ // Calculer la valeur à afficher en fonction du paramètre max
21391
+ const getDisplayValue = () => {
21392
+ if (max === undefined) {
21393
+ return children;
21394
+ }
21395
+ const numericValue = typeof children === "string" ? parseInt(children, 10) : children;
21396
+ const numericMax = typeof max === "string" ? parseInt(max, 10) : max;
21397
+ if (isNaN(numericValue) || isNaN(numericMax)) {
21398
+ return children;
21399
+ }
21400
+ if (numericValue > numericMax) {
21401
+ return `${numericMax}+`;
21402
+ }
21403
+ return children;
21404
+ };
21405
+ const displayValue = getDisplayValue();
21406
+ useContrastingColor(ref);
21407
+ return jsx(Text, {
21835
21408
  ...props,
21836
- as: "img"
21409
+ ref: ref,
21410
+ className: "navi_badge",
21411
+ bold: bold,
21412
+ managedByCSSVars: BadgeManagedByCSSVars,
21413
+ children: displayValue
21837
21414
  });
21838
21415
  };
21839
21416
 
@@ -21849,6 +21426,13 @@ const Code = ({
21849
21426
  });
21850
21427
  };
21851
21428
 
21429
+ const Image = props => {
21430
+ return jsx(Box, {
21431
+ ...props,
21432
+ as: "img"
21433
+ });
21434
+ };
21435
+
21852
21436
  const LinkWithIcon = () => {};
21853
21437
 
21854
21438
  installImportMetaCss(import.meta);import.meta.css = /* css */`
@@ -22009,6 +21593,118 @@ const Title = ({
22009
21593
  });
22010
21594
  };
22011
21595
 
21596
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
21597
+ @layer navi {
21598
+ .navi_dialog_layout {
21599
+ --margin: 30px;
21600
+ --padding: 20px;
21601
+ --background: white;
21602
+ --border-width: 2px;
21603
+ --border-color: lightgrey;
21604
+ --border-radius: 10px;
21605
+ --min-width: 300px;
21606
+ --min-height: auto;
21607
+ }
21608
+ }
21609
+ .navi_dialog_layout {
21610
+ padding-top: var(--margin-top, var(--margin-y, var(--margin)));
21611
+ padding-right: var(--margin-right, var(--margin-x, var(--margin)));
21612
+ padding-bottom: var(--margin-bottom, var(--margin-y, var(--margin)));
21613
+ padding-left: var(--margin-left, var(--margin-x, var(--margin)));
21614
+ }
21615
+
21616
+ .navi_dialog_content {
21617
+ min-width: var(--min-width);
21618
+ min-height: var(--min-height);
21619
+ padding-top: var(--padding-top, var(--padding-y, var(--padding)));
21620
+ padding-right: var(--padding-right, var(--padding-x, var(--padding)));
21621
+ padding-bottom: var(--padding-bottom, var(--padding-y, var(--padding)));
21622
+ padding-left: var(--padding-left, var(--padding-x, var(--padding)));
21623
+ background: var(--background);
21624
+ background-color: var(--background-color, var(--background));
21625
+ border-width: var(--border-width);
21626
+ border-style: solid;
21627
+ border-color: var(--border-color);
21628
+ border-radius: var(--border-radius);
21629
+ }
21630
+ `;
21631
+ const DialogManagedByCSSVars = {
21632
+ margin: "--margin",
21633
+ marginTop: "--margin-top",
21634
+ marginBottom: "--margin-bottom",
21635
+ marginLeft: "--margin-left",
21636
+ marginRight: "--margin-right",
21637
+ borderRadius: "--border-radius",
21638
+ borderWidth: "--border-width",
21639
+ borderColor: "--border-color",
21640
+ background: "--background",
21641
+ backgroundColor: "--background-color",
21642
+ padding: "--padding",
21643
+ paddingTop: "--padding-top",
21644
+ paddingBottom: "--padding-bottom",
21645
+ paddingLeft: "--padding-left",
21646
+ paddingRight: "--padding-right",
21647
+ minWidth: "--min-width",
21648
+ minHeight: "--min-height"
21649
+ };
21650
+ const DialogLayout = ({
21651
+ children,
21652
+ contentAlignX = "center",
21653
+ contentAlignY = "center",
21654
+ ...props
21655
+ }) => {
21656
+ return jsx(Box, {
21657
+ className: "navi_dialog_layout",
21658
+ managedByCSSVars: DialogManagedByCSSVars,
21659
+ visualSelector: ".navi_dialog_content",
21660
+ ...props,
21661
+ contentAlignX: contentAlignX,
21662
+ contentAlignY: contentAlignY,
21663
+ children: jsx(Box, {
21664
+ className: "navi_dialog_content",
21665
+ row: true,
21666
+ children: children
21667
+ })
21668
+ });
21669
+ };
21670
+
21671
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
21672
+ @layer navi {
21673
+ .navi_viewport_layout {
21674
+ --padding: 40px;
21675
+ --background: white;
21676
+ }
21677
+ }
21678
+
21679
+ .navi_viewport_layout {
21680
+ padding-top: var(--padding-top, var(--padding-y, var(--padding)));
21681
+ padding-right: var(--padding-right, var(--padding-x, var(--padding)));
21682
+ padding-bottom: var(--padding-bottom, var(--padding-y, var(--padding)));
21683
+ padding-left: var(--padding-left, var(--padding-x, var(--padding)));
21684
+ background: var(--background);
21685
+ }
21686
+ `;
21687
+ const ViewportManagedByCSSVars = {
21688
+ padding: "--padding",
21689
+ paddingTop: "--padding-top",
21690
+ paddingBottom: "--padding-bottom",
21691
+ paddingLeft: "--padding-left",
21692
+ paddingRight: "--padding-right",
21693
+ background: "--background"
21694
+ };
21695
+ const ViewportLayout = props => {
21696
+ return jsx(Box, {
21697
+ row: true,
21698
+ ...props,
21699
+ className: "navi_viewport_layout",
21700
+ managedByCSSVars: ViewportManagedByCSSVars,
21701
+ minWidth: "max-content",
21702
+ minHeight: "max-content",
21703
+ width: "100%",
21704
+ height: "100%"
21705
+ });
21706
+ };
21707
+
22012
21708
  const createUniqueValueConstraint = (
22013
21709
  // the set might be incomplete (the front usually don't have the full copy of all the items from the backend)
22014
21710
  // but this is already nice to help user with what we know
@@ -22086,5 +21782,5 @@ const useDependenciesDiff = (inputs) => {
22086
21782
  return diffRef.current;
22087
21783
  };
22088
21784
 
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 };
21785
+ export { ActionRenderer, ActiveKeyboardShortcuts, BadgeCount, Box, Button, Checkbox, CheckboxList, Code, Col, Colgroup, Details, DialogLayout, 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, ViewportLayout, 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, useActiveRouteInfo, useCellsAndColumns, useDependenciesDiff, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, useUrlSearchParam, valueInLocalStorage };
22090
21786
  //# sourceMappingURL=jsenv_navi.js.map