@jsenv/navi 0.29.2 → 0.29.4
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.
- package/dist/jsenv_navi.js +1490 -63
- package/dist/jsenv_navi.js.map +107 -21
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -76,7 +76,7 @@ const useActionStatus = (action) => {
|
|
|
76
76
|
};
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
79
|
+
installImportMetaCssBuild(import.meta);const css$W = /* css */`
|
|
80
80
|
.action_error {
|
|
81
81
|
margin-top: 0;
|
|
82
82
|
margin-bottom: 20px;
|
|
@@ -101,7 +101,7 @@ const ActionRenderer = ({
|
|
|
101
101
|
children,
|
|
102
102
|
disabled
|
|
103
103
|
}) => {
|
|
104
|
-
import.meta.css = [css$
|
|
104
|
+
import.meta.css = [css$W, "@jsenv/navi/src/action/action_renderer.jsx"];
|
|
105
105
|
if (action === undefined) {
|
|
106
106
|
throw new Error("ActionRenderer requires an action to render, but none was provided.");
|
|
107
107
|
}
|
|
@@ -16381,7 +16381,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
16381
16381
|
* - Arrow automatically shows when pointing at a valid anchor element
|
|
16382
16382
|
* - Centers in viewport when no anchor element provided or anchor is too big
|
|
16383
16383
|
*/
|
|
16384
|
-
const css$
|
|
16384
|
+
const css$V = /* css */`
|
|
16385
16385
|
@layer navi {
|
|
16386
16386
|
.navi_callout {
|
|
16387
16387
|
/* A callout is parented to what it explains, so it inherits from it — and
|
|
@@ -16601,7 +16601,7 @@ const openCallout = (message, {
|
|
|
16601
16601
|
skipFocus = false,
|
|
16602
16602
|
debug = () => {}
|
|
16603
16603
|
} = {}) => {
|
|
16604
|
-
import.meta.css = [css$
|
|
16604
|
+
import.meta.css = [css$V, "@jsenv/navi/src/control/rules/callout/callout.js"];
|
|
16605
16605
|
if (debug === true) {
|
|
16606
16606
|
debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
|
|
16607
16607
|
}
|
|
@@ -28374,7 +28374,7 @@ const setupNetworkMonitoring = () => {
|
|
|
28374
28374
|
};
|
|
28375
28375
|
setupNetworkMonitoring();
|
|
28376
28376
|
|
|
28377
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
28377
|
+
installImportMetaCssBuild(import.meta);const css$U = /* css */`
|
|
28378
28378
|
.navi_loading_indicator_fluid_container {
|
|
28379
28379
|
position: relative;
|
|
28380
28380
|
display: flex;
|
|
@@ -28406,7 +28406,7 @@ const LoadingIndicatorFluid = ({
|
|
|
28406
28406
|
visuallyHidden,
|
|
28407
28407
|
...rest
|
|
28408
28408
|
}) => {
|
|
28409
|
-
import.meta.css = [css$
|
|
28409
|
+
import.meta.css = [css$U, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
|
|
28410
28410
|
const ref = useRef(null);
|
|
28411
28411
|
// The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
|
|
28412
28412
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
@@ -28611,7 +28611,7 @@ const LoadingRectangleSvg = ({
|
|
|
28611
28611
|
});
|
|
28612
28612
|
};
|
|
28613
28613
|
|
|
28614
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
28614
|
+
installImportMetaCssBuild(import.meta);const css$T = /* css */`
|
|
28615
28615
|
.navi_loading_outline_wrapper {
|
|
28616
28616
|
position: absolute;
|
|
28617
28617
|
/* Controls place the outline slightly outside their box, right on top of
|
|
@@ -28648,7 +28648,7 @@ installImportMetaCssBuild(import.meta);const css$R = /* css */`
|
|
|
28648
28648
|
}
|
|
28649
28649
|
`;
|
|
28650
28650
|
const LoadingOutline = props => {
|
|
28651
|
-
import.meta.css = [css$
|
|
28651
|
+
import.meta.css = [css$T, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
|
|
28652
28652
|
if (props.containerRef) {
|
|
28653
28653
|
const container = props.containerRef.current;
|
|
28654
28654
|
if (!container) {
|
|
@@ -28873,7 +28873,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
|
|
|
28873
28873
|
};
|
|
28874
28874
|
|
|
28875
28875
|
installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
|
|
28876
|
-
const css$
|
|
28876
|
+
const css$S = /* css */`
|
|
28877
28877
|
@layer navi {
|
|
28878
28878
|
.navi_text {
|
|
28879
28879
|
&[data-skeleton] {
|
|
@@ -29378,7 +29378,7 @@ const TextShrinkWrap = props => {
|
|
|
29378
29378
|
});
|
|
29379
29379
|
};
|
|
29380
29380
|
const TextUI = props => {
|
|
29381
|
-
import.meta.css = [css$
|
|
29381
|
+
import.meta.css = [css$S, "@jsenv/navi/src/text/text.jsx"];
|
|
29382
29382
|
let {
|
|
29383
29383
|
ref,
|
|
29384
29384
|
spacing,
|
|
@@ -29529,7 +29529,7 @@ const TextWithSelectRange = ({
|
|
|
29529
29529
|
};
|
|
29530
29530
|
|
|
29531
29531
|
installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
|
|
29532
|
-
const css$
|
|
29532
|
+
const css$R = /* css */`
|
|
29533
29533
|
.navi_text_anchor {
|
|
29534
29534
|
vertical-align: baseline;
|
|
29535
29535
|
user-select: none;
|
|
@@ -29564,7 +29564,7 @@ const TextAnchor = ({
|
|
|
29564
29564
|
textSize,
|
|
29565
29565
|
lineLayout
|
|
29566
29566
|
}) => {
|
|
29567
|
-
import.meta.css = [css$
|
|
29567
|
+
import.meta.css = [css$R, "@jsenv/navi/src/text/text_anchor.jsx"];
|
|
29568
29568
|
const anchorRef = useRef();
|
|
29569
29569
|
|
|
29570
29570
|
// Plain useLayoutEffect would also fire while an ancestor dialog/popover
|
|
@@ -29679,7 +29679,7 @@ const computeTopOffset = ({
|
|
|
29679
29679
|
};
|
|
29680
29680
|
const charTopCanvas = document.createElement("canvas");
|
|
29681
29681
|
|
|
29682
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
29682
|
+
installImportMetaCssBuild(import.meta);const css$Q = /* css */`
|
|
29683
29683
|
@layer navi {
|
|
29684
29684
|
/* Ensure data attributes from box.jsx can win to update display */
|
|
29685
29685
|
.navi_icon {
|
|
@@ -29837,7 +29837,7 @@ const Icon = ({
|
|
|
29837
29837
|
fillLine,
|
|
29838
29838
|
...props
|
|
29839
29839
|
}) => {
|
|
29840
|
-
import.meta.css = [css$
|
|
29840
|
+
import.meta.css = [css$Q, "@jsenv/navi/src/text/icon.jsx"];
|
|
29841
29841
|
const innerChildren = href ? jsx("svg", {
|
|
29842
29842
|
width: "100%",
|
|
29843
29843
|
height: "100%",
|
|
@@ -29941,6 +29941,18 @@ const useIsVisited = (url) => {
|
|
|
29941
29941
|
}, [url, visitedUrlsSignal.value]);
|
|
29942
29942
|
};
|
|
29943
29943
|
|
|
29944
|
+
/**
|
|
29945
|
+
* What a binder tab hands to whatever it contains: a way to say "I am the
|
|
29946
|
+
* current one". A `Link` inside a tab knows it is current (its route matches)
|
|
29947
|
+
* long before the binder could work it out, so it reports instead of the
|
|
29948
|
+
* binder guessing — see `Link`'s use of this context.
|
|
29949
|
+
*
|
|
29950
|
+
* Its own module so `link.jsx` can read it without importing the binder.
|
|
29951
|
+
*
|
|
29952
|
+
* @type {import("preact").Context<{ reportCurrent: (current: boolean) => void } | null>}
|
|
29953
|
+
*/
|
|
29954
|
+
const BinderItemContext = createContext(null);
|
|
29955
|
+
|
|
29944
29956
|
/*
|
|
29945
29957
|
* Custom hook to apply semi-transparent color when an element should be dimmed.
|
|
29946
29958
|
*
|
|
@@ -29978,7 +29990,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
|
|
|
29978
29990
|
});
|
|
29979
29991
|
};
|
|
29980
29992
|
|
|
29981
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
29993
|
+
installImportMetaCssBuild(import.meta);const css$P = /* css */`
|
|
29982
29994
|
@layer navi {
|
|
29983
29995
|
.navi_link {
|
|
29984
29996
|
--link-border-radius: unset;
|
|
@@ -30053,7 +30065,12 @@ installImportMetaCssBuild(import.meta);const css$N = /* css */`
|
|
|
30053
30065
|
aspect-ratio: inherit;
|
|
30054
30066
|
padding-top: var(--x-link-padding-top);
|
|
30055
30067
|
padding-right: var(--x-link-padding-right);
|
|
30056
|
-
|
|
30068
|
+
/* The loading underline is drawn inside the box: its own room is kept at
|
|
30069
|
+
the bottom on top of whatever padding was asked for, so a link that
|
|
30070
|
+
starts loading never reflows. */
|
|
30071
|
+
padding-bottom: calc(
|
|
30072
|
+
var(--x-link-padding-bottom) + var(--link-loading-outline-size)
|
|
30073
|
+
);
|
|
30057
30074
|
padding-left: var(--x-link-padding-left);
|
|
30058
30075
|
color: var(--x-link-color);
|
|
30059
30076
|
text-decoration: var(--x-link-text-decoration);
|
|
@@ -30311,6 +30328,21 @@ const LinkStyleCSSVars = {
|
|
|
30311
30328
|
color: "--link-color-selected"
|
|
30312
30329
|
}
|
|
30313
30330
|
};
|
|
30331
|
+
// A link placed inside a binder tab tells that tab whether it is the current
|
|
30332
|
+
// one. The binder cannot know: which page an url opens is the link's business,
|
|
30333
|
+
// and it is settled before the binder renders anything.
|
|
30334
|
+
const useReportCurrentToBinderItem = current => {
|
|
30335
|
+
const reportCurrent = useContext(BinderItemContext);
|
|
30336
|
+
useLayoutEffect(() => {
|
|
30337
|
+
if (!reportCurrent) {
|
|
30338
|
+
return undefined;
|
|
30339
|
+
}
|
|
30340
|
+
reportCurrent(current);
|
|
30341
|
+
return () => {
|
|
30342
|
+
reportCurrent(false);
|
|
30343
|
+
};
|
|
30344
|
+
}, [reportCurrent, current]);
|
|
30345
|
+
};
|
|
30314
30346
|
const LinkPseudoClasses = [":hover", ":active", ":-navi-pressed", ":focus", ":focus-visible", ":read-only", ":disabled", ":visited", ":-navi-loading", ":-navi-href-internal", ":-navi-href-external", ":-navi-href-anchor", ":-navi-href-current", ":-navi-selected"];
|
|
30315
30347
|
const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
|
|
30316
30348
|
Object.assign(PSEUDO_CLASSES, {
|
|
@@ -30400,7 +30432,7 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
30400
30432
|
* @param {boolean} [props.readOnly]
|
|
30401
30433
|
*/
|
|
30402
30434
|
const Link = props => {
|
|
30403
|
-
import.meta.css = [css$
|
|
30435
|
+
import.meta.css = [css$P, "@jsenv/navi/src/nav/link/link.jsx"];
|
|
30404
30436
|
if (props.route) {
|
|
30405
30437
|
return jsx(LinkWithRoute, {
|
|
30406
30438
|
...props
|
|
@@ -30489,6 +30521,7 @@ const LinkPlain = props => {
|
|
|
30489
30521
|
isCurrent
|
|
30490
30522
|
} = getHrefTargetInfo(href);
|
|
30491
30523
|
const innerCurrent = current || isCurrent;
|
|
30524
|
+
useReportCurrentToBinderItem(innerCurrent);
|
|
30492
30525
|
controlHostProps.basePseudoState = {
|
|
30493
30526
|
...basePseudoState,
|
|
30494
30527
|
":visited": visited,
|
|
@@ -30634,7 +30667,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
30634
30667
|
* TabList component with support for horizontal and vertical layouts
|
|
30635
30668
|
* https://dribbble.com/search/tabs
|
|
30636
30669
|
*/
|
|
30637
|
-
const css$
|
|
30670
|
+
const css$O = /* css */`
|
|
30638
30671
|
@layer navi {
|
|
30639
30672
|
.navi_nav {
|
|
30640
30673
|
--nav-border: none;
|
|
@@ -30727,20 +30760,60 @@ const css$M = /* css */`
|
|
|
30727
30760
|
}
|
|
30728
30761
|
}
|
|
30729
30762
|
|
|
30730
|
-
|
|
30731
|
-
|
|
30763
|
+
/* Folder tabs: the current tab and the panel share one surface. Every tab
|
|
30764
|
+
carries the panel's line on the side facing it, and the current one
|
|
30765
|
+
paints that side with the panel background instead, so the line opens
|
|
30766
|
+
there. The negative margin makes the two lines overlap — without it the
|
|
30767
|
+
panel's own line would still be drawn under the tabs; and since a tab
|
|
30768
|
+
background is painted under its border too, a tab with a background of
|
|
30769
|
+
its own would otherwise hide the panel's line over its width.
|
|
30770
|
+
|
|
30771
|
+
The line is 1px and never thicker: past that, the corner where a square
|
|
30772
|
+
tab meets the panel shows the miter as a visible notch. */
|
|
30773
|
+
&[data-panel-position] {
|
|
30774
|
+
--nav-border-color: transparent;
|
|
30775
|
+
--nav-tab-border-radius: 0px;
|
|
30776
|
+
|
|
30777
|
+
--x-nav-panel-background: var(
|
|
30778
|
+
--nav-panel-background,
|
|
30779
|
+
var(--link-background-current, white)
|
|
30780
|
+
);
|
|
30781
|
+
|
|
30732
30782
|
position: relative;
|
|
30733
30783
|
z-index: 1;
|
|
30734
30784
|
|
|
30735
30785
|
.navi_link {
|
|
30736
|
-
border:
|
|
30786
|
+
border: 1px solid transparent;
|
|
30787
|
+
|
|
30788
|
+
&[data-href-current] {
|
|
30789
|
+
border-color: var(--nav-border-color);
|
|
30790
|
+
}
|
|
30791
|
+
}
|
|
30792
|
+
|
|
30793
|
+
&[data-panel-position="after"] {
|
|
30794
|
+
margin-bottom: -1px;
|
|
30737
30795
|
|
|
30738
|
-
|
|
30739
|
-
border-color:
|
|
30740
|
-
border-bottom-color: var(--nav-background);
|
|
30796
|
+
.navi_link {
|
|
30797
|
+
border-bottom-color: var(--nav-border-color);
|
|
30741
30798
|
|
|
30742
|
-
|
|
30743
|
-
|
|
30799
|
+
&[data-href-current] {
|
|
30800
|
+
border-bottom-color: var(--x-nav-panel-background);
|
|
30801
|
+
border-top-left-radius: var(--nav-tab-border-radius);
|
|
30802
|
+
border-top-right-radius: var(--nav-tab-border-radius);
|
|
30803
|
+
}
|
|
30804
|
+
}
|
|
30805
|
+
}
|
|
30806
|
+
&[data-panel-position="before"] {
|
|
30807
|
+
margin-top: -1px;
|
|
30808
|
+
|
|
30809
|
+
.navi_link {
|
|
30810
|
+
border-top-color: var(--nav-border-color);
|
|
30811
|
+
|
|
30812
|
+
&[data-href-current] {
|
|
30813
|
+
border-top-color: var(--x-nav-panel-background);
|
|
30814
|
+
border-bottom-right-radius: var(--nav-tab-border-radius);
|
|
30815
|
+
border-bottom-left-radius: var(--nav-tab-border-radius);
|
|
30816
|
+
}
|
|
30744
30817
|
}
|
|
30745
30818
|
}
|
|
30746
30819
|
}
|
|
@@ -30766,11 +30839,10 @@ const Nav = ({
|
|
|
30766
30839
|
expandX,
|
|
30767
30840
|
linkBorderRadiusInherit,
|
|
30768
30841
|
panelPosition,
|
|
30769
|
-
// before or after
|
|
30770
|
-
panelBorderConnection,
|
|
30842
|
+
// "before" or "after": which side the panel sits on, turning the nav into folder tabs
|
|
30771
30843
|
...props
|
|
30772
30844
|
}) => {
|
|
30773
|
-
import.meta.css = [css$
|
|
30845
|
+
import.meta.css = [css$O, "@jsenv/navi/src/nav/link/nav.jsx"];
|
|
30774
30846
|
children = toChildArray(children);
|
|
30775
30847
|
return jsx(Box, {
|
|
30776
30848
|
as: "nav",
|
|
@@ -30781,7 +30853,6 @@ const Nav = ({
|
|
|
30781
30853
|
"data-expand": expand || expandX ? "" : undefined,
|
|
30782
30854
|
"data-vertical": vertical ? "" : undefined,
|
|
30783
30855
|
"data-panel-position": panelPosition,
|
|
30784
|
-
"data-panel-border-connection": panelBorderConnection ? "" : undefined,
|
|
30785
30856
|
expand: expand,
|
|
30786
30857
|
expandX: expandX,
|
|
30787
30858
|
spacing: spacing,
|
|
@@ -30794,6 +30865,1268 @@ const Nav = ({
|
|
|
30794
30865
|
});
|
|
30795
30866
|
};
|
|
30796
30867
|
|
|
30868
|
+
/**
|
|
30869
|
+
* The binder outline: one closed path wrapping the tab row AND the page, with
|
|
30870
|
+
* the current tab opening into the page through concave junction curves.
|
|
30871
|
+
*
|
|
30872
|
+
* Why a path instead of CSS borders — CSS cannot do either of these:
|
|
30873
|
+
* 1. Omit one side of a border while keeping the surrounding corners intact
|
|
30874
|
+
* (the current tab has no border on the page side, so it merges with it).
|
|
30875
|
+
* 2. Curve a corner inward (concave), which is what a tab-to-page junction
|
|
30876
|
+
* is. `border-radius` only bulges outward.
|
|
30877
|
+
*
|
|
30878
|
+
* Everything here is in "path space": the main axis (along the tab row) is x
|
|
30879
|
+
* and the cross axis goes from the tabs toward the page, whatever the real
|
|
30880
|
+
* orientation. Tabs left/right reuse the top/bottom builders and transpose the
|
|
30881
|
+
* result at the end.
|
|
30882
|
+
*
|
|
30883
|
+
* The path is a centerline: it runs half a border-width inside the box edges,
|
|
30884
|
+
* so a stroke of `borderWidth` covers exactly the border area the CSS borders
|
|
30885
|
+
* of the inactive tabs occupy, and the two line up.
|
|
30886
|
+
*/
|
|
30887
|
+
|
|
30888
|
+
// Low-level arc helpers: produce SVG arc commands with an explicit sweep flag.
|
|
30889
|
+
// sweep=1 → clockwise arc; sweep=0 → counter-clockwise arc.
|
|
30890
|
+
const arcSweep1 = (r, dx, dy) => {
|
|
30891
|
+
if (r <= 0) {
|
|
30892
|
+
return `l ${dx},${dy}`;
|
|
30893
|
+
}
|
|
30894
|
+
return `a ${r},${r} 0 0 1 ${dx},${dy}`;
|
|
30895
|
+
};
|
|
30896
|
+
const arcSweep0 = (r, dx, dy) => {
|
|
30897
|
+
if (r <= 0) {
|
|
30898
|
+
return `l ${dx},${dy}`;
|
|
30899
|
+
}
|
|
30900
|
+
return `a ${r},${r} 0 0 0 ${dx},${dy}`;
|
|
30901
|
+
};
|
|
30902
|
+
// Semantic aliases used when drawing the CW outer path.
|
|
30903
|
+
// In a CW path: convex (outward) corners use sweep=1, concave (inward) use sweep=0.
|
|
30904
|
+
const arcConvex = arcSweep1;
|
|
30905
|
+
const arcConcave = arcSweep0;
|
|
30906
|
+
|
|
30907
|
+
// Swaps x and y in a command list produced by the path builders (only
|
|
30908
|
+
// M/h/v/l/a/Z commands, in the exact format they emit). Swapping axes is a
|
|
30909
|
+
// mirror transform, so arc sweep flags must flip.
|
|
30910
|
+
const transposePath = (commands) =>
|
|
30911
|
+
commands.map((command) => {
|
|
30912
|
+
const parts = command.split(" ");
|
|
30913
|
+
const type = parts[0];
|
|
30914
|
+
if (type === "M" || type === "l") {
|
|
30915
|
+
const [x, y] = parts[1].split(",");
|
|
30916
|
+
return `${type} ${y},${x}`;
|
|
30917
|
+
}
|
|
30918
|
+
if (type === "h") {
|
|
30919
|
+
return `v ${parts[1]}`;
|
|
30920
|
+
}
|
|
30921
|
+
if (type === "v") {
|
|
30922
|
+
return `h ${parts[1]}`;
|
|
30923
|
+
}
|
|
30924
|
+
if (type === "a") {
|
|
30925
|
+
const sweep = parts[4] === "1" ? "0" : "1";
|
|
30926
|
+
const [dx, dy] = parts[5].split(",");
|
|
30927
|
+
return `a ${parts[1]} ${parts[2]} ${parts[3]} ${sweep} ${dy},${dx}`;
|
|
30928
|
+
}
|
|
30929
|
+
return command;
|
|
30930
|
+
});
|
|
30931
|
+
|
|
30932
|
+
/**
|
|
30933
|
+
* @param {object} params
|
|
30934
|
+
* @param {number} params.tabX - Where the current tab starts along the tab row.
|
|
30935
|
+
* @param {number} params.tabWidth - Its size along the tab row.
|
|
30936
|
+
* @param {number} params.tabRowHeight - From the outer edge of the tab row to
|
|
30937
|
+
* the junction line where tabs meet the page.
|
|
30938
|
+
* @param {number} params.panelWidth - The whole binder, along the tab row.
|
|
30939
|
+
* @param {number} params.panelHeight - From the junction line to the far edge
|
|
30940
|
+
* of the page.
|
|
30941
|
+
* @param {number} params.gapBeforeTabs - Free room before the first tab, and
|
|
30942
|
+
* @param {number} params.gapAfterTabs - after the last one: a page corner on
|
|
30943
|
+
* the tab-row side is only rounded when its gap has room for the radius.
|
|
30944
|
+
* @param {number} params.outerRadius - The corners of the shape as a whole:
|
|
30945
|
+
* the page's, and the ones the tab row shares with it. Square when the
|
|
30946
|
+
* binder fills its container, rounded when it sits in a rounded one.
|
|
30947
|
+
* @param {number} params.tabRadius - The tabs' own corners, away from the
|
|
30948
|
+
* page, and the concave curves where a tab opens into it. Independent: tabs
|
|
30949
|
+
* can be rounded inside a square binder, and the other way round.
|
|
30950
|
+
* @param {"top"|"bottom"|"left"|"right"} params.tabsPosition
|
|
30951
|
+
* @returns {string} An SVG path `d` attribute.
|
|
30952
|
+
*/
|
|
30953
|
+
const buildBinderPath = ({
|
|
30954
|
+
tabX,
|
|
30955
|
+
tabWidth,
|
|
30956
|
+
tabRowHeight,
|
|
30957
|
+
panelWidth,
|
|
30958
|
+
panelHeight,
|
|
30959
|
+
borderWidth,
|
|
30960
|
+
outerRadius,
|
|
30961
|
+
tabRadius,
|
|
30962
|
+
gapBeforeTabs,
|
|
30963
|
+
gapAfterTabs,
|
|
30964
|
+
tabsPosition,
|
|
30965
|
+
}) => {
|
|
30966
|
+
const h = borderWidth / 2;
|
|
30967
|
+
const withoutBorder = (radius) => (radius - h < 0 ? 0 : radius - h);
|
|
30968
|
+
const r = withoutBorder(tabRadius);
|
|
30969
|
+
const rOuter = withoutBorder(outerRadius);
|
|
30970
|
+
// Page corners on the tab-row side only get rounded when the gap before /
|
|
30971
|
+
// after the tabs leaves room for the full radius.
|
|
30972
|
+
const gapAfter = gapAfterTabs < 0 ? 0 : gapAfterTabs;
|
|
30973
|
+
const rTR = gapAfter >= rOuter ? rOuter : 0;
|
|
30974
|
+
const gapBefore = gapBeforeTabs < 0 ? 0 : gapBeforeTabs;
|
|
30975
|
+
const rTL = gapBefore >= rOuter ? rOuter : 0;
|
|
30976
|
+
|
|
30977
|
+
const params = {
|
|
30978
|
+
tabX,
|
|
30979
|
+
tabWidth,
|
|
30980
|
+
tabRowHeight,
|
|
30981
|
+
panelWidth,
|
|
30982
|
+
panelHeight,
|
|
30983
|
+
h,
|
|
30984
|
+
r,
|
|
30985
|
+
rOuter,
|
|
30986
|
+
rTR,
|
|
30987
|
+
rTL,
|
|
30988
|
+
};
|
|
30989
|
+
const d =
|
|
30990
|
+
tabsPosition === "top" || tabsPosition === "left"
|
|
30991
|
+
? buildBinderCenterlinePathAbove(params)
|
|
30992
|
+
: buildBinderCenterlinePathBelow(params);
|
|
30993
|
+
const commands =
|
|
30994
|
+
tabsPosition === "left" || tabsPosition === "right" ? transposePath(d) : d;
|
|
30995
|
+
return commands.join(" ");
|
|
30996
|
+
};
|
|
30997
|
+
|
|
30998
|
+
// Builds the centerline path when tabs are ABOVE the panel.
|
|
30999
|
+
// CW path: M(tabLeft+r, top) → tab top → tab TR → [right junction] → panel TR
|
|
31000
|
+
// → panel right ↓ → panel BR → panel bottom ← → panel BL → panel left ↑ → [left junction] → tab TL → Z
|
|
31001
|
+
const buildBinderCenterlinePathAbove = ({
|
|
31002
|
+
tabX,
|
|
31003
|
+
tabWidth,
|
|
31004
|
+
tabRowHeight,
|
|
31005
|
+
panelWidth,
|
|
31006
|
+
panelHeight,
|
|
31007
|
+
h,
|
|
31008
|
+
r,
|
|
31009
|
+
rOuter,
|
|
31010
|
+
rTR,
|
|
31011
|
+
rTL,
|
|
31012
|
+
}) => {
|
|
31013
|
+
const hasRightJunction = tabX + tabWidth < panelWidth;
|
|
31014
|
+
const hasLeftJunction = tabX > 0;
|
|
31015
|
+
|
|
31016
|
+
const left = h;
|
|
31017
|
+
const top = h;
|
|
31018
|
+
const right = panelWidth - h;
|
|
31019
|
+
const bottom = tabRowHeight + panelHeight - h;
|
|
31020
|
+
const tabLeft = tabX + h;
|
|
31021
|
+
const tabRight = tabX + tabWidth - h;
|
|
31022
|
+
const junctionY = tabRowHeight - h;
|
|
31023
|
+
|
|
31024
|
+
const rVSpace = junctionY - top - r;
|
|
31025
|
+
const rTotalHSpace = right - tabRight;
|
|
31026
|
+
const rFits = hasRightJunction && rVSpace >= r && rTotalHSpace >= r + rTR;
|
|
31027
|
+
|
|
31028
|
+
const lVSpace = junctionY - top - r;
|
|
31029
|
+
const lTotalHSpace = tabLeft - left - rTL; // capped by gap left of first tab
|
|
31030
|
+
// lFits: room for panel TL arc (rTL) + concave junction arc (r)
|
|
31031
|
+
const lFits = hasLeftJunction && lVSpace >= r && lTotalHSpace >= r;
|
|
31032
|
+
// lTight: only room for panel TL arc, skip concave — favor panel rounding
|
|
31033
|
+
const lTight = hasLeftJunction && lVSpace >= r && lTotalHSpace >= 0;
|
|
31034
|
+
|
|
31035
|
+
const d = [];
|
|
31036
|
+
d.push(`M ${tabLeft + r},${top}`);
|
|
31037
|
+
d.push(`h ${tabRight - r - (tabLeft + r)}`);
|
|
31038
|
+
|
|
31039
|
+
// ── Right side ────────────────────────────────────────────────────────────
|
|
31040
|
+
if (!hasRightJunction) {
|
|
31041
|
+
d.push(arcConvex(r, r, r));
|
|
31042
|
+
d.push(`v ${bottom - top - r - rOuter}`);
|
|
31043
|
+
} else if (rFits) {
|
|
31044
|
+
d.push(arcConvex(r, r, r)); // tab TR: right→down
|
|
31045
|
+
d.push(`v ${rVSpace - r}`); // tab right ↓ to concave arc start
|
|
31046
|
+
d.push(arcConcave(r, r, r)); // junction concave: down→right
|
|
31047
|
+
d.push(`h ${rTotalHSpace - r - rTR}`); // panel top →
|
|
31048
|
+
d.push(arcConvex(rTR, rTR, rTR)); // panel TR: right→down
|
|
31049
|
+
d.push(`v ${bottom - junctionY - rTR - rOuter}`); // panel right ↓
|
|
31050
|
+
} else {
|
|
31051
|
+
d.push(arcConvex(r, r, r)); // tab TR: right→down
|
|
31052
|
+
d.push(`v ${rVSpace}`); // straight down to junctionY
|
|
31053
|
+
d.push(`h ${rTotalHSpace - rTR}`); // panel top →
|
|
31054
|
+
d.push(arcConvex(rTR, rTR, rTR)); // panel TR: right→down (clamped)
|
|
31055
|
+
d.push(`v ${bottom - junctionY - rTR - rOuter}`); // panel right ↓
|
|
31056
|
+
}
|
|
31057
|
+
|
|
31058
|
+
// ── Panel BR → bottom → BL ────────────────────────────────────────────────
|
|
31059
|
+
d.push(arcConvex(rOuter, -rOuter, rOuter));
|
|
31060
|
+
d.push(`h -${right - left - 2 * rOuter}`);
|
|
31061
|
+
d.push(arcConvex(rOuter, -rOuter, -rOuter));
|
|
31062
|
+
|
|
31063
|
+
// ── Panel left ↑ + left junction back to M ───────────────────────────────
|
|
31064
|
+
if (!hasLeftJunction) {
|
|
31065
|
+
d.push(`v -${bottom - top - rOuter - r}`);
|
|
31066
|
+
d.push(arcConvex(r, r, -r)); // panel TL = tab TL: up→right
|
|
31067
|
+
} else if (lFits) {
|
|
31068
|
+
d.push(`v -${bottom - junctionY - rOuter - rTL}`); // panel left ↑ to (left, junctionY+rTL)
|
|
31069
|
+
d.push(arcConvex(rTL, rTL, -rTL)); // panel TL: up→right
|
|
31070
|
+
d.push(`h ${lTotalHSpace - r}`); // panel top → to (tabLeft-r, junctionY)
|
|
31071
|
+
d.push(arcConcave(r, r, -r)); // junction concave: right→up
|
|
31072
|
+
d.push(`v -${lVSpace - r}`); // tab left ↑
|
|
31073
|
+
d.push(arcConvex(r, r, -r)); // tab TL: up→right
|
|
31074
|
+
} else if (lTight) {
|
|
31075
|
+
// Panel TL rounded, no concave — square tab corner
|
|
31076
|
+
d.push(`v -${bottom - junctionY - rOuter - rTL}`); // panel left ↑ to (left, junctionY+rTL)
|
|
31077
|
+
d.push(arcConvex(rTL, rTL, -rTL)); // panel TL: up→right
|
|
31078
|
+
d.push(`h ${lTotalHSpace}`); // panel top → to (tabLeft, junctionY)
|
|
31079
|
+
d.push(`v -${lVSpace}`); // tab left ↑
|
|
31080
|
+
d.push(arcConvex(r, r, -r)); // tab TL: up→right
|
|
31081
|
+
} else {
|
|
31082
|
+
d.push(`v -${bottom - junctionY - rOuter}`); // panel left ↑ to junctionY
|
|
31083
|
+
d.push(`h ${lTotalHSpace}`); // panel top →
|
|
31084
|
+
d.push(`v -${lVSpace}`); // tab left ↑
|
|
31085
|
+
d.push(arcConvex(r, r, -r)); // tab TL: up→right
|
|
31086
|
+
}
|
|
31087
|
+
|
|
31088
|
+
d.push("Z");
|
|
31089
|
+
return d;
|
|
31090
|
+
};
|
|
31091
|
+
|
|
31092
|
+
// Builds the centerline path when tabs are BELOW the panel.
|
|
31093
|
+
// CW path: M(left+r, top) → panel top → panel TR → panel right ↓ → [right junction] →
|
|
31094
|
+
// tab right ↓ → tab BR → tab bottom ← → [left junction] → tab BL → tab left ↑ → panel left ↑ → panel TL → Z
|
|
31095
|
+
//
|
|
31096
|
+
// Right junction: panel right ↓ to junctionY (square corner), left along panel bottom gap,
|
|
31097
|
+
// concave notch arc (left→down), tab right ↓.
|
|
31098
|
+
// Left junction: tab left ↑, concave notch arc (up→left), left along panel bottom gap to panel left
|
|
31099
|
+
// (square corner), panel left ↑.
|
|
31100
|
+
const buildBinderCenterlinePathBelow = ({
|
|
31101
|
+
tabX,
|
|
31102
|
+
tabWidth,
|
|
31103
|
+
tabRowHeight,
|
|
31104
|
+
panelWidth,
|
|
31105
|
+
panelHeight,
|
|
31106
|
+
h,
|
|
31107
|
+
r,
|
|
31108
|
+
rOuter,
|
|
31109
|
+
rTR,
|
|
31110
|
+
rTL,
|
|
31111
|
+
}) => {
|
|
31112
|
+
const hasRightJunction = tabX + tabWidth < panelWidth;
|
|
31113
|
+
const hasLeftJunction = tabX > 0;
|
|
31114
|
+
|
|
31115
|
+
const left = h;
|
|
31116
|
+
const top = h;
|
|
31117
|
+
const right = panelWidth - h;
|
|
31118
|
+
const bottom = panelHeight + tabRowHeight - h;
|
|
31119
|
+
const tabLeft = tabX + h;
|
|
31120
|
+
const tabRight = tabX + tabWidth - h;
|
|
31121
|
+
const junctionY = panelHeight - h;
|
|
31122
|
+
|
|
31123
|
+
// Vertical room on the page's right/left sides, between its top corner
|
|
31124
|
+
// (outer) and the junction.
|
|
31125
|
+
const rPanelVSpace = junctionY - top - rOuter;
|
|
31126
|
+
|
|
31127
|
+
// Right junction: the page corner beside the last tab uses rTR.
|
|
31128
|
+
const rTotalHSpace = right - tabRight - rTR;
|
|
31129
|
+
// rFits: room for the page corner arc (rTR) + the concave junction arc (r)
|
|
31130
|
+
const rFits = hasRightJunction && rPanelVSpace >= r && rTotalHSpace >= r;
|
|
31131
|
+
// rTight: only room for the page corner arc, skip concave — favor the page
|
|
31132
|
+
const rTight = hasRightJunction && rPanelVSpace >= r && rTotalHSpace >= 0;
|
|
31133
|
+
|
|
31134
|
+
// Left junction: the page corner beside the first tab uses rTL.
|
|
31135
|
+
const lTotalHSpace = tabLeft - left - rTL;
|
|
31136
|
+
const lFits = hasLeftJunction && rPanelVSpace >= r && lTotalHSpace >= r;
|
|
31137
|
+
const lTight = hasLeftJunction && rPanelVSpace >= r && lTotalHSpace >= 0;
|
|
31138
|
+
|
|
31139
|
+
const d = [];
|
|
31140
|
+
|
|
31141
|
+
// M: start after the page's TL arc, going right along the page top
|
|
31142
|
+
d.push(`M ${left + rOuter},${top}`);
|
|
31143
|
+
d.push(`h ${right - left - 2 * rOuter}`); // page top →
|
|
31144
|
+
|
|
31145
|
+
// ── Right side: page right ↓ then (junction or direct) tab right ↓ ──────
|
|
31146
|
+
if (!hasRightJunction) {
|
|
31147
|
+
d.push(arcConvex(rOuter, rOuter, rOuter)); // page TR: right→down
|
|
31148
|
+
d.push(`v ${bottom - top - rOuter - r}`); // full right side ↓
|
|
31149
|
+
d.push(arcConvex(r, -r, r)); // tab BR: down→left
|
|
31150
|
+
} else if (rFits) {
|
|
31151
|
+
d.push(arcConvex(rOuter, rOuter, rOuter)); // page TR: right→down
|
|
31152
|
+
d.push(`v ${rPanelVSpace - rTR}`); // page right ↓ to (right, junctionY-rTR)
|
|
31153
|
+
d.push(arcConvex(rTR, -rTR, rTR)); // page corner: down→left
|
|
31154
|
+
d.push(`h -${rTotalHSpace - r}`); // page bottom ← to (tabRight+r, junctionY)
|
|
31155
|
+
d.push(arcConcave(r, -r, r)); // junction concave: left→down
|
|
31156
|
+
d.push(`v ${bottom - junctionY - 2 * r}`); // tab right ↓ to (tabRight, bottom-r)
|
|
31157
|
+
d.push(arcConvex(r, -r, r)); // tab BR: down→left
|
|
31158
|
+
} else if (rTight) {
|
|
31159
|
+
// Page corner rounded, no concave — square tab corner
|
|
31160
|
+
d.push(arcConvex(rOuter, rOuter, rOuter)); // page TR: right→down
|
|
31161
|
+
d.push(`v ${rPanelVSpace - rTR}`); // page right ↓ to (right, junctionY-rTR)
|
|
31162
|
+
d.push(arcConvex(rTR, -rTR, rTR)); // page corner: down→left
|
|
31163
|
+
d.push(`h -${rTotalHSpace}`); // page bottom ← to (tabRight, junctionY)
|
|
31164
|
+
d.push(`v ${bottom - junctionY - r}`); // tab right ↓ to (tabRight, bottom-r)
|
|
31165
|
+
d.push(arcConvex(r, -r, r)); // tab BR: down→left
|
|
31166
|
+
} else {
|
|
31167
|
+
// Very tight: all square corners
|
|
31168
|
+
d.push(arcConvex(rOuter, rOuter, rOuter)); // page TR: right→down
|
|
31169
|
+
d.push(`v ${rPanelVSpace}`); // page right ↓ to junctionY
|
|
31170
|
+
d.push(`h -${right - tabRight}`); // page bottom ← to tabRight
|
|
31171
|
+
d.push(`v ${bottom - junctionY - r}`); // tab right ↓ to (tabRight, bottom-r)
|
|
31172
|
+
d.push(arcConvex(r, -r, r)); // tab BR: down→left
|
|
31173
|
+
}
|
|
31174
|
+
|
|
31175
|
+
// ── Tab bottom ← ──────────────────────────────────────────────────────────
|
|
31176
|
+
d.push(`h -${tabRight - r - (tabLeft + r)}`); // tab bottom ←
|
|
31177
|
+
|
|
31178
|
+
// ── Left side: tab BL → tab left ↑ → (junction or direct) → page left ↑ ──
|
|
31179
|
+
if (!hasLeftJunction) {
|
|
31180
|
+
d.push(arcConvex(r, -r, -r)); // tab BL: left→up
|
|
31181
|
+
d.push(`v -${bottom - top - rOuter - r}`); // full left side ↑
|
|
31182
|
+
d.push(arcConvex(rOuter, rOuter, -rOuter)); // page TL: up→right → M
|
|
31183
|
+
} else if (lFits) {
|
|
31184
|
+
d.push(arcConvex(r, -r, -r)); // tab BL: left→up
|
|
31185
|
+
d.push(`v -${bottom - junctionY - 2 * r}`); // tab left ↑ to (tabLeft, junctionY+r)
|
|
31186
|
+
d.push(arcConcave(r, -r, -r)); // junction concave: up→left
|
|
31187
|
+
d.push(`h -${lTotalHSpace - r}`); // page bottom ← to (left+rTL, junctionY)
|
|
31188
|
+
d.push(arcConvex(rTL, -rTL, -rTL)); // page corner: left→up
|
|
31189
|
+
d.push(`v -${rPanelVSpace - rTL}`); // page left ↑ to (left, top+rOuter)
|
|
31190
|
+
d.push(arcConvex(rOuter, rOuter, -rOuter)); // page TL: up→right → M
|
|
31191
|
+
} else if (lTight) {
|
|
31192
|
+
// Page corner rounded, no concave — square tab corner
|
|
31193
|
+
d.push(arcConvex(r, -r, -r)); // tab BL: left→up
|
|
31194
|
+
d.push(`v -${bottom - junctionY - r}`); // tab left ↑ to (tabLeft, junctionY)
|
|
31195
|
+
d.push(`h -${lTotalHSpace}`); // page bottom ← to (left+rTL, junctionY)
|
|
31196
|
+
d.push(arcConvex(rTL, -rTL, -rTL)); // page corner: left→up
|
|
31197
|
+
d.push(`v -${rPanelVSpace - rTL}`); // page left ↑ to (left, top+rOuter)
|
|
31198
|
+
d.push(arcConvex(rOuter, rOuter, -rOuter)); // page TL: up→right → M
|
|
31199
|
+
} else {
|
|
31200
|
+
// Very tight: all square corners
|
|
31201
|
+
d.push(arcConvex(r, -r, -r)); // tab BL: left→up
|
|
31202
|
+
d.push(`v -${bottom - junctionY - r}`); // tab left ↑ to junctionY
|
|
31203
|
+
d.push(`h -${tabLeft - left}`); // page bottom ← to left
|
|
31204
|
+
d.push(`v -${rPanelVSpace}`); // page left ↑ to (left, top+rOuter)
|
|
31205
|
+
d.push(arcConvex(rOuter, rOuter, -rOuter)); // page TL: up→right → M
|
|
31206
|
+
}
|
|
31207
|
+
|
|
31208
|
+
d.push("Z");
|
|
31209
|
+
return d;
|
|
31210
|
+
};
|
|
31211
|
+
|
|
31212
|
+
installImportMetaCssBuild(import.meta);/**
|
|
31213
|
+
* A binder: tabs and the page they open, drawn as one shape.
|
|
31214
|
+
*
|
|
31215
|
+
* Unlike a `Nav`, a binder owns its content — the tab and the page it shows are
|
|
31216
|
+
* the same item, declared together:
|
|
31217
|
+
*
|
|
31218
|
+
* <Binder>
|
|
31219
|
+
* <Binder.Item value="a" label="Approved">…the page…</Binder.Item>
|
|
31220
|
+
* </Binder>
|
|
31221
|
+
*
|
|
31222
|
+
* Layers, and why they are what they are:
|
|
31223
|
+
* - The outline (tab row + page, with the concave junctions around the current
|
|
31224
|
+
* tab) is a single SVG path — see binder_path.js for why CSS borders cannot
|
|
31225
|
+
* draw it. It covers the whole binder and sits above the inactive tabs but
|
|
31226
|
+
* below the current tab and the page.
|
|
31227
|
+
* - The inactive tabs keep plain CSS borders: the path only runs along the
|
|
31228
|
+
* current tab, so the top of an inactive tab is its own to draw. Their border
|
|
31229
|
+
* width matches the path's stroke and lands in the same band, which is what
|
|
31230
|
+
* makes the two read as one continuous line.
|
|
31231
|
+
* - Every tab reserves that band on the page side with a margin, so the stroke
|
|
31232
|
+
* never lands on a tab's own edge.
|
|
31233
|
+
*
|
|
31234
|
+
* Border width participates in layout (it is added to the tab and page
|
|
31235
|
+
* padding): a thick border grows the binder rather than eating into the text.
|
|
31236
|
+
*/
|
|
31237
|
+
const css$N = /* css */`
|
|
31238
|
+
@layer navi {
|
|
31239
|
+
.navi_binder {
|
|
31240
|
+
--binder-border-width: var(--navi-control-border-width);
|
|
31241
|
+
--binder-border-radius: var(--navi-control-border-radius);
|
|
31242
|
+
/* The tabs' own corners. Their own knob because the two answer different
|
|
31243
|
+
questions: the outer radius follows the container the binder sits in
|
|
31244
|
+
(square when it fills the page), the tab radius is the tabs' shape. */
|
|
31245
|
+
--binder-tab-border-radius: var(--binder-border-radius);
|
|
31246
|
+
--binder-border-color: var(--navi-control-border-color);
|
|
31247
|
+
--binder-background: var(--navi-surface-color);
|
|
31248
|
+
/* The tabs that are not open read as the same paper, slightly shaded:
|
|
31249
|
+
derived from the border color so one override themes both, in light
|
|
31250
|
+
and dark alike. */
|
|
31251
|
+
--binder-tab-background: color-mix(
|
|
31252
|
+
in srgb,
|
|
31253
|
+
var(--binder-border-color) 12%,
|
|
31254
|
+
var(--binder-background)
|
|
31255
|
+
);
|
|
31256
|
+
--binder-tab-border-color: var(--binder-border-color);
|
|
31257
|
+
--binder-tab-color: var(--navi-color-secondary);
|
|
31258
|
+
--binder-tab-background-hover: color-mix(
|
|
31259
|
+
in srgb,
|
|
31260
|
+
var(--binder-border-color) 22%,
|
|
31261
|
+
var(--binder-background)
|
|
31262
|
+
);
|
|
31263
|
+
--binder-padding-x: var(--navi-s);
|
|
31264
|
+
--binder-padding-y: var(--navi-xs);
|
|
31265
|
+
--binder-page-padding: var(--navi-m);
|
|
31266
|
+
}
|
|
31267
|
+
}
|
|
31268
|
+
|
|
31269
|
+
.navi_binder {
|
|
31270
|
+
position: relative;
|
|
31271
|
+
display: flex;
|
|
31272
|
+
width: fit-content;
|
|
31273
|
+
align-items: stretch;
|
|
31274
|
+
|
|
31275
|
+
&[data-tabs-position="top"] {
|
|
31276
|
+
flex-direction: column;
|
|
31277
|
+
}
|
|
31278
|
+
&[data-tabs-position="bottom"] {
|
|
31279
|
+
flex-direction: column-reverse;
|
|
31280
|
+
}
|
|
31281
|
+
&[data-tabs-position="left"] {
|
|
31282
|
+
flex-direction: row;
|
|
31283
|
+
}
|
|
31284
|
+
&[data-tabs-position="right"] {
|
|
31285
|
+
flex-direction: row-reverse;
|
|
31286
|
+
}
|
|
31287
|
+
}
|
|
31288
|
+
|
|
31289
|
+
.navi_binder_outline {
|
|
31290
|
+
position: absolute;
|
|
31291
|
+
inset: 0;
|
|
31292
|
+
z-index: 1;
|
|
31293
|
+
pointer-events: none;
|
|
31294
|
+
overflow: visible;
|
|
31295
|
+
}
|
|
31296
|
+
|
|
31297
|
+
.navi_binder_tabs {
|
|
31298
|
+
display: flex;
|
|
31299
|
+
/* The page decides how wide the binder is; the tab row only fills it.
|
|
31300
|
+
Sizing it to 0 keeps its labels out of the binder's intrinsic size (a
|
|
31301
|
+
long one would otherwise stretch the whole thing) while min-size brings
|
|
31302
|
+
it back to the width the page settled on — which is what leaves the
|
|
31303
|
+
labels something to truncate against. */
|
|
31304
|
+
.navi_binder[data-tabs-position="top"] &,
|
|
31305
|
+
.navi_binder[data-tabs-position="bottom"] & {
|
|
31306
|
+
width: 0;
|
|
31307
|
+
min-width: 100%;
|
|
31308
|
+
}
|
|
31309
|
+
.navi_binder[data-tabs-position="left"] &,
|
|
31310
|
+
.navi_binder[data-tabs-position="right"] & {
|
|
31311
|
+
height: 0;
|
|
31312
|
+
min-height: 100%;
|
|
31313
|
+
}
|
|
31314
|
+
/* Tabs of unequal size line up on the page side, so the junction band is
|
|
31315
|
+
one straight line whatever their content. */
|
|
31316
|
+
.navi_binder[data-tabs-position="top"] & {
|
|
31317
|
+
align-items: flex-end;
|
|
31318
|
+
}
|
|
31319
|
+
.navi_binder[data-tabs-position="bottom"] & {
|
|
31320
|
+
align-items: flex-start;
|
|
31321
|
+
}
|
|
31322
|
+
/* A column of tabs is as wide as its widest one, all of them that wide, so
|
|
31323
|
+
the junction band is one straight line here too. */
|
|
31324
|
+
.navi_binder[data-tabs-position="left"] &,
|
|
31325
|
+
.navi_binder[data-tabs-position="right"] & {
|
|
31326
|
+
flex-direction: column;
|
|
31327
|
+
align-items: stretch;
|
|
31328
|
+
}
|
|
31329
|
+
}
|
|
31330
|
+
|
|
31331
|
+
.navi_binder_tab {
|
|
31332
|
+
position: relative;
|
|
31333
|
+
z-index: 0;
|
|
31334
|
+
/* A flex item refuses to go under its content unless told to; without this
|
|
31335
|
+
a long label widens the tab row instead of truncating. */
|
|
31336
|
+
min-width: 0;
|
|
31337
|
+
min-height: 0;
|
|
31338
|
+
/* The border is part of the tab's box on every tab, current one included
|
|
31339
|
+
(transparent there — the path draws it), so opening a tab never moves
|
|
31340
|
+
anything. */
|
|
31341
|
+
padding: var(--binder-padding-y) var(--binder-padding-x);
|
|
31342
|
+
flex: 0 1 auto;
|
|
31343
|
+
color: var(--binder-tab-color);
|
|
31344
|
+
font: inherit;
|
|
31345
|
+
text-align: center;
|
|
31346
|
+
text-decoration: none;
|
|
31347
|
+
background: var(--binder-tab-background);
|
|
31348
|
+
border: var(--binder-border-width) solid var(--binder-tab-border-color);
|
|
31349
|
+
cursor: pointer;
|
|
31350
|
+
/* "stretch" is an equal share each, whatever a tab holds — a basis read
|
|
31351
|
+
from the content would hand the longest label the widest tab. */
|
|
31352
|
+
.navi_binder[data-tabs-align="stretch"] & {
|
|
31353
|
+
flex: 1 1 0;
|
|
31354
|
+
}
|
|
31355
|
+
|
|
31356
|
+
/* The navi attributes rather than the CSS pseudo-classes: they are what a
|
|
31357
|
+
demo can hold with pseudoState={{ ":focus-visible": true }}. */
|
|
31358
|
+
&[data-hover] {
|
|
31359
|
+
background: var(--binder-tab-background-hover);
|
|
31360
|
+
}
|
|
31361
|
+
/* The ring would be cut by the neighbours and by the outline band around
|
|
31362
|
+
it, so it is drawn inside and lifted above them. */
|
|
31363
|
+
&[data-focus-visible] {
|
|
31364
|
+
z-index: 3;
|
|
31365
|
+
outline: var(--navi-focus-outline-width) solid
|
|
31366
|
+
var(--navi-focus-outline-color);
|
|
31367
|
+
outline-offset: calc(-1 * var(--navi-focus-outline-width));
|
|
31368
|
+
}
|
|
31369
|
+
&[data-current] {
|
|
31370
|
+
z-index: 2;
|
|
31371
|
+
color: inherit;
|
|
31372
|
+
background: transparent;
|
|
31373
|
+
border-color: transparent;
|
|
31374
|
+
cursor: default;
|
|
31375
|
+
}
|
|
31376
|
+
/* Adjacent tabs share one line rather than stacking two borders. */
|
|
31377
|
+
&:not(:first-child) {
|
|
31378
|
+
margin-inline-start: calc(-1 * var(--binder-border-width));
|
|
31379
|
+
}
|
|
31380
|
+
|
|
31381
|
+
/* The margin on the page side is the junction band the path draws in; the
|
|
31382
|
+
corners away from the page are the ones that round. */
|
|
31383
|
+
.navi_binder[data-tabs-position="top"] & {
|
|
31384
|
+
margin-bottom: var(--binder-border-width);
|
|
31385
|
+
border-bottom: none;
|
|
31386
|
+
border-radius: var(--binder-tab-border-radius)
|
|
31387
|
+
var(--binder-tab-border-radius) 0 0;
|
|
31388
|
+
}
|
|
31389
|
+
.navi_binder[data-tabs-position="bottom"] & {
|
|
31390
|
+
margin-top: var(--binder-border-width);
|
|
31391
|
+
border-top: none;
|
|
31392
|
+
border-radius: 0 0 var(--binder-tab-border-radius)
|
|
31393
|
+
var(--binder-tab-border-radius);
|
|
31394
|
+
}
|
|
31395
|
+
.navi_binder[data-tabs-position="left"] & {
|
|
31396
|
+
margin-right: var(--binder-border-width);
|
|
31397
|
+
border-right: none;
|
|
31398
|
+
border-radius: var(--binder-tab-border-radius) 0 0
|
|
31399
|
+
var(--binder-tab-border-radius);
|
|
31400
|
+
}
|
|
31401
|
+
.navi_binder[data-tabs-position="right"] & {
|
|
31402
|
+
margin-left: var(--binder-border-width);
|
|
31403
|
+
border-left: none;
|
|
31404
|
+
border-radius: 0 var(--binder-tab-border-radius)
|
|
31405
|
+
var(--binder-tab-border-radius) 0;
|
|
31406
|
+
}
|
|
31407
|
+
/* A column of tabs shares its main axis with the block direction, where
|
|
31408
|
+
margin-inline-start would collapse the wrong pair. */
|
|
31409
|
+
.navi_binder[data-tabs-position="left"] &:not(:first-child),
|
|
31410
|
+
.navi_binder[data-tabs-position="right"] &:not(:first-child) {
|
|
31411
|
+
margin-inline-start: 0;
|
|
31412
|
+
margin-top: calc(-1 * var(--binder-border-width));
|
|
31413
|
+
}
|
|
31414
|
+
}
|
|
31415
|
+
|
|
31416
|
+
/* A tab that grows with its label would push the binder wider than the page
|
|
31417
|
+
it opens; by default a label stays on one line and is cut with an ellipsis.
|
|
31418
|
+
maxLines={n} lets it use n lines, maxLines={false} lets it wrap freely. */
|
|
31419
|
+
.navi_binder_tab_label {
|
|
31420
|
+
display: block;
|
|
31421
|
+
|
|
31422
|
+
&[data-max-lines="1"] {
|
|
31423
|
+
text-overflow: ellipsis;
|
|
31424
|
+
white-space: nowrap;
|
|
31425
|
+
overflow: hidden;
|
|
31426
|
+
}
|
|
31427
|
+
&[data-max-lines]:not([data-max-lines="1"]) {
|
|
31428
|
+
display: -webkit-box;
|
|
31429
|
+
overflow: hidden;
|
|
31430
|
+
-webkit-box-orient: vertical;
|
|
31431
|
+
-webkit-line-clamp: var(--binder-tab-max-lines);
|
|
31432
|
+
}
|
|
31433
|
+
}
|
|
31434
|
+
|
|
31435
|
+
.navi_binder_page {
|
|
31436
|
+
position: relative;
|
|
31437
|
+
z-index: 2;
|
|
31438
|
+
/* Same reservation as the tabs, on the three sides the path runs along. */
|
|
31439
|
+
padding: calc(var(--binder-page-padding) + var(--binder-border-width));
|
|
31440
|
+
flex: 1;
|
|
31441
|
+
color: inherit;
|
|
31442
|
+
|
|
31443
|
+
/* An app shell: the binder is the window, the page is what scrolls in it,
|
|
31444
|
+
and the tab row stays put without being fixed — it is simply the part of
|
|
31445
|
+
the binder that does not scroll. min-size:0 because a flex item refuses
|
|
31446
|
+
to shrink under its content, which is what makes overflow do nothing. */
|
|
31447
|
+
.navi_binder[data-scrollable-page] & {
|
|
31448
|
+
min-width: 0;
|
|
31449
|
+
min-height: 0;
|
|
31450
|
+
overflow: auto;
|
|
31451
|
+
}
|
|
31452
|
+
|
|
31453
|
+
.navi_binder[data-tabs-position="top"] & {
|
|
31454
|
+
padding-top: var(--binder-page-padding);
|
|
31455
|
+
}
|
|
31456
|
+
.navi_binder[data-tabs-position="bottom"] & {
|
|
31457
|
+
padding-bottom: var(--binder-page-padding);
|
|
31458
|
+
}
|
|
31459
|
+
.navi_binder[data-tabs-position="left"] & {
|
|
31460
|
+
padding-left: var(--binder-page-padding);
|
|
31461
|
+
}
|
|
31462
|
+
.navi_binder[data-tabs-position="right"] & {
|
|
31463
|
+
padding-right: var(--binder-page-padding);
|
|
31464
|
+
}
|
|
31465
|
+
}
|
|
31466
|
+
`;
|
|
31467
|
+
const BinderStyleCSSVars = {
|
|
31468
|
+
borderWidth: "--binder-border-width",
|
|
31469
|
+
borderRadius: "--binder-border-radius",
|
|
31470
|
+
tabBorderRadius: "--binder-tab-border-radius",
|
|
31471
|
+
borderColor: "--binder-border-color",
|
|
31472
|
+
background: "--binder-background",
|
|
31473
|
+
tabBackground: "--binder-tab-background",
|
|
31474
|
+
tabBackgroundHover: "--binder-tab-background-hover",
|
|
31475
|
+
tabBorderColor: "--binder-tab-border-color",
|
|
31476
|
+
tabColor: "--binder-tab-color",
|
|
31477
|
+
paddingX: "--binder-padding-x",
|
|
31478
|
+
paddingY: "--binder-padding-y",
|
|
31479
|
+
pagePadding: "--binder-page-padding"
|
|
31480
|
+
};
|
|
31481
|
+
const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
|
|
31482
|
+
start: "flex-start",
|
|
31483
|
+
center: "center",
|
|
31484
|
+
end: "flex-end",
|
|
31485
|
+
stretch: "stretch"
|
|
31486
|
+
};
|
|
31487
|
+
|
|
31488
|
+
/**
|
|
31489
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
31490
|
+
* value?: any,
|
|
31491
|
+
* defaultValue?: any,
|
|
31492
|
+
* onChange?: (value: any) => void,
|
|
31493
|
+
* tabsPosition?: "top"|"bottom"|"left"|"right",
|
|
31494
|
+
* tabsAlign?: "start"|"center"|"end"|"stretch",
|
|
31495
|
+
* borderWidth?: string|number,
|
|
31496
|
+
* borderRadius?: string|number,
|
|
31497
|
+
* tabBorderRadius?: string|number,
|
|
31498
|
+
* borderColor?: string,
|
|
31499
|
+
* background?: string,
|
|
31500
|
+
* tabBackground?: string,
|
|
31501
|
+
* tabBackgroundHover?: string,
|
|
31502
|
+
* tabBorderColor?: string,
|
|
31503
|
+
* tabColor?: string,
|
|
31504
|
+
* paddingX?: string|number,
|
|
31505
|
+
* paddingY?: string|number,
|
|
31506
|
+
* pagePadding?: string|number,
|
|
31507
|
+
* }>}
|
|
31508
|
+
* @param {any} [props.value] - The open item, when the caller keeps that state.
|
|
31509
|
+
* Leave it out for a binder that keeps its own (see `defaultValue`).
|
|
31510
|
+
* @param {any} [props.defaultValue] - Which item is open to begin with;
|
|
31511
|
+
* defaults to the first one.
|
|
31512
|
+
* @param {"top"|"bottom"|"left"|"right"} [props.tabsPosition="top"] - Which
|
|
31513
|
+
* side of the page the tabs sit on.
|
|
31514
|
+
* @param {"start"|"center"|"end"|"stretch"} [props.tabsAlign="stretch"] - How
|
|
31515
|
+
* the tabs spread along that side.
|
|
31516
|
+
* @param {boolean} [props.scrollablePage] - Makes the page scroll inside the
|
|
31517
|
+
* binder instead of growing it. What an app shell wants: give the binder the
|
|
31518
|
+
* size of the window and the tab row stays in place while the page scrolls
|
|
31519
|
+
* under it — no `position: fixed` anywhere, so the tabs and the page are
|
|
31520
|
+
* still one shape and the junction still merges them. The trade is that the
|
|
31521
|
+
* document itself no longer scrolls.
|
|
31522
|
+
* @param {number|false} [props.maxLines=1] - How many lines a tab label may
|
|
31523
|
+
* use before being cut with an ellipsis; `false` lets it wrap freely. A tab
|
|
31524
|
+
* is never allowed to widen the binder past the page it opens, so the
|
|
31525
|
+
* default keeps every label on one line. Overridable per item.
|
|
31526
|
+
* @param {string|number} [props.borderWidth] - Thickness of the whole outline,
|
|
31527
|
+
* the inactive tabs included. It is added to the paddings, so a thick border
|
|
31528
|
+
* grows the binder instead of closing in on the text.
|
|
31529
|
+
* @param {string} [props.tabBorderColor] - The inactive tabs alone; the rest of
|
|
31530
|
+
* the outline follows `borderColor`.
|
|
31531
|
+
* @param {import("ignore:preact").ComponentChildren} props.children - `Binder.Item`s.
|
|
31532
|
+
*/
|
|
31533
|
+
const Binder = ({
|
|
31534
|
+
children,
|
|
31535
|
+
value,
|
|
31536
|
+
defaultValue,
|
|
31537
|
+
onChange,
|
|
31538
|
+
tabsPosition = "top",
|
|
31539
|
+
tabsAlign = "stretch",
|
|
31540
|
+
maxLines = 1,
|
|
31541
|
+
scrollablePage,
|
|
31542
|
+
borderWidth,
|
|
31543
|
+
borderRadius,
|
|
31544
|
+
tabBorderRadius,
|
|
31545
|
+
paddingX,
|
|
31546
|
+
paddingY,
|
|
31547
|
+
pagePadding,
|
|
31548
|
+
...props
|
|
31549
|
+
}) => {
|
|
31550
|
+
import.meta.css = [css$N, "@jsenv/navi/src/nav/binder/binder.jsx"];
|
|
31551
|
+
const items = toChildArray(children).map((child, index) => {
|
|
31552
|
+
const {
|
|
31553
|
+
value: itemValue,
|
|
31554
|
+
label,
|
|
31555
|
+
children: content,
|
|
31556
|
+
...tabProps
|
|
31557
|
+
} = child.props;
|
|
31558
|
+
return {
|
|
31559
|
+
value: itemValue === undefined ? index : itemValue,
|
|
31560
|
+
label,
|
|
31561
|
+
content,
|
|
31562
|
+
// Everything else belongs to the tab: an item is how one styles and
|
|
31563
|
+
// configures the control that opens it.
|
|
31564
|
+
tabProps
|
|
31565
|
+
};
|
|
31566
|
+
});
|
|
31567
|
+
const [valueOwnState, setValueOwnState] = useState(defaultValue === undefined ? items[0]?.value : defaultValue);
|
|
31568
|
+
// A tab holding a Link knows it is the current one (its route matches) before
|
|
31569
|
+
// the binder could work it out, and says so — see BinderItemContext.
|
|
31570
|
+
const [reportedValue, setReportedValue] = useState(undefined);
|
|
31571
|
+
let wantedValue;
|
|
31572
|
+
if (value !== undefined) {
|
|
31573
|
+
wantedValue = value;
|
|
31574
|
+
} else if (reportedValue !== undefined) {
|
|
31575
|
+
wantedValue = reportedValue;
|
|
31576
|
+
} else {
|
|
31577
|
+
wantedValue = valueOwnState;
|
|
31578
|
+
}
|
|
31579
|
+
const currentIndex = items.findIndex(item => item.value === wantedValue);
|
|
31580
|
+
const currentItem = currentIndex === -1 ? items[0] : items[currentIndex];
|
|
31581
|
+
const currentValue = currentItem?.value;
|
|
31582
|
+
const open = item => {
|
|
31583
|
+
if (value === undefined) {
|
|
31584
|
+
setValueOwnState(item.value);
|
|
31585
|
+
}
|
|
31586
|
+
onChange?.(item.value);
|
|
31587
|
+
};
|
|
31588
|
+
const reportCurrent = (item, current) => {
|
|
31589
|
+
setReportedValue(previous => {
|
|
31590
|
+
if (current) {
|
|
31591
|
+
return item.value;
|
|
31592
|
+
}
|
|
31593
|
+
// Only the tab that claimed it may withdraw it, otherwise the tabs that
|
|
31594
|
+
// just stopped being current would erase the one that just became it.
|
|
31595
|
+
return previous === item.value ? undefined : previous;
|
|
31596
|
+
});
|
|
31597
|
+
};
|
|
31598
|
+
const vertical = tabsPosition === "left" || tabsPosition === "right";
|
|
31599
|
+
const tabsFirst = tabsPosition === "top" || tabsPosition === "left";
|
|
31600
|
+
const rootRef = useRef(null);
|
|
31601
|
+
const tabRefs = useRef([]);
|
|
31602
|
+
const onKeyDown = event => {
|
|
31603
|
+
const {
|
|
31604
|
+
key
|
|
31605
|
+
} = event;
|
|
31606
|
+
const previousKey = vertical ? "ArrowUp" : "ArrowLeft";
|
|
31607
|
+
const nextKey = vertical ? "ArrowDown" : "ArrowRight";
|
|
31608
|
+
let nextIndex;
|
|
31609
|
+
if (key === previousKey) {
|
|
31610
|
+
nextIndex = currentIndex <= 0 ? items.length - 1 : currentIndex - 1;
|
|
31611
|
+
} else if (key === nextKey) {
|
|
31612
|
+
nextIndex = currentIndex >= items.length - 1 ? 0 : currentIndex + 1;
|
|
31613
|
+
} else if (key === "Home") {
|
|
31614
|
+
nextIndex = 0;
|
|
31615
|
+
} else if (key === "End") {
|
|
31616
|
+
nextIndex = items.length - 1;
|
|
31617
|
+
} else {
|
|
31618
|
+
return;
|
|
31619
|
+
}
|
|
31620
|
+
event.preventDefault();
|
|
31621
|
+
open(items[nextIndex]);
|
|
31622
|
+
tabRefs.current[nextIndex]?.focus();
|
|
31623
|
+
};
|
|
31624
|
+
return jsxs(Box, {
|
|
31625
|
+
ref: rootRef,
|
|
31626
|
+
baseClassName: "navi_binder",
|
|
31627
|
+
"data-tabs-position": tabsPosition,
|
|
31628
|
+
"data-tabs-align": tabsAlign,
|
|
31629
|
+
"data-scrollable-page": scrollablePage ? "" : undefined,
|
|
31630
|
+
borderWidth: withPixelUnit(borderWidth),
|
|
31631
|
+
borderRadius: withPixelUnit(borderRadius),
|
|
31632
|
+
tabBorderRadius: withPixelUnit(tabBorderRadius),
|
|
31633
|
+
paddingX: withPixelUnit(paddingX),
|
|
31634
|
+
paddingY: withPixelUnit(paddingY),
|
|
31635
|
+
pagePadding: withPixelUnit(pagePadding),
|
|
31636
|
+
...props,
|
|
31637
|
+
styleCSSVars: BinderStyleCSSVars,
|
|
31638
|
+
children: [jsx(BinderOutline, {
|
|
31639
|
+
rootRef: rootRef,
|
|
31640
|
+
currentValue: currentValue,
|
|
31641
|
+
tabsPosition: tabsPosition,
|
|
31642
|
+
vertical: vertical,
|
|
31643
|
+
tabsFirst: tabsFirst
|
|
31644
|
+
}), jsx("div", {
|
|
31645
|
+
className: "navi_binder_tabs",
|
|
31646
|
+
role: "tablist",
|
|
31647
|
+
"aria-orientation": vertical ? "vertical" : "horizontal",
|
|
31648
|
+
style: {
|
|
31649
|
+
justifyContent: TABS_ALIGN_TO_JUSTIFY_CONTENT[tabsAlign]
|
|
31650
|
+
},
|
|
31651
|
+
children: items.map((item, index) => jsx(BinderTab, {
|
|
31652
|
+
item: item,
|
|
31653
|
+
index: index,
|
|
31654
|
+
tabRefs: tabRefs,
|
|
31655
|
+
current: item === currentItem,
|
|
31656
|
+
maxLines: maxLines,
|
|
31657
|
+
onOpen: () => open(item),
|
|
31658
|
+
onKeyDown: onKeyDown,
|
|
31659
|
+
onReportCurrent: current => reportCurrent(item, current)
|
|
31660
|
+
}, item.value))
|
|
31661
|
+
}), jsx("div", {
|
|
31662
|
+
className: "navi_binder_page",
|
|
31663
|
+
role: "tabpanel",
|
|
31664
|
+
children: currentItem?.content
|
|
31665
|
+
})]
|
|
31666
|
+
});
|
|
31667
|
+
};
|
|
31668
|
+
|
|
31669
|
+
/**
|
|
31670
|
+
* @param {any} [props.value] - What identifies this item; defaults to its
|
|
31671
|
+
* position among its siblings.
|
|
31672
|
+
* @param {import("ignore:preact").ComponentChildren} [props.label] - What the tab
|
|
31673
|
+
* shows.
|
|
31674
|
+
* @param {import("../route.js").Route} [props.route] - Makes the tab a `Link`
|
|
31675
|
+
* to that route, and the binder follows the url: the open item is the one
|
|
31676
|
+
* whose route matches, no `value`/`onChange` needed.
|
|
31677
|
+
* @param {string} [props.href] - Same, for a plain url.
|
|
31678
|
+
* @param {import("ignore:preact").ComponentChildren} [props.children] - The page.
|
|
31679
|
+
*
|
|
31680
|
+
* Every other prop goes to the tab (a `Box`, or a `Link` when routed): an item
|
|
31681
|
+
* styles and configures the control that opens it.
|
|
31682
|
+
*/
|
|
31683
|
+
// Read by Binder, never rendered on its own: an item declares a tab AND its
|
|
31684
|
+
// page, and the binder needs both before it renders either.
|
|
31685
|
+
const BinderItem = () => null;
|
|
31686
|
+
Binder.Item = BinderItem;
|
|
31687
|
+
const BINDER_TAB_PSEUDO_CLASSES = [":hover", ":active", ":focus-visible"];
|
|
31688
|
+
const BinderTab = ({
|
|
31689
|
+
item,
|
|
31690
|
+
index,
|
|
31691
|
+
tabRefs,
|
|
31692
|
+
current,
|
|
31693
|
+
maxLines,
|
|
31694
|
+
onOpen,
|
|
31695
|
+
onKeyDown,
|
|
31696
|
+
onReportCurrent
|
|
31697
|
+
}) => {
|
|
31698
|
+
const {
|
|
31699
|
+
label,
|
|
31700
|
+
tabProps
|
|
31701
|
+
} = item;
|
|
31702
|
+
const {
|
|
31703
|
+
maxLines: itemMaxLines = maxLines,
|
|
31704
|
+
onClick,
|
|
31705
|
+
...rest
|
|
31706
|
+
} = tabProps;
|
|
31707
|
+
return jsx(Box, {
|
|
31708
|
+
as: "button",
|
|
31709
|
+
type: "button",
|
|
31710
|
+
baseClassName: "navi_binder_tab",
|
|
31711
|
+
pseudoClasses: BINDER_TAB_PSEUDO_CLASSES,
|
|
31712
|
+
ref: element => {
|
|
31713
|
+
tabRefs.current[index] = element;
|
|
31714
|
+
},
|
|
31715
|
+
role: "tab",
|
|
31716
|
+
"aria-selected": current,
|
|
31717
|
+
tabIndex: current ? 0 : -1,
|
|
31718
|
+
"data-current": current ? "" : undefined,
|
|
31719
|
+
onClick: event => {
|
|
31720
|
+
onOpen();
|
|
31721
|
+
onClick?.(event);
|
|
31722
|
+
},
|
|
31723
|
+
onKeyDown: onKeyDown,
|
|
31724
|
+
...rest,
|
|
31725
|
+
children: jsx(BinderItemContext.Provider, {
|
|
31726
|
+
value: onReportCurrent,
|
|
31727
|
+
children: jsx("span", {
|
|
31728
|
+
className: "navi_binder_tab_label",
|
|
31729
|
+
"data-max-lines": itemMaxLines || undefined,
|
|
31730
|
+
style: itemMaxLines > 1 ? {
|
|
31731
|
+
"--binder-tab-max-lines": itemMaxLines
|
|
31732
|
+
} : undefined,
|
|
31733
|
+
children: label
|
|
31734
|
+
})
|
|
31735
|
+
})
|
|
31736
|
+
});
|
|
31737
|
+
};
|
|
31738
|
+
const BinderOutline = ({
|
|
31739
|
+
rootRef,
|
|
31740
|
+
currentValue,
|
|
31741
|
+
tabsPosition,
|
|
31742
|
+
vertical,
|
|
31743
|
+
tabsFirst
|
|
31744
|
+
}) => {
|
|
31745
|
+
const [drawing, setDrawing] = useState(null);
|
|
31746
|
+
const measure = useCallback(() => {
|
|
31747
|
+
setDrawing(measureDrawing({
|
|
31748
|
+
rootEl: rootRef.current,
|
|
31749
|
+
vertical,
|
|
31750
|
+
tabsFirst
|
|
31751
|
+
}));
|
|
31752
|
+
}, [rootRef, vertical, tabsFirst]);
|
|
31753
|
+
useLayoutEffect(() => {
|
|
31754
|
+
const rootEl = rootRef.current;
|
|
31755
|
+
// The outline is positioned absolutely, so drawing it cannot change any
|
|
31756
|
+
// observed size — the observer settles after one pass.
|
|
31757
|
+
const resizeObserver = new ResizeObserver(measure);
|
|
31758
|
+
resizeObserver.observe(rootEl);
|
|
31759
|
+
for (const tabEl of rootEl.querySelectorAll(".navi_binder_tab")) {
|
|
31760
|
+
resizeObserver.observe(tabEl);
|
|
31761
|
+
}
|
|
31762
|
+
measure();
|
|
31763
|
+
return () => resizeObserver.disconnect();
|
|
31764
|
+
}, [rootRef, measure, currentValue]);
|
|
31765
|
+
if (!drawing) {
|
|
31766
|
+
return jsx("svg", {
|
|
31767
|
+
className: "navi_binder_outline",
|
|
31768
|
+
"aria-hidden": "true"
|
|
31769
|
+
});
|
|
31770
|
+
}
|
|
31771
|
+
return jsx("svg", {
|
|
31772
|
+
className: "navi_binder_outline",
|
|
31773
|
+
"aria-hidden": "true",
|
|
31774
|
+
width: drawing.width,
|
|
31775
|
+
height: drawing.height,
|
|
31776
|
+
children: jsx("path", {
|
|
31777
|
+
d: buildBinderPath({
|
|
31778
|
+
...drawing.pathParams,
|
|
31779
|
+
tabsPosition
|
|
31780
|
+
}),
|
|
31781
|
+
fill: drawing.background,
|
|
31782
|
+
stroke: drawing.borderColor,
|
|
31783
|
+
"stroke-width": drawing.borderWidth
|
|
31784
|
+
})
|
|
31785
|
+
});
|
|
31786
|
+
};
|
|
31787
|
+
|
|
31788
|
+
// Everything the path needs, read off the DOM rather than taken from props:
|
|
31789
|
+
// the shape has to follow the sizes the layout actually produced — text length,
|
|
31790
|
+
// font, whatever the page contains.
|
|
31791
|
+
const measureDrawing = ({
|
|
31792
|
+
rootEl,
|
|
31793
|
+
vertical,
|
|
31794
|
+
tabsFirst
|
|
31795
|
+
}) => {
|
|
31796
|
+
const tabEls = rootEl.querySelectorAll(".navi_binder_tab");
|
|
31797
|
+
const currentTabEl = rootEl.querySelector(".navi_binder_tab[data-current]");
|
|
31798
|
+
if (!currentTabEl) {
|
|
31799
|
+
return null;
|
|
31800
|
+
}
|
|
31801
|
+
const rootComputedStyle = getComputedStyle(rootEl);
|
|
31802
|
+
const readVar = name => rootComputedStyle.getPropertyValue(name).trim();
|
|
31803
|
+
const borderWidth = parseFloat(readVar("--binder-border-width")) || 0;
|
|
31804
|
+
const outerRadius = parseFloat(readVar("--binder-border-radius")) || 0;
|
|
31805
|
+
const tabRadius = parseFloat(readVar("--binder-tab-border-radius")) || 0;
|
|
31806
|
+
|
|
31807
|
+
// Path space: the main axis runs along the tab row, the cross axis goes from
|
|
31808
|
+
// the tabs toward the page.
|
|
31809
|
+
const mainSizeOf = el => vertical ? el.offsetHeight : el.offsetWidth;
|
|
31810
|
+
const mainStartOf = el => {
|
|
31811
|
+
let start = vertical ? el.offsetTop : el.offsetLeft;
|
|
31812
|
+
let offsetParent = el.offsetParent;
|
|
31813
|
+
while (offsetParent && offsetParent !== rootEl) {
|
|
31814
|
+
start += vertical ? offsetParent.offsetTop : offsetParent.offsetLeft;
|
|
31815
|
+
offsetParent = offsetParent.offsetParent;
|
|
31816
|
+
}
|
|
31817
|
+
return start;
|
|
31818
|
+
};
|
|
31819
|
+
const crossStartOf = el => {
|
|
31820
|
+
let start = vertical ? el.offsetLeft : el.offsetTop;
|
|
31821
|
+
let offsetParent = el.offsetParent;
|
|
31822
|
+
while (offsetParent && offsetParent !== rootEl) {
|
|
31823
|
+
start += vertical ? offsetParent.offsetLeft : offsetParent.offsetTop;
|
|
31824
|
+
offsetParent = offsetParent.offsetParent;
|
|
31825
|
+
}
|
|
31826
|
+
return start;
|
|
31827
|
+
};
|
|
31828
|
+
const crossSizeOf = el => vertical ? el.offsetWidth : el.offsetHeight;
|
|
31829
|
+
const mainSize = vertical ? rootEl.offsetHeight : rootEl.offsetWidth;
|
|
31830
|
+
const crossSize = vertical ? rootEl.offsetWidth : rootEl.offsetHeight;
|
|
31831
|
+
const currentTabCrossStart = crossStartOf(currentTabEl);
|
|
31832
|
+
|
|
31833
|
+
// The junction line lands in the margin every tab keeps on the page side —
|
|
31834
|
+
// right against the tab boxes, so it continues the CSS borders drawn inside
|
|
31835
|
+
// them instead of covering their last row of pixels.
|
|
31836
|
+
let tabRowHeight;
|
|
31837
|
+
let panelHeight;
|
|
31838
|
+
if (tabsFirst) {
|
|
31839
|
+
tabRowHeight = currentTabCrossStart + crossSizeOf(currentTabEl) + borderWidth;
|
|
31840
|
+
panelHeight = crossSize - tabRowHeight;
|
|
31841
|
+
} else {
|
|
31842
|
+
panelHeight = currentTabCrossStart;
|
|
31843
|
+
tabRowHeight = crossSize - panelHeight;
|
|
31844
|
+
}
|
|
31845
|
+
const firstTabEl = tabEls[0];
|
|
31846
|
+
const lastTabEl = tabEls[tabEls.length - 1];
|
|
31847
|
+
return {
|
|
31848
|
+
width: vertical ? crossSize : mainSize,
|
|
31849
|
+
height: vertical ? mainSize : crossSize,
|
|
31850
|
+
borderWidth,
|
|
31851
|
+
borderColor: readVar("--binder-border-color"),
|
|
31852
|
+
background: readVar("--binder-background"),
|
|
31853
|
+
pathParams: {
|
|
31854
|
+
tabX: mainStartOf(currentTabEl),
|
|
31855
|
+
tabWidth: mainSizeOf(currentTabEl),
|
|
31856
|
+
tabRowHeight,
|
|
31857
|
+
panelWidth: mainSize,
|
|
31858
|
+
panelHeight,
|
|
31859
|
+
borderWidth,
|
|
31860
|
+
outerRadius,
|
|
31861
|
+
tabRadius,
|
|
31862
|
+
gapBeforeTabs: mainStartOf(firstTabEl),
|
|
31863
|
+
gapAfterTabs: mainSize - (mainStartOf(lastTabEl) + mainSizeOf(lastTabEl))
|
|
31864
|
+
}
|
|
31865
|
+
};
|
|
31866
|
+
};
|
|
31867
|
+
|
|
31868
|
+
// A bare number in a CSS var stays a bare number and the declaration is
|
|
31869
|
+
// dropped; every length prop here is in pixels when it is not spelled out.
|
|
31870
|
+
const withPixelUnit = value => {
|
|
31871
|
+
if (typeof value === "number") {
|
|
31872
|
+
return `${value}px`;
|
|
31873
|
+
}
|
|
31874
|
+
return value;
|
|
31875
|
+
};
|
|
31876
|
+
|
|
31877
|
+
/**
|
|
31878
|
+
* The room a fixed bar takes from the content, published so whatever scrolls
|
|
31879
|
+
* under it can give that room back.
|
|
31880
|
+
*
|
|
31881
|
+
* There are TWO rooms to give back, and forgetting the second one is the
|
|
31882
|
+
* classic bug:
|
|
31883
|
+
*
|
|
31884
|
+
* - **padding**, so the end of the content can be scrolled out from under the
|
|
31885
|
+
* bar. Without it the last screenful stays covered, unreachable.
|
|
31886
|
+
* - **scroll-padding**, so anything the browser scrolls TO lands in front of
|
|
31887
|
+
* the bar rather than under it. An anchor link, `scrollIntoView()`, a focused
|
|
31888
|
+
* field brought into view, restoring a scroll position — all of them align
|
|
31889
|
+
* the target with the edge of the scrollport, which is behind the bar. The
|
|
31890
|
+
* padding above does not help here: it moves the content, not the place the
|
|
31891
|
+
* browser scrolls the target to.
|
|
31892
|
+
*
|
|
31893
|
+
* Published on <html> as CSS variables rather than applied to some element:
|
|
31894
|
+
* which element scrolls is the app's business, and an app with more than one
|
|
31895
|
+
* would have to fight a component that picked for it. The app either marks its
|
|
31896
|
+
* scrolling area with `data-navi-fixed-bar-space` (the rules below) or reads
|
|
31897
|
+
* the variables itself. `:root` gets the scroll-padding unconditionally,
|
|
31898
|
+
* because the document is the scrollport in the common case and an anchor
|
|
31899
|
+
* landing under a bar is never what anyone wants.
|
|
31900
|
+
*
|
|
31901
|
+
* The variables hold the bar's measured size — see the comment where FixedBar
|
|
31902
|
+
* sets them.
|
|
31903
|
+
*/
|
|
31904
|
+
|
|
31905
|
+
const FIXED_BAR_SPACE_CSS = /* css */ `
|
|
31906
|
+
:root {
|
|
31907
|
+
--navi-fixed-bar-space-top: 0px;
|
|
31908
|
+
--navi-fixed-bar-space-bottom: 0px;
|
|
31909
|
+
--navi-fixed-bar-space-left: 0px;
|
|
31910
|
+
--navi-fixed-bar-space-right: 0px;
|
|
31911
|
+
|
|
31912
|
+
scroll-padding-top: var(--navi-fixed-bar-space-top);
|
|
31913
|
+
scroll-padding-right: var(--navi-fixed-bar-space-right);
|
|
31914
|
+
scroll-padding-bottom: var(--navi-fixed-bar-space-bottom);
|
|
31915
|
+
scroll-padding-left: var(--navi-fixed-bar-space-left);
|
|
31916
|
+
}
|
|
31917
|
+
|
|
31918
|
+
/* Put this on whatever scrolls under the bars. */
|
|
31919
|
+
[data-navi-fixed-bar-space] {
|
|
31920
|
+
padding-top: var(--navi-fixed-bar-space-top);
|
|
31921
|
+
padding-right: var(--navi-fixed-bar-space-right);
|
|
31922
|
+
padding-bottom: var(--navi-fixed-bar-space-bottom);
|
|
31923
|
+
padding-left: var(--navi-fixed-bar-space-left);
|
|
31924
|
+
|
|
31925
|
+
scroll-padding-top: var(--navi-fixed-bar-space-top);
|
|
31926
|
+
scroll-padding-right: var(--navi-fixed-bar-space-right);
|
|
31927
|
+
scroll-padding-bottom: var(--navi-fixed-bar-space-bottom);
|
|
31928
|
+
scroll-padding-left: var(--navi-fixed-bar-space-left);
|
|
31929
|
+
}
|
|
31930
|
+
`;
|
|
31931
|
+
|
|
31932
|
+
/**
|
|
31933
|
+
* @param {"top"|"bottom"|"left"|"right"} area
|
|
31934
|
+
* @param {string|null} value - `null` gives the room back to the content.
|
|
31935
|
+
*/
|
|
31936
|
+
const setFixedBarSpace = (area, value) => {
|
|
31937
|
+
const property = `--navi-fixed-bar-space-${area}`;
|
|
31938
|
+
const { style } = document.documentElement;
|
|
31939
|
+
if (value === null) {
|
|
31940
|
+
style.removeProperty(property);
|
|
31941
|
+
} else {
|
|
31942
|
+
style.setProperty(property, value);
|
|
31943
|
+
}
|
|
31944
|
+
};
|
|
31945
|
+
|
|
31946
|
+
installImportMetaCssBuild(import.meta);/**
|
|
31947
|
+
* A strip pinned to one edge of the window, and the space it takes back from
|
|
31948
|
+
* the content. What goes in it is none of its business — a nav, a title, a
|
|
31949
|
+
* toolbar, an action.
|
|
31950
|
+
*
|
|
31951
|
+
* The four things it is here to get right, each of which is a trap:
|
|
31952
|
+
*
|
|
31953
|
+
* 1. **`position: fixed`, never sticky.** A sticky element sticks to its
|
|
31954
|
+
* nearest scrolling ancestor, and an app shell almost always has one (an
|
|
31955
|
+
* `overflow` somewhere) — the bar would then stick inside that box and
|
|
31956
|
+
* never to the window. Fixed, centered and bounded by `maxWidth`, it also
|
|
31957
|
+
* stays lined up with the content on a wide screen.
|
|
31958
|
+
* 2. **It gives its space back.** Being fixed it covers the content: without a
|
|
31959
|
+
* reserve the end of a long page stays under it, unreachable. It publishes
|
|
31960
|
+
* what it takes on <html> — see fixed_bar_space.js.
|
|
31961
|
+
* 3. **It reaches under the notch.** `env(safe-area-inset-*)` pushes the
|
|
31962
|
+
* content in while the background keeps running to the edge of the screen.
|
|
31963
|
+
* Note that every `env(safe-area-inset-*)` is 0 unless the page asks for
|
|
31964
|
+
* it: `<meta name="viewport" content="…, viewport-fit=cover">`.
|
|
31965
|
+
* 4. **Its hairline is a box-shadow, not a border.** `width`/`height` is the
|
|
31966
|
+
* bar's content box, so the safe-area padding adds outside it: the size you
|
|
31967
|
+
* asked for is the size the content gets, whatever the device does. A real
|
|
31968
|
+
* border would eat into it; a box-shadow draws the identical line and stays
|
|
31969
|
+
* out of layout.
|
|
31970
|
+
*/
|
|
31971
|
+
const css$M = /* css */`
|
|
31972
|
+
@layer navi {
|
|
31973
|
+
:root {
|
|
31974
|
+
--navi-fixed-bar-width: 56px;
|
|
31975
|
+
--navi-fixed-bar-height: 56px;
|
|
31976
|
+
--navi-fixed-bar-max-width: none;
|
|
31977
|
+
--navi-fixed-bar-background: var(--navi-surface-color);
|
|
31978
|
+
--navi-fixed-bar-border-width: 1px;
|
|
31979
|
+
--navi-fixed-bar-border-color: var(--navi-separator-color-default);
|
|
31980
|
+
/* Along the bar only: room so its content never touches the edge of the
|
|
31981
|
+
screen. Across it there is nothing to add — that direction is what the
|
|
31982
|
+
width/height prop names, and padding there would only make the bar
|
|
31983
|
+
thicker than the size asked for. */
|
|
31984
|
+
--navi-fixed-bar-padding: var(--navi-s);
|
|
31985
|
+
}
|
|
31986
|
+
}
|
|
31987
|
+
|
|
31988
|
+
${FIXED_BAR_SPACE_CSS}
|
|
31989
|
+
|
|
31990
|
+
.navi_fixed_bar {
|
|
31991
|
+
position: fixed;
|
|
31992
|
+
z-index: 1;
|
|
31993
|
+
display: flex;
|
|
31994
|
+
/* content-box against an app that is border-box everywhere else: the size
|
|
31995
|
+
stays bare and the safe-area padding adds OUTSIDE it, so a bar asked for
|
|
31996
|
+
56px still shows 56px of bar under the notch instead of 56px minus it. */
|
|
31997
|
+
box-sizing: content-box;
|
|
31998
|
+
margin: auto;
|
|
31999
|
+
align-items: center;
|
|
32000
|
+
background: var(--navi-fixed-bar-background);
|
|
32001
|
+
|
|
32002
|
+
&[data-area="top"],
|
|
32003
|
+
&[data-area="bottom"] {
|
|
32004
|
+
right: 0;
|
|
32005
|
+
left: 0;
|
|
32006
|
+
/* No width of its own: pinned to both edges, the used width absorbs the
|
|
32007
|
+
padding instead of being inflated by it — which a content-box
|
|
32008
|
+
"width: 100%" would do. max-width then narrows it and the auto
|
|
32009
|
+
margins re-center it. */
|
|
32010
|
+
max-width: var(--navi-fixed-bar-max-width);
|
|
32011
|
+
height: var(--navi-fixed-bar-height);
|
|
32012
|
+
padding-right: var(--navi-fixed-bar-padding);
|
|
32013
|
+
padding-left: var(--navi-fixed-bar-padding);
|
|
32014
|
+
}
|
|
32015
|
+
&[data-area="left"],
|
|
32016
|
+
&[data-area="right"] {
|
|
32017
|
+
top: 0;
|
|
32018
|
+
bottom: 0;
|
|
32019
|
+
width: var(--navi-fixed-bar-width);
|
|
32020
|
+
padding-top: var(--navi-fixed-bar-padding);
|
|
32021
|
+
padding-bottom: var(--navi-fixed-bar-padding);
|
|
32022
|
+
flex-direction: column;
|
|
32023
|
+
}
|
|
32024
|
+
|
|
32025
|
+
/* The inset on this edge alone: the one the bar is pinned to. */
|
|
32026
|
+
&[data-area="top"] {
|
|
32027
|
+
top: 0;
|
|
32028
|
+
padding-top: env(safe-area-inset-top);
|
|
32029
|
+
box-shadow: 0 var(--navi-fixed-bar-border-width) 0
|
|
32030
|
+
var(--navi-fixed-bar-border-color);
|
|
32031
|
+
}
|
|
32032
|
+
&[data-area="bottom"] {
|
|
32033
|
+
bottom: 0;
|
|
32034
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
32035
|
+
box-shadow: 0 calc(-1 * var(--navi-fixed-bar-border-width)) 0
|
|
32036
|
+
var(--navi-fixed-bar-border-color);
|
|
32037
|
+
}
|
|
32038
|
+
&[data-area="left"] {
|
|
32039
|
+
left: 0;
|
|
32040
|
+
padding-left: env(safe-area-inset-left);
|
|
32041
|
+
box-shadow: var(--navi-fixed-bar-border-width) 0 0
|
|
32042
|
+
var(--navi-fixed-bar-border-color);
|
|
32043
|
+
}
|
|
32044
|
+
&[data-area="right"] {
|
|
32045
|
+
right: 0;
|
|
32046
|
+
padding-right: env(safe-area-inset-right);
|
|
32047
|
+
box-shadow: calc(-1 * var(--navi-fixed-bar-border-width)) 0 0
|
|
32048
|
+
var(--navi-fixed-bar-border-color);
|
|
32049
|
+
}
|
|
32050
|
+
&[data-border="none"] {
|
|
32051
|
+
box-shadow: none;
|
|
32052
|
+
}
|
|
32053
|
+
}
|
|
32054
|
+
`;
|
|
32055
|
+
const FixedBarStyleCSSVars = {
|
|
32056
|
+
maxWidth: "--navi-fixed-bar-max-width",
|
|
32057
|
+
padding: "--navi-fixed-bar-padding",
|
|
32058
|
+
background: "--navi-fixed-bar-background",
|
|
32059
|
+
borderWidth: "--navi-fixed-bar-border-width",
|
|
32060
|
+
borderColor: "--navi-fixed-bar-border-color"
|
|
32061
|
+
};
|
|
32062
|
+
|
|
32063
|
+
/**
|
|
32064
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
32065
|
+
* area?: "top"|"bottom"|"left"|"right",
|
|
32066
|
+
* width?: string|number,
|
|
32067
|
+
* height?: string|number,
|
|
32068
|
+
* maxWidth?: string|number,
|
|
32069
|
+
* padding?: string|number,
|
|
32070
|
+
* background?: string,
|
|
32071
|
+
* border?: boolean,
|
|
32072
|
+
* borderWidth?: string|number,
|
|
32073
|
+
* borderColor?: string,
|
|
32074
|
+
* }>}
|
|
32075
|
+
* @param {"top"|"bottom"|"left"|"right"} [props.area="top"] - Which edge of
|
|
32076
|
+
* the window it is pinned to.
|
|
32077
|
+
* @param {string|number} [props.height] - For a bar on the top or bottom
|
|
32078
|
+
* @param {string|number} [props.width] - …and for one on a side. The safe-area
|
|
32079
|
+
* inset is NOT included in it: the inset is added outside, and the content
|
|
32080
|
+
* keeps the size asked for.
|
|
32081
|
+
* @param {boolean} [props.border=true] - The hairline on the content side.
|
|
32082
|
+
* Drawn with a box-shadow so it never eats into the size; give it a
|
|
32083
|
+
* `borderWidth`/`borderColor`, or `border={false}` for none.
|
|
32084
|
+
* @param {string|number} [props.maxWidth] - Keeps the bar lined up with a
|
|
32085
|
+
* content column narrower than the window (it stays centered).
|
|
32086
|
+
*/
|
|
32087
|
+
const FixedBar = ({
|
|
32088
|
+
children,
|
|
32089
|
+
area = "top",
|
|
32090
|
+
border = true,
|
|
32091
|
+
...props
|
|
32092
|
+
}) => {
|
|
32093
|
+
import.meta.css = [css$M, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
|
|
32094
|
+
const defaultRef = useRef();
|
|
32095
|
+
props.ref = props.ref || defaultRef;
|
|
32096
|
+
// Whichever of width/height crosses the edge the bar sits on is what the
|
|
32097
|
+
// content has to be given back — and the bar's border box already IS that:
|
|
32098
|
+
// the size it was given, plus the safe-area padding added outside it.
|
|
32099
|
+
// Measured rather than rebuilt as a calc() expression, so a size coming from
|
|
32100
|
+
// anywhere — a prop, a theme variable, the content itself — is reserved just
|
|
32101
|
+
// the same, and each `env()` inset stays the browser's business alone.
|
|
32102
|
+
const vertical = area === "left" || area === "right";
|
|
32103
|
+
const {
|
|
32104
|
+
ref
|
|
32105
|
+
} = props;
|
|
32106
|
+
useLayoutEffect(() => {
|
|
32107
|
+
const barElement = ref.current;
|
|
32108
|
+
if (!barElement) {
|
|
32109
|
+
return undefined;
|
|
32110
|
+
}
|
|
32111
|
+
const {
|
|
32112
|
+
width,
|
|
32113
|
+
height
|
|
32114
|
+
} = barElement.getBoundingClientRect();
|
|
32115
|
+
setFixedBarSpace(area, `${vertical ? width : height}px`);
|
|
32116
|
+
return () => {
|
|
32117
|
+
setFixedBarSpace(area, null);
|
|
32118
|
+
};
|
|
32119
|
+
}, [area]);
|
|
32120
|
+
return jsx(Box, {
|
|
32121
|
+
baseClassName: "navi_fixed_bar",
|
|
32122
|
+
"data-area": area,
|
|
32123
|
+
"data-border": border ? undefined : "none",
|
|
32124
|
+
...props,
|
|
32125
|
+
styleCSSVars: FixedBarStyleCSSVars,
|
|
32126
|
+
children: children
|
|
32127
|
+
});
|
|
32128
|
+
};
|
|
32129
|
+
|
|
30797
32130
|
const detectMac = () => {
|
|
30798
32131
|
// Modern way using User-Agent Client Hints API
|
|
30799
32132
|
if (window.navigator.userAgentData) {
|
|
@@ -47001,8 +48334,41 @@ installImportMetaCssBuild(import.meta);const css$t = /* css */`
|
|
|
47001
48334
|
}
|
|
47002
48335
|
.navi_badge {
|
|
47003
48336
|
--font-size: 0.7em;
|
|
47004
|
-
--padding-x: 0.8em;
|
|
47005
|
-
--padding-y: 0.4em;
|
|
48337
|
+
--badge-padding-x-default: 0.8em;
|
|
48338
|
+
--badge-padding-y-default: 0.4em;
|
|
48339
|
+
|
|
48340
|
+
/* Each side resolves the most specific value it was given, from the side
|
|
48341
|
+
itself down to the axis, the shorthand, then the default. Resolved once
|
|
48342
|
+
here because the close button below reuses these to cancel the badge
|
|
48343
|
+
padding on the edge it sits on. */
|
|
48344
|
+
--x-badge-padding-top: var(
|
|
48345
|
+
--badge-padding-top,
|
|
48346
|
+
var(
|
|
48347
|
+
--badge-padding-y,
|
|
48348
|
+
var(--badge-padding, var(--badge-padding-y-default))
|
|
48349
|
+
)
|
|
48350
|
+
);
|
|
48351
|
+
--x-badge-padding-right: var(
|
|
48352
|
+
--badge-padding-right,
|
|
48353
|
+
var(
|
|
48354
|
+
--badge-padding-x,
|
|
48355
|
+
var(--badge-padding, var(--badge-padding-x-default))
|
|
48356
|
+
)
|
|
48357
|
+
);
|
|
48358
|
+
--x-badge-padding-bottom: var(
|
|
48359
|
+
--badge-padding-bottom,
|
|
48360
|
+
var(
|
|
48361
|
+
--badge-padding-y,
|
|
48362
|
+
var(--badge-padding, var(--badge-padding-y-default))
|
|
48363
|
+
)
|
|
48364
|
+
);
|
|
48365
|
+
--x-badge-padding-left: var(
|
|
48366
|
+
--badge-padding-left,
|
|
48367
|
+
var(
|
|
48368
|
+
--badge-padding-x,
|
|
48369
|
+
var(--badge-padding, var(--badge-padding-x-default))
|
|
48370
|
+
)
|
|
48371
|
+
);
|
|
47006
48372
|
|
|
47007
48373
|
--x-background: var(--background, light-dark(#e0e0e0, #3a3a3a));
|
|
47008
48374
|
--x-background-color: var(--background-color, var(--x-background));
|
|
@@ -47014,10 +48380,10 @@ installImportMetaCssBuild(import.meta);const css$t = /* css */`
|
|
|
47014
48380
|
position: relative;
|
|
47015
48381
|
display: inline;
|
|
47016
48382
|
max-width: 200px;
|
|
47017
|
-
padding-top: var(--padding-
|
|
47018
|
-
padding-right: var(--padding-
|
|
47019
|
-
padding-bottom: var(--padding-
|
|
47020
|
-
padding-left: var(--padding-
|
|
48383
|
+
padding-top: var(--x-badge-padding-top);
|
|
48384
|
+
padding-right: var(--x-badge-padding-right);
|
|
48385
|
+
padding-bottom: var(--x-badge-padding-bottom);
|
|
48386
|
+
padding-left: var(--x-badge-padding-left);
|
|
47021
48387
|
align-items: stretch;
|
|
47022
48388
|
color: var(--x-color);
|
|
47023
48389
|
font-size: var(--font-size);
|
|
@@ -47041,25 +48407,25 @@ installImportMetaCssBuild(import.meta);const css$t = /* css */`
|
|
|
47041
48407
|
|
|
47042
48408
|
[role="button"] {
|
|
47043
48409
|
display: inline-flex;
|
|
47044
|
-
margin-top: calc(-1 * var(--padding-
|
|
47045
|
-
margin-bottom: calc(-1 * var(--padding-
|
|
47046
|
-
padding-top: var(--padding-
|
|
47047
|
-
padding-right: calc(var(--padding-
|
|
47048
|
-
padding-bottom: var(--padding-
|
|
47049
|
-
padding-left: calc(var(--padding-
|
|
48410
|
+
margin-top: calc(-1 * var(--x-badge-padding-top));
|
|
48411
|
+
margin-bottom: calc(-1 * var(--x-badge-padding-bottom));
|
|
48412
|
+
padding-top: var(--x-badge-padding-top);
|
|
48413
|
+
padding-right: calc(var(--x-badge-padding-right) / 2);
|
|
48414
|
+
padding-bottom: var(--x-badge-padding-bottom);
|
|
48415
|
+
padding-left: calc(var(--x-badge-padding-left) / 2);
|
|
47050
48416
|
align-items: center;
|
|
47051
48417
|
cursor: pointer;
|
|
47052
48418
|
pointer-events: auto;
|
|
47053
48419
|
user-select: none;
|
|
47054
48420
|
|
|
47055
48421
|
&:first-child {
|
|
47056
|
-
margin-left: calc(-1 * var(--padding-
|
|
48422
|
+
margin-left: calc(-1 * var(--x-badge-padding-left));
|
|
47057
48423
|
border-top-left-radius: inherit;
|
|
47058
48424
|
border-bottom-left-radius: inherit;
|
|
47059
48425
|
}
|
|
47060
48426
|
|
|
47061
48427
|
&:last-child {
|
|
47062
|
-
margin-right: calc(-1 * var(--padding-
|
|
48428
|
+
margin-right: calc(-1 * var(--x-badge-padding-right));
|
|
47063
48429
|
border-top-right-radius: inherit;
|
|
47064
48430
|
border-bottom-right-radius: inherit;
|
|
47065
48431
|
}
|
|
@@ -47087,16 +48453,21 @@ const Badge = ({
|
|
|
47087
48453
|
bold: true,
|
|
47088
48454
|
maxLines: 1,
|
|
47089
48455
|
...props,
|
|
47090
|
-
styleCSSVars: BadgeStyleCSSVars
|
|
48456
|
+
styleCSSVars: BadgeStyleCSSVars,
|
|
47091
48457
|
spacing: jsx("span", {}),
|
|
47092
48458
|
children: children
|
|
47093
48459
|
});
|
|
47094
48460
|
};
|
|
47095
|
-
const BadgeStyleCSSVars
|
|
48461
|
+
const BadgeStyleCSSVars = {
|
|
47096
48462
|
borderWidth: "--border-width",
|
|
47097
48463
|
borderRadius: "--border-radius",
|
|
47098
|
-
|
|
47099
|
-
|
|
48464
|
+
padding: "--badge-padding",
|
|
48465
|
+
paddingX: "--badge-padding-x",
|
|
48466
|
+
paddingY: "--badge-padding-y",
|
|
48467
|
+
paddingTop: "--badge-padding-top",
|
|
48468
|
+
paddingRight: "--badge-padding-right",
|
|
48469
|
+
paddingBottom: "--badge-padding-bottom",
|
|
48470
|
+
paddingLeft: "--badge-padding-left",
|
|
47100
48471
|
backgroundColor: "--background-color",
|
|
47101
48472
|
background: "--background",
|
|
47102
48473
|
borderColor: "--border-color",
|
|
@@ -56715,8 +58086,40 @@ installImportMetaCssBuild(import.meta);const css$b = /* css */`
|
|
|
56715
58086
|
--x-background-color: var(--background-color, var(--x-background));
|
|
56716
58087
|
--x-color-contrasting: var(--navi-color-white);
|
|
56717
58088
|
--x-color: var(--color, var(--x-color-contrasting));
|
|
56718
|
-
--padding-x: 0.5em;
|
|
56719
|
-
--padding-y: 0.2em;
|
|
58089
|
+
--badge-count-padding-x-default: 0.5em;
|
|
58090
|
+
--badge-count-padding-y-default: 0.2em;
|
|
58091
|
+
|
|
58092
|
+
/* Each side resolves the most specific value it was given, from the side
|
|
58093
|
+
itself down to the axis, the shorthand, then the default. */
|
|
58094
|
+
--x-badge-count-padding-top: var(
|
|
58095
|
+
--badge-count-padding-top,
|
|
58096
|
+
var(
|
|
58097
|
+
--badge-count-padding-y,
|
|
58098
|
+
var(--badge-count-padding, var(--badge-count-padding-y-default))
|
|
58099
|
+
)
|
|
58100
|
+
);
|
|
58101
|
+
--x-badge-count-padding-right: var(
|
|
58102
|
+
--badge-count-padding-right,
|
|
58103
|
+
var(
|
|
58104
|
+
--badge-count-padding-x,
|
|
58105
|
+
var(--badge-count-padding, var(--badge-count-padding-x-default))
|
|
58106
|
+
)
|
|
58107
|
+
);
|
|
58108
|
+
--x-badge-count-padding-bottom: var(
|
|
58109
|
+
--badge-count-padding-bottom,
|
|
58110
|
+
var(
|
|
58111
|
+
--badge-count-padding-y,
|
|
58112
|
+
var(--badge-count-padding, var(--badge-count-padding-y-default))
|
|
58113
|
+
)
|
|
58114
|
+
);
|
|
58115
|
+
--x-badge-count-padding-left: var(
|
|
58116
|
+
--badge-count-padding-left,
|
|
58117
|
+
var(
|
|
58118
|
+
--badge-count-padding-x,
|
|
58119
|
+
var(--badge-count-padding, var(--badge-count-padding-x-default))
|
|
58120
|
+
)
|
|
58121
|
+
);
|
|
58122
|
+
|
|
56720
58123
|
position: relative;
|
|
56721
58124
|
color: var(--x-color);
|
|
56722
58125
|
font-size: var(--font-size);
|
|
@@ -56740,10 +58143,10 @@ installImportMetaCssBuild(import.meta);const css$b = /* css */`
|
|
|
56740
58143
|
|
|
56741
58144
|
/* Ellipse */
|
|
56742
58145
|
&[data-ellipse] {
|
|
56743
|
-
padding-top: var(--padding-
|
|
56744
|
-
padding-right: var(--padding-
|
|
56745
|
-
padding-bottom: var(--padding-
|
|
56746
|
-
padding-left: var(--padding-
|
|
58146
|
+
padding-top: var(--x-badge-count-padding-top);
|
|
58147
|
+
padding-right: var(--x-badge-count-padding-right);
|
|
58148
|
+
padding-bottom: var(--x-badge-count-padding-bottom);
|
|
58149
|
+
padding-left: var(--x-badge-count-padding-left);
|
|
56747
58150
|
line-height: normal;
|
|
56748
58151
|
background: var(--x-background);
|
|
56749
58152
|
background-color: var(--x-background-color);
|
|
@@ -56751,29 +58154,48 @@ installImportMetaCssBuild(import.meta);const css$b = /* css */`
|
|
|
56751
58154
|
|
|
56752
58155
|
/* For ellipse + single char force the circle aspect as it's prettier */
|
|
56753
58156
|
&[data-single-char] {
|
|
58157
|
+
/* The digit sits in a square content box that the radius rounds into a
|
|
58158
|
+
circle, so no padding is needed to obtain the shape. Sizing is
|
|
58159
|
+
content-box: a padding prop then grows the square from the outside —
|
|
58160
|
+
a bigger circle, or a pill on one axis — instead of eating into it
|
|
58161
|
+
and pushing the digit out. */
|
|
58162
|
+
--badge-count-padding-x-default: 0;
|
|
58163
|
+
--badge-count-padding-y-default: 0;
|
|
58164
|
+
|
|
56754
58165
|
display: inline-block;
|
|
56755
|
-
|
|
58166
|
+
box-sizing: content-box;
|
|
58167
|
+
width: 1.6em;
|
|
56756
58168
|
height: 1.6em;
|
|
56757
|
-
padding: 0;
|
|
56758
58169
|
text-align: center;
|
|
56759
58170
|
line-height: 1.6em;
|
|
58171
|
+
/* Larger than any half-height it can reach, so the shape stays fully
|
|
58172
|
+
round whatever the padding adds. */
|
|
58173
|
+
border-radius: 100em;
|
|
56760
58174
|
}
|
|
56761
58175
|
}
|
|
56762
58176
|
|
|
56763
58177
|
/* Circle */
|
|
56764
58178
|
&[data-circle] {
|
|
56765
58179
|
--x-number-font-size: var(--font-size);
|
|
58180
|
+
/* Same as the single char ellipse: the radius comes from the number of
|
|
58181
|
+
characters, padding grows it from the outside. */
|
|
58182
|
+
--badge-count-padding-x-default: 0;
|
|
58183
|
+
--badge-count-padding-y-default: 0;
|
|
56766
58184
|
|
|
56767
58185
|
display: inline-flex;
|
|
56768
58186
|
box-sizing: content-box;
|
|
56769
58187
|
aspect-ratio: 1/1;
|
|
56770
58188
|
width: var(--x-radius);
|
|
56771
58189
|
height: var(--x-radius);
|
|
58190
|
+
padding-top: var(--x-badge-count-padding-top);
|
|
58191
|
+
padding-right: var(--x-badge-count-padding-right);
|
|
58192
|
+
padding-bottom: var(--x-badge-count-padding-bottom);
|
|
58193
|
+
padding-left: var(--x-badge-count-padding-left);
|
|
56772
58194
|
align-items: center;
|
|
56773
58195
|
justify-content: center;
|
|
56774
58196
|
background: var(--x-background);
|
|
56775
58197
|
background-color: var(--x-background-color);
|
|
56776
|
-
border-radius:
|
|
58198
|
+
border-radius: 100em;
|
|
56777
58199
|
|
|
56778
58200
|
&[data-single-char] {
|
|
56779
58201
|
--x-radius: 1.6em;
|
|
@@ -56875,11 +58297,16 @@ const BadgeCount = ({
|
|
|
56875
58297
|
})
|
|
56876
58298
|
});
|
|
56877
58299
|
};
|
|
56878
|
-
const
|
|
58300
|
+
const BadgeCountStyleCSSVars = {
|
|
56879
58301
|
borderWidth: "--border-width",
|
|
56880
58302
|
borderRadius: "--border-radius",
|
|
56881
|
-
|
|
56882
|
-
|
|
58303
|
+
padding: "--badge-count-padding",
|
|
58304
|
+
paddingX: "--badge-count-padding-x",
|
|
58305
|
+
paddingY: "--badge-count-padding-y",
|
|
58306
|
+
paddingTop: "--badge-count-padding-top",
|
|
58307
|
+
paddingRight: "--badge-count-padding-right",
|
|
58308
|
+
paddingBottom: "--badge-count-padding-bottom",
|
|
58309
|
+
paddingLeft: "--badge-count-padding-left",
|
|
56883
58310
|
backgroundColor: "--background-color",
|
|
56884
58311
|
background: "--background",
|
|
56885
58312
|
borderColor: "--border-color",
|
|
@@ -56918,7 +58345,7 @@ const BadgeCountEllipse = ({
|
|
|
56918
58345
|
"data-loading": loading ? "" : undefined,
|
|
56919
58346
|
"data-single-char": charCount === 1 ? "" : undefined,
|
|
56920
58347
|
...props,
|
|
56921
|
-
styleCSSVars:
|
|
58348
|
+
styleCSSVars: BadgeCountStyleCSSVars,
|
|
56922
58349
|
spacing: "pre",
|
|
56923
58350
|
children: loading ? jsx(Icon, {
|
|
56924
58351
|
children: jsx(LoadingDotsSvg, {})
|
|
@@ -56944,7 +58371,7 @@ const BadgeCountCircle = ({
|
|
|
56944
58371
|
"data-four-chars": charCount === 4 ? "" : undefined,
|
|
56945
58372
|
"data-value-overflow": hasOverflow ? "" : undefined,
|
|
56946
58373
|
...props,
|
|
56947
|
-
styleCSSVars:
|
|
58374
|
+
styleCSSVars: BadgeCountStyleCSSVars,
|
|
56948
58375
|
spacing: "pre",
|
|
56949
58376
|
children: loading ? jsx(Icon, {
|
|
56950
58377
|
children: jsx(LoadingDotsSvg, {})
|
|
@@ -58882,5 +60309,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
58882
60309
|
})
|
|
58883
60310
|
});
|
|
58884
60311
|
|
|
58885
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
|
|
60312
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
|
|
58886
60313
|
//# sourceMappingURL=jsenv_navi.js.map
|