@jsenv/navi 0.29.3 → 0.29.5
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 +1463 -42
- package/dist/jsenv_navi.js.map +109 -23
- 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
|
}
|
|
@@ -6422,6 +6422,16 @@ const applyOnTwoProps = (propA, propB) => {
|
|
|
6422
6422
|
};
|
|
6423
6423
|
};
|
|
6424
6424
|
|
|
6425
|
+
// How much of the free space an expanding item claims next to its siblings:
|
|
6426
|
+
// expand={30} between four expand={18} is the wide one in the middle. A bare
|
|
6427
|
+
// `expand` claims one share, like everyone else asking for their part.
|
|
6428
|
+
const expandWeight = (value) => {
|
|
6429
|
+
if (value === true || value === "") {
|
|
6430
|
+
return 1;
|
|
6431
|
+
}
|
|
6432
|
+
return value;
|
|
6433
|
+
};
|
|
6434
|
+
|
|
6425
6435
|
const LAYOUT_PROPS = {
|
|
6426
6436
|
// all are handled by navi-attributes
|
|
6427
6437
|
inline: () => {},
|
|
@@ -6529,7 +6539,7 @@ const DIMENSION_PROPS = {
|
|
|
6529
6539
|
return { flexGrow: 1 };
|
|
6530
6540
|
}
|
|
6531
6541
|
// Parent is flex-x: grow as flex item
|
|
6532
|
-
return { flexGrow:
|
|
6542
|
+
return { flexGrow: expandWeight(value), flexBasis: "0%" };
|
|
6533
6543
|
}
|
|
6534
6544
|
if (parentBoxFlow === "flex-y" || parentBoxFlow === "inline-flex-y") {
|
|
6535
6545
|
return {
|
|
@@ -6553,7 +6563,7 @@ const DIMENSION_PROPS = {
|
|
|
6553
6563
|
return { flexGrow: 1 };
|
|
6554
6564
|
}
|
|
6555
6565
|
// Parent is flex-y: grow as flex item
|
|
6556
|
-
return { flexGrow:
|
|
6566
|
+
return { flexGrow: expandWeight(value), flexBasis: "0%" };
|
|
6557
6567
|
}
|
|
6558
6568
|
if (parentBoxFlow === "flex-x" || parentBoxFlow === "inline-flex-x") {
|
|
6559
6569
|
return {
|
|
@@ -7834,6 +7844,11 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
7834
7844
|
":visited": {
|
|
7835
7845
|
attribute: "data-visited",
|
|
7836
7846
|
},
|
|
7847
|
+
// Written by whoever knows the current url — a Link from its href, a Button
|
|
7848
|
+
// from its route — so it lives here rather than with one of them.
|
|
7849
|
+
":-navi-href-current": {
|
|
7850
|
+
attribute: "data-href-current",
|
|
7851
|
+
},
|
|
7837
7852
|
});
|
|
7838
7853
|
const definePseudoClass = (pseudoClass, definition) => {
|
|
7839
7854
|
PSEUDO_CLASSES[pseudoClass] = definition;
|
|
@@ -16381,7 +16396,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
16381
16396
|
* - Arrow automatically shows when pointing at a valid anchor element
|
|
16382
16397
|
* - Centers in viewport when no anchor element provided or anchor is too big
|
|
16383
16398
|
*/
|
|
16384
|
-
const css$
|
|
16399
|
+
const css$V = /* css */`
|
|
16385
16400
|
@layer navi {
|
|
16386
16401
|
.navi_callout {
|
|
16387
16402
|
/* A callout is parented to what it explains, so it inherits from it — and
|
|
@@ -16601,7 +16616,7 @@ const openCallout = (message, {
|
|
|
16601
16616
|
skipFocus = false,
|
|
16602
16617
|
debug = () => {}
|
|
16603
16618
|
} = {}) => {
|
|
16604
|
-
import.meta.css = [css$
|
|
16619
|
+
import.meta.css = [css$V, "@jsenv/navi/src/control/rules/callout/callout.js"];
|
|
16605
16620
|
if (debug === true) {
|
|
16606
16621
|
debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
|
|
16607
16622
|
}
|
|
@@ -28374,7 +28389,7 @@ const setupNetworkMonitoring = () => {
|
|
|
28374
28389
|
};
|
|
28375
28390
|
setupNetworkMonitoring();
|
|
28376
28391
|
|
|
28377
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
28392
|
+
installImportMetaCssBuild(import.meta);const css$U = /* css */`
|
|
28378
28393
|
.navi_loading_indicator_fluid_container {
|
|
28379
28394
|
position: relative;
|
|
28380
28395
|
display: flex;
|
|
@@ -28406,7 +28421,7 @@ const LoadingIndicatorFluid = ({
|
|
|
28406
28421
|
visuallyHidden,
|
|
28407
28422
|
...rest
|
|
28408
28423
|
}) => {
|
|
28409
|
-
import.meta.css = [css$
|
|
28424
|
+
import.meta.css = [css$U, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
|
|
28410
28425
|
const ref = useRef(null);
|
|
28411
28426
|
// The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
|
|
28412
28427
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
@@ -28611,7 +28626,7 @@ const LoadingRectangleSvg = ({
|
|
|
28611
28626
|
});
|
|
28612
28627
|
};
|
|
28613
28628
|
|
|
28614
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
28629
|
+
installImportMetaCssBuild(import.meta);const css$T = /* css */`
|
|
28615
28630
|
.navi_loading_outline_wrapper {
|
|
28616
28631
|
position: absolute;
|
|
28617
28632
|
/* Controls place the outline slightly outside their box, right on top of
|
|
@@ -28648,7 +28663,7 @@ installImportMetaCssBuild(import.meta);const css$R = /* css */`
|
|
|
28648
28663
|
}
|
|
28649
28664
|
`;
|
|
28650
28665
|
const LoadingOutline = props => {
|
|
28651
|
-
import.meta.css = [css$
|
|
28666
|
+
import.meta.css = [css$T, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
|
|
28652
28667
|
if (props.containerRef) {
|
|
28653
28668
|
const container = props.containerRef.current;
|
|
28654
28669
|
if (!container) {
|
|
@@ -28873,7 +28888,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
|
|
|
28873
28888
|
};
|
|
28874
28889
|
|
|
28875
28890
|
installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
|
|
28876
|
-
const css$
|
|
28891
|
+
const css$S = /* css */`
|
|
28877
28892
|
@layer navi {
|
|
28878
28893
|
.navi_text {
|
|
28879
28894
|
&[data-skeleton] {
|
|
@@ -29378,7 +29393,7 @@ const TextShrinkWrap = props => {
|
|
|
29378
29393
|
});
|
|
29379
29394
|
};
|
|
29380
29395
|
const TextUI = props => {
|
|
29381
|
-
import.meta.css = [css$
|
|
29396
|
+
import.meta.css = [css$S, "@jsenv/navi/src/text/text.jsx"];
|
|
29382
29397
|
let {
|
|
29383
29398
|
ref,
|
|
29384
29399
|
spacing,
|
|
@@ -29529,7 +29544,7 @@ const TextWithSelectRange = ({
|
|
|
29529
29544
|
};
|
|
29530
29545
|
|
|
29531
29546
|
installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
|
|
29532
|
-
const css$
|
|
29547
|
+
const css$R = /* css */`
|
|
29533
29548
|
.navi_text_anchor {
|
|
29534
29549
|
vertical-align: baseline;
|
|
29535
29550
|
user-select: none;
|
|
@@ -29564,7 +29579,7 @@ const TextAnchor = ({
|
|
|
29564
29579
|
textSize,
|
|
29565
29580
|
lineLayout
|
|
29566
29581
|
}) => {
|
|
29567
|
-
import.meta.css = [css$
|
|
29582
|
+
import.meta.css = [css$R, "@jsenv/navi/src/text/text_anchor.jsx"];
|
|
29568
29583
|
const anchorRef = useRef();
|
|
29569
29584
|
|
|
29570
29585
|
// Plain useLayoutEffect would also fire while an ancestor dialog/popover
|
|
@@ -29679,7 +29694,7 @@ const computeTopOffset = ({
|
|
|
29679
29694
|
};
|
|
29680
29695
|
const charTopCanvas = document.createElement("canvas");
|
|
29681
29696
|
|
|
29682
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
29697
|
+
installImportMetaCssBuild(import.meta);const css$Q = /* css */`
|
|
29683
29698
|
@layer navi {
|
|
29684
29699
|
/* Ensure data attributes from box.jsx can win to update display */
|
|
29685
29700
|
.navi_icon {
|
|
@@ -29837,7 +29852,7 @@ const Icon = ({
|
|
|
29837
29852
|
fillLine,
|
|
29838
29853
|
...props
|
|
29839
29854
|
}) => {
|
|
29840
|
-
import.meta.css = [css$
|
|
29855
|
+
import.meta.css = [css$Q, "@jsenv/navi/src/text/icon.jsx"];
|
|
29841
29856
|
const innerChildren = href ? jsx("svg", {
|
|
29842
29857
|
width: "100%",
|
|
29843
29858
|
height: "100%",
|
|
@@ -29941,6 +29956,18 @@ const useIsVisited = (url) => {
|
|
|
29941
29956
|
}, [url, visitedUrlsSignal.value]);
|
|
29942
29957
|
};
|
|
29943
29958
|
|
|
29959
|
+
/**
|
|
29960
|
+
* What a binder tab hands to whatever it contains: a way to say "I am the
|
|
29961
|
+
* current one". A `Link` inside a tab knows it is current (its route matches)
|
|
29962
|
+
* long before the binder could work it out, so it reports instead of the
|
|
29963
|
+
* binder guessing — see `Link`'s use of this context.
|
|
29964
|
+
*
|
|
29965
|
+
* Its own module so `link.jsx` can read it without importing the binder.
|
|
29966
|
+
*
|
|
29967
|
+
* @type {import("preact").Context<{ reportCurrent: (current: boolean) => void } | null>}
|
|
29968
|
+
*/
|
|
29969
|
+
const BinderItemContext = createContext(null);
|
|
29970
|
+
|
|
29944
29971
|
/*
|
|
29945
29972
|
* Custom hook to apply semi-transparent color when an element should be dimmed.
|
|
29946
29973
|
*
|
|
@@ -29978,7 +30005,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
|
|
|
29978
30005
|
});
|
|
29979
30006
|
};
|
|
29980
30007
|
|
|
29981
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
30008
|
+
installImportMetaCssBuild(import.meta);const css$P = /* css */`
|
|
29982
30009
|
@layer navi {
|
|
29983
30010
|
.navi_link {
|
|
29984
30011
|
--link-border-radius: unset;
|
|
@@ -30053,7 +30080,12 @@ installImportMetaCssBuild(import.meta);const css$N = /* css */`
|
|
|
30053
30080
|
aspect-ratio: inherit;
|
|
30054
30081
|
padding-top: var(--x-link-padding-top);
|
|
30055
30082
|
padding-right: var(--x-link-padding-right);
|
|
30056
|
-
|
|
30083
|
+
/* The loading underline is drawn inside the box: its own room is kept at
|
|
30084
|
+
the bottom on top of whatever padding was asked for, so a link that
|
|
30085
|
+
starts loading never reflows. */
|
|
30086
|
+
padding-bottom: calc(
|
|
30087
|
+
var(--x-link-padding-bottom) + var(--link-loading-outline-size)
|
|
30088
|
+
);
|
|
30057
30089
|
padding-left: var(--x-link-padding-left);
|
|
30058
30090
|
color: var(--x-link-color);
|
|
30059
30091
|
text-decoration: var(--x-link-text-decoration);
|
|
@@ -30311,6 +30343,21 @@ const LinkStyleCSSVars = {
|
|
|
30311
30343
|
color: "--link-color-selected"
|
|
30312
30344
|
}
|
|
30313
30345
|
};
|
|
30346
|
+
// A link placed inside a binder tab tells that tab whether it is the current
|
|
30347
|
+
// one. The binder cannot know: which page an url opens is the link's business,
|
|
30348
|
+
// and it is settled before the binder renders anything.
|
|
30349
|
+
const useReportCurrentToBinderItem = current => {
|
|
30350
|
+
const reportCurrent = useContext(BinderItemContext);
|
|
30351
|
+
useLayoutEffect(() => {
|
|
30352
|
+
if (!reportCurrent) {
|
|
30353
|
+
return undefined;
|
|
30354
|
+
}
|
|
30355
|
+
reportCurrent(current);
|
|
30356
|
+
return () => {
|
|
30357
|
+
reportCurrent(false);
|
|
30358
|
+
};
|
|
30359
|
+
}, [reportCurrent, current]);
|
|
30360
|
+
};
|
|
30314
30361
|
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
30362
|
const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
|
|
30316
30363
|
Object.assign(PSEUDO_CLASSES, {
|
|
@@ -30322,9 +30369,6 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
30322
30369
|
},
|
|
30323
30370
|
":-navi-href-anchor": {
|
|
30324
30371
|
attribute: "data-href-anchor"
|
|
30325
|
-
},
|
|
30326
|
-
":-navi-href-current": {
|
|
30327
|
-
attribute: "data-href-current"
|
|
30328
30372
|
}
|
|
30329
30373
|
});
|
|
30330
30374
|
|
|
@@ -30400,7 +30444,7 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
30400
30444
|
* @param {boolean} [props.readOnly]
|
|
30401
30445
|
*/
|
|
30402
30446
|
const Link = props => {
|
|
30403
|
-
import.meta.css = [css$
|
|
30447
|
+
import.meta.css = [css$P, "@jsenv/navi/src/nav/link/link.jsx"];
|
|
30404
30448
|
if (props.route) {
|
|
30405
30449
|
return jsx(LinkWithRoute, {
|
|
30406
30450
|
...props
|
|
@@ -30489,6 +30533,7 @@ const LinkPlain = props => {
|
|
|
30489
30533
|
isCurrent
|
|
30490
30534
|
} = getHrefTargetInfo(href);
|
|
30491
30535
|
const innerCurrent = current || isCurrent;
|
|
30536
|
+
useReportCurrentToBinderItem(innerCurrent);
|
|
30492
30537
|
controlHostProps.basePseudoState = {
|
|
30493
30538
|
...basePseudoState,
|
|
30494
30539
|
":visited": visited,
|
|
@@ -30634,7 +30679,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
30634
30679
|
* TabList component with support for horizontal and vertical layouts
|
|
30635
30680
|
* https://dribbble.com/search/tabs
|
|
30636
30681
|
*/
|
|
30637
|
-
const css$
|
|
30682
|
+
const css$O = /* css */`
|
|
30638
30683
|
@layer navi {
|
|
30639
30684
|
.navi_nav {
|
|
30640
30685
|
--nav-border: none;
|
|
@@ -30727,20 +30772,60 @@ const css$M = /* css */`
|
|
|
30727
30772
|
}
|
|
30728
30773
|
}
|
|
30729
30774
|
|
|
30730
|
-
|
|
30731
|
-
|
|
30775
|
+
/* Folder tabs: the current tab and the panel share one surface. Every tab
|
|
30776
|
+
carries the panel's line on the side facing it, and the current one
|
|
30777
|
+
paints that side with the panel background instead, so the line opens
|
|
30778
|
+
there. The negative margin makes the two lines overlap — without it the
|
|
30779
|
+
panel's own line would still be drawn under the tabs; and since a tab
|
|
30780
|
+
background is painted under its border too, a tab with a background of
|
|
30781
|
+
its own would otherwise hide the panel's line over its width.
|
|
30782
|
+
|
|
30783
|
+
The line is 1px and never thicker: past that, the corner where a square
|
|
30784
|
+
tab meets the panel shows the miter as a visible notch. */
|
|
30785
|
+
&[data-panel-position] {
|
|
30786
|
+
--nav-border-color: transparent;
|
|
30787
|
+
--nav-tab-border-radius: 0px;
|
|
30788
|
+
|
|
30789
|
+
--x-nav-panel-background: var(
|
|
30790
|
+
--nav-panel-background,
|
|
30791
|
+
var(--link-background-current, white)
|
|
30792
|
+
);
|
|
30793
|
+
|
|
30732
30794
|
position: relative;
|
|
30733
30795
|
z-index: 1;
|
|
30734
30796
|
|
|
30735
30797
|
.navi_link {
|
|
30736
|
-
border:
|
|
30798
|
+
border: 1px solid transparent;
|
|
30799
|
+
|
|
30800
|
+
&[data-href-current] {
|
|
30801
|
+
border-color: var(--nav-border-color);
|
|
30802
|
+
}
|
|
30803
|
+
}
|
|
30804
|
+
|
|
30805
|
+
&[data-panel-position="after"] {
|
|
30806
|
+
margin-bottom: -1px;
|
|
30737
30807
|
|
|
30738
|
-
|
|
30739
|
-
border-color:
|
|
30740
|
-
border-bottom-color: var(--nav-background);
|
|
30808
|
+
.navi_link {
|
|
30809
|
+
border-bottom-color: var(--nav-border-color);
|
|
30741
30810
|
|
|
30742
|
-
|
|
30743
|
-
|
|
30811
|
+
&[data-href-current] {
|
|
30812
|
+
border-bottom-color: var(--x-nav-panel-background);
|
|
30813
|
+
border-top-left-radius: var(--nav-tab-border-radius);
|
|
30814
|
+
border-top-right-radius: var(--nav-tab-border-radius);
|
|
30815
|
+
}
|
|
30816
|
+
}
|
|
30817
|
+
}
|
|
30818
|
+
&[data-panel-position="before"] {
|
|
30819
|
+
margin-top: -1px;
|
|
30820
|
+
|
|
30821
|
+
.navi_link {
|
|
30822
|
+
border-top-color: var(--nav-border-color);
|
|
30823
|
+
|
|
30824
|
+
&[data-href-current] {
|
|
30825
|
+
border-top-color: var(--x-nav-panel-background);
|
|
30826
|
+
border-bottom-right-radius: var(--nav-tab-border-radius);
|
|
30827
|
+
border-bottom-left-radius: var(--nav-tab-border-radius);
|
|
30828
|
+
}
|
|
30744
30829
|
}
|
|
30745
30830
|
}
|
|
30746
30831
|
}
|
|
@@ -30766,11 +30851,10 @@ const Nav = ({
|
|
|
30766
30851
|
expandX,
|
|
30767
30852
|
linkBorderRadiusInherit,
|
|
30768
30853
|
panelPosition,
|
|
30769
|
-
// before or after
|
|
30770
|
-
panelBorderConnection,
|
|
30854
|
+
// "before" or "after": which side the panel sits on, turning the nav into folder tabs
|
|
30771
30855
|
...props
|
|
30772
30856
|
}) => {
|
|
30773
|
-
import.meta.css = [css$
|
|
30857
|
+
import.meta.css = [css$O, "@jsenv/navi/src/nav/link/nav.jsx"];
|
|
30774
30858
|
children = toChildArray(children);
|
|
30775
30859
|
return jsx(Box, {
|
|
30776
30860
|
as: "nav",
|
|
@@ -30781,7 +30865,6 @@ const Nav = ({
|
|
|
30781
30865
|
"data-expand": expand || expandX ? "" : undefined,
|
|
30782
30866
|
"data-vertical": vertical ? "" : undefined,
|
|
30783
30867
|
"data-panel-position": panelPosition,
|
|
30784
|
-
"data-panel-border-connection": panelBorderConnection ? "" : undefined,
|
|
30785
30868
|
expand: expand,
|
|
30786
30869
|
expandX: expandX,
|
|
30787
30870
|
spacing: spacing,
|
|
@@ -30794,6 +30877,1285 @@ const Nav = ({
|
|
|
30794
30877
|
});
|
|
30795
30878
|
};
|
|
30796
30879
|
|
|
30880
|
+
/**
|
|
30881
|
+
* The binder outline: one closed path wrapping the tab row AND the page, with
|
|
30882
|
+
* the current tab opening into the page through concave junction curves.
|
|
30883
|
+
*
|
|
30884
|
+
* Why a path instead of CSS borders — CSS cannot do either of these:
|
|
30885
|
+
* 1. Omit one side of a border while keeping the surrounding corners intact
|
|
30886
|
+
* (the current tab has no border on the page side, so it merges with it).
|
|
30887
|
+
* 2. Curve a corner inward (concave), which is what a tab-to-page junction
|
|
30888
|
+
* is. `border-radius` only bulges outward.
|
|
30889
|
+
*
|
|
30890
|
+
* Everything here is in "path space": the main axis (along the tab row) is x
|
|
30891
|
+
* and the cross axis goes from the tabs toward the page, whatever the real
|
|
30892
|
+
* orientation. Tabs left/right reuse the top/bottom builders and transpose the
|
|
30893
|
+
* result at the end.
|
|
30894
|
+
*
|
|
30895
|
+
* The path is a centerline: it runs half a border-width inside the box edges,
|
|
30896
|
+
* so a stroke of `borderWidth` covers exactly the border area the CSS borders
|
|
30897
|
+
* of the inactive tabs occupy, and the two line up.
|
|
30898
|
+
*/
|
|
30899
|
+
|
|
30900
|
+
// Low-level arc helpers: produce SVG arc commands with an explicit sweep flag.
|
|
30901
|
+
// sweep=1 → clockwise arc; sweep=0 → counter-clockwise arc.
|
|
30902
|
+
const arcSweep1 = (r, dx, dy) => {
|
|
30903
|
+
if (r <= 0) {
|
|
30904
|
+
return `l ${dx},${dy}`;
|
|
30905
|
+
}
|
|
30906
|
+
return `a ${r},${r} 0 0 1 ${dx},${dy}`;
|
|
30907
|
+
};
|
|
30908
|
+
const arcSweep0 = (r, dx, dy) => {
|
|
30909
|
+
if (r <= 0) {
|
|
30910
|
+
return `l ${dx},${dy}`;
|
|
30911
|
+
}
|
|
30912
|
+
return `a ${r},${r} 0 0 0 ${dx},${dy}`;
|
|
30913
|
+
};
|
|
30914
|
+
// Semantic aliases used when drawing the CW outer path.
|
|
30915
|
+
// In a CW path: convex (outward) corners use sweep=1, concave (inward) use sweep=0.
|
|
30916
|
+
const arcConvex = arcSweep1;
|
|
30917
|
+
const arcConcave = arcSweep0;
|
|
30918
|
+
|
|
30919
|
+
// Swaps x and y in a command list produced by the path builders (only
|
|
30920
|
+
// M/h/v/l/a/Z commands, in the exact format they emit). Swapping axes is a
|
|
30921
|
+
// mirror transform, so arc sweep flags must flip.
|
|
30922
|
+
const transposePath = (commands) =>
|
|
30923
|
+
commands.map((command) => {
|
|
30924
|
+
const parts = command.split(" ");
|
|
30925
|
+
const type = parts[0];
|
|
30926
|
+
if (type === "M" || type === "l") {
|
|
30927
|
+
const [x, y] = parts[1].split(",");
|
|
30928
|
+
return `${type} ${y},${x}`;
|
|
30929
|
+
}
|
|
30930
|
+
if (type === "h") {
|
|
30931
|
+
return `v ${parts[1]}`;
|
|
30932
|
+
}
|
|
30933
|
+
if (type === "v") {
|
|
30934
|
+
return `h ${parts[1]}`;
|
|
30935
|
+
}
|
|
30936
|
+
if (type === "a") {
|
|
30937
|
+
const sweep = parts[4] === "1" ? "0" : "1";
|
|
30938
|
+
const [dx, dy] = parts[5].split(",");
|
|
30939
|
+
return `a ${parts[1]} ${parts[2]} ${parts[3]} ${sweep} ${dy},${dx}`;
|
|
30940
|
+
}
|
|
30941
|
+
return command;
|
|
30942
|
+
});
|
|
30943
|
+
|
|
30944
|
+
/**
|
|
30945
|
+
* @param {object} params
|
|
30946
|
+
* @param {number} params.tabX - Where the current tab starts along the tab row.
|
|
30947
|
+
* @param {number} params.tabWidth - Its size along the tab row.
|
|
30948
|
+
* @param {number} params.tabRowHeight - From the outer edge of the tab row to
|
|
30949
|
+
* the junction line where tabs meet the page.
|
|
30950
|
+
* @param {number} params.panelWidth - The whole binder, along the tab row.
|
|
30951
|
+
* @param {number} params.panelHeight - From the junction line to the far edge
|
|
30952
|
+
* of the page.
|
|
30953
|
+
* @param {number} params.gapBeforeTabs - Free room before the first tab, and
|
|
30954
|
+
* @param {number} params.gapAfterTabs - after the last one: a page corner on
|
|
30955
|
+
* the tab-row side is only rounded when its gap has room for the radius.
|
|
30956
|
+
* @param {number} params.outerRadius - The corners of the shape as a whole:
|
|
30957
|
+
* the page's, and the ones the tab row shares with it. Square when the
|
|
30958
|
+
* binder fills its container, rounded when it sits in a rounded one.
|
|
30959
|
+
* @param {number} params.tabRadius - The tabs' own corners, away from the
|
|
30960
|
+
* page, and the concave curves where a tab opens into it. Independent: tabs
|
|
30961
|
+
* can be rounded inside a square binder, and the other way round.
|
|
30962
|
+
* @param {"top"|"bottom"|"left"|"right"} params.tabsPosition
|
|
30963
|
+
* @returns {string} An SVG path `d` attribute.
|
|
30964
|
+
*/
|
|
30965
|
+
const buildBinderPath = ({
|
|
30966
|
+
tabX,
|
|
30967
|
+
tabWidth,
|
|
30968
|
+
tabRowHeight,
|
|
30969
|
+
panelWidth,
|
|
30970
|
+
panelHeight,
|
|
30971
|
+
borderWidth,
|
|
30972
|
+
outerRadius,
|
|
30973
|
+
tabRadius,
|
|
30974
|
+
gapBeforeTabs,
|
|
30975
|
+
gapAfterTabs,
|
|
30976
|
+
tabsPosition,
|
|
30977
|
+
}) => {
|
|
30978
|
+
const h = borderWidth / 2;
|
|
30979
|
+
const withoutBorder = (radius) => (radius - h < 0 ? 0 : radius - h);
|
|
30980
|
+
const r = withoutBorder(tabRadius);
|
|
30981
|
+
const rOuter = withoutBorder(outerRadius);
|
|
30982
|
+
// Page corners on the tab-row side only get rounded when the gap before /
|
|
30983
|
+
// after the tabs leaves room for the full radius.
|
|
30984
|
+
const gapAfter = gapAfterTabs < 0 ? 0 : gapAfterTabs;
|
|
30985
|
+
const rTR = gapAfter >= rOuter ? rOuter : 0;
|
|
30986
|
+
const gapBefore = gapBeforeTabs < 0 ? 0 : gapBeforeTabs;
|
|
30987
|
+
const rTL = gapBefore >= rOuter ? rOuter : 0;
|
|
30988
|
+
|
|
30989
|
+
const params = {
|
|
30990
|
+
tabX,
|
|
30991
|
+
tabWidth,
|
|
30992
|
+
tabRowHeight,
|
|
30993
|
+
panelWidth,
|
|
30994
|
+
panelHeight,
|
|
30995
|
+
h,
|
|
30996
|
+
r,
|
|
30997
|
+
rOuter,
|
|
30998
|
+
rTR,
|
|
30999
|
+
rTL,
|
|
31000
|
+
};
|
|
31001
|
+
const d =
|
|
31002
|
+
tabsPosition === "top" || tabsPosition === "left"
|
|
31003
|
+
? buildBinderCenterlinePathAbove(params)
|
|
31004
|
+
: buildBinderCenterlinePathBelow(params);
|
|
31005
|
+
const commands =
|
|
31006
|
+
tabsPosition === "left" || tabsPosition === "right" ? transposePath(d) : d;
|
|
31007
|
+
return commands.join(" ");
|
|
31008
|
+
};
|
|
31009
|
+
|
|
31010
|
+
// Builds the centerline path when tabs are ABOVE the panel.
|
|
31011
|
+
// CW path: M(tabLeft+r, top) → tab top → tab TR → [right junction] → panel TR
|
|
31012
|
+
// → panel right ↓ → panel BR → panel bottom ← → panel BL → panel left ↑ → [left junction] → tab TL → Z
|
|
31013
|
+
const buildBinderCenterlinePathAbove = ({
|
|
31014
|
+
tabX,
|
|
31015
|
+
tabWidth,
|
|
31016
|
+
tabRowHeight,
|
|
31017
|
+
panelWidth,
|
|
31018
|
+
panelHeight,
|
|
31019
|
+
h,
|
|
31020
|
+
r,
|
|
31021
|
+
rOuter,
|
|
31022
|
+
rTR,
|
|
31023
|
+
rTL,
|
|
31024
|
+
}) => {
|
|
31025
|
+
const hasRightJunction = tabX + tabWidth < panelWidth;
|
|
31026
|
+
const hasLeftJunction = tabX > 0;
|
|
31027
|
+
|
|
31028
|
+
const left = h;
|
|
31029
|
+
const top = h;
|
|
31030
|
+
const right = panelWidth - h;
|
|
31031
|
+
const bottom = tabRowHeight + panelHeight - h;
|
|
31032
|
+
const tabLeft = tabX + h;
|
|
31033
|
+
const tabRight = tabX + tabWidth - h;
|
|
31034
|
+
const junctionY = tabRowHeight - h;
|
|
31035
|
+
|
|
31036
|
+
const rVSpace = junctionY - top - r;
|
|
31037
|
+
const rTotalHSpace = right - tabRight;
|
|
31038
|
+
const rFits = hasRightJunction && rVSpace >= r && rTotalHSpace >= r + rTR;
|
|
31039
|
+
|
|
31040
|
+
const lVSpace = junctionY - top - r;
|
|
31041
|
+
const lTotalHSpace = tabLeft - left - rTL; // capped by gap left of first tab
|
|
31042
|
+
// lFits: room for panel TL arc (rTL) + concave junction arc (r)
|
|
31043
|
+
const lFits = hasLeftJunction && lVSpace >= r && lTotalHSpace >= r;
|
|
31044
|
+
// lTight: only room for panel TL arc, skip concave — favor panel rounding
|
|
31045
|
+
const lTight = hasLeftJunction && lVSpace >= r && lTotalHSpace >= 0;
|
|
31046
|
+
|
|
31047
|
+
const d = [];
|
|
31048
|
+
d.push(`M ${tabLeft + r},${top}`);
|
|
31049
|
+
d.push(`h ${tabRight - r - (tabLeft + r)}`);
|
|
31050
|
+
|
|
31051
|
+
// ── Right side ────────────────────────────────────────────────────────────
|
|
31052
|
+
if (!hasRightJunction) {
|
|
31053
|
+
d.push(arcConvex(r, r, r));
|
|
31054
|
+
d.push(`v ${bottom - top - r - rOuter}`);
|
|
31055
|
+
} else if (rFits) {
|
|
31056
|
+
d.push(arcConvex(r, r, r)); // tab TR: right→down
|
|
31057
|
+
d.push(`v ${rVSpace - r}`); // tab right ↓ to concave arc start
|
|
31058
|
+
d.push(arcConcave(r, r, r)); // junction concave: down→right
|
|
31059
|
+
d.push(`h ${rTotalHSpace - r - rTR}`); // panel top →
|
|
31060
|
+
d.push(arcConvex(rTR, rTR, rTR)); // panel TR: right→down
|
|
31061
|
+
d.push(`v ${bottom - junctionY - rTR - rOuter}`); // panel right ↓
|
|
31062
|
+
} else {
|
|
31063
|
+
d.push(arcConvex(r, r, r)); // tab TR: right→down
|
|
31064
|
+
d.push(`v ${rVSpace}`); // straight down to junctionY
|
|
31065
|
+
d.push(`h ${rTotalHSpace - rTR}`); // panel top →
|
|
31066
|
+
d.push(arcConvex(rTR, rTR, rTR)); // panel TR: right→down (clamped)
|
|
31067
|
+
d.push(`v ${bottom - junctionY - rTR - rOuter}`); // panel right ↓
|
|
31068
|
+
}
|
|
31069
|
+
|
|
31070
|
+
// ── Panel BR → bottom → BL ────────────────────────────────────────────────
|
|
31071
|
+
d.push(arcConvex(rOuter, -rOuter, rOuter));
|
|
31072
|
+
d.push(`h -${right - left - 2 * rOuter}`);
|
|
31073
|
+
d.push(arcConvex(rOuter, -rOuter, -rOuter));
|
|
31074
|
+
|
|
31075
|
+
// ── Panel left ↑ + left junction back to M ───────────────────────────────
|
|
31076
|
+
if (!hasLeftJunction) {
|
|
31077
|
+
d.push(`v -${bottom - top - rOuter - r}`);
|
|
31078
|
+
d.push(arcConvex(r, r, -r)); // panel TL = tab TL: up→right
|
|
31079
|
+
} else if (lFits) {
|
|
31080
|
+
d.push(`v -${bottom - junctionY - rOuter - rTL}`); // panel left ↑ to (left, junctionY+rTL)
|
|
31081
|
+
d.push(arcConvex(rTL, rTL, -rTL)); // panel TL: up→right
|
|
31082
|
+
d.push(`h ${lTotalHSpace - r}`); // panel top → to (tabLeft-r, junctionY)
|
|
31083
|
+
d.push(arcConcave(r, r, -r)); // junction concave: right→up
|
|
31084
|
+
d.push(`v -${lVSpace - r}`); // tab left ↑
|
|
31085
|
+
d.push(arcConvex(r, r, -r)); // tab TL: up→right
|
|
31086
|
+
} else if (lTight) {
|
|
31087
|
+
// Panel TL rounded, no concave — square tab corner
|
|
31088
|
+
d.push(`v -${bottom - junctionY - rOuter - rTL}`); // panel left ↑ to (left, junctionY+rTL)
|
|
31089
|
+
d.push(arcConvex(rTL, rTL, -rTL)); // panel TL: up→right
|
|
31090
|
+
d.push(`h ${lTotalHSpace}`); // panel top → to (tabLeft, junctionY)
|
|
31091
|
+
d.push(`v -${lVSpace}`); // tab left ↑
|
|
31092
|
+
d.push(arcConvex(r, r, -r)); // tab TL: up→right
|
|
31093
|
+
} else {
|
|
31094
|
+
d.push(`v -${bottom - junctionY - rOuter}`); // panel left ↑ to junctionY
|
|
31095
|
+
d.push(`h ${lTotalHSpace}`); // panel top →
|
|
31096
|
+
d.push(`v -${lVSpace}`); // tab left ↑
|
|
31097
|
+
d.push(arcConvex(r, r, -r)); // tab TL: up→right
|
|
31098
|
+
}
|
|
31099
|
+
|
|
31100
|
+
d.push("Z");
|
|
31101
|
+
return d;
|
|
31102
|
+
};
|
|
31103
|
+
|
|
31104
|
+
// Builds the centerline path when tabs are BELOW the panel.
|
|
31105
|
+
// CW path: M(left+r, top) → panel top → panel TR → panel right ↓ → [right junction] →
|
|
31106
|
+
// tab right ↓ → tab BR → tab bottom ← → [left junction] → tab BL → tab left ↑ → panel left ↑ → panel TL → Z
|
|
31107
|
+
//
|
|
31108
|
+
// Right junction: panel right ↓ to junctionY (square corner), left along panel bottom gap,
|
|
31109
|
+
// concave notch arc (left→down), tab right ↓.
|
|
31110
|
+
// Left junction: tab left ↑, concave notch arc (up→left), left along panel bottom gap to panel left
|
|
31111
|
+
// (square corner), panel left ↑.
|
|
31112
|
+
const buildBinderCenterlinePathBelow = ({
|
|
31113
|
+
tabX,
|
|
31114
|
+
tabWidth,
|
|
31115
|
+
tabRowHeight,
|
|
31116
|
+
panelWidth,
|
|
31117
|
+
panelHeight,
|
|
31118
|
+
h,
|
|
31119
|
+
r,
|
|
31120
|
+
rOuter,
|
|
31121
|
+
rTR,
|
|
31122
|
+
rTL,
|
|
31123
|
+
}) => {
|
|
31124
|
+
const hasRightJunction = tabX + tabWidth < panelWidth;
|
|
31125
|
+
const hasLeftJunction = tabX > 0;
|
|
31126
|
+
|
|
31127
|
+
const left = h;
|
|
31128
|
+
const top = h;
|
|
31129
|
+
const right = panelWidth - h;
|
|
31130
|
+
const bottom = panelHeight + tabRowHeight - h;
|
|
31131
|
+
const tabLeft = tabX + h;
|
|
31132
|
+
const tabRight = tabX + tabWidth - h;
|
|
31133
|
+
const junctionY = panelHeight - h;
|
|
31134
|
+
|
|
31135
|
+
// Vertical room on the page's right/left sides, between its top corner
|
|
31136
|
+
// (outer) and the junction.
|
|
31137
|
+
const rPanelVSpace = junctionY - top - rOuter;
|
|
31138
|
+
|
|
31139
|
+
// Right junction: the page corner beside the last tab uses rTR.
|
|
31140
|
+
const rTotalHSpace = right - tabRight - rTR;
|
|
31141
|
+
// rFits: room for the page corner arc (rTR) + the concave junction arc (r)
|
|
31142
|
+
const rFits = hasRightJunction && rPanelVSpace >= r && rTotalHSpace >= r;
|
|
31143
|
+
// rTight: only room for the page corner arc, skip concave — favor the page
|
|
31144
|
+
const rTight = hasRightJunction && rPanelVSpace >= r && rTotalHSpace >= 0;
|
|
31145
|
+
|
|
31146
|
+
// Left junction: the page corner beside the first tab uses rTL.
|
|
31147
|
+
const lTotalHSpace = tabLeft - left - rTL;
|
|
31148
|
+
const lFits = hasLeftJunction && rPanelVSpace >= r && lTotalHSpace >= r;
|
|
31149
|
+
const lTight = hasLeftJunction && rPanelVSpace >= r && lTotalHSpace >= 0;
|
|
31150
|
+
|
|
31151
|
+
const d = [];
|
|
31152
|
+
|
|
31153
|
+
// M: start after the page's TL arc, going right along the page top
|
|
31154
|
+
d.push(`M ${left + rOuter},${top}`);
|
|
31155
|
+
d.push(`h ${right - left - 2 * rOuter}`); // page top →
|
|
31156
|
+
|
|
31157
|
+
// ── Right side: page right ↓ then (junction or direct) tab right ↓ ──────
|
|
31158
|
+
if (!hasRightJunction) {
|
|
31159
|
+
d.push(arcConvex(rOuter, rOuter, rOuter)); // page TR: right→down
|
|
31160
|
+
d.push(`v ${bottom - top - rOuter - r}`); // full right side ↓
|
|
31161
|
+
d.push(arcConvex(r, -r, r)); // tab BR: down→left
|
|
31162
|
+
} else if (rFits) {
|
|
31163
|
+
d.push(arcConvex(rOuter, rOuter, rOuter)); // page TR: right→down
|
|
31164
|
+
d.push(`v ${rPanelVSpace - rTR}`); // page right ↓ to (right, junctionY-rTR)
|
|
31165
|
+
d.push(arcConvex(rTR, -rTR, rTR)); // page corner: down→left
|
|
31166
|
+
d.push(`h -${rTotalHSpace - r}`); // page bottom ← to (tabRight+r, junctionY)
|
|
31167
|
+
d.push(arcConcave(r, -r, r)); // junction concave: left→down
|
|
31168
|
+
d.push(`v ${bottom - junctionY - 2 * r}`); // tab right ↓ to (tabRight, bottom-r)
|
|
31169
|
+
d.push(arcConvex(r, -r, r)); // tab BR: down→left
|
|
31170
|
+
} else if (rTight) {
|
|
31171
|
+
// Page corner rounded, no concave — square tab corner
|
|
31172
|
+
d.push(arcConvex(rOuter, rOuter, rOuter)); // page TR: right→down
|
|
31173
|
+
d.push(`v ${rPanelVSpace - rTR}`); // page right ↓ to (right, junctionY-rTR)
|
|
31174
|
+
d.push(arcConvex(rTR, -rTR, rTR)); // page corner: down→left
|
|
31175
|
+
d.push(`h -${rTotalHSpace}`); // page bottom ← to (tabRight, junctionY)
|
|
31176
|
+
d.push(`v ${bottom - junctionY - r}`); // tab right ↓ to (tabRight, bottom-r)
|
|
31177
|
+
d.push(arcConvex(r, -r, r)); // tab BR: down→left
|
|
31178
|
+
} else {
|
|
31179
|
+
// Very tight: all square corners
|
|
31180
|
+
d.push(arcConvex(rOuter, rOuter, rOuter)); // page TR: right→down
|
|
31181
|
+
d.push(`v ${rPanelVSpace}`); // page right ↓ to junctionY
|
|
31182
|
+
d.push(`h -${right - tabRight}`); // page bottom ← to tabRight
|
|
31183
|
+
d.push(`v ${bottom - junctionY - r}`); // tab right ↓ to (tabRight, bottom-r)
|
|
31184
|
+
d.push(arcConvex(r, -r, r)); // tab BR: down→left
|
|
31185
|
+
}
|
|
31186
|
+
|
|
31187
|
+
// ── Tab bottom ← ──────────────────────────────────────────────────────────
|
|
31188
|
+
d.push(`h -${tabRight - r - (tabLeft + r)}`); // tab bottom ←
|
|
31189
|
+
|
|
31190
|
+
// ── Left side: tab BL → tab left ↑ → (junction or direct) → page left ↑ ──
|
|
31191
|
+
if (!hasLeftJunction) {
|
|
31192
|
+
d.push(arcConvex(r, -r, -r)); // tab BL: left→up
|
|
31193
|
+
d.push(`v -${bottom - top - rOuter - r}`); // full left side ↑
|
|
31194
|
+
d.push(arcConvex(rOuter, rOuter, -rOuter)); // page TL: up→right → M
|
|
31195
|
+
} else if (lFits) {
|
|
31196
|
+
d.push(arcConvex(r, -r, -r)); // tab BL: left→up
|
|
31197
|
+
d.push(`v -${bottom - junctionY - 2 * r}`); // tab left ↑ to (tabLeft, junctionY+r)
|
|
31198
|
+
d.push(arcConcave(r, -r, -r)); // junction concave: up→left
|
|
31199
|
+
d.push(`h -${lTotalHSpace - r}`); // page bottom ← to (left+rTL, junctionY)
|
|
31200
|
+
d.push(arcConvex(rTL, -rTL, -rTL)); // page corner: left→up
|
|
31201
|
+
d.push(`v -${rPanelVSpace - rTL}`); // page left ↑ to (left, top+rOuter)
|
|
31202
|
+
d.push(arcConvex(rOuter, rOuter, -rOuter)); // page TL: up→right → M
|
|
31203
|
+
} else if (lTight) {
|
|
31204
|
+
// Page corner rounded, no concave — square tab corner
|
|
31205
|
+
d.push(arcConvex(r, -r, -r)); // tab BL: left→up
|
|
31206
|
+
d.push(`v -${bottom - junctionY - r}`); // tab left ↑ to (tabLeft, junctionY)
|
|
31207
|
+
d.push(`h -${lTotalHSpace}`); // page bottom ← to (left+rTL, junctionY)
|
|
31208
|
+
d.push(arcConvex(rTL, -rTL, -rTL)); // page corner: left→up
|
|
31209
|
+
d.push(`v -${rPanelVSpace - rTL}`); // page left ↑ to (left, top+rOuter)
|
|
31210
|
+
d.push(arcConvex(rOuter, rOuter, -rOuter)); // page TL: up→right → M
|
|
31211
|
+
} else {
|
|
31212
|
+
// Very tight: all square corners
|
|
31213
|
+
d.push(arcConvex(r, -r, -r)); // tab BL: left→up
|
|
31214
|
+
d.push(`v -${bottom - junctionY - r}`); // tab left ↑ to junctionY
|
|
31215
|
+
d.push(`h -${tabLeft - left}`); // page bottom ← to left
|
|
31216
|
+
d.push(`v -${rPanelVSpace}`); // page left ↑ to (left, top+rOuter)
|
|
31217
|
+
d.push(arcConvex(rOuter, rOuter, -rOuter)); // page TL: up→right → M
|
|
31218
|
+
}
|
|
31219
|
+
|
|
31220
|
+
d.push("Z");
|
|
31221
|
+
return d;
|
|
31222
|
+
};
|
|
31223
|
+
|
|
31224
|
+
installImportMetaCssBuild(import.meta);/**
|
|
31225
|
+
* A binder: tabs and the page they open, drawn as one shape.
|
|
31226
|
+
*
|
|
31227
|
+
* Unlike a `Nav`, a binder owns its content — the tab and the page it shows are
|
|
31228
|
+
* the same item, declared together:
|
|
31229
|
+
*
|
|
31230
|
+
* <Binder>
|
|
31231
|
+
* <Binder.Item value="a" label="Approved">…the page…</Binder.Item>
|
|
31232
|
+
* </Binder>
|
|
31233
|
+
*
|
|
31234
|
+
* Layers, and why they are what they are:
|
|
31235
|
+
* - The outline (tab row + page, with the concave junctions around the current
|
|
31236
|
+
* tab) is a single SVG path — see binder_path.js for why CSS borders cannot
|
|
31237
|
+
* draw it. It covers the whole binder and sits above the inactive tabs but
|
|
31238
|
+
* below the current tab and the page.
|
|
31239
|
+
* - The inactive tabs keep plain CSS borders: the path only runs along the
|
|
31240
|
+
* current tab, so the top of an inactive tab is its own to draw. Their border
|
|
31241
|
+
* width matches the path's stroke and lands in the same band, which is what
|
|
31242
|
+
* makes the two read as one continuous line.
|
|
31243
|
+
* - Every tab reserves that band on the page side with a margin, so the stroke
|
|
31244
|
+
* never lands on a tab's own edge.
|
|
31245
|
+
*
|
|
31246
|
+
* Border width participates in layout (it is added to the tab and page
|
|
31247
|
+
* padding): a thick border grows the binder rather than eating into the text.
|
|
31248
|
+
*/
|
|
31249
|
+
const css$N = /* css */`
|
|
31250
|
+
@layer navi {
|
|
31251
|
+
.navi_binder {
|
|
31252
|
+
--binder-border-width: var(--navi-control-border-width);
|
|
31253
|
+
--binder-border-radius: var(--navi-control-border-radius);
|
|
31254
|
+
/* The tabs' own corners. Their own knob because the two answer different
|
|
31255
|
+
questions: the outer radius follows the container the binder sits in
|
|
31256
|
+
(square when it fills the page), the tab radius is the tabs' shape. */
|
|
31257
|
+
--binder-tab-border-radius: var(--binder-border-radius);
|
|
31258
|
+
--binder-border-color: var(--navi-control-border-color);
|
|
31259
|
+
--binder-background: var(--navi-surface-color);
|
|
31260
|
+
/* The tabs that are not open read as the same paper, slightly shaded:
|
|
31261
|
+
derived from the border color so one override themes both, in light
|
|
31262
|
+
and dark alike. */
|
|
31263
|
+
--binder-tab-background: color-mix(
|
|
31264
|
+
in srgb,
|
|
31265
|
+
var(--binder-border-color) 12%,
|
|
31266
|
+
var(--binder-background)
|
|
31267
|
+
);
|
|
31268
|
+
--binder-tab-border-color: var(--binder-border-color);
|
|
31269
|
+
--binder-tab-color: var(--navi-color-secondary);
|
|
31270
|
+
--binder-tab-background-hover: color-mix(
|
|
31271
|
+
in srgb,
|
|
31272
|
+
var(--binder-border-color) 22%,
|
|
31273
|
+
var(--binder-background)
|
|
31274
|
+
);
|
|
31275
|
+
--binder-padding-x: var(--navi-s);
|
|
31276
|
+
--binder-padding-y: var(--navi-xs);
|
|
31277
|
+
--binder-page-padding: var(--navi-m);
|
|
31278
|
+
}
|
|
31279
|
+
}
|
|
31280
|
+
|
|
31281
|
+
.navi_binder {
|
|
31282
|
+
position: relative;
|
|
31283
|
+
display: flex;
|
|
31284
|
+
width: fit-content;
|
|
31285
|
+
align-items: stretch;
|
|
31286
|
+
|
|
31287
|
+
&[data-tabs-position="top"] {
|
|
31288
|
+
flex-direction: column;
|
|
31289
|
+
}
|
|
31290
|
+
&[data-tabs-position="bottom"] {
|
|
31291
|
+
flex-direction: column-reverse;
|
|
31292
|
+
}
|
|
31293
|
+
&[data-tabs-position="left"] {
|
|
31294
|
+
flex-direction: row;
|
|
31295
|
+
}
|
|
31296
|
+
&[data-tabs-position="right"] {
|
|
31297
|
+
flex-direction: row-reverse;
|
|
31298
|
+
}
|
|
31299
|
+
}
|
|
31300
|
+
|
|
31301
|
+
.navi_binder_outline {
|
|
31302
|
+
position: absolute;
|
|
31303
|
+
inset: 0;
|
|
31304
|
+
z-index: 1;
|
|
31305
|
+
pointer-events: none;
|
|
31306
|
+
overflow: visible;
|
|
31307
|
+
}
|
|
31308
|
+
|
|
31309
|
+
.navi_binder_tabs {
|
|
31310
|
+
display: flex;
|
|
31311
|
+
/* The page decides how wide the binder is; the tab row only fills it.
|
|
31312
|
+
Sizing it to 0 keeps its labels out of the binder's intrinsic size (a
|
|
31313
|
+
long one would otherwise stretch the whole thing) while min-size brings
|
|
31314
|
+
it back to the width the page settled on — which is what leaves the
|
|
31315
|
+
labels something to truncate against. */
|
|
31316
|
+
.navi_binder[data-tabs-position="top"] &,
|
|
31317
|
+
.navi_binder[data-tabs-position="bottom"] & {
|
|
31318
|
+
width: 0;
|
|
31319
|
+
min-width: 100%;
|
|
31320
|
+
}
|
|
31321
|
+
.navi_binder[data-tabs-position="left"] &,
|
|
31322
|
+
.navi_binder[data-tabs-position="right"] & {
|
|
31323
|
+
height: 0;
|
|
31324
|
+
min-height: 100%;
|
|
31325
|
+
}
|
|
31326
|
+
/* Tabs of unequal size line up on the page side, so the junction band is
|
|
31327
|
+
one straight line whatever their content. */
|
|
31328
|
+
.navi_binder[data-tabs-position="top"] & {
|
|
31329
|
+
align-items: flex-end;
|
|
31330
|
+
}
|
|
31331
|
+
.navi_binder[data-tabs-position="bottom"] & {
|
|
31332
|
+
align-items: flex-start;
|
|
31333
|
+
}
|
|
31334
|
+
/* A column of tabs is as wide as its widest one, all of them that wide, so
|
|
31335
|
+
the junction band is one straight line here too. */
|
|
31336
|
+
.navi_binder[data-tabs-position="left"] &,
|
|
31337
|
+
.navi_binder[data-tabs-position="right"] & {
|
|
31338
|
+
flex-direction: column;
|
|
31339
|
+
align-items: stretch;
|
|
31340
|
+
}
|
|
31341
|
+
}
|
|
31342
|
+
|
|
31343
|
+
.navi_binder_tab {
|
|
31344
|
+
position: relative;
|
|
31345
|
+
z-index: 0;
|
|
31346
|
+
/* A flex item refuses to go under its content unless told to; without this
|
|
31347
|
+
a long label widens the tab row instead of truncating. */
|
|
31348
|
+
min-width: 0;
|
|
31349
|
+
min-height: 0;
|
|
31350
|
+
/* The border is part of the tab's box on every tab, current one included
|
|
31351
|
+
(transparent there — the path draws it), so opening a tab never moves
|
|
31352
|
+
anything. */
|
|
31353
|
+
padding: var(--binder-padding-y) var(--binder-padding-x);
|
|
31354
|
+
flex: 0 1 auto;
|
|
31355
|
+
color: var(--binder-tab-color);
|
|
31356
|
+
font: inherit;
|
|
31357
|
+
text-align: center;
|
|
31358
|
+
text-decoration: none;
|
|
31359
|
+
background: var(--binder-tab-background);
|
|
31360
|
+
border: var(--binder-border-width) solid var(--binder-tab-border-color);
|
|
31361
|
+
cursor: pointer;
|
|
31362
|
+
/* "stretch" is an equal share each, whatever a tab holds — a basis read
|
|
31363
|
+
from the content would hand the longest label the widest tab. */
|
|
31364
|
+
.navi_binder[data-tabs-align="stretch"] & {
|
|
31365
|
+
flex: 1 1 0;
|
|
31366
|
+
}
|
|
31367
|
+
|
|
31368
|
+
/* The navi attributes rather than the CSS pseudo-classes: they are what a
|
|
31369
|
+
demo can hold with pseudoState={{ ":focus-visible": true }}. */
|
|
31370
|
+
&[data-hover] {
|
|
31371
|
+
background: var(--binder-tab-background-hover);
|
|
31372
|
+
}
|
|
31373
|
+
/* The ring would be cut by the neighbours and by the outline band around
|
|
31374
|
+
it, so it is drawn inside and lifted above them. */
|
|
31375
|
+
&[data-focus-visible] {
|
|
31376
|
+
z-index: 3;
|
|
31377
|
+
outline: var(--navi-focus-outline-width) solid
|
|
31378
|
+
var(--navi-focus-outline-color);
|
|
31379
|
+
outline-offset: calc(-1 * var(--navi-focus-outline-width));
|
|
31380
|
+
}
|
|
31381
|
+
&[data-current] {
|
|
31382
|
+
z-index: 2;
|
|
31383
|
+
color: inherit;
|
|
31384
|
+
background: transparent;
|
|
31385
|
+
border-color: transparent;
|
|
31386
|
+
cursor: default;
|
|
31387
|
+
}
|
|
31388
|
+
/* Adjacent tabs share one line rather than stacking two borders. */
|
|
31389
|
+
&:not(:first-child) {
|
|
31390
|
+
margin-inline-start: calc(-1 * var(--binder-border-width));
|
|
31391
|
+
}
|
|
31392
|
+
|
|
31393
|
+
/* The margin on the page side is the junction band the path draws in; the
|
|
31394
|
+
corners away from the page are the ones that round. */
|
|
31395
|
+
.navi_binder[data-tabs-position="top"] & {
|
|
31396
|
+
margin-bottom: var(--binder-border-width);
|
|
31397
|
+
border-bottom: none;
|
|
31398
|
+
border-radius: var(--binder-tab-border-radius)
|
|
31399
|
+
var(--binder-tab-border-radius) 0 0;
|
|
31400
|
+
}
|
|
31401
|
+
.navi_binder[data-tabs-position="bottom"] & {
|
|
31402
|
+
margin-top: var(--binder-border-width);
|
|
31403
|
+
border-top: none;
|
|
31404
|
+
border-radius: 0 0 var(--binder-tab-border-radius)
|
|
31405
|
+
var(--binder-tab-border-radius);
|
|
31406
|
+
}
|
|
31407
|
+
.navi_binder[data-tabs-position="left"] & {
|
|
31408
|
+
margin-right: var(--binder-border-width);
|
|
31409
|
+
border-right: none;
|
|
31410
|
+
border-radius: var(--binder-tab-border-radius) 0 0
|
|
31411
|
+
var(--binder-tab-border-radius);
|
|
31412
|
+
}
|
|
31413
|
+
.navi_binder[data-tabs-position="right"] & {
|
|
31414
|
+
margin-left: var(--binder-border-width);
|
|
31415
|
+
border-left: none;
|
|
31416
|
+
border-radius: 0 var(--binder-tab-border-radius)
|
|
31417
|
+
var(--binder-tab-border-radius) 0;
|
|
31418
|
+
}
|
|
31419
|
+
/* A column of tabs shares its main axis with the block direction, where
|
|
31420
|
+
margin-inline-start would collapse the wrong pair. */
|
|
31421
|
+
.navi_binder[data-tabs-position="left"] &:not(:first-child),
|
|
31422
|
+
.navi_binder[data-tabs-position="right"] &:not(:first-child) {
|
|
31423
|
+
margin-inline-start: 0;
|
|
31424
|
+
margin-top: calc(-1 * var(--binder-border-width));
|
|
31425
|
+
}
|
|
31426
|
+
}
|
|
31427
|
+
|
|
31428
|
+
/* A tab that grows with its label would push the binder wider than the page
|
|
31429
|
+
it opens; by default a label stays on one line and is cut with an ellipsis.
|
|
31430
|
+
maxLines={n} lets it use n lines, maxLines={false} lets it wrap freely. */
|
|
31431
|
+
.navi_binder_tab_label {
|
|
31432
|
+
display: block;
|
|
31433
|
+
|
|
31434
|
+
&[data-max-lines="1"] {
|
|
31435
|
+
text-overflow: ellipsis;
|
|
31436
|
+
white-space: nowrap;
|
|
31437
|
+
overflow: hidden;
|
|
31438
|
+
}
|
|
31439
|
+
&[data-max-lines]:not([data-max-lines="1"]) {
|
|
31440
|
+
display: -webkit-box;
|
|
31441
|
+
overflow: hidden;
|
|
31442
|
+
-webkit-box-orient: vertical;
|
|
31443
|
+
-webkit-line-clamp: var(--binder-tab-max-lines);
|
|
31444
|
+
}
|
|
31445
|
+
}
|
|
31446
|
+
|
|
31447
|
+
.navi_binder_page {
|
|
31448
|
+
position: relative;
|
|
31449
|
+
z-index: 2;
|
|
31450
|
+
/* Same reservation as the tabs, on the three sides the path runs along. */
|
|
31451
|
+
padding: calc(var(--binder-page-padding) + var(--binder-border-width));
|
|
31452
|
+
flex: 1;
|
|
31453
|
+
color: inherit;
|
|
31454
|
+
|
|
31455
|
+
/* An app shell: the binder is the window, the page is what scrolls in it,
|
|
31456
|
+
and the tab row stays put without being fixed — it is simply the part of
|
|
31457
|
+
the binder that does not scroll. min-size:0 because a flex item refuses
|
|
31458
|
+
to shrink under its content, which is what makes overflow do nothing. */
|
|
31459
|
+
.navi_binder[data-scrollable-page] & {
|
|
31460
|
+
min-width: 0;
|
|
31461
|
+
min-height: 0;
|
|
31462
|
+
overflow: auto;
|
|
31463
|
+
}
|
|
31464
|
+
|
|
31465
|
+
.navi_binder[data-tabs-position="top"] & {
|
|
31466
|
+
padding-top: var(--binder-page-padding);
|
|
31467
|
+
}
|
|
31468
|
+
.navi_binder[data-tabs-position="bottom"] & {
|
|
31469
|
+
padding-bottom: var(--binder-page-padding);
|
|
31470
|
+
}
|
|
31471
|
+
.navi_binder[data-tabs-position="left"] & {
|
|
31472
|
+
padding-left: var(--binder-page-padding);
|
|
31473
|
+
}
|
|
31474
|
+
.navi_binder[data-tabs-position="right"] & {
|
|
31475
|
+
padding-right: var(--binder-page-padding);
|
|
31476
|
+
}
|
|
31477
|
+
}
|
|
31478
|
+
`;
|
|
31479
|
+
const BinderStyleCSSVars = {
|
|
31480
|
+
borderWidth: "--binder-border-width",
|
|
31481
|
+
borderRadius: "--binder-border-radius",
|
|
31482
|
+
tabBorderRadius: "--binder-tab-border-radius",
|
|
31483
|
+
borderColor: "--binder-border-color",
|
|
31484
|
+
background: "--binder-background",
|
|
31485
|
+
tabBackground: "--binder-tab-background",
|
|
31486
|
+
tabBackgroundHover: "--binder-tab-background-hover",
|
|
31487
|
+
tabBorderColor: "--binder-tab-border-color",
|
|
31488
|
+
tabColor: "--binder-tab-color",
|
|
31489
|
+
paddingX: "--binder-padding-x",
|
|
31490
|
+
paddingY: "--binder-padding-y",
|
|
31491
|
+
pagePadding: "--binder-page-padding"
|
|
31492
|
+
};
|
|
31493
|
+
const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
|
|
31494
|
+
start: "flex-start",
|
|
31495
|
+
center: "center",
|
|
31496
|
+
end: "flex-end",
|
|
31497
|
+
stretch: "stretch"
|
|
31498
|
+
};
|
|
31499
|
+
|
|
31500
|
+
/**
|
|
31501
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
31502
|
+
* value?: any,
|
|
31503
|
+
* defaultValue?: any,
|
|
31504
|
+
* onChange?: (value: any) => void,
|
|
31505
|
+
* tabsPosition?: "top"|"bottom"|"left"|"right",
|
|
31506
|
+
* tabsAlign?: "start"|"center"|"end"|"stretch",
|
|
31507
|
+
* borderWidth?: string|number,
|
|
31508
|
+
* borderRadius?: string|number,
|
|
31509
|
+
* tabBorderRadius?: string|number,
|
|
31510
|
+
* borderColor?: string,
|
|
31511
|
+
* background?: string,
|
|
31512
|
+
* tabBackground?: string,
|
|
31513
|
+
* tabBackgroundHover?: string,
|
|
31514
|
+
* tabBorderColor?: string,
|
|
31515
|
+
* tabColor?: string,
|
|
31516
|
+
* paddingX?: string|number,
|
|
31517
|
+
* paddingY?: string|number,
|
|
31518
|
+
* pagePadding?: string|number,
|
|
31519
|
+
* }>}
|
|
31520
|
+
* @param {any} [props.value] - The open item, when the caller keeps that state.
|
|
31521
|
+
* Leave it out for a binder that keeps its own (see `defaultValue`).
|
|
31522
|
+
* @param {any} [props.defaultValue] - Which item is open to begin with;
|
|
31523
|
+
* defaults to the first one.
|
|
31524
|
+
* @param {"top"|"bottom"|"left"|"right"} [props.tabsPosition="top"] - Which
|
|
31525
|
+
* side of the page the tabs sit on.
|
|
31526
|
+
* @param {"start"|"center"|"end"|"stretch"} [props.tabsAlign="stretch"] - How
|
|
31527
|
+
* the tabs spread along that side.
|
|
31528
|
+
* @param {boolean} [props.scrollablePage] - Makes the page scroll inside the
|
|
31529
|
+
* binder instead of growing it. What an app shell wants: give the binder the
|
|
31530
|
+
* size of the window and the tab row stays in place while the page scrolls
|
|
31531
|
+
* under it — no `position: fixed` anywhere, so the tabs and the page are
|
|
31532
|
+
* still one shape and the junction still merges them. The trade is that the
|
|
31533
|
+
* document itself no longer scrolls.
|
|
31534
|
+
* @param {number|false} [props.maxLines=1] - How many lines a tab label may
|
|
31535
|
+
* use before being cut with an ellipsis; `false` lets it wrap freely. A tab
|
|
31536
|
+
* is never allowed to widen the binder past the page it opens, so the
|
|
31537
|
+
* default keeps every label on one line. Overridable per item.
|
|
31538
|
+
* @param {string|number} [props.borderWidth] - Thickness of the whole outline,
|
|
31539
|
+
* the inactive tabs included. It is added to the paddings, so a thick border
|
|
31540
|
+
* grows the binder instead of closing in on the text.
|
|
31541
|
+
* @param {string} [props.tabBorderColor] - The inactive tabs alone; the rest of
|
|
31542
|
+
* the outline follows `borderColor`.
|
|
31543
|
+
* @param {import("ignore:preact").ComponentChildren} props.children - `Binder.Item`s.
|
|
31544
|
+
*/
|
|
31545
|
+
const Binder = ({
|
|
31546
|
+
children,
|
|
31547
|
+
value,
|
|
31548
|
+
defaultValue,
|
|
31549
|
+
onChange,
|
|
31550
|
+
tabsPosition = "top",
|
|
31551
|
+
tabsAlign = "stretch",
|
|
31552
|
+
maxLines = 1,
|
|
31553
|
+
scrollablePage,
|
|
31554
|
+
borderWidth,
|
|
31555
|
+
borderRadius,
|
|
31556
|
+
tabBorderRadius,
|
|
31557
|
+
paddingX,
|
|
31558
|
+
paddingY,
|
|
31559
|
+
pagePadding,
|
|
31560
|
+
...props
|
|
31561
|
+
}) => {
|
|
31562
|
+
import.meta.css = [css$N, "@jsenv/navi/src/nav/binder/binder.jsx"];
|
|
31563
|
+
const items = toChildArray(children).map((child, index) => {
|
|
31564
|
+
const {
|
|
31565
|
+
value: itemValue,
|
|
31566
|
+
label,
|
|
31567
|
+
children: content,
|
|
31568
|
+
...tabProps
|
|
31569
|
+
} = child.props;
|
|
31570
|
+
return {
|
|
31571
|
+
value: itemValue === undefined ? index : itemValue,
|
|
31572
|
+
label,
|
|
31573
|
+
content,
|
|
31574
|
+
// Everything else belongs to the tab: an item is how one styles and
|
|
31575
|
+
// configures the control that opens it.
|
|
31576
|
+
tabProps
|
|
31577
|
+
};
|
|
31578
|
+
});
|
|
31579
|
+
const [valueOwnState, setValueOwnState] = useState(defaultValue === undefined ? items[0]?.value : defaultValue);
|
|
31580
|
+
// A tab holding a Link knows it is the current one (its route matches) before
|
|
31581
|
+
// the binder could work it out, and says so — see BinderItemContext.
|
|
31582
|
+
const [reportedValue, setReportedValue] = useState(undefined);
|
|
31583
|
+
let wantedValue;
|
|
31584
|
+
if (value !== undefined) {
|
|
31585
|
+
wantedValue = value;
|
|
31586
|
+
} else if (reportedValue !== undefined) {
|
|
31587
|
+
wantedValue = reportedValue;
|
|
31588
|
+
} else {
|
|
31589
|
+
wantedValue = valueOwnState;
|
|
31590
|
+
}
|
|
31591
|
+
const currentIndex = items.findIndex(item => item.value === wantedValue);
|
|
31592
|
+
const currentItem = currentIndex === -1 ? items[0] : items[currentIndex];
|
|
31593
|
+
const currentValue = currentItem?.value;
|
|
31594
|
+
const open = item => {
|
|
31595
|
+
if (value === undefined) {
|
|
31596
|
+
setValueOwnState(item.value);
|
|
31597
|
+
}
|
|
31598
|
+
onChange?.(item.value);
|
|
31599
|
+
};
|
|
31600
|
+
const reportCurrent = (item, current) => {
|
|
31601
|
+
setReportedValue(previous => {
|
|
31602
|
+
if (current) {
|
|
31603
|
+
return item.value;
|
|
31604
|
+
}
|
|
31605
|
+
// Only the tab that claimed it may withdraw it, otherwise the tabs that
|
|
31606
|
+
// just stopped being current would erase the one that just became it.
|
|
31607
|
+
return previous === item.value ? undefined : previous;
|
|
31608
|
+
});
|
|
31609
|
+
};
|
|
31610
|
+
const vertical = tabsPosition === "left" || tabsPosition === "right";
|
|
31611
|
+
const tabsFirst = tabsPosition === "top" || tabsPosition === "left";
|
|
31612
|
+
const rootRef = useRef(null);
|
|
31613
|
+
const tabRefs = useRef([]);
|
|
31614
|
+
const onKeyDown = event => {
|
|
31615
|
+
const {
|
|
31616
|
+
key
|
|
31617
|
+
} = event;
|
|
31618
|
+
const previousKey = vertical ? "ArrowUp" : "ArrowLeft";
|
|
31619
|
+
const nextKey = vertical ? "ArrowDown" : "ArrowRight";
|
|
31620
|
+
let nextIndex;
|
|
31621
|
+
if (key === previousKey) {
|
|
31622
|
+
nextIndex = currentIndex <= 0 ? items.length - 1 : currentIndex - 1;
|
|
31623
|
+
} else if (key === nextKey) {
|
|
31624
|
+
nextIndex = currentIndex >= items.length - 1 ? 0 : currentIndex + 1;
|
|
31625
|
+
} else if (key === "Home") {
|
|
31626
|
+
nextIndex = 0;
|
|
31627
|
+
} else if (key === "End") {
|
|
31628
|
+
nextIndex = items.length - 1;
|
|
31629
|
+
} else {
|
|
31630
|
+
return;
|
|
31631
|
+
}
|
|
31632
|
+
event.preventDefault();
|
|
31633
|
+
open(items[nextIndex]);
|
|
31634
|
+
tabRefs.current[nextIndex]?.focus();
|
|
31635
|
+
};
|
|
31636
|
+
return jsxs(Box, {
|
|
31637
|
+
ref: rootRef,
|
|
31638
|
+
baseClassName: "navi_binder",
|
|
31639
|
+
"data-tabs-position": tabsPosition,
|
|
31640
|
+
"data-tabs-align": tabsAlign,
|
|
31641
|
+
"data-scrollable-page": scrollablePage ? "" : undefined,
|
|
31642
|
+
borderWidth: withPixelUnit(borderWidth),
|
|
31643
|
+
borderRadius: withPixelUnit(borderRadius),
|
|
31644
|
+
tabBorderRadius: withPixelUnit(tabBorderRadius),
|
|
31645
|
+
paddingX: withPixelUnit(paddingX),
|
|
31646
|
+
paddingY: withPixelUnit(paddingY),
|
|
31647
|
+
pagePadding: withPixelUnit(pagePadding),
|
|
31648
|
+
...props,
|
|
31649
|
+
styleCSSVars: BinderStyleCSSVars,
|
|
31650
|
+
children: [jsx(BinderOutline, {
|
|
31651
|
+
rootRef: rootRef,
|
|
31652
|
+
currentValue: currentValue,
|
|
31653
|
+
tabsPosition: tabsPosition,
|
|
31654
|
+
vertical: vertical,
|
|
31655
|
+
tabsFirst: tabsFirst
|
|
31656
|
+
}), jsx("div", {
|
|
31657
|
+
className: "navi_binder_tabs",
|
|
31658
|
+
role: "tablist",
|
|
31659
|
+
"aria-orientation": vertical ? "vertical" : "horizontal",
|
|
31660
|
+
style: {
|
|
31661
|
+
justifyContent: TABS_ALIGN_TO_JUSTIFY_CONTENT[tabsAlign]
|
|
31662
|
+
},
|
|
31663
|
+
children: items.map((item, index) => jsx(BinderTab, {
|
|
31664
|
+
item: item,
|
|
31665
|
+
index: index,
|
|
31666
|
+
tabRefs: tabRefs,
|
|
31667
|
+
current: item === currentItem,
|
|
31668
|
+
maxLines: maxLines,
|
|
31669
|
+
onOpen: () => open(item),
|
|
31670
|
+
onKeyDown: onKeyDown,
|
|
31671
|
+
onReportCurrent: current => reportCurrent(item, current)
|
|
31672
|
+
}, item.value))
|
|
31673
|
+
}), jsx("div", {
|
|
31674
|
+
className: "navi_binder_page",
|
|
31675
|
+
role: "tabpanel",
|
|
31676
|
+
children: currentItem?.content
|
|
31677
|
+
})]
|
|
31678
|
+
});
|
|
31679
|
+
};
|
|
31680
|
+
|
|
31681
|
+
/**
|
|
31682
|
+
* @param {any} [props.value] - What identifies this item; defaults to its
|
|
31683
|
+
* position among its siblings.
|
|
31684
|
+
* @param {import("ignore:preact").ComponentChildren} [props.label] - What the tab
|
|
31685
|
+
* shows.
|
|
31686
|
+
* @param {import("../route.js").Route} [props.route] - Makes the tab a `Link`
|
|
31687
|
+
* to that route, and the binder follows the url: the open item is the one
|
|
31688
|
+
* whose route matches, no `value`/`onChange` needed.
|
|
31689
|
+
* @param {string} [props.href] - Same, for a plain url.
|
|
31690
|
+
* @param {import("ignore:preact").ComponentChildren} [props.children] - The page.
|
|
31691
|
+
*
|
|
31692
|
+
* Every other prop goes to the tab (a `Box`, or a `Link` when routed): an item
|
|
31693
|
+
* styles and configures the control that opens it.
|
|
31694
|
+
*/
|
|
31695
|
+
// Read by Binder, never rendered on its own: an item declares a tab AND its
|
|
31696
|
+
// page, and the binder needs both before it renders either.
|
|
31697
|
+
const BinderItem = () => null;
|
|
31698
|
+
Binder.Item = BinderItem;
|
|
31699
|
+
const BINDER_TAB_PSEUDO_CLASSES = [":hover", ":active", ":focus-visible"];
|
|
31700
|
+
const BinderTab = ({
|
|
31701
|
+
item,
|
|
31702
|
+
index,
|
|
31703
|
+
tabRefs,
|
|
31704
|
+
current,
|
|
31705
|
+
maxLines,
|
|
31706
|
+
onOpen,
|
|
31707
|
+
onKeyDown,
|
|
31708
|
+
onReportCurrent
|
|
31709
|
+
}) => {
|
|
31710
|
+
const {
|
|
31711
|
+
label,
|
|
31712
|
+
tabProps
|
|
31713
|
+
} = item;
|
|
31714
|
+
const {
|
|
31715
|
+
maxLines: itemMaxLines = maxLines,
|
|
31716
|
+
onClick,
|
|
31717
|
+
...rest
|
|
31718
|
+
} = tabProps;
|
|
31719
|
+
return jsx(Box, {
|
|
31720
|
+
as: "button",
|
|
31721
|
+
type: "button",
|
|
31722
|
+
baseClassName: "navi_binder_tab",
|
|
31723
|
+
pseudoClasses: BINDER_TAB_PSEUDO_CLASSES,
|
|
31724
|
+
ref: element => {
|
|
31725
|
+
tabRefs.current[index] = element;
|
|
31726
|
+
},
|
|
31727
|
+
role: "tab",
|
|
31728
|
+
"aria-selected": current,
|
|
31729
|
+
tabIndex: current ? 0 : -1,
|
|
31730
|
+
"data-current": current ? "" : undefined,
|
|
31731
|
+
onClick: event => {
|
|
31732
|
+
onOpen();
|
|
31733
|
+
onClick?.(event);
|
|
31734
|
+
},
|
|
31735
|
+
onKeyDown: onKeyDown,
|
|
31736
|
+
...rest,
|
|
31737
|
+
children: jsx(BinderItemContext.Provider, {
|
|
31738
|
+
value: onReportCurrent,
|
|
31739
|
+
children: jsx("span", {
|
|
31740
|
+
className: "navi_binder_tab_label",
|
|
31741
|
+
"data-max-lines": itemMaxLines || undefined,
|
|
31742
|
+
style: itemMaxLines > 1 ? {
|
|
31743
|
+
"--binder-tab-max-lines": itemMaxLines
|
|
31744
|
+
} : undefined,
|
|
31745
|
+
children: label
|
|
31746
|
+
})
|
|
31747
|
+
})
|
|
31748
|
+
});
|
|
31749
|
+
};
|
|
31750
|
+
const BinderOutline = ({
|
|
31751
|
+
rootRef,
|
|
31752
|
+
currentValue,
|
|
31753
|
+
tabsPosition,
|
|
31754
|
+
vertical,
|
|
31755
|
+
tabsFirst
|
|
31756
|
+
}) => {
|
|
31757
|
+
const [drawing, setDrawing] = useState(null);
|
|
31758
|
+
const measure = useCallback(() => {
|
|
31759
|
+
setDrawing(measureDrawing({
|
|
31760
|
+
rootEl: rootRef.current,
|
|
31761
|
+
vertical,
|
|
31762
|
+
tabsFirst
|
|
31763
|
+
}));
|
|
31764
|
+
}, [rootRef, vertical, tabsFirst]);
|
|
31765
|
+
useLayoutEffect(() => {
|
|
31766
|
+
const rootEl = rootRef.current;
|
|
31767
|
+
// The outline is positioned absolutely, so drawing it cannot change any
|
|
31768
|
+
// observed size — the observer settles after one pass.
|
|
31769
|
+
const resizeObserver = new ResizeObserver(measure);
|
|
31770
|
+
resizeObserver.observe(rootEl);
|
|
31771
|
+
for (const tabEl of rootEl.querySelectorAll(".navi_binder_tab")) {
|
|
31772
|
+
resizeObserver.observe(tabEl);
|
|
31773
|
+
}
|
|
31774
|
+
measure();
|
|
31775
|
+
return () => resizeObserver.disconnect();
|
|
31776
|
+
}, [rootRef, measure, currentValue]);
|
|
31777
|
+
if (!drawing) {
|
|
31778
|
+
return jsx("svg", {
|
|
31779
|
+
className: "navi_binder_outline",
|
|
31780
|
+
"aria-hidden": "true"
|
|
31781
|
+
});
|
|
31782
|
+
}
|
|
31783
|
+
return jsx("svg", {
|
|
31784
|
+
className: "navi_binder_outline",
|
|
31785
|
+
"aria-hidden": "true",
|
|
31786
|
+
width: drawing.width,
|
|
31787
|
+
height: drawing.height,
|
|
31788
|
+
children: jsx("path", {
|
|
31789
|
+
d: buildBinderPath({
|
|
31790
|
+
...drawing.pathParams,
|
|
31791
|
+
tabsPosition
|
|
31792
|
+
}),
|
|
31793
|
+
fill: drawing.background,
|
|
31794
|
+
stroke: drawing.borderColor,
|
|
31795
|
+
"stroke-width": drawing.borderWidth
|
|
31796
|
+
})
|
|
31797
|
+
});
|
|
31798
|
+
};
|
|
31799
|
+
|
|
31800
|
+
// Everything the path needs, read off the DOM rather than taken from props:
|
|
31801
|
+
// the shape has to follow the sizes the layout actually produced — text length,
|
|
31802
|
+
// font, whatever the page contains.
|
|
31803
|
+
const measureDrawing = ({
|
|
31804
|
+
rootEl,
|
|
31805
|
+
vertical,
|
|
31806
|
+
tabsFirst
|
|
31807
|
+
}) => {
|
|
31808
|
+
const tabEls = rootEl.querySelectorAll(".navi_binder_tab");
|
|
31809
|
+
const currentTabEl = rootEl.querySelector(".navi_binder_tab[data-current]");
|
|
31810
|
+
if (!currentTabEl) {
|
|
31811
|
+
return null;
|
|
31812
|
+
}
|
|
31813
|
+
const rootComputedStyle = getComputedStyle(rootEl);
|
|
31814
|
+
const readVar = name => rootComputedStyle.getPropertyValue(name).trim();
|
|
31815
|
+
const borderWidth = parseFloat(readVar("--binder-border-width")) || 0;
|
|
31816
|
+
const outerRadius = parseFloat(readVar("--binder-border-radius")) || 0;
|
|
31817
|
+
const tabRadius = parseFloat(readVar("--binder-tab-border-radius")) || 0;
|
|
31818
|
+
|
|
31819
|
+
// Path space: the main axis runs along the tab row, the cross axis goes from
|
|
31820
|
+
// the tabs toward the page.
|
|
31821
|
+
const mainSizeOf = el => vertical ? el.offsetHeight : el.offsetWidth;
|
|
31822
|
+
const mainStartOf = el => {
|
|
31823
|
+
let start = vertical ? el.offsetTop : el.offsetLeft;
|
|
31824
|
+
let offsetParent = el.offsetParent;
|
|
31825
|
+
while (offsetParent && offsetParent !== rootEl) {
|
|
31826
|
+
start += vertical ? offsetParent.offsetTop : offsetParent.offsetLeft;
|
|
31827
|
+
offsetParent = offsetParent.offsetParent;
|
|
31828
|
+
}
|
|
31829
|
+
return start;
|
|
31830
|
+
};
|
|
31831
|
+
const crossStartOf = el => {
|
|
31832
|
+
let start = vertical ? el.offsetLeft : el.offsetTop;
|
|
31833
|
+
let offsetParent = el.offsetParent;
|
|
31834
|
+
while (offsetParent && offsetParent !== rootEl) {
|
|
31835
|
+
start += vertical ? offsetParent.offsetLeft : offsetParent.offsetTop;
|
|
31836
|
+
offsetParent = offsetParent.offsetParent;
|
|
31837
|
+
}
|
|
31838
|
+
return start;
|
|
31839
|
+
};
|
|
31840
|
+
const crossSizeOf = el => vertical ? el.offsetWidth : el.offsetHeight;
|
|
31841
|
+
const mainSize = vertical ? rootEl.offsetHeight : rootEl.offsetWidth;
|
|
31842
|
+
const crossSize = vertical ? rootEl.offsetWidth : rootEl.offsetHeight;
|
|
31843
|
+
const currentTabCrossStart = crossStartOf(currentTabEl);
|
|
31844
|
+
|
|
31845
|
+
// The junction line lands in the margin every tab keeps on the page side —
|
|
31846
|
+
// right against the tab boxes, so it continues the CSS borders drawn inside
|
|
31847
|
+
// them instead of covering their last row of pixels.
|
|
31848
|
+
let tabRowHeight;
|
|
31849
|
+
let panelHeight;
|
|
31850
|
+
if (tabsFirst) {
|
|
31851
|
+
tabRowHeight = currentTabCrossStart + crossSizeOf(currentTabEl) + borderWidth;
|
|
31852
|
+
panelHeight = crossSize - tabRowHeight;
|
|
31853
|
+
} else {
|
|
31854
|
+
panelHeight = currentTabCrossStart;
|
|
31855
|
+
tabRowHeight = crossSize - panelHeight;
|
|
31856
|
+
}
|
|
31857
|
+
const firstTabEl = tabEls[0];
|
|
31858
|
+
const lastTabEl = tabEls[tabEls.length - 1];
|
|
31859
|
+
return {
|
|
31860
|
+
width: vertical ? crossSize : mainSize,
|
|
31861
|
+
height: vertical ? mainSize : crossSize,
|
|
31862
|
+
borderWidth,
|
|
31863
|
+
borderColor: readVar("--binder-border-color"),
|
|
31864
|
+
background: readVar("--binder-background"),
|
|
31865
|
+
pathParams: {
|
|
31866
|
+
tabX: mainStartOf(currentTabEl),
|
|
31867
|
+
tabWidth: mainSizeOf(currentTabEl),
|
|
31868
|
+
tabRowHeight,
|
|
31869
|
+
panelWidth: mainSize,
|
|
31870
|
+
panelHeight,
|
|
31871
|
+
borderWidth,
|
|
31872
|
+
outerRadius,
|
|
31873
|
+
tabRadius,
|
|
31874
|
+
gapBeforeTabs: mainStartOf(firstTabEl),
|
|
31875
|
+
gapAfterTabs: mainSize - (mainStartOf(lastTabEl) + mainSizeOf(lastTabEl))
|
|
31876
|
+
}
|
|
31877
|
+
};
|
|
31878
|
+
};
|
|
31879
|
+
|
|
31880
|
+
// A bare number in a CSS var stays a bare number and the declaration is
|
|
31881
|
+
// dropped; every length prop here is in pixels when it is not spelled out.
|
|
31882
|
+
const withPixelUnit = value => {
|
|
31883
|
+
if (typeof value === "number") {
|
|
31884
|
+
return `${value}px`;
|
|
31885
|
+
}
|
|
31886
|
+
return value;
|
|
31887
|
+
};
|
|
31888
|
+
|
|
31889
|
+
/**
|
|
31890
|
+
* The room a fixed bar takes from the content, published so whatever scrolls
|
|
31891
|
+
* under it can give that room back.
|
|
31892
|
+
*
|
|
31893
|
+
* There are TWO rooms to give back, and forgetting the second one is the
|
|
31894
|
+
* classic bug:
|
|
31895
|
+
*
|
|
31896
|
+
* - **padding**, so the end of the content can be scrolled out from under the
|
|
31897
|
+
* bar. Without it the last screenful stays covered, unreachable.
|
|
31898
|
+
* - **scroll-padding**, so anything the browser scrolls TO lands in front of
|
|
31899
|
+
* the bar rather than under it. An anchor link, `scrollIntoView()`, a focused
|
|
31900
|
+
* field brought into view, restoring a scroll position — all of them align
|
|
31901
|
+
* the target with the edge of the scrollport, which is behind the bar. The
|
|
31902
|
+
* padding above does not help here: it moves the content, not the place the
|
|
31903
|
+
* browser scrolls the target to.
|
|
31904
|
+
*
|
|
31905
|
+
* Published on <html> as CSS variables rather than applied to some element:
|
|
31906
|
+
* which element scrolls is the app's business, and an app with more than one
|
|
31907
|
+
* would have to fight a component that picked for it. The app either marks its
|
|
31908
|
+
* scrolling area with `data-navi-fixed-bar-space` (the rules below) or reads
|
|
31909
|
+
* the variables itself. `:root` gets the scroll-padding unconditionally,
|
|
31910
|
+
* because the document is the scrollport in the common case and an anchor
|
|
31911
|
+
* landing under a bar is never what anyone wants.
|
|
31912
|
+
*
|
|
31913
|
+
* The variables hold the bar's measured size — see the comment where FixedBar
|
|
31914
|
+
* sets them.
|
|
31915
|
+
*/
|
|
31916
|
+
|
|
31917
|
+
const FIXED_BAR_SPACE_CSS = /* css */ `
|
|
31918
|
+
:root {
|
|
31919
|
+
--navi-fixed-bar-space-top: 0px;
|
|
31920
|
+
--navi-fixed-bar-space-bottom: 0px;
|
|
31921
|
+
--navi-fixed-bar-space-left: 0px;
|
|
31922
|
+
--navi-fixed-bar-space-right: 0px;
|
|
31923
|
+
|
|
31924
|
+
scroll-padding-top: var(--navi-fixed-bar-space-top);
|
|
31925
|
+
scroll-padding-right: var(--navi-fixed-bar-space-right);
|
|
31926
|
+
scroll-padding-bottom: var(--navi-fixed-bar-space-bottom);
|
|
31927
|
+
scroll-padding-left: var(--navi-fixed-bar-space-left);
|
|
31928
|
+
}
|
|
31929
|
+
|
|
31930
|
+
/* Put this on whatever scrolls under the bars. */
|
|
31931
|
+
[data-navi-fixed-bar-space] {
|
|
31932
|
+
padding-top: var(--navi-fixed-bar-space-top);
|
|
31933
|
+
padding-right: var(--navi-fixed-bar-space-right);
|
|
31934
|
+
padding-bottom: var(--navi-fixed-bar-space-bottom);
|
|
31935
|
+
padding-left: var(--navi-fixed-bar-space-left);
|
|
31936
|
+
|
|
31937
|
+
scroll-padding-top: var(--navi-fixed-bar-space-top);
|
|
31938
|
+
scroll-padding-right: var(--navi-fixed-bar-space-right);
|
|
31939
|
+
scroll-padding-bottom: var(--navi-fixed-bar-space-bottom);
|
|
31940
|
+
scroll-padding-left: var(--navi-fixed-bar-space-left);
|
|
31941
|
+
}
|
|
31942
|
+
`;
|
|
31943
|
+
|
|
31944
|
+
/**
|
|
31945
|
+
* @param {"top"|"bottom"|"left"|"right"} area
|
|
31946
|
+
* @param {string|null} value - `null` gives the room back to the content.
|
|
31947
|
+
*/
|
|
31948
|
+
const setFixedBarSpace = (area, value) => {
|
|
31949
|
+
const property = `--navi-fixed-bar-space-${area}`;
|
|
31950
|
+
const { style } = document.documentElement;
|
|
31951
|
+
if (value === null) {
|
|
31952
|
+
style.removeProperty(property);
|
|
31953
|
+
} else {
|
|
31954
|
+
style.setProperty(property, value);
|
|
31955
|
+
}
|
|
31956
|
+
};
|
|
31957
|
+
|
|
31958
|
+
installImportMetaCssBuild(import.meta);/**
|
|
31959
|
+
* A strip pinned to one edge of the window, and the space it takes back from
|
|
31960
|
+
* the content. What goes in it is none of its business — a nav, a title, a
|
|
31961
|
+
* toolbar, an action.
|
|
31962
|
+
*
|
|
31963
|
+
* The four things it is here to get right, each of which is a trap:
|
|
31964
|
+
*
|
|
31965
|
+
* 1. **`position: fixed`, never sticky.** A sticky element sticks to its
|
|
31966
|
+
* nearest scrolling ancestor, and an app shell almost always has one (an
|
|
31967
|
+
* `overflow` somewhere) — the bar would then stick inside that box and
|
|
31968
|
+
* never to the window. Fixed, centered and bounded by `maxWidth`, it also
|
|
31969
|
+
* stays lined up with the content on a wide screen.
|
|
31970
|
+
* 2. **It gives its space back.** Being fixed it covers the content: without a
|
|
31971
|
+
* reserve the end of a long page stays under it, unreachable. It publishes
|
|
31972
|
+
* what it takes on <html> — see fixed_bar_space.js.
|
|
31973
|
+
* 3. **It reaches under the notch.** `env(safe-area-inset-*)` is padding, so
|
|
31974
|
+
* the content is pushed in while the background keeps running to the edge of
|
|
31975
|
+
* the screen. Across the bar that inset is added to `width`/`height` too, so
|
|
31976
|
+
* the size asked for is the size the content gets whatever the device does;
|
|
31977
|
+
* along the bar it adds to the padding asked for. Note that every
|
|
31978
|
+
* `env(safe-area-inset-*)` is 0 unless the page asks for it:
|
|
31979
|
+
* `<meta name="viewport" content="…, viewport-fit=cover">`.
|
|
31980
|
+
* 4. **Its hairline is a box-shadow, not a border.** A real border would eat
|
|
31981
|
+
* into the size; a box-shadow draws the identical line and stays out of
|
|
31982
|
+
* layout.
|
|
31983
|
+
*/
|
|
31984
|
+
const css$M = /* css */`
|
|
31985
|
+
@layer navi {
|
|
31986
|
+
:root {
|
|
31987
|
+
--navi-fixed-bar-width: 56px;
|
|
31988
|
+
--navi-fixed-bar-height: 56px;
|
|
31989
|
+
--navi-fixed-bar-max-width: none;
|
|
31990
|
+
--navi-fixed-bar-background: var(--navi-surface-color);
|
|
31991
|
+
--navi-fixed-bar-border-width: 1px;
|
|
31992
|
+
--navi-fixed-bar-border-color: var(--navi-separator-color-default);
|
|
31993
|
+
/* Along the bar only — across it there is nothing to add, that direction
|
|
31994
|
+
is what the width/height prop names. None by default: a row of items
|
|
31995
|
+
sharing the whole strip is as common as a toolbar wanting air at its
|
|
31996
|
+
ends, and only the second can ask. */
|
|
31997
|
+
--navi-fixed-bar-padding: 0px;
|
|
31998
|
+
}
|
|
31999
|
+
}
|
|
32000
|
+
|
|
32001
|
+
${FIXED_BAR_SPACE_CSS}
|
|
32002
|
+
|
|
32003
|
+
.navi_fixed_bar {
|
|
32004
|
+
position: fixed;
|
|
32005
|
+
z-index: 1;
|
|
32006
|
+
display: flex;
|
|
32007
|
+
box-sizing: border-box;
|
|
32008
|
+
margin: auto;
|
|
32009
|
+
align-items: center;
|
|
32010
|
+
background: var(--navi-fixed-bar-background);
|
|
32011
|
+
|
|
32012
|
+
/* Along the bar, the two insets that can bite into it there are added to
|
|
32013
|
+
the padding asked for: in landscape the notch is on a side, and a top bar
|
|
32014
|
+
whose padding ignored it would put its first item under it. */
|
|
32015
|
+
&[data-area="top"],
|
|
32016
|
+
&[data-area="bottom"] {
|
|
32017
|
+
right: 0;
|
|
32018
|
+
left: 0;
|
|
32019
|
+
/* No width of its own: pinned to both edges, the used width absorbs the
|
|
32020
|
+
padding instead of being inflated by it. max-width then narrows it and
|
|
32021
|
+
the auto margins re-center it. */
|
|
32022
|
+
max-width: var(--navi-fixed-bar-max-width);
|
|
32023
|
+
padding-right: calc(
|
|
32024
|
+
var(--navi-fixed-bar-padding) + env(safe-area-inset-right)
|
|
32025
|
+
);
|
|
32026
|
+
padding-left: calc(
|
|
32027
|
+
var(--navi-fixed-bar-padding) + env(safe-area-inset-left)
|
|
32028
|
+
);
|
|
32029
|
+
}
|
|
32030
|
+
&[data-area="left"],
|
|
32031
|
+
&[data-area="right"] {
|
|
32032
|
+
top: 0;
|
|
32033
|
+
bottom: 0;
|
|
32034
|
+
padding-top: calc(
|
|
32035
|
+
var(--navi-fixed-bar-padding) + env(safe-area-inset-top)
|
|
32036
|
+
);
|
|
32037
|
+
padding-bottom: calc(
|
|
32038
|
+
var(--navi-fixed-bar-padding) + env(safe-area-inset-bottom)
|
|
32039
|
+
);
|
|
32040
|
+
flex-direction: column;
|
|
32041
|
+
}
|
|
32042
|
+
|
|
32043
|
+
/* Across the bar, the inset of the edge it is pinned to is padding AND is
|
|
32044
|
+
added to the size: the background then runs under the notch while the
|
|
32045
|
+
content keeps the whole width/height asked for. */
|
|
32046
|
+
&[data-area="top"] {
|
|
32047
|
+
top: 0;
|
|
32048
|
+
height: calc(var(--navi-fixed-bar-height) + env(safe-area-inset-top));
|
|
32049
|
+
padding-top: env(safe-area-inset-top);
|
|
32050
|
+
box-shadow: 0 var(--navi-fixed-bar-border-width) 0
|
|
32051
|
+
var(--navi-fixed-bar-border-color);
|
|
32052
|
+
}
|
|
32053
|
+
&[data-area="bottom"] {
|
|
32054
|
+
bottom: 0;
|
|
32055
|
+
height: calc(var(--navi-fixed-bar-height) + env(safe-area-inset-bottom));
|
|
32056
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
32057
|
+
box-shadow: 0 calc(-1 * var(--navi-fixed-bar-border-width)) 0
|
|
32058
|
+
var(--navi-fixed-bar-border-color);
|
|
32059
|
+
}
|
|
32060
|
+
&[data-area="left"] {
|
|
32061
|
+
left: 0;
|
|
32062
|
+
width: calc(var(--navi-fixed-bar-width) + env(safe-area-inset-left));
|
|
32063
|
+
padding-left: env(safe-area-inset-left);
|
|
32064
|
+
box-shadow: var(--navi-fixed-bar-border-width) 0 0
|
|
32065
|
+
var(--navi-fixed-bar-border-color);
|
|
32066
|
+
}
|
|
32067
|
+
&[data-area="right"] {
|
|
32068
|
+
right: 0;
|
|
32069
|
+
width: calc(var(--navi-fixed-bar-width) + env(safe-area-inset-right));
|
|
32070
|
+
padding-right: env(safe-area-inset-right);
|
|
32071
|
+
box-shadow: calc(-1 * var(--navi-fixed-bar-border-width)) 0 0
|
|
32072
|
+
var(--navi-fixed-bar-border-color);
|
|
32073
|
+
}
|
|
32074
|
+
&[data-border="none"] {
|
|
32075
|
+
box-shadow: none;
|
|
32076
|
+
}
|
|
32077
|
+
}
|
|
32078
|
+
`;
|
|
32079
|
+
const FixedBarStyleCSSVars = {
|
|
32080
|
+
// width/height go through the vars the size calc reads: written as plain
|
|
32081
|
+
// inline styles they would land on the border box and the safe-area inset
|
|
32082
|
+
// would eat into the size asked for.
|
|
32083
|
+
width: "--navi-fixed-bar-width",
|
|
32084
|
+
height: "--navi-fixed-bar-height",
|
|
32085
|
+
maxWidth: "--navi-fixed-bar-max-width",
|
|
32086
|
+
padding: "--navi-fixed-bar-padding",
|
|
32087
|
+
background: "--navi-fixed-bar-background",
|
|
32088
|
+
borderWidth: "--navi-fixed-bar-border-width",
|
|
32089
|
+
borderColor: "--navi-fixed-bar-border-color"
|
|
32090
|
+
};
|
|
32091
|
+
|
|
32092
|
+
/**
|
|
32093
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
32094
|
+
* area?: "top"|"bottom"|"left"|"right",
|
|
32095
|
+
* width?: string|number,
|
|
32096
|
+
* height?: string|number,
|
|
32097
|
+
* maxWidth?: string|number,
|
|
32098
|
+
* padding?: string|number,
|
|
32099
|
+
* background?: string,
|
|
32100
|
+
* border?: boolean,
|
|
32101
|
+
* borderWidth?: string|number,
|
|
32102
|
+
* borderColor?: string,
|
|
32103
|
+
* }>}
|
|
32104
|
+
* @param {"top"|"bottom"|"left"|"right"} [props.area="top"] - Which edge of
|
|
32105
|
+
* the window it is pinned to.
|
|
32106
|
+
* @param {string|number} [props.height] - For a bar on the top or bottom
|
|
32107
|
+
* @param {string|number} [props.width] - …and for one on a side. The safe-area
|
|
32108
|
+
* inset is NOT part of it: it is added on top, so the content keeps the size
|
|
32109
|
+
* asked for.
|
|
32110
|
+
* @param {boolean} [props.border=true] - The hairline on the content side.
|
|
32111
|
+
* Drawn with a box-shadow so it never eats into the size; give it a
|
|
32112
|
+
* `borderWidth`/`borderColor`, or `border={false}` for none.
|
|
32113
|
+
* @param {string|number} [props.maxWidth] - Keeps the bar lined up with a
|
|
32114
|
+
* content column narrower than the window (it stays centered).
|
|
32115
|
+
*/
|
|
32116
|
+
const FixedBar = ({
|
|
32117
|
+
children,
|
|
32118
|
+
area = "top",
|
|
32119
|
+
border = true,
|
|
32120
|
+
...props
|
|
32121
|
+
}) => {
|
|
32122
|
+
import.meta.css = [css$M, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
|
|
32123
|
+
const defaultRef = useRef();
|
|
32124
|
+
props.ref = props.ref || defaultRef;
|
|
32125
|
+
// Whichever of width/height crosses the edge the bar sits on is what the
|
|
32126
|
+
// content has to be given back — and the bar's border box already IS that:
|
|
32127
|
+
// the size it was given plus the inset of that edge. Measured rather than
|
|
32128
|
+
// rebuilt as a calc() expression, so a size coming from anywhere — a prop, a
|
|
32129
|
+
// theme variable, the content itself — is reserved just the same, and each
|
|
32130
|
+
// `env()` inset stays the browser's business alone.
|
|
32131
|
+
const vertical = area === "left" || area === "right";
|
|
32132
|
+
const {
|
|
32133
|
+
ref
|
|
32134
|
+
} = props;
|
|
32135
|
+
useLayoutEffect(() => {
|
|
32136
|
+
const barElement = ref.current;
|
|
32137
|
+
if (!barElement) {
|
|
32138
|
+
return undefined;
|
|
32139
|
+
}
|
|
32140
|
+
const {
|
|
32141
|
+
width,
|
|
32142
|
+
height
|
|
32143
|
+
} = barElement.getBoundingClientRect();
|
|
32144
|
+
setFixedBarSpace(area, `${vertical ? width : height}px`);
|
|
32145
|
+
return () => {
|
|
32146
|
+
setFixedBarSpace(area, null);
|
|
32147
|
+
};
|
|
32148
|
+
}, [area]);
|
|
32149
|
+
return jsx(Box, {
|
|
32150
|
+
baseClassName: "navi_fixed_bar",
|
|
32151
|
+
"data-area": area,
|
|
32152
|
+
"data-border": border ? undefined : "none",
|
|
32153
|
+
...props,
|
|
32154
|
+
styleCSSVars: FixedBarStyleCSSVars,
|
|
32155
|
+
children: children
|
|
32156
|
+
});
|
|
32157
|
+
};
|
|
32158
|
+
|
|
30797
32159
|
const detectMac = () => {
|
|
30798
32160
|
// Modern way using User-Agent Client Hints API
|
|
30799
32161
|
if (window.navigator.userAgentData) {
|
|
@@ -33887,6 +35249,7 @@ const ButtonWithRoute = props => {
|
|
|
33887
35249
|
route,
|
|
33888
35250
|
routeParams,
|
|
33889
35251
|
children,
|
|
35252
|
+
pseudoState,
|
|
33890
35253
|
...rest
|
|
33891
35254
|
} = props;
|
|
33892
35255
|
const url = route.buildUrl(routeParams);
|
|
@@ -33895,9 +35258,15 @@ const ButtonWithRoute = props => {
|
|
|
33895
35258
|
} = useRouteStatus(route);
|
|
33896
35259
|
const paramsAreMatching = route.matchesParams(routeParams);
|
|
33897
35260
|
const linkMatching = matching && paramsAreMatching;
|
|
35261
|
+
|
|
35262
|
+
// Merged into whatever the caller already holds: a button can be forced into
|
|
35263
|
+
// a state for a demo and still learn its own current-ness from its route.
|
|
33898
35264
|
return jsx(Next, {
|
|
33899
35265
|
href: url,
|
|
33900
|
-
|
|
35266
|
+
pseudoState: {
|
|
35267
|
+
...pseudoState,
|
|
35268
|
+
":-navi-href-current": linkMatching
|
|
35269
|
+
},
|
|
33901
35270
|
...rest,
|
|
33902
35271
|
children: children || route.buildRelativeUrl(routeParams)
|
|
33903
35272
|
});
|
|
@@ -33939,6 +35308,11 @@ installImportMetaCssBuild(import.meta);const css$E = /* css */`
|
|
|
33939
35308
|
black
|
|
33940
35309
|
);
|
|
33941
35310
|
--button-color-hover: var(--button-color);
|
|
35311
|
+
/* Current: the button points at the page one is already on. Nothing by
|
|
35312
|
+
default — it is a nav item that wants to say so, not every button. */
|
|
35313
|
+
--button-border-color-current: var(--button-border-color);
|
|
35314
|
+
--button-background-color-current: var(--button-background-color);
|
|
35315
|
+
--button-color-current: var(--button-color);
|
|
33942
35316
|
/* Pressed */
|
|
33943
35317
|
--button-border-color-pressed: color-mix(
|
|
33944
35318
|
in srgb,
|
|
@@ -33982,6 +35356,13 @@ installImportMetaCssBuild(import.meta);const css$E = /* css */`
|
|
|
33982
35356
|
.navi_button {
|
|
33983
35357
|
--x-button-outline-offset: var(--button-outline-offset);
|
|
33984
35358
|
--x-button-border-color: var(--button-border-color);
|
|
35359
|
+
/* The shorthand wins over the parts when it is given, which is what makes
|
|
35360
|
+
border="none" remove the line: the parts alone can only ever describe
|
|
35361
|
+
a border, never the absence of one. */
|
|
35362
|
+
--x-button-border: var(
|
|
35363
|
+
--button-border,
|
|
35364
|
+
var(--button-border-width) solid var(--x-button-border-color)
|
|
35365
|
+
);
|
|
33985
35366
|
--x-button-background: var(--button-background);
|
|
33986
35367
|
--x-button-background-color: var(--button-background-color);
|
|
33987
35368
|
--x-button-color: var(--button-color);
|
|
@@ -34045,9 +35426,7 @@ installImportMetaCssBuild(import.meta);const css$E = /* css */`
|
|
|
34045
35426
|
--x-button-background-color,
|
|
34046
35427
|
var(--x-button-background)
|
|
34047
35428
|
);
|
|
34048
|
-
border
|
|
34049
|
-
border-style: solid;
|
|
34050
|
-
border-color: var(--x-button-border-color);
|
|
35429
|
+
border: var(--x-button-border);
|
|
34051
35430
|
border-radius: inherit;
|
|
34052
35431
|
outline-width: var(--button-outline-width);
|
|
34053
35432
|
outline-color: var(--button-outline-color);
|
|
@@ -34068,6 +35447,12 @@ installImportMetaCssBuild(import.meta);const css$E = /* css */`
|
|
|
34068
35447
|
}
|
|
34069
35448
|
}
|
|
34070
35449
|
|
|
35450
|
+
/* Current */
|
|
35451
|
+
&[data-href-current] {
|
|
35452
|
+
--x-button-border-color: var(--button-border-color-current);
|
|
35453
|
+
--x-button-background-color: var(--button-background-color-current);
|
|
35454
|
+
--x-button-color: var(--button-color-current);
|
|
35455
|
+
}
|
|
34071
35456
|
/* Hover */
|
|
34072
35457
|
&[data-hover] {
|
|
34073
35458
|
--x-button-border-color: var(--button-border-color-hover);
|
|
@@ -34214,6 +35599,21 @@ installImportMetaCssBuild(import.meta);const css$E = /* css */`
|
|
|
34214
35599
|
--x-button-background-color: transparent;
|
|
34215
35600
|
}
|
|
34216
35601
|
}
|
|
35602
|
+
/* Last word on the shrink, over whatever the variant decided: the variant
|
|
35603
|
+
guesses from how the button is drawn, and that guess is wrong as soon as
|
|
35604
|
+
the content is a box of its own — a discrete button holding a filled
|
|
35605
|
+
pill reads as a button being pressed, not as its content flinching. */
|
|
35606
|
+
&[data-press-effect="scale"][data-pressed] {
|
|
35607
|
+
.navi_button_content {
|
|
35608
|
+
transform: scale(0.9);
|
|
35609
|
+
}
|
|
35610
|
+
}
|
|
35611
|
+
&[data-press-effect="none"][data-pressed] {
|
|
35612
|
+
.navi_button_content {
|
|
35613
|
+
transform: none;
|
|
35614
|
+
}
|
|
35615
|
+
}
|
|
35616
|
+
|
|
34217
35617
|
&[data-icon] {
|
|
34218
35618
|
--button-padding: 0;
|
|
34219
35619
|
display: inline-flex;
|
|
@@ -34274,6 +35674,7 @@ const ButtonUI = props => {
|
|
|
34274
35674
|
rel,
|
|
34275
35675
|
// visual
|
|
34276
35676
|
variant,
|
|
35677
|
+
pressEffect,
|
|
34277
35678
|
icon,
|
|
34278
35679
|
cta,
|
|
34279
35680
|
spacing
|
|
@@ -34287,18 +35688,31 @@ const ButtonUI = props => {
|
|
|
34287
35688
|
children
|
|
34288
35689
|
} = buttonControlHostProps;
|
|
34289
35690
|
const loading = basePseudoState[":-navi-loading"];
|
|
35691
|
+
|
|
35692
|
+
// subscribe to document url to re-render and re-compute getHrefTargetInfo
|
|
35693
|
+
useDocumentUrl();
|
|
34290
35694
|
const isLink = href !== undefined;
|
|
34291
35695
|
let as = "button";
|
|
34292
35696
|
let innerTarget;
|
|
34293
35697
|
let innerRel;
|
|
35698
|
+
let innerCurrent;
|
|
34294
35699
|
if (isLink) {
|
|
34295
35700
|
as = "a";
|
|
34296
35701
|
const {
|
|
34297
|
-
isSameSite
|
|
35702
|
+
isSameSite,
|
|
35703
|
+
isCurrent
|
|
34298
35704
|
} = getHrefTargetInfo(href);
|
|
34299
35705
|
innerTarget = target === undefined ? isSameSite ? undefined : "_blank" : target;
|
|
34300
35706
|
innerRel = rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel;
|
|
35707
|
+
innerCurrent = isCurrent;
|
|
34301
35708
|
}
|
|
35709
|
+
// For a button that has only an href: nothing else knows it points at the
|
|
35710
|
+
// page one is on. A route says so through `pseudoState`, which Box lays over
|
|
35711
|
+
// this one.
|
|
35712
|
+
buttonControlHostProps.basePseudoState = {
|
|
35713
|
+
...basePseudoState,
|
|
35714
|
+
":-navi-href-current": innerCurrent
|
|
35715
|
+
};
|
|
34302
35716
|
const visualSelector = ".navi_button_content";
|
|
34303
35717
|
useAccentColorAttributes(ref, null, {
|
|
34304
35718
|
elementSelector: visualSelector
|
|
@@ -34318,6 +35732,7 @@ const ButtonUI = props => {
|
|
|
34318
35732
|
type: "button",
|
|
34319
35733
|
spacing: undefined,
|
|
34320
35734
|
cta: undefined,
|
|
35735
|
+
pressEffect: undefined,
|
|
34321
35736
|
ref: ref,
|
|
34322
35737
|
as: as,
|
|
34323
35738
|
href: href,
|
|
@@ -34347,6 +35762,7 @@ const ButtonUI = props => {
|
|
|
34347
35762
|
e.preventDefault();
|
|
34348
35763
|
},
|
|
34349
35764
|
"data-variant": variant,
|
|
35765
|
+
"data-press-effect": pressEffect,
|
|
34350
35766
|
"data-icon": icon ? "" : undefined,
|
|
34351
35767
|
"data-cta": cta ? "" : undefined,
|
|
34352
35768
|
"data-callout-arrow-x": "center"
|
|
@@ -34408,6 +35824,11 @@ const ButtonStyleCSSVars = {
|
|
|
34408
35824
|
":-navi-pressed": {
|
|
34409
35825
|
borderColor: "--button-border-color-pressed"
|
|
34410
35826
|
},
|
|
35827
|
+
":-navi-href-current": {
|
|
35828
|
+
backgroundColor: "--button-background-color-current",
|
|
35829
|
+
borderColor: "--button-border-color-current",
|
|
35830
|
+
color: "--button-color-current"
|
|
35831
|
+
},
|
|
34411
35832
|
":read-only": {
|
|
34412
35833
|
backgroundColor: "--button-background-color-readonly",
|
|
34413
35834
|
borderColor: "--button-border-color-readonly",
|
|
@@ -34419,7 +35840,7 @@ const ButtonStyleCSSVars = {
|
|
|
34419
35840
|
color: "--button-color-disabled"
|
|
34420
35841
|
}
|
|
34421
35842
|
};
|
|
34422
|
-
const ButtonPseudoClasses = [":hover", ":active", ":-navi-pressed", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
|
|
35843
|
+
const ButtonPseudoClasses = [":-navi-href-current", ":hover", ":active", ":-navi-pressed", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
|
|
34423
35844
|
const ButtonPseudoElements = ["::-navi-loader"];
|
|
34424
35845
|
const ButtonShadow = () => {
|
|
34425
35846
|
return jsx("span", {
|
|
@@ -58976,5 +60397,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
58976
60397
|
})
|
|
58977
60398
|
});
|
|
58978
60399
|
|
|
58979
|
-
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 };
|
|
60400
|
+
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 };
|
|
58980
60401
|
//# sourceMappingURL=jsenv_navi.js.map
|