@jsenv/navi 0.29.29 → 0.29.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  * using @jsenv/navi as intended.
4
4
  */
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, coarsePointerSignal } from "./jsenv_navi_side_effects.js";
6
- import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, startDragToTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
6
+ import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
7
7
  export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, batch, useSignal } from "@preact/signals";
9
9
  import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
@@ -11,8 +11,66 @@ import { useContext, useLayoutEffect, useCallback, useRef, useState, useEffect,
11
11
  import { jsx, jsxs, Fragment as Fragment$1 } from "preact/jsx-runtime";
12
12
  import { createValidity, parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, durationToISOString } from "@jsenv/validity";
13
13
  export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
14
- import { createPortal, Suspense, forwardRef } from "preact/compat";
15
14
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
15
+ import { createPortal, Suspense, forwardRef } from "preact/compat";
16
+
17
+ installImportMetaCssBuild(import.meta);/**
18
+ * Every z-index navi can be seen competing on, in one place, ordered.
19
+ *
20
+ * The point is the overview: reading this file must be enough to know what
21
+ * paints over what, and adding a value here forces the question "against
22
+ * what?" to be answered before a number is written. These are not meant as a
23
+ * theming surface — override one only to get out of a conflict with an app
24
+ * that cannot move.
25
+ *
26
+ * What belongs here: anything whose stacking is decided against an element
27
+ * from ANOTHER component — a bar, a popup, a control raising itself above the
28
+ * neighbour it overlaps. A z-index that only orders a component's own parts
29
+ * (a shadow under its own content, a checkbox mark over its own box) stays a
30
+ * literal next to the rule that needs it: it says "above my sibling", nothing
31
+ * more, and routing it through a token would only dilute the overview.
32
+ *
33
+ * The bands are decades apart so a band can grow without reaching the next
34
+ * one, and so a value read in a devtools panel says which band it came from.
35
+ *
36
+ * Read docs/z_index.md first — DOM order and `isolation: isolate` come before
37
+ * any of these, and a number is the last resort, not the first tool.
38
+ */
39
+
40
+ const css$10 = /* css */`
41
+ @layer navi {
42
+ :root {
43
+ /* A control that overlaps its neighbours (the members of a Group share
44
+ one border along each seam) and has to paint in front of them while
45
+ the user is on it: its border color change, then its focus ring, which
46
+ a neighbour painted later would otherwise slice in half. Focus above
47
+ hover, since a focused member can sit next to a hovered one. */
48
+ --navi-z-index-control-hovered: 1;
49
+ --navi-z-index-control-focused: 2;
50
+
51
+ /* Kept stuck while something scrolls under it: a list header, the head
52
+ and foot of a side panel, a table's sticky cells. Above raised
53
+ controls — a control scrolling past must go under the header that
54
+ pins the column it belongs to, never over it. */
55
+ --navi-z-index-sticky: 10;
56
+
57
+ /* Pinned to the viewport, over the whole page: FixedBar. A decade of its
58
+ own and well above the sticky band, so that no sticky cell and no
59
+ hovered control can ever be seen crossing it. */
60
+ --navi-z-index-bar: 100;
61
+
62
+ /* Popups: Dialog/Popover with layer="local", their backdrop, and the
63
+ validation callouts. Above everything the page can produce, which is
64
+ the whole point of the gap — a popup never has to guess. Each opened
65
+ popup adds its stack order on top, so the last one opened wins.
66
+ Dialog/Popover with layer="top" use the browser top layer instead and
67
+ appear in no scale at all. */
68
+ --navi-z-index-popup: 1000;
69
+ --navi-z-index-callout: var(--navi-z-index-popup);
70
+ }
71
+ }
72
+ `;
73
+ import.meta.css = [css$10, "@jsenv/navi/src/navi_z_indexes.js"];
16
74
 
17
75
  const addIntoArray = (array, ...valuesToAdd) => {
18
76
  if (valuesToAdd.length === 1) {
@@ -7583,7 +7641,7 @@ const css$_ = /* css */`
7583
7641
  --callout-background-color: white;
7584
7642
  --callout-icon-color: black;
7585
7643
  --callout-padding: 8px;
7586
- --callout-z-index: 1000;
7644
+ --callout-z-index: var(--navi-z-index-callout);
7587
7645
  }
7588
7646
  }
7589
7647
 
@@ -11697,6 +11755,657 @@ const readNumberFromDom = (element, attribute, defaultValue) => {
11697
11755
  return value;
11698
11756
  };
11699
11757
 
11758
+ /**
11759
+ * The interactions the browser already has an event for.
11760
+ *
11761
+ * Nothing to detect: the name IS the event type, so this listens and says it
11762
+ * happened. Registered through the same door an application uses (see
11763
+ * interaction_registry.js) — navi has no private one.
11764
+ *
11765
+ * `contextmenu` is the only one that takes something away: the browser's own menu
11766
+ * would cover the answer to the request it is. It takes it away AFTER, though —
11767
+ * a native interaction IS its own event, the very one the gate reads, and a gate
11768
+ * refuses what is already `defaultPrevented`. Cancelling first would therefore
11769
+ * make the interaction refuse itself, and no right click could ever get through.
11770
+ * So: ask, and only cover the browser's menu once something answered.
11771
+ */
11772
+
11773
+
11774
+ const NATIVE_TYPE_SET = new Set([
11775
+ "mousedown",
11776
+ "mouseup",
11777
+ "click",
11778
+ "dblclick",
11779
+ "contextmenu",
11780
+ ]);
11781
+
11782
+ defineInteractionDetector({
11783
+ name: "native",
11784
+ claims: (type) => NATIVE_TYPE_SET.has(type),
11785
+ setup: (element, trigger, { types }) => {
11786
+ const listeners = types.map((type) => {
11787
+ const listener = (nativeEvent) => {
11788
+ const performed = trigger(type, nativeEvent);
11789
+ if (type === "contextmenu" && performed) {
11790
+ // Something answered the request, so the browser's own menu would only
11791
+ // cover it. Nothing answered (the gate refused, a listener said "not
11792
+ // this time"): the right click stays what it was, and opens the menu.
11793
+ nativeEvent.preventDefault();
11794
+ }
11795
+ };
11796
+ element.addEventListener(type, listener);
11797
+ return [type, listener];
11798
+ });
11799
+ return () => {
11800
+ for (const [type, listener] of listeners) {
11801
+ element.removeEventListener(type, listener);
11802
+ }
11803
+ };
11804
+ },
11805
+ });
11806
+
11807
+ installImportMetaCssBuild(import.meta);/**
11808
+ * What a press can turn out to be: a swipe, or a hold.
11809
+ *
11810
+ * One detector for both, because they dispute the SAME press and something has to
11811
+ * arbitrate them in one place — read apart, a hold that drifts three pixels both
11812
+ * opens the menu and starts putting the row away. A finger lands; it leaves
11813
+ * sideways (a swipe), it stays still (a hold), it lifts at once (a click, which is
11814
+ * not this detector's business).
11815
+ *
11816
+ * Naming the interactions in `interactions` rather than letting one callback read
11817
+ * the pointer is what lets this know, BEFORE the first pixel, which of them the
11818
+ * element takes. Two things depend on knowing that early:
11819
+ *
11820
+ * - **The arbitration between nested boxes.** A row swiped sideways lives inside a
11821
+ * container that travels sideways too, and the innermost box must take the axis
11822
+ * it walks — read from the DOM at the press, and only from there (see axesLeftBy
11823
+ * in @jsenv/dom). The axis comes from the name (`swipe_left` and `swipe_right`
11824
+ * say `x`), so the attribute is written at render time. Written during the
11825
+ * gesture it would arrive too late: a browser decides what a touch may do when
11826
+ * the touch BEGINS. Same for `touch-action` and iOS's callout below, which is
11827
+ * why they are a stylesheet and not a line of JS in the pointerdown.
11828
+ *
11829
+ * - **The click a gesture leaves behind.** A hold ends with a `pointerup`, so the
11830
+ * browser follows it with a `click` on whatever the finger was over — a link,
11831
+ * and the page navigates as well as opening the menu.
11832
+ *
11833
+ * A swipe makes the element follow the finger — there is nothing to decide about
11834
+ * that — and says where it is up to, for the caller to draw the rest with:
11835
+ *
11836
+ * - `--swipe-pulled`: how far it has come, signed, in px.
11837
+ * - `--swipe-progress`: the same as a fraction of the element, signed.
11838
+ *
11839
+ * Both inherit, so anything inside the element can read them.
11840
+ * - `[data-swiping="left|right|up|down"]`: which way, while a finger holds it.
11841
+ * - `[data-swipe-past-threshold]`: letting go now would go through with it.
11842
+ *
11843
+ * WHAT is revealed behind is the caller's: navi does not know what putting a row
11844
+ * away looks like. A trail is usually a child of the swiped element sized off
11845
+ * `--swipe-pulled`, which is what makes those values reachable from CSS at all — a
11846
+ * sibling could not read them.
11847
+ *
11848
+ * A swipe is its own gesture, not the drag-to-travel of a slide container: it
11849
+ * borrows the same reader (`startDragToTravel`, whose axis lock, resistance, flick
11850
+ * and click-swallowing are exactly what a swipe needs) with its own settings, and
11851
+ * it is those settings that differ — a third of a row rather than a third of a
11852
+ * screen, tuned per element with `data-swipe-threshold`.
11853
+ *
11854
+ * A hold does NOT take the context menu with it. Declaring one says what a held
11855
+ * FINGER does — a finger held down being the system's own context-menu gesture,
11856
+ * which is why that one is refused while the wait runs (see waitForPressHeld). A
11857
+ * right click is not that press: it comes from the other button and it is the
11858
+ * user asking for the browser's menu, so it keeps opening it. An element that
11859
+ * wants the right click to do what the hold does says so, with `contextmenu`
11860
+ * beside it.
11861
+ *
11862
+ * A hold CAN open a popup while the finger is still down — a menu appearing under
11863
+ * a waiting finger, which is the native gesture. navi's Popover is `popover="manual"`
11864
+ * and owns its own dismissal, so the `pointerup` that ends the press is not read as
11865
+ * an interaction outside it (the browser's light dismiss, which would close it on
11866
+ * that very event, only applies to `popover="auto"`).
11867
+ */
11868
+
11869
+ // The axis each swipe names, which is the whole reason they are named rather than
11870
+ // counted: an element that takes a horizontal swipe has to say so in the DOM
11871
+ // before it is touched.
11872
+ const AXIS_BY_SWIPE_TYPE = {
11873
+ swipe_left: "x",
11874
+ swipe_right: "x",
11875
+ swipe_up: "y",
11876
+ swipe_down: "y"
11877
+ };
11878
+ // Which way the content goes. A drag towards positive x moves the row right, which
11879
+ // is a swipe right — and, for the travel underneath, the direction that brings in
11880
+ // what comes BEFORE (`travelBack`).
11881
+ const SWIPE_TYPE_BY_AXIS = {
11882
+ x: {
11883
+ positive: "swipe_right",
11884
+ negative: "swipe_left"
11885
+ },
11886
+ y: {
11887
+ positive: "swipe_down",
11888
+ negative: "swipe_up"
11889
+ }
11890
+ };
11891
+
11892
+ // How much of the element has to be pulled for letting go to go through with it. A
11893
+ // FRACTION and never a distance: the same gesture must mean the same thing on a
11894
+ // phone and on a wide screen. Speed answers on its own on top of this — a brief
11895
+ // flick counts whatever the distance covered.
11896
+ const SWIPE_THRESHOLD_DEFAULT = 0.33;
11897
+ // Under the system context-menu delay, so the press is answered before the menu it
11898
+ // would otherwise open.
11899
+ const LONGPRESS_DELAY_DEFAULT = 450;
11900
+ // Past this the finger is going somewhere: it swipes, it does not hold.
11901
+ const LONGPRESS_SLOP_DEFAULT = 8;
11902
+ // How long the element takes to reach where the gesture leaves it, or to come
11903
+ // back. Written into the CSS below from here: the state is cleaned up when the
11904
+ // movement is over, so a duration living only in the stylesheet would be a timing
11905
+ // JS has to know and cannot read reliably.
11906
+ const SETTLE_DURATION_MS = 200;
11907
+
11908
+ // Read off the element or off any ancestor carrying it, so a whole list is tuned
11909
+ // in one place and a stylesheet can read the same value.
11910
+ const SWIPE_THRESHOLD_ATTRIBUTE = "data-swipe-threshold";
11911
+ const LONGPRESS_DELAY_ATTRIBUTE = "data-longpress-delay";
11912
+ const LONGPRESS_SLOP_ATTRIBUTE = "data-longpress-slop";
11913
+
11914
+ // Which axes this element takes a swipe on, and that it takes a hold: said in the
11915
+ // DOM at render time, for the CSS below and for the boxes above to read.
11916
+ const SWIPE_AXES_ATTRIBUTE = "data-swipe";
11917
+ const LONGPRESS_ATTRIBUTE = "data-longpress";
11918
+ import.meta.css = [/* css */`
11919
+ /* Declared, so the browser sees a NUMBER it can interpolate and calculate with:
11920
+ what a swipe reveals behind the element is drawn from this, and an undeclared
11921
+ custom property only ever jumps from one value to the next. Both inherited,
11922
+ because what is drawn from them is drawn by a CHILD of the swiped element:
11923
+ the trail behind a row is inside the row, and a value that stopped at the
11924
+ element itself would reach 0 exactly where it is read. */
11925
+ @property --swipe-progress {
11926
+ syntax: "<number>";
11927
+ inherits: true;
11928
+ initial-value: 0;
11929
+ }
11930
+ @property --swipe-pulled {
11931
+ syntax: "<length>";
11932
+ inherits: true;
11933
+ initial-value: 0px;
11934
+ }
11935
+
11936
+ /* What a touch may do on an element that takes a swipe: the axis the swipe walks
11937
+ is taken, the other is left to the page — so a row is swiped sideways and the
11938
+ list still scrolls under the same finger. */
11939
+ [${SWIPE_AXES_ATTRIBUTE}="x"] {
11940
+ touch-action: pan-y;
11941
+ }
11942
+ [${SWIPE_AXES_ATTRIBUTE}="y"] {
11943
+ touch-action: pan-x;
11944
+ }
11945
+ [${SWIPE_AXES_ATTRIBUTE}="xy"] {
11946
+ touch-action: none;
11947
+ }
11948
+
11949
+ /* iOS shows its callout (Copy / Look Up) and selects the word under the finger on
11950
+ a press held still, and does not always route that through an event that can be
11951
+ refused. Same reason as the drag sources in @jsenv/dom: it has to be true
11952
+ before the finger lands. */
11953
+ [${LONGPRESS_ATTRIBUTE}] {
11954
+ -webkit-touch-callout: none;
11955
+ }
11956
+
11957
+ /* The element follows the finger. The translate property rather than a transform,
11958
+ so whatever transform the element (or its theme) already has is left alone. */
11959
+ [data-swiping="left"],
11960
+ [data-swiping="right"] {
11961
+ translate: var(--swipe-pulled) 0;
11962
+ }
11963
+ [data-swiping="up"],
11964
+ [data-swiping="down"] {
11965
+ translate: 0 var(--swipe-pulled);
11966
+ }
11967
+ /* No transition while the finger holds it — the element is where the hand put
11968
+ it — and one when the hand lets go. */
11969
+ [data-swipe-settling] {
11970
+ transition: translate ${SETTLE_DURATION_MS}ms ease-out;
11971
+ }
11972
+ `, "@jsenv/navi/src/control/interaction/interaction_press.js"];
11973
+ defineInteractionDetector({
11974
+ name: "press",
11975
+ claims: type => type in AXIS_BY_SWIPE_TYPE || type === "longpress",
11976
+ setup: (element, trigger, {
11977
+ types,
11978
+ readConfig
11979
+ }) => {
11980
+ let axes = "";
11981
+ for (const type of types) {
11982
+ const axis = AXIS_BY_SWIPE_TYPE[type];
11983
+ if (axis && !axes.includes(axis)) {
11984
+ axes += axis;
11985
+ }
11986
+ }
11987
+ const hasLongPress = types.includes("longpress");
11988
+ const undo = [];
11989
+ const mark = (attribute, value) => {
11990
+ element.setAttribute(attribute, value);
11991
+ undo.push(() => {
11992
+ element.removeAttribute(attribute);
11993
+ });
11994
+ };
11995
+ if (axes) {
11996
+ mark(SWIPE_AXES_ATTRIBUTE, axes);
11997
+ // Read by the boxes ABOVE this one: an axis swiped here is not theirs to
11998
+ // travel (see axesLeftBy).
11999
+ mark("data-travel-by-drag", axes);
12000
+ // …and nothing inside hands its leftover scroll to the page while the
12001
+ // element is being pulled.
12002
+ mark("data-drag-travel", axes);
12003
+
12004
+ // A link and an image are draggable without anyone asking, and a native
12005
+ // drag IS press-and-move: the browser claims the gesture, takes the pointer
12006
+ // events with it and paints a ghost of the row the hand is trying to swipe.
12007
+ // Two things are needed, and neither covers the other:
12008
+ // - `draggable` refuses it on the element itself, before it starts;
12009
+ // - a swipe is usually a row with a link or a thumbnail INSIDE it, and
12010
+ // those are draggable in their own right. `dragstart` bubbles, so
12011
+ // refusing it here refuses theirs too — and it is the only refusal every
12012
+ // browser honours (`-webkit-user-drag` is one engine's).
12013
+ // The cost is stated rather than worked around: an element that takes a
12014
+ // swipe cannot also be dragged out of the page, because there is one
12015
+ // gesture and it cannot mean both.
12016
+ mark("draggable", "false");
12017
+ const onDragStart = dragStartEvent => {
12018
+ dragStartEvent.preventDefault();
12019
+ };
12020
+ element.addEventListener("dragstart", onDragStart);
12021
+ undo.push(() => {
12022
+ element.removeEventListener("dragstart", onDragStart);
12023
+ });
12024
+ }
12025
+ if (hasLongPress) {
12026
+ mark(LONGPRESS_ATTRIBUTE, "");
12027
+ }
12028
+ const onPointerDown = pointerDownEvent => {
12029
+ if (pointerDownEvent.button !== 0) {
12030
+ return;
12031
+ }
12032
+ let swipe = null;
12033
+ let press = null;
12034
+ if (axes) {
12035
+ swipe = startSwipe(pointerDownEvent, {
12036
+ element,
12037
+ axes,
12038
+ types,
12039
+ trigger,
12040
+ threshold: readConfig(SWIPE_THRESHOLD_ATTRIBUTE, SWIPE_THRESHOLD_DEFAULT),
12041
+ // The axis is the definitive word on what this press is: a finger that
12042
+ // has picked one is swiping, not holding. The slop below cancels most
12043
+ // holds before this (it is the smaller distance), but a press that
12044
+ // resolves the axis without drifting — a mouse, a flick — has to be
12045
+ // taken from the wait too.
12046
+ onSwipeStart: () => {
12047
+ press?.cancel();
12048
+ press = null;
12049
+ }
12050
+ });
12051
+ }
12052
+ if (hasLongPress) {
12053
+ press = waitForPressHeld(pointerDownEvent, {
12054
+ delay: readConfig(LONGPRESS_DELAY_ATTRIBUTE, LONGPRESS_DELAY_DEFAULT),
12055
+ slop: readConfig(LONGPRESS_SLOP_ATTRIBUTE, LONGPRESS_SLOP_DEFAULT),
12056
+ onPressHeld: (pressEvent, {
12057
+ endPress
12058
+ }) => {
12059
+ // The hold won the arbitration: the swipe never got the distance it
12060
+ // needed, and must not get it from whatever the finger does next.
12061
+ swipe?.stop();
12062
+ swipe = null;
12063
+ const clickSuppressionIsOver = suppressClickAfterGesture();
12064
+ const onPointerEnd = () => {
12065
+ window.removeEventListener("pointerup", onPointerEnd, true);
12066
+ window.removeEventListener("pointercancel", onPointerEnd, true);
12067
+ endPress();
12068
+ clickSuppressionIsOver();
12069
+ };
12070
+ window.addEventListener("pointerup", onPointerEnd, true);
12071
+ window.addEventListener("pointercancel", onPointerEnd, true);
12072
+ trigger("longpress", pressEvent, {
12073
+ pointerType: pressEvent.pointerType
12074
+ });
12075
+ }
12076
+ });
12077
+ }
12078
+ };
12079
+ element.addEventListener("pointerdown", onPointerDown);
12080
+ undo.push(() => {
12081
+ element.removeEventListener("pointerdown", onPointerDown);
12082
+ });
12083
+ return () => {
12084
+ for (const undoOne of undo) {
12085
+ undoOne();
12086
+ }
12087
+ };
12088
+ }
12089
+ });
12090
+ const startSwipe = (pointerDownEvent, {
12091
+ element,
12092
+ axes,
12093
+ types,
12094
+ trigger,
12095
+ threshold,
12096
+ onSwipeStart
12097
+ }) => {
12098
+ let settleTimeout = null;
12099
+ const paint = ({
12100
+ pulled,
12101
+ progress,
12102
+ type
12103
+ }) => {
12104
+ element.style.setProperty("--swipe-pulled", `${pulled}px`);
12105
+ element.style.setProperty("--swipe-progress", progress);
12106
+ element.setAttribute("data-swiping", type.slice("swipe_".length));
12107
+ element.toggleAttribute("data-swipe-past-threshold", Math.abs(progress) >= threshold);
12108
+ };
12109
+ const forget = () => {
12110
+ clearTimeout(settleTimeout);
12111
+ element.removeAttribute("data-swiping");
12112
+ element.removeAttribute("data-swipe-past-threshold");
12113
+ element.removeAttribute("data-swipe-settling");
12114
+ element.style.removeProperty("--swipe-pulled");
12115
+ element.style.removeProperty("--swipe-progress");
12116
+ };
12117
+ // Where the gesture leaves the element, and later back to rest. Cleaned up on a
12118
+ // timeout rather than on transitionend: a swipe let go of at the very edge has
12119
+ // nothing left to move (the pull is clamped to the element's size), and a
12120
+ // movement that does not happen reports no end.
12121
+ const settleTo = ({
12122
+ pulled,
12123
+ progress,
12124
+ type
12125
+ }, {
12126
+ thenForget = true
12127
+ } = {}) => {
12128
+ clearTimeout(settleTimeout);
12129
+ element.setAttribute("data-swipe-settling", "");
12130
+ paint({
12131
+ pulled,
12132
+ progress,
12133
+ type
12134
+ });
12135
+ if (thenForget) {
12136
+ settleTimeout = setTimeout(forget, SETTLE_DURATION_MS);
12137
+ }
12138
+ };
12139
+ return startDragToTravel(pointerDownEvent, {
12140
+ element,
12141
+ axes,
12142
+ commitRatio: threshold,
12143
+ onStart: ({
12144
+ axis
12145
+ }) => {
12146
+ const {
12147
+ positive,
12148
+ negative
12149
+ } = SWIPE_TYPE_BY_AXIS[axis];
12150
+ const hasPositive = types.includes(positive);
12151
+ const hasNegative = types.includes(negative);
12152
+ if (!hasPositive && !hasNegative) {
12153
+ return false;
12154
+ }
12155
+ const {
12156
+ width,
12157
+ height
12158
+ } = element.getBoundingClientRect();
12159
+ const size = axis === "x" ? width : height;
12160
+ if (!size) {
12161
+ return false;
12162
+ }
12163
+ onSwipeStart();
12164
+ clearTimeout(settleTimeout);
12165
+ element.removeAttribute("data-swipe-settling");
12166
+ // A side nothing is declared for is not refused, it resists: the gesture is
12167
+ // answered (something moves) while saying there is nothing that way.
12168
+ return {
12169
+ size,
12170
+ travelBack: hasPositive,
12171
+ travelOn: hasNegative
12172
+ };
12173
+ },
12174
+ onPull: ({
12175
+ axis,
12176
+ pulled,
12177
+ progress
12178
+ }) => {
12179
+ paint({
12180
+ pulled,
12181
+ progress,
12182
+ type: swipeTypeOf(axis, pulled)
12183
+ });
12184
+ },
12185
+ onEnd: ({
12186
+ axis,
12187
+ pulled,
12188
+ size,
12189
+ sign,
12190
+ travels,
12191
+ event
12192
+ }) => {
12193
+ const type = swipeTypeOf(axis, pulled);
12194
+ const restingPlace = {
12195
+ pulled: 0,
12196
+ progress: 0,
12197
+ type
12198
+ };
12199
+ if (!travels) {
12200
+ settleTo(restingPlace);
12201
+ return;
12202
+ }
12203
+ // Out, and out is where it stays for as long as the answer takes: a row that
12204
+ // comes back and leaves again says the gesture was not understood.
12205
+ settleTo({
12206
+ pulled: sign * size,
12207
+ progress: sign,
12208
+ type
12209
+ }, {
12210
+ thenForget: false
12211
+ });
12212
+ const pending = trigger(type, event, {
12213
+ axis,
12214
+ sign,
12215
+ pulled,
12216
+ size,
12217
+ progress: pulled / size
12218
+ });
12219
+ if (!pending) {
12220
+ settleTo(restingPlace);
12221
+ return;
12222
+ }
12223
+ // However it ended, the element comes back: a failure leaves the row in
12224
+ // place and it can be tried again, and a success is the caller's to answer
12225
+ // (a list that redemands its rows, a row that leaves). Nothing here makes it
12226
+ // disappear — navi does not know what "put away" means.
12227
+ const comeBack = () => {
12228
+ settleTo(restingPlace);
12229
+ };
12230
+ pending.then(comeBack, comeBack);
12231
+ },
12232
+ // Nothing to put back: onGiveUp only ever comes from a press that never became
12233
+ // a swipe, so nothing was painted by it.
12234
+ onGiveUp: () => {}
12235
+ });
12236
+ };
12237
+ const swipeTypeOf = (axis, pulled) => {
12238
+ const {
12239
+ positive,
12240
+ negative
12241
+ } = SWIPE_TYPE_BY_AXIS[axis];
12242
+ return pulled > 0 ? positive : negative;
12243
+ };
12244
+
12245
+ /**
12246
+ * `move`, `reorder`, `toss` — one grab, and what letting go of it means.
12247
+ *
12248
+ * All three are the same gesture: the element is picked up and carried. What
12249
+ * differs is the answer at the release, so one detector reads them all — it is one
12250
+ * press, and something has to arbitrate it.
12251
+ *
12252
+ * interactions={{ reorder: moveBefore, toss: remove }}
12253
+ * interactions={{ move: remember }}
12254
+ *
12255
+ * `reorder` and `toss` combine: a task dragged onto another changes places, the
12256
+ * same task thrown far and fast is gotten rid of. `move` does not combine with
12257
+ * `reorder` — an element either goes where it is put or takes a place in a list,
12258
+ * and the two answers cannot both be true of one release.
12259
+ *
12260
+ * `move` carries the element ITSELF and leaves it where it was put; the other two
12261
+ * carry a copy and put the original back. That is the same difference said in
12262
+ * layout terms: something moved has a new place of its own, something reordered
12263
+ * had its place taken by the list.
12264
+ *
12265
+ * Nothing of the gesture is decided here. `startDragTo` owns all of it — the
12266
+ * copy carried above the page while the original keeps its place in the layout, the
12267
+ * drop hint, the drop targets found by intersection, the no-op drops filtered out,
12268
+ * the flight of a thrown copy and its return when the answer refuses. This says
12269
+ * which elements are the items, how they are named, and what a given release means.
12270
+ *
12271
+ * WHICH ELEMENTS. Every element declaring `reorder` marks itself, so the set of
12272
+ * items IS the set of elements that declared it — no selector to pass, nothing to
12273
+ * keep in sync with the markup, and an item that must not move simply does not
12274
+ * declare it. An element that only declares `toss` marks nothing: it is not a place
12275
+ * anything lands.
12276
+ *
12277
+ * HOW THEY ARE NAMED: by `id`. A reorder is answered in terms of what moved and
12278
+ * what it landed before, and a DOM index cannot say it — a list draws fewer rows
12279
+ * than it has, and a search reorders them.
12280
+ *
12281
+ * WHAT COMES BACK, in the interaction's own detail:
12282
+ *
12283
+ * interactions={{
12284
+ * reorder: (event) => {
12285
+ * const { fromId, toId, syncCloneWithDropTarget } = event.detail;
12286
+ * return document.startViewTransition(() => {
12287
+ * syncCloneWithDropTarget();
12288
+ * setOrder(moveBefore(order, fromId, toId));
12289
+ * }).finished;
12290
+ * },
12291
+ * toss: (event) => remove(event.detail.id),
12292
+ * }}
12293
+ *
12294
+ * `toId` is null for a drop at the end. `syncCloneWithDropTarget` must be called
12295
+ * synchronously inside the transition callback, next to the state change, so the
12296
+ * copy is captured where it lands rather than where it was let go of.
12297
+ *
12298
+ * And the promise matters in both cases: the gesture holds its copy until the
12299
+ * answer settles. Returning the transition is what makes a landing continuous; a
12300
+ * `toss` that rejects brings the copy back, because the thing still exists and the
12301
+ * screen has to say so.
12302
+ *
12303
+ * Starting a document transition is the application's call and not navi's: a
12304
+ * `view-transition-name` must be unique per document, so only the application can
12305
+ * name what moves.
12306
+ *
12307
+ * What the copy LOOKS like is the application's too. A copy of a transparent
12308
+ * element is invisible — a row usually gets its background from the list around it,
12309
+ * which the copy has left — so it is dressed through the attributes the gesture
12310
+ * writes: `navi-drag-clone` on the copy, `navi-drag-clone-source` on the original.
12311
+ */
12312
+
12313
+
12314
+ const MOVE = "move";
12315
+ const REORDER = "reorder";
12316
+ const TOSS = "toss";
12317
+
12318
+ // What makes an element a place something can land, written by the detector itself.
12319
+ const REORDERABLE_ATTRIBUTE = "data-reorderable";
12320
+ // Which axes the drag walks: "x", "y" or "xy". Its default is not the same for
12321
+ // every outcome — a list runs one way, and something being put somewhere goes
12322
+ // wherever it is put.
12323
+ const AXIS_ATTRIBUTE = "data-drag-axis";
12324
+ const DELAY_ATTRIBUTE = "data-drag-delay";
12325
+ const SLOP_ATTRIBUTE = "data-drag-slop";
12326
+ const THRESHOLD_ATTRIBUTE = "data-drag-threshold";
12327
+ const TOSS_DISTANCE_ATTRIBUTE = "data-toss-distance";
12328
+ const TOSS_SPEED_ATTRIBUTE = "data-toss-speed";
12329
+
12330
+ defineInteractionDetector({
12331
+ name: "drag",
12332
+ claims: (type) => type === MOVE || type === REORDER || type === TOSS,
12333
+ setup: (element, trigger, { types, readConfig }) => {
12334
+ const canMove = types.includes(MOVE);
12335
+ const canReorder = types.includes(REORDER);
12336
+ const canToss = types.includes(TOSS);
12337
+ // Read at setup rather than at the press: a container can say it, and it is
12338
+ // what the gesture is about rather than something it discovers.
12339
+ const axisHolder = element.closest(`[${AXIS_ATTRIBUTE}]`);
12340
+ const axes =
12341
+ axisHolder?.getAttribute(AXIS_ATTRIBUTE) ||
12342
+ // A list runs one way, and reordering walks it. Anything else goes wherever
12343
+ // the hand takes it: a thing put somewhere has two axes to be put along, and
12344
+ // a throw goes where it was thrown.
12345
+ (canReorder && !canToss ? "y" : "xy");
12346
+
12347
+ if (canReorder) {
12348
+ element.setAttribute(REORDERABLE_ATTRIBUTE, "");
12349
+ }
12350
+ // What @jsenv/dom puts on a drag source: no iOS callout, and the touch left to
12351
+ // the scroll until the press becomes a grab. Its value is the axis the
12352
+ // SURROUNDINGS scroll on, which for a list is the axis the list runs on.
12353
+ element.setAttribute("data-drag-source", axes === "x" ? "x" : "");
12354
+
12355
+ const onPointerDown = (pointerDownEvent) => {
12356
+ // What this element says a release can mean. The gesture then runs only what
12357
+ // those need — no copy for a move, no drop hint for something that can only
12358
+ // be thrown away.
12359
+ startDragTo(pointerDownEvent, types, {
12360
+ draggedElement: element,
12361
+ // Nothing to land on when nothing reorders.
12362
+ itemSelector: canReorder ? `[${REORDERABLE_ATTRIBUTE}]` : undefined,
12363
+ getItemId: (itemElement) => itemElement.id,
12364
+ direction: { x: axes.includes("x"), y: axes.includes("y") },
12365
+ // Where it may go, said in the DOM. A thing that is put somewhere stays
12366
+ // inside what one can SEE of its container ("scrollport", not "scroll":
12367
+ // the scrollable area can be far larger than the box, and "inside the box"
12368
+ // is what a hand expects). `data-drag-free` lifts that. Left alone for a
12369
+ // throw, which frees the area on its own — it has to be able to leave.
12370
+ areaConstraint: element.closest(`[data-drag-free]`)
12371
+ ? "none"
12372
+ : canMove
12373
+ ? "scrollport"
12374
+ : undefined,
12375
+ threshold: readConfig(THRESHOLD_ATTRIBUTE, undefined),
12376
+ longPressDelay: readConfig(DELAY_ATTRIBUTE, undefined),
12377
+ longPressSlop: readConfig(SLOP_ATTRIBUTE, undefined),
12378
+ tossDistance: readConfig(TOSS_DISTANCE_ATTRIBUTE, undefined),
12379
+ tossSpeed: readConfig(TOSS_SPEED_ATTRIBUTE, undefined),
12380
+ // Handed straight back in all three cases: what `trigger` returns is a
12381
+ // promise while the answer is still going, which is what the gesture waits
12382
+ // on before it lets go of what it carries.
12383
+ onReorder: (fromId, toId, syncCloneWithDropTarget) =>
12384
+ trigger(REORDER, pointerDownEvent, {
12385
+ fromId,
12386
+ toId,
12387
+ syncCloneWithDropTarget,
12388
+ }),
12389
+ onToss: ({ gestureInfo }) =>
12390
+ trigger(TOSS, pointerDownEvent, {
12391
+ id: element.id,
12392
+ velocity: gestureInfo.velocity,
12393
+ x: gestureInfo.layout.xDelta,
12394
+ y: gestureInfo.layout.yDelta,
12395
+ }),
12396
+ onMove: ({ x, y }) => trigger(MOVE, pointerDownEvent, { x, y }),
12397
+ });
12398
+ };
12399
+ element.addEventListener("pointerdown", onPointerDown);
12400
+
12401
+ return () => {
12402
+ element.removeAttribute(REORDERABLE_ATTRIBUTE);
12403
+ element.removeAttribute("data-drag-source");
12404
+ element.removeEventListener("pointerdown", onPointerDown);
12405
+ };
12406
+ },
12407
+ });
12408
+
11700
12409
  const isSignal = (value) => {
11701
12410
  return getSignalType(value) !== null;
11702
12411
  };
@@ -15399,6 +16108,43 @@ const isSameKey = (browserEventKey, key) => {
15399
16108
  return false;
15400
16109
  };
15401
16110
 
16111
+ /**
16112
+ * `"keyboard:ctrl+backspace"` — an interaction asked for with keys.
16113
+ *
16114
+ * The name carries the shortcut, so a control says what a shortcut DOES in the
16115
+ * same place it says what a swipe does, rather than in a separate shortcut list
16116
+ * whose entries then have to find their way back to the action.
16117
+ *
16118
+ * The shortcut itself is not parsed here: `createOnKeyDownForShortcuts` already
16119
+ * knows what "ctrl+backspace" means, which keys a control may take without
16120
+ * swallowing what the browser owed them, and which it may not.
16121
+ *
16122
+ * The interaction is dispatched under its full name, colon included, so
16123
+ * `findEvent(event, "keyboard:ctrl+backspace")` gets back to it — the shortcut
16124
+ * that asked is what the action wants to know, not that a key went down.
16125
+ */
16126
+
16127
+
16128
+ const KEYBOARD_PREFIX = "keyboard:";
16129
+
16130
+ defineInteractionDetector({
16131
+ name: "keyboard",
16132
+ claims: (type) => type.startsWith(KEYBOARD_PREFIX),
16133
+ setup: (element, trigger, { types }) => {
16134
+ const shortcuts = {};
16135
+ for (const type of types) {
16136
+ shortcuts[type.slice(KEYBOARD_PREFIX.length)] = (keyboardEvent) => {
16137
+ trigger(type, keyboardEvent);
16138
+ };
16139
+ }
16140
+ const onKeyDown = createOnKeyDownForShortcuts(shortcuts);
16141
+ element.addEventListener("keydown", onKeyDown);
16142
+ return () => {
16143
+ element.removeEventListener("keydown", onKeyDown);
16144
+ };
16145
+ },
16146
+ });
16147
+
15402
16148
  /**
15403
16149
  * Merges a component's base className with className received from props.
15404
16150
  *
@@ -24989,7 +25735,28 @@ installImportMetaCssBuild(import.meta);const css$W = /* css */`
24989
25735
  color: var(--x-button-color);
24990
25736
  background: none;
24991
25737
  border: none;
24992
- border-radius: var(--button-border-radius);
25738
+ /* Squared from the outside, corner by corner: a Group asks the member it
25739
+ joins for square corners along the seam, and the button it means may
25740
+ arrive wrapped (in a tooltip, in a link), so the ask travels down as
25741
+ inherited custom properties rather than as a selector aimed at the
25742
+ button. Each corner falls back to the button's own radius when nothing
25743
+ asks for anything. */
25744
+ border-top-left-radius: var(
25745
+ --x-corner-top-left-radius,
25746
+ var(--button-border-radius)
25747
+ );
25748
+ border-top-right-radius: var(
25749
+ --x-corner-top-right-radius,
25750
+ var(--button-border-radius)
25751
+ );
25752
+ border-bottom-right-radius: var(
25753
+ --x-corner-bottom-right-radius,
25754
+ var(--button-border-radius)
25755
+ );
25756
+ border-bottom-left-radius: var(
25757
+ --x-corner-bottom-left-radius,
25758
+ var(--button-border-radius)
25759
+ );
24993
25760
  outline: none;
24994
25761
  cursor: var(--x-button-cursor);
24995
25762
  touch-action: manipulation;
@@ -24997,6 +25764,14 @@ installImportMetaCssBuild(import.meta);const css$W = /* css */`
24997
25764
  -webkit-tap-highlight-color: var(--navi-control-tap-highlight-color);
24998
25765
 
24999
25766
  .navi_button_content {
25767
+ /* The ask stops here: this element is the button's frame, so what is
25768
+ inside it (a popover the button opens, a button of its own) is no
25769
+ longer at the seam. */
25770
+ --x-corner-top-left-radius: initial;
25771
+ --x-corner-top-right-radius: initial;
25772
+ --x-corner-bottom-right-radius: initial;
25773
+ --x-corner-bottom-left-radius: initial;
25774
+
25000
25775
  position: relative;
25001
25776
  display: inherit;
25002
25777
  box-sizing: border-box;
@@ -27009,6 +27784,15 @@ const css$V = /* css */`
27009
27784
  }
27010
27785
 
27011
27786
  .navi_dialog {
27787
+ /* A popup renders inside its opener's own subtree, so a corner claimed from
27788
+ the outside (see group.jsx) would otherwise reach the controls in here.
27789
+ It stops at the popup: what a popup holds is never at a seam of the group
27790
+ its opener belongs to. */
27791
+ --x-corner-top-left-radius: initial;
27792
+ --x-corner-top-right-radius: initial;
27793
+ --x-corner-bottom-right-radius: initial;
27794
+ --x-corner-bottom-left-radius: initial;
27795
+
27012
27796
  /* Computed once, reused by both max-width itself and min-width's own
27013
27797
  clamp below (see its comment for why) — avoids repeating the same
27014
27798
  min(..., ...) expression twice. */
@@ -27033,7 +27817,7 @@ const css$V = /* css */`
27033
27817
  position: absolute;
27034
27818
  inset: unset;
27035
27819
  /* Custom renderer only — see openLocalDialogCount above */
27036
- z-index: calc(var(--navi-popup-z-index) + var(--dialog-stack-order, 0));
27820
+ z-index: calc(var(--navi-z-index-popup) + var(--dialog-stack-order, 0));
27037
27821
  min-width: min(
27038
27822
  max(var(--anchor-width, 0px), var(--dialog-min-width, 0px)),
27039
27823
  var(--x-dialog-max-width)
@@ -28278,6 +29062,15 @@ const css$U = /* css */`
28278
29062
  }
28279
29063
 
28280
29064
  .navi_popover {
29065
+ /* A popup renders inside its opener's own subtree, so a corner claimed from
29066
+ the outside (see group.jsx) would otherwise reach the controls in here.
29067
+ It stops at the popup: what a popup holds is never at a seam of the group
29068
+ its opener belongs to. */
29069
+ --x-corner-top-left-radius: initial;
29070
+ --x-corner-top-right-radius: initial;
29071
+ --x-corner-bottom-right-radius: initial;
29072
+ --x-corner-bottom-left-radius: initial;
29073
+
28281
29074
  --x-popover-max-width: min(
28282
29075
  var(--popover-max-width, var(--popover-maxmax-width)),
28283
29076
  var(--container-position-remaining-width, var(--popover-maxmax-width)),
@@ -28302,7 +29095,7 @@ const css$U = /* css */`
28302
29095
  most-recently-opened local popover always outranks an earlier one,
28303
29096
  regardless of DOM position — a plain positioned div gets no free
28304
29097
  "last shown wins" the way the top-layer renderer does. */
28305
- z-index: calc(var(--navi-popup-z-index) + var(--popover-stack-order, 0));
29098
+ z-index: calc(var(--navi-z-index-popup) + var(--popover-stack-order, 0));
28306
29099
  min-width: min(var(--popover-min-width, 0px), var(--x-popover-max-width));
28307
29100
  max-width: var(--x-popover-max-width);
28308
29101
  min-height: min(
@@ -28435,7 +29228,7 @@ const css$U = /* css */`
28435
29228
  instance still resolve via DOM order (content rendered after its own
28436
29229
  backdrop, see this file's top comment), so this only needs to beat
28437
29230
  *other* popovers' own backdrop/content, not its own. */
28438
- z-index: calc(var(--navi-popup-z-index) + var(--popover-stack-order, 0));
29231
+ z-index: calc(var(--navi-z-index-popup) + var(--popover-stack-order, 0));
28439
29232
  margin: 0;
28440
29233
  padding: 0;
28441
29234
  background: transparent;
@@ -41486,7 +42279,7 @@ const css$L = /* css */`
41486
42279
 
41487
42280
  .navi_fixed_bar {
41488
42281
  position: fixed;
41489
- z-index: 1;
42282
+ z-index: var(--navi-z-index-bar);
41490
42283
  display: flex;
41491
42284
  box-sizing: border-box;
41492
42285
  margin: auto;
@@ -44933,6 +45726,14 @@ const inputCss = /* css */`
44933
45726
  }
44934
45727
 
44935
45728
  .navi_input_slot {
45729
+ /* A corner claimed from the outside (see group.jsx) is the frame's, and
45730
+ what lives in here is not the frame — a button in a slot, the content
45731
+ of a popup — so the ask stops at this boundary. */
45732
+ --x-corner-top-left-radius: initial;
45733
+ --x-corner-top-right-radius: initial;
45734
+ --x-corner-bottom-right-radius: initial;
45735
+ --x-corner-bottom-left-radius: initial;
45736
+
44936
45737
  margin-right: var(--slot-spacing, calc(2px + 0.1em));
44937
45738
  margin-left: var(--slot-spacing, calc(2px + 0.1em));
44938
45739
  color: #5e4e4e;
@@ -46354,92 +47155,117 @@ HTMLFormElement.prototype.requestSubmit = function (submitter) {
46354
47155
  // form.dispatchEvent(customEvent);
46355
47156
  // };
46356
47157
 
46357
- installImportMetaCssBuild(import.meta);const css$B = /* css */`
47158
+ installImportMetaCssBuild(import.meta);/**
47159
+ * A row or column of controls sharing one frame: the borders where two of them
47160
+ * meet are drawn once instead of twice, and only the outer corners stay
47161
+ * rounded. See docs/control_group.md.
47162
+ */
47163
+ const css$B = /* css */`
46358
47164
  .navi_group {
46359
- --group-border-width: 1px;
46360
-
47165
+ --group-border-width: var(--navi-control-border-width);
47166
+
47167
+ /* Two ways of asking for a square corner, and no selector ever reaching
47168
+ inside a member:
47169
+ - the property, on the member itself: a navi control declares the radius
47170
+ of its frame on its own root, and the inner element that draws that
47171
+ frame inherits it;
47172
+ - the custom property, which travels: a member can be an enrobage (a
47173
+ tooltip, a link) around the control that carries the frame, so the ask
47174
+ also goes down as --x-corner-*-radius, which a control reads as an
47175
+ override of its own radius. Private (the --x- prefix): navi's own
47176
+ controls answer it, an app never writes it. Whoever answers it also
47177
+ stops it (see .navi_button_content in button_ui.jsx), so a button
47178
+ deeper in — the clear cross in a picker's slot, the Save of a form in
47179
+ a popup the member opens — never mistakes itself for the seam. */
47180
+
47181
+ /* Members overlap by the width of one border, so along each seam one of the
47182
+ two borders covers the other. Whichever member the user is on has to be
47183
+ the one on top: it is the one whose border changes color, and the one
47184
+ whose focus ring goes all the way around — half a ring, cut by the
47185
+ neighbour painted after it, is what this avoids. z-index needs a
47186
+ positioned element to mean anything, hence position: relative.
47187
+ Deliberately not paired with isolation: isolate — a stacking context
47188
+ here would also trap the popup of a picker held in the group, which
47189
+ counts on its own band reaching the whole page. What keeps these two
47190
+ values from escaping is instead that everything they could reach is a
47191
+ band above them (see navi_z_indexes.js). */
46361
47192
  > *:hover,
46362
47193
  > *[data-hover] {
46363
47194
  position: relative;
46364
- z-index: 1;
46365
- }
47195
+ z-index: var(--navi-z-index-control-hovered);
47196
+ }
47197
+ /* Three spellings for one thing — the member showing a focus ring. Some
47198
+ controls take the focus on their own root (a button); others wrap a real
47199
+ input and draw the ring on their frame while the keyboard is held
47200
+ somewhere inside (a picker, a spin). The ring is what must not be sliced,
47201
+ so the member holding it is raised whether it wears the state itself or
47202
+ merely contains it. */
46366
47203
  > *:focus-visible,
46367
- > *[data-focus-visible] {
47204
+ > *[data-focus-visible],
47205
+ > *:has([data-focus-visible]) {
46368
47206
  position: relative;
46369
- z-index: 1;
47207
+ z-index: var(--navi-z-index-control-focused);
46370
47208
  }
46371
47209
 
46372
47210
  /* Horizontal (default): Cumulative margin for border overlap */
46373
47211
  &:not([data-vertical]) {
46374
47212
  > *:not(:first-child) {
46375
- margin-left: calc(var(--border-width) * -1);
47213
+ margin-left: calc(var(--border-width, var(--group-border-width)) * -1);
46376
47214
  }
46377
47215
  > *:first-child:not(:only-child) {
47216
+ --x-corner-top-right-radius: 0;
47217
+ --x-corner-bottom-right-radius: 0;
47218
+
46378
47219
  border-top-right-radius: 0 !important;
46379
47220
  border-bottom-right-radius: 0 !important;
46380
-
46381
- .navi_button_content,
46382
- .navi_native_input {
46383
- border-top-right-radius: 0 !important;
46384
- border-bottom-right-radius: 0 !important;
46385
- }
46386
47221
  }
46387
47222
 
46388
47223
  > *:last-child:not(:only-child) {
47224
+ --x-corner-top-left-radius: 0;
47225
+ --x-corner-bottom-left-radius: 0;
47226
+
46389
47227
  border-top-left-radius: 0 !important;
46390
47228
  border-bottom-left-radius: 0 !important;
46391
-
46392
- .navi_button_content,
46393
- .navi_native_input {
46394
- border-top-left-radius: 0 !important;
46395
- border-bottom-left-radius: 0 !important;
46396
- }
46397
47229
  }
46398
47230
 
46399
47231
  > *:not(:first-child):not(:last-child) {
46400
- border-radius: 0 !important;
47232
+ --x-corner-top-left-radius: 0;
47233
+ --x-corner-top-right-radius: 0;
47234
+ --x-corner-bottom-right-radius: 0;
47235
+ --x-corner-bottom-left-radius: 0;
46401
47236
 
46402
- .navi_button_content,
46403
- .navi_native_input {
46404
- border-radius: 0 !important;
46405
- }
47237
+ border-radius: 0 !important;
46406
47238
  }
46407
47239
  }
46408
47240
 
46409
47241
  /* Vertical: Cumulative margin for border overlap */
46410
47242
  &[data-vertical] {
46411
47243
  > *:not(:first-child) {
46412
- margin-top: calc(var(--group-border-width) * -1);
47244
+ margin-top: calc(var(--border-width, var(--group-border-width)) * -1);
46413
47245
  }
46414
47246
  > *:first-child:not(:only-child) {
47247
+ --x-corner-bottom-right-radius: 0;
47248
+ --x-corner-bottom-left-radius: 0;
47249
+
46415
47250
  border-bottom-right-radius: 0 !important;
46416
47251
  border-bottom-left-radius: 0 !important;
46417
-
46418
- .navi_button_content,
46419
- .navi_native_input {
46420
- border-bottom-right-radius: 0 !important;
46421
- border-bottom-left-radius: 0 !important;
46422
- }
46423
47252
  }
46424
47253
 
46425
47254
  > *:last-child:not(:only-child) {
47255
+ --x-corner-top-left-radius: 0;
47256
+ --x-corner-top-right-radius: 0;
47257
+
46426
47258
  border-top-left-radius: 0 !important;
46427
47259
  border-top-right-radius: 0 !important;
46428
-
46429
- .navi_button_content,
46430
- .navi_native_input {
46431
- border-top-left-radius: 0 !important;
46432
- border-top-right-radius: 0 !important;
46433
- }
46434
47260
  }
46435
47261
 
46436
47262
  > *:not(:first-child):not(:last-child) {
46437
- border-radius: 0 !important;
47263
+ --x-corner-top-left-radius: 0;
47264
+ --x-corner-top-right-radius: 0;
47265
+ --x-corner-bottom-right-radius: 0;
47266
+ --x-corner-bottom-left-radius: 0;
46438
47267
 
46439
- .navi_button_content,
46440
- .navi_native_input {
46441
- border-radius: 0 !important;
46442
- }
47268
+ border-radius: 0 !important;
46443
47269
  }
46444
47270
  }
46445
47271
  }
@@ -46453,8 +47279,15 @@ const Group = ({
46453
47279
  import.meta.css = [css$B, "@jsenv/navi/src/control/group.jsx"];
46454
47280
  return jsx(Box, {
46455
47281
  baseClassName: "navi_group",
46456
- "data-vertical": vertical ? "" : undefined,
46457
- row: row,
47282
+ "data-vertical": vertical ? "" : undefined
47283
+ /* A group draws one frame around its members, so they must share their
47284
+ top and bottom edges. A block box lays them out inline, where they
47285
+ align on baselines instead: a member whose content carries no text —
47286
+ the swatch of a color picker — has no baseline of its own, the one
47287
+ synthesized from its bottom edge does not land where its neighbours'
47288
+ text does, and the member rides a few pixels off, slicing the frame.
47289
+ Flex aligns the edges, in both directions. */,
47290
+ flex: vertical ? "y" : "x",
46458
47291
  ...props,
46459
47292
  children: children
46460
47293
  });
@@ -52387,7 +53220,7 @@ const css$v = /* css */`
52387
53220
  position: sticky;
52388
53221
  top: 0;
52389
53222
  left: 0;
52390
- z-index: 1;
53223
+ z-index: var(--navi-z-index-sticky);
52391
53224
  order: -2;
52392
53225
  }
52393
53226
  .navi_list_fallback,
@@ -52494,7 +53327,7 @@ const css$v = /* css */`
52494
53327
  position: sticky;
52495
53328
  right: 0;
52496
53329
  bottom: 0;
52497
- z-index: 1;
53330
+ z-index: var(--navi-z-index-sticky);
52498
53331
  order: 2;
52499
53332
  }
52500
53333
 
@@ -52510,7 +53343,7 @@ const css$v = /* css */`
52510
53343
  .navi_list_item_group_label {
52511
53344
  position: sticky;
52512
53345
  top: var(--list-group-label-top, var(--x-list-group-label-top, 0px));
52513
- z-index: 1;
53346
+ z-index: var(--navi-z-index-sticky);
52514
53347
  display: block;
52515
53348
  background-color: var(--list-group-label-background-color);
52516
53349
  user-select: none;
@@ -56622,6 +57455,11 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
56622
57455
  font-size: var(--picker-font-size);
56623
57456
  font-family: var(--picker-font-family);
56624
57457
  text-align: inherit;
57458
+ /* The frame is drawn by the box, but its radius is declared here, on the
57459
+ control root, like every other navi control does — so anything styling
57460
+ the picker from the outside (a Group squaring the corners it joins) has
57461
+ one element to talk to, and the box follows. */
57462
+ border-radius: var(--picker-border-radius);
56625
57463
 
56626
57464
  .navi_picker_box {
56627
57465
  position: relative;
@@ -56643,7 +57481,7 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
56643
57481
  border-width: var(--picker-border-width);
56644
57482
  border-style: solid;
56645
57483
  border-color: var(--x-picker-border-color);
56646
- border-radius: var(--picker-border-radius);
57484
+ border-radius: inherit;
56647
57485
  outline-width: var(--picker-outline-width);
56648
57486
  outline-style: none;
56649
57487
  outline-color: var(--picker-outline-color);
@@ -56674,6 +57512,14 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
56674
57512
  }
56675
57513
  }
56676
57514
  .navi_picker_right_slot {
57515
+ /* A corner claimed from the outside (see group.jsx) is the frame's, and
57516
+ what lives in here is not the frame — a button in a slot, the content
57517
+ of a popup — so the ask stops at this boundary. */
57518
+ --x-corner-top-left-radius: initial;
57519
+ --x-corner-top-right-radius: initial;
57520
+ --x-corner-bottom-right-radius: initial;
57521
+ --x-corner-bottom-left-radius: initial;
57522
+
56677
57523
  display: inline-flex;
56678
57524
  height: 1em;
56679
57525
  height: 1lh;
@@ -62536,6 +63382,13 @@ const createIsolatedItemTracker = () => {
62536
63382
  return [useIsolatedItemTrackerProvider, useTrackIsolatedItem, useTrackedIsolatedItem, useTrackedIsolatedItems];
62537
63383
  };
62538
63384
 
63385
+ /**
63386
+ * The table's own scale: these order the table's parts among themselves and are
63387
+ * derived from each other, never written as literals twice. They stay under the
63388
+ * bands of src/navi_z_indexes.js, which is where anything competing with
63389
+ * another component belongs.
63390
+ */
63391
+
62539
63392
  const Z_INDEX_EDITING = 1; /* To go above neighbours, but should not be too big to stay under the sticky cells */
62540
63393
 
62541
63394
  /* needed because cell uses position:relative, sticky must win even if before in DOM order */
@@ -67954,7 +68807,7 @@ const css = /* css */`
67954
68807
  .navi_side_panel_head,
67955
68808
  .navi_side_panel_foot {
67956
68809
  position: sticky;
67957
- z-index: 1;
68810
+ z-index: var(--navi-z-index-sticky);
67958
68811
  background-color: var(--navi-popup-background-color);
67959
68812
  }
67960
68813
  /* The separator token, not the popup border: these lines split two