@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.
@@ -24953,7 +24953,7 @@ defineInteractionDetector({
24953
24953
  const transitionAfter = peekLastViewTransition();
24954
24954
  const drawn =
24955
24955
  transitionAfter === transitionBefore
24956
- ? afterFrames(2)
24956
+ ? afterFrames$1(2)
24957
24957
  : transitionAfter.finished.then(NOOP, NOOP);
24958
24958
  const write = {
24959
24959
  settled: false,
@@ -25212,7 +25212,7 @@ defineInteractionDetector({
25212
25212
 
25213
25213
  const NOOP = () => {};
25214
25214
 
25215
- const afterFrames = (count) =>
25215
+ const afterFrames$1 = (count) =>
25216
25216
  new Promise((resolve) => {
25217
25217
  const next = (remaining) => {
25218
25218
  if (remaining === 0) {
@@ -36505,9 +36505,23 @@ const collectRoutePages = children => {
36505
36505
  * Whether the next container down is a row a hand scrolls: every page has a
36506
36506
  * place on it from the start, and the browser moves between them (see
36507
36507
  * RouteTravel's `nativeScroll`). Told to one container, not to the pages it holds.
36508
+ * False, or `{ renderNeighbour }`.
36508
36509
  */
36509
36510
  const RouteRowContext = createContext(false);
36510
36511
 
36512
+ /**
36513
+ * What stands in the pane of a neighbour until its page does. A component of its
36514
+ * own: what the application calls in there (a hook, a signal) answers to it, however
36515
+ * many neighbours the row has at that moment.
36516
+ */
36517
+ const RouteRowNeighbour = ({
36518
+ renderNeighbour,
36519
+ page
36520
+ }) => jsx("div", {
36521
+ "data-route-row-neighbour": "",
36522
+ children: renderNeighbour(page)
36523
+ });
36524
+
36511
36525
  // A container as a row: one pane per leaf, in the order they are written. A page
36512
36526
  // still builds when its route first matches, since a route's actions only ask
36513
36527
  // while it does: before that its pane is empty. Once built it stays, with what
@@ -36520,6 +36534,9 @@ const RouteRowContext = createContext(false);
36520
36534
  const RouteRowContainer = ({
36521
36535
  children
36522
36536
  }) => {
36537
+ const {
36538
+ renderNeighbour
36539
+ } = useContext(RouteRowContext);
36523
36540
  const {
36524
36541
  activeBranch,
36525
36542
  leaves
@@ -36568,16 +36585,19 @@ const RouteRowContainer = ({
36568
36585
  shown: true
36569
36586
  }, "fallback");
36570
36587
  }
36571
- return leaves.map(leaf => jsx(RouteRowPane, {
36588
+ const shownIndex = activeBranch ? leaves.findIndex(leaf => leaf.id === activeBranch.id) : -1;
36589
+ return leaves.map((leaf, index) => jsx(RouteRowPane, {
36572
36590
  branch: leaf,
36573
36591
  page: pageOf(leaf),
36574
- shown: activeBranch !== null && leaf.id === activeBranch.id
36592
+ shown: index === shownIndex,
36593
+ renderNeighbour: shownIndex !== -1 && (index === shownIndex - 1 || index === shownIndex + 1) ? renderNeighbour : undefined
36575
36594
  }, leaf.id));
36576
36595
  };
36577
36596
  const RouteRowPane = ({
36578
36597
  branch,
36579
36598
  page,
36580
- shown
36599
+ shown,
36600
+ renderNeighbour
36581
36601
  }) => {
36582
36602
  // The very vnode rendered last, while not on screen: Preact does not look into
36583
36603
  // it, and a page nobody can see costs nothing when the address moves.
@@ -36596,7 +36616,10 @@ const RouteRowPane = ({
36596
36616
  "data-route-row-pane": "",
36597
36617
  "data-built": contentRef.current ? "" : undefined,
36598
36618
  inert: !shown,
36599
- children: contentRef.current
36619
+ children: contentRef.current || renderNeighbour && jsx(RouteRowNeighbour, {
36620
+ renderNeighbour: renderNeighbour,
36621
+ page: branchRoutePage(branch)
36622
+ })
36600
36623
  });
36601
36624
  };
36602
36625
 
@@ -40279,8 +40302,9 @@ const publishRouteRowMove = (move) => {
40279
40302
  * itself; a counter, a title or a breadcrumb around the box reads it first.
40280
40303
  *
40281
40304
  * It goes back to `null` as soon as the hand has let go and the row moves. Nothing
40282
- * says when the fingers leave a trackpad: there it lasts until the row has come to
40283
- * rest. A travel no hand set off holds nothing: it goes where it goes.
40305
+ * says when the fingers leave a trackpad: there it is the page nearest the row, until
40306
+ * the row has come to rest. A travel no hand set off holds nothing: it goes where it
40307
+ * goes.
40284
40308
  *
40285
40309
  * One for the document. `route` says which row, for a page holding several boxes.
40286
40310
  */
@@ -40298,6 +40322,13 @@ const holdPage = (page, headingPage) => {
40298
40322
  }
40299
40323
  };
40300
40324
 
40325
+ // A hand no browser says has let go: the page nearest the row is where the reader is,
40326
+ // and the address, wherever it stands, is not.
40327
+ const followPageOnScreen = (page, addressPage) => {
40328
+ headingPageSignal.value = addressPage;
40329
+ routeTravelSettledPageSignal.value = page;
40330
+ };
40331
+
40301
40332
  const releaseHeldPage = () => {
40302
40333
  routeTravelSettledPageSignal.value = null;
40303
40334
  headingPageSignal.value = null;
@@ -40412,7 +40443,9 @@ const readRowStretch = (scroller, pages, page) => {
40412
40443
  /**
40413
40444
  * @param {Element} scroller
40414
40445
  * @param {object} options
40415
- * @param {() => void} options.onScroll - the row moved.
40446
+ * @param {(move: {finger: boolean}) => void} options.onScroll - the row moved.
40447
+ * `finger`: a touch has come since the row last stopped, so a lift will say where
40448
+ * it goes (`onLeftToSnap`). Without one (a trackpad, a wheel) nothing will.
40416
40449
  * @param {(event: Event|null) => void} options.onLand - the row may have stopped.
40417
40450
  * Read `pageAtRest` to know whether it stopped on a page. Heard more than once
40418
40451
  * for one landing, so what it does must find nothing to do the second time.
@@ -40428,11 +40461,13 @@ const watchRowScroll = (
40428
40461
  ) => {
40429
40462
  let stoppedTimeout = null;
40430
40463
  let touching = false;
40464
+ let touchedSinceStop = false;
40431
40465
  let liftedAt = null;
40432
40466
  const landOnSilence = () => {
40433
40467
  liftedAt = null;
40434
40468
  // A finger holding the row still: nothing has stopped.
40435
40469
  if (!touching) {
40470
+ touchedSinceStop = false;
40436
40471
  onLand(null);
40437
40472
  }
40438
40473
  };
@@ -40441,7 +40476,7 @@ const watchRowScroll = (
40441
40476
  stoppedTimeout = setTimeout(landOnSilence, STOPPED_SILENCE);
40442
40477
  };
40443
40478
  const onScrollEvent = () => {
40444
- onScroll();
40479
+ onScroll({ finger: touchedSinceStop });
40445
40480
  armSilence();
40446
40481
  if (!liftedAt) {
40447
40482
  return;
@@ -40455,11 +40490,6 @@ const watchRowScroll = (
40455
40490
  liftedAt = null;
40456
40491
  onLeftToSnap?.(moved > 0);
40457
40492
  };
40458
- const onTouchStart = () => {
40459
- touching = true;
40460
- liftedAt = null;
40461
- clearTimeout(stoppedTimeout);
40462
- };
40463
40493
  const onTouchEnd = (touchEvent) => {
40464
40494
  touching = touchEvent.touches.length > 0;
40465
40495
  if (!touching) {
@@ -40467,23 +40497,38 @@ const watchRowScroll = (
40467
40497
  armSilence();
40468
40498
  }
40469
40499
  };
40500
+ const onTouchStart = (touchEvent) => {
40501
+ touching = true;
40502
+ touchedSinceStop = true;
40503
+ liftedAt = null;
40504
+ clearTimeout(stoppedTimeout);
40505
+ // Listened for on what the finger landed on, where the end of a touch is always
40506
+ // told: a page changing under the finger may have taken it out of the document by
40507
+ // then, and nothing comes up to the row from there.
40508
+ const { target } = touchEvent;
40509
+ const onEnd = (endEvent) => {
40510
+ target.removeEventListener("touchend", onEnd);
40511
+ target.removeEventListener("touchcancel", onEnd);
40512
+ onTouchEnd(endEvent);
40513
+ };
40514
+ target.addEventListener("touchend", onEnd, { passive: true });
40515
+ target.addEventListener("touchcancel", onEnd, { passive: true });
40516
+ };
40470
40517
  scroller.addEventListener("scroll", onScrollEvent, { passive: true });
40471
40518
  scroller.addEventListener("scrollend", onLand);
40472
40519
  scroller.addEventListener("touchstart", onTouchStart, { passive: true });
40473
- scroller.addEventListener("touchend", onTouchEnd, { passive: true });
40474
- scroller.addEventListener("touchcancel", onTouchEnd, { passive: true });
40475
40520
  return () => {
40476
40521
  clearTimeout(stoppedTimeout);
40477
40522
  scroller.removeEventListener("scroll", onScrollEvent);
40478
40523
  scroller.removeEventListener("scrollend", onLand);
40479
40524
  scroller.removeEventListener("touchstart", onTouchStart);
40480
- scroller.removeEventListener("touchend", onTouchEnd);
40481
- scroller.removeEventListener("touchcancel", onTouchEnd);
40482
40525
  };
40483
40526
  };
40484
40527
 
40485
40528
  installImportMetaCssBuild(import.meta);
40486
40529
  const css$16 = /* css */`.navi_route_travel {
40530
+ flex-direction: column;
40531
+ display: flex;
40487
40532
  position: relative;
40488
40533
  }
40489
40534
 
@@ -40514,6 +40559,19 @@ const css$16 = /* css */`.navi_route_travel {
40514
40559
  overflow: visible;
40515
40560
  }
40516
40561
 
40562
+ &[data-route-row-taken] {
40563
+ scroll-snap-type: none;
40564
+ }
40565
+
40566
+ &[data-route-row-taken="clip"] {
40567
+ overflow: clip;
40568
+
40569
+ & > [data-route-row-pane], & > [data-route-row-live], & > [data-route-row-pictures] > * {
40570
+ left: var(--navi-route-row-stand);
40571
+ position: relative;
40572
+ }
40573
+ }
40574
+
40517
40575
  & > [data-route-row-pane] {
40518
40576
  scroll-snap-align: start;
40519
40577
  scroll-snap-stop: always;
@@ -40535,6 +40593,15 @@ const css$16 = /* css */`.navi_route_travel {
40535
40593
  overflow: clip;
40536
40594
  }
40537
40595
 
40596
+ & > [data-route-row-pane] > [data-route-row-neighbour] {
40597
+ contain: layout paint;
40598
+ visibility: visible;
40599
+ flex-direction: column;
40600
+ display: flex;
40601
+ position: absolute;
40602
+ inset: 0;
40603
+ }
40604
+
40538
40605
  & > [data-route-row-live] {
40539
40606
  flex-direction: column;
40540
40607
  grid-row: 1;
@@ -40567,6 +40634,7 @@ const SET_OFF_PULL = 16;
40567
40634
  * routes?: Array<object|{route: object, params?: object}>,
40568
40635
  * travelByDrag?: boolean,
40569
40636
  * onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
40637
+ * renderNeighbour?: (page: {route: object, params: object|undefined}) => import("preact").ComponentChildren,
40570
40638
  * }>}
40571
40639
  * @param {Array<object|{route: object, params?: object}>} [props.routes] - a window
40572
40640
  * over something longer, in order. Read from the <Route> children by default,
@@ -40583,6 +40651,13 @@ const SET_OFF_PULL = 16;
40583
40651
  * of the page under six entries. A tab pressed goes the way its <Link> says —
40584
40652
  * `<Link replace>` for a row of tabs, so a press and a swipe towards the same
40585
40653
  * neighbour write the same history.
40654
+ * @param {(page: {route: object, params: object|undefined}) => import("preact").ComponentChildren} [props.renderNeighbour]
40655
+ * - what stands in the pane of a page next to the one on screen, until the hand
40656
+ * has pulled far enough to bring that page: its background, its skeleton, what the
40657
+ * list already knows. A `<Route>` page already built shows itself. Receives
40658
+ * `{ route, params }`, or the entry of `routes` as it was given, and is a column
40659
+ * the height of the row, as the page is. Its route does not match while it stands
40660
+ * there: what it reads is not asked for.
40586
40661
  *
40587
40662
  * Every other prop lands on the box itself (`id`, `data-testid`, `className`, …).
40588
40663
  * The box has to outlive every page of its row: around those <Route>s, or around
@@ -40598,6 +40673,7 @@ const RouteTravel = ({
40598
40673
  replace: true,
40599
40674
  routeTransition: "none"
40600
40675
  }),
40676
+ renderNeighbour,
40601
40677
  className,
40602
40678
  children,
40603
40679
  ...rest
@@ -40658,9 +40734,7 @@ const RouteTravel = ({
40658
40734
  travelling: false,
40659
40735
  pictures: new Map(),
40660
40736
  resumeScrollRecording: null,
40661
- scrollsDocument: getScrollContainer(row.parentElement, {
40662
- includeHidden: true
40663
- }) === document.documentElement,
40737
+ scrollsDocument: rowFollowsDocument(row),
40664
40738
  ...fields
40665
40739
  };
40666
40740
  movementRef.current = movement;
@@ -40755,6 +40829,37 @@ const RouteTravel = ({
40755
40829
  });
40756
40830
  };
40757
40831
 
40832
+ // A tab pressed over a row a hand has left moving, which the browser plays on and
40833
+ // overwrites a travel with once that travel is over: the snap is ended first. Given
40834
+ // back, the row stands where it stood. A frame later, because until then WebKit still
40835
+ // starts from where a box that is no scroller stood: a smooth scroll to the first page
40836
+ // sees nothing to do.
40837
+ const takeRowFromHand = movement => {
40838
+ const row = rowRef.current;
40839
+ const position = row.scrollLeft;
40840
+ movement.rowTaken = true;
40841
+ // A travel no hand set off holds nothing: it goes where it goes.
40842
+ releaseHeldPage();
40843
+ row.style.setProperty("--navi-route-row-stand", `${-position}px`);
40844
+ row.setAttribute("data-route-row-taken", "clip");
40845
+ afterFrames(2, () => {
40846
+ if (movementRef.current !== movement) {
40847
+ return;
40848
+ }
40849
+ row.setAttribute("data-route-row-taken", "");
40850
+ row.style.removeProperty("--navi-route-row-stand");
40851
+ afterFrames(1, () => {
40852
+ const pane = panesOf(row)[latestRef.current.currentIndex];
40853
+ if (movementRef.current === movement && pane) {
40854
+ row.scrollTo({
40855
+ left: pane.offsetLeft,
40856
+ behavior: "smooth"
40857
+ });
40858
+ }
40859
+ });
40860
+ });
40861
+ };
40862
+
40758
40863
  // The row has stopped, on the page of the address: everything held for the
40759
40864
  // movement is let go of in one go, before anything paints.
40760
40865
  const endMovement = () => {
@@ -40765,6 +40870,7 @@ const RouteTravel = ({
40765
40870
  }
40766
40871
  movementRef.current = null;
40767
40872
  row.removeAttribute("data-moving");
40873
+ row.removeAttribute("data-route-row-taken");
40768
40874
  publishRouteRowMove(null);
40769
40875
  releaseHeldPage();
40770
40876
  if (liveRef.current) {
@@ -40832,16 +40938,23 @@ const RouteTravel = ({
40832
40938
  }
40833
40939
  return;
40834
40940
  }
40835
- beginMovement({
40941
+ const movementBefore = movementRef.current;
40942
+ const movement = beginMovement({
40836
40943
  travelling: true
40837
40944
  });
40838
- row.scrollTo({
40839
- left: pane.offsetLeft,
40840
- behavior: "smooth"
40841
- });
40842
40945
  // In this very commit: the tab pressed already wears its bar, and the first
40843
40946
  // scroll event is a frame away.
40844
40947
  sayRowMoves();
40948
+ if (movementBefore?.byHand && !movementBefore.rowTaken) {
40949
+ takeRowFromHand(movement);
40950
+ return;
40951
+ }
40952
+ if (row.getAttribute("data-route-row-taken") !== "clip") {
40953
+ row.scrollTo({
40954
+ left: pane.offsetLeft,
40955
+ behavior: "smooth"
40956
+ });
40957
+ }
40845
40958
  });
40846
40959
 
40847
40960
  // Somebody else is about to move the address to another page of a window: its
@@ -40873,14 +40986,17 @@ const RouteTravel = ({
40873
40986
  }, [windowed]);
40874
40987
  useLayoutEffect(() => {
40875
40988
  const row = rowRef.current;
40876
- const onScroll = () => {
40989
+ const isClipped = () => row.getAttribute("data-route-row-taken") === "clip";
40990
+ const onScroll = ({
40991
+ finger
40992
+ }) => {
40877
40993
  const {
40878
40994
  pages,
40879
40995
  currentIndex,
40880
40996
  travelByDrag
40881
40997
  } = latestRef.current;
40882
40998
  const panes = panesOf(row);
40883
- if (panes.length < 2) {
40999
+ if (panes.length < 2 || isClipped()) {
40884
41000
  return;
40885
41001
  }
40886
41002
  const moving = movementRef.current;
@@ -40889,11 +41005,24 @@ const RouteTravel = ({
40889
41005
  // Written by this box, at rest: nobody is moving anything.
40890
41006
  return;
40891
41007
  }
40892
- const movement = moving || beginMovement({});
41008
+ const movement = moving || beginMovement({
41009
+ byHand: true
41010
+ });
40893
41011
  sayRowMoves();
40894
41012
  if (movement.travelling || !travelByDrag || !pane) {
40895
41013
  return;
40896
41014
  }
41015
+ // Nothing will say when a trackpad's fingers leave, and a long stroke would keep
41016
+ // the first tab lit for the whole trip: the nearest page answers. A finger has a
41017
+ // lift to wait for (onLeftToSnap), and until then the page held stands.
41018
+ if (!finger) {
41019
+ const {
41020
+ base,
41021
+ toward,
41022
+ fraction
41023
+ } = readRowStretch(row, panes, panes[0]);
41024
+ followPageOnScreen(pages[panes.indexOf(fraction >= 0.5 && toward ? toward : base)], askedPageRef.current || pages[currentIndex]);
41025
+ }
40897
41026
  const pulled = row.scrollLeft - pane.offsetLeft;
40898
41027
  if (Math.abs(pulled) < SET_OFF_PULL) {
40899
41028
  return;
@@ -40946,7 +41075,7 @@ const RouteTravel = ({
40946
41075
  };
40947
41076
  const onLand = () => {
40948
41077
  const movement = movementRef.current;
40949
- if (!movement) {
41078
+ if (!movement || isClipped()) {
40950
41079
  return;
40951
41080
  }
40952
41081
  const panes = panesOf(row);
@@ -40975,8 +41104,48 @@ const RouteTravel = ({
40975
41104
  onLand,
40976
41105
  onLeftToSnap
40977
41106
  });
41107
+ // A hand back on a row taken from one: it is the hand's again, snapping included.
41108
+ const onHandBack = () => {
41109
+ if (row.getAttribute("data-route-row-taken") === "") {
41110
+ row.removeAttribute("data-route-row-taken");
41111
+ }
41112
+ };
41113
+ // A press for the page the address is already on changes nothing the router can
41114
+ // tell, and a stroke no lift ends leaves the address where it was: it is heard here,
41115
+ // the way a tab says which page it is through its address.
41116
+ const onPress = clickEvent => {
41117
+ const movement = movementRef.current;
41118
+ if (!movement?.byHand || movement.rowTaken) {
41119
+ return;
41120
+ }
41121
+ const link = clickEvent.target.closest?.("a[href]");
41122
+ const {
41123
+ pages,
41124
+ currentIndex
41125
+ } = latestRef.current;
41126
+ const page = pages[currentIndex];
41127
+ if (!link || !page || new URL(link.href).pathname !== pathOf(page)) {
41128
+ return;
41129
+ }
41130
+ movement.travelling = true;
41131
+ takeRowFromHand(movement);
41132
+ };
41133
+ row.addEventListener("touchstart", onHandBack, {
41134
+ passive: true
41135
+ });
41136
+ row.addEventListener("wheel", onHandBack, {
41137
+ passive: true
41138
+ });
41139
+ document.addEventListener("click", onPress, {
41140
+ capture: true
41141
+ });
40978
41142
  return () => {
40979
41143
  stopWatching();
41144
+ row.removeEventListener("touchstart", onHandBack);
41145
+ row.removeEventListener("wheel", onHandBack);
41146
+ document.removeEventListener("click", onPress, {
41147
+ capture: true
41148
+ });
40980
41149
  movementRef.current?.resumeScrollRecording?.();
40981
41150
  movementRef.current = null;
40982
41151
  publishRouteRowMove(null);
@@ -40996,6 +41165,30 @@ const RouteTravel = ({
40996
41165
  Promise.resolve().then(endMovement);
40997
41166
  }
40998
41167
  }, [currentIndex]);
41168
+
41169
+ // A neighbour shows from the first frame the compositor moves the row, before
41170
+ // any scroll event: a finger landing is the last word before that.
41171
+ const paintsNeighbours = Boolean(renderNeighbour);
41172
+ useLayoutEffect(() => {
41173
+ if (!paintsNeighbours) {
41174
+ return undefined;
41175
+ }
41176
+ const row = rowRef.current;
41177
+ const onTouchStart = () => {
41178
+ if (!movementRef.current && rowFollowsDocument(row)) {
41179
+ row.style.setProperty("--navi-route-row-pin", `${pinOf(row)}px`);
41180
+ }
41181
+ };
41182
+ row.addEventListener("touchstart", onTouchStart, {
41183
+ passive: true
41184
+ });
41185
+ return () => {
41186
+ row.removeEventListener("touchstart", onTouchStart);
41187
+ };
41188
+ }, [paintsNeighbours]);
41189
+ const rowContext = useMemo(() => ({
41190
+ renderNeighbour
41191
+ }), [renderNeighbour]);
40999
41192
  const rowKind = pages.length < 2 && windowed ? "still" : travelByDrag ? "" : "code";
41000
41193
  return jsx("div", {
41001
41194
  ...rest,
@@ -41008,12 +41201,20 @@ const RouteTravel = ({
41008
41201
  ,
41009
41202
  "data-travel-by-drag": travelByDrag && rowKind !== "still" ? "x" : undefined,
41010
41203
  children: windowed ? jsxs(Fragment, {
41011
- children: [pages.map((page, index) => jsx("div", {
41012
- "data-route-row-pane": "",
41013
- style: {
41014
- gridColumn: index + 1
41015
- }
41016
- }, index)), jsx("div", {
41204
+ children: [pages.map((page, index) => {
41205
+ const holdsNeighbour = paintsNeighbours && isNeighbourPane(index, liveIndex, movementRef.current);
41206
+ return jsx("div", {
41207
+ "data-route-row-pane": "",
41208
+ inert: holdsNeighbour,
41209
+ style: {
41210
+ gridColumn: index + 1
41211
+ },
41212
+ children: holdsNeighbour && jsx(RouteRowNeighbour, {
41213
+ renderNeighbour: renderNeighbour,
41214
+ page: page
41215
+ })
41216
+ }, index);
41217
+ }), jsx("div", {
41017
41218
  ref: liveRef,
41018
41219
  "data-route-row-live": "",
41019
41220
  style: {
@@ -41025,13 +41226,25 @@ const RouteTravel = ({
41025
41226
  "data-route-row-pictures": ""
41026
41227
  })]
41027
41228
  }) : jsx(RouteRowContext.Provider, {
41028
- value: true,
41229
+ value: rowContext,
41029
41230
  children: children
41030
41231
  })
41031
41232
  })
41032
41233
  });
41033
41234
  };
41034
41235
 
41236
+ // The panes on each side of the live page. Not one holding a picture of the page
41237
+ // left there: a page with no background of its own would show it through.
41238
+ const isNeighbourPane = (index, liveIndex, movement) => {
41239
+ if (index !== liveIndex - 1 && index !== liveIndex + 1) {
41240
+ return false;
41241
+ }
41242
+ return !movement || !movement.pictures.has(index);
41243
+ };
41244
+ const rowFollowsDocument = row => getScrollContainer(row.parentElement, {
41245
+ includeHidden: true
41246
+ }) === document.documentElement;
41247
+
41035
41248
  // How far down the row the reader is looking, for a page brought in: a page on
41036
41249
  // screen scrolls with the document, and one arriving has to start from there.
41037
41250
  const pinOf = row => {
@@ -41060,6 +41273,15 @@ const copyScrollPositions = (from, to) => {
41060
41273
  // A page of the row: a route, and the params that say which of its tabs when
41061
41274
  // several of them share it. Written as a bare route by a caller whose tabs are
41062
41275
  // routes of their own — which is the same page with nothing to tell apart.
41276
+ const afterFrames = (count, callback) => {
41277
+ if (count === 0) {
41278
+ callback();
41279
+ return;
41280
+ }
41281
+ requestAnimationFrame(() => {
41282
+ afterFrames(count - 1, callback);
41283
+ });
41284
+ };
41063
41285
  const normalizePage = page => page.isRoute ? {
41064
41286
  route: page,
41065
41287
  params: undefined
@@ -56513,6 +56735,10 @@ const SLIDE_TOWARD_ATTRIBUTE = "data-slide-travel-toward";
56513
56735
  // one: a hand on a row the browser scrolls can carry it past several slides, and
56514
56736
  // what follows it goes on between the two it is standing between.
56515
56737
  const SLIDE_BASE_ATTRIBUTE = "data-slide-travel-base";
56738
+ // The slide the row is on its way to while the current one has not changed yet,
56739
+ // because that only happens at rest: nothing is rendered and no address is written
56740
+ // under a snap, and what says where one is should not wait for it.
56741
+ const SLIDE_HEADING_ATTRIBUTE = "data-slide-heading";
56516
56742
  // Every way out that would DO something right now, in the words the commands
56517
56743
  // use: a direction ("left", "right", "up", "down") when there is a slide that
56518
56744
  // way, plus "first" and "last" when one is not already standing at that end of
@@ -56547,9 +56773,17 @@ const wordsOf = (element, attribute) => {
56547
56773
  return value ? value.split(" ") : NO_WAYS;
56548
56774
  };
56549
56775
 
56776
+ // The slide one is on, for whatever tells the reader where they are or what a press
56777
+ // would do: the current one while a hand holds the row, where the row is going once
56778
+ // it has let go. data-slide-current only changes at rest.
56779
+ const readSlideOnScreen = (element) =>
56780
+ element.getAttribute(SLIDE_HEADING_ATTRIBUTE) ??
56781
+ element.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ??
56782
+ undefined;
56783
+
56550
56784
  const readSlideContainerState = (element) => ({
56551
56785
  known: true,
56552
- current: element.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ?? undefined,
56786
+ current: readSlideOnScreen(element),
56553
56787
  toward: element.getAttribute(SLIDE_TOWARD_ATTRIBUTE) ?? undefined,
56554
56788
  // In DOM order — which is the order of the WALK for a line, and not
56555
56789
  // necessarily for a map: there the order is the one the areas are written in
@@ -56587,11 +56821,13 @@ const sameSlideContainerState = (a, b) =>
56587
56821
  * can: (wayOut: "left"|"right"|"up"|"down"|"first"|"last") => boolean,
56588
56822
  * held: (wayOut: "left"|"right"|"up"|"down"|"first"|"last") => boolean,
56589
56823
  * }} where the box stands. `current` is the slide on screen — the one being
56590
- * travelled TO while a travel plays, because that is what one is looking at;
56824
+ * travelled TO while a travel plays, and the one the row is going to once a hand
56825
+ * has let go, because that is what one is looking at. It can come before the
56826
+ * slide's own signal and `onCurrentChange`, which wait for the row to rest.
56591
56827
  * `toward` is the other slide in the frame while the picture is between two,
56592
- * and nothing at rest. `can` is "asking for it would do something", `held` is
56593
- * "it is there and this slide says no" — a way out is dead in both cases, and
56594
- * only the second is worth a word to the reader.
56828
+ * and nothing at rest. `can` is "asking for it would do something", from that
56829
+ * same slide, `held` is "it is there and this slide says no" — a way out is dead
56830
+ * in both cases, and only the second is worth a word to the reader.
56595
56831
  * `known` is false until the box has been read: no box was named and none is
56596
56832
  * above, the id names nothing, or — for one commit — this mounted before the
56597
56833
  * box had painted. Until then `can` answers YES, because it is the answer that
@@ -57445,7 +57681,7 @@ const Nav = ({
57445
57681
  }
57446
57682
  slideContainerElementRef.current = containerElement;
57447
57683
  const readContainer = () => {
57448
- setCurrentSlideArea(containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ?? undefined);
57684
+ setCurrentSlideArea(readSlideOnScreen(containerElement));
57449
57685
  paintIndicatorGeometryRef.current();
57450
57686
  scheduleTabScrollSyncRef.current();
57451
57687
  };
@@ -67563,6 +67799,10 @@ const css$I = /* css */`@property --slide-travel-progress {
67563
67799
  overflow-y: auto;
67564
67800
  }
67565
67801
 
67802
+ &[data-slide-row-taken] {
67803
+ overflow: clip;
67804
+ }
67805
+
67566
67806
  &:focus {
67567
67807
  outline: none;
67568
67808
  }
@@ -67614,6 +67854,77 @@ const css$I = /* css */`@property --slide-travel-progress {
67614
67854
  }
67615
67855
  `;
67616
67856
 
67857
+ // What a row left moving shows when a travel is asked for over it, read before the
67858
+ // slides are written: the one or two slides in the frame, and how far the track has
67859
+ // to stand, in a box that is no scroller, for that frame not to move.
67860
+ const readRowStand = (containerEl, slideElements) => {
67861
+ const restElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
67862
+ if (!restElement) {
67863
+ return null;
67864
+ }
67865
+ const {
67866
+ base,
67867
+ toward,
67868
+ fraction
67869
+ } = readRowStretch(containerEl, slideElements, restElement);
67870
+ return {
67871
+ base: readArea(base),
67872
+ toward: toward && fraction > 0 ? readArea(toward) : null,
67873
+ fraction,
67874
+ offset: `${-containerEl.scrollLeft}px ${-containerEl.scrollTop}px`
67875
+ };
67876
+ };
67877
+ // The stage of a travel asked for over a row left moving. What is in the frame stays
67878
+ // where it is, and the slide asked for comes one box past it, unless it is already
67879
+ // there: then what is left to make is a part of a box.
67880
+ const stageOverRow = (rowStand, area, realPlaceOf, stepAsked) => {
67881
+ const {
67882
+ base,
67883
+ toward,
67884
+ fraction
67885
+ } = rowStand;
67886
+ const basePlace = realPlaceOf(base);
67887
+ const placeByArea = new Map([[base, basePlace]]);
67888
+ const towardPlace = toward ? realPlaceOf(toward) : null;
67889
+ if (toward) {
67890
+ placeByArea.set(toward, towardPlace);
67891
+ }
67892
+ if (area === base || area === toward) {
67893
+ const otherArea = area === base ? toward : base;
67894
+ if (!otherArea) {
67895
+ return {
67896
+ placeByArea,
67897
+ leaningArea: null,
67898
+ progress: 0
67899
+ };
67900
+ }
67901
+ const from = realPlaceOf(otherArea);
67902
+ const to = realPlaceOf(area);
67903
+ const sign = Math.sign(to.x - from.x) || Math.sign(to.y - from.y);
67904
+ return {
67905
+ placeByArea,
67906
+ leaningArea: otherArea,
67907
+ progress: sign * (area === base ? fraction : 1 - fraction)
67908
+ };
67909
+ }
67910
+ const to = realPlaceOf(area);
67911
+ const step = stepAsked || {
67912
+ x: Math.sign(to.x - basePlace.x),
67913
+ y: Math.sign(to.y - basePlace.y)
67914
+ };
67915
+ const pastToward = towardPlace && (towardPlace.x - basePlace.x) * step.x + (towardPlace.y - basePlace.y) * step.y > 0;
67916
+ const side = pastToward ? towardPlace : basePlace;
67917
+ placeByArea.set(area, {
67918
+ x: side.x + step.x,
67919
+ y: side.y + step.y
67920
+ });
67921
+ return {
67922
+ placeByArea,
67923
+ leaningArea: toward && fraction >= 0.5 ? toward : base,
67924
+ progress: step.x || step.y
67925
+ };
67926
+ };
67927
+
67617
67928
  // How much of a travel is left to make, as a fraction of the travel that was
67618
67929
  // asked for: 1 for a move starting from rest, less for one picked up while the
67619
67930
  // track was already moving. Everything is measured in px against the track's
@@ -67630,6 +67941,19 @@ const ratioOfOneTravel = (track, from, to, targetBefore) => {
67630
67941
  const ratio = left / asked;
67631
67942
  return ratio > 1 ? 1 : ratio;
67632
67943
  };
67944
+ // The same for a travel taken over a row, which nobody asked for from anywhere in
67945
+ // particular: what is left, in boxes. Laid out sizes, like the scroll position that
67946
+ // `from` is read off: a row inside a popup still growing is not its transformed size.
67947
+ const ratioOfOneBox = (track, from, to) => {
67948
+ const box = {
67949
+ width: track.offsetWidth,
67950
+ height: track.offsetHeight
67951
+ };
67952
+ const start = offsetToPx(from, box);
67953
+ const end = offsetToPx(to, box);
67954
+ const ratio = Math.abs(end.x - start.x) / box.width + Math.abs(end.y - start.y) / box.height;
67955
+ return ratio > 1 ? 1 : ratio;
67956
+ };
67633
67957
 
67634
67958
  // A translate ("-100% 0%", "-260px 0px", "none") as two numbers of pixels.
67635
67959
  // Percentages are the size of the box, which is what a translate resolves them
@@ -68207,6 +68531,10 @@ const SlideContainer = ({
68207
68531
  // The slide a hand has just landed on, for the render it asks for: it is
68208
68532
  // already there, so there is nothing to play.
68209
68533
  const handLandedRef = useRef(null);
68534
+ // The slide a row is being carried away from, pressed: it stays current, so only
68535
+ // a render says the row has to come back to it (see goToArea).
68536
+ const rowBackAskedRef = useRef(false);
68537
+ const [, askRowBack] = useState(0);
68210
68538
  // Where the box rests on the row, and what the row starts from: a slide that
68211
68539
  // holds on to the user ends the row on that side, and a scroll range starts at
68212
68540
  // its first reachable slide (see writeRowAtRest).
@@ -68444,6 +68772,26 @@ const SlideContainer = ({
68444
68772
  const paintCurrentArea = area => {
68445
68773
  containerRef.current?.setAttribute(SLIDE_CURRENT_ATTRIBUTE, area);
68446
68774
  };
68775
+
68776
+ // Where the row is going, for as long as the current slide has not changed:
68777
+ // anything saying where one is reads this first, and so does what a press would
68778
+ // do from there (see readSlideOnScreen). Left to the caller to announce, a landing
68779
+ // having more to say in the same breath.
68780
+ const writeHeading = headingElement => {
68781
+ const containerEl = containerRef.current;
68782
+ const area = headingElement ? readArea(headingElement) : null;
68783
+ // A window does not change slide, it rolls: it has nowhere to be heading.
68784
+ const heading = loop || area === null || area === containerEl.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ? null : area;
68785
+ if (heading === containerEl.getAttribute(SLIDE_HEADING_ATTRIBUTE)) {
68786
+ return;
68787
+ }
68788
+ if (heading === null) {
68789
+ containerEl.removeAttribute(SLIDE_HEADING_ATTRIBUTE);
68790
+ } else {
68791
+ containerEl.setAttribute(SLIDE_HEADING_ATTRIBUTE, heading);
68792
+ }
68793
+ paintWays();
68794
+ };
68447
68795
  const markAnswered = area => {
68448
68796
  const order = readMap().slideElements.map(readArea);
68449
68797
  const rank = order.indexOf(area);
@@ -68609,6 +68957,7 @@ const SlideContainer = ({
68609
68957
  offsetRef.current = "0% 0%";
68610
68958
  track.style.setProperty("--slide-container-offset", "0% 0%");
68611
68959
  containerRef.current.removeAttribute("data-slide-travelling");
68960
+ containerRef.current.removeAttribute("data-slide-row-taken");
68612
68961
  sendRowToRest(currentElement);
68613
68962
  renderNear(currentArea, {
68614
68963
  alongside: true
@@ -68649,6 +68998,10 @@ const SlideContainer = ({
68649
68998
  slideElements[0];
68650
68999
  const currentArea = readArea(currentElement);
68651
69000
  paintCurrentArea(currentArea);
69001
+ // In the commit the slide changes in, so nothing reads the slide left in between.
69002
+ if (!handMovingRef.current) {
69003
+ writeHeading(null);
69004
+ }
68652
69005
  const realPlaceOf = area => placeOf.get(area) || {
68653
69006
  x: 0,
68654
69007
  y: 0
@@ -68679,8 +69032,26 @@ const SlideContainer = ({
68679
69032
  // between the picture and the slide arriving, on the axis it walks.
68680
69033
  let travelStep = null;
68681
69034
  const drawnArea = stage ? stage.area : drawnAreaRef.current;
68682
- const travelStarts = !still && durationMs > 0 && drawnArea !== undefined && drawnArea !== currentArea && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
68683
- if (travelStarts) {
69035
+ const rowBackAsked = rowBackAskedRef.current;
69036
+ rowBackAskedRef.current = false;
69037
+ // Asked for over a row a hand has left moving: the row belongs to what was asked
69038
+ // for, and where the hand's snap would have ended decides nothing.
69039
+ const rowTaken = handMovingRef.current && !handLanded && (drawnArea !== currentArea || rowBackAsked);
69040
+ const rowStand = rowTaken ? readRowStand(containerRef.current, slideElements) : null;
69041
+ if (rowTaken) {
69042
+ handMovingRef.current = false;
69043
+ writeHeading(null);
69044
+ paintTravelProgress();
69045
+ }
69046
+ const travelStarts = !still && durationMs > 0 && drawnArea !== undefined && (drawnArea !== currentArea || rowTaken) && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
69047
+ let overRow = null;
69048
+ if (travelStarts && rowStand) {
69049
+ overRow = stageOverRow(rowStand, currentArea, realPlaceOf, travelStepRef.current);
69050
+ stage = stageRef.current = {
69051
+ placeByArea: overRow.placeByArea,
69052
+ area: currentArea
69053
+ };
69054
+ } else if (travelStarts) {
68684
69055
  // Where the slide being left IS — the place the stage gave it if a travel
68685
69056
  // was already playing (a press landing mid-travel departs from where the
68686
69057
  // eye is, not from the map), its own place otherwise.
@@ -68765,9 +69136,16 @@ const SlideContainer = ({
68765
69136
  }
68766
69137
  const track = trackRef.current;
68767
69138
  containerRef.current.toggleAttribute("data-slide-travelling", Boolean(stage));
68768
- // How far the track stands from where the scroll position rests: none at rest.
69139
+ if (rowTaken) {
69140
+ containerRef.current.setAttribute("data-slide-row-taken", "");
69141
+ }
69142
+ // How far the track stands from where the scroll position rests: none at rest. A
69143
+ // box that is no scroller has no position, and the track says it all.
68769
69144
  const rowPlace = onRow(currentPlace);
68770
- const restPlace = restPlaceRef.current;
69145
+ const restPlace = containerRef.current.hasAttribute("data-slide-row-taken") ? {
69146
+ x: 0,
69147
+ y: 0
69148
+ } : restPlaceRef.current;
68771
69149
  const offset = `${(restPlace.x - rowPlace.x) * 100}% ${(restPlace.y - rowPlace.y) * 100}%`;
68772
69150
  // The travel that was ASKED for is still one box, whatever is left of it to
68773
69151
  // cover: what is left takes what is left of the duration.
@@ -68790,6 +69168,9 @@ const SlideContainer = ({
68790
69168
  const onScreenPx = trackOffsetPx(track, containerRef.current);
68791
69169
  offsetOnScreen = `${onScreenPx.x}px ${onScreenPx.y}px`;
68792
69170
  }
69171
+ if (overRow) {
69172
+ offsetOnScreen = rowStand.offset;
69173
+ }
68793
69174
  const offsetBefore = offsetOnScreen ?? offsetRef.current;
68794
69175
  offsetRef.current = offset;
68795
69176
  track.style.setProperty("--slide-container-offset", offset);
@@ -68808,10 +69189,10 @@ const SlideContainer = ({
68808
69189
  // short distance for a full duration. One box in `duration` is the pace;
68809
69190
  // this only ever shortens it (a longer travel is not made slower, which
68810
69191
  // would make a two-box move drag).
68811
- const travelRatio = ratioOfOneTravel(track, offsetBefore, offset, offsetTargetBefore);
69192
+ const travelRatio = overRow ? ratioOfOneBox(track, offsetBefore, offset) : ratioOfOneTravel(track, offsetBefore, offset, offsetTargetBefore);
68812
69193
  // Already moving, so there is no ease-in to play: it would stall the track
68813
69194
  // for an instant right where the eye is following it.
68814
- const easing = travelInFlight ? "ease-out" : "ease";
69195
+ const easing = travelInFlight || overRow ? "ease-out" : "ease";
68815
69196
  // Cancelled rather than layered: two animations on the same property
68816
69197
  // would blend, and what one sees then is neither of the two moves.
68817
69198
  trackAnimationRef.current?.cancel();
@@ -68819,15 +69200,21 @@ const SlideContainer = ({
68819
69200
  translate: offsetBefore
68820
69201
  }, {
68821
69202
  translate: offset
68822
- }], {
69203
+ }],
69204
+ // Backwards: the first frame's clock can read a fraction of a millisecond
69205
+ // before the start time Chrome gives the animation, which would show the
69206
+ // travel already over.
69207
+ {
68823
69208
  duration: durationMs * travelRatio,
68824
- easing
69209
+ easing,
69210
+ fill: "backwards"
68825
69211
  });
68826
69212
  // The trait travels with the slides, from a whole box away.
68827
- const progressFrom = travelStep ? travelStep.x || travelStep.y : 0;
69213
+ const stepProgress = travelStep ? travelStep.x || travelStep.y : 0;
69214
+ const progressFrom = overRow ? overRow.progress : stepProgress;
68828
69215
  // The slide the picture leans on for the length of it is the one being
68829
69216
  // LEFT: it is the second one in the frame until the travel is over.
68830
- animateTravelProgress(progressFrom, durationMs * travelRatio, easing, drawnArea);
69217
+ animateTravelProgress(progressFrom, durationMs * travelRatio, easing, overRow ? overRow.leaningArea : drawnArea);
68831
69218
  // Presses still waiting behind this one: it is already late, so it is
68832
69219
  // sent home at once rather than played out at the pace of someone who
68833
69220
  // has stopped pressing. Someone pressing → four times is asking to be
@@ -68848,6 +69235,17 @@ const SlideContainer = ({
68848
69235
  // thing it was standing for is over.
68849
69236
  settleTravel();
68850
69237
  }
69238
+ // The row taken with nothing playing to give it back (no duration, a page change
69239
+ // carrying the slide): no scroller for as long as the compositor takes to hear it.
69240
+ if (!stageRef.current && containerRef.current.hasAttribute("data-slide-row-taken")) {
69241
+ requestAnimationFrame(() => {
69242
+ requestAnimationFrame(() => {
69243
+ if (trackAnimationRef.current?.playState !== "running") {
69244
+ settleTravel();
69245
+ }
69246
+ });
69247
+ });
69248
+ }
68851
69249
  // A window waiting for its travel to be over (see goToArea's own loop
68852
69250
  // branch): the animation says when, and says it about the move that just
68853
69251
  // started rather than about a duration counted out beside it.
@@ -68911,9 +69309,9 @@ const SlideContainer = ({
68911
69309
  sendRowToRest(currentElement);
68912
69310
  }
68913
69311
  // Said last, and after data-current has been written: what a travel would do
68914
- // is read off the map from the slide that is now current, and off the locks
68915
- // that slide is wearing in this very commit.
68916
- paintWays(currentElement);
69312
+ // is read off the map from the slide on screen, and off the locks that slide is
69313
+ // wearing in this very commit.
69314
+ paintWays();
68917
69315
  // …and once everything this commit had to write is written, the box says so
68918
69316
  // — a single announcement for a render that may have changed several of the
68919
69317
  // facts at once.
@@ -68960,14 +69358,18 @@ const SlideContainer = ({
68960
69358
  placeOf
68961
69359
  } = readMap();
68962
69360
  const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
68963
- if (!area || area === readArea(currentElement)) {
69361
+ if (!area) {
68964
69362
  return false;
68965
69363
  }
69364
+ // What a press leaves is the slide on screen, which is what paintWays offers from.
69365
+ // A landing is the row arriving there, and what it leaves is the current one.
69366
+ const areaLeft = landed ? readArea(currentElement) : readSlideOnScreen(containerRef.current) ?? readArea(currentElement);
69367
+ const elementLeft = slideElements.find(slideElement => readArea(slideElement) === areaLeft) || currentElement;
68966
69368
  if (forward === undefined) {
68967
69369
  // Asked for by name (--navi-go-to-slide, --navi-back) rather than by a
68968
69370
  // direction: which way it is is still a fact, read off the map, and it is
68969
69371
  // what says which of the two locks below applies.
68970
- const from = placeOf.get(readArea(currentElement)) || {
69372
+ const from = placeOf.get(areaLeft) || {
68971
69373
  x: 0,
68972
69374
  y: 0
68973
69375
  };
@@ -68985,9 +69387,18 @@ const SlideContainer = ({
68985
69387
  // one departure the forward hold does not apply to. Said here rather than
68986
69388
  // by dropping the attribute, which stays the render's to write — a hold
68987
69389
  // meant to stay (an explicit preventNavNext) is still there on the way back.
68988
- if (forward ? !released && currentElement?.hasAttribute("data-prevent-nav-next") : currentElement?.hasAttribute("data-prevent-nav-previous")) {
69390
+ if (area !== areaLeft && (forward ? !released && elementLeft.hasAttribute("data-prevent-nav-next") : elementLeft.hasAttribute("data-prevent-nav-previous"))) {
68989
69391
  return false;
68990
69392
  }
69393
+ if (area === readArea(currentElement)) {
69394
+ // Nowhere to go, unless a hand has left the row on its way off this slide.
69395
+ if (!handMovingRef.current) {
69396
+ return false;
69397
+ }
69398
+ rowBackAskedRef.current = true;
69399
+ askRowBack(count => count + 1);
69400
+ return true;
69401
+ }
68991
69402
  // The focus moves here, while the event that asked for it is still in hand:
68992
69403
  // who pressed what, and whether they were on the keyboard, is knowable now
68993
69404
  // and gone a render later. The slide arriving is made reachable on the spot
@@ -69369,7 +69780,7 @@ const SlideContainer = ({
69369
69780
  const move = (dx, dy, event, {
69370
69781
  value,
69371
69782
  released
69372
- } = {}) => goToArea(areaTowards(dx, dy), {
69783
+ } = {}) => goToArea(areaTowards(dx, dy, readSlideOnScreen(containerRef.current)), {
69373
69784
  forward: dx > 0 || dy > 0,
69374
69785
  event,
69375
69786
  value,
@@ -69447,11 +69858,13 @@ const SlideContainer = ({
69447
69858
  // Where a hand has the row, in the terms of --slide-travel-progress: from the
69448
69859
  // current slide while it stands next to it, and past that between the two it
69449
69860
  // stands between, as far as the hand carries it.
69450
- const paintNativeRow = () => {
69451
- if (trackAnimationRef.current?.playState === "running") {
69861
+ const paintNativeRow = ({
69862
+ finger
69863
+ }) => {
69864
+ const containerEl = containerRef.current;
69865
+ if (trackAnimationRef.current?.playState === "running" || containerEl.hasAttribute("data-slide-row-taken")) {
69452
69866
  return;
69453
69867
  }
69454
- const containerEl = containerRef.current;
69455
69868
  const slideElements = Array.from(trackRef.current.children);
69456
69869
  const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
69457
69870
  if (!currentElement) {
@@ -69459,6 +69872,9 @@ const SlideContainer = ({
69459
69872
  }
69460
69873
  if (restsOn(containerEl, currentElement)) {
69461
69874
  if (handMovingRef.current) {
69875
+ if (!finger) {
69876
+ writeHeading(null);
69877
+ }
69462
69878
  paintTravelProgress();
69463
69879
  }
69464
69880
  return;
@@ -69479,6 +69895,12 @@ const SlideContainer = ({
69479
69895
  }
69480
69896
  element.style.setProperty("--slide-travel-progress", forward ? -fraction : fraction);
69481
69897
  }
69898
+ // Nothing will say when a trackpad's fingers leave, and a long stroke would
69899
+ // keep the first tab lit for the whole trip: the nearest slide answers. A finger
69900
+ // has a lift to wait for (headNativeRow), and until then the slide held stands.
69901
+ if (!finger) {
69902
+ writeHeading(fraction >= 0.5 && toward ? toward : base);
69903
+ }
69482
69904
  paintTravelToward(toward ? readArea(toward) : null);
69483
69905
  // What the hand reaches ahead of the neighbours built at rest is asked for on
69484
69906
  // the way: an empty slide goes by otherwise.
@@ -69486,6 +69908,28 @@ const SlideContainer = ({
69486
69908
  alongside: true
69487
69909
  });
69488
69910
  };
69911
+
69912
+ // A finger has lifted and the row moves: on past the slide it stands after, or
69913
+ // back to it. Already standing on one, it has arrived, whichever way it came.
69914
+ const headNativeRow = movedForward => {
69915
+ if (!handMovingRef.current || trackAnimationRef.current?.playState === "running") {
69916
+ return;
69917
+ }
69918
+ const slideElements = Array.from(trackRef.current.children);
69919
+ const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
69920
+ if (!currentElement) {
69921
+ return;
69922
+ }
69923
+ const {
69924
+ base,
69925
+ toward,
69926
+ fraction,
69927
+ forward
69928
+ } = readRowStretch(containerRef.current, slideElements, currentElement);
69929
+ const goesOn = fraction > 0 && movedForward === forward;
69930
+ writeHeading(goesOn && toward ? toward : base);
69931
+ announceState();
69932
+ };
69489
69933
  const landNativeRow = scrollEndEvent => {
69490
69934
  if (!handMovingRef.current) {
69491
69935
  return;
@@ -69500,13 +69944,15 @@ const SlideContainer = ({
69500
69944
  return;
69501
69945
  }
69502
69946
  handMovingRef.current = false;
69503
- paintTravelProgress();
69504
69947
  const area = readArea(landedElement);
69505
69948
  const currentArea = containerEl.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
69506
69949
  if (area === currentArea) {
69950
+ writeHeading(null);
69951
+ paintTravelProgress();
69507
69952
  settleTravel();
69508
69953
  return;
69509
69954
  }
69955
+ paintTravelProgress();
69510
69956
  const event = scrollEndEvent ?? new Event("scrollend");
69511
69957
  if (loop) {
69512
69958
  // A window does not change slide, it rolls: back at rest in the render the
@@ -69537,6 +69983,8 @@ const SlideContainer = ({
69537
69983
  }
69538
69984
  // Refused: the row is put back in view of the slide that stays.
69539
69985
  handLandedRef.current = null;
69986
+ writeHeading(null);
69987
+ announceState();
69540
69988
  const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
69541
69989
  containerEl.scrollTo({
69542
69990
  left: currentElement.offsetLeft,
@@ -69550,11 +69998,13 @@ const SlideContainer = ({
69550
69998
  const nativeRowHandlersRef = useRef(null);
69551
69999
  nativeRowHandlersRef.current = {
69552
70000
  paintNativeRow,
70001
+ headNativeRow,
69553
70002
  landNativeRow
69554
70003
  };
69555
70004
  useLayoutEffect(() => {
69556
70005
  return watchRowScroll(containerRef.current, {
69557
- onScroll: () => nativeRowHandlersRef.current.paintNativeRow(),
70006
+ onScroll: move => nativeRowHandlersRef.current.paintNativeRow(move),
70007
+ onLeftToSnap: forward => nativeRowHandlersRef.current.headNativeRow(forward),
69558
70008
  onLand: event => nativeRowHandlersRef.current.landNativeRow(event)
69559
70009
  });
69560
70010
  }, []);
@@ -69632,13 +70082,19 @@ const SlideContainer = ({
69632
70082
  // ([data-slide-ways~="right"]), or through useSlideContainer, which watches
69633
70083
  // them. Nothing is told, everything is read: it is what lets them sit
69634
70084
  // anywhere, and what lets a container nobody drives still be followed.
69635
- const paintWays = currentElement => {
70085
+ const paintWays = () => {
69636
70086
  const ways = [];
69637
70087
  const held = [];
70088
+ const {
70089
+ slideElements,
70090
+ order
70091
+ } = readMap();
70092
+ const areaOnScreen = readSlideOnScreen(containerRef.current);
70093
+ const elementOnScreen = slideElements.find(slideElement => readArea(slideElement) === areaOnScreen);
69638
70094
  // Whether the slide being left says no, this way. The one gate again, read
69639
70095
  // exactly as goToArea reads it at the moment of the travel — so what is
69640
70096
  // published here and what would happen cannot disagree.
69641
- const holdsTowards = forward => currentElement?.hasAttribute(forward ? "data-prevent-nav-next" : "data-prevent-nav-previous");
70097
+ const holdsTowards = forward => elementOnScreen?.hasAttribute(forward ? "data-prevent-nav-next" : "data-prevent-nav-previous");
69642
70098
  const say = (name, offered, forward) => {
69643
70099
  if (!offered) {
69644
70100
  return;
@@ -69655,18 +70111,14 @@ const SlideContainer = ({
69655
70111
  // would be saying it about every row on the page.
69656
70112
  continue;
69657
70113
  }
69658
- say(direction, Boolean(areaTowards(dx, dy)), dx > 0 || dy > 0);
70114
+ say(direction, Boolean(areaTowards(dx, dy, areaOnScreen)), dx > 0 || dy > 0);
69659
70115
  }
69660
70116
  // The two ends are ways out like the others — "all the way that way", said
69661
70117
  // by the map's own order rather than by a direction (see goToEnd, and
69662
70118
  // SlideContainer.First / .Last). Offered while one is not already standing
69663
70119
  // there, and held by the same lock the direction they lie in would be.
69664
- const {
69665
- order
69666
- } = readMap();
69667
- const currentArea = currentElement ? readArea(currentElement) : undefined;
69668
- say("first", order.length > 0 && order[0] !== currentArea, false);
69669
- say("last", order.length > 0 && order[order.length - 1] !== currentArea, true);
70120
+ say("first", order.length > 0 && order[0] !== areaOnScreen, false);
70121
+ say("last", order.length > 0 && order[order.length - 1] !== areaOnScreen, true);
69670
70122
  for (const element of travelPainters()) {
69671
70123
  if (ways.length) {
69672
70124
  element.setAttribute(SLIDE_WAYS_ATTRIBUTE, ways.join(" "));
@@ -69759,7 +70211,8 @@ const SlideContainer = ({
69759
70211
  "--slide-travel-progress": 0
69760
70212
  }], {
69761
70213
  duration: durationMs,
69762
- easing
70214
+ easing,
70215
+ fill: "backwards"
69763
70216
  }));
69764
70217
  progressAnimationsRef.current[0].finished.then(() => {
69765
70218
  progressAnimationsRef.current = [];
@@ -105704,7 +106157,9 @@ const StepList = ({
105704
106157
  const read = () => {
105705
106158
  const currentArea = containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
105706
106159
  const towardArea = containerElement.getAttribute(SLIDE_TOWARD_ATTRIBUTE);
105707
- setContainerCurrent(currentArea ?? undefined);
106160
+ // Which step is lit follows the screen. The halo counts from the current slide,
106161
+ // which is what the progress it draws with is measured against.
106162
+ setContainerCurrent(readSlideOnScreen(containerElement));
105708
106163
  const currentIdx = currentArea === null ? -1 : indexOf(currentArea);
105709
106164
  if (currentIdx === -1) {
105710
106165
  // A slide no step names (a confirmation screen): the halo is not