@jsenv/navi 0.29.25 → 0.29.27

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.
@@ -7,7 +7,7 @@ import { createContext, isValidElement, h, Fragment, toChildArray, render, optio
7
7
  import { useContext, useLayoutEffect, useRef, useCallback, useState, useMemo, useId, useEffect, useErrorBoundary } from "preact/hooks";
8
8
  import { jsx, jsxs, Fragment as Fragment$1 } from "preact/jsx-runtime";
9
9
  import { computed, signal, effect, batch, useSignal } from "@preact/signals";
10
- import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeTwoStyles, normalizeStyles, resolveCSSSize, measureLongestVisualLineWidth, hasCSSSizeUnit, resolveOklchLightness, contrastColor, createIterableWeakSet, getElementSignature, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, createEventGroupLogger, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, activeElementSignal, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, startDragToTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
10
+ import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeTwoStyles, normalizeStyles, resolveCSSSize, measureLongestVisualLineWidth, hasCSSSizeUnit, resolveOklchLightness, contrastColor, createIterableWeakSet, getElementSignature, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, createEventGroupLogger, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, activeElementSignal, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, startDragToTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
11
11
  export { contrastColor, startDragToReorder } from "@jsenv/dom";
12
12
  import { createValidity, parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, durationToISOString } from "@jsenv/validity";
13
13
  export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
@@ -2775,6 +2775,32 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2775
2775
  return false;
2776
2776
  }
2777
2777
 
2778
+ // Descending into a child path param means "this URL keeps the value you
2779
+ // are on" — right for a param that QUALIFIES a position (a tab, a mode),
2780
+ // wrong for one that NAMES a page. Two things must both hold for it to be
2781
+ // a name:
2782
+ //
2783
+ // - literal routes are declared for its other values ("/games/me/done").
2784
+ // Declaring them is the developer saying these values are places; a tab
2785
+ // nobody named a route after stays a qualifier;
2786
+ // - it has a default value, which makes THIS url the url of that default:
2787
+ // "/games/me" IS section=a-venir. Descending would leave the default
2788
+ // unaddressable — two states, one url. Without a default ("/map" is not
2789
+ // a panel, it is the absence of one) this url means nothing yet and
2790
+ // stays free to remember where you were.
2791
+ const thisUrlAlreadyMeansAParamValue = (connection) => {
2792
+ if (connection.paramType !== "path") {
2793
+ return false;
2794
+ }
2795
+ if (pathConnectionMap.has(connection.paramName)) {
2796
+ return false; // we carry that param ourselves, we are not its default
2797
+ }
2798
+ if (!connection.namedByLiteralRoutes) {
2799
+ return false;
2800
+ }
2801
+ return connection.getDefaultValue() !== undefined;
2802
+ };
2803
+
2778
2804
  // Check if child has active non-default signal values
2779
2805
  let hasActiveParams = false;
2780
2806
  const childParams = { ...compatibility.childParams };
@@ -2801,7 +2827,10 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2801
2827
  if (signalValue !== undefined) {
2802
2828
  // No explicit override - use signal value
2803
2829
  childParams[paramName] = signalValue;
2804
- if (connection.isCustomValue(signalValue)) {
2830
+ if (
2831
+ connection.isCustomValue(signalValue) &&
2832
+ !thisUrlAlreadyMeansAParamValue(connection)
2833
+ ) {
2805
2834
  hasActiveParams = true;
2806
2835
  }
2807
2836
  }
@@ -5323,6 +5352,57 @@ const setupRoutePatterns = (routePatterns) => {
5323
5352
  collectDescendantPathSignals(routePattern);
5324
5353
  routePattern.descendantPathSignals = descendantPathSignalsByIndex;
5325
5354
  }
5355
+ // Phase 5b: Flag path params whose values are ALSO declared as literal routes
5356
+ // ("/games/me/done" next to "/games/me/:section"). That declaration is the
5357
+ // only reliable statement that the param names pages rather than qualifying
5358
+ // one — read by shouldUseChildRoute to decide whether an ancestor url may
5359
+ // descend into it.
5360
+ for (const routePattern of routePatternSet) {
5361
+ for (const connection of routePattern.connections) {
5362
+ if (connection.paramType !== "path") {
5363
+ continue;
5364
+ }
5365
+ const paramSegment = routePattern.pattern.segments.find(
5366
+ (seg) => seg.type === "param" && seg.name === connection.paramName,
5367
+ );
5368
+ if (!paramSegment) {
5369
+ continue;
5370
+ }
5371
+ const { index } = paramSegment;
5372
+ const sharesPathUpTo = (otherSegments) => {
5373
+ for (let i = 0; i < index; i++) {
5374
+ const seg = routePattern.pattern.segments[i];
5375
+ const otherSeg = otherSegments[i];
5376
+ if (!otherSeg) {
5377
+ return false;
5378
+ }
5379
+ if (seg.type === "literal" && otherSeg.type === "literal") {
5380
+ if (seg.value !== otherSeg.value) {
5381
+ return false;
5382
+ }
5383
+ } else if (seg.type !== otherSeg.type) {
5384
+ return false;
5385
+ }
5386
+ }
5387
+ return true;
5388
+ };
5389
+ for (const otherPattern of routePatternSet) {
5390
+ if (otherPattern === routePattern) {
5391
+ continue;
5392
+ }
5393
+ const otherSegments = otherPattern.pattern.segments;
5394
+ const otherSegment = otherSegments[index];
5395
+ if (!otherSegment || otherSegment.type !== "literal") {
5396
+ continue;
5397
+ }
5398
+ if (!sharesPathUpTo(otherSegments)) {
5399
+ continue;
5400
+ }
5401
+ connection.namedByLiteralRoutes = true;
5402
+ break;
5403
+ }
5404
+ }
5405
+ }
5326
5406
  // Phase 6: Calculate depths for all patterns
5327
5407
  for (const routePattern of routePatternSet) {
5328
5408
  calculatePatternDepth(routePattern);
@@ -34654,17 +34734,25 @@ const Route = props => {
34654
34734
  });
34655
34735
  };
34656
34736
  /**
34657
- * The routes a tree of <Route> children is made of, in the order they are
34737
+ * The pages a tree of <Route> children is made of, in the order they are
34658
34738
  * written. Reading them is what turns a router into a row one can walk: "one
34659
34739
  * step that way" is a fact about the order the branches were declared in, and
34660
34740
  * nothing in a URL says it.
34661
34741
  *
34742
+ * A page is `{ route, params }`, never the route alone: a section of a page is
34743
+ * as often a PARAM as it is a route of its own — `<Route route={PAGE}
34744
+ * routeParams={{ section: "done" }}>` is how this very file selects a branch on
34745
+ * one — and three branches of the same route are then the same object three
34746
+ * times. Told apart by their params, they are three pages one walks between;
34747
+ * told apart by identity, they are one page and there is nowhere to walk.
34748
+ * `params` is undefined for a branch that is a route on its own.
34749
+ *
34662
34750
  * The same walk the container does to find the active branch (collectBranches),
34663
34751
  * except that it keeps every leaf rather than the one that matches — and reads
34664
34752
  * no signal, so asking does not subscribe the asker to anything.
34665
34753
  */
34666
- const collectRoutes = children => {
34667
- const routes = [];
34754
+ const collectRoutePages = children => {
34755
+ const pages = [];
34668
34756
  const visit = child => {
34669
34757
  if (!child || child === true || child === false) {
34670
34758
  return;
@@ -34680,18 +34768,22 @@ const collectRoutes = children => {
34680
34768
  }
34681
34769
  const {
34682
34770
  children: nodeChildren,
34683
- route
34771
+ route,
34772
+ routeParams
34684
34773
  } = child.props;
34685
34774
  if (nodeChildren) {
34686
34775
  visit(nodeChildren);
34687
34776
  return;
34688
34777
  }
34689
34778
  if (route) {
34690
- routes.push(route);
34779
+ pages.push({
34780
+ route,
34781
+ params: routeParams
34782
+ });
34691
34783
  }
34692
34784
  };
34693
34785
  visit(children);
34694
- return routes;
34786
+ return pages;
34695
34787
  };
34696
34788
 
34697
34789
  // RouteContainer: traverses children statically per render, finds the active branch,
@@ -34897,14 +34989,19 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
34897
34989
  // transition carries was measured once, at the start, against a destination
34898
34990
  // this travel is no longer going to.
34899
34991
  const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
34992
+ // The name the box wears while it travels, and only then (see nameForTravel).
34993
+ const TRAVEL_NAME = "navi-route-travel";
34900
34994
  const css$R = /* css */`
34995
+ /* The name that makes the page inside this box a picture of its own during a
34996
+ transition — rather than part of the one big picture the document takes, so
34997
+ the two pages can move past each other while everything else stays where it
34998
+ is — is not written here: it is worn only for the length of a travel (see
34999
+ nameForTravel). A name belongs to ONE element at a time, and a page can hold
35000
+ several of these boxes at once — a section of the url and a search param of
35001
+ the root route are two rows of tabs, both live, and only one of them is ever
35002
+ travelling. */
34901
35003
  .navi_route_travel {
34902
35004
  position: relative;
34903
- /* Named, so the page inside this box is a picture of its own during a
34904
- transition rather than part of the one big picture the document takes:
34905
- the two pages can then move past each other while everything else stays
34906
- where it is. */
34907
- view-transition-name: navi-route-travel;
34908
35005
  /* The gesture takes the axis the pages travel on and leaves the other one
34909
35006
  to the page, so a list still scrolls under the same finger. */
34910
35007
  touch-action: pan-y;
@@ -35102,21 +35199,30 @@ const css$R = /* css */`
35102
35199
 
35103
35200
  /**
35104
35201
  * @type {import("ignore:preact").FunctionComponent<{
35105
- * routes?: Array<object>,
35202
+ * routes?: Array<object|{route: object, params?: object}>,
35106
35203
  * axis?: "x"|"y",
35107
35204
  * travelByDrag?: boolean,
35108
- * onTravel?: (detail: {route: object, cause: string}) => void|Promise<void>,
35205
+ * onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
35109
35206
  * }>}
35110
- * @param {Array<object>} [props.routes] - the tabs, in the order they are shown.
35111
- * Read from the <Route> children by default, in the order they are written:
35112
- * the router already holds that list, and asking a caller to write it twice is
35113
- * asking for the two to disagree. Pass it to say another order, or when the
35114
- * pages are not children of this box.
35207
+ * @param {Array<object|{route: object, params?: object}>} [props.routes] - the
35208
+ * tabs, in the order they are shown. Read from the <Route> children by
35209
+ * default, in the order they are written: the router already holds that list,
35210
+ * and asking a caller to write it twice is asking for the two to disagree.
35211
+ * Pass it to say another order, when the pages are not children of this box,
35212
+ * or to name a tab the children cannot — the section a <Route fallback> shows
35213
+ * is a tab like the others, and only its params say which one.
35214
+ *
35215
+ * An entry is a route, or `{ route, params }` when the tabs of the row are a
35216
+ * PARAM of one route rather than routes of their own (the form
35217
+ * `<Route routeParams>` selects a branch on, and the form that lets a link
35218
+ * with no params reopen the section one was looking at). Written as bare
35219
+ * routes, three tabs of one route are the same object three times: there is
35220
+ * then one tab, and nowhere to travel.
35115
35221
  * @param {"x"|"y"} [props.axis="x"] - which way the pages are laid out.
35116
35222
  * @param {boolean} [props.travelByDrag=true] - whether a pointer dragging the
35117
35223
  * page travels. Off where the gesture belongs to the content.
35118
- * @param {(detail: {route: object, cause: "drag"|"wheel"|"revert"}) => void|Promise<void>} [props.onTravel]
35119
- * - how to go to a route. The default REPLACES the current history entry
35224
+ * @param {(detail: {route: object, params: object|undefined, cause: "drag"|"wheel"|"revert"}) => void|Promise<void>} [props.onTravel]
35225
+ * - how to go to a tab. The default REPLACES the current history entry
35120
35226
  * rather than pushing one: a swipe is how one browses a page, not a place one
35121
35227
  * aimed at, and three swipes back and forth must not bury the way out of the
35122
35228
  * page under six entries. A tab pressed is the other case and pushes, which
@@ -35139,8 +35245,9 @@ const RouteTravel = ({
35139
35245
  axis = "x",
35140
35246
  travelByDrag = true,
35141
35247
  onTravel = ({
35142
- route
35143
- }) => route.redirectTo(),
35248
+ route,
35249
+ params
35250
+ }) => route.redirectTo(params),
35144
35251
  className,
35145
35252
  children,
35146
35253
  ...rest
@@ -35152,51 +35259,58 @@ const RouteTravel = ({
35152
35259
  // left, the animations the finger drives, and what to do with them once the
35153
35260
  // browser has them ready. Null when no page is on its way anywhere.
35154
35261
  const travelRef = useRef(null);
35155
- // The route this box has ASKED for and is still waiting to see arrive.
35262
+ // The page this box has ASKED for and is still waiting to see arrive.
35156
35263
  // Routing is asynchronous: a travel's own navigation lands well after the
35157
35264
  // travel decided anything about it — sometimes after the travel was undone —
35158
- // and read back as "the route changed" it would start a second travel nobody
35265
+ // and read back as "the page changed" it would start a second travel nobody
35159
35266
  // asked for, over pictures that are already showing something else.
35160
- const routeAskedForRef = useRef(null);
35267
+ const pageAskedForRef = useRef(null);
35161
35268
  // What a press stopped in flight, until the gesture says what it is about.
35162
35269
  const caughtAtPressRef = useRef(null);
35163
35270
  // The latest way to answer a gesture, for a watcher that outlives every
35164
35271
  // render (see the wheel effect below).
35165
35272
  const travelHandlersRef = useRef(null);
35166
35273
  const pointerDownRef = useRef(null);
35167
- const routesFromChildren = useMemo(() => collectRoutes(children), [children]);
35168
- const routes = routesProp || routesFromChildren;
35169
-
35170
- // Which page is on screen, read from the routes themselves: every one of them
35171
- // is read, so this re-renders when any of them starts or stops matching.
35172
- let currentIndex = -1;
35173
- for (let i = 0; i < routes.length; i++) {
35174
- if (routes[i].matchingSignal.value) {
35175
- currentIndex = i;
35176
- }
35177
- }
35274
+ const pagesFromChildren = useMemo(() => collectRoutePages(children), [children]);
35275
+ const pagesFromProp = useMemo(() => routesProp && routesProp.map(normalizePage), [routesProp]);
35276
+ const pages = pagesFromProp || pagesFromChildren;
35277
+
35278
+ // Which page is on screen, read from the pages themselves: every one of them
35279
+ // is read, so this re-renders when any of them starts or stops matching — and
35280
+ // for a row whose tabs are params of one route, when the params move from one
35281
+ // tab to the next (see pageIsCurrent).
35282
+ const currentIndex = currentPageIndex(pages);
35178
35283
  // The page that was on screen when the change now happening was asked for:
35179
35284
  // a travel is between two of them, and by the time anything renders the first
35180
35285
  // one is already gone. Written after each render (below), so a subscriber
35181
35286
  // reading it — they all run before Preact flushes — reads the one being left.
35182
35287
  const currentIndexRef = useRef(currentIndex);
35183
35288
 
35289
+ // Where a page is asked for, whoever asks: a page is a route AND the params
35290
+ // that say which of its tabs, and a caller told only the route would send the
35291
+ // row back to whichever tab the URL already says (see redirectTo).
35292
+ const travelTo = (page, cause) => onTravel({
35293
+ route: page.route,
35294
+ params: page.params,
35295
+ cause
35296
+ });
35297
+
35184
35298
  // One travel, whoever asked for it: a finger, a tab pressed, the browser's
35185
35299
  // own back button. What differs is only who moves it — the finger drives it
35186
35300
  // frame by frame (`scrub`), everything else lets it play.
35187
35301
  const beginTravel = ({
35188
- route,
35189
- fromRoute,
35302
+ page,
35303
+ fromPage,
35190
35304
  direction,
35191
35305
  scrub,
35192
35306
  change
35193
35307
  }) => {
35194
35308
  const travel = {
35195
- route,
35309
+ page,
35196
35310
  // The page this set off from, kept rather than looked up again: the URL
35197
35311
  // changes at the first pixel, so a moment later nothing on screen
35198
35312
  // remembers where it started.
35199
- fromRoute,
35313
+ fromPage,
35200
35314
  direction,
35201
35315
  scrub,
35202
35316
  ratio: 0,
@@ -35207,12 +35321,16 @@ const RouteTravel = ({
35207
35321
  ended: false
35208
35322
  };
35209
35323
  travelRef.current = travel;
35324
+ // Taken before the picture is: the browser reads the name off the DOM as it
35325
+ // stands when the transition starts, and this box is only a picture of its
35326
+ // own for as long as it is the one travelling.
35327
+ nameForTravel(elementRef.current);
35210
35328
  document.documentElement.setAttribute(TRAVEL_ATTRIBUTE, direction);
35211
35329
  if (scrub) {
35212
35330
  holdPictures(travel);
35213
35331
  document.documentElement.setAttribute(DRAGGED_ATTRIBUTE, "");
35214
35332
  }
35215
- routeAskedForRef.current = route;
35333
+ pageAskedForRef.current = page;
35216
35334
  // The box as it stands before anything moves: rendering is held, so this is
35217
35335
  // still the page being left (see holdTravelHeight).
35218
35336
  const heightBefore = elementRef.current.getBoundingClientRect().height;
@@ -35223,7 +35341,7 @@ const RouteTravel = ({
35223
35341
  // The picture the browser is about to take must be of the page that was
35224
35342
  // asked for, and a route matching is not yet a page rendered.
35225
35343
  const viewTransition = startViewTransition(async () => {
35226
- await whileRouteRenders(route, async () => {
35344
+ await whilePageRenders(page, async () => {
35227
35345
  releaseRendering();
35228
35346
  if (change) {
35229
35347
  await change();
@@ -35276,21 +35394,41 @@ const RouteTravel = ({
35276
35394
  };
35277
35395
 
35278
35396
  // A page change nobody here asked for: a tab pressed, a key, the back button.
35279
- // The transition is started from the route's own announcement rather than
35280
- // from a render, because a render is one flush too late — by then the DOM
35281
- // holds the new page and the picture of the old one cannot be taken anymore.
35397
+ // The transition is started from what the router SAYS rather than from a
35398
+ // render, because a render is one flush too late — by then the DOM holds the
35399
+ // new page and the picture of the old one cannot be taken anymore.
35400
+ //
35401
+ // Watched as a position in the row rather than route by route. A route
35402
+ // announces its own status, and the row's tabs can all be one route: the
35403
+ // announcement then says a section changed without saying which is on screen,
35404
+ // and it says it about things this row does not move for (a route that has
35405
+ // now been visited, a param of its own that is not a tab of this row). Worse,
35406
+ // a status is published from inside the routing and the params it carries are
35407
+ // the ones known at that instant — a section that lands as a signal settles
35408
+ // is announced late, or not at all. The signals ARE the position, so the
35409
+ // position is read from them: one computed over the whole row, notified once
35410
+ // per move, whichever route moved and whether by matching or by params.
35282
35411
  useLayoutEffect(() => {
35283
- const unsubscribes = routes.map((route, index) => route.subscribeStatus(({
35284
- matching
35285
- }) => {
35286
- if (!matching) {
35412
+ const currentIndexSignal = computed(() => currentPageIndex(pages));
35413
+ const onRowMove = index => {
35414
+ if (index === -1) {
35287
35415
  return;
35288
35416
  }
35417
+ if (index === currentIndexRef.current) {
35418
+ // Where the row already was — the first reading of all, and any move
35419
+ // this box has already taken note of (a render writes it too). What it
35420
+ // was still waiting for is here nonetheless, so the wait is called off.
35421
+ if (samePage(pageAskedForRef.current, pages[index])) {
35422
+ pageAskedForRef.current = null;
35423
+ }
35424
+ return;
35425
+ }
35426
+ const page = pages[index];
35289
35427
  // A page this box asked for itself — a travel's own navigation, or one
35290
35428
  // it had given up waiting on: what arrives here is the answer to a
35291
35429
  // question already answered, not somebody going somewhere.
35292
- if (routeAskedForRef.current === route) {
35293
- routeAskedForRef.current = null;
35430
+ if (samePage(pageAskedForRef.current, page)) {
35431
+ pageAskedForRef.current = null;
35294
35432
  currentIndexRef.current = index;
35295
35433
  return;
35296
35434
  }
@@ -35298,13 +35436,13 @@ const RouteTravel = ({
35298
35436
  // is not coming, or no longer means anything. Forgotten here rather
35299
35437
  // than kept, or the next press on that very tab would be taken for the
35300
35438
  // late answer to a question nobody remembers asking.
35301
- routeAskedForRef.current = null;
35439
+ pageAskedForRef.current = null;
35302
35440
  // Asked for a page while one was already on its way: the travel in
35303
35441
  // flight is the answer, aimed somewhere else. Starting a second one on
35304
35442
  // top would leave this one's pictures to be dropped mid-slide.
35305
35443
  if (travelRef.current) {
35306
35444
  currentIndexRef.current = index;
35307
- retargetTravel(travelRef.current, route);
35445
+ retargetTravel(travelRef.current, page);
35308
35446
  return;
35309
35447
  }
35310
35448
  const fromIndex = currentIndexRef.current;
@@ -35315,18 +35453,18 @@ const RouteTravel = ({
35315
35453
  return;
35316
35454
  }
35317
35455
  beginTravel({
35318
- route,
35319
- fromRoute: routes[fromIndex],
35456
+ page,
35457
+ fromPage: pages[fromIndex],
35320
35458
  direction: index > fromIndex ? "forward" : "back",
35321
35459
  scrub: false
35322
35460
  });
35323
- }));
35324
- return () => {
35325
- for (const unsubscribe of unsubscribes) {
35326
- unsubscribe();
35327
- }
35328
35461
  };
35329
- }, [routes]);
35462
+ // `subscribe` rather than `effect`: it hands the value to a callback that
35463
+ // is NOT being tracked, and what this one does — navigate, ask the router
35464
+ // for another page — reads and writes the very signals the row is watched
35465
+ // through.
35466
+ return currentIndexSignal.subscribe(onRowMove);
35467
+ }, [pages]);
35330
35468
 
35331
35469
  // Rendering is held for the length of a navigation, so that whatever picture
35332
35470
  // this box is about to take is of the page being LEFT (see holdRendering).
@@ -35363,6 +35501,13 @@ const RouteTravel = ({
35363
35501
  // Let go of far enough: the movement carries on from under the finger, at its
35364
35502
  // own pace, to the end.
35365
35503
  const finishTravel = travel => {
35504
+ // Nobody is driving it anymore. `scrub` is who MOVES the pictures, not what
35505
+ // set them off: left standing after the release, this travel would go on
35506
+ // claiming a hand that is no longer there — and everything that asks
35507
+ // "is somebody holding this?" before touching it (a press on the tab one
35508
+ // came from, a wheel push) would be answered yes and do nothing, while the
35509
+ // pages carry on to a page the router has already left.
35510
+ travel.scrub = false;
35366
35511
  releaseHold();
35367
35512
  travel.viewTransition.finished.then(() => endTravel(travel), () => endTravel(travel));
35368
35513
  };
@@ -35415,19 +35560,20 @@ const RouteTravel = ({
35415
35560
  Promise.resolve();
35416
35561
  backAtTheStart.then(async () => {
35417
35562
  try {
35418
- routeAskedForRef.current = travel.fromRoute;
35563
+ pageAskedForRef.current = travel.fromPage;
35419
35564
  // The page that was left is put back UNDER the picture before the
35420
35565
  // picture is dropped, so the two are the same thing at the moment they
35421
35566
  // are swapped: that only holds once the page is really back.
35422
- if (travel.fromRoute.matchingSignal.peek()) {
35567
+ if (pageIsCurrent(travel.fromPage)) {
35423
35568
  // It never left: the press that set this revert off put it back
35424
35569
  // there, and the pages have been held where they were until now.
35425
35570
  // Nothing to ask for, and nothing to wait for — waiting anyway is a
35426
35571
  // render that never comes and a page frozen under its own pictures.
35427
35572
  releaseRendering();
35428
35573
  } else {
35429
- await whileRouteRenders(travel.fromRoute, () => onTravel({
35430
- route: travel.fromRoute,
35574
+ await whilePageRenders(travel.fromPage, () => onTravel({
35575
+ route: travel.fromPage.route,
35576
+ params: travel.fromPage.params,
35431
35577
  cause: "revert"
35432
35578
  }));
35433
35579
  }
@@ -35466,8 +35612,8 @@ const RouteTravel = ({
35466
35612
  // change — only what is being brought in against it, and that one is LIVE:
35467
35613
  // pointing the router elsewhere is all it takes for the picture to show that
35468
35614
  // page instead.
35469
- const redirectTravel = (travel, route, direction) => {
35470
- travel.route = route;
35615
+ const redirectTravel = (travel, page, direction) => {
35616
+ travel.page = page;
35471
35617
  // Everything the transition carries that is NOT the pages was measured
35472
35618
  // against a destination this travel is no longer going to (see the CSS).
35473
35619
  document.documentElement.setAttribute(TURNED_ATTRIBUTE, "");
@@ -35488,28 +35634,28 @@ const RouteTravel = ({
35488
35634
 
35489
35635
  // Somebody asked for a page while one was on its way. Where they asked for
35490
35636
  // decides what that means.
35491
- const retargetTravel = (travel, route) => {
35637
+ const retargetTravel = (travel, page) => {
35492
35638
  if (travel.scrub || travel.reverting || travel.ended || travel.noPicture) {
35493
35639
  // A hand is holding the pages, or they are already on their way back:
35494
35640
  // either way this travel's end is decided by somebody else.
35495
35641
  return;
35496
35642
  }
35497
- if (route === travel.route) {
35643
+ if (samePage(page, travel.page)) {
35498
35644
  // Already on its way there.
35499
35645
  return;
35500
35646
  }
35501
- if (route === travel.fromRoute) {
35647
+ if (samePage(page, travel.fromPage)) {
35502
35648
  // Back where it set off from: that is not another travel, it is this one
35503
35649
  // undone — the same pictures, run backwards.
35504
35650
  revertTravel(travel);
35505
35651
  return;
35506
35652
  }
35507
- const fromIndex = routes.indexOf(travel.fromRoute);
35508
- const toIndex = routes.indexOf(route);
35653
+ const fromIndex = pageIndexOf(pages, travel.fromPage);
35654
+ const toIndex = pageIndexOf(pages, page);
35509
35655
  if (fromIndex === -1 || toIndex === -1) {
35510
35656
  return;
35511
35657
  }
35512
- redirectTravel(travel, route, toIndex > fromIndex ? "forward" : "back");
35658
+ redirectTravel(travel, page, toIndex > fromIndex ? "forward" : "back");
35513
35659
  };
35514
35660
  const endTravel = travel => {
35515
35661
  if (travel.ended) {
@@ -35523,6 +35669,10 @@ const RouteTravel = ({
35523
35669
  releaseHold(travel);
35524
35670
  if (travelRef.current === travel) {
35525
35671
  travelRef.current = null;
35672
+ // Given back, so another box may wear it: kept, two of them on a page
35673
+ // would both answer to it and the browser refuses the whole transition
35674
+ // rather than pick.
35675
+ unnameAfterTravel(elementRef.current);
35526
35676
  document.documentElement.removeAttribute(TRAVEL_ATTRIBUTE);
35527
35677
  document.documentElement.removeAttribute(DRAGGED_ATTRIBUTE);
35528
35678
  document.documentElement.removeAttribute(TURNED_ATTRIBUTE);
@@ -35587,24 +35737,21 @@ const RouteTravel = ({
35587
35737
  }
35588
35738
  // Dragging the page towards the end of the axis brings in what is
35589
35739
  // BEFORE it, the way pushing a sheet to the right reveals its left.
35590
- const route = sign > 0 ? routes[currentIndex - 1] : routes[currentIndex + 1];
35591
- if (!route || !size || scrollRoomTowards(target, elementRef.current, axis, sign)) {
35740
+ const page = sign > 0 ? pages[currentIndex - 1] : pages[currentIndex + 1];
35741
+ if (!page || !size || scrollRoomTowards(target, elementRef.current, axis, sign)) {
35592
35742
  return false;
35593
35743
  }
35594
35744
  if (CAN_KEEP_PICTURE) {
35595
35745
  beginTravel({
35596
- route,
35597
- fromRoute: routes[currentIndex],
35746
+ page,
35747
+ fromPage: pages[currentIndex],
35598
35748
  direction: sign > 0 ? "back" : "forward",
35599
35749
  scrub: true,
35600
- change: () => onTravel({
35601
- route,
35602
- cause: "drag"
35603
- })
35750
+ change: () => travelTo(page, "drag")
35604
35751
  });
35605
35752
  } else {
35606
35753
  travelRef.current = {
35607
- route,
35754
+ page,
35608
35755
  noPicture: true,
35609
35756
  ended: false
35610
35757
  };
@@ -35656,9 +35803,9 @@ const RouteTravel = ({
35656
35803
  // Where we are is what THIS travel was bringing in — the URL changed at
35657
35804
  // the first pixel, so `currentIndex` belongs to a render this gesture
35658
35805
  // is older than.
35659
- const fromIndex = routes.indexOf(travel.route);
35660
- const route = direction === "back" ? routes[fromIndex - 1] : routes[fromIndex + 1];
35661
- if (fromIndex === -1 || !route) {
35806
+ const fromIndex = pageIndexOf(pages, travel.page);
35807
+ const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
35808
+ if (fromIndex === -1 || !page) {
35662
35809
  return false;
35663
35810
  }
35664
35811
  // At their very end before they are let go of: what ends the travel in
@@ -35668,14 +35815,11 @@ const RouteTravel = ({
35668
35815
  scrubTravel(travel, 1);
35669
35816
  travel.ratio = 1;
35670
35817
  beginTravel({
35671
- route,
35672
- fromRoute: travel.route,
35818
+ page,
35819
+ fromPage: travel.page,
35673
35820
  direction,
35674
35821
  scrub: true,
35675
- change: () => onTravel({
35676
- route,
35677
- cause: "drag"
35678
- })
35822
+ change: () => travelTo(page, "drag")
35679
35823
  });
35680
35824
  return {
35681
35825
  size: boxSizeOnAxis(),
@@ -35690,17 +35834,14 @@ const RouteTravel = ({
35690
35834
  // other neighbour is enough for it to show that one instead. The travel
35691
35835
  // turns around where it stands, on the same transition and under the same
35692
35836
  // hand, and there is no gap at all.
35693
- const fromIndex = routes.indexOf(travel.fromRoute);
35694
- const route = direction === "back" ? routes[fromIndex - 1] : routes[fromIndex + 1];
35695
- if (fromIndex === -1 || !route) {
35837
+ const fromIndex = pageIndexOf(pages, travel.fromPage);
35838
+ const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
35839
+ if (fromIndex === -1 || !page) {
35696
35840
  return false;
35697
35841
  }
35698
- redirectTravel(travel, route, direction);
35699
- routeAskedForRef.current = route;
35700
- onTravel({
35701
- route,
35702
- cause: "drag"
35703
- });
35842
+ redirectTravel(travel, page, direction);
35843
+ pageAskedForRef.current = page;
35844
+ travelTo(page, "drag");
35704
35845
  return {
35705
35846
  size: boxSizeOnAxis(),
35706
35847
  travelBack: sign > 0,
@@ -35723,10 +35864,7 @@ const RouteTravel = ({
35723
35864
  if (travel.noPicture) {
35724
35865
  travelRef.current = null;
35725
35866
  if (travels) {
35726
- onTravel({
35727
- route: travel.route,
35728
- cause: "drag"
35729
- });
35867
+ travelTo(travel.page, "drag");
35730
35868
  }
35731
35869
  return;
35732
35870
  }
@@ -35758,6 +35896,16 @@ const RouteTravel = ({
35758
35896
  if (currentIndex === -1) {
35759
35897
  return;
35760
35898
  }
35899
+ // A press that never became a gesture: whatever it stopped goes on its way,
35900
+ // from where the finger caught it.
35901
+ const giveUp = () => {
35902
+ gestureRef.current = null;
35903
+ const caught = caughtAtPressRef.current;
35904
+ caughtAtPressRef.current = null;
35905
+ if (caught && !caught.ended) {
35906
+ releaseHold(caught);
35907
+ }
35908
+ };
35761
35909
  const gesture = startDragToTravel(pointerDownEvent, {
35762
35910
  element: elementRef.current,
35763
35911
  axes: axis,
@@ -35765,17 +35913,14 @@ const RouteTravel = ({
35765
35913
  // is answered from its first pixel, on the axis the pages travel.
35766
35914
  immediate: caughtAtPressRef.current ? axis : false,
35767
35915
  ...travelHandlers,
35768
- onGiveUp: () => {
35769
- gestureRef.current = null;
35770
- // A press that never became a gesture: whatever it stopped goes on its
35771
- // way, from where the finger caught it.
35772
- const caught = caughtAtPressRef.current;
35773
- caughtAtPressRef.current = null;
35774
- if (caught && !caught.ended) {
35775
- releaseHold(caught);
35776
- }
35777
- }
35916
+ onGiveUp: giveUp
35778
35917
  });
35918
+ if (!gesture) {
35919
+ // Not a press this box can be about — something that reads the pointer
35920
+ // itself, a box below it that travels the same way.
35921
+ giveUp();
35922
+ return;
35923
+ }
35779
35924
  gestureRef.current = gesture;
35780
35925
  };
35781
35926
 
@@ -35827,13 +35972,13 @@ const RouteTravel = ({
35827
35972
  }
35828
35973
  // Where the box is going, which is not where it is: a step asked for while
35829
35974
  // a travel plays is the page after the one on its way.
35830
- const fromRoute = travelInFlight ? travelInFlight.route : routes[currentIndex];
35831
- const fromIndex = routes.indexOf(fromRoute);
35975
+ const fromPage = travelInFlight ? travelInFlight.page : pages[currentIndex];
35976
+ const fromIndex = pageIndexOf(pages, fromPage);
35832
35977
  if (fromIndex === -1) {
35833
35978
  return;
35834
35979
  }
35835
- const route = sign > 0 ? routes[fromIndex - 1] : routes[fromIndex + 1];
35836
- if (!route) {
35980
+ const page = sign > 0 ? pages[fromIndex - 1] : pages[fromIndex + 1];
35981
+ if (!page) {
35837
35982
  return;
35838
35983
  }
35839
35984
  if (travelInFlight) {
@@ -35844,14 +35989,11 @@ const RouteTravel = ({
35844
35989
  travelInFlight.ratio = 1;
35845
35990
  }
35846
35991
  beginTravel({
35847
- route,
35848
- fromRoute,
35992
+ page,
35993
+ fromPage,
35849
35994
  direction: sign > 0 ? "back" : "forward",
35850
35995
  scrub: false,
35851
- change: () => onTravel({
35852
- route,
35853
- cause: "wheel"
35854
- })
35996
+ change: () => travelTo(page, "wheel")
35855
35997
  });
35856
35998
  };
35857
35999
 
@@ -35882,7 +36024,15 @@ const RouteTravel = ({
35882
36024
  // page (see drag_to_travel.js).
35883
36025
  ,
35884
36026
 
35885
- "data-drag-travel": travelByDrag ? axis : undefined,
36027
+ "data-drag-travel": travelByDrag ? axis : undefined
36028
+ // The same fact said once per gesture, and for the other question the
36029
+ // DOM answers: a box that travels INSIDE this one — a row of slides in a
36030
+ // page — takes the axis it walks, and these are what it reads to know
36031
+ // this box walks it too.
36032
+ ,
36033
+
36034
+ "data-travel-by-drag": travelByDrag ? axis : undefined,
36035
+ "data-travel-by-wheel": travelByDrag ? axis : undefined,
35886
36036
  onPointerDown: onPointerDown,
35887
36037
  children: children
35888
36038
  });
@@ -36076,7 +36226,7 @@ const scrubTravel = (travel, ratio) => {
36076
36226
  }
36077
36227
  };
36078
36228
 
36079
- // A route change, carried out and then waited for until the page it selects is
36229
+ // A page change, carried out and then waited for until the page it selects is
36080
36230
  // really on screen. The container doing the swapping is the only one who knows
36081
36231
  // when that is (observeRouteRender): a route matching is a signal changing, and
36082
36232
  // how many passes Preact takes to answer it is its own business.
@@ -36086,7 +36236,7 @@ const scrubTravel = (travel, ratio) => {
36086
36236
  // inside the callback of a view transition: the browser has stopped rendering
36087
36237
  // and is waiting on this very promise to take its picture, so a wait that never
36088
36238
  // ends is a page frozen under a transition that never became ready.
36089
- const whileRouteRenders = async (route, change) => {
36239
+ const whilePageRenders = async (page, change) => {
36090
36240
  let stopListening;
36091
36241
  const rendered = new Promise(resolve => {
36092
36242
  // Listened for before the change, or a render landing while the change is
@@ -36095,7 +36245,7 @@ const whileRouteRenders = async (route, change) => {
36095
36245
  });
36096
36246
  try {
36097
36247
  await change();
36098
- if (route.matchingSignal.peek()) {
36248
+ if (pageIsCurrent(page)) {
36099
36249
  await rendered;
36100
36250
  }
36101
36251
  } finally {
@@ -36103,9 +36253,76 @@ const whileRouteRenders = async (route, change) => {
36103
36253
  }
36104
36254
  };
36105
36255
 
36256
+ // A page of the row: a route, and the params that say which of its tabs when
36257
+ // several of them share it. Written as a bare route by a caller whose tabs are
36258
+ // routes of their own — which is the same page with nothing to tell apart.
36259
+ const normalizePage = page => page.isRoute ? {
36260
+ route: page,
36261
+ params: undefined
36262
+ } : page;
36263
+
36264
+ // Two pages are the same page when they select the same thing, not when they
36265
+ // were written by the same hand: the params of a tab are a literal in JSX, so
36266
+ // every render builds another object for what is plainly the same tab.
36267
+ const samePage = (a, b) => {
36268
+ if (a === b) {
36269
+ return true;
36270
+ }
36271
+ if (!a || !b) {
36272
+ return false;
36273
+ }
36274
+ return a.route === b.route && compareTwoJsValues(a.params, b.params);
36275
+ };
36276
+ const pageIndexOf = (pages, page) => pages.findIndex(candidate => samePage(candidate, page));
36277
+
36278
+ // Whether this page is the one on screen. `matchesParams` reads paramsSignal,
36279
+ // so a caller reading this during a render is subscribed to the param changes
36280
+ // that walk from one tab to the next — matchingSignal alone never moves there,
36281
+ // and a row whose tabs are params of one route would never re-render.
36282
+ //
36283
+ // The params are read only for a route that matches, and that is not a signal
36284
+ // left unread: a reader wakes on anything it read last time, so what matters is
36285
+ // that everything able to make this answer change is among them.
36286
+ // matchingSignal is read whatever happens, and it is a NECESSARY condition —
36287
+ // while it is false no param of that route can put this page on screen, and the
36288
+ // day one could, matchingSignal itself has to turn true to say so, which is the
36289
+ // read that brings the params back in. (Asking anyway would be worse than
36290
+ // useless: the params of a route that does not match are not params.)
36291
+ const pageIsCurrent = ({
36292
+ route,
36293
+ params
36294
+ }) => {
36295
+ if (!route.matchingSignal.value) {
36296
+ return false;
36297
+ }
36298
+ return params ? route.matchesParams(params) : true;
36299
+ };
36300
+ // Every page is read, never only up to the one that answers yes: a page that is
36301
+ // not the current one today is the one that must wake the reader tomorrow.
36302
+ const currentPageIndex = pages => {
36303
+ let currentIndex = -1;
36304
+ for (let i = 0; i < pages.length; i++) {
36305
+ if (pageIsCurrent(pages[i])) {
36306
+ currentIndex = i;
36307
+ }
36308
+ }
36309
+ return currentIndex;
36310
+ };
36311
+
36106
36312
  // A transition skipped by another one starting is an outcome, not a failure.
36107
36313
  const ignoreSkipped = () => {};
36108
36314
 
36315
+ // The name is lent to the box that is travelling and taken back afterwards.
36316
+ // There is one transition in a document at a time, so one box wears it at a
36317
+ // time — and the others, unnamed, are simply not captured: they stay live
36318
+ // under the pictures rather than being frozen with the page.
36319
+ const nameForTravel = element => {
36320
+ element.style.viewTransitionName = TRAVEL_NAME;
36321
+ };
36322
+ const unnameAfterTravel = element => {
36323
+ element.style.viewTransitionName = "";
36324
+ };
36325
+
36109
36326
  const routeAction = (
36110
36327
  routeOrRoutes,
36111
36328
  action,
@@ -38068,8 +38285,10 @@ const BinderItemContext = createContext(null);
38068
38285
 
38069
38286
  /**
38070
38287
  * What a <Link> learns from the <Nav> around it: where to draw the bar that
38071
- * says "you are here", and the name under which the browser is to recognise
38072
- * that bar from one page to the next (see nav.jsx).
38288
+ * says "you are here", the name under which the browser is to recognise that
38289
+ * bar from one page to the next, and — for a row of tabs that are slides — which
38290
+ * <SlideContainer> they are about and which of its slides is on screen (see
38291
+ * nav.jsx).
38073
38292
  */
38074
38293
  const NavContext = createContext(null);
38075
38294
 
@@ -38502,6 +38721,12 @@ Object.assign(PSEUDO_CLASSES, {
38502
38721
  * instead of a raw `href`: the URL is built from the route (see
38503
38722
  * `routeParams`) and "current" is derived from whether the route matches.
38504
38723
  * @param {object} [props.routeParams] - Params passed to `route.buildUrl`.
38724
+ * @param {string} [props.slide] - Makes this a tab for a slide rather than for
38725
+ * a URL: the area of a `<SlideContainer>` it goes to. The container is the one
38726
+ * the surrounding `<Nav slideContainer={id}>` names, and it is also what says
38727
+ * whether this tab is the current one. Nothing is written to the URL — these
38728
+ * are places within one screen, not pages of their own — so there is no href
38729
+ * and the tab behaves like a button.
38505
38730
  * @param {string} [props.target] - Native anchor target; defaults from
38506
38731
  * internal/external detection when omitted.
38507
38732
  * @param {string} [props.rel] - Native anchor rel; defaults to
@@ -38591,6 +38816,7 @@ const LinkPlain = props => {
38591
38816
  target,
38592
38817
  rel,
38593
38818
  anchor,
38819
+ slide,
38594
38820
  value = href,
38595
38821
  // visual
38596
38822
  variant,
@@ -38641,7 +38867,9 @@ const LinkPlain = props => {
38641
38867
  isAnchor,
38642
38868
  isCurrent
38643
38869
  } = getHrefTargetInfo(href);
38644
- const innerCurrent = current || isCurrent;
38870
+ // A tab that is a SLIDE is current when the container is on it — which the
38871
+ // <Nav> around reads off that container, so nothing here has to be told.
38872
+ const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
38645
38873
  useReportCurrentToBinderItem(innerCurrent);
38646
38874
  controlHostProps.basePseudoState = {
38647
38875
  ...basePseudoState,
@@ -38728,6 +38956,12 @@ const LinkPlain = props => {
38728
38956
  onClick,
38729
38957
  preventDefault
38730
38958
  } = props;
38959
+ // Travelling there is the container's business, said as the command anything
38960
+ // else in the page would say it with: the tab knows the name of a slide and
38961
+ // the id of the box, and nothing more about either.
38962
+ const goToSlide = (element, event) => {
38963
+ triggerNaviCommand(element, `--navi-go-to-slide:${slide}`, event);
38964
+ };
38731
38965
  return jsxs(Text, {
38732
38966
  as: "a",
38733
38967
  color: anchor && !innerChildren ? "inherit" : undefined,
@@ -38740,6 +38974,7 @@ const LinkPlain = props => {
38740
38974
  // was handed.
38741
38975
  preventDefault: undefined,
38742
38976
  anchor: undefined,
38977
+ slide: undefined,
38743
38978
  revealOnInteraction: undefined,
38744
38979
  variant: undefined,
38745
38980
  current: undefined,
@@ -38753,15 +38988,43 @@ const LinkPlain = props => {
38753
38988
  hrefFallback: undefined,
38754
38989
  onClick: e => {
38755
38990
  onClick?.(e);
38991
+ if (slide) {
38992
+ goToSlide(e.currentTarget, e);
38993
+ }
38756
38994
  if (preventDefault) {
38757
38995
  e.preventDefault();
38758
38996
  }
38997
+ }
38998
+ // A tab with no href is not a link the browser knows how to press: it is
38999
+ // focusable because it says so (tabIndex below) and it answers the two
39000
+ // keys a button answers, since that is what it behaves like.
39001
+ ,
39002
+
39003
+ onKeyDown: e => {
39004
+ props.onKeyDown?.(e);
39005
+ if (!slide || e.defaultPrevented) {
39006
+ return;
39007
+ }
39008
+ if (e.key === "Enter" || e.key === " ") {
39009
+ e.preventDefault();
39010
+ goToSlide(e.currentTarget, e);
39011
+ }
38759
39012
  },
38760
39013
  href: href,
38761
39014
  rel: innerRel,
38762
- target: innerTarget === "_self" ? undefined : target,
39015
+ target: innerTarget === "_self" ? undefined : target
39016
+ // Which slide this tab is, and which box to say it to: read by the <Nav>
39017
+ // around it to place the row's own bar, and by the command above to find
39018
+ // the container across the document.
39019
+ ,
39020
+
39021
+ "data-slide-target": slide,
39022
+ commandfor: slide ? nav?.slideContainer : undefined,
39023
+ "aria-controls": slide ? nav?.slideContainer : undefined,
39024
+ tabIndex: slide ? props.tabIndex ?? 0 : props.tabIndex,
39025
+ role: slide ? "tab" : props.role,
38763
39026
  "aria-current": isCurrent ? "page" : undefined,
38764
- "aria-selected": selectionContext ? selected : undefined,
39027
+ "aria-selected": slide ? innerCurrent : selectionContext ? selected : undefined,
38765
39028
  "data-value-event": "navi_value",
38766
39029
  onnavi_value: e => {
38767
39030
  e.detail.setValue(value);
@@ -38824,6 +39087,75 @@ const css$N = /* css */`
38824
39087
  --nav-padding: 0px;
38825
39088
  --nav-border-radius: 0px;
38826
39089
  --nav-background: transparent;
39090
+ --nav-current-indicator-size: 2px;
39091
+ --nav-current-indicator-color: var(--navi-link-current-indicator-color);
39092
+ }
39093
+ }
39094
+
39095
+ /* The bar of a nav whose tabs are SLIDES: one element for the whole row,
39096
+ placed over the current tab and interpolated towards the one the picture
39097
+ leans on (see paintIndicatorGeometry). The two ends are written in pixels
39098
+ as plain numbers, so the whole of the movement is a calc() the browser
39099
+ runs itself — the trait then follows a finger dragging the slides without a
39100
+ render per frame, and rides the same animation as the track when the travel
39101
+ was asked for rather than dragged.
39102
+ No named view transition here, unlike the bar of a nav made of routes:
39103
+ there is no transition to be part of — the slides travel under an animation
39104
+ of their own, which a finger can hold. */
39105
+ .navi_nav[data-nav-indicator] {
39106
+ position: relative;
39107
+
39108
+ > .navi_nav_indicator {
39109
+ --x-nav-indicator-position: calc(
39110
+ var(--nav-indicator-position) + var(--slide-travel-progress) *
39111
+ var(--nav-indicator-position-delta)
39112
+ );
39113
+ --x-nav-indicator-length: calc(
39114
+ var(--nav-indicator-length) + var(--slide-travel-progress) *
39115
+ var(--nav-indicator-length-delta)
39116
+ );
39117
+
39118
+ position: absolute;
39119
+ z-index: 1;
39120
+ background: var(--nav-current-indicator-color);
39121
+ border-radius: 0.1px;
39122
+ pointer-events: none;
39123
+ }
39124
+ /* Nothing to draw until the row has been measured: a tab bar whose current
39125
+ tab is not among its links (a container on a slide no tab names) has no
39126
+ place to put the trait. */
39127
+ &:not([data-nav-indicator-measured]) > .navi_nav_indicator {
39128
+ display: none;
39129
+ }
39130
+
39131
+ &[data-nav-indicator="top"],
39132
+ &[data-nav-indicator="bottom"] {
39133
+ > .navi_nav_indicator {
39134
+ left: calc(var(--x-nav-indicator-position) * 1px);
39135
+ width: calc(var(--x-nav-indicator-length) * 1px);
39136
+ height: var(--nav-current-indicator-size);
39137
+ }
39138
+ }
39139
+ &[data-nav-indicator="top"] > .navi_nav_indicator {
39140
+ top: 0;
39141
+ }
39142
+ &[data-nav-indicator="bottom"] > .navi_nav_indicator {
39143
+ bottom: 0;
39144
+ }
39145
+
39146
+ &[data-nav-indicator="left"],
39147
+ &[data-nav-indicator="right"] {
39148
+ > .navi_nav_indicator {
39149
+ top: calc(var(--x-nav-indicator-position) * 1px);
39150
+ width: var(--nav-current-indicator-size);
39151
+ height: calc(var(--x-nav-indicator-length) * 1px);
39152
+ }
39153
+ }
39154
+ &[data-nav-indicator="left"] > .navi_nav_indicator {
39155
+ left: 0;
39156
+ }
39157
+ &[data-nav-indicator="right"] > .navi_nav_indicator {
39158
+ right: 0;
38827
39159
  }
38828
39160
  }
38829
39161
 
@@ -38979,23 +39311,43 @@ const NavStyleCSSVars = {
38979
39311
  paddingRight: "--nav-padding-right",
38980
39312
  paddingBottom: "--nav-padding-bottom",
38981
39313
  paddingLeft: "--nav-padding-left",
38982
- background: "--nav-background"
39314
+ background: "--nav-background",
39315
+ currentIndicatorColor: "--nav-current-indicator-color",
39316
+ currentIndicatorSize: "--nav-current-indicator-size"
39317
+ };
39318
+ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
39319
+ if (currentIndicator === true) {
39320
+ return vertical ? "left" : "bottom";
39321
+ }
39322
+ if (currentIndicator === "top" || currentIndicator === "bottom" || currentIndicator === "left" || currentIndicator === "right") {
39323
+ return currentIndicator;
39324
+ }
39325
+ return null;
38983
39326
  };
39327
+
38984
39328
  /**
38985
39329
  * @type {import("ignore:preact").FunctionComponent<{
38986
39330
  * currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
38987
39331
  * currentIndicatorSlides?: boolean,
39332
+ * slideContainer?: string,
38988
39333
  * }>}
38989
39334
  * @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - the
38990
39335
  * bar that says which tab one is on, said once here rather than on every
38991
39336
  * `<Link>`. A link may still say otherwise for itself.
38992
39337
  * @param {boolean} [props.currentIndicatorSlides=true] - whether that bar
38993
39338
  * travels from the tab it was under to the tab it is under now, instead of
38994
- * going out on one and coming back on the other. It does so by being NAMED,
38995
- * which is all the browser needs: any change played as a view transition
38996
- * animates it on the same clock as everything else in that transition. Inside
38997
- * a `RouteTravel` that means it follows the pages, and the thumb dragging
38998
- * them, without either of them being told about the other.
39339
+ * going out on one and coming back on the other. For a nav made of routes it
39340
+ * does so by being NAMED, which is all the browser needs: any change played as
39341
+ * a view transition animates it on the same clock as everything else in that
39342
+ * transition. Inside a `RouteTravel` that means it follows the pages, and the
39343
+ * thumb dragging them, without either of them being told about the other. For
39344
+ * a nav made of slides (`slideContainer`) the bar is one element for the whole
39345
+ * row, and it reads the travel the container publishes.
39346
+ * @param {string} [props.slideContainer] - the id of a `<SlideContainer>` these
39347
+ * tabs are about: each one says which slide it is (`<Link slide="…">`), the
39348
+ * container says which one is on screen, and pressing a tab travels there.
39349
+ * Tabs that are places in the same screen rather than pages of their own —
39350
+ * nothing is written to the URL and nothing is a link.
38999
39351
  */
39000
39352
  const Nav = ({
39001
39353
  children,
@@ -39008,21 +39360,132 @@ const Nav = ({
39008
39360
  currentIndicatorSlides = true,
39009
39361
  panelPosition,
39010
39362
  // "before" or "after": which side the panel sits on, turning the nav into folder tabs
39363
+ slideContainer,
39011
39364
  ...props
39012
39365
  }) => {
39013
39366
  import.meta.css = [css$N, "@jsenv/navi/src/nav/link/nav.jsx"];
39367
+ const defaultRef = useRef();
39368
+ props.ref = props.ref || defaultRef;
39369
+ const navRef = props.ref;
39014
39370
  const indicatorNameRef = useRef(null);
39015
39371
  if (indicatorNameRef.current === null) {
39016
39372
  indicatorNameRef.current = `navi-nav-indicator-${++navCount}`;
39017
39373
  }
39374
+ const [currentSlideArea, setCurrentSlideArea] = useState(undefined);
39375
+ const slideContainerElementRef = useRef(null);
39376
+ const indicatorPosition = slideContainer ? positionOfCurrentIndicator(currentIndicator, vertical) : null;
39377
+
39378
+ // Where the trait is and where it is headed, as four numbers of pixels the
39379
+ // CSS above interpolates between (see the .navi_nav_indicator rules). Written
39380
+ // by hand rather than rendered: it is read off the row as it stands, and the
39381
+ // travel it must agree with starts in the same frame the container publishes
39382
+ // it — a render would land after the movement had begun.
39383
+ const paintIndicatorGeometry = () => {
39384
+ const navElement = navRef.current;
39385
+ const containerElement = slideContainerElementRef.current;
39386
+ if (!navElement || !containerElement || !indicatorPosition) {
39387
+ return;
39388
+ }
39389
+ const tabElements = Array.from(navElement.querySelectorAll("[data-slide-target]"));
39390
+ const areaOf = tabElement => tabElement.getAttribute("data-slide-target");
39391
+ const currentArea = containerElement.getAttribute("data-slide-current");
39392
+ const currentIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === currentArea);
39393
+ if (currentIndex === -1) {
39394
+ // On a slide no tab in this row names: there is no tab to sit under.
39395
+ navElement.removeAttribute("data-nav-indicator-measured");
39396
+ return;
39397
+ }
39398
+ const measure = tabElement => vertical ? {
39399
+ position: tabElement.offsetTop,
39400
+ length: tabElement.offsetHeight
39401
+ } : {
39402
+ position: tabElement.offsetLeft,
39403
+ length: tabElement.offsetWidth
39404
+ };
39405
+ const currentMeasure = measure(tabElements[currentIndex]);
39406
+ const towardArea = containerElement.getAttribute("data-slide-travel-toward");
39407
+ const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardArea);
39408
+ let positionDelta = 0;
39409
+ let lengthDelta = 0;
39410
+ if (towardIndex !== -1 && towardIndex !== currentIndex) {
39411
+ const towardMeasure = measure(tabElements[towardIndex]);
39412
+ // What one box of travel is worth in pixels of this row, signed so that
39413
+ // the trait is exactly on the other tab when the progress is at its own
39414
+ // end: the container counts +1 when the picture leans on a slide sitting
39415
+ // BEFORE the current one and -1 when it sits after.
39416
+ const sign = towardIndex > currentIndex ? -1 : 1;
39417
+ positionDelta = (towardMeasure.position - currentMeasure.position) * sign;
39418
+ lengthDelta = (towardMeasure.length - currentMeasure.length) * sign;
39419
+ }
39420
+ const {
39421
+ style
39422
+ } = navElement;
39423
+ style.setProperty("--nav-indicator-position", currentMeasure.position);
39424
+ style.setProperty("--nav-indicator-length", currentMeasure.length);
39425
+ style.setProperty("--nav-indicator-position-delta", positionDelta);
39426
+ style.setProperty("--nav-indicator-length-delta", lengthDelta);
39427
+ navElement.setAttribute("data-nav-indicator-measured", "");
39428
+ };
39429
+ // Reached through a ref by everything watching the DOM below: those watchers
39430
+ // outlive a render, and what they must run is the version of this that knows
39431
+ // about the row as it is now.
39432
+ const paintIndicatorGeometryRef = useRef(null);
39433
+ paintIndicatorGeometryRef.current = paintIndicatorGeometry;
39434
+ useLayoutEffect(() => {
39435
+ if (!slideContainer) {
39436
+ return undefined;
39437
+ }
39438
+ const containerElement = document.getElementById(slideContainer);
39439
+ if (!containerElement) {
39440
+ console.warn(`<Nav slideContainer="${slideContainer}"> but no element with that id found`);
39441
+ return undefined;
39442
+ }
39443
+ slideContainerElementRef.current = containerElement;
39444
+ const readContainer = () => {
39445
+ setCurrentSlideArea(containerElement.getAttribute("data-slide-current") ?? undefined);
39446
+ paintIndicatorGeometryRef.current();
39447
+ };
39448
+ readContainer();
39449
+ // The container says where one is and what the picture leans on, and says
39450
+ // it in the DOM: nothing here is told, everything is read — which is what
39451
+ // lets this row sit anywhere on the page (above the box, in a fixed bar)
39452
+ // rather than inside it.
39453
+ const attributeObserver = new MutationObserver(readContainer);
39454
+ attributeObserver.observe(containerElement, {
39455
+ attributes: true,
39456
+ attributeFilter: ["data-slide-current", "data-slide-travel-toward"]
39457
+ });
39458
+ // A row whose tabs changed width — a badge count, a font that just
39459
+ // arrived, a window resized — is measured again: what was written is
39460
+ // pixels, and pixels go stale.
39461
+ const sizeObserver = new ResizeObserver(() => {
39462
+ paintIndicatorGeometryRef.current();
39463
+ });
39464
+ sizeObserver.observe(navRef.current);
39465
+ return () => {
39466
+ attributeObserver.disconnect();
39467
+ sizeObserver.disconnect();
39468
+ slideContainerElementRef.current = null;
39469
+ };
39470
+ }, [slideContainer]);
39471
+
39472
+ // Said after every commit: a tab added, removed or renamed moves the trait,
39473
+ // and no observer above watches this row's own children.
39474
+ useLayoutEffect(() => {
39475
+ paintIndicatorGeometry();
39476
+ });
39018
39477
  const navContextValue = useMemo(() => ({
39019
- currentIndicator,
39478
+ // The bar belongs to the row itself when the tabs are slides, so the
39479
+ // links draw none of their own.
39480
+ currentIndicator: slideContainer ? undefined : currentIndicator,
39020
39481
  // Read by the link that is current, and by it alone: a name belongs to
39021
39482
  // one element at a time, and the bar exists in every tab.
39022
- indicatorName: currentIndicatorSlides ? indicatorNameRef.current : null
39023
- }), [currentIndicator, currentIndicatorSlides]);
39483
+ indicatorName: currentIndicatorSlides ? indicatorNameRef.current : null,
39484
+ slideContainer,
39485
+ currentSlideArea
39486
+ }), [currentIndicator, currentIndicatorSlides, slideContainer, currentSlideArea]);
39024
39487
  children = toChildArray(children);
39025
- return jsx(Box, {
39488
+ return jsxs(Box, {
39026
39489
  as: "nav",
39027
39490
  row: vertical,
39028
39491
  column: !vertical,
@@ -39031,15 +39494,30 @@ const Nav = ({
39031
39494
  "data-expand": expand || expandX ? "" : undefined,
39032
39495
  "data-vertical": vertical ? "" : undefined,
39033
39496
  "data-panel-position": panelPosition,
39497
+ "data-nav-indicator": indicatorPosition ?? undefined
39498
+ // "write your travel here too": a custom property cannot be read across
39499
+ // the DOM, so the container paints its progress onto this element and the
39500
+ // trait follows in CSS alone (see SlideContainer's followerElements).
39501
+ ,
39502
+
39503
+ "data-slide-container-follows": slideContainer
39504
+ // Tabs over one screen, not links to pages: a screen reader is told so,
39505
+ // and told which way the row runs.
39506
+ ,
39507
+
39508
+ role: slideContainer ? "tablist" : undefined,
39509
+ "aria-orientation": slideContainer && vertical ? "vertical" : undefined,
39034
39510
  expand: expand,
39035
39511
  expandX: expandX,
39036
39512
  spacing: spacing,
39037
39513
  ...props,
39038
39514
  styleCSSVars: NavStyleCSSVars,
39039
- children: jsx(NavContext.Provider, {
39515
+ children: [indicatorPosition && jsx("span", {
39516
+ className: "navi_nav_indicator"
39517
+ }), jsx(NavContext.Provider, {
39040
39518
  value: navContextValue,
39041
39519
  children: children
39042
- })
39520
+ })]
39043
39521
  });
39044
39522
  };
39045
39523
 
@@ -45922,7 +46400,16 @@ const SlideContainer = ({
45922
46400
  // picture is from the slide ARRIVING when the travel starts, in boxes. Null
45923
46401
  // for a travel nobody dragged, where a whole box is what is left to close.
45924
46402
  const travelProgressFromRef = useRef(null);
45925
- const progressAnimationRef = useRef(null);
46403
+ // One per element painting the progress: the box, plus everything following
46404
+ // it (see followerElementsRef). All started together and with the same
46405
+ // options, so they are one movement said in several places.
46406
+ const progressAnimationsRef = useRef([]);
46407
+ // Elements outside the box that draw something about this travel — a tab bar
46408
+ // above it, most of all. A custom property cannot be read across the DOM, so
46409
+ // the progress is WRITTEN on each of them: they then interpolate whatever they
46410
+ // draw in CSS alone, at the pace of the travel and under the finger, with
46411
+ // nothing measured per frame.
46412
+ const followerElementsRef = useRef([]);
45926
46413
  const current = rollingArea ?? provisionalArea ?? currentProp ?? currentAreaState;
45927
46414
  const vertical = layout === "column";
45928
46415
  // What the map has, and what each way of asking is allowed to use of it.
@@ -46048,6 +46535,28 @@ const SlideContainer = ({
46048
46535
  ...map
46049
46536
  };
46050
46537
  };
46538
+
46539
+ // Who is drawing something about this box from outside it: a tab bar saying
46540
+ // where one is, a row of dots. They name the box they follow by its id, the
46541
+ // way everything else that talks to it across the document does (commandfor).
46542
+ const readFollowerElements = () => {
46543
+ const containerEl = containerRef.current;
46544
+ const {
46545
+ id
46546
+ } = containerEl;
46547
+ if (!id) {
46548
+ return [];
46549
+ }
46550
+ return Array.from(document.querySelectorAll(`[data-slide-container-follows="${CSS.escape(id)}"]`));
46551
+ };
46552
+
46553
+ // Which slide is on screen, said in the DOM: it is what anything outside the
46554
+ // box reads to know where one is (a tab bar marking its current tab), and
46555
+ // there is nothing else for it to read — a slide the container holds by
46556
+ // itself is known to no one else.
46557
+ const paintCurrentArea = area => {
46558
+ containerRef.current?.setAttribute("data-slide-current", area);
46559
+ };
46051
46560
  const markAnswered = area => {
46052
46561
  const order = readMap().slideElements.map(readArea);
46053
46562
  const rank = order.indexOf(area);
@@ -46130,6 +46639,10 @@ const SlideContainer = ({
46130
46639
  // the children free — their shape says nothing about the arrangement, the map
46131
46640
  // does — and it is also the only place that has to agree with itself.
46132
46641
  useLayoutEffect(() => {
46642
+ // Read on every render rather than subscribed to: a follower says who it
46643
+ // follows in the DOM, and the render that mounted one is the render this
46644
+ // runs after.
46645
+ followerElementsRef.current = readFollowerElements();
46133
46646
  const {
46134
46647
  slideElements,
46135
46648
  placeOf
@@ -46142,6 +46655,7 @@ const SlideContainer = ({
46142
46655
  // shown, the way a stack of pages opens on its first page.
46143
46656
  slideElements[0];
46144
46657
  const currentArea = readArea(currentElement);
46658
+ paintCurrentArea(currentArea);
46145
46659
  const realPlaceOf = area => placeOf.get(area) || {
46146
46660
  x: 0,
46147
46661
  y: 0
@@ -46287,7 +46801,9 @@ const SlideContainer = ({
46287
46801
  // there was one, from a whole box away when the travel was asked for.
46288
46802
  const progressFrom = travelProgressFromRef.current ?? (travelStep ? travelStep.x || travelStep.y : 0);
46289
46803
  travelProgressFromRef.current = null;
46290
- animateTravelProgress(progressFrom, durationMs * travelRatio, easing);
46804
+ // The slide the picture leans on for the length of it is the one being
46805
+ // LEFT: it is the second one in the frame until the travel is over.
46806
+ animateTravelProgress(progressFrom, durationMs * travelRatio, easing, drawnArea);
46291
46807
  // Presses still waiting behind this one: it is already late, so it is
46292
46808
  // sent home at once rather than played out at the pace of someone who
46293
46809
  // has stopped pressing. Someone pressing → four times is asking to be
@@ -46707,6 +47223,30 @@ const SlideContainer = ({
46707
47223
  paintTravelProgress(drag.progress, drag.areaPulled);
46708
47224
  };
46709
47225
 
47226
+ // Everything that draws this travel: the box itself and whoever follows it.
47227
+ const travelPainters = () => {
47228
+ const containerEl = containerRef.current;
47229
+ if (!containerEl) {
47230
+ return [];
47231
+ }
47232
+ return [containerEl, ...followerElementsRef.current];
47233
+ };
47234
+
47235
+ // Which OTHER slide the picture leans on while it is not on the current one:
47236
+ // the slide being pulled in under a finger, the slide being left during a
47237
+ // travel. Two slides are in the frame and the number below says how far
47238
+ // between them one is — this says which the second one is, so a trait can be
47239
+ // drawn between two places rather than merely offset from one.
47240
+ const paintTravelToward = area => {
47241
+ for (const element of travelPainters()) {
47242
+ if (area) {
47243
+ element.setAttribute("data-slide-travel-toward", area);
47244
+ } else {
47245
+ element.removeAttribute("data-slide-travel-toward");
47246
+ }
47247
+ }
47248
+ };
47249
+
46710
47250
  // Where the picture stands relative to the slide that is CURRENT, in boxes:
46711
47251
  // 0 on it, +1 one whole box before it, -1 one box after. Written on the
46712
47252
  // container so an indicator drawn inside the box — a tab bar, a dot row, a
@@ -46715,45 +47255,50 @@ const SlideContainer = ({
46715
47255
  // gesture, so the number stays continuous when the travel commits and the
46716
47256
  // current slide changes under it.
46717
47257
  const paintTravelProgress = (progress, area) => {
46718
- const containerEl = containerRef.current;
46719
- if (!containerEl) {
46720
- return;
46721
- }
46722
- if (!progress) {
46723
- containerEl.style.removeProperty("--slide-travel-progress");
46724
- containerEl.removeAttribute("data-slide-travel-to");
46725
- return;
47258
+ for (const element of travelPainters()) {
47259
+ if (progress) {
47260
+ element.style.setProperty("--slide-travel-progress", progress);
47261
+ } else {
47262
+ element.style.removeProperty("--slide-travel-progress");
47263
+ }
46726
47264
  }
46727
- containerEl.style.setProperty("--slide-travel-progress", progress);
46728
- if (area) {
46729
- containerEl.setAttribute("data-slide-travel-to", area);
46730
- } else {
46731
- containerEl.removeAttribute("data-slide-travel-to");
47265
+ paintTravelToward(progress ? area : null);
47266
+ };
47267
+ const cancelTravelProgressAnimation = () => {
47268
+ for (const animation of progressAnimationsRef.current) {
47269
+ animation.cancel();
46732
47270
  }
47271
+ progressAnimationsRef.current = [];
46733
47272
  };
46734
47273
 
46735
47274
  // The indicator, brought home at the pace of the travel it belongs to: the
46736
47275
  // same duration and the same easing as the track, so the trait and the slides
46737
47276
  // are one movement. The value it lands on is the one nothing writes (0), so
46738
- // the animation is left to fall away on its own.
46739
- const animateTravelProgress = (from, durationMs, easing) => {
46740
- const containerEl = containerRef.current;
46741
- progressAnimationRef.current?.cancel();
46742
- progressAnimationRef.current = null;
47277
+ // the animation is left to fall away on its own — only the name of the slide
47278
+ // being leant on is taken back by hand, at the end.
47279
+ const animateTravelProgress = (from, durationMs, easing, area) => {
47280
+ // Read again at the start of every travel, not only at every render: a
47281
+ // follower appearing does not make this box render, and the travel it must
47282
+ // draw is the one about to start.
47283
+ followerElementsRef.current = readFollowerElements();
47284
+ cancelTravelProgressAnimation();
46743
47285
  paintTravelProgress(0);
46744
- if (!containerEl || !from || !durationMs) {
47286
+ const painters = travelPainters();
47287
+ if (!painters.length || !from || !durationMs) {
46745
47288
  return;
46746
47289
  }
46747
- progressAnimationRef.current = containerEl.animate([{
47290
+ paintTravelToward(area);
47291
+ progressAnimationsRef.current = painters.map(element => element.animate([{
46748
47292
  "--slide-travel-progress": from
46749
47293
  }, {
46750
47294
  "--slide-travel-progress": 0
46751
47295
  }], {
46752
47296
  duration: durationMs,
46753
47297
  easing
46754
- });
46755
- progressAnimationRef.current.finished.then(() => {
46756
- progressAnimationRef.current = null;
47298
+ }));
47299
+ progressAnimationsRef.current[0].finished.then(() => {
47300
+ progressAnimationsRef.current = [];
47301
+ paintTravelToward(null);
46757
47302
  }, () => {
46758
47303
  // cancelled by the next travel — that one says where the trait goes
46759
47304
  });
@@ -46826,7 +47371,7 @@ const SlideContainer = ({
46826
47371
  settleTravel();
46827
47372
  return;
46828
47373
  }
46829
- animateTravelProgress(drag.progress, durationMs * (pulled / size), "ease-out");
47374
+ animateTravelProgress(drag.progress, durationMs * (pulled / size), "ease-out", drag.areaPulled);
46830
47375
  const animation = track.animate([{
46831
47376
  translate: drag.offset
46832
47377
  }, {
@@ -46952,8 +47497,8 @@ const SlideContainer = ({
46952
47497
  caughtTravel = null;
46953
47498
  trackAnimationRef.current?.cancel();
46954
47499
  trackAnimationRef.current = null;
46955
- progressAnimationRef.current?.cancel();
46956
- progressAnimationRef.current = null;
47500
+ cancelTravelProgressAnimation();
47501
+ followerElementsRef.current = readFollowerElements();
46957
47502
  drag.axis = axis;
46958
47503
  drag.areaBack = areaBack;
46959
47504
  drag.areaOn = areaOn;
@@ -47098,6 +47643,10 @@ const SlideContainer = ({
47098
47643
  ...handlers
47099
47644
  });
47100
47645
  if (!gesture) {
47646
+ // Not a press this box can be about — something that reads the pointer
47647
+ // itself, a box below it that travels the same way. Whatever the press
47648
+ // stopped on its way in goes back on its way.
47649
+ handlers.onGiveUp();
47101
47650
  return;
47102
47651
  }
47103
47652
  handlers.drag.gesture = gesture;
@@ -47144,7 +47693,7 @@ const SlideContainer = ({
47144
47693
  return () => {
47145
47694
  dragRef.current?.gesture?.stop();
47146
47695
  dragRef.current = null;
47147
- progressAnimationRef.current?.cancel();
47696
+ cancelTravelProgressAnimation();
47148
47697
  };
47149
47698
  }, []);
47150
47699
 
@@ -47209,10 +47758,16 @@ const SlideContainer = ({
47209
47758
  "data-slide-container": ""
47210
47759
  // Which axes a touch may travel on, said in the DOM: what the browser
47211
47760
  // does with a finger is decided by CSS (touch-action) before any of this
47212
- // has seen the gesture.
47761
+ // has seen the gesture — and it is also what a box HOLDING this one reads
47762
+ // to know the gesture is not its own (see drag_to_travel.js).
47213
47763
  ,
47214
47764
 
47215
47765
  "data-travel-by-drag": dragAxes ?? undefined
47766
+ // The same fact for a wheel, and only for that second reason: this box
47767
+ // takes the push, whatever the box around it also travels on.
47768
+ ,
47769
+
47770
+ "data-travel-by-wheel": scrollAxes ?? undefined
47216
47771
  // The same fact, read by the shared gesture stylesheet: what scrolls
47217
47772
  // inside a box that travels must not spill onto the page behind it (see
47218
47773
  // drag_to_travel.js).
@@ -59490,8 +60045,16 @@ const useWheelInteractions = ({
59490
60045
  document.removeEventListener("wheel", onDocumentWheel, {
59491
60046
  capture: true
59492
60047
  });
60048
+ releaseWheelGesture(vp);
59493
60049
  };
59494
60050
  const keepClaimingGesture = () => {
60051
+ // Said out loud as well as swallowed: preventDefault only settles it with
60052
+ // the browser, and a box that travels with the wheel answers the burst
60053
+ // from a listener of its own — it asks who owns the gesture instead (see
60054
+ // wheel_gesture.js in @jsenv/dom).
60055
+ claimWheelGesture(vp, {
60056
+ delay: WHEEL_GESTURE_MAX_GAP
60057
+ });
59495
60058
  if (!gestureGuardTimer) {
59496
60059
  document.addEventListener("wheel", onDocumentWheel, {
59497
60060
  capture: true,
@@ -59502,6 +60065,12 @@ const useWheelInteractions = ({
59502
60065
  gestureGuardTimer = setTimeout(stopClaimingGesture, WHEEL_GESTURE_MAX_GAP);
59503
60066
  };
59504
60067
  const onWheel = e => {
60068
+ // The burst belongs to something else — a box that travels with the
60069
+ // wheel, another wheel the pointer has just left. It is theirs until the
60070
+ // events stop coming.
60071
+ if (wheelGestureIsTakenFrom(vp)) {
60072
+ return;
60073
+ }
59505
60074
  const raw = isHorizontal ? e.deltaX || e.deltaY : e.deltaY;
59506
60075
  if (!raw) {
59507
60076
  return;