@jsenv/navi 0.29.426 → 0.29.428

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.
@@ -16881,9 +16881,9 @@ const armOutsidePressClose = (
16881
16881
  * `font-family` is deliberately absent: a surface keeps the face of what
16882
16882
  * opened it, so a section written in a display font gets its tooltips in that
16883
16883
  * font. `color`, `font-size` and `font-weight` are absent too — each surface
16884
- * answers those its own way (a popup writes in --navi-popup-color and follows
16885
- * the size of what opened it; a callout reverts to the document's ink and
16886
- * size).
16884
+ * answers those its own way (a popup and a callout write in
16885
+ * --navi-popup-color; a popup follows the size of what opened it, a callout
16886
+ * resets to the document's size).
16887
16887
  *
16888
16888
  * In `@layer navi` so an app that wants one of these back says so on the
16889
16889
  * surface and wins, without having to out-specify anything.
@@ -17113,8 +17113,7 @@ const css$14 = /* css */ `
17113
17113
  --callout-error-color: var(--navi-callout-error-color);
17114
17114
  --callout-neutral-color: var(--navi-callout-neutral-color);
17115
17115
 
17116
- --callout-background-color: white;
17117
- --callout-icon-color: black;
17116
+ --callout-background-color: var(--navi-popup-background-color);
17118
17117
  --callout-padding: 8px;
17119
17118
  --callout-z-index: var(--navi-z-index-callout);
17120
17119
  /* The page's line, said here rather than inherited: the icon and the
@@ -17168,11 +17167,11 @@ const css$14 = /* css */ `
17168
17167
  height: auto; /* User agent reset */
17169
17168
  margin: 0;
17170
17169
  padding: 0; /* User agent reset */
17171
- /* The UA's ink for a [popover] element (CanvasText), not the element's:
17172
- a callout sits inside the element but writes on its own paper. The
17173
- color keywords are re-declared against that same ink in
17174
- navi_css_vars.js (.navi_callout). */
17175
- color: revert;
17170
+ /* A callout sits inside the element but writes on its own paper: the
17171
+ popup's ink, paired with the popup's paper it is drawn on, so the two
17172
+ follow color-scheme and the theme together. The color keywords are
17173
+ re-declared against that same ink in navi_css_vars.js. */
17174
+ color: var(--navi-popup-color);
17176
17175
  font-weight: initial; /* Callout fells disconnected from the element, font weight should be predictible and stable */
17177
17176
  font-size: initial; /* Callout fells disconnected from the element, font size should be predictible and stable */
17178
17177
  line-height: var(
@@ -19484,16 +19483,18 @@ const findBusySource = (field) => {
19484
19483
  // a new interaction is queued behind the run (see the action queue in
19485
19484
  // control_hooks.jsx) rather than refused.
19486
19485
  if (!field.optimistic && boundAction) {
19487
- // The INSTANCE the proxy resolves to right now, not the proxy's own
19488
- // signal: that one is a MIRROR, synced by an effect the settling batch
19489
- // defers — read mid-batch (a state echo carrying the user's event back
19490
- // down, an automatic follow-up), it still says RUNNING for an action
19491
- // that is already over, and the gate would refuse — callout included —
19492
- // for nothing. The resolved instance is the live truth: at that echo it
19493
- // is the instance that just settled, already COMPLETED. And it IS the
19494
- // running one whenever one runs — a non-optimistic control's state
19495
- // cannot move mid-run, this very gate blocks it.
19496
- const liveAction = boundAction.getCurrentAction?.() ?? boundAction;
19486
+ // The instance the run in flight uses, as the render reads it (see
19487
+ // runningActionSignal in control_hooks.jsx): the state a proxy follows can
19488
+ // move mid-run — a popup content unmounting takes the group's value — and
19489
+ // the proxy would resolve to an idle instance. With no run out, the
19490
+ // instance the proxy resolves to right now, never the proxy's own signal:
19491
+ // a MIRROR synced by an effect the settling batch defers, which still says
19492
+ // RUNNING mid-batch (a state echo, an automatic follow-up) for an action
19493
+ // already over. An instance is COMPLETED the moment it settles.
19494
+ const liveAction =
19495
+ field.runningActionSignal.value ??
19496
+ boundAction.getCurrentAction?.() ??
19497
+ boundAction;
19497
19498
  if (liveAction.runningStateSignal.value === RUNNING) {
19498
19499
  return { field, action: liveAction };
19499
19500
  }
@@ -20531,11 +20532,10 @@ const READONLY_CONSTRAINT = {
20531
20532
  name: "readonly",
20532
20533
  messageAttribute: "data-readonly-message",
20533
20534
  check: (field, { intent } = {}) => {
20534
- const readOnly = Boolean(
20535
- field.controlHostProps.readOnly ||
20536
- field.controlHostProps["aria-readonly"] === "true",
20537
- );
20538
- if (!readOnly) {
20535
+ // Not off the rendered `readOnly`/`aria-readonly`: they also carry the run
20536
+ // of the control's own action, a frame behind (see readOnlyWithoutRun in
20537
+ // control_hooks.jsx). That run is BUSY_CONSTRAINT's, which reads it live.
20538
+ if (!field.readOnlyWithoutRun) {
20539
20539
  return null;
20540
20540
  }
20541
20541
 
@@ -26867,10 +26867,11 @@ const createRangeReader = (
26867
26867
  });
26868
26868
  return { byIndex, count: composition.count };
26869
26869
  };
26870
- // `replace` is a revalidation: the ranks that are not in what just came back
26871
- // stood for a composition that has moved on. Otherwise the ranks are merged,
26872
- // so two lists reading the same collection through their own windows add up
26873
- // to one composition instead of taking turns erasing each other.
26870
+ // `replace` says the list dropped ranks it held (an item the collection moved
26871
+ // elsewhere, ranks past its end): kept here, they would come back at the next
26872
+ // mount. Otherwise the ranks are merged, so two lists reading the same
26873
+ // collection through their own windows add up to one composition instead of
26874
+ // taking turns erasing each other.
26874
26875
  readRange.writeComposition = ({ byIndex, count, replace }) => {
26875
26876
  const params = currentParams();
26876
26877
  let composition = findComposition(params);
@@ -44065,6 +44066,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
44065
44066
  ref: realUIStateController.ref,
44066
44067
  uiStateSignal: realUIStateController.uiStateSignal,
44067
44068
  controlHostProps: realUIStateController.controlHostProps,
44069
+ readOnlyWithoutRun: realUIStateController.readOnlyWithoutRun,
44068
44070
  registerChild: (child) => {
44069
44071
  if (!canRegisterAsFacadeChild(child)) {
44070
44072
  return;
@@ -44194,6 +44196,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
44194
44196
  s.controller.ref = realUIStateController.ref;
44195
44197
  s.controller.uiStateSignal = realUIStateController.uiStateSignal;
44196
44198
  s.controller.controlHostProps = realUIStateController.controlHostProps;
44199
+ s.controller.readOnlyWithoutRun =
44200
+ realUIStateController.readOnlyWithoutRun;
44197
44201
  realUIStateController.pushStateDownToFacadeChild =
44198
44202
  s.controller.pushStateDownToChild;
44199
44203
 
@@ -46102,6 +46106,11 @@ const useInteractiveProps = (props, {
46102
46106
  const actionLoading = optimistic ? false : actionRunning;
46103
46107
  const loadingResolved = loadingBase || actionLoading;
46104
46108
  const readOnlyResolved = readOnlyBase || actionLoading;
46109
+ // What READONLY_CONSTRAINT refuses for. The run of the control's own action
46110
+ // is BUSY_CONSTRAINT's, read live: readOnlyResolved is a render snapshot,
46111
+ // still read-only in the tick the run settles — the tick a form's
46112
+ // after-send `--navi-reset` asks the form to empty itself.
46113
+ uiStateController.readOnlyWithoutRun = Boolean(readOnlyBase);
46105
46114
  // Read-only, and what this control opens still opens: reading what is in
46106
46115
  // there changes nothing. Read by READONLY_CONSTRAINT, which lets an
46107
46116
  // interaction that only reads through on it.
@@ -46556,10 +46565,10 @@ const useInteractiveProps = (props, {
46556
46565
  });
46557
46566
  }
46558
46567
  // controlHostProps is a curated subset of props with resolved values applied
46559
- // (e.g. readOnly resolved from context + action loading). The interaction system
46560
- // reads off uiStateController.controlHostProps at runtime (e.g. READONLY_CONSTRAINT
46561
- // checks controlHostProps.readOnly), so pointing the controller at controlHostProps
46562
- // keeps those reads current without any extra bookkeeping.
46568
+ // (e.g. readOnly resolved from context + action loading). The constraints
46569
+ // read off uiStateController.controlHostProps at runtime (e.g. DISABLED_CONSTRAINT,
46570
+ // the readonly reason), so pointing the controller at controlHostProps keeps
46571
+ // those reads current without any extra bookkeeping.
46563
46572
  const firstRender = uiStateController.controlHostProps === undefined;
46564
46573
  uiStateController.controlHostProps = controlHostProps;
46565
46574
  // The action itself, not just what the last render made of it: its running
@@ -55616,14 +55625,34 @@ const ButtonConfirm = ({
55616
55625
  }
55617
55626
  return jsxs("span", {
55618
55627
  className: "navi_button_confirm",
55619
- ref: askingRef,
55628
+ ref: askingRef
55629
+ // A place for the focus, so a press anywhere in the question keeps it
55630
+ // inside: WebKit does not focus a pressed button, and a press on
55631
+ // "Confirmer" would otherwise blur toward <body> — a focusout with no
55632
+ // relatedTarget, read below as the person leaving. The callout answers
55633
+ // the same ambiguity the same way.
55634
+ ,
55635
+ tabIndex: -1
55636
+ // "not me, unless you have nothing else": the ladder in the effect above
55637
+ // walks past the question to what it holds.
55638
+ // eslint-disable-next-line react/no-unknown-property
55639
+ ,
55640
+ "navi-autofocus": "last-resort",
55620
55641
  onKeyDown: e => {
55621
55642
  if (e.key === "Escape") {
55622
55643
  cancel(true);
55623
55644
  }
55624
55645
  },
55625
55646
  onFocusOut: e => {
55626
- if (!askingRef.current.contains(e.relatedTarget)) {
55647
+ // Chrome also fires this from the question's own removal, once preact
55648
+ // has cleared the ref: nobody left, and cancel(false) would undo the
55649
+ // focus the answer gives back. That focus cannot go back before the
55650
+ // removal: the button it lands on is mounted by the same commit.
55651
+ const askingEl = askingRef.current;
55652
+ if (!askingEl) {
55653
+ return;
55654
+ }
55655
+ if (!askingEl.contains(e.relatedTarget)) {
55627
55656
  cancel(false);
55628
55657
  }
55629
55658
  },
@@ -73247,7 +73276,7 @@ const css$x = /* css */`@property --list-group-label-height {
73247
73276
  }
73248
73277
  }
73249
73278
 
73250
- .navi_list[navi-box-flow="grid"] > .navi_list_virtual_filler, .navi_list[navi-box-flow="grid"] > .navi_separator {
73279
+ .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 {
73251
73280
  grid-column: 1 / -1;
73252
73281
  }
73253
73282
 
@@ -73624,7 +73653,8 @@ const ListUI = props => {
73624
73653
  listId: rest.id,
73625
73654
  scroller,
73626
73655
  searchText,
73627
- horizontal
73656
+ horizontal,
73657
+ columns
73628
73658
  });
73629
73659
 
73630
73660
  // renderBudget frames the items of a run; a list whose items are all declared
@@ -73939,7 +73969,8 @@ const useListScrollSync = ({
73939
73969
  listId,
73940
73970
  scroller,
73941
73971
  searchText,
73942
- horizontal
73972
+ horizontal,
73973
+ columns
73943
73974
  }) => {
73944
73975
  const debugScroll = useDebugScroll();
73945
73976
  // The items drawn, [start, end) among the list's own. A ref as well as a
@@ -73954,6 +73985,7 @@ const useListScrollSync = ({
73954
73985
  return renderWindow.start > 0 || renderWindow.end < listItems.totalSignal.peek();
73955
73986
  };
73956
73987
  const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
73988
+ inLines: Boolean(columns),
73957
73989
  windowLeavesItemsOut,
73958
73990
  scrolledWanted: scrolled ?? defaultScrolled,
73959
73991
  // The fillers hold the room of items above the screen at this size: a
@@ -74022,6 +74054,34 @@ const useListScrollSync = ({
74022
74054
  });
74023
74055
  };
74024
74056
 
74057
+ // How many items share a line: the tracks a grid of items (`columns`)
74058
+ // resolves to, one otherwise. A layout result under auto-fill, so it is read
74059
+ // off the list once laid out, at each commit and each resize — for the runs
74060
+ // alone, which draw in lines: items declared one by one are all drawn. A
74061
+ // line of another length rewraps every item drawn: the item at the top of
74062
+ // the view is held where it is across it.
74063
+ const itemsPerLineRef = useRef(1);
74064
+ const [itemsPerLine, setItemsPerLine] = useState(1);
74065
+ const updateItemsPerLine = () => {
74066
+ if (!ref.current) {
74067
+ return;
74068
+ }
74069
+ const itemsPerLineNow = columns && listItems.hasRuns() ? readItemsPerLine(getListEl()) : 1;
74070
+ if (itemsPerLineNow === null || itemsPerLineNow === itemsPerLineRef.current) {
74071
+ return;
74072
+ }
74073
+ captureAnchor();
74074
+ itemsPerLineRef.current = itemsPerLineNow;
74075
+ setItemsPerLine(itemsPerLineNow);
74076
+ };
74077
+ const updateItemsPerLineRef = useRef(null);
74078
+ updateItemsPerLineRef.current = updateItemsPerLine;
74079
+ useLayoutEffect(() => {
74080
+ if (columns || itemsPerLineRef.current !== 1) {
74081
+ updateItemsPerLine();
74082
+ }
74083
+ });
74084
+
74025
74085
  // What the window holds goes through three stages (see settleWindow):
74026
74086
  // - "first": the first commit, a count of items since nothing is laid out
74027
74087
  // yet to measure a size with — `initial` or the budget when they are one,
@@ -74423,6 +74483,7 @@ const useListScrollSync = ({
74423
74483
  listEl: getListEl(),
74424
74484
  listItems,
74425
74485
  virtualItemSizeSignal,
74486
+ itemsPerLine: itemsPerLineRef.current,
74426
74487
  renderWindowRef,
74427
74488
  horizontal
74428
74489
  });
@@ -74763,20 +74824,14 @@ const useListScrollSync = ({
74763
74824
  }
74764
74825
  const scrollerEl = getScroller();
74765
74826
  const listEl = getListEl();
74766
- const observer = new ResizeObserver(entries => {
74767
- // A box that grows may be a box that starts to scroll — the scroller is
74768
- // resolved again before anything is done about the resize, so that a
74769
- // list which has outgrown a bounded ancestor stops holding on to the
74770
- // page.
74771
- resolveScroller();
74772
- rememberPosition();
74773
- // Two things resize here, and they call for opposite answers. The LIST
74774
- // growing is its own content settling: only a list holding itself
74775
- // somewhere cares (the end it aims at has moved), and a list the user is
74776
- // reading must not be touched — its items are held still by the anchoring,
74777
- // which this would undo. The SCROLLER resizing is the window around it
74778
- // changing shape, and then the item that was at the top goes back where it
74779
- // was.
74827
+ // Two things resize here, and they call for opposite answers. The LIST
74828
+ // growing is its own content settling: only a list holding itself
74829
+ // somewhere cares (the end it aims at has moved), and a list the user is
74830
+ // reading must not be touched — its items are held still by the anchoring,
74831
+ // which this would undo. The SCROLLER resizing is the window around it
74832
+ // changing shape, and then the item that was at the top goes back where it
74833
+ // was.
74834
+ const putTopItemBack = entries => {
74780
74835
  const listResized = entries.some(entry => entry.target === listEl);
74781
74836
  if (listResized && onGeometryChangeRef.current()) {
74782
74837
  return;
@@ -74805,6 +74860,19 @@ const useListScrollSync = ({
74805
74860
  } else {
74806
74861
  scrollerEl.scrollTop += delta;
74807
74862
  }
74863
+ };
74864
+ const observer = new ResizeObserver(entries => {
74865
+ // A box that grows may be a box that starts to scroll — the scroller is
74866
+ // resolved again before anything is done about the resize, so that a
74867
+ // list which has outgrown a bounded ancestor stops holding on to the
74868
+ // page.
74869
+ resolveScroller();
74870
+ rememberPosition();
74871
+ putTopItemBack(entries);
74872
+ // Once the top item is back where it was: that is the place a line of
74873
+ // another length has to keep, and putting it back drops any anchor
74874
+ // captured before.
74875
+ updateItemsPerLineRef.current();
74808
74876
  });
74809
74877
  observer.observe(scrollerEl);
74810
74878
  observer.observe(listEl);
@@ -74960,7 +75028,10 @@ const useListScrollSync = ({
74960
75028
  start,
74961
75029
  end
74962
75030
  } = renderWindowRef.current;
74963
- const itemSize = virtualItemSizeSignal.peek();
75031
+ // What an item not drawn weighs: its share of a line, which the items side
75032
+ // by side on it take together.
75033
+ const itemsPerLineNow = itemsPerLineRef.current;
75034
+ const itemSize = virtualItemSizeSignal.peek() / itemsPerLineNow;
74964
75035
  const pixelsOf = createItemSizeReader(items, start, end, itemSize);
74965
75036
  const walkBefore = (from, sizeWanted, sizeOf) => {
74966
75037
  let index = from;
@@ -75028,10 +75099,10 @@ const useListScrollSync = ({
75028
75099
  const behindSize = countsItems ? Math.floor(spare / 4) : spare / 4;
75029
75100
  const aheadSize = spare - behindSize;
75030
75101
  if (stage === "steady" && !budgetWarnedRef.current && bandEnd > bandStart) {
75031
- const twoItems = countsItems ? 2 : 2 * (bandTo - bandFrom) / (bandEnd - bandStart);
75032
- if (spare < twoItems) {
75102
+ const twoLines = (countsItems ? 2 : 2 * (bandTo - bandFrom) / (bandEnd - bandStart)) * itemsPerLineNow;
75103
+ if (spare < twoLines) {
75033
75104
  budgetWarnedRef.current = true;
75034
- console.warn(`List: renderBudget=${renderBudget.value}${renderBudget.unit} leaves less than two items beyond what the screen shows (${bandEnd - bandStart} items): items will go blank as it scrolls. Give it room for a screen ahead or more — "300%" of the box that scrolls it, say.`);
75105
+ console.warn(`List: renderBudget=${renderBudget.value}${renderBudget.unit} leaves less than two ${itemsPerLineNow > 1 ? "lines of items" : "items"} beyond what the screen shows (${bandEnd - bandStart} items): items will go blank as it scrolls. Give it room for a screen ahead or more — "300%" of the box that scrolls it, say.`);
75035
75106
  }
75036
75107
  }
75037
75108
  if (!force) {
@@ -75237,9 +75308,28 @@ const useListScrollSync = ({
75237
75308
  ifChanged: true
75238
75309
  });
75239
75310
  }, [windowStart, windowEnd]);
75311
+ // What the runs draw: the window widened to whole lines. The window itself
75312
+ // moves an item at a time, and a grid lays the first item drawn in its first
75313
+ // column — started anywhere but on a line, every item drawn would sit in the
75314
+ // column of another. Started on one, an item is in the column its place says
75315
+ // whatever the window frames. The same object while the numbers hold, so the
75316
+ // runs are not told about a window that did not move.
75317
+ const windowDrawnRef = useRef(null);
75318
+ const drawnStart = windowStart - windowStart % itemsPerLine;
75319
+ const endPastLine = windowEnd % itemsPerLine;
75320
+ const drawnEnd = endPastLine === 0 ? windowEnd : windowEnd + itemsPerLine - endPastLine;
75321
+ let windowDrawn = windowDrawnRef.current;
75322
+ if (!windowDrawn || windowDrawn.start !== drawnStart || windowDrawn.end !== drawnEnd || windowDrawn.itemsPerLine !== itemsPerLine) {
75323
+ windowDrawn = {
75324
+ start: drawnStart,
75325
+ end: drawnEnd,
75326
+ itemsPerLine
75327
+ };
75328
+ windowDrawnRef.current = windowDrawn;
75329
+ }
75240
75330
  return {
75241
75331
  virtualItemSizeSignal,
75242
- renderWindow: renderWindowRef.current,
75332
+ renderWindow: windowDrawn,
75243
75333
  pendingScrollRef,
75244
75334
  scrollToItem,
75245
75335
  captureAnchor
@@ -75263,6 +75353,21 @@ const getScrollerViewportRect = scrollerEl => {
75263
75353
  }
75264
75354
  return scrollerEl.getBoundingClientRect();
75265
75355
  };
75356
+ // The tracks a grid resolved its template to, as its computed
75357
+ // grid-template-columns lists them once laid out: "150px 150px [end] 150px".
75358
+ // Not laid out (a closed popup), that is the template as written, which says
75359
+ // nothing yet under auto-fill: `null`.
75360
+ const readItemsPerLine = listEl => {
75361
+ const tracks = getComputedStyle(listEl).gridTemplateColumns;
75362
+ if (tracks === "none" || tracks.includes("(")) {
75363
+ return null;
75364
+ }
75365
+ const sizes = tracks.replace(/\[[^\]]*\]/g, " ").trim();
75366
+ if (!sizes) {
75367
+ return null;
75368
+ }
75369
+ return sizes.split(/\s+/).length;
75370
+ };
75266
75371
  // A window that cannot move is the one failure of a virtualized list that
75267
75372
  // looks like nothing: the items outside it are fillers holding their room, so
75268
75373
  // the list simply ends on blank space, at exactly the height of the items never
@@ -76150,6 +76255,7 @@ const getScrollInfo = ({
76150
76255
  listEl,
76151
76256
  listItems,
76152
76257
  virtualItemSizeSignal,
76258
+ itemsPerLine,
76153
76259
  renderWindowRef,
76154
76260
  horizontal
76155
76261
  }) => {
@@ -76187,7 +76293,9 @@ const getScrollInfo = ({
76187
76293
  // mean we don't know the real on-screen index, only the scroll position,
76188
76294
  // so estimate from it rather than assume nothing changed.
76189
76295
  const estimateFromScrollPos = reasonPrefix => {
76190
- const virtualItemSize = virtualItemSizeSignal.peek();
76296
+ // An item's share of its line: the items side by side on it take it
76297
+ // together.
76298
+ const virtualItemSize = virtualItemSizeSignal.peek() / itemsPerLine;
76191
76299
  if (virtualItemSize === 0) {
76192
76300
  return null;
76193
76301
  }
@@ -76243,8 +76351,10 @@ const getScrollInfo = ({
76243
76351
  const VIRTUAL_ITEM_SIZE_EPSILON = 0.5;
76244
76352
  // Measures the items currently in the DOM, edge to edge: what a filler stands in
76245
76353
  // for is the room a run of items takes together — separators and group labels
76246
- // included — not the height of one <li>.
76247
- const measureItemSize = (listEl, horizontal) => {
76354
+ // included — not the height of one <li>. The size of a line of them: one item
76355
+ // in a list, the items side by side in a grid (`inLines`), counted where a new
76356
+ // line starts.
76357
+ const measureItemSize = (listEl, horizontal, inLines) => {
76248
76358
  let fromSkeletons = false;
76249
76359
  let itemEls = listEl.querySelectorAll(REAL_LIST_ITEM_SELECTOR);
76250
76360
  if (itemEls.length === 0) {
@@ -76266,13 +76376,27 @@ const measureItemSize = (listEl, horizontal) => {
76266
76376
  if (span <= 0) {
76267
76377
  return null;
76268
76378
  }
76379
+ let lineCount = itemEls.length;
76380
+ if (inLines) {
76381
+ lineCount = 0;
76382
+ let lineFrom = -Infinity;
76383
+ for (const itemEl of itemEls) {
76384
+ const rect = itemEl.getBoundingClientRect();
76385
+ const from = horizontal ? rect.left : rect.top;
76386
+ if (from > lineFrom + 0.5) {
76387
+ lineCount++;
76388
+ lineFrom = from;
76389
+ }
76390
+ }
76391
+ }
76269
76392
  return {
76270
- size: span / itemEls.length,
76271
- itemCount: itemEls.length,
76393
+ size: span / lineCount,
76394
+ lineCount,
76272
76395
  fromSkeletons
76273
76396
  };
76274
76397
  };
76275
76398
  const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
76399
+ inLines,
76276
76400
  windowLeavesItemsOut,
76277
76401
  scrolledWanted,
76278
76402
  beforeSizeChange
@@ -76316,8 +76440,8 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
76316
76440
  return;
76317
76441
  }
76318
76442
  }
76319
- samples.sum += measure.size * measure.itemCount;
76320
- samples.count += measure.itemCount;
76443
+ samples.sum += measure.size * measure.lineCount;
76444
+ samples.count += measure.lineCount;
76321
76445
  const next = samples.sum / samples.count;
76322
76446
  const current = virtualSizeSignal.peek();
76323
76447
  if (Math.abs(next - current) > VIRTUAL_ITEM_SIZE_EPSILON) {
@@ -76337,7 +76461,7 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
76337
76461
  const itemsHeldOffScreen = windowLeavesItemsOut();
76338
76462
  if (!virtualItemSizeProp && sizeAlreadyKnown && itemsHeldOffScreen && ref.current) {
76339
76463
  const listEl = ref.current.querySelector(".navi_list");
76340
- const measure = listEl ? measureItemSize(listEl, horizontal) : null;
76464
+ const measure = listEl ? measureItemSize(listEl, horizontal, inLines) : null;
76341
76465
  if (measure) {
76342
76466
  feedSample(measure);
76343
76467
  }
@@ -76358,11 +76482,11 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
76358
76482
  if (!listEl) {
76359
76483
  return undefined;
76360
76484
  }
76361
- const measure = measureItemSize(listEl, horizontal);
76485
+ const measure = measureItemSize(listEl, horizontal, inLines);
76362
76486
  if (measure) {
76363
76487
  const samples = samplesRef.current;
76364
- samples.sum = measure.size * measure.itemCount;
76365
- samples.count = measure.itemCount;
76488
+ samples.sum = measure.size * measure.lineCount;
76489
+ samples.count = measure.lineCount;
76366
76490
  samples.fromSkeletons = measure.fromSkeletons;
76367
76491
  virtualSizeSignal.value = measure.size;
76368
76492
  return undefined;
@@ -76507,14 +76631,17 @@ const Fallback = ({
76507
76631
  // Reads the item size itself: it is what the size is for, and a run holding
76508
76632
  // every item it draws must not be redrawn — every item of it — because the size
76509
76633
  // settled after the first commit.
76634
+ // It holds lines: the items side by side on one take its room together, and a
76635
+ // last line left short takes it all the same.
76510
76636
  const VirtualFiller = ({
76511
76637
  edge,
76512
76638
  itemCount,
76639
+ itemsPerLine,
76513
76640
  findChunks
76514
76641
  }) => {
76515
76642
  const listItems = useContext(ListItemsContext);
76516
- const virtualItemSize = listItems.virtualItemSizeSignal.value;
76517
- const sizeToFill = itemCount * virtualItemSize;
76643
+ const lineSize = listItems.virtualItemSizeSignal.value;
76644
+ const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize;
76518
76645
  // A filler resizing moves what stands below it — the items on screen, when it
76519
76646
  // holds the room of items above them — and it resizes in a commit of its own
76520
76647
  // when the item size settles after the list has rendered: the list puts its
@@ -76533,7 +76660,7 @@ const VirtualFiller = ({
76533
76660
  "aria-hidden": true,
76534
76661
  style: {
76535
76662
  "--size-to-fill": `${sizeToFill}px`,
76536
- "--x-find-line-size": findChunks ? `${virtualItemSize}px` : undefined
76663
+ "--x-find-line-size": findChunks ? `${lineSize / itemsPerLine}px` : undefined
76537
76664
  },
76538
76665
  children: findChunks && findChunks.map(chunk => jsx("div", {
76539
76666
  className: "navi_list_find_stand_in",
@@ -77416,6 +77543,9 @@ const useRunItems = (store, {
77416
77543
  const rankOf = itemIndex => itemIndex - runStart;
77417
77544
  const indexOfRank = rank => rank + runStart;
77418
77545
  const getItemAt = itemIndex => store.getItem(rankOf(itemIndex));
77546
+ const {
77547
+ itemsPerLine
77548
+ } = renderWindow;
77419
77549
  const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;
77420
77550
  const windowTo = renderWindow.end < runEnd ? renderWindow.end : runEnd;
77421
77551
  store.forget(rankOf(windowFrom), rankOf(windowTo));
@@ -77523,8 +77653,20 @@ const useRunItems = (store, {
77523
77653
  // band is drawn among the items. A negative start is not a rank but a count
77524
77654
  // back from the end (the very first ask, before the count is known) — there
77525
77655
  // is no item to convert it to, and the band falls back to the window.
77526
- const failureFrom = store.failure === null ? -1 : store.failure.start < 0 || indexOfRank(store.failure.start) < windowFrom ? windowFrom : indexOfRank(store.failure.start);
77527
- const failureTo = store.failure === null ? -1 : store.failure.end < 0 || indexOfRank(store.failure.end) > windowTo - 1 ? windowTo - 1 : indexOfRank(store.failure.end);
77656
+ let failureFrom = store.failure === null ? -1 : store.failure.start < 0 || indexOfRank(store.failure.start) < windowFrom ? windowFrom : indexOfRank(store.failure.start);
77657
+ let failureTo = store.failure === null ? -1 : store.failure.end < 0 || indexOfRank(store.failure.end) > windowTo - 1 ? windowTo - 1 : indexOfRank(store.failure.end);
77658
+ if (store.failure !== null && itemsPerLine > 1) {
77659
+ // In a grid the band takes whole lines, like the window: begun mid-line,
77660
+ // it would push every item after it into another column.
77661
+ failureFrom -= failureFrom % itemsPerLine;
77662
+ if (failureFrom < windowFrom) {
77663
+ failureFrom = windowFrom;
77664
+ }
77665
+ failureTo += itemsPerLine - 1 - failureTo % itemsPerLine;
77666
+ if (failureTo > windowTo - 1) {
77667
+ failureTo = windowTo - 1;
77668
+ }
77669
+ }
77528
77670
  const nodes = [];
77529
77671
  // Items that belong together, as the data says (a day of messages, a month of
77530
77672
  // games): consecutive items sharing a group key are wrapped in one group, so
@@ -77532,19 +77674,57 @@ const useRunItems = (store, {
77532
77674
  // rebuilt as the window slides — a group holds the items of its day that are
77533
77675
  // currently drawn, which is exactly the span its label has to survive.
77534
77676
  let group = null;
77677
+ // Two groups can still share a key (a failure band standing mid-group, data
77678
+ // not sorted by group): the later ones are told apart by their rank among
77679
+ // them, so the first keeps its element. A group keyed by where it starts
77680
+ // would lose its element each time the window slides past its first item.
77681
+ const groupCountByKey = new Map();
77535
77682
  const closeGroup = () => {
77536
77683
  if (!group) {
77537
77684
  return;
77538
77685
  }
77686
+ const groupCount = (groupCountByKey.get(group.key) || 0) + 1;
77687
+ groupCountByKey.set(group.key, groupCount);
77539
77688
  nodes.push(jsx(ListItemGroup, {
77540
77689
  label: group.label,
77541
77690
  labelProps: group.labelProps,
77542
77691
  children: group.children
77543
- }, `${ownerId}_group_${group.key}`));
77692
+ }, groupCount === 1 ? `${ownerId}_group_${group.key}` : `${ownerId}_group_${group.key}_${groupCount}`));
77544
77693
  group = null;
77545
77694
  };
77695
+ // An item not held has no group of its own. Between two items of the open
77696
+ // group it is a line of that group whose content has not arrived: drawn
77697
+ // outside, it would cut the group in two and show its label twice. A hole at
77698
+ // a group's edge stays outside — nothing says which side it belongs to.
77699
+ let holeTo = -1;
77700
+ let holeGroupKey;
77701
+ const groupKeyOfHole = itemIndex => {
77702
+ if (itemIndex < holeTo) {
77703
+ return holeGroupKey;
77704
+ }
77705
+ holeTo = itemIndex + 1;
77706
+ while (holeTo < windowTo && holeTo !== failureFrom && getItemAt(holeTo) === undefined) {
77707
+ holeTo++;
77708
+ }
77709
+ holeGroupKey = undefined;
77710
+ if (group && holeTo < windowTo && holeTo !== failureFrom) {
77711
+ const keyAfterHole = groupBy(getItemAt(holeTo), holeTo);
77712
+ if (keyAfterHole === group.key) {
77713
+ holeGroupKey = keyAfterHole;
77714
+ }
77715
+ }
77716
+ return holeGroupKey;
77717
+ };
77546
77718
  // Which group an item belongs to, or undefined when it belongs to none.
77547
- const groupKeyOf = (item, itemIndex) => groupBy && item !== undefined ? groupBy(item, itemIndex) : undefined;
77719
+ const groupKeyOf = (item, itemIndex) => {
77720
+ if (!groupBy) {
77721
+ return undefined;
77722
+ }
77723
+ if (item === undefined) {
77724
+ return groupKeyOfHole(itemIndex);
77725
+ }
77726
+ return groupBy(item, itemIndex);
77727
+ };
77548
77728
  const pushItem = (itemNode, item, itemIndex, groupKey) => {
77549
77729
  if (groupKey === undefined) {
77550
77730
  closeGroup();
@@ -77601,6 +77781,7 @@ const useRunItems = (store, {
77601
77781
  nodes.push(jsx(VirtualFiller, {
77602
77782
  edge: "before",
77603
77783
  itemCount: windowFrom - runStart,
77784
+ itemsPerLine: itemsPerLine,
77604
77785
  findChunks: getFindChunks(runStart, windowFrom)
77605
77786
  }, "navi-list-filler-before"));
77606
77787
  }
@@ -77615,7 +77796,7 @@ const useRunItems = (store, {
77615
77796
  nodes.push(jsx("li", {
77616
77797
  className: "navi_list_failed_items",
77617
77798
  style: {
77618
- "--size-to-fill": `${failedItemCount * listItems.virtualItemSizeSignal.value}px`
77799
+ "--size-to-fill": `${Math.ceil(failedItemCount / itemsPerLine) * listItems.virtualItemSizeSignal.value}px`
77619
77800
  },
77620
77801
  children: renderError ? renderError({
77621
77802
  error: store.failure.error,
@@ -77705,6 +77886,7 @@ const useRunItems = (store, {
77705
77886
  nodes.push(jsx(VirtualFiller, {
77706
77887
  edge: "after",
77707
77888
  itemCount: runEnd - windowTo,
77889
+ itemsPerLine: itemsPerLine,
77708
77890
  findChunks: getFindChunks(windowTo, runEnd)
77709
77891
  }, "navi-list-filler-after"));
77710
77892
  }
@@ -77879,10 +78061,15 @@ const useItemStore = ({
77879
78061
  }
77880
78062
  const pages = pagesRef.current;
77881
78063
  const [, setPageVersion] = useState(0);
77882
- // The items held are out of date and the run has not asked for the new ones
77883
- // yet. They stay on screen until the answer comes: what is drawn is from
77884
- // before, which is not the same thing as nothing to draw.
77885
- const staleRef = useRef(restored);
78064
+ // The ranks whose item is from before: the run came back to the screen, or
78065
+ // the source said the collection moved. They stay drawn until a page confirms
78066
+ // or replaces them, rank by rank. A page covers what the window framed when it
78067
+ // was asked for, and the window keeps growing meanwhile: dropping what the
78068
+ // page did not cover empties items on screen.
78069
+ const staleRanksRef = useRef(null);
78070
+ if (staleRanksRef.current === null) {
78071
+ staleRanksRef.current = restored ? new Set(pages.byIndex.keys()) : new Set();
78072
+ }
77886
78073
  const [refreshing, setRefreshing] = useState(false);
77887
78074
  // A source that says when what it reads has moved (a resource range reader
77888
78075
  // does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
@@ -77893,7 +78080,7 @@ const useItemStore = ({
77893
78080
  const invalidationRef = useRef(invalidation);
77894
78081
  if (invalidationRef.current !== invalidation) {
77895
78082
  invalidationRef.current = invalidation;
77896
- staleRef.current = true;
78083
+ staleRanksRef.current = new Set(pages.byIndex.keys());
77897
78084
  }
77898
78085
  // The one request in flight, with the means to call it off: a page asked for
77899
78086
  // a window the list has left is work the server and the browser are doing for
@@ -77926,11 +78113,6 @@ const useItemStore = ({
77926
78113
  // It stands for a page of them: a list that is about to be filled looks
77927
78114
  // like items on their way, not like an empty list.
77928
78115
  const itemCount = pages.count ?? count ?? listItems.pageSize;
77929
- // A run that never received anything has nothing to keep on screen: asking
77930
- // again is its first ask, not a refresh.
77931
- if (staleRef.current && pages.count === undefined) {
77932
- staleRef.current = false;
77933
- }
77934
78116
  useLayoutEffect(() => {
77935
78117
  if (!refreshing) {
77936
78118
  return null;
@@ -78003,9 +78185,11 @@ const useItemStore = ({
78003
78185
  const keepFrom = windowFrom - ITEM_STORE_KEEP_AROUND;
78004
78186
  const keepTo = windowTo + ITEM_STORE_KEEP_AROUND;
78005
78187
  if (pages.byIndex.size > budget) {
78188
+ const staleRanks = staleRanksRef.current;
78006
78189
  for (const index of pages.byIndex.keys()) {
78007
78190
  if (index < keepFrom || index > keepTo) {
78008
78191
  pages.byIndex.delete(index);
78192
+ staleRanks.delete(index);
78009
78193
  }
78010
78194
  }
78011
78195
  }
@@ -78040,10 +78224,24 @@ const useItemStore = ({
78040
78224
  let start = missingStart;
78041
78225
  let end = missingEnd;
78042
78226
  let around;
78043
- // Items that are all there but out of date: the ask is the window itself,
78044
- // anchored on the item at its top — a source paginating by cursor gets an
78045
- // item to count from, and the reading position is what must survive.
78046
- const revalidating = staleRef.current;
78227
+ // The window draws items from before: they are asked for again, and stay
78228
+ // drawn meanwhile.
78229
+ const staleRanks = staleRanksRef.current;
78230
+ let staleFrom = -1;
78231
+ let staleTo = -1;
78232
+ if (staleRanks.size > 0) {
78233
+ let rank = windowFrom;
78234
+ while (rank < windowTo) {
78235
+ if (staleRanks.has(rank)) {
78236
+ if (staleFrom === -1) {
78237
+ staleFrom = rank;
78238
+ }
78239
+ staleTo = rank;
78240
+ }
78241
+ rank++;
78242
+ }
78243
+ }
78244
+ const revalidating = staleFrom !== -1;
78047
78245
  // The list is held on an item nothing on screen leads to: the items it holds
78048
78246
  // do not contain it, so no window it could draw will ever bring it. Only
78049
78247
  // asking for it by name does.
@@ -78060,18 +78258,36 @@ const useItemStore = ({
78060
78258
  start = from < 0 ? 0 : from;
78061
78259
  end = start + budget - 1;
78062
78260
  } else if (revalidating) {
78063
- start = windowFrom;
78064
- end = windowTo - 1;
78065
- if (end < start) {
78066
- // Nothing of this run is on screen (the window frames another one, or
78067
- // the list is scrolled past it): its own first items are what it will
78068
- // draw next.
78069
- start = 0;
78070
- end = budget - 1;
78071
- }
78072
- const firstHeld = pages.byIndex.get(windowFrom);
78073
- if (firstHeld && firstHeld.id !== undefined) {
78074
- around = firstHeld.id;
78261
+ // The items from before that the window draws, and the missing ones
78262
+ // with them: one ask.
78263
+ if (start === -1 || staleFrom < start) {
78264
+ start = staleFrom;
78265
+ }
78266
+ if (staleTo > end) {
78267
+ end = staleTo;
78268
+ }
78269
+ // Grown over the items from before on either side, up to a page: the
78270
+ // window grows past its first picture right after it is painted, and
78271
+ // what it grows onto would otherwise be asked for once more.
78272
+ while (end - start + 1 < budget) {
78273
+ const staleBefore = staleRanks.has(start - 1);
78274
+ const staleAfter = staleRanks.has(end + 1);
78275
+ if (!staleBefore && !staleAfter) {
78276
+ break;
78277
+ }
78278
+ if (staleBefore) {
78279
+ start--;
78280
+ }
78281
+ if (staleAfter && end - start + 1 < budget) {
78282
+ end++;
78283
+ }
78284
+ }
78285
+ // Anchored on the first item from before on screen: a source
78286
+ // paginating by cursor gets an item to count from, and the reading
78287
+ // position is what must survive.
78288
+ const firstStale = pages.byIndex.get(staleFrom);
78289
+ if (firstStale && firstStale.id !== undefined) {
78290
+ around = firstStale.id;
78075
78291
  }
78076
78292
  } else if (pages.count === undefined) {
78077
78293
  const scrolled = listItems.scrolled;
@@ -78125,7 +78341,8 @@ const useItemStore = ({
78125
78341
  debugAsk("already revalidating");
78126
78342
  return;
78127
78343
  }
78128
- // A page for a window that is about to be replaced wholesale.
78344
+ // A page asked for before the items went stale answers for the
78345
+ // collection as it was.
78129
78346
  request.controller?.abort();
78130
78347
  request.busy = false;
78131
78348
  }
@@ -78146,11 +78363,11 @@ const useItemStore = ({
78146
78363
  request.controller?.abort();
78147
78364
  request.busy = false;
78148
78365
  }
78149
- const held = pages.byIndex.size;
78150
78366
  // Asking again for a range that was already asked for, having received
78151
- // nothing since, can only produce the same answer. A revalidation is
78152
- // exactly the case where it produces another one.
78153
- if (!revalidating && request.start === start && request.end === end && request.held === held) {
78367
+ // nothing since, can only produce the same answer — unless the
78368
+ // collection moved since, which is what an invalidation says.
78369
+ const held = `${pages.byIndex.size}/${staleRanks.size}/${invalidation}`;
78370
+ if (request.start === start && request.end === end && request.held === held) {
78154
78371
  debugAsk("this range was asked for already");
78155
78372
  return;
78156
78373
  }
@@ -78186,7 +78403,6 @@ const useItemStore = ({
78186
78403
  request.revalidating = false;
78187
78404
  setFailure(null);
78188
78405
  if (revalidating) {
78189
- staleRef.current = false;
78190
78406
  setRefreshing(false);
78191
78407
  }
78192
78408
  publishRequestState();
@@ -78205,15 +78421,35 @@ const useItemStore = ({
78205
78421
  // Before the items land: what is on screen has to stay where it is,
78206
78422
  // and the DOM still shows the state to hold onto.
78207
78423
  listItems.captureAnchor();
78208
- if (revalidating) {
78209
- // The items held stood for a composition that has moved on; the
78210
- // ones outside the window are forgotten and asked for again if the
78211
- // user goes back to them.
78212
- pages.byIndex = new Map();
78424
+ // An item from before is gone from where it stood when the page puts
78425
+ // it at another rank (it would be drawn twice), or when its rank is
78426
+ // past the end of the collection.
78427
+ const staleRanksNow = staleRanksRef.current;
78428
+ const pageEnd = pageStart + pageItems.length;
78429
+ let dropped = false;
78430
+ if (staleRanksNow.size > 0) {
78431
+ const pageIds = new Set();
78432
+ for (const pageItem of pageItems) {
78433
+ if (pageItem && pageItem.id !== undefined) {
78434
+ pageIds.add(pageItem.id);
78435
+ }
78436
+ }
78437
+ for (const rank of staleRanksNow) {
78438
+ if (rank >= pageStart && rank < pageEnd) {
78439
+ continue;
78440
+ }
78441
+ const staleItem = pages.byIndex.get(rank);
78442
+ if (rank >= pageCount || staleItem && pageIds.has(staleItem.id)) {
78443
+ staleRanksNow.delete(rank);
78444
+ pages.byIndex.delete(rank);
78445
+ dropped = true;
78446
+ }
78447
+ }
78213
78448
  }
78214
78449
  let i = 0;
78215
78450
  while (i < pageItems.length) {
78216
78451
  pages.byIndex.set(pageStart + i, pageItems[i]);
78452
+ staleRanksNow.delete(pageStart + i);
78217
78453
  i++;
78218
78454
  }
78219
78455
  pages.count = pageCount;
@@ -78223,7 +78459,7 @@ const useItemStore = ({
78223
78459
  itemsAction.writeComposition({
78224
78460
  byIndex: pages.byIndex,
78225
78461
  count: pageCount,
78226
- replace: revalidating
78462
+ replace: dropped
78227
78463
  });
78228
78464
  }
78229
78465
  listItems.pagesSignal.value = listItems.pagesSignal.peek() + 1;
@@ -78238,9 +78474,14 @@ const useItemStore = ({
78238
78474
  request.revalidating = false;
78239
78475
  publishRequestState();
78240
78476
  if (revalidating) {
78241
- // The items from before stay: a revalidation that failed has
78477
+ // The items from before stay, and are not asked for again until the
78478
+ // source says the collection moved: a revalidation that failed has
78242
78479
  // nothing better to put in their place.
78243
- staleRef.current = false;
78480
+ let rank = start;
78481
+ while (rank <= end) {
78482
+ staleRanksRef.current.delete(rank);
78483
+ rank++;
78484
+ }
78244
78485
  setRefreshing(false);
78245
78486
  return;
78246
78487
  }
@@ -78437,12 +78678,16 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
78437
78678
  * @param {string} [props.columns]
78438
78679
  * The list's own columns: a `grid-template-columns` value the ITEMS are laid
78439
78680
  * into — a sheet of icons (`repeat(auto-fill, minmax(2.5rem, 1fr))`), a row of
78440
- * choices (`repeat(3, minmax(0, 1fr))`). Each item takes one cell, and an item
78441
- * meant to take a whole line says so for itself
78442
- * (`style={{ gridColumn: "1 / -1" }}`). Several items to a line is a shape the
78443
- * list cannot virtualize — its render window and the room it holds for the
78444
- * items it does not draw both count one item per line — so it is for a set of
78445
- * items the caller renders whole, not for a `<List.Items>` collection.
78681
+ * choices (`repeat(3, minmax(0, 1fr))`), a feed of covers
78682
+ * (`repeat(auto-fill, minmax(150px, 1fr))` over a `<List.Items>`). Each item
78683
+ * takes one cell, and an item meant to take a whole line says so for itself
78684
+ * (`style={{ gridColumn: "1 / -1" }}`). A `<List.Items>` run is drawn in whole
78685
+ * lines — as many items to a line as the template resolves to, read again
78686
+ * when the list's width changes: its window starts and ends on a line, so an
78687
+ * item is always in the column its place says, and the fillers hold the room
78688
+ * of the lines it does not draw (`virtualItemSize` is then a line's size).
78689
+ * That count holds only while every item of the run takes one cell: an item
78690
+ * spanning a line, or a `groupBy`, puts the ones after it in other columns.
78446
78691
  * @param {string} [props.itemColumns]
78447
78692
  * The columns inside an ITEM: a `grid-template-columns` value each item fills
78448
78693
  * with its own children, a table whose cells line up down the list. Every item
@@ -78458,8 +78703,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
78458
78703
  * scroll box, which fills it and has nothing to arrange.
78459
78704
  * @param {boolean} [props.flexWrap]
78460
78705
  * Lets a horizontal list's items fall to the next line instead of running
78461
- * past the edge — a row of choices under a `maxWidth`, say. Same caveat as
78462
- * `columns` above: a line holding several items is not virtualizable.
78706
+ * past the edge — a row of choices under a `maxWidth`, say. A wrapped line
78707
+ * holding several items is not virtualizable: a long collection laid several
78708
+ * to a line goes in `columns`.
78463
78709
  * @param {string} [props.overflow]
78464
78710
  * `"visible"` lets the items paint outside the list — a check in an item's
78465
78711
  * corner, a badge crossing the edge. A list clips by default, which is what
@@ -78578,7 +78824,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
78578
78824
  * one-line size) — an item guessed too small builds one more, an item
78579
78825
  * guessed too big leaves a blank (see docs/scroll.md, "What the list knows,
78580
78826
  * and what it guesses"). The items drawn are measured either way: the render
78581
- * window is sized on them.
78827
+ * window is sized on them. In a grid (`columns`), the size is a line's: a
78828
+ * card's, which the items side by side on it share.
78582
78829
  * @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
78583
78830
  * Which box scrolls — and with it, which box the render window follows and
78584
78831
  * which box a scroll position is read from (`onScrolledChange`). `"self"`