@jsenv/navi 0.21.9 → 0.22.0

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.
@@ -3,7 +3,7 @@ import { isValidElement, h, createContext, toChildArray, render, createRef, clon
3
3
  import { useErrorBoundary, useLayoutEffect, useEffect, useMemo, useRef, useState, useCallback, useContext, useImperativeHandle, useId } from "preact/hooks";
4
4
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
5
5
  import { signal, effect, computed, batch, useSignal } from "@preact/signals";
6
- import { createIterableWeakSet, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, createStyleController, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, hasCSSSizeUnit, resolveCSSSize, activeElementSignal, canInterceptKeys, contrastColor, initFocusGroup, elementIsFocusable, resolveColorLuminance, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
6
+ import { createIterableWeakSet, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, createStyleController, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, resolveCSSSize, activeElementSignal, canInterceptKeys, hasCSSSizeUnit, contrastColor, initFocusGroup, elementIsFocusable, resolveColorLuminance, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
7
  export { contrastColor } from "@jsenv/dom";
8
8
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
9
9
  import { createValidity } from "@jsenv/validity";
@@ -18734,2036 +18734,2174 @@ const useConstraints = (elementRef, props, { targetSelector } = {}) => {
18734
18734
  return remainingProps;
18735
18735
  };
18736
18736
 
18737
- const useInitialTextSelection = (ref, textSelection) => {
18738
- const deps = [];
18739
- if (Array.isArray(textSelection)) {
18740
- deps.push(...textSelection);
18741
- } else {
18742
- deps.push(textSelection);
18743
- }
18737
+ const EmailSvg = () => {
18738
+ return jsxs("svg", {
18739
+ viewBox: "0 0 24 24",
18740
+ xmlns: "http://www.w3.org/2000/svg",
18741
+ children: [jsx("path", {
18742
+ 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",
18743
+ fill: "none",
18744
+ stroke: "currentColor",
18745
+ "stroke-width": "2"
18746
+ }), jsx("path", {
18747
+ d: "m2 6 8 5 2 1.5 2-1.5 8-5",
18748
+ fill: "none",
18749
+ stroke: "currentColor",
18750
+ "stroke-width": "2",
18751
+ "stroke-linecap": "round",
18752
+ "stroke-linejoin": "round"
18753
+ })]
18754
+ });
18755
+ };
18756
+
18757
+ const LinkBlankTargetSvg = () => {
18758
+ return jsx("svg", {
18759
+ viewBox: "0 0 24 24",
18760
+ xmlns: "http://www.w3.org/2000/svg",
18761
+ children: jsx("path", {
18762
+ 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",
18763
+ stroke: "currentColor",
18764
+ fill: "none",
18765
+ "stroke-width": "2",
18766
+ "stroke-linecap": "round",
18767
+ "stroke-linejoin": "round"
18768
+ })
18769
+ });
18770
+ };
18771
+ const LinkAnchorSvg = () => {
18772
+ return jsx("svg", {
18773
+ viewBox: "0 0 24 24",
18774
+ xmlns: "http://www.w3.org/2000/svg",
18775
+ children: jsxs("g", {
18776
+ children: [jsx("path", {
18777
+ 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",
18778
+ fill: "currentColor"
18779
+ }), jsx("path", {
18780
+ d: "M6.85787 9.6863C8.90184 7.64233 12.2261 7.60094 14.3494 9.42268C14.7319 9.75083 14.7008 10.3287 14.3444 10.685C13.9253 11.1041 13.2317 11.0404 12.7416 10.707C11.398 9.79292 9.48593 9.88667 8.27209 11.1005L4.73655 14.636C3.36972 16.0029 3.36972 18.219 4.73655 19.5858C6.10339 20.9526 8.31947 20.9526 9.6863 19.5858L10.747 18.5251C11.1375 18.1346 11.7706 18.1346 12.1612 18.5251C12.5517 18.9157 12.5517 19.5488 12.1612 19.9394L11.1005 21C8.95263 23.1479 5.47022 23.1479 3.32234 21C1.17445 18.8521 1.17445 15.3697 3.32234 13.2218L6.85787 9.6863Z",
18781
+ fill: "currentColor"
18782
+ })]
18783
+ })
18784
+ });
18785
+ };
18786
+ const LinkSmsSvg = () => {
18787
+ return jsx("svg", {
18788
+ viewBox: "0 0 24 24",
18789
+ xmlns: "http://www.w3.org/2000/svg",
18790
+ children: jsx("path", {
18791
+ 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",
18792
+ fill: "currentColor"
18793
+ })
18794
+ });
18795
+ };
18796
+ const LinkGithubSvg = () => {
18797
+ return jsx("svg", {
18798
+ viewBox: "0 0 24 24",
18799
+ xmlns: "http://www.w3.org/2000/svg",
18800
+ children: jsx("path", {
18801
+ 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",
18802
+ fill: "currentColor"
18803
+ })
18804
+ });
18805
+ };
18806
+ const LinkCurrentSvg = () => {
18807
+ return jsx("svg", {
18808
+ viewBox: "0 0 16 16",
18809
+ xmlns: "http://www.w3.org/2000/svg",
18810
+ children: jsx("path", {
18811
+ d: "m 8 0 c -3.3125 0 -6 2.6875 -6 6 c 0.007812 0.710938 0.136719 1.414062 0.386719 2.078125 l -0.015625 -0.003906 c 0.636718 1.988281 3.78125 5.082031 5.625 6.929687 h 0.003906 v -0.003906 c 1.507812 -1.507812 3.878906 -3.925781 5.046875 -5.753906 c 0.261719 -0.414063 0.46875 -0.808594 0.585937 -1.171875 l -0.019531 0.003906 c 0.25 -0.664063 0.382813 -1.367187 0.386719 -2.078125 c 0 -3.3125 -2.683594 -6 -6 -6 z m 0 3.691406 c 1.273438 0 2.308594 1.035156 2.308594 2.308594 s -1.035156 2.308594 -2.308594 2.308594 c -1.273438 -0.003906 -2.304688 -1.035156 -2.304688 -2.308594 c -0.003906 -1.273438 1.03125 -2.304688 2.304688 -2.308594 z m 0 0",
18812
+ fill: "currentColor"
18813
+ })
18814
+ });
18815
+ };
18816
+
18817
+ const PhoneSvg = () => {
18818
+ return jsx("svg", {
18819
+ viewBox: "0 0 24 24",
18820
+ xmlns: "http://www.w3.org/2000/svg",
18821
+ children: jsx("path", {
18822
+ 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",
18823
+ fill: "currentColor"
18824
+ })
18825
+ });
18826
+ };
18827
+
18828
+ const useDebounceTrue = (value, delay = 300) => {
18829
+ const [debouncedTrue, setDebouncedTrue] = useState(false);
18830
+ const timerRef = useRef(null);
18831
+
18744
18832
  useLayoutEffect(() => {
18745
- const el = ref.current;
18746
- if (!el || !textSelection) {
18747
- return;
18748
- }
18749
- const range = document.createRange();
18750
- const selection = window.getSelection();
18751
- if (Array.isArray(textSelection)) {
18752
- if (textSelection.length === 2) {
18753
- const [start, end] = textSelection;
18754
- if (typeof start === "number" && typeof end === "number") {
18755
- // Format: [0, 10] - character indices
18756
- selectByCharacterIndices(el, range, start, end);
18757
- } else if (typeof start === "string" && typeof end === "string") {
18758
- // Format: ["Click on the", "button to return"] - text strings
18759
- selectByTextStrings(el, range, start, end);
18760
- }
18833
+ // If value is true or becomes true, start a timer
18834
+ if (value) {
18835
+ timerRef.current = setTimeout(() => {
18836
+ setDebouncedTrue(true);
18837
+ }, delay);
18838
+ } else {
18839
+ // If value becomes false, clear any pending timer and immediately set to false
18840
+ if (timerRef.current) {
18841
+ clearTimeout(timerRef.current);
18842
+ timerRef.current = null;
18761
18843
  }
18762
- } else if (typeof textSelection === "string") {
18763
- // Format: "some text" - select the entire string occurrence
18764
- selectSingleTextString(el, range, textSelection);
18844
+ setDebouncedTrue(false);
18765
18845
  }
18766
- selection.removeAllRanges();
18767
- selection.addRange(range);
18768
- }, deps);
18769
- };
18770
- const selectByCharacterIndices = (element, range, startIndex, endIndex) => {
18771
- const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
18772
- let currentIndex = 0;
18773
- let startNode = null;
18774
- let startOffset = 0;
18775
- let endNode = null;
18776
- let endOffset = 0;
18777
- while (walker.nextNode()) {
18778
- const textContent = walker.currentNode.textContent;
18779
- const nodeLength = textContent.length;
18780
18846
 
18781
- // Check if start position is in this text node
18782
- if (!startNode && currentIndex + nodeLength > startIndex) {
18783
- startNode = walker.currentNode;
18784
- startOffset = startIndex - currentIndex;
18785
- }
18847
+ // Cleanup function
18848
+ return () => {
18849
+ if (timerRef.current) {
18850
+ clearTimeout(timerRef.current);
18851
+ }
18852
+ };
18853
+ }, [value, delay]);
18786
18854
 
18787
- // Check if end position is in this text node
18788
- if (currentIndex + nodeLength >= endIndex) {
18789
- endNode = walker.currentNode;
18790
- endOffset = endIndex - currentIndex;
18791
- break;
18792
- }
18793
- currentIndex += nodeLength;
18794
- }
18795
- if (startNode && endNode) {
18796
- range.setStart(startNode, startOffset);
18797
- range.setEnd(endNode, endOffset);
18798
- }
18855
+ return debouncedTrue;
18799
18856
  };
18800
- const selectSingleTextString = (element, range, text) => {
18801
- const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
18802
- while (walker.nextNode()) {
18803
- const textContent = walker.currentNode.textContent;
18804
- const index = textContent.indexOf(text);
18805
- if (index !== -1) {
18806
- range.setStart(walker.currentNode, index);
18807
- range.setEnd(walker.currentNode, index + text.length);
18808
- return;
18809
- }
18810
- }
18857
+
18858
+ const useNetworkSpeed = () => {
18859
+ return networkSpeedSignal.value;
18811
18860
  };
18812
- const selectByTextStrings = (element, range, startText, endText) => {
18813
- const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
18814
- let startNode = null;
18815
- let endNode = null;
18816
- let foundStart = false;
18817
- while (walker.nextNode()) {
18818
- const textContent = walker.currentNode.textContent;
18819
- if (!foundStart && textContent.includes(startText)) {
18820
- startNode = walker.currentNode;
18821
- foundStart = true;
18861
+
18862
+ const connection =
18863
+ window.navigator.connection ||
18864
+ window.navigator.mozConnection ||
18865
+ window.navigator.webkitConnection;
18866
+
18867
+ const getNetworkSpeed = () => {
18868
+ // Network Information API (support moderne)
18869
+ if (!connection) {
18870
+ return "3g";
18871
+ }
18872
+ if (connection) {
18873
+ const effectiveType = connection.effectiveType;
18874
+ if (effectiveType) {
18875
+ return effectiveType; // "slow-2g", "2g", "3g", "4g", "5g"
18822
18876
  }
18823
- if (foundStart && textContent.includes(endText)) {
18824
- endNode = walker.currentNode;
18825
- break;
18877
+ const downlink = connection.downlink;
18878
+ if (downlink) {
18879
+ // downlink is in Mbps
18880
+ if (downlink < 1) return "slow-2g"; // < 1 Mbps
18881
+ if (downlink < 2.5) return "2g"; // 1-2.5 Mbps
18882
+ if (downlink < 10) return "3g"; // 2.5-10 Mbps
18883
+ return "4g"; // > 10 Mbps
18826
18884
  }
18827
18885
  }
18828
- if (startNode && endNode) {
18829
- const startOffset = startNode.textContent.indexOf(startText);
18830
- const endOffset = endNode.textContent.indexOf(endText) + endText.length;
18831
- range.setStart(startNode, startOffset);
18832
- range.setEnd(endNode, endOffset);
18833
- }
18886
+ return "3g";
18834
18887
  };
18835
18888
 
18836
- installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
18837
- const css$4 = /* css */`
18838
- @layer navi {
18839
- .navi_text {
18840
- &[data-skeleton] {
18841
- border-radius: 0.2em;
18842
- }
18843
- }
18844
- }
18889
+ const updateNetworkSpeed = () => {
18890
+ networkSpeedSignal.value = getNetworkSpeed();
18891
+ };
18845
18892
 
18846
- *[data-navi-space] {
18847
- /* user-select: none; */
18848
- padding-left: 0.25em;
18893
+ const networkSpeedSignal = signal(getNetworkSpeed());
18894
+
18895
+ const setupNetworkMonitoring = () => {
18896
+ const cleanupFunctions = [];
18897
+
18898
+ // ✅ 1. Écouter les changements natifs
18899
+
18900
+ if (connection) {
18901
+ connection.addEventListener("change", updateNetworkSpeed);
18902
+ cleanupFunctions.push(() => {
18903
+ connection.removeEventListener("change", updateNetworkSpeed);
18904
+ });
18849
18905
  }
18850
18906
 
18851
- .navi_text {
18852
- position: relative;
18907
+ // ✅ 2. Polling de backup (toutes les 60 secondes)
18908
+ const pollInterval = setInterval(updateNetworkSpeed, 60000);
18909
+ cleanupFunctions.push(() => clearInterval(pollInterval));
18853
18910
 
18854
- /* There is a chrome specific bug that prevents text-transform: capitalize to be applied in nested DOM structure */
18855
- /* The CSS below ensure capitalize is propagated to the bold clones */
18856
- &[data-capitalize] {
18857
- &::first-letter {
18858
- text-transform: uppercase;
18859
- }
18860
- .navi_text_bold_clone::first-letter {
18861
- text-transform: uppercase;
18862
- }
18863
- .navi_text_bold_foreground::first-letter {
18864
- text-transform: uppercase;
18865
- }
18866
- }
18867
-
18868
- .navi_text_bold_wrapper,
18869
- .navi_text_bold_clone,
18870
- .navi_text_bold_foreground {
18871
- display: inherit;
18872
- width: inherit;
18873
- min-width: inherit;
18874
- height: inherit;
18875
- min-height: inherit;
18876
- flex-grow: inherit;
18877
- align-items: inherit;
18878
- justify-content: inherit;
18879
- gap: inherit;
18880
- text-align: inherit;
18881
- border-radius: inherit;
18882
- }
18883
-
18884
- &[data-text-overflow] {
18885
- min-width: 0;
18886
- flex-wrap: wrap;
18887
- text-overflow: ellipsis;
18888
- overflow: hidden;
18889
-
18890
- .navi_text_overflow_wrapper {
18891
- display: flex;
18892
- width: 100%;
18893
- flex-grow: 1;
18894
- gap: 0.3em;
18895
-
18896
- .navi_text_overflow_text {
18897
- max-width: 100%;
18898
- text-overflow: ellipsis;
18899
- overflow: hidden;
18900
- }
18901
- }
18911
+ // 3. Vérifier lors de la reprise d'activité
18912
+ const handleVisibilityChange = () => {
18913
+ if (!document.hidden) {
18914
+ updateNetworkSpeed();
18902
18915
  }
18916
+ };
18903
18917
 
18904
- &[data-skeleton] {
18905
- /* Children stay in the DOM to preserve natural layout dimensions,
18906
- but are hidden so only the skeleton is visible. */
18907
- visibility: hidden;
18908
-
18909
- /* When there are no children a placeholder "W" is injected (see JSX).
18910
- It must stretch to the full available width so the skeleton
18911
- fills the container rather than collapsing to a single character. */
18912
- .navi_text_skeleton_children_placeholder {
18913
- display: inline-flex;
18914
- width: 100%;
18915
- }
18916
-
18917
- /* Three-level structure to respect padding AND border-radius:
18918
-
18919
- 1. navi_text_skeleton_container — absolutely fills the border box
18920
- (inset:0), then applies padding:inherit so its content box equals
18921
- the parent's content box. line-height:normal prevents the container
18922
- from inheriting a large line-height that would make it taller than
18923
- the border box. border-radius:inherit passes the radius down.
18924
- visibility:visible overrides the parent's visibility:hidden.
18925
-
18926
- 2. navi_text_skeleton_inset — a relative block that fills 100% of the
18927
- container's content box (= parent's content box). It is the
18928
- positioned ancestor for the absolutely placed skeleton bar.
18929
- border-radius:inherit chains the radius further down.
18930
-
18931
- 3. navi_text_skeleton — the visible gradient bar. position:absolute
18932
- inset:0 fills the inset box precisely. border-radius:inherit
18933
- finally applies the radius at this level, which is now correctly
18934
- sized to the content area. */
18935
- .navi_text_skeleton_container {
18936
- position: absolute;
18937
- inset: 0;
18938
- padding: inherit;
18939
- line-height: normal;
18940
- border-radius: inherit;
18941
- visibility: visible;
18942
- }
18943
-
18944
- .navi_text_skeleton_inset {
18945
- position: relative;
18946
- display: inline-flex;
18947
- width: 100%;
18948
- height: 100%;
18949
- border-radius: inherit;
18950
- }
18918
+ document.addEventListener("visibilitychange", handleVisibilityChange);
18919
+ cleanupFunctions.push(() => {
18920
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
18921
+ });
18951
18922
 
18952
- .navi_text_skeleton {
18953
- position: absolute;
18954
- inset: 0;
18955
- background: linear-gradient(
18956
- 90deg,
18957
- #e0e0e0 25%,
18958
- #f0f0f0 50%,
18959
- #e0e0e0 75%
18960
- );
18961
- background-size: 200% 100%;
18962
- border-radius: inherit;
18963
- }
18923
+ // ✅ 4. Vérifier lors de la reprise de connexion
18924
+ const handleOnline = () => {
18925
+ updateNetworkSpeed();
18926
+ };
18964
18927
 
18965
- &[data-loading] {
18966
- .navi_text_skeleton {
18967
- animation: navi_text_skeleton_shimmer 1.5s infinite;
18968
- }
18969
- }
18970
- }
18971
- }
18928
+ window.addEventListener("online", handleOnline);
18929
+ cleanupFunctions.push(() => {
18930
+ window.removeEventListener("online", handleOnline);
18931
+ });
18972
18932
 
18973
- @keyframes navi_text_skeleton_shimmer {
18974
- 0% {
18975
- background-position: 200% 0;
18976
- }
18977
- 100% {
18978
- background-position: -200% 0;
18979
- }
18980
- }
18933
+ // Cleanup global
18934
+ return () => {
18935
+ cleanupFunctions.forEach((cleanup) => cleanup());
18936
+ };
18937
+ };
18938
+ setupNetworkMonitoring();
18981
18939
 
18982
- .navi_text_bold_wrapper {
18940
+ installImportMetaCssBuild(import.meta);/**
18941
+ * RectangleLoading Component
18942
+ *
18943
+ * A responsive loading indicator that dynamically adjusts to fit its container.
18944
+ * Displays an animated outline with a traveling dot that follows the container's shape.
18945
+ *
18946
+ * Features:
18947
+ * - Adapts to any container dimensions using ResizeObserver
18948
+ * - Scales stroke width, margins and corner radius proportionally
18949
+ * - Animates using native SVG animations for smooth performance
18950
+ * - High-quality SVG rendering with proper path calculations
18951
+ *
18952
+ * @param {Object} props - Component props
18953
+ * @param {string} [props.color="#383a36"] - Color of the loading indicator
18954
+ * @param {number} [props.radius=0] - Corner radius of the rectangle (px)
18955
+ */
18956
+ import.meta.css = [/* css */`
18957
+ .navi_rectangle_loading {
18983
18958
  position: relative;
18984
- display: inline-block;
18985
-
18986
- .navi_text_bold_clone {
18987
- font-weight: bold;
18988
- opacity: 0;
18989
- }
18990
- .navi_text_bold_foreground {
18991
- position: absolute;
18992
- inset: 0;
18993
- }
18994
- }
18995
-
18996
- .navi_text_bold_background {
18997
- position: absolute;
18998
- top: 0;
18999
- left: 0;
19000
- color: currentColor;
19001
- font-weight: normal;
19002
- background: currentColor;
19003
- background-clip: text;
19004
- -webkit-background-clip: text;
19005
- transform-origin: center;
19006
- -webkit-text-fill-color: transparent;
18959
+ display: flex;
18960
+ width: 100%;
18961
+ height: 100%;
19007
18962
  opacity: 0;
19008
18963
  }
19009
18964
 
19010
- .navi_text[data-bold] {
19011
- .navi_text_bold_background {
19012
- opacity: 1;
19013
- }
19014
- }
19015
-
19016
- .navi_text[data-bold-transition] {
19017
- .navi_text_bold_foreground {
19018
- transition-property: font-weight;
19019
- transition-duration: 0.3s;
19020
- transition-timing-function: ease;
19021
- }
19022
-
19023
- .navi_text_bold_background {
19024
- transition-property: opacity;
19025
- transition-duration: 0.3s;
19026
- transition-timing-function: ease;
19027
- }
18965
+ .navi_rectangle_loading[data-visible] {
18966
+ opacity: 1;
19028
18967
  }
19029
- `;
19030
-
19031
- // We could use <span data-navi-space=""> </span>
19032
- // but we prefer to use zero width space as it has the nice side effects of
19033
- // not being underlined by the browser (very cool because we typically don't want spaces to be underlined in links)
19034
- const REGULAR_SPACE = jsx("span", {
19035
- "data-navi-space": "",
19036
- children: "\u200B"
19037
- });
19038
- const CustomWidthSpace = ({
19039
- value
18968
+ `, "@jsenv/navi/src/graphic/loader/rectangle_loading.jsx"];
18969
+ const RectangleLoading = ({
18970
+ shouldShowSpinner,
18971
+ color = "currentColor",
18972
+ radius = 0,
18973
+ size = 2
19040
18974
  }) => {
19041
- return jsx("span", {
19042
- className: "navi_custom_space",
19043
- style: `padding-left: ${value}`,
19044
- children: "\u200B"
19045
- });
19046
- };
19047
- const applySpacingOnTextChildren = (children, spacing = REGULAR_SPACE) => {
19048
- if (spacing === "pre" || spacing === "0" || spacing === 0) {
19049
- return children;
19050
- }
19051
- if (!children) {
19052
- return children;
19053
- }
19054
- const childArray = toChildArray(children);
19055
- const childCount = childArray.length;
19056
- if (childCount <= 1) {
19057
- return children;
19058
- }
19059
- let separator;
19060
- if (spacing === undefined) {
19061
- spacing = REGULAR_SPACE;
19062
- } else if (typeof spacing === "string") {
19063
- if (isSizeSpacingScaleKey(spacing)) {
19064
- separator = jsx(CustomWidthSpace, {
19065
- value: resolveSpacingSize(spacing)
19066
- });
19067
- } else if (hasCSSSizeUnit(spacing)) {
19068
- separator = jsx(CustomWidthSpace, {
19069
- value: resolveSpacingSize(spacing)
19070
- });
19071
- } else {
19072
- separator = spacing;
18975
+ const containerRef = useRef(null);
18976
+ const [containerWidth, setContainerWidth] = useState(0);
18977
+ const [containerHeight, setContainerHeight] = useState(0);
18978
+ useLayoutEffect(() => {
18979
+ const container = containerRef.current;
18980
+ if (!container) {
18981
+ return null;
19073
18982
  }
19074
- } else if (typeof spacing === "number") {
19075
- separator = jsx(CustomWidthSpace, {
19076
- value: `${spacing}px`
19077
- });
19078
- } else {
19079
- separator = spacing;
19080
- }
19081
- const childrenWithGap = [];
19082
- let i = 0;
19083
- while (true) {
19084
- const child = childArray[i];
19085
- childrenWithGap.push(child);
19086
- i++;
19087
- if (i === childCount) {
19088
- break;
19089
- }
19090
- const currentChild = childArray[i - 1];
19091
- const nextChild = childArray[i];
19092
- if (!shouldInjectSpacingBetween(currentChild, nextChild)) {
19093
- continue;
19094
- }
19095
- childrenWithGap.push(separator);
19096
- }
19097
- return childrenWithGap;
19098
- };
19099
- const outsideTextFlowSet = new Set();
19100
- const markAsOutsideTextFlow = jsxElement => {
19101
- outsideTextFlowSet.add(jsxElement);
19102
- };
19103
- const isMarkedAsOutsideTextFlow = jsxElement => {
19104
- return outsideTextFlowSet.has(jsxElement.type);
19105
- };
19106
- const isPreactNode = jsxChild => {
19107
- return jsxChild !== null && typeof jsxChild === "object" && jsxChild.type !== undefined;
19108
- };
19109
- const shouldInjectSpacingBetween = (left, right) => {
19110
- const leftIsNode = isPreactNode(left);
19111
- const rightIsNode = isPreactNode(right);
19112
- // only inject spacing when at least one side is a preact node
19113
- if (!leftIsNode && !rightIsNode) {
19114
- return false;
19115
- }
19116
- if (leftIsNode && isMarkedAsOutsideTextFlow(left)) {
19117
- return false;
19118
- }
19119
- if (rightIsNode && isMarkedAsOutsideTextFlow(right)) {
19120
- return false;
19121
- }
19122
- if (rightIsNode && right.props && right.props.overflowPinned) {
19123
- return false;
19124
- }
19125
- if (typeof left === "string" && /\s$/.test(left)) {
19126
- return false;
19127
- }
19128
- if (typeof right === "string" && /^\s/.test(right)) {
19129
- return false;
19130
- }
19131
- return true;
19132
- };
19133
- const OverflowPinnedElementContext = createContext(null);
19134
- const Text = props => {
19135
- import.meta.css = [css$4, "@jsenv/navi/src/text/text.jsx"];
19136
- if (props.loading || props.skeleton) {
19137
- return jsx(TextSkeleton, {
19138
- ...props
19139
- });
19140
- }
19141
- if (props.overflowEllipsis) {
19142
- return jsx(TextOverflow, {
19143
- ...props
19144
- });
19145
- }
19146
- if (props.overflowPinned) {
19147
- return jsx(TextOverflowPinned, {
19148
- ...props
19149
- });
19150
- }
19151
- if (props.selectRange) {
19152
- return jsx(TextWithSelectRange, {
19153
- ...props
18983
+ const {
18984
+ width,
18985
+ height
18986
+ } = container.getBoundingClientRect();
18987
+ setContainerWidth(width);
18988
+ setContainerHeight(height);
18989
+ let animationFrameId = null;
18990
+ // Create a resize observer to detect changes in the container's dimensions
18991
+ const resizeObserver = new ResizeObserver(entries => {
18992
+ // Use requestAnimationFrame to debounce updates
18993
+ if (animationFrameId) {
18994
+ cancelAnimationFrame(animationFrameId);
18995
+ }
18996
+ animationFrameId = requestAnimationFrame(() => {
18997
+ const [containerEntry] = entries;
18998
+ const {
18999
+ width,
19000
+ height
19001
+ } = containerEntry.contentRect;
19002
+ setContainerWidth(width);
19003
+ setContainerHeight(height);
19004
+ });
19154
19005
  });
19155
- }
19156
- return jsx(TextBasic, {
19157
- ...props
19158
- });
19159
- };
19160
- const TextSkeleton = ({
19161
- loading,
19162
- children,
19163
- ...props
19164
- }) => {
19165
- // Three-level structure — see CSS comment on [data-skeleton] for details.
19166
- const skeletonOverlay = jsx("span", {
19167
- className: "navi_text_skeleton_container",
19168
- "aria-hidden": "true",
19169
- children: jsx("span", {
19170
- className: "navi_text_skeleton_inset",
19171
- children: jsx("span", {
19172
- className: "navi_text_skeleton"
19173
- })
19006
+ resizeObserver.observe(container);
19007
+ return () => {
19008
+ if (animationFrameId) {
19009
+ cancelAnimationFrame(animationFrameId);
19010
+ }
19011
+ resizeObserver.disconnect();
19012
+ };
19013
+ }, []);
19014
+ return jsx("span", {
19015
+ ref: containerRef,
19016
+ className: "navi_rectangle_loading",
19017
+ "data-visible": shouldShowSpinner ? "" : undefined,
19018
+ children: containerWidth > 0 && containerHeight > 0 && jsx(RectangleLoadingSvg, {
19019
+ radius: radius,
19020
+ color: color,
19021
+ width: containerWidth,
19022
+ height: containerHeight,
19023
+ strokeWidth: size
19174
19024
  })
19175
19025
  });
19176
- // When there are no children, inject a full-width placeholder so the element
19177
- // has measurable height driven by the current font-size/line-height, and the
19178
- // skeleton fills the available width instead of shrinking to a single char.
19179
- const hasChildren = children !== null && children !== undefined && children !== false;
19180
- const innerChildren = hasChildren ? children : jsx("span", {
19181
- className: "navi_text_skeleton_children_placeholder",
19182
- "aria-hidden": "true",
19183
- children: "W"
19184
- });
19185
- return jsx(Text, {
19186
- "data-skeleton": "",
19187
- "data-loading": loading ? "" : undefined,
19188
- ...props,
19189
- skeleton: undefined,
19190
- childrenOutsideFlow: skeletonOverlay,
19191
- children: innerChildren
19192
- });
19193
19026
  };
19194
- const TextOverflow = ({
19195
- noWrap,
19196
- spacing,
19197
- children,
19198
- ...rest
19027
+ const RectangleLoadingSvg = ({
19028
+ width,
19029
+ height,
19030
+ color,
19031
+ radius,
19032
+ trailColor = "transparent",
19033
+ strokeWidth
19199
19034
  }) => {
19200
- const [OverflowPinnedElement, setOverflowPinnedElement] = useState(null);
19201
- return jsx(Text, {
19202
- flex: true,
19203
- block: true,
19204
- as: "div",
19205
- nowWrap: noWrap,
19206
- pre: !noWrap
19207
- // For paragraph we prefer to keep lines and only hide unbreakable long sections
19208
- ,
19035
+ const margin = Math.max(2, Math.min(width, height) * 0.03);
19209
19036
 
19210
- preLine: rest.as === "p",
19211
- ...rest,
19212
- overflowEllipsis: undefined,
19213
- "data-text-overflow": "",
19214
- spacing: "pre",
19215
- children: jsxs("span", {
19216
- className: "navi_text_overflow_wrapper",
19217
- children: [jsx(OverflowPinnedElementContext.Provider, {
19218
- value: setOverflowPinnedElement,
19219
- children: jsx(Text, {
19220
- className: "navi_text_overflow_text",
19221
- spacing: spacing,
19222
- children: children
19223
- })
19224
- }), OverflowPinnedElement]
19225
- })
19226
- });
19227
- };
19228
- const TextOverflowPinned = ({
19229
- overflowPinned,
19230
- ...props
19231
- }) => {
19232
- const setOverflowPinnedElement = useContext(OverflowPinnedElementContext);
19233
- const text = jsx(Text, {
19234
- ...props,
19235
- "data-overflow-pinned": ""
19236
- });
19237
- if (!setOverflowPinnedElement) {
19238
- console.warn("<Text overflowPinned> declared outside a <Text overflowEllipsis>");
19239
- return text;
19240
- }
19241
- if (overflowPinned) {
19242
- setOverflowPinnedElement(text);
19243
- return null;
19244
- }
19245
- setOverflowPinnedElement(null);
19246
- return text;
19247
- };
19248
- const TextWithSelectRange = ({
19249
- selectRange,
19250
- ...props
19251
- }) => {
19252
- const defaultRef = useRef();
19253
- const ref = props.ref || defaultRef;
19254
- useInitialTextSelection(ref, selectRange);
19255
- return jsx(Text, {
19256
- ref: ref,
19257
- ...props
19258
- });
19259
- };
19260
- const TextBasic = ({
19261
- spacing = REGULAR_SPACE,
19262
- boldTransition,
19263
- boldStable,
19264
- preventBoldLayoutShift = boldTransition,
19265
- capitalize,
19266
- children,
19267
- childrenOutsideFlow,
19268
- ...rest
19269
- }) => {
19270
- const boxProps = {
19271
- "as": "span",
19272
- "data-bold-transition": boldTransition ? "" : undefined,
19273
- "data-capitalize": capitalize ? "" : undefined,
19274
- ...rest,
19275
- "baseClassName": withPropsClassName("navi_text", rest.baseClassName)
19276
- };
19277
- const shouldPreserveSpacing = rest.as === "pre" || rest.flex || rest.grid;
19278
- if (shouldPreserveSpacing) {
19279
- boxProps.spacing = spacing;
19280
- } else {
19281
- children = applySpacingOnTextChildren(children, spacing);
19282
- }
19283
- if (boldStable) {
19284
- const {
19285
- bold
19286
- } = boxProps;
19287
- return jsxs(Box, {
19288
- ...boxProps,
19289
- bold: undefined,
19290
- "data-bold": bold ? "" : undefined,
19291
- children: [jsx("span", {
19292
- className: "navi_text_bold_background",
19293
- "aria-hidden": "true",
19294
- children: children
19295
- }), children, childrenOutsideFlow]
19296
- });
19297
- }
19298
- if (preventBoldLayoutShift) {
19299
- const alignX = rest.alignX || rest.align || "start";
19037
+ // Calculate the drawable area
19038
+ const drawableWidth = width - margin * 2;
19039
+ const drawableHeight = height - margin * 2;
19300
19040
 
19301
- // La technique consiste a avoid un double gras qui force une taille
19302
- // et la version light par dessus en position absolute
19303
- // on la centre aussi pour donner l'impression que le gras s'applique depuis le centre
19304
- // ne fonctionne que sur une seule ligne de texte (donc lorsque noWrap est actif)
19305
- // on pourrait auto-active cela sur une prop genre boldCanChange
19306
- return jsxs(Box, {
19307
- ...boxProps,
19308
- children: [jsxs("span", {
19309
- className: "navi_text_bold_wrapper",
19310
- children: [jsx("span", {
19311
- className: "navi_text_bold_clone",
19312
- "aria-hidden": "true",
19313
- children: children
19314
- }), jsx("span", {
19315
- className: "navi_text_bold_foreground",
19316
- "data-align": alignX,
19317
- children: children
19318
- })]
19319
- }), childrenOutsideFlow]
19320
- });
19321
- }
19322
- return jsxs(Box, {
19323
- ...boxProps,
19324
- children: [children, childrenOutsideFlow]
19041
+ // Check if this should be a circle - only if width and height are nearly equal
19042
+ const maxPossibleRadius = Math.min(drawableWidth, drawableHeight) / 2;
19043
+ radius = resolveCSSSize(radius, {
19044
+ availableSize: Math.min(width, height)
19325
19045
  });
19326
- };
19327
-
19328
- installImportMetaCssBuild(import.meta);const css$3 = /* css */`
19329
- @layer navi {
19330
- /* Ensure data attributes from box.jsx can win to update display */
19331
- .navi_icon {
19332
- display: inline-block;
19333
- box-sizing: border-box;
19334
- max-width: 100%;
19335
- max-height: 100%;
19336
- }
19337
- }
19338
-
19339
- .navi_icon {
19340
- &[data-flow-inline] {
19341
- width: 1em;
19342
- height: 1em;
19343
- }
19344
- &[data-icon-char] {
19345
- flex-grow: 0 !important;
19046
+ const actualRadius = Math.min(radius || Math.min(drawableWidth, drawableHeight) * 0.05, maxPossibleRadius // ✅ Limité au radius maximum possible
19047
+ );
19048
+ const aspectRatio = Math.max(drawableWidth, drawableHeight) / Math.min(drawableWidth, drawableHeight);
19049
+ const isNearlySquare = aspectRatio <= 1.2; // Allow some tolerance for nearly square shapes
19050
+ const isCircle = isNearlySquare && actualRadius >= maxPossibleRadius * 0.95;
19051
+ let pathLength;
19052
+ let rectPath;
19053
+ if (isCircle) {
19054
+ // ✅ Circle: perimeter = 2πr
19055
+ pathLength = 2 * Math.PI * actualRadius;
19346
19056
 
19347
- svg,
19348
- img {
19349
- width: 100%;
19350
- height: 100%;
19351
- }
19352
- svg {
19353
- overflow: visible;
19354
- }
19355
- }
19356
- &[data-interactive] {
19357
- cursor: pointer;
19358
- }
19057
+ // ✅ Circle path centered in the drawable area
19058
+ const centerX = margin + drawableWidth / 2;
19059
+ const centerY = margin + drawableHeight / 2;
19060
+ rectPath = `
19061
+ M ${centerX + actualRadius},${centerY}
19062
+ A ${actualRadius},${actualRadius} 0 1 1 ${centerX - actualRadius},${centerY}
19063
+ A ${actualRadius},${actualRadius} 0 1 1 ${centerX + actualRadius},${centerY}
19064
+ `;
19065
+ } else {
19066
+ // ✅ Rectangle: calculate perimeter properly
19067
+ const straightEdges = 2 * (drawableWidth - 2 * actualRadius) + 2 * (drawableHeight - 2 * actualRadius);
19068
+ const cornerArcs = actualRadius > 0 ? 2 * Math.PI * actualRadius : 0;
19069
+ pathLength = straightEdges + cornerArcs;
19070
+ rectPath = `
19071
+ M ${margin + actualRadius},${margin}
19072
+ L ${margin + drawableWidth - actualRadius},${margin}
19073
+ A ${actualRadius},${actualRadius} 0 0 1 ${margin + drawableWidth},${margin + actualRadius}
19074
+ L ${margin + drawableWidth},${margin + drawableHeight - actualRadius}
19075
+ A ${actualRadius},${actualRadius} 0 0 1 ${margin + drawableWidth - actualRadius},${margin + drawableHeight}
19076
+ L ${margin + actualRadius},${margin + drawableHeight}
19077
+ A ${actualRadius},${actualRadius} 0 0 1 ${margin},${margin + drawableHeight - actualRadius}
19078
+ L ${margin},${margin + actualRadius}
19079
+ A ${actualRadius},${actualRadius} 0 0 1 ${margin + actualRadius},${margin}
19080
+ `;
19359
19081
  }
19360
19082
 
19361
- .navi_icon_char_slot {
19362
- opacity: 0;
19363
- cursor: default;
19364
- user-select: none;
19365
- }
19366
- .navi_text.navi_icon_foreground {
19367
- position: absolute;
19368
- inset: 0;
19369
- display: inline-flex;
19083
+ // Fixed segment size in pixels
19084
+ const maxSegmentSize = 40;
19085
+ const segmentLength = Math.min(maxSegmentSize, pathLength * 0.25);
19086
+ const gapLength = pathLength - segmentLength;
19370
19087
 
19371
- & > .navi_text {
19372
- display: flex;
19373
- aspect-ratio: 1 / 1;
19374
- min-width: 0;
19375
- height: 100%;
19376
- max-height: 1em;
19377
- align-items: center;
19378
- justify-content: center;
19379
- }
19380
- }
19088
+ // Vitesse constante en pixels par seconde
19089
+ const networkSpeed = useNetworkSpeed();
19090
+ const pixelsPerSecond = {
19091
+ "slow-2g": 40,
19092
+ "2g": 60,
19093
+ "3g": 80,
19094
+ "4g": 120
19095
+ }[networkSpeed] || 80;
19096
+ const animationDuration = Math.max(1.5, pathLength / pixelsPerSecond);
19381
19097
 
19382
- .navi_icon > svg,
19383
- .navi_icon > img {
19384
- width: 100%;
19385
- height: 100%;
19386
- backface-visibility: hidden;
19387
- }
19388
- .navi_icon[data-width-fixed] > svg,
19389
- .navi_icon[data-width-fixed] > img {
19390
- width: 100%;
19391
- height: auto;
19392
- }
19393
- .navi_icon[data-height-fixed] > svg,
19394
- .navi_icon[data-height-fixed] > img {
19395
- width: auto;
19396
- height: 100%;
19397
- }
19398
- .navi_icon[data-width-fixed][data-height-fixed] > svg,
19399
- .navi_icon[data-width-fixed][data-height-fixed] > img {
19400
- width: 100%;
19401
- height: 100%;
19402
- }
19403
- `;
19404
- const Icon = ({
19405
- href,
19406
- children,
19407
- charWidth = 1,
19408
- // 0 (zéro) is the real char width
19409
- // but 2 zéros gives too big icons
19410
- // while 1 "W" gives a nice result
19411
- baseChar = "W",
19412
- decorative,
19413
- onClick,
19414
- ...props
19415
- }) => {
19416
- import.meta.css = [css$3, "@jsenv/navi/src/graphic/icon.jsx"];
19417
- const innerChildren = href ? jsx("svg", {
19098
+ // Calculate correct offset based on actual segment size
19099
+ const segmentRatio = segmentLength / pathLength;
19100
+ const circleOffset = -animationDuration * segmentRatio;
19101
+ return jsxs("svg", {
19418
19102
  width: "100%",
19419
19103
  height: "100%",
19420
- children: jsx("use", {
19421
- href: href
19422
- })
19423
- }) : children;
19424
- let {
19425
- flex,
19426
- grid,
19427
- width,
19428
- height
19429
- } = props;
19430
- if (width === "auto") {
19431
- width = undefined;
19432
- }
19433
- if (height === "auto") {
19434
- height = undefined;
19435
- }
19436
- const hasExplicitWidth = width !== undefined;
19437
- const hasExplicitHeight = height !== undefined;
19438
- const widthFixed = hasExplicitWidth || hasExplicitHeight && (props.square || props.circle || props.aspectRatio);
19439
- const heightFixed = hasExplicitHeight || hasExplicitWidth && (props.square || props.circle || props.aspectRatio);
19440
- if (widthFixed || heightFixed) {
19441
- if (flex === undefined) {
19442
- flex = "x";
19443
- }
19444
- } else if (decorative === undefined && !onClick) {
19445
- decorative = true;
19446
- }
19447
- const ariaProps = decorative ? {
19448
- "aria-hidden": "true"
19449
- } : {};
19450
- if (typeof children === "string") {
19451
- return jsx(Text, {
19452
- ...props,
19453
- ...ariaProps,
19454
- "data-icon-text": "",
19455
- children: children
19456
- });
19457
- }
19458
- if (flex || grid) {
19459
- return jsx(Box, {
19460
- square: true,
19461
- ...props,
19462
- ...ariaProps,
19463
- flex: flex,
19464
- baseClassName: "navi_icon",
19465
- "data-width-fixed": widthFixed ? "" : undefined,
19466
- "data-height-fixed": heightFixed ? "" : undefined,
19467
- "data-interactive": onClick ? "" : undefined,
19468
- onClick: onClick,
19469
- children: innerChildren
19470
- });
19471
- }
19472
- const invisibleText = baseChar.repeat(charWidth);
19473
- return jsxs(Text, {
19474
- ...props,
19475
- ...ariaProps,
19476
- className: withPropsClassName("navi_icon", props.className),
19477
- spacing: "pre",
19478
- "data-icon-char": "",
19479
- "data-width-fixed": widthFixed ? "" : undefined,
19480
- "data-height-fixed": heightFixed ? "" : undefined,
19481
- "data-interactive": onClick ? "" : undefined,
19482
- onClick: onClick,
19483
- children: [jsx("span", {
19484
- className: "navi_icon_char_slot",
19485
- "aria-hidden": "true",
19486
- children: invisibleText
19487
- }), jsx(Text, {
19488
- className: "navi_icon_foreground",
19489
- spacing: "pre",
19490
- children: innerChildren
19491
- })]
19492
- });
19493
- };
19494
-
19495
- const EmailSvg = () => {
19496
- return jsxs("svg", {
19497
- viewBox: "0 0 24 24",
19104
+ viewBox: `0 0 ${width} ${height}`,
19105
+ preserveAspectRatio: "none",
19106
+ style: "overflow: visible",
19498
19107
  xmlns: "http://www.w3.org/2000/svg",
19499
- children: [jsx("path", {
19500
- 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",
19108
+ "shape-rendering": "geometricPrecision",
19109
+ children: [isCircle ? jsx("circle", {
19110
+ cx: margin + drawableWidth / 2,
19111
+ cy: margin + drawableHeight / 2,
19112
+ r: actualRadius,
19501
19113
  fill: "none",
19502
- stroke: "currentColor",
19503
- "stroke-width": "2"
19114
+ stroke: trailColor,
19115
+ strokeWidth: strokeWidth
19116
+ }) : jsx("rect", {
19117
+ x: margin,
19118
+ y: margin,
19119
+ width: drawableWidth,
19120
+ height: drawableHeight,
19121
+ fill: "none",
19122
+ stroke: trailColor,
19123
+ strokeWidth: strokeWidth,
19124
+ rx: actualRadius
19504
19125
  }), jsx("path", {
19505
- d: "m2 6 8 5 2 1.5 2-1.5 8-5",
19126
+ d: rectPath,
19506
19127
  fill: "none",
19507
- stroke: "currentColor",
19508
- "stroke-width": "2",
19509
- "stroke-linecap": "round",
19510
- "stroke-linejoin": "round"
19128
+ stroke: color,
19129
+ strokeWidth: strokeWidth,
19130
+ strokeLinecap: "round",
19131
+ strokeDasharray: `${segmentLength} ${gapLength}`,
19132
+ pathLength: pathLength,
19133
+ children: jsx("animate", {
19134
+ attributeName: "stroke-dashoffset",
19135
+ from: pathLength,
19136
+ to: "0",
19137
+ dur: `${animationDuration}s`,
19138
+ repeatCount: "indefinite",
19139
+ begin: "0s"
19140
+ })
19141
+ }), jsx("circle", {
19142
+ r: strokeWidth,
19143
+ fill: color,
19144
+ children: jsx("animateMotion", {
19145
+ path: rectPath,
19146
+ dur: `${animationDuration}s`,
19147
+ repeatCount: "indefinite",
19148
+ rotate: "auto",
19149
+ begin: `${circleOffset}s`
19150
+ })
19511
19151
  })]
19512
19152
  });
19513
- };
19514
-
19515
- const LinkBlankTargetSvg = () => {
19516
- return jsx("svg", {
19517
- viewBox: "0 0 24 24",
19518
- xmlns: "http://www.w3.org/2000/svg",
19519
- children: jsx("path", {
19520
- 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",
19521
- stroke: "currentColor",
19522
- fill: "none",
19523
- "stroke-width": "2",
19524
- "stroke-linecap": "round",
19525
- "stroke-linejoin": "round"
19526
- })
19527
- });
19528
- };
19529
- const LinkAnchorSvg = () => {
19530
- return jsx("svg", {
19531
- viewBox: "0 0 24 24",
19532
- xmlns: "http://www.w3.org/2000/svg",
19533
- children: jsxs("g", {
19534
- children: [jsx("path", {
19535
- 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",
19536
- fill: "currentColor"
19537
- }), jsx("path", {
19538
- d: "M6.85787 9.6863C8.90184 7.64233 12.2261 7.60094 14.3494 9.42268C14.7319 9.75083 14.7008 10.3287 14.3444 10.685C13.9253 11.1041 13.2317 11.0404 12.7416 10.707C11.398 9.79292 9.48593 9.88667 8.27209 11.1005L4.73655 14.636C3.36972 16.0029 3.36972 18.219 4.73655 19.5858C6.10339 20.9526 8.31947 20.9526 9.6863 19.5858L10.747 18.5251C11.1375 18.1346 11.7706 18.1346 12.1612 18.5251C12.5517 18.9157 12.5517 19.5488 12.1612 19.9394L11.1005 21C8.95263 23.1479 5.47022 23.1479 3.32234 21C1.17445 18.8521 1.17445 15.3697 3.32234 13.2218L6.85787 9.6863Z",
19539
- fill: "currentColor"
19540
- })]
19541
- })
19542
- });
19543
- };
19544
- const LinkSmsSvg = () => {
19545
- return jsx("svg", {
19546
- viewBox: "0 0 24 24",
19547
- xmlns: "http://www.w3.org/2000/svg",
19548
- children: jsx("path", {
19549
- 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",
19550
- fill: "currentColor"
19551
- })
19552
- });
19553
- };
19554
- const LinkGithubSvg = () => {
19555
- return jsx("svg", {
19556
- viewBox: "0 0 24 24",
19557
- xmlns: "http://www.w3.org/2000/svg",
19558
- children: jsx("path", {
19559
- 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",
19560
- fill: "currentColor"
19561
- })
19153
+ };
19154
+
19155
+ installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
19156
+ .navi_loading_rectangle_wrapper {
19157
+ position: absolute;
19158
+ top: var(--rectangle-top, 0);
19159
+ right: var(--rectangle-right, 0);
19160
+ bottom: var(--rectangle-bottom, 0);
19161
+ left: var(--rectangle-left, 0);
19162
+ z-index: 1;
19163
+ opacity: 0;
19164
+ pointer-events: none;
19165
+
19166
+ &[data-visible] {
19167
+ opacity: 1;
19168
+ }
19169
+ }
19170
+ `, "@jsenv/navi/src/graphic/loader/loader_background.jsx"];
19171
+ const LoaderBackground = ({
19172
+ loading,
19173
+ containerRef,
19174
+ targetSelector,
19175
+ color,
19176
+ inset = 0,
19177
+ borderRadius = 0,
19178
+ spacingTop = 0,
19179
+ spacingLeft = 0,
19180
+ spacingBottom = 0,
19181
+ spacingRight = 0,
19182
+ children
19183
+ }) => {
19184
+ if (containerRef) {
19185
+ const container = containerRef.current;
19186
+ if (!container) {
19187
+ return children;
19188
+ }
19189
+ return createPortal(jsx(LoaderBackgroundWithPortal, {
19190
+ container: container,
19191
+ loading: loading,
19192
+ color: color,
19193
+ inset: inset,
19194
+ spacingTop: spacingTop,
19195
+ spacingLeft: spacingLeft,
19196
+ spacingBottom: spacingBottom,
19197
+ spacingRight: spacingRight,
19198
+ children: children
19199
+ }), container);
19200
+ }
19201
+ return jsx(LoaderBackgroundBasic, {
19202
+ targetSelector: targetSelector,
19203
+ loading: loading,
19204
+ color: color,
19205
+ inset: inset,
19206
+ borderRadius: borderRadius,
19207
+ spacingTop: spacingTop,
19208
+ spacingLeft: spacingLeft,
19209
+ spacingBottom: spacingBottom,
19210
+ spacingRight: spacingRight,
19211
+ children: children
19562
19212
  });
19563
19213
  };
19564
- const LinkCurrentSvg = () => {
19565
- return jsx("svg", {
19566
- viewBox: "0 0 16 16",
19567
- xmlns: "http://www.w3.org/2000/svg",
19568
- children: jsx("path", {
19569
- d: "m 8 0 c -3.3125 0 -6 2.6875 -6 6 c 0.007812 0.710938 0.136719 1.414062 0.386719 2.078125 l -0.015625 -0.003906 c 0.636718 1.988281 3.78125 5.082031 5.625 6.929687 h 0.003906 v -0.003906 c 1.507812 -1.507812 3.878906 -3.925781 5.046875 -5.753906 c 0.261719 -0.414063 0.46875 -0.808594 0.585937 -1.171875 l -0.019531 0.003906 c 0.25 -0.664063 0.382813 -1.367187 0.386719 -2.078125 c 0 -3.3125 -2.683594 -6 -6 -6 z m 0 3.691406 c 1.273438 0 2.308594 1.035156 2.308594 2.308594 s -1.035156 2.308594 -2.308594 2.308594 c -1.273438 -0.003906 -2.304688 -1.035156 -2.304688 -2.308594 c -0.003906 -1.273438 1.03125 -2.304688 2.304688 -2.308594 z m 0 0",
19570
- fill: "currentColor"
19571
- })
19214
+ const LoaderBackgroundWithPortal = ({
19215
+ container,
19216
+ loading,
19217
+ color,
19218
+ inset,
19219
+ borderRadius,
19220
+ spacingTop,
19221
+ spacingLeft,
19222
+ spacingBottom,
19223
+ spacingRight,
19224
+ children
19225
+ }) => {
19226
+ const shouldShowSpinner = useDebounceTrue(loading, 300);
19227
+ if (!shouldShowSpinner) {
19228
+ return children;
19229
+ }
19230
+ container.style.position = "relative";
19231
+ let paddingTop = 0;
19232
+ if (container.nodeName === "DETAILS") {
19233
+ paddingTop = container.querySelector("summary").offsetHeight;
19234
+ }
19235
+ return jsxs(Fragment, {
19236
+ children: [jsx("div", {
19237
+ style: {
19238
+ position: "absolute",
19239
+ top: `${inset + paddingTop + spacingTop}px`,
19240
+ bottom: `${inset + spacingBottom}px`,
19241
+ left: `${inset + spacingLeft}px`,
19242
+ right: `${inset + spacingRight}px`
19243
+ },
19244
+ children: shouldShowSpinner && jsx(RectangleLoading, {
19245
+ color: color,
19246
+ radius: borderRadius
19247
+ })
19248
+ }), children]
19572
19249
  });
19573
19250
  };
19574
-
19575
- const PhoneSvg = () => {
19576
- return jsx("svg", {
19577
- viewBox: "0 0 24 24",
19578
- xmlns: "http://www.w3.org/2000/svg",
19579
- children: jsx("path", {
19580
- 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",
19581
- fill: "currentColor"
19582
- })
19251
+ const LoaderBackgroundBasic = ({
19252
+ loading,
19253
+ targetSelector,
19254
+ color,
19255
+ borderWidth = 0,
19256
+ borderRadius = 0,
19257
+ spacingTop,
19258
+ spacingLeft,
19259
+ spacingBottom,
19260
+ spacingRight,
19261
+ marginTop = 0,
19262
+ marginLeft = 0,
19263
+ marginBottom = 0,
19264
+ marginRight = 0,
19265
+ paddingTop = 0,
19266
+ paddingLeft = 0,
19267
+ paddingBottom = 0,
19268
+ paddingRight = 0,
19269
+ inset,
19270
+ children
19271
+ }) => {
19272
+ const shouldShowSpinner = useDebounceTrue(loading, 300);
19273
+ const rectangleRef = useRef(null);
19274
+ spacingTop += inset;
19275
+ // spacingTop += outlineOffset;
19276
+ // spacingTop -= borderTopWidth;
19277
+ spacingTop += marginTop;
19278
+ spacingLeft += inset;
19279
+ // spacingLeft += outlineOffset;
19280
+ // spacingLeft -= borderLeftWidth;
19281
+ spacingLeft += marginLeft;
19282
+ spacingRight += inset;
19283
+ // spacingRight += outlineOffset;
19284
+ // spacingRight -= borderRightWidth;
19285
+ spacingRight += marginRight;
19286
+ spacingBottom += inset;
19287
+ // spacingBottom += outlineOffset;
19288
+ // spacingBottom -= borderBottomWidth;
19289
+ spacingBottom += marginBottom;
19290
+ if (targetSelector) {
19291
+ // oversimplification that actually works
19292
+ // (simplified because it assumes the targeted element is a direct child of the contained element which may have padding)
19293
+ spacingTop += paddingTop;
19294
+ spacingLeft += paddingLeft;
19295
+ spacingRight += paddingRight;
19296
+ spacingBottom += paddingBottom;
19297
+ }
19298
+ const maxBorderWidth = Math.max(borderWidth);
19299
+ const halfMaxBorderSize = maxBorderWidth / 2;
19300
+ const size = halfMaxBorderSize < 2 ? 2 : halfMaxBorderSize;
19301
+ const lineHalfSize = size / 2;
19302
+ spacingTop -= lineHalfSize;
19303
+ spacingLeft -= lineHalfSize;
19304
+ spacingRight -= lineHalfSize;
19305
+ spacingBottom -= lineHalfSize;
19306
+ return jsxs(Fragment, {
19307
+ children: [jsx("span", {
19308
+ ref: rectangleRef,
19309
+ className: "navi_loading_rectangle_wrapper",
19310
+ "data-visible": shouldShowSpinner ? "" : undefined,
19311
+ style: {
19312
+ "--rectangle-top": `${spacingTop}px`,
19313
+ "--rectangle-left": `${spacingLeft}px`,
19314
+ "--rectangle-bottom": `${spacingBottom}px`,
19315
+ "--rectangle-right": `${spacingRight}px`
19316
+ },
19317
+ children: loading && jsx(RectangleLoading, {
19318
+ shouldShowSpinner: shouldShowSpinner,
19319
+ color: color,
19320
+ radius: borderRadius,
19321
+ size: size
19322
+ })
19323
+ }), children]
19583
19324
  });
19584
19325
  };
19585
19326
 
19586
- const useDebounceTrue = (value, delay = 300) => {
19587
- const [debouncedTrue, setDebouncedTrue] = useState(false);
19588
- const timerRef = useRef(null);
19327
+ // used by form elements such as <input>, <select>, <textarea> to have their own action bound to a single parameter
19328
+ // when inside a <form> the form params are updated when the form element single param is updated
19329
+ const useActionBoundToOneParam = (action, externalValue) => {
19330
+ const actionFirstArgSignal = useSignal(externalValue);
19331
+ const boundAction = useBoundAction(action, actionFirstArgSignal);
19332
+ const getValue = useCallback(() => actionFirstArgSignal.value, []);
19333
+ const setValue = useCallback((value) => {
19334
+ actionFirstArgSignal.value = value;
19335
+ }, []);
19336
+ const externalValueRef = useRef(externalValue);
19337
+ if (externalValue !== externalValueRef.current) {
19338
+ externalValueRef.current = externalValue;
19339
+ setValue(externalValue);
19340
+ }
19589
19341
 
19590
- useLayoutEffect(() => {
19591
- // If value is true or becomes true, start a timer
19592
- if (value) {
19593
- timerRef.current = setTimeout(() => {
19594
- setDebouncedTrue(true);
19595
- }, delay);
19596
- } else {
19597
- // If value becomes false, clear any pending timer and immediately set to false
19598
- if (timerRef.current) {
19599
- clearTimeout(timerRef.current);
19600
- timerRef.current = null;
19601
- }
19602
- setDebouncedTrue(false);
19603
- }
19342
+ const value = getValue();
19343
+ return [boundAction, value, setValue];
19344
+ };
19345
+ const useActionBoundToOneArrayParam = (
19346
+ action,
19347
+ name,
19348
+ externalValue,
19349
+ fallbackValue,
19350
+ defaultValue,
19351
+ ) => {
19352
+ const [boundAction, value, setValue] = useActionBoundToOneParam(
19353
+ action,
19354
+ name);
19604
19355
 
19605
- // Cleanup function
19606
- return () => {
19607
- if (timerRef.current) {
19608
- clearTimeout(timerRef.current);
19609
- }
19610
- };
19611
- }, [value, delay]);
19356
+ const add = (valueToAdd, valueArray = value) => {
19357
+ setValue(addIntoArray(valueArray, valueToAdd));
19358
+ };
19612
19359
 
19613
- return debouncedTrue;
19614
- };
19360
+ const remove = (valueToRemove, valueArray = value) => {
19361
+ setValue(removeFromArray(valueArray, valueToRemove));
19362
+ };
19615
19363
 
19616
- const useNetworkSpeed = () => {
19617
- return networkSpeedSignal.value;
19364
+ const result = [boundAction, value, setValue];
19365
+ result.add = add;
19366
+ result.remove = remove;
19367
+ return result;
19368
+ };
19369
+ // used by <details> to just call their action
19370
+ const useAction = (action, paramsSignal) => {
19371
+ return useBoundAction(action, paramsSignal);
19618
19372
  };
19619
19373
 
19620
- const connection =
19621
- window.navigator.connection ||
19622
- window.navigator.mozConnection ||
19623
- window.navigator.webkitConnection;
19624
-
19625
- const getNetworkSpeed = () => {
19626
- // ✅ Network Information API (support moderne)
19627
- if (!connection) {
19628
- return "3g";
19374
+ const useBoundAction = (action, actionParamsSignal) => {
19375
+ const actionRef = useRef();
19376
+ const actionCallbackRef = useRef();
19377
+
19378
+ if (!action) {
19379
+ return null;
19629
19380
  }
19630
- if (connection) {
19631
- const effectiveType = connection.effectiveType;
19632
- if (effectiveType) {
19633
- return effectiveType; // "slow-2g", "2g", "3g", "4g", "5g"
19381
+ if (isFunctionButNotAnActionFunction(action)) {
19382
+ actionCallbackRef.current = action;
19383
+ const existingAction = actionRef.current;
19384
+ if (existingAction) {
19385
+ return existingAction;
19634
19386
  }
19635
- const downlink = connection.downlink;
19636
- if (downlink) {
19637
- // downlink is in Mbps
19638
- if (downlink < 1) return "slow-2g"; // < 1 Mbps
19639
- if (downlink < 2.5) return "2g"; // 1-2.5 Mbps
19640
- if (downlink < 10) return "3g"; // 2.5-10 Mbps
19641
- return "4g"; // > 10 Mbps
19387
+ const actionFromFunction = createAction(
19388
+ (...args) => {
19389
+ return actionCallbackRef.current?.(...args);
19390
+ },
19391
+ {
19392
+ name: action.name,
19393
+ // We don't want to give empty params by default
19394
+ // we want to give undefined for regular functions
19395
+ params: undefined,
19396
+ },
19397
+ );
19398
+ if (!actionParamsSignal) {
19399
+ actionRef.current = actionFromFunction;
19400
+ return actionFromFunction;
19642
19401
  }
19402
+ const actionBoundToParams =
19403
+ actionFromFunction.bindParams(actionParamsSignal);
19404
+ actionRef.current = actionBoundToParams;
19405
+ return actionBoundToParams;
19643
19406
  }
19644
- return "3g";
19407
+ if (actionParamsSignal) {
19408
+ return action.bindParams(actionParamsSignal);
19409
+ }
19410
+ return action;
19645
19411
  };
19646
19412
 
19647
- const updateNetworkSpeed = () => {
19648
- networkSpeedSignal.value = getNetworkSpeed();
19413
+ const isFunctionButNotAnActionFunction = (action) => {
19414
+ return typeof action === "function" && !action.isAction;
19649
19415
  };
19650
19416
 
19651
- const networkSpeedSignal = signal(getNetworkSpeed());
19417
+ const ErrorBoundaryContext = createContext(null);
19652
19418
 
19653
- const setupNetworkMonitoring = () => {
19654
- const cleanupFunctions = [];
19419
+ const useResetErrorBoundary = () => {
19420
+ const resetErrorBoundary = useContext(ErrorBoundaryContext);
19421
+ return resetErrorBoundary;
19422
+ };
19655
19423
 
19656
- // 1. Écouter les changements natifs
19424
+ const addCustomMessage = (element, key, message, options) => {
19425
+ const customConstraintValidation =
19426
+ element.__validationInterface__ ||
19427
+ (element.__validationInterface__ =
19428
+ installCustomConstraintValidation(element));
19657
19429
 
19658
- if (connection) {
19659
- connection.addEventListener("change", updateNetworkSpeed);
19660
- cleanupFunctions.push(() => {
19661
- connection.removeEventListener("change", updateNetworkSpeed);
19662
- });
19663
- }
19430
+ return customConstraintValidation.addCustomMessage(key, message, options);
19431
+ };
19664
19432
 
19665
- // 2. Polling de backup (toutes les 60 secondes)
19666
- const pollInterval = setInterval(updateNetworkSpeed, 60000);
19667
- cleanupFunctions.push(() => clearInterval(pollInterval));
19433
+ const removeCustomMessage = (element, key) => {
19434
+ const customConstraintValidation = element.__validationInterface__;
19435
+ if (!customConstraintValidation) {
19436
+ return;
19437
+ }
19438
+ customConstraintValidation.removeCustomMessage(key);
19439
+ };
19668
19440
 
19669
- // 3. Vérifier lors de la reprise d'activité
19670
- const handleVisibilityChange = () => {
19671
- if (!document.hidden) {
19672
- updateNetworkSpeed();
19441
+ const useExecuteAction = (
19442
+ elementRef,
19443
+ {
19444
+ errorEffect = "show_validation_message", // "show_validation_message" or "throw"
19445
+ errorMapping,
19446
+ } = {},
19447
+ ) => {
19448
+ // see https://medium.com/trabe/catching-asynchronous-errors-in-react-using-error-boundaries-5e8a5fd7b971
19449
+ // and https://codepen.io/dmail/pen/XJJqeGp?editors=0010
19450
+ // To change if https://github.com/preactjs/preact/issues/4754 lands
19451
+ const [error, setError] = useState(null);
19452
+ const resetErrorBoundary = useResetErrorBoundary();
19453
+ useLayoutEffect(() => {
19454
+ if (error) {
19455
+ throw error;
19673
19456
  }
19674
- };
19675
-
19676
- document.addEventListener("visibilitychange", handleVisibilityChange);
19677
- cleanupFunctions.push(() => {
19678
- document.removeEventListener("visibilitychange", handleVisibilityChange);
19679
- });
19457
+ }, [error]);
19680
19458
 
19681
- // 4. Vérifier lors de la reprise de connexion
19682
- const handleOnline = () => {
19683
- updateNetworkSpeed();
19459
+ const validationMessageTargetRef = useRef(null);
19460
+ const addErrorMessage = (error) => {
19461
+ let calloutAnchor = validationMessageTargetRef.current;
19462
+ let message;
19463
+ if (errorMapping) {
19464
+ const errorMappingResult = errorMapping(error);
19465
+ if (typeof errorMappingResult === "string") {
19466
+ message = errorMappingResult;
19467
+ } else if (Error.isError(errorMappingResult)) {
19468
+ message = errorMappingResult;
19469
+ } else if (isValidElement(errorMappingResult)) {
19470
+ message = errorMappingResult;
19471
+ } else if (
19472
+ typeof errorMappingResult === "object" &&
19473
+ errorMappingResult !== null
19474
+ ) {
19475
+ message = errorMappingResult.message || error.message;
19476
+ calloutAnchor = errorMappingResult.target || calloutAnchor;
19477
+ }
19478
+ } else {
19479
+ message = error;
19480
+ }
19481
+ addCustomMessage(calloutAnchor, "action_error", message, {
19482
+ status: "error",
19483
+ // This error should not prevent <form> submission
19484
+ // so whenever user tries to submit the form the error is cleared
19485
+ // (Hitting enter key, clicking on submit button, etc. would allow to re-submit the form in error state)
19486
+ removeOnRequestAction: true,
19487
+ });
19684
19488
  };
19685
-
19686
- window.addEventListener("online", handleOnline);
19687
- cleanupFunctions.push(() => {
19688
- window.removeEventListener("online", handleOnline);
19689
- });
19690
-
19691
- // Cleanup global
19692
- return () => {
19693
- cleanupFunctions.forEach((cleanup) => cleanup());
19489
+ const removeErrorMessage = () => {
19490
+ const validationMessageTarget = validationMessageTargetRef.current;
19491
+ if (validationMessageTarget) {
19492
+ removeCustomMessage(validationMessageTarget, "action_error");
19493
+ }
19694
19494
  };
19695
- };
19696
- setupNetworkMonitoring();
19697
-
19698
- installImportMetaCssBuild(import.meta);/**
19699
- * RectangleLoading Component
19700
- *
19701
- * A responsive loading indicator that dynamically adjusts to fit its container.
19702
- * Displays an animated outline with a traveling dot that follows the container's shape.
19703
- *
19704
- * Features:
19705
- * - Adapts to any container dimensions using ResizeObserver
19706
- * - Scales stroke width, margins and corner radius proportionally
19707
- * - Animates using native SVG animations for smooth performance
19708
- * - High-quality SVG rendering with proper path calculations
19709
- *
19710
- * @param {Object} props - Component props
19711
- * @param {string} [props.color="#383a36"] - Color of the loading indicator
19712
- * @param {number} [props.radius=0] - Corner radius of the rectangle (px)
19713
- */
19714
- import.meta.css = [/* css */`
19715
- .navi_rectangle_loading {
19716
- position: relative;
19717
- display: flex;
19718
- width: 100%;
19719
- height: 100%;
19720
- opacity: 0;
19721
- }
19722
19495
 
19723
- .navi_rectangle_loading[data-visible] {
19724
- opacity: 1;
19725
- }
19726
- `, "@jsenv/navi/src/graphic/loader/rectangle_loading.jsx"];
19727
- const RectangleLoading = ({
19728
- shouldShowSpinner,
19729
- color = "currentColor",
19730
- radius = 0,
19731
- size = 2
19732
- }) => {
19733
- const containerRef = useRef(null);
19734
- const [containerWidth, setContainerWidth] = useState(0);
19735
- const [containerHeight, setContainerHeight] = useState(0);
19736
19496
  useLayoutEffect(() => {
19737
- const container = containerRef.current;
19738
- if (!container) {
19497
+ const element = elementRef.current;
19498
+ if (!element) {
19739
19499
  return null;
19740
19500
  }
19741
- const {
19742
- width,
19743
- height
19744
- } = container.getBoundingClientRect();
19745
- setContainerWidth(width);
19746
- setContainerHeight(height);
19747
- let animationFrameId = null;
19748
- // Create a resize observer to detect changes in the container's dimensions
19749
- const resizeObserver = new ResizeObserver(entries => {
19750
- // Use requestAnimationFrame to debounce updates
19751
- if (animationFrameId) {
19752
- cancelAnimationFrame(animationFrameId);
19753
- }
19754
- animationFrameId = requestAnimationFrame(() => {
19755
- const [containerEntry] = entries;
19756
- const {
19757
- width,
19758
- height
19759
- } = containerEntry.contentRect;
19760
- setContainerWidth(width);
19761
- setContainerHeight(height);
19762
- });
19763
- });
19764
- resizeObserver.observe(container);
19501
+ const form = element.tagName === "FORM" ? element : element.form;
19502
+ if (!form) {
19503
+ return null;
19504
+ }
19505
+ const onReset = () => {
19506
+ removeErrorMessage();
19507
+ };
19508
+ form.addEventListener("reset", onReset);
19765
19509
  return () => {
19766
- if (animationFrameId) {
19767
- cancelAnimationFrame(animationFrameId);
19510
+ form.removeEventListener("reset", onReset);
19511
+ };
19512
+ });
19513
+
19514
+ // const errorEffectRef = useRef();
19515
+ // errorEffectRef.current = errorEffect;
19516
+ const executeAction = useCallback(
19517
+ (actionEvent) => {
19518
+ const { action, actionOrigin, requester, event, method } =
19519
+ actionEvent.detail;
19520
+ const sharedActionEventDetail = {
19521
+ action,
19522
+ actionOrigin,
19523
+ requester,
19524
+ event,
19525
+ method,
19526
+ };
19527
+
19528
+ const dispatchCustomEvent = (type, options) => {
19529
+ const element = elementRef.current;
19530
+ const customEvent = new CustomEvent(type, options);
19531
+ element.dispatchEvent(customEvent);
19532
+ };
19533
+ if (resetErrorBoundary) {
19534
+ resetErrorBoundary();
19768
19535
  }
19769
- resizeObserver.disconnect();
19770
- };
19771
- }, []);
19772
- return jsx("span", {
19773
- ref: containerRef,
19774
- className: "navi_rectangle_loading",
19775
- "data-visible": shouldShowSpinner ? "" : undefined,
19776
- children: containerWidth > 0 && containerHeight > 0 && jsx(RectangleLoadingSvg, {
19777
- radius: radius,
19778
- color: color,
19779
- width: containerWidth,
19780
- height: containerHeight,
19781
- strokeWidth: size
19782
- })
19783
- });
19784
- };
19785
- const RectangleLoadingSvg = ({
19786
- width,
19787
- height,
19788
- color,
19789
- radius,
19790
- trailColor = "transparent",
19791
- strokeWidth
19792
- }) => {
19793
- const margin = Math.max(2, Math.min(width, height) * 0.03);
19536
+ removeErrorMessage();
19537
+ setError(null);
19794
19538
 
19795
- // Calculate the drawable area
19796
- const drawableWidth = width - margin * 2;
19797
- const drawableHeight = height - margin * 2;
19539
+ const validationMessageTarget = requester || elementRef.current;
19540
+ validationMessageTargetRef.current = validationMessageTarget;
19798
19541
 
19799
- // ✅ Check if this should be a circle - only if width and height are nearly equal
19800
- const maxPossibleRadius = Math.min(drawableWidth, drawableHeight) / 2;
19801
- radius = resolveCSSSize(radius, {
19802
- availableSize: Math.min(width, height)
19803
- });
19804
- const actualRadius = Math.min(radius || Math.min(drawableWidth, drawableHeight) * 0.05, maxPossibleRadius // ✅ Limité au radius maximum possible
19805
- );
19806
- const aspectRatio = Math.max(drawableWidth, drawableHeight) / Math.min(drawableWidth, drawableHeight);
19807
- const isNearlySquare = aspectRatio <= 1.2; // Allow some tolerance for nearly square shapes
19808
- const isCircle = isNearlySquare && actualRadius >= maxPossibleRadius * 0.95;
19809
- let pathLength;
19810
- let rectPath;
19811
- if (isCircle) {
19812
- // ✅ Circle: perimeter = 2πr
19813
- pathLength = 2 * Math.PI * actualRadius;
19542
+ dispatchCustomEvent("actionstart", {
19543
+ detail: sharedActionEventDetail,
19544
+ });
19814
19545
 
19815
- // ✅ Circle path centered in the drawable area
19816
- const centerX = margin + drawableWidth / 2;
19817
- const centerY = margin + drawableHeight / 2;
19818
- rectPath = `
19819
- M ${centerX + actualRadius},${centerY}
19820
- A ${actualRadius},${actualRadius} 0 1 1 ${centerX - actualRadius},${centerY}
19821
- A ${actualRadius},${actualRadius} 0 1 1 ${centerX + actualRadius},${centerY}
19822
- `;
19823
- } else {
19824
- // ✅ Rectangle: calculate perimeter properly
19825
- const straightEdges = 2 * (drawableWidth - 2 * actualRadius) + 2 * (drawableHeight - 2 * actualRadius);
19826
- const cornerArcs = actualRadius > 0 ? 2 * Math.PI * actualRadius : 0;
19827
- pathLength = straightEdges + cornerArcs;
19828
- rectPath = `
19829
- M ${margin + actualRadius},${margin}
19830
- L ${margin + drawableWidth - actualRadius},${margin}
19831
- A ${actualRadius},${actualRadius} 0 0 1 ${margin + drawableWidth},${margin + actualRadius}
19832
- L ${margin + drawableWidth},${margin + drawableHeight - actualRadius}
19833
- A ${actualRadius},${actualRadius} 0 0 1 ${margin + drawableWidth - actualRadius},${margin + drawableHeight}
19834
- L ${margin + actualRadius},${margin + drawableHeight}
19835
- A ${actualRadius},${actualRadius} 0 0 1 ${margin},${margin + drawableHeight - actualRadius}
19836
- L ${margin},${margin + actualRadius}
19837
- A ${actualRadius},${actualRadius} 0 0 1 ${margin + actualRadius},${margin}
19838
- `;
19839
- }
19546
+ return action[method]({
19547
+ reason: `"${event.type}" event on ${(() => {
19548
+ const target = event.target;
19549
+ const tagName = target.tagName.toLowerCase();
19840
19550
 
19841
- // Fixed segment size in pixels
19842
- const maxSegmentSize = 40;
19843
- const segmentLength = Math.min(maxSegmentSize, pathLength * 0.25);
19844
- const gapLength = pathLength - segmentLength;
19551
+ if (target.id) {
19552
+ return `${tagName}#${target.id}`;
19553
+ }
19845
19554
 
19846
- // Vitesse constante en pixels par seconde
19847
- const networkSpeed = useNetworkSpeed();
19848
- const pixelsPerSecond = {
19849
- "slow-2g": 40,
19850
- "2g": 60,
19851
- "3g": 80,
19852
- "4g": 120
19853
- }[networkSpeed] || 80;
19854
- const animationDuration = Math.max(1.5, pathLength / pixelsPerSecond);
19555
+ const uiName = target.getAttribute("data-ui-name");
19556
+ if (uiName) {
19557
+ return `${tagName}[data-ui-name="${uiName}"]`;
19558
+ }
19855
19559
 
19856
- // ✅ Calculate correct offset based on actual segment size
19857
- const segmentRatio = segmentLength / pathLength;
19858
- const circleOffset = -animationDuration * segmentRatio;
19859
- return jsxs("svg", {
19860
- width: "100%",
19861
- height: "100%",
19862
- viewBox: `0 0 ${width} ${height}`,
19863
- preserveAspectRatio: "none",
19864
- style: "overflow: visible",
19865
- xmlns: "http://www.w3.org/2000/svg",
19866
- "shape-rendering": "geometricPrecision",
19867
- children: [isCircle ? jsx("circle", {
19868
- cx: margin + drawableWidth / 2,
19869
- cy: margin + drawableHeight / 2,
19870
- r: actualRadius,
19871
- fill: "none",
19872
- stroke: trailColor,
19873
- strokeWidth: strokeWidth
19874
- }) : jsx("rect", {
19875
- x: margin,
19876
- y: margin,
19877
- width: drawableWidth,
19878
- height: drawableHeight,
19879
- fill: "none",
19880
- stroke: trailColor,
19881
- strokeWidth: strokeWidth,
19882
- rx: actualRadius
19883
- }), jsx("path", {
19884
- d: rectPath,
19885
- fill: "none",
19886
- stroke: color,
19887
- strokeWidth: strokeWidth,
19888
- strokeLinecap: "round",
19889
- strokeDasharray: `${segmentLength} ${gapLength}`,
19890
- pathLength: pathLength,
19891
- children: jsx("animate", {
19892
- attributeName: "stroke-dashoffset",
19893
- from: pathLength,
19894
- to: "0",
19895
- dur: `${animationDuration}s`,
19896
- repeatCount: "indefinite",
19897
- begin: "0s"
19898
- })
19899
- }), jsx("circle", {
19900
- r: strokeWidth,
19901
- fill: color,
19902
- children: jsx("animateMotion", {
19903
- path: rectPath,
19904
- dur: `${animationDuration}s`,
19905
- repeatCount: "indefinite",
19906
- rotate: "auto",
19907
- begin: `${circleOffset}s`
19908
- })
19909
- })]
19910
- });
19560
+ return `<${tagName}>`;
19561
+ })()}`,
19562
+ onAbort: (reason) => {
19563
+ if (
19564
+ // at this stage the action side effect might have removed the <element> from the DOM
19565
+ // (in theory no because action side effect are batched to happen after)
19566
+ // but other side effects might do this
19567
+ elementRef.current
19568
+ ) {
19569
+ dispatchCustomEvent("actionabort", {
19570
+ detail: {
19571
+ ...sharedActionEventDetail,
19572
+ reason,
19573
+ },
19574
+ });
19575
+ }
19576
+ },
19577
+ onError: (error) => {
19578
+ if (
19579
+ // at this stage the action side effect might have removed the <element> from the DOM
19580
+ // (in theory no because action side effect are batched to happen after)
19581
+ // but other side effects might do this
19582
+ elementRef.current
19583
+ ) {
19584
+ dispatchCustomEvent("actionerror", {
19585
+ detail: {
19586
+ ...sharedActionEventDetail,
19587
+ error,
19588
+ },
19589
+ });
19590
+ }
19591
+ if (errorEffect === "show_validation_message") {
19592
+ addErrorMessage(error);
19593
+ } else if (errorEffect === "throw") {
19594
+ setError(error);
19595
+ }
19596
+ },
19597
+ onComplete: (data) => {
19598
+ if (
19599
+ // at this stage the action side effect might have removed the <element> from the DOM
19600
+ // (in theory no because action side effect are batched to happen after)
19601
+ // but other side effects might do this
19602
+ elementRef.current
19603
+ ) {
19604
+ dispatchCustomEvent("actionend", {
19605
+ detail: {
19606
+ ...sharedActionEventDetail,
19607
+ data,
19608
+ },
19609
+ });
19610
+ }
19611
+ },
19612
+ });
19613
+ },
19614
+ [errorEffect],
19615
+ );
19616
+
19617
+ return executeAction;
19911
19618
  };
19912
19619
 
19913
- installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
19914
- .navi_loading_rectangle_wrapper {
19915
- position: absolute;
19916
- top: var(--rectangle-top, 0);
19917
- right: var(--rectangle-right, 0);
19918
- bottom: var(--rectangle-bottom, 0);
19919
- left: var(--rectangle-left, 0);
19920
- z-index: 1;
19921
- opacity: 0;
19922
- pointer-events: none;
19620
+ const detectMac = () => {
19621
+ // Modern way using User-Agent Client Hints API
19622
+ if (window.navigator.userAgentData) {
19623
+ return window.navigator.userAgentData.platform === "macOS";
19624
+ }
19625
+ // Fallback to userAgent string parsing
19626
+ return /Mac|iPhone|iPad|iPod/.test(window.navigator.userAgent);
19627
+ };
19628
+ const isMac = detectMac();
19629
+
19630
+ // Maps canonical browser key names to their user-friendly aliases.
19631
+ // Used for both event matching and ARIA normalization.
19632
+ const keyMapping = {
19633
+ " ": { alias: ["space"] },
19634
+ "escape": { alias: ["esc"] },
19635
+ "arrowup": { alias: ["up"] },
19636
+ "arrowdown": { alias: ["down"] },
19637
+ "arrowleft": { alias: ["left"] },
19638
+ "arrowright": { alias: ["right"] },
19639
+ "delete": { alias: ["del"] },
19640
+ // Platform-specific mappings
19641
+ ...(isMac
19642
+ ? { delete: { alias: ["backspace"] } }
19643
+ : { backspace: { alias: ["delete"] } }),
19644
+ };
19645
+
19646
+ const activeShortcutsSignal = signal([]);
19647
+ const shortcutsMap = new Map();
19923
19648
 
19924
- &[data-visible] {
19925
- opacity: 1;
19926
- }
19649
+ const areShortcutsEqual = (shortcutA, shortcutB) => {
19650
+ return (
19651
+ shortcutA.key === shortcutB.key &&
19652
+ shortcutA.description === shortcutB.description &&
19653
+ shortcutA.enabled === shortcutB.enabled
19654
+ );
19655
+ };
19656
+
19657
+ const areShortcutArraysEqual = (arrayA, arrayB) => {
19658
+ if (arrayA.length !== arrayB.length) {
19659
+ return false;
19927
19660
  }
19928
- `, "@jsenv/navi/src/graphic/loader/loader_background.jsx"];
19929
- const LoaderBackground = ({
19930
- loading,
19931
- containerRef,
19932
- targetSelector,
19933
- color,
19934
- inset = 0,
19935
- borderRadius = 0,
19936
- spacingTop = 0,
19937
- spacingLeft = 0,
19938
- spacingBottom = 0,
19939
- spacingRight = 0,
19940
- children
19941
- }) => {
19942
- if (containerRef) {
19943
- const container = containerRef.current;
19944
- if (!container) {
19945
- return children;
19661
+
19662
+ for (let i = 0; i < arrayA.length; i++) {
19663
+ if (!areShortcutsEqual(arrayA[i], arrayB[i])) {
19664
+ return false;
19946
19665
  }
19947
- return createPortal(jsx(LoaderBackgroundWithPortal, {
19948
- container: container,
19949
- loading: loading,
19950
- color: color,
19951
- inset: inset,
19952
- spacingTop: spacingTop,
19953
- spacingLeft: spacingLeft,
19954
- spacingBottom: spacingBottom,
19955
- spacingRight: spacingRight,
19956
- children: children
19957
- }), container);
19958
19666
  }
19959
- return jsx(LoaderBackgroundBasic, {
19960
- targetSelector: targetSelector,
19961
- loading: loading,
19962
- color: color,
19963
- inset: inset,
19964
- borderRadius: borderRadius,
19965
- spacingTop: spacingTop,
19966
- spacingLeft: spacingLeft,
19967
- spacingBottom: spacingBottom,
19968
- spacingRight: spacingRight,
19969
- children: children
19970
- });
19667
+
19668
+ return true;
19971
19669
  };
19972
- const LoaderBackgroundWithPortal = ({
19973
- container,
19974
- loading,
19975
- color,
19976
- inset,
19977
- borderRadius,
19978
- spacingTop,
19979
- spacingLeft,
19980
- spacingBottom,
19981
- spacingRight,
19982
- children
19983
- }) => {
19984
- const shouldShowSpinner = useDebounceTrue(loading, 300);
19985
- if (!shouldShowSpinner) {
19986
- return children;
19670
+
19671
+ const updateActiveShortcuts = () => {
19672
+ const activeElement = activeElementSignal.peek();
19673
+ const currentActiveShortcuts = activeShortcutsSignal.peek();
19674
+ const activeShortcuts = [];
19675
+ for (const [element, { shortcuts }] of shortcutsMap) {
19676
+ if (element === activeElement || element.contains(activeElement)) {
19677
+ activeShortcuts.push(...shortcuts);
19678
+ }
19987
19679
  }
19988
- container.style.position = "relative";
19989
- let paddingTop = 0;
19990
- if (container.nodeName === "DETAILS") {
19991
- paddingTop = container.querySelector("summary").offsetHeight;
19680
+
19681
+ // Only update if shortcuts have actually changed
19682
+ if (!areShortcutArraysEqual(currentActiveShortcuts, activeShortcuts)) {
19683
+ activeShortcutsSignal.value = activeShortcuts;
19992
19684
  }
19993
- return jsxs(Fragment, {
19994
- children: [jsx("div", {
19995
- style: {
19996
- position: "absolute",
19997
- top: `${inset + paddingTop + spacingTop}px`,
19998
- bottom: `${inset + spacingBottom}px`,
19999
- left: `${inset + spacingLeft}px`,
20000
- right: `${inset + spacingRight}px`
20001
- },
20002
- children: shouldShowSpinner && jsx(RectangleLoading, {
20003
- color: color,
20004
- radius: borderRadius
20005
- })
20006
- }), children]
20007
- });
20008
19685
  };
20009
- const LoaderBackgroundBasic = ({
20010
- loading,
20011
- targetSelector,
20012
- color,
20013
- borderWidth = 0,
20014
- borderRadius = 0,
20015
- spacingTop,
20016
- spacingLeft,
20017
- spacingBottom,
20018
- spacingRight,
20019
- marginTop = 0,
20020
- marginLeft = 0,
20021
- marginBottom = 0,
20022
- marginRight = 0,
20023
- paddingTop = 0,
20024
- paddingLeft = 0,
20025
- paddingBottom = 0,
20026
- paddingRight = 0,
20027
- inset,
20028
- children
20029
- }) => {
20030
- const shouldShowSpinner = useDebounceTrue(loading, 300);
20031
- const rectangleRef = useRef(null);
20032
- spacingTop += inset;
20033
- // spacingTop += outlineOffset;
20034
- // spacingTop -= borderTopWidth;
20035
- spacingTop += marginTop;
20036
- spacingLeft += inset;
20037
- // spacingLeft += outlineOffset;
20038
- // spacingLeft -= borderLeftWidth;
20039
- spacingLeft += marginLeft;
20040
- spacingRight += inset;
20041
- // spacingRight += outlineOffset;
20042
- // spacingRight -= borderRightWidth;
20043
- spacingRight += marginRight;
20044
- spacingBottom += inset;
20045
- // spacingBottom += outlineOffset;
20046
- // spacingBottom -= borderBottomWidth;
20047
- spacingBottom += marginBottom;
20048
- if (targetSelector) {
20049
- // oversimplification that actually works
20050
- // (simplified because it assumes the targeted element is a direct child of the contained element which may have padding)
20051
- spacingTop += paddingTop;
20052
- spacingLeft += paddingLeft;
20053
- spacingRight += paddingRight;
20054
- spacingBottom += paddingBottom;
19686
+ effect(() => {
19687
+ // eslint-disable-next-line no-unused-expressions
19688
+ activeElementSignal.value;
19689
+ updateActiveShortcuts();
19690
+ });
19691
+ const addShortcuts = (element, shortcuts) => {
19692
+ shortcutsMap.set(element, { shortcuts });
19693
+ updateActiveShortcuts();
19694
+ };
19695
+ const removeShortcuts = (element) => {
19696
+ shortcutsMap.delete(element);
19697
+ updateActiveShortcuts();
19698
+ };
19699
+
19700
+ const useKeyboardShortcuts = (
19701
+ elementRef,
19702
+ shortcuts,
19703
+ {
19704
+ onActionPrevented,
19705
+ onActionStart,
19706
+ onActionAbort,
19707
+ onActionError,
19708
+ onActionEnd,
19709
+ allowConcurrentActions,
19710
+ } = {},
19711
+ ) => {
19712
+ if (!elementRef) {
19713
+ throw new Error(
19714
+ "useKeyboardShortcuts requires an elementRef to attach shortcuts to.",
19715
+ );
20055
19716
  }
20056
- const maxBorderWidth = Math.max(borderWidth);
20057
- const halfMaxBorderSize = maxBorderWidth / 2;
20058
- const size = halfMaxBorderSize < 2 ? 2 : halfMaxBorderSize;
20059
- const lineHalfSize = size / 2;
20060
- spacingTop -= lineHalfSize;
20061
- spacingLeft -= lineHalfSize;
20062
- spacingRight -= lineHalfSize;
20063
- spacingBottom -= lineHalfSize;
20064
- return jsxs(Fragment, {
20065
- children: [jsx("span", {
20066
- ref: rectangleRef,
20067
- className: "navi_loading_rectangle_wrapper",
20068
- "data-visible": shouldShowSpinner ? "" : undefined,
20069
- style: {
20070
- "--rectangle-top": `${spacingTop}px`,
20071
- "--rectangle-left": `${spacingLeft}px`,
20072
- "--rectangle-bottom": `${spacingBottom}px`,
20073
- "--rectangle-right": `${spacingRight}px`
20074
- },
20075
- children: loading && jsx(RectangleLoading, {
20076
- shouldShowSpinner: shouldShowSpinner,
20077
- color: color,
20078
- radius: borderRadius,
20079
- size: size
20080
- })
20081
- }), children]
19717
+
19718
+ const executeAction = useExecuteAction(elementRef);
19719
+ const shortcutActionIsBusyRef = useRef(false);
19720
+ useActionEvents(elementRef, {
19721
+ actionOrigin: "keyboard_shortcut",
19722
+ onPrevented: onActionPrevented,
19723
+ onAction: (actionEvent) => {
19724
+ const { shortcut } = actionEvent.detail.meta || {};
19725
+ if (!shortcut) {
19726
+ // not a shortcut (an other interaction triggered the action, don't request it again)
19727
+ return;
19728
+ }
19729
+ // action can be a function or an action object, whem a function we must "wrap" it in a function returning that function
19730
+ // otherwise setState would call that action immediately
19731
+ // setAction(() => actionEvent.detail.action);
19732
+ executeAction(actionEvent, {
19733
+ requester: document.activeElement,
19734
+ });
19735
+ },
19736
+ onStart: (e) => {
19737
+ const { shortcut } = e.detail.meta || {};
19738
+ if (!shortcut) {
19739
+ return;
19740
+ }
19741
+ if (!allowConcurrentActions) {
19742
+ shortcutActionIsBusyRef.current = true;
19743
+ }
19744
+ shortcut.onStart?.(e);
19745
+ onActionStart?.(e);
19746
+ },
19747
+ onAbort: (e) => {
19748
+ const { shortcut } = e.detail.meta || {};
19749
+ if (!shortcut) {
19750
+ return;
19751
+ }
19752
+ shortcutActionIsBusyRef.current = false;
19753
+ shortcut.onAbort?.(e);
19754
+ onActionAbort?.(e);
19755
+ },
19756
+ onError: (error, e) => {
19757
+ const { shortcut } = e.detail.meta || {};
19758
+ if (!shortcut) {
19759
+ return;
19760
+ }
19761
+ shortcutActionIsBusyRef.current = false;
19762
+ shortcut.onError?.(error, e);
19763
+ onActionError?.(error, e);
19764
+ },
19765
+ onEnd: (e) => {
19766
+ const { shortcut } = e.detail.meta || {};
19767
+ if (!shortcut) {
19768
+ return;
19769
+ }
19770
+ shortcutActionIsBusyRef.current = false;
19771
+ shortcut.onEnd?.(e);
19772
+ onActionEnd?.(e);
19773
+ },
20082
19774
  });
20083
- };
20084
19775
 
20085
- // used by form elements such as <input>, <select>, <textarea> to have their own action bound to a single parameter
20086
- // when inside a <form> the form params are updated when the form element single param is updated
20087
- const useActionBoundToOneParam = (action, externalValue) => {
20088
- const actionFirstArgSignal = useSignal(externalValue);
20089
- const boundAction = useBoundAction(action, actionFirstArgSignal);
20090
- const getValue = useCallback(() => actionFirstArgSignal.value, []);
20091
- const setValue = useCallback((value) => {
20092
- actionFirstArgSignal.value = value;
20093
- }, []);
20094
- const externalValueRef = useRef(externalValue);
20095
- if (externalValue !== externalValueRef.current) {
20096
- externalValueRef.current = externalValue;
20097
- setValue(externalValue);
19776
+ const shortcutDeps = [];
19777
+ for (const shortcut of shortcuts) {
19778
+ shortcutDeps.push(
19779
+ shortcut.key,
19780
+ shortcut.description,
19781
+ shortcut.enabled,
19782
+ shortcut.confirmMessage,
19783
+ );
19784
+ shortcut.action = useAction(shortcut.action);
20098
19785
  }
20099
19786
 
20100
- const value = getValue();
20101
- return [boundAction, value, setValue];
20102
- };
20103
- const useActionBoundToOneArrayParam = (
20104
- action,
20105
- name,
20106
- externalValue,
20107
- fallbackValue,
20108
- defaultValue,
20109
- ) => {
20110
- const [boundAction, value, setValue] = useActionBoundToOneParam(
20111
- action,
20112
- name);
20113
-
20114
- const add = (valueToAdd, valueArray = value) => {
20115
- setValue(addIntoArray(valueArray, valueToAdd));
20116
- };
19787
+ useEffect(() => {
19788
+ const element = elementRef.current;
19789
+ if (!element) {
19790
+ return null;
19791
+ }
19792
+ const shortcutsCopy = [];
19793
+ for (const shortcutCandidate of shortcuts) {
19794
+ shortcutsCopy.push({
19795
+ ...shortcutCandidate,
19796
+ handler: (keyboardEvent) => {
19797
+ if (shortcutCandidate.handler) {
19798
+ return shortcutCandidate.handler(keyboardEvent);
19799
+ }
19800
+ if (shortcutActionIsBusyRef.current) {
19801
+ return false;
19802
+ }
19803
+ const { action } = shortcutCandidate;
19804
+ const actionWithEvent = action.bindParams(keyboardEvent);
19805
+ return requestAction(element, actionWithEvent, {
19806
+ actionOrigin: "keyboard_shortcut",
19807
+ event: keyboardEvent,
19808
+ requester: document.activeElement,
19809
+ confirmMessage: shortcutCandidate.confirmMessage,
19810
+ meta: {
19811
+ shortcut: shortcutCandidate,
19812
+ },
19813
+ });
19814
+ },
19815
+ });
19816
+ }
20117
19817
 
20118
- const remove = (valueToRemove, valueArray = value) => {
20119
- setValue(removeFromArray(valueArray, valueToRemove));
20120
- };
19818
+ addShortcuts(element, shortcuts);
20121
19819
 
20122
- const result = [boundAction, value, setValue];
20123
- result.add = add;
20124
- result.remove = remove;
20125
- return result;
20126
- };
20127
- // used by <details> to just call their action
20128
- const useAction = (action, paramsSignal) => {
20129
- return useBoundAction(action, paramsSignal);
19820
+ const onKeydown = (event) => {
19821
+ applyKeyboardShortcuts(shortcutsCopy, event);
19822
+ };
19823
+ element.addEventListener("keydown", onKeydown);
19824
+ return () => {
19825
+ element.removeEventListener("keydown", onKeydown);
19826
+ removeShortcuts(element);
19827
+ };
19828
+ }, [shortcutDeps]);
20130
19829
  };
20131
19830
 
20132
- const useBoundAction = (action, actionParamsSignal) => {
20133
- const actionRef = useRef();
20134
- const actionCallbackRef = useRef();
20135
-
20136
- if (!action) {
19831
+ const applyKeyboardShortcuts = (shortcuts, keyboardEvent) => {
19832
+ if (!canInterceptKeys(keyboardEvent)) {
20137
19833
  return null;
20138
19834
  }
20139
- if (isFunctionButNotAnActionFunction(action)) {
20140
- actionCallbackRef.current = action;
20141
- const existingAction = actionRef.current;
20142
- if (existingAction) {
20143
- return existingAction;
19835
+ for (const shortcutCandidate of shortcuts) {
19836
+ let { enabled = true, key } = shortcutCandidate;
19837
+ if (!enabled) {
19838
+ continue;
20144
19839
  }
20145
- const actionFromFunction = createAction(
20146
- (...args) => {
20147
- return actionCallbackRef.current?.(...args);
20148
- },
20149
- {
20150
- name: action.name,
20151
- // We don't want to give empty params by default
20152
- // we want to give undefined for regular functions
20153
- params: undefined,
20154
- },
20155
- );
20156
- if (!actionParamsSignal) {
20157
- actionRef.current = actionFromFunction;
20158
- return actionFromFunction;
19840
+
19841
+ if (typeof key === "function") {
19842
+ const keyReturnValue = key(keyboardEvent);
19843
+ if (!keyReturnValue) {
19844
+ continue;
19845
+ }
19846
+ key = keyReturnValue;
19847
+ }
19848
+ if (!key) {
19849
+ console.error(shortcutCandidate);
19850
+ throw new TypeError(`key is required in keyboard shortcut, got ${key}`);
20159
19851
  }
20160
- const actionBoundToParams =
20161
- actionFromFunction.bindParams(actionParamsSignal);
20162
- actionRef.current = actionBoundToParams;
20163
- return actionBoundToParams;
20164
- }
20165
- if (actionParamsSignal) {
20166
- return action.bindParams(actionParamsSignal);
20167
- }
20168
- return action;
20169
- };
20170
19852
 
20171
- const isFunctionButNotAnActionFunction = (action) => {
20172
- return typeof action === "function" && !action.isAction;
20173
- };
19853
+ // Handle platform-specific combination objects
19854
+ let actualCombination;
19855
+ let crossPlatformCombination;
19856
+ if (typeof key === "object" && key !== null) {
19857
+ actualCombination = isMac ? key.mac : key.other;
19858
+ } else {
19859
+ actualCombination = key;
19860
+ if (containsPlatformSpecificKeys(key)) {
19861
+ crossPlatformCombination = generateCrossPlatformCombination(key);
19862
+ }
19863
+ }
20174
19864
 
20175
- const ErrorBoundaryContext = createContext(null);
19865
+ // Check both the actual combination and cross-platform combination
19866
+ const matchesActual =
19867
+ actualCombination &&
19868
+ keyboardEventIsMatchingKeyCombination(keyboardEvent, actualCombination);
19869
+ const matchesCrossPlatform =
19870
+ crossPlatformCombination &&
19871
+ crossPlatformCombination !== actualCombination &&
19872
+ keyboardEventIsMatchingKeyCombination(
19873
+ keyboardEvent,
19874
+ crossPlatformCombination,
19875
+ );
20176
19876
 
20177
- const useResetErrorBoundary = () => {
20178
- const resetErrorBoundary = useContext(ErrorBoundaryContext);
20179
- return resetErrorBoundary;
19877
+ if (!matchesActual && !matchesCrossPlatform) {
19878
+ continue;
19879
+ }
19880
+ if (typeof enabled === "function" && !enabled(keyboardEvent)) {
19881
+ continue;
19882
+ }
19883
+ const returnValue = shortcutCandidate.handler(keyboardEvent);
19884
+ if (returnValue) {
19885
+ keyboardEvent.preventDefault();
19886
+ }
19887
+ return shortcutCandidate;
19888
+ }
19889
+ return null;
20180
19890
  };
19891
+ const containsPlatformSpecificKeys = (combination) => {
19892
+ const lowerCombination = combination.toLowerCase();
19893
+ const macSpecificKeys = ["command", "cmd"];
20181
19894
 
20182
- const addCustomMessage = (element, key, message, options) => {
20183
- const customConstraintValidation =
20184
- element.__validationInterface__ ||
20185
- (element.__validationInterface__ =
20186
- installCustomConstraintValidation(element));
20187
-
20188
- return customConstraintValidation.addCustomMessage(key, message, options);
19895
+ return macSpecificKeys.some((key) => lowerCombination.includes(key));
20189
19896
  };
19897
+ const generateCrossPlatformCombination = (combination) => {
19898
+ let crossPlatform = combination;
20190
19899
 
20191
- const removeCustomMessage = (element, key) => {
20192
- const customConstraintValidation = element.__validationInterface__;
20193
- if (!customConstraintValidation) {
20194
- return;
19900
+ if (isMac) {
19901
+ // No need to convert anything TO Windows/Linux-specific format since we're on Mac
19902
+ return null;
20195
19903
  }
20196
- customConstraintValidation.removeCustomMessage(key);
20197
- };
20198
-
20199
- const useExecuteAction = (
20200
- elementRef,
20201
- {
20202
- errorEffect = "show_validation_message", // "show_validation_message" or "throw"
20203
- errorMapping,
20204
- } = {},
20205
- ) => {
20206
- // see https://medium.com/trabe/catching-asynchronous-errors-in-react-using-error-boundaries-5e8a5fd7b971
20207
- // and https://codepen.io/dmail/pen/XJJqeGp?editors=0010
20208
- // To change if https://github.com/preactjs/preact/issues/4754 lands
20209
- const [error, setError] = useState(null);
20210
- const resetErrorBoundary = useResetErrorBoundary();
20211
- useLayoutEffect(() => {
20212
- if (error) {
20213
- throw error;
20214
- }
20215
- }, [error]);
19904
+ // If not on Mac but combination contains Mac-specific keys, generate Windows equivalent
19905
+ crossPlatform = crossPlatform.replace(/\bcommand\b/gi, "control");
19906
+ crossPlatform = crossPlatform.replace(/\bcmd\b/gi, "control");
20216
19907
 
20217
- const validationMessageTargetRef = useRef(null);
20218
- const addErrorMessage = (error) => {
20219
- let calloutAnchor = validationMessageTargetRef.current;
20220
- let message;
20221
- if (errorMapping) {
20222
- const errorMappingResult = errorMapping(error);
20223
- if (typeof errorMappingResult === "string") {
20224
- message = errorMappingResult;
20225
- } else if (Error.isError(errorMappingResult)) {
20226
- message = errorMappingResult;
20227
- } else if (isValidElement(errorMappingResult)) {
20228
- message = errorMappingResult;
20229
- } else if (
20230
- typeof errorMappingResult === "object" &&
20231
- errorMappingResult !== null
20232
- ) {
20233
- message = errorMappingResult.message || error.message;
20234
- calloutAnchor = errorMappingResult.target || calloutAnchor;
19908
+ return crossPlatform;
19909
+ };
19910
+ const keyboardEventIsMatchingKeyCombination = (event, keyCombination) => {
19911
+ const keys = keyCombination.toLowerCase().split("+");
19912
+
19913
+ for (const key of keys) {
19914
+ let modifierFound = false;
19915
+
19916
+ // Check if this key is a modifier
19917
+ for (const [eventProperty, config] of Object.entries(modifierKeyMapping)) {
19918
+ const allNames = [...config.names];
19919
+
19920
+ // Add Mac-specific names only if we're on Mac and they exist
19921
+ if (isMac && config.macNames) {
19922
+ allNames.push(...config.macNames);
20235
19923
  }
20236
- } else {
20237
- message = error;
20238
- }
20239
- addCustomMessage(calloutAnchor, "action_error", message, {
20240
- status: "error",
20241
- // This error should not prevent <form> submission
20242
- // so whenever user tries to submit the form the error is cleared
20243
- // (Hitting enter key, clicking on submit button, etc. would allow to re-submit the form in error state)
20244
- removeOnRequestAction: true,
20245
- });
20246
- };
20247
- const removeErrorMessage = () => {
20248
- const validationMessageTarget = validationMessageTargetRef.current;
20249
- if (validationMessageTarget) {
20250
- removeCustomMessage(validationMessageTarget, "action_error");
20251
- }
20252
- };
20253
19924
 
20254
- useLayoutEffect(() => {
20255
- const element = elementRef.current;
20256
- if (!element) {
20257
- return null;
19925
+ if (allNames.includes(key)) {
19926
+ // Check if the corresponding event property is pressed
19927
+ if (!event[eventProperty]) {
19928
+ return false;
19929
+ }
19930
+ modifierFound = true;
19931
+ break;
19932
+ }
20258
19933
  }
20259
- const form = element.tagName === "FORM" ? element : element.form;
20260
- if (!form) {
20261
- return null;
19934
+ if (modifierFound) {
19935
+ continue;
20262
19936
  }
20263
- const onReset = () => {
20264
- removeErrorMessage();
20265
- };
20266
- form.addEventListener("reset", onReset);
20267
- return () => {
20268
- form.removeEventListener("reset", onReset);
20269
- };
20270
- });
20271
19937
 
20272
- // const errorEffectRef = useRef();
20273
- // errorEffectRef.current = errorEffect;
20274
- const executeAction = useCallback(
20275
- (actionEvent) => {
20276
- const { action, actionOrigin, requester, event, method } =
20277
- actionEvent.detail;
20278
- const sharedActionEventDetail = {
20279
- action,
20280
- actionOrigin,
20281
- requester,
20282
- event,
20283
- method,
20284
- };
19938
+ // Check if it's a range pattern like "a-z" or "0-9"
19939
+ if (key.includes("-") && key.length === 3) {
19940
+ const [startChar, dash, endChar] = key;
19941
+ if (dash === "-") {
19942
+ // Only check ranges for single alphanumeric characters
19943
+ const eventKey = event.key.toLowerCase();
19944
+ if (eventKey.length !== 1) {
19945
+ return false; // Not a single character key
19946
+ }
20285
19947
 
20286
- const dispatchCustomEvent = (type, options) => {
20287
- const element = elementRef.current;
20288
- const customEvent = new CustomEvent(type, options);
20289
- element.dispatchEvent(customEvent);
20290
- };
20291
- if (resetErrorBoundary) {
20292
- resetErrorBoundary();
20293
- }
20294
- removeErrorMessage();
20295
- setError(null);
19948
+ // Only allow a-z and 0-9 ranges
19949
+ const isValidRange =
19950
+ (startChar >= "a" && endChar <= "z") ||
19951
+ (startChar >= "0" && endChar <= "9");
20296
19952
 
20297
- const validationMessageTarget = requester || elementRef.current;
20298
- validationMessageTargetRef.current = validationMessageTarget;
19953
+ if (!isValidRange) {
19954
+ return false; // Invalid range pattern
19955
+ }
20299
19956
 
20300
- dispatchCustomEvent("actionstart", {
20301
- detail: sharedActionEventDetail,
20302
- });
19957
+ const eventKeyCode = eventKey.charCodeAt(0);
19958
+ const startCode = startChar.charCodeAt(0);
19959
+ const endCode = endChar.charCodeAt(0);
20303
19960
 
20304
- return action[method]({
20305
- reason: `"${event.type}" event on ${(() => {
20306
- const target = event.target;
20307
- const tagName = target.tagName.toLowerCase();
19961
+ if (eventKeyCode >= startCode && eventKeyCode <= endCode) {
19962
+ continue; // Range matched
19963
+ }
19964
+ return false; // Range not matched
19965
+ }
19966
+ }
20308
19967
 
20309
- if (target.id) {
20310
- return `${tagName}#${target.id}`;
20311
- }
19968
+ // If it's not a modifier or range, check if it matches the actual key
19969
+ if (!isSameKey(event.key, key)) {
19970
+ return false;
19971
+ }
19972
+ }
19973
+ return true;
19974
+ };
19975
+ // Configuration for mapping shortcut key names to browser event properties
19976
+ const modifierKeyMapping = {
19977
+ metaKey: {
19978
+ names: ["meta"],
19979
+ macNames: ["command", "cmd"],
19980
+ },
19981
+ ctrlKey: {
19982
+ names: ["control", "ctrl"],
19983
+ },
19984
+ shiftKey: {
19985
+ names: ["shift"],
19986
+ },
19987
+ altKey: {
19988
+ names: ["alt"],
19989
+ macNames: ["option"],
19990
+ },
19991
+ };
19992
+ const isSameKey = (browserEventKey, key) => {
19993
+ browserEventKey = browserEventKey.toLowerCase();
19994
+ key = key.toLowerCase();
20312
19995
 
20313
- const uiName = target.getAttribute("data-ui-name");
20314
- if (uiName) {
20315
- return `${tagName}[data-ui-name="${uiName}"]`;
20316
- }
19996
+ if (browserEventKey === key) {
19997
+ return true;
19998
+ }
20317
19999
 
20318
- return `<${tagName}>`;
20319
- })()}`,
20320
- onAbort: (reason) => {
20321
- if (
20322
- // at this stage the action side effect might have removed the <element> from the DOM
20323
- // (in theory no because action side effect are batched to happen after)
20324
- // but other side effects might do this
20325
- elementRef.current
20326
- ) {
20327
- dispatchCustomEvent("actionabort", {
20328
- detail: {
20329
- ...sharedActionEventDetail,
20330
- reason,
20331
- },
20332
- });
20333
- }
20334
- },
20335
- onError: (error) => {
20336
- if (
20337
- // at this stage the action side effect might have removed the <element> from the DOM
20338
- // (in theory no because action side effect are batched to happen after)
20339
- // but other side effects might do this
20340
- elementRef.current
20341
- ) {
20342
- dispatchCustomEvent("actionerror", {
20343
- detail: {
20344
- ...sharedActionEventDetail,
20345
- error,
20346
- },
20347
- });
20348
- }
20349
- if (errorEffect === "show_validation_message") {
20350
- addErrorMessage(error);
20351
- } else if (errorEffect === "throw") {
20352
- setError(error);
20353
- }
20354
- },
20355
- onComplete: (data) => {
20356
- if (
20357
- // at this stage the action side effect might have removed the <element> from the DOM
20358
- // (in theory no because action side effect are batched to happen after)
20359
- // but other side effects might do this
20360
- elementRef.current
20361
- ) {
20362
- dispatchCustomEvent("actionend", {
20363
- detail: {
20364
- ...sharedActionEventDetail,
20365
- data,
20366
- },
20367
- });
20368
- }
20369
- },
20370
- });
20371
- },
20372
- [errorEffect],
20373
- );
20000
+ // Check if either key is an alias for the other
20001
+ for (const [canonicalKey, config] of Object.entries(keyMapping)) {
20002
+ const allKeys = [canonicalKey, ...config.alias];
20003
+ if (allKeys.includes(browserEventKey) && allKeys.includes(key)) {
20004
+ return true;
20005
+ }
20006
+ }
20374
20007
 
20375
- return executeAction;
20008
+ return false;
20376
20009
  };
20377
20010
 
20378
- const detectMac = () => {
20379
- // Modern way using User-Agent Client Hints API
20380
- if (window.navigator.userAgentData) {
20381
- return window.navigator.userAgentData.platform === "macOS";
20011
+ installImportMetaCssBuild(import.meta);const css$7 = /* css */`
20012
+ .navi_text_aligner_anchor {
20013
+ vertical-align: baseline;
20014
+ user-select: none;
20015
+ overflow: hidden;
20382
20016
  }
20383
- // Fallback to userAgent string parsing
20384
- return /Mac|iPhone|iPad|iPod/.test(window.navigator.userAgent);
20385
- };
20386
- const isMac = detectMac();
20017
+ `;
20387
20018
 
20388
- // Maps canonical browser key names to their user-friendly aliases.
20389
- // Used for both event matching and ARIA normalization.
20390
- const keyMapping = {
20391
- " ": { alias: ["space"] },
20392
- "escape": { alias: ["esc"] },
20393
- "arrowup": { alias: ["up"] },
20394
- "arrowdown": { alias: ["down"] },
20395
- "arrowleft": { alias: ["left"] },
20396
- "arrowright": { alias: ["right"] },
20397
- "delete": { alias: ["del"] },
20398
- // Platform-specific mappings
20399
- ...(isMac
20400
- ? { delete: { alias: ["backspace"] } }
20401
- : { backspace: { alias: ["delete"] } }),
20019
+ /**
20020
+ * Positions children vertically relative to the surrounding text, correcting for font-size differences.
20021
+ *
20022
+ * Place this component around any inline element whose font-size differs from the surrounding text.
20023
+ * It renders an invisible anchor that inherits the surrounding text's font metrics, then shifts
20024
+ * the child so that its visual position matches the requested `align` value — regardless of
20025
+ * font-size, display type (inline, inline-block, inline-flex…), or the active `vertical-align`.
20026
+ *
20027
+ * @param {"center"|"baseline"|"start"|"end"} [align="baseline"]
20028
+ * - `"center"` — child is vertically centered on the surrounding text's ink bounds
20029
+ * - `"baseline"` — no correction applied; child sits wherever the browser places it (default)
20030
+ * - `"start"` — child top aligns with the surrounding text's ink top
20031
+ * - `"end"` — child bottom aligns with the surrounding text's ink bottom
20032
+ * @param {import("ignore:preact").RefObject} childRef — ref on the child element to reposition
20033
+ */
20034
+ const SurroundingTextAligner = ({
20035
+ children,
20036
+ align = "baseline",
20037
+ childRef
20038
+ }) => {
20039
+ import.meta.css = [css$7, "@jsenv/navi/src/text/surrounding_text_aligner.jsx"];
20040
+ const anchorRef = useRef();
20041
+ useLayoutEffect(() => {
20042
+ const anchorEl = anchorRef.current;
20043
+ const childEl = childRef.current;
20044
+ if (!anchorEl || !childEl) {
20045
+ return;
20046
+ }
20047
+ const topOffset = computeTopOffset({
20048
+ anchorEl,
20049
+ childEl,
20050
+ align
20051
+ });
20052
+ if (topOffset) {
20053
+ childEl.style.position = "relative";
20054
+ childEl.style.top = `${topOffset}px`;
20055
+ } else {
20056
+ childEl.style.position = "";
20057
+ childEl.style.top = "";
20058
+ }
20059
+ });
20060
+ return jsxs(Fragment, {
20061
+ children: [children, jsx("span", {
20062
+ ref: anchorRef,
20063
+ className: "navi_text_aligner_anchor",
20064
+ children: "\u200B"
20065
+ })]
20066
+ });
20402
20067
  };
20403
-
20404
- const activeShortcutsSignal = signal([]);
20405
- const shortcutsMap = new Map();
20406
-
20407
- const areShortcutsEqual = (shortcutA, shortcutB) => {
20408
- return (
20409
- shortcutA.key === shortcutB.key &&
20410
- shortcutA.description === shortcutB.description &&
20411
- shortcutA.enabled === shortcutB.enabled
20412
- );
20068
+ const computeTopOffset = ({
20069
+ anchorEl,
20070
+ childEl,
20071
+ align
20072
+ }) => {
20073
+ if (align === "baseline") {
20074
+ return 0;
20075
+ }
20076
+ // Only correct when the anchor lives in an inline formatting context.
20077
+ // If the parent is a flex/grid container, inline layout rules don't apply
20078
+ // and our font-metrics model is invalid.
20079
+ const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
20080
+ if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
20081
+ return 0;
20082
+ }
20083
+ const anchorStyle = getComputedStyle(anchorEl);
20084
+ const anchorMetrics = measureFontAscDesc("M", anchorStyle);
20085
+ const [anchorABA, anchorABD] = anchorMetrics.actual;
20086
+ const anchorActH = anchorABA + anchorABD;
20087
+ const [, anchorFBBD] = anchorMetrics.font;
20088
+
20089
+ // Estimate the baseline Y from the anchor's bounding rect.
20090
+ // For an inline span, the font cell bottom is always at the element's bottom edge
20091
+ // (regardless of vertical-align), so baseline = rect.bottom - fontBoundingBoxDescent.
20092
+ const anchorRect = anchorEl.getBoundingClientRect();
20093
+ const baselineY = anchorRect.bottom - anchorFBBD;
20094
+ const anchorInkTopY = baselineY - anchorABA;
20095
+
20096
+ // Measure the child's current rect, then subtract any previously applied top correction
20097
+ // to recover its natural position — avoiding a style reset + reflow.
20098
+ const childRect = childEl.getBoundingClientRect();
20099
+ const childH = childRect.height;
20100
+ const previousTop = parseFloat(childEl.style.top) || 0;
20101
+ const childNaturalTop = childRect.top - previousTop;
20102
+
20103
+ // Compute desired child top Y based on align intention.
20104
+ let desiredChildTopY = 0;
20105
+ if (align === "center") {
20106
+ const anchorInkCenterY = anchorInkTopY + anchorActH / 2;
20107
+ desiredChildTopY = anchorInkCenterY - childH / 2;
20108
+ } else if (align === "start") {
20109
+ desiredChildTopY = anchorInkTopY;
20110
+ } else if (align === "end") {
20111
+ desiredChildTopY = anchorInkTopY + anchorActH - childH;
20112
+ }
20113
+ return desiredChildTopY - childNaturalTop;
20114
+ };
20115
+ const canvas = document.createElement("canvas");
20116
+ const measureFontAscDesc = (text, computedStyle) => {
20117
+ const ctx = canvas.getContext("2d");
20118
+ ctx.font = `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}`;
20119
+ const metrics = ctx.measureText(text);
20120
+ return {
20121
+ actual: [metrics.actualBoundingBoxAscent, metrics.actualBoundingBoxDescent],
20122
+ font: [metrics.fontBoundingBoxAscent, metrics.fontBoundingBoxDescent]
20123
+ };
20413
20124
  };
20414
20125
 
20415
- const areShortcutArraysEqual = (arrayA, arrayB) => {
20416
- if (arrayA.length !== arrayB.length) {
20417
- return false;
20126
+ const useInitialTextSelection = (ref, textSelection) => {
20127
+ const deps = [];
20128
+ if (Array.isArray(textSelection)) {
20129
+ deps.push(...textSelection);
20130
+ } else {
20131
+ deps.push(textSelection);
20418
20132
  }
20419
-
20420
- for (let i = 0; i < arrayA.length; i++) {
20421
- if (!areShortcutsEqual(arrayA[i], arrayB[i])) {
20422
- return false;
20133
+ useLayoutEffect(() => {
20134
+ const el = ref.current;
20135
+ if (!el || !textSelection) {
20136
+ return;
20423
20137
  }
20424
- }
20425
-
20426
- return true;
20138
+ const range = document.createRange();
20139
+ const selection = window.getSelection();
20140
+ if (Array.isArray(textSelection)) {
20141
+ if (textSelection.length === 2) {
20142
+ const [start, end] = textSelection;
20143
+ if (typeof start === "number" && typeof end === "number") {
20144
+ // Format: [0, 10] - character indices
20145
+ selectByCharacterIndices(el, range, start, end);
20146
+ } else if (typeof start === "string" && typeof end === "string") {
20147
+ // Format: ["Click on the", "button to return"] - text strings
20148
+ selectByTextStrings(el, range, start, end);
20149
+ }
20150
+ }
20151
+ } else if (typeof textSelection === "string") {
20152
+ // Format: "some text" - select the entire string occurrence
20153
+ selectSingleTextString(el, range, textSelection);
20154
+ }
20155
+ selection.removeAllRanges();
20156
+ selection.addRange(range);
20157
+ }, deps);
20427
20158
  };
20159
+ const selectByCharacterIndices = (element, range, startIndex, endIndex) => {
20160
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
20161
+ let currentIndex = 0;
20162
+ let startNode = null;
20163
+ let startOffset = 0;
20164
+ let endNode = null;
20165
+ let endOffset = 0;
20166
+ while (walker.nextNode()) {
20167
+ const textContent = walker.currentNode.textContent;
20168
+ const nodeLength = textContent.length;
20428
20169
 
20429
- const updateActiveShortcuts = () => {
20430
- const activeElement = activeElementSignal.peek();
20431
- const currentActiveShortcuts = activeShortcutsSignal.peek();
20432
- const activeShortcuts = [];
20433
- for (const [element, { shortcuts }] of shortcutsMap) {
20434
- if (element === activeElement || element.contains(activeElement)) {
20435
- activeShortcuts.push(...shortcuts);
20170
+ // Check if start position is in this text node
20171
+ if (!startNode && currentIndex + nodeLength > startIndex) {
20172
+ startNode = walker.currentNode;
20173
+ startOffset = startIndex - currentIndex;
20436
20174
  }
20437
- }
20438
20175
 
20439
- // Only update if shortcuts have actually changed
20440
- if (!areShortcutArraysEqual(currentActiveShortcuts, activeShortcuts)) {
20441
- activeShortcutsSignal.value = activeShortcuts;
20176
+ // Check if end position is in this text node
20177
+ if (currentIndex + nodeLength >= endIndex) {
20178
+ endNode = walker.currentNode;
20179
+ endOffset = endIndex - currentIndex;
20180
+ break;
20181
+ }
20182
+ currentIndex += nodeLength;
20183
+ }
20184
+ if (startNode && endNode) {
20185
+ range.setStart(startNode, startOffset);
20186
+ range.setEnd(endNode, endOffset);
20442
20187
  }
20443
20188
  };
20444
- effect(() => {
20445
- // eslint-disable-next-line no-unused-expressions
20446
- activeElementSignal.value;
20447
- updateActiveShortcuts();
20448
- });
20449
- const addShortcuts = (element, shortcuts) => {
20450
- shortcutsMap.set(element, { shortcuts });
20451
- updateActiveShortcuts();
20189
+ const selectSingleTextString = (element, range, text) => {
20190
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
20191
+ while (walker.nextNode()) {
20192
+ const textContent = walker.currentNode.textContent;
20193
+ const index = textContent.indexOf(text);
20194
+ if (index !== -1) {
20195
+ range.setStart(walker.currentNode, index);
20196
+ range.setEnd(walker.currentNode, index + text.length);
20197
+ return;
20198
+ }
20199
+ }
20452
20200
  };
20453
- const removeShortcuts = (element) => {
20454
- shortcutsMap.delete(element);
20455
- updateActiveShortcuts();
20201
+ const selectByTextStrings = (element, range, startText, endText) => {
20202
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
20203
+ let startNode = null;
20204
+ let endNode = null;
20205
+ let foundStart = false;
20206
+ while (walker.nextNode()) {
20207
+ const textContent = walker.currentNode.textContent;
20208
+ if (!foundStart && textContent.includes(startText)) {
20209
+ startNode = walker.currentNode;
20210
+ foundStart = true;
20211
+ }
20212
+ if (foundStart && textContent.includes(endText)) {
20213
+ endNode = walker.currentNode;
20214
+ break;
20215
+ }
20216
+ }
20217
+ if (startNode && endNode) {
20218
+ const startOffset = startNode.textContent.indexOf(startText);
20219
+ const endOffset = endNode.textContent.indexOf(endText) + endText.length;
20220
+ range.setStart(startNode, startOffset);
20221
+ range.setEnd(endNode, endOffset);
20222
+ }
20456
20223
  };
20457
20224
 
20458
- const useKeyboardShortcuts = (
20459
- elementRef,
20460
- shortcuts,
20461
- {
20462
- onActionPrevented,
20463
- onActionStart,
20464
- onActionAbort,
20465
- onActionError,
20466
- onActionEnd,
20467
- allowConcurrentActions,
20468
- } = {},
20469
- ) => {
20470
- if (!elementRef) {
20471
- throw new Error(
20472
- "useKeyboardShortcuts requires an elementRef to attach shortcuts to.",
20473
- );
20225
+ installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
20226
+ const css$6 = /* css */`
20227
+ @layer navi {
20228
+ .navi_text {
20229
+ &[data-skeleton] {
20230
+ border-radius: 0.2em;
20231
+ }
20232
+ }
20474
20233
  }
20475
20234
 
20476
- const executeAction = useExecuteAction(elementRef);
20477
- const shortcutActionIsBusyRef = useRef(false);
20478
- useActionEvents(elementRef, {
20479
- actionOrigin: "keyboard_shortcut",
20480
- onPrevented: onActionPrevented,
20481
- onAction: (actionEvent) => {
20482
- const { shortcut } = actionEvent.detail.meta || {};
20483
- if (!shortcut) {
20484
- // not a shortcut (an other interaction triggered the action, don't request it again)
20485
- return;
20235
+ *[data-navi-space] {
20236
+ }
20237
+
20238
+ .navi_text {
20239
+ position: relative;
20240
+
20241
+ /* There is a chrome specific bug that prevents text-transform: capitalize to be applied in nested DOM structure */
20242
+ /* The CSS below ensure capitalize is propagated to the bold clones */
20243
+ &[data-capitalize] {
20244
+ &::first-letter {
20245
+ text-transform: uppercase;
20486
20246
  }
20487
- // action can be a function or an action object, whem a function we must "wrap" it in a function returning that function
20488
- // otherwise setState would call that action immediately
20489
- // setAction(() => actionEvent.detail.action);
20490
- executeAction(actionEvent, {
20491
- requester: document.activeElement,
20492
- });
20493
- },
20494
- onStart: (e) => {
20495
- const { shortcut } = e.detail.meta || {};
20496
- if (!shortcut) {
20497
- return;
20247
+ .navi_text_bold_clone::first-letter {
20248
+ text-transform: uppercase;
20498
20249
  }
20499
- if (!allowConcurrentActions) {
20500
- shortcutActionIsBusyRef.current = true;
20250
+ .navi_text_bold_foreground::first-letter {
20251
+ text-transform: uppercase;
20501
20252
  }
20502
- shortcut.onStart?.(e);
20503
- onActionStart?.(e);
20504
- },
20505
- onAbort: (e) => {
20506
- const { shortcut } = e.detail.meta || {};
20507
- if (!shortcut) {
20508
- return;
20253
+ }
20254
+
20255
+ .navi_text_bold_wrapper,
20256
+ .navi_text_bold_clone,
20257
+ .navi_text_bold_foreground {
20258
+ display: inherit;
20259
+ width: inherit;
20260
+ min-width: inherit;
20261
+ height: inherit;
20262
+ min-height: inherit;
20263
+ flex-grow: inherit;
20264
+ align-items: inherit;
20265
+ justify-content: inherit;
20266
+ gap: inherit;
20267
+ text-align: inherit;
20268
+ border-radius: inherit;
20269
+ }
20270
+
20271
+ &[data-text-overflow] {
20272
+ min-width: 0;
20273
+ flex-wrap: wrap;
20274
+ text-overflow: ellipsis;
20275
+ overflow: hidden;
20276
+
20277
+ .navi_text_overflow_wrapper {
20278
+ display: flex;
20279
+ width: 100%;
20280
+ flex-grow: 1;
20281
+ gap: 0.3em;
20282
+
20283
+ .navi_text_overflow_text {
20284
+ max-width: 100%;
20285
+ text-overflow: ellipsis;
20286
+ overflow: hidden;
20287
+ }
20288
+ }
20289
+ }
20290
+
20291
+ &[data-skeleton] {
20292
+ /* Children stay in the DOM to preserve natural layout dimensions,
20293
+ but are hidden so only the skeleton is visible. */
20294
+ visibility: hidden;
20295
+
20296
+ /* When there are no children a placeholder "W" is injected (see JSX).
20297
+ It must stretch to the full available width so the skeleton
20298
+ fills the container rather than collapsing to a single character. */
20299
+ .navi_text_skeleton_children_placeholder {
20300
+ display: inline-flex;
20301
+ width: 100%;
20302
+ }
20303
+
20304
+ /* Three-level structure to respect padding AND border-radius:
20305
+
20306
+ 1. navi_text_skeleton_container — absolutely fills the border box
20307
+ (inset:0), then applies padding:inherit so its content box equals
20308
+ the parent's content box. line-height:normal prevents the container
20309
+ from inheriting a large line-height that would make it taller than
20310
+ the border box. border-radius:inherit passes the radius down.
20311
+ visibility:visible overrides the parent's visibility:hidden.
20312
+
20313
+ 2. navi_text_skeleton_inset — a relative block that fills 100% of the
20314
+ container's content box (= parent's content box). It is the
20315
+ positioned ancestor for the absolutely placed skeleton bar.
20316
+ border-radius:inherit chains the radius further down.
20317
+
20318
+ 3. navi_text_skeleton — the visible gradient bar. position:absolute
20319
+ inset:0 fills the inset box precisely. border-radius:inherit
20320
+ finally applies the radius at this level, which is now correctly
20321
+ sized to the content area. */
20322
+ .navi_text_skeleton_container {
20323
+ position: absolute;
20324
+ inset: 0;
20325
+ padding: inherit;
20326
+ line-height: normal;
20327
+ border-radius: inherit;
20328
+ visibility: visible;
20509
20329
  }
20510
- shortcutActionIsBusyRef.current = false;
20511
- shortcut.onAbort?.(e);
20512
- onActionAbort?.(e);
20513
- },
20514
- onError: (error, e) => {
20515
- const { shortcut } = e.detail.meta || {};
20516
- if (!shortcut) {
20517
- return;
20330
+
20331
+ .navi_text_skeleton_inset {
20332
+ position: relative;
20333
+ display: inline-flex;
20334
+ width: 100%;
20335
+ height: 100%;
20336
+ border-radius: inherit;
20518
20337
  }
20519
- shortcutActionIsBusyRef.current = false;
20520
- shortcut.onError?.(error, e);
20521
- onActionError?.(error, e);
20522
- },
20523
- onEnd: (e) => {
20524
- const { shortcut } = e.detail.meta || {};
20525
- if (!shortcut) {
20526
- return;
20338
+
20339
+ .navi_text_skeleton {
20340
+ position: absolute;
20341
+ inset: 0;
20342
+ background: linear-gradient(
20343
+ 90deg,
20344
+ #e0e0e0 25%,
20345
+ #f0f0f0 50%,
20346
+ #e0e0e0 75%
20347
+ );
20348
+ background-size: 200% 100%;
20349
+ border-radius: inherit;
20527
20350
  }
20528
- shortcutActionIsBusyRef.current = false;
20529
- shortcut.onEnd?.(e);
20530
- onActionEnd?.(e);
20531
- },
20532
- });
20533
20351
 
20534
- const shortcutDeps = [];
20535
- for (const shortcut of shortcuts) {
20536
- shortcutDeps.push(
20537
- shortcut.key,
20538
- shortcut.description,
20539
- shortcut.enabled,
20540
- shortcut.confirmMessage,
20541
- );
20542
- shortcut.action = useAction(shortcut.action);
20352
+ &[data-loading] {
20353
+ .navi_text_skeleton {
20354
+ animation: navi_text_skeleton_shimmer 1.5s infinite;
20355
+ }
20356
+ }
20357
+ }
20543
20358
  }
20544
20359
 
20545
- useEffect(() => {
20546
- const element = elementRef.current;
20547
- if (!element) {
20548
- return null;
20360
+ @keyframes navi_text_skeleton_shimmer {
20361
+ 0% {
20362
+ background-position: 200% 0;
20549
20363
  }
20550
- const shortcutsCopy = [];
20551
- for (const shortcutCandidate of shortcuts) {
20552
- shortcutsCopy.push({
20553
- ...shortcutCandidate,
20554
- handler: (keyboardEvent) => {
20555
- if (shortcutCandidate.handler) {
20556
- return shortcutCandidate.handler(keyboardEvent);
20557
- }
20558
- if (shortcutActionIsBusyRef.current) {
20559
- return false;
20560
- }
20561
- const { action } = shortcutCandidate;
20562
- const actionWithEvent = action.bindParams(keyboardEvent);
20563
- return requestAction(element, actionWithEvent, {
20564
- actionOrigin: "keyboard_shortcut",
20565
- event: keyboardEvent,
20566
- requester: document.activeElement,
20567
- confirmMessage: shortcutCandidate.confirmMessage,
20568
- meta: {
20569
- shortcut: shortcutCandidate,
20570
- },
20571
- });
20572
- },
20573
- });
20364
+ 100% {
20365
+ background-position: -200% 0;
20574
20366
  }
20367
+ }
20575
20368
 
20576
- addShortcuts(element, shortcuts);
20369
+ .navi_text_bold_wrapper {
20370
+ position: relative;
20371
+ display: inline-block;
20577
20372
 
20578
- const onKeydown = (event) => {
20579
- applyKeyboardShortcuts(shortcutsCopy, event);
20580
- };
20581
- element.addEventListener("keydown", onKeydown);
20582
- return () => {
20583
- element.removeEventListener("keydown", onKeydown);
20584
- removeShortcuts(element);
20585
- };
20586
- }, [shortcutDeps]);
20587
- };
20373
+ .navi_text_bold_clone {
20374
+ font-weight: bold;
20375
+ opacity: 0;
20376
+ }
20377
+ .navi_text_bold_foreground {
20378
+ position: absolute;
20379
+ inset: 0;
20380
+ }
20381
+ }
20588
20382
 
20589
- const applyKeyboardShortcuts = (shortcuts, keyboardEvent) => {
20590
- if (!canInterceptKeys(keyboardEvent)) {
20591
- return null;
20383
+ .navi_text_bold_background {
20384
+ position: absolute;
20385
+ top: 0;
20386
+ left: 0;
20387
+ color: currentColor;
20388
+ font-weight: normal;
20389
+ background: currentColor;
20390
+ background-clip: text;
20391
+ -webkit-background-clip: text;
20392
+ transform-origin: center;
20393
+ -webkit-text-fill-color: transparent;
20394
+ opacity: 0;
20592
20395
  }
20593
- for (const shortcutCandidate of shortcuts) {
20594
- let { enabled = true, key } = shortcutCandidate;
20595
- if (!enabled) {
20596
- continue;
20597
- }
20598
20396
 
20599
- if (typeof key === "function") {
20600
- const keyReturnValue = key(keyboardEvent);
20601
- if (!keyReturnValue) {
20602
- continue;
20603
- }
20604
- key = keyReturnValue;
20397
+ .navi_text[data-bold] {
20398
+ .navi_text_bold_background {
20399
+ opacity: 1;
20605
20400
  }
20606
- if (!key) {
20607
- console.error(shortcutCandidate);
20608
- throw new TypeError(`key is required in keyboard shortcut, got ${key}`);
20401
+ }
20402
+
20403
+ .navi_text[data-bold-transition] {
20404
+ .navi_text_bold_foreground {
20405
+ transition-property: font-weight;
20406
+ transition-duration: 0.3s;
20407
+ transition-timing-function: ease;
20609
20408
  }
20610
20409
 
20611
- // Handle platform-specific combination objects
20612
- let actualCombination;
20613
- let crossPlatformCombination;
20614
- if (typeof key === "object" && key !== null) {
20615
- actualCombination = isMac ? key.mac : key.other;
20410
+ .navi_text_bold_background {
20411
+ transition-property: opacity;
20412
+ transition-duration: 0.3s;
20413
+ transition-timing-function: ease;
20414
+ }
20415
+ }
20416
+ `;
20417
+ const REGULAR_SPACE = jsx("span", {
20418
+ "data-navi-space": "",
20419
+ children: " "
20420
+ });
20421
+ // A space that uses padding-left instead of a real space character.
20422
+ // This avoids the underline that browsers draw under spaces inside links.
20423
+ const FAKE_SPACE = jsx("span", {
20424
+ "data-navi-space": "",
20425
+ style: "padding-left: 0.25em",
20426
+ children: "\u200B"
20427
+ });
20428
+ const CustomWidthSpace = ({
20429
+ value,
20430
+ useRealSpaceChar
20431
+ }) => {
20432
+ if (useRealSpaceChar) {
20433
+ return jsxs("span", {
20434
+ children: [jsx("span", {
20435
+ style: "font-size: 0",
20436
+ children: " "
20437
+ }), jsx("span", {
20438
+ style: `padding-left: ${value}`,
20439
+ children: "\u200B"
20440
+ })]
20441
+ });
20442
+ }
20443
+ return jsx("span", {
20444
+ style: `padding-left: ${value}`,
20445
+ children: "\u200B"
20446
+ });
20447
+ };
20448
+ const applySpacingOnTextChildren = (children, spacing, defaultSpace) => {
20449
+ if (spacing === "pre" || spacing === "0" || spacing === 0) {
20450
+ return children;
20451
+ }
20452
+ if (!children) {
20453
+ return children;
20454
+ }
20455
+ const childArray = toChildArray(children);
20456
+ const childCount = childArray.length;
20457
+ if (childCount <= 1) {
20458
+ return children;
20459
+ }
20460
+ const useRealSpaceChar = defaultSpace !== FAKE_SPACE;
20461
+ let separator;
20462
+ if (spacing === REGULAR_SPACE || spacing === FAKE_SPACE) {
20463
+ separator = defaultSpace;
20464
+ } else if (typeof spacing === "string") {
20465
+ if (isSizeSpacingScaleKey(spacing) || hasCSSSizeUnit(spacing)) {
20466
+ separator = jsx(CustomWidthSpace, {
20467
+ value: resolveSpacingSize(spacing),
20468
+ useRealSpaceChar: useRealSpaceChar
20469
+ });
20616
20470
  } else {
20617
- actualCombination = key;
20618
- if (containsPlatformSpecificKeys(key)) {
20619
- crossPlatformCombination = generateCrossPlatformCombination(key);
20620
- }
20471
+ separator = spacing;
20621
20472
  }
20622
-
20623
- // Check both the actual combination and cross-platform combination
20624
- const matchesActual =
20625
- actualCombination &&
20626
- keyboardEventIsMatchingKeyCombination(keyboardEvent, actualCombination);
20627
- const matchesCrossPlatform =
20628
- crossPlatformCombination &&
20629
- crossPlatformCombination !== actualCombination &&
20630
- keyboardEventIsMatchingKeyCombination(
20631
- keyboardEvent,
20632
- crossPlatformCombination,
20633
- );
20634
-
20635
- if (!matchesActual && !matchesCrossPlatform) {
20636
- continue;
20473
+ } else if (typeof spacing === "number") {
20474
+ separator = jsx(CustomWidthSpace, {
20475
+ value: `${spacing}px`,
20476
+ useRealSpaceChar: useRealSpaceChar
20477
+ });
20478
+ } else {
20479
+ separator = spacing;
20480
+ }
20481
+ const childrenWithGap = [];
20482
+ let i = 0;
20483
+ while (true) {
20484
+ const child = childArray[i];
20485
+ childrenWithGap.push(child);
20486
+ i++;
20487
+ if (i === childCount) {
20488
+ break;
20637
20489
  }
20638
- if (typeof enabled === "function" && !enabled(keyboardEvent)) {
20490
+ const currentChild = childArray[i - 1];
20491
+ const nextChild = childArray[i];
20492
+ if (!shouldInjectSpacingBetween(currentChild, nextChild)) {
20639
20493
  continue;
20640
20494
  }
20641
- const returnValue = shortcutCandidate.handler(keyboardEvent);
20642
- if (returnValue) {
20643
- keyboardEvent.preventDefault();
20644
- }
20645
- return shortcutCandidate;
20495
+ childrenWithGap.push(separator);
20646
20496
  }
20647
- return null;
20497
+ return childrenWithGap;
20648
20498
  };
20649
- const containsPlatformSpecificKeys = (combination) => {
20650
- const lowerCombination = combination.toLowerCase();
20651
- const macSpecificKeys = ["command", "cmd"];
20652
-
20653
- return macSpecificKeys.some((key) => lowerCombination.includes(key));
20499
+ const outsideTextFlowSet = new Set();
20500
+ const markAsOutsideTextFlow = jsxElement => {
20501
+ outsideTextFlowSet.add(jsxElement);
20654
20502
  };
20655
- const generateCrossPlatformCombination = (combination) => {
20656
- let crossPlatform = combination;
20503
+ const isMarkedAsOutsideTextFlow = jsxElement => {
20504
+ return outsideTextFlowSet.has(jsxElement.type);
20505
+ };
20506
+ const isPreactNode = jsxChild => {
20507
+ return jsxChild !== null && typeof jsxChild === "object" && jsxChild.type !== undefined;
20508
+ };
20509
+ const shouldInjectSpacingBetween = (left, right) => {
20510
+ const leftIsNode = isPreactNode(left);
20511
+ const rightIsNode = isPreactNode(right);
20512
+ // only inject spacing when at least one side is a preact node
20513
+ if (!leftIsNode && !rightIsNode) {
20514
+ return false;
20515
+ }
20516
+ if (leftIsNode && isMarkedAsOutsideTextFlow(left)) {
20517
+ return false;
20518
+ }
20519
+ if (rightIsNode && isMarkedAsOutsideTextFlow(right)) {
20520
+ return false;
20521
+ }
20522
+ if (rightIsNode && right.props && right.props.overflowPinned) {
20523
+ return false;
20524
+ }
20525
+ if (typeof left === "string" && /\s$/.test(left)) {
20526
+ return false;
20527
+ }
20528
+ if (typeof right === "string" && /^\s/.test(right)) {
20529
+ return false;
20530
+ }
20531
+ return true;
20532
+ };
20533
+ const OverflowPinnedElementContext = createContext(null);
20534
+ const Text = props => {
20535
+ import.meta.css = [css$6, "@jsenv/navi/src/text/text.jsx"];
20536
+ if (props.loading || props.skeleton) {
20537
+ return jsx(TextSkeleton, {
20538
+ ...props
20539
+ });
20540
+ }
20541
+ if (props.overflowEllipsis) {
20542
+ return jsx(TextOverflow, {
20543
+ ...props
20544
+ });
20545
+ }
20546
+ if (props.overflowPinned) {
20547
+ return jsx(TextOverflowPinned, {
20548
+ ...props
20549
+ });
20550
+ }
20551
+ if (props.selectRange) {
20552
+ return jsx(TextWithSelectRange, {
20553
+ ...props
20554
+ });
20555
+ }
20556
+ return jsx(TextBasic, {
20557
+ ...props
20558
+ });
20559
+ };
20560
+ const TextSkeleton = ({
20561
+ loading,
20562
+ children,
20563
+ ...props
20564
+ }) => {
20565
+ // Three-level structure — see CSS comment on [data-skeleton] for details.
20566
+ const skeletonOverlay = jsx("span", {
20567
+ className: "navi_text_skeleton_container",
20568
+ "aria-hidden": "true",
20569
+ children: jsx("span", {
20570
+ className: "navi_text_skeleton_inset",
20571
+ children: jsx("span", {
20572
+ className: "navi_text_skeleton"
20573
+ })
20574
+ })
20575
+ });
20576
+ // When there are no children, inject a full-width placeholder so the element
20577
+ // has measurable height driven by the current font-size/line-height, and the
20578
+ // skeleton fills the available width instead of shrinking to a single char.
20579
+ const hasChildren = children !== null && children !== undefined && children !== false;
20580
+ const innerChildren = hasChildren ? children : jsx("span", {
20581
+ className: "navi_text_skeleton_children_placeholder",
20582
+ "aria-hidden": "true",
20583
+ children: "W"
20584
+ });
20585
+ return jsx(Text, {
20586
+ "data-skeleton": "",
20587
+ "data-loading": loading ? "" : undefined,
20588
+ ...props,
20589
+ skeleton: undefined,
20590
+ childrenOutsideFlow: skeletonOverlay,
20591
+ children: innerChildren
20592
+ });
20593
+ };
20594
+ const TextOverflow = ({
20595
+ noWrap,
20596
+ spacing,
20597
+ children,
20598
+ ...rest
20599
+ }) => {
20600
+ const [OverflowPinnedElement, setOverflowPinnedElement] = useState(null);
20601
+ return jsx(Text, {
20602
+ flex: true,
20603
+ block: true,
20604
+ as: "div",
20605
+ nowWrap: noWrap,
20606
+ pre: !noWrap
20607
+ // For paragraph we prefer to keep lines and only hide unbreakable long sections
20608
+ ,
20657
20609
 
20658
- if (isMac) {
20659
- // No need to convert anything TO Windows/Linux-specific format since we're on Mac
20610
+ preLine: rest.as === "p",
20611
+ ...rest,
20612
+ overflowEllipsis: undefined,
20613
+ "data-text-overflow": "",
20614
+ spacing: "pre",
20615
+ children: jsxs("span", {
20616
+ className: "navi_text_overflow_wrapper",
20617
+ children: [jsx(OverflowPinnedElementContext.Provider, {
20618
+ value: setOverflowPinnedElement,
20619
+ children: jsx(Text, {
20620
+ className: "navi_text_overflow_text",
20621
+ spacing: spacing,
20622
+ children: children
20623
+ })
20624
+ }), OverflowPinnedElement]
20625
+ })
20626
+ });
20627
+ };
20628
+ const TextOverflowPinned = ({
20629
+ overflowPinned,
20630
+ ...props
20631
+ }) => {
20632
+ const setOverflowPinnedElement = useContext(OverflowPinnedElementContext);
20633
+ const text = jsx(Text, {
20634
+ ...props,
20635
+ "data-overflow-pinned": ""
20636
+ });
20637
+ if (!setOverflowPinnedElement) {
20638
+ console.warn("<Text overflowPinned> declared outside a <Text overflowEllipsis>");
20639
+ return text;
20640
+ }
20641
+ if (overflowPinned) {
20642
+ setOverflowPinnedElement(text);
20660
20643
  return null;
20661
20644
  }
20662
- // If not on Mac but combination contains Mac-specific keys, generate Windows equivalent
20663
- crossPlatform = crossPlatform.replace(/\bcommand\b/gi, "control");
20664
- crossPlatform = crossPlatform.replace(/\bcmd\b/gi, "control");
20665
-
20666
- return crossPlatform;
20645
+ setOverflowPinnedElement(null);
20646
+ return text;
20667
20647
  };
20668
- const keyboardEventIsMatchingKeyCombination = (event, keyCombination) => {
20669
- const keys = keyCombination.toLowerCase().split("+");
20670
-
20671
- for (const key of keys) {
20672
- let modifierFound = false;
20673
-
20674
- // Check if this key is a modifier
20675
- for (const [eventProperty, config] of Object.entries(modifierKeyMapping)) {
20676
- const allNames = [...config.names];
20648
+ const TextWithSelectRange = ({
20649
+ selectRange,
20650
+ ...props
20651
+ }) => {
20652
+ const defaultRef = useRef();
20653
+ const ref = props.ref || defaultRef;
20654
+ useInitialTextSelection(ref, selectRange);
20655
+ return jsx(Text, {
20656
+ ref: ref,
20657
+ ...props
20658
+ });
20659
+ };
20660
+ const TextBasic = ({
20661
+ spacing,
20662
+ preventSpaceUnderlines = false,
20663
+ boldTransition,
20664
+ boldStable,
20665
+ preventBoldLayoutShift = boldTransition,
20666
+ capitalize,
20667
+ children,
20668
+ childrenOutsideFlow,
20669
+ ...rest
20670
+ }) => {
20671
+ const defaultSpace = preventSpaceUnderlines ? FAKE_SPACE : REGULAR_SPACE;
20672
+ const resolvedSpacing = spacing ?? defaultSpace;
20673
+ const boxProps = {
20674
+ "as": "span",
20675
+ "data-bold-transition": boldTransition ? "" : undefined,
20676
+ "data-capitalize": capitalize ? "" : undefined,
20677
+ ...rest,
20678
+ "baseClassName": withPropsClassName("navi_text", rest.baseClassName)
20679
+ };
20680
+ const shouldPreserveSpacing = rest.as === "pre" || rest.flex || rest.grid;
20681
+ if (shouldPreserveSpacing) {
20682
+ boxProps.spacing = resolvedSpacing;
20683
+ } else {
20684
+ children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
20685
+ }
20686
+ if (boldStable) {
20687
+ const {
20688
+ bold
20689
+ } = boxProps;
20690
+ return jsxs(Box, {
20691
+ ...boxProps,
20692
+ bold: undefined,
20693
+ "data-bold": bold ? "" : undefined,
20694
+ children: [jsx("span", {
20695
+ className: "navi_text_bold_background",
20696
+ "aria-hidden": "true",
20697
+ children: children
20698
+ }), children, childrenOutsideFlow]
20699
+ });
20700
+ }
20701
+ if (preventBoldLayoutShift) {
20702
+ const alignX = rest.alignX || rest.align || "start";
20677
20703
 
20678
- // Add Mac-specific names only if we're on Mac and they exist
20679
- if (isMac && config.macNames) {
20680
- allNames.push(...config.macNames);
20681
- }
20704
+ // La technique consiste a avoid un double gras qui force une taille
20705
+ // et la version light par dessus en position absolute
20706
+ // on la centre aussi pour donner l'impression que le gras s'applique depuis le centre
20707
+ // ne fonctionne que sur une seule ligne de texte (donc lorsque noWrap est actif)
20708
+ // on pourrait auto-active cela sur une prop genre boldCanChange
20709
+ return jsxs(Box, {
20710
+ ...boxProps,
20711
+ children: [jsxs("span", {
20712
+ className: "navi_text_bold_wrapper",
20713
+ children: [jsx("span", {
20714
+ className: "navi_text_bold_clone",
20715
+ "aria-hidden": "true",
20716
+ children: children
20717
+ }), jsx("span", {
20718
+ className: "navi_text_bold_foreground",
20719
+ "data-align": alignX,
20720
+ children: children
20721
+ })]
20722
+ }), childrenOutsideFlow]
20723
+ });
20724
+ }
20725
+ return jsxs(Box, {
20726
+ ...boxProps,
20727
+ children: [children, childrenOutsideFlow]
20728
+ });
20729
+ };
20682
20730
 
20683
- if (allNames.includes(key)) {
20684
- // Check if the corresponding event property is pressed
20685
- if (!event[eventProperty]) {
20686
- return false;
20687
- }
20688
- modifierFound = true;
20689
- break;
20690
- }
20691
- }
20692
- if (modifierFound) {
20693
- continue;
20731
+ installImportMetaCssBuild(import.meta);const css$5 = /* css */`
20732
+ @layer navi {
20733
+ /* Ensure data attributes from box.jsx can win to update display */
20734
+ .navi_icon {
20735
+ display: inline-block;
20736
+ box-sizing: border-box;
20737
+ max-width: 100%;
20738
+ max-height: 100%;
20694
20739
  }
20740
+ }
20695
20741
 
20696
- // Check if it's a range pattern like "a-z" or "0-9"
20697
- if (key.includes("-") && key.length === 3) {
20698
- const [startChar, dash, endChar] = key;
20699
- if (dash === "-") {
20700
- // Only check ranges for single alphanumeric characters
20701
- const eventKey = event.key.toLowerCase();
20702
- if (eventKey.length !== 1) {
20703
- return false; // Not a single character key
20704
- }
20705
-
20706
- // Only allow a-z and 0-9 ranges
20707
- const isValidRange =
20708
- (startChar >= "a" && endChar <= "z") ||
20709
- (startChar >= "0" && endChar <= "9");
20710
-
20711
- if (!isValidRange) {
20712
- return false; // Invalid range pattern
20713
- }
20742
+ .navi_icon {
20743
+ white-space: nowrap;
20744
+ vertical-align: inherit;
20714
20745
 
20715
- const eventKeyCode = eventKey.charCodeAt(0);
20716
- const startCode = startChar.charCodeAt(0);
20717
- const endCode = endChar.charCodeAt(0);
20746
+ &[data-flow-inline] {
20747
+ width: 1em;
20748
+ height: 1em;
20749
+ }
20750
+ &[data-icon-char] {
20751
+ flex-grow: 0 !important;
20718
20752
 
20719
- if (eventKeyCode >= startCode && eventKeyCode <= endCode) {
20720
- continue; // Range matched
20721
- }
20722
- return false; // Range not matched
20753
+ svg,
20754
+ img {
20755
+ width: 100%;
20756
+ height: 100%;
20757
+ }
20758
+ svg {
20759
+ overflow: visible;
20723
20760
  }
20724
20761
  }
20725
-
20726
- // If it's not a modifier or range, check if it matches the actual key
20727
- if (!isSameKey(event.key, key)) {
20728
- return false;
20762
+ &[data-interactive] {
20763
+ cursor: pointer;
20729
20764
  }
20730
20765
  }
20731
- return true;
20732
- };
20733
- // Configuration for mapping shortcut key names to browser event properties
20734
- const modifierKeyMapping = {
20735
- metaKey: {
20736
- names: ["meta"],
20737
- macNames: ["command", "cmd"],
20738
- },
20739
- ctrlKey: {
20740
- names: ["control", "ctrl"],
20741
- },
20742
- shiftKey: {
20743
- names: ["shift"],
20744
- },
20745
- altKey: {
20746
- names: ["alt"],
20747
- macNames: ["option"],
20748
- },
20749
- };
20750
- const isSameKey = (browserEventKey, key) => {
20751
- browserEventKey = browserEventKey.toLowerCase();
20752
- key = key.toLowerCase();
20753
20766
 
20754
- if (browserEventKey === key) {
20755
- return true;
20767
+ .navi_icon_char_slot {
20768
+ opacity: 0;
20769
+ cursor: default;
20770
+ user-select: none;
20756
20771
  }
20772
+ .navi_text.navi_icon_foreground {
20773
+ position: absolute;
20774
+ inset: 0;
20775
+ display: inline-flex;
20757
20776
 
20758
- // Check if either key is an alias for the other
20759
- for (const [canonicalKey, config] of Object.entries(keyMapping)) {
20760
- const allKeys = [canonicalKey, ...config.alias];
20761
- if (allKeys.includes(browserEventKey) && allKeys.includes(key)) {
20762
- return true;
20777
+ & > .navi_text {
20778
+ display: flex;
20779
+ aspect-ratio: 1 / 1;
20780
+ min-width: 0;
20781
+ height: 100%;
20782
+ max-height: 1em;
20783
+ align-items: center;
20784
+ justify-content: center;
20763
20785
  }
20764
20786
  }
20765
20787
 
20766
- return false;
20788
+ .navi_icon > svg,
20789
+ .navi_icon > img {
20790
+ width: 100%;
20791
+ height: 100%;
20792
+ backface-visibility: hidden;
20793
+ }
20794
+ .navi_icon[data-width-fixed] > svg,
20795
+ .navi_icon[data-width-fixed] > img {
20796
+ width: 100%;
20797
+ height: auto;
20798
+ }
20799
+ .navi_icon[data-height-fixed] > svg,
20800
+ .navi_icon[data-height-fixed] > img {
20801
+ width: auto;
20802
+ height: 100%;
20803
+ }
20804
+ .navi_icon[data-width-fixed][data-height-fixed] > svg,
20805
+ .navi_icon[data-width-fixed][data-height-fixed] > img {
20806
+ width: 100%;
20807
+ height: 100%;
20808
+ }
20809
+ `;
20810
+ const Icon = ({
20811
+ href,
20812
+ children,
20813
+ charWidth = 1,
20814
+ // 0 (zéro) is the real char width
20815
+ // but 2 zéros gives too big icons
20816
+ // while 1 "W" gives a nice result
20817
+ baseChar = "W",
20818
+ decorative,
20819
+ onClick,
20820
+ ...props
20821
+ }) => {
20822
+ import.meta.css = [css$5, "@jsenv/navi/src/text/icon.jsx"];
20823
+ const innerChildren = href ? jsx("svg", {
20824
+ width: "100%",
20825
+ height: "100%",
20826
+ children: jsx("use", {
20827
+ href: href
20828
+ })
20829
+ }) : children;
20830
+ let {
20831
+ flex,
20832
+ grid,
20833
+ width,
20834
+ height
20835
+ } = props;
20836
+ if (width === "auto") {
20837
+ width = undefined;
20838
+ }
20839
+ if (height === "auto") {
20840
+ height = undefined;
20841
+ }
20842
+ const hasExplicitWidth = width !== undefined;
20843
+ const hasExplicitHeight = height !== undefined;
20844
+ const widthFixed = hasExplicitWidth || hasExplicitHeight && (props.square || props.circle || props.aspectRatio);
20845
+ const heightFixed = hasExplicitHeight || hasExplicitWidth && (props.square || props.circle || props.aspectRatio);
20846
+ if (widthFixed || heightFixed) {
20847
+ if (flex === undefined) {
20848
+ flex = "x";
20849
+ }
20850
+ } else if (decorative === undefined && !onClick) {
20851
+ decorative = true;
20852
+ }
20853
+ const ariaProps = decorative ? {
20854
+ "aria-hidden": "true"
20855
+ } : {};
20856
+ const textRef = useRef();
20857
+ if (typeof children === "string") {
20858
+ return jsx(Text, {
20859
+ ...props,
20860
+ ...ariaProps,
20861
+ "data-icon-text": "",
20862
+ children: children
20863
+ });
20864
+ }
20865
+ if (flex || grid) {
20866
+ return jsx(Box, {
20867
+ square: true,
20868
+ ...props,
20869
+ ...ariaProps,
20870
+ flex: flex,
20871
+ baseClassName: "navi_icon",
20872
+ "data-width-fixed": widthFixed ? "" : undefined,
20873
+ "data-height-fixed": heightFixed ? "" : undefined,
20874
+ "data-interactive": onClick ? "" : undefined,
20875
+ onClick: onClick,
20876
+ children: innerChildren
20877
+ });
20878
+ }
20879
+ const invisibleText = baseChar.repeat(charWidth);
20880
+ return jsx(SurroundingTextAligner, {
20881
+ align: "center",
20882
+ childRef: textRef,
20883
+ children: jsxs(Text, {
20884
+ ...props,
20885
+ ...ariaProps,
20886
+ className: withPropsClassName("navi_icon", props.className),
20887
+ spacing: "pre",
20888
+ "data-icon-char": "",
20889
+ "data-width-fixed": widthFixed ? "" : undefined,
20890
+ "data-height-fixed": heightFixed ? "" : undefined,
20891
+ "data-interactive": onClick ? "" : undefined,
20892
+ onClick: onClick,
20893
+ ref: textRef,
20894
+ children: [jsx("span", {
20895
+ className: "navi_icon_char_slot",
20896
+ "aria-hidden": "true",
20897
+ children: invisibleText
20898
+ }), jsx(Text, {
20899
+ className: "navi_icon_foreground",
20900
+ spacing: "pre",
20901
+ children: innerChildren
20902
+ })]
20903
+ })
20904
+ });
20767
20905
  };
20768
20906
 
20769
20907
  const useFormEvents = (
@@ -21287,7 +21425,7 @@ const useUIState = (uiStateController) => {
21287
21425
  return trackedUIState;
21288
21426
  };
21289
21427
 
21290
- installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
21428
+ installImportMetaCssBuild(import.meta);const css$4 = /* css */`
21291
21429
  @layer navi {
21292
21430
  .navi_button {
21293
21431
  --button-outline-width: 1px;
@@ -21413,6 +21551,7 @@ installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
21413
21551
  align-items: inherit;
21414
21552
  justify-content: inherit;
21415
21553
  color: var(--x-button-color);
21554
+ vertical-align: inherit;
21416
21555
  background: var(--x-button-background);
21417
21556
  background-color: var(
21418
21557
  --x-button-background-color,
@@ -21536,8 +21675,9 @@ installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
21536
21675
  --x-button-border-color: var(--callout-color);
21537
21676
  }
21538
21677
  }
21539
- `, "@jsenv/navi/src/field/button.jsx"];
21678
+ `;
21540
21679
  const Button = props => {
21680
+ import.meta.css = [css$4, "@jsenv/navi/src/field/button.jsx"];
21541
21681
  return renderActionableComponent(props, {
21542
21682
  Basic: ButtonBasic,
21543
21683
  WithAction: ButtonWithAction,
@@ -21595,6 +21735,7 @@ const ButtonBasic = props => {
21595
21735
  icon,
21596
21736
  revealOnInteraction = icon,
21597
21737
  discrete = icon && !revealOnInteraction,
21738
+ spacing,
21598
21739
  children,
21599
21740
  ...rest
21600
21741
  } = props;
@@ -21608,11 +21749,12 @@ const ButtonBasic = props => {
21608
21749
  const renderButtonContent = buttonProps => {
21609
21750
  return jsxs(Text, {
21610
21751
  ...buttonProps,
21752
+ spacing: spacing,
21611
21753
  className: "navi_button_content",
21612
21754
  children: [children, jsx(ButtonShadow, {})]
21613
21755
  });
21614
21756
  };
21615
- const renderButtonContentMemoized = useCallback(renderButtonContent, [children]);
21757
+ const renderButtonContentMemoized = useCallback(renderButtonContent, [children, spacing]);
21616
21758
  return jsxs(Box, {
21617
21759
  "data-readonly-silent": innerLoading ? "" : undefined,
21618
21760
  ...remainingProps,
@@ -22100,7 +22242,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
22100
22242
  };
22101
22243
 
22102
22244
  installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
22103
- import.meta.css = [/* css */`
22245
+ const css$3 = /* css */`
22104
22246
  @layer navi {
22105
22247
  .navi_link {
22106
22248
  --link-border-radius: unset;
@@ -22372,7 +22514,7 @@ import.meta.css = [/* css */`
22372
22514
  .navi_title .navi_link[data-reveal-on-interaction] {
22373
22515
  top: 0.25em;
22374
22516
  }
22375
- `, "@jsenv/navi/src/nav/link/link.jsx"];
22517
+ `;
22376
22518
  const LinkStyleCSSVars = {
22377
22519
  "outlineColor": "--link-outline-color",
22378
22520
  "borderRadius": "--link-border-radius",
@@ -22421,6 +22563,7 @@ Object.assign(PSEUDO_CLASSES, {
22421
22563
  }
22422
22564
  });
22423
22565
  const Link = props => {
22566
+ import.meta.css = [css$3, "@jsenv/navi/src/nav/link/link.jsx"];
22424
22567
  return renderActionableComponent(props, {
22425
22568
  Basic: LinkBasic,
22426
22569
  WithAction: LinkWithAction
@@ -22569,6 +22712,7 @@ const LinkPlain = props => {
22569
22712
  e.detail.setValue(value);
22570
22713
  },
22571
22714
  preventBoldLayoutShift: currentEffectBold,
22715
+ preventSpaceUnderlines: true,
22572
22716
  overflowEllipsis: overflowEllipsis
22573
22717
  // Visual
22574
22718
  ,
@@ -30403,6 +30547,8 @@ installImportMetaCssBuild(import.meta);const css = /* css */`
30403
30547
  @layer navi {
30404
30548
  }
30405
30549
  .navi_text.navi_badge_count {
30550
+ /* Important to prevent anchor from breaking to a new line */
30551
+ white-space: nowrap;
30406
30552
  --font-size: 0.7em;
30407
30553
  --x-background: var(--background);
30408
30554
  --x-background-color: var(--background-color, var(--x-background));
@@ -30411,9 +30557,9 @@ installImportMetaCssBuild(import.meta);const css = /* css */`
30411
30557
  --padding-x: 0.5em;
30412
30558
  --padding-y: 0.2em;
30413
30559
  position: relative;
30414
- display: inline-block;
30415
30560
  color: var(--x-color);
30416
30561
  font-size: var(--font-size);
30562
+ vertical-align: inherit;
30417
30563
 
30418
30564
  &[data-dark-background] {
30419
30565
  --x-color-contrasting: var(--navi-color-white);
@@ -30443,11 +30589,12 @@ installImportMetaCssBuild(import.meta);const css = /* css */`
30443
30589
 
30444
30590
  /* For ellipse + single char force the circle aspect as it's prettier */
30445
30591
  &[data-single-char] {
30592
+ display: inline-block;
30446
30593
  aspect-ratio: 1/1;
30447
- height: 1.5em;
30594
+ height: 1.6em;
30448
30595
  padding: 0;
30449
30596
  text-align: center;
30450
- line-height: 1.5em;
30597
+ line-height: 1.6em;
30451
30598
  }
30452
30599
  }
30453
30600
 
@@ -30467,19 +30614,19 @@ installImportMetaCssBuild(import.meta);const css = /* css */`
30467
30614
  border-radius: 50%;
30468
30615
 
30469
30616
  &[data-single-char] {
30470
- --x-radius: 1.5em;
30617
+ --x-radius: 1.6em;
30471
30618
  --x-number-font-size: unset;
30472
30619
  }
30473
30620
  &[data-two-chars] {
30474
- --x-radius: 1.8em;
30475
- --x-number-font-size: 0.9em;
30621
+ --x-radius: 2em;
30622
+ --x-number-font-size: unset;
30476
30623
  }
30477
30624
  &[data-three-chars] {
30478
30625
  --x-radius: 2.4em;
30479
30626
  --x-number-font-size: 0.8em;
30480
30627
  }
30481
30628
  &[data-four-chars] {
30482
- --x-radius: 2.6em;
30629
+ --x-radius: 2.4em;
30483
30630
  --x-number-font-size: 0.8em;
30484
30631
  }
30485
30632
 
@@ -30538,25 +30685,33 @@ const BadgeCount = ({
30538
30685
  circle = false;
30539
30686
  }
30540
30687
  if (circle) {
30541
- return jsxs(BadgeCountCircle, {
30542
- ...props,
30543
- loading: loading,
30544
- ref: ref,
30545
- hasOverflow: hasOverflow,
30546
- charCount: charCount,
30547
- children: [valueDisplayed, hasOverflow && maxElement]
30688
+ return jsx(SurroundingTextAligner, {
30689
+ align: "center",
30690
+ childRef: ref,
30691
+ children: jsxs(BadgeCountCircle, {
30692
+ ...props,
30693
+ loading: loading,
30694
+ ref: ref,
30695
+ hasOverflow: hasOverflow,
30696
+ charCount: charCount,
30697
+ children: [valueDisplayed, hasOverflow && maxElement]
30698
+ })
30548
30699
  });
30549
30700
  }
30550
30701
  const valueFormatted = typeof valueDisplayed === "number" ? formatNumber(valueDisplayed, {
30551
30702
  lang
30552
30703
  }) : valueDisplayed;
30553
- return jsxs(BadgeCountEllipse, {
30554
- ...props,
30555
- loading: loading,
30556
- ref: ref,
30557
- hasOverflow: hasOverflow,
30558
- charCount: charCount,
30559
- children: [valueFormatted, hasOverflow && maxElement]
30704
+ return jsx(SurroundingTextAligner, {
30705
+ align: "center",
30706
+ childRef: ref,
30707
+ children: jsxs(BadgeCountEllipse, {
30708
+ ...props,
30709
+ loading: loading,
30710
+ ref: ref,
30711
+ hasOverflow: hasOverflow,
30712
+ charCount: charCount,
30713
+ children: [valueFormatted, hasOverflow && maxElement]
30714
+ })
30560
30715
  });
30561
30716
  };
30562
30717
  const applyMaxToValue = (max, value) => {
@@ -30597,14 +30752,8 @@ const BadgeCountEllipse = ({
30597
30752
  spacing: "pre",
30598
30753
  children: loading ? jsx(Icon, {
30599
30754
  children: jsx(LoadingDots, {})
30600
- }) : jsxs(Fragment, {
30601
- children: [jsx("span", {
30602
- style: "user-select: none",
30603
- children: "\u200B"
30604
- }), children, jsx("span", {
30605
- style: "user-select: none",
30606
- children: "\u200B"
30607
- })]
30755
+ }) : jsx(Fragment, {
30756
+ children: children
30608
30757
  })
30609
30758
  });
30610
30759
  };
@@ -30633,17 +30782,11 @@ const BadgeCountCircle = ({
30633
30782
  spacing: "pre",
30634
30783
  children: loading ? jsx(Icon, {
30635
30784
  children: jsx(LoadingDots, {})
30636
- }) : jsxs(Fragment, {
30637
- children: [jsx("span", {
30638
- style: "user-select: none",
30639
- children: "\u200B"
30640
- }), jsx("span", {
30785
+ }) : jsx(Fragment, {
30786
+ children: jsx("span", {
30641
30787
  className: "navi_badge_count_text",
30642
30788
  children: children
30643
- }), jsx("span", {
30644
- style: "user-select: none",
30645
- children: "\u200B"
30646
- })]
30789
+ })
30647
30790
  })
30648
30791
  });
30649
30792
  };