@jsenv/navi 0.29.446 → 0.29.448

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.
@@ -75001,6 +75001,7 @@ const css$x = /* css */`@property --list-group-label-height {
75001
75001
  }
75002
75002
 
75003
75003
  .navi_list {
75004
+ --x-list-item-between: 0px;
75004
75005
  box-sizing: border-box;
75005
75006
  outline: none;
75006
75007
  margin: 0;
@@ -75937,7 +75938,7 @@ const useListScrollSync = ({
75937
75938
  setScrollerElResolved(current => current === scrollerElNow ? current : scrollerElNow);
75938
75939
  };
75939
75940
  useLayoutEffect(resolveScroller);
75940
- useStickyScrollportWarning(ref, scroller);
75941
+ useStickyScrollportWarning(ref, scroller, getScroller, horizontal);
75941
75942
  useDuplicateHeaderWarning(ref);
75942
75943
  useStuckWindowWarning({
75943
75944
  ref,
@@ -76013,6 +76014,32 @@ const useListScrollSync = ({
76013
76014
  }
76014
76015
  });
76015
76016
 
76017
+ // What stands between two items along the axis — the gap, a separator and
76018
+ // the gaps around it — which an item on its way takes out of the room it is
76019
+ // given (see getSkeletonItem): virtualItemSize is an item's room, from the
76020
+ // end of the item before to its own end, and a skeleton whose box took all
76021
+ // of it would be taller than the item replacing it, moving everything below
76022
+ // by the difference. Read before the anchoring of this commit runs, and
76023
+ // written to the list's own box so the skeletons follow without rendering.
76024
+ // Items side by side in a grid share a line: none stands between them.
76025
+ useLayoutEffect(() => {
76026
+ if (columns || !ref.current || !listItems.hasRuns()) {
76027
+ return;
76028
+ }
76029
+ const listEl = getListEl();
76030
+ if (!listEl) {
76031
+ return;
76032
+ }
76033
+ const spaceBetween = readSpaceBetweenItems(listEl, horizontal);
76034
+ if (spaceBetween === null) {
76035
+ return;
76036
+ }
76037
+ const value = `${spaceBetween}px`;
76038
+ if (listEl.style.getPropertyValue("--x-list-item-between") !== value) {
76039
+ listEl.style.setProperty("--x-list-item-between", value);
76040
+ }
76041
+ });
76042
+
76016
76043
  // What the window holds goes through three stages (see settleWindow):
76017
76044
  // - "first": the first commit, a count of items since nothing is laid out
76018
76045
  // yet to measure a size with — `initial` or the budget when they are one,
@@ -77502,25 +77529,41 @@ const canScrollerScroll = (scrollerEl, axis) => {
77502
77529
  return scrollSize - clientSize > 1;
77503
77530
  };
77504
77531
  // What a sticky part of the list sticks to is the nearest scroll container in
77505
- // the DOM; `scroller` has no say in it. A list told the page scrolls it can
77506
- // therefore have its group labels and its header stuck to a wrapper that never
77507
- // scrolls — and pushed down by that wrapper's scroll-padding on top of it. The
77508
- // usual culprit is an app wrapper carrying `overflow-x: auto` to keep the
77509
- // document from overflowing horizontally on mobile; `overflow-x: clip` keeps
77510
- // that guarantee without making a scroll container.
77532
+ // the DOM; `scroller` has no say in it. A list scrolled by a box around it can
77533
+ // therefore have its group labels and its header stuck to a wrapper between
77534
+ // the two that never scrolls along that axis — and pushed down by that
77535
+ // wrapper's scroll-padding on top of it. The usual culprit is a wrapper
77536
+ // carrying `overflow-x: auto`: to keep the document from overflowing
77537
+ // horizontally on mobile, where `overflow-x: clip` keeps that guarantee without
77538
+ // making a scroll container, or to let a wide table scroll sideways, where the
77539
+ // header cannot stick to both boxes.
77511
77540
  const STICKY_LIST_PART_SELECTOR = `.navi_list_item_header, .navi_list_item_footer, .navi_list_item_group_label`;
77512
- const useStickyScrollportWarning = (ref, scroller) => {
77541
+ const useStickyScrollportWarning = (ref, scroller, getScroller, horizontal) => {
77513
77542
  const doneRef = useRef(false);
77514
77543
  useLayoutEffect(() => {
77515
- if (doneRef.current || scroller !== "document") {
77544
+ if (doneRef.current) {
77545
+ return;
77546
+ }
77547
+ const scrollerNamed = scroller && typeof scroller === "object";
77548
+ if (scroller !== "document" && scroller !== "parent" && !scrollerNamed) {
77549
+ // The list scrolls itself: its sticky parts stick to its own box.
77550
+ return;
77551
+ }
77552
+ if (scrollerNamed && !(scroller.nodeType === 1 || scroller.current)) {
77553
+ // The ref is not set yet: the box that scrolls is not known.
77516
77554
  return;
77517
77555
  }
77518
77556
  const listContainerEl = ref.current;
77519
77557
  if (!listContainerEl) {
77520
77558
  return;
77521
77559
  }
77560
+ const scrollerEl = getScroller();
77561
+ if (scroller === "parent" && scrollerEl !== document.scrollingElement && !canScroll(scrollerEl, horizontal ? "x" : "y")) {
77562
+ // Measured: until the box around scrolls, which one it is is not known.
77563
+ return;
77564
+ }
77522
77565
  const scrollportEl = findScrollportEl(listContainerEl);
77523
- if (!scrollportEl) {
77566
+ if ((scrollportEl || document.scrollingElement) === scrollerEl) {
77524
77567
  doneRef.current = true;
77525
77568
  return;
77526
77569
  }
@@ -77530,8 +77573,10 @@ const useStickyScrollportWarning = (ref, scroller) => {
77530
77573
  return;
77531
77574
  }
77532
77575
  doneRef.current = true;
77533
- console.warn(`<List scroller="document"> is inside ${getElementSignature(scrollportEl)}, a scroll container: its sticky group labels and header stick to that box instead of to the page. Give that box "overflow: clip" (it clips without creating a scroll container), or tell the list about it with scroller={element}.`, {
77576
+ const scrolledBy = scrollerEl === document.scrollingElement ? "the page" : getElementSignature(scrollerEl);
77577
+ console.warn(`<List> is scrolled by ${scrolledBy}, but it is inside ${getElementSignature(scrollportEl)}, a scroll container closer to it: its sticky header and group labels stick to that box instead. A box there to clip: give it "overflow: clip", which clips without creating a scroll container. A box that scrolls the list: name it, scroller={element}. A box there to scroll a wide list sideways: the sticky parts cannot stick to both boxes — let ${scrolledBy} scroll sideways too, or keep the header at the top of the list.`, {
77534
77578
  list: listContainerEl,
77579
+ scroller: scrollerEl,
77535
77580
  scrollport: scrollportEl,
77536
77581
  sticky: stickyEl
77537
77582
  });
@@ -78045,6 +78090,32 @@ const findItemsFrom = (listEl, from, horizontal) => {
78045
78090
  // items (a card holding a list of its own): both carry the same attributes.
78046
78091
  // Groups nest the items in a list of their own class, not in another list.
78047
78092
  const isOwnItem = (itemEl, listEl) => itemEl.closest(".navi_list") === listEl;
78093
+ // From where an item ends to where the item after it starts, read on the first
78094
+ // two that follow each other in the same container (a group's label stands
78095
+ // between two items of different groups). Null when no two items do.
78096
+ const readSpaceBetweenItems = (listEl, horizontal) => {
78097
+ let previous = null;
78098
+ for (const itemEl of listEl.querySelectorAll(`[${LIST_ITEM_INDEX_ATTRIBUTE}]`)) {
78099
+ if (!isOwnItem(itemEl, listEl)) {
78100
+ continue;
78101
+ }
78102
+ const rect = itemEl.getBoundingClientRect();
78103
+ if (rect.width === 0 && rect.height === 0) {
78104
+ previous = null;
78105
+ continue;
78106
+ }
78107
+ const index = Number(itemEl.getAttribute(LIST_ITEM_INDEX_ATTRIBUTE));
78108
+ if (previous && previous.index === index - 1 && previous.containerEl === itemEl.parentElement) {
78109
+ return (horizontal ? rect.left : rect.top) - previous.to;
78110
+ }
78111
+ previous = {
78112
+ index,
78113
+ containerEl: itemEl.parentElement,
78114
+ to: horizontal ? rect.right : rect.bottom
78115
+ };
78116
+ }
78117
+ return null;
78118
+ };
78048
78119
  // The item drawn at an index of the list, a stand-in or a real one.
78049
78120
  const findItemElementAt = (listEl, index) => {
78050
78121
  for (const itemEl of listEl.querySelectorAll(`[${LIST_ITEM_INDEX_ATTRIBUTE}="${index}"]`)) {
@@ -78160,7 +78231,10 @@ const readItemRooms = (listEl, horizontal, onRoom) => {
78160
78231
  // Where what stands before an item in the list ends: the element before the
78161
78232
  // item, or before its group when it is the first of it — or the start of the
78162
78233
  // list's content when nothing does. Null when the element before is not laid
78163
- // out.
78234
+ // out. It is where that element ends in the flow: where the item (or its
78235
+ // group) starts, less the gap and the margins between the two. Its own box is
78236
+ // where it is painted, which a sticky header stuck to the top of the screen
78237
+ // puts thousands of pixels away from its place in the list.
78164
78238
  const readEndBefore = (itemEl, listEl, horizontal) => {
78165
78239
  let slotEl = itemEl;
78166
78240
  const groupListEl = itemEl.parentElement;
@@ -78169,11 +78243,20 @@ const readEndBefore = (itemEl, listEl, horizontal) => {
78169
78243
  }
78170
78244
  const beforeEl = slotEl.previousElementSibling;
78171
78245
  if (beforeEl) {
78172
- const rect = beforeEl.getBoundingClientRect();
78173
- if (rect.width === 0 && rect.height === 0) {
78246
+ const beforeRect = beforeEl.getBoundingClientRect();
78247
+ if (beforeRect.width === 0 && beforeRect.height === 0) {
78174
78248
  return null;
78175
78249
  }
78176
- return horizontal ? rect.right : rect.bottom;
78250
+ const slotRect = slotEl.getBoundingClientRect();
78251
+ const containerStyle = window.getComputedStyle(slotEl.parentElement);
78252
+ const slotStyle = window.getComputedStyle(slotEl);
78253
+ const beforeStyle = window.getComputedStyle(beforeEl);
78254
+ // A gap the browser computes as "normal" is none in a flex or grid list.
78255
+ const lengthOf = value => parseFloat(value) || 0;
78256
+ if (horizontal) {
78257
+ return slotRect.left - lengthOf(containerStyle.columnGap) - lengthOf(slotStyle.marginLeft) - lengthOf(beforeStyle.marginRight);
78258
+ }
78259
+ return slotRect.top - lengthOf(containerStyle.rowGap) - lengthOf(slotStyle.marginTop) - lengthOf(beforeStyle.marginBottom);
78177
78260
  }
78178
78261
  if (slotEl.parentElement !== listEl) {
78179
78262
  return null;
@@ -79503,10 +79586,12 @@ const VISIBILITY_HIDDEN_STYLE = {
79503
79586
  * A collection read a slice at a time comes from `itemsAction(range)`: the run
79504
79587
  * asks for what it is about to draw and keeps what it gets. The range says the
79505
79588
  * same thing three ways, so a source can read it however it paginates —
79506
- * `{ start, end }` (places in the collection, a negative `start` counting back
79507
- * from the end like `Range: items=-25`, which is what a list opening on its
79508
- * last items asks for before it knows how many there are), `limit` (how many
79509
- * items), and
79589
+ * `{ start, end }` (places in the collection, `end` included as in
79590
+ * `Range: items=0-99`; a negative `start` counting back from the end like
79591
+ * `Range: items=-25`, which is what a list opening on its last items asks for
79592
+ * before it knows how many there are), `limit` (how many items:
79593
+ * `end - start + 1` — an API whose end is excluded is called with
79594
+ * `start + limit`), and
79510
79595
  * `before`/`after`/`around` (the id of an item to count from, for a source
79511
79596
  * paginating by cursor). Answer with the items (an array — that is all of
79512
79597
  * them), or with a range the way a Content-Range does: `{ items, start, count }`
@@ -79521,10 +79606,11 @@ const VISIBILITY_HIDDEN_STYLE = {
79521
79606
  * (see docs/resource.md).
79522
79607
  *
79523
79608
  * A collection that changes as a whole (a search reordering it) is a different
79524
- * collection: with `items`, another array is another collection and the run
79525
- * draws it from its first item; with `itemsAction`, give the run a `key` that
79526
- * changes with it, the way one does for anything else that is not the same
79527
- * thing anymore.
79609
+ * collection, and the run draws it from its first item: with `items`, another
79610
+ * array is another collection; with `itemsAction`, another reader is — a
79611
+ * reader bound to other params (`bindParams({ q })` with another `q`). A param
79612
+ * held in a signal changes what the reader reads without changing the reader:
79613
+ * give the run a `key` that follows it.
79528
79614
  *
79529
79615
  * An item says what it is where it is drawn: `renderItem` returns a
79530
79616
  * `<List.Item>` carrying its own props (`selectable`, `value`, `selected`…),
@@ -79539,6 +79625,7 @@ const VISIBILITY_HIDDEN_STYLE = {
79539
79625
  * findText?: (item: any, index: number) => string,
79540
79626
  * items?: any[],
79541
79627
  * itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
79628
+ * debounce?: number,
79542
79629
  * count?: number,
79543
79630
  * groupBy?: (item: any, index: number) => any,
79544
79631
  * renderGroupLabel?: (item: any, index: number) => import("ignore:preact").ComponentChildren,
@@ -79549,6 +79636,7 @@ const VISIBILITY_HIDDEN_STYLE = {
79549
79636
  * itemSize?: (index: number) => number,
79550
79637
  * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
79551
79638
  * onRequestStateChange?: (state: {busy: boolean, refreshing: boolean, range: {start: number, end: number}|null}) => void,
79639
+ * onCountChange?: (count: number|undefined) => void,
79552
79640
  * }>}
79553
79641
  * @param {(item: any, index: number, state: {refreshing: boolean}) => any} props.renderItem
79554
79642
  * What is drawn for one item, given its data and where it sits — its place in the list,
@@ -79573,6 +79661,12 @@ const VISIBILITY_HIDDEN_STYLE = {
79573
79661
  * @param {(range: object) => any} [props.itemsAction]
79574
79662
  * Where the items come from when the collection is read a slice at a time:
79575
79663
  * a resource's range reader (`RESOURCE.GET_RANGE.bindParams(...)`).
79664
+ * @param {number} [props.debounce]
79665
+ * Milliseconds the reader must hold still before the run draws the
79666
+ * collection it reads: params following a field as the user types
79667
+ * (`bindParams({ q })`) ask once typing stops, not once per key. Meanwhile
79668
+ * the run keeps drawing the collection from before, `refreshing`. The
79669
+ * first collection is drawn at once.
79576
79670
  * @param {(item: any, index: number) => any} [props.groupBy]
79577
79671
  * What tells items that belong together apart from the others — the day of a
79578
79672
  * message, the month of a game. Consecutive items sharing it are wrapped in a
@@ -79636,24 +79730,101 @@ const VISIBILITY_HIDDEN_STYLE = {
79636
79730
  * collection's own ranks as `itemsAction` sees them, `null` once nothing is.
79637
79731
  * A range called off and asked again right away stays one `busy`, and a list
79638
79732
  * unmounted while asking says `busy: false` on its way out.
79733
+ * @param {(count: number|undefined) => void} [props.onCountChange]
79734
+ * Called with how many items the collection holds when that changes — for
79735
+ * the screen around the list to say it ("5 of 83 books"): the `count` the
79736
+ * source answers with each slice, or the one given to the run.
79737
+ * `undefined` until it is known; a run drawing another collection says it
79738
+ * again, so the number always belongs to the collection on screen.
79639
79739
  */
79640
79740
  const ListItems = ({
79741
+ itemsAction,
79742
+ debounce,
79743
+ ...props
79744
+ }) => {
79745
+ // Read so that this component renders in the pass of the List moving its
79746
+ // window, and the run with it: the List frames its window on the items its
79747
+ // runs drew in that same commit (see settleWindow). Skipped as an unchanged
79748
+ // child, it would leave the run to a render of its own, after that commit.
79749
+ useContext(RenderWindowContext);
79750
+ const {
79751
+ reader,
79752
+ settling
79753
+ } = useSettledReader(itemsAction, debounce);
79754
+ return jsx(ListItemsRun
79755
+ // Another reader is another collection — bindParams gives back the
79756
+ // reader it made for equal params — and the run holds the ranks, the
79757
+ // count and the asks of the one it draws.
79758
+ , {
79759
+ itemsAction: reader,
79760
+ settling: settling,
79761
+ ...props
79762
+ }, reader && reader.isRangeReader ? readerKeyOf(reader) : undefined);
79763
+ };
79764
+ const ListItemsRun = ({
79641
79765
  items,
79642
79766
  itemsAction,
79767
+ settling,
79643
79768
  count,
79644
79769
  memoryBudget,
79645
79770
  onRequestStateChange,
79771
+ onCountChange,
79646
79772
  ...runProps
79647
79773
  }) => {
79648
79774
  const store = useItemStore({
79649
79775
  items,
79650
79776
  count,
79651
79777
  itemsAction,
79778
+ settling,
79652
79779
  memoryBudget,
79653
- onRequestStateChange
79780
+ onRequestStateChange,
79781
+ onCountChange
79654
79782
  });
79655
79783
  return useRunItems(store, runProps);
79656
79784
  };
79785
+ const readerKeyByReader = new WeakMap();
79786
+ let readerKeyPrevious = 0;
79787
+ const readerKeyOf = reader => {
79788
+ let key = readerKeyByReader.get(reader);
79789
+ if (key === undefined) {
79790
+ readerKeyPrevious++;
79791
+ key = readerKeyPrevious;
79792
+ readerKeyByReader.set(reader, key);
79793
+ }
79794
+ return key;
79795
+ };
79796
+ // The reader the run draws: the one it is given, or under `debounce` the one
79797
+ // it was drawing until the one given has held still that long. Only between
79798
+ // two range readers — the first collection is drawn at once, and a collection
79799
+ // held in memory costs no request to follow.
79800
+ const useSettledReader = (itemsAction, debounce) => {
79801
+ const settledRef = useRef(itemsAction);
79802
+ const [, setSettleCount] = useState(0);
79803
+ const settled = settledRef.current;
79804
+ const waits = Boolean(debounce) && itemsAction !== settled && Boolean(itemsAction && itemsAction.isRangeReader) && Boolean(settled && settled.isRangeReader);
79805
+ if (!waits) {
79806
+ settledRef.current = itemsAction;
79807
+ }
79808
+ useLayoutEffect(() => {
79809
+ if (!waits) {
79810
+ return undefined;
79811
+ }
79812
+ const timeout = setTimeout(() => {
79813
+ settledRef.current = itemsAction;
79814
+ setSettleCount(settleCount => settleCount + 1);
79815
+ }, debounce);
79816
+ return () => {
79817
+ clearTimeout(timeout);
79818
+ };
79819
+ }, [waits, itemsAction, debounce]);
79820
+ return waits ? {
79821
+ reader: settled,
79822
+ settling: true
79823
+ } : {
79824
+ reader: itemsAction,
79825
+ settling: false
79826
+ };
79827
+ };
79657
79828
 
79658
79829
  // The items a `loading` list is told to expect (loadingSkeletonCount): a run
79659
79830
  // holding none of them, and asking for none — they arrive through the app.
@@ -79734,10 +79905,13 @@ const useRunItems = (store, {
79734
79905
  skeletonItem = {};
79735
79906
  const virtualItemSize = listItems.virtualItemSizeSignal.value;
79736
79907
  if (virtualItemSize) {
79908
+ // The room is the item's and what stands before it (see
79909
+ // readSpaceBetweenItems): the box takes what is left of it.
79910
+ const minSize = `calc(${virtualItemSize}px - var(--x-list-item-between, 0px))`;
79737
79911
  if (listItems.horizontal) {
79738
- skeletonItem.itemMinWidth = `${virtualItemSize}px`;
79912
+ skeletonItem.itemMinWidth = minSize;
79739
79913
  } else {
79740
- skeletonItem.itemMinHeight = `${virtualItemSize}px`;
79914
+ skeletonItem.itemMinHeight = minSize;
79741
79915
  }
79742
79916
  }
79743
79917
  return skeletonItem;
@@ -80271,6 +80445,7 @@ const ListItemsFailure = ({
80271
80445
  // hits the network again.
80272
80446
  const ITEM_STORE_MAX_DEFAULT = 1000;
80273
80447
  const ITEM_STORE_KEEP_AROUND = 250;
80448
+ const COUNT_NOT_SAID = {};
80274
80449
  const rangeIsSame = (a, b) => {
80275
80450
  if (!a || !b) {
80276
80451
  return a === b;
@@ -80287,8 +80462,10 @@ const useItemStore = ({
80287
80462
  items,
80288
80463
  count,
80289
80464
  itemsAction,
80465
+ settling,
80290
80466
  memoryBudget,
80291
- onRequestStateChange
80467
+ onRequestStateChange,
80468
+ onCountChange
80292
80469
  }) => {
80293
80470
  // A collection given whole (`items`) is held from the first render: nothing
80294
80471
  // to ask for, nothing to keep across mounts, nothing to invalidate. The rest
@@ -80423,15 +80600,37 @@ const useItemStore = ({
80423
80600
  // It stands for a page of them: a list that is about to be filled looks
80424
80601
  // like items on their way, not like an empty list.
80425
80602
  const itemCount = pages.count ?? count ?? listItems.pageSize;
80603
+ // The items drawn are from before while they are read again, and while the
80604
+ // collection that replaces them waits for its params to hold still (see
80605
+ // useSettledReader).
80606
+ const refreshingShown = refreshing || Boolean(settling);
80426
80607
  useLayoutEffect(() => {
80427
- if (!refreshing) {
80608
+ if (!refreshingShown) {
80428
80609
  return null;
80429
80610
  }
80430
80611
  listItems.refreshingSignal.value = listItems.refreshingSignal.peek() + 1;
80431
80612
  return () => {
80432
80613
  listItems.refreshingSignal.value = listItems.refreshingSignal.peek() - 1;
80433
80614
  };
80434
- }, [refreshing]);
80615
+ }, [refreshingShown]);
80616
+
80617
+ // How many items the collection holds, for the screen around the list to
80618
+ // say: the count the source answered or the caller gave, never the page the
80619
+ // run stands for before it knows. Said by each run on its first commit, so a
80620
+ // run drawing another collection takes back what the one before said.
80621
+ const countKnown = pages.count ?? count;
80622
+ const onCountChangeRef = useRef(onCountChange);
80623
+ onCountChangeRef.current = onCountChange;
80624
+ const countSaidRef = useRef(COUNT_NOT_SAID);
80625
+ useLayoutEffect(() => {
80626
+ if (countSaidRef.current === countKnown) {
80627
+ return;
80628
+ }
80629
+ countSaidRef.current = countKnown;
80630
+ if (onCountChangeRef.current) {
80631
+ onCountChangeRef.current(countKnown);
80632
+ }
80633
+ });
80435
80634
 
80436
80635
  // What the run is doing, for the screen around the list to draw: the list has
80437
80636
  // its own skeletons, what is around it has to be told. Read from the request
@@ -80480,7 +80679,7 @@ const useItemStore = ({
80480
80679
  const store = {
80481
80680
  itemCount,
80482
80681
  failure,
80483
- refreshing,
80682
+ refreshing: refreshingShown,
80484
80683
  // JS memory is cheap next to the DOM, but a long enough scroll accumulates
80485
80684
  // everything it ever went through. Items far from what is on screen are
80486
80685
  // dropped and simply asked for again if the user goes back — the same
@@ -81137,8 +81336,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
81137
81336
  * @param {number} [props.virtualItemSize]
81138
81337
  * The room an item never drawn is held at, in px along the scroll axis: what
81139
81338
  * the fillers hold for it outside the render window, what a scroll position
81140
- * inside them is read with, and the least an item on its way takes (see
81141
- * `renderSkeleton`). An item the list has drawn is held at the room it took
81339
+ * inside them is read with, and the least room an item on its way takes
81340
+ * (see `renderSkeleton`) — a room counts what stands before the item, a
81341
+ * gap or a separator, so the item's own box takes the rest of it. An item the list has drawn is held at the room it took
81142
81342
  * then, and an item whose run says its room (`<List.Items itemSize>`) at
81143
81343
  * that room. Left out, it is the average of the items measured so far — once when
81144
81344
  * the list mounts, again when a popup around it opens, and after each commit
@@ -95716,6 +95916,12 @@ const DEFAULT_PLACEHOLDER_LIGHT = "#d4d8dd";
95716
95916
  const DEFAULT_PLACEHOLDER_DARK = "#374151";
95717
95917
 
95718
95918
  /**
95919
+ * The props may come in any order: `src` and `srcset` reach the element after
95920
+ * `loading` and the other hints. A plain `<img>` (or `<Box as="img">`) written
95921
+ * in JSX must put `loading` before `src`: WebKit, so every browser on iOS,
95922
+ * decides whether to load when `src` is set, and an image whose `src` came
95923
+ * first loads, lazy or not.
95924
+ *
95719
95925
  * @param {string|false} [props.placeholderColor] - Background color shown while the image loads.
95720
95926
  * Defaults to a light gray. Pass `false` to disable.
95721
95927
  * @param {boolean} [props.placeholderDark] - Use a dark default placeholder color,
@@ -95725,6 +95931,9 @@ const DEFAULT_PLACEHOLDER_DARK = "#374151";
95725
95931
  const Image = ({
95726
95932
  placeholderColor,
95727
95933
  placeholderDark = false,
95934
+ srcset,
95935
+ srcSet,
95936
+ src,
95728
95937
  ...rest
95729
95938
  }) => {
95730
95939
  import.meta.css = [css$5, "@jsenv/navi/src/graphic/image.jsx"];
@@ -95749,6 +95958,13 @@ const Image = ({
95749
95958
  imageEl.setAttribute("navi-loaded", "");
95750
95959
  rest.onLoad?.(e);
95751
95960
  }
95961
+ // Last, srcset before src: an <img> decides its request when they are
95962
+ // set, with the attributes it has at that moment. WebKit loads an image
95963
+ // whose src arrives before loading="lazy", and does not take it back.
95964
+ ,
95965
+ srcset: srcset,
95966
+ srcSet: srcSet,
95967
+ src: src
95752
95968
  });
95753
95969
  };
95754
95970