@jsenv/navi 0.30.29 → 0.30.31

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,76 @@ 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 inFlight = Boolean(movement.playing) || Boolean(movement.rowTaken);
41693
+ if (movement.playing) {
41694
+ movement.playing.stop();
41695
+ }
41696
+ const from = row.scrollLeft;
41697
+ const {
41698
+ durationMs
41699
+ } = latestRef.current;
41700
+ // One box in `duration` is the pace: what is left of a travel picked up on the
41701
+ // way takes what is left of the time. Never slower: two boxes are not made to drag.
41702
+ const boxes = Math.abs(pane.offsetLeft - from) / row.clientWidth;
41703
+ const travelMs = boxes < 1 ? durationMs * boxes : durationMs;
41704
+ if (!travelMs) {
41705
+ row.scrollLeft = pane.offsetLeft;
41706
+ // A write that changes nothing is followed by no scroll event to say the row
41707
+ // stopped. After the commit that asked, which goes on to say the row moves.
41708
+ Promise.resolve().then(landRow);
41709
+ return;
41710
+ }
41711
+ const curve = inFlight ? CSS_EASE_OUT : CSS_EASE;
41712
+ let start = null;
41713
+ let frame = null;
41714
+ const playing = {
41715
+ // Where this travel put the row, for a scroll event to tell it from a hand.
41716
+ written: from,
41717
+ stop: () => {
41718
+ cancelAnimationFrame(frame);
41719
+ if (movement.playing === playing) {
41720
+ movement.playing = null;
41721
+ row.removeAttribute("data-route-row-travelling");
41722
+ }
41723
+ }
41724
+ };
41725
+ const step = now => {
41726
+ if (start === null) {
41727
+ start = now;
41728
+ }
41729
+ const elapsed = (now - start) / travelMs;
41730
+ // Read at every frame: the layout may move under the travel (a scrollbar
41731
+ // coming in narrows the row), and it goes where the page stands.
41732
+ const to = pane.offsetLeft;
41733
+ const arrived = elapsed >= 1;
41734
+ playing.written = arrived ? to : from + (to - from) * easedProgress(elapsed, curve);
41735
+ row.scrollLeft = playing.written;
41736
+ if (!arrived) {
41737
+ frame = requestAnimationFrame(step);
41738
+ return;
41739
+ }
41740
+ playing.stop();
41741
+ // Said here rather than waited for: the last frames of a curve can move the row
41742
+ // by less than a pixel, and nothing then says it has stopped.
41743
+ landRow();
41744
+ };
41745
+ movement.playing = playing;
41746
+ row.setAttribute("data-route-row-travelling", "");
41747
+ frame = requestAnimationFrame(step);
41636
41748
  };
41637
41749
 
41638
41750
  // Every column back where the map says, and the row on the page of the address, in
@@ -41656,9 +41768,8 @@ const RouteTravel = ({
41656
41768
 
41657
41769
  // A tab pressed over a row a hand has left moving, which the browser plays on and
41658
41770
  // 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.
41771
+ // back, the row stands where it stood. The travel a frame later, because until then
41772
+ // WebKit still has the row where a box that is no scroller stood, at 0.
41662
41773
  const takeRowFromHand = movement => {
41663
41774
  const row = rowRef.current;
41664
41775
  const position = row.scrollLeft;
@@ -41666,12 +41777,14 @@ const RouteTravel = ({
41666
41777
  // A travel no hand set off holds nothing: it goes where it goes.
41667
41778
  releaseHeldPage();
41668
41779
  row.style.setProperty("--navi-route-row-stand", `${-position}px`);
41669
- row.setAttribute("data-route-row-taken", "clip");
41780
+ row.setAttribute("data-route-row-taken", "");
41670
41781
  afterFrames(2, () => {
41671
41782
  if (movementRef.current !== movement) {
41672
41783
  return;
41673
41784
  }
41674
- row.setAttribute("data-route-row-taken", "");
41785
+ // A scroller again, standing where no page does: not snapped meanwhile.
41786
+ row.setAttribute("data-route-row-travelling", "");
41787
+ row.removeAttribute("data-route-row-taken");
41675
41788
  row.style.removeProperty("--navi-route-row-stand");
41676
41789
  afterFrames(1, () => {
41677
41790
  if (movementRef.current === movement) {
@@ -41690,8 +41803,11 @@ const RouteTravel = ({
41690
41803
  return;
41691
41804
  }
41692
41805
  movementRef.current = null;
41806
+ if (movement.playing) {
41807
+ movement.playing.stop();
41808
+ }
41693
41809
  putRowBack(movement);
41694
- row.removeAttribute("data-moving");
41810
+ row.removeAttribute("data-route-row-travelling");
41695
41811
  row.removeAttribute("data-route-row-taken");
41696
41812
  publishRouteRowMove(null);
41697
41813
  releaseHeldPage();
@@ -41720,6 +41836,45 @@ const RouteTravel = ({
41720
41836
  render();
41721
41837
  };
41722
41838
 
41839
+ // The row may have stopped: on the page of the address, the movement is over; on
41840
+ // another, a hand put it there and the address follows.
41841
+ const landRow = () => {
41842
+ const row = rowRef.current;
41843
+ const movement = movementRef.current;
41844
+ // A travel playing says itself when it is over.
41845
+ if (!movement || movement.playing || isRowTaken(row)) {
41846
+ return;
41847
+ }
41848
+ const panes = panesOf(row);
41849
+ const pane = pageAtRest(row, panes, ROW_READING);
41850
+ if (!pane) {
41851
+ // A travel a hand took over (a trackpad, a wheel) and left between two pages:
41852
+ // sent again, the row then rests.
41853
+ const {
41854
+ currentIndex
41855
+ } = latestRef.current;
41856
+ if (movement.travelling && panes[currentIndex]) {
41857
+ travelRowTo(currentIndex, movement);
41858
+ }
41859
+ return;
41860
+ }
41861
+ movement.landed = true;
41862
+ const {
41863
+ pages
41864
+ } = latestRef.current;
41865
+ const page = pages[panes.indexOf(pane)];
41866
+ if (!page || pageIsCurrent(page)) {
41867
+ if (!askedPageRef.current || samePage(askedPageRef.current, page)) {
41868
+ endMovement();
41869
+ }
41870
+ return;
41871
+ }
41872
+ if (samePage(askedPageRef.current, page)) {
41873
+ return;
41874
+ }
41875
+ askForPageOfRow(panes.indexOf(pane), movement);
41876
+ };
41877
+
41723
41878
  // The address and the row, in step. The opening and a row given again around the
41724
41879
  // page on screen have nothing to show for it; somebody else moving the address
41725
41880
  // (a tab pressed, the back button) is a travel.
@@ -41740,6 +41895,13 @@ const RouteTravel = ({
41740
41895
  }
41741
41896
  answeredPageRef.current = page;
41742
41897
  if (!page || !pane) {
41898
+ // The address left the row while a travel nobody's hand is on was playing (a
41899
+ // link to a page off the row): there is nothing left to travel to, and the
41900
+ // landing would send the address back to the page the row rests on.
41901
+ const movement = movementRef.current;
41902
+ if (!page && movement?.travelling && !movement.byHand) {
41903
+ Promise.resolve().then(endMovement);
41904
+ }
41743
41905
  return;
41744
41906
  }
41745
41907
  const asked = askedPageRef.current;
@@ -41769,7 +41931,7 @@ const RouteTravel = ({
41769
41931
  takeRowFromHand(movement);
41770
41932
  return;
41771
41933
  }
41772
- if (row.getAttribute("data-route-row-taken") !== "clip") {
41934
+ if (!row.hasAttribute("data-route-row-taken")) {
41773
41935
  travelRowTo(currentIndex, movement);
41774
41936
  }
41775
41937
  // In this very commit: the tab pressed already wears its bar, and the first
@@ -41794,8 +41956,7 @@ const RouteTravel = ({
41794
41956
  if (asked || currentIndex === -1) {
41795
41957
  return;
41796
41958
  }
41797
- const path = new URL(url, window.location.href).pathname;
41798
- if (pathOf(pages[currentIndex]) === path || !pages.some(page => pathOf(page) === path)) {
41959
+ if (pageIsAt(pages[currentIndex], url) || !pages.some(page => pageIsAt(page, url))) {
41799
41960
  return;
41800
41961
  }
41801
41962
  const movement = beginMovement({
@@ -41806,7 +41967,6 @@ const RouteTravel = ({
41806
41967
  }, [windowed]);
41807
41968
  useLayoutEffect(() => {
41808
41969
  const row = rowRef.current;
41809
- const isClipped = () => row.getAttribute("data-route-row-taken") === "clip";
41810
41970
  const onScroll = ({
41811
41971
  finger,
41812
41972
  lifted
@@ -41817,10 +41977,15 @@ const RouteTravel = ({
41817
41977
  travelByDrag
41818
41978
  } = latestRef.current;
41819
41979
  const panes = panesOf(row);
41820
- if (panes.length < 2 || isClipped()) {
41980
+ if (panes.length < 2 || isRowTaken(row)) {
41821
41981
  return;
41822
41982
  }
41823
41983
  const moving = movementRef.current;
41984
+ // Not where the travel put it: a hand has taken the row (a trackpad, a wheel),
41985
+ // and the travel lets it go, snapping included, as the browser's own scroll does.
41986
+ if (moving?.playing && Math.abs(row.scrollLeft - moving.playing.written) > WRITE_ROUNDING) {
41987
+ moving.playing.stop();
41988
+ }
41824
41989
  const pane = panes[currentIndex];
41825
41990
  if (!moving && pane && restsOn(row, pane, ROW_READING)) {
41826
41991
  // Written by this box, at rest: nobody is moving anything.
@@ -41900,60 +42065,20 @@ const RouteTravel = ({
41900
42065
  // address ran ahead to, even for a render.
41901
42066
  releaseHeldPage();
41902
42067
  };
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
42068
  const stopWatching = watchRowScroll(row, {
41940
42069
  onScroll,
41941
- onLand,
42070
+ onLand: landRow,
41942
42071
  onLeftToSnap
41943
42072
  });
41944
42073
  // 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.
42074
+ // stands where the travel was going, as in a SlideContainer. Not a wheel: a page
42075
+ // scrolled vertically over the row is no hand on it, and one scrolling it sideways
42076
+ // is told apart by the travel itself (onScroll).
41947
42077
  const onTouchOnTravel = () => {
41948
- if (movementRef.current?.travelling && !isClipped()) {
42078
+ if (movementRef.current?.travelling && !isRowTaken(row)) {
41949
42079
  endMovement();
41950
42080
  }
41951
42081
  };
41952
- const onWheelOnTravel = () => {
41953
- if (row.getAttribute("data-route-row-taken") === "") {
41954
- row.removeAttribute("data-route-row-taken");
41955
- }
41956
- };
41957
42082
  // A press for the page the address is already on changes nothing the router can
41958
42083
  // tell, and a stroke no lift ends leaves the address where it was: it is heard here,
41959
42084
  // the way a tab says which page it is through its address.
@@ -41968,7 +42093,7 @@ const RouteTravel = ({
41968
42093
  currentIndex
41969
42094
  } = latestRef.current;
41970
42095
  const page = pages[currentIndex];
41971
- if (!link || !page || new URL(link.href).pathname !== pathOf(page)) {
42096
+ if (!link || !page || !pageIsAt(page, link.href)) {
41972
42097
  return;
41973
42098
  }
41974
42099
  movement.travelling = true;
@@ -41977,20 +42102,20 @@ const RouteTravel = ({
41977
42102
  row.addEventListener("touchstart", onTouchOnTravel, {
41978
42103
  passive: true
41979
42104
  });
41980
- row.addEventListener("wheel", onWheelOnTravel, {
41981
- passive: true
41982
- });
41983
42105
  document.addEventListener("click", onPress, {
41984
42106
  capture: true
41985
42107
  });
41986
42108
  return () => {
41987
42109
  stopWatching();
41988
42110
  row.removeEventListener("touchstart", onTouchOnTravel);
41989
- row.removeEventListener("wheel", onWheelOnTravel);
41990
42111
  document.removeEventListener("click", onPress, {
41991
42112
  capture: true
41992
42113
  });
41993
- movementRef.current?.resumeScrollRecording?.();
42114
+ const movement = movementRef.current;
42115
+ if (movement) {
42116
+ movement.playing?.stop();
42117
+ movement.resumeScrollRecording?.();
42118
+ }
41994
42119
  movementRef.current = null;
41995
42120
  publishRouteRowMove(null);
41996
42121
  releaseHeldPage();
@@ -42050,12 +42175,10 @@ const RouteTravel = ({
42050
42175
  }, [prefetch, neighbours, restCount]);
42051
42176
 
42052
42177
  // 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);
42178
+ // any scroll event, so it has to stand where the reader is looking by then: a
42179
+ // finger landing is the last word before that. A trackpad says nothing first, and
42180
+ // the pin comes with its first scroll event (beginMovement).
42055
42181
  useLayoutEffect(() => {
42056
- if (!paintsNeighbours) {
42057
- return undefined;
42058
- }
42059
42182
  const row = rowRef.current;
42060
42183
  const onTouchStart = () => {
42061
42184
  if (!movementRef.current && rowFollowsDocument(row)) {
@@ -42068,7 +42191,8 @@ const RouteTravel = ({
42068
42191
  return () => {
42069
42192
  row.removeEventListener("touchstart", onTouchStart);
42070
42193
  };
42071
- }, [paintsNeighbours]);
42194
+ }, []);
42195
+ const paintsNeighbours = Boolean(renderNeighbour);
42072
42196
  const rowContext = useMemo(() => ({
42073
42197
  renderNeighbour
42074
42198
  }), [renderNeighbour]);
@@ -42152,6 +42276,10 @@ const copyScrollPositions = (from, to) => {
42152
42276
  }
42153
42277
  }
42154
42278
  };
42279
+
42280
+ // A tab pressed over a row a hand has left moving holds it still (takeRowFromHand):
42281
+ // what the row says meanwhile is not a hand moving it.
42282
+ const isRowTaken = row => row.hasAttribute("data-route-row-taken");
42155
42283
  const afterFrames = (count, callback) => {
42156
42284
  if (count === 0) {
42157
42285
  callback();
@@ -69174,15 +69302,6 @@ const lineAreas = (slideElements, layout) => {
69174
69302
  return parseAreas(layout === "column" ? names : [names.join(" ")]);
69175
69303
  };
69176
69304
 
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
69305
  // What asked for a travel, read off the event that carried it: the hand said
69187
69306
  // "somewhere over there" (a gesture one browses with), or it said a name (a tab
69188
69307
  // pressed, a key, a command — a place aimed at). Nothing at all when the travel