@jsenv/navi 0.30.4 → 0.30.6

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.
@@ -25965,7 +25965,7 @@ defineInteractionDetector({
25965
25965
  const transitionAfter = peekLastViewTransition();
25966
25966
  const drawn =
25967
25967
  transitionAfter === transitionBefore
25968
- ? afterFrames(2)
25968
+ ? afterFrames$1(2)
25969
25969
  : transitionAfter.finished.then(NOOP, NOOP);
25970
25970
  const write = {
25971
25971
  settled: false,
@@ -26265,7 +26265,7 @@ const warnWhenNothingToLandOn = (element, dropContainer, canLeave) => {
26265
26265
 
26266
26266
  const NOOP = () => {};
26267
26267
 
26268
- const afterFrames = (count) =>
26268
+ const afterFrames$1 = (count) =>
26269
26269
  new Promise((resolve) => {
26270
26270
  const next = (remaining) => {
26271
26271
  if (remaining === 0) {
@@ -37756,9 +37756,23 @@ const collectRoutePages = children => {
37756
37756
  * Whether the next container down is a row a hand scrolls: every page has a
37757
37757
  * place on it from the start, and the browser moves between them (see
37758
37758
  * RouteTravel's `nativeScroll`). Told to one container, not to the pages it holds.
37759
+ * False, or `{ renderNeighbour }`.
37759
37760
  */
37760
37761
  const RouteRowContext = createContext(false);
37761
37762
 
37763
+ /**
37764
+ * What stands in the pane of a neighbour until its page does. A component of its
37765
+ * own: what the application calls in there (a hook, a signal) answers to it, however
37766
+ * many neighbours the row has at that moment.
37767
+ */
37768
+ const RouteRowNeighbour = ({
37769
+ renderNeighbour,
37770
+ page
37771
+ }) => jsx("div", {
37772
+ "data-route-row-neighbour": "",
37773
+ children: renderNeighbour(page)
37774
+ });
37775
+
37762
37776
  // A container as a row: one pane per leaf, in the order they are written. A page
37763
37777
  // still builds when its route first matches, since a route's actions only ask
37764
37778
  // while it does: before that its pane is empty. Once built it stays, with what
@@ -37771,6 +37785,9 @@ const RouteRowContext = createContext(false);
37771
37785
  const RouteRowContainer = ({
37772
37786
  children
37773
37787
  }) => {
37788
+ const {
37789
+ renderNeighbour
37790
+ } = useContext(RouteRowContext);
37774
37791
  const {
37775
37792
  activeBranch,
37776
37793
  leaves
@@ -37819,16 +37836,19 @@ const RouteRowContainer = ({
37819
37836
  shown: true
37820
37837
  }, "fallback");
37821
37838
  }
37822
- return leaves.map(leaf => jsx(RouteRowPane, {
37839
+ const shownIndex = activeBranch ? leaves.findIndex(leaf => leaf.id === activeBranch.id) : -1;
37840
+ return leaves.map((leaf, index) => jsx(RouteRowPane, {
37823
37841
  branch: leaf,
37824
37842
  page: pageOf(leaf),
37825
- shown: activeBranch !== null && leaf.id === activeBranch.id
37843
+ shown: index === shownIndex,
37844
+ renderNeighbour: shownIndex !== -1 && (index === shownIndex - 1 || index === shownIndex + 1) ? renderNeighbour : undefined
37826
37845
  }, leaf.id));
37827
37846
  };
37828
37847
  const RouteRowPane = ({
37829
37848
  branch,
37830
37849
  page,
37831
- shown
37850
+ shown,
37851
+ renderNeighbour
37832
37852
  }) => {
37833
37853
  // The very vnode rendered last, while not on screen: Preact does not look into
37834
37854
  // it, and a page nobody can see costs nothing when the address moves.
@@ -37847,7 +37867,10 @@ const RouteRowPane = ({
37847
37867
  "data-route-row-pane": "",
37848
37868
  "data-built": contentRef.current ? "" : undefined,
37849
37869
  inert: !shown,
37850
- children: contentRef.current
37870
+ children: contentRef.current || renderNeighbour && jsx(RouteRowNeighbour, {
37871
+ renderNeighbour: renderNeighbour,
37872
+ page: branchRoutePage(branch)
37873
+ })
37851
37874
  });
37852
37875
  };
37853
37876
 
@@ -41570,8 +41593,9 @@ const publishRouteRowMove = (move) => {
41570
41593
  * itself; a counter, a title or a breadcrumb around the box reads it first.
41571
41594
  *
41572
41595
  * It goes back to `null` as soon as the hand has let go and the row moves. Nothing
41573
- * says when the fingers leave a trackpad: there it lasts until the row has come to
41574
- * rest. A travel no hand set off holds nothing: it goes where it goes.
41596
+ * says when the fingers leave a trackpad: there it is the page nearest the row, until
41597
+ * the row has come to rest. A travel no hand set off holds nothing: it goes where it
41598
+ * goes.
41575
41599
  *
41576
41600
  * One for the document. `route` says which row, for a page holding several boxes.
41577
41601
  */
@@ -41589,6 +41613,13 @@ const holdPage = (page, headingPage) => {
41589
41613
  }
41590
41614
  };
41591
41615
 
41616
+ // A hand no browser says has let go: the page nearest the row is where the reader is,
41617
+ // and the address, wherever it stands, is not.
41618
+ const followPageOnScreen = (page, addressPage) => {
41619
+ headingPageSignal.value = addressPage;
41620
+ routeTravelSettledPageSignal.value = page;
41621
+ };
41622
+
41592
41623
  const releaseHeldPage = () => {
41593
41624
  routeTravelSettledPageSignal.value = null;
41594
41625
  headingPageSignal.value = null;
@@ -41703,7 +41734,9 @@ const readRowStretch = (scroller, pages, page) => {
41703
41734
  /**
41704
41735
  * @param {Element} scroller
41705
41736
  * @param {object} options
41706
- * @param {() => void} options.onScroll - the row moved.
41737
+ * @param {(move: {finger: boolean}) => void} options.onScroll - the row moved.
41738
+ * `finger`: a touch has come since the row last stopped, so a lift will say where
41739
+ * it goes (`onLeftToSnap`). Without one (a trackpad, a wheel) nothing will.
41707
41740
  * @param {(event: Event|null) => void} options.onLand - the row may have stopped.
41708
41741
  * Read `pageAtRest` to know whether it stopped on a page. Heard more than once
41709
41742
  * for one landing, so what it does must find nothing to do the second time.
@@ -41719,11 +41752,13 @@ const watchRowScroll = (
41719
41752
  ) => {
41720
41753
  let stoppedTimeout = null;
41721
41754
  let touching = false;
41755
+ let touchedSinceStop = false;
41722
41756
  let liftedAt = null;
41723
41757
  const landOnSilence = () => {
41724
41758
  liftedAt = null;
41725
41759
  // A finger holding the row still: nothing has stopped.
41726
41760
  if (!touching) {
41761
+ touchedSinceStop = false;
41727
41762
  onLand(null);
41728
41763
  }
41729
41764
  };
@@ -41732,7 +41767,7 @@ const watchRowScroll = (
41732
41767
  stoppedTimeout = setTimeout(landOnSilence, STOPPED_SILENCE);
41733
41768
  };
41734
41769
  const onScrollEvent = () => {
41735
- onScroll();
41770
+ onScroll({ finger: touchedSinceStop });
41736
41771
  armSilence();
41737
41772
  if (!liftedAt) {
41738
41773
  return;
@@ -41746,11 +41781,6 @@ const watchRowScroll = (
41746
41781
  liftedAt = null;
41747
41782
  onLeftToSnap?.(moved > 0);
41748
41783
  };
41749
- const onTouchStart = () => {
41750
- touching = true;
41751
- liftedAt = null;
41752
- clearTimeout(stoppedTimeout);
41753
- };
41754
41784
  const onTouchEnd = (touchEvent) => {
41755
41785
  touching = touchEvent.touches.length > 0;
41756
41786
  if (!touching) {
@@ -41758,23 +41788,38 @@ const watchRowScroll = (
41758
41788
  armSilence();
41759
41789
  }
41760
41790
  };
41791
+ const onTouchStart = (touchEvent) => {
41792
+ touching = true;
41793
+ touchedSinceStop = true;
41794
+ liftedAt = null;
41795
+ clearTimeout(stoppedTimeout);
41796
+ // Listened for on what the finger landed on, where the end of a touch is always
41797
+ // told: a page changing under the finger may have taken it out of the document by
41798
+ // then, and nothing comes up to the row from there.
41799
+ const { target } = touchEvent;
41800
+ const onEnd = (endEvent) => {
41801
+ target.removeEventListener("touchend", onEnd);
41802
+ target.removeEventListener("touchcancel", onEnd);
41803
+ onTouchEnd(endEvent);
41804
+ };
41805
+ target.addEventListener("touchend", onEnd, { passive: true });
41806
+ target.addEventListener("touchcancel", onEnd, { passive: true });
41807
+ };
41761
41808
  scroller.addEventListener("scroll", onScrollEvent, { passive: true });
41762
41809
  scroller.addEventListener("scrollend", onLand);
41763
41810
  scroller.addEventListener("touchstart", onTouchStart, { passive: true });
41764
- scroller.addEventListener("touchend", onTouchEnd, { passive: true });
41765
- scroller.addEventListener("touchcancel", onTouchEnd, { passive: true });
41766
41811
  return () => {
41767
41812
  clearTimeout(stoppedTimeout);
41768
41813
  scroller.removeEventListener("scroll", onScrollEvent);
41769
41814
  scroller.removeEventListener("scrollend", onLand);
41770
41815
  scroller.removeEventListener("touchstart", onTouchStart);
41771
- scroller.removeEventListener("touchend", onTouchEnd);
41772
- scroller.removeEventListener("touchcancel", onTouchEnd);
41773
41816
  };
41774
41817
  };
41775
41818
 
41776
41819
  installImportMetaCssBuild(import.meta);
41777
41820
  const css$16 = /* css */`.navi_route_travel {
41821
+ flex-direction: column;
41822
+ display: flex;
41778
41823
  position: relative;
41779
41824
  }
41780
41825
 
@@ -41805,6 +41850,19 @@ const css$16 = /* css */`.navi_route_travel {
41805
41850
  overflow: visible;
41806
41851
  }
41807
41852
 
41853
+ &[data-route-row-taken] {
41854
+ scroll-snap-type: none;
41855
+ }
41856
+
41857
+ &[data-route-row-taken="clip"] {
41858
+ overflow: clip;
41859
+
41860
+ & > [data-route-row-pane], & > [data-route-row-live], & > [data-route-row-pictures] > * {
41861
+ left: var(--navi-route-row-stand);
41862
+ position: relative;
41863
+ }
41864
+ }
41865
+
41808
41866
  & > [data-route-row-pane] {
41809
41867
  scroll-snap-align: start;
41810
41868
  scroll-snap-stop: always;
@@ -41826,6 +41884,15 @@ const css$16 = /* css */`.navi_route_travel {
41826
41884
  overflow: clip;
41827
41885
  }
41828
41886
 
41887
+ & > [data-route-row-pane] > [data-route-row-neighbour] {
41888
+ contain: layout paint;
41889
+ visibility: visible;
41890
+ flex-direction: column;
41891
+ display: flex;
41892
+ position: absolute;
41893
+ inset: 0;
41894
+ }
41895
+
41829
41896
  & > [data-route-row-live] {
41830
41897
  flex-direction: column;
41831
41898
  grid-row: 1;
@@ -41858,6 +41925,7 @@ const SET_OFF_PULL = 16;
41858
41925
  * routes?: Array<object|{route: object, params?: object}>,
41859
41926
  * travelByDrag?: boolean,
41860
41927
  * onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
41928
+ * renderNeighbour?: (page: {route: object, params: object|undefined}) => import("preact").ComponentChildren,
41861
41929
  * }>}
41862
41930
  * @param {Array<object|{route: object, params?: object}>} [props.routes] - a window
41863
41931
  * over something longer, in order. Read from the <Route> children by default,
@@ -41874,6 +41942,13 @@ const SET_OFF_PULL = 16;
41874
41942
  * of the page under six entries. A tab pressed goes the way its <Link> says —
41875
41943
  * `<Link replace>` for a row of tabs, so a press and a swipe towards the same
41876
41944
  * neighbour write the same history.
41945
+ * @param {(page: {route: object, params: object|undefined}) => import("preact").ComponentChildren} [props.renderNeighbour]
41946
+ * - what stands in the pane of a page next to the one on screen, until the hand
41947
+ * has pulled far enough to bring that page: its background, its skeleton, what the
41948
+ * list already knows. A `<Route>` page already built shows itself. Receives
41949
+ * `{ route, params }`, or the entry of `routes` as it was given, and is a column
41950
+ * the height of the row, as the page is. Its route does not match while it stands
41951
+ * there: what it reads is not asked for.
41877
41952
  *
41878
41953
  * Every other prop lands on the box itself (`id`, `data-testid`, `className`, …).
41879
41954
  * The box has to outlive every page of its row: around those <Route>s, or around
@@ -41889,6 +41964,7 @@ const RouteTravel = ({
41889
41964
  replace: true,
41890
41965
  routeTransition: "none"
41891
41966
  }),
41967
+ renderNeighbour,
41892
41968
  className,
41893
41969
  children,
41894
41970
  ...rest
@@ -41949,9 +42025,7 @@ const RouteTravel = ({
41949
42025
  travelling: false,
41950
42026
  pictures: new Map(),
41951
42027
  resumeScrollRecording: null,
41952
- scrollsDocument: getScrollContainer(row.parentElement, {
41953
- includeHidden: true
41954
- }) === document.documentElement,
42028
+ scrollsDocument: rowFollowsDocument(row),
41955
42029
  ...fields
41956
42030
  };
41957
42031
  movementRef.current = movement;
@@ -42046,6 +42120,37 @@ const RouteTravel = ({
42046
42120
  });
42047
42121
  };
42048
42122
 
42123
+ // A tab pressed over a row a hand has left moving, which the browser plays on and
42124
+ // overwrites a travel with once that travel is over: the snap is ended first. Given
42125
+ // back, the row stands where it stood. A frame later, because until then WebKit still
42126
+ // starts from where a box that is no scroller stood: a smooth scroll to the first page
42127
+ // sees nothing to do.
42128
+ const takeRowFromHand = movement => {
42129
+ const row = rowRef.current;
42130
+ const position = row.scrollLeft;
42131
+ movement.rowTaken = true;
42132
+ // A travel no hand set off holds nothing: it goes where it goes.
42133
+ releaseHeldPage();
42134
+ row.style.setProperty("--navi-route-row-stand", `${-position}px`);
42135
+ row.setAttribute("data-route-row-taken", "clip");
42136
+ afterFrames(2, () => {
42137
+ if (movementRef.current !== movement) {
42138
+ return;
42139
+ }
42140
+ row.setAttribute("data-route-row-taken", "");
42141
+ row.style.removeProperty("--navi-route-row-stand");
42142
+ afterFrames(1, () => {
42143
+ const pane = panesOf(row)[latestRef.current.currentIndex];
42144
+ if (movementRef.current === movement && pane) {
42145
+ row.scrollTo({
42146
+ left: pane.offsetLeft,
42147
+ behavior: "smooth"
42148
+ });
42149
+ }
42150
+ });
42151
+ });
42152
+ };
42153
+
42049
42154
  // The row has stopped, on the page of the address: everything held for the
42050
42155
  // movement is let go of in one go, before anything paints.
42051
42156
  const endMovement = () => {
@@ -42056,6 +42161,7 @@ const RouteTravel = ({
42056
42161
  }
42057
42162
  movementRef.current = null;
42058
42163
  row.removeAttribute("data-moving");
42164
+ row.removeAttribute("data-route-row-taken");
42059
42165
  publishRouteRowMove(null);
42060
42166
  releaseHeldPage();
42061
42167
  if (liveRef.current) {
@@ -42123,16 +42229,23 @@ const RouteTravel = ({
42123
42229
  }
42124
42230
  return;
42125
42231
  }
42126
- beginMovement({
42232
+ const movementBefore = movementRef.current;
42233
+ const movement = beginMovement({
42127
42234
  travelling: true
42128
42235
  });
42129
- row.scrollTo({
42130
- left: pane.offsetLeft,
42131
- behavior: "smooth"
42132
- });
42133
42236
  // In this very commit: the tab pressed already wears its bar, and the first
42134
42237
  // scroll event is a frame away.
42135
42238
  sayRowMoves();
42239
+ if (movementBefore?.byHand && !movementBefore.rowTaken) {
42240
+ takeRowFromHand(movement);
42241
+ return;
42242
+ }
42243
+ if (row.getAttribute("data-route-row-taken") !== "clip") {
42244
+ row.scrollTo({
42245
+ left: pane.offsetLeft,
42246
+ behavior: "smooth"
42247
+ });
42248
+ }
42136
42249
  });
42137
42250
 
42138
42251
  // Somebody else is about to move the address to another page of a window: its
@@ -42164,14 +42277,17 @@ const RouteTravel = ({
42164
42277
  }, [windowed]);
42165
42278
  useLayoutEffect(() => {
42166
42279
  const row = rowRef.current;
42167
- const onScroll = () => {
42280
+ const isClipped = () => row.getAttribute("data-route-row-taken") === "clip";
42281
+ const onScroll = ({
42282
+ finger
42283
+ }) => {
42168
42284
  const {
42169
42285
  pages,
42170
42286
  currentIndex,
42171
42287
  travelByDrag
42172
42288
  } = latestRef.current;
42173
42289
  const panes = panesOf(row);
42174
- if (panes.length < 2) {
42290
+ if (panes.length < 2 || isClipped()) {
42175
42291
  return;
42176
42292
  }
42177
42293
  const moving = movementRef.current;
@@ -42180,11 +42296,24 @@ const RouteTravel = ({
42180
42296
  // Written by this box, at rest: nobody is moving anything.
42181
42297
  return;
42182
42298
  }
42183
- const movement = moving || beginMovement({});
42299
+ const movement = moving || beginMovement({
42300
+ byHand: true
42301
+ });
42184
42302
  sayRowMoves();
42185
42303
  if (movement.travelling || !travelByDrag || !pane) {
42186
42304
  return;
42187
42305
  }
42306
+ // Nothing will say when a trackpad's fingers leave, and a long stroke would keep
42307
+ // the first tab lit for the whole trip: the nearest page answers. A finger has a
42308
+ // lift to wait for (onLeftToSnap), and until then the page held stands.
42309
+ if (!finger) {
42310
+ const {
42311
+ base,
42312
+ toward,
42313
+ fraction
42314
+ } = readRowStretch(row, panes, panes[0]);
42315
+ followPageOnScreen(pages[panes.indexOf(fraction >= 0.5 && toward ? toward : base)], askedPageRef.current || pages[currentIndex]);
42316
+ }
42188
42317
  const pulled = row.scrollLeft - pane.offsetLeft;
42189
42318
  if (Math.abs(pulled) < SET_OFF_PULL) {
42190
42319
  return;
@@ -42237,7 +42366,7 @@ const RouteTravel = ({
42237
42366
  };
42238
42367
  const onLand = () => {
42239
42368
  const movement = movementRef.current;
42240
- if (!movement) {
42369
+ if (!movement || isClipped()) {
42241
42370
  return;
42242
42371
  }
42243
42372
  const panes = panesOf(row);
@@ -42266,8 +42395,48 @@ const RouteTravel = ({
42266
42395
  onLand,
42267
42396
  onLeftToSnap
42268
42397
  });
42398
+ // A hand back on a row taken from one: it is the hand's again, snapping included.
42399
+ const onHandBack = () => {
42400
+ if (row.getAttribute("data-route-row-taken") === "") {
42401
+ row.removeAttribute("data-route-row-taken");
42402
+ }
42403
+ };
42404
+ // A press for the page the address is already on changes nothing the router can
42405
+ // tell, and a stroke no lift ends leaves the address where it was: it is heard here,
42406
+ // the way a tab says which page it is through its address.
42407
+ const onPress = clickEvent => {
42408
+ const movement = movementRef.current;
42409
+ if (!movement?.byHand || movement.rowTaken) {
42410
+ return;
42411
+ }
42412
+ const link = clickEvent.target.closest?.("a[href]");
42413
+ const {
42414
+ pages,
42415
+ currentIndex
42416
+ } = latestRef.current;
42417
+ const page = pages[currentIndex];
42418
+ if (!link || !page || new URL(link.href).pathname !== pathOf(page)) {
42419
+ return;
42420
+ }
42421
+ movement.travelling = true;
42422
+ takeRowFromHand(movement);
42423
+ };
42424
+ row.addEventListener("touchstart", onHandBack, {
42425
+ passive: true
42426
+ });
42427
+ row.addEventListener("wheel", onHandBack, {
42428
+ passive: true
42429
+ });
42430
+ document.addEventListener("click", onPress, {
42431
+ capture: true
42432
+ });
42269
42433
  return () => {
42270
42434
  stopWatching();
42435
+ row.removeEventListener("touchstart", onHandBack);
42436
+ row.removeEventListener("wheel", onHandBack);
42437
+ document.removeEventListener("click", onPress, {
42438
+ capture: true
42439
+ });
42271
42440
  movementRef.current?.resumeScrollRecording?.();
42272
42441
  movementRef.current = null;
42273
42442
  publishRouteRowMove(null);
@@ -42287,6 +42456,30 @@ const RouteTravel = ({
42287
42456
  Promise.resolve().then(endMovement);
42288
42457
  }
42289
42458
  }, [currentIndex]);
42459
+
42460
+ // A neighbour shows from the first frame the compositor moves the row, before
42461
+ // any scroll event: a finger landing is the last word before that.
42462
+ const paintsNeighbours = Boolean(renderNeighbour);
42463
+ useLayoutEffect(() => {
42464
+ if (!paintsNeighbours) {
42465
+ return undefined;
42466
+ }
42467
+ const row = rowRef.current;
42468
+ const onTouchStart = () => {
42469
+ if (!movementRef.current && rowFollowsDocument(row)) {
42470
+ row.style.setProperty("--navi-route-row-pin", `${pinOf(row)}px`);
42471
+ }
42472
+ };
42473
+ row.addEventListener("touchstart", onTouchStart, {
42474
+ passive: true
42475
+ });
42476
+ return () => {
42477
+ row.removeEventListener("touchstart", onTouchStart);
42478
+ };
42479
+ }, [paintsNeighbours]);
42480
+ const rowContext = useMemo(() => ({
42481
+ renderNeighbour
42482
+ }), [renderNeighbour]);
42290
42483
  const rowKind = pages.length < 2 && windowed ? "still" : travelByDrag ? "" : "code";
42291
42484
  return jsx("div", {
42292
42485
  ...rest,
@@ -42299,12 +42492,20 @@ const RouteTravel = ({
42299
42492
  ,
42300
42493
  "data-travel-by-drag": travelByDrag && rowKind !== "still" ? "x" : undefined,
42301
42494
  children: windowed ? jsxs(Fragment, {
42302
- children: [pages.map((page, index) => jsx("div", {
42303
- "data-route-row-pane": "",
42304
- style: {
42305
- gridColumn: index + 1
42306
- }
42307
- }, index)), jsx("div", {
42495
+ children: [pages.map((page, index) => {
42496
+ const holdsNeighbour = paintsNeighbours && isNeighbourPane(index, liveIndex, movementRef.current);
42497
+ return jsx("div", {
42498
+ "data-route-row-pane": "",
42499
+ inert: holdsNeighbour,
42500
+ style: {
42501
+ gridColumn: index + 1
42502
+ },
42503
+ children: holdsNeighbour && jsx(RouteRowNeighbour, {
42504
+ renderNeighbour: renderNeighbour,
42505
+ page: page
42506
+ })
42507
+ }, index);
42508
+ }), jsx("div", {
42308
42509
  ref: liveRef,
42309
42510
  "data-route-row-live": "",
42310
42511
  style: {
@@ -42316,13 +42517,25 @@ const RouteTravel = ({
42316
42517
  "data-route-row-pictures": ""
42317
42518
  })]
42318
42519
  }) : jsx(RouteRowContext.Provider, {
42319
- value: true,
42520
+ value: rowContext,
42320
42521
  children: children
42321
42522
  })
42322
42523
  })
42323
42524
  });
42324
42525
  };
42325
42526
 
42527
+ // The panes on each side of the live page. Not one holding a picture of the page
42528
+ // left there: a page with no background of its own would show it through.
42529
+ const isNeighbourPane = (index, liveIndex, movement) => {
42530
+ if (index !== liveIndex - 1 && index !== liveIndex + 1) {
42531
+ return false;
42532
+ }
42533
+ return !movement || !movement.pictures.has(index);
42534
+ };
42535
+ const rowFollowsDocument = row => getScrollContainer(row.parentElement, {
42536
+ includeHidden: true
42537
+ }) === document.documentElement;
42538
+
42326
42539
  // How far down the row the reader is looking, for a page brought in: a page on
42327
42540
  // screen scrolls with the document, and one arriving has to start from there.
42328
42541
  const pinOf = row => {
@@ -42351,6 +42564,15 @@ const copyScrollPositions = (from, to) => {
42351
42564
  // A page of the row: a route, and the params that say which of its tabs when
42352
42565
  // several of them share it. Written as a bare route by a caller whose tabs are
42353
42566
  // routes of their own — which is the same page with nothing to tell apart.
42567
+ const afterFrames = (count, callback) => {
42568
+ if (count === 0) {
42569
+ callback();
42570
+ return;
42571
+ }
42572
+ requestAnimationFrame(() => {
42573
+ afterFrames(count - 1, callback);
42574
+ });
42575
+ };
42354
42576
  const normalizePage = page => page.isRoute ? {
42355
42577
  route: page,
42356
42578
  params: undefined
@@ -58059,6 +58281,10 @@ const SLIDE_TOWARD_ATTRIBUTE = "data-slide-travel-toward";
58059
58281
  // one: a hand on a row the browser scrolls can carry it past several slides, and
58060
58282
  // what follows it goes on between the two it is standing between.
58061
58283
  const SLIDE_BASE_ATTRIBUTE = "data-slide-travel-base";
58284
+ // The slide the row is on its way to while the current one has not changed yet,
58285
+ // because that only happens at rest: nothing is rendered and no address is written
58286
+ // under a snap, and what says where one is should not wait for it.
58287
+ const SLIDE_HEADING_ATTRIBUTE = "data-slide-heading";
58062
58288
  // Every way out that would DO something right now, in the words the commands
58063
58289
  // use: a direction ("left", "right", "up", "down") when there is a slide that
58064
58290
  // way, plus "first" and "last" when one is not already standing at that end of
@@ -58093,9 +58319,17 @@ const wordsOf = (element, attribute) => {
58093
58319
  return value ? value.split(" ") : NO_WAYS;
58094
58320
  };
58095
58321
 
58322
+ // The slide one is on, for whatever tells the reader where they are or what a press
58323
+ // would do: the current one while a hand holds the row, where the row is going once
58324
+ // it has let go. data-slide-current only changes at rest.
58325
+ const readSlideOnScreen = (element) =>
58326
+ element.getAttribute(SLIDE_HEADING_ATTRIBUTE) ??
58327
+ element.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ??
58328
+ undefined;
58329
+
58096
58330
  const readSlideContainerState = (element) => ({
58097
58331
  known: true,
58098
- current: element.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ?? undefined,
58332
+ current: readSlideOnScreen(element),
58099
58333
  toward: element.getAttribute(SLIDE_TOWARD_ATTRIBUTE) ?? undefined,
58100
58334
  // In DOM order — which is the order of the WALK for a line, and not
58101
58335
  // necessarily for a map: there the order is the one the areas are written in
@@ -58133,11 +58367,13 @@ const sameSlideContainerState = (a, b) =>
58133
58367
  * can: (wayOut: "left"|"right"|"up"|"down"|"first"|"last") => boolean,
58134
58368
  * held: (wayOut: "left"|"right"|"up"|"down"|"first"|"last") => boolean,
58135
58369
  * }} where the box stands. `current` is the slide on screen — the one being
58136
- * travelled TO while a travel plays, because that is what one is looking at;
58370
+ * travelled TO while a travel plays, and the one the row is going to once a hand
58371
+ * has let go, because that is what one is looking at. It can come before the
58372
+ * slide's own signal and `onCurrentChange`, which wait for the row to rest.
58137
58373
  * `toward` is the other slide in the frame while the picture is between two,
58138
- * and nothing at rest. `can` is "asking for it would do something", `held` is
58139
- * "it is there and this slide says no" — a way out is dead in both cases, and
58140
- * only the second is worth a word to the reader.
58374
+ * and nothing at rest. `can` is "asking for it would do something", from that
58375
+ * same slide, `held` is "it is there and this slide says no" — a way out is dead
58376
+ * in both cases, and only the second is worth a word to the reader.
58141
58377
  * `known` is false until the box has been read: no box was named and none is
58142
58378
  * above, the id names nothing, or — for one commit — this mounted before the
58143
58379
  * box had painted. Until then `can` answers YES, because it is the answer that
@@ -58991,7 +59227,7 @@ const Nav = ({
58991
59227
  }
58992
59228
  slideContainerElementRef.current = containerElement;
58993
59229
  const readContainer = () => {
58994
- setCurrentSlideArea(containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ?? undefined);
59230
+ setCurrentSlideArea(readSlideOnScreen(containerElement));
58995
59231
  paintIndicatorGeometryRef.current();
58996
59232
  scheduleTabScrollSyncRef.current();
58997
59233
  };
@@ -69163,6 +69399,10 @@ const css$I = /* css */`@property --slide-travel-progress {
69163
69399
  overflow-y: auto;
69164
69400
  }
69165
69401
 
69402
+ &[data-slide-row-taken] {
69403
+ overflow: clip;
69404
+ }
69405
+
69166
69406
  &:focus {
69167
69407
  outline: none;
69168
69408
  }
@@ -69214,6 +69454,77 @@ const css$I = /* css */`@property --slide-travel-progress {
69214
69454
  }
69215
69455
  `;
69216
69456
 
69457
+ // What a row left moving shows when a travel is asked for over it, read before the
69458
+ // slides are written: the one or two slides in the frame, and how far the track has
69459
+ // to stand, in a box that is no scroller, for that frame not to move.
69460
+ const readRowStand = (containerEl, slideElements) => {
69461
+ const restElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
69462
+ if (!restElement) {
69463
+ return null;
69464
+ }
69465
+ const {
69466
+ base,
69467
+ toward,
69468
+ fraction
69469
+ } = readRowStretch(containerEl, slideElements, restElement);
69470
+ return {
69471
+ base: readArea(base),
69472
+ toward: toward && fraction > 0 ? readArea(toward) : null,
69473
+ fraction,
69474
+ offset: `${-containerEl.scrollLeft}px ${-containerEl.scrollTop}px`
69475
+ };
69476
+ };
69477
+ // The stage of a travel asked for over a row left moving. What is in the frame stays
69478
+ // where it is, and the slide asked for comes one box past it, unless it is already
69479
+ // there: then what is left to make is a part of a box.
69480
+ const stageOverRow = (rowStand, area, realPlaceOf, stepAsked) => {
69481
+ const {
69482
+ base,
69483
+ toward,
69484
+ fraction
69485
+ } = rowStand;
69486
+ const basePlace = realPlaceOf(base);
69487
+ const placeByArea = new Map([[base, basePlace]]);
69488
+ const towardPlace = toward ? realPlaceOf(toward) : null;
69489
+ if (toward) {
69490
+ placeByArea.set(toward, towardPlace);
69491
+ }
69492
+ if (area === base || area === toward) {
69493
+ const otherArea = area === base ? toward : base;
69494
+ if (!otherArea) {
69495
+ return {
69496
+ placeByArea,
69497
+ leaningArea: null,
69498
+ progress: 0
69499
+ };
69500
+ }
69501
+ const from = realPlaceOf(otherArea);
69502
+ const to = realPlaceOf(area);
69503
+ const sign = Math.sign(to.x - from.x) || Math.sign(to.y - from.y);
69504
+ return {
69505
+ placeByArea,
69506
+ leaningArea: otherArea,
69507
+ progress: sign * (area === base ? fraction : 1 - fraction)
69508
+ };
69509
+ }
69510
+ const to = realPlaceOf(area);
69511
+ const step = stepAsked || {
69512
+ x: Math.sign(to.x - basePlace.x),
69513
+ y: Math.sign(to.y - basePlace.y)
69514
+ };
69515
+ const pastToward = towardPlace && (towardPlace.x - basePlace.x) * step.x + (towardPlace.y - basePlace.y) * step.y > 0;
69516
+ const side = pastToward ? towardPlace : basePlace;
69517
+ placeByArea.set(area, {
69518
+ x: side.x + step.x,
69519
+ y: side.y + step.y
69520
+ });
69521
+ return {
69522
+ placeByArea,
69523
+ leaningArea: toward && fraction >= 0.5 ? toward : base,
69524
+ progress: step.x || step.y
69525
+ };
69526
+ };
69527
+
69217
69528
  // How much of a travel is left to make, as a fraction of the travel that was
69218
69529
  // asked for: 1 for a move starting from rest, less for one picked up while the
69219
69530
  // track was already moving. Everything is measured in px against the track's
@@ -69230,6 +69541,19 @@ const ratioOfOneTravel = (track, from, to, targetBefore) => {
69230
69541
  const ratio = left / asked;
69231
69542
  return ratio > 1 ? 1 : ratio;
69232
69543
  };
69544
+ // The same for a travel taken over a row, which nobody asked for from anywhere in
69545
+ // particular: what is left, in boxes. Laid out sizes, like the scroll position that
69546
+ // `from` is read off: a row inside a popup still growing is not its transformed size.
69547
+ const ratioOfOneBox = (track, from, to) => {
69548
+ const box = {
69549
+ width: track.offsetWidth,
69550
+ height: track.offsetHeight
69551
+ };
69552
+ const start = offsetToPx(from, box);
69553
+ const end = offsetToPx(to, box);
69554
+ const ratio = Math.abs(end.x - start.x) / box.width + Math.abs(end.y - start.y) / box.height;
69555
+ return ratio > 1 ? 1 : ratio;
69556
+ };
69233
69557
 
69234
69558
  // A translate ("-100% 0%", "-260px 0px", "none") as two numbers of pixels.
69235
69559
  // Percentages are the size of the box, which is what a translate resolves them
@@ -69807,6 +70131,10 @@ const SlideContainer = ({
69807
70131
  // The slide a hand has just landed on, for the render it asks for: it is
69808
70132
  // already there, so there is nothing to play.
69809
70133
  const handLandedRef = useRef(null);
70134
+ // The slide a row is being carried away from, pressed: it stays current, so only
70135
+ // a render says the row has to come back to it (see goToArea).
70136
+ const rowBackAskedRef = useRef(false);
70137
+ const [, askRowBack] = useState(0);
69810
70138
  // Where the box rests on the row, and what the row starts from: a slide that
69811
70139
  // holds on to the user ends the row on that side, and a scroll range starts at
69812
70140
  // its first reachable slide (see writeRowAtRest).
@@ -70051,6 +70379,26 @@ const SlideContainer = ({
70051
70379
  const paintCurrentArea = area => {
70052
70380
  containerRef.current?.setAttribute(SLIDE_CURRENT_ATTRIBUTE, area);
70053
70381
  };
70382
+
70383
+ // Where the row is going, for as long as the current slide has not changed:
70384
+ // anything saying where one is reads this first, and so does what a press would
70385
+ // do from there (see readSlideOnScreen). Left to the caller to announce, a landing
70386
+ // having more to say in the same breath.
70387
+ const writeHeading = headingElement => {
70388
+ const containerEl = containerRef.current;
70389
+ const area = headingElement ? readArea(headingElement) : null;
70390
+ // A window does not change slide, it rolls: it has nowhere to be heading.
70391
+ const heading = loop || area === null || area === containerEl.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ? null : area;
70392
+ if (heading === containerEl.getAttribute(SLIDE_HEADING_ATTRIBUTE)) {
70393
+ return;
70394
+ }
70395
+ if (heading === null) {
70396
+ containerEl.removeAttribute(SLIDE_HEADING_ATTRIBUTE);
70397
+ } else {
70398
+ containerEl.setAttribute(SLIDE_HEADING_ATTRIBUTE, heading);
70399
+ }
70400
+ paintWays();
70401
+ };
70054
70402
  const markAnswered = area => {
70055
70403
  const order = readMap().slideElements.map(readArea);
70056
70404
  const rank = order.indexOf(area);
@@ -70216,6 +70564,7 @@ const SlideContainer = ({
70216
70564
  offsetRef.current = "0% 0%";
70217
70565
  track.style.setProperty("--slide-container-offset", "0% 0%");
70218
70566
  containerRef.current.removeAttribute("data-slide-travelling");
70567
+ containerRef.current.removeAttribute("data-slide-row-taken");
70219
70568
  sendRowToRest(currentElement);
70220
70569
  renderNear(currentArea, {
70221
70570
  alongside: true
@@ -70256,6 +70605,10 @@ const SlideContainer = ({
70256
70605
  slideElements[0];
70257
70606
  const currentArea = readArea(currentElement);
70258
70607
  paintCurrentArea(currentArea);
70608
+ // In the commit the slide changes in, so nothing reads the slide left in between.
70609
+ if (!handMovingRef.current) {
70610
+ writeHeading(null);
70611
+ }
70259
70612
  const realPlaceOf = area => placeOf.get(area) || {
70260
70613
  x: 0,
70261
70614
  y: 0
@@ -70286,8 +70639,26 @@ const SlideContainer = ({
70286
70639
  // between the picture and the slide arriving, on the axis it walks.
70287
70640
  let travelStep = null;
70288
70641
  const drawnArea = stage ? stage.area : drawnAreaRef.current;
70289
- const travelStarts = !still && durationMs > 0 && drawnArea !== undefined && drawnArea !== currentArea && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
70290
- if (travelStarts) {
70642
+ const rowBackAsked = rowBackAskedRef.current;
70643
+ rowBackAskedRef.current = false;
70644
+ // Asked for over a row a hand has left moving: the row belongs to what was asked
70645
+ // for, and where the hand's snap would have ended decides nothing.
70646
+ const rowTaken = handMovingRef.current && !handLanded && (drawnArea !== currentArea || rowBackAsked);
70647
+ const rowStand = rowTaken ? readRowStand(containerRef.current, slideElements) : null;
70648
+ if (rowTaken) {
70649
+ handMovingRef.current = false;
70650
+ writeHeading(null);
70651
+ paintTravelProgress();
70652
+ }
70653
+ const travelStarts = !still && durationMs > 0 && drawnArea !== undefined && (drawnArea !== currentArea || rowTaken) && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
70654
+ let overRow = null;
70655
+ if (travelStarts && rowStand) {
70656
+ overRow = stageOverRow(rowStand, currentArea, realPlaceOf, travelStepRef.current);
70657
+ stage = stageRef.current = {
70658
+ placeByArea: overRow.placeByArea,
70659
+ area: currentArea
70660
+ };
70661
+ } else if (travelStarts) {
70291
70662
  // Where the slide being left IS — the place the stage gave it if a travel
70292
70663
  // was already playing (a press landing mid-travel departs from where the
70293
70664
  // eye is, not from the map), its own place otherwise.
@@ -70372,9 +70743,16 @@ const SlideContainer = ({
70372
70743
  }
70373
70744
  const track = trackRef.current;
70374
70745
  containerRef.current.toggleAttribute("data-slide-travelling", Boolean(stage));
70375
- // How far the track stands from where the scroll position rests: none at rest.
70746
+ if (rowTaken) {
70747
+ containerRef.current.setAttribute("data-slide-row-taken", "");
70748
+ }
70749
+ // How far the track stands from where the scroll position rests: none at rest. A
70750
+ // box that is no scroller has no position, and the track says it all.
70376
70751
  const rowPlace = onRow(currentPlace);
70377
- const restPlace = restPlaceRef.current;
70752
+ const restPlace = containerRef.current.hasAttribute("data-slide-row-taken") ? {
70753
+ x: 0,
70754
+ y: 0
70755
+ } : restPlaceRef.current;
70378
70756
  const offset = `${(restPlace.x - rowPlace.x) * 100}% ${(restPlace.y - rowPlace.y) * 100}%`;
70379
70757
  // The travel that was ASKED for is still one box, whatever is left of it to
70380
70758
  // cover: what is left takes what is left of the duration.
@@ -70397,6 +70775,9 @@ const SlideContainer = ({
70397
70775
  const onScreenPx = trackOffsetPx(track, containerRef.current);
70398
70776
  offsetOnScreen = `${onScreenPx.x}px ${onScreenPx.y}px`;
70399
70777
  }
70778
+ if (overRow) {
70779
+ offsetOnScreen = rowStand.offset;
70780
+ }
70400
70781
  const offsetBefore = offsetOnScreen ?? offsetRef.current;
70401
70782
  offsetRef.current = offset;
70402
70783
  track.style.setProperty("--slide-container-offset", offset);
@@ -70415,10 +70796,10 @@ const SlideContainer = ({
70415
70796
  // short distance for a full duration. One box in `duration` is the pace;
70416
70797
  // this only ever shortens it (a longer travel is not made slower, which
70417
70798
  // would make a two-box move drag).
70418
- const travelRatio = ratioOfOneTravel(track, offsetBefore, offset, offsetTargetBefore);
70799
+ const travelRatio = overRow ? ratioOfOneBox(track, offsetBefore, offset) : ratioOfOneTravel(track, offsetBefore, offset, offsetTargetBefore);
70419
70800
  // Already moving, so there is no ease-in to play: it would stall the track
70420
70801
  // for an instant right where the eye is following it.
70421
- const easing = travelInFlight ? "ease-out" : "ease";
70802
+ const easing = travelInFlight || overRow ? "ease-out" : "ease";
70422
70803
  // Cancelled rather than layered: two animations on the same property
70423
70804
  // would blend, and what one sees then is neither of the two moves.
70424
70805
  trackAnimationRef.current?.cancel();
@@ -70426,15 +70807,21 @@ const SlideContainer = ({
70426
70807
  translate: offsetBefore
70427
70808
  }, {
70428
70809
  translate: offset
70429
- }], {
70810
+ }],
70811
+ // Backwards: the first frame's clock can read a fraction of a millisecond
70812
+ // before the start time Chrome gives the animation, which would show the
70813
+ // travel already over.
70814
+ {
70430
70815
  duration: durationMs * travelRatio,
70431
- easing
70816
+ easing,
70817
+ fill: "backwards"
70432
70818
  });
70433
70819
  // The trait travels with the slides, from a whole box away.
70434
- const progressFrom = travelStep ? travelStep.x || travelStep.y : 0;
70820
+ const stepProgress = travelStep ? travelStep.x || travelStep.y : 0;
70821
+ const progressFrom = overRow ? overRow.progress : stepProgress;
70435
70822
  // The slide the picture leans on for the length of it is the one being
70436
70823
  // LEFT: it is the second one in the frame until the travel is over.
70437
- animateTravelProgress(progressFrom, durationMs * travelRatio, easing, drawnArea);
70824
+ animateTravelProgress(progressFrom, durationMs * travelRatio, easing, overRow ? overRow.leaningArea : drawnArea);
70438
70825
  // Presses still waiting behind this one: it is already late, so it is
70439
70826
  // sent home at once rather than played out at the pace of someone who
70440
70827
  // has stopped pressing. Someone pressing → four times is asking to be
@@ -70455,6 +70842,17 @@ const SlideContainer = ({
70455
70842
  // thing it was standing for is over.
70456
70843
  settleTravel();
70457
70844
  }
70845
+ // The row taken with nothing playing to give it back (no duration, a page change
70846
+ // carrying the slide): no scroller for as long as the compositor takes to hear it.
70847
+ if (!stageRef.current && containerRef.current.hasAttribute("data-slide-row-taken")) {
70848
+ requestAnimationFrame(() => {
70849
+ requestAnimationFrame(() => {
70850
+ if (trackAnimationRef.current?.playState !== "running") {
70851
+ settleTravel();
70852
+ }
70853
+ });
70854
+ });
70855
+ }
70458
70856
  // A window waiting for its travel to be over (see goToArea's own loop
70459
70857
  // branch): the animation says when, and says it about the move that just
70460
70858
  // started rather than about a duration counted out beside it.
@@ -70518,9 +70916,9 @@ const SlideContainer = ({
70518
70916
  sendRowToRest(currentElement);
70519
70917
  }
70520
70918
  // Said last, and after data-current has been written: what a travel would do
70521
- // is read off the map from the slide that is now current, and off the locks
70522
- // that slide is wearing in this very commit.
70523
- paintWays(currentElement);
70919
+ // is read off the map from the slide on screen, and off the locks that slide is
70920
+ // wearing in this very commit.
70921
+ paintWays();
70524
70922
  // …and once everything this commit had to write is written, the box says so
70525
70923
  // — a single announcement for a render that may have changed several of the
70526
70924
  // facts at once.
@@ -70567,14 +70965,18 @@ const SlideContainer = ({
70567
70965
  placeOf
70568
70966
  } = readMap();
70569
70967
  const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
70570
- if (!area || area === readArea(currentElement)) {
70968
+ if (!area) {
70571
70969
  return false;
70572
70970
  }
70971
+ // What a press leaves is the slide on screen, which is what paintWays offers from.
70972
+ // A landing is the row arriving there, and what it leaves is the current one.
70973
+ const areaLeft = landed ? readArea(currentElement) : readSlideOnScreen(containerRef.current) ?? readArea(currentElement);
70974
+ const elementLeft = slideElements.find(slideElement => readArea(slideElement) === areaLeft) || currentElement;
70573
70975
  if (forward === undefined) {
70574
70976
  // Asked for by name (--navi-go-to-slide, --navi-back) rather than by a
70575
70977
  // direction: which way it is is still a fact, read off the map, and it is
70576
70978
  // what says which of the two locks below applies.
70577
- const from = placeOf.get(readArea(currentElement)) || {
70979
+ const from = placeOf.get(areaLeft) || {
70578
70980
  x: 0,
70579
70981
  y: 0
70580
70982
  };
@@ -70592,9 +70994,18 @@ const SlideContainer = ({
70592
70994
  // one departure the forward hold does not apply to. Said here rather than
70593
70995
  // by dropping the attribute, which stays the render's to write — a hold
70594
70996
  // meant to stay (an explicit preventNavNext) is still there on the way back.
70595
- if (forward ? !released && currentElement?.hasAttribute("data-prevent-nav-next") : currentElement?.hasAttribute("data-prevent-nav-previous")) {
70997
+ if (area !== areaLeft && (forward ? !released && elementLeft.hasAttribute("data-prevent-nav-next") : elementLeft.hasAttribute("data-prevent-nav-previous"))) {
70596
70998
  return false;
70597
70999
  }
71000
+ if (area === readArea(currentElement)) {
71001
+ // Nowhere to go, unless a hand has left the row on its way off this slide.
71002
+ if (!handMovingRef.current) {
71003
+ return false;
71004
+ }
71005
+ rowBackAskedRef.current = true;
71006
+ askRowBack(count => count + 1);
71007
+ return true;
71008
+ }
70598
71009
  // The focus moves here, while the event that asked for it is still in hand:
70599
71010
  // who pressed what, and whether they were on the keyboard, is knowable now
70600
71011
  // and gone a render later. The slide arriving is made reachable on the spot
@@ -70976,7 +71387,7 @@ const SlideContainer = ({
70976
71387
  const move = (dx, dy, event, {
70977
71388
  value,
70978
71389
  released
70979
- } = {}) => goToArea(areaTowards(dx, dy), {
71390
+ } = {}) => goToArea(areaTowards(dx, dy, readSlideOnScreen(containerRef.current)), {
70980
71391
  forward: dx > 0 || dy > 0,
70981
71392
  event,
70982
71393
  value,
@@ -71054,11 +71465,13 @@ const SlideContainer = ({
71054
71465
  // Where a hand has the row, in the terms of --slide-travel-progress: from the
71055
71466
  // current slide while it stands next to it, and past that between the two it
71056
71467
  // stands between, as far as the hand carries it.
71057
- const paintNativeRow = () => {
71058
- if (trackAnimationRef.current?.playState === "running") {
71468
+ const paintNativeRow = ({
71469
+ finger
71470
+ }) => {
71471
+ const containerEl = containerRef.current;
71472
+ if (trackAnimationRef.current?.playState === "running" || containerEl.hasAttribute("data-slide-row-taken")) {
71059
71473
  return;
71060
71474
  }
71061
- const containerEl = containerRef.current;
71062
71475
  const slideElements = Array.from(trackRef.current.children);
71063
71476
  const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
71064
71477
  if (!currentElement) {
@@ -71066,6 +71479,9 @@ const SlideContainer = ({
71066
71479
  }
71067
71480
  if (restsOn(containerEl, currentElement)) {
71068
71481
  if (handMovingRef.current) {
71482
+ if (!finger) {
71483
+ writeHeading(null);
71484
+ }
71069
71485
  paintTravelProgress();
71070
71486
  }
71071
71487
  return;
@@ -71086,6 +71502,12 @@ const SlideContainer = ({
71086
71502
  }
71087
71503
  element.style.setProperty("--slide-travel-progress", forward ? -fraction : fraction);
71088
71504
  }
71505
+ // Nothing will say when a trackpad's fingers leave, and a long stroke would
71506
+ // keep the first tab lit for the whole trip: the nearest slide answers. A finger
71507
+ // has a lift to wait for (headNativeRow), and until then the slide held stands.
71508
+ if (!finger) {
71509
+ writeHeading(fraction >= 0.5 && toward ? toward : base);
71510
+ }
71089
71511
  paintTravelToward(toward ? readArea(toward) : null);
71090
71512
  // What the hand reaches ahead of the neighbours built at rest is asked for on
71091
71513
  // the way: an empty slide goes by otherwise.
@@ -71093,6 +71515,28 @@ const SlideContainer = ({
71093
71515
  alongside: true
71094
71516
  });
71095
71517
  };
71518
+
71519
+ // A finger has lifted and the row moves: on past the slide it stands after, or
71520
+ // back to it. Already standing on one, it has arrived, whichever way it came.
71521
+ const headNativeRow = movedForward => {
71522
+ if (!handMovingRef.current || trackAnimationRef.current?.playState === "running") {
71523
+ return;
71524
+ }
71525
+ const slideElements = Array.from(trackRef.current.children);
71526
+ const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
71527
+ if (!currentElement) {
71528
+ return;
71529
+ }
71530
+ const {
71531
+ base,
71532
+ toward,
71533
+ fraction,
71534
+ forward
71535
+ } = readRowStretch(containerRef.current, slideElements, currentElement);
71536
+ const goesOn = fraction > 0 && movedForward === forward;
71537
+ writeHeading(goesOn && toward ? toward : base);
71538
+ announceState();
71539
+ };
71096
71540
  const landNativeRow = scrollEndEvent => {
71097
71541
  if (!handMovingRef.current) {
71098
71542
  return;
@@ -71107,13 +71551,15 @@ const SlideContainer = ({
71107
71551
  return;
71108
71552
  }
71109
71553
  handMovingRef.current = false;
71110
- paintTravelProgress();
71111
71554
  const area = readArea(landedElement);
71112
71555
  const currentArea = containerEl.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
71113
71556
  if (area === currentArea) {
71557
+ writeHeading(null);
71558
+ paintTravelProgress();
71114
71559
  settleTravel();
71115
71560
  return;
71116
71561
  }
71562
+ paintTravelProgress();
71117
71563
  const event = scrollEndEvent ?? new Event("scrollend");
71118
71564
  if (loop) {
71119
71565
  // A window does not change slide, it rolls: back at rest in the render the
@@ -71144,6 +71590,8 @@ const SlideContainer = ({
71144
71590
  }
71145
71591
  // Refused: the row is put back in view of the slide that stays.
71146
71592
  handLandedRef.current = null;
71593
+ writeHeading(null);
71594
+ announceState();
71147
71595
  const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
71148
71596
  containerEl.scrollTo({
71149
71597
  left: currentElement.offsetLeft,
@@ -71157,11 +71605,13 @@ const SlideContainer = ({
71157
71605
  const nativeRowHandlersRef = useRef(null);
71158
71606
  nativeRowHandlersRef.current = {
71159
71607
  paintNativeRow,
71608
+ headNativeRow,
71160
71609
  landNativeRow
71161
71610
  };
71162
71611
  useLayoutEffect(() => {
71163
71612
  return watchRowScroll(containerRef.current, {
71164
- onScroll: () => nativeRowHandlersRef.current.paintNativeRow(),
71613
+ onScroll: move => nativeRowHandlersRef.current.paintNativeRow(move),
71614
+ onLeftToSnap: forward => nativeRowHandlersRef.current.headNativeRow(forward),
71165
71615
  onLand: event => nativeRowHandlersRef.current.landNativeRow(event)
71166
71616
  });
71167
71617
  }, []);
@@ -71239,13 +71689,19 @@ const SlideContainer = ({
71239
71689
  // ([data-slide-ways~="right"]), or through useSlideContainer, which watches
71240
71690
  // them. Nothing is told, everything is read: it is what lets them sit
71241
71691
  // anywhere, and what lets a container nobody drives still be followed.
71242
- const paintWays = currentElement => {
71692
+ const paintWays = () => {
71243
71693
  const ways = [];
71244
71694
  const held = [];
71695
+ const {
71696
+ slideElements,
71697
+ order
71698
+ } = readMap();
71699
+ const areaOnScreen = readSlideOnScreen(containerRef.current);
71700
+ const elementOnScreen = slideElements.find(slideElement => readArea(slideElement) === areaOnScreen);
71245
71701
  // Whether the slide being left says no, this way. The one gate again, read
71246
71702
  // exactly as goToArea reads it at the moment of the travel — so what is
71247
71703
  // published here and what would happen cannot disagree.
71248
- const holdsTowards = forward => currentElement?.hasAttribute(forward ? "data-prevent-nav-next" : "data-prevent-nav-previous");
71704
+ const holdsTowards = forward => elementOnScreen?.hasAttribute(forward ? "data-prevent-nav-next" : "data-prevent-nav-previous");
71249
71705
  const say = (name, offered, forward) => {
71250
71706
  if (!offered) {
71251
71707
  return;
@@ -71262,18 +71718,14 @@ const SlideContainer = ({
71262
71718
  // would be saying it about every row on the page.
71263
71719
  continue;
71264
71720
  }
71265
- say(direction, Boolean(areaTowards(dx, dy)), dx > 0 || dy > 0);
71721
+ say(direction, Boolean(areaTowards(dx, dy, areaOnScreen)), dx > 0 || dy > 0);
71266
71722
  }
71267
71723
  // The two ends are ways out like the others — "all the way that way", said
71268
71724
  // by the map's own order rather than by a direction (see goToEnd, and
71269
71725
  // SlideContainer.First / .Last). Offered while one is not already standing
71270
71726
  // there, and held by the same lock the direction they lie in would be.
71271
- const {
71272
- order
71273
- } = readMap();
71274
- const currentArea = currentElement ? readArea(currentElement) : undefined;
71275
- say("first", order.length > 0 && order[0] !== currentArea, false);
71276
- say("last", order.length > 0 && order[order.length - 1] !== currentArea, true);
71727
+ say("first", order.length > 0 && order[0] !== areaOnScreen, false);
71728
+ say("last", order.length > 0 && order[order.length - 1] !== areaOnScreen, true);
71277
71729
  for (const element of travelPainters()) {
71278
71730
  if (ways.length) {
71279
71731
  element.setAttribute(SLIDE_WAYS_ATTRIBUTE, ways.join(" "));
@@ -71366,7 +71818,8 @@ const SlideContainer = ({
71366
71818
  "--slide-travel-progress": 0
71367
71819
  }], {
71368
71820
  duration: durationMs,
71369
- easing
71821
+ easing,
71822
+ fill: "backwards"
71370
71823
  }));
71371
71824
  progressAnimationsRef.current[0].finished.then(() => {
71372
71825
  progressAnimationsRef.current = [];
@@ -107832,7 +108285,9 @@ const StepList = ({
107832
108285
  const read = () => {
107833
108286
  const currentArea = containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
107834
108287
  const towardArea = containerElement.getAttribute(SLIDE_TOWARD_ATTRIBUTE);
107835
- setContainerCurrent(currentArea ?? undefined);
108288
+ // Which step is lit follows the screen. The halo counts from the current slide,
108289
+ // which is what the progress it draws with is measured against.
108290
+ setContainerCurrent(readSlideOnScreen(containerElement));
107836
108291
  const currentIdx = currentArea === null ? -1 : indexOf(currentArea);
107837
108292
  if (currentIdx === -1) {
107838
108293
  // A slide no step names (a confirmation screen): the halo is not