@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.
- package/dist/jsenv_navi.js +311 -29
- package/dist/jsenv_navi.js.map +31 -15
- package/dist/jsenv_navi_side_effects.js +21 -0
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +4 -0
- package/docs/css_architecture.md +53 -10
- package/docs/scroll.md +250 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -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:
|
|
35839
|
-
--link-color-visited:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
37799
|
-
|
|
37800
|
-
|
|
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
|
|
37990
|
-
|
|
37991
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
} =
|
|
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
|