@jsenv/navi 0.29.413 → 0.29.415

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.
@@ -6,8 +6,8 @@ import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, win
6
6
  import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, createEventGroupLogger, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, getScrollContainer, watchWheelTravel, scrollRoomTowards, trapScrollInside, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange } from "@jsenv/dom";
7
7
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
9
- import { options, isValidElement, createContext, render, h, toChildArray, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
10
- import { useErrorBoundary, useLayoutEffect, useContext, useMemo, useRef, useEffect, useCallback, useState, useId } from "preact/hooks";
9
+ import { options, isValidElement, createContext, render, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
10
+ import { useErrorBoundary, useLayoutEffect, useContext, useMemo, useRef, useEffect, useCallback, useState, useReducer, useId } from "preact/hooks";
11
11
  import { humanizeI18n, prefixFirstAndIndentRemainingLines, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
12
12
  export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
13
13
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
@@ -79,7 +79,7 @@ installImportMetaCssBuild(import.meta);
79
79
  * any of these, and a number is the last resort, not the first tool.
80
80
  */
81
81
 
82
- const css$17 = /* css */ `@layer navi {
82
+ const css$18 = /* css */ `@layer navi {
83
83
  :root {
84
84
  --navi-z-index-control-hovered: 1;
85
85
  --navi-z-index-control-focused: 2;
@@ -92,7 +92,7 @@ const css$17 = /* css */ `@layer navi {
92
92
  }
93
93
  }
94
94
  `;
95
- import.meta.css = [css$17, "@jsenv/navi/src/navi_z_indexes.js"];
95
+ import.meta.css = [css$18, "@jsenv/navi/src/navi_z_indexes.js"];
96
96
 
97
97
  /**
98
98
  * A navigation is ABOUT to be applied — said before its very first write.
@@ -457,7 +457,7 @@ installImportMetaCssBuild(import.meta);
457
457
 
458
458
  const URL_TARGET_ATTRIBUTE = "data-url-target";
459
459
 
460
- const css$16 = /* css */ `@layer navi {
460
+ const css$17 = /* css */ `@layer navi {
461
461
  [data-url-target] {
462
462
  animation: navi_url_target var(--navi-url-target-duration, 2s)
463
463
  ease-out;
@@ -475,7 +475,7 @@ const css$16 = /* css */ `@layer navi {
475
475
  }
476
476
  }
477
477
  `;
478
- import.meta.css = [css$16, "@jsenv/navi/src/nav/url_target/url_target.js"];
478
+ import.meta.css = [css$17, "@jsenv/navi/src/nav/url_target/url_target.js"];
479
479
 
480
480
  let urlTargetOptions = {
481
481
  block: "start",
@@ -2080,7 +2080,7 @@ const useActionStatus = (action) => {
2080
2080
  };
2081
2081
 
2082
2082
  installImportMetaCssBuild(import.meta);
2083
- const css$15 = /* css */`.action_error {
2083
+ const css$16 = /* css */`.action_error {
2084
2084
  background: #fdd;
2085
2085
  border: 1px solid red;
2086
2086
  margin-top: 0;
@@ -2104,7 +2104,7 @@ const ActionRenderer = ({
2104
2104
  children,
2105
2105
  disabled
2106
2106
  }) => {
2107
- import.meta.css = [css$15, "@jsenv/navi/src/action/action_renderer.jsx"];
2107
+ import.meta.css = [css$16, "@jsenv/navi/src/action/action_renderer.jsx"];
2108
2108
  if (action === undefined) {
2109
2109
  throw new Error("ActionRenderer requires an action to render, but none was provided.");
2110
2110
  }
@@ -14697,6 +14697,31 @@ const moveFocusTo = (
14697
14697
  });
14698
14698
  };
14699
14699
 
14700
+ /**
14701
+ * Gives the focus back to `target` once the commit a close is part of is over:
14702
+ * for a close caused by an unmount (UNMOUNT_EVENT_TYPE in
14703
+ * layout/open_controller.js), which runs from a cleanup while the tree leaving
14704
+ * with it is still in the document. Only its end tells a target leaving with
14705
+ * it — disconnected by then, and skipped — from one staying, and a focused
14706
+ * element that leaves makes the removal of its whole subtree restyle it.
14707
+ *
14708
+ * And only into a void: the close took the focus with it, so something holding
14709
+ * the focus by then — a page arriving that focused a field of its own — took it
14710
+ * after, and keeps it.
14711
+ */
14712
+ const giveFocusBackAfterCommit = (target, giveFocus) => {
14713
+ queueMicrotask(() => {
14714
+ if (!target || !target.isConnected) {
14715
+ return;
14716
+ }
14717
+ const focusedNow = document.activeElement;
14718
+ if (focusedNow && focusedNow !== document.body) {
14719
+ return;
14720
+ }
14721
+ giveFocus();
14722
+ });
14723
+ };
14724
+
14700
14725
  const focusTransferTarget = (target, focusVisible) => {
14701
14726
  moveFocusTo(target, { focusVisible });
14702
14727
  if (target.hasAttribute("navi-autofocus-select")) {
@@ -15251,6 +15276,12 @@ const createOpenController = (
15251
15276
  closeEvent,
15252
15277
  `closed by focusout -> let focus go away`,
15253
15278
  );
15279
+ } else if (findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
15280
+ // Left the tree: what held the focus before the opening may be
15281
+ // leaving with it (see giveFocusBackAfterCommit).
15282
+ giveFocusBackAfterCommit(focusTransfer.focusedElement, () => {
15283
+ focusTransfer.restoreFocus(closeEvent);
15284
+ });
15254
15285
  } else {
15255
15286
  // Only the mousedown, deliberately: a popup with no backdrop is
15256
15287
  // closed by a `pointerdown` that belongs to the page (see
@@ -15941,7 +15972,7 @@ installImportMetaCssBuild(import.meta);
15941
15972
  * (see calloutTemplate in callout.js).
15942
15973
  */
15943
15974
 
15944
- const css$14 = /* css */`.navi_callout_status_icon {
15975
+ const css$15 = /* css */`.navi_callout_status_icon {
15945
15976
  --x-callout-status-icon-color: var(--navi-callout-neutral-color);
15946
15977
  box-sizing: border-box;
15947
15978
  aspect-ratio: 1;
@@ -16012,7 +16043,7 @@ const CalloutStatusIcon = ({
16012
16043
  status = "info",
16013
16044
  shape = "square"
16014
16045
  }) => {
16015
- import.meta.css = [css$14, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
16046
+ import.meta.css = [css$15, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
16016
16047
  return jsx("span", {
16017
16048
  className: "navi_callout_status_icon",
16018
16049
  "data-status": status === "none" ? undefined : status,
@@ -16039,7 +16070,7 @@ let calloutCount = 0;
16039
16070
  // large enough to survive the pixel or two a trackpad rests on.
16040
16071
  const CLOSE_BY_SCROLL_DISTANCE = 10;
16041
16072
 
16042
- const css$13 = /* css */ `
16073
+ const css$14 = /* css */ `
16043
16074
  /* jsenv-css-opaque: same as dialog.jsx, cascade order. */
16044
16075
  @layer navi {
16045
16076
  .navi_callout {
@@ -16400,7 +16431,7 @@ const openCallout = (
16400
16431
  debug = () => {},
16401
16432
  } = {},
16402
16433
  ) => {
16403
- import.meta.css = [css$13, "@jsenv/navi/src/control/rules/callout/callout.js"];
16434
+ import.meta.css = [css$14, "@jsenv/navi/src/control/rules/callout/callout.js"];
16404
16435
  if (debug === true) {
16405
16436
  debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
16406
16437
  }
@@ -18119,14 +18150,8 @@ const createCalloutManager = (
18119
18150
  };
18120
18151
  if (findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
18121
18152
  // Closed by a popup leaving the tree: the control may be leaving
18122
- // in the same commit, which only its end tells. A leaving control
18123
- // must not get the focus — the removal of a subtree holding it
18124
- // restyles the whole subtree.
18125
- queueMicrotask(() => {
18126
- if (focusTarget.isConnected) {
18127
- giveFocusBack();
18128
- }
18129
- });
18153
+ // with it (see giveFocusBackAfterCommit).
18154
+ giveFocusBackAfterCommit(focusTarget, giveFocusBack);
18130
18155
  } else {
18131
18156
  // Given before the callout leaves the document: removed while it
18132
18157
  // holds the focus, it sends a focusout with nowhere to go up
@@ -21532,7 +21557,9 @@ const swipeTypeOf = (axis, pulled) => {
21532
21557
  * meantime, nothing is picked up. The carry is the element's own, outcomes and
21533
21558
  * refusal included, without the wait for intent (the hold was the proof), and
21534
21559
  * the copy is brought to the finger rather than following it from wherever the
21535
- * element stands. A copy or nothing: `move` and `moving` carry the thing itself,
21560
+ * element stands. Let go of there before it has travelled — the hold only meant
21561
+ * to open the popup — it goes back and nothing is answered, whatever it was
21562
+ * brought over. A copy or nothing: `move` and `moving` carry the thing itself,
21536
21563
  * which would leave its place just for being handed a press. When it starts is
21537
21564
  * navi's — after a popup still being lifted (see `--navi-grab` in commands.js).
21538
21565
  *
@@ -31796,6 +31823,12 @@ const useUrlSearchParam = (paramName, defaultValue) => {
31796
31823
  return [value, setSearchParamValue];
31797
31824
  };
31798
31825
 
31826
+ // The title belongs to the last Head to arrive, whatever order they leave in:
31827
+ // the page being left by a route transition is taken down AFTER the page
31828
+ // arriving is up (see keepLeavingPages in route.jsx), and putting back the
31829
+ // title it found would put its own page's title over the new one.
31830
+ const titleEntries = [];
31831
+ let titleWithoutHead = null;
31799
31832
  const Head = ({
31800
31833
  children
31801
31834
  }) => {
@@ -31804,15 +31837,17 @@ const Head = ({
31804
31837
  return undefined;
31805
31838
  }
31806
31839
  const childArray = Array.isArray(children) ? children : [children];
31807
- const previousTitle = document.title;
31808
31840
  const appendedElements = [];
31841
+ let titleEntry = null;
31809
31842
  for (const child of childArray) {
31810
31843
  if (!child) {
31811
31844
  continue;
31812
31845
  }
31813
31846
  if (child.type === "title") {
31814
31847
  const titleChildren = child.props.children;
31815
- document.title = Array.isArray(titleChildren) ? titleChildren.join("") : titleChildren ?? "";
31848
+ titleEntry = {
31849
+ title: Array.isArray(titleChildren) ? titleChildren.join("") : titleChildren ?? ""
31850
+ };
31816
31851
  continue;
31817
31852
  }
31818
31853
  const el = document.createElement(child.type);
@@ -31823,8 +31858,19 @@ const Head = ({
31823
31858
  document.head.appendChild(el);
31824
31859
  appendedElements.push(el);
31825
31860
  }
31861
+ if (titleEntry) {
31862
+ if (titleEntries.length === 0) {
31863
+ titleWithoutHead = document.title;
31864
+ }
31865
+ titleEntries.push(titleEntry);
31866
+ document.title = titleEntry.title;
31867
+ }
31826
31868
  return () => {
31827
- document.title = previousTitle;
31869
+ if (titleEntry) {
31870
+ titleEntries.splice(titleEntries.indexOf(titleEntry), 1);
31871
+ const lastEntry = titleEntries[titleEntries.length - 1];
31872
+ document.title = lastEntry ? lastEntry.title : titleWithoutHead;
31873
+ }
31828
31874
  for (const el of appendedElements) {
31829
31875
  el.remove();
31830
31876
  }
@@ -31987,75 +32033,7 @@ const samePages = (pages, otherPages) => {
31987
32033
  return true;
31988
32034
  };
31989
32035
 
31990
- /**
31991
- * Route is the single primitive for URL-based rendering.
31992
- *
31993
- * ## Layout pattern
31994
- * Use this when multiple routes share a common layout but have no shared URL prefix,
31995
- * making it impossible to set a guard route on the parent container.
31996
- * For example, `/profile` and `/settings` both live inside `AuthLayout` but there
31997
- * is no `/auth/` prefix to match on. A container Route wraps them: the active
31998
- * child's element is injected as `children` into the layout element.
31999
- * If a page needs state owned by the layout, the layout must expose it via context.
32000
- *
32001
- * ```jsx
32002
- * const PROFILE_ROUTE = route("/profile");
32003
- * const SETTINGS_ROUTE = route("/settings");
32004
- *
32005
- * <Route element={AuthLayout}>
32006
- * <Route route={PROFILE_ROUTE} element={ProfilePage} />
32007
- * <Route route={SETTINGS_ROUTE} element={SettingsPage} />
32008
- * </Route>
32009
- * ```
32010
- *
32011
- * Such a container holds no fallback, and cannot: what makes this one the right
32012
- * layout is a child matching, and having no prefix is the very reason it was
32013
- * reached for — so there is no address that belongs to it and to none of its
32014
- * children. An address nothing claims is the router's to answer; write the
32015
- * fallback there.
32016
- *
32017
- * ## Self-contained section pattern
32018
- * Use this when routes share a common URL prefix (e.g. `/dashboard/`).
32019
- * A single leaf Route in the top-level router matches the prefix; the component
32020
- * it renders owns its sub-router and all related routes internally.
32021
- * Everything about the section — routes, structure, sub-pages — is co-located.
32022
- * The component is not a reusable layout; it is the section.
32023
- *
32024
- * Compared to the layout pattern, a dedicated section component is more powerful:
32025
- * - Wrapper elements (chrome, nav, containers) are part of the component's render,
32026
- * not injected via `children`, so a layout is not needed.
32027
- * - Local state can be passed directly via `elementProps` to sub-pages. With the
32028
- * layout pattern, sub-pages receive state only through `children` or context.
32029
- *
32030
- * The layout pattern remains necessary when a shared prefix does not exist
32031
- * (see "Layout pattern" above).
32032
- *
32033
- * ```jsx
32034
- * const DASHBOARD_SECTION_ROUTE = route("/dashboard/");
32035
- * const DASHBOARD_HOME_ROUTE = route("/dashboard");
32036
- * const DASHBOARD_POSTS_ROUTE = route("/dashboard/posts");
32037
- *
32038
- * // top-level router — only knows about the prefix
32039
- * <Route route={DASHBOARD_SECTION_ROUTE} element={DashboardSection} />
32040
- *
32041
- * // Dashboard owns the rest
32042
- * const DashboardSection = () => {
32043
- * const [sidebarOpen, setSidebarOpen] = useState(false);
32044
- *
32045
- * return <div
32046
- * style="background: lightblue; padding: 10px;"
32047
- * onClick={() => setSidebarOpen(o => !o)}
32048
- * >
32049
- * <Route>
32050
- * <Route route={DASHBOARD_HOME_ROUTE} element={DashboardHomePage} elementProps={{ sidebarOpen}} />
32051
- * <Route route={DASHBOARD_POSTS_ROUTE} element={DashboardPostsPage} elementProps={{ sidebarOpen }} />
32052
- * <Route fallback element={DashboardNotFound} />
32053
- * </Route>
32054
- * </div>;
32055
- * }
32056
- * ```
32057
- */
32058
-
32036
+ installImportMetaCssBuild(import.meta);
32059
32037
 
32060
32038
  /**
32061
32039
  * Keep every container showing the page it is showing, whatever the routes say.
@@ -32079,6 +32057,56 @@ const freezeRouteRender = () => {
32079
32057
  routeRenderFrozenSignal.value++;
32080
32058
  };
32081
32059
  };
32060
+
32061
+ /**
32062
+ * Keep the page a container leaves, hidden, until its route transition is over.
32063
+ *
32064
+ * The browser photographs the page being left before the change, and the page
32065
+ * arriving inside the transition's update callback — where everything done
32066
+ * delays the movement. Taking the page being left down is most of that: every
32067
+ * hook of every component in it is cleaned up, and a page of thirty cards
32068
+ * takes longer to take down than the page arriving takes to build. Its picture
32069
+ * is already taken, so it only has to stay out of the second one: a container
32070
+ * that changes page while this is held keeps the page it was showing, hidden,
32071
+ * and drops it once the movement is over (see route_transition.jsx).
32072
+ *
32073
+ * Kept only where Preact would have unmounted it: a page rendered by the same
32074
+ * element on both sides is the same component instance carried over, exactly
32075
+ * as without a movement.
32076
+ */
32077
+ let leavingPagesHold = null;
32078
+ const keepLeavingPages = () => {
32079
+ const hold = {
32080
+ released: false,
32081
+ containers: new Set()
32082
+ };
32083
+ leavingPagesHold = hold;
32084
+ return () => {
32085
+ if (hold.released) {
32086
+ return;
32087
+ }
32088
+ hold.released = true;
32089
+ if (leavingPagesHold === hold) {
32090
+ leavingPagesHold = null;
32091
+ }
32092
+ for (const rerender of hold.containers) {
32093
+ rerender();
32094
+ }
32095
+ hold.containers.clear();
32096
+ };
32097
+ };
32098
+
32099
+ // What a page can learn about its place from the container rendering it:
32100
+ // whether it is a page kept while leaving (see keepLeavingPages). Such a page
32101
+ // is still mounted, and whatever it fills OUTSIDE its own nodes — a slot in a
32102
+ // bar, say — would otherwise go on showing with the page arriving (see
32103
+ // layout/slot.jsx).
32104
+ const RoutePageContext = createContext(null);
32105
+ const LEAVING_ATTRIBUTE = "data-navi-route-leaving";
32106
+ const css$13 = /* css */`[data-navi-route-leaving] {
32107
+ display: none !important;
32108
+ }
32109
+ `;
32082
32110
  const debug$1 = (...args) => {
32083
32111
  {
32084
32112
  return;
@@ -32198,6 +32226,7 @@ const RouteContainer = ({
32198
32226
  shownBranchRef.current = activeBranch;
32199
32227
  }
32200
32228
  const branch = shownBranchRef.current || activeBranch;
32229
+ const content = useContentKeepingLeavingPages(branch);
32201
32230
 
32202
32231
  // The two things this component knows that nobody outside can find out: the
32203
32232
  // pages a named fallback is the absence of — the children it was written
@@ -32219,7 +32248,6 @@ const RouteContainer = ({
32219
32248
  };
32220
32249
  }, [fallbackPage]);
32221
32250
  debug$1(`[container "${id}"] RENDER, active=${branch ? branch.type : "none"}`);
32222
- const content = branch ? branch.node : null;
32223
32251
  if (!content) {
32224
32252
  return null;
32225
32253
  }
@@ -32228,12 +32256,152 @@ const RouteContainer = ({
32228
32256
  }
32229
32257
  return content;
32230
32258
  };
32259
+ // What a container renders: the page it shows and, while a route transition
32260
+ // keeps them (see keepLeavingPages), the pages it has left — each the very
32261
+ // vnode rendered last time, which Preact does not look into, between two
32262
+ // markers that say which nodes to hide. Always an array, even of one page: the
32263
+ // page being left must stay the child it was when a page is added beside it,
32264
+ // or Preact takes it down and builds it again.
32265
+ let leavingKeyCount = 0;
32266
+ const useContentKeepingLeavingPages = branch => {
32267
+ const [, rerender] = useReducer(countRenders, 0);
32268
+ const shownRef = useRef(null);
32269
+ const leavingRef = useRef([]);
32270
+ const shown = shownRef.current;
32271
+ const element = branchElement(branch);
32272
+ let key;
32273
+ let page = null;
32274
+ if (shown && branch && (shown.id === branch.id || shown.element === element)) {
32275
+ // The same page, or one Preact carries over from it: the key it was
32276
+ // mounted with, or it would be mounted again.
32277
+ key = shown.key;
32278
+ page = shown.page;
32279
+ } else if (shown && leavingPagesHold && !document.querySelector(":modal")) {
32280
+ // Never while a modal dialog is open: it keeps everything but itself out
32281
+ // of reach — the page arriving could not even take the focus — for as long
32282
+ // as it is open, and one in a page kept while leaving stays open for the
32283
+ // whole movement. The page is then taken down at once, as without one.
32284
+ leavingRef.current.push({
32285
+ vnode: shown.vnode,
32286
+ page: shown.page,
32287
+ key: `navi-route-leaving-${++leavingKeyCount}`,
32288
+ hold: leavingPagesHold,
32289
+ startRef: createRef(),
32290
+ endRef: createRef(),
32291
+ hidden: false
32292
+ });
32293
+ leavingPagesHold.containers.add(rerender);
32294
+ // Keyed apart from the page it takes the place of, which is still there.
32295
+ key = `navi-route-${++leavingKeyCount}`;
32296
+ }
32297
+ let vnode = null;
32298
+ if (branch) {
32299
+ if (!page) {
32300
+ page = {
32301
+ leavingSignal: signal(false)
32302
+ };
32303
+ }
32304
+ vnode = h(RoutePageContext.Provider, {
32305
+ key,
32306
+ value: page
32307
+ }, branch.node);
32308
+ }
32309
+ shownRef.current = branch ? {
32310
+ id: branch.id,
32311
+ element,
32312
+ key,
32313
+ vnode,
32314
+ page
32315
+ } : null;
32316
+ const leaving = leavingRef.current.filter(entry => !entry.hold.released);
32317
+ leavingRef.current = leaving;
32318
+
32319
+ // Hidden as soon as it is in the DOM next to the page arriving: this runs in
32320
+ // the update callback's render, before the second picture is taken.
32321
+ useLayoutEffect(() => {
32322
+ for (const entry of leavingRef.current) {
32323
+ const start = entry.startRef.current;
32324
+ const end = entry.endRef.current;
32325
+ if (entry.hidden || !start || !end) {
32326
+ continue;
32327
+ }
32328
+ entry.hidden = true;
32329
+ entry.page.leavingSignal.value = true;
32330
+ const focusedElement = document.activeElement;
32331
+ let node = start.nextSibling;
32332
+ while (node && node !== end) {
32333
+ if (node.nodeType === Node.ELEMENT_NODE) {
32334
+ node.setAttribute(LEAVING_ATTRIBUTE, "");
32335
+ // What the keyboard held in it goes where the page's removal would
32336
+ // have sent it: a hidden element keeps the focus, and would go on
32337
+ // receiving the keys pressed during the movement.
32338
+ if (node.contains(focusedElement)) {
32339
+ focusedElement.blur();
32340
+ }
32341
+ }
32342
+ node = node.nextSibling;
32343
+ }
32344
+ }
32345
+ });
32346
+ useLayoutEffect(() => {
32347
+ return () => {
32348
+ for (const entry of leavingRef.current) {
32349
+ entry.hold.containers.delete(rerender);
32350
+ }
32351
+ };
32352
+ }, []);
32353
+ if (leaving.length === 0) {
32354
+ return vnode ? [vnode] : null;
32355
+ }
32356
+ import.meta.css = [css$13, "@jsenv/navi/src/nav/route.jsx"];
32357
+ const children = vnode ? [vnode] : [];
32358
+ for (const entry of leaving) {
32359
+ children.push(h("template", {
32360
+ key: `${entry.key}-start`,
32361
+ ref: entry.startRef
32362
+ }), entry.vnode, h("template", {
32363
+ key: `${entry.key}-end`,
32364
+ ref: entry.endRef
32365
+ }));
32366
+ }
32367
+ return children;
32368
+ };
32369
+ const countRenders = count => count + 1;
32370
+ // What Preact compares when a container changes page, below the <Route> it
32371
+ // renders either way: the element the page is rendered with — a layout, for a
32372
+ // container branch.
32373
+ const branchElement = branch => {
32374
+ if (!branch) {
32375
+ return null;
32376
+ }
32377
+ let node = branch.node;
32378
+ while (node && node.type !== Route) {
32379
+ // A wrapper around the branch (see wrapBranch).
32380
+ node = node.props.children;
32381
+ }
32382
+ if (!node) {
32383
+ return null;
32384
+ }
32385
+ const {
32386
+ element
32387
+ } = node.props;
32388
+ if (element === undefined || typeof element === "function") {
32389
+ return element;
32390
+ }
32391
+ return element.type;
32392
+ };
32393
+
32231
32394
  // Walk JSX children vnodes (without rendering) to build a branch list and
32232
32395
  // find the active one in the same pass. Anything that is not a <Route> is read
32233
32396
  // through and kept around the branch it holds (see below).
32234
32397
  // Returns { matchingBranch, fallbackBranch, activeBranch, pages }.
32235
- const collectBranches = children => {
32398
+ //
32399
+ // Each branch is given an id from where it is written among the children,
32400
+ // which is what tells a container it changed page: the vnodes themselves are
32401
+ // new at every render of the parent.
32402
+ const collectBranches = (children, path = "") => {
32236
32403
  let matchingBranch = null;
32404
+ let childIndex = 0;
32237
32405
  let fallbackBranch = null;
32238
32406
  // Every page a branch can be on, at any depth: what a named fallback is the
32239
32407
  // ABSENCE of (see route_fallback.js). A container with a guard route counts
@@ -32251,6 +32419,7 @@ const collectBranches = children => {
32251
32419
  }
32252
32420
  return;
32253
32421
  }
32422
+ const id = `${path}${childIndex++}`;
32254
32423
  if (child.type !== Route) {
32255
32424
  // Anything else is a wrapper around branches, and the two that matter are
32256
32425
  // navi's own: a page says what it renders and delegates what it cannot —
@@ -32270,7 +32439,7 @@ const collectBranches = children => {
32270
32439
  matchingBranch: matchingInside,
32271
32440
  fallbackBranch: fallbackInside,
32272
32441
  pages: pagesInside
32273
- } = collectBranches(wrapperChildren);
32442
+ } = collectBranches(wrapperChildren, `${id}.`);
32274
32443
  for (const pageInside of pagesInside) {
32275
32444
  pages.push(pageInside);
32276
32445
  }
@@ -32293,7 +32462,7 @@ const collectBranches = children => {
32293
32462
  matchingBranch: matchingChild,
32294
32463
  fallbackBranch: fallbackChild,
32295
32464
  pages: pagesChild
32296
- } = collectBranches(nodeChildren);
32465
+ } = collectBranches(nodeChildren, `${id}.`);
32297
32466
  if (fallbackChild && !route) {
32298
32467
  // A fallback is the branch taken for an address none of its siblings
32299
32468
  // claim — so something has to make that address BELONG to this
@@ -32317,6 +32486,7 @@ const collectBranches = children => {
32317
32486
  }
32318
32487
  const branch = {
32319
32488
  type: "container",
32489
+ id,
32320
32490
  node: child
32321
32491
  };
32322
32492
  const guardMatching = route ? route.matchingSignal.value : false;
@@ -32334,6 +32504,7 @@ const collectBranches = children => {
32334
32504
  if (!fallbackBranch) {
32335
32505
  fallbackBranch = {
32336
32506
  type: "fallback",
32507
+ id,
32337
32508
  node: child,
32338
32509
  page: fallback.isRouteFallback ? fallback : null
32339
32510
  };
@@ -32341,6 +32512,7 @@ const collectBranches = children => {
32341
32512
  } else {
32342
32513
  const branch = {
32343
32514
  type: "leaf",
32515
+ id,
32344
32516
  node: child
32345
32517
  };
32346
32518
  pages.push({
@@ -32828,7 +33000,7 @@ const holdTransitionFurniture = (owner, areaElement) => {
32828
33000
  }
32829
33001
  const namesLeaving = [];
32830
33002
  for (const element of namedElements) {
32831
- if (!element.isConnected) {
33003
+ if (!isRendered(element)) {
32832
33004
  namesLeaving.push(nameByElement.get(element));
32833
33005
  } else if (element.matches(FIXED_BAR_SELECTOR$1)) {
32834
33006
  element.setAttribute(FURNITURE_ATTRIBUTE, "shared");
@@ -32947,11 +33119,16 @@ const travelRule = (side, names, movementProperty) => {
32947
33119
  * The pictures are up and about to move: what no picture covers heads to the
32948
33120
  * state arriving, on the same clock.
32949
33121
  */
32950
- const startTransitionFurniture = (owner) => {
33122
+ const startTransitionFurniture = (owner, { duration } = {}) => {
32951
33123
  if (owner !== furnitureOwner) {
32952
33124
  return;
32953
33125
  }
32954
33126
  for (const restWall of restWallElements) {
33127
+ // A duration of the movement's own is written for its pictures alone (see
33128
+ // transition_values.js); the stand-in is an element, and is told.
33129
+ if (duration !== undefined) {
33130
+ restWall.style.setProperty("--navi-route-transition-duration", duration);
33131
+ }
32955
33132
  restWall.style.opacity = restWallOpacity;
32956
33133
  }
32957
33134
  };
@@ -32982,7 +33159,9 @@ const releaseTransitionFurniture = (owner) => {
32982
33159
  // area's stand-in answers for it.
32983
33160
  const paintTransitionWalls = (areaElement) => {
32984
33161
  removeTransitionWalls();
32985
- const sources = document.querySelectorAll(TOP_LAYER_WALL_SELECTOR);
33162
+ const sources = Array.from(
33163
+ document.querySelectorAll(TOP_LAYER_WALL_SELECTOR),
33164
+ ).filter(isRendered);
32986
33165
  if (sources.length === 0) {
32987
33166
  return sources;
32988
33167
  }
@@ -33184,6 +33363,14 @@ const subtractRect = (rect, hole) => {
33184
33363
  return pieces;
33185
33364
  };
33186
33365
 
33366
+ // Out of the state arriving: taken out of the document, or in a page kept
33367
+ // hidden until its movement is over (see keepLeavingPages in route.jsx) —
33368
+ // where a dialog is still :modal and a popover still :popover-open, and
33369
+ // neither is painted or photographed.
33370
+ const isRendered = (element) => {
33371
+ return element.isConnected && element.checkVisibility();
33372
+ };
33373
+
33187
33374
  const removeRestWalls = () => {
33188
33375
  for (const restWall of restWallElements) {
33189
33376
  restWall.remove();
@@ -33193,6 +33380,52 @@ const removeRestWalls = () => {
33193
33380
  leavingRects = new Map();
33194
33381
  };
33195
33382
 
33383
+ /**
33384
+ * The values a movement hands to its pictures, written where only the pictures
33385
+ * read them: on ::view-transition, the root of the pseudo-element tree, rather
33386
+ * than on the document's root.
33387
+ *
33388
+ * A custom property written on :root is inherited by every element of the
33389
+ * document, and writing one restyles all of them. A movement writes its
33390
+ * numbers as its pages are held, again whenever a scroll moves the window, and
33391
+ * takes them off as it ends — each time a whole-document restyle of a page that
33392
+ * can be thousands of elements long. On ::view-transition they reach every
33393
+ * picture by plain inheritance, and no element at all.
33394
+ *
33395
+ * An inline style cannot target a pseudo-element, so they are declarations of
33396
+ * one rule, in a sheet of their own. Written only: what a pseudo-element that
33397
+ * is not on screen computes is not reliably readable, so nothing reads them
33398
+ * back from there.
33399
+ */
33400
+
33401
+ let valuesStyle = null;
33402
+ const getValuesStyle = () => {
33403
+ if (valuesStyle) {
33404
+ return valuesStyle;
33405
+ }
33406
+ const sheet = new CSSStyleSheet();
33407
+ sheet.replaceSync(":root::view-transition {}");
33408
+ const [rule] = sheet.cssRules;
33409
+ if (rule) {
33410
+ document.adoptedStyleSheets.push(sheet);
33411
+ valuesStyle = rule.style;
33412
+ } else {
33413
+ // A browser without view transitions drops the rule: the movement runs its
33414
+ // steps all the same (see start_view_transition_polyfill.js), with no
33415
+ // pictures to hand anything to.
33416
+ valuesStyle = document.createElement("div").style;
33417
+ }
33418
+ return valuesStyle;
33419
+ };
33420
+
33421
+ const setTransitionValue = (name, value) => {
33422
+ getValuesStyle().setProperty(name, value);
33423
+ };
33424
+
33425
+ const removeTransitionValue = (name) => {
33426
+ getValuesStyle().removeProperty(name);
33427
+ };
33428
+
33196
33429
  installImportMetaCssBuild(import.meta);
33197
33430
 
33198
33431
  const TRANSITION_WINDOW_CSS = /* css */ `@property --navi-transition-cover-top {
@@ -33227,96 +33460,6 @@ const TRANSITION_WINDOW_CSS = /* css */ `@property --navi-transition-cover-top {
33227
33460
  --navi-transition-cover-left: 0px;
33228
33461
  }
33229
33462
  }
33230
-
33231
- @property --navi-transition-window-top {
33232
- syntax: "*";
33233
- inherits: false
33234
- }
33235
-
33236
- @property --navi-transition-window-left {
33237
- syntax: "*";
33238
- inherits: false
33239
- }
33240
-
33241
- @property --navi-transition-window-width {
33242
- syntax: "*";
33243
- inherits: false
33244
- }
33245
-
33246
- @property --navi-transition-window-height {
33247
- syntax: "*";
33248
- inherits: false
33249
- }
33250
-
33251
- @property --navi-transition-window-old-top {
33252
- syntax: "*";
33253
- inherits: false
33254
- }
33255
-
33256
- @property --navi-transition-window-old-left {
33257
- syntax: "*";
33258
- inherits: false
33259
- }
33260
-
33261
- @property --navi-transition-window-new-top {
33262
- syntax: "*";
33263
- inherits: false
33264
- }
33265
-
33266
- @property --navi-transition-window-new-left {
33267
- syntax: "*";
33268
- inherits: false
33269
- }
33270
-
33271
- @property --navi-transition-old-band-top {
33272
- syntax: "*";
33273
- inherits: false
33274
- }
33275
-
33276
- @property --navi-transition-old-band-right {
33277
- syntax: "*";
33278
- inherits: false
33279
- }
33280
-
33281
- @property --navi-transition-old-band-bottom {
33282
- syntax: "*";
33283
- inherits: false
33284
- }
33285
-
33286
- @property --navi-transition-old-band-left {
33287
- syntax: "*";
33288
- inherits: false
33289
- }
33290
-
33291
- :root::view-transition, :root::view-transition-group(*), :root::view-transition-image-pair(*), :root::view-transition-old(*), :root::view-transition-new(*) {
33292
- --navi-transition-window-top: inherit;
33293
- --navi-transition-window-left: inherit;
33294
- --navi-transition-window-width: inherit;
33295
- --navi-transition-window-height: inherit;
33296
- --navi-transition-window-old-top: inherit;
33297
- --navi-transition-window-old-left: inherit;
33298
- --navi-transition-window-new-top: inherit;
33299
- --navi-transition-window-new-left: inherit;
33300
- --navi-transition-old-band-top: inherit;
33301
- --navi-transition-old-band-right: inherit;
33302
- --navi-transition-old-band-bottom: inherit;
33303
- --navi-transition-old-band-left: inherit;
33304
- }
33305
-
33306
- :root::view-transition-group-children(*) {
33307
- --navi-transition-window-top: inherit;
33308
- --navi-transition-window-left: inherit;
33309
- --navi-transition-window-width: inherit;
33310
- --navi-transition-window-height: inherit;
33311
- --navi-transition-window-old-top: inherit;
33312
- --navi-transition-window-old-left: inherit;
33313
- --navi-transition-window-new-top: inherit;
33314
- --navi-transition-window-new-left: inherit;
33315
- --navi-transition-old-band-top: inherit;
33316
- --navi-transition-old-band-right: inherit;
33317
- --navi-transition-old-band-bottom: inherit;
33318
- --navi-transition-old-band-left: inherit;
33319
- }
33320
33463
  `;
33321
33464
 
33322
33465
  // Called by whatever needs the window — a render, a movement declared or
@@ -33414,22 +33557,21 @@ const holdTransitionWindow = (
33414
33557
  const right =
33415
33558
  rectBefore.right > rectAfter.right ? rectBefore.right : rectAfter.right;
33416
33559
  windowOwner = owner;
33417
- const { style } = document.documentElement;
33418
- style.setProperty(WINDOW_TOP_PROPERTY, `${top}px`);
33419
- style.setProperty(WINDOW_LEFT_PROPERTY, `${left}px`);
33420
- style.setProperty(WINDOW_WIDTH_PROPERTY, `${right - left}px`);
33421
- style.setProperty(WINDOW_HEIGHT_PROPERTY, `${bottom - top}px`);
33422
- style.setProperty(WINDOW_OLD_TOP_PROPERTY, `${rectBefore.top}px`);
33423
- style.setProperty(WINDOW_OLD_LEFT_PROPERTY, `${rectBefore.left}px`);
33424
- style.setProperty(WINDOW_NEW_TOP_PROPERTY, `${rectAfter.top}px`);
33425
- style.setProperty(WINDOW_NEW_LEFT_PROPERTY, `${rectAfter.left}px`);
33560
+ setTransitionValue(WINDOW_TOP_PROPERTY, `${top}px`);
33561
+ setTransitionValue(WINDOW_LEFT_PROPERTY, `${left}px`);
33562
+ setTransitionValue(WINDOW_WIDTH_PROPERTY, `${right - left}px`);
33563
+ setTransitionValue(WINDOW_HEIGHT_PROPERTY, `${bottom - top}px`);
33564
+ setTransitionValue(WINDOW_OLD_TOP_PROPERTY, `${rectBefore.top}px`);
33565
+ setTransitionValue(WINDOW_OLD_LEFT_PROPERTY, `${rectBefore.left}px`);
33566
+ setTransitionValue(WINDOW_NEW_TOP_PROPERTY, `${rectAfter.top}px`);
33567
+ setTransitionValue(WINDOW_NEW_LEFT_PROPERTY, `${rectAfter.left}px`);
33426
33568
  const oldBand = furnitureLive
33427
33569
  ? bandTheCutMayBeRelaxedTo(bandBefore, bandAfter, rectAfter)
33428
33570
  : bandBefore;
33429
- style.setProperty(OLD_BAND_TOP_PROPERTY, `${oldBand.top}px`);
33430
- style.setProperty(OLD_BAND_RIGHT_PROPERTY, `${oldBand.right}px`);
33431
- style.setProperty(OLD_BAND_BOTTOM_PROPERTY, `${oldBand.bottom}px`);
33432
- style.setProperty(OLD_BAND_LEFT_PROPERTY, `${oldBand.left}px`);
33571
+ setTransitionValue(OLD_BAND_TOP_PROPERTY, `${oldBand.top}px`);
33572
+ setTransitionValue(OLD_BAND_RIGHT_PROPERTY, `${oldBand.right}px`);
33573
+ setTransitionValue(OLD_BAND_BOTTOM_PROPERTY, `${oldBand.bottom}px`);
33574
+ setTransitionValue(OLD_BAND_LEFT_PROPERTY, `${oldBand.left}px`);
33433
33575
  followArrivingState(element, rectAfter, {
33434
33576
  outerScroll,
33435
33577
  windowTop: top,
@@ -33451,9 +33593,8 @@ const releaseTransitionWindow = (owner) => {
33451
33593
  if (unfollowArrivingState) {
33452
33594
  unfollowArrivingState();
33453
33595
  }
33454
- const { style } = document.documentElement;
33455
33596
  for (const property of WINDOW_PROPERTIES) {
33456
- style.removeProperty(property);
33597
+ removeTransitionValue(property);
33457
33598
  }
33458
33599
  };
33459
33600
 
@@ -33462,12 +33603,11 @@ const releaseTransitionWindow = (owner) => {
33462
33603
  // file) — and with it the offset outside the box, which carries the window
33463
33604
  // and the corner of the state being left along. Requested, never awaited: the
33464
33605
  // first call runs inside the update callback, where a frame cannot come.
33465
- // Written only when it moved: every write restyles the root and the pictures.
33606
+ // Written only when it moved: every write restyles the pictures.
33466
33607
  const followArrivingState = (element, rectAtHold, heldAt) => {
33467
33608
  if (unfollowArrivingState) {
33468
33609
  unfollowArrivingState();
33469
33610
  }
33470
- const { style } = document.documentElement;
33471
33611
  let top = rectAtHold.top;
33472
33612
  let left = rectAtHold.left;
33473
33613
  let outerTop = heldAt.outerScroll.top;
@@ -33476,24 +33616,27 @@ const followArrivingState = (element, rectAtHold, heldAt) => {
33476
33616
  const rect = element.getBoundingClientRect();
33477
33617
  if (rect.top !== top) {
33478
33618
  top = rect.top;
33479
- style.setProperty(WINDOW_NEW_TOP_PROPERTY, `${top}px`);
33619
+ setTransitionValue(WINDOW_NEW_TOP_PROPERTY, `${top}px`);
33480
33620
  }
33481
33621
  if (rect.left !== left) {
33482
33622
  left = rect.left;
33483
- style.setProperty(WINDOW_NEW_LEFT_PROPERTY, `${left}px`);
33623
+ setTransitionValue(WINDOW_NEW_LEFT_PROPERTY, `${left}px`);
33484
33624
  }
33485
33625
  const outerScroll = readOuterScroll(element);
33486
33626
  if (outerScroll.top !== outerTop) {
33487
33627
  outerTop = outerScroll.top;
33488
33628
  const shift = heldAt.outerScroll.top - outerTop;
33489
- style.setProperty(WINDOW_TOP_PROPERTY, `${heldAt.windowTop + shift}px`);
33490
- style.setProperty(WINDOW_OLD_TOP_PROPERTY, `${heldAt.oldTop + shift}px`);
33629
+ setTransitionValue(WINDOW_TOP_PROPERTY, `${heldAt.windowTop + shift}px`);
33630
+ setTransitionValue(WINDOW_OLD_TOP_PROPERTY, `${heldAt.oldTop + shift}px`);
33491
33631
  }
33492
33632
  if (outerScroll.left !== outerLeft) {
33493
33633
  outerLeft = outerScroll.left;
33494
33634
  const shift = heldAt.outerScroll.left - outerLeft;
33495
- style.setProperty(WINDOW_LEFT_PROPERTY, `${heldAt.windowLeft + shift}px`);
33496
- style.setProperty(
33635
+ setTransitionValue(
33636
+ WINDOW_LEFT_PROPERTY,
33637
+ `${heldAt.windowLeft + shift}px`,
33638
+ );
33639
+ setTransitionValue(
33497
33640
  WINDOW_OLD_LEFT_PROPERTY,
33498
33641
  `${heldAt.oldLeft + shift}px`,
33499
33642
  );
@@ -33856,58 +33999,9 @@ const css$12 = /* css */`@property --navi-route-transition-leave {
33856
33999
  inherits: false
33857
34000
  }
33858
34001
 
33859
- @property --navi-route-transition-clip-top {
33860
- syntax: "*";
33861
- inherits: false
33862
- }
33863
-
33864
- @property --navi-route-transition-clip-right {
33865
- syntax: "*";
33866
- inherits: false
33867
- }
33868
-
33869
- @property --navi-route-transition-clip-bottom {
33870
- syntax: "*";
33871
- inherits: false
33872
- }
33873
-
33874
- @property --navi-route-transition-clip-left {
33875
- syntax: "*";
33876
- inherits: false
33877
- }
33878
-
33879
- @property --navi-route-transition-travel-x {
33880
- syntax: "*";
33881
- inherits: false
33882
- }
33883
-
33884
- @property --navi-route-transition-travel-y {
33885
- syntax: "*";
33886
- inherits: false
33887
- }
33888
-
33889
34002
  :root::view-transition, :root::view-transition-group(*), :root::view-transition-image-pair(*), :root::view-transition-old(*), :root::view-transition-new(*) {
33890
34003
  --navi-route-transition-leave: inherit;
33891
34004
  --navi-route-transition-enter: inherit;
33892
- --navi-route-transition-covered: inherit;
33893
- --navi-route-transition-clip-top: inherit;
33894
- --navi-route-transition-clip-right: inherit;
33895
- --navi-route-transition-clip-bottom: inherit;
33896
- --navi-route-transition-clip-left: inherit;
33897
- --navi-route-transition-travel-x: inherit;
33898
- --navi-route-transition-travel-y: inherit;
33899
- }
33900
-
33901
- :root::view-transition-group-children(*) {
33902
- --navi-route-transition-leave: inherit;
33903
- --navi-route-transition-enter: inherit;
33904
- --navi-route-transition-covered: inherit;
33905
- --navi-route-transition-clip-top: inherit;
33906
- --navi-route-transition-clip-right: inherit;
33907
- --navi-route-transition-clip-bottom: inherit;
33908
- --navi-route-transition-clip-left: inherit;
33909
- --navi-route-transition-travel-x: inherit;
33910
- --navi-route-transition-travel-y: inherit;
33911
34005
  }
33912
34006
 
33913
34007
  :root[data-navi-route-transition] [data-navi-route-transition-area] {
@@ -33928,28 +34022,32 @@ const css$12 = /* css */`@property --navi-route-transition-leave {
33928
34022
 
33929
34023
  &[data-navi-route-transition-target="area"] {
33930
34024
  view-transition-name: none;
33931
- --navi-route-transition-clip-top: max(0px,
33932
- min(var(--navi-safe-area-inset-top) + var(--navi-transition-cover-top),
33933
- var(--navi-transition-old-band-top)) - var(--navi-transition-window-top));
33934
- --navi-route-transition-clip-left: max(0px,
33935
- min(var(--navi-safe-area-inset-left) + var(--navi-transition-cover-left),
33936
- var(--navi-transition-old-band-left)) - var(--navi-transition-window-left));
33937
- --navi-route-transition-clip-bottom: max(0px,
33938
- var(--navi-transition-window-top) +
33939
- var(--navi-transition-window-height) +
33940
- min(var(--navi-safe-area-inset-bottom) +
33941
- var(--navi-transition-cover-bottom),
33942
- var(--navi-transition-old-band-bottom)) -
33943
- 100dvh);
33944
- --navi-route-transition-clip-right: max(0px,
33945
- var(--navi-transition-window-left) +
33946
- var(--navi-transition-window-width) +
33947
- min(var(--navi-safe-area-inset-right) +
33948
- var(--navi-transition-cover-right),
33949
- var(--navi-transition-old-band-right)) -
33950
- 100dvw);
33951
- --navi-route-transition-travel-x: calc(var(--navi-transition-window-width) - var(--navi-route-transition-clip-left) - var(--navi-route-transition-clip-right));
33952
- --navi-route-transition-travel-y: calc(var(--navi-transition-window-height) - var(--navi-route-transition-clip-top) - var(--navi-route-transition-clip-bottom));
34025
+
34026
+ &::view-transition {
34027
+ --navi-route-transition-clip-top: max(0px,
34028
+ min(var(--navi-safe-area-inset-top) + var(--navi-transition-cover-top),
34029
+ var(--navi-transition-old-band-top)) - var(--navi-transition-window-top));
34030
+ --navi-route-transition-clip-left: max(0px,
34031
+ min(var(--navi-safe-area-inset-left) +
34032
+ var(--navi-transition-cover-left),
34033
+ var(--navi-transition-old-band-left)) - var(--navi-transition-window-left));
34034
+ --navi-route-transition-clip-bottom: max(0px,
34035
+ var(--navi-transition-window-top) +
34036
+ var(--navi-transition-window-height) +
34037
+ min(var(--navi-safe-area-inset-bottom) +
34038
+ var(--navi-transition-cover-bottom),
34039
+ var(--navi-transition-old-band-bottom)) -
34040
+ 100dvh);
34041
+ --navi-route-transition-clip-right: max(0px,
34042
+ var(--navi-transition-window-left) +
34043
+ var(--navi-transition-window-width) +
34044
+ min(var(--navi-safe-area-inset-right) +
34045
+ var(--navi-transition-cover-right),
34046
+ var(--navi-transition-old-band-right)) -
34047
+ 100dvw);
34048
+ --navi-route-transition-travel-x: calc(var(--navi-transition-window-width) - var(--navi-route-transition-clip-left) - var(--navi-route-transition-clip-right));
34049
+ --navi-route-transition-travel-y: calc(var(--navi-transition-window-height) - var(--navi-route-transition-clip-top) - var(--navi-route-transition-clip-bottom));
34050
+ }
33953
34051
 
33954
34052
  &::view-transition-group(*) {
33955
34053
  z-index: 0;
@@ -34977,7 +35075,9 @@ const beginTransition = ({
34977
35075
  decision: navigationDecision,
34978
35076
  walkHome: null,
34979
35077
  releaseReverting: null,
34980
- restoreDuration: null
35078
+ duration: undefined,
35079
+ restoreDuration: null,
35080
+ releaseLeavingPages: null
34981
35081
  };
34982
35082
  // The document wears one transition at a time, and the one this interrupts
34983
35083
  // ends only after this one has begun — too late to take off what it wears
@@ -35030,18 +35130,13 @@ const beginTransition = ({
35030
35130
  if (areaElement) {
35031
35131
  documentElement.setAttribute(TRANSITION_TARGET_ATTRIBUTE, "area");
35032
35132
  }
35033
- // A duration of this relation's own, worn for the length of the transition —
35034
- // and whatever the application had written inline put back afterwards, not
35035
- // erased.
35133
+ // A duration of this relation's own, worn by the pictures for the length of
35134
+ // the transition, over whatever the application says on the root.
35036
35135
  if (duration !== undefined) {
35037
- const durationBefore = documentElement.style.getPropertyValue(TRANSITION_DURATION_PROPERTY);
35038
- documentElement.style.setProperty(TRANSITION_DURATION_PROPERTY, typeof duration === "number" ? `${duration}ms` : duration);
35136
+ transition.duration = typeof duration === "number" ? `${duration}ms` : duration;
35137
+ setTransitionValue(TRANSITION_DURATION_PROPERTY, transition.duration);
35039
35138
  transition.restoreDuration = () => {
35040
- if (durationBefore) {
35041
- documentElement.style.setProperty(TRANSITION_DURATION_PROPERTY, durationBefore);
35042
- } else {
35043
- documentElement.style.removeProperty(TRANSITION_DURATION_PROPERTY);
35044
- }
35139
+ removeTransitionValue(TRANSITION_DURATION_PROPERTY);
35045
35140
  };
35046
35141
  }
35047
35142
  const releaseRendering = takeoverRoutingRenderingHold();
@@ -35054,7 +35149,9 @@ const beginTransition = ({
35054
35149
  // the only place the silent misconfigurations show. They are all about the
35055
35150
  // same thing — a movement playing on pictures that are not the pages.
35056
35151
  const viewTransitionReady = () => {
35057
- startTransitionFurniture(transition);
35152
+ startTransitionFurniture(transition, {
35153
+ duration: transition.duration
35154
+ });
35058
35155
  const capturedNames = capturedViewTransitionNames();
35059
35156
  if (areaElements.length > 0) {
35060
35157
  if (!capturedNames.has(AREA_NAME)) {
@@ -35080,6 +35177,10 @@ const beginTransition = ({
35080
35177
  // is awaited here must be able to resolve without a frame: the document
35081
35178
  // is frozen for the whole of this callback.
35082
35179
  try {
35180
+ // The page being left is photographed already: it only has to stay out
35181
+ // of the second picture, not be taken down before it (see
35182
+ // keepLeavingPages in route.jsx).
35183
+ transition.releaseLeavingPages = keepLeavingPages();
35083
35184
  // Releasing flushes the held render synchronously, so a route that
35084
35185
  // rendered has already resolved the wait by the next line.
35085
35186
  releaseRendering();
@@ -35132,6 +35233,11 @@ const beginTransition = ({
35132
35233
  // must not strip what that one is wearing.
35133
35234
  renderWait.stop();
35134
35235
  releaseRendering();
35236
+ // Taken down once nobody sees it any more.
35237
+ if (transition.releaseLeavingPages) {
35238
+ transition.releaseLeavingPages();
35239
+ transition.releaseLeavingPages = null;
35240
+ }
35135
35241
  // The hold a way back took, when it is still standing: the pictures were
35136
35242
  // turned round a second time and played out, or something else took the
35137
35243
  // document over mid-walk. A hold nobody gives back freezes the page.
@@ -45068,8 +45174,12 @@ const useInteractiveProps = (props, {
45068
45174
  }
45069
45175
  // Read by READONLY_CONSTRAINT to say how many runs are already out, and by
45070
45176
  // the action handlers below, which are where a run joins and leaves the
45071
- // count (a labelled block away, hence through the controller).
45072
- uiStateController.parallelGuard = parallelGuard;
45177
+ // count (a labelled block away, hence through the controller). None for a
45178
+ // control without an action of its own, by the same rule as being held
45179
+ // back: what it runs (the noop every such control has, a link's
45180
+ // navigation) is nothing the guard counts, and would hold a slot for its
45181
+ // length.
45182
+ uiStateController.parallelGuard = props.action ? parallelGuard : null;
45073
45183
  if (heldByParallelGuard) {
45074
45184
  controlHostProps["data-readonly-reason"] = "parallel-guard";
45075
45185
  }
@@ -45344,7 +45454,11 @@ const useInteractiveProps = (props, {
45344
45454
  // carried by every navi_action_* event of that run).
45345
45455
  uiStateController.pendingActionEvent = e.detail.event;
45346
45456
  uiStateController.actionInFlight = true;
45347
- uiStateController.parallelGuard?.claim(uiStateController);
45457
+ // Given back to the guard it was counted in, whatever the control
45458
+ // holds by then: one that lost its action mid-run has no guard left
45459
+ // to name, and the slot would stay taken.
45460
+ const countedIn = uiStateController.parallelGuard;
45461
+ countedIn?.claim(uiStateController);
45348
45462
  if (optimistic) {
45349
45463
  syncOptimisticAttribute(true);
45350
45464
  }
@@ -45362,7 +45476,7 @@ const useInteractiveProps = (props, {
45362
45476
  e.detail.addSideEffect((outcome, outcomeEvent) => {
45363
45477
  uiStateController.actionInFlight = false;
45364
45478
  uiStateController.runningActionSignal.value = null;
45365
- uiStateController.parallelGuard?.release(uiStateController);
45479
+ countedIn?.release(uiStateController);
45366
45480
  // The outcome callbacks are told from here, the run's side effect,
45367
45481
  // and not from the onnavi_action_* handlers below: the run can
45368
45482
  // outlive this control (its own answer, written to a store the
@@ -48895,6 +49009,90 @@ const BinderItemContext = createContext(null);
48895
49009
  */
48896
49010
  const NavContext = createContext(null);
48897
49011
 
49012
+ /**
49013
+ * Whether the page one is on is `route` with `params` — what a link or a button
49014
+ * pointing at a route shows as current.
49015
+ *
49016
+ * Read through a computed of that one answer, so the component reading it
49017
+ * re-renders when the answer flips, and not whenever the route's status moves:
49018
+ * a page arriving on the route with other params — one profile opening, under a
49019
+ * list of links to a hundred other profiles — starts the route matching and
49020
+ * changes its params, and leaves every one of those links exactly as it was.
49021
+ *
49022
+ * `except`: routes whose matching vetoes the answer — a route named there is
49023
+ * somewhere else, whatever the rest of the reading says. `also`: routes whose
49024
+ * matching grants it. Both take a route or a list of routes.
49025
+ */
49026
+ const useRouteCurrent = (route, params, { except, also } = {}) => {
49027
+ const exceptRoutes = toRouteList(except);
49028
+ const alsoRoutes = toRouteList(also);
49029
+ const paramsKey = getParamsCacheKey(params);
49030
+ const exceptKey = routeListKey(exceptRoutes);
49031
+ const alsoKey = routeListKey(alsoRoutes);
49032
+ const currentSignal = useMemo(() => {
49033
+ if (paramsKey === null) {
49034
+ return null;
49035
+ }
49036
+ return computed(() =>
49037
+ readRouteCurrent(route, params, exceptRoutes, alsoRoutes),
49038
+ );
49039
+ }, [route, paramsKey, exceptKey, alsoKey]);
49040
+ if (!currentSignal) {
49041
+ // Params no key can be made of (a date, an object): read on the spot, the
49042
+ // component then re-renders with the status of every route it reads.
49043
+ return readRouteCurrent(route, params, exceptRoutes, alsoRoutes);
49044
+ }
49045
+ return currentSignal.value;
49046
+ };
49047
+
49048
+ // Every signal is read even once the answer is known: reading is what
49049
+ // subscribes, and a route skipped today is the one whose change has to be heard
49050
+ // tomorrow.
49051
+ const readRouteCurrent = (route, params, exceptRoutes, alsoRoutes) => {
49052
+ const matching = route.matchingSignal.value;
49053
+ const paramsMatching = route.matchesParams(params);
49054
+ let someExceptMatching = false;
49055
+ for (const exceptRoute of exceptRoutes) {
49056
+ if (exceptRoute.matchingSignal.value) {
49057
+ someExceptMatching = true;
49058
+ }
49059
+ }
49060
+ let someAlsoMatching = false;
49061
+ for (const alsoRoute of alsoRoutes) {
49062
+ if (alsoRoute.matchingSignal.value) {
49063
+ someAlsoMatching = true;
49064
+ }
49065
+ }
49066
+ if (someExceptMatching) {
49067
+ return false;
49068
+ }
49069
+ return (matching && paramsMatching) || someAlsoMatching;
49070
+ };
49071
+
49072
+ const toRouteList = (routes) => {
49073
+ if (!routes) {
49074
+ return [];
49075
+ }
49076
+ return Array.isArray(routes) ? routes : [routes];
49077
+ };
49078
+
49079
+ // A list of routes as a key, since an inline `currentAlso={[A, B]}` is a new
49080
+ // array at every render of the parent.
49081
+ const routeIdByRoute = new WeakMap();
49082
+ let routeIdCount = 0;
49083
+ const routeListKey = (routes) => {
49084
+ let key = "";
49085
+ for (const route of routes) {
49086
+ let id = routeIdByRoute.get(route);
49087
+ if (id === undefined) {
49088
+ id = ++routeIdCount;
49089
+ routeIdByRoute.set(route, id);
49090
+ }
49091
+ key += `${id},`;
49092
+ }
49093
+ return key;
49094
+ };
49095
+
48898
49096
  /*
48899
49097
  * Custom hook to apply semi-transparent color when an element should be dimmed.
48900
49098
  *
@@ -49444,15 +49642,17 @@ const LinkWithRoute = ({
49444
49642
  assertRoute(route);
49445
49643
  }
49446
49644
  const url = route.buildUrl(routeParams);
49447
- const {
49448
- matching
49449
- } = useRouteStatus(route);
49450
- const paramsAreMatching = route.matchesParams(routeParams);
49451
- const someExceptedRouteMatching = useSomeRouteMatching(currentExcept);
49452
- const someAlsoRouteMatching = useSomeRouteMatching(currentAlso);
49453
- // "Except" is a veto: a route named there is somewhere else, whatever the
49454
- // rest of the reading says.
49455
- const linkMatching = !someExceptedRouteMatching && (matching && paramsAreMatching || someAlsoRouteMatching);
49645
+ {
49646
+ for (const amendingRoute of [currentExcept, currentAlso].flat()) {
49647
+ if (amendingRoute) {
49648
+ assertRoute(amendingRoute);
49649
+ }
49650
+ }
49651
+ }
49652
+ const linkMatching = useRouteCurrent(route, routeParams, {
49653
+ except: currentExcept,
49654
+ also: currentAlso
49655
+ });
49456
49656
  const innerCurrent = current || linkMatching;
49457
49657
  return jsx(Link, {
49458
49658
  href: url,
@@ -49461,25 +49661,6 @@ const LinkWithRoute = ({
49461
49661
  children: children || route.buildRelativeUrl(routeParams)
49462
49662
  });
49463
49663
  };
49464
- // A route, or a list of them, read as one answer: does any of them match.
49465
- // Reading matchingSignal during the render is what subscribes the component to
49466
- // it, so every route is read even once one has answered yes — stopping early
49467
- // would leave the component deaf to the ones it skipped.
49468
- const useSomeRouteMatching = routes => {
49469
- if (!routes) {
49470
- return false;
49471
- }
49472
- let someMatching = false;
49473
- for (const route of Array.isArray(routes) ? routes : [routes]) {
49474
- {
49475
- assertRoute(route);
49476
- }
49477
- if (route.matchingSignal.value) {
49478
- someMatching = true;
49479
- }
49480
- }
49481
- return someMatching;
49482
- };
49483
49664
  const LinkPlain = props => {
49484
49665
  const defaultRef = useRef();
49485
49666
  props.ref = props.ref || defaultRef;
@@ -54566,11 +54747,7 @@ const ButtonWithRoute = props => {
54566
54747
  assertRoute(route);
54567
54748
  }
54568
54749
  const url = route.buildUrl(routeParams);
54569
- const {
54570
- matching
54571
- } = useRouteStatus(route);
54572
- const paramsAreMatching = route.matchesParams(routeParams);
54573
- const linkMatching = matching && paramsAreMatching;
54750
+ const linkMatching = useRouteCurrent(route, routeParams);
54574
54751
 
54575
54752
  // Merged into whatever the caller already holds: a button can be forced into
54576
54753
  // a state for a demo and still learn its own current-ness from its route.
@@ -66003,10 +66180,24 @@ const useDialogProps = props => {
66003
66180
  // whichever show call ran, not just when a stray authored display
66004
66181
  // property is actually present — harmless the rest of the time.
66005
66182
  dialogEl.setAttribute("navi-hidden", "");
66006
- if (isTopLayerPopover) {
66007
- dialogEl.hidePopover();
66183
+ const hideNatively = () => {
66184
+ if (isTopLayerPopover) {
66185
+ dialogEl.hidePopover();
66186
+ } else {
66187
+ dialogEl.close();
66188
+ }
66189
+ };
66190
+ if (findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
66191
+ // Leaving the tree: hidden natively once the commit is over. Its
66192
+ // removal takes it out of the top layer and hands the focus to
66193
+ // nobody, where closing it now hands the focus to whatever held it
66194
+ // before the opening — often a trigger leaving in the same commit
66195
+ // (see giveFocusBackAfterCommit). What is left to close then is its
66196
+ // state: a <dialog> removed while open keeps its `open` attribute, and
66197
+ // a subtree <Suspense> parks and puts back would show it again.
66198
+ queueMicrotask(hideNatively);
66008
66199
  } else {
66009
- dialogEl.close();
66200
+ hideNatively();
66010
66201
  }
66011
66202
  // Held at the size it has right now, for the whole way out. cleanup()
66012
66203
  // below already stops the JS repositioning, but the size is CSS-driven
@@ -67711,7 +67902,13 @@ const usePopoverProps = props => {
67711
67902
  // reliably sufficient once a consumer's own CSS also sets display.
67712
67903
  popoverEl.setAttribute("navi-hidden", "");
67713
67904
  if (isTopLayer) {
67714
- popoverEl.hidePopover();
67905
+ // Left to its removal when the popover leaves the tree: that hides it
67906
+ // and hands the focus to nobody, where hidePopover() hands the focus
67907
+ // to whatever held it before the opening — often a trigger leaving in
67908
+ // the same commit (see giveFocusBackAfterCommit).
67909
+ if (!findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
67910
+ popoverEl.hidePopover();
67911
+ }
67715
67912
  } else {
67716
67913
  openLocalPopoverCount = Math.max(0, openLocalPopoverCount - 1);
67717
67914
  }
@@ -92004,8 +92201,7 @@ const createSlot = (SlotRenderer = Box) => {
92004
92201
  };
92005
92202
  const SlotFill = props => {
92006
92203
  const fillerRef = useRef();
92007
- filler = fillerRef;
92008
- slotPropsSignal.value = props;
92204
+ const routePage = useContext(RoutePageContext);
92009
92205
  useLayoutEffect(() => {
92010
92206
  return () => {
92011
92207
  // Within one diff preact mounts the newcomer before unmounting the
@@ -92018,6 +92214,26 @@ const createSlot = (SlotRenderer = Box) => {
92018
92214
  slotPropsSignal.value = null;
92019
92215
  };
92020
92216
  }, []);
92217
+ // In a page kept while leaving: out of the slot as if unmounted, since
92218
+ // the page arriving is what the slot must show — or nothing, when it
92219
+ // fills none.
92220
+ if (routePage && routePage.leavingSignal.value) {
92221
+ if (filler === fillerRef) {
92222
+ filler = null;
92223
+ slotPropsSignal.value = null;
92224
+ }
92225
+ return null;
92226
+ }
92227
+ if (!fillerRef.current) {
92228
+ fillerRef.current = true;
92229
+ filler = fillerRef;
92230
+ }
92231
+ if (filler === null) {
92232
+ filler = fillerRef;
92233
+ }
92234
+ if (filler === fillerRef) {
92235
+ slotPropsSignal.value = props;
92236
+ }
92021
92237
  return null;
92022
92238
  };
92023
92239
  return [Slot, SlotFill];