@jsenv/navi 0.12.8 → 0.12.9

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
+ .active_group,
8738
+ .previous_group,
8739
+ .target_slot,
8740
+ .previous_target_slot,
8741
+ .outgoing_slot,
8742
+ .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
+ .target_slot,
8748
+ .outgoing_slot,
8749
+ .previous_target_slot,
8750
+ .previous_outgoing_slot {
8751
+ display: flex;
8752
+ align-items: var(--x-align-items);
8753
+ justify-content: var(--x-justify-content);
8754
+ }
8755
+ .target_slot[data-items-width-overflow],
8756
+ .previous_target_slot[data-items-width-overflow],
8757
+ .previous_target_slot[data-items-width-overflow],
8758
+ .previous_outgoing_slot[data-items-width-overflow] {
8759
+ --x-justify-content: flex-start;
8760
+ }
8761
+ .target_slot[data-items-height-overflow],
8762
+ .previous_slot[data-items-height-overflow],
8763
+ .previous_target_slot[data-items-height-overflow],
8764
+ .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
+ .active_group {
8769
+ position: relative;
8770
+ }
8771
+ .target_slot {
8772
+ position: relative;
8773
+ }
8774
+ .outgoing_slot,
8775
+ .previous_outgoing_slot {
8776
+ position: absolute;
8777
+ top: 0;
8778
+ left: 0;
8779
+ }
8780
+ .previous_group {
8629
8781
  position: absolute;
8630
8782
  inset: 0;
8783
+ }
8784
+ .ui_transition[data-only-previous-group] .previous_group {
8785
+ position: relative;
8786
+ }
8787
+
8788
+ .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] .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(".active_group");
8848
+ const targetSlot = root.querySelector(".target_slot");
8849
+ const outgoingSlot = root.querySelector(".outgoing_slot");
8850
+ const previousGroup = root.querySelector(".previous_group");
8851
+ const previousTargetSlot = previousGroup?.querySelector(
8852
+ ".previous_target_slot",
8677
8853
  );
8678
- const contentOverlay = container.querySelector(
8679
- ".ui_transition_content_overlay",
8854
+ const previousOutgoingSlot = previousGroup?.querySelector(
8855
+ ".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 = "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
9177
 
9297
- let targetWidth;
9298
- let targetHeight;
9299
- if (isContentPhase) {
9300
- const shouldUseNewDimensions =
9301
- naturalContentWidth === 0 && naturalContentHeight === 0;
9302
- if (shouldUseNewDimensions) {
9303
- // we don't have any natural content dimensions yet, we can use the content phase dimensions for now
9304
- [targetWidth, targetHeight] = measureSlotSize();
9305
- debug(
9306
- "size",
9307
- `content phase dimension measured to: ${targetWidth}x${targetHeight}`,
9308
- );
9309
- } else {
9310
- // we don't care about the content phase dimension.
9311
- // the content dimensions prevails
9312
- targetWidth = naturalContentWidth;
9313
- targetHeight = naturalContentHeight;
9314
- debug(
9315
- "size",
9316
- `content phase using natural content size: ${naturalContentWidth}x${naturalContentHeight}`,
9317
- );
9318
- }
9319
- } else {
9320
- outerWrapper.style.width = "";
9321
- outerWrapper.style.height = "";
9322
- const [slotNaturalWidth, slotNaturalHeight] = measureSlotSize();
9323
- outerWrapper.style.width = `${constrainedWidth}px`;
9324
- outerWrapper.style.height = `${constrainedHeight}px`;
9325
- updateNaturalContentSize(slotNaturalWidth, slotNaturalHeight);
9326
- targetWidth = slotNaturalWidth;
9327
- targetHeight = slotNaturalHeight;
9328
- debug(
9329
- "size",
9330
- `content size measured to: ${slotNaturalWidth}x${slotNaturalHeight}`,
9331
- );
9332
- }
9333
-
9334
- // If size transitions are disabled hold the previous size to avoid cropping during the content transition.
9335
- if (!hasSizeTransitions) {
9336
- debug(
9337
- "size",
9338
- `Holding previous size during content transition: ${constrainedWidth}x${constrainedHeight}`,
9339
- );
9340
- applySizeConstraints(
9341
- constrainedWidth,
9342
- constrainedHeight,
9343
- "hold size for content transition",
9344
- );
9345
- sizeTransition?.cancel();
9346
- onContentTransitionComplete = () => {
9347
- onContentTransitionComplete = null;
9348
- releaseSizeConstraints(
9349
- "content transition completed - release size hold",
9350
- );
9351
- };
9352
- return;
9353
- }
9354
-
9355
- if (
9356
- targetWidth === constrainedWidth &&
9357
- targetHeight === constrainedHeight
9358
- ) {
9359
- sizeTransition?.cancel();
9360
- debug("size", "No size change required");
9361
- releaseSizeConstraints("no size change needed");
9362
- return;
9363
- }
9364
- debug("size", "Size change needed:", {
9365
- width: `${constrainedWidth} → ${targetWidth}`,
9366
- height: `${constrainedHeight} → ${targetHeight}`,
9367
- });
9368
- updateToSize(targetWidth, targetHeight);
9369
- };
9370
- subscribeChange(updateSizeTransition);
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
9476
 
9915
- debug("content", "Slide transition:", {
9916
- oldContent: `${oldContentPosition} → ${-containerWidth}`,
9917
- newContent: `${effectiveFromPosition} → ${naturalNewPosition}`,
9918
- });
9919
-
9920
- const transitions = [];
9921
-
9922
- // Slide old content out
9923
- transitions.push(
9924
- createTranslateXTransition(oldElement, -containerWidth, {
9925
- setup: () =>
9926
- notifyTransition(newElement, {
9927
- modelId: slideLeft.id,
9928
- canOverflow: true,
9929
- id: "slide_out_old_content",
9930
- }),
9931
- from: oldContentPosition,
9932
- duration,
9933
- startProgress,
9934
- onUpdate: ({ value }) => {
9935
- debug("transition_updates", "Old content slide out:", value);
9936
- },
9937
- }),
9938
- );
9939
-
9940
- // Slide new content in
9941
- transitions.push(
9942
- createTranslateXTransition(newElement, naturalNewPosition, {
9943
- setup: () =>
9944
- notifyTransition(newElement, {
9945
- modelId: slideLeft.id,
9946
- canOverflow: true,
9947
- id: "slide_in_new_content",
9948
- }),
9949
- from: effectiveFromPosition,
9950
- duration,
9951
- startProgress,
9952
- onUpdate: ({ value, timing }) => {
9953
- debug("transition_updates", "New content slide in:", value);
9954
- if (timing === "end") {
9955
- debug("content", "Slide complete");
9956
- }
9957
- },
9958
- }),
9959
- );
9960
-
9961
- return transitions;
9962
- },
9963
- };
9964
-
9965
- const crossFade = {
9966
- id: "ui_transition_cross_fade",
9967
- name: "cross-fade",
9968
- apply: (
9969
- oldElement,
9970
- newElement,
9971
- { duration, startProgress = 0, isPhaseTransition = false, debug },
9972
- ) => {
9973
- if (!oldElement && !newElement) {
9974
- return [];
9975
- }
9976
-
9977
- if (!newElement) {
9978
- // Content -> Empty (fade out only)
9979
- const from = getOpacity(oldElement);
9980
- const to = 0;
9981
- debug("content", "Fade out to empty:", { from, to });
9982
- return [
9983
- createOpacityTransition(oldElement, to, {
9984
- setup: () =>
9985
- notifyTransition(newElement, {
9986
- modelId: crossFade.id,
9987
- canOverflow: true,
9988
- id: "fade_out_old_content",
9989
- }),
9990
- from,
9991
- duration,
9992
- startProgress,
9993
- onUpdate: ({ value, timing }) => {
9994
- debug("transition_updates", "Content fade out:", value.toFixed(3));
9995
- if (timing === "end") {
9996
- debug("content", "Fade out complete");
9997
- }
9998
- },
9999
- }),
10000
- ];
10001
- }
10002
-
10003
- if (!oldElement) {
10004
- // Empty -> Content (fade in only)
10005
- const from = 0;
10006
- const to = getOpacityWithoutTransition(newElement);
10007
- debug("content", "Fade in from empty:", { from, to });
10008
- return [
10009
- createOpacityTransition(newElement, to, {
10010
- setup: () =>
10011
- notifyTransition(newElement, {
10012
- modelId: crossFade.id,
10013
- canOverflow: true,
10014
- id: "fade_in_new_content",
10015
- }),
10016
- from,
10017
- duration,
10018
- startProgress,
10019
- onUpdate: ({ value, timing }) => {
10020
- debug("transition_updates", "Fade in progress:", value.toFixed(3));
10021
- if (timing === "end") {
10022
- debug("content", "Fade in complete");
10023
- }
10024
- },
10025
- }),
10026
- ];
10027
- }
10028
-
10029
- // Content -> Content (cross-fade)
10030
- // Get current opacity for both elements
10031
- const oldOpacity = getOpacity(oldElement);
10032
- const newOpacity = getOpacity(newElement);
10033
- const newNaturalOpacity = getOpacityWithoutTransition(newElement);
10034
-
10035
- // For phase transitions, always start new content from 0 for clean visual transition
10036
- // For content transitions, check for ongoing transitions to continue smoothly
10037
- let effectiveFromOpacity;
10038
- if (isPhaseTransition) {
10039
- effectiveFromOpacity = 0; // Always start fresh for phase transitions (loading → content, etc.)
10040
- } else {
10041
- // For content transitions: if new element has ongoing opacity transition
10042
- // (indicated by non-zero opacity when natural opacity is different),
10043
- // start from current opacity to continue smoothly, otherwise start from 0
10044
- const hasOngoingTransition =
10045
- newOpacity !== newNaturalOpacity && newOpacity > 0;
10046
- effectiveFromOpacity = hasOngoingTransition ? newOpacity : 0;
10047
- }
10048
-
10049
- debug("content", "Cross-fade transition:", {
10050
- oldOpacity: `${oldOpacity} → 0`,
10051
- newOpacity: `${effectiveFromOpacity} → ${newNaturalOpacity}`,
10052
- isPhaseTransition,
10053
- });
10054
-
10055
- return [
10056
- createOpacityTransition(oldElement, 0, {
10057
- setup: () =>
10058
- notifyTransition(newElement, {
10059
- modelId: crossFade.id,
10060
- canOverflow: true,
10061
- id: "fade_out_old_content",
10062
- }),
10063
- from: oldOpacity,
10064
- duration,
10065
- startProgress,
10066
- onUpdate: ({ value }) => {
10067
- if (value > 0) {
10068
- debug(
10069
- "transition_updates",
10070
- "Old content fade out:",
10071
- value.toFixed(3),
10072
- );
10073
- }
10074
- },
10075
- }),
10076
- createOpacityTransition(newElement, newNaturalOpacity, {
10077
- setup: () =>
10078
- notifyTransition(newElement, {
10079
- modelId: crossFade.id,
10080
- canOverflow: true,
10081
- id: "fade_in_new_content",
10082
- }),
10083
- from: effectiveFromOpacity,
10084
- duration,
10085
- startProgress: isPhaseTransition ? 0 : startProgress, // Phase transitions: new content always starts fresh
10086
- onUpdate: ({ value, timing }) => {
10087
- debug("transition_updates", "New content fade in:", value.toFixed(3));
10088
- if (timing === "end") {
10089
- debug("content", "Cross-fade complete");
10090
- }
10091
- },
10092
- }),
10093
- ];
10094
- },
10095
- };
10096
-
10097
- const notifyTransition = (element, detail) => {
10098
- dispatchUITransitionStartCustomEvent(element, detail);
10099
- return () => {
10100
- dispatchUITransitionEndCustomEvent(element, detail);
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,79 @@ 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,
10167
9533
  ...props
10168
9534
  }) => {
10169
- const [contentKeyFromContext, setContentKeyFromContext] = useState();
10170
- const contentKeyContextValue = useMemo(() => {
10171
- const keySet = new Set();
10172
- const onKeySetChange = () => {
10173
- setContentKeyFromContext(Array.from(keySet).join("|"));
9535
+ const contentIdRef = useRef(contentId);
9536
+ const updateContentId = () => {
9537
+ const uiTransition = uiTransitionRef.current;
9538
+ if (!uiTransition) {
9539
+ return;
9540
+ }
9541
+ const value = contentIdRef.current;
9542
+ uiTransition.updateContentId(value);
9543
+ };
9544
+ const uiTransitionContentIdContextValue = useMemo(() => {
9545
+ const set = new Set();
9546
+ const onSetChange = () => {
9547
+ const value = Array.from(set).join("|");
9548
+ contentIdRef.current = value;
9549
+ updateContentId();
10174
9550
  };
10175
- const update = (key, newKey) => {
10176
- if (!keySet.has(key)) {
10177
- console.warn(`UITransition: trying to update a key that does not exist: ${key}`);
9551
+ const update = (part, newPart) => {
9552
+ if (!set.has(part)) {
9553
+ console.warn(`UITransition: trying to update an id that does not exist: ${part}`);
10178
9554
  return;
10179
9555
  }
10180
- keySet.delete(key);
10181
- keySet.add(newKey);
10182
- onKeySetChange();
9556
+ set.delete(part);
9557
+ set.add(newPart);
9558
+ onSetChange();
10183
9559
  };
10184
- const add = key => {
10185
- if (!key) {
9560
+ const add = part => {
9561
+ if (!part) {
10186
9562
  return;
10187
9563
  }
10188
- if (keySet.has(key)) {
9564
+ if (set.has(part)) {
10189
9565
  return;
10190
9566
  }
10191
- keySet.add(key);
10192
- onKeySetChange();
9567
+ set.add(part);
9568
+ onSetChange();
10193
9569
  };
10194
- const remove = key => {
10195
- if (!key) {
9570
+ const remove = part => {
9571
+ if (!part) {
10196
9572
  return;
10197
9573
  }
10198
- if (!keySet.has(key)) {
9574
+ if (!set.has(part)) {
10199
9575
  return;
10200
9576
  }
10201
- keySet.delete(key);
10202
- onKeySetChange();
9577
+ set.delete(part);
9578
+ onSetChange();
10203
9579
  };
10204
9580
  return {
10205
9581
  add,
@@ -10207,42 +9583,51 @@ const UITransition = ({
10207
9583
  remove
10208
9584
  };
10209
9585
  }, []);
10210
- const effectiveContentKey = contentKey || contentKeyFromContext;
10211
9586
  const ref = useRef();
9587
+ const uiTransitionRefDefault = useRef();
9588
+ uiTransitionRef = uiTransitionRef || uiTransitionRefDefault;
10212
9589
  useLayoutEffect(() => {
10213
- const uiTransition = initUITransition(ref.current);
9590
+ if (disabled) {
9591
+ return null;
9592
+ }
9593
+ const uiTransition = createUITransitionController(ref.current);
9594
+ uiTransitionRef.current = uiTransition;
10214
9595
  return () => {
10215
9596
  uiTransition.cleanup();
10216
9597
  };
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,
9598
+ }, [disabled]);
9599
+ if (disabled) {
9600
+ return children;
9601
+ }
9602
+ return jsxs("div", {
9603
+ ref: ref,
9604
+ ...props,
9605
+ className: "ui_transition",
9606
+ "data-transition-type": type,
9607
+ "data-transition-duration": duration,
9608
+ "data-debug-detection": debugDetection ? "" : undefined,
9609
+ "data-debug-size": debugSize ? "" : undefined,
9610
+ "data-debug-content": debugContent ? "" : undefined,
9611
+ children: [jsxs("div", {
9612
+ className: "active_group",
9613
+ children: [jsx("div", {
9614
+ className: "target_slot",
9615
+ "data-content-id": contentIdRef.current ? contentIdRef.current : undefined,
9616
+ children: jsx(UITransitionContentIdContext.Provider, {
9617
+ value: uiTransitionContentIdContextValue,
10238
9618
  children: children
10239
- }), jsx("div", {
10240
- className: "ui_transition_phase_overlay"
10241
- })]
9619
+ })
10242
9620
  }), jsx("div", {
10243
- className: "ui_transition_content_overlay"
9621
+ className: "outgoing_slot"
10244
9622
  })]
10245
- })
9623
+ }), jsxs("div", {
9624
+ className: "previous_group",
9625
+ children: [jsx("div", {
9626
+ className: "previous_target_slot"
9627
+ }), jsx("div", {
9628
+ className: "previous_outgoing_slot"
9629
+ })]
9630
+ })]
10246
9631
  });
10247
9632
  };
10248
9633
 
@@ -10254,28 +9639,28 @@ const UITransition = ({
10254
9639
  * as changed even if the component is still the same
10255
9640
  *
10256
9641
  * 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
9642
+ * When the route becomes inactive it will call useUITransitionContentId(undefined)
9643
+ * And if a sibling route becones active it will call useUITransitionContentId with its own path
10259
9644
  *
10260
9645
  */
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);
9646
+ const useUITransitionContentId = value => {
9647
+ const contentId = useContext(UITransitionContentIdContext);
9648
+ const valueRef = useRef();
9649
+ if (contentId !== undefined && valueRef.current !== value) {
9650
+ const previousValue = valueRef.current;
9651
+ valueRef.current = value;
9652
+ if (previousValue === undefined) {
9653
+ contentId.add(value);
10269
9654
  } else {
10270
- contentKey.add(key);
9655
+ contentId.update(previousValue, value);
10271
9656
  }
10272
9657
  }
10273
9658
  useLayoutEffect(() => {
10274
- if (!contentKey) {
9659
+ if (contentId === undefined) {
10275
9660
  return null;
10276
9661
  }
10277
9662
  return () => {
10278
- contentKey.remove(keyRef.current);
9663
+ contentId.remove(valueRef.current);
10279
9664
  };
10280
9665
  }, []);
10281
9666
  };
@@ -10331,10 +9716,13 @@ const Routes = ({
10331
9716
  });
10332
9717
  };
10333
9718
  const SlotContext = createContext(null);
9719
+ const RouteInfoContext = createContext(null);
9720
+ const useActiveRouteInfo = () => useContext(RouteInfoContext);
10334
9721
  const Route = ({
10335
9722
  element,
10336
9723
  route,
10337
9724
  fallback,
9725
+ meta,
10338
9726
  children
10339
9727
  }) => {
10340
9728
  const forceRender = useForceRender();
@@ -10345,6 +9733,7 @@ const Route = ({
10345
9733
  element: element,
10346
9734
  route: route,
10347
9735
  fallback: fallback,
9736
+ meta: meta,
10348
9737
  onActiveInfoChange: activeInfo => {
10349
9738
  hasDiscoveredRef.current = true;
10350
9739
  activeInfoRef.current = activeInfo;
@@ -10371,18 +9760,23 @@ const ActiveRouteManager = ({
10371
9760
  element,
10372
9761
  route,
10373
9762
  fallback,
9763
+ meta,
10374
9764
  onActiveInfoChange,
10375
9765
  children
10376
9766
  }) => {
9767
+ if (route && fallback) {
9768
+ throw new Error("Route cannot have both route and fallback props");
9769
+ }
10377
9770
  const registerChildRouteFromContext = useContext(RegisterChildRouteContext);
10378
9771
  getElementSignature(element);
10379
9772
  const candidateSet = new Set();
10380
- const registerChildRoute = (ChildActiveElement, childRoute, childFallback) => {
9773
+ const registerChildRoute = (ChildActiveElement, childRoute, childFallback, childMeta) => {
10381
9774
  getElementSignature(ChildActiveElement);
10382
9775
  candidateSet.add({
10383
9776
  ActiveElement: ChildActiveElement,
10384
9777
  route: childRoute,
10385
- fallback: childFallback
9778
+ fallback: childFallback,
9779
+ meta: childMeta
10386
9780
  });
10387
9781
  };
10388
9782
  useLayoutEffect(() => {
@@ -10390,6 +9784,7 @@ const ActiveRouteManager = ({
10390
9784
  element,
10391
9785
  route,
10392
9786
  fallback,
9787
+ meta,
10393
9788
  candidateSet,
10394
9789
  onActiveInfoChange,
10395
9790
  registerChildRouteFromContext
@@ -10404,6 +9799,7 @@ const initRouteObserver = ({
10404
9799
  element,
10405
9800
  route,
10406
9801
  fallback,
9802
+ meta,
10407
9803
  candidateSet,
10408
9804
  onActiveInfoChange,
10409
9805
  registerChildRouteFromContext
@@ -10424,19 +9820,13 @@ const initRouteObserver = ({
10424
9820
  let fallbackInfo = null;
10425
9821
  for (const candidate of candidateSet) {
10426
9822
  if (candidate.route?.active) {
10427
- return {
10428
- ChildActiveElement: candidate.ActiveElement,
10429
- route: candidate.route
10430
- };
9823
+ return candidate;
10431
9824
  }
10432
9825
  // fallback without route can match when no other route matches.
10433
9826
  // This is useful solely for "catch all" fallback used on the <Routes>
10434
9827
  // otherwise a fallback would always match and make the parent route always active
10435
9828
  if (candidate.fallback && !candidate.route.routeFromProps) {
10436
- fallbackInfo = {
10437
- ChildActiveElement: candidate.ActiveElement,
10438
- route: candidate.route
10439
- };
9829
+ fallbackInfo = candidate;
10440
9830
  }
10441
9831
  }
10442
9832
  return fallbackInfo;
@@ -10453,8 +9843,9 @@ const initRouteObserver = ({
10453
9843
  return activeChildInfo;
10454
9844
  }
10455
9845
  return {
10456
- ChildActiveElement: null,
10457
- route
9846
+ ActiveElement: null,
9847
+ route,
9848
+ meta
10458
9849
  };
10459
9850
  } : () => {
10460
9851
  // we don't have a route, do we have an active child?
@@ -10464,22 +9855,22 @@ const initRouteObserver = ({
10464
9855
  }
10465
9856
  return null;
10466
9857
  };
10467
- const activeRouteSignal = signal();
9858
+ const activeRouteInfoSignal = signal();
10468
9859
  const SlotActiveElementSignal = signal();
10469
9860
  const ActiveElement = () => {
10470
- const activeRoute = activeRouteSignal.value;
10471
- useContentKey(activeRoute?.urlPattern);
9861
+ const activeRouteInfo = activeRouteInfoSignal.value;
9862
+ useUITransitionContentId(activeRouteInfo ? activeRouteInfo.route.urlPattern : fallback ? "fallback" : undefined);
10472
9863
  const SlotActiveElement = SlotActiveElementSignal.value;
10473
9864
  if (typeof element === "function") {
10474
9865
  const Element = element;
10475
- return jsx(SlotContext.Provider, {
10476
- value: SlotActiveElement,
10477
- children: jsx(Element, {})
10478
- });
9866
+ element = jsx(Element, {});
10479
9867
  }
10480
- return jsx(SlotContext.Provider, {
10481
- value: SlotActiveElement,
10482
- children: element
9868
+ return jsx(RouteInfoContext.Provider, {
9869
+ value: activeRouteInfo,
9870
+ children: jsx(SlotContext.Provider, {
9871
+ value: SlotActiveElement,
9872
+ children: element
9873
+ })
10483
9874
  });
10484
9875
  };
10485
9876
  ActiveElement.underlyingElementId = candidateSet.size === 0 ? `${getElementSignature(element)} without slot` : `[${getElementSignature(element)} with slot one of ${candidateElementIds}]`;
@@ -10487,20 +9878,17 @@ const initRouteObserver = ({
10487
9878
  const newActiveInfo = getActiveInfo();
10488
9879
  if (newActiveInfo) {
10489
9880
  compositeRoute.active = true;
10490
- const {
10491
- route,
10492
- ChildActiveElement
10493
- } = newActiveInfo;
10494
- activeRouteSignal.value = route;
10495
- SlotActiveElementSignal.value = ChildActiveElement;
9881
+ activeRouteInfoSignal.value = newActiveInfo;
9882
+ SlotActiveElementSignal.value = newActiveInfo.ActiveElement;
10496
9883
  onActiveInfoChange({
10497
- route: newActiveInfo.route,
10498
9884
  ActiveElement,
10499
- SlotActiveElement: ChildActiveElement
9885
+ SlotActiveElement: newActiveInfo.ActiveElement,
9886
+ route: newActiveInfo.route,
9887
+ meta: newActiveInfo.meta
10500
9888
  });
10501
9889
  } else {
10502
9890
  compositeRoute.active = false;
10503
- activeRouteSignal.value = null;
9891
+ activeRouteInfoSignal.value = null;
10504
9892
  SlotActiveElementSignal.value = null;
10505
9893
  onActiveInfoChange(null);
10506
9894
  }
@@ -10516,7 +9904,7 @@ const initRouteObserver = ({
10516
9904
  candidate.route.subscribeStatus(onChange);
10517
9905
  }
10518
9906
  if (registerChildRouteFromContext) {
10519
- registerChildRouteFromContext(ActiveElement, compositeRoute, fallback);
9907
+ registerChildRouteFromContext(ActiveElement, compositeRoute, fallback, meta);
10520
9908
  }
10521
9909
  updateActiveInfo();
10522
9910
  };
@@ -10718,14 +10106,11 @@ const renderActionableComponent = (props, {
10718
10106
 
10719
10107
  const normalizeSpacingStyle = (value, property = "padding") => {
10720
10108
  const cssSize = sizeSpacingScale[value];
10721
- return cssSize || normalizeStyle(value, property, "css");
10109
+ return cssSize || stringifyStyle(value, property);
10722
10110
  };
10723
10111
  const normalizeTypoStyle = (value, property = "fontSize") => {
10724
10112
  const cssSize = sizeTypoScale[value];
10725
- return cssSize || normalizeStyle(value, property, "css");
10726
- };
10727
- const normalizeCssStyle = (value, property) => {
10728
- return normalizeStyle(value, property, "css");
10113
+ return cssSize || stringifyStyle(value, property);
10729
10114
  };
10730
10115
 
10731
10116
  const PASS_THROUGH = { name: "pass_through" };
@@ -10776,6 +10161,13 @@ const applyOnTwoProps = (propA, propB) => {
10776
10161
  };
10777
10162
  };
10778
10163
 
10164
+ const LAYOUT_PROPS = {
10165
+ // all are handled by data-attributes
10166
+ inline: () => {},
10167
+ box: () => {},
10168
+ row: () => {},
10169
+ column: () => {},
10170
+ };
10779
10171
  const OUTER_SPACING_PROPS = {
10780
10172
  margin: PASS_THROUGH,
10781
10173
  marginLeft: PASS_THROUGH,
@@ -10812,21 +10204,21 @@ const DIMENSION_PROPS = {
10812
10204
  return { flexGrow: 1 }; // Grow horizontally in row
10813
10205
  }
10814
10206
  if (parentLayout === "row") {
10815
- return { minWidth: "100%" }; // Take full width in column
10207
+ return { minWidth: "100%", width: "auto" }; // Take full width in column
10816
10208
  }
10817
- return { minWidth: "100%" }; // Take full width outside flex
10209
+ return { minWidth: "100%", width: "auto" }; // Take full width outside flex
10818
10210
  },
10819
10211
  expandY: (value, { parentLayout }) => {
10820
10212
  if (!value) {
10821
10213
  return null;
10822
10214
  }
10823
10215
  if (parentLayout === "column") {
10824
- return { minHeight: "100%" }; // Make column full height
10216
+ return { minHeight: "100%", height: "auto" }; // Make column full height
10825
10217
  }
10826
10218
  if (parentLayout === "row" || parentLayout === "inline-row") {
10827
10219
  return { flexGrow: 1 }; // Make row full height
10828
10220
  }
10829
- return { minHeight: "100%" }; // Take full height outside flex
10221
+ return { minHeight: "100%", height: "auto" }; // Take full height outside flex
10830
10222
  },
10831
10223
  shrinkX: (value, { parentLayout }) => {
10832
10224
  if (!value) {
@@ -10846,6 +10238,23 @@ const DIMENSION_PROPS = {
10846
10238
  }
10847
10239
  return { maxHeight: "100%" };
10848
10240
  },
10241
+
10242
+ scaleX: (value) => {
10243
+ return { transform: `scaleX(${stringifyStyle(value, "scaleX")})` };
10244
+ },
10245
+ scaleY: (value) => {
10246
+ return { transform: `scaleY(${value})` };
10247
+ },
10248
+ scale: (value) => {
10249
+ if (Array.isArray(value)) {
10250
+ const [x, y] = value;
10251
+ return { transform: `scale(${x}, ${y})` };
10252
+ }
10253
+ return { transform: `scale(${value})` };
10254
+ },
10255
+ scaleZ: (value) => {
10256
+ return { transform: `scaleZ(${value})` };
10257
+ },
10849
10258
  };
10850
10259
  const POSITION_PROPS = {
10851
10260
  // For row, alignX uses auto margins for positioning
@@ -10886,7 +10295,7 @@ const POSITION_PROPS = {
10886
10295
 
10887
10296
  if (value === "start") {
10888
10297
  if (inlineColumnLayout) {
10889
- return undefined; // this is the default
10298
+ return { alignSelf: "start" };
10890
10299
  }
10891
10300
  return { marginBottom: "auto" };
10892
10301
  }
@@ -10906,8 +10315,49 @@ const POSITION_PROPS = {
10906
10315
  },
10907
10316
  left: PASS_THROUGH,
10908
10317
  top: PASS_THROUGH,
10318
+
10319
+ translateX: (value) => {
10320
+ return { transform: `translateX(${value})` };
10321
+ },
10322
+ translateY: (value) => {
10323
+ return { transform: `translateY(${value})` };
10324
+ },
10325
+ translate: (value) => {
10326
+ if (Array.isArray(value)) {
10327
+ const [x, y] = value;
10328
+ return { transform: `translate(${x}, ${y})` };
10329
+ }
10330
+ return { transform: `translate(${stringifyStyle(value, "translateX")})` };
10331
+ },
10332
+ rotateX: (value) => {
10333
+ return { transform: `rotateX(${value})` };
10334
+ },
10335
+ rotateY: (value) => {
10336
+ return { transform: `rotateY(${value})` };
10337
+ },
10338
+ rotateZ: (value) => {
10339
+ return { transform: `rotateZ(${value})` };
10340
+ },
10341
+ rotate: (value) => {
10342
+ return { transform: `rotate(${value})` };
10343
+ },
10344
+ skewX: (value) => {
10345
+ return { transform: `skewX(${value})` };
10346
+ },
10347
+ skewY: (value) => {
10348
+ return { transform: `skewY(${value})` };
10349
+ },
10350
+ skew: (value) => {
10351
+ if (Array.isArray(value)) {
10352
+ const [x, y] = value;
10353
+ return { transform: `skew(${x}, ${y})` };
10354
+ }
10355
+ return { transform: `skew(${value})` };
10356
+ },
10909
10357
  };
10910
10358
  const TYPO_PROPS = {
10359
+ font: PASS_THROUGH,
10360
+ fontFamily: PASS_THROUGH,
10911
10361
  size: applyOnCSSProp("fontSize"),
10912
10362
  fontSize: PASS_THROUGH,
10913
10363
  bold: applyToCssPropWhenTruthy("fontWeight", "bold", "normal"),
@@ -10924,6 +10374,11 @@ const TYPO_PROPS = {
10924
10374
  preWrap: applyToCssPropWhenTruthy("whiteSpace", "pre-wrap", "normal"),
10925
10375
  };
10926
10376
  const VISUAL_PROPS = {
10377
+ outline: PASS_THROUGH,
10378
+ outlineStyle: PASS_THROUGH,
10379
+ outlineColor: PASS_THROUGH,
10380
+ outlineWidth: PASS_THROUGH,
10381
+ boxDecorationBreak: PASS_THROUGH,
10927
10382
  boxShadow: PASS_THROUGH,
10928
10383
  background: PASS_THROUGH,
10929
10384
  backgroundColor: PASS_THROUGH,
@@ -11003,6 +10458,7 @@ const CONTENT_PROPS = {
11003
10458
  },
11004
10459
  };
11005
10460
  const All_PROPS = {
10461
+ ...LAYOUT_PROPS,
11006
10462
  ...OUTER_SPACING_PROPS,
11007
10463
  ...INNER_SPACING_PROPS,
11008
10464
  ...DIMENSION_PROPS,
@@ -11011,6 +10467,7 @@ const All_PROPS = {
11011
10467
  ...VISUAL_PROPS,
11012
10468
  ...CONTENT_PROPS,
11013
10469
  };
10470
+ const LAYOUT_PROP_NAME_SET = new Set(Object.keys(LAYOUT_PROPS));
11014
10471
  const OUTER_SPACING_PROP_NAME_SET = new Set(Object.keys(OUTER_SPACING_PROPS));
11015
10472
  const INNER_SPACING_PROP_NAME_SET = new Set(Object.keys(INNER_SPACING_PROPS));
11016
10473
  const DIMENSION_PROP_NAME_SET = new Set(Object.keys(DIMENSION_PROPS));
@@ -11026,6 +10483,7 @@ const HANDLED_BY_VISUAL_CHILD_PROP_SET = new Set([
11026
10483
  ...CONTENT_PROP_NAME_SET,
11027
10484
  ]);
11028
10485
  const COPIED_ON_VISUAL_CHILD_PROP_SET = new Set([
10486
+ ...LAYOUT_PROP_NAME_SET,
11029
10487
  "expand",
11030
10488
  "shrink",
11031
10489
  "expandX",
@@ -11037,6 +10495,9 @@ const COPIED_ON_VISUAL_CHILD_PROP_SET = new Set([
11037
10495
  const isStyleProp = (name) => STYLE_PROP_NAME_SET.has(name);
11038
10496
 
11039
10497
  const getStylePropGroup = (name) => {
10498
+ if (LAYOUT_PROP_NAME_SET.has(name)) {
10499
+ return "layout";
10500
+ }
11040
10501
  if (OUTER_SPACING_PROP_NAME_SET.has(name)) {
11041
10502
  return "margin";
11042
10503
  }
@@ -11068,7 +10529,7 @@ const getNormalizer = (key) => {
11068
10529
  if (group === "typo") {
11069
10530
  return normalizeTypoStyle;
11070
10531
  }
11071
- return normalizeCssStyle;
10532
+ return stringifyStyle;
11072
10533
  };
11073
10534
 
11074
10535
  const assignStyle = (
@@ -11076,7 +10537,7 @@ const assignStyle = (
11076
10537
  propValue,
11077
10538
  propName,
11078
10539
  styleContext,
11079
- normalizer = getNormalizer(propName),
10540
+ context = "js",
11080
10541
  ) => {
11081
10542
  if (propValue === undefined) {
11082
10543
  return;
@@ -11085,6 +10546,7 @@ const assignStyle = (
11085
10546
  if (!managedByCSSVars) {
11086
10547
  throw new Error("managedByCSSVars is required in styleContext");
11087
10548
  }
10549
+ const normalizer = getNormalizer(propName);
11088
10550
  const getStyle = All_PROPS[propName];
11089
10551
  if (
11090
10552
  getStyle === PASS_THROUGH ||
@@ -11093,10 +10555,16 @@ const assignStyle = (
11093
10555
  ) {
11094
10556
  const cssValue = normalizer(propValue, propName);
11095
10557
  const cssVar = managedByCSSVars[propName];
10558
+ const mergedValue = mergeOneStyle(
10559
+ styleObject[propName],
10560
+ cssValue,
10561
+ propName,
10562
+ context,
10563
+ );
11096
10564
  if (cssVar) {
11097
- styleObject[cssVar] = cssValue;
10565
+ styleObject[cssVar] = mergedValue;
11098
10566
  } else {
11099
- styleObject[propName] = cssValue;
10567
+ styleObject[propName] = mergedValue;
11100
10568
  }
11101
10569
  return;
11102
10570
  }
@@ -11108,10 +10576,11 @@ const assignStyle = (
11108
10576
  const value = values[key];
11109
10577
  const cssValue = normalizer(value, key);
11110
10578
  const cssVar = managedByCSSVars[key];
10579
+ const mergedValue = mergeOneStyle(styleObject[key], cssValue, key, context);
11111
10580
  if (cssVar) {
11112
- styleObject[cssVar] = cssValue;
10581
+ styleObject[cssVar] = mergedValue;
11113
10582
  } else {
11114
- styleObject[key] = cssValue;
10583
+ styleObject[key] = mergedValue;
11115
10584
  }
11116
10585
  }
11117
10586
  };
@@ -11128,12 +10597,8 @@ const sizeSpacingScale = {
11128
10597
  xl: "2em", // 2 = 32px at 16px base
11129
10598
  xxl: "3em", // 3 = 48px at 16px base
11130
10599
  };
11131
- const resolveSpacingSize = (
11132
- size,
11133
- property = "padding",
11134
- context = "css",
11135
- ) => {
11136
- return normalizeStyle(sizeSpacingScale[size] || size, property, context);
10600
+ const resolveSpacingSize = (size, property = "padding") => {
10601
+ return stringifyStyle(sizeSpacingScale[size] || size, property);
11137
10602
  };
11138
10603
 
11139
10604
  const sizeTypoScale = {
@@ -11511,7 +10976,7 @@ const initPseudoStyles = (
11511
10976
  }
11512
10977
  currentState[pseudoClass] = currentValue;
11513
10978
  const oldValue = state ? state[pseudoClass] : undefined;
11514
- if (oldValue !== currentValue) {
10979
+ if (oldValue !== currentValue || !state) {
11515
10980
  someChange = true;
11516
10981
  const { attribute, add, remove } = pseudoClassDefinition;
11517
10982
  if (currentValue) {
@@ -11566,8 +11031,15 @@ const applyStyle = (element, style, pseudoState, pseudoNamedStyles) => {
11566
11031
  updateStyle(element, getStyleToApply(style, pseudoState, pseudoNamedStyles));
11567
11032
  };
11568
11033
 
11034
+ const PSEUDO_STATE_DEFAULT = {};
11035
+ const PSEUDO_NAMED_STYLES_DEFAULT = {};
11569
11036
  const getStyleToApply = (styles, pseudoState, pseudoNamedStyles) => {
11570
- if (!pseudoState || !pseudoNamedStyles) {
11037
+ if (
11038
+ !pseudoState ||
11039
+ pseudoState === PSEUDO_STATE_DEFAULT ||
11040
+ !pseudoNamedStyles ||
11041
+ pseudoNamedStyles === PSEUDO_NAMED_STYLES_DEFAULT
11042
+ ) {
11571
11043
  return styles;
11572
11044
  }
11573
11045
 
@@ -11634,7 +11106,13 @@ const updateStyle = (element, style) => {
11634
11106
  }
11635
11107
  }
11636
11108
  for (const toDeleteKey of toDeleteKeySet) {
11637
- element.style.removeProperty(toDeleteKey);
11109
+ if (toDeleteKey.startsWith("--")) {
11110
+ element.style.removeProperty(toDeleteKey);
11111
+ } else {
11112
+ // we can't use removeProperty because "toDeleteKey" is in camelCase
11113
+ // e.g., backgroundColor (and it's safer to just let the browser do the conversion)
11114
+ element.style[toDeleteKey] = "";
11115
+ }
11638
11116
  }
11639
11117
  styleKeySetWeakMap.set(element, styleKeySet);
11640
11118
  return;
@@ -11693,8 +11171,10 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
11693
11171
  flex-direction: row;
11694
11172
  }
11695
11173
 
11696
- [data-layout-row] > *,
11697
- [data-layout-column] > * {
11174
+ [data-layout-row] > [data-layout-row],
11175
+ [data-layout-row] > [data-layout-column],
11176
+ [data-layout-column] > [data-layout-column],
11177
+ [data-layout-column] > [data-layout-row] {
11698
11178
  flex-shrink: 0;
11699
11179
  }
11700
11180
 
@@ -11709,9 +11189,6 @@ const MANAGED_BY_CSS_VARS_DEFAULT = {};
11709
11189
  const Box = props => {
11710
11190
  const {
11711
11191
  as = "div",
11712
- layoutRow,
11713
- layoutColumn,
11714
- layoutInline,
11715
11192
  baseClassName,
11716
11193
  className,
11717
11194
  baseStyle,
@@ -11741,30 +11218,78 @@ const Box = props => {
11741
11218
  const defaultRef = useRef();
11742
11219
  const ref = props.ref || defaultRef;
11743
11220
  const TagName = as;
11221
+ const {
11222
+ box,
11223
+ inline = box,
11224
+ row,
11225
+ column = box
11226
+ } = rest;
11744
11227
  let layout;
11745
- if (layoutInline) {
11746
- if (layoutRow) {
11228
+ if (inline) {
11229
+ if (row) {
11747
11230
  layout = "inline-row";
11748
- } else if (layoutColumn) {
11231
+ } else if (column) {
11749
11232
  layout = "inline-column";
11750
11233
  } else {
11751
11234
  layout = "inline";
11752
11235
  }
11753
- } else if (layoutRow) {
11236
+ } else if (row) {
11754
11237
  layout = "row";
11755
- } else if (layoutColumn) {
11238
+ } else if (column) {
11756
11239
  layout = "column";
11757
11240
  } else {
11758
11241
  layout = getDefaultDisplay(TagName);
11759
11242
  }
11760
11243
  const innerClassName = withPropsClassName(baseClassName, className);
11761
11244
  const remainingProps = {};
11245
+ const propsToForward = {};
11246
+ const shouldForwardAllToChild = visualSelector && pseudoStateSelector;
11762
11247
  {
11763
11248
  const parentLayout = useContext(BoxLayoutContext);
11764
- const innerPseudoState = basePseudoState && pseudoState ? {
11765
- ...basePseudoState,
11766
- ...pseudoState
11767
- } : basePseudoState;
11249
+ const styleDeps = [
11250
+ // Layout and alignment props
11251
+ parentLayout, layout,
11252
+ // Style context dependencies
11253
+ managedByCSSVars, pseudoClasses, pseudoElements,
11254
+ // Selectors
11255
+ visualSelector, pseudoStateSelector];
11256
+ let innerPseudoState;
11257
+ if (basePseudoState && pseudoState) {
11258
+ innerPseudoState = {};
11259
+ const baseStateKeys = Object.keys(basePseudoState);
11260
+ const pseudoStateKeySet = new Set(Object.keys(pseudoState));
11261
+ for (const key of baseStateKeys) {
11262
+ if (pseudoStateKeySet.has(key)) {
11263
+ pseudoStateKeySet.delete(key);
11264
+ const value = pseudoState[key];
11265
+ styleDeps.push(value);
11266
+ innerPseudoState[key] = value;
11267
+ } else {
11268
+ const value = basePseudoState[key];
11269
+ styleDeps.push(value);
11270
+ innerPseudoState[key] = value;
11271
+ }
11272
+ }
11273
+ for (const key of pseudoStateKeySet) {
11274
+ const value = pseudoState[key];
11275
+ styleDeps.push(value);
11276
+ innerPseudoState[key] = value;
11277
+ }
11278
+ } else if (basePseudoState) {
11279
+ innerPseudoState = basePseudoState;
11280
+ for (const key of Object.keys(basePseudoState)) {
11281
+ const value = basePseudoState[key];
11282
+ styleDeps.push(value);
11283
+ }
11284
+ } else if (pseudoState) {
11285
+ innerPseudoState = pseudoState;
11286
+ for (const key of Object.keys(pseudoState)) {
11287
+ const value = pseudoState[key];
11288
+ styleDeps.push(value);
11289
+ }
11290
+ } else {
11291
+ innerPseudoState = PSEUDO_STATE_DEFAULT;
11292
+ }
11768
11293
  const styleContext = {
11769
11294
  parentLayout,
11770
11295
  layout,
@@ -11773,13 +11298,6 @@ const Box = props => {
11773
11298
  pseudoClasses,
11774
11299
  pseudoElements
11775
11300
  };
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
11301
  const boxStyles = {};
11784
11302
  if (baseStyle) {
11785
11303
  for (const key of baseStyle) {
@@ -11789,32 +11307,43 @@ const Box = props => {
11789
11307
  }
11790
11308
  }
11791
11309
  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
11310
  const getPropEffect = propName => {
11809
11311
  if (visualSelector) {
11810
11312
  if (HANDLED_BY_VISUAL_CHILD_PROP_SET.has(propName)) {
11811
11313
  return "forward";
11812
11314
  }
11813
- if (COPIED_ON_VISUAL_CHILD_PROP_SET.has(propName)) {
11814
- return "style_and_forward";
11315
+ if (COPIED_ON_VISUAL_CHILD_PROP_SET.has(propName)) {
11316
+ return "style_and_forward";
11317
+ }
11318
+ }
11319
+ if (isStyleProp(propName)) {
11320
+ return "style";
11321
+ }
11322
+ if (propName.startsWith("data-")) {
11323
+ return "use";
11324
+ }
11325
+ return "forward";
11326
+ };
11327
+ const assignStyleFromProp = (propValue, propName, stylesTarget, styleContext) => {
11328
+ const propEffect = getPropEffect(propName);
11329
+ if (propEffect === "ignore") {
11330
+ return;
11331
+ }
11332
+ const useToStyle = propEffect === "style" || propEffect === "style_and_forward";
11333
+ const shouldForward = propEffect === "forward" || propEffect === "style_and_forward";
11334
+ if (useToStyle) {
11335
+ styleDeps.push(propValue);
11336
+ assignStyle(stylesTarget, propValue, propName, styleContext, "css");
11337
+ }
11338
+ if (stylesTarget === boxStyles) {
11339
+ if (!shouldForwardAllToChild && !useToStyle) {
11340
+ // we'll put these props on ourselves
11341
+ remainingProps[propName] = propValue;
11342
+ }
11343
+ if (shouldForward) {
11344
+ propsToForward[propName] = propValue;
11815
11345
  }
11816
11346
  }
11817
- return isStyleProp(propName) ? "style" : "forward";
11818
11347
  };
11819
11348
  for (const key of stylingKeyCandidateArray) {
11820
11349
  if (key === "ref") {
@@ -11825,46 +11354,49 @@ const Box = props => {
11825
11354
  const value = rest[key];
11826
11355
  assignStyleFromProp(value, key, boxStyles, styleContext);
11827
11356
  }
11828
- const pseudoNamedStyles = {};
11357
+ let pseudoNamedStyles = PSEUDO_NAMED_STYLES_DEFAULT;
11829
11358
  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
- };
11359
+ const pseudoStyleKeys = Object.keys(pseudoStyle);
11360
+ if (pseudoStyleKeys.length) {
11361
+ pseudoNamedStyles = {};
11362
+ for (const key of pseudoStyleKeys) {
11363
+ const pseudoStyleContext = {
11364
+ ...styleContext,
11365
+ managedByCSSVars: {
11366
+ ...managedByCSSVars,
11367
+ ...managedByCSSVars[key]
11368
+ },
11369
+ pseudoName: key
11370
+ };
11839
11371
 
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);
11372
+ // pseudo class
11373
+ if (key.startsWith(":")) {
11374
+ styleDeps.push(key);
11375
+ const pseudoClassStyles = {};
11376
+ const pseudoClassStyle = pseudoStyle[key];
11377
+ for (const pseudoClassStyleKey of Object.keys(pseudoClassStyle)) {
11378
+ const pseudoClassStyleValue = pseudoClassStyle[pseudoClassStyleKey];
11379
+ assignStyleFromProp(pseudoClassStyleValue, pseudoClassStyleKey, pseudoClassStyles, pseudoStyleContext);
11380
+ }
11381
+ pseudoNamedStyles[key] = pseudoClassStyles;
11382
+ continue;
11848
11383
  }
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);
11384
+ // pseudo element
11385
+ if (key.startsWith("::")) {
11386
+ styleDeps.push(key);
11387
+ const pseudoElementStyles = {};
11388
+ const pseudoElementStyle = pseudoStyle[key];
11389
+ for (const pseudoElementStyleKey of Object.keys(pseudoElementStyle)) {
11390
+ const pseudoElementStyleValue = pseudoElementStyle[pseudoElementStyleKey];
11391
+ assignStyleFromProp(pseudoElementStyleValue, pseudoElementStyleKey, pseudoElementStyles, pseudoStyleContext);
11392
+ }
11393
+ pseudoNamedStyles[key] = pseudoElementStyles;
11394
+ continue;
11860
11395
  }
11861
- pseudoNamedStyles[key] = pseudoElementStyles;
11862
- continue;
11396
+ console.warn(`unsupported pseudo style key "${key}"`);
11863
11397
  }
11864
- console.warn(`unsupported pseudo style key "${key}"`);
11865
11398
  }
11866
11399
  remainingProps.pseudoStyle = pseudoStyle;
11867
- // TODO: we should also pass pseudoState right?
11868
11400
  }
11869
11401
  if (typeof style === "string") {
11870
11402
  appendStyles(boxStyles, normalizeStyles(style, "css"), "css");
@@ -11872,7 +11404,7 @@ const Box = props => {
11872
11404
  } else if (style && typeof style === "object") {
11873
11405
  for (const key of Object.keys(style)) {
11874
11406
  const stylePropValue = style[key];
11875
- assignStyle(boxStyles, stylePropValue, key, styleContext);
11407
+ assignStyle(boxStyles, stylePropValue, key, styleContext, "css");
11876
11408
  styleDeps.push(stylePropValue); // impact box style -> add to deps
11877
11409
  }
11878
11410
  }
@@ -11924,23 +11456,22 @@ const Box = props => {
11924
11456
  let innerChildren;
11925
11457
  if (hasChildFunction) {
11926
11458
  if (Array.isArray(children)) {
11927
- innerChildren = children.map(child => typeof child === "function" ? child(remainingProps) : child);
11459
+ innerChildren = children.map(child => typeof child === "function" ? child(propsToForward) : child);
11928
11460
  } else if (typeof children === "function") {
11929
- innerChildren = children(remainingProps);
11461
+ innerChildren = children(propsToForward);
11930
11462
  } else {
11931
11463
  innerChildren = children;
11932
11464
  }
11933
11465
  } else {
11934
11466
  innerChildren = children;
11935
11467
  }
11936
- const shouldForwardAllToChild = visualSelector && pseudoStateSelector;
11937
11468
  return jsx(TagName, {
11938
11469
  ref: ref,
11939
11470
  className: innerClassName,
11940
- "data-layout-inline": layoutInline ? "" : undefined,
11941
- "data-layout-row": layoutRow ? "" : undefined,
11942
- "data-layout-column": layoutColumn ? "" : undefined,
11943
- ...(shouldForwardAllToChild ? undefined : remainingProps),
11471
+ "data-layout-inline": inline ? "" : undefined,
11472
+ "data-layout-row": row ? "" : undefined,
11473
+ "data-layout-column": column ? "" : undefined,
11474
+ ...remainingProps,
11944
11475
  children: jsx(BoxLayoutContext.Provider, {
11945
11476
  value: layout,
11946
11477
  children: innerChildren
@@ -11948,25 +11479,8 @@ const Box = props => {
11948
11479
  });
11949
11480
  };
11950
11481
  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
- }
11968
11482
  return jsx(Box, {
11969
- ...rest
11483
+ ...props
11970
11484
  });
11971
11485
  };
11972
11486
 
@@ -13907,31 +13421,6 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
13907
13421
  color: inherit;
13908
13422
  }
13909
13423
 
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
13424
  .navi_text_overflow_wrapper {
13936
13425
  display: flex;
13937
13426
  width: 0;
@@ -14016,91 +13505,18 @@ const TextOverflowPinned = ({
14016
13505
  };
14017
13506
  const TextBasic = ({
14018
13507
  as = "span",
14019
- foregroundColor,
14020
- foregroundElement,
14021
13508
  contentSpacing = " ",
14022
- box,
14023
13509
  children,
14024
13510
  ...rest
14025
13511
  }) => {
14026
- const hasForeground = Boolean(foregroundElement || foregroundColor);
14027
- const text = jsxs(Box, {
13512
+ const text = jsx(Box, {
14028
13513
  ...rest,
14029
13514
  baseClassName: "navi_text",
14030
13515
  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
- })]
13516
+ children: applyContentSpacingOnTextChildren(children, contentSpacing)
14041
13517
  });
14042
13518
  return text;
14043
13519
  };
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
13520
  const Paragraph = ({
14105
13521
  contentSpacing = " ",
14106
13522
  marginTop = "md",
@@ -14163,6 +13579,114 @@ const applyContentSpacingOnTextChildren = (children, contentSpacing) => {
14163
13579
  return childrenWithGap;
14164
13580
  };
14165
13581
 
13582
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
13583
+ .navi_icon {
13584
+ display: inline-block;
13585
+ box-sizing: border-box;
13586
+ }
13587
+
13588
+ .navi_icon_char_slot {
13589
+ opacity: 0;
13590
+ }
13591
+ .navi_icon_foreground {
13592
+ position: absolute;
13593
+ inset: 0;
13594
+ display: inline-flex;
13595
+ box-sizing: border-box;
13596
+ align-items: center;
13597
+ justify-content: start;
13598
+ }
13599
+ .navi_icon_foreground > .navi_text {
13600
+ display: flex;
13601
+ aspect-ratio: 1 / 1;
13602
+ height: 100%;
13603
+ max-height: 1em;
13604
+ align-items: center;
13605
+ justify-content: center;
13606
+ }
13607
+
13608
+ .navi_icon > svg,
13609
+ .navi_icon > img {
13610
+ width: 100%;
13611
+ height: 100%;
13612
+ }
13613
+ .navi_icon[data-width] > svg,
13614
+ .navi_icon[data-width] > img {
13615
+ width: 100%;
13616
+ height: auto;
13617
+ }
13618
+ .navi_icon[data-height] > svg,
13619
+ .navi_icon[data-height] > img {
13620
+ width: auto;
13621
+ height: 100%;
13622
+ }
13623
+ `;
13624
+ const Icon = ({
13625
+ href,
13626
+ children,
13627
+ className,
13628
+ charWidth = 1,
13629
+ // 0 (zéro) is the real char width
13630
+ // but 2 zéros gives too big icons
13631
+ // while 1 "W" gives a nice result
13632
+ baseChar = "W",
13633
+ "aria-label": ariaLabel,
13634
+ role,
13635
+ decorative = false,
13636
+ ...props
13637
+ }) => {
13638
+ const innerChildren = href ? jsx("svg", {
13639
+ width: "100%",
13640
+ height: "100%",
13641
+ children: jsx("use", {
13642
+ href: href
13643
+ })
13644
+ }) : children;
13645
+ let {
13646
+ box,
13647
+ width,
13648
+ height
13649
+ } = props;
13650
+ if (width !== undefined || height !== undefined) {
13651
+ box = true;
13652
+ }
13653
+ if (box) {
13654
+ return jsx(Box, {
13655
+ ...props,
13656
+ baseClassName: "navi_icon",
13657
+ "data-width": width,
13658
+ "data-height": height,
13659
+ children: innerChildren
13660
+ });
13661
+ }
13662
+ const invisibleText = baseChar.repeat(charWidth);
13663
+ const ariaProps = decorative ? {
13664
+ "aria-hidden": "true"
13665
+ } : {
13666
+ role,
13667
+ "aria-label": ariaLabel
13668
+ };
13669
+ return jsxs(Text, {
13670
+ ...props,
13671
+ ...ariaProps,
13672
+ box: box,
13673
+ className: withPropsClassName("navi_icon", className),
13674
+ "data-icon-char": "",
13675
+ "data-width": width,
13676
+ "data-height": height,
13677
+ children: [jsx("span", {
13678
+ className: "navi_icon_char_slot",
13679
+ "aria-hidden": "true",
13680
+ children: invisibleText
13681
+ }), jsx("span", {
13682
+ className: "navi_icon_foreground",
13683
+ children: jsx(Text, {
13684
+ children: innerChildren
13685
+ })
13686
+ })]
13687
+ });
13688
+ };
13689
+
14166
13690
  installImportMetaCss(import.meta);import.meta.css = /* css */`
14167
13691
  @layer navi {
14168
13692
  .navi_link {
@@ -14172,7 +13696,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
14172
13696
  --color-visited: light-dark(#6a1b9a, #ab47bc);
14173
13697
  --color-active: red;
14174
13698
  --text-decoration: underline;
14175
- --text-decoration-hover: underline;
13699
+ --text-decoration-hover: var(--text-decoration);
14176
13700
  --cursor: pointer;
14177
13701
  }
14178
13702
  }
@@ -14183,7 +13707,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
14183
13707
  --x-color-visited: var(--color-visited);
14184
13708
  --x-color-active: var(--color-active);
14185
13709
  --x-text-decoration: var(--text-decoration);
14186
- --x-text-decoration-hover: var(--text-decoration-hover,);
13710
+ --x-text-decoration-hover: var(--text-decoration-hover);
14187
13711
  --x-cursor: var(--cursor);
14188
13712
 
14189
13713
  position: relative;
@@ -14301,7 +13825,6 @@ const LinkPlain = props => {
14301
13825
  rel,
14302
13826
  preventDefault,
14303
13827
  // visual
14304
- box,
14305
13828
  blankTargetIcon,
14306
13829
  anchorIcon,
14307
13830
  icon,
@@ -14361,8 +13884,6 @@ const LinkPlain = props => {
14361
13884
  // Visual
14362
13885
  ,
14363
13886
  baseClassName: "navi_link",
14364
- layoutInline: true,
14365
- layoutColumn: box ? true : undefined,
14366
13887
  managedByCSSVars: LinkManagedByCSSVars,
14367
13888
  pseudoClasses: LinkPseudoClasses,
14368
13889
  pseudoElements: LinkPseudoElements,
@@ -14408,8 +13929,6 @@ const LinkPlain = props => {
14408
13929
  const BlankTargetLinkSvg = () => {
14409
13930
  return jsx("svg", {
14410
13931
  viewBox: "0 0 24 24",
14411
- width: "100%",
14412
- height: "100%",
14413
13932
  xmlns: "http://www.w3.org/2000/svg",
14414
13933
  children: jsx("path", {
14415
13934
  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 +13943,6 @@ const BlankTargetLinkSvg = () => {
14424
13943
  const AnchorLinkSvg = () => {
14425
13944
  return jsxs("svg", {
14426
13945
  viewBox: "0 0 24 24",
14427
- width: "100%",
14428
- height: "100%",
14429
13946
  xmlns: "http://www.w3.org/2000/svg",
14430
13947
  children: [jsx("path", {
14431
13948
  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 +13956,6 @@ const AnchorLinkSvg = () => {
14439
13956
  const PhoneSvg = () => {
14440
13957
  return jsx("svg", {
14441
13958
  viewBox: "0 0 24 24",
14442
- width: "100%",
14443
- height: "100%",
14444
13959
  xmlns: "http://www.w3.org/2000/svg",
14445
13960
  children: jsx("path", {
14446
13961
  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 +13966,6 @@ const PhoneSvg = () => {
14451
13966
  const SmsSvg = () => {
14452
13967
  return jsx("svg", {
14453
13968
  viewBox: "0 0 24 24",
14454
- width: "100%",
14455
- height: "100%",
14456
13969
  xmlns: "http://www.w3.org/2000/svg",
14457
13970
  children: jsx("path", {
14458
13971
  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 +13976,6 @@ const SmsSvg = () => {
14463
13976
  const EmailSvg = () => {
14464
13977
  return jsxs("svg", {
14465
13978
  viewBox: "0 0 24 24",
14466
- width: "100%",
14467
- height: "100%",
14468
13979
  xmlns: "http://www.w3.org/2000/svg",
14469
13980
  children: [jsx("path", {
14470
13981
  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 +13995,6 @@ const EmailSvg = () => {
14484
13995
  const GithubSvg = () => {
14485
13996
  return jsx("svg", {
14486
13997
  viewBox: "0 0 24 24",
14487
- width: "100%",
14488
- height: "100%",
14489
13998
  xmlns: "http://www.w3.org/2000/svg",
14490
13999
  children: jsx("path", {
14491
14000
  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 +16565,10 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
17056
16565
  --x-color: var(--color);
17057
16566
 
17058
16567
  position: relative;
16568
+ display: inline-flex;
17059
16569
  box-sizing: border-box;
17060
- width: fit-content;
17061
- height: fit-content;
17062
16570
  padding: 0;
17063
16571
  flex-direction: inherit;
17064
- align-items: inherit;
17065
- justify-content: inherit;
17066
16572
  background: none;
17067
16573
  border: none;
17068
16574
  border-radius: inherit;
@@ -17071,6 +16577,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
17071
16577
  }
17072
16578
  .navi_button_content {
17073
16579
  position: relative;
16580
+ box-sizing: border-box;
17074
16581
  padding-top: var(--padding-top, var(--padding-y, var(--padding, 1px)));
17075
16582
  padding-right: var(--padding-right, var(--padding-x, var(--padding, 6px)));
17076
16583
  padding-bottom: var(
@@ -17241,7 +16748,6 @@ const ButtonBasic = props => {
17241
16748
  ...buttonProps,
17242
16749
  as: "span",
17243
16750
  baseClassName: "navi_button_content",
17244
- layoutInline: true,
17245
16751
  children: [innerChildren, jsx("span", {
17246
16752
  className: "navi_button_shadow"
17247
16753
  })]
@@ -17259,8 +16765,6 @@ const ButtonBasic = props => {
17259
16765
  // style management
17260
16766
  ,
17261
16767
  baseClassName: "navi_button",
17262
- layoutInline: true,
17263
- layoutColumn: true,
17264
16768
  managedByCSSVars: ButtonManagedByCSSVars,
17265
16769
  pseudoClasses: ButtonPseudoClasses,
17266
16770
  pseudoElements: ButtonPseudoElements,
@@ -21811,29 +21315,97 @@ const SVGMaskOverlay = ({
21811
21315
  });
21812
21316
  };
21813
21317
 
21318
+ const CSS_VAR_NAME = "--color-contrasting";
21319
+
21320
+ const useContrastingColor = (ref) => {
21321
+ useLayoutEffect(() => {
21322
+ const el = ref.current;
21323
+ if (!el) {
21324
+ return;
21325
+ }
21326
+ const lightColor = "var(--navi-color-light)";
21327
+ const darkColor = "var(--navi-color-dark)";
21328
+ const backgroundColor = getComputedStyle(el).backgroundColor;
21329
+ if (!backgroundColor) {
21330
+ el.style.removeProperty(CSS_VAR_NAME);
21331
+ return;
21332
+ }
21333
+ const colorPicked = pickLightOrDark(
21334
+ backgroundColor,
21335
+ lightColor,
21336
+ darkColor,
21337
+ el,
21338
+ );
21339
+ el.style.setProperty(CSS_VAR_NAME, colorPicked);
21340
+ }, []);
21341
+ };
21342
+
21814
21343
  installImportMetaCss(import.meta);import.meta.css = /* css */`
21815
- .navi_count {
21816
- position: relative;
21817
- top: -1px;
21818
- color: rgba(28, 43, 52, 0.4);
21344
+ @layer navi {
21345
+ .navi_badge {
21346
+ --border-radius: 1em;
21347
+ }
21348
+ }
21349
+ .navi_badge {
21350
+ display: inline-block;
21351
+ box-sizing: border-box;
21352
+ min-width: 1.5em;
21353
+ height: 1.5em;
21354
+ max-height: 1.5em;
21355
+ padding-right: var(
21356
+ --padding-right,
21357
+ var(--padding-x, var(--padding, 0.4em))
21358
+ );
21359
+ padding-left: var(--padding-left, var(--padding-x, var(--padding, 0.4em)));
21360
+ color: var(--color, var(--color-contrasting));
21361
+ text-align: center;
21362
+ line-height: 1.5em;
21363
+ vertical-align: middle;
21364
+ border-radius: var(--border-radius, 1em);
21819
21365
  }
21820
21366
  `;
21821
- const Count = ({
21367
+ const BadgeManagedByCSSVars = {
21368
+ borderWidth: "--border-width",
21369
+ borderRadius: "--border-radius",
21370
+ paddingRight: "--padding-right",
21371
+ paddingLeft: "--padding-left",
21372
+ backgroundColor: "--background-color",
21373
+ borderColor: "--border-color",
21374
+ color: "--color"
21375
+ };
21376
+ const BadgeCount = ({
21822
21377
  children,
21823
- ...rest
21378
+ bold = true,
21379
+ max,
21380
+ ...props
21824
21381
  }) => {
21825
- return jsxs(Box, {
21826
- as: "span",
21827
- baseClassName: ".navi_count",
21828
- ...rest,
21829
- children: ["(", children, ")"]
21830
- });
21831
- };
21382
+ const defaultRef = useRef();
21383
+ const ref = props.ref || defaultRef;
21832
21384
 
21833
- const Image = props => {
21834
- return jsx(Box, {
21385
+ // Calculer la valeur à afficher en fonction du paramètre max
21386
+ const getDisplayValue = () => {
21387
+ if (max === undefined) {
21388
+ return children;
21389
+ }
21390
+ const numericValue = typeof children === "string" ? parseInt(children, 10) : children;
21391
+ const numericMax = typeof max === "string" ? parseInt(max, 10) : max;
21392
+ if (isNaN(numericValue) || isNaN(numericMax)) {
21393
+ return children;
21394
+ }
21395
+ if (numericValue > numericMax) {
21396
+ return `${numericMax}+`;
21397
+ }
21398
+ return children;
21399
+ };
21400
+ const displayValue = getDisplayValue();
21401
+ useContrastingColor(ref);
21402
+ return jsx(Text, {
21835
21403
  ...props,
21836
- as: "img"
21404
+ ref: ref,
21405
+ className: "navi_badge",
21406
+ bold: bold,
21407
+ managedByCSSVars: BadgeManagedByCSSVars,
21408
+ children: displayValue
21837
21409
  });
21838
21410
  };
21839
21411
 
@@ -21849,6 +21421,13 @@ const Code = ({
21849
21421
  });
21850
21422
  };
21851
21423
 
21424
+ const Image = props => {
21425
+ return jsx(Box, {
21426
+ ...props,
21427
+ as: "img"
21428
+ });
21429
+ };
21430
+
21852
21431
  const LinkWithIcon = () => {};
21853
21432
 
21854
21433
  installImportMetaCss(import.meta);import.meta.css = /* css */`
@@ -22086,5 +21665,5 @@ const useDependenciesDiff = (inputs) => {
22086
21665
  return diffRef.current;
22087
21666
  };
22088
21667
 
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 };
21668
+ export { ActionRenderer, ActiveKeyboardShortcuts, BadgeCount, Box, Button, Checkbox, CheckboxList, Code, Col, Colgroup, 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, useActiveRouteInfo, useCellsAndColumns, useDependenciesDiff, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, useUrlSearchParam, valueInLocalStorage };
22090
21669
  //# sourceMappingURL=jsenv_navi.js.map