@jsenv/navi 0.21.9 → 0.22.1

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,2178 @@ 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
19153
  };
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
- })
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
19495
 
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
-
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();
19648
+
19649
+ const areShortcutsEqual = (shortcutA, shortcutB) => {
19650
+ return (
19651
+ shortcutA.key === shortcutB.key &&
19652
+ shortcutA.description === shortcutB.description &&
19653
+ shortcutA.enabled === shortcutB.enabled
19654
+ );
19655
+ };
19923
19656
 
19924
- &[data-visible] {
19925
- opacity: 1;
19926
- }
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]);
20216
-
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;
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");
19907
+
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
+ textSize,
20039
+ size
20040
+ }) => {
20041
+ import.meta.css = [css$7, "@jsenv/navi/src/text/surrounding_text_aligner.jsx"];
20042
+ const anchorRef = useRef();
20043
+ useLayoutEffect(() => {
20044
+ const anchorEl = anchorRef.current;
20045
+ const childEl = childRef.current;
20046
+ if (!anchorEl || !childEl) {
20047
+ return;
20048
+ }
20049
+ const topOffset = computeTopOffset({
20050
+ anchorEl,
20051
+ childEl,
20052
+ align
20053
+ });
20054
+ if (topOffset) {
20055
+ childEl.style.position = "relative";
20056
+ childEl.style.top = `${topOffset}px`;
20057
+ } else {
20058
+ childEl.style.position = "";
20059
+ childEl.style.top = "";
20060
+ }
20061
+ }, [size, textSize]);
20062
+ return jsxs(Fragment, {
20063
+ children: [children, jsx("span", {
20064
+ ref: anchorRef,
20065
+ className: "navi_text_aligner_anchor",
20066
+ children: "\u200B"
20067
+ })]
20068
+ });
20402
20069
  };
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
- );
20070
+ const computeTopOffset = ({
20071
+ anchorEl,
20072
+ childEl,
20073
+ align
20074
+ }) => {
20075
+ if (align === "baseline") {
20076
+ return 0;
20077
+ }
20078
+ // Only correct when the anchor lives in an inline formatting context.
20079
+ // If the parent is a flex/grid container, inline layout rules don't apply
20080
+ // and our font-metrics model is invalid.
20081
+ const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
20082
+ if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
20083
+ return 0;
20084
+ }
20085
+ const anchorStyle = getComputedStyle(anchorEl);
20086
+ const anchorMetrics = measureFontAscDesc("M", anchorStyle);
20087
+ const [anchorABA, anchorABD] = anchorMetrics.actual;
20088
+ const anchorActH = anchorABA + anchorABD;
20089
+ const [, anchorFBBD] = anchorMetrics.font;
20090
+
20091
+ // Estimate the baseline Y from the anchor's bounding rect.
20092
+ // For an inline span, the font cell bottom is always at the element's bottom edge
20093
+ // (regardless of vertical-align), so baseline = rect.bottom - fontBoundingBoxDescent.
20094
+ const anchorRect = anchorEl.getBoundingClientRect();
20095
+ const baselineY = anchorRect.bottom - anchorFBBD;
20096
+ const anchorInkTopY = baselineY - anchorABA;
20097
+
20098
+ // Measure the child's current rect, then subtract any previously applied top correction
20099
+ // to recover its natural position — avoiding a style reset + reflow.
20100
+ const childRect = childEl.getBoundingClientRect();
20101
+ const childH = childRect.height;
20102
+ const previousTop = parseFloat(childEl.style.top) || 0;
20103
+ const childNaturalTop = childRect.top - previousTop;
20104
+
20105
+ // Compute desired child top Y based on align intention.
20106
+ let desiredChildTopY = 0;
20107
+ if (align === "center") {
20108
+ const anchorInkCenterY = anchorInkTopY + anchorActH / 2;
20109
+ desiredChildTopY = anchorInkCenterY - childH / 2;
20110
+ } else if (align === "start") {
20111
+ desiredChildTopY = anchorInkTopY;
20112
+ } else if (align === "end") {
20113
+ desiredChildTopY = anchorInkTopY + anchorActH - childH;
20114
+ }
20115
+ return desiredChildTopY - childNaturalTop;
20116
+ };
20117
+ const canvas = document.createElement("canvas");
20118
+ const measureFontAscDesc = (text, computedStyle) => {
20119
+ const ctx = canvas.getContext("2d");
20120
+ ctx.font = `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}`;
20121
+ const metrics = ctx.measureText(text);
20122
+ return {
20123
+ actual: [metrics.actualBoundingBoxAscent, metrics.actualBoundingBoxDescent],
20124
+ font: [metrics.fontBoundingBoxAscent, metrics.fontBoundingBoxDescent]
20125
+ };
20413
20126
  };
20414
20127
 
20415
- const areShortcutArraysEqual = (arrayA, arrayB) => {
20416
- if (arrayA.length !== arrayB.length) {
20417
- return false;
20128
+ const useInitialTextSelection = (ref, textSelection) => {
20129
+ const deps = [];
20130
+ if (Array.isArray(textSelection)) {
20131
+ deps.push(...textSelection);
20132
+ } else {
20133
+ deps.push(textSelection);
20418
20134
  }
20419
-
20420
- for (let i = 0; i < arrayA.length; i++) {
20421
- if (!areShortcutsEqual(arrayA[i], arrayB[i])) {
20422
- return false;
20135
+ useLayoutEffect(() => {
20136
+ const el = ref.current;
20137
+ if (!el || !textSelection) {
20138
+ return;
20423
20139
  }
20424
- }
20425
-
20426
- return true;
20140
+ const range = document.createRange();
20141
+ const selection = window.getSelection();
20142
+ if (Array.isArray(textSelection)) {
20143
+ if (textSelection.length === 2) {
20144
+ const [start, end] = textSelection;
20145
+ if (typeof start === "number" && typeof end === "number") {
20146
+ // Format: [0, 10] - character indices
20147
+ selectByCharacterIndices(el, range, start, end);
20148
+ } else if (typeof start === "string" && typeof end === "string") {
20149
+ // Format: ["Click on the", "button to return"] - text strings
20150
+ selectByTextStrings(el, range, start, end);
20151
+ }
20152
+ }
20153
+ } else if (typeof textSelection === "string") {
20154
+ // Format: "some text" - select the entire string occurrence
20155
+ selectSingleTextString(el, range, textSelection);
20156
+ }
20157
+ selection.removeAllRanges();
20158
+ selection.addRange(range);
20159
+ }, deps);
20427
20160
  };
20161
+ const selectByCharacterIndices = (element, range, startIndex, endIndex) => {
20162
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
20163
+ let currentIndex = 0;
20164
+ let startNode = null;
20165
+ let startOffset = 0;
20166
+ let endNode = null;
20167
+ let endOffset = 0;
20168
+ while (walker.nextNode()) {
20169
+ const textContent = walker.currentNode.textContent;
20170
+ const nodeLength = textContent.length;
20428
20171
 
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);
20172
+ // Check if start position is in this text node
20173
+ if (!startNode && currentIndex + nodeLength > startIndex) {
20174
+ startNode = walker.currentNode;
20175
+ startOffset = startIndex - currentIndex;
20436
20176
  }
20437
- }
20438
20177
 
20439
- // Only update if shortcuts have actually changed
20440
- if (!areShortcutArraysEqual(currentActiveShortcuts, activeShortcuts)) {
20441
- activeShortcutsSignal.value = activeShortcuts;
20178
+ // Check if end position is in this text node
20179
+ if (currentIndex + nodeLength >= endIndex) {
20180
+ endNode = walker.currentNode;
20181
+ endOffset = endIndex - currentIndex;
20182
+ break;
20183
+ }
20184
+ currentIndex += nodeLength;
20185
+ }
20186
+ if (startNode && endNode) {
20187
+ range.setStart(startNode, startOffset);
20188
+ range.setEnd(endNode, endOffset);
20442
20189
  }
20443
20190
  };
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();
20191
+ const selectSingleTextString = (element, range, text) => {
20192
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
20193
+ while (walker.nextNode()) {
20194
+ const textContent = walker.currentNode.textContent;
20195
+ const index = textContent.indexOf(text);
20196
+ if (index !== -1) {
20197
+ range.setStart(walker.currentNode, index);
20198
+ range.setEnd(walker.currentNode, index + text.length);
20199
+ return;
20200
+ }
20201
+ }
20452
20202
  };
20453
- const removeShortcuts = (element) => {
20454
- shortcutsMap.delete(element);
20455
- updateActiveShortcuts();
20203
+ const selectByTextStrings = (element, range, startText, endText) => {
20204
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
20205
+ let startNode = null;
20206
+ let endNode = null;
20207
+ let foundStart = false;
20208
+ while (walker.nextNode()) {
20209
+ const textContent = walker.currentNode.textContent;
20210
+ if (!foundStart && textContent.includes(startText)) {
20211
+ startNode = walker.currentNode;
20212
+ foundStart = true;
20213
+ }
20214
+ if (foundStart && textContent.includes(endText)) {
20215
+ endNode = walker.currentNode;
20216
+ break;
20217
+ }
20218
+ }
20219
+ if (startNode && endNode) {
20220
+ const startOffset = startNode.textContent.indexOf(startText);
20221
+ const endOffset = endNode.textContent.indexOf(endText) + endText.length;
20222
+ range.setStart(startNode, startOffset);
20223
+ range.setEnd(endNode, endOffset);
20224
+ }
20456
20225
  };
20457
20226
 
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
- );
20227
+ installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
20228
+ const css$6 = /* css */`
20229
+ @layer navi {
20230
+ .navi_text {
20231
+ &[data-skeleton] {
20232
+ border-radius: 0.2em;
20233
+ }
20234
+ }
20474
20235
  }
20475
20236
 
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;
20237
+ *[data-navi-space] {
20238
+ }
20239
+
20240
+ .navi_text {
20241
+ position: relative;
20242
+
20243
+ /* There is a chrome specific bug that prevents text-transform: capitalize to be applied in nested DOM structure */
20244
+ /* The CSS below ensure capitalize is propagated to the bold clones */
20245
+ &[data-capitalize] {
20246
+ &::first-letter {
20247
+ text-transform: uppercase;
20486
20248
  }
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;
20249
+ .navi_text_bold_clone::first-letter {
20250
+ text-transform: uppercase;
20498
20251
  }
20499
- if (!allowConcurrentActions) {
20500
- shortcutActionIsBusyRef.current = true;
20252
+ .navi_text_bold_foreground::first-letter {
20253
+ text-transform: uppercase;
20501
20254
  }
20502
- shortcut.onStart?.(e);
20503
- onActionStart?.(e);
20504
- },
20505
- onAbort: (e) => {
20506
- const { shortcut } = e.detail.meta || {};
20507
- if (!shortcut) {
20508
- return;
20255
+ }
20256
+
20257
+ .navi_text_bold_wrapper,
20258
+ .navi_text_bold_clone,
20259
+ .navi_text_bold_foreground {
20260
+ display: inherit;
20261
+ width: inherit;
20262
+ min-width: inherit;
20263
+ height: inherit;
20264
+ min-height: inherit;
20265
+ flex-grow: inherit;
20266
+ align-items: inherit;
20267
+ justify-content: inherit;
20268
+ gap: inherit;
20269
+ text-align: inherit;
20270
+ border-radius: inherit;
20271
+ }
20272
+
20273
+ &[data-text-overflow] {
20274
+ min-width: 0;
20275
+ flex-wrap: wrap;
20276
+ text-overflow: ellipsis;
20277
+ overflow: hidden;
20278
+
20279
+ .navi_text_overflow_wrapper {
20280
+ display: flex;
20281
+ width: 100%;
20282
+ flex-grow: 1;
20283
+ gap: 0.3em;
20284
+
20285
+ .navi_text_overflow_text {
20286
+ max-width: 100%;
20287
+ text-overflow: ellipsis;
20288
+ overflow: hidden;
20289
+ }
20290
+ }
20291
+ }
20292
+
20293
+ &[data-skeleton] {
20294
+ /* Children stay in the DOM to preserve natural layout dimensions,
20295
+ but are hidden so only the skeleton is visible. */
20296
+ visibility: hidden;
20297
+
20298
+ /* When there are no children a placeholder "W" is injected (see JSX).
20299
+ It must stretch to the full available width so the skeleton
20300
+ fills the container rather than collapsing to a single character. */
20301
+ .navi_text_skeleton_children_placeholder {
20302
+ display: inline-flex;
20303
+ width: 100%;
20304
+ }
20305
+
20306
+ /* Three-level structure to respect padding AND border-radius:
20307
+
20308
+ 1. navi_text_skeleton_container — absolutely fills the border box
20309
+ (inset:0), then applies padding:inherit so its content box equals
20310
+ the parent's content box. line-height:normal prevents the container
20311
+ from inheriting a large line-height that would make it taller than
20312
+ the border box. border-radius:inherit passes the radius down.
20313
+ visibility:visible overrides the parent's visibility:hidden.
20314
+
20315
+ 2. navi_text_skeleton_inset — a relative block that fills 100% of the
20316
+ container's content box (= parent's content box). It is the
20317
+ positioned ancestor for the absolutely placed skeleton bar.
20318
+ border-radius:inherit chains the radius further down.
20319
+
20320
+ 3. navi_text_skeleton — the visible gradient bar. position:absolute
20321
+ inset:0 fills the inset box precisely. border-radius:inherit
20322
+ finally applies the radius at this level, which is now correctly
20323
+ sized to the content area. */
20324
+ .navi_text_skeleton_container {
20325
+ position: absolute;
20326
+ inset: 0;
20327
+ padding: inherit;
20328
+ line-height: normal;
20329
+ border-radius: inherit;
20330
+ visibility: visible;
20509
20331
  }
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;
20332
+
20333
+ .navi_text_skeleton_inset {
20334
+ position: relative;
20335
+ display: inline-flex;
20336
+ width: 100%;
20337
+ height: 100%;
20338
+ border-radius: inherit;
20518
20339
  }
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;
20340
+
20341
+ .navi_text_skeleton {
20342
+ position: absolute;
20343
+ inset: 0;
20344
+ background: linear-gradient(
20345
+ 90deg,
20346
+ #e0e0e0 25%,
20347
+ #f0f0f0 50%,
20348
+ #e0e0e0 75%
20349
+ );
20350
+ background-size: 200% 100%;
20351
+ border-radius: inherit;
20527
20352
  }
20528
- shortcutActionIsBusyRef.current = false;
20529
- shortcut.onEnd?.(e);
20530
- onActionEnd?.(e);
20531
- },
20532
- });
20533
20353
 
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);
20354
+ &[data-loading] {
20355
+ .navi_text_skeleton {
20356
+ animation: navi_text_skeleton_shimmer 1.5s infinite;
20357
+ }
20358
+ }
20359
+ }
20543
20360
  }
20544
20361
 
20545
- useEffect(() => {
20546
- const element = elementRef.current;
20547
- if (!element) {
20548
- return null;
20362
+ @keyframes navi_text_skeleton_shimmer {
20363
+ 0% {
20364
+ background-position: 200% 0;
20549
20365
  }
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
- });
20366
+ 100% {
20367
+ background-position: -200% 0;
20574
20368
  }
20369
+ }
20575
20370
 
20576
- addShortcuts(element, shortcuts);
20371
+ .navi_text_bold_wrapper {
20372
+ position: relative;
20373
+ display: inline-block;
20577
20374
 
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
- };
20375
+ .navi_text_bold_clone {
20376
+ font-weight: bold;
20377
+ opacity: 0;
20378
+ }
20379
+ .navi_text_bold_foreground {
20380
+ position: absolute;
20381
+ inset: 0;
20382
+ }
20383
+ }
20588
20384
 
20589
- const applyKeyboardShortcuts = (shortcuts, keyboardEvent) => {
20590
- if (!canInterceptKeys(keyboardEvent)) {
20591
- return null;
20385
+ .navi_text_bold_background {
20386
+ position: absolute;
20387
+ top: 0;
20388
+ left: 0;
20389
+ color: currentColor;
20390
+ font-weight: normal;
20391
+ background: currentColor;
20392
+ background-clip: text;
20393
+ -webkit-background-clip: text;
20394
+ transform-origin: center;
20395
+ -webkit-text-fill-color: transparent;
20396
+ opacity: 0;
20592
20397
  }
20593
- for (const shortcutCandidate of shortcuts) {
20594
- let { enabled = true, key } = shortcutCandidate;
20595
- if (!enabled) {
20596
- continue;
20597
- }
20598
20398
 
20599
- if (typeof key === "function") {
20600
- const keyReturnValue = key(keyboardEvent);
20601
- if (!keyReturnValue) {
20602
- continue;
20603
- }
20604
- key = keyReturnValue;
20399
+ .navi_text[data-bold] {
20400
+ .navi_text_bold_background {
20401
+ opacity: 1;
20605
20402
  }
20606
- if (!key) {
20607
- console.error(shortcutCandidate);
20608
- throw new TypeError(`key is required in keyboard shortcut, got ${key}`);
20403
+ }
20404
+
20405
+ .navi_text[data-bold-transition] {
20406
+ .navi_text_bold_foreground {
20407
+ transition-property: font-weight;
20408
+ transition-duration: 0.3s;
20409
+ transition-timing-function: ease;
20609
20410
  }
20610
20411
 
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;
20412
+ .navi_text_bold_background {
20413
+ transition-property: opacity;
20414
+ transition-duration: 0.3s;
20415
+ transition-timing-function: ease;
20416
+ }
20417
+ }
20418
+ `;
20419
+ const REGULAR_SPACE = jsx("span", {
20420
+ "data-navi-space": "",
20421
+ children: " "
20422
+ });
20423
+ // A space that uses padding-left instead of a real space character.
20424
+ // This avoids the underline that browsers draw under spaces inside links.
20425
+ const FAKE_SPACE = jsx("span", {
20426
+ "data-navi-space": "",
20427
+ style: "padding-left: 0.25em",
20428
+ children: "\u200B"
20429
+ });
20430
+ const CustomWidthSpace = ({
20431
+ value,
20432
+ useRealSpaceChar
20433
+ }) => {
20434
+ if (useRealSpaceChar) {
20435
+ return jsxs("span", {
20436
+ children: [jsx("span", {
20437
+ style: "font-size: 0",
20438
+ children: " "
20439
+ }), jsx("span", {
20440
+ style: `padding-left: ${value}`,
20441
+ children: "\u200B"
20442
+ })]
20443
+ });
20444
+ }
20445
+ return jsx("span", {
20446
+ style: `padding-left: ${value}`,
20447
+ children: "\u200B"
20448
+ });
20449
+ };
20450
+ const applySpacingOnTextChildren = (children, spacing, defaultSpace) => {
20451
+ if (spacing === "pre" || spacing === "0" || spacing === 0) {
20452
+ return children;
20453
+ }
20454
+ if (!children) {
20455
+ return children;
20456
+ }
20457
+ const childArray = toChildArray(children);
20458
+ const childCount = childArray.length;
20459
+ if (childCount <= 1) {
20460
+ return children;
20461
+ }
20462
+ const useRealSpaceChar = defaultSpace !== FAKE_SPACE;
20463
+ let separator;
20464
+ if (spacing === REGULAR_SPACE || spacing === FAKE_SPACE) {
20465
+ separator = defaultSpace;
20466
+ } else if (typeof spacing === "string") {
20467
+ if (isSizeSpacingScaleKey(spacing) || hasCSSSizeUnit(spacing)) {
20468
+ separator = jsx(CustomWidthSpace, {
20469
+ value: resolveSpacingSize(spacing),
20470
+ useRealSpaceChar: useRealSpaceChar
20471
+ });
20616
20472
  } else {
20617
- actualCombination = key;
20618
- if (containsPlatformSpecificKeys(key)) {
20619
- crossPlatformCombination = generateCrossPlatformCombination(key);
20620
- }
20473
+ separator = spacing;
20621
20474
  }
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;
20475
+ } else if (typeof spacing === "number") {
20476
+ separator = jsx(CustomWidthSpace, {
20477
+ value: `${spacing}px`,
20478
+ useRealSpaceChar: useRealSpaceChar
20479
+ });
20480
+ } else {
20481
+ separator = spacing;
20482
+ }
20483
+ const childrenWithGap = [];
20484
+ let i = 0;
20485
+ while (true) {
20486
+ const child = childArray[i];
20487
+ childrenWithGap.push(child);
20488
+ i++;
20489
+ if (i === childCount) {
20490
+ break;
20637
20491
  }
20638
- if (typeof enabled === "function" && !enabled(keyboardEvent)) {
20492
+ const currentChild = childArray[i - 1];
20493
+ const nextChild = childArray[i];
20494
+ if (!shouldInjectSpacingBetween(currentChild, nextChild)) {
20639
20495
  continue;
20640
20496
  }
20641
- const returnValue = shortcutCandidate.handler(keyboardEvent);
20642
- if (returnValue) {
20643
- keyboardEvent.preventDefault();
20644
- }
20645
- return shortcutCandidate;
20497
+ childrenWithGap.push(separator);
20646
20498
  }
20647
- return null;
20499
+ return childrenWithGap;
20648
20500
  };
20649
- const containsPlatformSpecificKeys = (combination) => {
20650
- const lowerCombination = combination.toLowerCase();
20651
- const macSpecificKeys = ["command", "cmd"];
20652
-
20653
- return macSpecificKeys.some((key) => lowerCombination.includes(key));
20501
+ const outsideTextFlowSet = new Set();
20502
+ const markAsOutsideTextFlow = jsxElement => {
20503
+ outsideTextFlowSet.add(jsxElement);
20654
20504
  };
20655
- const generateCrossPlatformCombination = (combination) => {
20656
- let crossPlatform = combination;
20505
+ const isMarkedAsOutsideTextFlow = jsxElement => {
20506
+ return outsideTextFlowSet.has(jsxElement.type);
20507
+ };
20508
+ const isPreactNode = jsxChild => {
20509
+ return jsxChild !== null && typeof jsxChild === "object" && jsxChild.type !== undefined;
20510
+ };
20511
+ const shouldInjectSpacingBetween = (left, right) => {
20512
+ const leftIsNode = isPreactNode(left);
20513
+ const rightIsNode = isPreactNode(right);
20514
+ // only inject spacing when at least one side is a preact node
20515
+ if (!leftIsNode && !rightIsNode) {
20516
+ return false;
20517
+ }
20518
+ if (leftIsNode && isMarkedAsOutsideTextFlow(left)) {
20519
+ return false;
20520
+ }
20521
+ if (rightIsNode && isMarkedAsOutsideTextFlow(right)) {
20522
+ return false;
20523
+ }
20524
+ if (rightIsNode && right.props && right.props.overflowPinned) {
20525
+ return false;
20526
+ }
20527
+ if (typeof left === "string" && /\s$/.test(left)) {
20528
+ return false;
20529
+ }
20530
+ if (typeof right === "string" && /^\s/.test(right)) {
20531
+ return false;
20532
+ }
20533
+ return true;
20534
+ };
20535
+ const OverflowPinnedElementContext = createContext(null);
20536
+ const Text = props => {
20537
+ import.meta.css = [css$6, "@jsenv/navi/src/text/text.jsx"];
20538
+ if (props.loading || props.skeleton) {
20539
+ return jsx(TextSkeleton, {
20540
+ ...props
20541
+ });
20542
+ }
20543
+ if (props.overflowEllipsis) {
20544
+ return jsx(TextOverflow, {
20545
+ ...props
20546
+ });
20547
+ }
20548
+ if (props.overflowPinned) {
20549
+ return jsx(TextOverflowPinned, {
20550
+ ...props
20551
+ });
20552
+ }
20553
+ if (props.selectRange) {
20554
+ return jsx(TextWithSelectRange, {
20555
+ ...props
20556
+ });
20557
+ }
20558
+ return jsx(TextBasic, {
20559
+ ...props
20560
+ });
20561
+ };
20562
+ const TextSkeleton = ({
20563
+ loading,
20564
+ children,
20565
+ ...props
20566
+ }) => {
20567
+ // Three-level structure — see CSS comment on [data-skeleton] for details.
20568
+ const skeletonOverlay = jsx("span", {
20569
+ className: "navi_text_skeleton_container",
20570
+ "aria-hidden": "true",
20571
+ children: jsx("span", {
20572
+ className: "navi_text_skeleton_inset",
20573
+ children: jsx("span", {
20574
+ className: "navi_text_skeleton"
20575
+ })
20576
+ })
20577
+ });
20578
+ // When there are no children, inject a full-width placeholder so the element
20579
+ // has measurable height driven by the current font-size/line-height, and the
20580
+ // skeleton fills the available width instead of shrinking to a single char.
20581
+ const hasChildren = children !== null && children !== undefined && children !== false;
20582
+ const innerChildren = hasChildren ? children : jsx("span", {
20583
+ className: "navi_text_skeleton_children_placeholder",
20584
+ "aria-hidden": "true",
20585
+ children: "W"
20586
+ });
20587
+ return jsx(Text, {
20588
+ "data-skeleton": "",
20589
+ "data-loading": loading ? "" : undefined,
20590
+ ...props,
20591
+ skeleton: undefined,
20592
+ childrenOutsideFlow: skeletonOverlay,
20593
+ children: innerChildren
20594
+ });
20595
+ };
20596
+ const TextOverflow = ({
20597
+ noWrap,
20598
+ spacing,
20599
+ children,
20600
+ ...rest
20601
+ }) => {
20602
+ const [OverflowPinnedElement, setOverflowPinnedElement] = useState(null);
20603
+ return jsx(Text, {
20604
+ flex: true,
20605
+ block: true,
20606
+ as: "div",
20607
+ nowWrap: noWrap,
20608
+ pre: !noWrap
20609
+ // For paragraph we prefer to keep lines and only hide unbreakable long sections
20610
+ ,
20657
20611
 
20658
- if (isMac) {
20659
- // No need to convert anything TO Windows/Linux-specific format since we're on Mac
20612
+ preLine: rest.as === "p",
20613
+ ...rest,
20614
+ overflowEllipsis: undefined,
20615
+ "data-text-overflow": "",
20616
+ spacing: "pre",
20617
+ children: jsxs("span", {
20618
+ className: "navi_text_overflow_wrapper",
20619
+ children: [jsx(OverflowPinnedElementContext.Provider, {
20620
+ value: setOverflowPinnedElement,
20621
+ children: jsx(Text, {
20622
+ className: "navi_text_overflow_text",
20623
+ spacing: spacing,
20624
+ children: children
20625
+ })
20626
+ }), OverflowPinnedElement]
20627
+ })
20628
+ });
20629
+ };
20630
+ const TextOverflowPinned = ({
20631
+ overflowPinned,
20632
+ ...props
20633
+ }) => {
20634
+ const setOverflowPinnedElement = useContext(OverflowPinnedElementContext);
20635
+ const text = jsx(Text, {
20636
+ ...props,
20637
+ "data-overflow-pinned": ""
20638
+ });
20639
+ if (!setOverflowPinnedElement) {
20640
+ console.warn("<Text overflowPinned> declared outside a <Text overflowEllipsis>");
20641
+ return text;
20642
+ }
20643
+ if (overflowPinned) {
20644
+ setOverflowPinnedElement(text);
20660
20645
  return null;
20661
20646
  }
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;
20647
+ setOverflowPinnedElement(null);
20648
+ return text;
20667
20649
  };
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];
20650
+ const TextWithSelectRange = ({
20651
+ selectRange,
20652
+ ...props
20653
+ }) => {
20654
+ const defaultRef = useRef();
20655
+ const ref = props.ref || defaultRef;
20656
+ useInitialTextSelection(ref, selectRange);
20657
+ return jsx(Text, {
20658
+ ref: ref,
20659
+ ...props
20660
+ });
20661
+ };
20662
+ const TextBasic = ({
20663
+ spacing,
20664
+ preventSpaceUnderlines = false,
20665
+ boldTransition,
20666
+ boldStable,
20667
+ preventBoldLayoutShift = boldTransition,
20668
+ capitalize,
20669
+ children,
20670
+ childrenOutsideFlow,
20671
+ ...rest
20672
+ }) => {
20673
+ const defaultSpace = preventSpaceUnderlines ? FAKE_SPACE : REGULAR_SPACE;
20674
+ const resolvedSpacing = spacing ?? defaultSpace;
20675
+ const boxProps = {
20676
+ "as": "span",
20677
+ "data-bold-transition": boldTransition ? "" : undefined,
20678
+ "data-capitalize": capitalize ? "" : undefined,
20679
+ ...rest,
20680
+ "baseClassName": withPropsClassName("navi_text", rest.baseClassName)
20681
+ };
20682
+ const shouldPreserveSpacing = rest.as === "pre" || rest.flex || rest.grid;
20683
+ if (shouldPreserveSpacing) {
20684
+ boxProps.spacing = resolvedSpacing;
20685
+ } else {
20686
+ children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
20687
+ }
20688
+ if (boldStable) {
20689
+ const {
20690
+ bold
20691
+ } = boxProps;
20692
+ return jsxs(Box, {
20693
+ ...boxProps,
20694
+ bold: undefined,
20695
+ "data-bold": bold ? "" : undefined,
20696
+ children: [jsx("span", {
20697
+ className: "navi_text_bold_background",
20698
+ "aria-hidden": "true",
20699
+ children: children
20700
+ }), children, childrenOutsideFlow]
20701
+ });
20702
+ }
20703
+ if (preventBoldLayoutShift) {
20704
+ const alignX = rest.alignX || rest.align || "start";
20677
20705
 
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
- }
20706
+ // La technique consiste a avoid un double gras qui force une taille
20707
+ // et la version light par dessus en position absolute
20708
+ // on la centre aussi pour donner l'impression que le gras s'applique depuis le centre
20709
+ // ne fonctionne que sur une seule ligne de texte (donc lorsque noWrap est actif)
20710
+ // on pourrait auto-active cela sur une prop genre boldCanChange
20711
+ return jsxs(Box, {
20712
+ ...boxProps,
20713
+ children: [jsxs("span", {
20714
+ className: "navi_text_bold_wrapper",
20715
+ children: [jsx("span", {
20716
+ className: "navi_text_bold_clone",
20717
+ "aria-hidden": "true",
20718
+ children: children
20719
+ }), jsx("span", {
20720
+ className: "navi_text_bold_foreground",
20721
+ "data-align": alignX,
20722
+ children: children
20723
+ })]
20724
+ }), childrenOutsideFlow]
20725
+ });
20726
+ }
20727
+ return jsxs(Box, {
20728
+ ...boxProps,
20729
+ children: [children, childrenOutsideFlow]
20730
+ });
20731
+ };
20682
20732
 
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;
20733
+ installImportMetaCssBuild(import.meta);const css$5 = /* css */`
20734
+ @layer navi {
20735
+ /* Ensure data attributes from box.jsx can win to update display */
20736
+ .navi_icon {
20737
+ display: inline-block;
20738
+ box-sizing: border-box;
20739
+ max-width: 100%;
20740
+ max-height: 100%;
20694
20741
  }
20742
+ }
20695
20743
 
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
- }
20744
+ .navi_icon {
20745
+ white-space: nowrap;
20746
+ vertical-align: inherit;
20714
20747
 
20715
- const eventKeyCode = eventKey.charCodeAt(0);
20716
- const startCode = startChar.charCodeAt(0);
20717
- const endCode = endChar.charCodeAt(0);
20748
+ &[data-flow-inline] {
20749
+ width: 1em;
20750
+ height: 1em;
20751
+ }
20752
+ &[data-icon-char] {
20753
+ flex-grow: 0 !important;
20718
20754
 
20719
- if (eventKeyCode >= startCode && eventKeyCode <= endCode) {
20720
- continue; // Range matched
20721
- }
20722
- return false; // Range not matched
20755
+ svg,
20756
+ img {
20757
+ width: 100%;
20758
+ height: 100%;
20759
+ }
20760
+ svg {
20761
+ overflow: visible;
20723
20762
  }
20724
20763
  }
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;
20764
+ &[data-interactive] {
20765
+ cursor: pointer;
20729
20766
  }
20730
20767
  }
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
20768
 
20754
- if (browserEventKey === key) {
20755
- return true;
20769
+ .navi_icon_char_slot {
20770
+ opacity: 0;
20771
+ cursor: default;
20772
+ user-select: none;
20756
20773
  }
20774
+ .navi_text.navi_icon_foreground {
20775
+ position: absolute;
20776
+ inset: 0;
20777
+ display: inline-flex;
20757
20778
 
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;
20779
+ & > .navi_text {
20780
+ display: flex;
20781
+ aspect-ratio: 1 / 1;
20782
+ min-width: 0;
20783
+ height: 100%;
20784
+ max-height: 1em;
20785
+ align-items: center;
20786
+ justify-content: center;
20763
20787
  }
20764
20788
  }
20765
20789
 
20766
- return false;
20790
+ .navi_icon > svg,
20791
+ .navi_icon > img {
20792
+ width: 100%;
20793
+ height: 100%;
20794
+ backface-visibility: hidden;
20795
+ }
20796
+ .navi_icon[data-width-fixed] > svg,
20797
+ .navi_icon[data-width-fixed] > img {
20798
+ width: 100%;
20799
+ height: auto;
20800
+ }
20801
+ .navi_icon[data-height-fixed] > svg,
20802
+ .navi_icon[data-height-fixed] > img {
20803
+ width: auto;
20804
+ height: 100%;
20805
+ }
20806
+ .navi_icon[data-width-fixed][data-height-fixed] > svg,
20807
+ .navi_icon[data-width-fixed][data-height-fixed] > img {
20808
+ width: 100%;
20809
+ height: 100%;
20810
+ }
20811
+ `;
20812
+ const Icon = ({
20813
+ href,
20814
+ children,
20815
+ charWidth = 1,
20816
+ // 0 (zéro) is the real char width
20817
+ // but 2 zéros gives too big icons
20818
+ // while 1 "W" gives a nice result
20819
+ baseChar = "W",
20820
+ decorative,
20821
+ onClick,
20822
+ ...props
20823
+ }) => {
20824
+ import.meta.css = [css$5, "@jsenv/navi/src/text/icon.jsx"];
20825
+ const innerChildren = href ? jsx("svg", {
20826
+ width: "100%",
20827
+ height: "100%",
20828
+ children: jsx("use", {
20829
+ href: href
20830
+ })
20831
+ }) : children;
20832
+ let {
20833
+ flex,
20834
+ grid,
20835
+ width,
20836
+ height
20837
+ } = props;
20838
+ if (width === "auto") {
20839
+ width = undefined;
20840
+ }
20841
+ if (height === "auto") {
20842
+ height = undefined;
20843
+ }
20844
+ const hasExplicitWidth = width !== undefined;
20845
+ const hasExplicitHeight = height !== undefined;
20846
+ const widthFixed = hasExplicitWidth || hasExplicitHeight && (props.square || props.circle || props.aspectRatio);
20847
+ const heightFixed = hasExplicitHeight || hasExplicitWidth && (props.square || props.circle || props.aspectRatio);
20848
+ if (widthFixed || heightFixed) {
20849
+ if (flex === undefined) {
20850
+ flex = "x";
20851
+ }
20852
+ } else if (decorative === undefined && !onClick) {
20853
+ decorative = true;
20854
+ }
20855
+ const ariaProps = decorative ? {
20856
+ "aria-hidden": "true"
20857
+ } : {};
20858
+ const textRef = useRef();
20859
+ if (typeof children === "string") {
20860
+ return jsx(Text, {
20861
+ ...props,
20862
+ ...ariaProps,
20863
+ "data-icon-text": "",
20864
+ children: children
20865
+ });
20866
+ }
20867
+ if (flex || grid) {
20868
+ return jsx(Box, {
20869
+ square: true,
20870
+ ...props,
20871
+ ...ariaProps,
20872
+ flex: flex,
20873
+ baseClassName: "navi_icon",
20874
+ "data-width-fixed": widthFixed ? "" : undefined,
20875
+ "data-height-fixed": heightFixed ? "" : undefined,
20876
+ "data-interactive": onClick ? "" : undefined,
20877
+ onClick: onClick,
20878
+ children: innerChildren
20879
+ });
20880
+ }
20881
+ const invisibleText = baseChar.repeat(charWidth);
20882
+ return jsx(SurroundingTextAligner, {
20883
+ align: "center",
20884
+ childRef: textRef,
20885
+ size: props.size,
20886
+ textSize: props.textSize,
20887
+ children: jsxs(Text, {
20888
+ ...props,
20889
+ ...ariaProps,
20890
+ className: withPropsClassName("navi_icon", props.className),
20891
+ spacing: "pre",
20892
+ "data-icon-char": "",
20893
+ "data-width-fixed": widthFixed ? "" : undefined,
20894
+ "data-height-fixed": heightFixed ? "" : undefined,
20895
+ "data-interactive": onClick ? "" : undefined,
20896
+ onClick: onClick,
20897
+ ref: textRef,
20898
+ children: [jsx("span", {
20899
+ className: "navi_icon_char_slot",
20900
+ "aria-hidden": "true",
20901
+ children: invisibleText
20902
+ }), jsx(Text, {
20903
+ className: "navi_icon_foreground",
20904
+ spacing: "pre",
20905
+ children: innerChildren
20906
+ })]
20907
+ })
20908
+ });
20767
20909
  };
20768
20910
 
20769
20911
  const useFormEvents = (
@@ -21287,7 +21429,7 @@ const useUIState = (uiStateController) => {
21287
21429
  return trackedUIState;
21288
21430
  };
21289
21431
 
21290
- installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
21432
+ installImportMetaCssBuild(import.meta);const css$4 = /* css */`
21291
21433
  @layer navi {
21292
21434
  .navi_button {
21293
21435
  --button-outline-width: 1px;
@@ -21413,6 +21555,7 @@ installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
21413
21555
  align-items: inherit;
21414
21556
  justify-content: inherit;
21415
21557
  color: var(--x-button-color);
21558
+ vertical-align: inherit;
21416
21559
  background: var(--x-button-background);
21417
21560
  background-color: var(
21418
21561
  --x-button-background-color,
@@ -21536,8 +21679,9 @@ installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
21536
21679
  --x-button-border-color: var(--callout-color);
21537
21680
  }
21538
21681
  }
21539
- `, "@jsenv/navi/src/field/button.jsx"];
21682
+ `;
21540
21683
  const Button = props => {
21684
+ import.meta.css = [css$4, "@jsenv/navi/src/field/button.jsx"];
21541
21685
  return renderActionableComponent(props, {
21542
21686
  Basic: ButtonBasic,
21543
21687
  WithAction: ButtonWithAction,
@@ -21595,6 +21739,7 @@ const ButtonBasic = props => {
21595
21739
  icon,
21596
21740
  revealOnInteraction = icon,
21597
21741
  discrete = icon && !revealOnInteraction,
21742
+ spacing,
21598
21743
  children,
21599
21744
  ...rest
21600
21745
  } = props;
@@ -21608,11 +21753,12 @@ const ButtonBasic = props => {
21608
21753
  const renderButtonContent = buttonProps => {
21609
21754
  return jsxs(Text, {
21610
21755
  ...buttonProps,
21756
+ spacing: spacing,
21611
21757
  className: "navi_button_content",
21612
21758
  children: [children, jsx(ButtonShadow, {})]
21613
21759
  });
21614
21760
  };
21615
- const renderButtonContentMemoized = useCallback(renderButtonContent, [children]);
21761
+ const renderButtonContentMemoized = useCallback(renderButtonContent, [children, spacing]);
21616
21762
  return jsxs(Box, {
21617
21763
  "data-readonly-silent": innerLoading ? "" : undefined,
21618
21764
  ...remainingProps,
@@ -22100,7 +22246,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
22100
22246
  };
22101
22247
 
22102
22248
  installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
22103
- import.meta.css = [/* css */`
22249
+ const css$3 = /* css */`
22104
22250
  @layer navi {
22105
22251
  .navi_link {
22106
22252
  --link-border-radius: unset;
@@ -22372,7 +22518,7 @@ import.meta.css = [/* css */`
22372
22518
  .navi_title .navi_link[data-reveal-on-interaction] {
22373
22519
  top: 0.25em;
22374
22520
  }
22375
- `, "@jsenv/navi/src/nav/link/link.jsx"];
22521
+ `;
22376
22522
  const LinkStyleCSSVars = {
22377
22523
  "outlineColor": "--link-outline-color",
22378
22524
  "borderRadius": "--link-border-radius",
@@ -22421,6 +22567,7 @@ Object.assign(PSEUDO_CLASSES, {
22421
22567
  }
22422
22568
  });
22423
22569
  const Link = props => {
22570
+ import.meta.css = [css$3, "@jsenv/navi/src/nav/link/link.jsx"];
22424
22571
  return renderActionableComponent(props, {
22425
22572
  Basic: LinkBasic,
22426
22573
  WithAction: LinkWithAction
@@ -22569,6 +22716,7 @@ const LinkPlain = props => {
22569
22716
  e.detail.setValue(value);
22570
22717
  },
22571
22718
  preventBoldLayoutShift: currentEffectBold,
22719
+ preventSpaceUnderlines: true,
22572
22720
  overflowEllipsis: overflowEllipsis
22573
22721
  // Visual
22574
22722
  ,
@@ -30403,6 +30551,8 @@ installImportMetaCssBuild(import.meta);const css = /* css */`
30403
30551
  @layer navi {
30404
30552
  }
30405
30553
  .navi_text.navi_badge_count {
30554
+ /* Important to prevent anchor from breaking to a new line */
30555
+ white-space: nowrap;
30406
30556
  --font-size: 0.7em;
30407
30557
  --x-background: var(--background);
30408
30558
  --x-background-color: var(--background-color, var(--x-background));
@@ -30411,9 +30561,9 @@ installImportMetaCssBuild(import.meta);const css = /* css */`
30411
30561
  --padding-x: 0.5em;
30412
30562
  --padding-y: 0.2em;
30413
30563
  position: relative;
30414
- display: inline-block;
30415
30564
  color: var(--x-color);
30416
30565
  font-size: var(--font-size);
30566
+ vertical-align: inherit;
30417
30567
 
30418
30568
  &[data-dark-background] {
30419
30569
  --x-color-contrasting: var(--navi-color-white);
@@ -30443,11 +30593,12 @@ installImportMetaCssBuild(import.meta);const css = /* css */`
30443
30593
 
30444
30594
  /* For ellipse + single char force the circle aspect as it's prettier */
30445
30595
  &[data-single-char] {
30596
+ display: inline-block;
30446
30597
  aspect-ratio: 1/1;
30447
- height: 1.5em;
30598
+ height: 1.6em;
30448
30599
  padding: 0;
30449
30600
  text-align: center;
30450
- line-height: 1.5em;
30601
+ line-height: 1.6em;
30451
30602
  }
30452
30603
  }
30453
30604
 
@@ -30467,19 +30618,19 @@ installImportMetaCssBuild(import.meta);const css = /* css */`
30467
30618
  border-radius: 50%;
30468
30619
 
30469
30620
  &[data-single-char] {
30470
- --x-radius: 1.5em;
30621
+ --x-radius: 1.6em;
30471
30622
  --x-number-font-size: unset;
30472
30623
  }
30473
30624
  &[data-two-chars] {
30474
- --x-radius: 1.8em;
30475
- --x-number-font-size: 0.9em;
30625
+ --x-radius: 2em;
30626
+ --x-number-font-size: unset;
30476
30627
  }
30477
30628
  &[data-three-chars] {
30478
30629
  --x-radius: 2.4em;
30479
30630
  --x-number-font-size: 0.8em;
30480
30631
  }
30481
30632
  &[data-four-chars] {
30482
- --x-radius: 2.6em;
30633
+ --x-radius: 2.4em;
30483
30634
  --x-number-font-size: 0.8em;
30484
30635
  }
30485
30636
 
@@ -30513,6 +30664,7 @@ const BadgeCount = ({
30513
30664
  // so that visually the interface do not suddently switch from circle to ellipse depending on the count
30514
30665
  circle,
30515
30666
  max = circle ? MAX_FOR_CIRCLE : Infinity,
30667
+ textSize,
30516
30668
  integer,
30517
30669
  lang,
30518
30670
  loading,
@@ -30538,25 +30690,37 @@ const BadgeCount = ({
30538
30690
  circle = false;
30539
30691
  }
30540
30692
  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]
30693
+ return jsx(SurroundingTextAligner, {
30694
+ align: "center",
30695
+ childRef: ref,
30696
+ size: props.size,
30697
+ textSize: textSize,
30698
+ children: jsxs(BadgeCountCircle, {
30699
+ ...props,
30700
+ loading: loading,
30701
+ ref: ref,
30702
+ hasOverflow: hasOverflow,
30703
+ charCount: charCount,
30704
+ children: [valueDisplayed, hasOverflow && maxElement]
30705
+ })
30548
30706
  });
30549
30707
  }
30550
30708
  const valueFormatted = typeof valueDisplayed === "number" ? formatNumber(valueDisplayed, {
30551
30709
  lang
30552
30710
  }) : valueDisplayed;
30553
- return jsxs(BadgeCountEllipse, {
30554
- ...props,
30555
- loading: loading,
30556
- ref: ref,
30557
- hasOverflow: hasOverflow,
30558
- charCount: charCount,
30559
- children: [valueFormatted, hasOverflow && maxElement]
30711
+ return jsx(SurroundingTextAligner, {
30712
+ align: "center",
30713
+ childRef: ref,
30714
+ size: props.size,
30715
+ textSize: textSize,
30716
+ children: jsxs(BadgeCountEllipse, {
30717
+ ...props,
30718
+ loading: loading,
30719
+ ref: ref,
30720
+ hasOverflow: hasOverflow,
30721
+ charCount: charCount,
30722
+ children: [valueFormatted, hasOverflow && maxElement]
30723
+ })
30560
30724
  });
30561
30725
  };
30562
30726
  const applyMaxToValue = (max, value) => {
@@ -30597,14 +30761,8 @@ const BadgeCountEllipse = ({
30597
30761
  spacing: "pre",
30598
30762
  children: loading ? jsx(Icon, {
30599
30763
  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
- })]
30764
+ }) : jsx(Fragment, {
30765
+ children: children
30608
30766
  })
30609
30767
  });
30610
30768
  };
@@ -30633,17 +30791,11 @@ const BadgeCountCircle = ({
30633
30791
  spacing: "pre",
30634
30792
  children: loading ? jsx(Icon, {
30635
30793
  children: jsx(LoadingDots, {})
30636
- }) : jsxs(Fragment, {
30637
- children: [jsx("span", {
30638
- style: "user-select: none",
30639
- children: "\u200B"
30640
- }), jsx("span", {
30794
+ }) : jsx(Fragment, {
30795
+ children: jsx("span", {
30641
30796
  className: "navi_badge_count_text",
30642
30797
  children: children
30643
- }), jsx("span", {
30644
- style: "user-select: none",
30645
- children: "\u200B"
30646
- })]
30798
+ })
30647
30799
  })
30648
30800
  });
30649
30801
  };