@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.
@@ -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
 
@@ -44065,6 +44065,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
44065
44065
  ref: realUIStateController.ref,
44066
44066
  uiStateSignal: realUIStateController.uiStateSignal,
44067
44067
  controlHostProps: realUIStateController.controlHostProps,
44068
+ readOnlyWithoutRun: realUIStateController.readOnlyWithoutRun,
44068
44069
  registerChild: (child) => {
44069
44070
  if (!canRegisterAsFacadeChild(child)) {
44070
44071
  return;
@@ -44194,6 +44195,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
44194
44195
  s.controller.ref = realUIStateController.ref;
44195
44196
  s.controller.uiStateSignal = realUIStateController.uiStateSignal;
44196
44197
  s.controller.controlHostProps = realUIStateController.controlHostProps;
44198
+ s.controller.readOnlyWithoutRun =
44199
+ realUIStateController.readOnlyWithoutRun;
44197
44200
  realUIStateController.pushStateDownToFacadeChild =
44198
44201
  s.controller.pushStateDownToChild;
44199
44202
 
@@ -46102,6 +46105,11 @@ const useInteractiveProps = (props, {
46102
46105
  const actionLoading = optimistic ? false : actionRunning;
46103
46106
  const loadingResolved = loadingBase || actionLoading;
46104
46107
  const readOnlyResolved = readOnlyBase || actionLoading;
46108
+ // What READONLY_CONSTRAINT refuses for. The run of the control's own action
46109
+ // is BUSY_CONSTRAINT's, read live: readOnlyResolved is a render snapshot,
46110
+ // still read-only in the tick the run settles — the tick a form's
46111
+ // after-send `--navi-reset` asks the form to empty itself.
46112
+ uiStateController.readOnlyWithoutRun = Boolean(readOnlyBase);
46105
46113
  // Read-only, and what this control opens still opens: reading what is in
46106
46114
  // there changes nothing. Read by READONLY_CONSTRAINT, which lets an
46107
46115
  // interaction that only reads through on it.
@@ -46556,10 +46564,10 @@ const useInteractiveProps = (props, {
46556
46564
  });
46557
46565
  }
46558
46566
  // 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.
46567
+ // (e.g. readOnly resolved from context + action loading). The constraints
46568
+ // read off uiStateController.controlHostProps at runtime (e.g. DISABLED_CONSTRAINT,
46569
+ // the readonly reason), so pointing the controller at controlHostProps keeps
46570
+ // those reads current without any extra bookkeeping.
46563
46571
  const firstRender = uiStateController.controlHostProps === undefined;
46564
46572
  uiStateController.controlHostProps = controlHostProps;
46565
46573
  // The action itself, not just what the last render made of it: its running
@@ -55616,14 +55624,34 @@ const ButtonConfirm = ({
55616
55624
  }
55617
55625
  return jsxs("span", {
55618
55626
  className: "navi_button_confirm",
55619
- ref: askingRef,
55627
+ ref: askingRef
55628
+ // A place for the focus, so a press anywhere in the question keeps it
55629
+ // inside: WebKit does not focus a pressed button, and a press on
55630
+ // "Confirmer" would otherwise blur toward <body> — a focusout with no
55631
+ // relatedTarget, read below as the person leaving. The callout answers
55632
+ // the same ambiguity the same way.
55633
+ ,
55634
+ tabIndex: -1
55635
+ // "not me, unless you have nothing else": the ladder in the effect above
55636
+ // walks past the question to what it holds.
55637
+ // eslint-disable-next-line react/no-unknown-property
55638
+ ,
55639
+ "navi-autofocus": "last-resort",
55620
55640
  onKeyDown: e => {
55621
55641
  if (e.key === "Escape") {
55622
55642
  cancel(true);
55623
55643
  }
55624
55644
  },
55625
55645
  onFocusOut: e => {
55626
- if (!askingRef.current.contains(e.relatedTarget)) {
55646
+ // Chrome also fires this from the question's own removal, once preact
55647
+ // has cleared the ref: nobody left, and cancel(false) would undo the
55648
+ // focus the answer gives back. That focus cannot go back before the
55649
+ // removal: the button it lands on is mounted by the same commit.
55650
+ const askingEl = askingRef.current;
55651
+ if (!askingEl) {
55652
+ return;
55653
+ }
55654
+ if (!askingEl.contains(e.relatedTarget)) {
55627
55655
  cancel(false);
55628
55656
  }
55629
55657
  },
@@ -73247,7 +73275,7 @@ const css$x = /* css */`@property --list-group-label-height {
73247
73275
  }
73248
73276
  }
73249
73277
 
73250
- .navi_list[navi-box-flow="grid"] > .navi_list_virtual_filler, .navi_list[navi-box-flow="grid"] > .navi_separator {
73278
+ .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
73279
  grid-column: 1 / -1;
73252
73280
  }
73253
73281
 
@@ -73624,7 +73652,8 @@ const ListUI = props => {
73624
73652
  listId: rest.id,
73625
73653
  scroller,
73626
73654
  searchText,
73627
- horizontal
73655
+ horizontal,
73656
+ columns
73628
73657
  });
73629
73658
 
73630
73659
  // renderBudget frames the items of a run; a list whose items are all declared
@@ -73939,7 +73968,8 @@ const useListScrollSync = ({
73939
73968
  listId,
73940
73969
  scroller,
73941
73970
  searchText,
73942
- horizontal
73971
+ horizontal,
73972
+ columns
73943
73973
  }) => {
73944
73974
  const debugScroll = useDebugScroll();
73945
73975
  // The items drawn, [start, end) among the list's own. A ref as well as a
@@ -73954,6 +73984,7 @@ const useListScrollSync = ({
73954
73984
  return renderWindow.start > 0 || renderWindow.end < listItems.totalSignal.peek();
73955
73985
  };
73956
73986
  const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
73987
+ inLines: Boolean(columns),
73957
73988
  windowLeavesItemsOut,
73958
73989
  scrolledWanted: scrolled ?? defaultScrolled,
73959
73990
  // The fillers hold the room of items above the screen at this size: a
@@ -74022,6 +74053,34 @@ const useListScrollSync = ({
74022
74053
  });
74023
74054
  };
74024
74055
 
74056
+ // How many items share a line: the tracks a grid of items (`columns`)
74057
+ // resolves to, one otherwise. A layout result under auto-fill, so it is read
74058
+ // off the list once laid out, at each commit and each resize — for the runs
74059
+ // alone, which draw in lines: items declared one by one are all drawn. A
74060
+ // line of another length rewraps every item drawn: the item at the top of
74061
+ // the view is held where it is across it.
74062
+ const itemsPerLineRef = useRef(1);
74063
+ const [itemsPerLine, setItemsPerLine] = useState(1);
74064
+ const updateItemsPerLine = () => {
74065
+ if (!ref.current) {
74066
+ return;
74067
+ }
74068
+ const itemsPerLineNow = columns && listItems.hasRuns() ? readItemsPerLine(getListEl()) : 1;
74069
+ if (itemsPerLineNow === null || itemsPerLineNow === itemsPerLineRef.current) {
74070
+ return;
74071
+ }
74072
+ captureAnchor();
74073
+ itemsPerLineRef.current = itemsPerLineNow;
74074
+ setItemsPerLine(itemsPerLineNow);
74075
+ };
74076
+ const updateItemsPerLineRef = useRef(null);
74077
+ updateItemsPerLineRef.current = updateItemsPerLine;
74078
+ useLayoutEffect(() => {
74079
+ if (columns || itemsPerLineRef.current !== 1) {
74080
+ updateItemsPerLine();
74081
+ }
74082
+ });
74083
+
74025
74084
  // What the window holds goes through three stages (see settleWindow):
74026
74085
  // - "first": the first commit, a count of items since nothing is laid out
74027
74086
  // yet to measure a size with — `initial` or the budget when they are one,
@@ -74423,6 +74482,7 @@ const useListScrollSync = ({
74423
74482
  listEl: getListEl(),
74424
74483
  listItems,
74425
74484
  virtualItemSizeSignal,
74485
+ itemsPerLine: itemsPerLineRef.current,
74426
74486
  renderWindowRef,
74427
74487
  horizontal
74428
74488
  });
@@ -74763,20 +74823,14 @@ const useListScrollSync = ({
74763
74823
  }
74764
74824
  const scrollerEl = getScroller();
74765
74825
  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.
74826
+ // Two things resize here, and they call for opposite answers. The LIST
74827
+ // growing is its own content settling: only a list holding itself
74828
+ // somewhere cares (the end it aims at has moved), and a list the user is
74829
+ // reading must not be touched — its items are held still by the anchoring,
74830
+ // which this would undo. The SCROLLER resizing is the window around it
74831
+ // changing shape, and then the item that was at the top goes back where it
74832
+ // was.
74833
+ const putTopItemBack = entries => {
74780
74834
  const listResized = entries.some(entry => entry.target === listEl);
74781
74835
  if (listResized && onGeometryChangeRef.current()) {
74782
74836
  return;
@@ -74805,6 +74859,19 @@ const useListScrollSync = ({
74805
74859
  } else {
74806
74860
  scrollerEl.scrollTop += delta;
74807
74861
  }
74862
+ };
74863
+ const observer = new ResizeObserver(entries => {
74864
+ // A box that grows may be a box that starts to scroll — the scroller is
74865
+ // resolved again before anything is done about the resize, so that a
74866
+ // list which has outgrown a bounded ancestor stops holding on to the
74867
+ // page.
74868
+ resolveScroller();
74869
+ rememberPosition();
74870
+ putTopItemBack(entries);
74871
+ // Once the top item is back where it was: that is the place a line of
74872
+ // another length has to keep, and putting it back drops any anchor
74873
+ // captured before.
74874
+ updateItemsPerLineRef.current();
74808
74875
  });
74809
74876
  observer.observe(scrollerEl);
74810
74877
  observer.observe(listEl);
@@ -74960,7 +75027,10 @@ const useListScrollSync = ({
74960
75027
  start,
74961
75028
  end
74962
75029
  } = renderWindowRef.current;
74963
- const itemSize = virtualItemSizeSignal.peek();
75030
+ // What an item not drawn weighs: its share of a line, which the items side
75031
+ // by side on it take together.
75032
+ const itemsPerLineNow = itemsPerLineRef.current;
75033
+ const itemSize = virtualItemSizeSignal.peek() / itemsPerLineNow;
74964
75034
  const pixelsOf = createItemSizeReader(items, start, end, itemSize);
74965
75035
  const walkBefore = (from, sizeWanted, sizeOf) => {
74966
75036
  let index = from;
@@ -75028,10 +75098,10 @@ const useListScrollSync = ({
75028
75098
  const behindSize = countsItems ? Math.floor(spare / 4) : spare / 4;
75029
75099
  const aheadSize = spare - behindSize;
75030
75100
  if (stage === "steady" && !budgetWarnedRef.current && bandEnd > bandStart) {
75031
- const twoItems = countsItems ? 2 : 2 * (bandTo - bandFrom) / (bandEnd - bandStart);
75032
- if (spare < twoItems) {
75101
+ const twoLines = (countsItems ? 2 : 2 * (bandTo - bandFrom) / (bandEnd - bandStart)) * itemsPerLineNow;
75102
+ if (spare < twoLines) {
75033
75103
  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.`);
75104
+ 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
75105
  }
75036
75106
  }
75037
75107
  if (!force) {
@@ -75237,9 +75307,28 @@ const useListScrollSync = ({
75237
75307
  ifChanged: true
75238
75308
  });
75239
75309
  }, [windowStart, windowEnd]);
75310
+ // What the runs draw: the window widened to whole lines. The window itself
75311
+ // moves an item at a time, and a grid lays the first item drawn in its first
75312
+ // column — started anywhere but on a line, every item drawn would sit in the
75313
+ // column of another. Started on one, an item is in the column its place says
75314
+ // whatever the window frames. The same object while the numbers hold, so the
75315
+ // runs are not told about a window that did not move.
75316
+ const windowDrawnRef = useRef(null);
75317
+ const drawnStart = windowStart - windowStart % itemsPerLine;
75318
+ const endPastLine = windowEnd % itemsPerLine;
75319
+ const drawnEnd = endPastLine === 0 ? windowEnd : windowEnd + itemsPerLine - endPastLine;
75320
+ let windowDrawn = windowDrawnRef.current;
75321
+ if (!windowDrawn || windowDrawn.start !== drawnStart || windowDrawn.end !== drawnEnd || windowDrawn.itemsPerLine !== itemsPerLine) {
75322
+ windowDrawn = {
75323
+ start: drawnStart,
75324
+ end: drawnEnd,
75325
+ itemsPerLine
75326
+ };
75327
+ windowDrawnRef.current = windowDrawn;
75328
+ }
75240
75329
  return {
75241
75330
  virtualItemSizeSignal,
75242
- renderWindow: renderWindowRef.current,
75331
+ renderWindow: windowDrawn,
75243
75332
  pendingScrollRef,
75244
75333
  scrollToItem,
75245
75334
  captureAnchor
@@ -75263,6 +75352,21 @@ const getScrollerViewportRect = scrollerEl => {
75263
75352
  }
75264
75353
  return scrollerEl.getBoundingClientRect();
75265
75354
  };
75355
+ // The tracks a grid resolved its template to, as its computed
75356
+ // grid-template-columns lists them once laid out: "150px 150px [end] 150px".
75357
+ // Not laid out (a closed popup), that is the template as written, which says
75358
+ // nothing yet under auto-fill: `null`.
75359
+ const readItemsPerLine = listEl => {
75360
+ const tracks = getComputedStyle(listEl).gridTemplateColumns;
75361
+ if (tracks === "none" || tracks.includes("(")) {
75362
+ return null;
75363
+ }
75364
+ const sizes = tracks.replace(/\[[^\]]*\]/g, " ").trim();
75365
+ if (!sizes) {
75366
+ return null;
75367
+ }
75368
+ return sizes.split(/\s+/).length;
75369
+ };
75266
75370
  // A window that cannot move is the one failure of a virtualized list that
75267
75371
  // looks like nothing: the items outside it are fillers holding their room, so
75268
75372
  // the list simply ends on blank space, at exactly the height of the items never
@@ -76150,6 +76254,7 @@ const getScrollInfo = ({
76150
76254
  listEl,
76151
76255
  listItems,
76152
76256
  virtualItemSizeSignal,
76257
+ itemsPerLine,
76153
76258
  renderWindowRef,
76154
76259
  horizontal
76155
76260
  }) => {
@@ -76187,7 +76292,9 @@ const getScrollInfo = ({
76187
76292
  // mean we don't know the real on-screen index, only the scroll position,
76188
76293
  // so estimate from it rather than assume nothing changed.
76189
76294
  const estimateFromScrollPos = reasonPrefix => {
76190
- const virtualItemSize = virtualItemSizeSignal.peek();
76295
+ // An item's share of its line: the items side by side on it take it
76296
+ // together.
76297
+ const virtualItemSize = virtualItemSizeSignal.peek() / itemsPerLine;
76191
76298
  if (virtualItemSize === 0) {
76192
76299
  return null;
76193
76300
  }
@@ -76243,8 +76350,10 @@ const getScrollInfo = ({
76243
76350
  const VIRTUAL_ITEM_SIZE_EPSILON = 0.5;
76244
76351
  // Measures the items currently in the DOM, edge to edge: what a filler stands in
76245
76352
  // 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) => {
76353
+ // included — not the height of one <li>. The size of a line of them: one item
76354
+ // in a list, the items side by side in a grid (`inLines`), counted where a new
76355
+ // line starts.
76356
+ const measureItemSize = (listEl, horizontal, inLines) => {
76248
76357
  let fromSkeletons = false;
76249
76358
  let itemEls = listEl.querySelectorAll(REAL_LIST_ITEM_SELECTOR);
76250
76359
  if (itemEls.length === 0) {
@@ -76266,13 +76375,27 @@ const measureItemSize = (listEl, horizontal) => {
76266
76375
  if (span <= 0) {
76267
76376
  return null;
76268
76377
  }
76378
+ let lineCount = itemEls.length;
76379
+ if (inLines) {
76380
+ lineCount = 0;
76381
+ let lineFrom = -Infinity;
76382
+ for (const itemEl of itemEls) {
76383
+ const rect = itemEl.getBoundingClientRect();
76384
+ const from = horizontal ? rect.left : rect.top;
76385
+ if (from > lineFrom + 0.5) {
76386
+ lineCount++;
76387
+ lineFrom = from;
76388
+ }
76389
+ }
76390
+ }
76269
76391
  return {
76270
- size: span / itemEls.length,
76271
- itemCount: itemEls.length,
76392
+ size: span / lineCount,
76393
+ lineCount,
76272
76394
  fromSkeletons
76273
76395
  };
76274
76396
  };
76275
76397
  const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
76398
+ inLines,
76276
76399
  windowLeavesItemsOut,
76277
76400
  scrolledWanted,
76278
76401
  beforeSizeChange
@@ -76316,8 +76439,8 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
76316
76439
  return;
76317
76440
  }
76318
76441
  }
76319
- samples.sum += measure.size * measure.itemCount;
76320
- samples.count += measure.itemCount;
76442
+ samples.sum += measure.size * measure.lineCount;
76443
+ samples.count += measure.lineCount;
76321
76444
  const next = samples.sum / samples.count;
76322
76445
  const current = virtualSizeSignal.peek();
76323
76446
  if (Math.abs(next - current) > VIRTUAL_ITEM_SIZE_EPSILON) {
@@ -76337,7 +76460,7 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
76337
76460
  const itemsHeldOffScreen = windowLeavesItemsOut();
76338
76461
  if (!virtualItemSizeProp && sizeAlreadyKnown && itemsHeldOffScreen && ref.current) {
76339
76462
  const listEl = ref.current.querySelector(".navi_list");
76340
- const measure = listEl ? measureItemSize(listEl, horizontal) : null;
76463
+ const measure = listEl ? measureItemSize(listEl, horizontal, inLines) : null;
76341
76464
  if (measure) {
76342
76465
  feedSample(measure);
76343
76466
  }
@@ -76358,11 +76481,11 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
76358
76481
  if (!listEl) {
76359
76482
  return undefined;
76360
76483
  }
76361
- const measure = measureItemSize(listEl, horizontal);
76484
+ const measure = measureItemSize(listEl, horizontal, inLines);
76362
76485
  if (measure) {
76363
76486
  const samples = samplesRef.current;
76364
- samples.sum = measure.size * measure.itemCount;
76365
- samples.count = measure.itemCount;
76487
+ samples.sum = measure.size * measure.lineCount;
76488
+ samples.count = measure.lineCount;
76366
76489
  samples.fromSkeletons = measure.fromSkeletons;
76367
76490
  virtualSizeSignal.value = measure.size;
76368
76491
  return undefined;
@@ -76507,14 +76630,17 @@ const Fallback = ({
76507
76630
  // Reads the item size itself: it is what the size is for, and a run holding
76508
76631
  // every item it draws must not be redrawn — every item of it — because the size
76509
76632
  // settled after the first commit.
76633
+ // It holds lines: the items side by side on one take its room together, and a
76634
+ // last line left short takes it all the same.
76510
76635
  const VirtualFiller = ({
76511
76636
  edge,
76512
76637
  itemCount,
76638
+ itemsPerLine,
76513
76639
  findChunks
76514
76640
  }) => {
76515
76641
  const listItems = useContext(ListItemsContext);
76516
- const virtualItemSize = listItems.virtualItemSizeSignal.value;
76517
- const sizeToFill = itemCount * virtualItemSize;
76642
+ const lineSize = listItems.virtualItemSizeSignal.value;
76643
+ const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize;
76518
76644
  // A filler resizing moves what stands below it — the items on screen, when it
76519
76645
  // holds the room of items above them — and it resizes in a commit of its own
76520
76646
  // when the item size settles after the list has rendered: the list puts its
@@ -76533,7 +76659,7 @@ const VirtualFiller = ({
76533
76659
  "aria-hidden": true,
76534
76660
  style: {
76535
76661
  "--size-to-fill": `${sizeToFill}px`,
76536
- "--x-find-line-size": findChunks ? `${virtualItemSize}px` : undefined
76662
+ "--x-find-line-size": findChunks ? `${lineSize / itemsPerLine}px` : undefined
76537
76663
  },
76538
76664
  children: findChunks && findChunks.map(chunk => jsx("div", {
76539
76665
  className: "navi_list_find_stand_in",
@@ -77416,6 +77542,9 @@ const useRunItems = (store, {
77416
77542
  const rankOf = itemIndex => itemIndex - runStart;
77417
77543
  const indexOfRank = rank => rank + runStart;
77418
77544
  const getItemAt = itemIndex => store.getItem(rankOf(itemIndex));
77545
+ const {
77546
+ itemsPerLine
77547
+ } = renderWindow;
77419
77548
  const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;
77420
77549
  const windowTo = renderWindow.end < runEnd ? renderWindow.end : runEnd;
77421
77550
  store.forget(rankOf(windowFrom), rankOf(windowTo));
@@ -77523,8 +77652,20 @@ const useRunItems = (store, {
77523
77652
  // band is drawn among the items. A negative start is not a rank but a count
77524
77653
  // back from the end (the very first ask, before the count is known) — there
77525
77654
  // 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);
77655
+ let failureFrom = store.failure === null ? -1 : store.failure.start < 0 || indexOfRank(store.failure.start) < windowFrom ? windowFrom : indexOfRank(store.failure.start);
77656
+ let failureTo = store.failure === null ? -1 : store.failure.end < 0 || indexOfRank(store.failure.end) > windowTo - 1 ? windowTo - 1 : indexOfRank(store.failure.end);
77657
+ if (store.failure !== null && itemsPerLine > 1) {
77658
+ // In a grid the band takes whole lines, like the window: begun mid-line,
77659
+ // it would push every item after it into another column.
77660
+ failureFrom -= failureFrom % itemsPerLine;
77661
+ if (failureFrom < windowFrom) {
77662
+ failureFrom = windowFrom;
77663
+ }
77664
+ failureTo += itemsPerLine - 1 - failureTo % itemsPerLine;
77665
+ if (failureTo > windowTo - 1) {
77666
+ failureTo = windowTo - 1;
77667
+ }
77668
+ }
77528
77669
  const nodes = [];
77529
77670
  // Items that belong together, as the data says (a day of messages, a month of
77530
77671
  // games): consecutive items sharing a group key are wrapped in one group, so
@@ -77601,6 +77742,7 @@ const useRunItems = (store, {
77601
77742
  nodes.push(jsx(VirtualFiller, {
77602
77743
  edge: "before",
77603
77744
  itemCount: windowFrom - runStart,
77745
+ itemsPerLine: itemsPerLine,
77604
77746
  findChunks: getFindChunks(runStart, windowFrom)
77605
77747
  }, "navi-list-filler-before"));
77606
77748
  }
@@ -77615,7 +77757,7 @@ const useRunItems = (store, {
77615
77757
  nodes.push(jsx("li", {
77616
77758
  className: "navi_list_failed_items",
77617
77759
  style: {
77618
- "--size-to-fill": `${failedItemCount * listItems.virtualItemSizeSignal.value}px`
77760
+ "--size-to-fill": `${Math.ceil(failedItemCount / itemsPerLine) * listItems.virtualItemSizeSignal.value}px`
77619
77761
  },
77620
77762
  children: renderError ? renderError({
77621
77763
  error: store.failure.error,
@@ -77705,6 +77847,7 @@ const useRunItems = (store, {
77705
77847
  nodes.push(jsx(VirtualFiller, {
77706
77848
  edge: "after",
77707
77849
  itemCount: runEnd - windowTo,
77850
+ itemsPerLine: itemsPerLine,
77708
77851
  findChunks: getFindChunks(windowTo, runEnd)
77709
77852
  }, "navi-list-filler-after"));
77710
77853
  }
@@ -78437,12 +78580,16 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
78437
78580
  * @param {string} [props.columns]
78438
78581
  * The list's own columns: a `grid-template-columns` value the ITEMS are laid
78439
78582
  * 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.
78583
+ * choices (`repeat(3, minmax(0, 1fr))`), a feed of covers
78584
+ * (`repeat(auto-fill, minmax(150px, 1fr))` over a `<List.Items>`). Each item
78585
+ * takes one cell, and an item meant to take a whole line says so for itself
78586
+ * (`style={{ gridColumn: "1 / -1" }}`). A `<List.Items>` run is drawn in whole
78587
+ * lines — as many items to a line as the template resolves to, read again
78588
+ * when the list's width changes: its window starts and ends on a line, so an
78589
+ * item is always in the column its place says, and the fillers hold the room
78590
+ * of the lines it does not draw (`virtualItemSize` is then a line's size).
78591
+ * That count holds only while every item of the run takes one cell: an item
78592
+ * spanning a line, or a `groupBy`, puts the ones after it in other columns.
78446
78593
  * @param {string} [props.itemColumns]
78447
78594
  * The columns inside an ITEM: a `grid-template-columns` value each item fills
78448
78595
  * with its own children, a table whose cells line up down the list. Every item
@@ -78458,8 +78605,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
78458
78605
  * scroll box, which fills it and has nothing to arrange.
78459
78606
  * @param {boolean} [props.flexWrap]
78460
78607
  * 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.
78608
+ * past the edge — a row of choices under a `maxWidth`, say. A wrapped line
78609
+ * holding several items is not virtualizable: a long collection laid several
78610
+ * to a line goes in `columns`.
78463
78611
  * @param {string} [props.overflow]
78464
78612
  * `"visible"` lets the items paint outside the list — a check in an item's
78465
78613
  * corner, a badge crossing the edge. A list clips by default, which is what
@@ -78578,7 +78726,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
78578
78726
  * one-line size) — an item guessed too small builds one more, an item
78579
78727
  * guessed too big leaves a blank (see docs/scroll.md, "What the list knows,
78580
78728
  * and what it guesses"). The items drawn are measured either way: the render
78581
- * window is sized on them.
78729
+ * window is sized on them. In a grid (`columns`), the size is a line's: a
78730
+ * card's, which the items side by side on it share.
78582
78731
  * @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
78583
78732
  * Which box scrolls — and with it, which box the render window follows and
78584
78733
  * which box a scroll position is read from (`onScrolledChange`). `"self"`