@jsenv/navi 0.29.230 → 0.29.232

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
  }
@@ -2849,6 +2849,9 @@ ${lines.join("\n")}`,
2849
2849
  const willPrerunSet = new Set();
2850
2850
  const willRunSet = new Set();
2851
2851
  const willPromoteSet = new Set(); // prerun -> run requested
2852
+ // Asked for, already under way: the request adds no work, and answers with
2853
+ // the run that is already carrying it (see join_active_runs below).
2854
+ const willJoinSet = new Set();
2852
2855
  const staysRunningSet = new Set();
2853
2856
  const staysAbortedSet = new Set();
2854
2857
  const staysFailedSet = new Set();
@@ -2900,8 +2903,11 @@ ${lines.join("\n")}`,
2900
2903
  } else {
2901
2904
  willRunSet.add(action);
2902
2905
  }
2906
+ } else {
2907
+ // The request performs nothing, and still has to be answered: what
2908
+ // the caller asked for is the answer, not the running.
2909
+ willJoinSet.add(action);
2903
2910
  }
2904
- // Otherwise, ignore the request (action stays as-is)
2905
2911
  } else if (isPrerun) {
2906
2912
  willPrerunSet.add(action);
2907
2913
  } else {
@@ -2928,6 +2934,7 @@ ${lines.join("\n")}`,
2928
2934
  // Special case: action was prerun but not yet requested to run
2929
2935
  // Just promote it to "run requested" without rerunning
2930
2936
  willPromoteSet.add(actionToRun);
2937
+ willJoinSet.add(actionToRun);
2931
2938
  continue;
2932
2939
  }
2933
2940
  handleActionRequest(actionToRun, "run");
@@ -3074,6 +3081,27 @@ ${lines.join("\n")}`);
3074
3081
  actionToPromote.isPrerunSignal.value = false;
3075
3082
  }
3076
3083
  }
3084
+
3085
+ // A run asked for an action that is already running, or that already holds
3086
+ // the answer, performs nothing — and answers all the same, with the run
3087
+ // that IS carrying it. `await action.run()` then means the same thing to
3088
+ // every caller: the second one waits for the first one's request and reads
3089
+ // the same data, instead of being handed `undefined` before the network has
3090
+ // said anything. See the run() JSDoc.
3091
+ {
3092
+ for (const actionToJoin of willJoinSet) {
3093
+ const runningPromise = actionPromiseMap.get(actionToJoin);
3094
+ if (runningPromise) {
3095
+ hasAsync = true;
3096
+ resultArray.push({ type: "async", promise: runningPromise });
3097
+ } else {
3098
+ resultArray.push({
3099
+ type: "sync",
3100
+ result: actionToJoin.dataSignal.peek(),
3101
+ });
3102
+ }
3103
+ }
3104
+ }
3077
3105
  if (DEBUG$2) {
3078
3106
  console.groupEnd();
3079
3107
  }
@@ -3228,10 +3256,15 @@ const createAction = (callback, rootOptions = {}) => {
3228
3256
  return dispatchSingleAction(action, "prerun", options);
3229
3257
  };
3230
3258
  /**
3231
- * Requests the action's data. An action that is already RUNNING or
3232
- * COMPLETED already has it, so the request is a no-op there: use `rerun()`
3233
- * to force a fresh run ("refresh", "check now", any explicit user intent to
3234
- * go back to the network).
3259
+ * Requests the action's data, and resolves with it.
3260
+ *
3261
+ * An action that is already RUNNING or COMPLETED already has it, so the
3262
+ * request starts nothing — it JOINS: the caller waits for the run that is
3263
+ * already carrying the question and reads the same answer. Which is what
3264
+ * makes `await action.run()` say one thing to every caller, whether it is
3265
+ * the one that asked first or the one that arrived while the request was
3266
+ * out. Use `rerun()` to force a fresh run ("refresh", "check now", any
3267
+ * explicit user intent to go back to the network).
3235
3268
  */
3236
3269
  const run = (options) => {
3237
3270
  action.debug(`${action}.run(${stringifyForDisplay(options)})`);
@@ -5749,7 +5782,7 @@ installImportMetaCssBuild(import.meta);
5749
5782
  * (see calloutTemplate in callout.js).
5750
5783
  */
5751
5784
 
5752
- const css$13 = /* css */`.navi_callout_status_icon {
5785
+ const css$14 = /* css */`.navi_callout_status_icon {
5753
5786
  --x-callout-status-icon-color: var(--navi-callout-neutral-color);
5754
5787
  box-sizing: border-box;
5755
5788
  aspect-ratio: 1;
@@ -5820,7 +5853,7 @@ const CalloutStatusIcon = ({
5820
5853
  status = "info",
5821
5854
  shape = "square"
5822
5855
  }) => {
5823
- import.meta.css = [css$13, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
5856
+ import.meta.css = [css$14, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
5824
5857
  return jsx("span", {
5825
5858
  className: "navi_callout_status_icon",
5826
5859
  "data-status": status === "none" ? undefined : status,
@@ -5842,7 +5875,7 @@ installImportMetaCssBuild(import.meta);
5842
5875
  // Unique for the page's lifetime: a caller may write the id in a commandfor.
5843
5876
  let calloutCount = 0;
5844
5877
 
5845
- const css$12 = /* css */ `
5878
+ const css$13 = /* css */ `
5846
5879
  @layer navi {
5847
5880
  .navi_callout {
5848
5881
  /* A callout is parented to what it explains, so it inherits from it — and
@@ -6190,7 +6223,7 @@ const openCallout = (
6190
6223
  debug = () => {},
6191
6224
  } = {},
6192
6225
  ) => {
6193
- import.meta.css = [css$12, "@jsenv/navi/src/control/rules/callout/callout.js"];
6226
+ import.meta.css = [css$13, "@jsenv/navi/src/control/rules/callout/callout.js"];
6194
6227
  if (debug === true) {
6195
6228
  debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
6196
6229
  }
@@ -18238,8 +18271,12 @@ const VISUAL_PROPS = {
18238
18271
  outlineWidth: PASS_THROUGH,
18239
18272
  boxDecorationBreak: PASS_THROUGH,
18240
18273
  boxShadow: PASS_THROUGH,
18241
- background: PASS_THROUGH,
18242
- backgroundColor: PASS_THROUGH,
18274
+ background: (value) => {
18275
+ return { background: resolveSurfaceKeyword(value) };
18276
+ },
18277
+ backgroundColor: (value) => {
18278
+ return { backgroundColor: resolveSurfaceKeyword(value) };
18279
+ },
18243
18280
  backgroundImage: PASS_THROUGH,
18244
18281
  backgroundSize: PASS_THROUGH,
18245
18282
  border: PASS_THROUGH,
@@ -18604,6 +18641,18 @@ const resolveColorKeyword = (value) => {
18604
18641
  return COLOR_KEYWORD_MAP[value] || value;
18605
18642
  };
18606
18643
 
18644
+ // The two planes an app paints: the paper its content sits on, and the frame
18645
+ // around it (see navi_css_vars.js). Named rather than derived at each call
18646
+ // site, so a top bar and a side menu are the same plane without agreeing on a
18647
+ // formula.
18648
+ const SURFACE_KEYWORD_MAP = {
18649
+ surface: "var(--navi-surface-color)",
18650
+ chrome: "var(--navi-chrome-color)",
18651
+ };
18652
+ const resolveSurfaceKeyword = (value) => {
18653
+ return SURFACE_KEYWORD_MAP[value] || value;
18654
+ };
18655
+
18607
18656
  const DEFAULT_DISPLAY_BY_TAG_NAME = {
18608
18657
  "inline": new Set([
18609
18658
  "a",
@@ -20505,6 +20554,16 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
20505
20554
  * children?: import("ignore:preact").ComponentChildren,
20506
20555
  * [key: string]: any,
20507
20556
  * }>}
20557
+ * @param {string} [background] Any CSS background, plus two keywords for the
20558
+ * planes an app paints: `"surface"` is the paper content sits on,
20559
+ * `"chrome"` the frame around a screen — a top bar, a side menu, a toolbar.
20560
+ * Both follow the theme, so a bar stays a bar in dark mode
20561
+ * (`--navi-surface-color` / `--navi-chrome-color`). `backgroundColor` takes
20562
+ * the same two.
20563
+ * @param {string} [color] Any CSS color, plus the five ink keywords —
20564
+ * `"primary"`, `"secondary"`, `"emphasis"`, `"discrete"`, `"hint"` — which
20565
+ * say how loud a text is rather than which color it is (see
20566
+ * docs/typography.md).
20508
20567
  * @param {object} [interactions] What this box answers, by interaction name
20509
20568
  * (see docs/interactions.md). A plain box has no wiring of its own: it does
20510
20569
  * nothing with `action` (that is a control's prop), so a click on it is
@@ -22408,6 +22467,20 @@ const debug$2 = (...args) => {
22408
22467
  // Base URL management
22409
22468
  let baseFileUrl;
22410
22469
  let baseUrl;
22470
+ /**
22471
+ * Where this document's addresses start, for a document that is not at the
22472
+ * root: a panel served from "/admin/admin.html" declares
22473
+ * `setBaseUrl("/admin/admin.html")` and then writes its routes as "/places",
22474
+ * "/places/:placeId". The url "/admin/places/42" matches, and `buildUrl` puts
22475
+ * the prefix back.
22476
+ *
22477
+ * Call it in the routes module, above the `route()` calls it governs — they
22478
+ * read the base url as they are created, so an entry point calling it after
22479
+ * importing them is too late. The server must serve the document for every
22480
+ * address below it (see docs/navigation.md).
22481
+ *
22482
+ * @param {string} value - the document's own url, absolute or relative.
22483
+ */
22411
22484
  const setBaseUrl = (value) => {
22412
22485
  baseFileUrl = new URL(
22413
22486
  value,
@@ -28120,7 +28193,7 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
28120
28193
  // the root pictures must NOT move (they carry the whole viewport, blank bands
28121
28194
  // included).
28122
28195
 
28123
- const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-transition-area] {
28196
+ const css$12 = /* css */`:root[data-navi-route-transition] [data-navi-route-transition-area] {
28124
28197
  view-transition-name: navi-route-transition;
28125
28198
  }
28126
28199
 
@@ -28441,7 +28514,7 @@ const RouteTransitionArea = ({
28441
28514
  children,
28442
28515
  ...rest
28443
28516
  }) => {
28444
- import.meta.css = [css$11, "@jsenv/navi/src/nav/route_transition.jsx"];
28517
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
28445
28518
  installTransitionWindowCss();
28446
28519
  const props = {
28447
28520
  ...rest,
@@ -28511,7 +28584,7 @@ const RouteTransitionArea = ({
28511
28584
  * @returns {() => void} remove this relation.
28512
28585
  */
28513
28586
  const defineRouteTransition = (from, to, transition) => {
28514
- import.meta.css = [css$11, "@jsenv/navi/src/nav/route_transition.jsx"];
28587
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
28515
28588
  installTransitionWindowCss();
28516
28589
  if (!to) {
28517
28590
  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.`);
@@ -28552,7 +28625,7 @@ const defineRouteTransition = (from, to, transition) => {
28552
28625
  * @returns {() => void} remove this default.
28553
28626
  */
28554
28627
  const defineRouteDefaultTransition = transition => {
28555
- import.meta.css = [css$11, "@jsenv/navi/src/nav/route_transition.jsx"];
28628
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
28556
28629
  installTransitionWindowCss();
28557
28630
  const value = normalizeTransition(transition);
28558
28631
  defaultTransition = value;
@@ -29374,7 +29447,7 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
29374
29447
  const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
29375
29448
  // The name the box wears while it travels, and only then (see nameForTravel).
29376
29449
  const TRAVEL_NAME = "navi-route-travel";
29377
- const css$10 = /* css */`.navi_route_travel {
29450
+ const css$11 = /* css */`.navi_route_travel {
29378
29451
  position: relative;
29379
29452
 
29380
29453
  &[data-travel-by-drag="x"] {
@@ -29663,7 +29736,7 @@ const RouteTravel = ({
29663
29736
  children,
29664
29737
  ...rest
29665
29738
  }) => {
29666
- import.meta.css = [css$10, "@jsenv/navi/src/nav/route_travel.jsx"];
29739
+ import.meta.css = [css$11, "@jsenv/navi/src/nav/route_travel.jsx"];
29667
29740
  installTransitionWindowCss();
29668
29741
  const elementRef = useRef();
29669
29742
  const gestureRef = useRef(null);
@@ -39227,7 +39300,7 @@ const setupNetworkMonitoring = () => {
39227
39300
  setupNetworkMonitoring();
39228
39301
 
39229
39302
  installImportMetaCssBuild(import.meta);
39230
- const css$$ = /* css */`.navi_loading_indicator_fluid_container {
39303
+ const css$10 = /* css */`.navi_loading_indicator_fluid_container {
39231
39304
  border-radius: inherit;
39232
39305
  opacity: 1;
39233
39306
  width: 100%;
@@ -39258,7 +39331,7 @@ const LoadingIndicatorFluid = ({
39258
39331
  visuallyHidden,
39259
39332
  ...rest
39260
39333
  }) => {
39261
- import.meta.css = [css$$, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
39334
+ import.meta.css = [css$10, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
39262
39335
  const ref = useRef(null);
39263
39336
  // The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
39264
39337
  const [containerWidth, setContainerWidth] = useState(0);
@@ -39464,7 +39537,7 @@ const LoadingRectangleSvg = ({
39464
39537
  };
39465
39538
 
39466
39539
  installImportMetaCssBuild(import.meta);
39467
- const css$_ = /* css */`.navi_loading_outline_wrapper {
39540
+ const css$$ = /* css */`.navi_loading_outline_wrapper {
39468
39541
  top: max(var(--loading-outline-min-inset, -100vh),
39469
39542
  var(--loading-rectangle-top, 0px));
39470
39543
  right: max(var(--loading-outline-min-inset, -100vh),
@@ -39485,7 +39558,7 @@ const css$_ = /* css */`.navi_loading_outline_wrapper {
39485
39558
  }
39486
39559
  `;
39487
39560
  const LoadingOutline = props => {
39488
- import.meta.css = [css$_, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
39561
+ import.meta.css = [css$$, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
39489
39562
  if (props.containerRef) {
39490
39563
  const container = props.containerRef.current;
39491
39564
  if (!container) {
@@ -39620,7 +39693,7 @@ const LoadingOutlineWithPortal = props => {
39620
39693
  };
39621
39694
 
39622
39695
  installImportMetaCssBuild(import.meta);
39623
- const css$Z = /* css */`.navi_text_anchor {
39696
+ const css$_ = /* css */`.navi_text_anchor {
39624
39697
  vertical-align: baseline;
39625
39698
  user-select: none;
39626
39699
  overflow: hidden;
@@ -39658,7 +39731,7 @@ const TextAnchor = ({
39658
39731
  textSize,
39659
39732
  lineLayout
39660
39733
  }) => {
39661
- import.meta.css = [css$Z, "@jsenv/navi/src/text/text_anchor.jsx"];
39734
+ import.meta.css = [css$_, "@jsenv/navi/src/text/text_anchor.jsx"];
39662
39735
  const anchorRef = useRef();
39663
39736
 
39664
39737
  // Plain useLayoutEffect would also fire while an ancestor dialog/popover
@@ -39886,7 +39959,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
39886
39959
  };
39887
39960
 
39888
39961
  installImportMetaCssBuild(import.meta);
39889
- const css$Y = /* css */`@layer navi {
39962
+ const css$Z = /* css */`@layer navi {
39890
39963
  .navi_text {
39891
39964
  &[data-skeleton] {
39892
39965
  border-radius: .2em;
@@ -40455,7 +40528,7 @@ const TextShrinkWrap = props => {
40455
40528
  });
40456
40529
  };
40457
40530
  const TextUI = props => {
40458
- import.meta.css = [css$Y, "@jsenv/navi/src/text/text.jsx"];
40531
+ import.meta.css = [css$Z, "@jsenv/navi/src/text/text.jsx"];
40459
40532
  let {
40460
40533
  ref,
40461
40534
  spacing,
@@ -40679,7 +40752,7 @@ const Icon = ({
40679
40752
  fillLine,
40680
40753
  ...props
40681
40754
  }) => {
40682
- import.meta.css = [css$Y, "@jsenv/navi/src/text/text.jsx"];
40755
+ import.meta.css = [css$Z, "@jsenv/navi/src/text/text.jsx"];
40683
40756
  const innerChildren = href ? jsx("svg", {
40684
40757
  width: "100%",
40685
40758
  height: "100%",
@@ -40869,7 +40942,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
40869
40942
  };
40870
40943
 
40871
40944
  installImportMetaCssBuild(import.meta);
40872
- const css$X = /* css */`@layer navi {
40945
+ const css$Y = /* css */`@layer navi {
40873
40946
  .navi_link {
40874
40947
  --link-border-radius: unset;
40875
40948
  --link-outline-color: var(--navi-focus-outline-color);
@@ -41327,7 +41400,7 @@ Object.assign(PSEUDO_CLASSES, {
41327
41400
  * @param {boolean} [props.readOnly]
41328
41401
  */
41329
41402
  const Link = props => {
41330
- import.meta.css = [css$X, "@jsenv/navi/src/nav/link/link.jsx"];
41403
+ import.meta.css = [css$Y, "@jsenv/navi/src/nav/link/link.jsx"];
41331
41404
  if (props.route) {
41332
41405
  return jsx(LinkWithRoute, {
41333
41406
  ...props
@@ -41886,7 +41959,7 @@ const rememberProgressAnimation = (navElement, travel) => {
41886
41959
  travel.progressAnimation = animation;
41887
41960
  travel.progressFrom = animation ? parseFloat(animation.effect.getKeyframes()[0]["--slide-travel-progress"]) || 0 : 0;
41888
41961
  };
41889
- const css$W = /* css */`@layer navi {
41962
+ const css$X = /* css */`@layer navi {
41890
41963
  .navi_nav {
41891
41964
  --nav-border: none;
41892
41965
  --nav-padding: 0px;
@@ -42150,7 +42223,7 @@ const Nav = ({
42150
42223
  slideContainer,
42151
42224
  ...props
42152
42225
  }) => {
42153
- import.meta.css = [css$W, "@jsenv/navi/src/nav/link/nav.jsx"];
42226
+ import.meta.css = [css$X, "@jsenv/navi/src/nav/link/nav.jsx"];
42154
42227
  const defaultRef = useRef();
42155
42228
  props.ref = props.ref || defaultRef;
42156
42229
  const navRef = props.ref;
@@ -43026,7 +43099,7 @@ const buildBinderCenterlinePathBelow = ({
43026
43099
  };
43027
43100
 
43028
43101
  installImportMetaCssBuild(import.meta);
43029
- const css$V = /* css */`@layer navi {
43102
+ const css$W = /* css */`@layer navi {
43030
43103
  .navi_binder {
43031
43104
  --binder-border-width: var(--navi-control-border-width);
43032
43105
  --binder-border-radius: var(--navi-control-border-radius);
@@ -43311,7 +43384,7 @@ const Binder = ({
43311
43384
  pagePadding,
43312
43385
  ...props
43313
43386
  }) => {
43314
- import.meta.css = [css$V, "@jsenv/navi/src/nav/binder/binder.jsx"];
43387
+ import.meta.css = [css$W, "@jsenv/navi/src/nav/binder/binder.jsx"];
43315
43388
  const items = toChildArray(children).map((child, index) => {
43316
43389
  const {
43317
43390
  value: itemValue,
@@ -43758,7 +43831,7 @@ const writeSpace = (area) => {
43758
43831
  };
43759
43832
 
43760
43833
  installImportMetaCssBuild(import.meta);
43761
- const css$U = /* css */`@layer navi {
43834
+ const css$V = /* css */`@layer navi {
43762
43835
  :root {
43763
43836
  --navi-fixed-bar-width: 56px;
43764
43837
  --navi-fixed-bar-height: 56px;
@@ -43878,7 +43951,7 @@ const FixedBar = ({
43878
43951
  border = true,
43879
43952
  ...props
43880
43953
  }) => {
43881
- import.meta.css = [css$U, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
43954
+ import.meta.css = [css$V, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
43882
43955
  const defaultRef = useRef();
43883
43956
  props.ref = props.ref || defaultRef;
43884
43957
  // Said with the width the border rule reads rather than with an attribute of
@@ -43972,7 +44045,7 @@ const FixedBar = ({
43972
44045
  // Subpixel layout rounds rectangles up on boxes that fit exactly.
43973
44046
  const OVERFLOW_TOLERANCE = 1;
43974
44047
 
43975
- const css$T = /* css */ `
44048
+ const css$U = /* css */ `
43976
44049
  [data-navi-overflow-x] {
43977
44050
  outline: 2px dashed #e74c3c;
43978
44051
  outline-offset: -2px;
@@ -43996,7 +44069,7 @@ const detectHorizontalOverflow = ({
43996
44069
  let styleEl = null;
43997
44070
  if (highlight) {
43998
44071
  styleEl = document.createElement("style");
43999
- styleEl.textContent = css$T;
44072
+ styleEl.textContent = css$U;
44000
44073
  document.head.appendChild(styleEl);
44001
44074
  }
44002
44075
 
@@ -44152,7 +44225,7 @@ const useFocusGroup = (
44152
44225
 
44153
44226
  installImportMetaCssBuild(import.meta);
44154
44227
  const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
44155
- const css$S = /* css */`.navi_summary_marker {
44228
+ const css$T = /* css */`.navi_summary_marker {
44156
44229
  flex-shrink: 0;
44157
44230
  width: 1em;
44158
44231
  height: 1em;
@@ -44244,7 +44317,7 @@ const SummaryMarker = ({
44244
44317
  loading,
44245
44318
  openDirection = "down"
44246
44319
  }) => {
44247
- import.meta.css = [css$S, "@jsenv/navi/src/control/details/summary_marker.jsx"];
44320
+ import.meta.css = [css$T, "@jsenv/navi/src/control/details/summary_marker.jsx"];
44248
44321
  const showLoading = useDebounceTrue(loading, 300);
44249
44322
  return jsx("span", {
44250
44323
  className: "navi_summary_marker",
@@ -44290,7 +44363,7 @@ const SummaryMarker = ({
44290
44363
  };
44291
44364
 
44292
44365
  installImportMetaCssBuild(import.meta);
44293
- const css$R = /* css */`.navi_details {
44366
+ const css$S = /* css */`.navi_details {
44294
44367
  z-index: 1;
44295
44368
  flex-direction: column;
44296
44369
  flex-shrink: 0;
@@ -44335,7 +44408,7 @@ const Details = props => {
44335
44408
  return details;
44336
44409
  };
44337
44410
  const DetailsField = props => {
44338
- import.meta.css = [css$R, "@jsenv/navi/src/control/details/details.jsx"];
44411
+ import.meta.css = [css$S, "@jsenv/navi/src/control/details/details.jsx"];
44339
44412
  const {
44340
44413
  ref,
44341
44414
  persists,
@@ -46080,7 +46153,7 @@ const usePopupContentMount = (
46080
46153
  };
46081
46154
 
46082
46155
  installImportMetaCssBuild(import.meta);
46083
- const css$Q = /* css */`.navi_expandable {
46156
+ const css$R = /* css */`.navi_expandable {
46084
46157
  flex-direction: column;
46085
46158
  flex-shrink: 0;
46086
46159
  display: flex;
@@ -46310,7 +46383,7 @@ const useExpandableContext = partName => {
46310
46383
  * builds the content ahead of the click.
46311
46384
  */
46312
46385
  const Expandable = props => {
46313
- import.meta.css = [css$Q, "@jsenv/navi/src/control/expandable/expandable.jsx"];
46386
+ import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
46314
46387
  /* The open props are read from `props` by the open controller below; they
46315
46388
  are named here only to keep them out of `rest`, and so out of the DOM. */
46316
46389
  /* eslint-disable no-unused-vars */
@@ -46997,46 +47070,6 @@ const shallowDiffers = (a, b) => {
46997
47070
  return false;
46998
47071
  };
46999
47072
 
47000
- const ButtonRouteResolver = props => {
47001
- const Next = useNextResolver();
47002
- if (props.route) {
47003
- return jsx(ButtonWithRoute, {
47004
- ...props
47005
- });
47006
- }
47007
- return jsx(Next, {
47008
- ...props
47009
- });
47010
- };
47011
- const ButtonWithRoute = props => {
47012
- const Next = useNextResolver();
47013
- const {
47014
- route,
47015
- routeParams,
47016
- children,
47017
- pseudoState,
47018
- ...rest
47019
- } = props;
47020
- const url = route.buildUrl(routeParams);
47021
- const {
47022
- matching
47023
- } = useRouteStatus(route);
47024
- const paramsAreMatching = route.matchesParams(routeParams);
47025
- const linkMatching = matching && paramsAreMatching;
47026
-
47027
- // Merged into whatever the caller already holds: a button can be forced into
47028
- // a state for a demo and still learn its own current-ness from its route.
47029
- return jsx(Next, {
47030
- href: url,
47031
- pseudoState: {
47032
- ...pseudoState,
47033
- ":-navi-href-current": linkMatching
47034
- },
47035
- ...rest,
47036
- children: children || route.buildRelativeUrl(routeParams)
47037
- });
47038
- };
47039
-
47040
47073
  const LIGHT_ACCENT_ATTRIBUTE = "data-accent-light";
47041
47074
  const VERY_LIGHT_ACCENT_ATTRIBUTE = "data-accent-very-light";
47042
47075
  const DARK_CONTRAST_ATTRIBUTE = "data-accent-needs-dark-fg";
@@ -47137,7 +47170,7 @@ const useAccentColorAttributes = (
47137
47170
  };
47138
47171
 
47139
47172
  installImportMetaCssBuild(import.meta);
47140
- const css$P = /* css */`@layer navi {
47173
+ const css$Q = /* css */`@layer navi {
47141
47174
  .navi_button {
47142
47175
  --button-border-radius: var(--navi-control-border-radius);
47143
47176
  --button-border-width: var(--navi-control-border-width);
@@ -47438,7 +47471,7 @@ a.navi_button {
47438
47471
  }
47439
47472
  `;
47440
47473
  const ButtonUI = props => {
47441
- import.meta.css = [css$P, "@jsenv/navi/src/control/input/button_ui.jsx"];
47474
+ import.meta.css = [css$Q, "@jsenv/navi/src/control/input/button_ui.jsx"];
47442
47475
  const {
47443
47476
  ref,
47444
47477
  // href/link
@@ -47653,6 +47686,186 @@ const ButtonShadow = () => {
47653
47686
  };
47654
47687
  markAsOutsideTextFlow(ButtonShadow);
47655
47688
 
47689
+ installImportMetaCssBuild(import.meta);
47690
+ const css$P = /* css */`.navi_button_confirm {
47691
+ align-items: center;
47692
+ gap: var(--navi-s);
47693
+ flex-wrap: wrap;
47694
+ display: inline-flex;
47695
+ }
47696
+ `;
47697
+ const ButtonConfirmResolver = props => {
47698
+ const Next = useNextResolver();
47699
+ if (!props.confirm) {
47700
+ return jsx(Next, {
47701
+ ...props
47702
+ });
47703
+ }
47704
+ return jsx(ButtonConfirm, {
47705
+ ...props,
47706
+ Next: Next
47707
+ });
47708
+ };
47709
+ const ButtonConfirm = ({
47710
+ Next,
47711
+ confirm,
47712
+ confirmLabel = naviI18n("button.confirm"),
47713
+ cancelLabel = naviI18n("button.cancel"),
47714
+ confirmTestId,
47715
+ cancelTestId,
47716
+ ...props
47717
+ }) => {
47718
+ import.meta.css = [css$P, "@jsenv/navi/src/control/input/button_confirm.jsx"];
47719
+ const [asking, setAsking] = useState(false);
47720
+ const askingRef = useRef(null);
47721
+ const cancelRef = useRef(null);
47722
+ // Whether going back to the resting label owes the focus a place to land: a
47723
+ // press on "Annuler" or an Escape does, a focus that already left does not —
47724
+ // it is elsewhere by then, and pulling it back would take it from whatever
47725
+ // the person moved to.
47726
+ const restoreFocusRef = useRef(false);
47727
+ const questionId = `${useId()}_question`;
47728
+ const cancel = restoreFocus => {
47729
+ restoreFocusRef.current = restoreFocus;
47730
+ setAsking(false);
47731
+ };
47732
+ useLayoutEffect(() => {
47733
+ if (asking) {
47734
+ // The press was aimed at this button, and the button that answers now is
47735
+ // where it should land: navi's own ladder inside the question, which
47736
+ // finds "Confirmer" (first in the row) unless the caller marked
47737
+ // something else.
47738
+ const found = findFocusTarget(askingRef.current, {
47739
+ restoreMayClaim: true
47740
+ });
47741
+ if (found) {
47742
+ moveFocusTo(found.target);
47743
+ }
47744
+ return;
47745
+ }
47746
+ if (restoreFocusRef.current) {
47747
+ restoreFocusRef.current = false;
47748
+ // The answer may have taken the button with it — a row deleting itself
47749
+ // is the whole point of asking — and there is then nothing to give the
47750
+ // focus back to.
47751
+ const buttonEl = props.ref.current;
47752
+ if (buttonEl) {
47753
+ moveFocusTo(buttonEl);
47754
+ }
47755
+ }
47756
+ }, [asking]);
47757
+ if (!asking) {
47758
+ return jsx(Next, {
47759
+ ...props,
47760
+ // The first press is the question, not the act — so nothing that acts
47761
+ // is handed to the resolvers below. `type` with them: a submit button
47762
+ // would be given `--navi-send` back.
47763
+ type: "button",
47764
+ action: undefined,
47765
+ command: undefined,
47766
+ href: undefined,
47767
+ route: undefined,
47768
+ onClick: () => {
47769
+ setAsking(true);
47770
+ }
47771
+ });
47772
+ }
47773
+ const {
47774
+ action,
47775
+ onActionEnd,
47776
+ onClick
47777
+ } = props;
47778
+ return jsxs("span", {
47779
+ className: "navi_button_confirm",
47780
+ ref: askingRef,
47781
+ onKeyDown: e => {
47782
+ if (e.key === "Escape") {
47783
+ cancel(true);
47784
+ }
47785
+ },
47786
+ onFocusOut: e => {
47787
+ if (!askingRef.current.contains(e.relatedTarget)) {
47788
+ cancel(false);
47789
+ }
47790
+ },
47791
+ children: [jsx("span", {
47792
+ id: questionId,
47793
+ children: confirm === true ? naviI18n("confirm.message") : confirm
47794
+ }), jsx(Next, {
47795
+ ...props,
47796
+ "aria-describedby": questionId
47797
+ // The button confirming is the button that was pressed: it keeps the
47798
+ // caller's own testid unless the question names one of its own.
47799
+ ,
47800
+ "data-testid": confirmTestId || props["data-testid"],
47801
+ onClick: e => {
47802
+ onClick?.(e);
47803
+ if (!action) {
47804
+ // A command runs on the press itself: there is nothing to wait for,
47805
+ // the question is answered as soon as it is pressed.
47806
+ cancel(true);
47807
+ }
47808
+ }
47809
+ // A run that failed keeps the question up, with its callout on the
47810
+ // button that raised it: the retry is then one press away.
47811
+ ,
47812
+ onActionEnd: (data, e) => {
47813
+ onActionEnd?.(data, e);
47814
+ cancel(true);
47815
+ },
47816
+ children: confirmLabel
47817
+ }), jsx(ButtonUI, {
47818
+ ref: cancelRef,
47819
+ type: "button",
47820
+ "data-testid": cancelTestId,
47821
+ onClick: () => {
47822
+ cancel(true);
47823
+ },
47824
+ children: cancelLabel
47825
+ })]
47826
+ });
47827
+ };
47828
+
47829
+ const ButtonRouteResolver = props => {
47830
+ const Next = useNextResolver();
47831
+ if (props.route) {
47832
+ return jsx(ButtonWithRoute, {
47833
+ ...props
47834
+ });
47835
+ }
47836
+ return jsx(Next, {
47837
+ ...props
47838
+ });
47839
+ };
47840
+ const ButtonWithRoute = props => {
47841
+ const Next = useNextResolver();
47842
+ const {
47843
+ route,
47844
+ routeParams,
47845
+ children,
47846
+ pseudoState,
47847
+ ...rest
47848
+ } = props;
47849
+ const url = route.buildUrl(routeParams);
47850
+ const {
47851
+ matching
47852
+ } = useRouteStatus(route);
47853
+ const paramsAreMatching = route.matchesParams(routeParams);
47854
+ const linkMatching = matching && paramsAreMatching;
47855
+
47856
+ // Merged into whatever the caller already holds: a button can be forced into
47857
+ // a state for a demo and still learn its own current-ness from its route.
47858
+ return jsx(Next, {
47859
+ href: url,
47860
+ pseudoState: {
47861
+ ...pseudoState,
47862
+ ":-navi-href-current": linkMatching
47863
+ },
47864
+ ...rest,
47865
+ children: children || route.buildRelativeUrl(routeParams)
47866
+ });
47867
+ };
47868
+
47656
47869
  const ButtonFirstResolver = props => {
47657
47870
  const Next = useNextResolver();
47658
47871
  const defaultRef = useRef(null);
@@ -47814,6 +48027,20 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
47814
48027
  * more: the work may already have happened on the other side (see
47815
48028
  * docs/actions.md#aborting-saves-resources-it-does-not-undo), so say it only
47816
48029
  * where the screen can be re-opened on what is actually there.
48030
+ * @param {boolean|import("ignore:preact").ComponentChildren} [confirm] Ask before
48031
+ * doing it, in the place the button stands: the first press replaces the
48032
+ * button with this question and a "Confirmer"/"Annuler" pair, the second one
48033
+ * does what the button was for. `true` asks navi's default question. The
48034
+ * press stays under the finger and there is nothing to dismiss — "Annuler",
48035
+ * Escape, or the focus leaving puts the button back. Use `<Picker
48036
+ * type="confirm">` instead when the question is long enough to want a popup,
48037
+ * or when the row has no space for it.
48038
+ * @param {import("ignore:preact").ComponentChildren} [confirmLabel] Label of the
48039
+ * second press.
48040
+ * @param {import("ignore:preact").ComponentChildren} [cancelLabel] Label of the way
48041
+ * out of the question.
48042
+ * @param {string} [confirmTestId] `data-testid` of the second press.
48043
+ * @param {string} [cancelTestId] `data-testid` of the way out.
47817
48044
  * @param {string} [contentDisplay] The display of the frame the button draws
47818
48045
  * around its children. It follows the button's own by default — its display
47819
48046
  * and, a display alone saying nothing about direction, the rest of its flow
@@ -47828,7 +48055,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
47828
48055
  * (it wears it itself) and loses the shrink under the finger, which has
47829
48056
  * nothing left to scale but the interactive area itself.
47830
48057
  */
47831
- const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
48058
+ const Button = createComponentResolver([ButtonFirstResolver, ButtonConfirmResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
47832
48059
 
47833
48060
  installImportMetaCssBuild(import.meta);
47834
48061
  const css$O = /* css */`.navi_control_swap {
@@ -55820,13 +56047,7 @@ const TimeDate = ({
55820
56047
  })
55821
56048
  });
55822
56049
  }
55823
- const date = toDate(children, value => {
55824
- if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
55825
- const d = new Date(`${value}T00:00:00`);
55826
- return isNaN(d.getTime()) ? null : d;
55827
- }
55828
- return null;
55829
- });
56050
+ const date = toDate(children);
55830
56051
  if (!date) {
55831
56052
  return jsx(TimeText, {
55832
56053
  ...props,
@@ -72269,6 +72490,26 @@ const css$r = /* css */`.navi_checkbox_group {
72269
72490
  `;
72270
72491
 
72271
72492
  /**
72493
+ * A set of checkboxes answering one question, holding the array of the values
72494
+ * that are checked.
72495
+ *
72496
+ * It renders a `<fieldset>` and hands its `name` down, so the boxes inside are
72497
+ * plain `<Input type="checkbox" value="…">` with nothing else to declare; each
72498
+ * value that is checked appears in the array, in the order the boxes are
72499
+ * written. What it holds is said the way every control says it — `value` /
72500
+ * `defaultValue` (an array), or a bound `signal` — and inside a `<Form>` the
72501
+ * group is one entry of the submitted params, an array.
72502
+ *
72503
+ * ```jsx
72504
+ * <CheckboxGroup name="permissions" defaultValue={["places.moderate"]} action={save}>
72505
+ * <legend>Permissions</legend>
72506
+ * <Field as="label"><Input type="checkbox" value="places.moderate" /> Modérer les lieux</Field>
72507
+ * <Field as="label"><Input type="checkbox" value="games.list" /> Lister les parties</Field>
72508
+ * </CheckboxGroup>
72509
+ * ```
72510
+ *
72511
+ * See `<RadioGroup>` for the same shape holding a single value.
72512
+ *
72272
72513
  * @type {import("ignore:preact").FunctionComponent<{
72273
72514
  * maxLength?: number,
72274
72515
  * maxLengthGuard?: number,
@@ -72990,6 +73231,27 @@ const css$p = /* css */`.navi_radio_group {
72990
73231
  }
72991
73232
  }
72992
73233
  `;
73234
+
73235
+ /**
73236
+ * A set of radios answering one question, holding the value of the one that is
73237
+ * checked.
73238
+ *
73239
+ * It renders a `<fieldset>` and hands its `name` down, so the radios inside are
73240
+ * plain `<Input type="radio" value="…">` with nothing else to declare — sharing
73241
+ * a name is what makes them exclusive. What it holds is said the way every
73242
+ * control says it — `value` / `defaultValue`, or a bound `signal` — and inside a
73243
+ * `<Form>` the group is one entry of the submitted params.
73244
+ *
73245
+ * ```jsx
73246
+ * <RadioGroup name="role" defaultValue="viewer" action={save}>
73247
+ * <legend>Rôle</legend>
73248
+ * <Field as="label"><Input type="radio" value="viewer" /> Lecteur</Field>
73249
+ * <Field as="label"><Input type="radio" value="admin" /> Administrateur</Field>
73250
+ * </RadioGroup>
73251
+ * ```
73252
+ *
73253
+ * See `<CheckboxGroup>` for the same shape holding an array.
73254
+ */
72993
73255
  const RadioGroup = props => {
72994
73256
  const refDefault = useRef(null);
72995
73257
  props.ref = props.ref || refDefault;