@jsenv/navi 0.29.165 → 0.29.167
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/jsenv_navi.js +604 -104
- package/dist/jsenv_navi.js.map +14 -10
- package/docs/AI_INSTRUCTIONS.md +19 -3
- package/docs/actions.md +6 -4
- package/docs/data_states.md +147 -0
- package/docs/error_handling.md +9 -0
- package/docs/list_refresh.md +16 -1
- package/docs/navigation.md +167 -0
- package/docs/popup_open.md +4 -1
- package/docs/route_transitions.md +8 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -17635,7 +17635,9 @@ const useActionAsyncData = (action, {
|
|
|
17635
17635
|
onLoad
|
|
17636
17636
|
}) => {
|
|
17637
17637
|
const loadingRef = useContext(LoadingContext);
|
|
17638
|
-
|
|
17638
|
+
// The boundary is where a suspension lands, so only the delegating path needs
|
|
17639
|
+
// one: `loading: true` draws the wait itself and never suspends.
|
|
17640
|
+
if (!loadingRef && loadingEffect !== "use") {
|
|
17639
17641
|
throw new Error(`Missing <Loading>: useAsyncData delegates the wait, so it needs a <Loading> boundary above it — or "loading: true" to draw that wait here.`);
|
|
17640
17642
|
}
|
|
17641
17643
|
useOnLoad(action, onLoad);
|
|
@@ -17719,15 +17721,19 @@ const useActionAsyncData = (action, {
|
|
|
17719
17721
|
}
|
|
17720
17722
|
if (runningState === FAILED) {
|
|
17721
17723
|
if (dismissedActionWeakSet.has(action)) {
|
|
17724
|
+
// Dismissing removes the error and nothing else: what is handed back is
|
|
17725
|
+
// the action's state without it — the data it holds, and no run in
|
|
17726
|
+
// progress. Holding nothing is one of those states, not a wait, so a
|
|
17727
|
+
// component drawing its own states gets it rather than being suspended
|
|
17728
|
+
// away from the retry it is offering.
|
|
17722
17729
|
const staleData = action.dataSignal.peek();
|
|
17723
|
-
if (staleData !== undefined) {
|
|
17724
|
-
// Dismissed with stale data — return it so children render normally
|
|
17730
|
+
if (staleData !== undefined || loadingEffect === "use") {
|
|
17725
17731
|
return [staleData, false, undefined];
|
|
17726
17732
|
}
|
|
17727
|
-
//
|
|
17728
|
-
//
|
|
17729
|
-
// so
|
|
17730
|
-
//
|
|
17733
|
+
// Nothing to hand over and the waits are delegated: there is no state
|
|
17734
|
+
// this component can draw, so it waits. The promise resolves on RUNNING
|
|
17735
|
+
// rather than never, so a run started from anywhere brings it back
|
|
17736
|
+
// through the ordinary loading path instead of leaving it stuck.
|
|
17731
17737
|
let dismissedPromise = dismissedActionPendingPromiseWeakMap.get(action);
|
|
17732
17738
|
if (!dismissedPromise) {
|
|
17733
17739
|
dismissedPromise = new Promise(resolve => {
|
|
@@ -17756,7 +17762,10 @@ const useActionAsyncData = (action, {
|
|
|
17756
17762
|
dismissedActionWeakSet.add(action);
|
|
17757
17763
|
setTick(n => n + 1);
|
|
17758
17764
|
};
|
|
17759
|
-
|
|
17765
|
+
// The previous answer is still there and a failed refresh does not unmake
|
|
17766
|
+
// it, so both are handed over: the component can say the failure over the
|
|
17767
|
+
// content it already has instead of choosing between the two.
|
|
17768
|
+
return [action.dataSignal.peek(), false, actionError, dismissError];
|
|
17760
17769
|
}
|
|
17761
17770
|
// Not marked: nothing is displayed yet — the boundary that catches this is
|
|
17762
17771
|
// what says so, and only if it has something to show.
|
|
@@ -27612,6 +27621,105 @@ const releaseTransitionDestination = (owner) => {
|
|
|
27612
27621
|
publishDestination(null);
|
|
27613
27622
|
};
|
|
27614
27623
|
|
|
27624
|
+
/**
|
|
27625
|
+
* The press that lands on a control the movement turned into a picture.
|
|
27626
|
+
*
|
|
27627
|
+
* A captured element is dead in both senses: it stops being painted where it
|
|
27628
|
+
* stands, and nothing hit-tests to it — the press falls through to the nearest
|
|
27629
|
+
* ancestor still being painted, whatever the pseudo-elements are told about
|
|
27630
|
+
* pointer events. For the pages that is the whole point: both are pictures of
|
|
27631
|
+
* somewhere the reader is no longer, or not yet.
|
|
27632
|
+
*
|
|
27633
|
+
* For the DOOR it is not. A page reached from the furniture — a gear in a top
|
|
27634
|
+
* bar, a "+" in a tab bar — is opened and closed by the same control, and that
|
|
27635
|
+
* control is standing exactly where its picture is drawn: the bar the two
|
|
27636
|
+
* states share is one group the browser holds still. So a press on it during
|
|
27637
|
+
* those few hundred milliseconds is aimed at what the reader can see, and
|
|
27638
|
+
* swallowing it makes the toggle feel stuck at the very moment it is being used
|
|
27639
|
+
* as one — press to open, press again to close.
|
|
27640
|
+
*
|
|
27641
|
+
* A control therefore may ask to keep answering, and while a route transition
|
|
27642
|
+
* plays the press is caught at the document and handed to it when it fell
|
|
27643
|
+
* inside its rectangle — which is where the hand thinks it pressed. The same
|
|
27644
|
+
* move route_travel.jsx makes for the box a finger reaches for mid-flight.
|
|
27645
|
+
*
|
|
27646
|
+
* ASKED FOR, never derived, and that is the whole safety of it: only the
|
|
27647
|
+
* application knows the control does not move. One that travels with the pages
|
|
27648
|
+
* has a rectangle where it WILL be, not where it is seen, and a press handed to
|
|
27649
|
+
* it fires something the reader never aimed at — worse than the press being
|
|
27650
|
+
* lost. Hence the dev warning below for a control found inside the area.
|
|
27651
|
+
*
|
|
27652
|
+
* The click alone is re-aimed, not the pointer stream that leads to it: what
|
|
27653
|
+
* has to survive is the press meaning what it meant, and the press feedback is
|
|
27654
|
+
* lost anyway — the control is a picture, so nothing it paints for its own
|
|
27655
|
+
* `:active` state reaches the screen. Synthesising a pointerdown with no
|
|
27656
|
+
* pointer behind it would start gestures (a drag, a hold) that no pointerup
|
|
27657
|
+
* ever ends.
|
|
27658
|
+
*/
|
|
27659
|
+
|
|
27660
|
+
// Worn by a control that keeps answering while the pages move. Written by
|
|
27661
|
+
// <Link pressableDuringRouteTransition> / <Button pressableDuringRouteTransition>,
|
|
27662
|
+
// and by hand on anything else — this module owns the name and does the reading.
|
|
27663
|
+
const PRESSABLE_ATTRIBUTE = "data-navi-route-transition-pressable";
|
|
27664
|
+
const PRESSABLE_SELECTOR = `[${PRESSABLE_ATTRIBUTE}]`;
|
|
27665
|
+
|
|
27666
|
+
let pressOwner = null;
|
|
27667
|
+
|
|
27668
|
+
const holdTransitionPress = (owner, areaElement) => {
|
|
27669
|
+
pressOwner = owner;
|
|
27670
|
+
// Adding the same listener twice is a no-op, so a movement taking over from
|
|
27671
|
+
// another needs nothing but the new owner.
|
|
27672
|
+
document.addEventListener("click", onDocumentClick, true);
|
|
27673
|
+
};
|
|
27674
|
+
|
|
27675
|
+
const releaseTransitionPress = (owner) => {
|
|
27676
|
+
if (owner !== pressOwner) {
|
|
27677
|
+
return;
|
|
27678
|
+
}
|
|
27679
|
+
pressOwner = null;
|
|
27680
|
+
document.removeEventListener("click", onDocumentClick, true);
|
|
27681
|
+
};
|
|
27682
|
+
|
|
27683
|
+
const onDocumentClick = (clickEvent) => {
|
|
27684
|
+
// A click of our own making, or one an application dispatched by hand: both
|
|
27685
|
+
// are already aimed, and re-aiming them by coordinates that mean nothing
|
|
27686
|
+
// (0, 0 for a synthetic click) would fire whatever sits in the corner.
|
|
27687
|
+
if (!clickEvent.isTrusted) {
|
|
27688
|
+
return;
|
|
27689
|
+
}
|
|
27690
|
+
const pressables = document.querySelectorAll(PRESSABLE_SELECTOR);
|
|
27691
|
+
if (pressables.length === 0) {
|
|
27692
|
+
return;
|
|
27693
|
+
}
|
|
27694
|
+
const { target, clientX, clientY } = clickEvent;
|
|
27695
|
+
for (const pressable of pressables) {
|
|
27696
|
+
if (pressable.contains(target)) {
|
|
27697
|
+
// It got there on its own: nothing has been photographed yet, or the
|
|
27698
|
+
// control is not part of what was.
|
|
27699
|
+
return;
|
|
27700
|
+
}
|
|
27701
|
+
}
|
|
27702
|
+
for (const pressable of pressables) {
|
|
27703
|
+
const { left, right, top, bottom } = pressable.getBoundingClientRect();
|
|
27704
|
+
if (
|
|
27705
|
+
clientX < left ||
|
|
27706
|
+
clientX > right ||
|
|
27707
|
+
clientY < top ||
|
|
27708
|
+
clientY > bottom
|
|
27709
|
+
) {
|
|
27710
|
+
continue;
|
|
27711
|
+
}
|
|
27712
|
+
// The document is not where this press was going. Stopped here so the
|
|
27713
|
+
// application sees ONE click, on the control it was aimed at, rather than
|
|
27714
|
+
// two: this one falling through and the one below.
|
|
27715
|
+
clickEvent.stopPropagation();
|
|
27716
|
+
pressable.dispatchEvent(
|
|
27717
|
+
new clickEvent.constructor(clickEvent.type, clickEvent),
|
|
27718
|
+
);
|
|
27719
|
+
return;
|
|
27720
|
+
}
|
|
27721
|
+
};
|
|
27722
|
+
|
|
27615
27723
|
/**
|
|
27616
27724
|
* The furniture around the pages — the fixed bars, and the popups standing
|
|
27617
27725
|
* over them in the top layer — photographed with them for the length of a
|
|
@@ -28039,6 +28147,178 @@ const bandTheCutMayBeRelaxedTo = (bandBefore, bandAfter, rectAfter) => {
|
|
|
28039
28147
|
};
|
|
28040
28148
|
};
|
|
28041
28149
|
|
|
28150
|
+
/**
|
|
28151
|
+
* A view transition already playing, walked back to where it started.
|
|
28152
|
+
*
|
|
28153
|
+
* The way back is not a second transition. A transition photographs the state
|
|
28154
|
+
* it leaves, so one started to undo another photographs the state being undone:
|
|
28155
|
+
* the pictures then animate FROM where the reader is TO where they were, which
|
|
28156
|
+
* is a way forward to somewhere, not a return. The same pictures are run
|
|
28157
|
+
* backwards instead, and the state is put back UNDER them before they are
|
|
28158
|
+
* dropped — the two are the same thing at the instant they are swapped, so
|
|
28159
|
+
* nothing is seen changing.
|
|
28160
|
+
*
|
|
28161
|
+
* The way back is paid for in DISTANCE, not in time. A movement is eased: at
|
|
28162
|
+
* half of its TIME the pictures have covered ~80% of their distance, so one
|
|
28163
|
+
* caught "half-way" by the eye has barely begun by the clock. Rewound at -1 it
|
|
28164
|
+
* plays those few milliseconds back through the steep end of the curve — nearly
|
|
28165
|
+
* the whole visible distance collapses into two frames, and what one sees is a
|
|
28166
|
+
* snap, not a return. So the pictures are walked over how far they LOOK from
|
|
28167
|
+
* where they are going, at the movement's own pace, each animation at the rate
|
|
28168
|
+
* that gets it there in that time.
|
|
28169
|
+
*
|
|
28170
|
+
* Where they visibly stand is computed from the clock THROUGH the easing curve,
|
|
28171
|
+
* never read off the pseudo-elements: getComputedStyle on them answers with the
|
|
28172
|
+
* un-animated value — the animated one lives on the compositor, where no
|
|
28173
|
+
* reading from here reaches. It is the same trap as the playbackRate setter,
|
|
28174
|
+
* which is why every rate below is handed over with updatePlaybackRate.
|
|
28175
|
+
*/
|
|
28176
|
+
|
|
28177
|
+
const viewTransitionAnimations = () => {
|
|
28178
|
+
const animations = [];
|
|
28179
|
+
for (const animation of document.getAnimations()) {
|
|
28180
|
+
const pseudoElement = animation.effect?.pseudoElement;
|
|
28181
|
+
if (pseudoElement && pseudoElement.startsWith("::view-transition")) {
|
|
28182
|
+
animations.push(animation);
|
|
28183
|
+
}
|
|
28184
|
+
}
|
|
28185
|
+
return animations;
|
|
28186
|
+
};
|
|
28187
|
+
|
|
28188
|
+
/**
|
|
28189
|
+
* The animation carrying the movement, not the first that comes: everything a
|
|
28190
|
+
* movement takes along is animated too — a bar held where it stands, a trait
|
|
28191
|
+
* under a tab row, a popup travelling with its page — each with a pace of its
|
|
28192
|
+
* own. A time read on one of those and turned into a fraction of ANOTHER lands
|
|
28193
|
+
* anywhere, over 1 more often than not, which reads as a movement already over.
|
|
28194
|
+
* The names are tried in order, so a caller can say "the pages, or the document
|
|
28195
|
+
* when the pages are the document".
|
|
28196
|
+
*/
|
|
28197
|
+
const findLeadAnimation = (animations, leadNames) => {
|
|
28198
|
+
for (const leadName of leadNames) {
|
|
28199
|
+
const found = animations.find((candidate) =>
|
|
28200
|
+
candidate.effect?.pseudoElement?.includes(leadName),
|
|
28201
|
+
);
|
|
28202
|
+
if (found) {
|
|
28203
|
+
return found;
|
|
28204
|
+
}
|
|
28205
|
+
}
|
|
28206
|
+
return null;
|
|
28207
|
+
};
|
|
28208
|
+
|
|
28209
|
+
// How far the pictures visibly are into their movement, between 0 and 1.
|
|
28210
|
+
const visibleProgress = (animation) => {
|
|
28211
|
+
const timing = animation.effect.getComputedTiming();
|
|
28212
|
+
const duration = timing.delay + timing.activeDuration;
|
|
28213
|
+
if (!duration) {
|
|
28214
|
+
return null;
|
|
28215
|
+
}
|
|
28216
|
+
const temporal = animation.currentTime / duration;
|
|
28217
|
+
// The easing sits on the keyframes, where a CSS animation's
|
|
28218
|
+
// animation-timing-function ends up. "ease" is what these movements play;
|
|
28219
|
+
// anything else (linear under a finger) maps time to distance one for one.
|
|
28220
|
+
const easing = animation.effect.getKeyframes()[0]?.easing;
|
|
28221
|
+
const ratio =
|
|
28222
|
+
easing === "ease" ? easedProgress(temporal, CSS_EASE) : temporal;
|
|
28223
|
+
return { duration, ratio };
|
|
28224
|
+
};
|
|
28225
|
+
|
|
28226
|
+
// A frame's worth of movement is not a movement: under it the pictures are home
|
|
28227
|
+
// already, and a rate computed over it is a division by nearly nothing.
|
|
28228
|
+
const ONE_FRAME = 17;
|
|
28229
|
+
|
|
28230
|
+
// How long the walk should take, and 0 where the movement cannot say — a lead
|
|
28231
|
+
// nobody named, a duration of nothing. The rates below fall back to the
|
|
28232
|
+
// movement's own pace there, which is the browser's answer and the only one
|
|
28233
|
+
// left.
|
|
28234
|
+
const walkTime = (animations, leadNames, remaining) => {
|
|
28235
|
+
const lead = findLeadAnimation(animations, leadNames);
|
|
28236
|
+
const visible = lead ? visibleProgress(lead) : null;
|
|
28237
|
+
if (!visible) {
|
|
28238
|
+
return 0;
|
|
28239
|
+
}
|
|
28240
|
+
const ratio = remaining ? 1 - visible.ratio : visible.ratio;
|
|
28241
|
+
return ratio * visible.duration;
|
|
28242
|
+
};
|
|
28243
|
+
|
|
28244
|
+
/**
|
|
28245
|
+
* The pictures, sent back to the state the movement left. Returns false when
|
|
28246
|
+
* there are no pictures — a movement the browser skipped, or one whose
|
|
28247
|
+
* animations it has not created yet.
|
|
28248
|
+
*/
|
|
28249
|
+
const walkPicturesHome = (animations, leadNames) => {
|
|
28250
|
+
if (animations.length === 0) {
|
|
28251
|
+
return false;
|
|
28252
|
+
}
|
|
28253
|
+
const wallTime = walkTime(animations, leadNames, false);
|
|
28254
|
+
for (const animation of animations) {
|
|
28255
|
+
const timeCovered = animation.currentTime;
|
|
28256
|
+
const rate =
|
|
28257
|
+
wallTime > ONE_FRAME && timeCovered > 0 ? -(timeCovered / wallTime) : -1;
|
|
28258
|
+
animation.updatePlaybackRate(rate);
|
|
28259
|
+
}
|
|
28260
|
+
return true;
|
|
28261
|
+
};
|
|
28262
|
+
|
|
28263
|
+
/**
|
|
28264
|
+
* The pictures, sent on to the state the movement was going to — a way back
|
|
28265
|
+
* that is itself turned round, when the door is pressed a third time. The same
|
|
28266
|
+
* arithmetic read from the other end: what is left to cover, walked at the
|
|
28267
|
+
* movement's own pace.
|
|
28268
|
+
*/
|
|
28269
|
+
const walkPicturesOn = (animations, leadNames) => {
|
|
28270
|
+
if (animations.length === 0) {
|
|
28271
|
+
return false;
|
|
28272
|
+
}
|
|
28273
|
+
const wallTime = walkTime(animations, leadNames, true);
|
|
28274
|
+
for (const animation of animations) {
|
|
28275
|
+
const timing = animation.effect.getComputedTiming();
|
|
28276
|
+
const timeLeft =
|
|
28277
|
+
timing.delay + timing.activeDuration - animation.currentTime;
|
|
28278
|
+
const rate = wallTime > ONE_FRAME && timeLeft > 0 ? timeLeft / wallTime : 1;
|
|
28279
|
+
animation.updatePlaybackRate(rate);
|
|
28280
|
+
}
|
|
28281
|
+
return true;
|
|
28282
|
+
};
|
|
28283
|
+
|
|
28284
|
+
/**
|
|
28285
|
+
* When the pictures have arrived, whichever way they were walking. A rejection
|
|
28286
|
+
* is an outcome — animations cancelled by a transition that took this one's
|
|
28287
|
+
* place — and the caller finds nothing left to undo either way.
|
|
28288
|
+
*/
|
|
28289
|
+
const whenPicturesArrived = (animations) => {
|
|
28290
|
+
if (animations.length === 0) {
|
|
28291
|
+
// Nothing to run — a transition the browser skipped, or one that never
|
|
28292
|
+
// became ready. There is no picture to undo either, so what the caller does
|
|
28293
|
+
// next is the state alone.
|
|
28294
|
+
return Promise.resolve();
|
|
28295
|
+
}
|
|
28296
|
+
return Promise.all(
|
|
28297
|
+
animations.map((animation) => animation.finished.catch(() => {})),
|
|
28298
|
+
);
|
|
28299
|
+
};
|
|
28300
|
+
|
|
28301
|
+
// CSS `ease`, evaluated: time in, distance out. Solved numerically because the
|
|
28302
|
+
// curve is parametric — two cubics sharing a parameter, with no closed form for
|
|
28303
|
+
// one against the other. Twenty halvings put the answer well under a pixel of a
|
|
28304
|
+
// screen-wide movement.
|
|
28305
|
+
const CSS_EASE = [0.25, 0.1, 0.25, 1];
|
|
28306
|
+
const bezierAxis = (s, a, b) =>
|
|
28307
|
+
3 * (1 - s) * (1 - s) * s * a + 3 * (1 - s) * s * s * b + s * s * s;
|
|
28308
|
+
const easedProgress = (x, [x1, y1, x2, y2]) => {
|
|
28309
|
+
let low = 0;
|
|
28310
|
+
let high = 1;
|
|
28311
|
+
for (let i = 0; i < 20; i++) {
|
|
28312
|
+
const mid = (low + high) / 2;
|
|
28313
|
+
if (bezierAxis(mid, x1, x2) < x) {
|
|
28314
|
+
low = mid;
|
|
28315
|
+
} else {
|
|
28316
|
+
high = mid;
|
|
28317
|
+
}
|
|
28318
|
+
}
|
|
28319
|
+
return bezierAxis((low + high) / 2, y1, y2);
|
|
28320
|
+
};
|
|
28321
|
+
|
|
28042
28322
|
installImportMetaCssBuild(import.meta);/**
|
|
28043
28323
|
* How two routes move against each other, said one relation at a time —
|
|
28044
28324
|
* without putting them in a row, and without a box in the tree.
|
|
@@ -28110,6 +28390,17 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
28110
28390
|
* rendering_hold.js for how the picture is kept honest). A browser without
|
|
28111
28391
|
* view transitions navigates without the movement.
|
|
28112
28392
|
*
|
|
28393
|
+
* A movement can be turned round. A page whose door is in the furniture is
|
|
28394
|
+
* opened and closed by the same control, so the press that closes it often
|
|
28395
|
+
* lands while it is still opening: the navigation answering is exactly the way
|
|
28396
|
+
* back of the one on screen, and the pictures are walked backwards with the
|
|
28397
|
+
* state put under them before they are dropped (see
|
|
28398
|
+
* turnRunningTransitionRound). A second transition cannot do this — it
|
|
28399
|
+
* photographs the state being undone — and it would first skip the one playing,
|
|
28400
|
+
* which is the page snapping fully open before it closes. That press has to
|
|
28401
|
+
* arrive at all, and everything photographed is deaf to the pointer for the
|
|
28402
|
+
* length of the movement: transition_press.js is the other half.
|
|
28403
|
+
*
|
|
28113
28404
|
* However many relations are defined, there is ONE watcher: every definition
|
|
28114
28405
|
* lands in a shared registry, and the watcher is rebuilt over the whole of it
|
|
28115
28406
|
* — a navigation is a single fact about the document, and the first relation
|
|
@@ -28134,6 +28425,10 @@ const TRANSITION_TARGET_ATTRIBUTE = "data-navi-route-transition-target";
|
|
|
28134
28425
|
// for that navigation and for no other — the next one is back to the relations.
|
|
28135
28426
|
const TRANSITION_REQUEST_ATTRIBUTE = "data-navi-route-transition-request";
|
|
28136
28427
|
const AREA_NAME = "navi-route-transition";
|
|
28428
|
+
// The pictures carrying the movement, among everything else it takes along —
|
|
28429
|
+
// the pages', or the document's own when the pages ARE the document (see
|
|
28430
|
+
// findLeadAnimation).
|
|
28431
|
+
const LEAD_NAMES = [AREA_NAME, "root"];
|
|
28137
28432
|
// route_travel.jsx wears this on the root for the length of one of its
|
|
28138
28433
|
// travels (its TRAVEL_ATTRIBUTE — a comment there mirrors this one). Read by
|
|
28139
28434
|
// name rather than imported: importing route_travel.jsx would pull the whole
|
|
@@ -28735,6 +29030,12 @@ const rebuildWatcher = () => {
|
|
|
28735
29030
|
if (fromIndex === index) {
|
|
28736
29031
|
return;
|
|
28737
29032
|
}
|
|
29033
|
+
if (navigationAnimated) {
|
|
29034
|
+
// The movement on screen was turned round for this very navigation (see
|
|
29035
|
+
// turnRunningTransitionRound): it is already being answered, by the
|
|
29036
|
+
// pictures the relations would otherwise photograph over.
|
|
29037
|
+
return;
|
|
29038
|
+
}
|
|
28738
29039
|
// A page in no relation at all is a real end of the crossing, not a
|
|
28739
29040
|
// missing one: it is the "anywhere" a page reached from the furniture is
|
|
28740
29041
|
// opened over and closed back onto (see findRelation).
|
|
@@ -28761,6 +29062,7 @@ const rebuildWatcher = () => {
|
|
|
28761
29062
|
beginTransition({
|
|
28762
29063
|
page: toPage,
|
|
28763
29064
|
url: navigationUrl,
|
|
29065
|
+
fromUrl: navigationFromUrl,
|
|
28764
29066
|
// Which way it plays: what the navigation itself said first — the link
|
|
28765
29067
|
// being pressed is where the way the app is being walked is known — then
|
|
28766
29068
|
// the relation, and forward for a navigation that asked for a movement
|
|
@@ -28787,6 +29089,12 @@ const rebuildWatcher = () => {
|
|
|
28787
29089
|
let defaultTransition = null;
|
|
28788
29090
|
let navigationRequest = null;
|
|
28789
29091
|
let navigationUrl = null;
|
|
29092
|
+
// Where the document stands as the navigation begins — the other end of the
|
|
29093
|
+
// crossing, and what lets the NEXT navigation be recognised as this one's way
|
|
29094
|
+
// back. Read off the document url rather than off window.location: a traversal
|
|
29095
|
+
// on a browser without the Navigation API is announced from "popstate", where
|
|
29096
|
+
// the address has already moved and location would answer with the destination.
|
|
29097
|
+
let navigationFromUrl = null;
|
|
28790
29098
|
let navigationAnimated = false;
|
|
28791
29099
|
|
|
28792
29100
|
// The two ends of every navigation, watched from here on. The picture of the
|
|
@@ -28799,18 +29107,28 @@ observeBeforeRouting(details => {
|
|
|
28799
29107
|
navigationAnimated = false;
|
|
28800
29108
|
navigationRequest = readNavigationRequest(details);
|
|
28801
29109
|
navigationUrl = details.url;
|
|
29110
|
+
navigationFromUrl = documentUrlSignal.peek();
|
|
28802
29111
|
if (relations.length === 0 && !defaultTransition && !navigationRequest) {
|
|
28803
29112
|
return;
|
|
28804
29113
|
}
|
|
28805
29114
|
holdRenderingForRouting();
|
|
29115
|
+
// Said HERE, before the relations have their say: what is on screen may be
|
|
29116
|
+
// this navigation's own way back, and that is a fact about the two ends of
|
|
29117
|
+
// the crossing alone. A pair written "none" would silence a movement — it
|
|
29118
|
+
// must not silence the undoing of one already playing.
|
|
29119
|
+
if (turnRunningTransitionRound(navigationFromUrl, navigationUrl)) {
|
|
29120
|
+
navigationAnimated = true;
|
|
29121
|
+
}
|
|
28806
29122
|
});
|
|
28807
29123
|
observeAfterRouting(() => {
|
|
28808
29124
|
const request = navigationRequest;
|
|
28809
29125
|
const url = navigationUrl;
|
|
29126
|
+
const fromUrl = navigationFromUrl;
|
|
28810
29127
|
// Read here and dropped here: a request answers for the navigation it was
|
|
28811
29128
|
// made on, and the next one is back to the relations.
|
|
28812
29129
|
navigationRequest = null;
|
|
28813
29130
|
navigationUrl = null;
|
|
29131
|
+
navigationFromUrl = null;
|
|
28814
29132
|
if (!navigationAnimated && (request || defaultTransition)) {
|
|
28815
29133
|
const {
|
|
28816
29134
|
type,
|
|
@@ -28820,6 +29138,7 @@ observeAfterRouting(() => {
|
|
|
28820
29138
|
beginTransition({
|
|
28821
29139
|
page: null,
|
|
28822
29140
|
url,
|
|
29141
|
+
fromUrl,
|
|
28823
29142
|
// A default has no direction: nothing says which of two arbitrary
|
|
28824
29143
|
// pages is before the other, and the attribute is then worn empty —
|
|
28825
29144
|
// present for whoever keys on "one of ours is playing", silent on the
|
|
@@ -28903,6 +29222,7 @@ let currentTransition = null;
|
|
|
28903
29222
|
const beginTransition = ({
|
|
28904
29223
|
page,
|
|
28905
29224
|
url,
|
|
29225
|
+
fromUrl,
|
|
28906
29226
|
direction,
|
|
28907
29227
|
type,
|
|
28908
29228
|
duration
|
|
@@ -28917,7 +29237,15 @@ const beginTransition = ({
|
|
|
28917
29237
|
console.warn("A RouteTravel is animating this navigation; the route transition defined between these routes is skipped. Animate a pair of routes with RouteTravel or defineRouteTransition, not both.");
|
|
28918
29238
|
return;
|
|
28919
29239
|
}
|
|
28920
|
-
|
|
29240
|
+
// The two ends of the crossing, kept for the length of the movement: they are
|
|
29241
|
+
// what lets the navigation after this one be recognised as its way back (see
|
|
29242
|
+
// turnRunningTransitionRound).
|
|
29243
|
+
const transition = {
|
|
29244
|
+
fromUrl: absoluteUrl(fromUrl),
|
|
29245
|
+
url: absoluteUrl(url),
|
|
29246
|
+
walkHome: null,
|
|
29247
|
+
releaseReverting: null
|
|
29248
|
+
};
|
|
28921
29249
|
currentTransition = transition;
|
|
28922
29250
|
// Said before the picture is taken: whoever names something for a movement
|
|
28923
29251
|
// between two pages decides on it now (see transition_destination.js).
|
|
@@ -28934,6 +29262,10 @@ const beginTransition = ({
|
|
|
28934
29262
|
warnOnce("several-areas", `${areaElements.length} elements carry ${TRANSITION_AREA_ATTRIBUTE}. They all take the same view-transition-name, and a name belongs to one element at a time: the browser refuses EVERY view transition of the document while this holds. Mark the one element the pages live in.`);
|
|
28935
29263
|
}
|
|
28936
29264
|
const areaElement = areaElements.length > 0 ? areaElements[0] : null;
|
|
29265
|
+
// Everything about to be photographed goes deaf to the pointer; the controls
|
|
29266
|
+
// that asked to keep answering are heard at the document instead (see
|
|
29267
|
+
// transition_press.js).
|
|
29268
|
+
holdTransitionPress(transition);
|
|
28937
29269
|
// The area as it stands before anything moves, and the band the furniture
|
|
28938
29270
|
// around it leaves free: rendering is held, so both are still the page being
|
|
28939
29271
|
// left (see transition_window.js).
|
|
@@ -29027,6 +29359,13 @@ const beginTransition = ({
|
|
|
29027
29359
|
// ending late must not strip what a later one is wearing.
|
|
29028
29360
|
renderWait.stop();
|
|
29029
29361
|
releaseRendering();
|
|
29362
|
+
// The hold a way back took, when it is still standing: the pictures were
|
|
29363
|
+
// turned round a second time and played out, or something else took the
|
|
29364
|
+
// document over mid-walk. A hold nobody gives back freezes the page.
|
|
29365
|
+
if (transition.releaseReverting) {
|
|
29366
|
+
transition.releaseReverting();
|
|
29367
|
+
transition.releaseReverting = null;
|
|
29368
|
+
}
|
|
29030
29369
|
if (currentTransition === transition) {
|
|
29031
29370
|
currentTransition = null;
|
|
29032
29371
|
documentElement.removeAttribute(TRANSITION_ATTRIBUTE);
|
|
@@ -29035,15 +29374,99 @@ const beginTransition = ({
|
|
|
29035
29374
|
releaseTransitionWindow(transition);
|
|
29036
29375
|
releaseTransitionDestination(transition);
|
|
29037
29376
|
releaseTransitionFurniture(transition);
|
|
29377
|
+
releaseTransitionPress(transition);
|
|
29038
29378
|
if (restoreDuration) {
|
|
29039
29379
|
restoreDuration();
|
|
29040
29380
|
}
|
|
29041
29381
|
}
|
|
29042
29382
|
};
|
|
29383
|
+
transition.viewTransition = viewTransition;
|
|
29043
29384
|
viewTransition.ready.then(viewTransitionReady, ignoreSkipped$1);
|
|
29044
29385
|
viewTransition.finished.then(end, end);
|
|
29045
29386
|
};
|
|
29046
29387
|
|
|
29388
|
+
/**
|
|
29389
|
+
* The movement playing, turned round rather than replaced: this navigation is
|
|
29390
|
+
* exactly the way back of the one on screen — the door pressed again while the
|
|
29391
|
+
* page it opened is still arriving.
|
|
29392
|
+
*
|
|
29393
|
+
* A second transition cannot do this. It photographs the state it leaves, which
|
|
29394
|
+
* here is the state being undone, and animates from there to the one the reader
|
|
29395
|
+
* came from: a way forward to somewhere, not a return. It also skips the one
|
|
29396
|
+
* playing, so the page snaps fully open before it slides back off — the
|
|
29397
|
+
* teleport a toggle must never show. The same pictures are walked back instead,
|
|
29398
|
+
* and the state is put under them before they are dropped (see
|
|
29399
|
+
* view_transition_revert.js).
|
|
29400
|
+
*
|
|
29401
|
+
* Only the exact way back qualifies. Anything else — a third page, a bar entry
|
|
29402
|
+
* — is a different crossing, and gets a movement of its own.
|
|
29403
|
+
*/
|
|
29404
|
+
const turnRunningTransitionRound = (fromUrl, url) => {
|
|
29405
|
+
const running = currentTransition;
|
|
29406
|
+
if (!running || !running.viewTransition || !fromUrl || !url) {
|
|
29407
|
+
return false;
|
|
29408
|
+
}
|
|
29409
|
+
const from = absoluteUrl(fromUrl);
|
|
29410
|
+
const to = absoluteUrl(url);
|
|
29411
|
+
const isWayBack = !running.walkHome && from === running.url && to === running.fromUrl;
|
|
29412
|
+
// The door pressed a third time, while the way back is still being walked:
|
|
29413
|
+
// the pictures turn round once more, where the last press left them, rather
|
|
29414
|
+
// than a fresh movement starting over them.
|
|
29415
|
+
const isWayInAgain = running.walkHome && from === running.fromUrl && to === running.url;
|
|
29416
|
+
if (!isWayBack && !isWayInAgain) {
|
|
29417
|
+
return false;
|
|
29418
|
+
}
|
|
29419
|
+
const animations = viewTransitionAnimations();
|
|
29420
|
+
if (isWayInAgain) {
|
|
29421
|
+
// The token is dropped first: the walk it stands for is the one that must
|
|
29422
|
+
// not arrive anywhere anymore, and its promise is still pending.
|
|
29423
|
+
running.walkHome = null;
|
|
29424
|
+
walkPicturesOn(animations, LEAD_NAMES);
|
|
29425
|
+
// The pages stay held. The pictures are going to the state they were going
|
|
29426
|
+
// to all along, which is the one the DOM has held since the way back began
|
|
29427
|
+
// — what must not land under them is the render the way back queued. The
|
|
29428
|
+
// hold is given back when the movement ends (see end).
|
|
29429
|
+
return true;
|
|
29430
|
+
}
|
|
29431
|
+
if (!walkPicturesHome(animations, LEAD_NAMES)) {
|
|
29432
|
+
// The pictures do not exist yet: the movement was asked for a frame ago and
|
|
29433
|
+
// the browser has not taken them. There is nothing to turn round, and this
|
|
29434
|
+
// navigation gets a movement of its own — which skips the one that never
|
|
29435
|
+
// played, and nothing was on screen to teleport.
|
|
29436
|
+
return false;
|
|
29437
|
+
}
|
|
29438
|
+
// Which walk home this is, so the one that arrives is the one still wanted: a
|
|
29439
|
+
// walk turned round mid-way leaves a promise nobody cancelled, and it settles
|
|
29440
|
+
// when the pictures reach the far end.
|
|
29441
|
+
const walkHome = {};
|
|
29442
|
+
running.walkHome = walkHome;
|
|
29443
|
+
// The pages are held where they are until the pictures are home. The picture
|
|
29444
|
+
// of the state ARRIVING is live — it is the element being drawn, not a
|
|
29445
|
+
// photograph of it — so a page put back under it while it is still on screen
|
|
29446
|
+
// shows on BOTH sides of the movement, and the way back is watched happening
|
|
29447
|
+
// to nothing.
|
|
29448
|
+
const releaseRendering = takeoverRoutingRenderingHold();
|
|
29449
|
+
running.releaseReverting = releaseRendering;
|
|
29450
|
+
whenPicturesArrived(animations).then(() => {
|
|
29451
|
+
if (currentTransition !== running || running.walkHome !== walkHome) {
|
|
29452
|
+
// Turned round again on the way home, or replaced by another movement
|
|
29453
|
+
// altogether: whoever did that owns the pictures now.
|
|
29454
|
+
return;
|
|
29455
|
+
}
|
|
29456
|
+
running.walkHome = null;
|
|
29457
|
+
running.releaseReverting = null;
|
|
29458
|
+
// The state goes back UNDER the pictures before they are dropped, so the
|
|
29459
|
+
// two are the same thing at the instant they are swapped.
|
|
29460
|
+
releaseRendering();
|
|
29461
|
+
running.viewTransition.skipTransition();
|
|
29462
|
+
});
|
|
29463
|
+
return true;
|
|
29464
|
+
};
|
|
29465
|
+
|
|
29466
|
+
// Null rather than a url built out of nothing: two crossings that both failed
|
|
29467
|
+
// to say where they went must not look like each other's way back.
|
|
29468
|
+
const absoluteUrl = url => url ? new URL(url, window.location.href).href : null;
|
|
29469
|
+
|
|
29047
29470
|
// A transition skipped by another one starting is an outcome, not a failure.
|
|
29048
29471
|
const ignoreSkipped$1 = () => {};
|
|
29049
29472
|
|
|
@@ -29271,6 +29694,12 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
29271
29694
|
* was to where it is, on the same clock as the pages. That is why the tab row
|
|
29272
29695
|
* can stay where it is, outside this box, and still move with the finger.
|
|
29273
29696
|
*/
|
|
29697
|
+
const TRAVEL_LEAD_NAMES = ["navi-route-travel"];
|
|
29698
|
+
|
|
29699
|
+
// A browser with no view transitions of its own has no picture of the page
|
|
29700
|
+
// being left, so there is nothing to drag: the gesture is still read (it is how
|
|
29701
|
+
// one asks to change tab with a thumb), and the change happens on release, as a
|
|
29702
|
+
// press on the tab would have done.
|
|
29274
29703
|
const CAN_KEEP_PICTURE = Boolean(document.startViewTransition && !document.startViewTransition.isPolyfill);
|
|
29275
29704
|
const startViewTransition = ensureDocumentStartViewTransition();
|
|
29276
29705
|
|
|
@@ -29901,34 +30330,9 @@ const RouteTravel = ({
|
|
|
29901
30330
|
// may go on rendering, and a tab row beside the box keeps answering.
|
|
29902
30331
|
const releaseRendering = freezeRouteRender();
|
|
29903
30332
|
const animations = travelAnimations(travel);
|
|
29904
|
-
|
|
29905
|
-
// at half of its TIME the pictures have covered ~80% of their distance, so
|
|
29906
|
-
// a travel caught "half-way" by the eye has barely begun by the clock.
|
|
29907
|
-
// Rewound at -1 it plays those few milliseconds back through the steep end
|
|
29908
|
-
// of the curve — nearly the whole visible distance collapses into two
|
|
29909
|
-
// frames, and what one sees is a snap, not a return. So the pictures are
|
|
29910
|
-
// walked home over `how far they LOOK from home`, at the travel's own
|
|
29911
|
-
// pace, each animation at the rate that gets it there in that time.
|
|
29912
|
-
const wallTime = revertWalkTime(animations);
|
|
29913
|
-
for (const animation of animations) {
|
|
29914
|
-
const timeLeft = animation.currentTime;
|
|
29915
|
-
const rate = wallTime > 17 && timeLeft > 0 ? -(timeLeft / wallTime) : -1;
|
|
29916
|
-
// updatePlaybackRate, never the playbackRate setter: these animations
|
|
29917
|
-
// run on the compositor, and the setter is a non-seamless change there —
|
|
29918
|
-
// on screen the pictures jump straight to their end state while the
|
|
29919
|
-
// Animation object ticks backwards unseen. What one sees then is the
|
|
29920
|
-
// travel SNAPPING home instead of returning, and no reading of
|
|
29921
|
-
// getAnimations() will say so: only the compositor knows, and
|
|
29922
|
-
// updatePlaybackRate is how a new rate is handed to it in flight.
|
|
29923
|
-
animation.updatePlaybackRate(rate);
|
|
29924
|
-
}
|
|
30333
|
+
walkPicturesHome(animations, TRAVEL_LEAD_NAMES);
|
|
29925
30334
|
releaseHold(travel);
|
|
29926
|
-
|
|
29927
|
-
// Nothing to run backwards — a transition the browser skipped, or one
|
|
29928
|
-
// that never became ready. There is no picture to undo either, so the
|
|
29929
|
-
// way back is the state alone.
|
|
29930
|
-
Promise.resolve();
|
|
29931
|
-
backAtTheStart.then(async () => {
|
|
30335
|
+
whenPicturesArrived(animations).then(async () => {
|
|
29932
30336
|
try {
|
|
29933
30337
|
pageAskedForRef.current = travel.fromPage;
|
|
29934
30338
|
// The page that was left is put back UNDER the picture before the
|
|
@@ -30463,13 +30867,7 @@ const travelAnimations = travel => {
|
|
|
30463
30867
|
if (travel.animations) {
|
|
30464
30868
|
return travel.animations;
|
|
30465
30869
|
}
|
|
30466
|
-
const animations =
|
|
30467
|
-
for (const animation of document.getAnimations()) {
|
|
30468
|
-
const pseudoElement = animation.effect?.pseudoElement;
|
|
30469
|
-
if (pseudoElement && pseudoElement.startsWith("::view-transition")) {
|
|
30470
|
-
animations.push(animation);
|
|
30471
|
-
}
|
|
30472
|
-
}
|
|
30870
|
+
const animations = viewTransitionAnimations();
|
|
30473
30871
|
if (animations.length) {
|
|
30474
30872
|
travel.animations = animations;
|
|
30475
30873
|
}
|
|
@@ -30481,12 +30879,7 @@ const travelAnimations = travel => {
|
|
|
30481
30879
|
// reaching for it must find it where it IS.
|
|
30482
30880
|
const ratioOfTravel = travel => {
|
|
30483
30881
|
const animations = travelAnimations(travel);
|
|
30484
|
-
|
|
30485
|
-
// carries along is animated too (a trait under a tab row, the page behind),
|
|
30486
|
-
// each with a duration of its own. A time read on one of those and turned
|
|
30487
|
-
// into a fraction of ANOTHER lands anywhere — over 1 more often than not,
|
|
30488
|
-
// which reads as a travel already over and jumps the pictures to their end.
|
|
30489
|
-
const animation = animations.find(candidate => candidate.effect?.pseudoElement?.includes("navi-route-travel")) || animations[0];
|
|
30882
|
+
const animation = findLeadAnimation(animations, TRAVEL_LEAD_NAMES) || animations[0];
|
|
30490
30883
|
if (!animation) {
|
|
30491
30884
|
return travel.ratio;
|
|
30492
30885
|
}
|
|
@@ -30498,52 +30891,6 @@ const ratioOfTravel = travel => {
|
|
|
30498
30891
|
return animation.currentTime / duration;
|
|
30499
30892
|
};
|
|
30500
30893
|
|
|
30501
|
-
// CSS `ease`, evaluated: time in, distance out. Solved numerically because
|
|
30502
|
-
// the curve is parametric — two cubics sharing a parameter, with no closed
|
|
30503
|
-
// form for one against the other. Twenty halvings put the answer well under
|
|
30504
|
-
// a pixel of a screen-wide travel.
|
|
30505
|
-
const CSS_EASE = [0.25, 0.1, 0.25, 1];
|
|
30506
|
-
const bezierAxis = (s, a, b) => 3 * (1 - s) * (1 - s) * s * a + 3 * (1 - s) * s * s * b + s * s * s;
|
|
30507
|
-
const easedProgress = (x, [x1, y1, x2, y2]) => {
|
|
30508
|
-
let low = 0;
|
|
30509
|
-
let high = 1;
|
|
30510
|
-
for (let i = 0; i < 20; i++) {
|
|
30511
|
-
const mid = (low + high) / 2;
|
|
30512
|
-
if (bezierAxis(mid, x1, x2) < x) {
|
|
30513
|
-
low = mid;
|
|
30514
|
-
} else {
|
|
30515
|
-
high = mid;
|
|
30516
|
-
}
|
|
30517
|
-
}
|
|
30518
|
-
return bezierAxis((low + high) / 2, y1, y2);
|
|
30519
|
-
};
|
|
30520
|
-
|
|
30521
|
-
// How long the way back should take: the distance the pictures visibly are
|
|
30522
|
-
// from home, converted to time at the travel's own pace. The distance is
|
|
30523
|
-
// computed from the clock THROUGH the easing curve, never read off the
|
|
30524
|
-
// pseudo-elements: getComputedStyle on them answers with the un-animated
|
|
30525
|
-
// value — the animated one lives on the compositor, where no reading from
|
|
30526
|
-
// here reaches (the same trap as the playbackRate setter above).
|
|
30527
|
-
const revertWalkTime = animations => {
|
|
30528
|
-
const animation = animations.find(candidate => candidate.effect?.pseudoElement?.includes("navi-route-travel"));
|
|
30529
|
-
if (!animation) {
|
|
30530
|
-
return 0;
|
|
30531
|
-
}
|
|
30532
|
-
const timing = animation.effect.getComputedTiming();
|
|
30533
|
-
const duration = timing.delay + timing.activeDuration;
|
|
30534
|
-
if (!duration) {
|
|
30535
|
-
return 0;
|
|
30536
|
-
}
|
|
30537
|
-
const temporal = animation.currentTime / duration;
|
|
30538
|
-
// The easing sits on the keyframes, where a CSS animation's
|
|
30539
|
-
// animation-timing-function ends up. "ease" is what our travels play;
|
|
30540
|
-
// anything else (linear under a finger, see the DRAGGED attribute) maps
|
|
30541
|
-
// time to distance one for one.
|
|
30542
|
-
const easing = animation.effect.getKeyframes()[0]?.easing;
|
|
30543
|
-
const visibleRatio = easing === "ease" ? easedProgress(temporal, CSS_EASE) : temporal;
|
|
30544
|
-
return visibleRatio * duration;
|
|
30545
|
-
};
|
|
30546
|
-
|
|
30547
30894
|
// Where the two pictures stand, said as a moment in the movement they would
|
|
30548
30895
|
// have played on their own: the browser knows how they move (it is written in
|
|
30549
30896
|
// CSS), so the finger only has to say how far in. They are held still by CSS
|
|
@@ -39184,8 +39531,19 @@ const TextAnchor = ({
|
|
|
39184
39531
|
// which reads as the child "jumping" even though nothing about its own
|
|
39185
39532
|
// geometry changed. useDisplayedLayoutEffect skips the initial run in
|
|
39186
39533
|
// that case and reruns once the ancestor actually opens instead.
|
|
39187
|
-
|
|
39188
|
-
|
|
39534
|
+
//
|
|
39535
|
+
// It is asked about the CHILD, not about the anchor: the anchor is this
|
|
39536
|
+
// effect's own measuring device, displayed or not according to what the
|
|
39537
|
+
// effect last decided (see the hidden attribute below), so asking whether
|
|
39538
|
+
// it is on screen answers with that decision rather than with a fact about
|
|
39539
|
+
// the screen. On the façade of something closed — an icon in a picker's
|
|
39540
|
+
// trigger, where the openable ancestor is the picker itself — that answer
|
|
39541
|
+
// is "hidden", the correction waits for the popup to open, and the icon
|
|
39542
|
+
// jumps half a pixel on the first click. The child is what is being
|
|
39543
|
+
// positioned, and it is on screen exactly when the correction means
|
|
39544
|
+
// something.
|
|
39545
|
+
useDisplayedLayoutEffect(childRef, childEl => {
|
|
39546
|
+
const anchorEl = anchorRef.current;
|
|
39189
39547
|
if (!anchorEl || !childEl) {
|
|
39190
39548
|
return;
|
|
39191
39549
|
}
|
|
@@ -40700,6 +41058,17 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
40700
41058
|
* (`navi_value`); defaults to `href`.
|
|
40701
41059
|
* @param {boolean} [props.current] - Forces the "current" state on (otherwise
|
|
40702
41060
|
* derived from the href/route).
|
|
41061
|
+
* @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentExcept] -
|
|
41062
|
+
* Route(s) inside this link's own that are NOT it: while one of them matches,
|
|
41063
|
+
* the link is not current even though its route still is. A bar entry
|
|
41064
|
+
* standing for a whole section, and one place under that section the entry
|
|
41065
|
+
* does not stand for — settings reached from everywhere and shown over
|
|
41066
|
+
* whatever the reader was on. Nothing about the routing changes: the url is
|
|
41067
|
+
* in the section, only the link stops claiming "you are here".
|
|
41068
|
+
* @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentAlso] -
|
|
41069
|
+
* Route(s) other than this link's own that are it: the link is current while
|
|
41070
|
+
* one of them matches. The other side of `currentExcept` — an entry standing
|
|
41071
|
+
* for more than the one page it opens. `currentExcept` wins over it.
|
|
40703
41072
|
* @param {"text"|"icon"|"tab"} [props.variant] - Visual variant
|
|
40704
41073
|
* (`data-variant`); `"text"`/`"icon"` drop the link color/underline,
|
|
40705
41074
|
* `"tab"` renders a tab-like affordance.
|
|
@@ -40743,6 +41112,16 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
40743
41112
|
* the pair's movement and only turns it round, which is what the rare way
|
|
40744
41113
|
* round a pair usually needs. Said nowhere else, the relations answer as
|
|
40745
41114
|
* they always do.
|
|
41115
|
+
* @param {boolean} [props.pressableDuringRouteTransition] - Keep answering
|
|
41116
|
+
* presses while a route transition plays. Everything a movement photographs
|
|
41117
|
+
* goes deaf to the pointer for its whole length — a captured element is not
|
|
41118
|
+
* painted where it stands, so nothing hit-tests to it — which is right for
|
|
41119
|
+
* the pages and wrong for the door that opened them: a control in a bar the
|
|
41120
|
+
* two states share is standing exactly where its picture is drawn, and a
|
|
41121
|
+
* press on it is aimed at what the reader can see. Say it there, and only
|
|
41122
|
+
* there: a control that TRAVELS with the pages has a rectangle where it will
|
|
41123
|
+
* be, not where it is seen, and a press handed to it fires something nobody
|
|
41124
|
+
* aimed at.
|
|
40746
41125
|
* @param {boolean} [props.replace] - Go to the destination by TAKING THE PLACE
|
|
40747
41126
|
* of the current history entry instead of stacking onto it: the link stays a
|
|
40748
41127
|
* link (an address, a middle click, the keyboard, `aria-current`), only the
|
|
@@ -40781,6 +41160,8 @@ const LinkWithRoute = ({
|
|
|
40781
41160
|
route,
|
|
40782
41161
|
routeParams,
|
|
40783
41162
|
current,
|
|
41163
|
+
currentExcept,
|
|
41164
|
+
currentAlso,
|
|
40784
41165
|
children,
|
|
40785
41166
|
...rest
|
|
40786
41167
|
}) => {
|
|
@@ -40789,7 +41170,11 @@ const LinkWithRoute = ({
|
|
|
40789
41170
|
matching
|
|
40790
41171
|
} = useRouteStatus(route);
|
|
40791
41172
|
const paramsAreMatching = route.matchesParams(routeParams);
|
|
40792
|
-
const
|
|
41173
|
+
const someExceptedRouteMatching = useSomeRouteMatching(currentExcept);
|
|
41174
|
+
const someAlsoRouteMatching = useSomeRouteMatching(currentAlso);
|
|
41175
|
+
// "Except" is a veto: a route named there is somewhere else, whatever the
|
|
41176
|
+
// rest of the reading says.
|
|
41177
|
+
const linkMatching = !someExceptedRouteMatching && (matching && paramsAreMatching || someAlsoRouteMatching);
|
|
40793
41178
|
const innerCurrent = current || linkMatching;
|
|
40794
41179
|
return jsx(Link, {
|
|
40795
41180
|
href: url,
|
|
@@ -40798,6 +41183,22 @@ const LinkWithRoute = ({
|
|
|
40798
41183
|
children: children || route.buildRelativeUrl(routeParams)
|
|
40799
41184
|
});
|
|
40800
41185
|
};
|
|
41186
|
+
// A route, or a list of them, read as one answer: does any of them match.
|
|
41187
|
+
// Reading matchingSignal during the render is what subscribes the component to
|
|
41188
|
+
// it, so every route is read even once one has answered yes — stopping early
|
|
41189
|
+
// would leave the component deaf to the ones it skipped.
|
|
41190
|
+
const useSomeRouteMatching = routes => {
|
|
41191
|
+
if (!routes) {
|
|
41192
|
+
return false;
|
|
41193
|
+
}
|
|
41194
|
+
let someMatching = false;
|
|
41195
|
+
for (const route of Array.isArray(routes) ? routes : [routes]) {
|
|
41196
|
+
if (route.matchingSignal.value) {
|
|
41197
|
+
someMatching = true;
|
|
41198
|
+
}
|
|
41199
|
+
}
|
|
41200
|
+
return someMatching;
|
|
41201
|
+
};
|
|
40801
41202
|
const LinkPlain = props => {
|
|
40802
41203
|
const defaultRef = useRef();
|
|
40803
41204
|
props.ref = props.ref || defaultRef;
|
|
@@ -40821,6 +41222,7 @@ const LinkPlain = props => {
|
|
|
40821
41222
|
revealOnInteraction = false,
|
|
40822
41223
|
hrefFallback = !anchor,
|
|
40823
41224
|
routeTransition,
|
|
41225
|
+
pressableDuringRouteTransition,
|
|
40824
41226
|
replace,
|
|
40825
41227
|
children
|
|
40826
41228
|
} = props;
|
|
@@ -40981,6 +41383,8 @@ const LinkPlain = props => {
|
|
|
40981
41383
|
revealOnInteraction: undefined,
|
|
40982
41384
|
variant: undefined,
|
|
40983
41385
|
current: undefined,
|
|
41386
|
+
currentExcept: undefined,
|
|
41387
|
+
currentAlso: undefined,
|
|
40984
41388
|
currentIndicator: undefined,
|
|
40985
41389
|
currentEffectBold: undefined,
|
|
40986
41390
|
currentEffectShadow: undefined,
|
|
@@ -40990,8 +41394,12 @@ const LinkPlain = props => {
|
|
|
40990
41394
|
endIcon: undefined,
|
|
40991
41395
|
hrefFallback: undefined,
|
|
40992
41396
|
routeTransition: undefined,
|
|
41397
|
+
pressableDuringRouteTransition: undefined,
|
|
40993
41398
|
replace: undefined,
|
|
40994
41399
|
"data-navi-route-transition-request": routeTransitionRequest,
|
|
41400
|
+
...(pressableDuringRouteTransition ? {
|
|
41401
|
+
[PRESSABLE_ATTRIBUTE]: ""
|
|
41402
|
+
} : null),
|
|
40995
41403
|
...replaceRequest,
|
|
40996
41404
|
// The control's own handlers first — the interaction gate, the caller's
|
|
40997
41405
|
// onClick/onKeyDown, the command and the action — then what only a link
|
|
@@ -41032,8 +41440,14 @@ const LinkPlain = props => {
|
|
|
41032
41440
|
commandfor: slide ? nav?.slideContainer : undefined,
|
|
41033
41441
|
"aria-controls": slide ? nav?.slideContainer : undefined,
|
|
41034
41442
|
tabIndex: slide ? props.tabIndex ?? 0 : props.tabIndex,
|
|
41035
|
-
role: slide ? "tab" : props.role
|
|
41036
|
-
|
|
41443
|
+
role: slide ? "tab" : props.role
|
|
41444
|
+
// The same reading the paint uses: what the link claims about being
|
|
41445
|
+
// where the reader is has to be one claim, said to the eye and to a
|
|
41446
|
+
// screen reader alike. A slide is not a page — that tab says it with
|
|
41447
|
+
// aria-selected below.
|
|
41448
|
+
,
|
|
41449
|
+
|
|
41450
|
+
"aria-current": !slide && innerCurrent ? "page" : undefined,
|
|
41037
41451
|
"aria-selected": slide ? innerCurrent : selectionContext ? selected : undefined,
|
|
41038
41452
|
"data-value-event": "navi_value",
|
|
41039
41453
|
onnavi_value: e => {
|
|
@@ -44373,6 +44787,58 @@ const resolveNavStateProp = (navState, popupId, name) => {
|
|
|
44373
44787
|
return { id: navState.id || popupId, type: navState.type || "replace" };
|
|
44374
44788
|
};
|
|
44375
44789
|
|
|
44790
|
+
// What a write of the open state is worth in the history, when the signal
|
|
44791
|
+
// holding it is bound to a url (a route's `searchParams`, see route.js). A
|
|
44792
|
+
// plain signal has no `set` and takes the value as it always did.
|
|
44793
|
+
const writeInSignal = (signal, value, { history }) => {
|
|
44794
|
+
if (signal.set) {
|
|
44795
|
+
signal.set(value, { history });
|
|
44796
|
+
return;
|
|
44797
|
+
}
|
|
44798
|
+
signal.value = value;
|
|
44799
|
+
};
|
|
44800
|
+
|
|
44801
|
+
// The popup says where it is, into a `signal` the caller holds — and when that
|
|
44802
|
+
// signal lives in a url, saying so is a navigation. It is worth exactly what
|
|
44803
|
+
// the same move is worth when the open state lives in the history entry
|
|
44804
|
+
// instead (see useNavState's own leave()):
|
|
44805
|
+
// - the opening is worth what the state declares (`history: "push"` for a
|
|
44806
|
+
// popup one can back out of, the default replacement for one that merely
|
|
44807
|
+
// qualifies the screen one is on);
|
|
44808
|
+
// - the closing is never an entry of its own. Stacking one would leave the
|
|
44809
|
+
// entry that carries the popup BEHIND the reader, and their next back press
|
|
44810
|
+
// would walk straight back into the popup they just closed.
|
|
44811
|
+
// A cancel (Escape, the backdrop, --navi-cancel) goes back to before the
|
|
44812
|
+
// opening rather than rewriting the entry, so everything else written to the
|
|
44813
|
+
// url while the popup was open goes back with it. A close that is not a cancel
|
|
44814
|
+
// keeps those writes and only drops the popup.
|
|
44815
|
+
const writeOpenedInSignal = (signal, opened, event) => {
|
|
44816
|
+
if (signal.peek() === opened) {
|
|
44817
|
+
// The signal already says so, meaning this open/close IS what it asked
|
|
44818
|
+
// for: a back press that took the popup out of the url, the application
|
|
44819
|
+
// writing it. Nothing to write back — and nothing to go back to either,
|
|
44820
|
+
// since the navigation navBack would undo is the one that asked for this.
|
|
44821
|
+
return;
|
|
44822
|
+
}
|
|
44823
|
+
if (opened) {
|
|
44824
|
+
signal.value = true;
|
|
44825
|
+
return;
|
|
44826
|
+
}
|
|
44827
|
+
if (
|
|
44828
|
+
event?.detail?.isCancel &&
|
|
44829
|
+
signal.options?.getHistory?.() === "push" &&
|
|
44830
|
+
// Nothing of this document behind: the popup was opened by the url itself
|
|
44831
|
+
// (a shared link, a bookmark). navBack would do nothing at all there, so
|
|
44832
|
+
// the entry is rewritten instead — the address must not keep saying open
|
|
44833
|
+
// about a popup that just closed.
|
|
44834
|
+
canNavBackSignal.peek()
|
|
44835
|
+
) {
|
|
44836
|
+
navBack();
|
|
44837
|
+
return;
|
|
44838
|
+
}
|
|
44839
|
+
writeInSignal(signal, false, { history: "replace" });
|
|
44840
|
+
};
|
|
44841
|
+
|
|
44376
44842
|
/**
|
|
44377
44843
|
* Keeps an open controller in sync with where the caller says the popup should
|
|
44378
44844
|
* be: an `open`/`defaultOpen` pair, a `signal`, or a `navState` — the open
|
|
@@ -44431,7 +44897,7 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
44431
44897
|
}
|
|
44432
44898
|
}
|
|
44433
44899
|
if (signal) {
|
|
44434
|
-
signal
|
|
44900
|
+
writeOpenedInSignal(signal, opened, event);
|
|
44435
44901
|
}
|
|
44436
44902
|
}
|
|
44437
44903
|
: null;
|
|
@@ -44490,8 +44956,10 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
44490
44956
|
// The request can be refused (a busy form denying the close): the popup
|
|
44491
44957
|
// then stays where it was, and whoever holds the open state is told so —
|
|
44492
44958
|
// otherwise it would keep saying "closed" about a popup still open.
|
|
44959
|
+
// Written over rather than stacked on: a refusal corrects the state that
|
|
44960
|
+
// asked, it is not a place one came from.
|
|
44493
44961
|
if (signal) {
|
|
44494
|
-
signal
|
|
44962
|
+
writeInSignal(signal, openController.opened, { history: "replace" });
|
|
44495
44963
|
}
|
|
44496
44964
|
if (navStateId && openController.opened) {
|
|
44497
44965
|
enterNavState();
|
|
@@ -46218,6 +46686,7 @@ const ButtonUI = props => {
|
|
|
46218
46686
|
target,
|
|
46219
46687
|
rel,
|
|
46220
46688
|
replace,
|
|
46689
|
+
pressableDuringRouteTransition,
|
|
46221
46690
|
// visual
|
|
46222
46691
|
variant,
|
|
46223
46692
|
pressEffect,
|
|
@@ -46268,6 +46737,12 @@ const ButtonUI = props => {
|
|
|
46268
46737
|
const replaceRequest = replace ? {
|
|
46269
46738
|
[LINK_REPLACE_ATTRIBUTE]: ""
|
|
46270
46739
|
} : null;
|
|
46740
|
+
|
|
46741
|
+
// Worn as an attribute too, and read at the document by whoever catches the
|
|
46742
|
+
// press a movement would have swallowed (see transition_press.js).
|
|
46743
|
+
const pressableRequest = pressableDuringRouteTransition ? {
|
|
46744
|
+
[PRESSABLE_ATTRIBUTE]: ""
|
|
46745
|
+
} : null;
|
|
46271
46746
|
const visualSelector = ".navi_button_content";
|
|
46272
46747
|
useAccentColorAttributes(ref, null, {
|
|
46273
46748
|
elementSelector: visualSelector
|
|
@@ -46296,6 +46771,8 @@ const ButtonUI = props => {
|
|
|
46296
46771
|
rel: innerRel,
|
|
46297
46772
|
replace: undefined,
|
|
46298
46773
|
...replaceRequest,
|
|
46774
|
+
pressableDuringRouteTransition: undefined,
|
|
46775
|
+
...pressableRequest,
|
|
46299
46776
|
// Respond with the JS prop value directly so callers (e.g. resolveCommandValue)
|
|
46300
46777
|
// get the original type instead of the DOM-coerced string (e.g. "[object Object]").
|
|
46301
46778
|
onnavi_get_value: e => {
|
|
@@ -46519,6 +46996,13 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
46519
46996
|
* `--navi-nav-to` command — by TAKING THE PLACE of the current history entry
|
|
46520
46997
|
* rather than stacking on it: what `<Link replace>` says, for a press drawn
|
|
46521
46998
|
* as a button.
|
|
46999
|
+
* @param {boolean} [pressableDuringRouteTransition] Keep answering presses
|
|
47000
|
+
* while a route transition plays: what a movement photographs goes deaf to
|
|
47001
|
+
* the pointer for its whole length, and the door that opened the page — a
|
|
47002
|
+
* button in a bar the two states share, standing exactly where its picture is
|
|
47003
|
+
* drawn — must still close it. What `<Link pressableDuringRouteTransition>`
|
|
47004
|
+
* says, for the other half of a toggle. Only for a control that does not
|
|
47005
|
+
* travel with the pages.
|
|
46522
47006
|
* @param {Function} [action] On a button with an `href` or a `route`, the
|
|
46523
47007
|
* same order as a Link's: it runs on the press, before the navigation, and
|
|
46524
47008
|
* the navigation does not wait for it (see Link's `action`).
|
|
@@ -80760,6 +81244,14 @@ const css = /* css */`.navi_side_panel {
|
|
|
80760
81244
|
* required at all for a purely command-driven panel (an `id` plus a
|
|
80761
81245
|
* `<Button command="--navi-toggle" commandFor={id}>` elsewhere, same as
|
|
80762
81246
|
* `Dialog`/`Popover` themselves — see either's own doc).
|
|
81247
|
+
* @param {boolean|string|{id?: string, type?: "push"|"replace"}} [props.navState] -
|
|
81248
|
+
* Keeps the open state in the history entry, so a screen left and come back
|
|
81249
|
+
* to finds its panel as it was: `true` uses the panel's own `id`, a string
|
|
81250
|
+
* names the key, `{ id, type }` chooses between rewriting the entry
|
|
81251
|
+
* ("replace", the default) and pushing one ("push"), where closing goes
|
|
81252
|
+
* back — which is what makes the phone's back gesture close the panel.
|
|
81253
|
+
* Forwarded as-is to `Popup`; source of truth, so it excludes `open` and
|
|
81254
|
+
* `signal` (see `Dialog`/`Popover`'s own `navState`).
|
|
80763
81255
|
* @param {(event: Event) => void} [props.onClose] - Called when the panel
|
|
80764
81256
|
* actually closes (see `Dialog`/`Popover`'s own `onClose`).
|
|
80765
81257
|
* @param {"left"|"right"|"top"|"bottom"} [props.side="right"] - Which
|
|
@@ -80841,8 +81333,16 @@ const SidePanel = ({
|
|
|
80841
81333
|
import.meta.css = [css, "@jsenv/navi/src/layout/side_panel.jsx"];
|
|
80842
81334
|
const onSwipePointerDown = swipeToClose ? createSwipeToClose(side) : null;
|
|
80843
81335
|
return jsx(Popup, {
|
|
80844
|
-
mode: mode
|
|
80845
|
-
|
|
81336
|
+
mode: mode
|
|
81337
|
+
// Spread rather than written: the collision warning between `signal` and
|
|
81338
|
+
// `open` asks whether the prop is THERE, not what it holds, so a panel
|
|
81339
|
+
// handed a signal and nothing else would be told off for an `open` only
|
|
81340
|
+
// this line ever put on it.
|
|
81341
|
+
,
|
|
81342
|
+
|
|
81343
|
+
...(open === undefined ? null : {
|
|
81344
|
+
open
|
|
81345
|
+
}),
|
|
80846
81346
|
signal: signal,
|
|
80847
81347
|
defaultOpen: defaultOpen,
|
|
80848
81348
|
onClose: onClose,
|