@jsenv/navi 0.30.30 → 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.
@@ -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,76 @@ 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 inFlight = Boolean(movement.playing) || Boolean(movement.rowTaken);
42993
+ if (movement.playing) {
42994
+ movement.playing.stop();
42995
+ }
42996
+ const from = row.scrollLeft;
42997
+ const {
42998
+ durationMs
42999
+ } = latestRef.current;
43000
+ // One box in `duration` is the pace: what is left of a travel picked up on the
43001
+ // way takes what is left of the time. Never slower: two boxes are not made to drag.
43002
+ const boxes = Math.abs(pane.offsetLeft - from) / row.clientWidth;
43003
+ const travelMs = boxes < 1 ? durationMs * boxes : durationMs;
43004
+ if (!travelMs) {
43005
+ row.scrollLeft = pane.offsetLeft;
43006
+ // A write that changes nothing is followed by no scroll event to say the row
43007
+ // stopped. After the commit that asked, which goes on to say the row moves.
43008
+ Promise.resolve().then(landRow);
43009
+ return;
43010
+ }
43011
+ const curve = inFlight ? CSS_EASE_OUT : CSS_EASE;
43012
+ let start = null;
43013
+ let frame = null;
43014
+ const playing = {
43015
+ // Where this travel put the row, for a scroll event to tell it from a hand.
43016
+ written: from,
43017
+ stop: () => {
43018
+ cancelAnimationFrame(frame);
43019
+ if (movement.playing === playing) {
43020
+ movement.playing = null;
43021
+ row.removeAttribute("data-route-row-travelling");
43022
+ }
43023
+ }
43024
+ };
43025
+ const step = now => {
43026
+ if (start === null) {
43027
+ start = now;
43028
+ }
43029
+ const elapsed = (now - start) / travelMs;
43030
+ // Read at every frame: the layout may move under the travel (a scrollbar
43031
+ // coming in narrows the row), and it goes where the page stands.
43032
+ const to = pane.offsetLeft;
43033
+ const arrived = elapsed >= 1;
43034
+ playing.written = arrived ? to : from + (to - from) * easedProgress(elapsed, curve);
43035
+ row.scrollLeft = playing.written;
43036
+ if (!arrived) {
43037
+ frame = requestAnimationFrame(step);
43038
+ return;
43039
+ }
43040
+ playing.stop();
43041
+ // Said here rather than waited for: the last frames of a curve can move the row
43042
+ // by less than a pixel, and nothing then says it has stopped.
43043
+ landRow();
43044
+ };
43045
+ movement.playing = playing;
43046
+ row.setAttribute("data-route-row-travelling", "");
43047
+ frame = requestAnimationFrame(step);
42936
43048
  };
42937
43049
 
42938
43050
  // Every column back where the map says, and the row on the page of the address, in
@@ -42956,9 +43068,8 @@ const RouteTravel = ({
42956
43068
 
42957
43069
  // A tab pressed over a row a hand has left moving, which the browser plays on and
42958
43070
  // 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.
43071
+ // back, the row stands where it stood. The travel a frame later, because until then
43072
+ // WebKit still has the row where a box that is no scroller stood, at 0.
42962
43073
  const takeRowFromHand = movement => {
42963
43074
  const row = rowRef.current;
42964
43075
  const position = row.scrollLeft;
@@ -42966,12 +43077,14 @@ const RouteTravel = ({
42966
43077
  // A travel no hand set off holds nothing: it goes where it goes.
42967
43078
  releaseHeldPage();
42968
43079
  row.style.setProperty("--navi-route-row-stand", `${-position}px`);
42969
- row.setAttribute("data-route-row-taken", "clip");
43080
+ row.setAttribute("data-route-row-taken", "");
42970
43081
  afterFrames(2, () => {
42971
43082
  if (movementRef.current !== movement) {
42972
43083
  return;
42973
43084
  }
42974
- row.setAttribute("data-route-row-taken", "");
43085
+ // A scroller again, standing where no page does: not snapped meanwhile.
43086
+ row.setAttribute("data-route-row-travelling", "");
43087
+ row.removeAttribute("data-route-row-taken");
42975
43088
  row.style.removeProperty("--navi-route-row-stand");
42976
43089
  afterFrames(1, () => {
42977
43090
  if (movementRef.current === movement) {
@@ -42990,8 +43103,11 @@ const RouteTravel = ({
42990
43103
  return;
42991
43104
  }
42992
43105
  movementRef.current = null;
43106
+ if (movement.playing) {
43107
+ movement.playing.stop();
43108
+ }
42993
43109
  putRowBack(movement);
42994
- row.removeAttribute("data-moving");
43110
+ row.removeAttribute("data-route-row-travelling");
42995
43111
  row.removeAttribute("data-route-row-taken");
42996
43112
  publishRouteRowMove(null);
42997
43113
  releaseHeldPage();
@@ -43020,6 +43136,45 @@ const RouteTravel = ({
43020
43136
  render();
43021
43137
  };
43022
43138
 
43139
+ // The row may have stopped: on the page of the address, the movement is over; on
43140
+ // another, a hand put it there and the address follows.
43141
+ const landRow = () => {
43142
+ const row = rowRef.current;
43143
+ const movement = movementRef.current;
43144
+ // A travel playing says itself when it is over.
43145
+ if (!movement || movement.playing || isRowTaken(row)) {
43146
+ return;
43147
+ }
43148
+ const panes = panesOf(row);
43149
+ const pane = pageAtRest(row, panes, ROW_READING);
43150
+ if (!pane) {
43151
+ // A travel a hand took over (a trackpad, a wheel) and left between two pages:
43152
+ // sent again, the row then rests.
43153
+ const {
43154
+ currentIndex
43155
+ } = latestRef.current;
43156
+ if (movement.travelling && panes[currentIndex]) {
43157
+ travelRowTo(currentIndex, movement);
43158
+ }
43159
+ return;
43160
+ }
43161
+ movement.landed = true;
43162
+ const {
43163
+ pages
43164
+ } = latestRef.current;
43165
+ const page = pages[panes.indexOf(pane)];
43166
+ if (!page || pageIsCurrent(page)) {
43167
+ if (!askedPageRef.current || samePage(askedPageRef.current, page)) {
43168
+ endMovement();
43169
+ }
43170
+ return;
43171
+ }
43172
+ if (samePage(askedPageRef.current, page)) {
43173
+ return;
43174
+ }
43175
+ askForPageOfRow(panes.indexOf(pane), movement);
43176
+ };
43177
+
43023
43178
  // The address and the row, in step. The opening and a row given again around the
43024
43179
  // page on screen have nothing to show for it; somebody else moving the address
43025
43180
  // (a tab pressed, the back button) is a travel.
@@ -43040,6 +43195,13 @@ const RouteTravel = ({
43040
43195
  }
43041
43196
  answeredPageRef.current = page;
43042
43197
  if (!page || !pane) {
43198
+ // The address left the row while a travel nobody's hand is on was playing (a
43199
+ // link to a page off the row): there is nothing left to travel to, and the
43200
+ // landing would send the address back to the page the row rests on.
43201
+ const movement = movementRef.current;
43202
+ if (!page && movement?.travelling && !movement.byHand) {
43203
+ Promise.resolve().then(endMovement);
43204
+ }
43043
43205
  return;
43044
43206
  }
43045
43207
  const asked = askedPageRef.current;
@@ -43069,7 +43231,7 @@ const RouteTravel = ({
43069
43231
  takeRowFromHand(movement);
43070
43232
  return;
43071
43233
  }
43072
- if (row.getAttribute("data-route-row-taken") !== "clip") {
43234
+ if (!row.hasAttribute("data-route-row-taken")) {
43073
43235
  travelRowTo(currentIndex, movement);
43074
43236
  }
43075
43237
  // In this very commit: the tab pressed already wears its bar, and the first
@@ -43094,8 +43256,7 @@ const RouteTravel = ({
43094
43256
  if (asked || currentIndex === -1) {
43095
43257
  return;
43096
43258
  }
43097
- const path = new URL(url, window.location.href).pathname;
43098
- if (pathOf(pages[currentIndex]) === path || !pages.some(page => pathOf(page) === path)) {
43259
+ if (pageIsAt(pages[currentIndex], url) || !pages.some(page => pageIsAt(page, url))) {
43099
43260
  return;
43100
43261
  }
43101
43262
  const movement = beginMovement({
@@ -43106,7 +43267,6 @@ const RouteTravel = ({
43106
43267
  }, [windowed]);
43107
43268
  useLayoutEffect(() => {
43108
43269
  const row = rowRef.current;
43109
- const isClipped = () => row.getAttribute("data-route-row-taken") === "clip";
43110
43270
  const onScroll = ({
43111
43271
  finger,
43112
43272
  lifted
@@ -43117,10 +43277,15 @@ const RouteTravel = ({
43117
43277
  travelByDrag
43118
43278
  } = latestRef.current;
43119
43279
  const panes = panesOf(row);
43120
- if (panes.length < 2 || isClipped()) {
43280
+ if (panes.length < 2 || isRowTaken(row)) {
43121
43281
  return;
43122
43282
  }
43123
43283
  const moving = movementRef.current;
43284
+ // Not where the travel put it: a hand has taken the row (a trackpad, a wheel),
43285
+ // and the travel lets it go, snapping included, as the browser's own scroll does.
43286
+ if (moving?.playing && Math.abs(row.scrollLeft - moving.playing.written) > WRITE_ROUNDING) {
43287
+ moving.playing.stop();
43288
+ }
43124
43289
  const pane = panes[currentIndex];
43125
43290
  if (!moving && pane && restsOn(row, pane, ROW_READING)) {
43126
43291
  // Written by this box, at rest: nobody is moving anything.
@@ -43200,60 +43365,20 @@ const RouteTravel = ({
43200
43365
  // address ran ahead to, even for a render.
43201
43366
  releaseHeldPage();
43202
43367
  };
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
43368
  const stopWatching = watchRowScroll(row, {
43240
43369
  onScroll,
43241
- onLand,
43370
+ onLand: landRow,
43242
43371
  onLeftToSnap
43243
43372
  });
43244
43373
  // 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.
43374
+ // stands where the travel was going, as in a SlideContainer. Not a wheel: a page
43375
+ // scrolled vertically over the row is no hand on it, and one scrolling it sideways
43376
+ // is told apart by the travel itself (onScroll).
43247
43377
  const onTouchOnTravel = () => {
43248
- if (movementRef.current?.travelling && !isClipped()) {
43378
+ if (movementRef.current?.travelling && !isRowTaken(row)) {
43249
43379
  endMovement();
43250
43380
  }
43251
43381
  };
43252
- const onWheelOnTravel = () => {
43253
- if (row.getAttribute("data-route-row-taken") === "") {
43254
- row.removeAttribute("data-route-row-taken");
43255
- }
43256
- };
43257
43382
  // A press for the page the address is already on changes nothing the router can
43258
43383
  // tell, and a stroke no lift ends leaves the address where it was: it is heard here,
43259
43384
  // the way a tab says which page it is through its address.
@@ -43268,7 +43393,7 @@ const RouteTravel = ({
43268
43393
  currentIndex
43269
43394
  } = latestRef.current;
43270
43395
  const page = pages[currentIndex];
43271
- if (!link || !page || new URL(link.href).pathname !== pathOf(page)) {
43396
+ if (!link || !page || !pageIsAt(page, link.href)) {
43272
43397
  return;
43273
43398
  }
43274
43399
  movement.travelling = true;
@@ -43277,20 +43402,20 @@ const RouteTravel = ({
43277
43402
  row.addEventListener("touchstart", onTouchOnTravel, {
43278
43403
  passive: true
43279
43404
  });
43280
- row.addEventListener("wheel", onWheelOnTravel, {
43281
- passive: true
43282
- });
43283
43405
  document.addEventListener("click", onPress, {
43284
43406
  capture: true
43285
43407
  });
43286
43408
  return () => {
43287
43409
  stopWatching();
43288
43410
  row.removeEventListener("touchstart", onTouchOnTravel);
43289
- row.removeEventListener("wheel", onWheelOnTravel);
43290
43411
  document.removeEventListener("click", onPress, {
43291
43412
  capture: true
43292
43413
  });
43293
- movementRef.current?.resumeScrollRecording?.();
43414
+ const movement = movementRef.current;
43415
+ if (movement) {
43416
+ movement.playing?.stop();
43417
+ movement.resumeScrollRecording?.();
43418
+ }
43294
43419
  movementRef.current = null;
43295
43420
  publishRouteRowMove(null);
43296
43421
  releaseHeldPage();
@@ -43350,12 +43475,10 @@ const RouteTravel = ({
43350
43475
  }, [prefetch, neighbours, restCount]);
43351
43476
 
43352
43477
  // 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);
43478
+ // any scroll event, so it has to stand where the reader is looking by then: a
43479
+ // finger landing is the last word before that. A trackpad says nothing first, and
43480
+ // the pin comes with its first scroll event (beginMovement).
43355
43481
  useLayoutEffect(() => {
43356
- if (!paintsNeighbours) {
43357
- return undefined;
43358
- }
43359
43482
  const row = rowRef.current;
43360
43483
  const onTouchStart = () => {
43361
43484
  if (!movementRef.current && rowFollowsDocument(row)) {
@@ -43368,7 +43491,8 @@ const RouteTravel = ({
43368
43491
  return () => {
43369
43492
  row.removeEventListener("touchstart", onTouchStart);
43370
43493
  };
43371
- }, [paintsNeighbours]);
43494
+ }, []);
43495
+ const paintsNeighbours = Boolean(renderNeighbour);
43372
43496
  const rowContext = useMemo(() => ({
43373
43497
  renderNeighbour
43374
43498
  }), [renderNeighbour]);
@@ -43452,6 +43576,10 @@ const copyScrollPositions = (from, to) => {
43452
43576
  }
43453
43577
  }
43454
43578
  };
43579
+
43580
+ // A tab pressed over a row a hand has left moving holds it still (takeRowFromHand):
43581
+ // what the row says meanwhile is not a hand moving it.
43582
+ const isRowTaken = row => row.hasAttribute("data-route-row-taken");
43455
43583
  const afterFrames = (count, callback) => {
43456
43584
  if (count === 0) {
43457
43585
  callback();
@@ -70783,15 +70911,6 @@ const lineAreas = (slideElements, layout) => {
70783
70911
  return parseAreas(layout === "column" ? names : [names.join(" ")]);
70784
70912
  };
70785
70913
 
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
70914
  // What asked for a travel, read off the event that carried it: the hand said
70796
70915
  // "somewhere over there" (a gesture one browses with), or it said a name (a tab
70797
70916
  // pressed, a key, a command — a place aimed at). Nothing at all when the travel