@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.
@@ -38156,6 +38156,28 @@ const pageIsCurrent = ({ route, params }) => {
38156
38156
  return params ? route.matchesParams(params) : true;
38157
38157
  };
38158
38158
 
38159
+ // The same question about an address the router does not have yet (a navigation
38160
+ // about to be routed, a link pressed): its path, and every param the page names at
38161
+ // the value the page gives it. The same path under another search param is another
38162
+ // page — a person opened from another list — and a param the page does not name is
38163
+ // state on top of it, as it is for matchesParams.
38164
+ const pageIsAt = ({ route, params }, url) => {
38165
+ const destination = new URL(url, window.location.href);
38166
+ const own = new URL(route.buildUrl(params), window.location.href);
38167
+ if (own.pathname !== destination.pathname) {
38168
+ return false;
38169
+ }
38170
+ if (!params) {
38171
+ return true;
38172
+ }
38173
+ for (const name of Object.keys(params)) {
38174
+ if (own.searchParams.get(name) !== destination.searchParams.get(name)) {
38175
+ return false;
38176
+ }
38177
+ }
38178
+ return true;
38179
+ };
38180
+
38159
38181
  /**
38160
38182
  * The page a <Route> container shows when none of its branches matched, given
38161
38183
  * a name.
@@ -40391,6 +40413,45 @@ const readBand = () => {
40391
40413
  };
40392
40414
  };
40393
40415
 
40416
+ /**
40417
+ * CSS durations and easing curves, for a movement played from JS that has to look
40418
+ * like one played by CSS: a row scrolled frame by frame next to a track animated by
40419
+ * the browser, a view transition walked back from where its curve has it.
40420
+ */
40421
+
40422
+ // A CSS duration read as a number of milliseconds.
40423
+ const durationToMs = (duration) => {
40424
+ const number = parseFloat(duration);
40425
+ if (Number.isNaN(number)) {
40426
+ return 0;
40427
+ }
40428
+ return String(duration).trimEnd().endsWith("ms") ? number : number * 1000;
40429
+ };
40430
+
40431
+ const CSS_EASE = [0.25, 0.1, 0.25, 1];
40432
+ const CSS_EASE_OUT = [0, 0, 0.58, 1];
40433
+
40434
+ // A cubic-bezier curve, evaluated: time in, distance out. Solved numerically
40435
+ // because the curve is parametric — two cubics sharing a parameter, with no closed
40436
+ // form for one against the other. Twenty halvings put the answer well under a pixel
40437
+ // of a screen-wide movement.
40438
+ const easedProgress = (x, [x1, y1, x2, y2]) => {
40439
+ let low = 0;
40440
+ let high = 1;
40441
+ for (let i = 0; i < 20; i++) {
40442
+ const mid = (low + high) / 2;
40443
+ if (bezierAxis(mid, x1, x2) < x) {
40444
+ low = mid;
40445
+ } else {
40446
+ high = mid;
40447
+ }
40448
+ }
40449
+ return bezierAxis((low + high) / 2, y1, y2);
40450
+ };
40451
+
40452
+ const bezierAxis = (s, a, b) =>
40453
+ 3 * (1 - s) * (1 - s) * s * a + 3 * (1 - s) * s * s * b + s * s * s;
40454
+
40394
40455
  /**
40395
40456
  * A view transition already playing, walked back to where it started.
40396
40457
  *
@@ -40420,6 +40481,7 @@ const readBand = () => {
40420
40481
  * there while the clock counts down.
40421
40482
  */
40422
40483
 
40484
+
40423
40485
  const viewTransitionAnimations = () => {
40424
40486
  const animations = [];
40425
40487
  for (const animation of document.getAnimations()) {
@@ -40544,27 +40606,6 @@ const whenPicturesArrived = (animations) => {
40544
40606
  );
40545
40607
  };
40546
40608
 
40547
- // CSS `ease`, evaluated: time in, distance out. Solved numerically because the
40548
- // curve is parametric — two cubics sharing a parameter, with no closed form for
40549
- // one against the other. Twenty halvings put the answer well under a pixel of a
40550
- // screen-wide movement.
40551
- const CSS_EASE = [0.25, 0.1, 0.25, 1];
40552
- const bezierAxis = (s, a, b) =>
40553
- 3 * (1 - s) * (1 - s) * s * a + 3 * (1 - s) * s * s * b + s * s * s;
40554
- const easedProgress = (x, [x1, y1, x2, y2]) => {
40555
- let low = 0;
40556
- let high = 1;
40557
- for (let i = 0; i < 20; i++) {
40558
- const mid = (low + high) / 2;
40559
- if (bezierAxis(mid, x1, x2) < x) {
40560
- low = mid;
40561
- } else {
40562
- high = mid;
40563
- }
40564
- }
40565
- return bezierAxis((low + high) / 2, y1, y2);
40566
- };
40567
-
40568
40609
  installImportMetaCssBuild(import.meta);
40569
40610
  const startViewTransition = ensureDocumentStartViewTransition();
40570
40611
  const TRANSITION_ATTRIBUTE = "data-navi-route-transition";
@@ -42594,11 +42635,11 @@ const css$16 = /* css */`.navi_route_travel {
42594
42635
  overflow: visible;
42595
42636
  }
42596
42637
 
42597
- &[data-route-row-taken] {
42638
+ &[data-route-row-travelling] {
42598
42639
  scroll-snap-type: none;
42599
42640
  }
42600
42641
 
42601
- &[data-route-row-taken="clip"] {
42642
+ &[data-route-row-taken] {
42602
42643
  overflow: clip;
42603
42644
 
42604
42645
  & > [data-route-row-pane], & > [data-route-row-live], & > [data-route-row-pictures] > * {
@@ -42617,12 +42658,9 @@ const css$16 = /* css */`.navi_route_travel {
42617
42658
  display: flex;
42618
42659
  }
42619
42660
 
42620
- &:not([data-moving]) > [data-route-row-pane][inert] {
42621
- visibility: hidden;
42622
- }
42623
-
42624
42661
  & > [data-route-row-pane][inert] {
42625
42662
  top: var(--navi-route-row-pin, 0);
42663
+ contain: paint;
42626
42664
  max-height: 100dvh;
42627
42665
  position: relative;
42628
42666
  overflow: clip;
@@ -42630,7 +42668,6 @@ const css$16 = /* css */`.navi_route_travel {
42630
42668
 
42631
42669
  & > [data-route-row-pane] > [data-route-row-neighbour] {
42632
42670
  contain: layout paint;
42633
- visibility: visible;
42634
42671
  flex-direction: column;
42635
42672
  display: flex;
42636
42673
  position: absolute;
@@ -42668,6 +42705,9 @@ const SET_OFF_PULL = 16;
42668
42705
  const ROW_READING = {
42669
42706
  sideways: true
42670
42707
  };
42708
+ // How far a scroll position written can read back from what was written: rounded to
42709
+ // the device's pixels.
42710
+ const WRITE_ROUNDING = 1;
42671
42711
 
42672
42712
  /**
42673
42713
  * @type {import("preact").FunctionComponent<{
@@ -42676,6 +42716,7 @@ const ROW_READING = {
42676
42716
  * onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
42677
42717
  * renderNeighbour?: (page: {route: object, params: object|undefined}) => import("preact").ComponentChildren,
42678
42718
  * prefetch?: boolean,
42719
+ * duration?: string,
42679
42720
  * }>}
42680
42721
  * @param {Array<object|{route: object, params?: object}>} [props.routes] - a window
42681
42722
  * over something longer, in order. Read from the <Route> children by default,
@@ -42705,6 +42746,9 @@ const ROW_READING = {
42705
42746
  * `route.preload(params)`. A swipe then builds a page whose answers are already
42706
42747
  * there, with no wait to show. Off by default: every page one stops on costs the
42707
42748
  * reads of two more.
42749
+ * @param {string} [props.duration="300ms"] - how long a travel asked for takes: a
42750
+ * tab pressed, the back button. One box, on the curve a SlideContainer plays, so
42751
+ * two rows of tabs in one app move alike. A hand goes at its own pace.
42708
42752
  *
42709
42753
  * Every other prop lands on the box itself (`id`, `data-testid`, `className`, …).
42710
42754
  * The box has to outlive every page of its row: around those <Route>s, or around
@@ -42722,6 +42766,7 @@ const RouteTravel = ({
42722
42766
  }),
42723
42767
  renderNeighbour,
42724
42768
  prefetch = false,
42769
+ duration = "300ms",
42725
42770
  className,
42726
42771
  children,
42727
42772
  ...rest
@@ -42762,7 +42807,8 @@ const RouteTravel = ({
42762
42807
  currentIndex,
42763
42808
  liveIndex,
42764
42809
  onTravel,
42765
- travelByDrag
42810
+ travelByDrag,
42811
+ durationMs: durationToMs(duration)
42766
42812
  };
42767
42813
 
42768
42814
  // The page this box asked for: routing lands later, and read back as somebody
@@ -42782,9 +42828,9 @@ const RouteTravel = ({
42782
42828
  Object.assign(movementRef.current, fields);
42783
42829
  return movementRef.current;
42784
42830
  }
42785
- row.setAttribute("data-moving", "");
42786
42831
  const movement = {
42787
42832
  travelling: false,
42833
+ playing: null,
42788
42834
  pictures: new Map(),
42789
42835
  resumeScrollRecording: null,
42790
42836
  scrollsDocument: rowFollowsDocument(row),
@@ -42880,11 +42926,11 @@ const RouteTravel = ({
42880
42926
  };
42881
42927
 
42882
42928
  // A travel no hand set off is one box long however far the page is, as in a
42883
- // SlideContainer: the browser's smooth scroll would fly past every page in between.
42884
- // So the page asked for takes the column next to what is in the frame, on the side
42885
- // it already stands, which moves nothing on screen, and endMovement puts the columns
42886
- // back. Columns rather than offsets: every page stays a place to snap to, and none
42887
- // has to be hidden.
42929
+ // SlideContainer: a scroll to where the page stands would fly past every page in
42930
+ // between. So the page asked for takes the column next to what is in the frame, on
42931
+ // the side it already stands, which moves nothing on screen, and endMovement puts
42932
+ // the columns back. Columns rather than offsets: every page stays a place to snap
42933
+ // to, and none has to be hidden.
42888
42934
  const travelRowTo = (index, movement) => {
42889
42935
  const row = rowRef.current;
42890
42936
  const panes = panesOf(row);
@@ -42929,10 +42975,85 @@ const RouteTravel = ({
42929
42975
  }
42930
42976
  });
42931
42977
  }
42932
- row.scrollTo({
42933
- left: pane.offsetLeft,
42934
- behavior: "smooth"
42935
- });
42978
+ playTravel(pane, movement);
42979
+ };
42980
+
42981
+ // A travel asked for, in `duration`, on SlideContainer's curve. Not the browser's
42982
+ // smooth scroll, which has no duration to ask for and a curve of its own (Chrome's
42983
+ // hesitates, then peaks at 1.65 times this one's speed): two rows of tabs in one
42984
+ // app would not move alike. The scroll position is written at every frame, so the
42985
+ // row stays a scroller all the way: what follows it (a Nav's bar) reads it as it
42986
+ // reads a hand, and a finger landing finds it where it is. The clock starts at the
42987
+ // first frame, after what the press itself costs, as an animation's does.
42988
+ const playTravel = (pane, movement) => {
42989
+ const row = rowRef.current;
42990
+ // Already moving, so there is no ease-in to play: it would stall the row for an
42991
+ // instant right where the eye is following it.
42992
+ const replaced = movement.playing;
42993
+ const inFlight = Boolean(replaced) || Boolean(movement.rowTaken);
42994
+ if (replaced) {
42995
+ // Its frames only: the row stays travelling across the hand-over. Let go of,
42996
+ // it snaps to the nearest page at the read below, and the travel starts there.
42997
+ replaced.cancelFrame();
42998
+ }
42999
+ const from = row.scrollLeft;
43000
+ const {
43001
+ durationMs
43002
+ } = latestRef.current;
43003
+ // One box in `duration` is the pace: what is left of a travel picked up on the
43004
+ // way takes what is left of the time. Never slower: two boxes are not made to drag.
43005
+ const boxes = Math.abs(pane.offsetLeft - from) / row.clientWidth;
43006
+ const travelMs = boxes < 1 ? durationMs * boxes : durationMs;
43007
+ if (!travelMs) {
43008
+ row.scrollLeft = pane.offsetLeft;
43009
+ // A write that changes nothing is followed by no scroll event to say the row
43010
+ // stopped. After the commit that asked, which goes on to say the row moves.
43011
+ Promise.resolve().then(landRow);
43012
+ return;
43013
+ }
43014
+ const curve = inFlight ? CSS_EASE_OUT : CSS_EASE;
43015
+ let start = null;
43016
+ let frame = null;
43017
+ const playing = {
43018
+ // Where this travel put the row, for a scroll event to tell it from a hand.
43019
+ written: from,
43020
+ cancelFrame: () => {
43021
+ cancelAnimationFrame(frame);
43022
+ },
43023
+ stop: () => {
43024
+ playing.cancelFrame();
43025
+ if (movement.playing === playing) {
43026
+ movement.playing = null;
43027
+ row.removeAttribute("data-route-row-travelling");
43028
+ }
43029
+ }
43030
+ };
43031
+ const step = now => {
43032
+ if (start === null) {
43033
+ // When this frame runs, not when it began: under load it began while the
43034
+ // press was still being handled, and that time is no travel.
43035
+ start = performance.now();
43036
+ }
43037
+ // The next frame may have begun while this first one was still being handled.
43038
+ const elapsed = now > start ? (now - start) / travelMs : 0;
43039
+ // Read at every frame: the layout may move under the travel (a scrollbar
43040
+ // coming in narrows the row), and it goes where the page stands.
43041
+ const to = pane.offsetLeft;
43042
+ const arrived = elapsed >= 1;
43043
+ playing.written = arrived ? to : from + (to - from) * easedProgress(elapsed, curve);
43044
+ row.scrollLeft = playing.written;
43045
+ if (!arrived) {
43046
+ frame = requestAnimationFrame(step);
43047
+ return;
43048
+ }
43049
+ playing.stop();
43050
+ // Said here rather than waited for: the last frames of a curve can move the row
43051
+ // by less than a pixel, and nothing then says it has stopped.
43052
+ landRow();
43053
+ };
43054
+ movement.playing = playing;
43055
+ row.setAttribute("data-route-row-travelling", "");
43056
+ frame = requestAnimationFrame(step);
42936
43057
  };
42937
43058
 
42938
43059
  // Every column back where the map says, and the row on the page of the address, in
@@ -42956,9 +43077,8 @@ const RouteTravel = ({
42956
43077
 
42957
43078
  // A tab pressed over a row a hand has left moving, which the browser plays on and
42958
43079
  // overwrites a travel with once that travel is over: the snap is ended first. Given
42959
- // back, the row stands where it stood. A frame later, because until then WebKit still
42960
- // starts from where a box that is no scroller stood: a smooth scroll to the first page
42961
- // sees nothing to do.
43080
+ // back, the row stands where it stood. The travel a frame later, because until then
43081
+ // WebKit still has the row where a box that is no scroller stood, at 0.
42962
43082
  const takeRowFromHand = movement => {
42963
43083
  const row = rowRef.current;
42964
43084
  const position = row.scrollLeft;
@@ -42966,12 +43086,14 @@ const RouteTravel = ({
42966
43086
  // A travel no hand set off holds nothing: it goes where it goes.
42967
43087
  releaseHeldPage();
42968
43088
  row.style.setProperty("--navi-route-row-stand", `${-position}px`);
42969
- row.setAttribute("data-route-row-taken", "clip");
43089
+ row.setAttribute("data-route-row-taken", "");
42970
43090
  afterFrames(2, () => {
42971
43091
  if (movementRef.current !== movement) {
42972
43092
  return;
42973
43093
  }
42974
- row.setAttribute("data-route-row-taken", "");
43094
+ // A scroller again, standing where no page does: not snapped meanwhile.
43095
+ row.setAttribute("data-route-row-travelling", "");
43096
+ row.removeAttribute("data-route-row-taken");
42975
43097
  row.style.removeProperty("--navi-route-row-stand");
42976
43098
  afterFrames(1, () => {
42977
43099
  if (movementRef.current === movement) {
@@ -42990,8 +43112,11 @@ const RouteTravel = ({
42990
43112
  return;
42991
43113
  }
42992
43114
  movementRef.current = null;
43115
+ if (movement.playing) {
43116
+ movement.playing.stop();
43117
+ }
42993
43118
  putRowBack(movement);
42994
- row.removeAttribute("data-moving");
43119
+ row.removeAttribute("data-route-row-travelling");
42995
43120
  row.removeAttribute("data-route-row-taken");
42996
43121
  publishRouteRowMove(null);
42997
43122
  releaseHeldPage();
@@ -43020,6 +43145,45 @@ const RouteTravel = ({
43020
43145
  render();
43021
43146
  };
43022
43147
 
43148
+ // The row may have stopped: on the page of the address, the movement is over; on
43149
+ // another, a hand put it there and the address follows.
43150
+ const landRow = () => {
43151
+ const row = rowRef.current;
43152
+ const movement = movementRef.current;
43153
+ // A travel playing says itself when it is over.
43154
+ if (!movement || movement.playing || isRowTaken(row)) {
43155
+ return;
43156
+ }
43157
+ const panes = panesOf(row);
43158
+ const pane = pageAtRest(row, panes, ROW_READING);
43159
+ if (!pane) {
43160
+ // A travel a hand took over (a trackpad, a wheel) and left between two pages:
43161
+ // sent again, the row then rests.
43162
+ const {
43163
+ currentIndex
43164
+ } = latestRef.current;
43165
+ if (movement.travelling && panes[currentIndex]) {
43166
+ travelRowTo(currentIndex, movement);
43167
+ }
43168
+ return;
43169
+ }
43170
+ movement.landed = true;
43171
+ const {
43172
+ pages
43173
+ } = latestRef.current;
43174
+ const page = pages[panes.indexOf(pane)];
43175
+ if (!page || pageIsCurrent(page)) {
43176
+ if (!askedPageRef.current || samePage(askedPageRef.current, page)) {
43177
+ endMovement();
43178
+ }
43179
+ return;
43180
+ }
43181
+ if (samePage(askedPageRef.current, page)) {
43182
+ return;
43183
+ }
43184
+ askForPageOfRow(panes.indexOf(pane), movement);
43185
+ };
43186
+
43023
43187
  // The address and the row, in step. The opening and a row given again around the
43024
43188
  // page on screen have nothing to show for it; somebody else moving the address
43025
43189
  // (a tab pressed, the back button) is a travel.
@@ -43040,6 +43204,13 @@ const RouteTravel = ({
43040
43204
  }
43041
43205
  answeredPageRef.current = page;
43042
43206
  if (!page || !pane) {
43207
+ // The address left the row while a travel nobody's hand is on was playing (a
43208
+ // link to a page off the row): there is nothing left to travel to, and the
43209
+ // landing would send the address back to the page the row rests on.
43210
+ const movement = movementRef.current;
43211
+ if (!page && movement?.travelling && !movement.byHand) {
43212
+ Promise.resolve().then(endMovement);
43213
+ }
43043
43214
  return;
43044
43215
  }
43045
43216
  const asked = askedPageRef.current;
@@ -43069,7 +43240,7 @@ const RouteTravel = ({
43069
43240
  takeRowFromHand(movement);
43070
43241
  return;
43071
43242
  }
43072
- if (row.getAttribute("data-route-row-taken") !== "clip") {
43243
+ if (!row.hasAttribute("data-route-row-taken")) {
43073
43244
  travelRowTo(currentIndex, movement);
43074
43245
  }
43075
43246
  // In this very commit: the tab pressed already wears its bar, and the first
@@ -43094,8 +43265,7 @@ const RouteTravel = ({
43094
43265
  if (asked || currentIndex === -1) {
43095
43266
  return;
43096
43267
  }
43097
- const path = new URL(url, window.location.href).pathname;
43098
- if (pathOf(pages[currentIndex]) === path || !pages.some(page => pathOf(page) === path)) {
43268
+ if (pageIsAt(pages[currentIndex], url) || !pages.some(page => pageIsAt(page, url))) {
43099
43269
  return;
43100
43270
  }
43101
43271
  const movement = beginMovement({
@@ -43106,7 +43276,6 @@ const RouteTravel = ({
43106
43276
  }, [windowed]);
43107
43277
  useLayoutEffect(() => {
43108
43278
  const row = rowRef.current;
43109
- const isClipped = () => row.getAttribute("data-route-row-taken") === "clip";
43110
43279
  const onScroll = ({
43111
43280
  finger,
43112
43281
  lifted
@@ -43117,10 +43286,15 @@ const RouteTravel = ({
43117
43286
  travelByDrag
43118
43287
  } = latestRef.current;
43119
43288
  const panes = panesOf(row);
43120
- if (panes.length < 2 || isClipped()) {
43289
+ if (panes.length < 2 || isRowTaken(row)) {
43121
43290
  return;
43122
43291
  }
43123
43292
  const moving = movementRef.current;
43293
+ // Not where the travel put it: a hand has taken the row (a trackpad, a wheel),
43294
+ // and the travel lets it go, snapping included, as the browser's own scroll does.
43295
+ if (moving?.playing && Math.abs(row.scrollLeft - moving.playing.written) > WRITE_ROUNDING) {
43296
+ moving.playing.stop();
43297
+ }
43124
43298
  const pane = panes[currentIndex];
43125
43299
  if (!moving && pane && restsOn(row, pane, ROW_READING)) {
43126
43300
  // Written by this box, at rest: nobody is moving anything.
@@ -43200,60 +43374,20 @@ const RouteTravel = ({
43200
43374
  // address ran ahead to, even for a render.
43201
43375
  releaseHeldPage();
43202
43376
  };
43203
- const onLand = () => {
43204
- const movement = movementRef.current;
43205
- if (!movement || isClipped()) {
43206
- return;
43207
- }
43208
- const panes = panesOf(row);
43209
- const pane = pageAtRest(row, panes, ROW_READING);
43210
- if (!pane) {
43211
- // A travel stopped short of its page: the layout moved under it (a scrollbar
43212
- // coming in narrows the row), and where it was sent is not where the page
43213
- // stands. Sent again: the row then rests, or does not move and is not heard.
43214
- const target = panes[latestRef.current.currentIndex];
43215
- if (movement.travelling && !isClipped() && target) {
43216
- row.scrollTo({
43217
- left: target.offsetLeft,
43218
- behavior: "smooth"
43219
- });
43220
- }
43221
- return;
43222
- }
43223
- movement.landed = true;
43224
- const {
43225
- pages
43226
- } = latestRef.current;
43227
- const page = pages[panes.indexOf(pane)];
43228
- if (!page || pageIsCurrent(page)) {
43229
- if (!askedPageRef.current || samePage(askedPageRef.current, page)) {
43230
- endMovement();
43231
- }
43232
- return;
43233
- }
43234
- if (samePage(askedPageRef.current, page)) {
43235
- return;
43236
- }
43237
- askForPageOfRow(panes.indexOf(pane), movement);
43238
- };
43239
43377
  const stopWatching = watchRowScroll(row, {
43240
43378
  onScroll,
43241
- onLand,
43379
+ onLand: landRow,
43242
43380
  onLeftToSnap
43243
43381
  });
43244
43382
  // A finger on a travel still playing: the row is the finger's from here, and it
43245
- // stands where the travel was going, as in a SlideContainer. A wheel only gives the
43246
- // row its snapping back: a page scrolled vertically over it is no hand on it.
43383
+ // stands where the travel was going, as in a SlideContainer. Not a wheel: a page
43384
+ // scrolled vertically over the row is no hand on it, and one scrolling it sideways
43385
+ // is told apart by the travel itself (onScroll).
43247
43386
  const onTouchOnTravel = () => {
43248
- if (movementRef.current?.travelling && !isClipped()) {
43387
+ if (movementRef.current?.travelling && !isRowTaken(row)) {
43249
43388
  endMovement();
43250
43389
  }
43251
43390
  };
43252
- const onWheelOnTravel = () => {
43253
- if (row.getAttribute("data-route-row-taken") === "") {
43254
- row.removeAttribute("data-route-row-taken");
43255
- }
43256
- };
43257
43391
  // A press for the page the address is already on changes nothing the router can
43258
43392
  // tell, and a stroke no lift ends leaves the address where it was: it is heard here,
43259
43393
  // the way a tab says which page it is through its address.
@@ -43268,7 +43402,7 @@ const RouteTravel = ({
43268
43402
  currentIndex
43269
43403
  } = latestRef.current;
43270
43404
  const page = pages[currentIndex];
43271
- if (!link || !page || new URL(link.href).pathname !== pathOf(page)) {
43405
+ if (!link || !page || !pageIsAt(page, link.href)) {
43272
43406
  return;
43273
43407
  }
43274
43408
  movement.travelling = true;
@@ -43277,20 +43411,20 @@ const RouteTravel = ({
43277
43411
  row.addEventListener("touchstart", onTouchOnTravel, {
43278
43412
  passive: true
43279
43413
  });
43280
- row.addEventListener("wheel", onWheelOnTravel, {
43281
- passive: true
43282
- });
43283
43414
  document.addEventListener("click", onPress, {
43284
43415
  capture: true
43285
43416
  });
43286
43417
  return () => {
43287
43418
  stopWatching();
43288
43419
  row.removeEventListener("touchstart", onTouchOnTravel);
43289
- row.removeEventListener("wheel", onWheelOnTravel);
43290
43420
  document.removeEventListener("click", onPress, {
43291
43421
  capture: true
43292
43422
  });
43293
- movementRef.current?.resumeScrollRecording?.();
43423
+ const movement = movementRef.current;
43424
+ if (movement) {
43425
+ movement.playing?.stop();
43426
+ movement.resumeScrollRecording?.();
43427
+ }
43294
43428
  movementRef.current = null;
43295
43429
  publishRouteRowMove(null);
43296
43430
  releaseHeldPage();
@@ -43350,12 +43484,10 @@ const RouteTravel = ({
43350
43484
  }, [prefetch, neighbours, restCount]);
43351
43485
 
43352
43486
  // A neighbour shows from the first frame the compositor moves the row, before
43353
- // any scroll event: a finger landing is the last word before that.
43354
- const paintsNeighbours = Boolean(renderNeighbour);
43487
+ // any scroll event, so it has to stand where the reader is looking by then: a
43488
+ // finger landing is the last word before that. A trackpad says nothing first, and
43489
+ // the pin comes with its first scroll event (beginMovement).
43355
43490
  useLayoutEffect(() => {
43356
- if (!paintsNeighbours) {
43357
- return undefined;
43358
- }
43359
43491
  const row = rowRef.current;
43360
43492
  const onTouchStart = () => {
43361
43493
  if (!movementRef.current && rowFollowsDocument(row)) {
@@ -43368,7 +43500,8 @@ const RouteTravel = ({
43368
43500
  return () => {
43369
43501
  row.removeEventListener("touchstart", onTouchStart);
43370
43502
  };
43371
- }, [paintsNeighbours]);
43503
+ }, []);
43504
+ const paintsNeighbours = Boolean(renderNeighbour);
43372
43505
  const rowContext = useMemo(() => ({
43373
43506
  renderNeighbour
43374
43507
  }), [renderNeighbour]);
@@ -43452,6 +43585,10 @@ const copyScrollPositions = (from, to) => {
43452
43585
  }
43453
43586
  }
43454
43587
  };
43588
+
43589
+ // A tab pressed over a row a hand has left moving holds it still (takeRowFromHand):
43590
+ // what the row says meanwhile is not a hand moving it.
43591
+ const isRowTaken = row => row.hasAttribute("data-route-row-taken");
43455
43592
  const afterFrames = (count, callback) => {
43456
43593
  if (count === 0) {
43457
43594
  callback();
@@ -70783,15 +70920,6 @@ const lineAreas = (slideElements, layout) => {
70783
70920
  return parseAreas(layout === "column" ? names : [names.join(" ")]);
70784
70921
  };
70785
70922
 
70786
- // A CSS duration read as a number, so a window knows when its travel is over.
70787
- const durationToMs = duration => {
70788
- const number = parseFloat(duration);
70789
- if (Number.isNaN(number)) {
70790
- return 0;
70791
- }
70792
- return String(duration).trimEnd().endsWith("ms") ? number : number * 1000;
70793
- };
70794
-
70795
70923
  // What asked for a travel, read off the event that carried it: the hand said
70796
70924
  // "somewhere over there" (a gesture one browses with), or it said a name (a tab
70797
70925
  // pressed, a key, a command — a place aimed at). Nothing at all when the travel