@jsenv/navi 0.29.230 → 0.29.231

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.
@@ -80,7 +80,7 @@ installImportMetaCssBuild(import.meta);
80
80
  * any of these, and a number is the last resort, not the first tool.
81
81
  */
82
82
 
83
- const css$16 = /* css */ `@layer navi {
83
+ const css$17 = /* css */ `@layer navi {
84
84
  :root {
85
85
  --navi-z-index-control-hovered: 1;
86
86
  --navi-z-index-control-focused: 2;
@@ -92,7 +92,7 @@ const css$16 = /* css */ `@layer navi {
92
92
  }
93
93
  }
94
94
  `;
95
- import.meta.css = [css$16, "@jsenv/navi/src/navi_z_indexes.js"];
95
+ import.meta.css = [css$17, "@jsenv/navi/src/navi_z_indexes.js"];
96
96
 
97
97
  const addIntoArray = (array, ...valuesToAdd) => {
98
98
  if (valuesToAdd.length === 1) {
@@ -328,7 +328,7 @@ installImportMetaCssBuild(import.meta);
328
328
 
329
329
  const URL_TARGET_ATTRIBUTE = "data-url-target";
330
330
 
331
- const css$15 = /* css */ `@layer navi {
331
+ const css$16 = /* css */ `@layer navi {
332
332
  [data-url-target] {
333
333
  animation: navi_url_target var(--navi-url-target-duration, 2s)
334
334
  ease-out;
@@ -346,7 +346,7 @@ const css$15 = /* css */ `@layer navi {
346
346
  }
347
347
  }
348
348
  `;
349
- import.meta.css = [css$15, "@jsenv/navi/src/nav/url_target/url_target.js"];
349
+ import.meta.css = [css$16, "@jsenv/navi/src/nav/url_target/url_target.js"];
350
350
 
351
351
  let urlTargetOptions = {
352
352
  block: "start",
@@ -1683,7 +1683,7 @@ const useActionStatus = (action) => {
1683
1683
  };
1684
1684
 
1685
1685
  installImportMetaCssBuild(import.meta);
1686
- const css$14 = /* css */`.action_error {
1686
+ const css$15 = /* css */`.action_error {
1687
1687
  background: #fdd;
1688
1688
  border: 1px solid red;
1689
1689
  margin-top: 0;
@@ -1707,7 +1707,7 @@ const ActionRenderer = ({
1707
1707
  children,
1708
1708
  disabled
1709
1709
  }) => {
1710
- import.meta.css = [css$14, "@jsenv/navi/src/action/action_renderer.jsx"];
1710
+ import.meta.css = [css$15, "@jsenv/navi/src/action/action_renderer.jsx"];
1711
1711
  if (action === undefined) {
1712
1712
  throw new Error("ActionRenderer requires an action to render, but none was provided.");
1713
1713
  }
@@ -4854,6 +4854,25 @@ const warnSignalCollision = (props, controlType, stateProp) => {
4854
4854
  }
4855
4855
  };
4856
4856
 
4857
+ /**
4858
+ * A signal handed to a prop that holds a plain value is the `signal` binding
4859
+ * written in the wrong place. Nothing throws: the signal object becomes the
4860
+ * value, and a value that cannot be written to the DOM is replaced by a
4861
+ * `window.__navi_js('…')` reference string — which is then what the field
4862
+ * shows on screen (see toDomValue in controller_registry.js).
4863
+ */
4864
+ const warnSignalAsState = (props, controlType, stateProps) => {
4865
+ for (const stateProp of stateProps) {
4866
+ if (isSignal(props[stateProp])) {
4867
+ console.warn(
4868
+ `[navi] "${controlType}" got a signal as "${stateProp}". ` +
4869
+ `A signal binds a control through "signal" — did you mean signal={...}? ` +
4870
+ `As "${stateProp}" it is taken for the value itself.`,
4871
+ );
4872
+ }
4873
+ }
4874
+ };
4875
+
4857
4876
  // In-memory registry of all mounted ui state controllers keyed by their id.
4858
4877
  // Allows direct controller access without dispatching DOM events — used by external
4859
4878
  // callers (e.g. selectable_list) to call setUIState by id instead of via the DOM.
@@ -5864,7 +5883,7 @@ installImportMetaCssBuild(import.meta);
5864
5883
  * (see calloutTemplate in callout.js).
5865
5884
  */
5866
5885
 
5867
- const css$13 = /* css */`.navi_callout_status_icon {
5886
+ const css$14 = /* css */`.navi_callout_status_icon {
5868
5887
  --x-callout-status-icon-color: var(--navi-callout-neutral-color);
5869
5888
  box-sizing: border-box;
5870
5889
  aspect-ratio: 1;
@@ -5935,7 +5954,7 @@ const CalloutStatusIcon = ({
5935
5954
  status = "info",
5936
5955
  shape = "square"
5937
5956
  }) => {
5938
- import.meta.css = [css$13, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
5957
+ import.meta.css = [css$14, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
5939
5958
  return jsx("span", {
5940
5959
  className: "navi_callout_status_icon",
5941
5960
  "data-status": status === "none" ? undefined : status,
@@ -5957,7 +5976,7 @@ installImportMetaCssBuild(import.meta);
5957
5976
  // Unique for the page's lifetime: a caller may write the id in a commandfor.
5958
5977
  let calloutCount = 0;
5959
5978
 
5960
- const css$12 = /* css */ `
5979
+ const css$13 = /* css */ `
5961
5980
  @layer navi {
5962
5981
  .navi_callout {
5963
5982
  /* A callout is parented to what it explains, so it inherits from it — and
@@ -6305,7 +6324,7 @@ const openCallout = (
6305
6324
  debug = () => {},
6306
6325
  } = {},
6307
6326
  ) => {
6308
- import.meta.css = [css$12, "@jsenv/navi/src/control/rules/callout/callout.js"];
6327
+ import.meta.css = [css$13, "@jsenv/navi/src/control/rules/callout/callout.js"];
6309
6328
  if (debug === true) {
6310
6329
  debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
6311
6330
  }
@@ -18515,8 +18534,12 @@ const VISUAL_PROPS = {
18515
18534
  outlineWidth: PASS_THROUGH,
18516
18535
  boxDecorationBreak: PASS_THROUGH,
18517
18536
  boxShadow: PASS_THROUGH,
18518
- background: PASS_THROUGH,
18519
- backgroundColor: PASS_THROUGH,
18537
+ background: (value) => {
18538
+ return { background: resolveSurfaceKeyword(value) };
18539
+ },
18540
+ backgroundColor: (value) => {
18541
+ return { backgroundColor: resolveSurfaceKeyword(value) };
18542
+ },
18520
18543
  backgroundImage: PASS_THROUGH,
18521
18544
  backgroundSize: PASS_THROUGH,
18522
18545
  border: PASS_THROUGH,
@@ -18881,6 +18904,18 @@ const resolveColorKeyword = (value) => {
18881
18904
  return COLOR_KEYWORD_MAP[value] || value;
18882
18905
  };
18883
18906
 
18907
+ // The two planes an app paints: the paper its content sits on, and the frame
18908
+ // around it (see navi_css_vars.js). Named rather than derived at each call
18909
+ // site, so a top bar and a side menu are the same plane without agreeing on a
18910
+ // formula.
18911
+ const SURFACE_KEYWORD_MAP = {
18912
+ surface: "var(--navi-surface-color)",
18913
+ chrome: "var(--navi-chrome-color)",
18914
+ };
18915
+ const resolveSurfaceKeyword = (value) => {
18916
+ return SURFACE_KEYWORD_MAP[value] || value;
18917
+ };
18918
+
18884
18919
  const DEFAULT_DISPLAY_BY_TAG_NAME = {
18885
18920
  "inline": new Set([
18886
18921
  "a",
@@ -20810,6 +20845,16 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
20810
20845
  * children?: import("ignore:preact").ComponentChildren,
20811
20846
  * [key: string]: any,
20812
20847
  * }>}
20848
+ * @param {string} [background] Any CSS background, plus two keywords for the
20849
+ * planes an app paints: `"surface"` is the paper content sits on,
20850
+ * `"chrome"` the frame around a screen — a top bar, a side menu, a toolbar.
20851
+ * Both follow the theme, so a bar stays a bar in dark mode
20852
+ * (`--navi-surface-color` / `--navi-chrome-color`). `backgroundColor` takes
20853
+ * the same two.
20854
+ * @param {string} [color] Any CSS color, plus the five ink keywords —
20855
+ * `"primary"`, `"secondary"`, `"emphasis"`, `"discrete"`, `"hint"` — which
20856
+ * say how loud a text is rather than which color it is (see
20857
+ * docs/typography.md).
20813
20858
  * @param {object} [interactions] What this box answers, by interaction name
20814
20859
  * (see docs/interactions.md). A plain box has no wiring of its own: it does
20815
20860
  * nothing with `action` (that is a control's prop), so a click on it is
@@ -22721,6 +22766,20 @@ const debug$2 = (...args) => {
22721
22766
  // Base URL management
22722
22767
  let baseFileUrl;
22723
22768
  let baseUrl;
22769
+ /**
22770
+ * Where this document's addresses start, for a document that is not at the
22771
+ * root: a panel served from "/admin/admin.html" declares
22772
+ * `setBaseUrl("/admin/admin.html")` and then writes its routes as "/places",
22773
+ * "/places/:placeId". The url "/admin/places/42" matches, and `buildUrl` puts
22774
+ * the prefix back.
22775
+ *
22776
+ * Call it in the routes module, above the `route()` calls it governs — they
22777
+ * read the base url as they are created, so an entry point calling it after
22778
+ * importing them is too late. The server must serve the document for every
22779
+ * address below it (see docs/navigation.md).
22780
+ *
22781
+ * @param {string} value - the document's own url, absolute or relative.
22782
+ */
22724
22783
  const setBaseUrl = (value) => {
22725
22784
  baseFileUrl = new URL(
22726
22785
  value,
@@ -28563,7 +28622,7 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
28563
28622
  // the root pictures must NOT move (they carry the whole viewport, blank bands
28564
28623
  // included).
28565
28624
 
28566
- const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-transition-area] {
28625
+ const css$12 = /* css */`:root[data-navi-route-transition] [data-navi-route-transition-area] {
28567
28626
  view-transition-name: navi-route-transition;
28568
28627
  }
28569
28628
 
@@ -28884,7 +28943,7 @@ const RouteTransitionArea = ({
28884
28943
  children,
28885
28944
  ...rest
28886
28945
  }) => {
28887
- import.meta.css = [css$11, "@jsenv/navi/src/nav/route_transition.jsx"];
28946
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
28888
28947
  installTransitionWindowCss();
28889
28948
  const props = {
28890
28949
  ...rest,
@@ -28954,7 +29013,7 @@ const RouteTransitionArea = ({
28954
29013
  * @returns {() => void} remove this relation.
28955
29014
  */
28956
29015
  const defineRouteTransition = (from, to, transition) => {
28957
- import.meta.css = [css$11, "@jsenv/navi/src/nav/route_transition.jsx"];
29016
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
28958
29017
  installTransitionWindowCss();
28959
29018
  if (!to) {
28960
29019
  throw new TypeError(`defineRouteTransition needs a destination: "to" is ${to}. The page reached from anywhere is written defineRouteTransition(null, THAT_PAGE, ...) — there is no relation the other way round, a page LEFT for anywhere being the back half of that one.`);
@@ -28995,7 +29054,7 @@ const defineRouteTransition = (from, to, transition) => {
28995
29054
  * @returns {() => void} remove this default.
28996
29055
  */
28997
29056
  const defineRouteDefaultTransition = transition => {
28998
- import.meta.css = [css$11, "@jsenv/navi/src/nav/route_transition.jsx"];
29057
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
28999
29058
  installTransitionWindowCss();
29000
29059
  const value = normalizeTransition(transition);
29001
29060
  defaultTransition = value;
@@ -29817,7 +29876,7 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
29817
29876
  const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
29818
29877
  // The name the box wears while it travels, and only then (see nameForTravel).
29819
29878
  const TRAVEL_NAME = "navi-route-travel";
29820
- const css$10 = /* css */`.navi_route_travel {
29879
+ const css$11 = /* css */`.navi_route_travel {
29821
29880
  position: relative;
29822
29881
 
29823
29882
  &[data-travel-by-drag="x"] {
@@ -30106,7 +30165,7 @@ const RouteTravel = ({
30106
30165
  children,
30107
30166
  ...rest
30108
30167
  }) => {
30109
- import.meta.css = [css$10, "@jsenv/navi/src/nav/route_travel.jsx"];
30168
+ import.meta.css = [css$11, "@jsenv/navi/src/nav/route_travel.jsx"];
30110
30169
  installTransitionWindowCss();
30111
30170
  const elementRef = useRef();
30112
30171
  const gestureRef = useRef(null);
@@ -37154,9 +37213,11 @@ const useControlProps = (props, {
37154
37213
  * props, which is how `signal` came to be forgotten.
37155
37214
  */
37156
37215
  const isControlValueGivenByProps = props => Object.hasOwn(props, "value") || Object.hasOwn(props, "defaultValue") || Object.hasOwn(props, "signal");
37216
+ const CONTROL_STATE_PROPS = ["value", "defaultValue", "checked", "defaultChecked", "open", "defaultOpen"];
37157
37217
  const createControlInfo = (props, {
37158
37218
  controlType
37159
37219
  }) => {
37220
+ warnSignalAsState(props, controlType, CONTROL_STATE_PROPS);
37160
37221
  let statePropName;
37161
37222
  let defaultStatePropName;
37162
37223
  let stateInitial;
@@ -39792,7 +39853,7 @@ const setupNetworkMonitoring = () => {
39792
39853
  setupNetworkMonitoring();
39793
39854
 
39794
39855
  installImportMetaCssBuild(import.meta);
39795
- const css$$ = /* css */`.navi_loading_indicator_fluid_container {
39856
+ const css$10 = /* css */`.navi_loading_indicator_fluid_container {
39796
39857
  border-radius: inherit;
39797
39858
  opacity: 1;
39798
39859
  width: 100%;
@@ -39823,7 +39884,7 @@ const LoadingIndicatorFluid = ({
39823
39884
  visuallyHidden,
39824
39885
  ...rest
39825
39886
  }) => {
39826
- import.meta.css = [css$$, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
39887
+ import.meta.css = [css$10, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
39827
39888
  const ref = useRef(null);
39828
39889
  // The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
39829
39890
  const [containerWidth, setContainerWidth] = useState(0);
@@ -40029,7 +40090,7 @@ const LoadingRectangleSvg = ({
40029
40090
  };
40030
40091
 
40031
40092
  installImportMetaCssBuild(import.meta);
40032
- const css$_ = /* css */`.navi_loading_outline_wrapper {
40093
+ const css$$ = /* css */`.navi_loading_outline_wrapper {
40033
40094
  top: max(var(--loading-outline-min-inset, -100vh),
40034
40095
  var(--loading-rectangle-top, 0px));
40035
40096
  right: max(var(--loading-outline-min-inset, -100vh),
@@ -40050,7 +40111,7 @@ const css$_ = /* css */`.navi_loading_outline_wrapper {
40050
40111
  }
40051
40112
  `;
40052
40113
  const LoadingOutline = props => {
40053
- import.meta.css = [css$_, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
40114
+ import.meta.css = [css$$, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
40054
40115
  if (props.containerRef) {
40055
40116
  const container = props.containerRef.current;
40056
40117
  if (!container) {
@@ -40185,7 +40246,7 @@ const LoadingOutlineWithPortal = props => {
40185
40246
  };
40186
40247
 
40187
40248
  installImportMetaCssBuild(import.meta);
40188
- const css$Z = /* css */`.navi_text_anchor {
40249
+ const css$_ = /* css */`.navi_text_anchor {
40189
40250
  vertical-align: baseline;
40190
40251
  user-select: none;
40191
40252
  overflow: hidden;
@@ -40223,7 +40284,7 @@ const TextAnchor = ({
40223
40284
  textSize,
40224
40285
  lineLayout
40225
40286
  }) => {
40226
- import.meta.css = [css$Z, "@jsenv/navi/src/text/text_anchor.jsx"];
40287
+ import.meta.css = [css$_, "@jsenv/navi/src/text/text_anchor.jsx"];
40227
40288
  const anchorRef = useRef();
40228
40289
 
40229
40290
  // Plain useLayoutEffect would also fire while an ancestor dialog/popover
@@ -40451,7 +40512,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
40451
40512
  };
40452
40513
 
40453
40514
  installImportMetaCssBuild(import.meta);
40454
- const css$Y = /* css */`@layer navi {
40515
+ const css$Z = /* css */`@layer navi {
40455
40516
  .navi_text {
40456
40517
  &[data-skeleton] {
40457
40518
  border-radius: .2em;
@@ -41020,7 +41081,7 @@ const TextShrinkWrap = props => {
41020
41081
  });
41021
41082
  };
41022
41083
  const TextUI = props => {
41023
- import.meta.css = [css$Y, "@jsenv/navi/src/text/text.jsx"];
41084
+ import.meta.css = [css$Z, "@jsenv/navi/src/text/text.jsx"];
41024
41085
  let {
41025
41086
  ref,
41026
41087
  spacing,
@@ -41244,7 +41305,7 @@ const Icon = ({
41244
41305
  fillLine,
41245
41306
  ...props
41246
41307
  }) => {
41247
- import.meta.css = [css$Y, "@jsenv/navi/src/text/text.jsx"];
41308
+ import.meta.css = [css$Z, "@jsenv/navi/src/text/text.jsx"];
41248
41309
  const innerChildren = href ? jsx("svg", {
41249
41310
  width: "100%",
41250
41311
  height: "100%",
@@ -41434,7 +41495,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
41434
41495
  };
41435
41496
 
41436
41497
  installImportMetaCssBuild(import.meta);
41437
- const css$X = /* css */`@layer navi {
41498
+ const css$Y = /* css */`@layer navi {
41438
41499
  .navi_link {
41439
41500
  --link-border-radius: unset;
41440
41501
  --link-outline-color: var(--navi-focus-outline-color);
@@ -41892,7 +41953,7 @@ Object.assign(PSEUDO_CLASSES, {
41892
41953
  * @param {boolean} [props.readOnly]
41893
41954
  */
41894
41955
  const Link = props => {
41895
- import.meta.css = [css$X, "@jsenv/navi/src/nav/link/link.jsx"];
41956
+ import.meta.css = [css$Y, "@jsenv/navi/src/nav/link/link.jsx"];
41896
41957
  if (props.route) {
41897
41958
  return jsx(LinkWithRoute, {
41898
41959
  ...props
@@ -42460,7 +42521,7 @@ const rememberProgressAnimation = (navElement, travel) => {
42460
42521
  travel.progressAnimation = animation;
42461
42522
  travel.progressFrom = animation ? parseFloat(animation.effect.getKeyframes()[0]["--slide-travel-progress"]) || 0 : 0;
42462
42523
  };
42463
- const css$W = /* css */`@layer navi {
42524
+ const css$X = /* css */`@layer navi {
42464
42525
  .navi_nav {
42465
42526
  --nav-border: none;
42466
42527
  --nav-padding: 0px;
@@ -42724,7 +42785,7 @@ const Nav = ({
42724
42785
  slideContainer,
42725
42786
  ...props
42726
42787
  }) => {
42727
- import.meta.css = [css$W, "@jsenv/navi/src/nav/link/nav.jsx"];
42788
+ import.meta.css = [css$X, "@jsenv/navi/src/nav/link/nav.jsx"];
42728
42789
  const defaultRef = useRef();
42729
42790
  props.ref = props.ref || defaultRef;
42730
42791
  const navRef = props.ref;
@@ -43600,7 +43661,7 @@ const buildBinderCenterlinePathBelow = ({
43600
43661
  };
43601
43662
 
43602
43663
  installImportMetaCssBuild(import.meta);
43603
- const css$V = /* css */`@layer navi {
43664
+ const css$W = /* css */`@layer navi {
43604
43665
  .navi_binder {
43605
43666
  --binder-border-width: var(--navi-control-border-width);
43606
43667
  --binder-border-radius: var(--navi-control-border-radius);
@@ -43885,7 +43946,7 @@ const Binder = ({
43885
43946
  pagePadding,
43886
43947
  ...props
43887
43948
  }) => {
43888
- import.meta.css = [css$V, "@jsenv/navi/src/nav/binder/binder.jsx"];
43949
+ import.meta.css = [css$W, "@jsenv/navi/src/nav/binder/binder.jsx"];
43889
43950
  const items = toChildArray(children).map((child, index) => {
43890
43951
  const {
43891
43952
  value: itemValue,
@@ -44332,7 +44393,7 @@ const writeSpace = (area) => {
44332
44393
  };
44333
44394
 
44334
44395
  installImportMetaCssBuild(import.meta);
44335
- const css$U = /* css */`@layer navi {
44396
+ const css$V = /* css */`@layer navi {
44336
44397
  :root {
44337
44398
  --navi-fixed-bar-width: 56px;
44338
44399
  --navi-fixed-bar-height: 56px;
@@ -44452,7 +44513,7 @@ const FixedBar = ({
44452
44513
  border = true,
44453
44514
  ...props
44454
44515
  }) => {
44455
- import.meta.css = [css$U, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
44516
+ import.meta.css = [css$V, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
44456
44517
  const defaultRef = useRef();
44457
44518
  props.ref = props.ref || defaultRef;
44458
44519
  // Said with the width the border rule reads rather than with an attribute of
@@ -44546,7 +44607,7 @@ const FixedBar = ({
44546
44607
  // Subpixel layout rounds rectangles up on boxes that fit exactly.
44547
44608
  const OVERFLOW_TOLERANCE = 1;
44548
44609
 
44549
- const css$T = /* css */ `
44610
+ const css$U = /* css */ `
44550
44611
  [data-navi-overflow-x] {
44551
44612
  outline: 2px dashed #e74c3c;
44552
44613
  outline-offset: -2px;
@@ -44570,7 +44631,7 @@ const detectHorizontalOverflow = ({
44570
44631
  let styleEl = null;
44571
44632
  if (highlight) {
44572
44633
  styleEl = document.createElement("style");
44573
- styleEl.textContent = css$T;
44634
+ styleEl.textContent = css$U;
44574
44635
  document.head.appendChild(styleEl);
44575
44636
  }
44576
44637
 
@@ -44726,7 +44787,7 @@ const useFocusGroup = (
44726
44787
 
44727
44788
  installImportMetaCssBuild(import.meta);
44728
44789
  const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
44729
- const css$S = /* css */`.navi_summary_marker {
44790
+ const css$T = /* css */`.navi_summary_marker {
44730
44791
  flex-shrink: 0;
44731
44792
  width: 1em;
44732
44793
  height: 1em;
@@ -44818,7 +44879,7 @@ const SummaryMarker = ({
44818
44879
  loading,
44819
44880
  openDirection = "down"
44820
44881
  }) => {
44821
- import.meta.css = [css$S, "@jsenv/navi/src/control/details/summary_marker.jsx"];
44882
+ import.meta.css = [css$T, "@jsenv/navi/src/control/details/summary_marker.jsx"];
44822
44883
  const showLoading = useDebounceTrue(loading, 300);
44823
44884
  return jsx("span", {
44824
44885
  className: "navi_summary_marker",
@@ -44864,7 +44925,7 @@ const SummaryMarker = ({
44864
44925
  };
44865
44926
 
44866
44927
  installImportMetaCssBuild(import.meta);
44867
- const css$R = /* css */`.navi_details {
44928
+ const css$S = /* css */`.navi_details {
44868
44929
  z-index: 1;
44869
44930
  flex-direction: column;
44870
44931
  flex-shrink: 0;
@@ -44909,7 +44970,7 @@ const Details = props => {
44909
44970
  return details;
44910
44971
  };
44911
44972
  const DetailsField = props => {
44912
- import.meta.css = [css$R, "@jsenv/navi/src/control/details/details.jsx"];
44973
+ import.meta.css = [css$S, "@jsenv/navi/src/control/details/details.jsx"];
44913
44974
  const {
44914
44975
  ref,
44915
44976
  persists,
@@ -45825,6 +45886,7 @@ const useOpenPropsEffectOnOpenController = (
45825
45886
  name = "popup",
45826
45887
  ) => {
45827
45888
  const { signal, value, defaultOpen, navState } = props;
45889
+ warnSignalAsState(props, name, ["open", "defaultOpen"]);
45828
45890
  const { id: navStateId, type: navStateType } = resolveNavStateProp(
45829
45891
  navState,
45830
45892
  props.id,
@@ -46709,7 +46771,7 @@ const usePopupContentMount = (
46709
46771
  };
46710
46772
 
46711
46773
  installImportMetaCssBuild(import.meta);
46712
- const css$Q = /* css */`.navi_expandable {
46774
+ const css$R = /* css */`.navi_expandable {
46713
46775
  flex-direction: column;
46714
46776
  flex-shrink: 0;
46715
46777
  display: flex;
@@ -46939,7 +47001,7 @@ const useExpandableContext = partName => {
46939
47001
  * builds the content ahead of the click.
46940
47002
  */
46941
47003
  const Expandable = props => {
46942
- import.meta.css = [css$Q, "@jsenv/navi/src/control/expandable/expandable.jsx"];
47004
+ import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
46943
47005
  /* The open props are read from `props` by the open controller below; they
46944
47006
  are named here only to keep them out of `rest`, and so out of the DOM. */
46945
47007
  /* eslint-disable no-unused-vars */
@@ -47626,49 +47688,6 @@ const shallowDiffers = (a, b) => {
47626
47688
  return false;
47627
47689
  };
47628
47690
 
47629
- const ButtonRouteResolver = props => {
47630
- const Next = useNextResolver();
47631
- if (props.route) {
47632
- return jsx(ButtonWithRoute, {
47633
- ...props
47634
- });
47635
- }
47636
- return jsx(Next, {
47637
- ...props
47638
- });
47639
- };
47640
- const ButtonWithRoute = props => {
47641
- const Next = useNextResolver();
47642
- const {
47643
- route,
47644
- routeParams,
47645
- children,
47646
- pseudoState,
47647
- ...rest
47648
- } = props;
47649
- {
47650
- assertRoute(route);
47651
- }
47652
- const url = route.buildUrl(routeParams);
47653
- const {
47654
- matching
47655
- } = useRouteStatus(route);
47656
- const paramsAreMatching = route.matchesParams(routeParams);
47657
- const linkMatching = matching && paramsAreMatching;
47658
-
47659
- // Merged into whatever the caller already holds: a button can be forced into
47660
- // a state for a demo and still learn its own current-ness from its route.
47661
- return jsx(Next, {
47662
- href: url,
47663
- pseudoState: {
47664
- ...pseudoState,
47665
- ":-navi-href-current": linkMatching
47666
- },
47667
- ...rest,
47668
- children: children || route.buildRelativeUrl(routeParams)
47669
- });
47670
- };
47671
-
47672
47691
  const LIGHT_ACCENT_ATTRIBUTE = "data-accent-light";
47673
47692
  const VERY_LIGHT_ACCENT_ATTRIBUTE = "data-accent-very-light";
47674
47693
  const DARK_CONTRAST_ATTRIBUTE = "data-accent-needs-dark-fg";
@@ -47769,7 +47788,7 @@ const useAccentColorAttributes = (
47769
47788
  };
47770
47789
 
47771
47790
  installImportMetaCssBuild(import.meta);
47772
- const css$P = /* css */`@layer navi {
47791
+ const css$Q = /* css */`@layer navi {
47773
47792
  .navi_button {
47774
47793
  --button-border-radius: var(--navi-control-border-radius);
47775
47794
  --button-border-width: var(--navi-control-border-width);
@@ -48070,7 +48089,7 @@ a.navi_button {
48070
48089
  }
48071
48090
  `;
48072
48091
  const ButtonUI = props => {
48073
- import.meta.css = [css$P, "@jsenv/navi/src/control/input/button_ui.jsx"];
48092
+ import.meta.css = [css$Q, "@jsenv/navi/src/control/input/button_ui.jsx"];
48074
48093
  const {
48075
48094
  ref,
48076
48095
  // href/link
@@ -48285,6 +48304,189 @@ const ButtonShadow = () => {
48285
48304
  };
48286
48305
  markAsOutsideTextFlow(ButtonShadow);
48287
48306
 
48307
+ installImportMetaCssBuild(import.meta);
48308
+ const css$P = /* css */`.navi_button_confirm {
48309
+ align-items: center;
48310
+ gap: var(--navi-s);
48311
+ flex-wrap: wrap;
48312
+ display: inline-flex;
48313
+ }
48314
+ `;
48315
+ const ButtonConfirmResolver = props => {
48316
+ const Next = useNextResolver();
48317
+ if (!props.confirm) {
48318
+ return jsx(Next, {
48319
+ ...props
48320
+ });
48321
+ }
48322
+ return jsx(ButtonConfirm, {
48323
+ ...props,
48324
+ Next: Next
48325
+ });
48326
+ };
48327
+ const ButtonConfirm = ({
48328
+ Next,
48329
+ confirm,
48330
+ confirmLabel = naviI18n("button.confirm"),
48331
+ cancelLabel = naviI18n("button.cancel"),
48332
+ confirmTestId,
48333
+ cancelTestId,
48334
+ ...props
48335
+ }) => {
48336
+ import.meta.css = [css$P, "@jsenv/navi/src/control/input/button_confirm.jsx"];
48337
+ const [asking, setAsking] = useState(false);
48338
+ const askingRef = useRef(null);
48339
+ const cancelRef = useRef(null);
48340
+ // Whether going back to the resting label owes the focus a place to land: a
48341
+ // press on "Annuler" or an Escape does, a focus that already left does not —
48342
+ // it is elsewhere by then, and pulling it back would take it from whatever
48343
+ // the person moved to.
48344
+ const restoreFocusRef = useRef(false);
48345
+ const questionId = `${useId()}_question`;
48346
+ const cancel = restoreFocus => {
48347
+ restoreFocusRef.current = restoreFocus;
48348
+ setAsking(false);
48349
+ };
48350
+ useLayoutEffect(() => {
48351
+ if (asking) {
48352
+ // The press was aimed at this button, and the button that answers now is
48353
+ // where it should land: navi's own ladder inside the question, which
48354
+ // finds "Confirmer" (first in the row) unless the caller marked
48355
+ // something else.
48356
+ const found = findFocusTarget(askingRef.current, {
48357
+ restoreMayClaim: true
48358
+ });
48359
+ if (found) {
48360
+ moveFocusTo(found.target);
48361
+ }
48362
+ return;
48363
+ }
48364
+ if (restoreFocusRef.current) {
48365
+ restoreFocusRef.current = false;
48366
+ // The answer may have taken the button with it — a row deleting itself
48367
+ // is the whole point of asking — and there is then nothing to give the
48368
+ // focus back to.
48369
+ const buttonEl = props.ref.current;
48370
+ if (buttonEl) {
48371
+ moveFocusTo(buttonEl);
48372
+ }
48373
+ }
48374
+ }, [asking]);
48375
+ if (!asking) {
48376
+ return jsx(Next, {
48377
+ ...props,
48378
+ // The first press is the question, not the act — so nothing that acts
48379
+ // is handed to the resolvers below. `type` with them: a submit button
48380
+ // would be given `--navi-send` back.
48381
+ type: "button",
48382
+ action: undefined,
48383
+ command: undefined,
48384
+ href: undefined,
48385
+ route: undefined,
48386
+ onClick: () => {
48387
+ setAsking(true);
48388
+ }
48389
+ });
48390
+ }
48391
+ const {
48392
+ action,
48393
+ onActionEnd,
48394
+ onClick
48395
+ } = props;
48396
+ return jsxs("span", {
48397
+ className: "navi_button_confirm",
48398
+ ref: askingRef,
48399
+ onKeyDown: e => {
48400
+ if (e.key === "Escape") {
48401
+ cancel(true);
48402
+ }
48403
+ },
48404
+ onFocusOut: e => {
48405
+ if (!askingRef.current.contains(e.relatedTarget)) {
48406
+ cancel(false);
48407
+ }
48408
+ },
48409
+ children: [jsx("span", {
48410
+ id: questionId,
48411
+ children: confirm === true ? naviI18n("confirm.message") : confirm
48412
+ }), jsx(Next, {
48413
+ ...props,
48414
+ "aria-describedby": questionId
48415
+ // The button confirming is the button that was pressed: it keeps the
48416
+ // caller's own testid unless the question names one of its own.
48417
+ ,
48418
+ "data-testid": confirmTestId || props["data-testid"],
48419
+ onClick: e => {
48420
+ onClick?.(e);
48421
+ if (!action) {
48422
+ // A command runs on the press itself: there is nothing to wait for,
48423
+ // the question is answered as soon as it is pressed.
48424
+ cancel(true);
48425
+ }
48426
+ }
48427
+ // A run that failed keeps the question up, with its callout on the
48428
+ // button that raised it: the retry is then one press away.
48429
+ ,
48430
+ onActionEnd: (data, e) => {
48431
+ onActionEnd?.(data, e);
48432
+ cancel(true);
48433
+ },
48434
+ children: confirmLabel
48435
+ }), jsx(ButtonUI, {
48436
+ ref: cancelRef,
48437
+ type: "button",
48438
+ "data-testid": cancelTestId,
48439
+ onClick: () => {
48440
+ cancel(true);
48441
+ },
48442
+ children: cancelLabel
48443
+ })]
48444
+ });
48445
+ };
48446
+
48447
+ const ButtonRouteResolver = props => {
48448
+ const Next = useNextResolver();
48449
+ if (props.route) {
48450
+ return jsx(ButtonWithRoute, {
48451
+ ...props
48452
+ });
48453
+ }
48454
+ return jsx(Next, {
48455
+ ...props
48456
+ });
48457
+ };
48458
+ const ButtonWithRoute = props => {
48459
+ const Next = useNextResolver();
48460
+ const {
48461
+ route,
48462
+ routeParams,
48463
+ children,
48464
+ pseudoState,
48465
+ ...rest
48466
+ } = props;
48467
+ {
48468
+ assertRoute(route);
48469
+ }
48470
+ const url = route.buildUrl(routeParams);
48471
+ const {
48472
+ matching
48473
+ } = useRouteStatus(route);
48474
+ const paramsAreMatching = route.matchesParams(routeParams);
48475
+ const linkMatching = matching && paramsAreMatching;
48476
+
48477
+ // Merged into whatever the caller already holds: a button can be forced into
48478
+ // a state for a demo and still learn its own current-ness from its route.
48479
+ return jsx(Next, {
48480
+ href: url,
48481
+ pseudoState: {
48482
+ ...pseudoState,
48483
+ ":-navi-href-current": linkMatching
48484
+ },
48485
+ ...rest,
48486
+ children: children || route.buildRelativeUrl(routeParams)
48487
+ });
48488
+ };
48489
+
48288
48490
  const ButtonFirstResolver = props => {
48289
48491
  const Next = useNextResolver();
48290
48492
  const defaultRef = useRef(null);
@@ -48446,6 +48648,20 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
48446
48648
  * more: the work may already have happened on the other side (see
48447
48649
  * docs/actions.md#aborting-saves-resources-it-does-not-undo), so say it only
48448
48650
  * where the screen can be re-opened on what is actually there.
48651
+ * @param {boolean|import("ignore:preact").ComponentChildren} [confirm] Ask before
48652
+ * doing it, in the place the button stands: the first press replaces the
48653
+ * button with this question and a "Confirmer"/"Annuler" pair, the second one
48654
+ * does what the button was for. `true` asks navi's default question. The
48655
+ * press stays under the finger and there is nothing to dismiss — "Annuler",
48656
+ * Escape, or the focus leaving puts the button back. Use `<Picker
48657
+ * type="confirm">` instead when the question is long enough to want a popup,
48658
+ * or when the row has no space for it.
48659
+ * @param {import("ignore:preact").ComponentChildren} [confirmLabel] Label of the
48660
+ * second press.
48661
+ * @param {import("ignore:preact").ComponentChildren} [cancelLabel] Label of the way
48662
+ * out of the question.
48663
+ * @param {string} [confirmTestId] `data-testid` of the second press.
48664
+ * @param {string} [cancelTestId] `data-testid` of the way out.
48449
48665
  * @param {string} [contentDisplay] The display of the frame the button draws
48450
48666
  * around its children. It follows the button's own by default — its display
48451
48667
  * and, a display alone saying nothing about direction, the rest of its flow
@@ -48460,7 +48676,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
48460
48676
  * (it wears it itself) and loses the shrink under the finger, which has
48461
48677
  * nothing left to scale but the interactive area itself.
48462
48678
  */
48463
- const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
48679
+ const Button = createComponentResolver([ButtonFirstResolver, ButtonConfirmResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
48464
48680
 
48465
48681
  installImportMetaCssBuild(import.meta);
48466
48682
  const css$O = /* css */`.navi_control_swap {
@@ -56505,13 +56721,7 @@ const TimeDate = ({
56505
56721
  })
56506
56722
  });
56507
56723
  }
56508
- const date = toDate(children, value => {
56509
- if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
56510
- const d = new Date(`${value}T00:00:00`);
56511
- return isNaN(d.getTime()) ? null : d;
56512
- }
56513
- return null;
56514
- });
56724
+ const date = toDate(children);
56515
56725
  if (!date) {
56516
56726
  return jsx(TimeText, {
56517
56727
  ...props,
@@ -73491,6 +73701,26 @@ const css$r = /* css */`.navi_checkbox_group {
73491
73701
  `;
73492
73702
 
73493
73703
  /**
73704
+ * A set of checkboxes answering one question, holding the array of the values
73705
+ * that are checked.
73706
+ *
73707
+ * It renders a `<fieldset>` and hands its `name` down, so the boxes inside are
73708
+ * plain `<Input type="checkbox" value="…">` with nothing else to declare; each
73709
+ * value that is checked appears in the array, in the order the boxes are
73710
+ * written. What it holds is said the way every control says it — `value` /
73711
+ * `defaultValue` (an array), or a bound `signal` — and inside a `<Form>` the
73712
+ * group is one entry of the submitted params, an array.
73713
+ *
73714
+ * ```jsx
73715
+ * <CheckboxGroup name="permissions" defaultValue={["places.moderate"]} action={save}>
73716
+ * <legend>Permissions</legend>
73717
+ * <Field as="label"><Input type="checkbox" value="places.moderate" /> Modérer les lieux</Field>
73718
+ * <Field as="label"><Input type="checkbox" value="games.list" /> Lister les parties</Field>
73719
+ * </CheckboxGroup>
73720
+ * ```
73721
+ *
73722
+ * See `<RadioGroup>` for the same shape holding a single value.
73723
+ *
73494
73724
  * @type {import("ignore:preact").FunctionComponent<{
73495
73725
  * maxLength?: number,
73496
73726
  * maxLengthGuard?: number,
@@ -74212,6 +74442,27 @@ const css$p = /* css */`.navi_radio_group {
74212
74442
  }
74213
74443
  }
74214
74444
  `;
74445
+
74446
+ /**
74447
+ * A set of radios answering one question, holding the value of the one that is
74448
+ * checked.
74449
+ *
74450
+ * It renders a `<fieldset>` and hands its `name` down, so the radios inside are
74451
+ * plain `<Input type="radio" value="…">` with nothing else to declare — sharing
74452
+ * a name is what makes them exclusive. What it holds is said the way every
74453
+ * control says it — `value` / `defaultValue`, or a bound `signal` — and inside a
74454
+ * `<Form>` the group is one entry of the submitted params.
74455
+ *
74456
+ * ```jsx
74457
+ * <RadioGroup name="role" defaultValue="viewer" action={save}>
74458
+ * <legend>Rôle</legend>
74459
+ * <Field as="label"><Input type="radio" value="viewer" /> Lecteur</Field>
74460
+ * <Field as="label"><Input type="radio" value="admin" /> Administrateur</Field>
74461
+ * </RadioGroup>
74462
+ * ```
74463
+ *
74464
+ * See `<CheckboxGroup>` for the same shape holding an array.
74465
+ */
74215
74466
  const RadioGroup = props => {
74216
74467
  const refDefault = useRef(null);
74217
74468
  props.ref = props.ref || refDefault;