@jsenv/navi 0.21.7 → 0.22.0

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