@jsenv/navi 0.29.132 → 0.29.133

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.
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
6
  export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
- import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
10
10
  import { isValidElement, h, Fragment, createContext, render, toChildArray, options, cloneElement } from "preact";
@@ -6081,10 +6081,11 @@ const css$11 = /* css */`
6081
6081
  right where it's set) — same reasoning as Popover's identical
6082
6082
  attribute (popover.jsx's own top comment has the full case). */
6083
6083
  position: fixed;
6084
- /* Popover resets */
6085
- inset: auto;
6086
- top: 0;
6087
- left: 0;
6084
+ /* Popover resets. Laid out at its containing block's own origin and moved
6085
+ from there by a translate (applyNewPosition), never by left/top — see
6086
+ applyNewPosition's own doc for why the placement may not go through
6087
+ left/top themselves. */
6088
+ inset: 0 auto auto 0;
6088
6089
  /* For some reason callout could end up behing elements when it's redisplayed in a dialog
6089
6090
  (behind button relatively positioned in dialog footer while callout is appended into dialog body)
6090
6091
  To ensure ti goes above we put a z-index: 1, I hope it won't bite use in the future */
@@ -6108,10 +6109,9 @@ const css$11 = /* css */`
6108
6109
  border: none;
6109
6110
  outline: none; /* programmatic focus may land here briefly before being redirected to close button */
6110
6111
  opacity: 0;
6111
- /* Positioned with plain left/top (applyNewPosition, visible_rect.js) —
6112
- left/top are NOT transitioned here, applyNewPosition drives that
6113
- itself via the Web Animations API instead of CSS, same mechanism
6114
- Popover/Dialog use. */
6112
+ /* opacity only: the placement lives in the translate property, which
6113
+ applyNewPosition (visible_rect.js) owns and animates itself through the
6114
+ Web Animations API, same mechanism Popover/Dialog use. */
6115
6115
  transition: opacity 0.2s ease-in-out;
6116
6116
  cursor: initial; /* Do not inherit element cursor, inside the element but should use regular cursor */
6117
6117
  pointer-events: auto; /* Must be interactive to be closabled (overrid list item pointer-events none for instance) */
@@ -25350,6 +25350,43 @@ const updateDocumentState = (value) => {
25350
25350
  documentStateSignal.value = value;
25351
25351
  };
25352
25352
 
25353
+ /**
25354
+ * What the history entry being written ends up holding.
25355
+ *
25356
+ * `state`:
25357
+ * - `undefined` — a neutral navigation: a link click, a `replaceUrl` writing a
25358
+ * search param. A **replace** stays on the entry the document is already on,
25359
+ * so what that entry holds stays with it. A **push** opens a NEW entry, and
25360
+ * what was written for the one being left — an open dialog, an expanded
25361
+ * picker (see `useNavState`) — describes that entry alone: carried forward it
25362
+ * would reopen on the next screen, and on the one after that, until something
25363
+ * mounting the same id opens out of nowhere.
25364
+ * - `null` — an explicit reset.
25365
+ * - an object — built by the caller (`enter()`/`leave()` copy the current state
25366
+ * themselves), taken as given.
25367
+ *
25368
+ * `sharedState` always wins: it describes the document, not the entry.
25369
+ *
25370
+ * The push/replace split is the Navigation API's own rule for a `navigate()`
25371
+ * carrying no state — which is why via_navigation.js gets it from the browser
25372
+ * and via_history.js has to spell it out to say the same thing.
25373
+ */
25374
+ const resolveEffectiveDocumentState = (
25375
+ state,
25376
+ { navigationType, currentState, sharedState },
25377
+ ) => {
25378
+ if (state === undefined) {
25379
+ if (navigationType === "push") {
25380
+ return sharedState;
25381
+ }
25382
+ return { ...(currentState || {}), ...sharedState };
25383
+ }
25384
+ if (state === null) {
25385
+ return sharedState;
25386
+ }
25387
+ return { ...state, ...sharedState };
25388
+ };
25389
+
25353
25390
  /**
25354
25391
  * The document's rendering, held for the one frame a view transition needs.
25355
25392
  *
@@ -25482,6 +25519,13 @@ const takeoverRoutingRenderingHold = () => {
25482
25519
  * something still loading. Its content is not there at the moment it is put
25483
25520
  * back, so a position beyond what has arrived is clamped as before. Only the
25484
25521
  * page knows when it is whole.
25522
+ *
25523
+ * WHEN a page is arrived at is not decided here either. A document navigation
25524
+ * lands where its kind says (see via_history.js), and one scrollport can be
25525
+ * shared by pages the browser is never told apart: a row of tabs replaces the
25526
+ * url under the same document, so the arrival — and the deafness the swap
25527
+ * needs, see suspendScrollRecording — is asked for by the row itself (see
25528
+ * route_travel.jsx).
25485
25529
  */
25486
25530
 
25487
25531
 
@@ -25519,6 +25563,29 @@ const storePositions = () => {
25519
25563
  }
25520
25564
  };
25521
25565
 
25566
+ // The document is one scrollport for every page put in it, so a page swapped
25567
+ // under it for a shorter one is an offset the browser CLAMPS — and a clamp is
25568
+ // a scroll event like any other. It is not the reader scrolling, and by the
25569
+ // time it fires the url is already the arriving page's: written down, it is
25570
+ // that page's own position that the page being left destroys.
25571
+ //
25572
+ // Only whoever swaps the page knows when that is happening, so the deafness is
25573
+ // asked for from there and lasts exactly as long as the swap. Counted rather
25574
+ // than flagged: two swaps overlap — a travel relaying into the next one under
25575
+ // the same finger, a travel being undone while it plays.
25576
+ let suspendCount = 0;
25577
+ const suspendScrollRecording = () => {
25578
+ suspendCount++;
25579
+ let resumed = false;
25580
+ return () => {
25581
+ if (resumed) {
25582
+ return;
25583
+ }
25584
+ resumed = true;
25585
+ suspendCount--;
25586
+ };
25587
+ };
25588
+
25522
25589
  let installed = false;
25523
25590
  const installScrollRestoration = () => {
25524
25591
  if (installed) {
@@ -25536,6 +25603,9 @@ const installScrollRestoration = () => {
25536
25603
  window.addEventListener(
25537
25604
  "scroll",
25538
25605
  () => {
25606
+ if (suspendCount) {
25607
+ return;
25608
+ }
25539
25609
  positionByUrl.set(window.location.href, {
25540
25610
  x: window.scrollX,
25541
25611
  y: window.scrollY,
@@ -25558,12 +25628,43 @@ const installScrollRestoration = () => {
25558
25628
 
25559
25629
  // Nothing to put back is not the same as putting back the top: a page arrived
25560
25630
  // at for the first time is startAtTop's business, and this must not step on it.
25631
+ // Whether there was anything, for a caller who has an answer of its own for the
25632
+ // page that has never been read.
25561
25633
  const restoreScrollPosition = (url) => {
25562
25634
  const position = positionByUrl.get(new URL(url, window.location.href).href);
25563
25635
  if (!position) {
25564
- return;
25636
+ return false;
25565
25637
  }
25566
25638
  scrollTo(position);
25639
+ return true;
25640
+ };
25641
+
25642
+ // A page one arrives at for the first time starts at its top. Only a document
25643
+ // navigation does that on its own: a pushState creates its entry with whatever
25644
+ // scroll happened to be there, so without this the page opens at the offset of
25645
+ // the one before it — and that borrowed offset is what is then remembered FOR
25646
+ // it, and handed back on the way forward.
25647
+ //
25648
+ // The document, because the document is the scrollport in the common case. An
25649
+ // app that scrolls an element of its own scrolls it itself.
25650
+ const startAtTop = (url) => {
25651
+ // A fragment names where to land, and the browser is the one that finds it.
25652
+ if (new URL(url, window.location.href).hash) {
25653
+ return;
25654
+ }
25655
+ window.scrollTo({ top: 0, left: 0, behavior: "instant" });
25656
+ };
25657
+
25658
+ // An arrival at a page whose scrollport is already showing another one: the
25659
+ // tabs of a row share the document, and the offset on it is whichever tab was
25660
+ // last read. Where this one was read, and its top when it never was — leaving
25661
+ // the offset alone would seat the reader wherever the neighbour happened to
25662
+ // be, so here "nothing recorded" and "stay" are not the same thing.
25663
+ const arriveAtScrollPosition = (url) => {
25664
+ if (restoreScrollPosition(url)) {
25665
+ return;
25666
+ }
25667
+ startAtTop(url);
25567
25668
  };
25568
25669
 
25569
25670
  const scrollTo = ({ x, y }) => {
@@ -25907,28 +26008,14 @@ const setupBrowserIntegrationViaHistory = ({
25907
26008
 
25908
26009
  if (navigationType === "push" || navigationType === "replace") {
25909
26010
  markUrlAsVisited(url);
25910
- // undefined inherit current state (link click, neutral navigation)
25911
- // null → explicit reset (no nav-state keys carried over)
25912
- // {...} → explicit state from enter()/leave(), already built from currentState
25913
- // When state is given it's responsability of the caller to ensure it inherits document state (or not, you want it 99% of the time)
25914
- let effectiveState;
25915
- const sharedState = {
25916
- jsenv_visited_urls: Array.from(visitedUrlSet),
25917
- [NAV_DEPTH_STATE_KEY]: getNavDepth(),
25918
- };
25919
- if (state === undefined) {
25920
- effectiveState = {
25921
- ...(getDocumentState() || {}),
25922
- ...sharedState,
25923
- };
25924
- } else if (state === null) {
25925
- effectiveState = sharedState;
25926
- } else if (state) {
25927
- effectiveState = {
25928
- ...state,
25929
- ...sharedState,
25930
- };
25931
- }
26011
+ const effectiveState = resolveEffectiveDocumentState(state, {
26012
+ navigationType,
26013
+ currentState: getDocumentState(),
26014
+ sharedState: {
26015
+ jsenv_visited_urls: Array.from(visitedUrlSet),
26016
+ [NAV_DEPTH_STATE_KEY]: getNavDepth(),
26017
+ },
26018
+ });
25932
26019
  if (navigationType === "push") {
25933
26020
  window.history.pushState(effectiveState, null, url);
25934
26021
  } else {
@@ -25977,12 +26064,21 @@ const setupBrowserIntegrationViaHistory = ({
25977
26064
  isVisited,
25978
26065
  state,
25979
26066
  });
26067
+ // Where the document lands, said by what kind of arrival this is. Both are
26068
+ // waited for, and for the same two reasons: the page has to be there to be
26069
+ // scrolled, and a picture taken before it would be of a page at its top
26070
+ // (see rendering_hold.js, which is where the waiting happens). After the
26071
+ // history has been written too, so the entry being left keeps the offset
26072
+ // it is at.
26073
+ //
26074
+ // A replace gets neither: it is the same place said differently — a param
26075
+ // settling, a state written — and moving the reader for it would throw
26076
+ // them out of a page they never left. The one replace that IS an arrival
26077
+ // is a row of tabs travelling, and the row says so for itself (see
26078
+ // route_travel.jsx).
25980
26079
  if (navigationType === "push") {
25981
26080
  whenRenderingResumes(() => startAtTop(url));
25982
26081
  } else if (navigationType === "traverse") {
25983
- // Where this entry was left. Waited for like the reset above, and for
25984
- // the same two reasons: the page has to be there to be scrolled, and a
25985
- // picture taken before it would be of a page at its top.
25986
26082
  whenRenderingResumes(() => restoreScrollPosition(url));
25987
26083
  }
25988
26084
  executeWithCleanup(
@@ -26177,39 +26273,6 @@ const setupBrowserIntegrationViaHistory = ({
26177
26273
  };
26178
26274
  };
26179
26275
 
26180
- // A page one arrives at for the first time starts at its top. Only a document
26181
- // navigation does that on its own: a pushState creates its entry with whatever
26182
- // scroll happened to be there, so without this the new page opens at the offset
26183
- // of the one before it — and worse, that borrowed offset is what the browser
26184
- // then remembers FOR that entry, and hands back on the way forward.
26185
- //
26186
- // Push only. A traverse is the browser's business and it is already right: it
26187
- // keeps a position per entry and restores it. A replace is not an arrival —
26188
- // it is the same place, said differently (a tab row travelling, see
26189
- // route_travel.jsx), and resetting there would throw the reader out of a page
26190
- // they never left.
26191
- //
26192
- // After the routes have been told, and after the picture of the page being
26193
- // left has been taken — that ordering is the whole subtlety. The routes
26194
- // changing is what sets a movement off, and a movement measures the box it is
26195
- // leaving as it stands; put the document back to its top any earlier and the
26196
- // picture is of a page at its first line, which the reader was not at. The
26197
- // browser paints what the new offset shows and nothing else, so what is kept
26198
- // of the page being left is the band it had already painted, and the movement
26199
- // carries a fragment (see rendering_hold.js, which is where the waiting
26200
- // happens). After pushState too, so the entry being left keeps the offset it
26201
- // is at.
26202
- //
26203
- // The document, because the document is the scrollport in the common case. An
26204
- // app that scrolls an element of its own scrolls it itself.
26205
- const startAtTop = (url) => {
26206
- // A fragment names where to land, and the browser is the one that finds it.
26207
- if (new URL(url, window.location.href).hash) {
26208
- return;
26209
- }
26210
- window.scrollTo({ top: 0, left: 0, behavior: "instant" });
26211
- };
26212
-
26213
26276
  let updateRoutes;
26214
26277
 
26215
26278
  const applyActions = (params) => {
@@ -26996,8 +27059,38 @@ const releaseTransitionDestination = (owner) => {
26996
27059
  * the page arriving is in the DOM and the transition has not started playing.
26997
27060
  * Everything DERIVED from these numbers — the band a fixed bar covers, how far
26998
27061
  * a page travels — is derived in CSS, so the application's own numbers (the
26999
- * room its bars give back, see layout/safe_area.js) take part in it.
27062
+ * room its bars give back, see layout/safe_area.js; what covers the box from
27063
+ * inside the document, --navi-transition-cover-* below) take part in it.
27064
+ */
27065
+
27066
+ /**
27067
+ * What covers the box from INSIDE the document: a sticky row of tabs above the
27068
+ * pages, a header pinned to the top of a scroller. Declared at zero and
27069
+ * written by whoever covers it, exactly as a fixed bar publishes the room it
27070
+ * gives back (layout/safe_area.js).
27071
+ *
27072
+ * A slot rather than a measurement, and a slot navi cannot fill itself: the
27073
+ * safe area answers for what is pinned to the WINDOW's edges, and a sticky row
27074
+ * is none of those — it lives in the document, below the bars, and covers the
27075
+ * top of the box exactly as a fixed bar covers the top of the screen. The
27076
+ * pictures of a transition are drawn in the top layer, where no z-index of the
27077
+ * document reaches them, so what the document paints over the box has to be
27078
+ * counted here or it is the pictures that paint over it.
27079
+ *
27080
+ * Each one is a distance inward from the band the safe area already leaves
27081
+ * free, so a row states its own height and nothing else: what the bars above
27082
+ * it take is already counted.
27000
27083
  */
27084
+ const TRANSITION_WINDOW_CSS = /* css */ `
27085
+ @layer navi {
27086
+ :root {
27087
+ --navi-transition-cover-top: 0px;
27088
+ --navi-transition-cover-right: 0px;
27089
+ --navi-transition-cover-bottom: 0px;
27090
+ --navi-transition-cover-left: 0px;
27091
+ }
27092
+ }
27093
+ `;
27001
27094
 
27002
27095
  const WINDOW_TOP_PROPERTY = "--navi-transition-window-top";
27003
27096
  const WINDOW_LEFT_PROPERTY = "--navi-transition-window-left";
@@ -27144,6 +27237,8 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
27144
27237
  // included).
27145
27238
 
27146
27239
  const css$10 = /* css */`
27240
+ ${TRANSITION_WINDOW_CSS}
27241
+
27147
27242
  /* The marked region is a picture of its own for the length of a transition of
27148
27243
  OURS, and only then — the name is what makes the pages a picture the
27149
27244
  movement below can carry.
@@ -27228,39 +27323,46 @@ const css$10 = /* css */`
27228
27323
  height: var(--navi-transition-window-height);
27229
27324
  animation-name: none;
27230
27325
 
27231
- /* Cut at the safe area, on top of being cut at the area's own box. The
27232
- pictures are drawn in the top layer, so they cover a fixed bar as
27233
- easily as anything else — and the area runs UNDER the bars by design:
27234
- that is what a fixed bar is for, and what the room it gives back is
27235
- for. An area taller than the screen therefore ends below the bottom
27236
- bar, and a scrolled one starts above the top bar, so the movement
27237
- would be watched painting over them for its whole length.
27238
-
27239
- The band left free is the app's own safe area (see
27240
- layout/safe_area.js) every kind of furniture at once, not the bars
27241
- alone, and read rather than asked for, so one that grows, shrinks or
27242
- unmounts mid-transition is followed without anything being told. What
27243
- the window cannot know is only where it itself stands, and that is the
27244
- measured half. */
27326
+ /* Cut at what covers the area, on top of being cut at the area's own
27327
+ box. The pictures are drawn in the top layer, so they cover a fixed bar
27328
+ as easily as anything else — and the area runs UNDER the bars by
27329
+ design: that is what a fixed bar is for, and what the room it gives
27330
+ back is for. An area taller than the screen therefore ends below the
27331
+ bottom bar, and a scrolled one starts above the top bar, so the
27332
+ movement would be watched painting over them for its whole length.
27333
+
27334
+ Two bands are left free, and they answer for two different things: the
27335
+ app's own safe area (layout/safe_area.js), everything pinned to the
27336
+ WINDOW's edges, and --navi-transition-cover-* (transition_window.js),
27337
+ what covers the area from inside the document — a sticky header above
27338
+ the pages covers the top of the area exactly as a fixed bar covers the
27339
+ top of the screen. Both are read rather than asked for, so one that
27340
+ grows, shrinks or unmounts mid-transition is followed without anything
27341
+ being told. What the window cannot know is only where it itself stands,
27342
+ and that is the measured half. */
27245
27343
  --navi-route-transition-clip-top: max(
27246
27344
  0px,
27247
- var(--navi-safe-area-inset-top) - var(--navi-transition-window-top)
27345
+ var(--navi-safe-area-inset-top) +
27346
+ var(--navi-transition-cover-top) - var(--navi-transition-window-top)
27248
27347
  );
27249
27348
  --navi-route-transition-clip-left: max(
27250
27349
  0px,
27251
- var(--navi-safe-area-inset-left) - var(--navi-transition-window-left)
27350
+ var(--navi-safe-area-inset-left) +
27351
+ var(--navi-transition-cover-left) - var(--navi-transition-window-left)
27252
27352
  );
27253
27353
  --navi-route-transition-clip-bottom: max(
27254
27354
  0px,
27255
27355
  var(--navi-transition-window-top) +
27256
27356
  var(--navi-transition-window-height) +
27257
- var(--navi-safe-area-inset-bottom) - 100dvh
27357
+ var(--navi-safe-area-inset-bottom) +
27358
+ var(--navi-transition-cover-bottom) - 100dvh
27258
27359
  );
27259
27360
  --navi-route-transition-clip-right: max(
27260
27361
  0px,
27261
27362
  var(--navi-transition-window-left) +
27262
27363
  var(--navi-transition-window-width) +
27263
- var(--navi-safe-area-inset-right) - 100dvw
27364
+ var(--navi-safe-area-inset-right) +
27365
+ var(--navi-transition-cover-right) - 100dvw
27264
27366
  );
27265
27367
  clip-path: inset(
27266
27368
  var(--navi-route-transition-clip-top)
@@ -28278,6 +28380,8 @@ const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
28278
28380
  // The name the box wears while it travels, and only then (see nameForTravel).
28279
28381
  const TRAVEL_NAME = "navi-route-travel";
28280
28382
  const css$$ = /* css */`
28383
+ ${TRANSITION_WINDOW_CSS}
28384
+
28281
28385
  /* The name that makes the page inside this box a picture of its own during a
28282
28386
  transition — rather than part of the one big picture the document takes, so
28283
28387
  the two pages can move past each other while everything else stays where it
@@ -28396,38 +28500,46 @@ const css$$ = /* css */`
28396
28500
  route to the next. */
28397
28501
  height: var(--navi-transition-window-height);
28398
28502
 
28399
- /* Cut at the safe area, on top of being cut at the box. The pictures are
28400
- drawn in the top layer, so they cover a fixed bar as easily as anything
28401
- else — and the box they travel in runs UNDER the bars by design: that
28402
- is what a fixed bar is for, and what the room it gives back is for. A
28403
- box scrolled by so much as a pixel therefore starts above the top bar
28404
- and ends below the bottom one, and the travel would be watched painting
28405
- over both for its whole length.
28406
-
28407
- The band left free is the app's own safe area (see layout/safe_area.js)
28408
- every kind of furniture at once, not the bars alone, and read rather
28409
- than asked for, so one that grows, shrinks or unmounts mid-travel is
28410
- followed without anything being told. What the group cannot know is
28411
- only where it itself stands, and that is the measured half. */
28503
+ /* Cut at what covers the box, on top of being cut at the box. The
28504
+ pictures are drawn in the top layer, so they cover a fixed bar as
28505
+ easily as anything else — and the box they travel in runs UNDER the
28506
+ bars by design: that is what a fixed bar is for, and what the room it
28507
+ gives back is for. A box scrolled by so much as a pixel therefore
28508
+ starts above the top bar and ends below the bottom one, and the travel
28509
+ would be watched painting over both for its whole length.
28510
+
28511
+ Two bands are left free, and they answer for two different things: the
28512
+ app's own safe area (layout/safe_area.js), everything pinned to the
28513
+ WINDOW's edges, and --navi-transition-cover-* (transition_window.js),
28514
+ what covers the box from inside the document a sticky row of tabs
28515
+ above the pages covers the top of the box exactly as a fixed bar covers
28516
+ the top of the screen. Both are read rather than asked for, so one that
28517
+ grows, shrinks or unmounts mid-travel is followed without anything
28518
+ being told. What the group cannot know is only where it itself stands,
28519
+ and that is the measured half. */
28412
28520
  --navi-route-travel-clip-top: max(
28413
28521
  0px,
28414
- var(--navi-safe-area-inset-top) - var(--navi-transition-window-top)
28522
+ var(--navi-safe-area-inset-top) +
28523
+ var(--navi-transition-cover-top) - var(--navi-transition-window-top)
28415
28524
  );
28416
28525
  --navi-route-travel-clip-left: max(
28417
28526
  0px,
28418
- var(--navi-safe-area-inset-left) - var(--navi-transition-window-left)
28527
+ var(--navi-safe-area-inset-left) +
28528
+ var(--navi-transition-cover-left) - var(--navi-transition-window-left)
28419
28529
  );
28420
28530
  --navi-route-travel-clip-bottom: max(
28421
28531
  0px,
28422
28532
  var(--navi-transition-window-top) +
28423
28533
  var(--navi-transition-window-height) +
28424
- var(--navi-safe-area-inset-bottom) - 100dvh
28534
+ var(--navi-safe-area-inset-bottom) +
28535
+ var(--navi-transition-cover-bottom) - 100dvh
28425
28536
  );
28426
28537
  --navi-route-travel-clip-right: max(
28427
28538
  0px,
28428
28539
  var(--navi-transition-window-left) +
28429
28540
  var(--navi-transition-window-width) +
28430
- var(--navi-safe-area-inset-right) - 100dvw
28541
+ var(--navi-safe-area-inset-right) +
28542
+ var(--navi-transition-cover-right) - 100dvw
28431
28543
  );
28432
28544
  clip-path: inset(
28433
28545
  var(--navi-route-travel-clip-top) var(--navi-route-travel-clip-right)
@@ -28736,6 +28848,19 @@ const RouteTravel = ({
28736
28848
  ended: false
28737
28849
  };
28738
28850
  travelRef.current = travel;
28851
+ // Whether the document's offset is this row's business at all (see
28852
+ // pagesScrollTheDocument), asked once and before anything is swapped.
28853
+ travel.scrollsDocument = pagesScrollTheDocument(elementRef.current);
28854
+ if (travel.scrollsDocument) {
28855
+ // One of the tabs is about to be swapped out from under that scrollport,
28856
+ // and whatever the browser does with the offset meanwhile — clamping it
28857
+ // to a page shorter than the trip — is not the reader scrolling. The url
28858
+ // it would be written against is already the arriving tab's, so recorded
28859
+ // it is that tab's own position that the tab being left destroys. Deaf
28860
+ // for the whole travel, which is exactly as long as the offset belongs
28861
+ // to nobody (see endTravel).
28862
+ travel.resumeScrollRecording = suspendScrollRecording();
28863
+ }
28739
28864
  // Taken before the picture is: the browser reads the name off the DOM as it
28740
28865
  // stands when the transition starts, and this box is only a picture of its
28741
28866
  // own for as long as it is the one travelling. Where it travels to is said
@@ -28778,8 +28903,24 @@ const RouteTravel = ({
28778
28903
  await change();
28779
28904
  }
28780
28905
  }, renderWait);
28906
+ // Where the arriving tab was left, or its top when it has never been
28907
+ // read. A travel is an arrival — the tab pressed is another route, and
28908
+ // the replace it navigates by is the only thing about it that says
28909
+ // otherwise — and this is the one place that knows it. Read from the
28910
+ // travel rather than from the page it set off for: a travel aimed
28911
+ // somewhere else while it waited lands where it is aimed now.
28912
+ if (travel.scrollsDocument) {
28913
+ const {
28914
+ route,
28915
+ params
28916
+ } = travel.page;
28917
+ arriveAtScrollPosition(route.buildUrl(params));
28918
+ }
28781
28919
  // The page arriving is in the DOM and the transition has not started
28782
- // playing: the one moment both boxes can be known.
28920
+ // playing: the one moment both boxes can be known. After the scroll,
28921
+ // which is what the box is measured through: the two states are at the
28922
+ // same place in the layout without being at the same place in the window
28923
+ // (see transition_window.js).
28783
28924
  holdTransitionWindow(travel, elementRef.current, rectBefore);
28784
28925
  });
28785
28926
  travel.viewTransition = viewTransition;
@@ -29000,6 +29141,25 @@ const RouteTravel = ({
29000
29141
  }
29001
29142
  travel.viewTransition.skipTransition();
29002
29143
  } finally {
29144
+ if (travel.scrollsDocument) {
29145
+ // The page coming back was read somewhere else, and the offset
29146
+ // currently on the document is the one the page that came in put
29147
+ // there. Given back once the page is really back — the render is
29148
+ // what makes the document tall enough to hold that offset again —
29149
+ // and the recording stays deaf until then, over the clamp the swap
29150
+ // back makes on the way. Its own deafness rather than the travel's:
29151
+ // the travel ends here, and this outlives it by a render.
29152
+ const resumeScrollRecording = suspendScrollRecording();
29153
+ const stopWatchingRender = observeRouteRender(() => {
29154
+ stopWatchingRender();
29155
+ const {
29156
+ route,
29157
+ params
29158
+ } = travel.fromPage;
29159
+ arriveAtScrollPosition(route.buildUrl(params));
29160
+ resumeScrollRecording();
29161
+ });
29162
+ }
29003
29163
  releaseRendering();
29004
29164
  // A travel ENDS, whatever happened on the way back: put the state back,
29005
29165
  // fail to drop the picture, be interrupted by something else — the one
@@ -29085,6 +29245,11 @@ const RouteTravel = ({
29085
29245
  travel.ended = true;
29086
29246
  travel.dropHold?.();
29087
29247
  travel.dropHold = null;
29248
+ // The offset on the scrollport belongs to a page again — the one that
29249
+ // arrived, or the one put back. A travel that never got as far as saying
29250
+ // which still has to give the recording back; the gesture's own
29251
+ // pseudo-travel never took it (see noPicture).
29252
+ travel.resumeScrollRecording?.();
29088
29253
  // Its own hold, always — whether or not this travel is still the current
29089
29254
  // one. Nobody else will lift it.
29090
29255
  releaseHold(travel);
@@ -29620,6 +29785,26 @@ const whilePageRenders = async (page, change, wait = armRouteRenderWait()) => {
29620
29785
  }
29621
29786
  };
29622
29787
 
29788
+ // Whether the document's offset belongs to the tabs. The pages of a row scroll
29789
+ // the document when nothing between the box and the viewport scrolls or clips:
29790
+ // the tab on screen is then what makes the document tall, and the offset on it
29791
+ // is that tab's — it has to be given back with the tab, and the browser's
29792
+ // clamping of it while pages are swapped has to be ignored.
29793
+ //
29794
+ // A box that lives inside a scroller of its own — a frame in an article, a
29795
+ // panel beside other content — shares nothing with the document: the offset
29796
+ // there is the surrounding page's, the reader never left it, and a travel has
29797
+ // no business moving it. Each of its pages brings its own scrollport, which
29798
+ // goes away with the page and has nothing to restore.
29799
+ const pagesScrollTheDocument = element => {
29800
+ const scrollContainer = getScrollContainer(element, {
29801
+ includeHidden: true
29802
+ });
29803
+ // html and body are one answer: whichever of them the walk stops on, what
29804
+ // scrolls is the viewport, which is what window.scrollTo moves.
29805
+ return scrollContainer === document.documentElement || scrollContainer === document.body;
29806
+ };
29807
+
29623
29808
  // A page of the row: a route, and the params that say which of its tabs when
29624
29809
  // several of them share it. Written as a bare route by a caller whose tabs are
29625
29810
  // routes of their own — which is the same page with nothing to tell apart.
@@ -53768,7 +53953,7 @@ const createOpenController = (
53768
53953
  // Last: the close effects above are what starts the exit transition the
53769
53954
  // content must outlive (see popup_content_mount.js).
53770
53955
  controller.unmountContent?.();
53771
- controller.onOpenedChange?.(false);
53956
+ controller.onOpenedChange?.(false, closeEvent);
53772
53957
  };
53773
53958
  const controller = {
53774
53959
  opened: false,
@@ -53788,9 +53973,10 @@ const createOpenController = (
53788
53973
  // away on close (`unmountWhenClosed`). Called from performClose above.
53789
53974
  unmountContent: null,
53790
53975
  // Told whenever `opened` actually changes, whatever asked for it — an
53791
- // interaction, a command, a prop. What lets a `signal` prop reflect the
53792
- // popup's real state (see useOpenPropsEffectOnOpenController), called once
53793
- // the open/close has fully happened rather than mid-sequence.
53976
+ // interaction, a command, a prop — with the event that asked. What lets a
53977
+ // `signal` prop reflect the popup's real state, and a `navState` prop write
53978
+ // it into the history entry (see useOpenPropsEffectOnOpenController);
53979
+ // called once the open/close has fully happened rather than mid-sequence.
53794
53980
  onOpenedChange: null,
53795
53981
  open: (e, detail) => {
53796
53982
  if (controller.opened || !controller.openEffect) {
@@ -53894,7 +54080,7 @@ const createOpenController = (
53894
54080
  openEffectReturnValue?.(closeEvent);
53895
54081
  };
53896
54082
  closeHandlers = openHandler(requestOpenEvent) || null;
53897
- controller.onOpenedChange?.(true);
54083
+ controller.onOpenedChange?.(true, requestOpenEvent);
53898
54084
  },
53899
54085
  requestClose: (
53900
54086
  e = new CustomEvent("programmatic", { detail: {} }),
@@ -54014,33 +54200,83 @@ const scheduleMountOpen = (run) => {
54014
54200
  });
54015
54201
  };
54016
54202
 
54203
+ // Where the popup's open state is kept, when it is kept anywhere: `navState`
54204
+ // resolved to the `{ id, type }` useNavState wants.
54205
+ //
54206
+ // `true` takes the popup's own id — a popup a `--navi-open` command can name is
54207
+ // a popup that already has a stable one, and that id is what identifies its
54208
+ // open state too.
54209
+ const NO_NAV_STATE = { id: undefined, type: "replace" };
54210
+ const resolveNavStateProp = (navState, popupId) => {
54211
+ if (!navState) {
54212
+ return NO_NAV_STATE;
54213
+ }
54214
+ if (navState === true) {
54215
+ return { id: popupId, type: "replace" };
54216
+ }
54217
+ if (typeof navState === "string") {
54218
+ return { id: navState, type: "replace" };
54219
+ }
54220
+ return { id: navState.id || popupId, type: navState.type || "replace" };
54221
+ };
54222
+
54017
54223
  /**
54018
- * Keeps an open controller in sync with a plain `open`/`defaultOpen` pair
54019
- * shared between `useOpenControllerByProps` below (Dialog/Popover driving
54020
- * their own controller) and `picker_custom.jsx` (which derives its own
54021
- * boolean from history state instead of a literal `open` prop, but needs
54022
- * the exact same skip-if-already-matching / open-or-requestClose control
54023
- * flow, via a small `{ open, requestClose, opened }` adapter around its own
54024
- * `requestOpen`/`requestClose` wrappers).
54224
+ * Keeps an open controller in sync with where the caller says the popup should
54225
+ * be: an `open`/`defaultOpen` pair, a `signal`, or a `navState` — the open
54226
+ * state written into the history entry, so a screen left and come back to finds
54227
+ * its popup as it was.
54228
+ *
54229
+ * Shared between `useOpenControllerByProps` below (Dialog/Popover driving their
54230
+ * own controller) and `picker_custom.jsx` (which owns its controller but wants
54231
+ * the same skip-if-already-matching / open-or-requestClose control flow).
54025
54232
  *
54026
54233
  * @param {{ open: (e: Event, detail?: object) => void, requestClose: (e: Event, detail?: object) => void, opened: boolean }} openController
54027
- * @param {{ open?: boolean|"interaction", defaultOpen?: boolean|"interaction", signal?: import("@preact/signals").Signal<boolean> }} props
54234
+ * @param {{ id?: string, open?: boolean|"interaction", defaultOpen?: boolean|"interaction", signal?: import("@preact/signals").Signal<boolean>, navState?: boolean|string|{id?: string, type?: "push"|"replace"} }} props
54028
54235
  */
54029
54236
  const useOpenPropsEffectOnOpenController = (openController, props) => {
54030
- const { signal, defaultOpen } = props;
54031
- // What the caller holds, however they hold it: an `open` they re-render
54032
- // themselves, or a `signal` this hook also writes (see onOpenedChange below).
54033
- // Reading .value during render is what subscribes the popup to it.
54034
- const open = signal ? signal.value : props.open;
54237
+ const { signal, defaultOpen, navState } = props;
54238
+ const { id: navStateId, type: navStateType } = resolveNavStateProp(
54239
+ navState,
54240
+ props.id,
54241
+ );
54242
+ // Called unconditionally (it answers with no-ops for an absent id), like
54243
+ // every other hook here.
54244
+ const [navStateValue, enterNavState, leaveNavState] = useNavState(
54245
+ navStateId,
54246
+ { type: navStateType },
54247
+ );
54248
+ // What the caller holds, however they hold it: the history entry when there
54249
+ // is a `navState`, an `open` they re-render themselves, or a `signal` this
54250
+ // hook also writes (see onOpenedChange below). Reading .value during render
54251
+ // is what subscribes the popup to a signal; reading the document state is
54252
+ // what subscribes it to the history entry, back button included.
54253
+ const open = navStateId
54254
+ ? Boolean(navStateValue)
54255
+ : signal
54256
+ ? signal.value
54257
+ : props.open;
54035
54258
  // Assigned on every render, like openEffect, so it always closes over the
54036
54259
  // latest prop: a popup that opens or closes on its own (Escape, backdrop, a
54037
- // --navi-close command) writes what happened into the signal, so whoever
54038
- // holds it always reads where the popup is.
54039
- openController.onOpenedChange = signal
54040
- ? (opened) => {
54041
- signal.value = opened;
54042
- }
54043
- : null;
54260
+ // --navi-close command) writes what happened where the caller keeps it, so
54261
+ // whoever holds it always reads where the popup is.
54262
+ openController.onOpenedChange =
54263
+ navStateId || signal
54264
+ ? (opened, event) => {
54265
+ if (navStateId) {
54266
+ if (opened) {
54267
+ enterNavState();
54268
+ } else {
54269
+ // Under type "push" a cancel goes back rather than rewriting the
54270
+ // entry, so everything else written to the url while the popup
54271
+ // was open goes back with it (see useNavState's own leave()).
54272
+ leaveNavState({ isBack: Boolean(event?.detail?.isCancel) });
54273
+ }
54274
+ }
54275
+ if (signal) {
54276
+ signal.value = opened;
54277
+ }
54278
+ }
54279
+ : null;
54044
54280
  // Tracks whether the effect below has ever run before — only the very
54045
54281
  // first run gets the "mount already open" treatment (`open` truthy from
54046
54282
  // the start, or the uncontrolled, mount-only `defaultOpen`); every
@@ -54093,12 +54329,15 @@ const useOpenPropsEffectOnOpenController = (openController, props) => {
54093
54329
  { isCancel: true },
54094
54330
  );
54095
54331
  }
54332
+ // The request can be refused (a busy form denying the close): the popup
54333
+ // then stays where it was, and whoever holds the open state is told so —
54334
+ // otherwise it would keep saying "closed" about a popup still open.
54096
54335
  if (signal) {
54097
- // The request can be refused (a busy form denying the close): the popup
54098
- // then stays where it was, and the signal is told so — otherwise it
54099
- // would keep saying "closed" about a popup still open.
54100
54336
  signal.value = openController.opened;
54101
54337
  }
54338
+ if (navStateId && openController.opened) {
54339
+ enterNavState();
54340
+ }
54102
54341
  }, [open]);
54103
54342
  };
54104
54343
 
@@ -54358,7 +54597,14 @@ const usePopupContentMount = (
54358
54597
  *
54359
54598
  * `animation="slide-from-*"` (anchorReference/point mode only): a real
54360
54599
  * translate-based entrance, 8 directions (cardinal + 4 diagonals), each
54361
- * 100%-of-own-size. Popover always resolves `animation="auto"`/`"sliding"`
54600
+ * 100%-of-own-size. It travels through `transform`, not through the
54601
+ * `translate` property, which belongs to the popup's own placement
54602
+ * (applyNewPosition in visible_rect.js — see its doc for why the placement is
54603
+ * a transform at all, and why it has to be the outermost one: the individual
54604
+ * transform properties apply translate, then rotate, then scale, then
54605
+ * `transform`, so both the travel here and the `scale` below compose *under* a
54606
+ * placement that stays where it was put).
54607
+ * Popover always resolves `animation="auto"`/`"sliding"`
54362
54608
  * to one of these concretely in JS (see popover.jsx's
54363
54609
  * `resolveDirectionValue`), so there's no bare `animation="sliding"`
54364
54610
  * selector here at all — a point/corner has no anchor edge to grow out of,
@@ -54399,14 +54645,15 @@ const popupCss = /* css */ `
54399
54645
 
54400
54646
  .navi_popover,
54401
54647
  .navi_dialog {
54402
- /* left/top are deliberately absent from this list — applyNewPosition
54403
- (visible_rect.js) drives that transition itself via the Web
54404
- Animations API instead of CSS, so it stays independent of whatever
54405
- this list contains (no shared transition-property to clobber, no
54406
- propertyName to filter). */
54648
+ /* The translate property is deliberately absent from this list — it
54649
+ carries where the popup stands, and applyNewPosition (visible_rect.js)
54650
+ owns it and drives its own transition through the Web Animations API
54651
+ instead of CSS, so it stays independent of whatever this list contains
54652
+ (no shared transition-property to clobber, no propertyName to filter).
54653
+ What moves here is transform, which composes under it. */
54407
54654
  &[navi-animation] {
54408
54655
  transition-property:
54409
- display, overlay, opacity, translate, scale, box-shadow;
54656
+ display, overlay, opacity, transform, scale, box-shadow;
54410
54657
  transition-duration:
54411
54658
  var(--popup-animation-duration), var(--popup-animation-duration),
54412
54659
  var(--popup-opacity-duration), var(--popup-translate-duration),
@@ -54435,7 +54682,7 @@ const popupCss = /* css */ `
54435
54682
  centered, no direction involved. */
54436
54683
  &[navi-animation="scaling"] {
54437
54684
  opacity: 1;
54438
- translate: 0 0;
54685
+ transform: translate(0px, 0px);
54439
54686
  scale: 1;
54440
54687
  &[aria-expanded="false"] {
54441
54688
  opacity: 0;
@@ -54487,14 +54734,16 @@ const popupCss = /* css */ `
54487
54734
  &[navi-animation="slide-from-bottom-left"],
54488
54735
  &[navi-animation="slide-from-bottom-right"] {
54489
54736
  opacity: 1;
54490
- translate: 0 0;
54737
+ transform: translate(0px, 0px);
54491
54738
 
54492
54739
  /* No fade: the travel is the whole effect. Fading it out on top would
54493
54740
  make the popup disappear before it has finished leaving, which reads as
54494
54741
  two things happening rather than one movement. */
54495
54742
  &[aria-expanded="false"] {
54496
- translate: calc(var(--x-popup-slide-x, 0) * 100%)
54497
- calc(var(--x-popup-slide-y, -1) * 100%);
54743
+ transform: translate(
54744
+ calc(var(--x-popup-slide-x, 0) * 100%),
54745
+ calc(var(--x-popup-slide-y, -1) * 100%)
54746
+ );
54498
54747
  }
54499
54748
  }
54500
54749
  }
@@ -54686,14 +54935,17 @@ const createSwipeToClose = (side, { grip } = {}) => {
54686
54935
  }
54687
54936
  }
54688
54937
 
54689
- // Where the panel stands, written on it directly: the gesture reports a
54690
- // distance in screen coordinates, which is exactly what a translate takes.
54938
+ // How far the panel has been pulled, written on it directly: the gesture
54939
+ // reports a distance in screen coordinates, which is exactly what a
54940
+ // translate takes. It goes through `transform` because the `translate`
54941
+ // property carries where the panel *stands* (applyNewPosition in
54942
+ // visible_rect.js, which owns it) — the pull composes under the placement
54943
+ // instead of replacing it.
54691
54944
  const paint = (distance) => {
54692
- panelEl.style.translate =
54693
- axis === "x" ? `${distance}px 0px` : `0px ${distance}px`;
54945
+ panelEl.style.transform = translateOf(axis, distance);
54694
54946
  };
54695
54947
  const restore = () => {
54696
- panelEl.style.translate = "";
54948
+ panelEl.style.transform = "";
54697
54949
  panelEl.style.transitionProperty = "";
54698
54950
  panelEl.style.userSelect = "";
54699
54951
  };
@@ -54705,8 +54957,8 @@ const createSwipeToClose = (side, { grip } = {}) => {
54705
54957
  paint(to);
54706
54958
  const animation = panelEl.animate(
54707
54959
  [
54708
- { translate: translateOf(axis, from) },
54709
- { translate: translateOf(axis, to) },
54960
+ { transform: translateOf(axis, from) },
54961
+ { transform: translateOf(axis, to) },
54710
54962
  ],
54711
54963
  {
54712
54964
  duration: (covered / sizeOf(panelEl, axis)) * TRAVEL_DURATION,
@@ -54776,7 +55028,9 @@ const sizeOf = (element, axis) => {
54776
55028
  return axis === "x" ? rect.width : rect.height;
54777
55029
  };
54778
55030
  const translateOf = (axis, distance) =>
54779
- axis === "x" ? `${distance}px 0px` : `0px ${distance}px`;
55031
+ axis === "x"
55032
+ ? `translate(${distance}px, 0px)`
55033
+ : `translate(0px, ${distance}px)`;
54780
55034
 
54781
55035
  const PopupClose = ({
54782
55036
  label,
@@ -54973,11 +55227,17 @@ const css$E = /* css */`
54973
55227
  containing block is genuinely its nearest positioned ancestor,
54974
55228
  regardless of positionArea. See the [data-layer="top"] rule below for
54975
55229
  why the via-attribute renderer overrides this. Position is always
54976
- JS-driven (pickPositionRelativeTo sets top/left directly, see
54977
- useDialogProps below) — no CSS alignment/inset math here at all,
54978
- unlike an earlier version of this file. */
55230
+ JS-driven (pickPositionRelativeTo, see useDialogProps below) — no CSS
55231
+ alignment/inset math here at all. */
54979
55232
  position: absolute;
54980
- inset: unset;
55233
+ /* Laid out at its containing block's own origin and moved from there by a
55234
+ translate (applyNewPosition), never by left/top: a shrink-to-fit box
55235
+ placed with left is only ever as wide as what is left of the container
55236
+ to its right, and that width is what decides where it gets placed — see
55237
+ applyNewPosition's own doc. right/bottom stay auto: an inset there would
55238
+ over-constrain the box against the UA's margin: auto and re-center
55239
+ it. */
55240
+ inset: 0 auto auto 0;
54981
55241
  /* Custom renderer only — see openLocalDialogCount above */
54982
55242
  z-index: calc(var(--navi-z-index-popup) + var(--dialog-stack-order, 0));
54983
55243
  min-width: min(
@@ -55073,11 +55333,12 @@ const css$E = /* css */`
55073
55333
  &[data-flush-bottom][data-flush-left] {
55074
55334
  border-bottom-left-radius: 0;
55075
55335
  }
55076
- /* left/top are NOT transitioned here applyNewPosition (visible_rect.js)
55077
- drives that itself via the Web Animations API instead of CSS, so it
55078
- stays independent from navi-animation's own opacity/scale/display
55079
- transition list below (no shared transition-property to clobber, no
55080
- propertyName to filter). */
55336
+ /* The placement is a translate, so the translate property is spoken for
55337
+ here (see applyNewPosition in visible_rect.js, which owns it and animates
55338
+ it itself through the Web Animations API rather than through this file's
55339
+ transitions no shared transition-property to clobber, no propertyName
55340
+ to filter). An entrance animation moves the dialog through scale and
55341
+ transform instead, which compose under it: see popup_css.js. */
55081
55342
 
55082
55343
  &::backdrop {
55083
55344
  background: var(--navi-backdrop-close-background);
@@ -55157,7 +55418,7 @@ const css$E = /* css */`
55157
55418
  containing block is the viewport rather than any positioned
55158
55419
  ancestor. Not left to the native :modal UA stylesheet's own default
55159
55420
  (also position: fixed, but with its own margin/inset assumptions) so
55160
- that JS-set top/left (see useDialogProps below) always wins
55421
+ that the JS-driven placement (see useDialogProps below) always wins
55161
55422
  cleanly. */
55162
55423
  &[data-layer="top"] {
55163
55424
  position: fixed;
@@ -55442,6 +55703,16 @@ const css$E = /* css */`
55442
55703
  * actually closes — not preventable (see `open_controller.js`'s own
55443
55704
  * `onRequestClose`/`onClose` distinction; `onRequestClose` is where you'd
55444
55705
  * veto a close instead).
55706
+ * @param {boolean|string|{id?: string, type?: "push"|"replace"}} [props.navState] -
55707
+ * Keeps the open state in the history entry, so a screen left and come back
55708
+ * to finds this popup as it was — open, and without an entrance playing: it
55709
+ * was already open when the page reappeared. `true` stores it under the
55710
+ * popup's own `id`; a string names the key instead.
55711
+ * `{ type: "push" }` also makes the opening a history entry of its own, so
55712
+ * the back button closes the popup rather than leaving the screen — and a
55713
+ * cancel (Escape) goes back, taking whatever was written to the url while it
55714
+ * was open with it. The state belongs to the entry that wrote it: a
55715
+ * navigation that stacks a new entry does not carry it along.
55445
55716
  * @param {object} [props.openController] - Advanced: an externally-owned
55446
55717
  * open controller (see `open_controller.js`) for a caller that wants to
55447
55718
  * drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
@@ -55509,6 +55780,7 @@ const UncontrolledDialog = props => {
55509
55780
  open: undefined,
55510
55781
  signal: undefined,
55511
55782
  defaultOpen: undefined,
55783
+ navState: undefined,
55512
55784
  onClose: undefined,
55513
55785
  openController: openController,
55514
55786
  onnavi_request_open: e => {
@@ -56088,8 +56360,8 @@ const useDialogProps = props => {
56088
56360
  rectEffect.disconnect();
56089
56361
  });
56090
56362
  // A descendant anchored to something inside this dialog (a Callout, a
56091
- // nested Popover) needing to know about this dialog's own left/top
56092
- // repositioning transition — not just that the target changed
56363
+ // nested Popover) needing to know about this dialog's own repositioning
56364
+ // transition — not just that the target changed
56093
56365
  // (navi_position_change above), but that a real, currently-playing
56094
56366
  // transition is moving it right now — is handled generically by
56095
56367
  // applyNewPosition itself (see its own notifyPositionTransition), since
@@ -56536,7 +56808,14 @@ const css$D = /* css */`
56536
56808
  whether it has a real anchor — this file's top comment has the full
56537
56809
  reasoning. */
56538
56810
  position: absolute;
56539
- inset: unset;
56811
+ /* Laid out at its containing block's own origin and moved from there by a
56812
+ translate (applyNewPosition), never by left/top: a shrink-to-fit box
56813
+ placed with left is only ever as wide as what is left of the container
56814
+ to its right, and that width is what decides which side it gets placed
56815
+ on — see applyNewPosition's own doc. right/bottom stay auto: an inset
56816
+ there would over-constrain the box against the UA's margin: auto and
56817
+ re-center it. */
56818
+ inset: 0 auto auto 0;
56540
56819
  /* Custom renderer only: --popover-stack-order is set to
56541
56820
  openLocalPopoverCount on every open (see openEffect below) so the
56542
56821
  most-recently-opened local popover always outranks an earlier one,
@@ -56568,35 +56847,13 @@ const css$D = /* css */`
56568
56847
  (see box.jsx), which is what makes it scroll — and what a
56569
56848
  header/footer/body inside it then rearranges. */
56570
56849
  overscroll-behavior: none;
56571
- /* left/top are NOT transitioned here applyNewPosition (visible_rect.js)
56572
- drives that itself via the Web Animations API instead of CSS, so it
56573
- stays independent from navi-animation's own opacity/scale/display
56574
- transition list below (no shared transition-property to clobber, no
56575
- propertyName to filter). */
56576
- /* overflow is not declared here: the popover carries [data-scrollable]
56577
- (see box.jsx), which is what makes it scroll — and what a
56578
- header/footer/body inside it then rearranges. */
56579
- overscroll-behavior: none;
56850
+ /* The placement is a translate, so the translate property is spoken for
56851
+ here (see applyNewPosition in visible_rect.js, which owns it and animates
56852
+ it itself through the Web Animations API rather than through this file's
56853
+ transitions no shared transition-property to clobber, no propertyName
56854
+ to filter). An entrance animation moves the popover through scale and
56855
+ transform instead, which compose under it: see popup_css.js. */
56580
56856
 
56581
- /* overflow is not declared here: the popover carries [data-scrollable]
56582
- (see box.jsx), which is what makes it scroll — and what a
56583
- header/footer/body inside it then rearranges. */
56584
- overscroll-behavior: none;
56585
- /* left/top are NOT transitioned here — applyNewPosition (visible_rect.js)
56586
- drives that itself via the Web Animations API instead of CSS, so it
56587
- stays independent from navi-animation's own opacity/scale/display
56588
- transition list below (no shared transition-property to clobber, no
56589
- propertyName to filter). */
56590
- /* overflow is not declared here: the popover carries [data-scrollable]
56591
- (see box.jsx), which is what makes it scroll — and what a
56592
- header/footer/body inside it then rearranges. */
56593
- overscroll-behavior: none;
56594
-
56595
- /* left/top are NOT transitioned here — applyNewPosition (visible_rect.js)
56596
- drives that itself via the Web Animations API instead of CSS, so it
56597
- stays independent from navi-animation's own opacity/scale/display
56598
- transition list below (no shared transition-property to clobber, no
56599
- propertyName to filter). */
56600
56857
  /* The via-attribute renderer starts hidden for free (native UA default
56601
56858
  for any [popover] element, same as <dialog> without [open]) — the
56602
56859
  custom renderer is a plain div with no such native default, so
@@ -56900,6 +57157,16 @@ const css$D = /* css */`
56900
57157
  * actually closes — not preventable (see `open_controller.js`'s own
56901
57158
  * `onRequestClose`/`onClose` distinction; `onRequestClose` is where you'd
56902
57159
  * veto a close instead).
57160
+ * @param {boolean|string|{id?: string, type?: "push"|"replace"}} [props.navState] -
57161
+ * Keeps the open state in the history entry, so a screen left and come back
57162
+ * to finds this popup as it was — open, and without an entrance playing: it
57163
+ * was already open when the page reappeared. `true` stores it under the
57164
+ * popup's own `id`; a string names the key instead.
57165
+ * `{ type: "push" }` also makes the opening a history entry of its own, so
57166
+ * the back button closes the popup rather than leaving the screen — and a
57167
+ * cancel (Escape) goes back, taking whatever was written to the url while it
57168
+ * was open with it. The state belongs to the entry that wrote it: a
57169
+ * navigation that stacks a new entry does not carry it along.
56903
57170
  * @param {object} [props.openController] - Advanced: an externally-owned
56904
57171
  * open controller (see `open_controller.js`) for a caller that wants to
56905
57172
  * drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
@@ -56965,6 +57232,7 @@ const UncontrolledPopover = props => {
56965
57232
  open: undefined,
56966
57233
  signal: undefined,
56967
57234
  defaultOpen: undefined,
57235
+ navState: undefined,
56968
57236
  onClose: undefined,
56969
57237
  openController: openController,
56970
57238
  onnavi_request_open: e => {
@@ -57580,7 +57848,7 @@ const usePopoverProps = props => {
57580
57848
  });
57581
57849
  // A descendant anchored to something inside this popover (a Callout, a
57582
57850
  // further-nested Popover) needing to know about this popover's own
57583
- // left/top repositioning transition — not just that the target changed
57851
+ // repositioning transition — not just that the target changed
57584
57852
  // (navi_position_change above), but that a real, currently-playing
57585
57853
  // transition is moving it right now — is handled generically by
57586
57854
  // applyNewPosition itself (see its own notifyPositionTransition in