@jsenv/navi 0.29.165 → 0.29.166

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.
@@ -27612,6 +27612,105 @@ const releaseTransitionDestination = (owner) => {
27612
27612
  publishDestination(null);
27613
27613
  };
27614
27614
 
27615
+ /**
27616
+ * The press that lands on a control the movement turned into a picture.
27617
+ *
27618
+ * A captured element is dead in both senses: it stops being painted where it
27619
+ * stands, and nothing hit-tests to it — the press falls through to the nearest
27620
+ * ancestor still being painted, whatever the pseudo-elements are told about
27621
+ * pointer events. For the pages that is the whole point: both are pictures of
27622
+ * somewhere the reader is no longer, or not yet.
27623
+ *
27624
+ * For the DOOR it is not. A page reached from the furniture — a gear in a top
27625
+ * bar, a "+" in a tab bar — is opened and closed by the same control, and that
27626
+ * control is standing exactly where its picture is drawn: the bar the two
27627
+ * states share is one group the browser holds still. So a press on it during
27628
+ * those few hundred milliseconds is aimed at what the reader can see, and
27629
+ * swallowing it makes the toggle feel stuck at the very moment it is being used
27630
+ * as one — press to open, press again to close.
27631
+ *
27632
+ * A control therefore may ask to keep answering, and while a route transition
27633
+ * plays the press is caught at the document and handed to it when it fell
27634
+ * inside its rectangle — which is where the hand thinks it pressed. The same
27635
+ * move route_travel.jsx makes for the box a finger reaches for mid-flight.
27636
+ *
27637
+ * ASKED FOR, never derived, and that is the whole safety of it: only the
27638
+ * application knows the control does not move. One that travels with the pages
27639
+ * has a rectangle where it WILL be, not where it is seen, and a press handed to
27640
+ * it fires something the reader never aimed at — worse than the press being
27641
+ * lost. Hence the dev warning below for a control found inside the area.
27642
+ *
27643
+ * The click alone is re-aimed, not the pointer stream that leads to it: what
27644
+ * has to survive is the press meaning what it meant, and the press feedback is
27645
+ * lost anyway — the control is a picture, so nothing it paints for its own
27646
+ * `:active` state reaches the screen. Synthesising a pointerdown with no
27647
+ * pointer behind it would start gestures (a drag, a hold) that no pointerup
27648
+ * ever ends.
27649
+ */
27650
+
27651
+ // Worn by a control that keeps answering while the pages move. Written by
27652
+ // <Link pressableDuringRouteTransition> / <Button pressableDuringRouteTransition>,
27653
+ // and by hand on anything else — this module owns the name and does the reading.
27654
+ const PRESSABLE_ATTRIBUTE = "data-navi-route-transition-pressable";
27655
+ const PRESSABLE_SELECTOR = `[${PRESSABLE_ATTRIBUTE}]`;
27656
+
27657
+ let pressOwner = null;
27658
+
27659
+ const holdTransitionPress = (owner, areaElement) => {
27660
+ pressOwner = owner;
27661
+ // Adding the same listener twice is a no-op, so a movement taking over from
27662
+ // another needs nothing but the new owner.
27663
+ document.addEventListener("click", onDocumentClick, true);
27664
+ };
27665
+
27666
+ const releaseTransitionPress = (owner) => {
27667
+ if (owner !== pressOwner) {
27668
+ return;
27669
+ }
27670
+ pressOwner = null;
27671
+ document.removeEventListener("click", onDocumentClick, true);
27672
+ };
27673
+
27674
+ const onDocumentClick = (clickEvent) => {
27675
+ // A click of our own making, or one an application dispatched by hand: both
27676
+ // are already aimed, and re-aiming them by coordinates that mean nothing
27677
+ // (0, 0 for a synthetic click) would fire whatever sits in the corner.
27678
+ if (!clickEvent.isTrusted) {
27679
+ return;
27680
+ }
27681
+ const pressables = document.querySelectorAll(PRESSABLE_SELECTOR);
27682
+ if (pressables.length === 0) {
27683
+ return;
27684
+ }
27685
+ const { target, clientX, clientY } = clickEvent;
27686
+ for (const pressable of pressables) {
27687
+ if (pressable.contains(target)) {
27688
+ // It got there on its own: nothing has been photographed yet, or the
27689
+ // control is not part of what was.
27690
+ return;
27691
+ }
27692
+ }
27693
+ for (const pressable of pressables) {
27694
+ const { left, right, top, bottom } = pressable.getBoundingClientRect();
27695
+ if (
27696
+ clientX < left ||
27697
+ clientX > right ||
27698
+ clientY < top ||
27699
+ clientY > bottom
27700
+ ) {
27701
+ continue;
27702
+ }
27703
+ // The document is not where this press was going. Stopped here so the
27704
+ // application sees ONE click, on the control it was aimed at, rather than
27705
+ // two: this one falling through and the one below.
27706
+ clickEvent.stopPropagation();
27707
+ pressable.dispatchEvent(
27708
+ new clickEvent.constructor(clickEvent.type, clickEvent),
27709
+ );
27710
+ return;
27711
+ }
27712
+ };
27713
+
27615
27714
  /**
27616
27715
  * The furniture around the pages — the fixed bars, and the popups standing
27617
27716
  * over them in the top layer — photographed with them for the length of a
@@ -28039,6 +28138,178 @@ const bandTheCutMayBeRelaxedTo = (bandBefore, bandAfter, rectAfter) => {
28039
28138
  };
28040
28139
  };
28041
28140
 
28141
+ /**
28142
+ * A view transition already playing, walked back to where it started.
28143
+ *
28144
+ * The way back is not a second transition. A transition photographs the state
28145
+ * it leaves, so one started to undo another photographs the state being undone:
28146
+ * the pictures then animate FROM where the reader is TO where they were, which
28147
+ * is a way forward to somewhere, not a return. The same pictures are run
28148
+ * backwards instead, and the state is put back UNDER them before they are
28149
+ * dropped — the two are the same thing at the instant they are swapped, so
28150
+ * nothing is seen changing.
28151
+ *
28152
+ * The way back is paid for in DISTANCE, not in time. A movement is eased: at
28153
+ * half of its TIME the pictures have covered ~80% of their distance, so one
28154
+ * caught "half-way" by the eye has barely begun by the clock. Rewound at -1 it
28155
+ * plays those few milliseconds back through the steep end of the curve — nearly
28156
+ * the whole visible distance collapses into two frames, and what one sees is a
28157
+ * snap, not a return. So the pictures are walked over how far they LOOK from
28158
+ * where they are going, at the movement's own pace, each animation at the rate
28159
+ * that gets it there in that time.
28160
+ *
28161
+ * Where they visibly stand is computed from the clock THROUGH the easing curve,
28162
+ * never read off the pseudo-elements: getComputedStyle on them answers with the
28163
+ * un-animated value — the animated one lives on the compositor, where no
28164
+ * reading from here reaches. It is the same trap as the playbackRate setter,
28165
+ * which is why every rate below is handed over with updatePlaybackRate.
28166
+ */
28167
+
28168
+ const viewTransitionAnimations = () => {
28169
+ const animations = [];
28170
+ for (const animation of document.getAnimations()) {
28171
+ const pseudoElement = animation.effect?.pseudoElement;
28172
+ if (pseudoElement && pseudoElement.startsWith("::view-transition")) {
28173
+ animations.push(animation);
28174
+ }
28175
+ }
28176
+ return animations;
28177
+ };
28178
+
28179
+ /**
28180
+ * The animation carrying the movement, not the first that comes: everything a
28181
+ * movement takes along is animated too — a bar held where it stands, a trait
28182
+ * under a tab row, a popup travelling with its page — each with a pace of its
28183
+ * own. A time read on one of those and turned into a fraction of ANOTHER lands
28184
+ * anywhere, over 1 more often than not, which reads as a movement already over.
28185
+ * The names are tried in order, so a caller can say "the pages, or the document
28186
+ * when the pages are the document".
28187
+ */
28188
+ const findLeadAnimation = (animations, leadNames) => {
28189
+ for (const leadName of leadNames) {
28190
+ const found = animations.find((candidate) =>
28191
+ candidate.effect?.pseudoElement?.includes(leadName),
28192
+ );
28193
+ if (found) {
28194
+ return found;
28195
+ }
28196
+ }
28197
+ return null;
28198
+ };
28199
+
28200
+ // How far the pictures visibly are into their movement, between 0 and 1.
28201
+ const visibleProgress = (animation) => {
28202
+ const timing = animation.effect.getComputedTiming();
28203
+ const duration = timing.delay + timing.activeDuration;
28204
+ if (!duration) {
28205
+ return null;
28206
+ }
28207
+ const temporal = animation.currentTime / duration;
28208
+ // The easing sits on the keyframes, where a CSS animation's
28209
+ // animation-timing-function ends up. "ease" is what these movements play;
28210
+ // anything else (linear under a finger) maps time to distance one for one.
28211
+ const easing = animation.effect.getKeyframes()[0]?.easing;
28212
+ const ratio =
28213
+ easing === "ease" ? easedProgress(temporal, CSS_EASE) : temporal;
28214
+ return { duration, ratio };
28215
+ };
28216
+
28217
+ // A frame's worth of movement is not a movement: under it the pictures are home
28218
+ // already, and a rate computed over it is a division by nearly nothing.
28219
+ const ONE_FRAME = 17;
28220
+
28221
+ // How long the walk should take, and 0 where the movement cannot say — a lead
28222
+ // nobody named, a duration of nothing. The rates below fall back to the
28223
+ // movement's own pace there, which is the browser's answer and the only one
28224
+ // left.
28225
+ const walkTime = (animations, leadNames, remaining) => {
28226
+ const lead = findLeadAnimation(animations, leadNames);
28227
+ const visible = lead ? visibleProgress(lead) : null;
28228
+ if (!visible) {
28229
+ return 0;
28230
+ }
28231
+ const ratio = remaining ? 1 - visible.ratio : visible.ratio;
28232
+ return ratio * visible.duration;
28233
+ };
28234
+
28235
+ /**
28236
+ * The pictures, sent back to the state the movement left. Returns false when
28237
+ * there are no pictures — a movement the browser skipped, or one whose
28238
+ * animations it has not created yet.
28239
+ */
28240
+ const walkPicturesHome = (animations, leadNames) => {
28241
+ if (animations.length === 0) {
28242
+ return false;
28243
+ }
28244
+ const wallTime = walkTime(animations, leadNames, false);
28245
+ for (const animation of animations) {
28246
+ const timeCovered = animation.currentTime;
28247
+ const rate =
28248
+ wallTime > ONE_FRAME && timeCovered > 0 ? -(timeCovered / wallTime) : -1;
28249
+ animation.updatePlaybackRate(rate);
28250
+ }
28251
+ return true;
28252
+ };
28253
+
28254
+ /**
28255
+ * The pictures, sent on to the state the movement was going to — a way back
28256
+ * that is itself turned round, when the door is pressed a third time. The same
28257
+ * arithmetic read from the other end: what is left to cover, walked at the
28258
+ * movement's own pace.
28259
+ */
28260
+ const walkPicturesOn = (animations, leadNames) => {
28261
+ if (animations.length === 0) {
28262
+ return false;
28263
+ }
28264
+ const wallTime = walkTime(animations, leadNames, true);
28265
+ for (const animation of animations) {
28266
+ const timing = animation.effect.getComputedTiming();
28267
+ const timeLeft =
28268
+ timing.delay + timing.activeDuration - animation.currentTime;
28269
+ const rate = wallTime > ONE_FRAME && timeLeft > 0 ? timeLeft / wallTime : 1;
28270
+ animation.updatePlaybackRate(rate);
28271
+ }
28272
+ return true;
28273
+ };
28274
+
28275
+ /**
28276
+ * When the pictures have arrived, whichever way they were walking. A rejection
28277
+ * is an outcome — animations cancelled by a transition that took this one's
28278
+ * place — and the caller finds nothing left to undo either way.
28279
+ */
28280
+ const whenPicturesArrived = (animations) => {
28281
+ if (animations.length === 0) {
28282
+ // Nothing to run — a transition the browser skipped, or one that never
28283
+ // became ready. There is no picture to undo either, so what the caller does
28284
+ // next is the state alone.
28285
+ return Promise.resolve();
28286
+ }
28287
+ return Promise.all(
28288
+ animations.map((animation) => animation.finished.catch(() => {})),
28289
+ );
28290
+ };
28291
+
28292
+ // CSS `ease`, evaluated: time in, distance out. Solved numerically because the
28293
+ // curve is parametric — two cubics sharing a parameter, with no closed form for
28294
+ // one against the other. Twenty halvings put the answer well under a pixel of a
28295
+ // screen-wide movement.
28296
+ const CSS_EASE = [0.25, 0.1, 0.25, 1];
28297
+ const bezierAxis = (s, a, b) =>
28298
+ 3 * (1 - s) * (1 - s) * s * a + 3 * (1 - s) * s * s * b + s * s * s;
28299
+ const easedProgress = (x, [x1, y1, x2, y2]) => {
28300
+ let low = 0;
28301
+ let high = 1;
28302
+ for (let i = 0; i < 20; i++) {
28303
+ const mid = (low + high) / 2;
28304
+ if (bezierAxis(mid, x1, x2) < x) {
28305
+ low = mid;
28306
+ } else {
28307
+ high = mid;
28308
+ }
28309
+ }
28310
+ return bezierAxis((low + high) / 2, y1, y2);
28311
+ };
28312
+
28042
28313
  installImportMetaCssBuild(import.meta);/**
28043
28314
  * How two routes move against each other, said one relation at a time —
28044
28315
  * without putting them in a row, and without a box in the tree.
@@ -28110,6 +28381,17 @@ installImportMetaCssBuild(import.meta);/**
28110
28381
  * rendering_hold.js for how the picture is kept honest). A browser without
28111
28382
  * view transitions navigates without the movement.
28112
28383
  *
28384
+ * A movement can be turned round. A page whose door is in the furniture is
28385
+ * opened and closed by the same control, so the press that closes it often
28386
+ * lands while it is still opening: the navigation answering is exactly the way
28387
+ * back of the one on screen, and the pictures are walked backwards with the
28388
+ * state put under them before they are dropped (see
28389
+ * turnRunningTransitionRound). A second transition cannot do this — it
28390
+ * photographs the state being undone — and it would first skip the one playing,
28391
+ * which is the page snapping fully open before it closes. That press has to
28392
+ * arrive at all, and everything photographed is deaf to the pointer for the
28393
+ * length of the movement: transition_press.js is the other half.
28394
+ *
28113
28395
  * However many relations are defined, there is ONE watcher: every definition
28114
28396
  * lands in a shared registry, and the watcher is rebuilt over the whole of it
28115
28397
  * — a navigation is a single fact about the document, and the first relation
@@ -28134,6 +28416,10 @@ const TRANSITION_TARGET_ATTRIBUTE = "data-navi-route-transition-target";
28134
28416
  // for that navigation and for no other — the next one is back to the relations.
28135
28417
  const TRANSITION_REQUEST_ATTRIBUTE = "data-navi-route-transition-request";
28136
28418
  const AREA_NAME = "navi-route-transition";
28419
+ // The pictures carrying the movement, among everything else it takes along —
28420
+ // the pages', or the document's own when the pages ARE the document (see
28421
+ // findLeadAnimation).
28422
+ const LEAD_NAMES = [AREA_NAME, "root"];
28137
28423
  // route_travel.jsx wears this on the root for the length of one of its
28138
28424
  // travels (its TRAVEL_ATTRIBUTE — a comment there mirrors this one). Read by
28139
28425
  // name rather than imported: importing route_travel.jsx would pull the whole
@@ -28735,6 +29021,12 @@ const rebuildWatcher = () => {
28735
29021
  if (fromIndex === index) {
28736
29022
  return;
28737
29023
  }
29024
+ if (navigationAnimated) {
29025
+ // The movement on screen was turned round for this very navigation (see
29026
+ // turnRunningTransitionRound): it is already being answered, by the
29027
+ // pictures the relations would otherwise photograph over.
29028
+ return;
29029
+ }
28738
29030
  // A page in no relation at all is a real end of the crossing, not a
28739
29031
  // missing one: it is the "anywhere" a page reached from the furniture is
28740
29032
  // opened over and closed back onto (see findRelation).
@@ -28761,6 +29053,7 @@ const rebuildWatcher = () => {
28761
29053
  beginTransition({
28762
29054
  page: toPage,
28763
29055
  url: navigationUrl,
29056
+ fromUrl: navigationFromUrl,
28764
29057
  // Which way it plays: what the navigation itself said first — the link
28765
29058
  // being pressed is where the way the app is being walked is known — then
28766
29059
  // the relation, and forward for a navigation that asked for a movement
@@ -28787,6 +29080,12 @@ const rebuildWatcher = () => {
28787
29080
  let defaultTransition = null;
28788
29081
  let navigationRequest = null;
28789
29082
  let navigationUrl = null;
29083
+ // Where the document stands as the navigation begins — the other end of the
29084
+ // crossing, and what lets the NEXT navigation be recognised as this one's way
29085
+ // back. Read off the document url rather than off window.location: a traversal
29086
+ // on a browser without the Navigation API is announced from "popstate", where
29087
+ // the address has already moved and location would answer with the destination.
29088
+ let navigationFromUrl = null;
28790
29089
  let navigationAnimated = false;
28791
29090
 
28792
29091
  // The two ends of every navigation, watched from here on. The picture of the
@@ -28799,18 +29098,28 @@ observeBeforeRouting(details => {
28799
29098
  navigationAnimated = false;
28800
29099
  navigationRequest = readNavigationRequest(details);
28801
29100
  navigationUrl = details.url;
29101
+ navigationFromUrl = documentUrlSignal.peek();
28802
29102
  if (relations.length === 0 && !defaultTransition && !navigationRequest) {
28803
29103
  return;
28804
29104
  }
28805
29105
  holdRenderingForRouting();
29106
+ // Said HERE, before the relations have their say: what is on screen may be
29107
+ // this navigation's own way back, and that is a fact about the two ends of
29108
+ // the crossing alone. A pair written "none" would silence a movement — it
29109
+ // must not silence the undoing of one already playing.
29110
+ if (turnRunningTransitionRound(navigationFromUrl, navigationUrl)) {
29111
+ navigationAnimated = true;
29112
+ }
28806
29113
  });
28807
29114
  observeAfterRouting(() => {
28808
29115
  const request = navigationRequest;
28809
29116
  const url = navigationUrl;
29117
+ const fromUrl = navigationFromUrl;
28810
29118
  // Read here and dropped here: a request answers for the navigation it was
28811
29119
  // made on, and the next one is back to the relations.
28812
29120
  navigationRequest = null;
28813
29121
  navigationUrl = null;
29122
+ navigationFromUrl = null;
28814
29123
  if (!navigationAnimated && (request || defaultTransition)) {
28815
29124
  const {
28816
29125
  type,
@@ -28820,6 +29129,7 @@ observeAfterRouting(() => {
28820
29129
  beginTransition({
28821
29130
  page: null,
28822
29131
  url,
29132
+ fromUrl,
28823
29133
  // A default has no direction: nothing says which of two arbitrary
28824
29134
  // pages is before the other, and the attribute is then worn empty —
28825
29135
  // present for whoever keys on "one of ours is playing", silent on the
@@ -28903,6 +29213,7 @@ let currentTransition = null;
28903
29213
  const beginTransition = ({
28904
29214
  page,
28905
29215
  url,
29216
+ fromUrl,
28906
29217
  direction,
28907
29218
  type,
28908
29219
  duration
@@ -28917,7 +29228,15 @@ const beginTransition = ({
28917
29228
  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
29229
  return;
28919
29230
  }
28920
- const transition = {};
29231
+ // The two ends of the crossing, kept for the length of the movement: they are
29232
+ // what lets the navigation after this one be recognised as its way back (see
29233
+ // turnRunningTransitionRound).
29234
+ const transition = {
29235
+ fromUrl: absoluteUrl(fromUrl),
29236
+ url: absoluteUrl(url),
29237
+ walkHome: null,
29238
+ releaseReverting: null
29239
+ };
28921
29240
  currentTransition = transition;
28922
29241
  // Said before the picture is taken: whoever names something for a movement
28923
29242
  // between two pages decides on it now (see transition_destination.js).
@@ -28934,6 +29253,10 @@ const beginTransition = ({
28934
29253
  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
29254
  }
28936
29255
  const areaElement = areaElements.length > 0 ? areaElements[0] : null;
29256
+ // Everything about to be photographed goes deaf to the pointer; the controls
29257
+ // that asked to keep answering are heard at the document instead (see
29258
+ // transition_press.js).
29259
+ holdTransitionPress(transition);
28937
29260
  // The area as it stands before anything moves, and the band the furniture
28938
29261
  // around it leaves free: rendering is held, so both are still the page being
28939
29262
  // left (see transition_window.js).
@@ -29027,6 +29350,13 @@ const beginTransition = ({
29027
29350
  // ending late must not strip what a later one is wearing.
29028
29351
  renderWait.stop();
29029
29352
  releaseRendering();
29353
+ // The hold a way back took, when it is still standing: the pictures were
29354
+ // turned round a second time and played out, or something else took the
29355
+ // document over mid-walk. A hold nobody gives back freezes the page.
29356
+ if (transition.releaseReverting) {
29357
+ transition.releaseReverting();
29358
+ transition.releaseReverting = null;
29359
+ }
29030
29360
  if (currentTransition === transition) {
29031
29361
  currentTransition = null;
29032
29362
  documentElement.removeAttribute(TRANSITION_ATTRIBUTE);
@@ -29035,15 +29365,99 @@ const beginTransition = ({
29035
29365
  releaseTransitionWindow(transition);
29036
29366
  releaseTransitionDestination(transition);
29037
29367
  releaseTransitionFurniture(transition);
29368
+ releaseTransitionPress(transition);
29038
29369
  if (restoreDuration) {
29039
29370
  restoreDuration();
29040
29371
  }
29041
29372
  }
29042
29373
  };
29374
+ transition.viewTransition = viewTransition;
29043
29375
  viewTransition.ready.then(viewTransitionReady, ignoreSkipped$1);
29044
29376
  viewTransition.finished.then(end, end);
29045
29377
  };
29046
29378
 
29379
+ /**
29380
+ * The movement playing, turned round rather than replaced: this navigation is
29381
+ * exactly the way back of the one on screen — the door pressed again while the
29382
+ * page it opened is still arriving.
29383
+ *
29384
+ * A second transition cannot do this. It photographs the state it leaves, which
29385
+ * here is the state being undone, and animates from there to the one the reader
29386
+ * came from: a way forward to somewhere, not a return. It also skips the one
29387
+ * playing, so the page snaps fully open before it slides back off — the
29388
+ * teleport a toggle must never show. The same pictures are walked back instead,
29389
+ * and the state is put under them before they are dropped (see
29390
+ * view_transition_revert.js).
29391
+ *
29392
+ * Only the exact way back qualifies. Anything else — a third page, a bar entry
29393
+ * — is a different crossing, and gets a movement of its own.
29394
+ */
29395
+ const turnRunningTransitionRound = (fromUrl, url) => {
29396
+ const running = currentTransition;
29397
+ if (!running || !running.viewTransition || !fromUrl || !url) {
29398
+ return false;
29399
+ }
29400
+ const from = absoluteUrl(fromUrl);
29401
+ const to = absoluteUrl(url);
29402
+ const isWayBack = !running.walkHome && from === running.url && to === running.fromUrl;
29403
+ // The door pressed a third time, while the way back is still being walked:
29404
+ // the pictures turn round once more, where the last press left them, rather
29405
+ // than a fresh movement starting over them.
29406
+ const isWayInAgain = running.walkHome && from === running.fromUrl && to === running.url;
29407
+ if (!isWayBack && !isWayInAgain) {
29408
+ return false;
29409
+ }
29410
+ const animations = viewTransitionAnimations();
29411
+ if (isWayInAgain) {
29412
+ // The token is dropped first: the walk it stands for is the one that must
29413
+ // not arrive anywhere anymore, and its promise is still pending.
29414
+ running.walkHome = null;
29415
+ walkPicturesOn(animations, LEAD_NAMES);
29416
+ // The pages stay held. The pictures are going to the state they were going
29417
+ // to all along, which is the one the DOM has held since the way back began
29418
+ // — what must not land under them is the render the way back queued. The
29419
+ // hold is given back when the movement ends (see end).
29420
+ return true;
29421
+ }
29422
+ if (!walkPicturesHome(animations, LEAD_NAMES)) {
29423
+ // The pictures do not exist yet: the movement was asked for a frame ago and
29424
+ // the browser has not taken them. There is nothing to turn round, and this
29425
+ // navigation gets a movement of its own — which skips the one that never
29426
+ // played, and nothing was on screen to teleport.
29427
+ return false;
29428
+ }
29429
+ // Which walk home this is, so the one that arrives is the one still wanted: a
29430
+ // walk turned round mid-way leaves a promise nobody cancelled, and it settles
29431
+ // when the pictures reach the far end.
29432
+ const walkHome = {};
29433
+ running.walkHome = walkHome;
29434
+ // The pages are held where they are until the pictures are home. The picture
29435
+ // of the state ARRIVING is live — it is the element being drawn, not a
29436
+ // photograph of it — so a page put back under it while it is still on screen
29437
+ // shows on BOTH sides of the movement, and the way back is watched happening
29438
+ // to nothing.
29439
+ const releaseRendering = takeoverRoutingRenderingHold();
29440
+ running.releaseReverting = releaseRendering;
29441
+ whenPicturesArrived(animations).then(() => {
29442
+ if (currentTransition !== running || running.walkHome !== walkHome) {
29443
+ // Turned round again on the way home, or replaced by another movement
29444
+ // altogether: whoever did that owns the pictures now.
29445
+ return;
29446
+ }
29447
+ running.walkHome = null;
29448
+ running.releaseReverting = null;
29449
+ // The state goes back UNDER the pictures before they are dropped, so the
29450
+ // two are the same thing at the instant they are swapped.
29451
+ releaseRendering();
29452
+ running.viewTransition.skipTransition();
29453
+ });
29454
+ return true;
29455
+ };
29456
+
29457
+ // Null rather than a url built out of nothing: two crossings that both failed
29458
+ // to say where they went must not look like each other's way back.
29459
+ const absoluteUrl = url => url ? new URL(url, window.location.href).href : null;
29460
+
29047
29461
  // A transition skipped by another one starting is an outcome, not a failure.
29048
29462
  const ignoreSkipped$1 = () => {};
29049
29463
 
@@ -29271,6 +29685,12 @@ installImportMetaCssBuild(import.meta);/**
29271
29685
  * was to where it is, on the same clock as the pages. That is why the tab row
29272
29686
  * can stay where it is, outside this box, and still move with the finger.
29273
29687
  */
29688
+ const TRAVEL_LEAD_NAMES = ["navi-route-travel"];
29689
+
29690
+ // A browser with no view transitions of its own has no picture of the page
29691
+ // being left, so there is nothing to drag: the gesture is still read (it is how
29692
+ // one asks to change tab with a thumb), and the change happens on release, as a
29693
+ // press on the tab would have done.
29274
29694
  const CAN_KEEP_PICTURE = Boolean(document.startViewTransition && !document.startViewTransition.isPolyfill);
29275
29695
  const startViewTransition = ensureDocumentStartViewTransition();
29276
29696
 
@@ -29901,34 +30321,9 @@ const RouteTravel = ({
29901
30321
  // may go on rendering, and a tab row beside the box keeps answering.
29902
30322
  const releaseRendering = freezeRouteRender();
29903
30323
  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
- }
30324
+ walkPicturesHome(animations, TRAVEL_LEAD_NAMES);
29925
30325
  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 () => {
30326
+ whenPicturesArrived(animations).then(async () => {
29932
30327
  try {
29933
30328
  pageAskedForRef.current = travel.fromPage;
29934
30329
  // The page that was left is put back UNDER the picture before the
@@ -30463,13 +30858,7 @@ const travelAnimations = travel => {
30463
30858
  if (travel.animations) {
30464
30859
  return travel.animations;
30465
30860
  }
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
- }
30861
+ const animations = viewTransitionAnimations();
30473
30862
  if (animations.length) {
30474
30863
  travel.animations = animations;
30475
30864
  }
@@ -30481,12 +30870,7 @@ const travelAnimations = travel => {
30481
30870
  // reaching for it must find it where it IS.
30482
30871
  const ratioOfTravel = travel => {
30483
30872
  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];
30873
+ const animation = findLeadAnimation(animations, TRAVEL_LEAD_NAMES) || animations[0];
30490
30874
  if (!animation) {
30491
30875
  return travel.ratio;
30492
30876
  }
@@ -30498,52 +30882,6 @@ const ratioOfTravel = travel => {
30498
30882
  return animation.currentTime / duration;
30499
30883
  };
30500
30884
 
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
30885
  // Where the two pictures stand, said as a moment in the movement they would
30548
30886
  // have played on their own: the browser knows how they move (it is written in
30549
30887
  // CSS), so the finger only has to say how far in. They are held still by CSS
@@ -40700,6 +41038,17 @@ Object.assign(PSEUDO_CLASSES, {
40700
41038
  * (`navi_value`); defaults to `href`.
40701
41039
  * @param {boolean} [props.current] - Forces the "current" state on (otherwise
40702
41040
  * derived from the href/route).
41041
+ * @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentExcept] -
41042
+ * Route(s) inside this link's own that are NOT it: while one of them matches,
41043
+ * the link is not current even though its route still is. A bar entry
41044
+ * standing for a whole section, and one place under that section the entry
41045
+ * does not stand for — settings reached from everywhere and shown over
41046
+ * whatever the reader was on. Nothing about the routing changes: the url is
41047
+ * in the section, only the link stops claiming "you are here".
41048
+ * @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentAlso] -
41049
+ * Route(s) other than this link's own that are it: the link is current while
41050
+ * one of them matches. The other side of `currentExcept` — an entry standing
41051
+ * for more than the one page it opens. `currentExcept` wins over it.
40703
41052
  * @param {"text"|"icon"|"tab"} [props.variant] - Visual variant
40704
41053
  * (`data-variant`); `"text"`/`"icon"` drop the link color/underline,
40705
41054
  * `"tab"` renders a tab-like affordance.
@@ -40743,6 +41092,16 @@ Object.assign(PSEUDO_CLASSES, {
40743
41092
  * the pair's movement and only turns it round, which is what the rare way
40744
41093
  * round a pair usually needs. Said nowhere else, the relations answer as
40745
41094
  * they always do.
41095
+ * @param {boolean} [props.pressableDuringRouteTransition] - Keep answering
41096
+ * presses while a route transition plays. Everything a movement photographs
41097
+ * goes deaf to the pointer for its whole length — a captured element is not
41098
+ * painted where it stands, so nothing hit-tests to it — which is right for
41099
+ * the pages and wrong for the door that opened them: a control in a bar the
41100
+ * two states share is standing exactly where its picture is drawn, and a
41101
+ * press on it is aimed at what the reader can see. Say it there, and only
41102
+ * there: a control that TRAVELS with the pages has a rectangle where it will
41103
+ * be, not where it is seen, and a press handed to it fires something nobody
41104
+ * aimed at.
40746
41105
  * @param {boolean} [props.replace] - Go to the destination by TAKING THE PLACE
40747
41106
  * of the current history entry instead of stacking onto it: the link stays a
40748
41107
  * link (an address, a middle click, the keyboard, `aria-current`), only the
@@ -40781,6 +41140,8 @@ const LinkWithRoute = ({
40781
41140
  route,
40782
41141
  routeParams,
40783
41142
  current,
41143
+ currentExcept,
41144
+ currentAlso,
40784
41145
  children,
40785
41146
  ...rest
40786
41147
  }) => {
@@ -40789,7 +41150,11 @@ const LinkWithRoute = ({
40789
41150
  matching
40790
41151
  } = useRouteStatus(route);
40791
41152
  const paramsAreMatching = route.matchesParams(routeParams);
40792
- const linkMatching = matching && paramsAreMatching;
41153
+ const someExceptedRouteMatching = useSomeRouteMatching(currentExcept);
41154
+ const someAlsoRouteMatching = useSomeRouteMatching(currentAlso);
41155
+ // "Except" is a veto: a route named there is somewhere else, whatever the
41156
+ // rest of the reading says.
41157
+ const linkMatching = !someExceptedRouteMatching && (matching && paramsAreMatching || someAlsoRouteMatching);
40793
41158
  const innerCurrent = current || linkMatching;
40794
41159
  return jsx(Link, {
40795
41160
  href: url,
@@ -40798,6 +41163,22 @@ const LinkWithRoute = ({
40798
41163
  children: children || route.buildRelativeUrl(routeParams)
40799
41164
  });
40800
41165
  };
41166
+ // A route, or a list of them, read as one answer: does any of them match.
41167
+ // Reading matchingSignal during the render is what subscribes the component to
41168
+ // it, so every route is read even once one has answered yes — stopping early
41169
+ // would leave the component deaf to the ones it skipped.
41170
+ const useSomeRouteMatching = routes => {
41171
+ if (!routes) {
41172
+ return false;
41173
+ }
41174
+ let someMatching = false;
41175
+ for (const route of Array.isArray(routes) ? routes : [routes]) {
41176
+ if (route.matchingSignal.value) {
41177
+ someMatching = true;
41178
+ }
41179
+ }
41180
+ return someMatching;
41181
+ };
40801
41182
  const LinkPlain = props => {
40802
41183
  const defaultRef = useRef();
40803
41184
  props.ref = props.ref || defaultRef;
@@ -40821,6 +41202,7 @@ const LinkPlain = props => {
40821
41202
  revealOnInteraction = false,
40822
41203
  hrefFallback = !anchor,
40823
41204
  routeTransition,
41205
+ pressableDuringRouteTransition,
40824
41206
  replace,
40825
41207
  children
40826
41208
  } = props;
@@ -40981,6 +41363,8 @@ const LinkPlain = props => {
40981
41363
  revealOnInteraction: undefined,
40982
41364
  variant: undefined,
40983
41365
  current: undefined,
41366
+ currentExcept: undefined,
41367
+ currentAlso: undefined,
40984
41368
  currentIndicator: undefined,
40985
41369
  currentEffectBold: undefined,
40986
41370
  currentEffectShadow: undefined,
@@ -40990,8 +41374,12 @@ const LinkPlain = props => {
40990
41374
  endIcon: undefined,
40991
41375
  hrefFallback: undefined,
40992
41376
  routeTransition: undefined,
41377
+ pressableDuringRouteTransition: undefined,
40993
41378
  replace: undefined,
40994
41379
  "data-navi-route-transition-request": routeTransitionRequest,
41380
+ ...(pressableDuringRouteTransition ? {
41381
+ [PRESSABLE_ATTRIBUTE]: ""
41382
+ } : null),
40995
41383
  ...replaceRequest,
40996
41384
  // The control's own handlers first — the interaction gate, the caller's
40997
41385
  // onClick/onKeyDown, the command and the action — then what only a link
@@ -41032,8 +41420,14 @@ const LinkPlain = props => {
41032
41420
  commandfor: slide ? nav?.slideContainer : undefined,
41033
41421
  "aria-controls": slide ? nav?.slideContainer : undefined,
41034
41422
  tabIndex: slide ? props.tabIndex ?? 0 : props.tabIndex,
41035
- role: slide ? "tab" : props.role,
41036
- "aria-current": isCurrent ? "page" : undefined,
41423
+ role: slide ? "tab" : props.role
41424
+ // The same reading the paint uses: what the link claims about being
41425
+ // where the reader is has to be one claim, said to the eye and to a
41426
+ // screen reader alike. A slide is not a page — that tab says it with
41427
+ // aria-selected below.
41428
+ ,
41429
+
41430
+ "aria-current": !slide && innerCurrent ? "page" : undefined,
41037
41431
  "aria-selected": slide ? innerCurrent : selectionContext ? selected : undefined,
41038
41432
  "data-value-event": "navi_value",
41039
41433
  onnavi_value: e => {
@@ -44373,6 +44767,58 @@ const resolveNavStateProp = (navState, popupId, name) => {
44373
44767
  return { id: navState.id || popupId, type: navState.type || "replace" };
44374
44768
  };
44375
44769
 
44770
+ // What a write of the open state is worth in the history, when the signal
44771
+ // holding it is bound to a url (a route's `searchParams`, see route.js). A
44772
+ // plain signal has no `set` and takes the value as it always did.
44773
+ const writeInSignal = (signal, value, { history }) => {
44774
+ if (signal.set) {
44775
+ signal.set(value, { history });
44776
+ return;
44777
+ }
44778
+ signal.value = value;
44779
+ };
44780
+
44781
+ // The popup says where it is, into a `signal` the caller holds — and when that
44782
+ // signal lives in a url, saying so is a navigation. It is worth exactly what
44783
+ // the same move is worth when the open state lives in the history entry
44784
+ // instead (see useNavState's own leave()):
44785
+ // - the opening is worth what the state declares (`history: "push"` for a
44786
+ // popup one can back out of, the default replacement for one that merely
44787
+ // qualifies the screen one is on);
44788
+ // - the closing is never an entry of its own. Stacking one would leave the
44789
+ // entry that carries the popup BEHIND the reader, and their next back press
44790
+ // would walk straight back into the popup they just closed.
44791
+ // A cancel (Escape, the backdrop, --navi-cancel) goes back to before the
44792
+ // opening rather than rewriting the entry, so everything else written to the
44793
+ // url while the popup was open goes back with it. A close that is not a cancel
44794
+ // keeps those writes and only drops the popup.
44795
+ const writeOpenedInSignal = (signal, opened, event) => {
44796
+ if (signal.peek() === opened) {
44797
+ // The signal already says so, meaning this open/close IS what it asked
44798
+ // for: a back press that took the popup out of the url, the application
44799
+ // writing it. Nothing to write back — and nothing to go back to either,
44800
+ // since the navigation navBack would undo is the one that asked for this.
44801
+ return;
44802
+ }
44803
+ if (opened) {
44804
+ signal.value = true;
44805
+ return;
44806
+ }
44807
+ if (
44808
+ event?.detail?.isCancel &&
44809
+ signal.options?.getHistory?.() === "push" &&
44810
+ // Nothing of this document behind: the popup was opened by the url itself
44811
+ // (a shared link, a bookmark). navBack would do nothing at all there, so
44812
+ // the entry is rewritten instead — the address must not keep saying open
44813
+ // about a popup that just closed.
44814
+ canNavBackSignal.peek()
44815
+ ) {
44816
+ navBack();
44817
+ return;
44818
+ }
44819
+ writeInSignal(signal, false, { history: "replace" });
44820
+ };
44821
+
44376
44822
  /**
44377
44823
  * Keeps an open controller in sync with where the caller says the popup should
44378
44824
  * be: an `open`/`defaultOpen` pair, a `signal`, or a `navState` — the open
@@ -44431,7 +44877,7 @@ const useOpenPropsEffectOnOpenController = (
44431
44877
  }
44432
44878
  }
44433
44879
  if (signal) {
44434
- signal.value = opened;
44880
+ writeOpenedInSignal(signal, opened, event);
44435
44881
  }
44436
44882
  }
44437
44883
  : null;
@@ -44490,8 +44936,10 @@ const useOpenPropsEffectOnOpenController = (
44490
44936
  // The request can be refused (a busy form denying the close): the popup
44491
44937
  // then stays where it was, and whoever holds the open state is told so —
44492
44938
  // otherwise it would keep saying "closed" about a popup still open.
44939
+ // Written over rather than stacked on: a refusal corrects the state that
44940
+ // asked, it is not a place one came from.
44493
44941
  if (signal) {
44494
- signal.value = openController.opened;
44942
+ writeInSignal(signal, openController.opened, { history: "replace" });
44495
44943
  }
44496
44944
  if (navStateId && openController.opened) {
44497
44945
  enterNavState();
@@ -46218,6 +46666,7 @@ const ButtonUI = props => {
46218
46666
  target,
46219
46667
  rel,
46220
46668
  replace,
46669
+ pressableDuringRouteTransition,
46221
46670
  // visual
46222
46671
  variant,
46223
46672
  pressEffect,
@@ -46268,6 +46717,12 @@ const ButtonUI = props => {
46268
46717
  const replaceRequest = replace ? {
46269
46718
  [LINK_REPLACE_ATTRIBUTE]: ""
46270
46719
  } : null;
46720
+
46721
+ // Worn as an attribute too, and read at the document by whoever catches the
46722
+ // press a movement would have swallowed (see transition_press.js).
46723
+ const pressableRequest = pressableDuringRouteTransition ? {
46724
+ [PRESSABLE_ATTRIBUTE]: ""
46725
+ } : null;
46271
46726
  const visualSelector = ".navi_button_content";
46272
46727
  useAccentColorAttributes(ref, null, {
46273
46728
  elementSelector: visualSelector
@@ -46296,6 +46751,8 @@ const ButtonUI = props => {
46296
46751
  rel: innerRel,
46297
46752
  replace: undefined,
46298
46753
  ...replaceRequest,
46754
+ pressableDuringRouteTransition: undefined,
46755
+ ...pressableRequest,
46299
46756
  // Respond with the JS prop value directly so callers (e.g. resolveCommandValue)
46300
46757
  // get the original type instead of the DOM-coerced string (e.g. "[object Object]").
46301
46758
  onnavi_get_value: e => {
@@ -46519,6 +46976,13 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
46519
46976
  * `--navi-nav-to` command — by TAKING THE PLACE of the current history entry
46520
46977
  * rather than stacking on it: what `<Link replace>` says, for a press drawn
46521
46978
  * as a button.
46979
+ * @param {boolean} [pressableDuringRouteTransition] Keep answering presses
46980
+ * while a route transition plays: what a movement photographs goes deaf to
46981
+ * the pointer for its whole length, and the door that opened the page — a
46982
+ * button in a bar the two states share, standing exactly where its picture is
46983
+ * drawn — must still close it. What `<Link pressableDuringRouteTransition>`
46984
+ * says, for the other half of a toggle. Only for a control that does not
46985
+ * travel with the pages.
46522
46986
  * @param {Function} [action] On a button with an `href` or a `route`, the
46523
46987
  * same order as a Link's: it runs on the press, before the navigation, and
46524
46988
  * the navigation does not wait for it (see Link's `action`).
@@ -80760,6 +81224,14 @@ const css = /* css */`.navi_side_panel {
80760
81224
  * required at all for a purely command-driven panel (an `id` plus a
80761
81225
  * `<Button command="--navi-toggle" commandFor={id}>` elsewhere, same as
80762
81226
  * `Dialog`/`Popover` themselves — see either's own doc).
81227
+ * @param {boolean|string|{id?: string, type?: "push"|"replace"}} [props.navState] -
81228
+ * Keeps the open state in the history entry, so a screen left and come back
81229
+ * to finds its panel as it was: `true` uses the panel's own `id`, a string
81230
+ * names the key, `{ id, type }` chooses between rewriting the entry
81231
+ * ("replace", the default) and pushing one ("push"), where closing goes
81232
+ * back — which is what makes the phone's back gesture close the panel.
81233
+ * Forwarded as-is to `Popup`; source of truth, so it excludes `open` and
81234
+ * `signal` (see `Dialog`/`Popover`'s own `navState`).
80763
81235
  * @param {(event: Event) => void} [props.onClose] - Called when the panel
80764
81236
  * actually closes (see `Dialog`/`Popover`'s own `onClose`).
80765
81237
  * @param {"left"|"right"|"top"|"bottom"} [props.side="right"] - Which
@@ -80841,8 +81313,16 @@ const SidePanel = ({
80841
81313
  import.meta.css = [css, "@jsenv/navi/src/layout/side_panel.jsx"];
80842
81314
  const onSwipePointerDown = swipeToClose ? createSwipeToClose(side) : null;
80843
81315
  return jsx(Popup, {
80844
- mode: mode,
80845
- open: open,
81316
+ mode: mode
81317
+ // Spread rather than written: the collision warning between `signal` and
81318
+ // `open` asks whether the prop is THERE, not what it holds, so a panel
81319
+ // handed a signal and nothing else would be told off for an `open` only
81320
+ // this line ever put on it.
81321
+ ,
81322
+
81323
+ ...(open === undefined ? null : {
81324
+ open
81325
+ }),
80846
81326
  signal: signal,
80847
81327
  defaultOpen: defaultOpen,
80848
81328
  onClose: onClose,