@jsenv/navi 0.29.229 → 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
  }
@@ -5749,7 +5749,7 @@ installImportMetaCssBuild(import.meta);
5749
5749
  * (see calloutTemplate in callout.js).
5750
5750
  */
5751
5751
 
5752
- const css$13 = /* css */`.navi_callout_status_icon {
5752
+ const css$14 = /* css */`.navi_callout_status_icon {
5753
5753
  --x-callout-status-icon-color: var(--navi-callout-neutral-color);
5754
5754
  box-sizing: border-box;
5755
5755
  aspect-ratio: 1;
@@ -5820,7 +5820,7 @@ const CalloutStatusIcon = ({
5820
5820
  status = "info",
5821
5821
  shape = "square"
5822
5822
  }) => {
5823
- import.meta.css = [css$13, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
5823
+ import.meta.css = [css$14, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
5824
5824
  return jsx("span", {
5825
5825
  className: "navi_callout_status_icon",
5826
5826
  "data-status": status === "none" ? undefined : status,
@@ -5842,7 +5842,7 @@ installImportMetaCssBuild(import.meta);
5842
5842
  // Unique for the page's lifetime: a caller may write the id in a commandfor.
5843
5843
  let calloutCount = 0;
5844
5844
 
5845
- const css$12 = /* css */ `
5845
+ const css$13 = /* css */ `
5846
5846
  @layer navi {
5847
5847
  .navi_callout {
5848
5848
  /* A callout is parented to what it explains, so it inherits from it — and
@@ -6190,7 +6190,7 @@ const openCallout = (
6190
6190
  debug = () => {},
6191
6191
  } = {},
6192
6192
  ) => {
6193
- import.meta.css = [css$12, "@jsenv/navi/src/control/rules/callout/callout.js"];
6193
+ import.meta.css = [css$13, "@jsenv/navi/src/control/rules/callout/callout.js"];
6194
6194
  if (debug === true) {
6195
6195
  debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
6196
6196
  }
@@ -18238,8 +18238,12 @@ const VISUAL_PROPS = {
18238
18238
  outlineWidth: PASS_THROUGH,
18239
18239
  boxDecorationBreak: PASS_THROUGH,
18240
18240
  boxShadow: PASS_THROUGH,
18241
- background: PASS_THROUGH,
18242
- backgroundColor: PASS_THROUGH,
18241
+ background: (value) => {
18242
+ return { background: resolveSurfaceKeyword(value) };
18243
+ },
18244
+ backgroundColor: (value) => {
18245
+ return { backgroundColor: resolveSurfaceKeyword(value) };
18246
+ },
18243
18247
  backgroundImage: PASS_THROUGH,
18244
18248
  backgroundSize: PASS_THROUGH,
18245
18249
  border: PASS_THROUGH,
@@ -18604,6 +18608,18 @@ const resolveColorKeyword = (value) => {
18604
18608
  return COLOR_KEYWORD_MAP[value] || value;
18605
18609
  };
18606
18610
 
18611
+ // The two planes an app paints: the paper its content sits on, and the frame
18612
+ // around it (see navi_css_vars.js). Named rather than derived at each call
18613
+ // site, so a top bar and a side menu are the same plane without agreeing on a
18614
+ // formula.
18615
+ const SURFACE_KEYWORD_MAP = {
18616
+ surface: "var(--navi-surface-color)",
18617
+ chrome: "var(--navi-chrome-color)",
18618
+ };
18619
+ const resolveSurfaceKeyword = (value) => {
18620
+ return SURFACE_KEYWORD_MAP[value] || value;
18621
+ };
18622
+
18607
18623
  const DEFAULT_DISPLAY_BY_TAG_NAME = {
18608
18624
  "inline": new Set([
18609
18625
  "a",
@@ -20505,6 +20521,16 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
20505
20521
  * children?: import("ignore:preact").ComponentChildren,
20506
20522
  * [key: string]: any,
20507
20523
  * }>}
20524
+ * @param {string} [background] Any CSS background, plus two keywords for the
20525
+ * planes an app paints: `"surface"` is the paper content sits on,
20526
+ * `"chrome"` the frame around a screen — a top bar, a side menu, a toolbar.
20527
+ * Both follow the theme, so a bar stays a bar in dark mode
20528
+ * (`--navi-surface-color` / `--navi-chrome-color`). `backgroundColor` takes
20529
+ * the same two.
20530
+ * @param {string} [color] Any CSS color, plus the five ink keywords —
20531
+ * `"primary"`, `"secondary"`, `"emphasis"`, `"discrete"`, `"hint"` — which
20532
+ * say how loud a text is rather than which color it is (see
20533
+ * docs/typography.md).
20508
20534
  * @param {object} [interactions] What this box answers, by interaction name
20509
20535
  * (see docs/interactions.md). A plain box has no wiring of its own: it does
20510
20536
  * nothing with `action` (that is a control's prop), so a click on it is
@@ -22408,6 +22434,20 @@ const debug$2 = (...args) => {
22408
22434
  // Base URL management
22409
22435
  let baseFileUrl;
22410
22436
  let baseUrl;
22437
+ /**
22438
+ * Where this document's addresses start, for a document that is not at the
22439
+ * root: a panel served from "/admin/admin.html" declares
22440
+ * `setBaseUrl("/admin/admin.html")` and then writes its routes as "/places",
22441
+ * "/places/:placeId". The url "/admin/places/42" matches, and `buildUrl` puts
22442
+ * the prefix back.
22443
+ *
22444
+ * Call it in the routes module, above the `route()` calls it governs — they
22445
+ * read the base url as they are created, so an entry point calling it after
22446
+ * importing them is too late. The server must serve the document for every
22447
+ * address below it (see docs/navigation.md).
22448
+ *
22449
+ * @param {string} value - the document's own url, absolute or relative.
22450
+ */
22411
22451
  const setBaseUrl = (value) => {
22412
22452
  baseFileUrl = new URL(
22413
22453
  value,
@@ -28120,7 +28160,7 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
28120
28160
  // the root pictures must NOT move (they carry the whole viewport, blank bands
28121
28161
  // included).
28122
28162
 
28123
- const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-transition-area] {
28163
+ const css$12 = /* css */`:root[data-navi-route-transition] [data-navi-route-transition-area] {
28124
28164
  view-transition-name: navi-route-transition;
28125
28165
  }
28126
28166
 
@@ -28441,7 +28481,7 @@ const RouteTransitionArea = ({
28441
28481
  children,
28442
28482
  ...rest
28443
28483
  }) => {
28444
- import.meta.css = [css$11, "@jsenv/navi/src/nav/route_transition.jsx"];
28484
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
28445
28485
  installTransitionWindowCss();
28446
28486
  const props = {
28447
28487
  ...rest,
@@ -28511,7 +28551,7 @@ const RouteTransitionArea = ({
28511
28551
  * @returns {() => void} remove this relation.
28512
28552
  */
28513
28553
  const defineRouteTransition = (from, to, transition) => {
28514
- import.meta.css = [css$11, "@jsenv/navi/src/nav/route_transition.jsx"];
28554
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
28515
28555
  installTransitionWindowCss();
28516
28556
  if (!to) {
28517
28557
  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 +28592,7 @@ const defineRouteTransition = (from, to, transition) => {
28552
28592
  * @returns {() => void} remove this default.
28553
28593
  */
28554
28594
  const defineRouteDefaultTransition = transition => {
28555
- import.meta.css = [css$11, "@jsenv/navi/src/nav/route_transition.jsx"];
28595
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
28556
28596
  installTransitionWindowCss();
28557
28597
  const value = normalizeTransition(transition);
28558
28598
  defaultTransition = value;
@@ -29374,7 +29414,7 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
29374
29414
  const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
29375
29415
  // The name the box wears while it travels, and only then (see nameForTravel).
29376
29416
  const TRAVEL_NAME = "navi-route-travel";
29377
- const css$10 = /* css */`.navi_route_travel {
29417
+ const css$11 = /* css */`.navi_route_travel {
29378
29418
  position: relative;
29379
29419
 
29380
29420
  &[data-travel-by-drag="x"] {
@@ -29663,7 +29703,7 @@ const RouteTravel = ({
29663
29703
  children,
29664
29704
  ...rest
29665
29705
  }) => {
29666
- import.meta.css = [css$10, "@jsenv/navi/src/nav/route_travel.jsx"];
29706
+ import.meta.css = [css$11, "@jsenv/navi/src/nav/route_travel.jsx"];
29667
29707
  installTransitionWindowCss();
29668
29708
  const elementRef = useRef();
29669
29709
  const gestureRef = useRef(null);
@@ -39227,7 +39267,7 @@ const setupNetworkMonitoring = () => {
39227
39267
  setupNetworkMonitoring();
39228
39268
 
39229
39269
  installImportMetaCssBuild(import.meta);
39230
- const css$$ = /* css */`.navi_loading_indicator_fluid_container {
39270
+ const css$10 = /* css */`.navi_loading_indicator_fluid_container {
39231
39271
  border-radius: inherit;
39232
39272
  opacity: 1;
39233
39273
  width: 100%;
@@ -39258,7 +39298,7 @@ const LoadingIndicatorFluid = ({
39258
39298
  visuallyHidden,
39259
39299
  ...rest
39260
39300
  }) => {
39261
- import.meta.css = [css$$, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
39301
+ import.meta.css = [css$10, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
39262
39302
  const ref = useRef(null);
39263
39303
  // The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
39264
39304
  const [containerWidth, setContainerWidth] = useState(0);
@@ -39464,7 +39504,7 @@ const LoadingRectangleSvg = ({
39464
39504
  };
39465
39505
 
39466
39506
  installImportMetaCssBuild(import.meta);
39467
- const css$_ = /* css */`.navi_loading_outline_wrapper {
39507
+ const css$$ = /* css */`.navi_loading_outline_wrapper {
39468
39508
  top: max(var(--loading-outline-min-inset, -100vh),
39469
39509
  var(--loading-rectangle-top, 0px));
39470
39510
  right: max(var(--loading-outline-min-inset, -100vh),
@@ -39485,7 +39525,7 @@ const css$_ = /* css */`.navi_loading_outline_wrapper {
39485
39525
  }
39486
39526
  `;
39487
39527
  const LoadingOutline = props => {
39488
- import.meta.css = [css$_, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
39528
+ import.meta.css = [css$$, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
39489
39529
  if (props.containerRef) {
39490
39530
  const container = props.containerRef.current;
39491
39531
  if (!container) {
@@ -39620,7 +39660,7 @@ const LoadingOutlineWithPortal = props => {
39620
39660
  };
39621
39661
 
39622
39662
  installImportMetaCssBuild(import.meta);
39623
- const css$Z = /* css */`.navi_text_anchor {
39663
+ const css$_ = /* css */`.navi_text_anchor {
39624
39664
  vertical-align: baseline;
39625
39665
  user-select: none;
39626
39666
  overflow: hidden;
@@ -39658,7 +39698,7 @@ const TextAnchor = ({
39658
39698
  textSize,
39659
39699
  lineLayout
39660
39700
  }) => {
39661
- import.meta.css = [css$Z, "@jsenv/navi/src/text/text_anchor.jsx"];
39701
+ import.meta.css = [css$_, "@jsenv/navi/src/text/text_anchor.jsx"];
39662
39702
  const anchorRef = useRef();
39663
39703
 
39664
39704
  // Plain useLayoutEffect would also fire while an ancestor dialog/popover
@@ -39886,7 +39926,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
39886
39926
  };
39887
39927
 
39888
39928
  installImportMetaCssBuild(import.meta);
39889
- const css$Y = /* css */`@layer navi {
39929
+ const css$Z = /* css */`@layer navi {
39890
39930
  .navi_text {
39891
39931
  &[data-skeleton] {
39892
39932
  border-radius: .2em;
@@ -40455,7 +40495,7 @@ const TextShrinkWrap = props => {
40455
40495
  });
40456
40496
  };
40457
40497
  const TextUI = props => {
40458
- import.meta.css = [css$Y, "@jsenv/navi/src/text/text.jsx"];
40498
+ import.meta.css = [css$Z, "@jsenv/navi/src/text/text.jsx"];
40459
40499
  let {
40460
40500
  ref,
40461
40501
  spacing,
@@ -40679,7 +40719,7 @@ const Icon = ({
40679
40719
  fillLine,
40680
40720
  ...props
40681
40721
  }) => {
40682
- import.meta.css = [css$Y, "@jsenv/navi/src/text/text.jsx"];
40722
+ import.meta.css = [css$Z, "@jsenv/navi/src/text/text.jsx"];
40683
40723
  const innerChildren = href ? jsx("svg", {
40684
40724
  width: "100%",
40685
40725
  height: "100%",
@@ -40869,7 +40909,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
40869
40909
  };
40870
40910
 
40871
40911
  installImportMetaCssBuild(import.meta);
40872
- const css$X = /* css */`@layer navi {
40912
+ const css$Y = /* css */`@layer navi {
40873
40913
  .navi_link {
40874
40914
  --link-border-radius: unset;
40875
40915
  --link-outline-color: var(--navi-focus-outline-color);
@@ -41327,7 +41367,7 @@ Object.assign(PSEUDO_CLASSES, {
41327
41367
  * @param {boolean} [props.readOnly]
41328
41368
  */
41329
41369
  const Link = props => {
41330
- import.meta.css = [css$X, "@jsenv/navi/src/nav/link/link.jsx"];
41370
+ import.meta.css = [css$Y, "@jsenv/navi/src/nav/link/link.jsx"];
41331
41371
  if (props.route) {
41332
41372
  return jsx(LinkWithRoute, {
41333
41373
  ...props
@@ -41886,7 +41926,7 @@ const rememberProgressAnimation = (navElement, travel) => {
41886
41926
  travel.progressAnimation = animation;
41887
41927
  travel.progressFrom = animation ? parseFloat(animation.effect.getKeyframes()[0]["--slide-travel-progress"]) || 0 : 0;
41888
41928
  };
41889
- const css$W = /* css */`@layer navi {
41929
+ const css$X = /* css */`@layer navi {
41890
41930
  .navi_nav {
41891
41931
  --nav-border: none;
41892
41932
  --nav-padding: 0px;
@@ -42150,7 +42190,7 @@ const Nav = ({
42150
42190
  slideContainer,
42151
42191
  ...props
42152
42192
  }) => {
42153
- import.meta.css = [css$W, "@jsenv/navi/src/nav/link/nav.jsx"];
42193
+ import.meta.css = [css$X, "@jsenv/navi/src/nav/link/nav.jsx"];
42154
42194
  const defaultRef = useRef();
42155
42195
  props.ref = props.ref || defaultRef;
42156
42196
  const navRef = props.ref;
@@ -43026,7 +43066,7 @@ const buildBinderCenterlinePathBelow = ({
43026
43066
  };
43027
43067
 
43028
43068
  installImportMetaCssBuild(import.meta);
43029
- const css$V = /* css */`@layer navi {
43069
+ const css$W = /* css */`@layer navi {
43030
43070
  .navi_binder {
43031
43071
  --binder-border-width: var(--navi-control-border-width);
43032
43072
  --binder-border-radius: var(--navi-control-border-radius);
@@ -43311,7 +43351,7 @@ const Binder = ({
43311
43351
  pagePadding,
43312
43352
  ...props
43313
43353
  }) => {
43314
- import.meta.css = [css$V, "@jsenv/navi/src/nav/binder/binder.jsx"];
43354
+ import.meta.css = [css$W, "@jsenv/navi/src/nav/binder/binder.jsx"];
43315
43355
  const items = toChildArray(children).map((child, index) => {
43316
43356
  const {
43317
43357
  value: itemValue,
@@ -43758,7 +43798,7 @@ const writeSpace = (area) => {
43758
43798
  };
43759
43799
 
43760
43800
  installImportMetaCssBuild(import.meta);
43761
- const css$U = /* css */`@layer navi {
43801
+ const css$V = /* css */`@layer navi {
43762
43802
  :root {
43763
43803
  --navi-fixed-bar-width: 56px;
43764
43804
  --navi-fixed-bar-height: 56px;
@@ -43878,7 +43918,7 @@ const FixedBar = ({
43878
43918
  border = true,
43879
43919
  ...props
43880
43920
  }) => {
43881
- import.meta.css = [css$U, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
43921
+ import.meta.css = [css$V, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
43882
43922
  const defaultRef = useRef();
43883
43923
  props.ref = props.ref || defaultRef;
43884
43924
  // Said with the width the border rule reads rather than with an attribute of
@@ -43972,7 +44012,7 @@ const FixedBar = ({
43972
44012
  // Subpixel layout rounds rectangles up on boxes that fit exactly.
43973
44013
  const OVERFLOW_TOLERANCE = 1;
43974
44014
 
43975
- const css$T = /* css */ `
44015
+ const css$U = /* css */ `
43976
44016
  [data-navi-overflow-x] {
43977
44017
  outline: 2px dashed #e74c3c;
43978
44018
  outline-offset: -2px;
@@ -43996,7 +44036,7 @@ const detectHorizontalOverflow = ({
43996
44036
  let styleEl = null;
43997
44037
  if (highlight) {
43998
44038
  styleEl = document.createElement("style");
43999
- styleEl.textContent = css$T;
44039
+ styleEl.textContent = css$U;
44000
44040
  document.head.appendChild(styleEl);
44001
44041
  }
44002
44042
 
@@ -44152,7 +44192,7 @@ const useFocusGroup = (
44152
44192
 
44153
44193
  installImportMetaCssBuild(import.meta);
44154
44194
  const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
44155
- const css$S = /* css */`.navi_summary_marker {
44195
+ const css$T = /* css */`.navi_summary_marker {
44156
44196
  flex-shrink: 0;
44157
44197
  width: 1em;
44158
44198
  height: 1em;
@@ -44244,7 +44284,7 @@ const SummaryMarker = ({
44244
44284
  loading,
44245
44285
  openDirection = "down"
44246
44286
  }) => {
44247
- import.meta.css = [css$S, "@jsenv/navi/src/control/details/summary_marker.jsx"];
44287
+ import.meta.css = [css$T, "@jsenv/navi/src/control/details/summary_marker.jsx"];
44248
44288
  const showLoading = useDebounceTrue(loading, 300);
44249
44289
  return jsx("span", {
44250
44290
  className: "navi_summary_marker",
@@ -44290,7 +44330,7 @@ const SummaryMarker = ({
44290
44330
  };
44291
44331
 
44292
44332
  installImportMetaCssBuild(import.meta);
44293
- const css$R = /* css */`.navi_details {
44333
+ const css$S = /* css */`.navi_details {
44294
44334
  z-index: 1;
44295
44335
  flex-direction: column;
44296
44336
  flex-shrink: 0;
@@ -44335,7 +44375,7 @@ const Details = props => {
44335
44375
  return details;
44336
44376
  };
44337
44377
  const DetailsField = props => {
44338
- import.meta.css = [css$R, "@jsenv/navi/src/control/details/details.jsx"];
44378
+ import.meta.css = [css$S, "@jsenv/navi/src/control/details/details.jsx"];
44339
44379
  const {
44340
44380
  ref,
44341
44381
  persists,
@@ -46080,7 +46120,7 @@ const usePopupContentMount = (
46080
46120
  };
46081
46121
 
46082
46122
  installImportMetaCssBuild(import.meta);
46083
- const css$Q = /* css */`.navi_expandable {
46123
+ const css$R = /* css */`.navi_expandable {
46084
46124
  flex-direction: column;
46085
46125
  flex-shrink: 0;
46086
46126
  display: flex;
@@ -46310,7 +46350,7 @@ const useExpandableContext = partName => {
46310
46350
  * builds the content ahead of the click.
46311
46351
  */
46312
46352
  const Expandable = props => {
46313
- import.meta.css = [css$Q, "@jsenv/navi/src/control/expandable/expandable.jsx"];
46353
+ import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
46314
46354
  /* The open props are read from `props` by the open controller below; they
46315
46355
  are named here only to keep them out of `rest`, and so out of the DOM. */
46316
46356
  /* eslint-disable no-unused-vars */
@@ -46997,46 +47037,6 @@ const shallowDiffers = (a, b) => {
46997
47037
  return false;
46998
47038
  };
46999
47039
 
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
47040
  const LIGHT_ACCENT_ATTRIBUTE = "data-accent-light";
47041
47041
  const VERY_LIGHT_ACCENT_ATTRIBUTE = "data-accent-very-light";
47042
47042
  const DARK_CONTRAST_ATTRIBUTE = "data-accent-needs-dark-fg";
@@ -47137,7 +47137,7 @@ const useAccentColorAttributes = (
47137
47137
  };
47138
47138
 
47139
47139
  installImportMetaCssBuild(import.meta);
47140
- const css$P = /* css */`@layer navi {
47140
+ const css$Q = /* css */`@layer navi {
47141
47141
  .navi_button {
47142
47142
  --button-border-radius: var(--navi-control-border-radius);
47143
47143
  --button-border-width: var(--navi-control-border-width);
@@ -47438,7 +47438,7 @@ a.navi_button {
47438
47438
  }
47439
47439
  `;
47440
47440
  const ButtonUI = props => {
47441
- import.meta.css = [css$P, "@jsenv/navi/src/control/input/button_ui.jsx"];
47441
+ import.meta.css = [css$Q, "@jsenv/navi/src/control/input/button_ui.jsx"];
47442
47442
  const {
47443
47443
  ref,
47444
47444
  // href/link
@@ -47653,6 +47653,186 @@ const ButtonShadow = () => {
47653
47653
  };
47654
47654
  markAsOutsideTextFlow(ButtonShadow);
47655
47655
 
47656
+ installImportMetaCssBuild(import.meta);
47657
+ const css$P = /* css */`.navi_button_confirm {
47658
+ align-items: center;
47659
+ gap: var(--navi-s);
47660
+ flex-wrap: wrap;
47661
+ display: inline-flex;
47662
+ }
47663
+ `;
47664
+ const ButtonConfirmResolver = props => {
47665
+ const Next = useNextResolver();
47666
+ if (!props.confirm) {
47667
+ return jsx(Next, {
47668
+ ...props
47669
+ });
47670
+ }
47671
+ return jsx(ButtonConfirm, {
47672
+ ...props,
47673
+ Next: Next
47674
+ });
47675
+ };
47676
+ const ButtonConfirm = ({
47677
+ Next,
47678
+ confirm,
47679
+ confirmLabel = naviI18n("button.confirm"),
47680
+ cancelLabel = naviI18n("button.cancel"),
47681
+ confirmTestId,
47682
+ cancelTestId,
47683
+ ...props
47684
+ }) => {
47685
+ import.meta.css = [css$P, "@jsenv/navi/src/control/input/button_confirm.jsx"];
47686
+ const [asking, setAsking] = useState(false);
47687
+ const askingRef = useRef(null);
47688
+ const cancelRef = useRef(null);
47689
+ // Whether going back to the resting label owes the focus a place to land: a
47690
+ // press on "Annuler" or an Escape does, a focus that already left does not —
47691
+ // it is elsewhere by then, and pulling it back would take it from whatever
47692
+ // the person moved to.
47693
+ const restoreFocusRef = useRef(false);
47694
+ const questionId = `${useId()}_question`;
47695
+ const cancel = restoreFocus => {
47696
+ restoreFocusRef.current = restoreFocus;
47697
+ setAsking(false);
47698
+ };
47699
+ useLayoutEffect(() => {
47700
+ if (asking) {
47701
+ // The press was aimed at this button, and the button that answers now is
47702
+ // where it should land: navi's own ladder inside the question, which
47703
+ // finds "Confirmer" (first in the row) unless the caller marked
47704
+ // something else.
47705
+ const found = findFocusTarget(askingRef.current, {
47706
+ restoreMayClaim: true
47707
+ });
47708
+ if (found) {
47709
+ moveFocusTo(found.target);
47710
+ }
47711
+ return;
47712
+ }
47713
+ if (restoreFocusRef.current) {
47714
+ restoreFocusRef.current = false;
47715
+ // The answer may have taken the button with it — a row deleting itself
47716
+ // is the whole point of asking — and there is then nothing to give the
47717
+ // focus back to.
47718
+ const buttonEl = props.ref.current;
47719
+ if (buttonEl) {
47720
+ moveFocusTo(buttonEl);
47721
+ }
47722
+ }
47723
+ }, [asking]);
47724
+ if (!asking) {
47725
+ return jsx(Next, {
47726
+ ...props,
47727
+ // The first press is the question, not the act — so nothing that acts
47728
+ // is handed to the resolvers below. `type` with them: a submit button
47729
+ // would be given `--navi-send` back.
47730
+ type: "button",
47731
+ action: undefined,
47732
+ command: undefined,
47733
+ href: undefined,
47734
+ route: undefined,
47735
+ onClick: () => {
47736
+ setAsking(true);
47737
+ }
47738
+ });
47739
+ }
47740
+ const {
47741
+ action,
47742
+ onActionEnd,
47743
+ onClick
47744
+ } = props;
47745
+ return jsxs("span", {
47746
+ className: "navi_button_confirm",
47747
+ ref: askingRef,
47748
+ onKeyDown: e => {
47749
+ if (e.key === "Escape") {
47750
+ cancel(true);
47751
+ }
47752
+ },
47753
+ onFocusOut: e => {
47754
+ if (!askingRef.current.contains(e.relatedTarget)) {
47755
+ cancel(false);
47756
+ }
47757
+ },
47758
+ children: [jsx("span", {
47759
+ id: questionId,
47760
+ children: confirm === true ? naviI18n("confirm.message") : confirm
47761
+ }), jsx(Next, {
47762
+ ...props,
47763
+ "aria-describedby": questionId
47764
+ // The button confirming is the button that was pressed: it keeps the
47765
+ // caller's own testid unless the question names one of its own.
47766
+ ,
47767
+ "data-testid": confirmTestId || props["data-testid"],
47768
+ onClick: e => {
47769
+ onClick?.(e);
47770
+ if (!action) {
47771
+ // A command runs on the press itself: there is nothing to wait for,
47772
+ // the question is answered as soon as it is pressed.
47773
+ cancel(true);
47774
+ }
47775
+ }
47776
+ // A run that failed keeps the question up, with its callout on the
47777
+ // button that raised it: the retry is then one press away.
47778
+ ,
47779
+ onActionEnd: (data, e) => {
47780
+ onActionEnd?.(data, e);
47781
+ cancel(true);
47782
+ },
47783
+ children: confirmLabel
47784
+ }), jsx(ButtonUI, {
47785
+ ref: cancelRef,
47786
+ type: "button",
47787
+ "data-testid": cancelTestId,
47788
+ onClick: () => {
47789
+ cancel(true);
47790
+ },
47791
+ children: cancelLabel
47792
+ })]
47793
+ });
47794
+ };
47795
+
47796
+ const ButtonRouteResolver = props => {
47797
+ const Next = useNextResolver();
47798
+ if (props.route) {
47799
+ return jsx(ButtonWithRoute, {
47800
+ ...props
47801
+ });
47802
+ }
47803
+ return jsx(Next, {
47804
+ ...props
47805
+ });
47806
+ };
47807
+ const ButtonWithRoute = props => {
47808
+ const Next = useNextResolver();
47809
+ const {
47810
+ route,
47811
+ routeParams,
47812
+ children,
47813
+ pseudoState,
47814
+ ...rest
47815
+ } = props;
47816
+ const url = route.buildUrl(routeParams);
47817
+ const {
47818
+ matching
47819
+ } = useRouteStatus(route);
47820
+ const paramsAreMatching = route.matchesParams(routeParams);
47821
+ const linkMatching = matching && paramsAreMatching;
47822
+
47823
+ // Merged into whatever the caller already holds: a button can be forced into
47824
+ // a state for a demo and still learn its own current-ness from its route.
47825
+ return jsx(Next, {
47826
+ href: url,
47827
+ pseudoState: {
47828
+ ...pseudoState,
47829
+ ":-navi-href-current": linkMatching
47830
+ },
47831
+ ...rest,
47832
+ children: children || route.buildRelativeUrl(routeParams)
47833
+ });
47834
+ };
47835
+
47656
47836
  const ButtonFirstResolver = props => {
47657
47837
  const Next = useNextResolver();
47658
47838
  const defaultRef = useRef(null);
@@ -47814,6 +47994,20 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
47814
47994
  * more: the work may already have happened on the other side (see
47815
47995
  * docs/actions.md#aborting-saves-resources-it-does-not-undo), so say it only
47816
47996
  * where the screen can be re-opened on what is actually there.
47997
+ * @param {boolean|import("ignore:preact").ComponentChildren} [confirm] Ask before
47998
+ * doing it, in the place the button stands: the first press replaces the
47999
+ * button with this question and a "Confirmer"/"Annuler" pair, the second one
48000
+ * does what the button was for. `true` asks navi's default question. The
48001
+ * press stays under the finger and there is nothing to dismiss — "Annuler",
48002
+ * Escape, or the focus leaving puts the button back. Use `<Picker
48003
+ * type="confirm">` instead when the question is long enough to want a popup,
48004
+ * or when the row has no space for it.
48005
+ * @param {import("ignore:preact").ComponentChildren} [confirmLabel] Label of the
48006
+ * second press.
48007
+ * @param {import("ignore:preact").ComponentChildren} [cancelLabel] Label of the way
48008
+ * out of the question.
48009
+ * @param {string} [confirmTestId] `data-testid` of the second press.
48010
+ * @param {string} [cancelTestId] `data-testid` of the way out.
47817
48011
  * @param {string} [contentDisplay] The display of the frame the button draws
47818
48012
  * around its children. It follows the button's own by default — its display
47819
48013
  * and, a display alone saying nothing about direction, the rest of its flow
@@ -47828,7 +48022,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
47828
48022
  * (it wears it itself) and loses the shrink under the finger, which has
47829
48023
  * nothing left to scale but the interactive area itself.
47830
48024
  */
47831
- const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
48025
+ const Button = createComponentResolver([ButtonFirstResolver, ButtonConfirmResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
47832
48026
 
47833
48027
  installImportMetaCssBuild(import.meta);
47834
48028
  const css$O = /* css */`.navi_control_swap {
@@ -55820,13 +56014,7 @@ const TimeDate = ({
55820
56014
  })
55821
56015
  });
55822
56016
  }
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
- });
56017
+ const date = toDate(children);
55830
56018
  if (!date) {
55831
56019
  return jsx(TimeText, {
55832
56020
  ...props,
@@ -63429,158 +63617,163 @@ const ListSelectable = props => {
63429
63617
  }
63430
63618
  setCurrentId(initialEl.id);
63431
63619
  }, []);
63432
- const listVnode = jsx(Next, {
63433
- "navi-selectable": "",
63434
- ...listControlRootProps,
63435
- ...listControlProps,
63436
- // "loading" is a control prop, so useControlgroupProps consumes it (into
63437
- // aria-busy / the :-navi-loading pseudo state) and it does not survive
63438
- // into the props below. ListUI needs it too — it is what makes the list
63439
- // render skeleton rows instead of its (not yet known) items.
63440
- loading: props.loading,
63441
- name: undefined,
63442
- value: undefined,
63443
- defaultValue: undefined,
63444
- selectable: undefined,
63445
- multiple: undefined,
63446
- deselectable: undefined,
63447
- focusGroupDirection: undefined,
63448
- focusGroupWrap: undefined
63449
- // Track focus inside the list: whichever item gets focus becomes current.
63450
- ,
63451
- onFocusIn: e => {
63452
- const realInput = e.target.closest("[navi-selectable-real-input]");
63453
- if (!realInput) {
63454
- return;
63455
- }
63456
- const itemEl = realInput.closest("[navi-list-item-real]");
63457
- if (itemEl && itemEl.id) {
63458
- setCurrentId(itemEl.id, e);
63459
- }
63460
- },
63461
- onnavi_request_select: e => {
63462
- const {
63463
- id
63464
- } = e.detail;
63465
- if (id === undefined) {
63466
- return;
63467
- }
63468
- const inputId = `${id}_input`;
63469
- const childController = uiGroupStateController.findChildById(inputId);
63470
- if (!childController) {
63471
- return;
63472
- }
63473
- const list = ref.current;
63474
- dispatchRequestInteraction(list, {
63475
- event: e,
63476
- name: "select",
63477
- prevented: () => e.preventDefault(),
63478
- // tell the requester that we don't want to select this item
63479
- // Asked of the item too, not only of the list: an item can be the one
63480
- // refusing (the list already holds all it accepts, see maxLength), and
63481
- // it is the one that then says why.
63482
- allowed: () => {
63483
- dispatchRequestInteraction(childController.ref.current, {
63484
- event: e,
63485
- name: "select",
63486
- prevented: () => e.preventDefault(),
63487
- allowed: () => childController.setUIState(childController.value, e)
63488
- });
63620
+
63621
+ // The group's contexts stand around the whole list, never between the <ul>
63622
+ // and the rows: the walk that gives the rows their places is over the <ul>'s
63623
+ // children, and a component sitting there is one child — every row would then
63624
+ // stand in a single slot, and reordering them (a search) would move nothing.
63625
+ const listVnode = jsx(ControlgroupChildrenWrapper, {
63626
+ ...childrenWrapperProps,
63627
+ children: jsx(Next, {
63628
+ "navi-selectable": "",
63629
+ ...listControlRootProps,
63630
+ ...listControlProps,
63631
+ // "loading" is a control prop, so useControlgroupProps consumes it (into
63632
+ // aria-busy / the :-navi-loading pseudo state) and it does not survive
63633
+ // into the props below. ListUI needs it too — it is what makes the list
63634
+ // render skeleton rows instead of its (not yet known) items.
63635
+ loading: props.loading,
63636
+ name: undefined,
63637
+ value: undefined,
63638
+ defaultValue: undefined,
63639
+ selectable: undefined,
63640
+ multiple: undefined,
63641
+ deselectable: undefined,
63642
+ focusGroupDirection: undefined,
63643
+ focusGroupWrap: undefined
63644
+ // Track focus inside the list: whichever item gets focus becomes current.
63645
+ ,
63646
+ onFocusIn: e => {
63647
+ const realInput = e.target.closest("[navi-selectable-real-input]");
63648
+ if (!realInput) {
63649
+ return;
63489
63650
  }
63490
- });
63491
- },
63492
- onnavi_request_unselect: e => {
63493
- const {
63494
- id
63495
- } = e.detail;
63496
- if (id === undefined) {
63497
- return;
63498
- }
63499
- const inputId = `${id}_input`;
63500
- const childController = uiGroupStateController.findChildById(inputId);
63501
- if (!childController) {
63502
- return;
63503
- }
63504
- const list = ref.current;
63505
- dispatchRequestInteraction(list, {
63506
- event: e,
63507
- name: "unselect",
63508
- prevented: () => e.preventDefault(),
63509
- // tell the requester that we don't want to unselect this item
63510
- allowed: () => childController.setUIState(undefined, e)
63511
- });
63512
- }
63513
- // "previous"/"next", not "up"/"down": a list is a line of items whichever
63514
- // way it is laid out, and a horizontal one walks sideways. The keys that
63515
- // drive it (arrows, Home/End) map onto that here, and so do the commands
63516
- // (--navi-previous / --navi-next / --navi-first / --navi-last).
63517
- ,
63518
- onnavi_request_nav: e => {
63519
- const {
63520
- goal
63521
- } = e.detail;
63522
- const navigableEls = getNavigableElements();
63523
- if (navigableEls.length === 0) {
63524
- return;
63651
+ const itemEl = realInput.closest("[navi-list-item-real]");
63652
+ if (itemEl && itemEl.id) {
63653
+ setCurrentId(itemEl.id, e);
63654
+ }
63655
+ },
63656
+ onnavi_request_select: e => {
63657
+ const {
63658
+ id
63659
+ } = e.detail;
63660
+ if (id === undefined) {
63661
+ return;
63662
+ }
63663
+ const inputId = `${id}_input`;
63664
+ const childController = uiGroupStateController.findChildById(inputId);
63665
+ if (!childController) {
63666
+ return;
63667
+ }
63668
+ const list = ref.current;
63669
+ dispatchRequestInteraction(list, {
63670
+ event: e,
63671
+ name: "select",
63672
+ prevented: () => e.preventDefault(),
63673
+ // tell the requester that we don't want to select this item
63674
+ // Asked of the item too, not only of the list: an item can be the one
63675
+ // refusing (the list already holds all it accepts, see maxLength), and
63676
+ // it is the one that then says why.
63677
+ allowed: () => {
63678
+ dispatchRequestInteraction(childController.ref.current, {
63679
+ event: e,
63680
+ name: "select",
63681
+ prevented: () => e.preventDefault(),
63682
+ allowed: () => childController.setUIState(childController.value, e)
63683
+ });
63684
+ }
63685
+ });
63686
+ },
63687
+ onnavi_request_unselect: e => {
63688
+ const {
63689
+ id
63690
+ } = e.detail;
63691
+ if (id === undefined) {
63692
+ return;
63693
+ }
63694
+ const inputId = `${id}_input`;
63695
+ const childController = uiGroupStateController.findChildById(inputId);
63696
+ if (!childController) {
63697
+ return;
63698
+ }
63699
+ const list = ref.current;
63700
+ dispatchRequestInteraction(list, {
63701
+ event: e,
63702
+ name: "unselect",
63703
+ prevented: () => e.preventDefault(),
63704
+ // tell the requester that we don't want to unselect this item
63705
+ allowed: () => childController.setUIState(undefined, e)
63706
+ });
63525
63707
  }
63526
- const currentId = currentIdRef.current;
63527
- let currentIndex = -1;
63528
- if (currentId) {
63529
- currentIndex = navigableEls.findIndex(el => el.id === currentId);
63530
- }
63531
- let targetEl;
63532
- if (goal === "first") {
63533
- targetEl = navigableEls[0];
63534
- } else if (goal === "last") {
63535
- targetEl = navigableEls[navigableEls.length - 1];
63536
- } else if (goal === "next") {
63537
- if (currentIndex === -1) {
63708
+ // "previous"/"next", not "up"/"down": a list is a line of items whichever
63709
+ // way it is laid out, and a horizontal one walks sideways. The keys that
63710
+ // drive it (arrows, Home/End) map onto that here, and so do the commands
63711
+ // (--navi-previous / --navi-next / --navi-first / --navi-last).
63712
+ ,
63713
+ onnavi_request_nav: e => {
63714
+ const {
63715
+ goal
63716
+ } = e.detail;
63717
+ const navigableEls = getNavigableElements();
63718
+ if (navigableEls.length === 0) {
63719
+ return;
63720
+ }
63721
+ const currentId = currentIdRef.current;
63722
+ let currentIndex = -1;
63723
+ if (currentId) {
63724
+ currentIndex = navigableEls.findIndex(el => el.id === currentId);
63725
+ }
63726
+ let targetEl;
63727
+ if (goal === "first") {
63538
63728
  targetEl = navigableEls[0];
63539
- } else if (currentIndex < navigableEls.length - 1) {
63540
- targetEl = navigableEls[currentIndex + 1];
63541
- } else {
63729
+ } else if (goal === "last") {
63542
63730
  targetEl = navigableEls[navigableEls.length - 1];
63731
+ } else if (goal === "next") {
63732
+ if (currentIndex === -1) {
63733
+ targetEl = navigableEls[0];
63734
+ } else if (currentIndex < navigableEls.length - 1) {
63735
+ targetEl = navigableEls[currentIndex + 1];
63736
+ } else {
63737
+ targetEl = navigableEls[navigableEls.length - 1];
63738
+ }
63739
+ } else if (goal === "previous") {
63740
+ if (currentIndex === -1) {
63741
+ targetEl = navigableEls[0];
63742
+ } else if (currentIndex > 0) {
63743
+ targetEl = navigableEls[currentIndex - 1];
63744
+ } else {
63745
+ targetEl = navigableEls[0];
63746
+ }
63543
63747
  }
63544
- } else if (goal === "previous") {
63545
- if (currentIndex === -1) {
63546
- targetEl = navigableEls[0];
63547
- } else if (currentIndex > 0) {
63548
- targetEl = navigableEls[currentIndex - 1];
63549
- } else {
63550
- targetEl = navigableEls[0];
63748
+ if (!targetEl) {
63749
+ return;
63551
63750
  }
63552
- }
63553
- if (!targetEl) {
63554
- return;
63555
- }
63556
- setCurrentId(targetEl.id, e);
63557
- dispatchCustomEvent(ref.current, "navi_request_scroll", {
63558
- event: e,
63559
- id: targetEl.id
63560
- });
63561
- },
63562
- onnavi_request_activate: e => {
63563
- const currentId = currentIdRef.current;
63564
- if (!currentId) {
63565
- return;
63566
- }
63567
- if (multiple || deselectable) {
63568
- const inputId = `${currentId}_input`;
63569
- const childController = uiGroupStateController.findChildById(inputId);
63570
- const isSelected = childController && childController.uiState;
63571
- dispatchCustomEvent(ref.current, isSelected ? "navi_request_unselect" : "navi_request_select", {
63751
+ setCurrentId(targetEl.id, e);
63752
+ dispatchCustomEvent(ref.current, "navi_request_scroll", {
63753
+ event: e,
63754
+ id: targetEl.id
63755
+ });
63756
+ },
63757
+ onnavi_request_activate: e => {
63758
+ const currentId = currentIdRef.current;
63759
+ if (!currentId) {
63760
+ return;
63761
+ }
63762
+ if (multiple || deselectable) {
63763
+ const inputId = `${currentId}_input`;
63764
+ const childController = uiGroupStateController.findChildById(inputId);
63765
+ const isSelected = childController && childController.uiState;
63766
+ dispatchCustomEvent(ref.current, isSelected ? "navi_request_unselect" : "navi_request_select", {
63767
+ event: e,
63768
+ id: currentId
63769
+ });
63770
+ return;
63771
+ }
63772
+ dispatchCustomEvent(ref.current, "navi_request_select", {
63572
63773
  event: e,
63573
63774
  id: currentId
63574
63775
  });
63575
- return;
63576
- }
63577
- dispatchCustomEvent(ref.current, "navi_request_select", {
63578
- event: e,
63579
- id: currentId
63580
- });
63581
- },
63582
- children: jsx(ControlgroupChildrenWrapper, {
63583
- ...childrenWrapperProps,
63776
+ },
63584
63777
  children: props.children
63585
63778
  })
63586
63779
  });
@@ -65207,6 +65400,10 @@ const useListScrollSync = ({
65207
65400
  const block = blockRequested || (event.type === "navi_displayed" ? "center" : "nearest");
65208
65401
  const scrollToItemCall = `${getElementSignature(itemEl)}.scrollIntoView({ block: "${block}", container: "nearest" })`;
65209
65402
  debugScroll(`${trigger} -> ${scrollToItemCall}`);
65403
+ // The list is going somewhere on purpose, so there is no view to hold
65404
+ // still any more: an anchor captured before this drop it, or it would
65405
+ // put the list back where it was the moment the rows move under it.
65406
+ anchorRef.current = null;
65210
65407
  scrollIntoViewScoped(itemEl, {
65211
65408
  container: getScroller(),
65212
65409
  block
@@ -67255,6 +67452,28 @@ const createListVirtual = () => {
67255
67452
  } else {
67256
67453
  ownerIdsBySlot.set(slotId, [ownerId]);
67257
67454
  }
67455
+ warnIfEveryRowInOneSlot(slotId);
67456
+ };
67457
+ // Rows that all stand in the same slot keep the order they first mounted in:
67458
+ // the walk is over the children the list is given, and a component holding
67459
+ // them is one child however many rows it renders. Everything about a place
67460
+ // then stops following what the caller writes — a search reordering the rows
67461
+ // moves nothing. Said once, and only for the shape that can be nothing else:
67462
+ // the list's whole content is one child, and several rows came out of it.
67463
+ let everyRowInOneSlotWarned = false;
67464
+ const warnIfEveryRowInOneSlot = slotId => {
67465
+ if (everyRowInOneSlotWarned) {
67466
+ return;
67467
+ }
67468
+ const rootSlotIds = slotIdsByParent.get(null);
67469
+ if (!rootSlotIds || rootSlotIds.length !== 1 || rootSlotIds[0] !== slotId) {
67470
+ return;
67471
+ }
67472
+ if (ownerIdsBySlot.get(slotId).length < 2) {
67473
+ return;
67474
+ }
67475
+ everyRowInOneSlotWarned = true;
67476
+ console.warn(`List: every row stands in the same slot, so they keep the order they first rendered in — reordering them (a search, a sort) will not move them. The list's rows must be its own children: give it the rows (or a <List.Items>), not a component rendering them.`);
67258
67477
  };
67259
67478
  const removeFromSlot = (slotId, ownerId) => {
67260
67479
  const ownerIds = ownerIdsBySlot.get(slotId);
@@ -72238,6 +72457,26 @@ const css$r = /* css */`.navi_checkbox_group {
72238
72457
  `;
72239
72458
 
72240
72459
  /**
72460
+ * A set of checkboxes answering one question, holding the array of the values
72461
+ * that are checked.
72462
+ *
72463
+ * It renders a `<fieldset>` and hands its `name` down, so the boxes inside are
72464
+ * plain `<Input type="checkbox" value="…">` with nothing else to declare; each
72465
+ * value that is checked appears in the array, in the order the boxes are
72466
+ * written. What it holds is said the way every control says it — `value` /
72467
+ * `defaultValue` (an array), or a bound `signal` — and inside a `<Form>` the
72468
+ * group is one entry of the submitted params, an array.
72469
+ *
72470
+ * ```jsx
72471
+ * <CheckboxGroup name="permissions" defaultValue={["places.moderate"]} action={save}>
72472
+ * <legend>Permissions</legend>
72473
+ * <Field as="label"><Input type="checkbox" value="places.moderate" /> Modérer les lieux</Field>
72474
+ * <Field as="label"><Input type="checkbox" value="games.list" /> Lister les parties</Field>
72475
+ * </CheckboxGroup>
72476
+ * ```
72477
+ *
72478
+ * See `<RadioGroup>` for the same shape holding a single value.
72479
+ *
72241
72480
  * @type {import("ignore:preact").FunctionComponent<{
72242
72481
  * maxLength?: number,
72243
72482
  * maxLengthGuard?: number,
@@ -72959,6 +73198,27 @@ const css$p = /* css */`.navi_radio_group {
72959
73198
  }
72960
73199
  }
72961
73200
  `;
73201
+
73202
+ /**
73203
+ * A set of radios answering one question, holding the value of the one that is
73204
+ * checked.
73205
+ *
73206
+ * It renders a `<fieldset>` and hands its `name` down, so the radios inside are
73207
+ * plain `<Input type="radio" value="…">` with nothing else to declare — sharing
73208
+ * a name is what makes them exclusive. What it holds is said the way every
73209
+ * control says it — `value` / `defaultValue`, or a bound `signal` — and inside a
73210
+ * `<Form>` the group is one entry of the submitted params.
73211
+ *
73212
+ * ```jsx
73213
+ * <RadioGroup name="role" defaultValue="viewer" action={save}>
73214
+ * <legend>Rôle</legend>
73215
+ * <Field as="label"><Input type="radio" value="viewer" /> Lecteur</Field>
73216
+ * <Field as="label"><Input type="radio" value="admin" /> Administrateur</Field>
73217
+ * </RadioGroup>
73218
+ * ```
73219
+ *
73220
+ * See `<CheckboxGroup>` for the same shape holding an array.
73221
+ */
72962
73222
  const RadioGroup = props => {
72963
73223
  const refDefault = useRef(null);
72964
73224
  props.ref = props.ref || refDefault;