@jsenv/navi 0.29.14 → 0.29.16
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 +434 -28
- package/dist/jsenv_navi.js.map +47 -21
- package/dist/jsenv_navi_side_effects.js +21 -0
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +7 -0
- package/docs/css_architecture.md +53 -10
- package/docs/scroll.md +250 -0
- package/docs/z_index.md +111 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -6490,6 +6490,10 @@ const POSITION_PROPS = {
|
|
|
6490
6490
|
fixed: applyToCssPropWhenTruthy("position", "fixed", "static"),
|
|
6491
6491
|
sticky: applyToCssPropWhenTruthy("position", "sticky", "static"),
|
|
6492
6492
|
zIndex: PASS_THROUGH,
|
|
6493
|
+
// Keeps the zIndex values used inside this box local to it — see
|
|
6494
|
+
// docs/z_index.md: a z-index that opens no stacking context competes with
|
|
6495
|
+
// the whole page, fixed bars included.
|
|
6496
|
+
isolation: PASS_THROUGH,
|
|
6493
6497
|
order: PASS_THROUGH,
|
|
6494
6498
|
left: (value) => {
|
|
6495
6499
|
return { left: value === true ? 0 : value };
|
|
@@ -8905,6 +8909,20 @@ import.meta.css = [/* css */`
|
|
|
8905
8909
|
between them — and a control flush against the edge of a scrolling area
|
|
8906
8910
|
overflows it (a focus outline is drawn outside the control it belongs to)
|
|
8907
8911
|
and raises a scrollbar of its own. */
|
|
8912
|
+
/* A control sitting right against the edge of what scrolls must keep its
|
|
8913
|
+
loading outline within its own box: the outline is drawn a couple pixels
|
|
8914
|
+
outside the control (see loading_outline.jsx), and that bleed alone is
|
|
8915
|
+
enough to make the area scrollable — a scrollbar appearing and disappearing
|
|
8916
|
+
as things load. Only what the scroller directly contains is against that
|
|
8917
|
+
edge; anything nested deeper has room around it and keeps the outline it
|
|
8918
|
+
asked for, hence the child combinators. Written on the outline itself
|
|
8919
|
+
rather than on the control, because the var inherits: setting it on a
|
|
8920
|
+
container would reach every control below it, edge or not. */
|
|
8921
|
+
[data-scrollable] > .navi_loading_outline_wrapper,
|
|
8922
|
+
[data-scrollable] > * > .navi_loading_outline_wrapper {
|
|
8923
|
+
--loading-outline-min-inset: 0px;
|
|
8924
|
+
}
|
|
8925
|
+
|
|
8908
8926
|
[data-scrollable] {
|
|
8909
8927
|
overflow: var(--x-scrollable-overflow, auto);
|
|
8910
8928
|
|
|
@@ -10057,6 +10075,15 @@ const LoadingOutlineUI = props => {
|
|
|
10057
10075
|
} = props;
|
|
10058
10076
|
const shouldShowSpinner = useDebounceTrue(loading, debounce);
|
|
10059
10077
|
const rectangleRef = useRef(null);
|
|
10078
|
+
|
|
10079
|
+
// Nothing in the DOM until something actually loads: the box below is
|
|
10080
|
+
// absolutely positioned slightly outside the control, which is enough to
|
|
10081
|
+
// make an ancestor scrollable (a 1px scrollbar on a control sitting against
|
|
10082
|
+
// the edge of a scrolling area). A control that never loads must not pay for
|
|
10083
|
+
// a decoration it will never draw.
|
|
10084
|
+
if (!loading) {
|
|
10085
|
+
return children;
|
|
10086
|
+
}
|
|
10060
10087
|
let insetTop = inset + spacingTop + marginTop;
|
|
10061
10088
|
let insetRight = inset + spacingRight + marginRight;
|
|
10062
10089
|
let insetBottom = inset + spacingBottom + marginBottom;
|
|
@@ -10087,7 +10114,7 @@ const LoadingOutlineUI = props => {
|
|
|
10087
10114
|
"--loading-rectangle-bottom": `${insetBottom}px`,
|
|
10088
10115
|
"--loading-rectangle-left": `${insetLeft}px`
|
|
10089
10116
|
},
|
|
10090
|
-
children:
|
|
10117
|
+
children: jsx(LoadingIndicatorFluid, {
|
|
10091
10118
|
visuallyHidden: !shouldShowSpinner,
|
|
10092
10119
|
radius: radius,
|
|
10093
10120
|
color: color,
|
|
@@ -13764,6 +13791,25 @@ const css$V = /* css */`
|
|
|
13764
13791
|
* @param {Function} [options.onClose] - Callback when callout is closed
|
|
13765
13792
|
* @param {boolean} [options.closeOnClickOutside] - Whether to close on outside clicks (defaults to true for "info" status)
|
|
13766
13793
|
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
13794
|
+
*
|
|
13795
|
+
* Positioning is also driven by attributes read on the anchor element itself
|
|
13796
|
+
* (so markup can tune a callout without going through this function):
|
|
13797
|
+
* - `data-callout-arrow-x="start" | "center" | "end"`: where the arrow points
|
|
13798
|
+
* horizontally along the anchor — `start`/`end` target the left/right edge of
|
|
13799
|
+
* the anchor's text content (inside its borders and padding), `center` targets
|
|
13800
|
+
* the middle of the anchor. Without it the arrow follows the anchor's computed
|
|
13801
|
+
* `text-align` (`center` → center, `right`/`end` → end, anything else → start),
|
|
13802
|
+
* so it lands where the text visually begins. The arrow is always clamped to
|
|
13803
|
+
* stay within the callout's own rounded corners, so a value pointing outside
|
|
13804
|
+
* the callout's width sticks to the nearest side.
|
|
13805
|
+
* - `data-callout-position="top" | "bottom" | …`: preferred side of the anchor
|
|
13806
|
+
* (defaults to `"bottom"`, flipped when there isn't enough space).
|
|
13807
|
+
* - `data-callout-position-fixed`: opt out of that flipping.
|
|
13808
|
+
* - `data-callout-anchor="<selector>"`: point at an inner element instead of the
|
|
13809
|
+
* anchor itself.
|
|
13810
|
+
* - `data-callout-point-to-border-box` / `data-callout-point-to-content-box`:
|
|
13811
|
+
* which box the callout aligns to.
|
|
13812
|
+
* - `data-callout-viewport-spacing="<number>"`: minimum gap with the viewport edges.
|
|
13767
13813
|
* @returns {Object} - Callout object with properties:
|
|
13768
13814
|
* - {Function} close - Function to close the callout
|
|
13769
13815
|
* - {Function} update - Function to update message and options
|
|
@@ -17775,6 +17821,104 @@ const useExecuteAction = (
|
|
|
17775
17821
|
return executeAction;
|
|
17776
17822
|
};
|
|
17777
17823
|
|
|
17824
|
+
/**
|
|
17825
|
+
* A control placed inside a region that expands on click — a `<summary>`, an
|
|
17826
|
+
* accordion header carrying `aria-expanded` — has its click read twice: once by
|
|
17827
|
+
* the control it was aimed at, once by the region around it. The second reading
|
|
17828
|
+
* is never wanted; a menu opened from a collapsed row should not also unfold the
|
|
17829
|
+
* row.
|
|
17830
|
+
*
|
|
17831
|
+
* Cancelling the click is the only way to stop the region: a `<summary>` runs
|
|
17832
|
+
* its default action after the propagation, so `stopPropagation` does not reach
|
|
17833
|
+
* it. And it can only be done once the control has taken the click for itself —
|
|
17834
|
+
* navi refuses an interaction on an already-cancelled event (see
|
|
17835
|
+
* `onRequestInteraction`), so cancelling any earlier silences the control
|
|
17836
|
+
* instead of the region.
|
|
17837
|
+
*
|
|
17838
|
+
* That moment — right after an interaction was allowed — only exists inside
|
|
17839
|
+
* navi, which is why the cancellation lives here rather than in application code.
|
|
17840
|
+
*/
|
|
17841
|
+
|
|
17842
|
+
const CLICK_TO_EXPAND_SELECTOR = "summary, [aria-expanded]";
|
|
17843
|
+
|
|
17844
|
+
/**
|
|
17845
|
+
* Cancels `event` when the control consumed a click that a surrounding
|
|
17846
|
+
* click-to-expand region would otherwise read as "unfold me".
|
|
17847
|
+
*
|
|
17848
|
+
* Does nothing when cancelling the click would also cancel what the control
|
|
17849
|
+
* itself does with it (a link navigating, a checkbox toggling): there, the two
|
|
17850
|
+
* behaviours cannot be separated and the control's own comes first.
|
|
17851
|
+
*/
|
|
17852
|
+
const preventClickToExpand = (element, event) => {
|
|
17853
|
+
if (!event || event.type !== "click") {
|
|
17854
|
+
return;
|
|
17855
|
+
}
|
|
17856
|
+
if (event.defaultPrevented) {
|
|
17857
|
+
return;
|
|
17858
|
+
}
|
|
17859
|
+
if (!clickDefaultActionIsInert(element, event)) {
|
|
17860
|
+
return;
|
|
17861
|
+
}
|
|
17862
|
+
const parentElement = element.parentElement;
|
|
17863
|
+
if (!parentElement) {
|
|
17864
|
+
return;
|
|
17865
|
+
}
|
|
17866
|
+
// From the parent: a control that opens something carries its own
|
|
17867
|
+
// `aria-expanded` and would find itself.
|
|
17868
|
+
const clickToExpandRegion = parentElement.closest(CLICK_TO_EXPAND_SELECTOR);
|
|
17869
|
+
if (!clickToExpandRegion) {
|
|
17870
|
+
return;
|
|
17871
|
+
}
|
|
17872
|
+
event.preventDefault();
|
|
17873
|
+
};
|
|
17874
|
+
|
|
17875
|
+
const clickDefaultActionIsInert = (element, event) => {
|
|
17876
|
+
if (!isInertOnClick(element)) {
|
|
17877
|
+
return false;
|
|
17878
|
+
}
|
|
17879
|
+
// The activation belongs to what was clicked, which can be deeper than the
|
|
17880
|
+
// control host (a button inside it) or above it (a label wrapping it).
|
|
17881
|
+
const { target } = event;
|
|
17882
|
+
if (target && target !== element && target.nodeType === 1) {
|
|
17883
|
+
let ancestor = target;
|
|
17884
|
+
while (ancestor) {
|
|
17885
|
+
if (!isInertOnClick(ancestor)) {
|
|
17886
|
+
return false;
|
|
17887
|
+
}
|
|
17888
|
+
ancestor = ancestor.parentElement;
|
|
17889
|
+
}
|
|
17890
|
+
}
|
|
17891
|
+
return true;
|
|
17892
|
+
};
|
|
17893
|
+
|
|
17894
|
+
const NON_INERT_INPUT_TYPE_SET = new Set([
|
|
17895
|
+
"checkbox",
|
|
17896
|
+
"radio",
|
|
17897
|
+
"submit",
|
|
17898
|
+
"reset",
|
|
17899
|
+
"image",
|
|
17900
|
+
"file",
|
|
17901
|
+
]);
|
|
17902
|
+
|
|
17903
|
+
const isInertOnClick = (element) => {
|
|
17904
|
+
const { tagName } = element;
|
|
17905
|
+
if (tagName === "A" || tagName === "AREA") {
|
|
17906
|
+
return !element.hasAttribute("href");
|
|
17907
|
+
}
|
|
17908
|
+
if (tagName === "LABEL") {
|
|
17909
|
+
// A label forwards the click to its control, whose activation would be
|
|
17910
|
+
// cancelled along with the click.
|
|
17911
|
+
return false;
|
|
17912
|
+
}
|
|
17913
|
+
if (tagName === "INPUT") {
|
|
17914
|
+
return !NON_INERT_INPUT_TYPE_SET.has(element.type);
|
|
17915
|
+
}
|
|
17916
|
+
if (tagName === "BUTTON") {
|
|
17917
|
+
return element.type === "button";
|
|
17918
|
+
}
|
|
17919
|
+
return true;
|
|
17920
|
+
};
|
|
17921
|
+
|
|
17778
17922
|
const BUSY_CONSTRAINT = {
|
|
17779
17923
|
name: "busy",
|
|
17780
17924
|
messageAttribute: "data-busy-message",
|
|
@@ -18143,6 +18287,9 @@ const onRequestInteraction = (
|
|
|
18143
18287
|
debugInteraction(event, `"${name}" allowed`);
|
|
18144
18288
|
allowed?.();
|
|
18145
18289
|
always?.();
|
|
18290
|
+
// The click served this control; it must not serve a second time whatever
|
|
18291
|
+
// unfolds around it (see click_to_expand.js).
|
|
18292
|
+
preventClickToExpand(controlHost, event);
|
|
18146
18293
|
return true;
|
|
18147
18294
|
};
|
|
18148
18295
|
|
|
@@ -25713,8 +25860,8 @@ const css$T = /* css */`
|
|
|
25713
25860
|
* the screen and a centered box ends up both cramped and out of thumb
|
|
25714
25861
|
* reach, while under a mouse the centered box is already the right shape —
|
|
25715
25862
|
* hence a prop that only ever does something on touch. It supplies defaults
|
|
25716
|
-
* for `positionArea`, `marginWithContainer` and `
|
|
25717
|
-
*
|
|
25863
|
+
* for `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`,
|
|
25864
|
+
* so any of them can still be pinned explicitly. Keyed off `(pointer: coarse)` (the
|
|
25718
25865
|
* input device, not a width breakpoint — a narrow desktop window is still a
|
|
25719
25866
|
* mouse) via `coarsePointerSignal`, so it re-resolves live.
|
|
25720
25867
|
* @param {string} [props.positionArea="center"] - Where to dock the dialog
|
|
@@ -25751,7 +25898,7 @@ const css$T = /* css */`
|
|
|
25751
25898
|
* A `layer="local"` dialog always locks its own positioned ancestor's
|
|
25752
25899
|
* scroll while open (its backdrop only covers the scrollport, so scrolling
|
|
25753
25900
|
* there would reveal uncovered content); this prop extends the lock to the
|
|
25754
|
-
* whole page.
|
|
25901
|
+
* whole page. Defaults to `true` for a dialog docked by `dockedOnTouch`.
|
|
25755
25902
|
* @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`} [props.animation]
|
|
25756
25903
|
* - `true`/`"auto"` resolves to `"scaling"` for a centered `positionArea`,
|
|
25757
25904
|
* or a concrete `"slide-from-*"` direction otherwise. Any other explicit
|
|
@@ -25920,7 +26067,11 @@ const DialogLocal = props => {
|
|
|
25920
26067
|
const DOCKED = {
|
|
25921
26068
|
positionArea: "bottom",
|
|
25922
26069
|
marginWithContainer: 0,
|
|
25923
|
-
expandX: true
|
|
26070
|
+
expandX: true,
|
|
26071
|
+
// A sheet resting on the bottom edge is dragged with a thumb, and a drag that
|
|
26072
|
+
// runs past its own edge must not land on the page behind it: the same
|
|
26073
|
+
// reasoning as "bottom" above, applied to the gesture instead of the shape.
|
|
26074
|
+
scrollCapture: true
|
|
25924
26075
|
};
|
|
25925
26076
|
|
|
25926
26077
|
// The first control inside `dialogEl` that is mid-action, if any. Walks the
|
|
@@ -25966,7 +26117,7 @@ const useDialogProps = props => {
|
|
|
25966
26117
|
// there's no native inert-ing, so the real backdrop below is what
|
|
25967
26118
|
// actually makes "capture"/"none" behave the same way here too.
|
|
25968
26119
|
pointerInteractionOutsideEffect = "close",
|
|
25969
|
-
scrollCapture,
|
|
26120
|
+
scrollCapture: scrollCaptureProp,
|
|
25970
26121
|
animation,
|
|
25971
26122
|
// Only ever affects --anchor-width/--anchor-height (see this file's top
|
|
25972
26123
|
// comment) — Dialog's own positioning is never relative to it.
|
|
@@ -26001,6 +26152,7 @@ const useDialogProps = props => {
|
|
|
26001
26152
|
const expandXUnset = expand === undefined && expandXProp === undefined;
|
|
26002
26153
|
const expandX = expandXUnset ? isDocked && DOCKED.expandX : Boolean(expand) || Boolean(expandXProp);
|
|
26003
26154
|
const expandY = Boolean(expand) || Boolean(expandYProp);
|
|
26155
|
+
const scrollCapture = scrollCaptureProp ?? (isDocked ? DOCKED.scrollCapture : false);
|
|
26004
26156
|
const backdropRef = useRef();
|
|
26005
26157
|
// Disarms a still-pending backdrop hide from a previous close (see
|
|
26006
26158
|
// armPointerDownOutsideClose below) — same pattern as popover.jsx's own.
|
|
@@ -26560,8 +26712,13 @@ const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible",
|
|
|
26560
26712
|
|
|
26561
26713
|
// Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
|
|
26562
26714
|
// it to the CSS var for us (see box.jsx's styleCSSVars handling).
|
|
26715
|
+
// borderRadius goes through --dialog-border-radius rather than the
|
|
26716
|
+
// border-radius property itself so the flush-corner rules above (a plain
|
|
26717
|
+
// stylesheet) can still square the corners that land on the container's own —
|
|
26718
|
+
// an inline border-radius would outrank them.
|
|
26563
26719
|
const DIALOG_STYLE_CSS_VARS = {
|
|
26564
26720
|
animationDuration: "--popup-animation-duration",
|
|
26721
|
+
borderRadius: "--dialog-border-radius",
|
|
26565
26722
|
minWidth: "--dialog-min-width",
|
|
26566
26723
|
maxWidth: "--dialog-max-width",
|
|
26567
26724
|
minHeight: "--dialog-min-height",
|
|
@@ -31521,12 +31678,30 @@ const useActionAsyncData = (action, {
|
|
|
31521
31678
|
const runningState = action.runningStateSignal.peek();
|
|
31522
31679
|
const [, setTick] = useState(0);
|
|
31523
31680
|
useEffect(() => {
|
|
31524
|
-
|
|
31681
|
+
const unsubscribeFromRunningState = action.runningStateSignal.subscribe(state => {
|
|
31525
31682
|
if (state === RUNNING) {
|
|
31526
31683
|
dismissedActionWeakSet.delete(action);
|
|
31527
31684
|
}
|
|
31528
31685
|
setTick(n => n + 1);
|
|
31529
31686
|
});
|
|
31687
|
+
// The data does not come from this action's runs alone: dataSignal is a
|
|
31688
|
+
// computed over the resource store, so an other action writing that store
|
|
31689
|
+
// (a PUT upserting an item that a GET_MANY list already holds) changes the
|
|
31690
|
+
// data while this action stays COMPLETED. Subscribing here re-renders
|
|
31691
|
+
// through the same controlled path as the run state, instead of `.value`.
|
|
31692
|
+
let dataNotificationIsInitial = true;
|
|
31693
|
+
const unsubscribeFromData = action.dataSignal.subscribe(() => {
|
|
31694
|
+
if (dataNotificationIsInitial) {
|
|
31695
|
+
// subscribe() calls back synchronously with the current value
|
|
31696
|
+
dataNotificationIsInitial = false;
|
|
31697
|
+
return;
|
|
31698
|
+
}
|
|
31699
|
+
setTick(n => n + 1);
|
|
31700
|
+
});
|
|
31701
|
+
return () => {
|
|
31702
|
+
unsubscribeFromRunningState();
|
|
31703
|
+
unsubscribeFromData();
|
|
31704
|
+
};
|
|
31530
31705
|
}, []);
|
|
31531
31706
|
if (runningState === COMPLETED) {
|
|
31532
31707
|
return [action.dataSignal.peek(), false, undefined];
|
|
@@ -35842,10 +36017,13 @@ installImportMetaCssBuild(import.meta);const css$N = /* css */`
|
|
|
35842
36017
|
--link-background: unset;
|
|
35843
36018
|
--link-background-current: unset;
|
|
35844
36019
|
--link-background-selected: light-dark(#bbdefb, #2563eb);
|
|
35845
|
-
--link-color:
|
|
35846
|
-
--link-color-visited:
|
|
36020
|
+
--link-color: var(--navi-link-color);
|
|
36021
|
+
--link-color-visited: var(
|
|
36022
|
+
--navi-link-color-visited,
|
|
36023
|
+
color-mix(in srgb, var(--link-color), black 40%)
|
|
36024
|
+
);
|
|
35847
36025
|
|
|
35848
|
-
--link-color-pressed:
|
|
36026
|
+
--link-color-pressed: var(--navi-link-color-pressed);
|
|
35849
36027
|
--link-text-decoration: underline;
|
|
35850
36028
|
--link-text-decoration-hover: var(--link-text-decoration);
|
|
35851
36029
|
--link-cursor: pointer;
|
|
@@ -35854,7 +36032,7 @@ installImportMetaCssBuild(import.meta);const css$N = /* css */`
|
|
|
35854
36032
|
|
|
35855
36033
|
--link-current-indicator-size: 2px;
|
|
35856
36034
|
--link-current-indicator-spacing: 0;
|
|
35857
|
-
--link-current-indicator-color:
|
|
36035
|
+
--link-current-indicator-color: var(--navi-link-current-indicator-color);
|
|
35858
36036
|
}
|
|
35859
36037
|
}
|
|
35860
36038
|
|
|
@@ -38898,7 +39076,7 @@ installImportMetaCssBuild(import.meta);const css$F = /* css */`
|
|
|
38898
39076
|
--loader-color: var(--navi-loader-color);
|
|
38899
39077
|
--border-color: var(--navi-control-border-color);
|
|
38900
39078
|
--background-color: white;
|
|
38901
|
-
--accent-color:
|
|
39079
|
+
--accent-color: var(--navi-control-accent-color);
|
|
38902
39080
|
--background-color-checked: var(--accent-color);
|
|
38903
39081
|
--border-color-checked: var(--accent-color);
|
|
38904
39082
|
--checkmark-color: white;
|
|
@@ -39638,7 +39816,7 @@ installImportMetaCssBuild(import.meta);const css$D = /* css */`
|
|
|
39638
39816
|
--border-color: var(--navi-control-border-color);
|
|
39639
39817
|
--background-color: white;
|
|
39640
39818
|
--background-color-checked: var(--background-color);
|
|
39641
|
-
--accent-color:
|
|
39819
|
+
--accent-color: var(--navi-control-accent-color);
|
|
39642
39820
|
--radiomark-color: var(--accent-color);
|
|
39643
39821
|
--border-color-checked: var(--accent-color);
|
|
39644
39822
|
--cursor: pointer;
|
|
@@ -40150,7 +40328,7 @@ installImportMetaCssBuild(import.meta);const css$C = /* css */`
|
|
|
40150
40328
|
--font-family: var(--navi-control-font-family);
|
|
40151
40329
|
|
|
40152
40330
|
--loader-color: var(--navi-loader-color);
|
|
40153
|
-
--accent-color:
|
|
40331
|
+
--accent-color: var(--navi-control-accent-color);
|
|
40154
40332
|
--color-mix-light: black;
|
|
40155
40333
|
--color-mix-dark: white;
|
|
40156
40334
|
--color-mix: var(--color-mix-dark);
|
|
@@ -42983,6 +43161,13 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
42983
43161
|
* the map one would have drawn for a line, so it is drawn here and everything
|
|
42984
43162
|
* below only ever knows about maps.
|
|
42985
43163
|
*
|
|
43164
|
+
* A travel is ONE BOX long, whatever the distance between the two slides on the
|
|
43165
|
+
* map: the slide arriving is placed next to the one being left for the duration
|
|
43166
|
+
* and put back where the map says afterwards, so a tab bar jumping from the
|
|
43167
|
+
* first tab to the last shows those two and nothing else. Nobody wants to watch
|
|
43168
|
+
* the slides in between fly past — least of all in a tab bar, where they are
|
|
43169
|
+
* not a road one travels but places one goes straight to.
|
|
43170
|
+
*
|
|
42986
43171
|
* The slides live INSIDE the box, which is what makes this work for a popup: a
|
|
42987
43172
|
* dialog and a popover are both promoted to the browser's top layer, so no
|
|
42988
43173
|
* container of ours could ever hold two of them side by side and translate the
|
|
@@ -43086,6 +43271,18 @@ const css$z = /* css */`
|
|
|
43086
43271
|
slides. */
|
|
43087
43272
|
translate: var(--slide-offset, 0);
|
|
43088
43273
|
}
|
|
43274
|
+
/* Off stage: everything but the slide one is looking at and, while the
|
|
43275
|
+
track moves, the slide one is leaving. A travel is one box long
|
|
43276
|
+
whatever the distance on the map (the two are placed a box apart for
|
|
43277
|
+
the occasion, see the layout effect), so the slides in between are
|
|
43278
|
+
never crossed — but on a map wider than one box they would still sit
|
|
43279
|
+
in the frame, and a tab bar jumping from the first tab to the last must
|
|
43280
|
+
show those two and nothing else.
|
|
43281
|
+
visibility, not display: this box is measured on its LARGEST slide, and
|
|
43282
|
+
a slide taken out of the layout would take its size out with it. */
|
|
43283
|
+
> [data-slide][data-slide-offstage] {
|
|
43284
|
+
visibility: hidden;
|
|
43285
|
+
}
|
|
43089
43286
|
/* Nothing here for a slide not on screen: [inert] (set from JS) already
|
|
43090
43287
|
takes it out of reach of the pointer, of Tab and of a screen reader —
|
|
43091
43288
|
one attribute instead of pointer-events plus aria-hidden, and the only
|
|
@@ -43317,6 +43514,25 @@ const SlideContainer = ({
|
|
|
43317
43514
|
// mid-travel would read a moving value.
|
|
43318
43515
|
const offsetRef = useRef();
|
|
43319
43516
|
const trackAnimationRef = useRef(null);
|
|
43517
|
+
// Where the slides are while the track moves, which is not where the map says
|
|
43518
|
+
// they are: the one being left stays put and the one arriving is placed ONE
|
|
43519
|
+
// BOX away from it, whichever way the travel goes and however far apart the
|
|
43520
|
+
// two are on the map. So a jump from the first tab to the last is one box of
|
|
43521
|
+
// travel and the tabs in between are never seen flying past — and everything
|
|
43522
|
+
// not in this map is off stage for the duration. Null at rest, when the map
|
|
43523
|
+
// is the whole truth again (see settleTravel).
|
|
43524
|
+
const stageRef = useRef(null);
|
|
43525
|
+
// Which slide the last drawing put on screen: the current one at rest, the
|
|
43526
|
+
// one being travelled TO while the track moves. What the next travel departs
|
|
43527
|
+
// from, because it is what one is looking at.
|
|
43528
|
+
const drawnAreaRef = useRef(undefined);
|
|
43529
|
+
// Which way the travel about to be drawn goes, when whatever asked for it
|
|
43530
|
+
// knows: a window stepping off its last slide comes back on its first, and
|
|
43531
|
+
// only the press says that is a step forward — the map, read between those
|
|
43532
|
+
// two places, says the opposite. Undefined for a travel asked for by name
|
|
43533
|
+
// (--navi-go-to-slide, --navi-back), where the map is the only thing that
|
|
43534
|
+
// knows and is right.
|
|
43535
|
+
const travelStepRef = useRef(null);
|
|
43320
43536
|
// What to do once the travel now starting is over, handed to the animation as
|
|
43321
43537
|
// soon as there is one.
|
|
43322
43538
|
const rollBackRef = useRef(null);
|
|
@@ -43418,6 +43634,52 @@ const SlideContainer = ({
|
|
|
43418
43634
|
}
|
|
43419
43635
|
};
|
|
43420
43636
|
|
|
43637
|
+
// The travel is over: the stage is struck and every slide goes back where the
|
|
43638
|
+
// map says it is. Nothing is seen moving for it — the slide on screen sits at
|
|
43639
|
+
// the same place whatever the arrangement (its own offset and the track's are
|
|
43640
|
+
// opposites and cancel out), and the others are off stage — as long as the
|
|
43641
|
+
// two are written in one go, which is why this is done here rather than left
|
|
43642
|
+
// to a render.
|
|
43643
|
+
const settleTravel = () => {
|
|
43644
|
+
const track = trackRef.current;
|
|
43645
|
+
if (!track) {
|
|
43646
|
+
return;
|
|
43647
|
+
}
|
|
43648
|
+
const {
|
|
43649
|
+
slideElements,
|
|
43650
|
+
placeOf
|
|
43651
|
+
} = readMap();
|
|
43652
|
+
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
|
|
43653
|
+
if (!currentElement) {
|
|
43654
|
+
return;
|
|
43655
|
+
}
|
|
43656
|
+
stageRef.current = null;
|
|
43657
|
+
trackAnimationRef.current = null;
|
|
43658
|
+
for (const slideElement of slideElements) {
|
|
43659
|
+
const {
|
|
43660
|
+
x,
|
|
43661
|
+
y
|
|
43662
|
+
} = placeOf.get(readArea(slideElement)) || {
|
|
43663
|
+
x: 0,
|
|
43664
|
+
y: 0
|
|
43665
|
+
};
|
|
43666
|
+
slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
|
|
43667
|
+
slideElement.toggleAttribute("data-slide-offstage", slideElement !== currentElement);
|
|
43668
|
+
}
|
|
43669
|
+
const currentArea = readArea(currentElement);
|
|
43670
|
+
drawnAreaRef.current = currentArea;
|
|
43671
|
+
const {
|
|
43672
|
+
x,
|
|
43673
|
+
y
|
|
43674
|
+
} = placeOf.get(currentArea) || {
|
|
43675
|
+
x: 0,
|
|
43676
|
+
y: 0
|
|
43677
|
+
};
|
|
43678
|
+
const offset = `${-x * 100}% ${-y * 100}%`;
|
|
43679
|
+
offsetRef.current = offset;
|
|
43680
|
+
track.style.setProperty("--slide-container-offset", offset);
|
|
43681
|
+
};
|
|
43682
|
+
|
|
43421
43683
|
// Everything positional is decided here, from the DOM, once per render: where
|
|
43422
43684
|
// each slide stands on the map, which one is current, and how far the track
|
|
43423
43685
|
// must be for that one to be the one on screen. Reading the DOM is what makes
|
|
@@ -43435,10 +43697,59 @@ const SlideContainer = ({
|
|
|
43435
43697
|
// Nothing named, or a name nothing answers to: the first slide is the one
|
|
43436
43698
|
// shown, the way a stack of pages opens on its first page.
|
|
43437
43699
|
slideElements[0];
|
|
43438
|
-
const
|
|
43700
|
+
const currentArea = readArea(currentElement);
|
|
43701
|
+
const realPlaceOf = area => placeOf.get(area) || {
|
|
43439
43702
|
x: 0,
|
|
43440
43703
|
y: 0
|
|
43441
43704
|
};
|
|
43705
|
+
const durationMs = durationToMs(duration);
|
|
43706
|
+
// Nothing is travelling, so nothing is staged: the picture to paint is the
|
|
43707
|
+
// map itself, and a stage left over from a travel that has just been given
|
|
43708
|
+
// up on would be painted instead of it.
|
|
43709
|
+
if (noTravel) {
|
|
43710
|
+
stageRef.current = null;
|
|
43711
|
+
}
|
|
43712
|
+
let stage = stageRef.current;
|
|
43713
|
+
const drawnArea = stage ? stage.area : drawnAreaRef.current;
|
|
43714
|
+
const travelStarts = !noTravel && durationMs > 0 && drawnArea !== undefined && drawnArea !== currentArea && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
|
|
43715
|
+
if (travelStarts) {
|
|
43716
|
+
// Where the slide being left IS — the place the stage gave it if a travel
|
|
43717
|
+
// was already playing (a press landing mid-travel departs from where the
|
|
43718
|
+
// eye is, not from the map), its own place otherwise.
|
|
43719
|
+
const departurePlace = stage ? stage.placeByArea.get(drawnArea) ?? realPlaceOf(drawnArea) : realPlaceOf(drawnArea);
|
|
43720
|
+
const step = travelStepRef.current || {
|
|
43721
|
+
x: Math.sign(realPlaceOf(currentArea).x - realPlaceOf(drawnArea).x),
|
|
43722
|
+
y: Math.sign(realPlaceOf(currentArea).y - realPlaceOf(drawnArea).y)
|
|
43723
|
+
};
|
|
43724
|
+
// Kept, not replaced: the slides a chain of quick presses has already
|
|
43725
|
+
// left behind are still trailing off screen, and taking them off stage
|
|
43726
|
+
// now would blink them out mid-travel.
|
|
43727
|
+
const placeByArea = new Map(stage?.placeByArea);
|
|
43728
|
+
placeByArea.set(drawnArea, departurePlace);
|
|
43729
|
+
const arrivalPlace = {
|
|
43730
|
+
x: departurePlace.x + step.x,
|
|
43731
|
+
y: departurePlace.y + step.y
|
|
43732
|
+
};
|
|
43733
|
+
// The cell the arriving slide takes, taken back from whoever was left
|
|
43734
|
+
// standing on it: a travel turned around mid-flight comes back over
|
|
43735
|
+
// ground it has just covered, and the slide it left there would be
|
|
43736
|
+
// underneath the one arriving — two pictures in one box.
|
|
43737
|
+
for (const [stagedArea, stagedPlace] of placeByArea) {
|
|
43738
|
+
if (stagedArea !== drawnArea && stagedArea !== currentArea && stagedPlace.x === arrivalPlace.x && stagedPlace.y === arrivalPlace.y) {
|
|
43739
|
+
placeByArea.delete(stagedArea);
|
|
43740
|
+
}
|
|
43741
|
+
}
|
|
43742
|
+
placeByArea.set(currentArea, arrivalPlace);
|
|
43743
|
+
stage = stageRef.current = {
|
|
43744
|
+
placeByArea,
|
|
43745
|
+
area: currentArea
|
|
43746
|
+
};
|
|
43747
|
+
}
|
|
43748
|
+
// Said about the travel now being drawn and about no other: a re-render in
|
|
43749
|
+
// the middle of one reads the stage back, which already knows.
|
|
43750
|
+
travelStepRef.current = null;
|
|
43751
|
+
const placeOfArea = area => stage && stage.placeByArea.get(area) || realPlaceOf(area);
|
|
43752
|
+
const currentPlace = placeOfArea(currentArea);
|
|
43442
43753
|
// A transfer waiting for a travel that never happened — a controlled
|
|
43443
43754
|
// `current` the caller chose not to move: dropped, the focus has no
|
|
43444
43755
|
// business going anywhere. The one for a travel that DID happen stays,
|
|
@@ -43454,17 +43765,18 @@ const SlideContainer = ({
|
|
|
43454
43765
|
// and that one knows what was pressed.
|
|
43455
43766
|
const focusIsLeaving = !focusHandOverRef.current && slideElements.some(slideElement => slideElement !== currentElement && slideElement.contains(document.activeElement));
|
|
43456
43767
|
for (const slideElement of slideElements) {
|
|
43768
|
+
const area = readArea(slideElement);
|
|
43457
43769
|
const {
|
|
43458
43770
|
x,
|
|
43459
43771
|
y
|
|
43460
|
-
} =
|
|
43461
|
-
x: 0,
|
|
43462
|
-
y: 0
|
|
43463
|
-
};
|
|
43772
|
+
} = placeOfArea(area);
|
|
43464
43773
|
slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
|
|
43465
43774
|
const isCurrent = slideElement === currentElement;
|
|
43466
43775
|
slideElement.toggleAttribute("data-current", isCurrent);
|
|
43467
43776
|
slideElement.toggleAttribute("data-slide-displaced", !isCurrent);
|
|
43777
|
+
// On stage: the two ends of the travel while there is one, and the slide
|
|
43778
|
+
// being shown when there is not.
|
|
43779
|
+
slideElement.toggleAttribute("data-slide-offstage", stage ? !stage.placeByArea.has(area) : !isCurrent);
|
|
43468
43780
|
if (isCurrent) {
|
|
43469
43781
|
// Reachable again first, so the focus below has somewhere to land: an
|
|
43470
43782
|
// inert element cannot take it.
|
|
@@ -43491,7 +43803,6 @@ const SlideContainer = ({
|
|
|
43491
43803
|
// Where the track ends up, always — the animation below only covers the way
|
|
43492
43804
|
// there, and when it is over this is what holds.
|
|
43493
43805
|
track.style.setProperty("--slide-container-offset", offset);
|
|
43494
|
-
const durationMs = durationToMs(duration);
|
|
43495
43806
|
const travels = !noTravel && durationMs > 0 && offsetBefore !== undefined && offsetBefore !== offset;
|
|
43496
43807
|
if (travels) {
|
|
43497
43808
|
// The time it takes is the distance it has left to cover: a travel picked
|
|
@@ -43522,6 +43833,18 @@ const SlideContainer = ({
|
|
|
43522
43833
|
if (pendingRollsRef.current.length) {
|
|
43523
43834
|
hurryTravel(trackAnimationRef.current);
|
|
43524
43835
|
}
|
|
43836
|
+
// Arrived: the map is the truth again (see settleTravel). Attached before
|
|
43837
|
+
// the window's own roll back just below, so the stage is struck first and
|
|
43838
|
+
// whatever that one renders is drawn from the map.
|
|
43839
|
+
trackAnimationRef.current.finished.then(settleTravel, () => {
|
|
43840
|
+
// cancelled by the next travel — that one carries the stage on
|
|
43841
|
+
});
|
|
43842
|
+
} else if (stage && trackAnimationRef.current?.playState !== "running") {
|
|
43843
|
+
// Staged with nothing left to play: a travel that was drawn and then had
|
|
43844
|
+
// its animation taken away (a duration set to 0, a re-render landing
|
|
43845
|
+
// between the two). Struck at once rather than left standing, since the
|
|
43846
|
+
// thing it was standing for is over.
|
|
43847
|
+
settleTravel();
|
|
43525
43848
|
}
|
|
43526
43849
|
// A window waiting for its travel to be over (see goToArea's own loop
|
|
43527
43850
|
// branch): the animation says when, and says it about the move that just
|
|
@@ -43558,6 +43881,13 @@ const SlideContainer = ({
|
|
|
43558
43881
|
for (const slideElement of slideElements) {
|
|
43559
43882
|
slideElement.toggleAttribute("inert", slideElement !== currentElement);
|
|
43560
43883
|
}
|
|
43884
|
+
// What is on screen now, for the travel after this one to depart from.
|
|
43885
|
+
// While a travel is playing the stage is the one that knows (it holds the
|
|
43886
|
+
// slide being travelled TO, which is what one is looking at), so this is
|
|
43887
|
+
// only ever written at rest.
|
|
43888
|
+
if (!stageRef.current) {
|
|
43889
|
+
drawnAreaRef.current = currentArea;
|
|
43890
|
+
}
|
|
43561
43891
|
});
|
|
43562
43892
|
|
|
43563
43893
|
/**
|
|
@@ -43652,6 +43982,15 @@ const SlideContainer = ({
|
|
|
43652
43982
|
...cameFromRef.current,
|
|
43653
43983
|
[area]: readArea(currentElement)
|
|
43654
43984
|
};
|
|
43985
|
+
// Which way this travel is drawn, said by what asked for it rather than
|
|
43986
|
+
// read off the map: a window stepping off its last slide comes back on its
|
|
43987
|
+
// first, and between those two places the map says "all the way back" when
|
|
43988
|
+
// the press said "one forward". Nothing to say when the travel was asked
|
|
43989
|
+
// for by name — there the map is the only one who knows.
|
|
43990
|
+
travelStepRef.current = dx || dy ? {
|
|
43991
|
+
x: Math.sign(dx),
|
|
43992
|
+
y: Math.sign(dy)
|
|
43993
|
+
} : null;
|
|
43655
43994
|
if (loop) {
|
|
43656
43995
|
// A window does not change slide, it rolls: the travel plays, and once it
|
|
43657
43996
|
// is over the window is put back where it rests while whoever owns the
|
|
@@ -47826,13 +48165,10 @@ const css$u = /* css */`
|
|
|
47826
48165
|
font-size: 1em;
|
|
47827
48166
|
line-height: 1.4;
|
|
47828
48167
|
}
|
|
47829
|
-
/*
|
|
47830
|
-
|
|
47831
|
-
|
|
47832
|
-
|
|
47833
|
-
and disappear as things load. Targeted on the outline itself rather than
|
|
47834
|
-
inherited from the item, so a control nested deeper (which has room around
|
|
47835
|
-
it, and does not reach the edges) keeps the outline it asked for. */
|
|
48168
|
+
/* Same rule as [data-scrollable] in box.jsx, said again for this scroller:
|
|
48169
|
+
what an item holds IS against the edge of the scroll container — the list
|
|
48170
|
+
element between the two is markup, not spacing — so its loading outline
|
|
48171
|
+
stays inside its own box rather than raising a scrollbar. */
|
|
47836
48172
|
.navi_list_item > .navi_loading_outline_wrapper,
|
|
47837
48173
|
.navi_list_item > * > .navi_loading_outline_wrapper,
|
|
47838
48174
|
.navi_list_item_header > * > .navi_loading_outline_wrapper,
|
|
@@ -53586,6 +53922,18 @@ const css$n = /* css */`
|
|
|
53586
53922
|
/* The control grows itself; resizable below hands the handle back. */
|
|
53587
53923
|
resize: none;
|
|
53588
53924
|
overflow: auto;
|
|
53925
|
+
/* A placeholder must be readable in full before anything is typed: a
|
|
53926
|
+
field that opens already scrolled reads as a field that already has
|
|
53927
|
+
text in it. Its wrapped height is measured (see usePlaceholderHeight)
|
|
53928
|
+
because it only exists once laid out, and it only raises the floor
|
|
53929
|
+
while the placeholder is what is being shown — what is typed sizes the
|
|
53930
|
+
box on its own. */
|
|
53931
|
+
&:placeholder-shown {
|
|
53932
|
+
min-height: max(
|
|
53933
|
+
calc(var(--textarea-min-rows, 1.5) * 1lh),
|
|
53934
|
+
var(--x-textarea-placeholder-height, 0px)
|
|
53935
|
+
);
|
|
53936
|
+
}
|
|
53589
53937
|
}
|
|
53590
53938
|
&[data-resizable] .navi_control_input {
|
|
53591
53939
|
height: calc(var(--textarea-min-rows, 1.5) * 1lh);
|
|
@@ -53623,7 +53971,9 @@ const css$n = /* css */`
|
|
|
53623
53971
|
* @param {number} [maxRows] Lines after which the control stops growing and
|
|
53624
53972
|
* scrolls instead. Without it the control grows with its content.
|
|
53625
53973
|
* @param {boolean} [resizable] Give the browser's vertical resize handle back.
|
|
53626
|
-
*
|
|
53974
|
+
* An exchange, not an addition: the hand takes over from the automatic
|
|
53975
|
+
* growth, so the control stops following what is typed and stays at the
|
|
53976
|
+
* height it was last dragged to (starting at `minRows`).
|
|
53627
53977
|
* @param {number} [maxLength] The character limit, validated at submit. Pair
|
|
53628
53978
|
* with `maxLengthGuard` to block typing past it, and render a
|
|
53629
53979
|
* TextareaCharCount to show it.
|
|
@@ -53645,6 +53995,7 @@ const Textarea = ({
|
|
|
53645
53995
|
import.meta.css = [inputCss + css$n, "@jsenv/navi/src/control/input/textarea.jsx"];
|
|
53646
53996
|
const defaultRef = useRef(null);
|
|
53647
53997
|
props.ref = props.ref || defaultRef;
|
|
53998
|
+
usePlaceholderHeight(props.ref, props.placeholder);
|
|
53648
53999
|
const [rootProps, hostProps, childrenWrapperProps] = useControlProps(props, {
|
|
53649
54000
|
controlType: "input"
|
|
53650
54001
|
});
|
|
@@ -53725,6 +54076,61 @@ const TextareaCharCount = ({
|
|
|
53725
54076
|
children: maxLength === undefined ? length : `${length}/${maxLength}`
|
|
53726
54077
|
});
|
|
53727
54078
|
};
|
|
54079
|
+
|
|
54080
|
+
// `field-sizing: content` sizes the box from the value, and an empty field has
|
|
54081
|
+
// none — the placeholder is text the browser refuses to make room for. So the
|
|
54082
|
+
// height it wraps to is measured and published as --x-textarea-placeholder-height
|
|
54083
|
+
// for the CSS above to use as a floor.
|
|
54084
|
+
const usePlaceholderHeight = (ref, placeholder) => {
|
|
54085
|
+
useLayoutEffect(() => {
|
|
54086
|
+
const textareaEl = ref.current;
|
|
54087
|
+
if (!placeholder) {
|
|
54088
|
+
textareaEl.style.removeProperty("--x-textarea-placeholder-height");
|
|
54089
|
+
return null;
|
|
54090
|
+
}
|
|
54091
|
+
let widthMeasured;
|
|
54092
|
+
const measure = () => {
|
|
54093
|
+
// What is typed sizes the box itself; the placeholder is not displayed
|
|
54094
|
+
// then, and scrollHeight would report the value's height instead.
|
|
54095
|
+
if (textareaEl.value !== "") {
|
|
54096
|
+
return;
|
|
54097
|
+
}
|
|
54098
|
+
const {
|
|
54099
|
+
paddingTop,
|
|
54100
|
+
paddingBottom
|
|
54101
|
+
} = getComputedStyle(textareaEl);
|
|
54102
|
+
// Cleared before reading: scrollHeight can never report less than the
|
|
54103
|
+
// height already applied, so measuring on top of a previous measure could
|
|
54104
|
+
// only ever grow the box, never let it shrink back on a wider viewport.
|
|
54105
|
+
textareaEl.style.setProperty("--x-textarea-placeholder-height", "0px");
|
|
54106
|
+
const contentHeight = textareaEl.scrollHeight - parseFloat(paddingTop) - parseFloat(paddingBottom);
|
|
54107
|
+
widthMeasured = textareaEl.clientWidth;
|
|
54108
|
+
textareaEl.style.setProperty("--x-textarea-placeholder-height", `${contentHeight}px`);
|
|
54109
|
+
};
|
|
54110
|
+
measure();
|
|
54111
|
+
// The placeholder wraps against the available width, so a new width is a
|
|
54112
|
+
// new number of lines. Height changes are ignored: this measure is what
|
|
54113
|
+
// causes them, and reacting to them would be reacting to ourselves.
|
|
54114
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
54115
|
+
if (textareaEl.clientWidth !== widthMeasured) {
|
|
54116
|
+
measure();
|
|
54117
|
+
}
|
|
54118
|
+
});
|
|
54119
|
+
resizeObserver.observe(textareaEl);
|
|
54120
|
+
// The width may have changed while the field held a value, when measuring
|
|
54121
|
+
// was impossible — emptying it is when the placeholder comes back.
|
|
54122
|
+
const onInput = () => {
|
|
54123
|
+
if (textareaEl.value === "") {
|
|
54124
|
+
measure();
|
|
54125
|
+
}
|
|
54126
|
+
};
|
|
54127
|
+
textareaEl.addEventListener("input", onInput);
|
|
54128
|
+
return () => {
|
|
54129
|
+
resizeObserver.disconnect();
|
|
54130
|
+
textareaEl.removeEventListener("input", onInput);
|
|
54131
|
+
};
|
|
54132
|
+
}, [placeholder]);
|
|
54133
|
+
};
|
|
53728
54134
|
const RealTextarea = ({
|
|
53729
54135
|
maxLength,
|
|
53730
54136
|
...domProps
|