@jsenv/navi 0.29.425 → 0.29.426
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 +372 -119
- package/dist/dev/jsenv_navi.js.map +15 -13
- package/dist/jsenv_navi.js +372 -119
- package/dist/jsenv_navi.js.map +15 -13
- package/docs/AI_INSTRUCTIONS.md +9 -5
- package/docs/control_object.md +76 -2
- package/docs/navigation.md +4 -1
- package/docs/popup_open.md +6 -3
- package/docs/scroll.md +53 -0
- package/package.json +1 -1
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
|
|
|
@@ -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,
|
|
@@ -52551,7 +52834,8 @@ const useExpandableContext = partName => {
|
|
|
52551
52834
|
* other three, intent on the UI part (pointer entering it, focus landing in
|
|
52552
52835
|
* it) builds the content ahead of the click; `"while-opened"` content is
|
|
52553
52836
|
* built by a press on it only, and thrown away if that press does not
|
|
52554
|
-
* expand it.
|
|
52837
|
+
* expand it. Named controls in content not built are kept by the group
|
|
52838
|
+
* around them (docs/control_object.md, "A control that is not there").
|
|
52555
52839
|
*/
|
|
52556
52840
|
const Expandable = props => {
|
|
52557
52841
|
import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
@@ -59613,12 +59897,19 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
59613
59897
|
* would bring alongside (neighbours on the map, along the axes
|
|
59614
59898
|
* `travelByDrag` opens, the other end of a `loop` included) — and kept from
|
|
59615
59899
|
* then on. For slides that are costly to build (a grid of a few hundred
|
|
59616
|
-
*
|
|
59900
|
+
* items each): opening builds the slide shown and its neighbours rather
|
|
59617
59901
|
* than all of them, and a jump to a far slide builds that one and none of
|
|
59618
59902
|
* those in between, which a travel never shows. The box is then as large as
|
|
59619
59903
|
* the largest slide BUILT so far (see `sizing`), so it grows as a larger one
|
|
59620
59904
|
* is reached: give it a size of its own when the slides differ.
|
|
59621
59905
|
* `<Slide mount>` says it for one slide — a heavy one among light ones.
|
|
59906
|
+
* A slide not built has no controls, and a group around the container keeps
|
|
59907
|
+
* their keys for them: what it was handed reaches them once built. What is
|
|
59908
|
+
* shared between slides is written through that group (its `signal`, a
|
|
59909
|
+
* `--navi-update` aimed at it), never into one control's own `signal`, which
|
|
59910
|
+
* nobody reads while that control is not built; and nothing validates a
|
|
59911
|
+
* field before its slide is built. See docs/control_object.md, "A control
|
|
59912
|
+
* that is not there".
|
|
59622
59913
|
*/
|
|
59623
59914
|
const SlideContainer = ({
|
|
59624
59915
|
layout = "row",
|
|
@@ -69543,44 +69834,6 @@ document.addEventListener(
|
|
|
69543
69834
|
{ capture: true, passive: true },
|
|
69544
69835
|
);
|
|
69545
69836
|
|
|
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
69837
|
const LoadingDotsSvg = () => {
|
|
69585
69838
|
return jsxs("svg", {
|
|
69586
69839
|
viewBox: "0 0 200 200",
|