@jsenv/navi 0.30.30 → 0.30.32

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.
@@ -36896,6 +36896,28 @@ const pageIsCurrent = ({ route, params }) => {
36896
36896
  return params ? route.matchesParams(params) : true;
36897
36897
  };
36898
36898
 
36899
+ // The same question about an address the router does not have yet (a navigation
36900
+ // about to be routed, a link pressed): its path, and every param the page names at
36901
+ // the value the page gives it. The same path under another search param is another
36902
+ // page — a person opened from another list — and a param the page does not name is
36903
+ // state on top of it, as it is for matchesParams.
36904
+ const pageIsAt = ({ route, params }, url) => {
36905
+ const destination = new URL(url, window.location.href);
36906
+ const own = new URL(route.buildUrl(params), window.location.href);
36907
+ if (own.pathname !== destination.pathname) {
36908
+ return false;
36909
+ }
36910
+ if (!params) {
36911
+ return true;
36912
+ }
36913
+ for (const name of Object.keys(params)) {
36914
+ if (own.searchParams.get(name) !== destination.searchParams.get(name)) {
36915
+ return false;
36916
+ }
36917
+ }
36918
+ return true;
36919
+ };
36920
+
36899
36921
  /**
36900
36922
  * The page a <Route> container shows when none of its branches matched, given
36901
36923
  * a name.
@@ -39091,6 +39113,45 @@ const readBand = () => {
39091
39113
  };
39092
39114
  };
39093
39115
 
39116
+ /**
39117
+ * CSS durations and easing curves, for a movement played from JS that has to look
39118
+ * like one played by CSS: a row scrolled frame by frame next to a track animated by
39119
+ * the browser, a view transition walked back from where its curve has it.
39120
+ */
39121
+
39122
+ // A CSS duration read as a number of milliseconds.
39123
+ const durationToMs = (duration) => {
39124
+ const number = parseFloat(duration);
39125
+ if (Number.isNaN(number)) {
39126
+ return 0;
39127
+ }
39128
+ return String(duration).trimEnd().endsWith("ms") ? number : number * 1000;
39129
+ };
39130
+
39131
+ const CSS_EASE = [0.25, 0.1, 0.25, 1];
39132
+ const CSS_EASE_OUT = [0, 0, 0.58, 1];
39133
+
39134
+ // A cubic-bezier curve, evaluated: time in, distance out. Solved numerically
39135
+ // because the curve is parametric — two cubics sharing a parameter, with no closed
39136
+ // form for one against the other. Twenty halvings put the answer well under a pixel
39137
+ // of a screen-wide movement.
39138
+ const easedProgress = (x, [x1, y1, x2, y2]) => {
39139
+ let low = 0;
39140
+ let high = 1;
39141
+ for (let i = 0; i < 20; i++) {
39142
+ const mid = (low + high) / 2;
39143
+ if (bezierAxis(mid, x1, x2) < x) {
39144
+ low = mid;
39145
+ } else {
39146
+ high = mid;
39147
+ }
39148
+ }
39149
+ return bezierAxis((low + high) / 2, y1, y2);
39150
+ };
39151
+
39152
+ const bezierAxis = (s, a, b) =>
39153
+ 3 * (1 - s) * (1 - s) * s * a + 3 * (1 - s) * s * s * b + s * s * s;
39154
+
39094
39155
  /**
39095
39156
  * A view transition already playing, walked back to where it started.
39096
39157
  *
@@ -39120,6 +39181,7 @@ const readBand = () => {
39120
39181
  * there while the clock counts down.
39121
39182
  */
39122
39183
 
39184
+
39123
39185
  const viewTransitionAnimations = () => {
39124
39186
  const animations = [];
39125
39187
  for (const animation of document.getAnimations()) {
@@ -39244,27 +39306,6 @@ const whenPicturesArrived = (animations) => {
39244
39306
  );
39245
39307
  };
39246
39308
 
39247
- // CSS `ease`, evaluated: time in, distance out. Solved numerically because the
39248
- // curve is parametric — two cubics sharing a parameter, with no closed form for
39249
- // one against the other. Twenty halvings put the answer well under a pixel of a
39250
- // screen-wide movement.
39251
- const CSS_EASE = [0.25, 0.1, 0.25, 1];
39252
- const bezierAxis = (s, a, b) =>
39253
- 3 * (1 - s) * (1 - s) * s * a + 3 * (1 - s) * s * s * b + s * s * s;
39254
- const easedProgress = (x, [x1, y1, x2, y2]) => {
39255
- let low = 0;
39256
- let high = 1;
39257
- for (let i = 0; i < 20; i++) {
39258
- const mid = (low + high) / 2;
39259
- if (bezierAxis(mid, x1, x2) < x) {
39260
- low = mid;
39261
- } else {
39262
- high = mid;
39263
- }
39264
- }
39265
- return bezierAxis((low + high) / 2, y1, y2);
39266
- };
39267
-
39268
39309
  installImportMetaCssBuild(import.meta);
39269
39310
  const startViewTransition = ensureDocumentStartViewTransition();
39270
39311
  const TRANSITION_ATTRIBUTE = "data-navi-route-transition";
@@ -41294,11 +41335,11 @@ const css$16 = /* css */`.navi_route_travel {
41294
41335
  overflow: visible;
41295
41336
  }
41296
41337
 
41297
- &[data-route-row-taken] {
41338
+ &[data-route-row-travelling] {
41298
41339
  scroll-snap-type: none;
41299
41340
  }
41300
41341
 
41301
- &[data-route-row-taken="clip"] {
41342
+ &[data-route-row-taken] {
41302
41343
  overflow: clip;
41303
41344
 
41304
41345
  & > [data-route-row-pane], & > [data-route-row-live], & > [data-route-row-pictures] > * {
@@ -41317,12 +41358,9 @@ const css$16 = /* css */`.navi_route_travel {
41317
41358
  display: flex;
41318
41359
  }
41319
41360
 
41320
- &:not([data-moving]) > [data-route-row-pane][inert] {
41321
- visibility: hidden;
41322
- }
41323
-
41324
41361
  & > [data-route-row-pane][inert] {
41325
41362
  top: var(--navi-route-row-pin, 0);
41363
+ contain: paint;
41326
41364
  max-height: 100dvh;
41327
41365
  position: relative;
41328
41366
  overflow: clip;
@@ -41330,7 +41368,6 @@ const css$16 = /* css */`.navi_route_travel {
41330
41368
 
41331
41369
  & > [data-route-row-pane] > [data-route-row-neighbour] {
41332
41370
  contain: layout paint;
41333
- visibility: visible;
41334
41371
  flex-direction: column;
41335
41372
  display: flex;
41336
41373
  position: absolute;
@@ -41368,6 +41405,9 @@ const SET_OFF_PULL = 16;
41368
41405
  const ROW_READING = {
41369
41406
  sideways: true
41370
41407
  };
41408
+ // How far a scroll position written can read back from what was written: rounded to
41409
+ // the device's pixels.
41410
+ const WRITE_ROUNDING = 1;
41371
41411
 
41372
41412
  /**
41373
41413
  * @type {import("preact").FunctionComponent<{
@@ -41376,6 +41416,7 @@ const ROW_READING = {
41376
41416
  * onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
41377
41417
  * renderNeighbour?: (page: {route: object, params: object|undefined}) => import("preact").ComponentChildren,
41378
41418
  * prefetch?: boolean,
41419
+ * duration?: string,
41379
41420
  * }>}
41380
41421
  * @param {Array<object|{route: object, params?: object}>} [props.routes] - a window
41381
41422
  * over something longer, in order. Read from the <Route> children by default,
@@ -41405,6 +41446,9 @@ const ROW_READING = {
41405
41446
  * `route.preload(params)`. A swipe then builds a page whose answers are already
41406
41447
  * there, with no wait to show. Off by default: every page one stops on costs the
41407
41448
  * reads of two more.
41449
+ * @param {string} [props.duration="300ms"] - how long a travel asked for takes: a
41450
+ * tab pressed, the back button. One box, on the curve a SlideContainer plays, so
41451
+ * two rows of tabs in one app move alike. A hand goes at its own pace.
41408
41452
  *
41409
41453
  * Every other prop lands on the box itself (`id`, `data-testid`, `className`, …).
41410
41454
  * The box has to outlive every page of its row: around those <Route>s, or around
@@ -41422,6 +41466,7 @@ const RouteTravel = ({
41422
41466
  }),
41423
41467
  renderNeighbour,
41424
41468
  prefetch = false,
41469
+ duration = "300ms",
41425
41470
  className,
41426
41471
  children,
41427
41472
  ...rest
@@ -41462,7 +41507,8 @@ const RouteTravel = ({
41462
41507
  currentIndex,
41463
41508
  liveIndex,
41464
41509
  onTravel,
41465
- travelByDrag
41510
+ travelByDrag,
41511
+ durationMs: durationToMs(duration)
41466
41512
  };
41467
41513
 
41468
41514
  // The page this box asked for: routing lands later, and read back as somebody
@@ -41482,9 +41528,9 @@ const RouteTravel = ({
41482
41528
  Object.assign(movementRef.current, fields);
41483
41529
  return movementRef.current;
41484
41530
  }
41485
- row.setAttribute("data-moving", "");
41486
41531
  const movement = {
41487
41532
  travelling: false,
41533
+ playing: null,
41488
41534
  pictures: new Map(),
41489
41535
  resumeScrollRecording: null,
41490
41536
  scrollsDocument: rowFollowsDocument(row),
@@ -41580,11 +41626,11 @@ const RouteTravel = ({
41580
41626
  };
41581
41627
 
41582
41628
  // A travel no hand set off is one box long however far the page is, as in a
41583
- // SlideContainer: the browser's smooth scroll would fly past every page in between.
41584
- // So the page asked for takes the column next to what is in the frame, on the side
41585
- // it already stands, which moves nothing on screen, and endMovement puts the columns
41586
- // back. Columns rather than offsets: every page stays a place to snap to, and none
41587
- // has to be hidden.
41629
+ // SlideContainer: a scroll to where the page stands would fly past every page in
41630
+ // between. So the page asked for takes the column next to what is in the frame, on
41631
+ // the side it already stands, which moves nothing on screen, and endMovement puts
41632
+ // the columns back. Columns rather than offsets: every page stays a place to snap
41633
+ // to, and none has to be hidden.
41588
41634
  const travelRowTo = (index, movement) => {
41589
41635
  const row = rowRef.current;
41590
41636
  const panes = panesOf(row);
@@ -41629,10 +41675,85 @@ const RouteTravel = ({
41629
41675
  }
41630
41676
  });
41631
41677
  }
41632
- row.scrollTo({
41633
- left: pane.offsetLeft,
41634
- behavior: "smooth"
41635
- });
41678
+ playTravel(pane, movement);
41679
+ };
41680
+
41681
+ // A travel asked for, in `duration`, on SlideContainer's curve. Not the browser's
41682
+ // smooth scroll, which has no duration to ask for and a curve of its own (Chrome's
41683
+ // hesitates, then peaks at 1.65 times this one's speed): two rows of tabs in one
41684
+ // app would not move alike. The scroll position is written at every frame, so the
41685
+ // row stays a scroller all the way: what follows it (a Nav's bar) reads it as it
41686
+ // reads a hand, and a finger landing finds it where it is. The clock starts at the
41687
+ // first frame, after what the press itself costs, as an animation's does.
41688
+ const playTravel = (pane, movement) => {
41689
+ const row = rowRef.current;
41690
+ // Already moving, so there is no ease-in to play: it would stall the row for an
41691
+ // instant right where the eye is following it.
41692
+ const replaced = movement.playing;
41693
+ const inFlight = Boolean(replaced) || Boolean(movement.rowTaken);
41694
+ if (replaced) {
41695
+ // Its frames only: the row stays travelling across the hand-over. Let go of,
41696
+ // it snaps to the nearest page at the read below, and the travel starts there.
41697
+ replaced.cancelFrame();
41698
+ }
41699
+ const from = row.scrollLeft;
41700
+ const {
41701
+ durationMs
41702
+ } = latestRef.current;
41703
+ // One box in `duration` is the pace: what is left of a travel picked up on the
41704
+ // way takes what is left of the time. Never slower: two boxes are not made to drag.
41705
+ const boxes = Math.abs(pane.offsetLeft - from) / row.clientWidth;
41706
+ const travelMs = boxes < 1 ? durationMs * boxes : durationMs;
41707
+ if (!travelMs) {
41708
+ row.scrollLeft = pane.offsetLeft;
41709
+ // A write that changes nothing is followed by no scroll event to say the row
41710
+ // stopped. After the commit that asked, which goes on to say the row moves.
41711
+ Promise.resolve().then(landRow);
41712
+ return;
41713
+ }
41714
+ const curve = inFlight ? CSS_EASE_OUT : CSS_EASE;
41715
+ let start = null;
41716
+ let frame = null;
41717
+ const playing = {
41718
+ // Where this travel put the row, for a scroll event to tell it from a hand.
41719
+ written: from,
41720
+ cancelFrame: () => {
41721
+ cancelAnimationFrame(frame);
41722
+ },
41723
+ stop: () => {
41724
+ playing.cancelFrame();
41725
+ if (movement.playing === playing) {
41726
+ movement.playing = null;
41727
+ row.removeAttribute("data-route-row-travelling");
41728
+ }
41729
+ }
41730
+ };
41731
+ const step = now => {
41732
+ if (start === null) {
41733
+ // When this frame runs, not when it began: under load it began while the
41734
+ // press was still being handled, and that time is no travel.
41735
+ start = performance.now();
41736
+ }
41737
+ // The next frame may have begun while this first one was still being handled.
41738
+ const elapsed = now > start ? (now - start) / travelMs : 0;
41739
+ // Read at every frame: the layout may move under the travel (a scrollbar
41740
+ // coming in narrows the row), and it goes where the page stands.
41741
+ const to = pane.offsetLeft;
41742
+ const arrived = elapsed >= 1;
41743
+ playing.written = arrived ? to : from + (to - from) * easedProgress(elapsed, curve);
41744
+ row.scrollLeft = playing.written;
41745
+ if (!arrived) {
41746
+ frame = requestAnimationFrame(step);
41747
+ return;
41748
+ }
41749
+ playing.stop();
41750
+ // Said here rather than waited for: the last frames of a curve can move the row
41751
+ // by less than a pixel, and nothing then says it has stopped.
41752
+ landRow();
41753
+ };
41754
+ movement.playing = playing;
41755
+ row.setAttribute("data-route-row-travelling", "");
41756
+ frame = requestAnimationFrame(step);
41636
41757
  };
41637
41758
 
41638
41759
  // Every column back where the map says, and the row on the page of the address, in
@@ -41656,9 +41777,8 @@ const RouteTravel = ({
41656
41777
 
41657
41778
  // A tab pressed over a row a hand has left moving, which the browser plays on and
41658
41779
  // overwrites a travel with once that travel is over: the snap is ended first. Given
41659
- // back, the row stands where it stood. A frame later, because until then WebKit still
41660
- // starts from where a box that is no scroller stood: a smooth scroll to the first page
41661
- // sees nothing to do.
41780
+ // back, the row stands where it stood. The travel a frame later, because until then
41781
+ // WebKit still has the row where a box that is no scroller stood, at 0.
41662
41782
  const takeRowFromHand = movement => {
41663
41783
  const row = rowRef.current;
41664
41784
  const position = row.scrollLeft;
@@ -41666,12 +41786,14 @@ const RouteTravel = ({
41666
41786
  // A travel no hand set off holds nothing: it goes where it goes.
41667
41787
  releaseHeldPage();
41668
41788
  row.style.setProperty("--navi-route-row-stand", `${-position}px`);
41669
- row.setAttribute("data-route-row-taken", "clip");
41789
+ row.setAttribute("data-route-row-taken", "");
41670
41790
  afterFrames(2, () => {
41671
41791
  if (movementRef.current !== movement) {
41672
41792
  return;
41673
41793
  }
41674
- row.setAttribute("data-route-row-taken", "");
41794
+ // A scroller again, standing where no page does: not snapped meanwhile.
41795
+ row.setAttribute("data-route-row-travelling", "");
41796
+ row.removeAttribute("data-route-row-taken");
41675
41797
  row.style.removeProperty("--navi-route-row-stand");
41676
41798
  afterFrames(1, () => {
41677
41799
  if (movementRef.current === movement) {
@@ -41690,8 +41812,11 @@ const RouteTravel = ({
41690
41812
  return;
41691
41813
  }
41692
41814
  movementRef.current = null;
41815
+ if (movement.playing) {
41816
+ movement.playing.stop();
41817
+ }
41693
41818
  putRowBack(movement);
41694
- row.removeAttribute("data-moving");
41819
+ row.removeAttribute("data-route-row-travelling");
41695
41820
  row.removeAttribute("data-route-row-taken");
41696
41821
  publishRouteRowMove(null);
41697
41822
  releaseHeldPage();
@@ -41720,6 +41845,45 @@ const RouteTravel = ({
41720
41845
  render();
41721
41846
  };
41722
41847
 
41848
+ // The row may have stopped: on the page of the address, the movement is over; on
41849
+ // another, a hand put it there and the address follows.
41850
+ const landRow = () => {
41851
+ const row = rowRef.current;
41852
+ const movement = movementRef.current;
41853
+ // A travel playing says itself when it is over.
41854
+ if (!movement || movement.playing || isRowTaken(row)) {
41855
+ return;
41856
+ }
41857
+ const panes = panesOf(row);
41858
+ const pane = pageAtRest(row, panes, ROW_READING);
41859
+ if (!pane) {
41860
+ // A travel a hand took over (a trackpad, a wheel) and left between two pages:
41861
+ // sent again, the row then rests.
41862
+ const {
41863
+ currentIndex
41864
+ } = latestRef.current;
41865
+ if (movement.travelling && panes[currentIndex]) {
41866
+ travelRowTo(currentIndex, movement);
41867
+ }
41868
+ return;
41869
+ }
41870
+ movement.landed = true;
41871
+ const {
41872
+ pages
41873
+ } = latestRef.current;
41874
+ const page = pages[panes.indexOf(pane)];
41875
+ if (!page || pageIsCurrent(page)) {
41876
+ if (!askedPageRef.current || samePage(askedPageRef.current, page)) {
41877
+ endMovement();
41878
+ }
41879
+ return;
41880
+ }
41881
+ if (samePage(askedPageRef.current, page)) {
41882
+ return;
41883
+ }
41884
+ askForPageOfRow(panes.indexOf(pane), movement);
41885
+ };
41886
+
41723
41887
  // The address and the row, in step. The opening and a row given again around the
41724
41888
  // page on screen have nothing to show for it; somebody else moving the address
41725
41889
  // (a tab pressed, the back button) is a travel.
@@ -41740,6 +41904,13 @@ const RouteTravel = ({
41740
41904
  }
41741
41905
  answeredPageRef.current = page;
41742
41906
  if (!page || !pane) {
41907
+ // The address left the row while a travel nobody's hand is on was playing (a
41908
+ // link to a page off the row): there is nothing left to travel to, and the
41909
+ // landing would send the address back to the page the row rests on.
41910
+ const movement = movementRef.current;
41911
+ if (!page && movement?.travelling && !movement.byHand) {
41912
+ Promise.resolve().then(endMovement);
41913
+ }
41743
41914
  return;
41744
41915
  }
41745
41916
  const asked = askedPageRef.current;
@@ -41769,7 +41940,7 @@ const RouteTravel = ({
41769
41940
  takeRowFromHand(movement);
41770
41941
  return;
41771
41942
  }
41772
- if (row.getAttribute("data-route-row-taken") !== "clip") {
41943
+ if (!row.hasAttribute("data-route-row-taken")) {
41773
41944
  travelRowTo(currentIndex, movement);
41774
41945
  }
41775
41946
  // In this very commit: the tab pressed already wears its bar, and the first
@@ -41794,8 +41965,7 @@ const RouteTravel = ({
41794
41965
  if (asked || currentIndex === -1) {
41795
41966
  return;
41796
41967
  }
41797
- const path = new URL(url, window.location.href).pathname;
41798
- if (pathOf(pages[currentIndex]) === path || !pages.some(page => pathOf(page) === path)) {
41968
+ if (pageIsAt(pages[currentIndex], url) || !pages.some(page => pageIsAt(page, url))) {
41799
41969
  return;
41800
41970
  }
41801
41971
  const movement = beginMovement({
@@ -41806,7 +41976,6 @@ const RouteTravel = ({
41806
41976
  }, [windowed]);
41807
41977
  useLayoutEffect(() => {
41808
41978
  const row = rowRef.current;
41809
- const isClipped = () => row.getAttribute("data-route-row-taken") === "clip";
41810
41979
  const onScroll = ({
41811
41980
  finger,
41812
41981
  lifted
@@ -41817,10 +41986,15 @@ const RouteTravel = ({
41817
41986
  travelByDrag
41818
41987
  } = latestRef.current;
41819
41988
  const panes = panesOf(row);
41820
- if (panes.length < 2 || isClipped()) {
41989
+ if (panes.length < 2 || isRowTaken(row)) {
41821
41990
  return;
41822
41991
  }
41823
41992
  const moving = movementRef.current;
41993
+ // Not where the travel put it: a hand has taken the row (a trackpad, a wheel),
41994
+ // and the travel lets it go, snapping included, as the browser's own scroll does.
41995
+ if (moving?.playing && Math.abs(row.scrollLeft - moving.playing.written) > WRITE_ROUNDING) {
41996
+ moving.playing.stop();
41997
+ }
41824
41998
  const pane = panes[currentIndex];
41825
41999
  if (!moving && pane && restsOn(row, pane, ROW_READING)) {
41826
42000
  // Written by this box, at rest: nobody is moving anything.
@@ -41900,60 +42074,20 @@ const RouteTravel = ({
41900
42074
  // address ran ahead to, even for a render.
41901
42075
  releaseHeldPage();
41902
42076
  };
41903
- const onLand = () => {
41904
- const movement = movementRef.current;
41905
- if (!movement || isClipped()) {
41906
- return;
41907
- }
41908
- const panes = panesOf(row);
41909
- const pane = pageAtRest(row, panes, ROW_READING);
41910
- if (!pane) {
41911
- // A travel stopped short of its page: the layout moved under it (a scrollbar
41912
- // coming in narrows the row), and where it was sent is not where the page
41913
- // stands. Sent again: the row then rests, or does not move and is not heard.
41914
- const target = panes[latestRef.current.currentIndex];
41915
- if (movement.travelling && !isClipped() && target) {
41916
- row.scrollTo({
41917
- left: target.offsetLeft,
41918
- behavior: "smooth"
41919
- });
41920
- }
41921
- return;
41922
- }
41923
- movement.landed = true;
41924
- const {
41925
- pages
41926
- } = latestRef.current;
41927
- const page = pages[panes.indexOf(pane)];
41928
- if (!page || pageIsCurrent(page)) {
41929
- if (!askedPageRef.current || samePage(askedPageRef.current, page)) {
41930
- endMovement();
41931
- }
41932
- return;
41933
- }
41934
- if (samePage(askedPageRef.current, page)) {
41935
- return;
41936
- }
41937
- askForPageOfRow(panes.indexOf(pane), movement);
41938
- };
41939
42077
  const stopWatching = watchRowScroll(row, {
41940
42078
  onScroll,
41941
- onLand,
42079
+ onLand: landRow,
41942
42080
  onLeftToSnap
41943
42081
  });
41944
42082
  // A finger on a travel still playing: the row is the finger's from here, and it
41945
- // stands where the travel was going, as in a SlideContainer. A wheel only gives the
41946
- // row its snapping back: a page scrolled vertically over it is no hand on it.
42083
+ // stands where the travel was going, as in a SlideContainer. Not a wheel: a page
42084
+ // scrolled vertically over the row is no hand on it, and one scrolling it sideways
42085
+ // is told apart by the travel itself (onScroll).
41947
42086
  const onTouchOnTravel = () => {
41948
- if (movementRef.current?.travelling && !isClipped()) {
42087
+ if (movementRef.current?.travelling && !isRowTaken(row)) {
41949
42088
  endMovement();
41950
42089
  }
41951
42090
  };
41952
- const onWheelOnTravel = () => {
41953
- if (row.getAttribute("data-route-row-taken") === "") {
41954
- row.removeAttribute("data-route-row-taken");
41955
- }
41956
- };
41957
42091
  // A press for the page the address is already on changes nothing the router can
41958
42092
  // tell, and a stroke no lift ends leaves the address where it was: it is heard here,
41959
42093
  // the way a tab says which page it is through its address.
@@ -41968,7 +42102,7 @@ const RouteTravel = ({
41968
42102
  currentIndex
41969
42103
  } = latestRef.current;
41970
42104
  const page = pages[currentIndex];
41971
- if (!link || !page || new URL(link.href).pathname !== pathOf(page)) {
42105
+ if (!link || !page || !pageIsAt(page, link.href)) {
41972
42106
  return;
41973
42107
  }
41974
42108
  movement.travelling = true;
@@ -41977,20 +42111,20 @@ const RouteTravel = ({
41977
42111
  row.addEventListener("touchstart", onTouchOnTravel, {
41978
42112
  passive: true
41979
42113
  });
41980
- row.addEventListener("wheel", onWheelOnTravel, {
41981
- passive: true
41982
- });
41983
42114
  document.addEventListener("click", onPress, {
41984
42115
  capture: true
41985
42116
  });
41986
42117
  return () => {
41987
42118
  stopWatching();
41988
42119
  row.removeEventListener("touchstart", onTouchOnTravel);
41989
- row.removeEventListener("wheel", onWheelOnTravel);
41990
42120
  document.removeEventListener("click", onPress, {
41991
42121
  capture: true
41992
42122
  });
41993
- movementRef.current?.resumeScrollRecording?.();
42123
+ const movement = movementRef.current;
42124
+ if (movement) {
42125
+ movement.playing?.stop();
42126
+ movement.resumeScrollRecording?.();
42127
+ }
41994
42128
  movementRef.current = null;
41995
42129
  publishRouteRowMove(null);
41996
42130
  releaseHeldPage();
@@ -42050,12 +42184,10 @@ const RouteTravel = ({
42050
42184
  }, [prefetch, neighbours, restCount]);
42051
42185
 
42052
42186
  // A neighbour shows from the first frame the compositor moves the row, before
42053
- // any scroll event: a finger landing is the last word before that.
42054
- const paintsNeighbours = Boolean(renderNeighbour);
42187
+ // any scroll event, so it has to stand where the reader is looking by then: a
42188
+ // finger landing is the last word before that. A trackpad says nothing first, and
42189
+ // the pin comes with its first scroll event (beginMovement).
42055
42190
  useLayoutEffect(() => {
42056
- if (!paintsNeighbours) {
42057
- return undefined;
42058
- }
42059
42191
  const row = rowRef.current;
42060
42192
  const onTouchStart = () => {
42061
42193
  if (!movementRef.current && rowFollowsDocument(row)) {
@@ -42068,7 +42200,8 @@ const RouteTravel = ({
42068
42200
  return () => {
42069
42201
  row.removeEventListener("touchstart", onTouchStart);
42070
42202
  };
42071
- }, [paintsNeighbours]);
42203
+ }, []);
42204
+ const paintsNeighbours = Boolean(renderNeighbour);
42072
42205
  const rowContext = useMemo(() => ({
42073
42206
  renderNeighbour
42074
42207
  }), [renderNeighbour]);
@@ -42152,6 +42285,10 @@ const copyScrollPositions = (from, to) => {
42152
42285
  }
42153
42286
  }
42154
42287
  };
42288
+
42289
+ // A tab pressed over a row a hand has left moving holds it still (takeRowFromHand):
42290
+ // what the row says meanwhile is not a hand moving it.
42291
+ const isRowTaken = row => row.hasAttribute("data-route-row-taken");
42155
42292
  const afterFrames = (count, callback) => {
42156
42293
  if (count === 0) {
42157
42294
  callback();
@@ -69174,15 +69311,6 @@ const lineAreas = (slideElements, layout) => {
69174
69311
  return parseAreas(layout === "column" ? names : [names.join(" ")]);
69175
69312
  };
69176
69313
 
69177
- // A CSS duration read as a number, so a window knows when its travel is over.
69178
- const durationToMs = duration => {
69179
- const number = parseFloat(duration);
69180
- if (Number.isNaN(number)) {
69181
- return 0;
69182
- }
69183
- return String(duration).trimEnd().endsWith("ms") ? number : number * 1000;
69184
- };
69185
-
69186
69314
  // What asked for a travel, read off the event that carried it: the hand said
69187
69315
  // "somewhere over there" (a gesture one browses with), or it said a name (a tab
69188
69316
  // pressed, a key, a command — a place aimed at). Nothing at all when the travel