@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.
@@ -17635,7 +17635,9 @@ const useActionAsyncData = (action, {
17635
17635
  onLoad
17636
17636
  }) => {
17637
17637
  const loadingRef = useContext(LoadingContext);
17638
- if (!loadingRef) {
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
- // Dismissed with no data — suspend until the action re-runs.
17728
- // A never-resolving promise would leave the component stuck forever,
17729
- // so we use an action-specific promise that resolves on RUNNING,
17730
- // which lets the component re-render and go through the normal loading path.
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
- return [undefined, false, actionError, dismissError];
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
- const transition = {};
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
- // The way back is paid for in DISTANCE, not in time. The way in is eased:
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
- const backAtTheStart = animations.length ? Promise.all(animations.map(animation => animation.finished.catch(() => {}))) :
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
- // The pages' own animation, not the first that comes: everything the travel
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
- useDisplayedLayoutEffect(anchorRef, anchorEl => {
39188
- const childEl = childRef.current;
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 linkMatching = matching && paramsAreMatching;
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
- "aria-current": isCurrent ? "page" : undefined,
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.value = opened;
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.value = openController.opened;
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
- open: open,
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,