@jsenv/navi 0.12.30 → 0.12.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import { installImportMetaCss } from "./jsenv_navi_side_effects.js";
2
- import { createIterableWeakSet, createPubSub, createValueEffect, createStyleController, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, activeElementSignal, canInterceptKeys, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, stringifyStyle, mergeOneStyle, mergeTwoStyles, normalizeStyles, resolveCSSSize, findBefore, findAfter, hasCSSSizeUnit, initFocusGroup, elementIsFocusable, pickLightOrDark, resolveColorLuminance, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
2
+ import { createIterableWeakSet, createPubSub, createValueEffect, createStyleController, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, activeElementSignal, canInterceptKeys, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, stringifyStyle, mergeOneStyle, mergeTwoStyles, normalizeStyles, resolveCSSSize, findBefore, findAfter, hasCSSSizeUnit, pickLightOrDark, resolveColorLuminance, initFocusGroup, elementIsFocusable, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
3
3
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
4
4
  import { effect, signal, computed, batch, useSignal } from "@preact/signals";
5
- import { useEffect, useRef, useCallback, useContext, useState, useLayoutEffect, useMemo, useErrorBoundary, useImperativeHandle, useId } from "preact/hooks";
5
+ import { useEffect, useRef, useCallback, useContext, useState, useLayoutEffect, useMemo, useImperativeHandle, useErrorBoundary, useId } from "preact/hooks";
6
6
  import { createContext, toChildArray, createRef, cloneElement } from "preact";
7
7
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
8
8
  import { createPortal, forwardRef } from "preact/compat";
@@ -7690,7 +7690,9 @@ const createRoute = (urlPatternInput) => {
7690
7690
  // always remove the wildcard part for URL building since it's optional
7691
7691
  if (relativeUrl.endsWith("/*?")) {
7692
7692
  // Always remove the optional wildcard part for URL building
7693
- relativeUrl = relativeUrl.replace(/\/\*\?$/, "");
7693
+ relativeUrl = relativeUrl.slice(0, -"/*?".length);
7694
+ } else if (relativeUrl.endsWith("{/}?*")) {
7695
+ relativeUrl = relativeUrl.slice(0, -"{/}?*".length);
7694
7696
  } else {
7695
7697
  // For required wildcards (/*) or other patterns, replace normally
7696
7698
  let wildcardIndex = 0;
@@ -13851,7 +13853,7 @@ const TextBasic = ({
13851
13853
  as: "span",
13852
13854
  baseClassName: "navi_text",
13853
13855
  ...rest,
13854
- children: rest.as === "pre" ? children : applySpacingOnTextChildren(children, spacing)
13856
+ children: rest.as === "pre" || rest.box || rest.column || rest.row ? children : applySpacingOnTextChildren(children, spacing)
13855
13857
  });
13856
13858
  };
13857
13859
 
@@ -15622,562 +15624,294 @@ const RouteLink = ({
15622
15624
  });
15623
15625
  };
15624
15626
 
15625
- installImportMetaCss(import.meta);import.meta.css = /* css */`
15626
- .action_error {
15627
- padding: 20px;
15628
- background: #fdd;
15629
- border: 1px solid red;
15630
- margin-top: 0;
15631
- margin-bottom: 20px;
15632
- }
15633
- `;
15634
- const renderIdleDefault = () => null;
15635
- const renderLoadingDefault = () => null;
15636
- const renderAbortedDefault = () => null;
15637
- const renderErrorDefault = error => {
15638
- let routeErrorText = error && error.message ? error.message : error;
15639
- return jsxs("p", {
15640
- className: "action_error",
15641
- children: ["An error occured: ", routeErrorText]
15642
- });
15643
- };
15644
- const renderCompletedDefault = () => null;
15645
- const ActionRenderer = ({
15646
- action,
15647
- children,
15648
- disabled
15649
- }) => {
15650
- const {
15651
- idle: renderIdle = renderIdleDefault,
15652
- loading: renderLoading = renderLoadingDefault,
15653
- aborted: renderAborted = renderAbortedDefault,
15654
- error: renderError = renderErrorDefault,
15655
- completed: renderCompleted,
15656
- always: renderAlways
15657
- } = typeof children === "function" ? {
15658
- completed: children
15659
- } : children || {};
15660
- if (disabled) {
15661
- return null;
15662
- }
15663
- if (action === undefined) {
15664
- throw new Error("ActionRenderer requires an action to render, but none was provided.");
15627
+ installImportMetaCss(import.meta);Object.assign(PSEUDO_CLASSES, {
15628
+ ":-navi-selected": {
15629
+ attribute: "data-selected"
15665
15630
  }
15666
- const {
15667
- idle,
15668
- loading,
15669
- aborted,
15670
- error,
15671
- data
15672
- } = useActionStatus(action);
15673
- const UIRenderedPromise = useUIRenderedPromise(action);
15674
- const [errorBoundary, resetErrorBoundary] = useErrorBoundary();
15675
-
15676
- // Mark this action as bound to UI components (has renderers)
15677
- // This tells the action system that errors should be caught and stored
15678
- // in the action's error state rather than bubbling up
15679
- useLayoutEffect(() => {
15680
- if (action) {
15681
- const {
15682
- ui
15683
- } = getActionPrivateProperties(action);
15684
- ui.hasRenderers = true;
15685
- }
15686
- }, [action]);
15687
- useLayoutEffect(() => {
15688
- resetErrorBoundary();
15689
- }, [action, loading, idle, resetErrorBoundary]);
15690
- useLayoutEffect(() => {
15691
- UIRenderedPromise.resolve();
15692
- return () => {
15693
- actionUIRenderedPromiseWeakMap.delete(action);
15694
- };
15695
- }, [action]);
15631
+ });
15632
+ import.meta.css = /* css */`
15633
+ @layer navi {
15634
+ .navi_tablist {
15635
+ --tablist-border-radius: 8px;
15636
+ --tablist-background: transparent;
15637
+ --tab-border-radius: calc(var(--tablist-border-radius) - 2px);
15696
15638
 
15697
- // If renderAlways is provided, it wins and handles all rendering
15698
- if (renderAlways) {
15699
- return renderAlways({
15700
- idle,
15701
- loading,
15702
- aborted,
15703
- error,
15704
- data
15705
- });
15706
- }
15707
- if (idle) {
15708
- return renderIdle(action);
15709
- }
15710
- if (errorBoundary) {
15711
- return renderError(errorBoundary, "ui_error", action);
15712
- }
15713
- if (error) {
15714
- return renderError(error, "action_error", action);
15715
- }
15716
- if (aborted) {
15717
- return renderAborted(action);
15718
- }
15719
- let renderCompletedSafe;
15720
- if (renderCompleted) {
15721
- renderCompletedSafe = renderCompleted;
15722
- } else {
15723
- const {
15724
- ui
15725
- } = getActionPrivateProperties(action);
15726
- if (ui.renderCompleted) {
15727
- renderCompletedSafe = ui.renderCompleted;
15728
- } else {
15729
- renderCompletedSafe = renderCompletedDefault;
15639
+ --tab-background: transparent;
15640
+ --tab-background-hover: #dae0e7;
15641
+ --tab-background-selected: transparent;
15642
+ --tab-color: inherit;
15643
+ --tab-color-hover: #010409;
15644
+ --tab-color-selected: inherit;
15645
+ --tab-marker-height: 2px;
15646
+ --tab-marker-color: rgb(205, 52, 37);
15730
15647
  }
15731
15648
  }
15732
- if (loading) {
15733
- if (action.canDisplayOldData && data !== undefined) {
15734
- return renderCompletedSafe(data, action);
15735
- }
15736
- return renderLoading(action);
15649
+
15650
+ .navi_tablist {
15651
+ display: flex;
15652
+ line-height: 2em;
15653
+ background: var(--tablist-background);
15654
+ border-radius: var(--tablist-border-radius);
15655
+ overflow-x: auto;
15656
+ overflow-y: hidden;
15737
15657
  }
15738
- return renderCompletedSafe(data, action);
15739
- };
15740
- const defaultPromise = Promise.resolve();
15741
- defaultPromise.resolve = () => {};
15742
- const actionUIRenderedPromiseWeakMap = new WeakMap();
15743
- const useUIRenderedPromise = action => {
15744
- if (!action) {
15745
- return defaultPromise;
15658
+ .navi_tablist > ul {
15659
+ display: flex;
15660
+ width: 100%;
15661
+ margin: 0;
15662
+ padding: 0;
15663
+ align-items: center;
15664
+ gap: 0.5rem;
15665
+ list-style: none;
15746
15666
  }
15747
- const actionUIRenderedPromise = actionUIRenderedPromiseWeakMap.get(action);
15748
- if (actionUIRenderedPromise) {
15749
- return actionUIRenderedPromise;
15667
+ .navi_tablist > ul > li {
15668
+ position: relative;
15669
+ display: inline-flex;
15750
15670
  }
15751
- let resolve;
15752
- const promise = new Promise(res => {
15753
- resolve = res;
15754
- });
15755
- promise.resolve = resolve;
15756
- actionUIRenderedPromiseWeakMap.set(action, promise);
15757
- return promise;
15758
- };
15759
15671
 
15760
- const useFocusGroup = (
15761
- elementRef,
15762
- { enabled = true, direction, skipTab, loop, name } = {},
15763
- ) => {
15764
- useLayoutEffect(() => {
15765
- if (!enabled) {
15766
- return null;
15767
- }
15768
- const focusGroup = initFocusGroup(elementRef.current, {
15769
- direction,
15770
- skipTab,
15771
- loop,
15772
- name,
15773
- });
15774
- return focusGroup.cleanup;
15775
- }, [direction, skipTab, loop, name]);
15776
- };
15672
+ .navi_tab {
15673
+ --x-tab-background: var(--tab-background);
15674
+ --x-tab-color: var(--tab-color);
15777
15675
 
15778
- installImportMetaCss(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
15779
- const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z";
15780
- import.meta.css = /* css */`
15781
- .summary_marker {
15782
- width: 1em;
15783
- height: 1em;
15784
- line-height: 1em;
15785
- }
15786
- .summary_marker_svg .arrow {
15787
- animation-duration: 0.3s;
15788
- animation-fill-mode: forwards;
15789
- animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
15790
- }
15791
- .summary_marker_svg .arrow[data-animation-target="down"] {
15792
- animation-name: morph-to-down;
15793
- }
15794
- @keyframes morph-to-down {
15795
- from {
15796
- d: path("${rightArrowPath}");
15676
+ display: flex;
15677
+ flex-direction: column;
15678
+ white-space: nowrap;
15679
+ border-radius: var(--tab-border-radius);
15680
+
15681
+ .navi_tab_content {
15682
+ display: flex;
15683
+ padding: 0 0.5rem;
15684
+ color: var(--x-tab-color);
15685
+ background: var(--x-tab-background);
15686
+ border-radius: inherit;
15687
+ transition: background 0.12s ease-out;
15688
+ }
15689
+ /* Hidden bold clone to reserve space for bold width without affecting height */
15690
+ .navi_tab_content_bold_clone {
15691
+ display: block; /* in-flow so it contributes to width */
15692
+ height: 0; /* zero height so it doesn't change layout height */
15693
+ font-weight: 600; /* force bold to compute max width */
15694
+ visibility: hidden; /* not visible */
15695
+ pointer-events: none; /* inert */
15696
+ overflow: hidden; /* avoid any accidental height */
15697
+ }
15698
+ .navi_tab_selected_marker {
15699
+ z-index: 1;
15700
+ display: flex;
15701
+ width: 100%;
15702
+ height: var(--tab-marker-height);
15703
+ margin-top: 5px;
15704
+ background: transparent;
15705
+ border-radius: 0.1px;
15797
15706
  }
15798
- to {
15799
- d: path("${downArrowPath}");
15707
+
15708
+ /* Interactive */
15709
+ &[data-interactive] {
15710
+ cursor: pointer;
15800
15711
  }
15801
- }
15802
- .summary_marker_svg .arrow[data-animation-target="right"] {
15803
- animation-name: morph-to-right;
15804
- }
15805
- @keyframes morph-to-right {
15806
- from {
15807
- d: path("${downArrowPath}");
15712
+ /* Hover */
15713
+ &:hover {
15714
+ --x-tab-background: var(--tab-background-hover);
15715
+ --x-tab-color: var(--tab-color-hover);
15808
15716
  }
15809
- to {
15810
- d: path("${rightArrowPath}");
15717
+ /* Selected */
15718
+ &[data-selected] {
15719
+ --x-tab-background: var(--tab-background-selected);
15720
+ --x-tab-color: var(--tab-color-selected);
15721
+
15722
+ .navi_tab_content {
15723
+ font-weight: 600;
15724
+ }
15725
+ .navi_tab_selected_marker {
15726
+ background: var(--tab-marker-color);
15727
+ }
15811
15728
  }
15812
15729
  }
15813
15730
 
15814
- .summary_marker_svg .foreground_circle {
15815
- stroke-dasharray: 503 1507; /* ~25% of circle perimeter */
15816
- stroke-dashoffset: 0;
15817
- animation: progress-around-circle 1.5s linear infinite;
15818
- }
15819
- @keyframes progress-around-circle {
15820
- 0% {
15821
- stroke-dashoffset: 0;
15822
- }
15823
- 100% {
15824
- stroke-dashoffset: -2010;
15731
+ .navi_tablist[data-expand] {
15732
+ .navi_tab {
15733
+ flex: 1;
15734
+ align-items: center;
15825
15735
  }
15826
- }
15827
15736
 
15828
- /* fading and scaling */
15829
- .summary_marker_svg .arrow {
15830
- transition: opacity 0.3s ease-in-out;
15831
- opacity: 1;
15832
- }
15833
- .summary_marker_svg .loading_container {
15834
- transition: transform 0.3s linear;
15835
- transform: scale(0.3);
15836
- }
15837
- .summary_marker_svg .background_circle,
15838
- .summary_marker_svg .foreground_circle {
15839
- transition: opacity 0.3s ease-in-out;
15840
- opacity: 0;
15841
- }
15842
- .summary_marker_svg[data-loading] .arrow {
15843
- opacity: 0;
15844
- }
15845
- .summary_marker_svg[data-loading] .loading_container {
15846
- transform: scale(1);
15847
- }
15848
- .summary_marker_svg[data-loading] .background_circle {
15849
- opacity: 0.2;
15850
- }
15851
- .summary_marker_svg[data-loading] .foreground_circle {
15852
- opacity: 1;
15737
+ .navi_tab_content {
15738
+ width: 100%;
15739
+ justify-content: center;
15740
+ }
15853
15741
  }
15854
15742
  `;
15855
- const SummaryMarker = ({
15856
- open,
15857
- loading
15743
+ const TabListUnderlinerContext = createContext();
15744
+ const TabListStyleCSSVars = {
15745
+ borderRadius: "--tablist-border-radius",
15746
+ background: "--tablist-background"
15747
+ };
15748
+ const TabList = ({
15749
+ children,
15750
+ spacing,
15751
+ underline,
15752
+ expand,
15753
+ expandX,
15754
+ ...props
15858
15755
  }) => {
15859
- const showLoading = useDebounceTrue(loading, 300);
15860
- const mountedRef = useRef(false);
15861
- const prevOpenRef = useRef(open);
15862
- useLayoutEffect(() => {
15863
- mountedRef.current = true;
15864
- return () => {
15865
- mountedRef.current = false;
15866
- };
15867
- }, []);
15868
- const shouldAnimate = mountedRef.current && prevOpenRef.current !== open;
15869
- prevOpenRef.current = open;
15870
- return jsx("span", {
15871
- className: "summary_marker",
15872
- children: jsxs("svg", {
15873
- className: "summary_marker_svg",
15874
- viewBox: "0 -960 960 960",
15875
- xmlns: "http://www.w3.org/2000/svg",
15876
- "data-loading": open ? showLoading || undefined : undefined,
15877
- children: [jsxs("g", {
15878
- className: "loading_container",
15879
- "transform-origin": "480px -480px",
15880
- children: [jsx("circle", {
15881
- className: "background_circle",
15882
- cx: "480",
15883
- cy: "-480",
15884
- r: "320",
15885
- stroke: "currentColor",
15886
- fill: "none",
15887
- strokeWidth: "60",
15888
- opacity: "0.2"
15889
- }), jsx("circle", {
15890
- className: "foreground_circle",
15891
- cx: "480",
15892
- cy: "-480",
15893
- r: "320",
15894
- stroke: "currentColor",
15895
- fill: "none",
15896
- strokeWidth: "60",
15897
- strokeLinecap: "round",
15898
- strokeDasharray: "503 1507"
15899
- })]
15900
- }), jsx("g", {
15901
- className: "arrow_container",
15902
- "transform-origin": "480px -480px",
15903
- children: jsx("path", {
15904
- className: "arrow",
15905
- fill: "currentColor",
15906
- "data-animation-target": shouldAnimate ? open ? "down" : "right" : undefined,
15907
- d: open ? downArrowPath : rightArrowPath
15756
+ return jsx(Box, {
15757
+ as: "nav",
15758
+ baseClassName: "navi_tablist",
15759
+ role: "tablist",
15760
+ "data-expand": expand || expandX ? "" : undefined,
15761
+ expand: expand,
15762
+ expandX: expandX,
15763
+ ...props,
15764
+ styleCSSVars: TabListStyleCSSVars,
15765
+ children: jsx(Box, {
15766
+ as: "ul",
15767
+ column: true,
15768
+ role: "list",
15769
+ spacing: spacing,
15770
+ children: jsx(TabListUnderlinerContext.Provider, {
15771
+ value: underline,
15772
+ children: children.map(child => {
15773
+ return jsx(Box, {
15774
+ as: "li",
15775
+ column: true,
15776
+ expandX: expandX,
15777
+ expand: expand,
15778
+ children: child
15779
+ }, child.props.key);
15908
15780
  })
15909
- })]
15781
+ })
15910
15782
  })
15911
15783
  });
15912
15784
  };
15913
-
15914
- installImportMetaCss(import.meta);import.meta.css = /* css */`
15915
- .navi_details {
15916
- position: relative;
15917
- z-index: 1;
15918
- display: flex;
15919
- flex-shrink: 0;
15920
- flex-direction: column;
15921
- }
15922
-
15923
- .navi_details > summary {
15924
- display: flex;
15925
- flex-shrink: 0;
15926
- flex-direction: column;
15927
- cursor: pointer;
15928
- user-select: none;
15929
- }
15930
- .summary_body {
15931
- display: flex;
15932
- width: 100%;
15933
- flex-direction: row;
15934
- align-items: center;
15935
- gap: 0.2em;
15785
+ const TAB_STYLE_CSS_VARS = {
15786
+ "background": "--tab-background",
15787
+ "color": "--tab-color",
15788
+ ":hover": {
15789
+ background: "--tab-background-hover",
15790
+ color: "--tab-color-hover"
15791
+ },
15792
+ ":-navi-selected": {
15793
+ background: "--tab-color-selected",
15794
+ color: "--tab-color-selected"
15936
15795
  }
15937
- .summary_label {
15938
- display: flex;
15939
- padding-right: 10px;
15940
- flex: 1;
15941
- align-items: center;
15942
- gap: 0.2em;
15796
+ };
15797
+ const TAB_PSEUDO_CLASSES = [":hover", ":-navi-selected"];
15798
+ const TAB_PSEUDO_ELEMENTS = ["::-navi-marker"];
15799
+ const Tab = props => {
15800
+ if (props.route) {
15801
+ return jsx(TabRoute, {
15802
+ ...props
15803
+ });
15943
15804
  }
15805
+ return jsx(TabBasic, {
15806
+ ...props
15807
+ });
15808
+ };
15809
+ const TabRoute = ({
15810
+ route,
15811
+ children,
15812
+ ...props
15813
+ }) => {
15814
+ const {
15815
+ active
15816
+ } = useRouteStatus(route);
15817
+ return jsx(TabBasic, {
15818
+ selected: active,
15819
+ ...props,
15820
+ children: jsx(RouteLink, {
15821
+ route: route,
15822
+ underline: false,
15823
+ color: "inherit",
15824
+ children: children
15825
+ })
15826
+ });
15827
+ };
15828
+ const TabBasic = ({
15829
+ children,
15830
+ selected,
15831
+ onClick,
15832
+ ...props
15833
+ }) => {
15834
+ const tabListUnderline = useContext(TabListUnderlinerContext);
15835
+ return jsxs(Box, {
15836
+ role: "tab",
15837
+ "aria-selected": selected ? "true" : "false",
15838
+ "data-interactive": onClick ? "" : undefined,
15839
+ onClick: onClick
15840
+ // Style system
15841
+ ,
15842
+ baseClassName: "navi_tab",
15843
+ styleCSSVars: TAB_STYLE_CSS_VARS,
15844
+ pseudoClasses: TAB_PSEUDO_CLASSES,
15845
+ pseudoElements: TAB_PSEUDO_ELEMENTS,
15846
+ basePseudoState: {
15847
+ ":-navi-selected": selected
15848
+ },
15849
+ ...props,
15850
+ children: [jsx("div", {
15851
+ className: "navi_tab_content",
15852
+ children: children
15853
+ }), jsx("div", {
15854
+ className: "navi_tab_content_bold_clone",
15855
+ "aria-hidden": "true",
15856
+ children: children
15857
+ }), tabListUnderline && jsx("span", {
15858
+ className: "navi_tab_selected_marker"
15859
+ })]
15860
+ });
15861
+ };
15944
15862
 
15945
- .navi_details > summary:focus {
15946
- z-index: 1;
15863
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
15864
+ @layer navi {
15865
+ label {
15866
+ cursor: pointer;
15867
+ }
15868
+
15869
+ label[data-readonly],
15870
+ label[data-disabled] {
15871
+ color: rgba(0, 0, 0, 0.5);
15872
+ cursor: default;
15873
+ }
15947
15874
  }
15948
15875
  `;
15949
- const Details = forwardRef((props, ref) => {
15950
- return renderActionableComponent(props, ref);
15951
- });
15952
- const DetailsBasic = forwardRef((props, ref) => {
15876
+ const ReportReadOnlyOnLabelContext = createContext();
15877
+ const ReportDisabledOnLabelContext = createContext();
15878
+ const Label = props => {
15953
15879
  const {
15954
- id,
15955
- label = "Summary",
15956
- open,
15957
- loading,
15958
- className,
15959
- focusGroup,
15960
- focusGroupDirection,
15961
- arrowKeyShortcuts = true,
15962
- openKeyShortcut = "ArrowRight",
15963
- closeKeyShortcut = "ArrowLeft",
15964
- onToggle,
15880
+ readOnly,
15881
+ disabled,
15965
15882
  children,
15966
15883
  ...rest
15967
15884
  } = props;
15968
- const innerRef = useRef();
15969
- useImperativeHandle(ref, () => innerRef.current);
15970
- const [navState, setNavState] = useNavState(id);
15971
- const [innerOpen, innerOpenSetter] = useState(open || navState);
15972
- useFocusGroup(innerRef, {
15973
- enabled: focusGroup,
15974
- name: typeof focusGroup === "string" ? focusGroup : undefined,
15975
- direction: focusGroupDirection
15885
+ const [inputReadOnly, setInputReadOnly] = useState(false);
15886
+ const innerReadOnly = readOnly || inputReadOnly;
15887
+ const [inputDisabled, setInputDisabled] = useState(false);
15888
+ const innerDisabled = disabled || inputDisabled;
15889
+ return jsx(Box, {
15890
+ ...rest,
15891
+ as: "label",
15892
+ basePseudoState: {
15893
+ readOnly: innerReadOnly,
15894
+ disabled: innerDisabled
15895
+ },
15896
+ children: jsx(ReportReadOnlyOnLabelContext.Provider, {
15897
+ value: setInputReadOnly,
15898
+ children: jsx(ReportDisabledOnLabelContext.Provider, {
15899
+ value: setInputDisabled,
15900
+ children: children
15901
+ })
15902
+ })
15976
15903
  });
15904
+ };
15977
15905
 
15978
- /**
15979
- * Browser will dispatch "toggle" event even if we set open={true}
15980
- * When rendering the component for the first time
15981
- * We have to ensure the initial "toggle" event is ignored.
15982
- *
15983
- * If we don't do that code will think the details has changed and run logic accordingly
15984
- * For example it will try to navigate to the current url while we are already there
15985
- *
15986
- * See:
15987
- * - https://techblog.thescore.com/2024/10/08/why-we-decided-to-change-how-the-details-element-works/
15988
- * - https://github.com/whatwg/html/issues/4500
15989
- * - https://stackoverflow.com/questions/58942600/react-html-details-toggles-uncontrollably-when-starts-open
15990
- *
15991
- */
15992
-
15993
- const summaryRef = useRef(null);
15994
- useKeyboardShortcuts(innerRef, [{
15995
- key: openKeyShortcut,
15996
- enabled: arrowKeyShortcuts,
15997
- when: e => document.activeElement === summaryRef.current &&
15998
- // avoid handling openKeyShortcut twice when keydown occurs inside nested details
15999
- !e.defaultPrevented,
16000
- action: e => {
16001
- const details = innerRef.current;
16002
- if (!details.open) {
16003
- e.preventDefault();
16004
- details.open = true;
16005
- return;
16006
- }
16007
- const summary = summaryRef.current;
16008
- const firstFocusableElementInDetails = findAfter(summary, elementIsFocusable, {
16009
- root: details
16010
- });
16011
- if (!firstFocusableElementInDetails) {
16012
- return;
16013
- }
16014
- e.preventDefault();
16015
- firstFocusableElementInDetails.focus();
16016
- }
16017
- }, {
16018
- key: closeKeyShortcut,
16019
- enabled: arrowKeyShortcuts,
16020
- when: () => {
16021
- const details = innerRef.current;
16022
- return details.open;
16023
- },
16024
- action: e => {
16025
- const details = innerRef.current;
16026
- const summary = summaryRef.current;
16027
- if (document.activeElement === summary) {
16028
- e.preventDefault();
16029
- summary.focus();
16030
- details.open = false;
16031
- } else {
16032
- e.preventDefault();
16033
- summary.focus();
16034
- }
16035
- }
16036
- }]);
16037
- const mountedRef = useRef(false);
16038
- useEffect(() => {
16039
- mountedRef.current = true;
16040
- }, []);
16041
- return jsxs("details", {
16042
- ...rest,
16043
- ref: innerRef,
16044
- id: id,
16045
- className: ["navi_details", ...(className ? className.split(" ") : [])].join(" "),
16046
- onToggle: e => {
16047
- const isOpen = e.newState === "open";
16048
- if (mountedRef.current) {
16049
- if (isOpen) {
16050
- innerOpenSetter(true);
16051
- setNavState(true);
16052
- } else {
16053
- innerOpenSetter(false);
16054
- setNavState(undefined);
16055
- }
16056
- }
16057
- onToggle?.(e);
16058
- },
16059
- open: innerOpen,
16060
- children: [jsx("summary", {
16061
- ref: summaryRef,
16062
- children: jsxs("div", {
16063
- className: "summary_body",
16064
- children: [jsx(SummaryMarker, {
16065
- open: innerOpen,
16066
- loading: loading
16067
- }), jsx("div", {
16068
- className: "summary_label",
16069
- children: label
16070
- })]
16071
- })
16072
- }), children]
16073
- });
16074
- });
16075
- forwardRef((props, ref) => {
16076
- const {
16077
- action,
16078
- loading,
16079
- onToggle,
16080
- onActionPrevented,
16081
- onActionStart,
16082
- onActionError,
16083
- onActionEnd,
16084
- children,
16085
- ...rest
16086
- } = props;
16087
- const innerRef = useRef();
16088
- useImperativeHandle(ref, () => innerRef.current);
16089
- const effectiveAction = useAction(action);
16090
- const {
16091
- loading: actionLoading
16092
- } = useActionStatus(effectiveAction);
16093
- const executeAction = useExecuteAction(innerRef, {
16094
- // the error will be displayed by actionRenderer inside <details>
16095
- errorEffect: "none"
16096
- });
16097
- useActionEvents(innerRef, {
16098
- onPrevented: onActionPrevented,
16099
- onAction: e => {
16100
- executeAction(e);
16101
- },
16102
- onStart: onActionStart,
16103
- onError: onActionError,
16104
- onEnd: onActionEnd
16105
- });
16106
- return jsx(DetailsBasic, {
16107
- ...rest,
16108
- ref: innerRef,
16109
- loading: loading || actionLoading,
16110
- onToggle: toggleEvent => {
16111
- const isOpen = toggleEvent.newState === "open";
16112
- if (isOpen) {
16113
- requestAction(toggleEvent.target, effectiveAction, {
16114
- event: toggleEvent,
16115
- method: "run"
16116
- });
16117
- } else {
16118
- effectiveAction.abort();
16119
- }
16120
- onToggle?.(toggleEvent);
16121
- },
16122
- children: jsx(ActionRenderer, {
16123
- action: effectiveAction,
16124
- children: children
16125
- })
16126
- });
16127
- });
16128
-
16129
- installImportMetaCss(import.meta);import.meta.css = /* css */`
16130
- @layer navi {
16131
- label {
16132
- cursor: pointer;
16133
- }
16134
-
16135
- label[data-readonly],
16136
- label[data-disabled] {
16137
- color: rgba(0, 0, 0, 0.5);
16138
- cursor: default;
16139
- }
16140
- }
16141
- `;
16142
- const ReportReadOnlyOnLabelContext = createContext();
16143
- const ReportDisabledOnLabelContext = createContext();
16144
- const Label = props => {
16145
- const {
16146
- readOnly,
16147
- disabled,
16148
- children,
16149
- ...rest
16150
- } = props;
16151
- const [inputReadOnly, setInputReadOnly] = useState(false);
16152
- const innerReadOnly = readOnly || inputReadOnly;
16153
- const [inputDisabled, setInputDisabled] = useState(false);
16154
- const innerDisabled = disabled || inputDisabled;
16155
- return jsx(Box, {
16156
- ...rest,
16157
- as: "label",
16158
- basePseudoState: {
16159
- readOnly: innerReadOnly,
16160
- disabled: innerDisabled
16161
- },
16162
- children: jsx(ReportReadOnlyOnLabelContext.Provider, {
16163
- value: setInputReadOnly,
16164
- children: jsx(ReportDisabledOnLabelContext.Provider, {
16165
- value: setInputDisabled,
16166
- children: children
16167
- })
16168
- })
16169
- });
16170
- };
16171
-
16172
- installImportMetaCss(import.meta);import.meta.css = /* css */`
16173
- @layer navi {
16174
- .navi_checkbox {
16175
- --outline-offset: 1px;
16176
- --outline-width: 2px;
16177
- --border-width: 1px;
16178
- --border-radius: 2px;
16179
- --width: 13px;
16180
- --height: 13px;
15906
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
15907
+ @layer navi {
15908
+ .navi_checkbox {
15909
+ --outline-offset: 1px;
15910
+ --outline-width: 2px;
15911
+ --border-width: 1px;
15912
+ --border-radius: 2px;
15913
+ --width: 13px;
15914
+ --height: 13px;
16181
15915
 
16182
15916
  --outline-color: var(--navi-focus-outline-color);
16183
15917
  --loader-color: var(--navi-loader-color);
@@ -18065,252 +17799,798 @@ forwardRef((props, ref) => {
18065
17799
  children: children
18066
17800
  })
18067
17801
  });
18068
- });
18069
-
18070
- const useRefArray = (items, keyFromItem) => {
18071
- const refMapRef = useRef(new Map());
18072
- const previousKeySetRef = useRef(new Set());
18073
-
18074
- return useMemo(() => {
18075
- const refMap = refMapRef.current;
18076
- const previousKeySet = previousKeySetRef.current;
18077
- const currentKeySet = new Set();
18078
- const refArray = [];
18079
-
18080
- for (let i = 0; i < items.length; i++) {
18081
- const item = items[i];
18082
- const key = keyFromItem(item);
18083
- currentKeySet.add(key);
18084
-
18085
- const refForKey = refMap.get(key);
18086
- if (refForKey) {
18087
- refArray[i] = refForKey;
18088
- } else {
18089
- const newRef = createRef();
18090
- refMap.set(key, newRef);
18091
- refArray[i] = newRef;
18092
- }
18093
- }
18094
-
18095
- for (const key of previousKeySet) {
18096
- if (!currentKeySet.has(key)) {
18097
- refMap.delete(key);
18098
- }
18099
- }
18100
- previousKeySetRef.current = currentKeySet;
18101
-
18102
- return refArray;
18103
- }, [items]);
17802
+ });
17803
+
17804
+ const useRefArray = (items, keyFromItem) => {
17805
+ const refMapRef = useRef(new Map());
17806
+ const previousKeySetRef = useRef(new Set());
17807
+
17808
+ return useMemo(() => {
17809
+ const refMap = refMapRef.current;
17810
+ const previousKeySet = previousKeySetRef.current;
17811
+ const currentKeySet = new Set();
17812
+ const refArray = [];
17813
+
17814
+ for (let i = 0; i < items.length; i++) {
17815
+ const item = items[i];
17816
+ const key = keyFromItem(item);
17817
+ currentKeySet.add(key);
17818
+
17819
+ const refForKey = refMap.get(key);
17820
+ if (refForKey) {
17821
+ refArray[i] = refForKey;
17822
+ } else {
17823
+ const newRef = createRef();
17824
+ refMap.set(key, newRef);
17825
+ refArray[i] = newRef;
17826
+ }
17827
+ }
17828
+
17829
+ for (const key of previousKeySet) {
17830
+ if (!currentKeySet.has(key)) {
17831
+ refMap.delete(key);
17832
+ }
17833
+ }
17834
+ previousKeySetRef.current = currentKeySet;
17835
+
17836
+ return refArray;
17837
+ }, [items]);
17838
+ };
17839
+
17840
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
17841
+ .navi_select[data-readonly] {
17842
+ pointer-events: none;
17843
+ }
17844
+ `;
17845
+ const Select = forwardRef((props, ref) => {
17846
+ const select = renderActionableComponent(props, ref);
17847
+ return select;
17848
+ });
17849
+ const SelectControlled = forwardRef((props, ref) => {
17850
+ const {
17851
+ name,
17852
+ value,
17853
+ loading,
17854
+ disabled,
17855
+ readOnly,
17856
+ children,
17857
+ ...rest
17858
+ } = props;
17859
+ const innerRef = useRef();
17860
+ useImperativeHandle(ref, () => innerRef.current);
17861
+ const selectElement = jsx("select", {
17862
+ className: "navi_select",
17863
+ ref: innerRef,
17864
+ "data-readonly": readOnly && !disabled ? "" : undefined,
17865
+ onKeyDown: e => {
17866
+ if (readOnly) {
17867
+ e.preventDefault();
17868
+ }
17869
+ },
17870
+ ...rest,
17871
+ children: children.map(child => {
17872
+ const {
17873
+ label,
17874
+ readOnly: childReadOnly,
17875
+ disabled: childDisabled,
17876
+ loading: childLoading,
17877
+ value: childValue,
17878
+ ...childRest
17879
+ } = child;
17880
+ return jsx("option", {
17881
+ name: name,
17882
+ value: childValue,
17883
+ selected: childValue === value,
17884
+ readOnly: readOnly || childReadOnly,
17885
+ disabled: disabled || childDisabled,
17886
+ loading: loading || childLoading,
17887
+ ...childRest,
17888
+ children: label
17889
+ }, childValue);
17890
+ })
17891
+ });
17892
+ return jsx(LoaderBackground, {
17893
+ loading: loading,
17894
+ color: "light-dark(#355fcc, #3b82f6)",
17895
+ inset: -1,
17896
+ children: selectElement
17897
+ });
17898
+ });
17899
+ forwardRef((props, ref) => {
17900
+ const {
17901
+ value: initialValue,
17902
+ id,
17903
+ children,
17904
+ ...rest
17905
+ } = props;
17906
+ const innerRef = useRef();
17907
+ useImperativeHandle(ref, () => innerRef.current);
17908
+ const [navState, setNavState] = useNavState(id);
17909
+ const valueAtStart = navState === undefined ? initialValue : navState;
17910
+ const [value, setValue] = useState(valueAtStart);
17911
+ useEffect(() => {
17912
+ setNavState(value);
17913
+ }, [value]);
17914
+ return jsx(SelectControlled, {
17915
+ ref: innerRef,
17916
+ value: value,
17917
+ onChange: event => {
17918
+ const select = event.target;
17919
+ const selectedValue = select.value;
17920
+ setValue(selectedValue);
17921
+ },
17922
+ ...rest,
17923
+ children: children
17924
+ });
17925
+ });
17926
+ forwardRef((props, ref) => {
17927
+ const {
17928
+ id,
17929
+ name,
17930
+ value: externalValue,
17931
+ valueSignal,
17932
+ action,
17933
+ children,
17934
+ onCancel,
17935
+ onActionPrevented,
17936
+ onActionStart,
17937
+ onActionAbort,
17938
+ onActionError,
17939
+ onActionEnd,
17940
+ actionErrorEffect,
17941
+ ...rest
17942
+ } = props;
17943
+ const innerRef = useRef();
17944
+ useImperativeHandle(ref, () => innerRef.current);
17945
+ const [navState, setNavState, resetNavState] = useNavState(id);
17946
+ const [boundAction, value, setValue, initialValue] = useActionBoundToOneParam(action, name);
17947
+ const {
17948
+ loading: actionLoading
17949
+ } = useActionStatus(boundAction);
17950
+ const executeAction = useExecuteAction(innerRef, {
17951
+ errorEffect: actionErrorEffect
17952
+ });
17953
+ useEffect(() => {
17954
+ setNavState(value);
17955
+ }, [value]);
17956
+ const actionRequesterRef = useRef(null);
17957
+ useActionEvents(innerRef, {
17958
+ onCancel: (e, reason) => {
17959
+ resetNavState();
17960
+ setValue(initialValue);
17961
+ onCancel?.(e, reason);
17962
+ },
17963
+ onPrevented: onActionPrevented,
17964
+ onAction: actionEvent => {
17965
+ actionRequesterRef.current = actionEvent.detail.requester;
17966
+ executeAction(actionEvent);
17967
+ },
17968
+ onStart: onActionStart,
17969
+ onAbort: e => {
17970
+ setValue(initialValue);
17971
+ onActionAbort?.(e);
17972
+ },
17973
+ onError: error => {
17974
+ setValue(initialValue);
17975
+ onActionError?.(error);
17976
+ },
17977
+ onEnd: () => {
17978
+ resetNavState();
17979
+ onActionEnd?.();
17980
+ }
17981
+ });
17982
+ const childRefArray = useRefArray(children, child => child.value);
17983
+ return jsx(SelectControlled, {
17984
+ ref: innerRef,
17985
+ name: name,
17986
+ value: value,
17987
+ "data-action": boundAction,
17988
+ onChange: event => {
17989
+ const select = event.target;
17990
+ const selectedValue = select.value;
17991
+ setValue(selectedValue);
17992
+ const radioListContainer = innerRef.current;
17993
+ const optionSelected = select.querySelector(`option[value="${selectedValue}"]`);
17994
+ requestAction(radioListContainer, boundAction, {
17995
+ event,
17996
+ requester: optionSelected
17997
+ });
17998
+ },
17999
+ ...rest,
18000
+ children: children.map((child, i) => {
18001
+ const childRef = childRefArray[i];
18002
+ return {
18003
+ ...child,
18004
+ ref: childRef,
18005
+ loading: child.loading || actionLoading && actionRequesterRef.current === childRef.current,
18006
+ readOnly: child.readOnly || actionLoading
18007
+ };
18008
+ })
18009
+ });
18010
+ });
18011
+ forwardRef((props, ref) => {
18012
+ const {
18013
+ id,
18014
+ name,
18015
+ value: externalValue,
18016
+ children,
18017
+ ...rest
18018
+ } = props;
18019
+ const innerRef = useRef();
18020
+ useImperativeHandle(ref, () => innerRef.current);
18021
+ const [navState, setNavState] = useNavState(id);
18022
+ const [value, setValue, initialValue] = [name, externalValue, navState];
18023
+ useEffect(() => {
18024
+ setNavState(value);
18025
+ }, [value]);
18026
+ useFormEvents(innerRef, {
18027
+ onFormReset: () => {
18028
+ setValue(undefined);
18029
+ },
18030
+ onFormActionAbort: () => {
18031
+ setValue(initialValue);
18032
+ },
18033
+ onFormActionError: () => {
18034
+ setValue(initialValue);
18035
+ }
18036
+ });
18037
+ return jsx(SelectControlled, {
18038
+ ref: innerRef,
18039
+ name: name,
18040
+ value: value,
18041
+ onChange: event => {
18042
+ const select = event.target;
18043
+ const selectedValue = select.checked;
18044
+ setValue(selectedValue);
18045
+ },
18046
+ ...rest,
18047
+ children: children
18048
+ });
18049
+ });
18050
+
18051
+ const createUniqueValueConstraint = (
18052
+ // the set might be incomplete (the front usually don't have the full copy of all the items from the backend)
18053
+ // but this is already nice to help user with what we know
18054
+ // it's also possible that front is unsync with backend, preventing user to choose a value
18055
+ // that is actually free.
18056
+ // But this is unlikely to happen and user could reload the page to be able to choose that name
18057
+ // that suddenly became available
18058
+ existingValueSet,
18059
+ message = `"{value}" already exists. Please choose another value.`,
18060
+ ) => {
18061
+ return {
18062
+ name: "unique_value",
18063
+ check: (input) => {
18064
+ const inputValue = input.value;
18065
+ const hasConflict = existingValueSet.has(inputValue);
18066
+ // console.log({
18067
+ // inputValue,
18068
+ // names: Array.from(otherNameSet.values()),
18069
+ // hasConflict,
18070
+ // });
18071
+ if (hasConflict) {
18072
+ return message.replace("{value}", inputValue);
18073
+ }
18074
+ return "";
18075
+ },
18076
+ };
18077
+ };
18078
+
18079
+ const SINGLE_SPACE_CONSTRAINT = {
18080
+ name: "single_space",
18081
+ check: (input) => {
18082
+ const inputValue = input.value;
18083
+ const hasLeadingSpace = inputValue.startsWith(" ");
18084
+ const hasTrailingSpace = inputValue.endsWith(" ");
18085
+ const hasDoubleSpace = inputValue.includes(" ");
18086
+ if (hasLeadingSpace || hasDoubleSpace || hasTrailingSpace) {
18087
+ return "Spaces at the beginning, end, or consecutive spaces are not allowed";
18088
+ }
18089
+ return "";
18090
+ },
18091
+ };
18092
+
18093
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
18094
+ .action_error {
18095
+ padding: 20px;
18096
+ background: #fdd;
18097
+ border: 1px solid red;
18098
+ margin-top: 0;
18099
+ margin-bottom: 20px;
18100
+ }
18101
+ `;
18102
+ const renderIdleDefault = () => null;
18103
+ const renderLoadingDefault = () => null;
18104
+ const renderAbortedDefault = () => null;
18105
+ const renderErrorDefault = error => {
18106
+ let routeErrorText = error && error.message ? error.message : error;
18107
+ return jsxs("p", {
18108
+ className: "action_error",
18109
+ children: ["An error occured: ", routeErrorText]
18110
+ });
18111
+ };
18112
+ const renderCompletedDefault = () => null;
18113
+ const ActionRenderer = ({
18114
+ action,
18115
+ children,
18116
+ disabled
18117
+ }) => {
18118
+ const {
18119
+ idle: renderIdle = renderIdleDefault,
18120
+ loading: renderLoading = renderLoadingDefault,
18121
+ aborted: renderAborted = renderAbortedDefault,
18122
+ error: renderError = renderErrorDefault,
18123
+ completed: renderCompleted,
18124
+ always: renderAlways
18125
+ } = typeof children === "function" ? {
18126
+ completed: children
18127
+ } : children || {};
18128
+ if (disabled) {
18129
+ return null;
18130
+ }
18131
+ if (action === undefined) {
18132
+ throw new Error("ActionRenderer requires an action to render, but none was provided.");
18133
+ }
18134
+ const {
18135
+ idle,
18136
+ loading,
18137
+ aborted,
18138
+ error,
18139
+ data
18140
+ } = useActionStatus(action);
18141
+ const UIRenderedPromise = useUIRenderedPromise(action);
18142
+ const [errorBoundary, resetErrorBoundary] = useErrorBoundary();
18143
+
18144
+ // Mark this action as bound to UI components (has renderers)
18145
+ // This tells the action system that errors should be caught and stored
18146
+ // in the action's error state rather than bubbling up
18147
+ useLayoutEffect(() => {
18148
+ if (action) {
18149
+ const {
18150
+ ui
18151
+ } = getActionPrivateProperties(action);
18152
+ ui.hasRenderers = true;
18153
+ }
18154
+ }, [action]);
18155
+ useLayoutEffect(() => {
18156
+ resetErrorBoundary();
18157
+ }, [action, loading, idle, resetErrorBoundary]);
18158
+ useLayoutEffect(() => {
18159
+ UIRenderedPromise.resolve();
18160
+ return () => {
18161
+ actionUIRenderedPromiseWeakMap.delete(action);
18162
+ };
18163
+ }, [action]);
18164
+
18165
+ // If renderAlways is provided, it wins and handles all rendering
18166
+ if (renderAlways) {
18167
+ return renderAlways({
18168
+ idle,
18169
+ loading,
18170
+ aborted,
18171
+ error,
18172
+ data
18173
+ });
18174
+ }
18175
+ if (idle) {
18176
+ return renderIdle(action);
18177
+ }
18178
+ if (errorBoundary) {
18179
+ return renderError(errorBoundary, "ui_error", action);
18180
+ }
18181
+ if (error) {
18182
+ return renderError(error, "action_error", action);
18183
+ }
18184
+ if (aborted) {
18185
+ return renderAborted(action);
18186
+ }
18187
+ let renderCompletedSafe;
18188
+ if (renderCompleted) {
18189
+ renderCompletedSafe = renderCompleted;
18190
+ } else {
18191
+ const {
18192
+ ui
18193
+ } = getActionPrivateProperties(action);
18194
+ if (ui.renderCompleted) {
18195
+ renderCompletedSafe = ui.renderCompleted;
18196
+ } else {
18197
+ renderCompletedSafe = renderCompletedDefault;
18198
+ }
18199
+ }
18200
+ if (loading) {
18201
+ if (action.canDisplayOldData && data !== undefined) {
18202
+ return renderCompletedSafe(data, action);
18203
+ }
18204
+ return renderLoading(action);
18205
+ }
18206
+ return renderCompletedSafe(data, action);
18207
+ };
18208
+ const defaultPromise = Promise.resolve();
18209
+ defaultPromise.resolve = () => {};
18210
+ const actionUIRenderedPromiseWeakMap = new WeakMap();
18211
+ const useUIRenderedPromise = action => {
18212
+ if (!action) {
18213
+ return defaultPromise;
18214
+ }
18215
+ const actionUIRenderedPromise = actionUIRenderedPromiseWeakMap.get(action);
18216
+ if (actionUIRenderedPromise) {
18217
+ return actionUIRenderedPromise;
18218
+ }
18219
+ let resolve;
18220
+ const promise = new Promise(res => {
18221
+ resolve = res;
18222
+ });
18223
+ promise.resolve = resolve;
18224
+ actionUIRenderedPromiseWeakMap.set(action, promise);
18225
+ return promise;
18226
+ };
18227
+
18228
+ const useFocusGroup = (
18229
+ elementRef,
18230
+ { enabled = true, direction, skipTab, loop, name } = {},
18231
+ ) => {
18232
+ useLayoutEffect(() => {
18233
+ if (!enabled) {
18234
+ return null;
18235
+ }
18236
+ const focusGroup = initFocusGroup(elementRef.current, {
18237
+ direction,
18238
+ skipTab,
18239
+ loop,
18240
+ name,
18241
+ });
18242
+ return focusGroup.cleanup;
18243
+ }, [direction, skipTab, loop, name]);
18244
+ };
18245
+
18246
+ installImportMetaCss(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
18247
+ const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z";
18248
+ import.meta.css = /* css */`
18249
+ .summary_marker {
18250
+ width: 1em;
18251
+ height: 1em;
18252
+ line-height: 1em;
18253
+ }
18254
+ .summary_marker_svg .arrow {
18255
+ animation-duration: 0.3s;
18256
+ animation-fill-mode: forwards;
18257
+ animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
18258
+ }
18259
+ .summary_marker_svg .arrow[data-animation-target="down"] {
18260
+ animation-name: morph-to-down;
18261
+ }
18262
+ @keyframes morph-to-down {
18263
+ from {
18264
+ d: path("${rightArrowPath}");
18265
+ }
18266
+ to {
18267
+ d: path("${downArrowPath}");
18268
+ }
18269
+ }
18270
+ .summary_marker_svg .arrow[data-animation-target="right"] {
18271
+ animation-name: morph-to-right;
18272
+ }
18273
+ @keyframes morph-to-right {
18274
+ from {
18275
+ d: path("${downArrowPath}");
18276
+ }
18277
+ to {
18278
+ d: path("${rightArrowPath}");
18279
+ }
18280
+ }
18281
+
18282
+ .summary_marker_svg .foreground_circle {
18283
+ stroke-dasharray: 503 1507; /* ~25% of circle perimeter */
18284
+ stroke-dashoffset: 0;
18285
+ animation: progress-around-circle 1.5s linear infinite;
18286
+ }
18287
+ @keyframes progress-around-circle {
18288
+ 0% {
18289
+ stroke-dashoffset: 0;
18290
+ }
18291
+ 100% {
18292
+ stroke-dashoffset: -2010;
18293
+ }
18294
+ }
18295
+
18296
+ /* fading and scaling */
18297
+ .summary_marker_svg .arrow {
18298
+ transition: opacity 0.3s ease-in-out;
18299
+ opacity: 1;
18300
+ }
18301
+ .summary_marker_svg .loading_container {
18302
+ transition: transform 0.3s linear;
18303
+ transform: scale(0.3);
18304
+ }
18305
+ .summary_marker_svg .background_circle,
18306
+ .summary_marker_svg .foreground_circle {
18307
+ transition: opacity 0.3s ease-in-out;
18308
+ opacity: 0;
18309
+ }
18310
+ .summary_marker_svg[data-loading] .arrow {
18311
+ opacity: 0;
18312
+ }
18313
+ .summary_marker_svg[data-loading] .loading_container {
18314
+ transform: scale(1);
18315
+ }
18316
+ .summary_marker_svg[data-loading] .background_circle {
18317
+ opacity: 0.2;
18318
+ }
18319
+ .summary_marker_svg[data-loading] .foreground_circle {
18320
+ opacity: 1;
18321
+ }
18322
+ `;
18323
+ const SummaryMarker = ({
18324
+ open,
18325
+ loading
18326
+ }) => {
18327
+ const showLoading = useDebounceTrue(loading, 300);
18328
+ const mountedRef = useRef(false);
18329
+ const prevOpenRef = useRef(open);
18330
+ useLayoutEffect(() => {
18331
+ mountedRef.current = true;
18332
+ return () => {
18333
+ mountedRef.current = false;
18334
+ };
18335
+ }, []);
18336
+ const shouldAnimate = mountedRef.current && prevOpenRef.current !== open;
18337
+ prevOpenRef.current = open;
18338
+ return jsx("span", {
18339
+ className: "summary_marker",
18340
+ children: jsxs("svg", {
18341
+ className: "summary_marker_svg",
18342
+ viewBox: "0 -960 960 960",
18343
+ xmlns: "http://www.w3.org/2000/svg",
18344
+ "data-loading": open ? showLoading || undefined : undefined,
18345
+ children: [jsxs("g", {
18346
+ className: "loading_container",
18347
+ "transform-origin": "480px -480px",
18348
+ children: [jsx("circle", {
18349
+ className: "background_circle",
18350
+ cx: "480",
18351
+ cy: "-480",
18352
+ r: "320",
18353
+ stroke: "currentColor",
18354
+ fill: "none",
18355
+ strokeWidth: "60",
18356
+ opacity: "0.2"
18357
+ }), jsx("circle", {
18358
+ className: "foreground_circle",
18359
+ cx: "480",
18360
+ cy: "-480",
18361
+ r: "320",
18362
+ stroke: "currentColor",
18363
+ fill: "none",
18364
+ strokeWidth: "60",
18365
+ strokeLinecap: "round",
18366
+ strokeDasharray: "503 1507"
18367
+ })]
18368
+ }), jsx("g", {
18369
+ className: "arrow_container",
18370
+ "transform-origin": "480px -480px",
18371
+ children: jsx("path", {
18372
+ className: "arrow",
18373
+ fill: "currentColor",
18374
+ "data-animation-target": shouldAnimate ? open ? "down" : "right" : undefined,
18375
+ d: open ? downArrowPath : rightArrowPath
18376
+ })
18377
+ })]
18378
+ })
18379
+ });
18104
18380
  };
18105
18381
 
18106
18382
  installImportMetaCss(import.meta);import.meta.css = /* css */`
18107
- .navi_select[data-readonly] {
18108
- pointer-events: none;
18383
+ .navi_details {
18384
+ position: relative;
18385
+ z-index: 1;
18386
+ display: flex;
18387
+ flex-shrink: 0;
18388
+ flex-direction: column;
18389
+ }
18390
+
18391
+ .navi_details > summary {
18392
+ display: flex;
18393
+ flex-shrink: 0;
18394
+ flex-direction: column;
18395
+ cursor: pointer;
18396
+ user-select: none;
18397
+ }
18398
+ .summary_body {
18399
+ display: flex;
18400
+ width: 100%;
18401
+ flex-direction: row;
18402
+ align-items: center;
18403
+ gap: 0.2em;
18404
+ }
18405
+ .summary_label {
18406
+ display: flex;
18407
+ padding-right: 10px;
18408
+ flex: 1;
18409
+ align-items: center;
18410
+ gap: 0.2em;
18411
+ }
18412
+
18413
+ .navi_details > summary:focus {
18414
+ z-index: 1;
18109
18415
  }
18110
18416
  `;
18111
- const Select = forwardRef((props, ref) => {
18112
- const select = renderActionableComponent(props, ref);
18113
- return select;
18417
+ const Details = forwardRef((props, ref) => {
18418
+ return renderActionableComponent(props, ref);
18114
18419
  });
18115
- const SelectControlled = forwardRef((props, ref) => {
18420
+ const DetailsBasic = forwardRef((props, ref) => {
18116
18421
  const {
18117
- name,
18118
- value,
18422
+ id,
18423
+ label = "Summary",
18424
+ open,
18119
18425
  loading,
18120
- disabled,
18121
- readOnly,
18426
+ className,
18427
+ focusGroup,
18428
+ focusGroupDirection,
18429
+ arrowKeyShortcuts = true,
18430
+ openKeyShortcut = "ArrowRight",
18431
+ closeKeyShortcut = "ArrowLeft",
18432
+ onToggle,
18122
18433
  children,
18123
18434
  ...rest
18124
18435
  } = props;
18125
18436
  const innerRef = useRef();
18126
18437
  useImperativeHandle(ref, () => innerRef.current);
18127
- const selectElement = jsx("select", {
18128
- className: "navi_select",
18129
- ref: innerRef,
18130
- "data-readonly": readOnly && !disabled ? "" : undefined,
18131
- onKeyDown: e => {
18132
- if (readOnly) {
18438
+ const [navState, setNavState] = useNavState(id);
18439
+ const [innerOpen, innerOpenSetter] = useState(open || navState);
18440
+ useFocusGroup(innerRef, {
18441
+ enabled: focusGroup,
18442
+ name: typeof focusGroup === "string" ? focusGroup : undefined,
18443
+ direction: focusGroupDirection
18444
+ });
18445
+
18446
+ /**
18447
+ * Browser will dispatch "toggle" event even if we set open={true}
18448
+ * When rendering the component for the first time
18449
+ * We have to ensure the initial "toggle" event is ignored.
18450
+ *
18451
+ * If we don't do that code will think the details has changed and run logic accordingly
18452
+ * For example it will try to navigate to the current url while we are already there
18453
+ *
18454
+ * See:
18455
+ * - https://techblog.thescore.com/2024/10/08/why-we-decided-to-change-how-the-details-element-works/
18456
+ * - https://github.com/whatwg/html/issues/4500
18457
+ * - https://stackoverflow.com/questions/58942600/react-html-details-toggles-uncontrollably-when-starts-open
18458
+ *
18459
+ */
18460
+
18461
+ const summaryRef = useRef(null);
18462
+ useKeyboardShortcuts(innerRef, [{
18463
+ key: openKeyShortcut,
18464
+ enabled: arrowKeyShortcuts,
18465
+ when: e => document.activeElement === summaryRef.current &&
18466
+ // avoid handling openKeyShortcut twice when keydown occurs inside nested details
18467
+ !e.defaultPrevented,
18468
+ action: e => {
18469
+ const details = innerRef.current;
18470
+ if (!details.open) {
18133
18471
  e.preventDefault();
18472
+ details.open = true;
18473
+ return;
18474
+ }
18475
+ const summary = summaryRef.current;
18476
+ const firstFocusableElementInDetails = findAfter(summary, elementIsFocusable, {
18477
+ root: details
18478
+ });
18479
+ if (!firstFocusableElementInDetails) {
18480
+ return;
18134
18481
  }
18482
+ e.preventDefault();
18483
+ firstFocusableElementInDetails.focus();
18484
+ }
18485
+ }, {
18486
+ key: closeKeyShortcut,
18487
+ enabled: arrowKeyShortcuts,
18488
+ when: () => {
18489
+ const details = innerRef.current;
18490
+ return details.open;
18135
18491
  },
18136
- ...rest,
18137
- children: children.map(child => {
18138
- const {
18139
- label,
18140
- readOnly: childReadOnly,
18141
- disabled: childDisabled,
18142
- loading: childLoading,
18143
- value: childValue,
18144
- ...childRest
18145
- } = child;
18146
- return jsx("option", {
18147
- name: name,
18148
- value: childValue,
18149
- selected: childValue === value,
18150
- readOnly: readOnly || childReadOnly,
18151
- disabled: disabled || childDisabled,
18152
- loading: loading || childLoading,
18153
- ...childRest,
18154
- children: label
18155
- }, childValue);
18156
- })
18157
- });
18158
- return jsx(LoaderBackground, {
18159
- loading: loading,
18160
- color: "light-dark(#355fcc, #3b82f6)",
18161
- inset: -1,
18162
- children: selectElement
18163
- });
18164
- });
18165
- forwardRef((props, ref) => {
18166
- const {
18167
- value: initialValue,
18168
- id,
18169
- children,
18170
- ...rest
18171
- } = props;
18172
- const innerRef = useRef();
18173
- useImperativeHandle(ref, () => innerRef.current);
18174
- const [navState, setNavState] = useNavState(id);
18175
- const valueAtStart = navState === undefined ? initialValue : navState;
18176
- const [value, setValue] = useState(valueAtStart);
18492
+ action: e => {
18493
+ const details = innerRef.current;
18494
+ const summary = summaryRef.current;
18495
+ if (document.activeElement === summary) {
18496
+ e.preventDefault();
18497
+ summary.focus();
18498
+ details.open = false;
18499
+ } else {
18500
+ e.preventDefault();
18501
+ summary.focus();
18502
+ }
18503
+ }
18504
+ }]);
18505
+ const mountedRef = useRef(false);
18177
18506
  useEffect(() => {
18178
- setNavState(value);
18179
- }, [value]);
18180
- return jsx(SelectControlled, {
18507
+ mountedRef.current = true;
18508
+ }, []);
18509
+ return jsxs("details", {
18510
+ ...rest,
18181
18511
  ref: innerRef,
18182
- value: value,
18183
- onChange: event => {
18184
- const select = event.target;
18185
- const selectedValue = select.value;
18186
- setValue(selectedValue);
18512
+ id: id,
18513
+ className: ["navi_details", ...(className ? className.split(" ") : [])].join(" "),
18514
+ onToggle: e => {
18515
+ const isOpen = e.newState === "open";
18516
+ if (mountedRef.current) {
18517
+ if (isOpen) {
18518
+ innerOpenSetter(true);
18519
+ setNavState(true);
18520
+ } else {
18521
+ innerOpenSetter(false);
18522
+ setNavState(undefined);
18523
+ }
18524
+ }
18525
+ onToggle?.(e);
18187
18526
  },
18188
- ...rest,
18189
- children: children
18527
+ open: innerOpen,
18528
+ children: [jsx("summary", {
18529
+ ref: summaryRef,
18530
+ children: jsxs("div", {
18531
+ className: "summary_body",
18532
+ children: [jsx(SummaryMarker, {
18533
+ open: innerOpen,
18534
+ loading: loading
18535
+ }), jsx("div", {
18536
+ className: "summary_label",
18537
+ children: label
18538
+ })]
18539
+ })
18540
+ }), children]
18190
18541
  });
18191
18542
  });
18192
18543
  forwardRef((props, ref) => {
18193
18544
  const {
18194
- id,
18195
- name,
18196
- value: externalValue,
18197
- valueSignal,
18198
18545
  action,
18199
- children,
18200
- onCancel,
18546
+ loading,
18547
+ onToggle,
18201
18548
  onActionPrevented,
18202
18549
  onActionStart,
18203
- onActionAbort,
18204
18550
  onActionError,
18205
18551
  onActionEnd,
18206
- actionErrorEffect,
18552
+ children,
18207
18553
  ...rest
18208
18554
  } = props;
18209
18555
  const innerRef = useRef();
18210
18556
  useImperativeHandle(ref, () => innerRef.current);
18211
- const [navState, setNavState, resetNavState] = useNavState(id);
18212
- const [boundAction, value, setValue, initialValue] = useActionBoundToOneParam(action, name);
18557
+ const effectiveAction = useAction(action);
18213
18558
  const {
18214
18559
  loading: actionLoading
18215
- } = useActionStatus(boundAction);
18560
+ } = useActionStatus(effectiveAction);
18216
18561
  const executeAction = useExecuteAction(innerRef, {
18217
- errorEffect: actionErrorEffect
18562
+ // the error will be displayed by actionRenderer inside <details>
18563
+ errorEffect: "none"
18218
18564
  });
18219
- useEffect(() => {
18220
- setNavState(value);
18221
- }, [value]);
18222
- const actionRequesterRef = useRef(null);
18223
18565
  useActionEvents(innerRef, {
18224
- onCancel: (e, reason) => {
18225
- resetNavState();
18226
- setValue(initialValue);
18227
- onCancel?.(e, reason);
18228
- },
18229
18566
  onPrevented: onActionPrevented,
18230
- onAction: actionEvent => {
18231
- actionRequesterRef.current = actionEvent.detail.requester;
18232
- executeAction(actionEvent);
18567
+ onAction: e => {
18568
+ executeAction(e);
18233
18569
  },
18234
18570
  onStart: onActionStart,
18235
- onAbort: e => {
18236
- setValue(initialValue);
18237
- onActionAbort?.(e);
18238
- },
18239
- onError: error => {
18240
- setValue(initialValue);
18241
- onActionError?.(error);
18242
- },
18243
- onEnd: () => {
18244
- resetNavState();
18245
- onActionEnd?.();
18246
- }
18571
+ onError: onActionError,
18572
+ onEnd: onActionEnd
18247
18573
  });
18248
- const childRefArray = useRefArray(children, child => child.value);
18249
- return jsx(SelectControlled, {
18250
- ref: innerRef,
18251
- name: name,
18252
- value: value,
18253
- "data-action": boundAction,
18254
- onChange: event => {
18255
- const select = event.target;
18256
- const selectedValue = select.value;
18257
- setValue(selectedValue);
18258
- const radioListContainer = innerRef.current;
18259
- const optionSelected = select.querySelector(`option[value="${selectedValue}"]`);
18260
- requestAction(radioListContainer, boundAction, {
18261
- event,
18262
- requester: optionSelected
18263
- });
18264
- },
18574
+ return jsx(DetailsBasic, {
18265
18575
  ...rest,
18266
- children: children.map((child, i) => {
18267
- const childRef = childRefArray[i];
18268
- return {
18269
- ...child,
18270
- ref: childRef,
18271
- loading: child.loading || actionLoading && actionRequesterRef.current === childRef.current,
18272
- readOnly: child.readOnly || actionLoading
18273
- };
18274
- })
18275
- });
18276
- });
18277
- forwardRef((props, ref) => {
18278
- const {
18279
- id,
18280
- name,
18281
- value: externalValue,
18282
- children,
18283
- ...rest
18284
- } = props;
18285
- const innerRef = useRef();
18286
- useImperativeHandle(ref, () => innerRef.current);
18287
- const [navState, setNavState] = useNavState(id);
18288
- const [value, setValue, initialValue] = [name, externalValue, navState];
18289
- useEffect(() => {
18290
- setNavState(value);
18291
- }, [value]);
18292
- useFormEvents(innerRef, {
18293
- onFormReset: () => {
18294
- setValue(undefined);
18295
- },
18296
- onFormActionAbort: () => {
18297
- setValue(initialValue);
18298
- },
18299
- onFormActionError: () => {
18300
- setValue(initialValue);
18301
- }
18302
- });
18303
- return jsx(SelectControlled, {
18304
18576
  ref: innerRef,
18305
- name: name,
18306
- value: value,
18307
- onChange: event => {
18308
- const select = event.target;
18309
- const selectedValue = select.checked;
18310
- setValue(selectedValue);
18577
+ loading: loading || actionLoading,
18578
+ onToggle: toggleEvent => {
18579
+ const isOpen = toggleEvent.newState === "open";
18580
+ if (isOpen) {
18581
+ requestAction(toggleEvent.target, effectiveAction, {
18582
+ event: toggleEvent,
18583
+ method: "run"
18584
+ });
18585
+ } else {
18586
+ effectiveAction.abort();
18587
+ }
18588
+ onToggle?.(toggleEvent);
18311
18589
  },
18312
- ...rest,
18313
- children: children
18590
+ children: jsx(ActionRenderer, {
18591
+ action: effectiveAction,
18592
+ children: children
18593
+ })
18314
18594
  });
18315
18595
  });
18316
18596
 
@@ -21678,116 +21958,6 @@ const useCellsAndColumns = (cells, columns) => {
21678
21958
  };
21679
21959
  };
21680
21960
 
21681
- installImportMetaCss(import.meta);import.meta.css = /* css */`
21682
- .navi_tablist {
21683
- display: flex;
21684
- overflow-x: auto;
21685
- overflow-y: hidden;
21686
- justify-content: space-between;
21687
- }
21688
-
21689
- .navi_tablist > ul {
21690
- align-items: center;
21691
- display: flex;
21692
- gap: 0.5rem;
21693
- list-style: none;
21694
- margin: 0;
21695
- padding: 0;
21696
- }
21697
-
21698
- .navi_tablist > ul > li {
21699
- display: inline-flex;
21700
- position: relative;
21701
- }
21702
-
21703
- .navi_tab {
21704
- white-space: nowrap;
21705
- display: flex;
21706
- flex-direction: column;
21707
- }
21708
-
21709
- .navi_tab_content {
21710
- transition: background 0.12s ease-out;
21711
- border-radius: 6px;
21712
- text-decoration: none;
21713
- line-height: 30px;
21714
- display: flex;
21715
- padding: 0 0.5rem;
21716
- }
21717
-
21718
- .navi_tab:hover .navi_tab_content {
21719
- background: #dae0e7;
21720
- color: #010409;
21721
- }
21722
-
21723
- .navi_tab .active_marker {
21724
- display: flex;
21725
- background: transparent;
21726
- border-radius: 0.1px;
21727
- width: 100%;
21728
- z-index: 1;
21729
- height: 2px;
21730
- margin-top: 5px;
21731
- }
21732
-
21733
- /* Hidden bold clone to reserve space for bold width without affecting height */
21734
- .navi_tab_content_bold_clone {
21735
- font-weight: 600; /* force bold to compute max width */
21736
- visibility: hidden; /* not visible */
21737
- display: block; /* in-flow so it contributes to width */
21738
- height: 0; /* zero height so it doesn't change layout height */
21739
- overflow: hidden; /* avoid any accidental height */
21740
- pointer-events: none; /* inert */
21741
- }
21742
-
21743
- .navi_tab[aria-selected="true"] .active_marker {
21744
- background: rgb(205, 52, 37);
21745
- }
21746
-
21747
- .navi_tab[aria-selected="true"] .navi_tab_content {
21748
- font-weight: 600;
21749
- }
21750
- `;
21751
- const TabList = ({
21752
- children,
21753
- ...props
21754
- }) => {
21755
- return jsx("nav", {
21756
- className: "navi_tablist",
21757
- role: "tablist",
21758
- ...props,
21759
- children: jsx("ul", {
21760
- children: children.map(child => {
21761
- return jsx("li", {
21762
- children: child
21763
- }, child.props.key);
21764
- })
21765
- })
21766
- });
21767
- };
21768
- const Tab = ({
21769
- children,
21770
- selected,
21771
- ...props
21772
- }) => {
21773
- return jsxs("div", {
21774
- className: "navi_tab",
21775
- role: "tab",
21776
- "aria-selected": selected ? "true" : "false",
21777
- ...props,
21778
- children: [jsx("div", {
21779
- className: "navi_tab_content",
21780
- children: children
21781
- }), jsx("div", {
21782
- className: "navi_tab_content_bold_clone",
21783
- "aria-hidden": "true",
21784
- children: children
21785
- }), jsx("span", {
21786
- className: "active_marker"
21787
- })]
21788
- });
21789
- };
21790
-
21791
21961
  /**
21792
21962
  * Creates a signal that stays synchronized with an external value,
21793
21963
  * only updating the signal when the value actually changes.
@@ -22357,48 +22527,6 @@ const ViewportLayout = props => {
22357
22527
  });
22358
22528
  };
22359
22529
 
22360
- const createUniqueValueConstraint = (
22361
- // the set might be incomplete (the front usually don't have the full copy of all the items from the backend)
22362
- // but this is already nice to help user with what we know
22363
- // it's also possible that front is unsync with backend, preventing user to choose a value
22364
- // that is actually free.
22365
- // But this is unlikely to happen and user could reload the page to be able to choose that name
22366
- // that suddenly became available
22367
- existingValueSet,
22368
- message = `"{value}" already exists. Please choose another value.`,
22369
- ) => {
22370
- return {
22371
- name: "unique_value",
22372
- check: (input) => {
22373
- const inputValue = input.value;
22374
- const hasConflict = existingValueSet.has(inputValue);
22375
- // console.log({
22376
- // inputValue,
22377
- // names: Array.from(otherNameSet.values()),
22378
- // hasConflict,
22379
- // });
22380
- if (hasConflict) {
22381
- return message.replace("{value}", inputValue);
22382
- }
22383
- return "";
22384
- },
22385
- };
22386
- };
22387
-
22388
- const SINGLE_SPACE_CONSTRAINT = {
22389
- name: "single_space",
22390
- check: (input) => {
22391
- const inputValue = input.value;
22392
- const hasLeadingSpace = inputValue.startsWith(" ");
22393
- const hasTrailingSpace = inputValue.endsWith(" ");
22394
- const hasDoubleSpace = inputValue.includes(" ");
22395
- if (hasLeadingSpace || hasDoubleSpace || hasTrailingSpace) {
22396
- return "Spaces at the beginning, end, or consecutive spaces are not allowed";
22397
- }
22398
- return "";
22399
- },
22400
- };
22401
-
22402
22530
  /*
22403
22531
  * - Usage
22404
22532
  * useEffect(() => {