@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/dev/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, prefixFirstAndIndentRemainingLines, 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";
|
|
@@ -11240,6 +11240,179 @@ const dropGeneratedIdKeys = (state) => {
|
|
|
11240
11240
|
return stateWithoutGeneratedIdKeys;
|
|
11241
11241
|
};
|
|
11242
11242
|
|
|
11243
|
+
const FRAME_TIMEOUT_MS = 100;
|
|
11244
|
+
|
|
11245
|
+
/**
|
|
11246
|
+
* Calls back once the browser has painted what is committed now.
|
|
11247
|
+
*
|
|
11248
|
+
* The callbacks of the next frame run before that frame paints, so the call is
|
|
11249
|
+
* made from a task queued from inside one — the same way preact schedules
|
|
11250
|
+
* useEffect. Unlike an effect, nothing runs this early: preact flushes a
|
|
11251
|
+
* component's pending effects as soon as that component renders again, and a
|
|
11252
|
+
* re-render before the paint is exactly what a caller waiting for the paint
|
|
11253
|
+
* has to survive. A frame not coming at all (a background tab) still answers,
|
|
11254
|
+
* late, through the timeout.
|
|
11255
|
+
*
|
|
11256
|
+
* @param {() => void} callback
|
|
11257
|
+
* @returns {() => void} cancel
|
|
11258
|
+
*/
|
|
11259
|
+
const afterPaint = (callback) => {
|
|
11260
|
+
let called = false;
|
|
11261
|
+
let timeoutId;
|
|
11262
|
+
let frameId;
|
|
11263
|
+
const onFrame = () => {
|
|
11264
|
+
if (called) {
|
|
11265
|
+
return;
|
|
11266
|
+
}
|
|
11267
|
+
called = true;
|
|
11268
|
+
clearTimeout(timeoutId);
|
|
11269
|
+
cancelAnimationFrame(frameId);
|
|
11270
|
+
timeoutId = setTimeout(callback, 0);
|
|
11271
|
+
};
|
|
11272
|
+
timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
|
|
11273
|
+
frameId = requestAnimationFrame(onFrame);
|
|
11274
|
+
return () => {
|
|
11275
|
+
called = true;
|
|
11276
|
+
clearTimeout(timeoutId);
|
|
11277
|
+
cancelAnimationFrame(frameId);
|
|
11278
|
+
};
|
|
11279
|
+
};
|
|
11280
|
+
|
|
11281
|
+
/**
|
|
11282
|
+
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
11283
|
+
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
11284
|
+
* mounted run inside the call too, exactly as they would on any other commit.
|
|
11285
|
+
*
|
|
11286
|
+
* For the caller that has to read the DOM it just asked for — measuring an
|
|
11287
|
+
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
11288
|
+
* to do it, because what comes after is a browser event still in flight
|
|
11289
|
+
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
11290
|
+
*
|
|
11291
|
+
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
11292
|
+
* render queue drains; swapping it for "right now" for the duration of the call
|
|
11293
|
+
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
11294
|
+
* case above: rendering synchronously in the middle of an event gives up the
|
|
11295
|
+
* batching that makes several state changes one commit.
|
|
11296
|
+
*/
|
|
11297
|
+
const flushSyncRendering = (fn) => {
|
|
11298
|
+
const debounceRenderingPrevious = options.debounceRendering;
|
|
11299
|
+
options.debounceRendering = (drainRenderQueue) => {
|
|
11300
|
+
drainRenderQueue();
|
|
11301
|
+
};
|
|
11302
|
+
try {
|
|
11303
|
+
fn();
|
|
11304
|
+
} finally {
|
|
11305
|
+
options.debounceRendering = debounceRenderingPrevious;
|
|
11306
|
+
}
|
|
11307
|
+
};
|
|
11308
|
+
|
|
11309
|
+
/**
|
|
11310
|
+
* The content of a `<Box mount="after-paint">`: built right after the first
|
|
11311
|
+
* frame that shows the box, unless that frame would show it.
|
|
11312
|
+
*
|
|
11313
|
+
* The browser paints nothing until the render that builds a page has ended, and
|
|
11314
|
+
* what lies below the first screen weighs in that render as much as what is on
|
|
11315
|
+
* it. A box marked this way is left empty in that render, then built once the
|
|
11316
|
+
* frame is painted — the page-level twin of a list's render window (see
|
|
11317
|
+
* `renderBudget` in list.jsx), for a page made of sections.
|
|
11318
|
+
*
|
|
11319
|
+
* Which boxes lie below the screen is measured, not taken from the caller. The
|
|
11320
|
+
* caller cannot know: a taller phone, a profile with nothing in its first
|
|
11321
|
+
* section, and the box it marked is on screen. So each box looks where it
|
|
11322
|
+
* stands in the animation callbacks of its first frame — laid out, not yet
|
|
11323
|
+
* painted — and one the frame would show, or one above it, is built there:
|
|
11324
|
+
* content appearing on screen is a pop-in, and content growing ABOVE what is
|
|
11325
|
+
* read pushes it down. Marking too much costs a second render in that frame,
|
|
11326
|
+
* never a wrong picture.
|
|
11327
|
+
*
|
|
11328
|
+
* The document put back where it was read is the other frame that shows it.
|
|
11329
|
+
* That offset is written by navi (see scroll_restoration.js), and written into
|
|
11330
|
+
* a document still missing its bottom it is clamped: the page opens higher than
|
|
11331
|
+
* it was left, and the bottom arrives under a page that has already jumped. So
|
|
11332
|
+
* whoever writes an arrival's offset builds first the boxes that offset shows
|
|
11333
|
+
* (buildDeferredBoxesAbove), in the same task — synchronously, since what comes
|
|
11334
|
+
* right after it (the offset, a route travel measuring its window) reads the
|
|
11335
|
+
* layout.
|
|
11336
|
+
*/
|
|
11337
|
+
|
|
11338
|
+
const deferredBoxes = new Set();
|
|
11339
|
+
const MountAfterPaint = ({
|
|
11340
|
+
boxRef,
|
|
11341
|
+
children
|
|
11342
|
+
}) => {
|
|
11343
|
+
const [built, setBuilt] = useState(false);
|
|
11344
|
+
useLayoutEffect(() => {
|
|
11345
|
+
let frame;
|
|
11346
|
+
let cancelAfterPaint;
|
|
11347
|
+
const stopWaiting = () => {
|
|
11348
|
+
deferredBoxes.delete(deferredBox);
|
|
11349
|
+
cancelAnimationFrame(frame);
|
|
11350
|
+
cancelAfterPaint();
|
|
11351
|
+
};
|
|
11352
|
+
// The first of the three that come — the offset of an arrival, the frame
|
|
11353
|
+
// finding the box on screen, the paint — builds it and cancels the others.
|
|
11354
|
+
const deferredBox = {
|
|
11355
|
+
getElement: () => boxRef.current,
|
|
11356
|
+
build: () => {
|
|
11357
|
+
stopWaiting();
|
|
11358
|
+
setBuilt(true);
|
|
11359
|
+
}
|
|
11360
|
+
};
|
|
11361
|
+
deferredBoxes.add(deferredBox);
|
|
11362
|
+
// Requested before afterPaint's own frame callback, so the box is looked at
|
|
11363
|
+
// before that one turns the paint into a build.
|
|
11364
|
+
frame = requestAnimationFrame(() => {
|
|
11365
|
+
const top = readTopInWindow(boxRef.current);
|
|
11366
|
+
if (top !== null && top < window.innerHeight) {
|
|
11367
|
+
deferredBox.build();
|
|
11368
|
+
}
|
|
11369
|
+
});
|
|
11370
|
+
cancelAfterPaint = afterPaint(deferredBox.build);
|
|
11371
|
+
return stopWaiting;
|
|
11372
|
+
}, []);
|
|
11373
|
+
return built ? children : null;
|
|
11374
|
+
};
|
|
11375
|
+
|
|
11376
|
+
/**
|
|
11377
|
+
* Builds, now, every box still waiting whose top is above `bottom` (in the
|
|
11378
|
+
* document's coordinates): what the document will show once scrolled so that
|
|
11379
|
+
* `bottom` is the bottom of the screen. Boxes built push those below them
|
|
11380
|
+
* further down, so the ones measured above `bottom` are all the ones that can
|
|
11381
|
+
* be shown there, and a few more at worst.
|
|
11382
|
+
*
|
|
11383
|
+
* Renders synchronously, so it must be called outside a Preact commit: a render
|
|
11384
|
+
* flushed from a layout effect runs inside the commit still walking the tree.
|
|
11385
|
+
*/
|
|
11386
|
+
const buildDeferredBoxesAbove = bottom => {
|
|
11387
|
+
if (deferredBoxes.size === 0) {
|
|
11388
|
+
return;
|
|
11389
|
+
}
|
|
11390
|
+
const toBuild = [];
|
|
11391
|
+
for (const deferredBox of deferredBoxes) {
|
|
11392
|
+
const top = readTopInWindow(deferredBox.getElement());
|
|
11393
|
+
if (top !== null && top + window.scrollY < bottom) {
|
|
11394
|
+
toBuild.push(deferredBox);
|
|
11395
|
+
}
|
|
11396
|
+
}
|
|
11397
|
+
if (toBuild.length === 0) {
|
|
11398
|
+
return;
|
|
11399
|
+
}
|
|
11400
|
+
flushSyncRendering(() => {
|
|
11401
|
+
for (const deferredBox of toBuild) {
|
|
11402
|
+
deferredBox.build();
|
|
11403
|
+
}
|
|
11404
|
+
});
|
|
11405
|
+
};
|
|
11406
|
+
|
|
11407
|
+
// null for a box nothing draws (a closed popup, a hidden tab): no frame shows
|
|
11408
|
+
// it, wherever the document is.
|
|
11409
|
+
const readTopInWindow = element => {
|
|
11410
|
+
if (!element || element.getClientRects().length === 0) {
|
|
11411
|
+
return null;
|
|
11412
|
+
}
|
|
11413
|
+
return element.getBoundingClientRect().top;
|
|
11414
|
+
};
|
|
11415
|
+
|
|
11243
11416
|
/**
|
|
11244
11417
|
* A container has put its page on screen — or as much of it as it can.
|
|
11245
11418
|
*
|
|
@@ -11295,7 +11468,9 @@ const [publishRouteRender, observeRouteRender] = createPubSub();
|
|
|
11295
11468
|
* What is NOT covered, and cannot be from here: a page whose height depends on
|
|
11296
11469
|
* something still loading. Its content is not there at the moment it is put
|
|
11297
11470
|
* back, so a position beyond what has arrived is clamped as before. Only the
|
|
11298
|
-
* page knows when it is whole.
|
|
11471
|
+
* page knows when it is whole. What navi itself leaves out of a first render
|
|
11472
|
+
* is the exception — a `<Box mount="after-paint">` — and is built before the
|
|
11473
|
+
* offset that shows it is written (see mount_after_paint.jsx).
|
|
11299
11474
|
*
|
|
11300
11475
|
* WHEN a page is arrived at is not decided here either. A document navigation
|
|
11301
11476
|
* lands where its kind says (see via_history.js), and one scrollport can be
|
|
@@ -11444,14 +11619,18 @@ const installScrollRestoration = () => {
|
|
|
11444
11619
|
// What a reload asks for, now that the browser has been told not to do it.
|
|
11445
11620
|
// Once, and at the first render of a route: the position is only meaningful
|
|
11446
11621
|
// once there is a page under it.
|
|
11622
|
+
// Written once the commit that rendered it has ended: said from inside it,
|
|
11623
|
+
// and what the offset shows may still have to be built (see scrollTo).
|
|
11447
11624
|
const positionOnLoad = positionByUrl.get(window.location.href);
|
|
11448
11625
|
if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
|
|
11449
11626
|
const stopListening = observeRouteRender(() => {
|
|
11450
11627
|
stopListening();
|
|
11451
|
-
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
|
|
11628
|
+
queueMicrotask(() => {
|
|
11629
|
+
if (documentHoldCount) {
|
|
11630
|
+
return;
|
|
11631
|
+
}
|
|
11632
|
+
scrollTo(positionOnLoad);
|
|
11633
|
+
});
|
|
11455
11634
|
});
|
|
11456
11635
|
}
|
|
11457
11636
|
};
|
|
@@ -11558,14 +11737,25 @@ const forgetScrollersOnArrival = (url, { from } = {}) => {
|
|
|
11558
11737
|
// last read. Where this one was read, and its top when it never was — leaving
|
|
11559
11738
|
// the offset alone would seat the reader wherever the neighbour happened to
|
|
11560
11739
|
// be, so here "nothing recorded" and "stay" are not the same thing.
|
|
11740
|
+
//
|
|
11741
|
+
// Called outside a Preact commit only, like scrollTo: a row of tabs measures
|
|
11742
|
+
// its window right after, so what the top shows is built first here rather
|
|
11743
|
+
// than in the frame (see mount_after_paint.jsx).
|
|
11561
11744
|
const arriveAtScrollPosition = (url) => {
|
|
11562
11745
|
if (restoreScrollPosition(url)) {
|
|
11563
11746
|
return;
|
|
11564
11747
|
}
|
|
11748
|
+
buildDeferredBoxesAbove(window.innerHeight);
|
|
11565
11749
|
startAtTop(url);
|
|
11566
11750
|
};
|
|
11567
11751
|
|
|
11752
|
+
// What the offset shows is built first: written into a document still missing
|
|
11753
|
+
// a deferred box, the offset is clamped (see mount_after_paint.jsx). That build
|
|
11754
|
+
// is a render of its own, so this is never called from inside a commit — a
|
|
11755
|
+
// caller that learns the page is there from a layout effect (observeRouteRender)
|
|
11756
|
+
// writes once that commit has ended.
|
|
11568
11757
|
const scrollTo = ({ x, y }) => {
|
|
11758
|
+
buildDeferredBoxesAbove(y + window.innerHeight);
|
|
11569
11759
|
window.scrollTo({ top: y, left: x, behavior: "instant" });
|
|
11570
11760
|
};
|
|
11571
11761
|
|
|
@@ -16691,9 +16881,9 @@ const armOutsidePressClose = (
|
|
|
16691
16881
|
* `font-family` is deliberately absent: a surface keeps the face of what
|
|
16692
16882
|
* opened it, so a section written in a display font gets its tooltips in that
|
|
16693
16883
|
* font. `color`, `font-size` and `font-weight` are absent too — each surface
|
|
16694
|
-
* answers those its own way (a popup
|
|
16695
|
-
* the size of what opened it
|
|
16696
|
-
* size).
|
|
16884
|
+
* answers those its own way (a popup and a callout write in
|
|
16885
|
+
* --navi-popup-color; a popup follows the size of what opened it, a callout
|
|
16886
|
+
* resets to the document's size).
|
|
16697
16887
|
*
|
|
16698
16888
|
* In `@layer navi` so an app that wants one of these back says so on the
|
|
16699
16889
|
* surface and wins, without having to out-specify anything.
|
|
@@ -16923,8 +17113,7 @@ const css$14 = /* css */ `
|
|
|
16923
17113
|
--callout-error-color: var(--navi-callout-error-color);
|
|
16924
17114
|
--callout-neutral-color: var(--navi-callout-neutral-color);
|
|
16925
17115
|
|
|
16926
|
-
--callout-background-color:
|
|
16927
|
-
--callout-icon-color: black;
|
|
17116
|
+
--callout-background-color: var(--navi-popup-background-color);
|
|
16928
17117
|
--callout-padding: 8px;
|
|
16929
17118
|
--callout-z-index: var(--navi-z-index-callout);
|
|
16930
17119
|
/* The page's line, said here rather than inherited: the icon and the
|
|
@@ -16978,11 +17167,11 @@ const css$14 = /* css */ `
|
|
|
16978
17167
|
height: auto; /* User agent reset */
|
|
16979
17168
|
margin: 0;
|
|
16980
17169
|
padding: 0; /* User agent reset */
|
|
16981
|
-
/*
|
|
16982
|
-
|
|
16983
|
-
color
|
|
16984
|
-
navi_css_vars.js
|
|
16985
|
-
color:
|
|
17170
|
+
/* A callout sits inside the element but writes on its own paper: the
|
|
17171
|
+
popup's ink, paired with the popup's paper it is drawn on, so the two
|
|
17172
|
+
follow color-scheme and the theme together. The color keywords are
|
|
17173
|
+
re-declared against that same ink in navi_css_vars.js. */
|
|
17174
|
+
color: var(--navi-popup-color);
|
|
16986
17175
|
font-weight: initial; /* Callout fells disconnected from the element, font weight should be predictible and stable */
|
|
16987
17176
|
font-size: initial; /* Callout fells disconnected from the element, font size should be predictible and stable */
|
|
16988
17177
|
line-height: var(
|
|
@@ -19294,16 +19483,18 @@ const findBusySource = (field) => {
|
|
|
19294
19483
|
// a new interaction is queued behind the run (see the action queue in
|
|
19295
19484
|
// control_hooks.jsx) rather than refused.
|
|
19296
19485
|
if (!field.optimistic && boundAction) {
|
|
19297
|
-
// The
|
|
19298
|
-
//
|
|
19299
|
-
//
|
|
19300
|
-
//
|
|
19301
|
-
//
|
|
19302
|
-
//
|
|
19303
|
-
//
|
|
19304
|
-
//
|
|
19305
|
-
|
|
19306
|
-
|
|
19486
|
+
// The instance the run in flight uses, as the render reads it (see
|
|
19487
|
+
// runningActionSignal in control_hooks.jsx): the state a proxy follows can
|
|
19488
|
+
// move mid-run — a popup content unmounting takes the group's value — and
|
|
19489
|
+
// the proxy would resolve to an idle instance. With no run out, the
|
|
19490
|
+
// instance the proxy resolves to right now, never the proxy's own signal:
|
|
19491
|
+
// a MIRROR synced by an effect the settling batch defers, which still says
|
|
19492
|
+
// RUNNING mid-batch (a state echo, an automatic follow-up) for an action
|
|
19493
|
+
// already over. An instance is COMPLETED the moment it settles.
|
|
19494
|
+
const liveAction =
|
|
19495
|
+
field.runningActionSignal.value ??
|
|
19496
|
+
boundAction.getCurrentAction?.() ??
|
|
19497
|
+
boundAction;
|
|
19307
19498
|
if (liveAction.runningStateSignal.value === RUNNING) {
|
|
19308
19499
|
return { field, action: liveAction };
|
|
19309
19500
|
}
|
|
@@ -20341,11 +20532,10 @@ const READONLY_CONSTRAINT = {
|
|
|
20341
20532
|
name: "readonly",
|
|
20342
20533
|
messageAttribute: "data-readonly-message",
|
|
20343
20534
|
check: (field, { intent } = {}) => {
|
|
20344
|
-
|
|
20345
|
-
|
|
20346
|
-
|
|
20347
|
-
)
|
|
20348
|
-
if (!readOnly) {
|
|
20535
|
+
// Not off the rendered `readOnly`/`aria-readonly`: they also carry the run
|
|
20536
|
+
// of the control's own action, a frame behind (see readOnlyWithoutRun in
|
|
20537
|
+
// control_hooks.jsx). That run is BUSY_CONSTRAINT's, which reads it live.
|
|
20538
|
+
if (!field.readOnlyWithoutRun) {
|
|
20349
20539
|
return null;
|
|
20350
20540
|
}
|
|
20351
20541
|
|
|
@@ -30928,6 +31118,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
|
|
|
30928
31118
|
* footer?: boolean,
|
|
30929
31119
|
* body?: boolean,
|
|
30930
31120
|
* sticky?: boolean,
|
|
31121
|
+
* mount?: "always" | "after-paint",
|
|
30931
31122
|
* children?: import("ignore:preact").ComponentChildren,
|
|
30932
31123
|
* [key: string]: any,
|
|
30933
31124
|
* }>}
|
|
@@ -30976,6 +31167,16 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
|
|
|
30976
31167
|
* (see docs/interactions.md). A plain box has no wiring of its own: it does
|
|
30977
31168
|
* nothing with `action` (that is a control's prop), so a click on it is
|
|
30978
31169
|
* declared here like any other interaction: `interactions={{ click: fn }}`.
|
|
31170
|
+
* @param {"always"|"after-paint"} [mount="always"] When `children` are built.
|
|
31171
|
+
* `"after-paint"` leaves the box empty in the render that first mounts it and
|
|
31172
|
+
* builds them right after the first frame that shows the page, then keeps
|
|
31173
|
+
* them: for the sections of a page that lie below its first screen. navi
|
|
31174
|
+
* measures where the box stands before that frame, and one the frame would
|
|
31175
|
+
* show (or where the document is put back, on a return) is built before the
|
|
31176
|
+
* paint all the same. Until built, what is inside is not in the document:
|
|
31177
|
+
* no id there is found, no named control there is there to hear its own
|
|
31178
|
+
* `signal` (see docs/control_object.md). Only for content nothing reaches
|
|
31179
|
+
* before the first frame (see docs/scroll.md).
|
|
30979
31180
|
*/
|
|
30980
31181
|
const Box = props => {
|
|
30981
31182
|
const {
|
|
@@ -30983,6 +31184,7 @@ const Box = props => {
|
|
|
30983
31184
|
children,
|
|
30984
31185
|
separator,
|
|
30985
31186
|
interactions,
|
|
31187
|
+
mount,
|
|
30986
31188
|
...computeProps
|
|
30987
31189
|
} = props;
|
|
30988
31190
|
const parentBoxFlow = useContext(BoxFlowContext);
|
|
@@ -31060,6 +31262,12 @@ const Box = props => {
|
|
|
31060
31262
|
children: innerChildren
|
|
31061
31263
|
});
|
|
31062
31264
|
}
|
|
31265
|
+
if (mount === "after-paint") {
|
|
31266
|
+
innerChildren = jsx(MountAfterPaint, {
|
|
31267
|
+
boxRef: finalRef,
|
|
31268
|
+
children: innerChildren
|
|
31269
|
+
});
|
|
31270
|
+
}
|
|
31063
31271
|
return jsx(TagName, {
|
|
31064
31272
|
ref: finalRef,
|
|
31065
31273
|
className: innerClassName,
|
|
@@ -34630,6 +34838,13 @@ const OLD_BAND_TOP_PROPERTY = "--navi-transition-old-band-top";
|
|
|
34630
34838
|
const OLD_BAND_RIGHT_PROPERTY = "--navi-transition-old-band-right";
|
|
34631
34839
|
const OLD_BAND_BOTTOM_PROPERTY = "--navi-transition-old-band-bottom";
|
|
34632
34840
|
const OLD_BAND_LEFT_PROPERTY = "--navi-transition-old-band-left";
|
|
34841
|
+
const VIEWPORT_TOP_PROPERTY = "--navi-transition-viewport-top";
|
|
34842
|
+
const VIEWPORT_LEFT_PROPERTY = "--navi-transition-viewport-left";
|
|
34843
|
+
// Worn by the root once the group is held at the window by our numbers alone
|
|
34844
|
+
// (see placeTransitionWindow): the group rule of each movement is written on
|
|
34845
|
+
// it. Read by rules on pseudo-elements only, so wearing it restyles nothing
|
|
34846
|
+
// the document inherits.
|
|
34847
|
+
const WINDOW_PLACED_ATTRIBUTE = "data-navi-transition-window-placed";
|
|
34633
34848
|
const WINDOW_PROPERTIES = [
|
|
34634
34849
|
WINDOW_TOP_PROPERTY,
|
|
34635
34850
|
WINDOW_LEFT_PROPERTY,
|
|
@@ -34643,6 +34858,8 @@ const WINDOW_PROPERTIES = [
|
|
|
34643
34858
|
OLD_BAND_RIGHT_PROPERTY,
|
|
34644
34859
|
OLD_BAND_BOTTOM_PROPERTY,
|
|
34645
34860
|
OLD_BAND_LEFT_PROPERTY,
|
|
34861
|
+
VIEWPORT_TOP_PROPERTY,
|
|
34862
|
+
VIEWPORT_LEFT_PROPERTY,
|
|
34646
34863
|
];
|
|
34647
34864
|
|
|
34648
34865
|
// Whose numbers are currently published. The window belongs to the movement
|
|
@@ -34650,6 +34867,8 @@ const WINDOW_PROPERTIES = [
|
|
|
34650
34867
|
// after another has replaced it must not wipe numbers the new one is standing
|
|
34651
34868
|
// on.
|
|
34652
34869
|
let windowOwner = null;
|
|
34870
|
+
// The element the box is captured as, for as long as the window is held.
|
|
34871
|
+
let windowElement = null;
|
|
34653
34872
|
// Stops re-reading the arriving state's corner, when a movement is on.
|
|
34654
34873
|
let unfollowArrivingState = null;
|
|
34655
34874
|
|
|
@@ -34705,6 +34924,10 @@ const holdTransitionWindow = (
|
|
|
34705
34924
|
const right =
|
|
34706
34925
|
rectBefore.right > rectAfter.right ? rectBefore.right : rectAfter.right;
|
|
34707
34926
|
windowOwner = owner;
|
|
34927
|
+
windowElement = element;
|
|
34928
|
+
// Pictures of a movement this one took over from are placed by the browser
|
|
34929
|
+
// until this one's own pictures exist.
|
|
34930
|
+
document.documentElement.removeAttribute(WINDOW_PLACED_ATTRIBUTE);
|
|
34708
34931
|
setTransitionValue(WINDOW_TOP_PROPERTY, `${top}px`);
|
|
34709
34932
|
setTransitionValue(WINDOW_LEFT_PROPERTY, `${left}px`);
|
|
34710
34933
|
setTransitionValue(WINDOW_WIDTH_PROPERTY, `${right - left}px`);
|
|
@@ -34729,6 +34952,33 @@ const holdTransitionWindow = (
|
|
|
34729
34952
|
});
|
|
34730
34953
|
};
|
|
34731
34954
|
|
|
34955
|
+
/**
|
|
34956
|
+
* Called once the pictures exist — the view transition's `ready` — with the
|
|
34957
|
+
* name the box was captured under. Where the viewport's corner stands in the
|
|
34958
|
+
* space the pictures are drawn in is the browser's placement of the group
|
|
34959
|
+
* minus our reading of the same element, both taken in this one instant; the
|
|
34960
|
+
* group is held at the window with it from then on (see the top of the file).
|
|
34961
|
+
* A browser that does not hand the group's placement back leaves the group
|
|
34962
|
+
* where it places it, moved to the window by the arriving corner.
|
|
34963
|
+
*/
|
|
34964
|
+
const placeTransitionWindow = (owner, groupName) => {
|
|
34965
|
+
if (owner !== windowOwner) {
|
|
34966
|
+
return;
|
|
34967
|
+
}
|
|
34968
|
+
const { transform } = getComputedStyle(
|
|
34969
|
+
document.documentElement,
|
|
34970
|
+
`::view-transition-group(${groupName})`,
|
|
34971
|
+
);
|
|
34972
|
+
if (!transform || transform === "none") {
|
|
34973
|
+
return;
|
|
34974
|
+
}
|
|
34975
|
+
const placement = new DOMMatrixReadOnly(transform);
|
|
34976
|
+
const rect = windowElement.getBoundingClientRect();
|
|
34977
|
+
setTransitionValue(VIEWPORT_TOP_PROPERTY, `${placement.m42 - rect.top}px`);
|
|
34978
|
+
setTransitionValue(VIEWPORT_LEFT_PROPERTY, `${placement.m41 - rect.left}px`);
|
|
34979
|
+
document.documentElement.setAttribute(WINDOW_PLACED_ATTRIBUTE, "");
|
|
34980
|
+
};
|
|
34981
|
+
|
|
34732
34982
|
// The live layout takes the box back. A discontinuity by construction — the
|
|
34733
34983
|
// window stands at the held height, the box is at its own — and an invisible
|
|
34734
34984
|
// one: the page arriving is fully in place, and the strip below it that the
|
|
@@ -34738,20 +34988,22 @@ const releaseTransitionWindow = (owner) => {
|
|
|
34738
34988
|
return;
|
|
34739
34989
|
}
|
|
34740
34990
|
windowOwner = null;
|
|
34991
|
+
windowElement = null;
|
|
34741
34992
|
if (unfollowArrivingState) {
|
|
34742
34993
|
unfollowArrivingState();
|
|
34743
34994
|
}
|
|
34995
|
+
document.documentElement.removeAttribute(WINDOW_PLACED_ATTRIBUTE);
|
|
34744
34996
|
for (const property of WINDOW_PROPERTIES) {
|
|
34745
34997
|
removeTransitionValue(property);
|
|
34746
34998
|
}
|
|
34747
34999
|
};
|
|
34748
35000
|
|
|
34749
|
-
// The arriving state's corner, re-read every frame — as often as
|
|
34750
|
-
//
|
|
34751
|
-
//
|
|
34752
|
-
//
|
|
34753
|
-
//
|
|
34754
|
-
//
|
|
35001
|
+
// The arriving state's corner, re-read every frame — as often as anything can
|
|
35002
|
+
// move the page arriving (see the top of the file) — and with it the offset
|
|
35003
|
+
// outside the box, which carries the window and the corner of the state being
|
|
35004
|
+
// left along. Requested, never awaited: the first call runs inside the update
|
|
35005
|
+
// callback, where a frame cannot come. Written only when it moved: every write
|
|
35006
|
+
// restyles the pictures.
|
|
34755
35007
|
const followArrivingState = (element, rectAtHold, heldAt) => {
|
|
34756
35008
|
if (unfollowArrivingState) {
|
|
34757
35009
|
unfollowArrivingState();
|
|
@@ -35256,6 +35508,13 @@ const css$12 = /* css */`@property --navi-route-transition-leave {
|
|
|
35256
35508
|
animation-name: none;
|
|
35257
35509
|
}
|
|
35258
35510
|
|
|
35511
|
+
&[data-navi-transition-window-placed]::view-transition-group(navi-route-transition) {
|
|
35512
|
+
top: calc(var(--navi-transition-window-top) + var(--navi-transition-viewport-top));
|
|
35513
|
+
left: calc(var(--navi-transition-window-left) +
|
|
35514
|
+
var(--navi-transition-viewport-left));
|
|
35515
|
+
transform: none;
|
|
35516
|
+
}
|
|
35517
|
+
|
|
35259
35518
|
&::view-transition-old(navi-route-transition) {
|
|
35260
35519
|
top: calc(var(--navi-transition-window-old-top) - var(--navi-transition-window-top));
|
|
35261
35520
|
left: calc(var(--navi-transition-window-old-left) - var(--navi-transition-window-left));
|
|
@@ -36303,6 +36562,8 @@ const beginTransition = ({
|
|
|
36303
36562
|
// the only place the silent misconfigurations show. They are all about the
|
|
36304
36563
|
// same thing — a movement playing on pictures that are not the pages.
|
|
36305
36564
|
const viewTransitionReady = () => {
|
|
36565
|
+
// Read before anything below writes to the document.
|
|
36566
|
+
placeTransitionWindow(transition, AREA_NAME);
|
|
36306
36567
|
startTransitionFurniture(transition, {
|
|
36307
36568
|
duration: transition.duration
|
|
36308
36569
|
});
|
|
@@ -36814,6 +37075,13 @@ const css$11 = /* css */`.navi_route_travel {
|
|
|
36814
37075
|
animation-duration: var(--navi-route-travel-duration, .3s);
|
|
36815
37076
|
animation-name: none;
|
|
36816
37077
|
}
|
|
37078
|
+
|
|
37079
|
+
&[data-navi-transition-window-placed]::view-transition-group(navi-route-travel) {
|
|
37080
|
+
top: calc(var(--navi-transition-window-top) + var(--navi-transition-viewport-top));
|
|
37081
|
+
left: calc(var(--navi-transition-window-left) +
|
|
37082
|
+
var(--navi-transition-viewport-left));
|
|
37083
|
+
transform: none;
|
|
37084
|
+
}
|
|
36817
37085
|
}
|
|
36818
37086
|
|
|
36819
37087
|
:root[data-navi-route-travel-held] {
|
|
@@ -37192,6 +37460,10 @@ const RouteTravel = ({
|
|
|
37192
37460
|
});
|
|
37193
37461
|
});
|
|
37194
37462
|
travel.viewTransition = viewTransition;
|
|
37463
|
+
// Subscribed first, so it reads before the scrub below writes anything.
|
|
37464
|
+
viewTransition.ready.then(() => {
|
|
37465
|
+
placeTransitionWindow(travel, TRAVEL_NAME);
|
|
37466
|
+
}, ignoreSkipped);
|
|
37195
37467
|
if (scrub) {
|
|
37196
37468
|
// Said only now: the release has to have something to let go of, and the
|
|
37197
37469
|
// transition did not exist a line above.
|
|
@@ -37391,16 +37663,20 @@ const RouteTravel = ({
|
|
|
37391
37663
|
// what makes the document tall enough to hold that offset again —
|
|
37392
37664
|
// and the recording stays deaf until then, over the clamp the swap
|
|
37393
37665
|
// back makes on the way. Its own deafness rather than the travel's:
|
|
37394
|
-
// the travel ends here, and this outlives it by a render.
|
|
37666
|
+
// the travel ends here, and this outlives it by a render. Written
|
|
37667
|
+
// once the commit that says so has ended, as arriveAtScrollPosition
|
|
37668
|
+
// asks.
|
|
37395
37669
|
const resumeScrollRecording = suspendScrollRecording();
|
|
37396
37670
|
const stopWatchingRender = observeRouteRender(() => {
|
|
37397
37671
|
stopWatchingRender();
|
|
37398
|
-
|
|
37399
|
-
|
|
37400
|
-
|
|
37401
|
-
|
|
37402
|
-
|
|
37403
|
-
|
|
37672
|
+
queueMicrotask(() => {
|
|
37673
|
+
const {
|
|
37674
|
+
route,
|
|
37675
|
+
params
|
|
37676
|
+
} = travel.fromPage;
|
|
37677
|
+
arriveAtScrollPosition(route.buildUrl(params));
|
|
37678
|
+
resumeScrollRecording();
|
|
37679
|
+
});
|
|
37404
37680
|
});
|
|
37405
37681
|
}
|
|
37406
37682
|
releaseRendering();
|
|
@@ -40184,34 +40460,6 @@ registerNaviCommand("--navi-unselect", (source, event) => {
|
|
|
40184
40460
|
};
|
|
40185
40461
|
});
|
|
40186
40462
|
|
|
40187
|
-
/**
|
|
40188
|
-
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
40189
|
-
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
40190
|
-
* mounted run inside the call too, exactly as they would on any other commit.
|
|
40191
|
-
*
|
|
40192
|
-
* For the caller that has to read the DOM it just asked for — measuring an
|
|
40193
|
-
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
40194
|
-
* to do it, because what comes after is a browser event still in flight
|
|
40195
|
-
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
40196
|
-
*
|
|
40197
|
-
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
40198
|
-
* render queue drains; swapping it for "right now" for the duration of the call
|
|
40199
|
-
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
40200
|
-
* case above: rendering synchronously in the middle of an event gives up the
|
|
40201
|
-
* batching that makes several state changes one commit.
|
|
40202
|
-
*/
|
|
40203
|
-
const flushSyncRendering = (fn) => {
|
|
40204
|
-
const debounceRenderingPrevious = options.debounceRendering;
|
|
40205
|
-
options.debounceRendering = (drainRenderQueue) => {
|
|
40206
|
-
drainRenderQueue();
|
|
40207
|
-
};
|
|
40208
|
-
try {
|
|
40209
|
-
fn();
|
|
40210
|
-
} finally {
|
|
40211
|
-
options.debounceRendering = debounceRenderingPrevious;
|
|
40212
|
-
}
|
|
40213
|
-
};
|
|
40214
|
-
|
|
40215
40463
|
/**
|
|
40216
40464
|
* When a popup builds what it holds, and when it throws it away.
|
|
40217
40465
|
*
|
|
@@ -42534,6 +42782,65 @@ const keptOrFirstDefinedChildUIState = (children, fallbackState, kept) => {
|
|
|
42534
42782
|
}
|
|
42535
42783
|
return kept;
|
|
42536
42784
|
};
|
|
42785
|
+
// The same claim, for an object: a key whose control is not there cannot be
|
|
42786
|
+
// contradicted by it — a slide not built yet (SlideContainer mount="near"), a
|
|
42787
|
+
// row scrolled out, a page parked. A key of `kept` stays until a control of
|
|
42788
|
+
// that name, there, says otherwise; a value put ON the group replaces the
|
|
42789
|
+
// whole object, which is how a key is dropped on purpose.
|
|
42790
|
+
const keptAndNamedChildUIStates = (children, fallbackState, kept) => {
|
|
42791
|
+
const groupValues =
|
|
42792
|
+
kept !== null && typeof kept === "object" && !Array.isArray(kept)
|
|
42793
|
+
? { ...kept }
|
|
42794
|
+
: {};
|
|
42795
|
+
return Object.assign(
|
|
42796
|
+
groupValues,
|
|
42797
|
+
readNamedChildUIStates(children, { warnNameless: true }),
|
|
42798
|
+
);
|
|
42799
|
+
};
|
|
42800
|
+
// What the named controls among `children` say, one key each. A nameless
|
|
42801
|
+
// GROUP is a grouping, not a value: it exists to hold its children together
|
|
42802
|
+
// (a WheelGroup sharing navigation, a fieldset-ish cluster) without claiming a
|
|
42803
|
+
// key of its own, so what its controls say is written as if they were here.
|
|
42804
|
+
// Naming it is what turns the same group into one key — see ControlGroup's
|
|
42805
|
+
// `name`. Only what its CONTROLS say: such a grouping is handed the whole
|
|
42806
|
+
// object around it (see distributeChildUIState below) and keeps what they do
|
|
42807
|
+
// not answer for, so merged whole it would carry its siblings' keys back up,
|
|
42808
|
+
// stale.
|
|
42809
|
+
const readNamedChildUIStates = (children, { warnNameless }) => {
|
|
42810
|
+
const values = {};
|
|
42811
|
+
for (const child of children) {
|
|
42812
|
+
const { name, emptyUIState } = child;
|
|
42813
|
+
// A control holding nothing writes its own empty, not a hole: the key is in
|
|
42814
|
+
// the object either way, and what is read from it keeps the shape the
|
|
42815
|
+
// reader was promised (see resolveEmptyUIState).
|
|
42816
|
+
const uiState =
|
|
42817
|
+
child.uiState === undefined && emptyUIState !== undefined
|
|
42818
|
+
? emptyUIState
|
|
42819
|
+
: child.uiState;
|
|
42820
|
+
if (!name) {
|
|
42821
|
+
if (isNamelessGrouping(child, uiState)) {
|
|
42822
|
+
Object.assign(
|
|
42823
|
+
values,
|
|
42824
|
+
child.keepsUnansweredKeys
|
|
42825
|
+
? readNamedChildUIStates(child.getChildControllers(), {
|
|
42826
|
+
warnNameless: false,
|
|
42827
|
+
})
|
|
42828
|
+
: uiState,
|
|
42829
|
+
);
|
|
42830
|
+
continue;
|
|
42831
|
+
}
|
|
42832
|
+
if (warnNameless) {
|
|
42833
|
+
console.warn(
|
|
42834
|
+
"A group child is missing a name property, its state won't be included in the group state",
|
|
42835
|
+
child,
|
|
42836
|
+
);
|
|
42837
|
+
}
|
|
42838
|
+
continue;
|
|
42839
|
+
}
|
|
42840
|
+
values[name] = uiState;
|
|
42841
|
+
}
|
|
42842
|
+
return values;
|
|
42843
|
+
};
|
|
42537
42844
|
|
|
42538
42845
|
// Default aggregate/distribute implementations keyed by controlType or stateType.
|
|
42539
42846
|
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
@@ -42603,37 +42910,7 @@ const GROUP_DEFAULTS = {
|
|
|
42603
42910
|
childControlFilter: (child) => {
|
|
42604
42911
|
return child.controlType !== "button" && child.controlType !== "link";
|
|
42605
42912
|
},
|
|
42606
|
-
aggregateChildStates:
|
|
42607
|
-
const groupValues = {};
|
|
42608
|
-
for (const child of children) {
|
|
42609
|
-
const { name, emptyUIState } = child;
|
|
42610
|
-
// A control holding nothing writes its own empty, not a hole: the key
|
|
42611
|
-
// is in the object either way, and what is read from it keeps the shape
|
|
42612
|
-
// the reader was promised (see resolveEmptyUIState).
|
|
42613
|
-
const uiState =
|
|
42614
|
-
child.uiState === undefined && emptyUIState !== undefined
|
|
42615
|
-
? emptyUIState
|
|
42616
|
-
: child.uiState;
|
|
42617
|
-
if (!name) {
|
|
42618
|
-
// A nameless GROUP is a grouping, not a value: it exists to hold its
|
|
42619
|
-
// children together (a WheelGroup sharing navigation, a fieldset-ish
|
|
42620
|
-
// cluster) without claiming a key of its own, so what it holds is
|
|
42621
|
-
// merged in as if its children had been written here. Naming it is
|
|
42622
|
-
// what turns the same group into one key — see ControlGroup's `name`.
|
|
42623
|
-
if (isNamelessGrouping(child, uiState)) {
|
|
42624
|
-
Object.assign(groupValues, uiState);
|
|
42625
|
-
continue;
|
|
42626
|
-
}
|
|
42627
|
-
console.warn(
|
|
42628
|
-
"A group child is missing a name property, its state won't be included in the group state",
|
|
42629
|
-
child,
|
|
42630
|
-
);
|
|
42631
|
-
continue;
|
|
42632
|
-
}
|
|
42633
|
-
groupValues[name] = uiState;
|
|
42634
|
-
}
|
|
42635
|
-
return groupValues;
|
|
42636
|
-
},
|
|
42913
|
+
aggregateChildStates: keptAndNamedChildUIStates,
|
|
42637
42914
|
distributeChildUIState: (newUIState, child) => {
|
|
42638
42915
|
const childName = child.name;
|
|
42639
42916
|
if (
|
|
@@ -42657,7 +42934,8 @@ const GROUP_DEFAULTS = {
|
|
|
42657
42934
|
return childUIState;
|
|
42658
42935
|
}
|
|
42659
42936
|
// Merged in on the way up (see above), so on the way down it takes the
|
|
42660
|
-
// whole object
|
|
42937
|
+
// whole object: its controls pick out their own keys, and it keeps the
|
|
42938
|
+
// rest for those of its controls that are not there yet.
|
|
42661
42939
|
// Only for a child with no name of its own: the same condition the
|
|
42662
42940
|
// aggregate applies before merging. A NAMED child holding an object (a
|
|
42663
42941
|
// `type="object"` picker, say) answers for one key, and the object it is
|
|
@@ -43145,6 +43423,11 @@ const useUIGroupStateController = (
|
|
|
43145
43423
|
// a picker filling its popup, a value prop) rather than worked out from
|
|
43146
43424
|
// its children. What it protects is read in onChange.
|
|
43147
43425
|
stateGivenFromAbove: hasValueProp || hasDefaultValueProp,
|
|
43426
|
+
// Its value carries keys none of its controls answers for (see
|
|
43427
|
+
// keptAndNamedChildUIStates): a group merging it as a nameless
|
|
43428
|
+
// grouping reads its controls instead of its value.
|
|
43429
|
+
keepsUnansweredKeys:
|
|
43430
|
+
resolvedAggregateChildStates === keptAndNamedChildUIStates,
|
|
43148
43431
|
uiStateSignal,
|
|
43149
43432
|
wantRequesterButtonState,
|
|
43150
43433
|
ref,
|
|
@@ -43782,6 +44065,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
43782
44065
|
ref: realUIStateController.ref,
|
|
43783
44066
|
uiStateSignal: realUIStateController.uiStateSignal,
|
|
43784
44067
|
controlHostProps: realUIStateController.controlHostProps,
|
|
44068
|
+
readOnlyWithoutRun: realUIStateController.readOnlyWithoutRun,
|
|
43785
44069
|
registerChild: (child) => {
|
|
43786
44070
|
if (!canRegisterAsFacadeChild(child)) {
|
|
43787
44071
|
return;
|
|
@@ -43911,6 +44195,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
43911
44195
|
s.controller.ref = realUIStateController.ref;
|
|
43912
44196
|
s.controller.uiStateSignal = realUIStateController.uiStateSignal;
|
|
43913
44197
|
s.controller.controlHostProps = realUIStateController.controlHostProps;
|
|
44198
|
+
s.controller.readOnlyWithoutRun =
|
|
44199
|
+
realUIStateController.readOnlyWithoutRun;
|
|
43914
44200
|
realUIStateController.pushStateDownToFacadeChild =
|
|
43915
44201
|
s.controller.pushStateDownToChild;
|
|
43916
44202
|
|
|
@@ -45819,6 +46105,11 @@ const useInteractiveProps = (props, {
|
|
|
45819
46105
|
const actionLoading = optimistic ? false : actionRunning;
|
|
45820
46106
|
const loadingResolved = loadingBase || actionLoading;
|
|
45821
46107
|
const readOnlyResolved = readOnlyBase || actionLoading;
|
|
46108
|
+
// What READONLY_CONSTRAINT refuses for. The run of the control's own action
|
|
46109
|
+
// is BUSY_CONSTRAINT's, read live: readOnlyResolved is a render snapshot,
|
|
46110
|
+
// still read-only in the tick the run settles — the tick a form's
|
|
46111
|
+
// after-send `--navi-reset` asks the form to empty itself.
|
|
46112
|
+
uiStateController.readOnlyWithoutRun = Boolean(readOnlyBase);
|
|
45822
46113
|
// Read-only, and what this control opens still opens: reading what is in
|
|
45823
46114
|
// there changes nothing. Read by READONLY_CONSTRAINT, which lets an
|
|
45824
46115
|
// interaction that only reads through on it.
|
|
@@ -46273,10 +46564,10 @@ const useInteractiveProps = (props, {
|
|
|
46273
46564
|
});
|
|
46274
46565
|
}
|
|
46275
46566
|
// controlHostProps is a curated subset of props with resolved values applied
|
|
46276
|
-
// (e.g. readOnly resolved from context + action loading). The
|
|
46277
|
-
//
|
|
46278
|
-
//
|
|
46279
|
-
//
|
|
46567
|
+
// (e.g. readOnly resolved from context + action loading). The constraints
|
|
46568
|
+
// read off uiStateController.controlHostProps at runtime (e.g. DISABLED_CONSTRAINT,
|
|
46569
|
+
// the readonly reason), so pointing the controller at controlHostProps keeps
|
|
46570
|
+
// those reads current without any extra bookkeeping.
|
|
46280
46571
|
const firstRender = uiStateController.controlHostProps === undefined;
|
|
46281
46572
|
uiStateController.controlHostProps = controlHostProps;
|
|
46282
46573
|
// The action itself, not just what the last render made of it: its running
|
|
@@ -53746,7 +54037,8 @@ const useExpandableContext = partName => {
|
|
|
53746
54037
|
* other three, intent on the UI part (pointer entering it, focus landing in
|
|
53747
54038
|
* it) builds the content ahead of the click; `"while-opened"` content is
|
|
53748
54039
|
* built by a press on it only, and thrown away if that press does not
|
|
53749
|
-
* expand it.
|
|
54040
|
+
* expand it. Named controls in content not built are kept by the group
|
|
54041
|
+
* around them (docs/control_object.md, "A control that is not there").
|
|
53750
54042
|
*/
|
|
53751
54043
|
const Expandable = props => {
|
|
53752
54044
|
import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
@@ -55332,14 +55624,34 @@ const ButtonConfirm = ({
|
|
|
55332
55624
|
}
|
|
55333
55625
|
return jsxs("span", {
|
|
55334
55626
|
className: "navi_button_confirm",
|
|
55335
|
-
ref: askingRef
|
|
55627
|
+
ref: askingRef
|
|
55628
|
+
// A place for the focus, so a press anywhere in the question keeps it
|
|
55629
|
+
// inside: WebKit does not focus a pressed button, and a press on
|
|
55630
|
+
// "Confirmer" would otherwise blur toward <body> — a focusout with no
|
|
55631
|
+
// relatedTarget, read below as the person leaving. The callout answers
|
|
55632
|
+
// the same ambiguity the same way.
|
|
55633
|
+
,
|
|
55634
|
+
tabIndex: -1
|
|
55635
|
+
// "not me, unless you have nothing else": the ladder in the effect above
|
|
55636
|
+
// walks past the question to what it holds.
|
|
55637
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
55638
|
+
,
|
|
55639
|
+
"navi-autofocus": "last-resort",
|
|
55336
55640
|
onKeyDown: e => {
|
|
55337
55641
|
if (e.key === "Escape") {
|
|
55338
55642
|
cancel(true);
|
|
55339
55643
|
}
|
|
55340
55644
|
},
|
|
55341
55645
|
onFocusOut: e => {
|
|
55342
|
-
|
|
55646
|
+
// Chrome also fires this from the question's own removal, once preact
|
|
55647
|
+
// has cleared the ref: nobody left, and cancel(false) would undo the
|
|
55648
|
+
// focus the answer gives back. That focus cannot go back before the
|
|
55649
|
+
// removal: the button it lands on is mounted by the same commit.
|
|
55650
|
+
const askingEl = askingRef.current;
|
|
55651
|
+
if (!askingEl) {
|
|
55652
|
+
return;
|
|
55653
|
+
}
|
|
55654
|
+
if (!askingEl.contains(e.relatedTarget)) {
|
|
55343
55655
|
cancel(false);
|
|
55344
55656
|
}
|
|
55345
55657
|
},
|
|
@@ -60837,12 +61149,19 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
60837
61149
|
* would bring alongside (neighbours on the map, along the axes
|
|
60838
61150
|
* `travelByDrag` opens, the other end of a `loop` included) — and kept from
|
|
60839
61151
|
* then on. For slides that are costly to build (a grid of a few hundred
|
|
60840
|
-
*
|
|
61152
|
+
* items each): opening builds the slide shown and its neighbours rather
|
|
60841
61153
|
* than all of them, and a jump to a far slide builds that one and none of
|
|
60842
61154
|
* those in between, which a travel never shows. The box is then as large as
|
|
60843
61155
|
* the largest slide BUILT so far (see `sizing`), so it grows as a larger one
|
|
60844
61156
|
* is reached: give it a size of its own when the slides differ.
|
|
60845
61157
|
* `<Slide mount>` says it for one slide — a heavy one among light ones.
|
|
61158
|
+
* A slide not built has no controls, and a group around the container keeps
|
|
61159
|
+
* their keys for them: what it was handed reaches them once built. What is
|
|
61160
|
+
* shared between slides is written through that group (its `signal`, a
|
|
61161
|
+
* `--navi-update` aimed at it), never into one control's own `signal`, which
|
|
61162
|
+
* nobody reads while that control is not built; and nothing validates a
|
|
61163
|
+
* field before its slide is built. See docs/control_object.md, "A control
|
|
61164
|
+
* that is not there".
|
|
60846
61165
|
*/
|
|
60847
61166
|
const SlideContainer = ({
|
|
60848
61167
|
layout = "row",
|
|
@@ -70926,44 +71245,6 @@ document.addEventListener(
|
|
|
70926
71245
|
{ capture: true, passive: true },
|
|
70927
71246
|
);
|
|
70928
71247
|
|
|
70929
|
-
const FRAME_TIMEOUT_MS = 100;
|
|
70930
|
-
|
|
70931
|
-
/**
|
|
70932
|
-
* Calls back once the browser has painted what is committed now.
|
|
70933
|
-
*
|
|
70934
|
-
* The callbacks of the next frame run before that frame paints, so the call is
|
|
70935
|
-
* made from a task queued from inside one — the same way preact schedules
|
|
70936
|
-
* useEffect. Unlike an effect, nothing runs this early: preact flushes a
|
|
70937
|
-
* component's pending effects as soon as that component renders again, and a
|
|
70938
|
-
* re-render before the paint is exactly what a caller waiting for the paint
|
|
70939
|
-
* has to survive. A frame not coming at all (a background tab) still answers,
|
|
70940
|
-
* late, through the timeout.
|
|
70941
|
-
*
|
|
70942
|
-
* @param {() => void} callback
|
|
70943
|
-
* @returns {() => void} cancel
|
|
70944
|
-
*/
|
|
70945
|
-
const afterPaint = (callback) => {
|
|
70946
|
-
let called = false;
|
|
70947
|
-
let timeoutId;
|
|
70948
|
-
let frameId;
|
|
70949
|
-
const onFrame = () => {
|
|
70950
|
-
if (called) {
|
|
70951
|
-
return;
|
|
70952
|
-
}
|
|
70953
|
-
called = true;
|
|
70954
|
-
clearTimeout(timeoutId);
|
|
70955
|
-
cancelAnimationFrame(frameId);
|
|
70956
|
-
timeoutId = setTimeout(callback, 0);
|
|
70957
|
-
};
|
|
70958
|
-
timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
|
|
70959
|
-
frameId = requestAnimationFrame(onFrame);
|
|
70960
|
-
return () => {
|
|
70961
|
-
called = true;
|
|
70962
|
-
clearTimeout(timeoutId);
|
|
70963
|
-
cancelAnimationFrame(frameId);
|
|
70964
|
-
};
|
|
70965
|
-
};
|
|
70966
|
-
|
|
70967
71248
|
const LoadingDotsSvg = () => {
|
|
70968
71249
|
return jsxs("svg", {
|
|
70969
71250
|
viewBox: "0 0 200 200",
|
|
@@ -72994,7 +73275,7 @@ const css$x = /* css */`@property --list-group-label-height {
|
|
|
72994
73275
|
}
|
|
72995
73276
|
}
|
|
72996
73277
|
|
|
72997
|
-
.navi_list[navi-box-flow="grid"] > .navi_list_virtual_filler, .navi_list[navi-box-flow="grid"] > .navi_separator {
|
|
73278
|
+
.navi_list[navi-box-flow="grid"] > .navi_list_virtual_filler, .navi_list[navi-box-flow="grid"] > .navi_list_failed_items, .navi_list[navi-box-flow="grid"] > .navi_separator {
|
|
72998
73279
|
grid-column: 1 / -1;
|
|
72999
73280
|
}
|
|
73000
73281
|
|
|
@@ -73371,7 +73652,8 @@ const ListUI = props => {
|
|
|
73371
73652
|
listId: rest.id,
|
|
73372
73653
|
scroller,
|
|
73373
73654
|
searchText,
|
|
73374
|
-
horizontal
|
|
73655
|
+
horizontal,
|
|
73656
|
+
columns
|
|
73375
73657
|
});
|
|
73376
73658
|
|
|
73377
73659
|
// renderBudget frames the items of a run; a list whose items are all declared
|
|
@@ -73686,7 +73968,8 @@ const useListScrollSync = ({
|
|
|
73686
73968
|
listId,
|
|
73687
73969
|
scroller,
|
|
73688
73970
|
searchText,
|
|
73689
|
-
horizontal
|
|
73971
|
+
horizontal,
|
|
73972
|
+
columns
|
|
73690
73973
|
}) => {
|
|
73691
73974
|
const debugScroll = useDebugScroll();
|
|
73692
73975
|
// The items drawn, [start, end) among the list's own. A ref as well as a
|
|
@@ -73701,6 +73984,7 @@ const useListScrollSync = ({
|
|
|
73701
73984
|
return renderWindow.start > 0 || renderWindow.end < listItems.totalSignal.peek();
|
|
73702
73985
|
};
|
|
73703
73986
|
const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
|
|
73987
|
+
inLines: Boolean(columns),
|
|
73704
73988
|
windowLeavesItemsOut,
|
|
73705
73989
|
scrolledWanted: scrolled ?? defaultScrolled,
|
|
73706
73990
|
// The fillers hold the room of items above the screen at this size: a
|
|
@@ -73769,6 +74053,34 @@ const useListScrollSync = ({
|
|
|
73769
74053
|
});
|
|
73770
74054
|
};
|
|
73771
74055
|
|
|
74056
|
+
// How many items share a line: the tracks a grid of items (`columns`)
|
|
74057
|
+
// resolves to, one otherwise. A layout result under auto-fill, so it is read
|
|
74058
|
+
// off the list once laid out, at each commit and each resize — for the runs
|
|
74059
|
+
// alone, which draw in lines: items declared one by one are all drawn. A
|
|
74060
|
+
// line of another length rewraps every item drawn: the item at the top of
|
|
74061
|
+
// the view is held where it is across it.
|
|
74062
|
+
const itemsPerLineRef = useRef(1);
|
|
74063
|
+
const [itemsPerLine, setItemsPerLine] = useState(1);
|
|
74064
|
+
const updateItemsPerLine = () => {
|
|
74065
|
+
if (!ref.current) {
|
|
74066
|
+
return;
|
|
74067
|
+
}
|
|
74068
|
+
const itemsPerLineNow = columns && listItems.hasRuns() ? readItemsPerLine(getListEl()) : 1;
|
|
74069
|
+
if (itemsPerLineNow === null || itemsPerLineNow === itemsPerLineRef.current) {
|
|
74070
|
+
return;
|
|
74071
|
+
}
|
|
74072
|
+
captureAnchor();
|
|
74073
|
+
itemsPerLineRef.current = itemsPerLineNow;
|
|
74074
|
+
setItemsPerLine(itemsPerLineNow);
|
|
74075
|
+
};
|
|
74076
|
+
const updateItemsPerLineRef = useRef(null);
|
|
74077
|
+
updateItemsPerLineRef.current = updateItemsPerLine;
|
|
74078
|
+
useLayoutEffect(() => {
|
|
74079
|
+
if (columns || itemsPerLineRef.current !== 1) {
|
|
74080
|
+
updateItemsPerLine();
|
|
74081
|
+
}
|
|
74082
|
+
});
|
|
74083
|
+
|
|
73772
74084
|
// What the window holds goes through three stages (see settleWindow):
|
|
73773
74085
|
// - "first": the first commit, a count of items since nothing is laid out
|
|
73774
74086
|
// yet to measure a size with — `initial` or the budget when they are one,
|
|
@@ -74170,6 +74482,7 @@ const useListScrollSync = ({
|
|
|
74170
74482
|
listEl: getListEl(),
|
|
74171
74483
|
listItems,
|
|
74172
74484
|
virtualItemSizeSignal,
|
|
74485
|
+
itemsPerLine: itemsPerLineRef.current,
|
|
74173
74486
|
renderWindowRef,
|
|
74174
74487
|
horizontal
|
|
74175
74488
|
});
|
|
@@ -74510,20 +74823,14 @@ const useListScrollSync = ({
|
|
|
74510
74823
|
}
|
|
74511
74824
|
const scrollerEl = getScroller();
|
|
74512
74825
|
const listEl = getListEl();
|
|
74513
|
-
|
|
74514
|
-
|
|
74515
|
-
|
|
74516
|
-
|
|
74517
|
-
|
|
74518
|
-
|
|
74519
|
-
|
|
74520
|
-
|
|
74521
|
-
// growing is its own content settling: only a list holding itself
|
|
74522
|
-
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
74523
|
-
// reading must not be touched — its items are held still by the anchoring,
|
|
74524
|
-
// which this would undo. The SCROLLER resizing is the window around it
|
|
74525
|
-
// changing shape, and then the item that was at the top goes back where it
|
|
74526
|
-
// was.
|
|
74826
|
+
// Two things resize here, and they call for opposite answers. The LIST
|
|
74827
|
+
// growing is its own content settling: only a list holding itself
|
|
74828
|
+
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
74829
|
+
// reading must not be touched — its items are held still by the anchoring,
|
|
74830
|
+
// which this would undo. The SCROLLER resizing is the window around it
|
|
74831
|
+
// changing shape, and then the item that was at the top goes back where it
|
|
74832
|
+
// was.
|
|
74833
|
+
const putTopItemBack = entries => {
|
|
74527
74834
|
const listResized = entries.some(entry => entry.target === listEl);
|
|
74528
74835
|
if (listResized && onGeometryChangeRef.current()) {
|
|
74529
74836
|
return;
|
|
@@ -74552,6 +74859,19 @@ const useListScrollSync = ({
|
|
|
74552
74859
|
} else {
|
|
74553
74860
|
scrollerEl.scrollTop += delta;
|
|
74554
74861
|
}
|
|
74862
|
+
};
|
|
74863
|
+
const observer = new ResizeObserver(entries => {
|
|
74864
|
+
// A box that grows may be a box that starts to scroll — the scroller is
|
|
74865
|
+
// resolved again before anything is done about the resize, so that a
|
|
74866
|
+
// list which has outgrown a bounded ancestor stops holding on to the
|
|
74867
|
+
// page.
|
|
74868
|
+
resolveScroller();
|
|
74869
|
+
rememberPosition();
|
|
74870
|
+
putTopItemBack(entries);
|
|
74871
|
+
// Once the top item is back where it was: that is the place a line of
|
|
74872
|
+
// another length has to keep, and putting it back drops any anchor
|
|
74873
|
+
// captured before.
|
|
74874
|
+
updateItemsPerLineRef.current();
|
|
74555
74875
|
});
|
|
74556
74876
|
observer.observe(scrollerEl);
|
|
74557
74877
|
observer.observe(listEl);
|
|
@@ -74707,7 +75027,10 @@ const useListScrollSync = ({
|
|
|
74707
75027
|
start,
|
|
74708
75028
|
end
|
|
74709
75029
|
} = renderWindowRef.current;
|
|
74710
|
-
|
|
75030
|
+
// What an item not drawn weighs: its share of a line, which the items side
|
|
75031
|
+
// by side on it take together.
|
|
75032
|
+
const itemsPerLineNow = itemsPerLineRef.current;
|
|
75033
|
+
const itemSize = virtualItemSizeSignal.peek() / itemsPerLineNow;
|
|
74711
75034
|
const pixelsOf = createItemSizeReader(items, start, end, itemSize);
|
|
74712
75035
|
const walkBefore = (from, sizeWanted, sizeOf) => {
|
|
74713
75036
|
let index = from;
|
|
@@ -74775,10 +75098,10 @@ const useListScrollSync = ({
|
|
|
74775
75098
|
const behindSize = countsItems ? Math.floor(spare / 4) : spare / 4;
|
|
74776
75099
|
const aheadSize = spare - behindSize;
|
|
74777
75100
|
if (stage === "steady" && !budgetWarnedRef.current && bandEnd > bandStart) {
|
|
74778
|
-
const
|
|
74779
|
-
if (spare <
|
|
75101
|
+
const twoLines = (countsItems ? 2 : 2 * (bandTo - bandFrom) / (bandEnd - bandStart)) * itemsPerLineNow;
|
|
75102
|
+
if (spare < twoLines) {
|
|
74780
75103
|
budgetWarnedRef.current = true;
|
|
74781
|
-
console.warn(`List: renderBudget=${renderBudget.value}${renderBudget.unit} leaves less than two items beyond what the screen shows (${bandEnd - bandStart} items): items will go blank as it scrolls. Give it room for a screen ahead or more — "300%" of the box that scrolls it, say.`);
|
|
75104
|
+
console.warn(`List: renderBudget=${renderBudget.value}${renderBudget.unit} leaves less than two ${itemsPerLineNow > 1 ? "lines of items" : "items"} beyond what the screen shows (${bandEnd - bandStart} items): items will go blank as it scrolls. Give it room for a screen ahead or more — "300%" of the box that scrolls it, say.`);
|
|
74782
75105
|
}
|
|
74783
75106
|
}
|
|
74784
75107
|
if (!force) {
|
|
@@ -74984,9 +75307,28 @@ const useListScrollSync = ({
|
|
|
74984
75307
|
ifChanged: true
|
|
74985
75308
|
});
|
|
74986
75309
|
}, [windowStart, windowEnd]);
|
|
75310
|
+
// What the runs draw: the window widened to whole lines. The window itself
|
|
75311
|
+
// moves an item at a time, and a grid lays the first item drawn in its first
|
|
75312
|
+
// column — started anywhere but on a line, every item drawn would sit in the
|
|
75313
|
+
// column of another. Started on one, an item is in the column its place says
|
|
75314
|
+
// whatever the window frames. The same object while the numbers hold, so the
|
|
75315
|
+
// runs are not told about a window that did not move.
|
|
75316
|
+
const windowDrawnRef = useRef(null);
|
|
75317
|
+
const drawnStart = windowStart - windowStart % itemsPerLine;
|
|
75318
|
+
const endPastLine = windowEnd % itemsPerLine;
|
|
75319
|
+
const drawnEnd = endPastLine === 0 ? windowEnd : windowEnd + itemsPerLine - endPastLine;
|
|
75320
|
+
let windowDrawn = windowDrawnRef.current;
|
|
75321
|
+
if (!windowDrawn || windowDrawn.start !== drawnStart || windowDrawn.end !== drawnEnd || windowDrawn.itemsPerLine !== itemsPerLine) {
|
|
75322
|
+
windowDrawn = {
|
|
75323
|
+
start: drawnStart,
|
|
75324
|
+
end: drawnEnd,
|
|
75325
|
+
itemsPerLine
|
|
75326
|
+
};
|
|
75327
|
+
windowDrawnRef.current = windowDrawn;
|
|
75328
|
+
}
|
|
74987
75329
|
return {
|
|
74988
75330
|
virtualItemSizeSignal,
|
|
74989
|
-
renderWindow:
|
|
75331
|
+
renderWindow: windowDrawn,
|
|
74990
75332
|
pendingScrollRef,
|
|
74991
75333
|
scrollToItem,
|
|
74992
75334
|
captureAnchor
|
|
@@ -75010,6 +75352,21 @@ const getScrollerViewportRect = scrollerEl => {
|
|
|
75010
75352
|
}
|
|
75011
75353
|
return scrollerEl.getBoundingClientRect();
|
|
75012
75354
|
};
|
|
75355
|
+
// The tracks a grid resolved its template to, as its computed
|
|
75356
|
+
// grid-template-columns lists them once laid out: "150px 150px [end] 150px".
|
|
75357
|
+
// Not laid out (a closed popup), that is the template as written, which says
|
|
75358
|
+
// nothing yet under auto-fill: `null`.
|
|
75359
|
+
const readItemsPerLine = listEl => {
|
|
75360
|
+
const tracks = getComputedStyle(listEl).gridTemplateColumns;
|
|
75361
|
+
if (tracks === "none" || tracks.includes("(")) {
|
|
75362
|
+
return null;
|
|
75363
|
+
}
|
|
75364
|
+
const sizes = tracks.replace(/\[[^\]]*\]/g, " ").trim();
|
|
75365
|
+
if (!sizes) {
|
|
75366
|
+
return null;
|
|
75367
|
+
}
|
|
75368
|
+
return sizes.split(/\s+/).length;
|
|
75369
|
+
};
|
|
75013
75370
|
// A window that cannot move is the one failure of a virtualized list that
|
|
75014
75371
|
// looks like nothing: the items outside it are fillers holding their room, so
|
|
75015
75372
|
// the list simply ends on blank space, at exactly the height of the items never
|
|
@@ -75897,6 +76254,7 @@ const getScrollInfo = ({
|
|
|
75897
76254
|
listEl,
|
|
75898
76255
|
listItems,
|
|
75899
76256
|
virtualItemSizeSignal,
|
|
76257
|
+
itemsPerLine,
|
|
75900
76258
|
renderWindowRef,
|
|
75901
76259
|
horizontal
|
|
75902
76260
|
}) => {
|
|
@@ -75934,7 +76292,9 @@ const getScrollInfo = ({
|
|
|
75934
76292
|
// mean we don't know the real on-screen index, only the scroll position,
|
|
75935
76293
|
// so estimate from it rather than assume nothing changed.
|
|
75936
76294
|
const estimateFromScrollPos = reasonPrefix => {
|
|
75937
|
-
|
|
76295
|
+
// An item's share of its line: the items side by side on it take it
|
|
76296
|
+
// together.
|
|
76297
|
+
const virtualItemSize = virtualItemSizeSignal.peek() / itemsPerLine;
|
|
75938
76298
|
if (virtualItemSize === 0) {
|
|
75939
76299
|
return null;
|
|
75940
76300
|
}
|
|
@@ -75990,8 +76350,10 @@ const getScrollInfo = ({
|
|
|
75990
76350
|
const VIRTUAL_ITEM_SIZE_EPSILON = 0.5;
|
|
75991
76351
|
// Measures the items currently in the DOM, edge to edge: what a filler stands in
|
|
75992
76352
|
// for is the room a run of items takes together — separators and group labels
|
|
75993
|
-
// included — not the height of one <li>.
|
|
75994
|
-
|
|
76353
|
+
// included — not the height of one <li>. The size of a line of them: one item
|
|
76354
|
+
// in a list, the items side by side in a grid (`inLines`), counted where a new
|
|
76355
|
+
// line starts.
|
|
76356
|
+
const measureItemSize = (listEl, horizontal, inLines) => {
|
|
75995
76357
|
let fromSkeletons = false;
|
|
75996
76358
|
let itemEls = listEl.querySelectorAll(REAL_LIST_ITEM_SELECTOR);
|
|
75997
76359
|
if (itemEls.length === 0) {
|
|
@@ -76013,13 +76375,27 @@ const measureItemSize = (listEl, horizontal) => {
|
|
|
76013
76375
|
if (span <= 0) {
|
|
76014
76376
|
return null;
|
|
76015
76377
|
}
|
|
76378
|
+
let lineCount = itemEls.length;
|
|
76379
|
+
if (inLines) {
|
|
76380
|
+
lineCount = 0;
|
|
76381
|
+
let lineFrom = -Infinity;
|
|
76382
|
+
for (const itemEl of itemEls) {
|
|
76383
|
+
const rect = itemEl.getBoundingClientRect();
|
|
76384
|
+
const from = horizontal ? rect.left : rect.top;
|
|
76385
|
+
if (from > lineFrom + 0.5) {
|
|
76386
|
+
lineCount++;
|
|
76387
|
+
lineFrom = from;
|
|
76388
|
+
}
|
|
76389
|
+
}
|
|
76390
|
+
}
|
|
76016
76391
|
return {
|
|
76017
|
-
size: span /
|
|
76018
|
-
|
|
76392
|
+
size: span / lineCount,
|
|
76393
|
+
lineCount,
|
|
76019
76394
|
fromSkeletons
|
|
76020
76395
|
};
|
|
76021
76396
|
};
|
|
76022
76397
|
const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
76398
|
+
inLines,
|
|
76023
76399
|
windowLeavesItemsOut,
|
|
76024
76400
|
scrolledWanted,
|
|
76025
76401
|
beforeSizeChange
|
|
@@ -76063,8 +76439,8 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
76063
76439
|
return;
|
|
76064
76440
|
}
|
|
76065
76441
|
}
|
|
76066
|
-
samples.sum += measure.size * measure.
|
|
76067
|
-
samples.count += measure.
|
|
76442
|
+
samples.sum += measure.size * measure.lineCount;
|
|
76443
|
+
samples.count += measure.lineCount;
|
|
76068
76444
|
const next = samples.sum / samples.count;
|
|
76069
76445
|
const current = virtualSizeSignal.peek();
|
|
76070
76446
|
if (Math.abs(next - current) > VIRTUAL_ITEM_SIZE_EPSILON) {
|
|
@@ -76084,7 +76460,7 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
76084
76460
|
const itemsHeldOffScreen = windowLeavesItemsOut();
|
|
76085
76461
|
if (!virtualItemSizeProp && sizeAlreadyKnown && itemsHeldOffScreen && ref.current) {
|
|
76086
76462
|
const listEl = ref.current.querySelector(".navi_list");
|
|
76087
|
-
const measure = listEl ? measureItemSize(listEl, horizontal) : null;
|
|
76463
|
+
const measure = listEl ? measureItemSize(listEl, horizontal, inLines) : null;
|
|
76088
76464
|
if (measure) {
|
|
76089
76465
|
feedSample(measure);
|
|
76090
76466
|
}
|
|
@@ -76105,11 +76481,11 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
|
|
|
76105
76481
|
if (!listEl) {
|
|
76106
76482
|
return undefined;
|
|
76107
76483
|
}
|
|
76108
|
-
const measure = measureItemSize(listEl, horizontal);
|
|
76484
|
+
const measure = measureItemSize(listEl, horizontal, inLines);
|
|
76109
76485
|
if (measure) {
|
|
76110
76486
|
const samples = samplesRef.current;
|
|
76111
|
-
samples.sum = measure.size * measure.
|
|
76112
|
-
samples.count = measure.
|
|
76487
|
+
samples.sum = measure.size * measure.lineCount;
|
|
76488
|
+
samples.count = measure.lineCount;
|
|
76113
76489
|
samples.fromSkeletons = measure.fromSkeletons;
|
|
76114
76490
|
virtualSizeSignal.value = measure.size;
|
|
76115
76491
|
return undefined;
|
|
@@ -76254,14 +76630,17 @@ const Fallback = ({
|
|
|
76254
76630
|
// Reads the item size itself: it is what the size is for, and a run holding
|
|
76255
76631
|
// every item it draws must not be redrawn — every item of it — because the size
|
|
76256
76632
|
// settled after the first commit.
|
|
76633
|
+
// It holds lines: the items side by side on one take its room together, and a
|
|
76634
|
+
// last line left short takes it all the same.
|
|
76257
76635
|
const VirtualFiller = ({
|
|
76258
76636
|
edge,
|
|
76259
76637
|
itemCount,
|
|
76638
|
+
itemsPerLine,
|
|
76260
76639
|
findChunks
|
|
76261
76640
|
}) => {
|
|
76262
76641
|
const listItems = useContext(ListItemsContext);
|
|
76263
|
-
const
|
|
76264
|
-
const sizeToFill = itemCount *
|
|
76642
|
+
const lineSize = listItems.virtualItemSizeSignal.value;
|
|
76643
|
+
const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize;
|
|
76265
76644
|
// A filler resizing moves what stands below it — the items on screen, when it
|
|
76266
76645
|
// holds the room of items above them — and it resizes in a commit of its own
|
|
76267
76646
|
// when the item size settles after the list has rendered: the list puts its
|
|
@@ -76280,7 +76659,7 @@ const VirtualFiller = ({
|
|
|
76280
76659
|
"aria-hidden": true,
|
|
76281
76660
|
style: {
|
|
76282
76661
|
"--size-to-fill": `${sizeToFill}px`,
|
|
76283
|
-
"--x-find-line-size": findChunks ? `${
|
|
76662
|
+
"--x-find-line-size": findChunks ? `${lineSize / itemsPerLine}px` : undefined
|
|
76284
76663
|
},
|
|
76285
76664
|
children: findChunks && findChunks.map(chunk => jsx("div", {
|
|
76286
76665
|
className: "navi_list_find_stand_in",
|
|
@@ -77163,6 +77542,9 @@ const useRunItems = (store, {
|
|
|
77163
77542
|
const rankOf = itemIndex => itemIndex - runStart;
|
|
77164
77543
|
const indexOfRank = rank => rank + runStart;
|
|
77165
77544
|
const getItemAt = itemIndex => store.getItem(rankOf(itemIndex));
|
|
77545
|
+
const {
|
|
77546
|
+
itemsPerLine
|
|
77547
|
+
} = renderWindow;
|
|
77166
77548
|
const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;
|
|
77167
77549
|
const windowTo = renderWindow.end < runEnd ? renderWindow.end : runEnd;
|
|
77168
77550
|
store.forget(rankOf(windowFrom), rankOf(windowTo));
|
|
@@ -77270,8 +77652,20 @@ const useRunItems = (store, {
|
|
|
77270
77652
|
// band is drawn among the items. A negative start is not a rank but a count
|
|
77271
77653
|
// back from the end (the very first ask, before the count is known) — there
|
|
77272
77654
|
// is no item to convert it to, and the band falls back to the window.
|
|
77273
|
-
|
|
77274
|
-
|
|
77655
|
+
let failureFrom = store.failure === null ? -1 : store.failure.start < 0 || indexOfRank(store.failure.start) < windowFrom ? windowFrom : indexOfRank(store.failure.start);
|
|
77656
|
+
let failureTo = store.failure === null ? -1 : store.failure.end < 0 || indexOfRank(store.failure.end) > windowTo - 1 ? windowTo - 1 : indexOfRank(store.failure.end);
|
|
77657
|
+
if (store.failure !== null && itemsPerLine > 1) {
|
|
77658
|
+
// In a grid the band takes whole lines, like the window: begun mid-line,
|
|
77659
|
+
// it would push every item after it into another column.
|
|
77660
|
+
failureFrom -= failureFrom % itemsPerLine;
|
|
77661
|
+
if (failureFrom < windowFrom) {
|
|
77662
|
+
failureFrom = windowFrom;
|
|
77663
|
+
}
|
|
77664
|
+
failureTo += itemsPerLine - 1 - failureTo % itemsPerLine;
|
|
77665
|
+
if (failureTo > windowTo - 1) {
|
|
77666
|
+
failureTo = windowTo - 1;
|
|
77667
|
+
}
|
|
77668
|
+
}
|
|
77275
77669
|
const nodes = [];
|
|
77276
77670
|
// Items that belong together, as the data says (a day of messages, a month of
|
|
77277
77671
|
// games): consecutive items sharing a group key are wrapped in one group, so
|
|
@@ -77348,6 +77742,7 @@ const useRunItems = (store, {
|
|
|
77348
77742
|
nodes.push(jsx(VirtualFiller, {
|
|
77349
77743
|
edge: "before",
|
|
77350
77744
|
itemCount: windowFrom - runStart,
|
|
77745
|
+
itemsPerLine: itemsPerLine,
|
|
77351
77746
|
findChunks: getFindChunks(runStart, windowFrom)
|
|
77352
77747
|
}, "navi-list-filler-before"));
|
|
77353
77748
|
}
|
|
@@ -77362,7 +77757,7 @@ const useRunItems = (store, {
|
|
|
77362
77757
|
nodes.push(jsx("li", {
|
|
77363
77758
|
className: "navi_list_failed_items",
|
|
77364
77759
|
style: {
|
|
77365
|
-
"--size-to-fill": `${failedItemCount * listItems.virtualItemSizeSignal.value}px`
|
|
77760
|
+
"--size-to-fill": `${Math.ceil(failedItemCount / itemsPerLine) * listItems.virtualItemSizeSignal.value}px`
|
|
77366
77761
|
},
|
|
77367
77762
|
children: renderError ? renderError({
|
|
77368
77763
|
error: store.failure.error,
|
|
@@ -77452,6 +77847,7 @@ const useRunItems = (store, {
|
|
|
77452
77847
|
nodes.push(jsx(VirtualFiller, {
|
|
77453
77848
|
edge: "after",
|
|
77454
77849
|
itemCount: runEnd - windowTo,
|
|
77850
|
+
itemsPerLine: itemsPerLine,
|
|
77455
77851
|
findChunks: getFindChunks(windowTo, runEnd)
|
|
77456
77852
|
}, "navi-list-filler-after"));
|
|
77457
77853
|
}
|
|
@@ -78184,12 +78580,16 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
78184
78580
|
* @param {string} [props.columns]
|
|
78185
78581
|
* The list's own columns: a `grid-template-columns` value the ITEMS are laid
|
|
78186
78582
|
* into — a sheet of icons (`repeat(auto-fill, minmax(2.5rem, 1fr))`), a row of
|
|
78187
|
-
* choices (`repeat(3, minmax(0, 1fr))`)
|
|
78188
|
-
*
|
|
78189
|
-
*
|
|
78190
|
-
*
|
|
78191
|
-
*
|
|
78192
|
-
*
|
|
78583
|
+
* choices (`repeat(3, minmax(0, 1fr))`), a feed of covers
|
|
78584
|
+
* (`repeat(auto-fill, minmax(150px, 1fr))` over a `<List.Items>`). Each item
|
|
78585
|
+
* takes one cell, and an item meant to take a whole line says so for itself
|
|
78586
|
+
* (`style={{ gridColumn: "1 / -1" }}`). A `<List.Items>` run is drawn in whole
|
|
78587
|
+
* lines — as many items to a line as the template resolves to, read again
|
|
78588
|
+
* when the list's width changes: its window starts and ends on a line, so an
|
|
78589
|
+
* item is always in the column its place says, and the fillers hold the room
|
|
78590
|
+
* of the lines it does not draw (`virtualItemSize` is then a line's size).
|
|
78591
|
+
* That count holds only while every item of the run takes one cell: an item
|
|
78592
|
+
* spanning a line, or a `groupBy`, puts the ones after it in other columns.
|
|
78193
78593
|
* @param {string} [props.itemColumns]
|
|
78194
78594
|
* The columns inside an ITEM: a `grid-template-columns` value each item fills
|
|
78195
78595
|
* with its own children, a table whose cells line up down the list. Every item
|
|
@@ -78205,8 +78605,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
78205
78605
|
* scroll box, which fills it and has nothing to arrange.
|
|
78206
78606
|
* @param {boolean} [props.flexWrap]
|
|
78207
78607
|
* Lets a horizontal list's items fall to the next line instead of running
|
|
78208
|
-
* past the edge — a row of choices under a `maxWidth`, say.
|
|
78209
|
-
*
|
|
78608
|
+
* past the edge — a row of choices under a `maxWidth`, say. A wrapped line
|
|
78609
|
+
* holding several items is not virtualizable: a long collection laid several
|
|
78610
|
+
* to a line goes in `columns`.
|
|
78210
78611
|
* @param {string} [props.overflow]
|
|
78211
78612
|
* `"visible"` lets the items paint outside the list — a check in an item's
|
|
78212
78613
|
* corner, a badge crossing the edge. A list clips by default, which is what
|
|
@@ -78325,7 +78726,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
78325
78726
|
* one-line size) — an item guessed too small builds one more, an item
|
|
78326
78727
|
* guessed too big leaves a blank (see docs/scroll.md, "What the list knows,
|
|
78327
78728
|
* and what it guesses"). The items drawn are measured either way: the render
|
|
78328
|
-
* window is sized on them.
|
|
78729
|
+
* window is sized on them. In a grid (`columns`), the size is a line's: a
|
|
78730
|
+
* card's, which the items side by side on it share.
|
|
78329
78731
|
* @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
|
|
78330
78732
|
* Which box scrolls — and with it, which box the render window follows and
|
|
78331
78733
|
* which box a scroll position is read from (`onScrolledChange`). `"self"`
|