@jsenv/navi 0.29.426 → 0.29.427

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.
@@ -16238,9 +16238,9 @@ const armOutsidePressClose = (
16238
16238
  * `font-family` is deliberately absent: a surface keeps the face of what
16239
16239
  * opened it, so a section written in a display font gets its tooltips in that
16240
16240
  * font. `color`, `font-size` and `font-weight` are absent too — each surface
16241
- * answers those its own way (a popup writes in --navi-popup-color and follows
16242
- * the size of what opened it; a callout reverts to the document's ink and
16243
- * size).
16241
+ * answers those its own way (a popup and a callout write in
16242
+ * --navi-popup-color; a popup follows the size of what opened it, a callout
16243
+ * resets to the document's size).
16244
16244
  *
16245
16245
  * In `@layer navi` so an app that wants one of these back says so on the
16246
16246
  * surface and wins, without having to out-specify anything.
@@ -16470,8 +16470,7 @@ const css$14 = /* css */ `
16470
16470
  --callout-error-color: var(--navi-callout-error-color);
16471
16471
  --callout-neutral-color: var(--navi-callout-neutral-color);
16472
16472
 
16473
- --callout-background-color: white;
16474
- --callout-icon-color: black;
16473
+ --callout-background-color: var(--navi-popup-background-color);
16475
16474
  --callout-padding: 8px;
16476
16475
  --callout-z-index: var(--navi-z-index-callout);
16477
16476
  /* The page's line, said here rather than inherited: the icon and the
@@ -16525,11 +16524,11 @@ const css$14 = /* css */ `
16525
16524
  height: auto; /* User agent reset */
16526
16525
  margin: 0;
16527
16526
  padding: 0; /* User agent reset */
16528
- /* The UA's ink for a [popover] element (CanvasText), not the element's:
16529
- a callout sits inside the element but writes on its own paper. The
16530
- color keywords are re-declared against that same ink in
16531
- navi_css_vars.js (.navi_callout). */
16532
- color: revert;
16527
+ /* A callout sits inside the element but writes on its own paper: the
16528
+ popup's ink, paired with the popup's paper it is drawn on, so the two
16529
+ follow color-scheme and the theme together. The color keywords are
16530
+ re-declared against that same ink in navi_css_vars.js. */
16531
+ color: var(--navi-popup-color);
16533
16532
  font-weight: initial; /* Callout fells disconnected from the element, font weight should be predictible and stable */
16534
16533
  font-size: initial; /* Callout fells disconnected from the element, font size should be predictible and stable */
16535
16534
  line-height: var(
@@ -18752,16 +18751,18 @@ const findBusySource = (field) => {
18752
18751
  // a new interaction is queued behind the run (see the action queue in
18753
18752
  // control_hooks.jsx) rather than refused.
18754
18753
  if (!field.optimistic && boundAction) {
18755
- // The INSTANCE the proxy resolves to right now, not the proxy's own
18756
- // signal: that one is a MIRROR, synced by an effect the settling batch
18757
- // defers — read mid-batch (a state echo carrying the user's event back
18758
- // down, an automatic follow-up), it still says RUNNING for an action
18759
- // that is already over, and the gate would refuse — callout included —
18760
- // for nothing. The resolved instance is the live truth: at that echo it
18761
- // is the instance that just settled, already COMPLETED. And it IS the
18762
- // running one whenever one runs — a non-optimistic control's state
18763
- // cannot move mid-run, this very gate blocks it.
18764
- const liveAction = boundAction.getCurrentAction?.() ?? boundAction;
18754
+ // The instance the run in flight uses, as the render reads it (see
18755
+ // runningActionSignal in control_hooks.jsx): the state a proxy follows can
18756
+ // move mid-run — a popup content unmounting takes the group's value — and
18757
+ // the proxy would resolve to an idle instance. With no run out, the
18758
+ // instance the proxy resolves to right now, never the proxy's own signal:
18759
+ // a MIRROR synced by an effect the settling batch defers, which still says
18760
+ // RUNNING mid-batch (a state echo, an automatic follow-up) for an action
18761
+ // already over. An instance is COMPLETED the moment it settles.
18762
+ const liveAction =
18763
+ field.runningActionSignal.value ??
18764
+ boundAction.getCurrentAction?.() ??
18765
+ boundAction;
18765
18766
  if (liveAction.runningStateSignal.value === RUNNING) {
18766
18767
  return { field, action: liveAction };
18767
18768
  }
@@ -19799,11 +19800,10 @@ const READONLY_CONSTRAINT = {
19799
19800
  name: "readonly",
19800
19801
  messageAttribute: "data-readonly-message",
19801
19802
  check: (field, { intent } = {}) => {
19802
- const readOnly = Boolean(
19803
- field.controlHostProps.readOnly ||
19804
- field.controlHostProps["aria-readonly"] === "true",
19805
- );
19806
- if (!readOnly) {
19803
+ // Not off the rendered `readOnly`/`aria-readonly`: they also carry the run
19804
+ // of the control's own action, a frame behind (see readOnlyWithoutRun in
19805
+ // control_hooks.jsx). That run is BUSY_CONSTRAINT's, which reads it live.
19806
+ if (!field.readOnlyWithoutRun) {
19807
19807
  return null;
19808
19808
  }
19809
19809
 
@@ -42988,6 +42988,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
42988
42988
  ref: realUIStateController.ref,
42989
42989
  uiStateSignal: realUIStateController.uiStateSignal,
42990
42990
  controlHostProps: realUIStateController.controlHostProps,
42991
+ readOnlyWithoutRun: realUIStateController.readOnlyWithoutRun,
42991
42992
  registerChild: (child) => {
42992
42993
  if (!canRegisterAsFacadeChild(child)) {
42993
42994
  return;
@@ -43116,6 +43117,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
43116
43117
  s.controller.ref = realUIStateController.ref;
43117
43118
  s.controller.uiStateSignal = realUIStateController.uiStateSignal;
43118
43119
  s.controller.controlHostProps = realUIStateController.controlHostProps;
43120
+ s.controller.readOnlyWithoutRun =
43121
+ realUIStateController.readOnlyWithoutRun;
43119
43122
  realUIStateController.pushStateDownToFacadeChild =
43120
43123
  s.controller.pushStateDownToChild;
43121
43124
 
@@ -44922,6 +44925,11 @@ const useInteractiveProps = (props, {
44922
44925
  const actionLoading = optimistic ? false : actionRunning;
44923
44926
  const loadingResolved = loadingBase || actionLoading;
44924
44927
  const readOnlyResolved = readOnlyBase || actionLoading;
44928
+ // What READONLY_CONSTRAINT refuses for. The run of the control's own action
44929
+ // is BUSY_CONSTRAINT's, read live: readOnlyResolved is a render snapshot,
44930
+ // still read-only in the tick the run settles — the tick a form's
44931
+ // after-send `--navi-reset` asks the form to empty itself.
44932
+ uiStateController.readOnlyWithoutRun = Boolean(readOnlyBase);
44925
44933
  // Read-only, and what this control opens still opens: reading what is in
44926
44934
  // there changes nothing. Read by READONLY_CONSTRAINT, which lets an
44927
44935
  // interaction that only reads through on it.
@@ -45374,10 +45382,10 @@ const useInteractiveProps = (props, {
45374
45382
  });
45375
45383
  }
45376
45384
  // controlHostProps is a curated subset of props with resolved values applied
45377
- // (e.g. readOnly resolved from context + action loading). The interaction system
45378
- // reads off uiStateController.controlHostProps at runtime (e.g. READONLY_CONSTRAINT
45379
- // checks controlHostProps.readOnly), so pointing the controller at controlHostProps
45380
- // keeps those reads current without any extra bookkeeping.
45385
+ // (e.g. readOnly resolved from context + action loading). The constraints
45386
+ // read off uiStateController.controlHostProps at runtime (e.g. DISABLED_CONSTRAINT,
45387
+ // the readonly reason), so pointing the controller at controlHostProps keeps
45388
+ // those reads current without any extra bookkeeping.
45381
45389
  const firstRender = uiStateController.controlHostProps === undefined;
45382
45390
  uiStateController.controlHostProps = controlHostProps;
45383
45391
  // The action itself, not just what the last render made of it: its running
@@ -54421,14 +54429,34 @@ const ButtonConfirm = ({
54421
54429
  }
54422
54430
  return jsxs("span", {
54423
54431
  className: "navi_button_confirm",
54424
- ref: askingRef,
54432
+ ref: askingRef
54433
+ // A place for the focus, so a press anywhere in the question keeps it
54434
+ // inside: WebKit does not focus a pressed button, and a press on
54435
+ // "Confirmer" would otherwise blur toward <body> — a focusout with no
54436
+ // relatedTarget, read below as the person leaving. The callout answers
54437
+ // the same ambiguity the same way.
54438
+ ,
54439
+ tabIndex: -1
54440
+ // "not me, unless you have nothing else": the ladder in the effect above
54441
+ // walks past the question to what it holds.
54442
+ // eslint-disable-next-line react/no-unknown-property
54443
+ ,
54444
+ "navi-autofocus": "last-resort",
54425
54445
  onKeyDown: e => {
54426
54446
  if (e.key === "Escape") {
54427
54447
  cancel(true);
54428
54448
  }
54429
54449
  },
54430
54450
  onFocusOut: e => {
54431
- if (!askingRef.current.contains(e.relatedTarget)) {
54451
+ // Chrome also fires this from the question's own removal, once preact
54452
+ // has cleared the ref: nobody left, and cancel(false) would undo the
54453
+ // focus the answer gives back. That focus cannot go back before the
54454
+ // removal: the button it lands on is mounted by the same commit.
54455
+ const askingEl = askingRef.current;
54456
+ if (!askingEl) {
54457
+ return;
54458
+ }
54459
+ if (!askingEl.contains(e.relatedTarget)) {
54432
54460
  cancel(false);
54433
54461
  }
54434
54462
  },
@@ -71864,7 +71892,7 @@ const css$x = /* css */`@property --list-group-label-height {
71864
71892
  }
71865
71893
  }
71866
71894
 
71867
- .navi_list[navi-box-flow="grid"] > .navi_list_virtual_filler, .navi_list[navi-box-flow="grid"] > .navi_separator {
71895
+ .navi_list[navi-box-flow="grid"] > .navi_list_virtual_filler, .navi_list[navi-box-flow="grid"] > .navi_list_failed_items, .navi_list[navi-box-flow="grid"] > .navi_separator {
71868
71896
  grid-column: 1 / -1;
71869
71897
  }
71870
71898
 
@@ -72241,7 +72269,8 @@ const ListUI = props => {
72241
72269
  listId: rest.id,
72242
72270
  scroller,
72243
72271
  searchText,
72244
- horizontal
72272
+ horizontal,
72273
+ columns
72245
72274
  });
72246
72275
 
72247
72276
  // renderBudget frames the items of a run; a list whose items are all declared
@@ -72556,7 +72585,8 @@ const useListScrollSync = ({
72556
72585
  listId,
72557
72586
  scroller,
72558
72587
  searchText,
72559
- horizontal
72588
+ horizontal,
72589
+ columns
72560
72590
  }) => {
72561
72591
  const debugScroll = useDebugScroll();
72562
72592
  // The items drawn, [start, end) among the list's own. A ref as well as a
@@ -72571,6 +72601,7 @@ const useListScrollSync = ({
72571
72601
  return renderWindow.start > 0 || renderWindow.end < listItems.totalSignal.peek();
72572
72602
  };
72573
72603
  const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
72604
+ inLines: Boolean(columns),
72574
72605
  windowLeavesItemsOut,
72575
72606
  scrolledWanted: scrolled ?? defaultScrolled,
72576
72607
  // The fillers hold the room of items above the screen at this size: a
@@ -72639,6 +72670,34 @@ const useListScrollSync = ({
72639
72670
  });
72640
72671
  };
72641
72672
 
72673
+ // How many items share a line: the tracks a grid of items (`columns`)
72674
+ // resolves to, one otherwise. A layout result under auto-fill, so it is read
72675
+ // off the list once laid out, at each commit and each resize — for the runs
72676
+ // alone, which draw in lines: items declared one by one are all drawn. A
72677
+ // line of another length rewraps every item drawn: the item at the top of
72678
+ // the view is held where it is across it.
72679
+ const itemsPerLineRef = useRef(1);
72680
+ const [itemsPerLine, setItemsPerLine] = useState(1);
72681
+ const updateItemsPerLine = () => {
72682
+ if (!ref.current) {
72683
+ return;
72684
+ }
72685
+ const itemsPerLineNow = columns && listItems.hasRuns() ? readItemsPerLine(getListEl()) : 1;
72686
+ if (itemsPerLineNow === null || itemsPerLineNow === itemsPerLineRef.current) {
72687
+ return;
72688
+ }
72689
+ captureAnchor();
72690
+ itemsPerLineRef.current = itemsPerLineNow;
72691
+ setItemsPerLine(itemsPerLineNow);
72692
+ };
72693
+ const updateItemsPerLineRef = useRef(null);
72694
+ updateItemsPerLineRef.current = updateItemsPerLine;
72695
+ useLayoutEffect(() => {
72696
+ if (columns || itemsPerLineRef.current !== 1) {
72697
+ updateItemsPerLine();
72698
+ }
72699
+ });
72700
+
72642
72701
  // What the window holds goes through three stages (see settleWindow):
72643
72702
  // - "first": the first commit, a count of items since nothing is laid out
72644
72703
  // yet to measure a size with — `initial` or the budget when they are one,
@@ -73037,6 +73096,7 @@ const useListScrollSync = ({
73037
73096
  listEl: getListEl(),
73038
73097
  listItems,
73039
73098
  virtualItemSizeSignal,
73099
+ itemsPerLine: itemsPerLineRef.current,
73040
73100
  renderWindowRef,
73041
73101
  horizontal
73042
73102
  });
@@ -73377,20 +73437,14 @@ const useListScrollSync = ({
73377
73437
  }
73378
73438
  const scrollerEl = getScroller();
73379
73439
  const listEl = getListEl();
73380
- const observer = new ResizeObserver(entries => {
73381
- // A box that grows may be a box that starts to scroll — the scroller is
73382
- // resolved again before anything is done about the resize, so that a
73383
- // list which has outgrown a bounded ancestor stops holding on to the
73384
- // page.
73385
- resolveScroller();
73386
- rememberPosition();
73387
- // Two things resize here, and they call for opposite answers. The LIST
73388
- // growing is its own content settling: only a list holding itself
73389
- // somewhere cares (the end it aims at has moved), and a list the user is
73390
- // reading must not be touched — its items are held still by the anchoring,
73391
- // which this would undo. The SCROLLER resizing is the window around it
73392
- // changing shape, and then the item that was at the top goes back where it
73393
- // was.
73440
+ // Two things resize here, and they call for opposite answers. The LIST
73441
+ // growing is its own content settling: only a list holding itself
73442
+ // somewhere cares (the end it aims at has moved), and a list the user is
73443
+ // reading must not be touched — its items are held still by the anchoring,
73444
+ // which this would undo. The SCROLLER resizing is the window around it
73445
+ // changing shape, and then the item that was at the top goes back where it
73446
+ // was.
73447
+ const putTopItemBack = entries => {
73394
73448
  const listResized = entries.some(entry => entry.target === listEl);
73395
73449
  if (listResized && onGeometryChangeRef.current()) {
73396
73450
  return;
@@ -73419,6 +73473,19 @@ const useListScrollSync = ({
73419
73473
  } else {
73420
73474
  scrollerEl.scrollTop += delta;
73421
73475
  }
73476
+ };
73477
+ const observer = new ResizeObserver(entries => {
73478
+ // A box that grows may be a box that starts to scroll — the scroller is
73479
+ // resolved again before anything is done about the resize, so that a
73480
+ // list which has outgrown a bounded ancestor stops holding on to the
73481
+ // page.
73482
+ resolveScroller();
73483
+ rememberPosition();
73484
+ putTopItemBack(entries);
73485
+ // Once the top item is back where it was: that is the place a line of
73486
+ // another length has to keep, and putting it back drops any anchor
73487
+ // captured before.
73488
+ updateItemsPerLineRef.current();
73422
73489
  });
73423
73490
  observer.observe(scrollerEl);
73424
73491
  observer.observe(listEl);
@@ -73574,7 +73641,10 @@ const useListScrollSync = ({
73574
73641
  start,
73575
73642
  end
73576
73643
  } = renderWindowRef.current;
73577
- const itemSize = virtualItemSizeSignal.peek();
73644
+ // What an item not drawn weighs: its share of a line, which the items side
73645
+ // by side on it take together.
73646
+ const itemsPerLineNow = itemsPerLineRef.current;
73647
+ const itemSize = virtualItemSizeSignal.peek() / itemsPerLineNow;
73578
73648
  const pixelsOf = createItemSizeReader(items, start, end, itemSize);
73579
73649
  const walkBefore = (from, sizeWanted, sizeOf) => {
73580
73650
  let index = from;
@@ -73844,9 +73914,28 @@ const useListScrollSync = ({
73844
73914
  ifChanged: true
73845
73915
  });
73846
73916
  }, [windowStart, windowEnd]);
73917
+ // What the runs draw: the window widened to whole lines. The window itself
73918
+ // moves an item at a time, and a grid lays the first item drawn in its first
73919
+ // column — started anywhere but on a line, every item drawn would sit in the
73920
+ // column of another. Started on one, an item is in the column its place says
73921
+ // whatever the window frames. The same object while the numbers hold, so the
73922
+ // runs are not told about a window that did not move.
73923
+ const windowDrawnRef = useRef(null);
73924
+ const drawnStart = windowStart - windowStart % itemsPerLine;
73925
+ const endPastLine = windowEnd % itemsPerLine;
73926
+ const drawnEnd = endPastLine === 0 ? windowEnd : windowEnd + itemsPerLine - endPastLine;
73927
+ let windowDrawn = windowDrawnRef.current;
73928
+ if (!windowDrawn || windowDrawn.start !== drawnStart || windowDrawn.end !== drawnEnd || windowDrawn.itemsPerLine !== itemsPerLine) {
73929
+ windowDrawn = {
73930
+ start: drawnStart,
73931
+ end: drawnEnd,
73932
+ itemsPerLine
73933
+ };
73934
+ windowDrawnRef.current = windowDrawn;
73935
+ }
73847
73936
  return {
73848
73937
  virtualItemSizeSignal,
73849
- renderWindow: renderWindowRef.current,
73938
+ renderWindow: windowDrawn,
73850
73939
  pendingScrollRef,
73851
73940
  scrollToItem,
73852
73941
  captureAnchor
@@ -73870,6 +73959,21 @@ const getScrollerViewportRect = scrollerEl => {
73870
73959
  }
73871
73960
  return scrollerEl.getBoundingClientRect();
73872
73961
  };
73962
+ // The tracks a grid resolved its template to, as its computed
73963
+ // grid-template-columns lists them once laid out: "150px 150px [end] 150px".
73964
+ // Not laid out (a closed popup), that is the template as written, which says
73965
+ // nothing yet under auto-fill: `null`.
73966
+ const readItemsPerLine = listEl => {
73967
+ const tracks = getComputedStyle(listEl).gridTemplateColumns;
73968
+ if (tracks === "none" || tracks.includes("(")) {
73969
+ return null;
73970
+ }
73971
+ const sizes = tracks.replace(/\[[^\]]*\]/g, " ").trim();
73972
+ if (!sizes) {
73973
+ return null;
73974
+ }
73975
+ return sizes.split(/\s+/).length;
73976
+ };
73873
73977
  // A window that cannot move is the one failure of a virtualized list that
73874
73978
  // looks like nothing: the items outside it are fillers holding their room, so
73875
73979
  // the list simply ends on blank space, at exactly the height of the items never
@@ -74672,6 +74776,7 @@ const getScrollInfo = ({
74672
74776
  listEl,
74673
74777
  listItems,
74674
74778
  virtualItemSizeSignal,
74779
+ itemsPerLine,
74675
74780
  renderWindowRef,
74676
74781
  horizontal
74677
74782
  }) => {
@@ -74709,7 +74814,9 @@ const getScrollInfo = ({
74709
74814
  // mean we don't know the real on-screen index, only the scroll position,
74710
74815
  // so estimate from it rather than assume nothing changed.
74711
74816
  const estimateFromScrollPos = reasonPrefix => {
74712
- const virtualItemSize = virtualItemSizeSignal.peek();
74817
+ // An item's share of its line: the items side by side on it take it
74818
+ // together.
74819
+ const virtualItemSize = virtualItemSizeSignal.peek() / itemsPerLine;
74713
74820
  if (virtualItemSize === 0) {
74714
74821
  return null;
74715
74822
  }
@@ -74765,8 +74872,10 @@ const getScrollInfo = ({
74765
74872
  const VIRTUAL_ITEM_SIZE_EPSILON = 0.5;
74766
74873
  // Measures the items currently in the DOM, edge to edge: what a filler stands in
74767
74874
  // for is the room a run of items takes together — separators and group labels
74768
- // included — not the height of one <li>.
74769
- const measureItemSize = (listEl, horizontal) => {
74875
+ // included — not the height of one <li>. The size of a line of them: one item
74876
+ // in a list, the items side by side in a grid (`inLines`), counted where a new
74877
+ // line starts.
74878
+ const measureItemSize = (listEl, horizontal, inLines) => {
74770
74879
  let fromSkeletons = false;
74771
74880
  let itemEls = listEl.querySelectorAll(REAL_LIST_ITEM_SELECTOR);
74772
74881
  if (itemEls.length === 0) {
@@ -74788,13 +74897,27 @@ const measureItemSize = (listEl, horizontal) => {
74788
74897
  if (span <= 0) {
74789
74898
  return null;
74790
74899
  }
74900
+ let lineCount = itemEls.length;
74901
+ if (inLines) {
74902
+ lineCount = 0;
74903
+ let lineFrom = -Infinity;
74904
+ for (const itemEl of itemEls) {
74905
+ const rect = itemEl.getBoundingClientRect();
74906
+ const from = horizontal ? rect.left : rect.top;
74907
+ if (from > lineFrom + 0.5) {
74908
+ lineCount++;
74909
+ lineFrom = from;
74910
+ }
74911
+ }
74912
+ }
74791
74913
  return {
74792
- size: span / itemEls.length,
74793
- itemCount: itemEls.length,
74914
+ size: span / lineCount,
74915
+ lineCount,
74794
74916
  fromSkeletons
74795
74917
  };
74796
74918
  };
74797
74919
  const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
74920
+ inLines,
74798
74921
  windowLeavesItemsOut,
74799
74922
  scrolledWanted,
74800
74923
  beforeSizeChange
@@ -74838,8 +74961,8 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
74838
74961
  return;
74839
74962
  }
74840
74963
  }
74841
- samples.sum += measure.size * measure.itemCount;
74842
- samples.count += measure.itemCount;
74964
+ samples.sum += measure.size * measure.lineCount;
74965
+ samples.count += measure.lineCount;
74843
74966
  const next = samples.sum / samples.count;
74844
74967
  const current = virtualSizeSignal.peek();
74845
74968
  if (Math.abs(next - current) > VIRTUAL_ITEM_SIZE_EPSILON) {
@@ -74859,7 +74982,7 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
74859
74982
  const itemsHeldOffScreen = windowLeavesItemsOut();
74860
74983
  if (!virtualItemSizeProp && sizeAlreadyKnown && itemsHeldOffScreen && ref.current) {
74861
74984
  const listEl = ref.current.querySelector(".navi_list");
74862
- const measure = listEl ? measureItemSize(listEl, horizontal) : null;
74985
+ const measure = listEl ? measureItemSize(listEl, horizontal, inLines) : null;
74863
74986
  if (measure) {
74864
74987
  feedSample(measure);
74865
74988
  }
@@ -74880,11 +75003,11 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
74880
75003
  if (!listEl) {
74881
75004
  return undefined;
74882
75005
  }
74883
- const measure = measureItemSize(listEl, horizontal);
75006
+ const measure = measureItemSize(listEl, horizontal, inLines);
74884
75007
  if (measure) {
74885
75008
  const samples = samplesRef.current;
74886
- samples.sum = measure.size * measure.itemCount;
74887
- samples.count = measure.itemCount;
75009
+ samples.sum = measure.size * measure.lineCount;
75010
+ samples.count = measure.lineCount;
74888
75011
  samples.fromSkeletons = measure.fromSkeletons;
74889
75012
  virtualSizeSignal.value = measure.size;
74890
75013
  return undefined;
@@ -75029,14 +75152,17 @@ const Fallback = ({
75029
75152
  // Reads the item size itself: it is what the size is for, and a run holding
75030
75153
  // every item it draws must not be redrawn — every item of it — because the size
75031
75154
  // settled after the first commit.
75155
+ // It holds lines: the items side by side on one take its room together, and a
75156
+ // last line left short takes it all the same.
75032
75157
  const VirtualFiller = ({
75033
75158
  edge,
75034
75159
  itemCount,
75160
+ itemsPerLine,
75035
75161
  findChunks
75036
75162
  }) => {
75037
75163
  const listItems = useContext(ListItemsContext);
75038
- const virtualItemSize = listItems.virtualItemSizeSignal.value;
75039
- const sizeToFill = itemCount * virtualItemSize;
75164
+ const lineSize = listItems.virtualItemSizeSignal.value;
75165
+ const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize;
75040
75166
  // A filler resizing moves what stands below it — the items on screen, when it
75041
75167
  // holds the room of items above them — and it resizes in a commit of its own
75042
75168
  // when the item size settles after the list has rendered: the list puts its
@@ -75055,7 +75181,7 @@ const VirtualFiller = ({
75055
75181
  "aria-hidden": true,
75056
75182
  style: {
75057
75183
  "--size-to-fill": `${sizeToFill}px`,
75058
- "--x-find-line-size": findChunks ? `${virtualItemSize}px` : undefined
75184
+ "--x-find-line-size": findChunks ? `${lineSize / itemsPerLine}px` : undefined
75059
75185
  },
75060
75186
  children: findChunks && findChunks.map(chunk => jsx("div", {
75061
75187
  className: "navi_list_find_stand_in",
@@ -75935,6 +76061,9 @@ const useRunItems = (store, {
75935
76061
  const rankOf = itemIndex => itemIndex - runStart;
75936
76062
  const indexOfRank = rank => rank + runStart;
75937
76063
  const getItemAt = itemIndex => store.getItem(rankOf(itemIndex));
76064
+ const {
76065
+ itemsPerLine
76066
+ } = renderWindow;
75938
76067
  const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;
75939
76068
  const windowTo = renderWindow.end < runEnd ? renderWindow.end : runEnd;
75940
76069
  store.forget(rankOf(windowFrom), rankOf(windowTo));
@@ -76042,8 +76171,20 @@ const useRunItems = (store, {
76042
76171
  // band is drawn among the items. A negative start is not a rank but a count
76043
76172
  // back from the end (the very first ask, before the count is known) — there
76044
76173
  // is no item to convert it to, and the band falls back to the window.
76045
- const failureFrom = store.failure === null ? -1 : store.failure.start < 0 || indexOfRank(store.failure.start) < windowFrom ? windowFrom : indexOfRank(store.failure.start);
76046
- const failureTo = store.failure === null ? -1 : store.failure.end < 0 || indexOfRank(store.failure.end) > windowTo - 1 ? windowTo - 1 : indexOfRank(store.failure.end);
76174
+ let failureFrom = store.failure === null ? -1 : store.failure.start < 0 || indexOfRank(store.failure.start) < windowFrom ? windowFrom : indexOfRank(store.failure.start);
76175
+ let failureTo = store.failure === null ? -1 : store.failure.end < 0 || indexOfRank(store.failure.end) > windowTo - 1 ? windowTo - 1 : indexOfRank(store.failure.end);
76176
+ if (store.failure !== null && itemsPerLine > 1) {
76177
+ // In a grid the band takes whole lines, like the window: begun mid-line,
76178
+ // it would push every item after it into another column.
76179
+ failureFrom -= failureFrom % itemsPerLine;
76180
+ if (failureFrom < windowFrom) {
76181
+ failureFrom = windowFrom;
76182
+ }
76183
+ failureTo += itemsPerLine - 1 - failureTo % itemsPerLine;
76184
+ if (failureTo > windowTo - 1) {
76185
+ failureTo = windowTo - 1;
76186
+ }
76187
+ }
76047
76188
  const nodes = [];
76048
76189
  // Items that belong together, as the data says (a day of messages, a month of
76049
76190
  // games): consecutive items sharing a group key are wrapped in one group, so
@@ -76120,6 +76261,7 @@ const useRunItems = (store, {
76120
76261
  nodes.push(jsx(VirtualFiller, {
76121
76262
  edge: "before",
76122
76263
  itemCount: windowFrom - runStart,
76264
+ itemsPerLine: itemsPerLine,
76123
76265
  findChunks: getFindChunks(runStart, windowFrom)
76124
76266
  }, "navi-list-filler-before"));
76125
76267
  }
@@ -76134,7 +76276,7 @@ const useRunItems = (store, {
76134
76276
  nodes.push(jsx("li", {
76135
76277
  className: "navi_list_failed_items",
76136
76278
  style: {
76137
- "--size-to-fill": `${failedItemCount * listItems.virtualItemSizeSignal.value}px`
76279
+ "--size-to-fill": `${Math.ceil(failedItemCount / itemsPerLine) * listItems.virtualItemSizeSignal.value}px`
76138
76280
  },
76139
76281
  children: renderError ? renderError({
76140
76282
  error: store.failure.error,
@@ -76224,6 +76366,7 @@ const useRunItems = (store, {
76224
76366
  nodes.push(jsx(VirtualFiller, {
76225
76367
  edge: "after",
76226
76368
  itemCount: runEnd - windowTo,
76369
+ itemsPerLine: itemsPerLine,
76227
76370
  findChunks: getFindChunks(windowTo, runEnd)
76228
76371
  }, "navi-list-filler-after"));
76229
76372
  }
@@ -76947,12 +77090,16 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
76947
77090
  * @param {string} [props.columns]
76948
77091
  * The list's own columns: a `grid-template-columns` value the ITEMS are laid
76949
77092
  * into — a sheet of icons (`repeat(auto-fill, minmax(2.5rem, 1fr))`), a row of
76950
- * choices (`repeat(3, minmax(0, 1fr))`). Each item takes one cell, and an item
76951
- * meant to take a whole line says so for itself
76952
- * (`style={{ gridColumn: "1 / -1" }}`). Several items to a line is a shape the
76953
- * list cannot virtualize — its render window and the room it holds for the
76954
- * items it does not draw both count one item per line — so it is for a set of
76955
- * items the caller renders whole, not for a `<List.Items>` collection.
77093
+ * choices (`repeat(3, minmax(0, 1fr))`), a feed of covers
77094
+ * (`repeat(auto-fill, minmax(150px, 1fr))` over a `<List.Items>`). Each item
77095
+ * takes one cell, and an item meant to take a whole line says so for itself
77096
+ * (`style={{ gridColumn: "1 / -1" }}`). A `<List.Items>` run is drawn in whole
77097
+ * lines — as many items to a line as the template resolves to, read again
77098
+ * when the list's width changes: its window starts and ends on a line, so an
77099
+ * item is always in the column its place says, and the fillers hold the room
77100
+ * of the lines it does not draw (`virtualItemSize` is then a line's size).
77101
+ * That count holds only while every item of the run takes one cell: an item
77102
+ * spanning a line, or a `groupBy`, puts the ones after it in other columns.
76956
77103
  * @param {string} [props.itemColumns]
76957
77104
  * The columns inside an ITEM: a `grid-template-columns` value each item fills
76958
77105
  * with its own children, a table whose cells line up down the list. Every item
@@ -76968,8 +77115,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
76968
77115
  * scroll box, which fills it and has nothing to arrange.
76969
77116
  * @param {boolean} [props.flexWrap]
76970
77117
  * Lets a horizontal list's items fall to the next line instead of running
76971
- * past the edge — a row of choices under a `maxWidth`, say. Same caveat as
76972
- * `columns` above: a line holding several items is not virtualizable.
77118
+ * past the edge — a row of choices under a `maxWidth`, say. A wrapped line
77119
+ * holding several items is not virtualizable: a long collection laid several
77120
+ * to a line goes in `columns`.
76973
77121
  * @param {string} [props.overflow]
76974
77122
  * `"visible"` lets the items paint outside the list — a check in an item's
76975
77123
  * corner, a badge crossing the edge. A list clips by default, which is what
@@ -77088,7 +77236,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
77088
77236
  * one-line size) — an item guessed too small builds one more, an item
77089
77237
  * guessed too big leaves a blank (see docs/scroll.md, "What the list knows,
77090
77238
  * and what it guesses"). The items drawn are measured either way: the render
77091
- * window is sized on them.
77239
+ * window is sized on them. In a grid (`columns`), the size is a line's: a
77240
+ * card's, which the items side by side on it share.
77092
77241
  * @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
77093
77242
  * Which box scrolls — and with it, which box the render window follows and
77094
77243
  * which box a scroll position is read from (`onScrolledChange`). `"self"`