@jsenv/navi 0.29.12 → 0.29.15

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.
@@ -12042,6 +12042,13 @@ const mergeActionParams = (currentParams, newParams) => {
12042
12042
  if (currentParams === NO_PARAMS) {
12043
12043
  return newParams;
12044
12044
  }
12045
+ if (newParams === undefined) {
12046
+ // Every control binds its action to its UI state signal; a control that carries
12047
+ // no value (a button) has an undefined one. It contributes no params, which must
12048
+ // not be confused with "the params are undefined" — the params bound by
12049
+ // bindParams stay in place.
12050
+ return currentParams;
12051
+ }
12045
12052
  return mergeTwoJsValues(currentParams, newParams);
12046
12053
  };
12047
12054
 
@@ -13757,6 +13764,25 @@ const css$V = /* css */`
13757
13764
  * @param {Function} [options.onClose] - Callback when callout is closed
13758
13765
  * @param {boolean} [options.closeOnClickOutside] - Whether to close on outside clicks (defaults to true for "info" status)
13759
13766
  * @param {boolean} [options.debug=false] - Enable debug logging
13767
+ *
13768
+ * Positioning is also driven by attributes read on the anchor element itself
13769
+ * (so markup can tune a callout without going through this function):
13770
+ * - `data-callout-arrow-x="start" | "center" | "end"`: where the arrow points
13771
+ * horizontally along the anchor — `start`/`end` target the left/right edge of
13772
+ * the anchor's text content (inside its borders and padding), `center` targets
13773
+ * the middle of the anchor. Without it the arrow follows the anchor's computed
13774
+ * `text-align` (`center` → center, `right`/`end` → end, anything else → start),
13775
+ * so it lands where the text visually begins. The arrow is always clamped to
13776
+ * stay within the callout's own rounded corners, so a value pointing outside
13777
+ * the callout's width sticks to the nearest side.
13778
+ * - `data-callout-position="top" | "bottom" | …`: preferred side of the anchor
13779
+ * (defaults to `"bottom"`, flipped when there isn't enough space).
13780
+ * - `data-callout-position-fixed`: opt out of that flipping.
13781
+ * - `data-callout-anchor="<selector>"`: point at an inner element instead of the
13782
+ * anchor itself.
13783
+ * - `data-callout-point-to-border-box` / `data-callout-point-to-content-box`:
13784
+ * which box the callout aligns to.
13785
+ * - `data-callout-viewport-spacing="<number>"`: minimum gap with the viewport edges.
13760
13786
  * @returns {Object} - Callout object with properties:
13761
13787
  * - {Function} close - Function to close the callout
13762
13788
  * - {Function} update - Function to update message and options
@@ -35835,10 +35861,13 @@ installImportMetaCssBuild(import.meta);const css$N = /* css */`
35835
35861
  --link-background: unset;
35836
35862
  --link-background-current: unset;
35837
35863
  --link-background-selected: light-dark(#bbdefb, #2563eb);
35838
- --link-color: rgb(0, 0, 238);
35839
- --link-color-visited: color-mix(in srgb, var(--link-color), black 40%);
35864
+ --link-color: var(--navi-link-color);
35865
+ --link-color-visited: var(
35866
+ --navi-link-color-visited,
35867
+ color-mix(in srgb, var(--link-color), black 40%)
35868
+ );
35840
35869
 
35841
- --link-color-pressed: red;
35870
+ --link-color-pressed: var(--navi-link-color-pressed);
35842
35871
  --link-text-decoration: underline;
35843
35872
  --link-text-decoration-hover: var(--link-text-decoration);
35844
35873
  --link-cursor: pointer;
@@ -35847,7 +35876,7 @@ installImportMetaCssBuild(import.meta);const css$N = /* css */`
35847
35876
 
35848
35877
  --link-current-indicator-size: 2px;
35849
35878
  --link-current-indicator-spacing: 0;
35850
- --link-current-indicator-color: rgb(205, 52, 37);
35879
+ --link-current-indicator-color: var(--navi-link-current-indicator-color);
35851
35880
  }
35852
35881
  }
35853
35882
 
@@ -37767,7 +37796,16 @@ const FIXED_BAR_SPACE_CSS = /* css */ `
37767
37796
  // they overlap: the room to give back is the largest of them, not their sum,
37768
37797
  // and one leaving must leave the others' room in place.
37769
37798
  const sizeMapByArea = new Map();
37770
-
37799
+ // What is currently on <html> for each area (absent = the variable is not set).
37800
+ // Writing the value that is already there would invalidate layout for nothing,
37801
+ // and several bars sharing an edge means most calls compute the same largest
37802
+ // size again — the ones for the smaller bars, and the second half of a page
37803
+ // transition.
37804
+ const writtenValueByArea = new Map();
37805
+
37806
+ // A size that must land before the next paint: a render changed the bar, so
37807
+ // the room it takes is given back in that same commit and the content is never
37808
+ // painted under it.
37771
37809
  /**
37772
37810
  * @param {"top"|"bottom"|"left"|"right"} area
37773
37811
  * @param {Element} barElement - Which bar this size belongs to.
@@ -37775,6 +37813,65 @@ const sizeMapByArea = new Map();
37775
37813
  * content.
37776
37814
  */
37777
37815
  const setFixedBarSpace = (area, barElement, size) => {
37816
+ dropPendingSize(area, barElement);
37817
+ storeSize(area, barElement, size);
37818
+ writeSpace(area);
37819
+ };
37820
+
37821
+ // A size nothing asked for, coming from a ResizeObserver. Writing the variable
37822
+ // resizes an ANCESTOR of the bars — the scroll container takes its padding
37823
+ // from it — and mutating layout from inside a resize callback is what makes
37824
+ // the browser report "ResizeObserver loop completed with undelivered
37825
+ // notifications". So the write waits for the frame that resize produced.
37826
+ // Queued here rather than deferred by each bar on its own, so the bars sharing
37827
+ // an edge resolve to a single write instead of one per bar.
37828
+ /**
37829
+ * @param {"top"|"bottom"|"left"|"right"} area
37830
+ * @param {Element} barElement - Which bar this size belongs to.
37831
+ * @param {number} size - In px.
37832
+ */
37833
+ const requestFixedBarSpace = (area, barElement, size) => {
37834
+ let pendingSizeMap = pendingSizeMapByArea.get(area);
37835
+ if (!pendingSizeMap) {
37836
+ pendingSizeMap = new Map();
37837
+ pendingSizeMapByArea.set(area, pendingSizeMap);
37838
+ }
37839
+ pendingSizeMap.set(barElement, size);
37840
+ if (flushFrame !== null) {
37841
+ return;
37842
+ }
37843
+ flushFrame = requestAnimationFrame(flushPendingSizes);
37844
+ };
37845
+
37846
+ const pendingSizeMapByArea = new Map();
37847
+ let flushFrame = null;
37848
+
37849
+ const flushPendingSizes = () => {
37850
+ flushFrame = null;
37851
+ for (const [area, pendingSizeMap] of pendingSizeMapByArea) {
37852
+ for (const [barElement, size] of pendingSizeMap) {
37853
+ storeSize(area, barElement, size);
37854
+ }
37855
+ writeSpace(area);
37856
+ }
37857
+ pendingSizeMapByArea.clear();
37858
+ };
37859
+
37860
+ // What the bar itself just said wins over what its observer had queued about
37861
+ // it: a bar unmounting gives its room back, and a size queued for it before
37862
+ // that must not put it back.
37863
+ const dropPendingSize = (area, barElement) => {
37864
+ const pendingSizeMap = pendingSizeMapByArea.get(area);
37865
+ if (!pendingSizeMap) {
37866
+ return;
37867
+ }
37868
+ pendingSizeMap.delete(barElement);
37869
+ if (pendingSizeMap.size === 0) {
37870
+ pendingSizeMapByArea.delete(area);
37871
+ }
37872
+ };
37873
+
37874
+ const storeSize = (area, barElement, size) => {
37778
37875
  let sizeMap = sizeMapByArea.get(area);
37779
37876
  if (!sizeMap) {
37780
37877
  sizeMap = new Map();
@@ -37785,7 +37882,10 @@ const setFixedBarSpace = (area, barElement, size) => {
37785
37882
  } else {
37786
37883
  sizeMap.set(barElement, size);
37787
37884
  }
37885
+ };
37788
37886
 
37887
+ const writeSpace = (area) => {
37888
+ const sizeMap = sizeMapByArea.get(area);
37789
37889
  let largestSize = 0;
37790
37890
  for (const barSize of sizeMap.values()) {
37791
37891
  if (barSize > largestSize) {
@@ -37795,10 +37895,18 @@ const setFixedBarSpace = (area, barElement, size) => {
37795
37895
  const property = `--navi-fixed-bar-space-${area}`;
37796
37896
  const { style } = document.documentElement;
37797
37897
  if (sizeMap.size === 0) {
37798
- style.removeProperty(property);
37799
- } else {
37800
- style.setProperty(property, `${largestSize}px`);
37898
+ if (writtenValueByArea.has(area)) {
37899
+ writtenValueByArea.delete(area);
37900
+ style.removeProperty(property);
37901
+ }
37902
+ return;
37801
37903
  }
37904
+ const value = `${largestSize}px`;
37905
+ if (writtenValueByArea.get(area) === value) {
37906
+ return;
37907
+ }
37908
+ writtenValueByArea.set(area, value);
37909
+ style.setProperty(property, value);
37802
37910
  };
37803
37911
 
37804
37912
  installImportMetaCssBuild(import.meta);/**
@@ -37974,27 +38082,40 @@ const FixedBar = ({
37974
38082
  // rebuilt as a calc() expression, so a size coming from anywhere — a prop, a
37975
38083
  // theme variable, the content itself — is reserved just the same, and each
37976
38084
  // `env()` inset stays the browser's business alone.
37977
- // And measured again whenever it changes: a ResizeObserver on the bar covers
37978
- // in one go a size prop that changes, content arriving or leaving, a font
37979
- // loading late, a rotation moving the notch.
37980
38085
  const vertical = area === "left" || area === "right";
37981
38086
  const {
37982
38087
  ref
37983
38088
  } = props;
38089
+ const measureSpace = barElement => {
38090
+ const {
38091
+ width,
38092
+ height
38093
+ } = barElement.getBoundingClientRect();
38094
+ return vertical ? width : height;
38095
+ };
38096
+
38097
+ // Anything a render can change — a size prop, the children, a theme variable
38098
+ // — is measured in that same commit, before paint: no frame where the
38099
+ // content sits under the bar, and nothing written from inside an observer.
38100
+ useLayoutEffect(() => {
38101
+ const barElement = ref.current;
38102
+ if (!barElement) {
38103
+ return;
38104
+ }
38105
+ setFixedBarSpace(area, barElement, measureSpace(barElement));
38106
+ });
38107
+
38108
+ // What no render caused: a font loading late, content arriving from outside,
38109
+ // a rotation moving the notch. Measuring here is safe; the write is what has
38110
+ // to wait, and fixed_bar_space.js is the one that holds it back.
37984
38111
  useLayoutEffect(() => {
37985
38112
  const barElement = ref.current;
37986
38113
  if (!barElement) {
37987
38114
  return undefined;
37988
38115
  }
37989
- const publishSize = () => {
37990
- const {
37991
- width,
37992
- height
37993
- } = barElement.getBoundingClientRect();
37994
- setFixedBarSpace(area, barElement, vertical ? width : height);
37995
- };
37996
- publishSize();
37997
- const resizeObserver = new ResizeObserver(publishSize);
38116
+ const resizeObserver = new ResizeObserver(() => {
38117
+ requestFixedBarSpace(area, barElement, measureSpace(barElement));
38118
+ });
37998
38119
  resizeObserver.observe(barElement);
37999
38120
  return () => {
38000
38121
  resizeObserver.disconnect();
@@ -38799,7 +38920,7 @@ installImportMetaCssBuild(import.meta);const css$F = /* css */`
38799
38920
  --loader-color: var(--navi-loader-color);
38800
38921
  --border-color: var(--navi-control-border-color);
38801
38922
  --background-color: white;
38802
- --accent-color: light-dark(#4476ff, #3b82f6);
38923
+ --accent-color: var(--navi-control-accent-color);
38803
38924
  --background-color-checked: var(--accent-color);
38804
38925
  --border-color-checked: var(--accent-color);
38805
38926
  --checkmark-color: white;
@@ -39539,7 +39660,7 @@ installImportMetaCssBuild(import.meta);const css$D = /* css */`
39539
39660
  --border-color: var(--navi-control-border-color);
39540
39661
  --background-color: white;
39541
39662
  --background-color-checked: var(--background-color);
39542
- --accent-color: light-dark(#4476ff, #3b82f6);
39663
+ --accent-color: var(--navi-control-accent-color);
39543
39664
  --radiomark-color: var(--accent-color);
39544
39665
  --border-color-checked: var(--accent-color);
39545
39666
  --cursor: pointer;
@@ -40051,7 +40172,7 @@ installImportMetaCssBuild(import.meta);const css$C = /* css */`
40051
40172
  --font-family: var(--navi-control-font-family);
40052
40173
 
40053
40174
  --loader-color: var(--navi-loader-color);
40054
- --accent-color: rgb(24, 117, 255);
40175
+ --accent-color: var(--navi-control-accent-color);
40055
40176
  --color-mix-light: black;
40056
40177
  --color-mix-dark: white;
40057
40178
  --color-mix: var(--color-mix-dark);
@@ -42884,6 +43005,13 @@ installImportMetaCssBuild(import.meta);/**
42884
43005
  * the map one would have drawn for a line, so it is drawn here and everything
42885
43006
  * below only ever knows about maps.
42886
43007
  *
43008
+ * A travel is ONE BOX long, whatever the distance between the two slides on the
43009
+ * map: the slide arriving is placed next to the one being left for the duration
43010
+ * and put back where the map says afterwards, so a tab bar jumping from the
43011
+ * first tab to the last shows those two and nothing else. Nobody wants to watch
43012
+ * the slides in between fly past — least of all in a tab bar, where they are
43013
+ * not a road one travels but places one goes straight to.
43014
+ *
42887
43015
  * The slides live INSIDE the box, which is what makes this work for a popup: a
42888
43016
  * dialog and a popover are both promoted to the browser's top layer, so no
42889
43017
  * container of ours could ever hold two of them side by side and translate the
@@ -42987,6 +43115,18 @@ const css$z = /* css */`
42987
43115
  slides. */
42988
43116
  translate: var(--slide-offset, 0);
42989
43117
  }
43118
+ /* Off stage: everything but the slide one is looking at and, while the
43119
+ track moves, the slide one is leaving. A travel is one box long
43120
+ whatever the distance on the map (the two are placed a box apart for
43121
+ the occasion, see the layout effect), so the slides in between are
43122
+ never crossed — but on a map wider than one box they would still sit
43123
+ in the frame, and a tab bar jumping from the first tab to the last must
43124
+ show those two and nothing else.
43125
+ visibility, not display: this box is measured on its LARGEST slide, and
43126
+ a slide taken out of the layout would take its size out with it. */
43127
+ > [data-slide][data-slide-offstage] {
43128
+ visibility: hidden;
43129
+ }
42990
43130
  /* Nothing here for a slide not on screen: [inert] (set from JS) already
42991
43131
  takes it out of reach of the pointer, of Tab and of a screen reader —
42992
43132
  one attribute instead of pointer-events plus aria-hidden, and the only
@@ -43218,6 +43358,25 @@ const SlideContainer = ({
43218
43358
  // mid-travel would read a moving value.
43219
43359
  const offsetRef = useRef();
43220
43360
  const trackAnimationRef = useRef(null);
43361
+ // Where the slides are while the track moves, which is not where the map says
43362
+ // they are: the one being left stays put and the one arriving is placed ONE
43363
+ // BOX away from it, whichever way the travel goes and however far apart the
43364
+ // two are on the map. So a jump from the first tab to the last is one box of
43365
+ // travel and the tabs in between are never seen flying past — and everything
43366
+ // not in this map is off stage for the duration. Null at rest, when the map
43367
+ // is the whole truth again (see settleTravel).
43368
+ const stageRef = useRef(null);
43369
+ // Which slide the last drawing put on screen: the current one at rest, the
43370
+ // one being travelled TO while the track moves. What the next travel departs
43371
+ // from, because it is what one is looking at.
43372
+ const drawnAreaRef = useRef(undefined);
43373
+ // Which way the travel about to be drawn goes, when whatever asked for it
43374
+ // knows: a window stepping off its last slide comes back on its first, and
43375
+ // only the press says that is a step forward — the map, read between those
43376
+ // two places, says the opposite. Undefined for a travel asked for by name
43377
+ // (--navi-go-to-slide, --navi-back), where the map is the only thing that
43378
+ // knows and is right.
43379
+ const travelStepRef = useRef(null);
43221
43380
  // What to do once the travel now starting is over, handed to the animation as
43222
43381
  // soon as there is one.
43223
43382
  const rollBackRef = useRef(null);
@@ -43319,6 +43478,52 @@ const SlideContainer = ({
43319
43478
  }
43320
43479
  };
43321
43480
 
43481
+ // The travel is over: the stage is struck and every slide goes back where the
43482
+ // map says it is. Nothing is seen moving for it — the slide on screen sits at
43483
+ // the same place whatever the arrangement (its own offset and the track's are
43484
+ // opposites and cancel out), and the others are off stage — as long as the
43485
+ // two are written in one go, which is why this is done here rather than left
43486
+ // to a render.
43487
+ const settleTravel = () => {
43488
+ const track = trackRef.current;
43489
+ if (!track) {
43490
+ return;
43491
+ }
43492
+ const {
43493
+ slideElements,
43494
+ placeOf
43495
+ } = readMap();
43496
+ const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
43497
+ if (!currentElement) {
43498
+ return;
43499
+ }
43500
+ stageRef.current = null;
43501
+ trackAnimationRef.current = null;
43502
+ for (const slideElement of slideElements) {
43503
+ const {
43504
+ x,
43505
+ y
43506
+ } = placeOf.get(readArea(slideElement)) || {
43507
+ x: 0,
43508
+ y: 0
43509
+ };
43510
+ slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
43511
+ slideElement.toggleAttribute("data-slide-offstage", slideElement !== currentElement);
43512
+ }
43513
+ const currentArea = readArea(currentElement);
43514
+ drawnAreaRef.current = currentArea;
43515
+ const {
43516
+ x,
43517
+ y
43518
+ } = placeOf.get(currentArea) || {
43519
+ x: 0,
43520
+ y: 0
43521
+ };
43522
+ const offset = `${-x * 100}% ${-y * 100}%`;
43523
+ offsetRef.current = offset;
43524
+ track.style.setProperty("--slide-container-offset", offset);
43525
+ };
43526
+
43322
43527
  // Everything positional is decided here, from the DOM, once per render: where
43323
43528
  // each slide stands on the map, which one is current, and how far the track
43324
43529
  // must be for that one to be the one on screen. Reading the DOM is what makes
@@ -43336,10 +43541,59 @@ const SlideContainer = ({
43336
43541
  // Nothing named, or a name nothing answers to: the first slide is the one
43337
43542
  // shown, the way a stack of pages opens on its first page.
43338
43543
  slideElements[0];
43339
- const currentPlace = placeOf.get(readArea(currentElement)) || {
43544
+ const currentArea = readArea(currentElement);
43545
+ const realPlaceOf = area => placeOf.get(area) || {
43340
43546
  x: 0,
43341
43547
  y: 0
43342
43548
  };
43549
+ const durationMs = durationToMs(duration);
43550
+ // Nothing is travelling, so nothing is staged: the picture to paint is the
43551
+ // map itself, and a stage left over from a travel that has just been given
43552
+ // up on would be painted instead of it.
43553
+ if (noTravel) {
43554
+ stageRef.current = null;
43555
+ }
43556
+ let stage = stageRef.current;
43557
+ const drawnArea = stage ? stage.area : drawnAreaRef.current;
43558
+ const travelStarts = !noTravel && durationMs > 0 && drawnArea !== undefined && drawnArea !== currentArea && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
43559
+ if (travelStarts) {
43560
+ // Where the slide being left IS — the place the stage gave it if a travel
43561
+ // was already playing (a press landing mid-travel departs from where the
43562
+ // eye is, not from the map), its own place otherwise.
43563
+ const departurePlace = stage ? stage.placeByArea.get(drawnArea) ?? realPlaceOf(drawnArea) : realPlaceOf(drawnArea);
43564
+ const step = travelStepRef.current || {
43565
+ x: Math.sign(realPlaceOf(currentArea).x - realPlaceOf(drawnArea).x),
43566
+ y: Math.sign(realPlaceOf(currentArea).y - realPlaceOf(drawnArea).y)
43567
+ };
43568
+ // Kept, not replaced: the slides a chain of quick presses has already
43569
+ // left behind are still trailing off screen, and taking them off stage
43570
+ // now would blink them out mid-travel.
43571
+ const placeByArea = new Map(stage?.placeByArea);
43572
+ placeByArea.set(drawnArea, departurePlace);
43573
+ const arrivalPlace = {
43574
+ x: departurePlace.x + step.x,
43575
+ y: departurePlace.y + step.y
43576
+ };
43577
+ // The cell the arriving slide takes, taken back from whoever was left
43578
+ // standing on it: a travel turned around mid-flight comes back over
43579
+ // ground it has just covered, and the slide it left there would be
43580
+ // underneath the one arriving — two pictures in one box.
43581
+ for (const [stagedArea, stagedPlace] of placeByArea) {
43582
+ if (stagedArea !== drawnArea && stagedArea !== currentArea && stagedPlace.x === arrivalPlace.x && stagedPlace.y === arrivalPlace.y) {
43583
+ placeByArea.delete(stagedArea);
43584
+ }
43585
+ }
43586
+ placeByArea.set(currentArea, arrivalPlace);
43587
+ stage = stageRef.current = {
43588
+ placeByArea,
43589
+ area: currentArea
43590
+ };
43591
+ }
43592
+ // Said about the travel now being drawn and about no other: a re-render in
43593
+ // the middle of one reads the stage back, which already knows.
43594
+ travelStepRef.current = null;
43595
+ const placeOfArea = area => stage && stage.placeByArea.get(area) || realPlaceOf(area);
43596
+ const currentPlace = placeOfArea(currentArea);
43343
43597
  // A transfer waiting for a travel that never happened — a controlled
43344
43598
  // `current` the caller chose not to move: dropped, the focus has no
43345
43599
  // business going anywhere. The one for a travel that DID happen stays,
@@ -43355,17 +43609,18 @@ const SlideContainer = ({
43355
43609
  // and that one knows what was pressed.
43356
43610
  const focusIsLeaving = !focusHandOverRef.current && slideElements.some(slideElement => slideElement !== currentElement && slideElement.contains(document.activeElement));
43357
43611
  for (const slideElement of slideElements) {
43612
+ const area = readArea(slideElement);
43358
43613
  const {
43359
43614
  x,
43360
43615
  y
43361
- } = placeOf.get(readArea(slideElement)) || {
43362
- x: 0,
43363
- y: 0
43364
- };
43616
+ } = placeOfArea(area);
43365
43617
  slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
43366
43618
  const isCurrent = slideElement === currentElement;
43367
43619
  slideElement.toggleAttribute("data-current", isCurrent);
43368
43620
  slideElement.toggleAttribute("data-slide-displaced", !isCurrent);
43621
+ // On stage: the two ends of the travel while there is one, and the slide
43622
+ // being shown when there is not.
43623
+ slideElement.toggleAttribute("data-slide-offstage", stage ? !stage.placeByArea.has(area) : !isCurrent);
43369
43624
  if (isCurrent) {
43370
43625
  // Reachable again first, so the focus below has somewhere to land: an
43371
43626
  // inert element cannot take it.
@@ -43392,7 +43647,6 @@ const SlideContainer = ({
43392
43647
  // Where the track ends up, always — the animation below only covers the way
43393
43648
  // there, and when it is over this is what holds.
43394
43649
  track.style.setProperty("--slide-container-offset", offset);
43395
- const durationMs = durationToMs(duration);
43396
43650
  const travels = !noTravel && durationMs > 0 && offsetBefore !== undefined && offsetBefore !== offset;
43397
43651
  if (travels) {
43398
43652
  // The time it takes is the distance it has left to cover: a travel picked
@@ -43423,6 +43677,18 @@ const SlideContainer = ({
43423
43677
  if (pendingRollsRef.current.length) {
43424
43678
  hurryTravel(trackAnimationRef.current);
43425
43679
  }
43680
+ // Arrived: the map is the truth again (see settleTravel). Attached before
43681
+ // the window's own roll back just below, so the stage is struck first and
43682
+ // whatever that one renders is drawn from the map.
43683
+ trackAnimationRef.current.finished.then(settleTravel, () => {
43684
+ // cancelled by the next travel — that one carries the stage on
43685
+ });
43686
+ } else if (stage && trackAnimationRef.current?.playState !== "running") {
43687
+ // Staged with nothing left to play: a travel that was drawn and then had
43688
+ // its animation taken away (a duration set to 0, a re-render landing
43689
+ // between the two). Struck at once rather than left standing, since the
43690
+ // thing it was standing for is over.
43691
+ settleTravel();
43426
43692
  }
43427
43693
  // A window waiting for its travel to be over (see goToArea's own loop
43428
43694
  // branch): the animation says when, and says it about the move that just
@@ -43459,6 +43725,13 @@ const SlideContainer = ({
43459
43725
  for (const slideElement of slideElements) {
43460
43726
  slideElement.toggleAttribute("inert", slideElement !== currentElement);
43461
43727
  }
43728
+ // What is on screen now, for the travel after this one to depart from.
43729
+ // While a travel is playing the stage is the one that knows (it holds the
43730
+ // slide being travelled TO, which is what one is looking at), so this is
43731
+ // only ever written at rest.
43732
+ if (!stageRef.current) {
43733
+ drawnAreaRef.current = currentArea;
43734
+ }
43462
43735
  });
43463
43736
 
43464
43737
  /**
@@ -43553,6 +43826,15 @@ const SlideContainer = ({
43553
43826
  ...cameFromRef.current,
43554
43827
  [area]: readArea(currentElement)
43555
43828
  };
43829
+ // Which way this travel is drawn, said by what asked for it rather than
43830
+ // read off the map: a window stepping off its last slide comes back on its
43831
+ // first, and between those two places the map says "all the way back" when
43832
+ // the press said "one forward". Nothing to say when the travel was asked
43833
+ // for by name — there the map is the only one who knows.
43834
+ travelStepRef.current = dx || dy ? {
43835
+ x: Math.sign(dx),
43836
+ y: Math.sign(dy)
43837
+ } : null;
43556
43838
  if (loop) {
43557
43839
  // A window does not change slide, it rolls: the travel plays, and once it
43558
43840
  // is over the window is put back where it rests while whoever owns the