@jsenv/navi 0.29.425 → 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.
- package/dist/dev/jsenv_navi.js +600 -198
- package/dist/dev/jsenv_navi.js.map +22 -20
- package/dist/dev/jsenv_navi_side_effects.js +1 -5
- package/dist/dev/jsenv_navi_side_effects.js.map +2 -2
- package/dist/jsenv_navi.js +597 -195
- package/dist/jsenv_navi.js.map +22 -20
- package/dist/jsenv_navi_side_effects.js +1 -5
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +9 -5
- package/docs/control_object.md +76 -2
- package/docs/css_architecture.md +1 -1
- package/docs/navigation.md +4 -1
- package/docs/popup_open.md +6 -3
- package/docs/scroll.md +53 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchIntern
|
|
|
7
7
|
export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
|
|
8
8
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
9
9
|
import { options, isValidElement, createContext, render, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
|
|
10
|
-
import { useErrorBoundary, useLayoutEffect, useContext, useMemo, useRef, useEffect, useCallback,
|
|
10
|
+
import { useErrorBoundary, useLayoutEffect, useContext, useState, useMemo, useRef, useEffect, useCallback, useReducer, useId } from "preact/hooks";
|
|
11
11
|
import { humanizeI18n, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
|
|
12
12
|
export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
|
|
13
13
|
import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
|
|
@@ -10812,6 +10812,179 @@ const dropGeneratedIdKeys = (state) => {
|
|
|
10812
10812
|
return stateWithoutGeneratedIdKeys;
|
|
10813
10813
|
};
|
|
10814
10814
|
|
|
10815
|
+
const FRAME_TIMEOUT_MS = 100;
|
|
10816
|
+
|
|
10817
|
+
/**
|
|
10818
|
+
* Calls back once the browser has painted what is committed now.
|
|
10819
|
+
*
|
|
10820
|
+
* The callbacks of the next frame run before that frame paints, so the call is
|
|
10821
|
+
* made from a task queued from inside one — the same way preact schedules
|
|
10822
|
+
* useEffect. Unlike an effect, nothing runs this early: preact flushes a
|
|
10823
|
+
* component's pending effects as soon as that component renders again, and a
|
|
10824
|
+
* re-render before the paint is exactly what a caller waiting for the paint
|
|
10825
|
+
* has to survive. A frame not coming at all (a background tab) still answers,
|
|
10826
|
+
* late, through the timeout.
|
|
10827
|
+
*
|
|
10828
|
+
* @param {() => void} callback
|
|
10829
|
+
* @returns {() => void} cancel
|
|
10830
|
+
*/
|
|
10831
|
+
const afterPaint = (callback) => {
|
|
10832
|
+
let called = false;
|
|
10833
|
+
let timeoutId;
|
|
10834
|
+
let frameId;
|
|
10835
|
+
const onFrame = () => {
|
|
10836
|
+
if (called) {
|
|
10837
|
+
return;
|
|
10838
|
+
}
|
|
10839
|
+
called = true;
|
|
10840
|
+
clearTimeout(timeoutId);
|
|
10841
|
+
cancelAnimationFrame(frameId);
|
|
10842
|
+
timeoutId = setTimeout(callback, 0);
|
|
10843
|
+
};
|
|
10844
|
+
timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
|
|
10845
|
+
frameId = requestAnimationFrame(onFrame);
|
|
10846
|
+
return () => {
|
|
10847
|
+
called = true;
|
|
10848
|
+
clearTimeout(timeoutId);
|
|
10849
|
+
cancelAnimationFrame(frameId);
|
|
10850
|
+
};
|
|
10851
|
+
};
|
|
10852
|
+
|
|
10853
|
+
/**
|
|
10854
|
+
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
10855
|
+
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
10856
|
+
* mounted run inside the call too, exactly as they would on any other commit.
|
|
10857
|
+
*
|
|
10858
|
+
* For the caller that has to read the DOM it just asked for — measuring an
|
|
10859
|
+
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
10860
|
+
* to do it, because what comes after is a browser event still in flight
|
|
10861
|
+
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
10862
|
+
*
|
|
10863
|
+
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
10864
|
+
* render queue drains; swapping it for "right now" for the duration of the call
|
|
10865
|
+
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
10866
|
+
* case above: rendering synchronously in the middle of an event gives up the
|
|
10867
|
+
* batching that makes several state changes one commit.
|
|
10868
|
+
*/
|
|
10869
|
+
const flushSyncRendering = (fn) => {
|
|
10870
|
+
const debounceRenderingPrevious = options.debounceRendering;
|
|
10871
|
+
options.debounceRendering = (drainRenderQueue) => {
|
|
10872
|
+
drainRenderQueue();
|
|
10873
|
+
};
|
|
10874
|
+
try {
|
|
10875
|
+
fn();
|
|
10876
|
+
} finally {
|
|
10877
|
+
options.debounceRendering = debounceRenderingPrevious;
|
|
10878
|
+
}
|
|
10879
|
+
};
|
|
10880
|
+
|
|
10881
|
+
/**
|
|
10882
|
+
* The content of a `<Box mount="after-paint">`: built right after the first
|
|
10883
|
+
* frame that shows the box, unless that frame would show it.
|
|
10884
|
+
*
|
|
10885
|
+
* The browser paints nothing until the render that builds a page has ended, and
|
|
10886
|
+
* what lies below the first screen weighs in that render as much as what is on
|
|
10887
|
+
* it. A box marked this way is left empty in that render, then built once the
|
|
10888
|
+
* frame is painted — the page-level twin of a list's render window (see
|
|
10889
|
+
* `renderBudget` in list.jsx), for a page made of sections.
|
|
10890
|
+
*
|
|
10891
|
+
* Which boxes lie below the screen is measured, not taken from the caller. The
|
|
10892
|
+
* caller cannot know: a taller phone, a profile with nothing in its first
|
|
10893
|
+
* section, and the box it marked is on screen. So each box looks where it
|
|
10894
|
+
* stands in the animation callbacks of its first frame — laid out, not yet
|
|
10895
|
+
* painted — and one the frame would show, or one above it, is built there:
|
|
10896
|
+
* content appearing on screen is a pop-in, and content growing ABOVE what is
|
|
10897
|
+
* read pushes it down. Marking too much costs a second render in that frame,
|
|
10898
|
+
* never a wrong picture.
|
|
10899
|
+
*
|
|
10900
|
+
* The document put back where it was read is the other frame that shows it.
|
|
10901
|
+
* That offset is written by navi (see scroll_restoration.js), and written into
|
|
10902
|
+
* a document still missing its bottom it is clamped: the page opens higher than
|
|
10903
|
+
* it was left, and the bottom arrives under a page that has already jumped. So
|
|
10904
|
+
* whoever writes an arrival's offset builds first the boxes that offset shows
|
|
10905
|
+
* (buildDeferredBoxesAbove), in the same task — synchronously, since what comes
|
|
10906
|
+
* right after it (the offset, a route travel measuring its window) reads the
|
|
10907
|
+
* layout.
|
|
10908
|
+
*/
|
|
10909
|
+
|
|
10910
|
+
const deferredBoxes = new Set();
|
|
10911
|
+
const MountAfterPaint = ({
|
|
10912
|
+
boxRef,
|
|
10913
|
+
children
|
|
10914
|
+
}) => {
|
|
10915
|
+
const [built, setBuilt] = useState(false);
|
|
10916
|
+
useLayoutEffect(() => {
|
|
10917
|
+
let frame;
|
|
10918
|
+
let cancelAfterPaint;
|
|
10919
|
+
const stopWaiting = () => {
|
|
10920
|
+
deferredBoxes.delete(deferredBox);
|
|
10921
|
+
cancelAnimationFrame(frame);
|
|
10922
|
+
cancelAfterPaint();
|
|
10923
|
+
};
|
|
10924
|
+
// The first of the three that come — the offset of an arrival, the frame
|
|
10925
|
+
// finding the box on screen, the paint — builds it and cancels the others.
|
|
10926
|
+
const deferredBox = {
|
|
10927
|
+
getElement: () => boxRef.current,
|
|
10928
|
+
build: () => {
|
|
10929
|
+
stopWaiting();
|
|
10930
|
+
setBuilt(true);
|
|
10931
|
+
}
|
|
10932
|
+
};
|
|
10933
|
+
deferredBoxes.add(deferredBox);
|
|
10934
|
+
// Requested before afterPaint's own frame callback, so the box is looked at
|
|
10935
|
+
// before that one turns the paint into a build.
|
|
10936
|
+
frame = requestAnimationFrame(() => {
|
|
10937
|
+
const top = readTopInWindow(boxRef.current);
|
|
10938
|
+
if (top !== null && top < window.innerHeight) {
|
|
10939
|
+
deferredBox.build();
|
|
10940
|
+
}
|
|
10941
|
+
});
|
|
10942
|
+
cancelAfterPaint = afterPaint(deferredBox.build);
|
|
10943
|
+
return stopWaiting;
|
|
10944
|
+
}, []);
|
|
10945
|
+
return built ? children : null;
|
|
10946
|
+
};
|
|
10947
|
+
|
|
10948
|
+
/**
|
|
10949
|
+
* Builds, now, every box still waiting whose top is above `bottom` (in the
|
|
10950
|
+
* document's coordinates): what the document will show once scrolled so that
|
|
10951
|
+
* `bottom` is the bottom of the screen. Boxes built push those below them
|
|
10952
|
+
* further down, so the ones measured above `bottom` are all the ones that can
|
|
10953
|
+
* be shown there, and a few more at worst.
|
|
10954
|
+
*
|
|
10955
|
+
* Renders synchronously, so it must be called outside a Preact commit: a render
|
|
10956
|
+
* flushed from a layout effect runs inside the commit still walking the tree.
|
|
10957
|
+
*/
|
|
10958
|
+
const buildDeferredBoxesAbove = bottom => {
|
|
10959
|
+
if (deferredBoxes.size === 0) {
|
|
10960
|
+
return;
|
|
10961
|
+
}
|
|
10962
|
+
const toBuild = [];
|
|
10963
|
+
for (const deferredBox of deferredBoxes) {
|
|
10964
|
+
const top = readTopInWindow(deferredBox.getElement());
|
|
10965
|
+
if (top !== null && top + window.scrollY < bottom) {
|
|
10966
|
+
toBuild.push(deferredBox);
|
|
10967
|
+
}
|
|
10968
|
+
}
|
|
10969
|
+
if (toBuild.length === 0) {
|
|
10970
|
+
return;
|
|
10971
|
+
}
|
|
10972
|
+
flushSyncRendering(() => {
|
|
10973
|
+
for (const deferredBox of toBuild) {
|
|
10974
|
+
deferredBox.build();
|
|
10975
|
+
}
|
|
10976
|
+
});
|
|
10977
|
+
};
|
|
10978
|
+
|
|
10979
|
+
// null for a box nothing draws (a closed popup, a hidden tab): no frame shows
|
|
10980
|
+
// it, wherever the document is.
|
|
10981
|
+
const readTopInWindow = element => {
|
|
10982
|
+
if (!element || element.getClientRects().length === 0) {
|
|
10983
|
+
return null;
|
|
10984
|
+
}
|
|
10985
|
+
return element.getBoundingClientRect().top;
|
|
10986
|
+
};
|
|
10987
|
+
|
|
10815
10988
|
/**
|
|
10816
10989
|
* A container has put its page on screen — or as much of it as it can.
|
|
10817
10990
|
*
|
|
@@ -10867,7 +11040,9 @@ const [publishRouteRender, observeRouteRender] = createPubSub();
|
|
|
10867
11040
|
* What is NOT covered, and cannot be from here: a page whose height depends on
|
|
10868
11041
|
* something still loading. Its content is not there at the moment it is put
|
|
10869
11042
|
* back, so a position beyond what has arrived is clamped as before. Only the
|
|
10870
|
-
* page knows when it is whole.
|
|
11043
|
+
* page knows when it is whole. What navi itself leaves out of a first render
|
|
11044
|
+
* is the exception — a `<Box mount="after-paint">` — and is built before the
|
|
11045
|
+
* offset that shows it is written (see mount_after_paint.jsx).
|
|
10871
11046
|
*
|
|
10872
11047
|
* WHEN a page is arrived at is not decided here either. A document navigation
|
|
10873
11048
|
* lands where its kind says (see via_history.js), and one scrollport can be
|
|
@@ -11016,14 +11191,18 @@ const installScrollRestoration = () => {
|
|
|
11016
11191
|
// What a reload asks for, now that the browser has been told not to do it.
|
|
11017
11192
|
// Once, and at the first render of a route: the position is only meaningful
|
|
11018
11193
|
// once there is a page under it.
|
|
11194
|
+
// Written once the commit that rendered it has ended: said from inside it,
|
|
11195
|
+
// and what the offset shows may still have to be built (see scrollTo).
|
|
11019
11196
|
const positionOnLoad = positionByUrl.get(window.location.href);
|
|
11020
11197
|
if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
|
|
11021
11198
|
const stopListening = observeRouteRender(() => {
|
|
11022
11199
|
stopListening();
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
|
|
11026
|
-
|
|
11200
|
+
queueMicrotask(() => {
|
|
11201
|
+
if (documentHoldCount) {
|
|
11202
|
+
return;
|
|
11203
|
+
}
|
|
11204
|
+
scrollTo(positionOnLoad);
|
|
11205
|
+
});
|
|
11027
11206
|
});
|
|
11028
11207
|
}
|
|
11029
11208
|
};
|
|
@@ -11130,14 +11309,25 @@ const forgetScrollersOnArrival = (url, { from } = {}) => {
|
|
|
11130
11309
|
// last read. Where this one was read, and its top when it never was — leaving
|
|
11131
11310
|
// the offset alone would seat the reader wherever the neighbour happened to
|
|
11132
11311
|
// be, so here "nothing recorded" and "stay" are not the same thing.
|
|
11312
|
+
//
|
|
11313
|
+
// Called outside a Preact commit only, like scrollTo: a row of tabs measures
|
|
11314
|
+
// its window right after, so what the top shows is built first here rather
|
|
11315
|
+
// than in the frame (see mount_after_paint.jsx).
|
|
11133
11316
|
const arriveAtScrollPosition = (url) => {
|
|
11134
11317
|
if (restoreScrollPosition(url)) {
|
|
11135
11318
|
return;
|
|
11136
11319
|
}
|
|
11320
|
+
buildDeferredBoxesAbove(window.innerHeight);
|
|
11137
11321
|
startAtTop(url);
|
|
11138
11322
|
};
|
|
11139
11323
|
|
|
11324
|
+
// What the offset shows is built first: written into a document still missing
|
|
11325
|
+
// a deferred box, the offset is clamped (see mount_after_paint.jsx). That build
|
|
11326
|
+
// is a render of its own, so this is never called from inside a commit — a
|
|
11327
|
+
// caller that learns the page is there from a layout effect (observeRouteRender)
|
|
11328
|
+
// writes once that commit has ended.
|
|
11140
11329
|
const scrollTo = ({ x, y }) => {
|
|
11330
|
+
buildDeferredBoxesAbove(y + window.innerHeight);
|
|
11141
11331
|
window.scrollTo({ top: y, left: x, behavior: "instant" });
|
|
11142
11332
|
};
|
|
11143
11333
|
|
|
@@ -16048,9 +16238,9 @@ const armOutsidePressClose = (
|
|
|
16048
16238
|
* `font-family` is deliberately absent: a surface keeps the face of what
|
|
16049
16239
|
* opened it, so a section written in a display font gets its tooltips in that
|
|
16050
16240
|
* font. `color`, `font-size` and `font-weight` are absent too — each surface
|
|
16051
|
-
* answers those its own way (a popup
|
|
16052
|
-
* the size of what opened it
|
|
16053
|
-
* size).
|
|
16241
|
+
* answers those its own way (a popup and a callout write in
|
|
16242
|
+
* --navi-popup-color; a popup follows the size of what opened it, a callout
|
|
16243
|
+
* resets to the document's size).
|
|
16054
16244
|
*
|
|
16055
16245
|
* In `@layer navi` so an app that wants one of these back says so on the
|
|
16056
16246
|
* surface and wins, without having to out-specify anything.
|
|
@@ -16280,8 +16470,7 @@ const css$14 = /* css */ `
|
|
|
16280
16470
|
--callout-error-color: var(--navi-callout-error-color);
|
|
16281
16471
|
--callout-neutral-color: var(--navi-callout-neutral-color);
|
|
16282
16472
|
|
|
16283
|
-
--callout-background-color:
|
|
16284
|
-
--callout-icon-color: black;
|
|
16473
|
+
--callout-background-color: var(--navi-popup-background-color);
|
|
16285
16474
|
--callout-padding: 8px;
|
|
16286
16475
|
--callout-z-index: var(--navi-z-index-callout);
|
|
16287
16476
|
/* The page's line, said here rather than inherited: the icon and the
|
|
@@ -16335,11 +16524,11 @@ const css$14 = /* css */ `
|
|
|
16335
16524
|
height: auto; /* User agent reset */
|
|
16336
16525
|
margin: 0;
|
|
16337
16526
|
padding: 0; /* User agent reset */
|
|
16338
|
-
/*
|
|
16339
|
-
|
|
16340
|
-
color
|
|
16341
|
-
navi_css_vars.js
|
|
16342
|
-
color:
|
|
16527
|
+
/* A callout sits inside the element but writes on its own paper: the
|
|
16528
|
+
popup's ink, paired with the popup's paper it is drawn on, so the two
|
|
16529
|
+
follow color-scheme and the theme together. The color keywords are
|
|
16530
|
+
re-declared against that same ink in navi_css_vars.js. */
|
|
16531
|
+
color: var(--navi-popup-color);
|
|
16343
16532
|
font-weight: initial; /* Callout fells disconnected from the element, font weight should be predictible and stable */
|
|
16344
16533
|
font-size: initial; /* Callout fells disconnected from the element, font size should be predictible and stable */
|
|
16345
16534
|
line-height: var(
|
|
@@ -18562,16 +18751,18 @@ const findBusySource = (field) => {
|
|
|
18562
18751
|
// a new interaction is queued behind the run (see the action queue in
|
|
18563
18752
|
// control_hooks.jsx) rather than refused.
|
|
18564
18753
|
if (!field.optimistic && boundAction) {
|
|
18565
|
-
// The
|
|
18566
|
-
//
|
|
18567
|
-
//
|
|
18568
|
-
//
|
|
18569
|
-
//
|
|
18570
|
-
//
|
|
18571
|
-
//
|
|
18572
|
-
//
|
|
18573
|
-
|
|
18574
|
-
|
|
18754
|
+
// The instance the run in flight uses, as the render reads it (see
|
|
18755
|
+
// runningActionSignal in control_hooks.jsx): the state a proxy follows can
|
|
18756
|
+
// move mid-run — a popup content unmounting takes the group's value — and
|
|
18757
|
+
// the proxy would resolve to an idle instance. With no run out, the
|
|
18758
|
+
// instance the proxy resolves to right now, never the proxy's own signal:
|
|
18759
|
+
// a MIRROR synced by an effect the settling batch defers, which still says
|
|
18760
|
+
// RUNNING mid-batch (a state echo, an automatic follow-up) for an action
|
|
18761
|
+
// already over. An instance is COMPLETED the moment it settles.
|
|
18762
|
+
const liveAction =
|
|
18763
|
+
field.runningActionSignal.value ??
|
|
18764
|
+
boundAction.getCurrentAction?.() ??
|
|
18765
|
+
boundAction;
|
|
18575
18766
|
if (liveAction.runningStateSignal.value === RUNNING) {
|
|
18576
18767
|
return { field, action: liveAction };
|
|
18577
18768
|
}
|
|
@@ -19609,11 +19800,10 @@ const READONLY_CONSTRAINT = {
|
|
|
19609
19800
|
name: "readonly",
|
|
19610
19801
|
messageAttribute: "data-readonly-message",
|
|
19611
19802
|
check: (field, { intent } = {}) => {
|
|
19612
|
-
|
|
19613
|
-
|
|
19614
|
-
|
|
19615
|
-
)
|
|
19616
|
-
if (!readOnly) {
|
|
19803
|
+
// Not off the rendered `readOnly`/`aria-readonly`: they also carry the run
|
|
19804
|
+
// of the control's own action, a frame behind (see readOnlyWithoutRun in
|
|
19805
|
+
// control_hooks.jsx). That run is BUSY_CONSTRAINT's, which reads it live.
|
|
19806
|
+
if (!field.readOnlyWithoutRun) {
|
|
19617
19807
|
return null;
|
|
19618
19808
|
}
|
|
19619
19809
|
|
|
@@ -30032,6 +30222,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
|
|
|
30032
30222
|
* footer?: boolean,
|
|
30033
30223
|
* body?: boolean,
|
|
30034
30224
|
* sticky?: boolean,
|
|
30225
|
+
* mount?: "always" | "after-paint",
|
|
30035
30226
|
* children?: import("ignore:preact").ComponentChildren,
|
|
30036
30227
|
* [key: string]: any,
|
|
30037
30228
|
* }>}
|
|
@@ -30080,6 +30271,16 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
|
|
|
30080
30271
|
* (see docs/interactions.md). A plain box has no wiring of its own: it does
|
|
30081
30272
|
* nothing with `action` (that is a control's prop), so a click on it is
|
|
30082
30273
|
* declared here like any other interaction: `interactions={{ click: fn }}`.
|
|
30274
|
+
* @param {"always"|"after-paint"} [mount="always"] When `children` are built.
|
|
30275
|
+
* `"after-paint"` leaves the box empty in the render that first mounts it and
|
|
30276
|
+
* builds them right after the first frame that shows the page, then keeps
|
|
30277
|
+
* them: for the sections of a page that lie below its first screen. navi
|
|
30278
|
+
* measures where the box stands before that frame, and one the frame would
|
|
30279
|
+
* show (or where the document is put back, on a return) is built before the
|
|
30280
|
+
* paint all the same. Until built, what is inside is not in the document:
|
|
30281
|
+
* no id there is found, no named control there is there to hear its own
|
|
30282
|
+
* `signal` (see docs/control_object.md). Only for content nothing reaches
|
|
30283
|
+
* before the first frame (see docs/scroll.md).
|
|
30083
30284
|
*/
|
|
30084
30285
|
const Box = props => {
|
|
30085
30286
|
const {
|
|
@@ -30087,6 +30288,7 @@ const Box = props => {
|
|
|
30087
30288
|
children,
|
|
30088
30289
|
separator,
|
|
30089
30290
|
interactions,
|
|
30291
|
+
mount,
|
|
30090
30292
|
...computeProps
|
|
30091
30293
|
} = props;
|
|
30092
30294
|
const parentBoxFlow = useContext(BoxFlowContext);
|
|
@@ -30164,6 +30366,12 @@ const Box = props => {
|
|
|
30164
30366
|
children: innerChildren
|
|
30165
30367
|
});
|
|
30166
30368
|
}
|
|
30369
|
+
if (mount === "after-paint") {
|
|
30370
|
+
innerChildren = jsx(MountAfterPaint, {
|
|
30371
|
+
boxRef: finalRef,
|
|
30372
|
+
children: innerChildren
|
|
30373
|
+
});
|
|
30374
|
+
}
|
|
30167
30375
|
return jsx(TagName, {
|
|
30168
30376
|
ref: finalRef,
|
|
30169
30377
|
className: innerClassName,
|
|
@@ -33642,6 +33850,13 @@ const OLD_BAND_TOP_PROPERTY = "--navi-transition-old-band-top";
|
|
|
33642
33850
|
const OLD_BAND_RIGHT_PROPERTY = "--navi-transition-old-band-right";
|
|
33643
33851
|
const OLD_BAND_BOTTOM_PROPERTY = "--navi-transition-old-band-bottom";
|
|
33644
33852
|
const OLD_BAND_LEFT_PROPERTY = "--navi-transition-old-band-left";
|
|
33853
|
+
const VIEWPORT_TOP_PROPERTY = "--navi-transition-viewport-top";
|
|
33854
|
+
const VIEWPORT_LEFT_PROPERTY = "--navi-transition-viewport-left";
|
|
33855
|
+
// Worn by the root once the group is held at the window by our numbers alone
|
|
33856
|
+
// (see placeTransitionWindow): the group rule of each movement is written on
|
|
33857
|
+
// it. Read by rules on pseudo-elements only, so wearing it restyles nothing
|
|
33858
|
+
// the document inherits.
|
|
33859
|
+
const WINDOW_PLACED_ATTRIBUTE = "data-navi-transition-window-placed";
|
|
33645
33860
|
const WINDOW_PROPERTIES = [
|
|
33646
33861
|
WINDOW_TOP_PROPERTY,
|
|
33647
33862
|
WINDOW_LEFT_PROPERTY,
|
|
@@ -33655,6 +33870,8 @@ const WINDOW_PROPERTIES = [
|
|
|
33655
33870
|
OLD_BAND_RIGHT_PROPERTY,
|
|
33656
33871
|
OLD_BAND_BOTTOM_PROPERTY,
|
|
33657
33872
|
OLD_BAND_LEFT_PROPERTY,
|
|
33873
|
+
VIEWPORT_TOP_PROPERTY,
|
|
33874
|
+
VIEWPORT_LEFT_PROPERTY,
|
|
33658
33875
|
];
|
|
33659
33876
|
|
|
33660
33877
|
// Whose numbers are currently published. The window belongs to the movement
|
|
@@ -33662,6 +33879,8 @@ const WINDOW_PROPERTIES = [
|
|
|
33662
33879
|
// after another has replaced it must not wipe numbers the new one is standing
|
|
33663
33880
|
// on.
|
|
33664
33881
|
let windowOwner = null;
|
|
33882
|
+
// The element the box is captured as, for as long as the window is held.
|
|
33883
|
+
let windowElement = null;
|
|
33665
33884
|
// Stops re-reading the arriving state's corner, when a movement is on.
|
|
33666
33885
|
let unfollowArrivingState = null;
|
|
33667
33886
|
|
|
@@ -33717,6 +33936,10 @@ const holdTransitionWindow = (
|
|
|
33717
33936
|
const right =
|
|
33718
33937
|
rectBefore.right > rectAfter.right ? rectBefore.right : rectAfter.right;
|
|
33719
33938
|
windowOwner = owner;
|
|
33939
|
+
windowElement = element;
|
|
33940
|
+
// Pictures of a movement this one took over from are placed by the browser
|
|
33941
|
+
// until this one's own pictures exist.
|
|
33942
|
+
document.documentElement.removeAttribute(WINDOW_PLACED_ATTRIBUTE);
|
|
33720
33943
|
setTransitionValue(WINDOW_TOP_PROPERTY, `${top}px`);
|
|
33721
33944
|
setTransitionValue(WINDOW_LEFT_PROPERTY, `${left}px`);
|
|
33722
33945
|
setTransitionValue(WINDOW_WIDTH_PROPERTY, `${right - left}px`);
|
|
@@ -33741,6 +33964,33 @@ const holdTransitionWindow = (
|
|
|
33741
33964
|
});
|
|
33742
33965
|
};
|
|
33743
33966
|
|
|
33967
|
+
/**
|
|
33968
|
+
* Called once the pictures exist — the view transition's `ready` — with the
|
|
33969
|
+
* name the box was captured under. Where the viewport's corner stands in the
|
|
33970
|
+
* space the pictures are drawn in is the browser's placement of the group
|
|
33971
|
+
* minus our reading of the same element, both taken in this one instant; the
|
|
33972
|
+
* group is held at the window with it from then on (see the top of the file).
|
|
33973
|
+
* A browser that does not hand the group's placement back leaves the group
|
|
33974
|
+
* where it places it, moved to the window by the arriving corner.
|
|
33975
|
+
*/
|
|
33976
|
+
const placeTransitionWindow = (owner, groupName) => {
|
|
33977
|
+
if (owner !== windowOwner) {
|
|
33978
|
+
return;
|
|
33979
|
+
}
|
|
33980
|
+
const { transform } = getComputedStyle(
|
|
33981
|
+
document.documentElement,
|
|
33982
|
+
`::view-transition-group(${groupName})`,
|
|
33983
|
+
);
|
|
33984
|
+
if (!transform || transform === "none") {
|
|
33985
|
+
return;
|
|
33986
|
+
}
|
|
33987
|
+
const placement = new DOMMatrixReadOnly(transform);
|
|
33988
|
+
const rect = windowElement.getBoundingClientRect();
|
|
33989
|
+
setTransitionValue(VIEWPORT_TOP_PROPERTY, `${placement.m42 - rect.top}px`);
|
|
33990
|
+
setTransitionValue(VIEWPORT_LEFT_PROPERTY, `${placement.m41 - rect.left}px`);
|
|
33991
|
+
document.documentElement.setAttribute(WINDOW_PLACED_ATTRIBUTE, "");
|
|
33992
|
+
};
|
|
33993
|
+
|
|
33744
33994
|
// The live layout takes the box back. A discontinuity by construction — the
|
|
33745
33995
|
// window stands at the held height, the box is at its own — and an invisible
|
|
33746
33996
|
// one: the page arriving is fully in place, and the strip below it that the
|
|
@@ -33750,20 +34000,22 @@ const releaseTransitionWindow = (owner) => {
|
|
|
33750
34000
|
return;
|
|
33751
34001
|
}
|
|
33752
34002
|
windowOwner = null;
|
|
34003
|
+
windowElement = null;
|
|
33753
34004
|
if (unfollowArrivingState) {
|
|
33754
34005
|
unfollowArrivingState();
|
|
33755
34006
|
}
|
|
34007
|
+
document.documentElement.removeAttribute(WINDOW_PLACED_ATTRIBUTE);
|
|
33756
34008
|
for (const property of WINDOW_PROPERTIES) {
|
|
33757
34009
|
removeTransitionValue(property);
|
|
33758
34010
|
}
|
|
33759
34011
|
};
|
|
33760
34012
|
|
|
33761
|
-
// The arriving state's corner, re-read every frame — as often as
|
|
33762
|
-
//
|
|
33763
|
-
//
|
|
33764
|
-
//
|
|
33765
|
-
//
|
|
33766
|
-
//
|
|
34013
|
+
// The arriving state's corner, re-read every frame — as often as anything can
|
|
34014
|
+
// move the page arriving (see the top of the file) — and with it the offset
|
|
34015
|
+
// outside the box, which carries the window and the corner of the state being
|
|
34016
|
+
// left along. Requested, never awaited: the first call runs inside the update
|
|
34017
|
+
// callback, where a frame cannot come. Written only when it moved: every write
|
|
34018
|
+
// restyles the pictures.
|
|
33767
34019
|
const followArrivingState = (element, rectAtHold, heldAt) => {
|
|
33768
34020
|
if (unfollowArrivingState) {
|
|
33769
34021
|
unfollowArrivingState();
|
|
@@ -34268,6 +34520,13 @@ const css$12 = /* css */`@property --navi-route-transition-leave {
|
|
|
34268
34520
|
animation-name: none;
|
|
34269
34521
|
}
|
|
34270
34522
|
|
|
34523
|
+
&[data-navi-transition-window-placed]::view-transition-group(navi-route-transition) {
|
|
34524
|
+
top: calc(var(--navi-transition-window-top) + var(--navi-transition-viewport-top));
|
|
34525
|
+
left: calc(var(--navi-transition-window-left) +
|
|
34526
|
+
var(--navi-transition-viewport-left));
|
|
34527
|
+
transform: none;
|
|
34528
|
+
}
|
|
34529
|
+
|
|
34271
34530
|
&::view-transition-old(navi-route-transition) {
|
|
34272
34531
|
top: calc(var(--navi-transition-window-old-top) - var(--navi-transition-window-top));
|
|
34273
34532
|
left: calc(var(--navi-transition-window-old-left) - var(--navi-transition-window-left));
|
|
@@ -35315,6 +35574,8 @@ const beginTransition = ({
|
|
|
35315
35574
|
// the only place the silent misconfigurations show. They are all about the
|
|
35316
35575
|
// same thing — a movement playing on pictures that are not the pages.
|
|
35317
35576
|
const viewTransitionReady = () => {
|
|
35577
|
+
// Read before anything below writes to the document.
|
|
35578
|
+
placeTransitionWindow(transition, AREA_NAME);
|
|
35318
35579
|
startTransitionFurniture(transition, {
|
|
35319
35580
|
duration: transition.duration
|
|
35320
35581
|
});
|
|
@@ -35826,6 +36087,13 @@ const css$11 = /* css */`.navi_route_travel {
|
|
|
35826
36087
|
animation-duration: var(--navi-route-travel-duration, .3s);
|
|
35827
36088
|
animation-name: none;
|
|
35828
36089
|
}
|
|
36090
|
+
|
|
36091
|
+
&[data-navi-transition-window-placed]::view-transition-group(navi-route-travel) {
|
|
36092
|
+
top: calc(var(--navi-transition-window-top) + var(--navi-transition-viewport-top));
|
|
36093
|
+
left: calc(var(--navi-transition-window-left) +
|
|
36094
|
+
var(--navi-transition-viewport-left));
|
|
36095
|
+
transform: none;
|
|
36096
|
+
}
|
|
35829
36097
|
}
|
|
35830
36098
|
|
|
35831
36099
|
:root[data-navi-route-travel-held] {
|
|
@@ -36204,6 +36472,10 @@ const RouteTravel = ({
|
|
|
36204
36472
|
});
|
|
36205
36473
|
});
|
|
36206
36474
|
travel.viewTransition = viewTransition;
|
|
36475
|
+
// Subscribed first, so it reads before the scrub below writes anything.
|
|
36476
|
+
viewTransition.ready.then(() => {
|
|
36477
|
+
placeTransitionWindow(travel, TRAVEL_NAME);
|
|
36478
|
+
}, ignoreSkipped);
|
|
36207
36479
|
if (scrub) {
|
|
36208
36480
|
// Said only now: the release has to have something to let go of, and the
|
|
36209
36481
|
// transition did not exist a line above.
|
|
@@ -36403,16 +36675,20 @@ const RouteTravel = ({
|
|
|
36403
36675
|
// what makes the document tall enough to hold that offset again —
|
|
36404
36676
|
// and the recording stays deaf until then, over the clamp the swap
|
|
36405
36677
|
// back makes on the way. Its own deafness rather than the travel's:
|
|
36406
|
-
// the travel ends here, and this outlives it by a render.
|
|
36678
|
+
// the travel ends here, and this outlives it by a render. Written
|
|
36679
|
+
// once the commit that says so has ended, as arriveAtScrollPosition
|
|
36680
|
+
// asks.
|
|
36407
36681
|
const resumeScrollRecording = suspendScrollRecording();
|
|
36408
36682
|
const stopWatchingRender = observeRouteRender(() => {
|
|
36409
36683
|
stopWatchingRender();
|
|
36410
|
-
|
|
36411
|
-
|
|
36412
|
-
|
|
36413
|
-
|
|
36414
|
-
|
|
36415
|
-
|
|
36684
|
+
queueMicrotask(() => {
|
|
36685
|
+
const {
|
|
36686
|
+
route,
|
|
36687
|
+
params
|
|
36688
|
+
} = travel.fromPage;
|
|
36689
|
+
arriveAtScrollPosition(route.buildUrl(params));
|
|
36690
|
+
resumeScrollRecording();
|
|
36691
|
+
});
|
|
36416
36692
|
});
|
|
36417
36693
|
}
|
|
36418
36694
|
releaseRendering();
|
|
@@ -39145,34 +39421,6 @@ registerNaviCommand("--navi-unselect", (source, event) => {
|
|
|
39145
39421
|
};
|
|
39146
39422
|
});
|
|
39147
39423
|
|
|
39148
|
-
/**
|
|
39149
|
-
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
39150
|
-
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
39151
|
-
* mounted run inside the call too, exactly as they would on any other commit.
|
|
39152
|
-
*
|
|
39153
|
-
* For the caller that has to read the DOM it just asked for — measuring an
|
|
39154
|
-
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
39155
|
-
* to do it, because what comes after is a browser event still in flight
|
|
39156
|
-
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
39157
|
-
*
|
|
39158
|
-
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
39159
|
-
* render queue drains; swapping it for "right now" for the duration of the call
|
|
39160
|
-
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
39161
|
-
* case above: rendering synchronously in the middle of an event gives up the
|
|
39162
|
-
* batching that makes several state changes one commit.
|
|
39163
|
-
*/
|
|
39164
|
-
const flushSyncRendering = (fn) => {
|
|
39165
|
-
const debounceRenderingPrevious = options.debounceRendering;
|
|
39166
|
-
options.debounceRendering = (drainRenderQueue) => {
|
|
39167
|
-
drainRenderQueue();
|
|
39168
|
-
};
|
|
39169
|
-
try {
|
|
39170
|
-
fn();
|
|
39171
|
-
} finally {
|
|
39172
|
-
options.debounceRendering = debounceRenderingPrevious;
|
|
39173
|
-
}
|
|
39174
|
-
};
|
|
39175
|
-
|
|
39176
39424
|
/**
|
|
39177
39425
|
* When a popup builds what it holds, and when it throws it away.
|
|
39178
39426
|
*
|
|
@@ -41471,6 +41719,65 @@ const keptOrFirstDefinedChildUIState = (children, fallbackState, kept) => {
|
|
|
41471
41719
|
}
|
|
41472
41720
|
return kept;
|
|
41473
41721
|
};
|
|
41722
|
+
// The same claim, for an object: a key whose control is not there cannot be
|
|
41723
|
+
// contradicted by it — a slide not built yet (SlideContainer mount="near"), a
|
|
41724
|
+
// row scrolled out, a page parked. A key of `kept` stays until a control of
|
|
41725
|
+
// that name, there, says otherwise; a value put ON the group replaces the
|
|
41726
|
+
// whole object, which is how a key is dropped on purpose.
|
|
41727
|
+
const keptAndNamedChildUIStates = (children, fallbackState, kept) => {
|
|
41728
|
+
const groupValues =
|
|
41729
|
+
kept !== null && typeof kept === "object" && !Array.isArray(kept)
|
|
41730
|
+
? { ...kept }
|
|
41731
|
+
: {};
|
|
41732
|
+
return Object.assign(
|
|
41733
|
+
groupValues,
|
|
41734
|
+
readNamedChildUIStates(children, { warnNameless: true }),
|
|
41735
|
+
);
|
|
41736
|
+
};
|
|
41737
|
+
// What the named controls among `children` say, one key each. A nameless
|
|
41738
|
+
// GROUP is a grouping, not a value: it exists to hold its children together
|
|
41739
|
+
// (a WheelGroup sharing navigation, a fieldset-ish cluster) without claiming a
|
|
41740
|
+
// key of its own, so what its controls say is written as if they were here.
|
|
41741
|
+
// Naming it is what turns the same group into one key — see ControlGroup's
|
|
41742
|
+
// `name`. Only what its CONTROLS say: such a grouping is handed the whole
|
|
41743
|
+
// object around it (see distributeChildUIState below) and keeps what they do
|
|
41744
|
+
// not answer for, so merged whole it would carry its siblings' keys back up,
|
|
41745
|
+
// stale.
|
|
41746
|
+
const readNamedChildUIStates = (children, { warnNameless }) => {
|
|
41747
|
+
const values = {};
|
|
41748
|
+
for (const child of children) {
|
|
41749
|
+
const { name, emptyUIState } = child;
|
|
41750
|
+
// A control holding nothing writes its own empty, not a hole: the key is in
|
|
41751
|
+
// the object either way, and what is read from it keeps the shape the
|
|
41752
|
+
// reader was promised (see resolveEmptyUIState).
|
|
41753
|
+
const uiState =
|
|
41754
|
+
child.uiState === undefined && emptyUIState !== undefined
|
|
41755
|
+
? emptyUIState
|
|
41756
|
+
: child.uiState;
|
|
41757
|
+
if (!name) {
|
|
41758
|
+
if (isNamelessGrouping(child, uiState)) {
|
|
41759
|
+
Object.assign(
|
|
41760
|
+
values,
|
|
41761
|
+
child.keepsUnansweredKeys
|
|
41762
|
+
? readNamedChildUIStates(child.getChildControllers(), {
|
|
41763
|
+
warnNameless: false,
|
|
41764
|
+
})
|
|
41765
|
+
: uiState,
|
|
41766
|
+
);
|
|
41767
|
+
continue;
|
|
41768
|
+
}
|
|
41769
|
+
if (warnNameless) {
|
|
41770
|
+
console.warn(
|
|
41771
|
+
"A group child is missing a name property, its state won't be included in the group state",
|
|
41772
|
+
child,
|
|
41773
|
+
);
|
|
41774
|
+
}
|
|
41775
|
+
continue;
|
|
41776
|
+
}
|
|
41777
|
+
values[name] = uiState;
|
|
41778
|
+
}
|
|
41779
|
+
return values;
|
|
41780
|
+
};
|
|
41474
41781
|
|
|
41475
41782
|
// Default aggregate/distribute implementations keyed by controlType or stateType.
|
|
41476
41783
|
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
@@ -41540,37 +41847,7 @@ const GROUP_DEFAULTS = {
|
|
|
41540
41847
|
childControlFilter: (child) => {
|
|
41541
41848
|
return child.controlType !== "button" && child.controlType !== "link";
|
|
41542
41849
|
},
|
|
41543
|
-
aggregateChildStates:
|
|
41544
|
-
const groupValues = {};
|
|
41545
|
-
for (const child of children) {
|
|
41546
|
-
const { name, emptyUIState } = child;
|
|
41547
|
-
// A control holding nothing writes its own empty, not a hole: the key
|
|
41548
|
-
// is in the object either way, and what is read from it keeps the shape
|
|
41549
|
-
// the reader was promised (see resolveEmptyUIState).
|
|
41550
|
-
const uiState =
|
|
41551
|
-
child.uiState === undefined && emptyUIState !== undefined
|
|
41552
|
-
? emptyUIState
|
|
41553
|
-
: child.uiState;
|
|
41554
|
-
if (!name) {
|
|
41555
|
-
// A nameless GROUP is a grouping, not a value: it exists to hold its
|
|
41556
|
-
// children together (a WheelGroup sharing navigation, a fieldset-ish
|
|
41557
|
-
// cluster) without claiming a key of its own, so what it holds is
|
|
41558
|
-
// merged in as if its children had been written here. Naming it is
|
|
41559
|
-
// what turns the same group into one key — see ControlGroup's `name`.
|
|
41560
|
-
if (isNamelessGrouping(child, uiState)) {
|
|
41561
|
-
Object.assign(groupValues, uiState);
|
|
41562
|
-
continue;
|
|
41563
|
-
}
|
|
41564
|
-
console.warn(
|
|
41565
|
-
"A group child is missing a name property, its state won't be included in the group state",
|
|
41566
|
-
child,
|
|
41567
|
-
);
|
|
41568
|
-
continue;
|
|
41569
|
-
}
|
|
41570
|
-
groupValues[name] = uiState;
|
|
41571
|
-
}
|
|
41572
|
-
return groupValues;
|
|
41573
|
-
},
|
|
41850
|
+
aggregateChildStates: keptAndNamedChildUIStates,
|
|
41574
41851
|
distributeChildUIState: (newUIState, child) => {
|
|
41575
41852
|
const childName = child.name;
|
|
41576
41853
|
if (
|
|
@@ -41594,7 +41871,8 @@ const GROUP_DEFAULTS = {
|
|
|
41594
41871
|
return childUIState;
|
|
41595
41872
|
}
|
|
41596
41873
|
// Merged in on the way up (see above), so on the way down it takes the
|
|
41597
|
-
// whole object
|
|
41874
|
+
// whole object: its controls pick out their own keys, and it keeps the
|
|
41875
|
+
// rest for those of its controls that are not there yet.
|
|
41598
41876
|
// Only for a child with no name of its own: the same condition the
|
|
41599
41877
|
// aggregate applies before merging. A NAMED child holding an object (a
|
|
41600
41878
|
// `type="object"` picker, say) answers for one key, and the object it is
|
|
@@ -42072,6 +42350,11 @@ const useUIGroupStateController = (
|
|
|
42072
42350
|
// a picker filling its popup, a value prop) rather than worked out from
|
|
42073
42351
|
// its children. What it protects is read in onChange.
|
|
42074
42352
|
stateGivenFromAbove: hasValueProp || hasDefaultValueProp,
|
|
42353
|
+
// Its value carries keys none of its controls answers for (see
|
|
42354
|
+
// keptAndNamedChildUIStates): a group merging it as a nameless
|
|
42355
|
+
// grouping reads its controls instead of its value.
|
|
42356
|
+
keepsUnansweredKeys:
|
|
42357
|
+
resolvedAggregateChildStates === keptAndNamedChildUIStates,
|
|
42075
42358
|
uiStateSignal,
|
|
42076
42359
|
wantRequesterButtonState,
|
|
42077
42360
|
ref,
|
|
@@ -42705,6 +42988,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
42705
42988
|
ref: realUIStateController.ref,
|
|
42706
42989
|
uiStateSignal: realUIStateController.uiStateSignal,
|
|
42707
42990
|
controlHostProps: realUIStateController.controlHostProps,
|
|
42991
|
+
readOnlyWithoutRun: realUIStateController.readOnlyWithoutRun,
|
|
42708
42992
|
registerChild: (child) => {
|
|
42709
42993
|
if (!canRegisterAsFacadeChild(child)) {
|
|
42710
42994
|
return;
|
|
@@ -42833,6 +43117,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
42833
43117
|
s.controller.ref = realUIStateController.ref;
|
|
42834
43118
|
s.controller.uiStateSignal = realUIStateController.uiStateSignal;
|
|
42835
43119
|
s.controller.controlHostProps = realUIStateController.controlHostProps;
|
|
43120
|
+
s.controller.readOnlyWithoutRun =
|
|
43121
|
+
realUIStateController.readOnlyWithoutRun;
|
|
42836
43122
|
realUIStateController.pushStateDownToFacadeChild =
|
|
42837
43123
|
s.controller.pushStateDownToChild;
|
|
42838
43124
|
|
|
@@ -44639,6 +44925,11 @@ const useInteractiveProps = (props, {
|
|
|
44639
44925
|
const actionLoading = optimistic ? false : actionRunning;
|
|
44640
44926
|
const loadingResolved = loadingBase || actionLoading;
|
|
44641
44927
|
const readOnlyResolved = readOnlyBase || actionLoading;
|
|
44928
|
+
// What READONLY_CONSTRAINT refuses for. The run of the control's own action
|
|
44929
|
+
// is BUSY_CONSTRAINT's, read live: readOnlyResolved is a render snapshot,
|
|
44930
|
+
// still read-only in the tick the run settles — the tick a form's
|
|
44931
|
+
// after-send `--navi-reset` asks the form to empty itself.
|
|
44932
|
+
uiStateController.readOnlyWithoutRun = Boolean(readOnlyBase);
|
|
44642
44933
|
// Read-only, and what this control opens still opens: reading what is in
|
|
44643
44934
|
// there changes nothing. Read by READONLY_CONSTRAINT, which lets an
|
|
44644
44935
|
// interaction that only reads through on it.
|
|
@@ -45091,10 +45382,10 @@ const useInteractiveProps = (props, {
|
|
|
45091
45382
|
});
|
|
45092
45383
|
}
|
|
45093
45384
|
// controlHostProps is a curated subset of props with resolved values applied
|
|
45094
|
-
// (e.g. readOnly resolved from context + action loading). The
|
|
45095
|
-
//
|
|
45096
|
-
//
|
|
45097
|
-
//
|
|
45385
|
+
// (e.g. readOnly resolved from context + action loading). The constraints
|
|
45386
|
+
// read off uiStateController.controlHostProps at runtime (e.g. DISABLED_CONSTRAINT,
|
|
45387
|
+
// the readonly reason), so pointing the controller at controlHostProps keeps
|
|
45388
|
+
// those reads current without any extra bookkeeping.
|
|
45098
45389
|
const firstRender = uiStateController.controlHostProps === undefined;
|
|
45099
45390
|
uiStateController.controlHostProps = controlHostProps;
|
|
45100
45391
|
// The action itself, not just what the last render made of it: its running
|
|
@@ -52551,7 +52842,8 @@ const useExpandableContext = partName => {
|
|
|
52551
52842
|
* other three, intent on the UI part (pointer entering it, focus landing in
|
|
52552
52843
|
* it) builds the content ahead of the click; `"while-opened"` content is
|
|
52553
52844
|
* built by a press on it only, and thrown away if that press does not
|
|
52554
|
-
* expand it.
|
|
52845
|
+
* expand it. Named controls in content not built are kept by the group
|
|
52846
|
+
* around them (docs/control_object.md, "A control that is not there").
|
|
52555
52847
|
*/
|
|
52556
52848
|
const Expandable = props => {
|
|
52557
52849
|
import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
@@ -54137,14 +54429,34 @@ const ButtonConfirm = ({
|
|
|
54137
54429
|
}
|
|
54138
54430
|
return jsxs("span", {
|
|
54139
54431
|
className: "navi_button_confirm",
|
|
54140
|
-
ref: askingRef
|
|
54432
|
+
ref: askingRef
|
|
54433
|
+
// A place for the focus, so a press anywhere in the question keeps it
|
|
54434
|
+
// inside: WebKit does not focus a pressed button, and a press on
|
|
54435
|
+
// "Confirmer" would otherwise blur toward <body> — a focusout with no
|
|
54436
|
+
// relatedTarget, read below as the person leaving. The callout answers
|
|
54437
|
+
// the same ambiguity the same way.
|
|
54438
|
+
,
|
|
54439
|
+
tabIndex: -1
|
|
54440
|
+
// "not me, unless you have nothing else": the ladder in the effect above
|
|
54441
|
+
// walks past the question to what it holds.
|
|
54442
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
54443
|
+
,
|
|
54444
|
+
"navi-autofocus": "last-resort",
|
|
54141
54445
|
onKeyDown: e => {
|
|
54142
54446
|
if (e.key === "Escape") {
|
|
54143
54447
|
cancel(true);
|
|
54144
54448
|
}
|
|
54145
54449
|
},
|
|
54146
54450
|
onFocusOut: e => {
|
|
54147
|
-
|
|
54451
|
+
// Chrome also fires this from the question's own removal, once preact
|
|
54452
|
+
// has cleared the ref: nobody left, and cancel(false) would undo the
|
|
54453
|
+
// focus the answer gives back. That focus cannot go back before the
|
|
54454
|
+
// removal: the button it lands on is mounted by the same commit.
|
|
54455
|
+
const askingEl = askingRef.current;
|
|
54456
|
+
if (!askingEl) {
|
|
54457
|
+
return;
|
|
54458
|
+
}
|
|
54459
|
+
if (!askingEl.contains(e.relatedTarget)) {
|
|
54148
54460
|
cancel(false);
|
|
54149
54461
|
}
|
|
54150
54462
|
},
|
|
@@ -59613,12 +59925,19 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
59613
59925
|
* would bring alongside (neighbours on the map, along the axes
|
|
59614
59926
|
* `travelByDrag` opens, the other end of a `loop` included) — and kept from
|
|
59615
59927
|
* then on. For slides that are costly to build (a grid of a few hundred
|
|
59616
|
-
*
|
|
59928
|
+
* items each): opening builds the slide shown and its neighbours rather
|
|
59617
59929
|
* than all of them, and a jump to a far slide builds that one and none of
|
|
59618
59930
|
* those in between, which a travel never shows. The box is then as large as
|
|
59619
59931
|
* the largest slide BUILT so far (see `sizing`), so it grows as a larger one
|
|
59620
59932
|
* is reached: give it a size of its own when the slides differ.
|
|
59621
59933
|
* `<Slide mount>` says it for one slide — a heavy one among light ones.
|
|
59934
|
+
* A slide not built has no controls, and a group around the container keeps
|
|
59935
|
+
* their keys for them: what it was handed reaches them once built. What is
|
|
59936
|
+
* shared between slides is written through that group (its `signal`, a
|
|
59937
|
+
* `--navi-update` aimed at it), never into one control's own `signal`, which
|
|
59938
|
+
* nobody reads while that control is not built; and nothing validates a
|
|
59939
|
+
* field before its slide is built. See docs/control_object.md, "A control
|
|
59940
|
+
* that is not there".
|
|
59622
59941
|
*/
|
|
59623
59942
|
const SlideContainer = ({
|
|
59624
59943
|
layout = "row",
|
|
@@ -69543,44 +69862,6 @@ document.addEventListener(
|
|
|
69543
69862
|
{ capture: true, passive: true },
|
|
69544
69863
|
);
|
|
69545
69864
|
|
|
69546
|
-
const FRAME_TIMEOUT_MS = 100;
|
|
69547
|
-
|
|
69548
|
-
/**
|
|
69549
|
-
* Calls back once the browser has painted what is committed now.
|
|
69550
|
-
*
|
|
69551
|
-
* The callbacks of the next frame run before that frame paints, so the call is
|
|
69552
|
-
* made from a task queued from inside one — the same way preact schedules
|
|
69553
|
-
* useEffect. Unlike an effect, nothing runs this early: preact flushes a
|
|
69554
|
-
* component's pending effects as soon as that component renders again, and a
|
|
69555
|
-
* re-render before the paint is exactly what a caller waiting for the paint
|
|
69556
|
-
* has to survive. A frame not coming at all (a background tab) still answers,
|
|
69557
|
-
* late, through the timeout.
|
|
69558
|
-
*
|
|
69559
|
-
* @param {() => void} callback
|
|
69560
|
-
* @returns {() => void} cancel
|
|
69561
|
-
*/
|
|
69562
|
-
const afterPaint = (callback) => {
|
|
69563
|
-
let called = false;
|
|
69564
|
-
let timeoutId;
|
|
69565
|
-
let frameId;
|
|
69566
|
-
const onFrame = () => {
|
|
69567
|
-
if (called) {
|
|
69568
|
-
return;
|
|
69569
|
-
}
|
|
69570
|
-
called = true;
|
|
69571
|
-
clearTimeout(timeoutId);
|
|
69572
|
-
cancelAnimationFrame(frameId);
|
|
69573
|
-
timeoutId = setTimeout(callback, 0);
|
|
69574
|
-
};
|
|
69575
|
-
timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
|
|
69576
|
-
frameId = requestAnimationFrame(onFrame);
|
|
69577
|
-
return () => {
|
|
69578
|
-
called = true;
|
|
69579
|
-
clearTimeout(timeoutId);
|
|
69580
|
-
cancelAnimationFrame(frameId);
|
|
69581
|
-
};
|
|
69582
|
-
};
|
|
69583
|
-
|
|
69584
69865
|
const LoadingDotsSvg = () => {
|
|
69585
69866
|
return jsxs("svg", {
|
|
69586
69867
|
viewBox: "0 0 200 200",
|
|
@@ -71611,7 +71892,7 @@ const css$x = /* css */`@property --list-group-label-height {
|
|
|
71611
71892
|
}
|
|
71612
71893
|
}
|
|
71613
71894
|
|
|
71614
|
-
.navi_list[navi-box-flow="grid"] > .navi_list_virtual_filler, .navi_list[navi-box-flow="grid"] > .navi_separator {
|
|
71895
|
+
.navi_list[navi-box-flow="grid"] > .navi_list_virtual_filler, .navi_list[navi-box-flow="grid"] > .navi_list_failed_items, .navi_list[navi-box-flow="grid"] > .navi_separator {
|
|
71615
71896
|
grid-column: 1 / -1;
|
|
71616
71897
|
}
|
|
71617
71898
|
|
|
@@ -71988,7 +72269,8 @@ const ListUI = props => {
|
|
|
71988
72269
|
listId: rest.id,
|
|
71989
72270
|
scroller,
|
|
71990
72271
|
searchText,
|
|
71991
|
-
horizontal
|
|
72272
|
+
horizontal,
|
|
72273
|
+
columns
|
|
71992
72274
|
});
|
|
71993
72275
|
|
|
71994
72276
|
// renderBudget frames the items of a run; a list whose items are all declared
|
|
@@ -72303,7 +72585,8 @@ const useListScrollSync = ({
|
|
|
72303
72585
|
listId,
|
|
72304
72586
|
scroller,
|
|
72305
72587
|
searchText,
|
|
72306
|
-
horizontal
|
|
72588
|
+
horizontal,
|
|
72589
|
+
columns
|
|
72307
72590
|
}) => {
|
|
72308
72591
|
const debugScroll = useDebugScroll();
|
|
72309
72592
|
// The items drawn, [start, end) among the list's own. A ref as well as a
|
|
@@ -72318,6 +72601,7 @@ const useListScrollSync = ({
|
|
|
72318
72601
|
return renderWindow.start > 0 || renderWindow.end < listItems.totalSignal.peek();
|
|
72319
72602
|
};
|
|
72320
72603
|
const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
|
|
72604
|
+
inLines: Boolean(columns),
|
|
72321
72605
|
windowLeavesItemsOut,
|
|
72322
72606
|
scrolledWanted: scrolled ?? defaultScrolled,
|
|
72323
72607
|
// The fillers hold the room of items above the screen at this size: a
|
|
@@ -72386,6 +72670,34 @@ const useListScrollSync = ({
|
|
|
72386
72670
|
});
|
|
72387
72671
|
};
|
|
72388
72672
|
|
|
72673
|
+
// How many items share a line: the tracks a grid of items (`columns`)
|
|
72674
|
+
// resolves to, one otherwise. A layout result under auto-fill, so it is read
|
|
72675
|
+
// off the list once laid out, at each commit and each resize — for the runs
|
|
72676
|
+
// alone, which draw in lines: items declared one by one are all drawn. A
|
|
72677
|
+
// line of another length rewraps every item drawn: the item at the top of
|
|
72678
|
+
// the view is held where it is across it.
|
|
72679
|
+
const itemsPerLineRef = useRef(1);
|
|
72680
|
+
const [itemsPerLine, setItemsPerLine] = useState(1);
|
|
72681
|
+
const updateItemsPerLine = () => {
|
|
72682
|
+
if (!ref.current) {
|
|
72683
|
+
return;
|
|
72684
|
+
}
|
|
72685
|
+
const itemsPerLineNow = columns && listItems.hasRuns() ? readItemsPerLine(getListEl()) : 1;
|
|
72686
|
+
if (itemsPerLineNow === null || itemsPerLineNow === itemsPerLineRef.current) {
|
|
72687
|
+
return;
|
|
72688
|
+
}
|
|
72689
|
+
captureAnchor();
|
|
72690
|
+
itemsPerLineRef.current = itemsPerLineNow;
|
|
72691
|
+
setItemsPerLine(itemsPerLineNow);
|
|
72692
|
+
};
|
|
72693
|
+
const updateItemsPerLineRef = useRef(null);
|
|
72694
|
+
updateItemsPerLineRef.current = updateItemsPerLine;
|
|
72695
|
+
useLayoutEffect(() => {
|
|
72696
|
+
if (columns || itemsPerLineRef.current !== 1) {
|
|
72697
|
+
updateItemsPerLine();
|
|
72698
|
+
}
|
|
72699
|
+
});
|
|
72700
|
+
|
|
72389
72701
|
// What the window holds goes through three stages (see settleWindow):
|
|
72390
72702
|
// - "first": the first commit, a count of items since nothing is laid out
|
|
72391
72703
|
// yet to measure a size with — `initial` or the budget when they are one,
|
|
@@ -72784,6 +73096,7 @@ const useListScrollSync = ({
|
|
|
72784
73096
|
listEl: getListEl(),
|
|
72785
73097
|
listItems,
|
|
72786
73098
|
virtualItemSizeSignal,
|
|
73099
|
+
itemsPerLine: itemsPerLineRef.current,
|
|
72787
73100
|
renderWindowRef,
|
|
72788
73101
|
horizontal
|
|
72789
73102
|
});
|
|
@@ -73124,20 +73437,14 @@ const useListScrollSync = ({
|
|
|
73124
73437
|
}
|
|
73125
73438
|
const scrollerEl = getScroller();
|
|
73126
73439
|
const listEl = getListEl();
|
|
73127
|
-
|
|
73128
|
-
|
|
73129
|
-
|
|
73130
|
-
|
|
73131
|
-
|
|
73132
|
-
|
|
73133
|
-
|
|
73134
|
-
|
|
73135
|
-
// growing is its own content settling: only a list holding itself
|
|
73136
|
-
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
73137
|
-
// reading must not be touched — its items are held still by the anchoring,
|
|
73138
|
-
// which this would undo. The SCROLLER resizing is the window around it
|
|
73139
|
-
// changing shape, and then the item that was at the top goes back where it
|
|
73140
|
-
// was.
|
|
73440
|
+
// Two things resize here, and they call for opposite answers. The LIST
|
|
73441
|
+
// growing is its own content settling: only a list holding itself
|
|
73442
|
+
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
73443
|
+
// reading must not be touched — its items are held still by the anchoring,
|
|
73444
|
+
// which this would undo. The SCROLLER resizing is the window around it
|
|
73445
|
+
// changing shape, and then the item that was at the top goes back where it
|
|
73446
|
+
// was.
|
|
73447
|
+
const putTopItemBack = entries => {
|
|
73141
73448
|
const listResized = entries.some(entry => entry.target === listEl);
|
|
73142
73449
|
if (listResized && onGeometryChangeRef.current()) {
|
|
73143
73450
|
return;
|
|
@@ -73166,6 +73473,19 @@ const useListScrollSync = ({
|
|
|
73166
73473
|
} else {
|
|
73167
73474
|
scrollerEl.scrollTop += delta;
|
|
73168
73475
|
}
|
|
73476
|
+
};
|
|
73477
|
+
const observer = new ResizeObserver(entries => {
|
|
73478
|
+
// A box that grows may be a box that starts to scroll — the scroller is
|
|
73479
|
+
// resolved again before anything is done about the resize, so that a
|
|
73480
|
+
// list which has outgrown a bounded ancestor stops holding on to the
|
|
73481
|
+
// page.
|
|
73482
|
+
resolveScroller();
|
|
73483
|
+
rememberPosition();
|
|
73484
|
+
putTopItemBack(entries);
|
|
73485
|
+
// Once the top item is back where it was: that is the place a line of
|
|
73486
|
+
// another length has to keep, and putting it back drops any anchor
|
|
73487
|
+
// captured before.
|
|
73488
|
+
updateItemsPerLineRef.current();
|
|
73169
73489
|
});
|
|
73170
73490
|
observer.observe(scrollerEl);
|
|
73171
73491
|
observer.observe(listEl);
|
|
@@ -73321,7 +73641,10 @@ const useListScrollSync = ({
|
|
|
73321
73641
|
start,
|
|
73322
73642
|
end
|
|
73323
73643
|
} = renderWindowRef.current;
|
|
73324
|
-
|
|
73644
|
+
// What an item not drawn weighs: its share of a line, which the items side
|
|
73645
|
+
// by side on it take together.
|
|
73646
|
+
const itemsPerLineNow = itemsPerLineRef.current;
|
|
73647
|
+
const itemSize = virtualItemSizeSignal.peek() / itemsPerLineNow;
|
|
73325
73648
|
const pixelsOf = createItemSizeReader(items, start, end, itemSize);
|
|
73326
73649
|
const walkBefore = (from, sizeWanted, sizeOf) => {
|
|
73327
73650
|
let index = from;
|
|
@@ -73591,9 +73914,28 @@ const useListScrollSync = ({
|
|
|
73591
73914
|
ifChanged: true
|
|
73592
73915
|
});
|
|
73593
73916
|
}, [windowStart, windowEnd]);
|
|
73917
|
+
// What the runs draw: the window widened to whole lines. The window itself
|
|
73918
|
+
// moves an item at a time, and a grid lays the first item drawn in its first
|
|
73919
|
+
// column — started anywhere but on a line, every item drawn would sit in the
|
|
73920
|
+
// column of another. Started on one, an item is in the column its place says
|
|
73921
|
+
// whatever the window frames. The same object while the numbers hold, so the
|
|
73922
|
+
// runs are not told about a window that did not move.
|
|
73923
|
+
const windowDrawnRef = useRef(null);
|
|
73924
|
+
const drawnStart = windowStart - windowStart % itemsPerLine;
|
|
73925
|
+
const endPastLine = windowEnd % itemsPerLine;
|
|
73926
|
+
const drawnEnd = endPastLine === 0 ? windowEnd : windowEnd + itemsPerLine - endPastLine;
|
|
73927
|
+
let windowDrawn = windowDrawnRef.current;
|
|
73928
|
+
if (!windowDrawn || windowDrawn.start !== drawnStart || windowDrawn.end !== drawnEnd || windowDrawn.itemsPerLine !== itemsPerLine) {
|
|
73929
|
+
windowDrawn = {
|
|
73930
|
+
start: drawnStart,
|
|
73931
|
+
end: drawnEnd,
|
|
73932
|
+
itemsPerLine
|
|
73933
|
+
};
|
|
73934
|
+
windowDrawnRef.current = windowDrawn;
|
|
73935
|
+
}
|
|
73594
73936
|
return {
|
|
73595
73937
|
virtualItemSizeSignal,
|
|
73596
|
-
renderWindow:
|
|
73938
|
+
renderWindow: windowDrawn,
|
|
73597
73939
|
pendingScrollRef,
|
|
73598
73940
|
scrollToItem,
|
|
73599
73941
|
captureAnchor
|
|
@@ -73617,6 +73959,21 @@ const getScrollerViewportRect = scrollerEl => {
|
|
|
73617
73959
|
}
|
|
73618
73960
|
return scrollerEl.getBoundingClientRect();
|
|
73619
73961
|
};
|
|
73962
|
+
// The tracks a grid resolved its template to, as its computed
|
|
73963
|
+
// grid-template-columns lists them once laid out: "150px 150px [end] 150px".
|
|
73964
|
+
// Not laid out (a closed popup), that is the template as written, which says
|
|
73965
|
+
// nothing yet under auto-fill: `null`.
|
|
73966
|
+
const readItemsPerLine = listEl => {
|
|
73967
|
+
const tracks = getComputedStyle(listEl).gridTemplateColumns;
|
|
73968
|
+
if (tracks === "none" || tracks.includes("(")) {
|
|
73969
|
+
return null;
|
|
73970
|
+
}
|
|
73971
|
+
const sizes = tracks.replace(/\[[^\]]*\]/g, " ").trim();
|
|
73972
|
+
if (!sizes) {
|
|
73973
|
+
return null;
|
|
73974
|
+
}
|
|
73975
|
+
return sizes.split(/\s+/).length;
|
|
73976
|
+
};
|
|
73620
73977
|
// A window that cannot move is the one failure of a virtualized list that
|
|
73621
73978
|
// looks like nothing: the items outside it are fillers holding their room, so
|
|
73622
73979
|
// the list simply ends on blank space, at exactly the height of the items never
|
|
@@ -74419,6 +74776,7 @@ const getScrollInfo = ({
|
|
|
74419
74776
|
listEl,
|
|
74420
74777
|
listItems,
|
|
74421
74778
|
virtualItemSizeSignal,
|
|
74779
|
+
itemsPerLine,
|
|
74422
74780
|
renderWindowRef,
|
|
74423
74781
|
horizontal
|
|
74424
74782
|
}) => {
|
|
@@ -74456,7 +74814,9 @@ const getScrollInfo = ({
|
|
|
74456
74814
|
// mean we don't know the real on-screen index, only the scroll position,
|
|
74457
74815
|
// so estimate from it rather than assume nothing changed.
|
|
74458
74816
|
const estimateFromScrollPos = reasonPrefix => {
|
|
74459
|
-
|
|
74817
|
+
// An item's share of its line: the items side by side on it take it
|
|
74818
|
+
// together.
|
|
74819
|
+
const virtualItemSize = virtualItemSizeSignal.peek() / itemsPerLine;
|
|
74460
74820
|
if (virtualItemSize === 0) {
|
|
74461
74821
|
return null;
|
|
74462
74822
|
}
|
|
@@ -74512,8 +74872,10 @@ const getScrollInfo = ({
|
|
|
74512
74872
|
const VIRTUAL_ITEM_SIZE_EPSILON = 0.5;
|
|
74513
74873
|
// Measures the items currently in the DOM, edge to edge: what a filler stands in
|
|
74514
74874
|
// for is the room a run of items takes together — separators and group labels
|
|
74515
|
-
// included — not the height of one <li>.
|
|
74516
|
-
|
|
74875
|
+
// included — not the height of one <li>. The size of a line of them: one item
|
|
74876
|
+
// in a list, the items side by side in a grid (`inLines`), counted where a new
|
|
74877
|
+
// line starts.
|
|
74878
|
+
const measureItemSize = (listEl, horizontal, inLines) => {
|
|
74517
74879
|
let fromSkeletons = false;
|
|
74518
74880
|
let itemEls = listEl.querySelectorAll(REAL_LIST_ITEM_SELECTOR);
|
|
74519
74881
|
if (itemEls.length === 0) {
|
|
@@ -74535,13 +74897,27 @@ const measureItemSize = (listEl, horizontal) => {
|
|
|
74535
74897
|
if (span <= 0) {
|
|
74536
74898
|
return null;
|
|
74537
74899
|
}
|
|
74900
|
+
let lineCount = itemEls.length;
|
|
74901
|
+
if (inLines) {
|
|
74902
|
+
lineCount = 0;
|
|
74903
|
+
let lineFrom = -Infinity;
|
|
74904
|
+
for (const itemEl of itemEls) {
|
|
74905
|
+
const rect = itemEl.getBoundingClientRect();
|
|
74906
|
+
const from = horizontal ? rect.left : rect.top;
|
|
74907
|
+
if (from > lineFrom + 0.5) {
|
|
74908
|
+
lineCount++;
|
|
74909
|
+
lineFrom = from;
|
|
74910
|
+
}
|
|
74911
|
+
}
|
|
74912
|
+
}
|
|
74538
74913
|
return {
|
|
74539
|
-
size: span /
|
|
74540
|
-
|
|
74914
|
+
size: span / lineCount,
|
|
74915
|
+
lineCount,
|
|
74541
74916
|
fromSkeletons
|
|
74542
74917
|
};
|
|
74543
74918
|
};
|
|
74544
74919
|
const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
74920
|
+
inLines,
|
|
74545
74921
|
windowLeavesItemsOut,
|
|
74546
74922
|
scrolledWanted,
|
|
74547
74923
|
beforeSizeChange
|
|
@@ -74585,8 +74961,8 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
74585
74961
|
return;
|
|
74586
74962
|
}
|
|
74587
74963
|
}
|
|
74588
|
-
samples.sum += measure.size * measure.
|
|
74589
|
-
samples.count += measure.
|
|
74964
|
+
samples.sum += measure.size * measure.lineCount;
|
|
74965
|
+
samples.count += measure.lineCount;
|
|
74590
74966
|
const next = samples.sum / samples.count;
|
|
74591
74967
|
const current = virtualSizeSignal.peek();
|
|
74592
74968
|
if (Math.abs(next - current) > VIRTUAL_ITEM_SIZE_EPSILON) {
|
|
@@ -74606,7 +74982,7 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
74606
74982
|
const itemsHeldOffScreen = windowLeavesItemsOut();
|
|
74607
74983
|
if (!virtualItemSizeProp && sizeAlreadyKnown && itemsHeldOffScreen && ref.current) {
|
|
74608
74984
|
const listEl = ref.current.querySelector(".navi_list");
|
|
74609
|
-
const measure = listEl ? measureItemSize(listEl, horizontal) : null;
|
|
74985
|
+
const measure = listEl ? measureItemSize(listEl, horizontal, inLines) : null;
|
|
74610
74986
|
if (measure) {
|
|
74611
74987
|
feedSample(measure);
|
|
74612
74988
|
}
|
|
@@ -74627,11 +75003,11 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
74627
75003
|
if (!listEl) {
|
|
74628
75004
|
return undefined;
|
|
74629
75005
|
}
|
|
74630
|
-
const measure = measureItemSize(listEl, horizontal);
|
|
75006
|
+
const measure = measureItemSize(listEl, horizontal, inLines);
|
|
74631
75007
|
if (measure) {
|
|
74632
75008
|
const samples = samplesRef.current;
|
|
74633
|
-
samples.sum = measure.size * measure.
|
|
74634
|
-
samples.count = measure.
|
|
75009
|
+
samples.sum = measure.size * measure.lineCount;
|
|
75010
|
+
samples.count = measure.lineCount;
|
|
74635
75011
|
samples.fromSkeletons = measure.fromSkeletons;
|
|
74636
75012
|
virtualSizeSignal.value = measure.size;
|
|
74637
75013
|
return undefined;
|
|
@@ -74776,14 +75152,17 @@ const Fallback = ({
|
|
|
74776
75152
|
// Reads the item size itself: it is what the size is for, and a run holding
|
|
74777
75153
|
// every item it draws must not be redrawn — every item of it — because the size
|
|
74778
75154
|
// settled after the first commit.
|
|
75155
|
+
// It holds lines: the items side by side on one take its room together, and a
|
|
75156
|
+
// last line left short takes it all the same.
|
|
74779
75157
|
const VirtualFiller = ({
|
|
74780
75158
|
edge,
|
|
74781
75159
|
itemCount,
|
|
75160
|
+
itemsPerLine,
|
|
74782
75161
|
findChunks
|
|
74783
75162
|
}) => {
|
|
74784
75163
|
const listItems = useContext(ListItemsContext);
|
|
74785
|
-
const
|
|
74786
|
-
const sizeToFill = itemCount *
|
|
75164
|
+
const lineSize = listItems.virtualItemSizeSignal.value;
|
|
75165
|
+
const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize;
|
|
74787
75166
|
// A filler resizing moves what stands below it — the items on screen, when it
|
|
74788
75167
|
// holds the room of items above them — and it resizes in a commit of its own
|
|
74789
75168
|
// when the item size settles after the list has rendered: the list puts its
|
|
@@ -74802,7 +75181,7 @@ const VirtualFiller = ({
|
|
|
74802
75181
|
"aria-hidden": true,
|
|
74803
75182
|
style: {
|
|
74804
75183
|
"--size-to-fill": `${sizeToFill}px`,
|
|
74805
|
-
"--x-find-line-size": findChunks ? `${
|
|
75184
|
+
"--x-find-line-size": findChunks ? `${lineSize / itemsPerLine}px` : undefined
|
|
74806
75185
|
},
|
|
74807
75186
|
children: findChunks && findChunks.map(chunk => jsx("div", {
|
|
74808
75187
|
className: "navi_list_find_stand_in",
|
|
@@ -75682,6 +76061,9 @@ const useRunItems = (store, {
|
|
|
75682
76061
|
const rankOf = itemIndex => itemIndex - runStart;
|
|
75683
76062
|
const indexOfRank = rank => rank + runStart;
|
|
75684
76063
|
const getItemAt = itemIndex => store.getItem(rankOf(itemIndex));
|
|
76064
|
+
const {
|
|
76065
|
+
itemsPerLine
|
|
76066
|
+
} = renderWindow;
|
|
75685
76067
|
const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;
|
|
75686
76068
|
const windowTo = renderWindow.end < runEnd ? renderWindow.end : runEnd;
|
|
75687
76069
|
store.forget(rankOf(windowFrom), rankOf(windowTo));
|
|
@@ -75789,8 +76171,20 @@ const useRunItems = (store, {
|
|
|
75789
76171
|
// band is drawn among the items. A negative start is not a rank but a count
|
|
75790
76172
|
// back from the end (the very first ask, before the count is known) — there
|
|
75791
76173
|
// is no item to convert it to, and the band falls back to the window.
|
|
75792
|
-
|
|
75793
|
-
|
|
76174
|
+
let failureFrom = store.failure === null ? -1 : store.failure.start < 0 || indexOfRank(store.failure.start) < windowFrom ? windowFrom : indexOfRank(store.failure.start);
|
|
76175
|
+
let failureTo = store.failure === null ? -1 : store.failure.end < 0 || indexOfRank(store.failure.end) > windowTo - 1 ? windowTo - 1 : indexOfRank(store.failure.end);
|
|
76176
|
+
if (store.failure !== null && itemsPerLine > 1) {
|
|
76177
|
+
// In a grid the band takes whole lines, like the window: begun mid-line,
|
|
76178
|
+
// it would push every item after it into another column.
|
|
76179
|
+
failureFrom -= failureFrom % itemsPerLine;
|
|
76180
|
+
if (failureFrom < windowFrom) {
|
|
76181
|
+
failureFrom = windowFrom;
|
|
76182
|
+
}
|
|
76183
|
+
failureTo += itemsPerLine - 1 - failureTo % itemsPerLine;
|
|
76184
|
+
if (failureTo > windowTo - 1) {
|
|
76185
|
+
failureTo = windowTo - 1;
|
|
76186
|
+
}
|
|
76187
|
+
}
|
|
75794
76188
|
const nodes = [];
|
|
75795
76189
|
// Items that belong together, as the data says (a day of messages, a month of
|
|
75796
76190
|
// games): consecutive items sharing a group key are wrapped in one group, so
|
|
@@ -75867,6 +76261,7 @@ const useRunItems = (store, {
|
|
|
75867
76261
|
nodes.push(jsx(VirtualFiller, {
|
|
75868
76262
|
edge: "before",
|
|
75869
76263
|
itemCount: windowFrom - runStart,
|
|
76264
|
+
itemsPerLine: itemsPerLine,
|
|
75870
76265
|
findChunks: getFindChunks(runStart, windowFrom)
|
|
75871
76266
|
}, "navi-list-filler-before"));
|
|
75872
76267
|
}
|
|
@@ -75881,7 +76276,7 @@ const useRunItems = (store, {
|
|
|
75881
76276
|
nodes.push(jsx("li", {
|
|
75882
76277
|
className: "navi_list_failed_items",
|
|
75883
76278
|
style: {
|
|
75884
|
-
"--size-to-fill": `${failedItemCount * listItems.virtualItemSizeSignal.value}px`
|
|
76279
|
+
"--size-to-fill": `${Math.ceil(failedItemCount / itemsPerLine) * listItems.virtualItemSizeSignal.value}px`
|
|
75885
76280
|
},
|
|
75886
76281
|
children: renderError ? renderError({
|
|
75887
76282
|
error: store.failure.error,
|
|
@@ -75971,6 +76366,7 @@ const useRunItems = (store, {
|
|
|
75971
76366
|
nodes.push(jsx(VirtualFiller, {
|
|
75972
76367
|
edge: "after",
|
|
75973
76368
|
itemCount: runEnd - windowTo,
|
|
76369
|
+
itemsPerLine: itemsPerLine,
|
|
75974
76370
|
findChunks: getFindChunks(windowTo, runEnd)
|
|
75975
76371
|
}, "navi-list-filler-after"));
|
|
75976
76372
|
}
|
|
@@ -76694,12 +77090,16 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76694
77090
|
* @param {string} [props.columns]
|
|
76695
77091
|
* The list's own columns: a `grid-template-columns` value the ITEMS are laid
|
|
76696
77092
|
* into — a sheet of icons (`repeat(auto-fill, minmax(2.5rem, 1fr))`), a row of
|
|
76697
|
-
* choices (`repeat(3, minmax(0, 1fr))`)
|
|
76698
|
-
*
|
|
76699
|
-
*
|
|
76700
|
-
*
|
|
76701
|
-
*
|
|
76702
|
-
*
|
|
77093
|
+
* choices (`repeat(3, minmax(0, 1fr))`), a feed of covers
|
|
77094
|
+
* (`repeat(auto-fill, minmax(150px, 1fr))` over a `<List.Items>`). Each item
|
|
77095
|
+
* takes one cell, and an item meant to take a whole line says so for itself
|
|
77096
|
+
* (`style={{ gridColumn: "1 / -1" }}`). A `<List.Items>` run is drawn in whole
|
|
77097
|
+
* lines — as many items to a line as the template resolves to, read again
|
|
77098
|
+
* when the list's width changes: its window starts and ends on a line, so an
|
|
77099
|
+
* item is always in the column its place says, and the fillers hold the room
|
|
77100
|
+
* of the lines it does not draw (`virtualItemSize` is then a line's size).
|
|
77101
|
+
* That count holds only while every item of the run takes one cell: an item
|
|
77102
|
+
* spanning a line, or a `groupBy`, puts the ones after it in other columns.
|
|
76703
77103
|
* @param {string} [props.itemColumns]
|
|
76704
77104
|
* The columns inside an ITEM: a `grid-template-columns` value each item fills
|
|
76705
77105
|
* with its own children, a table whose cells line up down the list. Every item
|
|
@@ -76715,8 +77115,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76715
77115
|
* scroll box, which fills it and has nothing to arrange.
|
|
76716
77116
|
* @param {boolean} [props.flexWrap]
|
|
76717
77117
|
* Lets a horizontal list's items fall to the next line instead of running
|
|
76718
|
-
* past the edge — a row of choices under a `maxWidth`, say.
|
|
76719
|
-
*
|
|
77118
|
+
* past the edge — a row of choices under a `maxWidth`, say. A wrapped line
|
|
77119
|
+
* holding several items is not virtualizable: a long collection laid several
|
|
77120
|
+
* to a line goes in `columns`.
|
|
76720
77121
|
* @param {string} [props.overflow]
|
|
76721
77122
|
* `"visible"` lets the items paint outside the list — a check in an item's
|
|
76722
77123
|
* corner, a badge crossing the edge. A list clips by default, which is what
|
|
@@ -76835,7 +77236,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76835
77236
|
* one-line size) — an item guessed too small builds one more, an item
|
|
76836
77237
|
* guessed too big leaves a blank (see docs/scroll.md, "What the list knows,
|
|
76837
77238
|
* and what it guesses"). The items drawn are measured either way: the render
|
|
76838
|
-
* window is sized on them.
|
|
77239
|
+
* window is sized on them. In a grid (`columns`), the size is a line's: a
|
|
77240
|
+
* card's, which the items side by side on it share.
|
|
76839
77241
|
* @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
|
|
76840
77242
|
* Which box scrolls — and with it, which box the render window follows and
|
|
76841
77243
|
* which box a scroll position is read from (`onScrolledChange`). `"self"`
|