@jsenv/navi 0.29.14 → 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 +196 -13
- package/dist/jsenv_navi.js.map +26 -11
- 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
|
@@ -13764,6 +13764,25 @@ const css$V = /* css */`
|
|
|
13764
13764
|
* @param {Function} [options.onClose] - Callback when callout is closed
|
|
13765
13765
|
* @param {boolean} [options.closeOnClickOutside] - Whether to close on outside clicks (defaults to true for "info" status)
|
|
13766
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.
|
|
13767
13786
|
* @returns {Object} - Callout object with properties:
|
|
13768
13787
|
* - {Function} close - Function to close the callout
|
|
13769
13788
|
* - {Function} update - Function to update message and options
|
|
@@ -35842,10 +35861,13 @@ installImportMetaCssBuild(import.meta);const css$N = /* css */`
|
|
|
35842
35861
|
--link-background: unset;
|
|
35843
35862
|
--link-background-current: unset;
|
|
35844
35863
|
--link-background-selected: light-dark(#bbdefb, #2563eb);
|
|
35845
|
-
--link-color:
|
|
35846
|
-
--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
|
+
);
|
|
35847
35869
|
|
|
35848
|
-
--link-color-pressed:
|
|
35870
|
+
--link-color-pressed: var(--navi-link-color-pressed);
|
|
35849
35871
|
--link-text-decoration: underline;
|
|
35850
35872
|
--link-text-decoration-hover: var(--link-text-decoration);
|
|
35851
35873
|
--link-cursor: pointer;
|
|
@@ -35854,7 +35876,7 @@ installImportMetaCssBuild(import.meta);const css$N = /* css */`
|
|
|
35854
35876
|
|
|
35855
35877
|
--link-current-indicator-size: 2px;
|
|
35856
35878
|
--link-current-indicator-spacing: 0;
|
|
35857
|
-
--link-current-indicator-color:
|
|
35879
|
+
--link-current-indicator-color: var(--navi-link-current-indicator-color);
|
|
35858
35880
|
}
|
|
35859
35881
|
}
|
|
35860
35882
|
|
|
@@ -38898,7 +38920,7 @@ installImportMetaCssBuild(import.meta);const css$F = /* css */`
|
|
|
38898
38920
|
--loader-color: var(--navi-loader-color);
|
|
38899
38921
|
--border-color: var(--navi-control-border-color);
|
|
38900
38922
|
--background-color: white;
|
|
38901
|
-
--accent-color:
|
|
38923
|
+
--accent-color: var(--navi-control-accent-color);
|
|
38902
38924
|
--background-color-checked: var(--accent-color);
|
|
38903
38925
|
--border-color-checked: var(--accent-color);
|
|
38904
38926
|
--checkmark-color: white;
|
|
@@ -39638,7 +39660,7 @@ installImportMetaCssBuild(import.meta);const css$D = /* css */`
|
|
|
39638
39660
|
--border-color: var(--navi-control-border-color);
|
|
39639
39661
|
--background-color: white;
|
|
39640
39662
|
--background-color-checked: var(--background-color);
|
|
39641
|
-
--accent-color:
|
|
39663
|
+
--accent-color: var(--navi-control-accent-color);
|
|
39642
39664
|
--radiomark-color: var(--accent-color);
|
|
39643
39665
|
--border-color-checked: var(--accent-color);
|
|
39644
39666
|
--cursor: pointer;
|
|
@@ -40150,7 +40172,7 @@ installImportMetaCssBuild(import.meta);const css$C = /* css */`
|
|
|
40150
40172
|
--font-family: var(--navi-control-font-family);
|
|
40151
40173
|
|
|
40152
40174
|
--loader-color: var(--navi-loader-color);
|
|
40153
|
-
--accent-color:
|
|
40175
|
+
--accent-color: var(--navi-control-accent-color);
|
|
40154
40176
|
--color-mix-light: black;
|
|
40155
40177
|
--color-mix-dark: white;
|
|
40156
40178
|
--color-mix: var(--color-mix-dark);
|
|
@@ -42983,6 +43005,13 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
42983
43005
|
* the map one would have drawn for a line, so it is drawn here and everything
|
|
42984
43006
|
* below only ever knows about maps.
|
|
42985
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
|
+
*
|
|
42986
43015
|
* The slides live INSIDE the box, which is what makes this work for a popup: a
|
|
42987
43016
|
* dialog and a popover are both promoted to the browser's top layer, so no
|
|
42988
43017
|
* container of ours could ever hold two of them side by side and translate the
|
|
@@ -43086,6 +43115,18 @@ const css$z = /* css */`
|
|
|
43086
43115
|
slides. */
|
|
43087
43116
|
translate: var(--slide-offset, 0);
|
|
43088
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
|
+
}
|
|
43089
43130
|
/* Nothing here for a slide not on screen: [inert] (set from JS) already
|
|
43090
43131
|
takes it out of reach of the pointer, of Tab and of a screen reader —
|
|
43091
43132
|
one attribute instead of pointer-events plus aria-hidden, and the only
|
|
@@ -43317,6 +43358,25 @@ const SlideContainer = ({
|
|
|
43317
43358
|
// mid-travel would read a moving value.
|
|
43318
43359
|
const offsetRef = useRef();
|
|
43319
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);
|
|
43320
43380
|
// What to do once the travel now starting is over, handed to the animation as
|
|
43321
43381
|
// soon as there is one.
|
|
43322
43382
|
const rollBackRef = useRef(null);
|
|
@@ -43418,6 +43478,52 @@ const SlideContainer = ({
|
|
|
43418
43478
|
}
|
|
43419
43479
|
};
|
|
43420
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
|
+
|
|
43421
43527
|
// Everything positional is decided here, from the DOM, once per render: where
|
|
43422
43528
|
// each slide stands on the map, which one is current, and how far the track
|
|
43423
43529
|
// must be for that one to be the one on screen. Reading the DOM is what makes
|
|
@@ -43435,10 +43541,59 @@ const SlideContainer = ({
|
|
|
43435
43541
|
// Nothing named, or a name nothing answers to: the first slide is the one
|
|
43436
43542
|
// shown, the way a stack of pages opens on its first page.
|
|
43437
43543
|
slideElements[0];
|
|
43438
|
-
const
|
|
43544
|
+
const currentArea = readArea(currentElement);
|
|
43545
|
+
const realPlaceOf = area => placeOf.get(area) || {
|
|
43439
43546
|
x: 0,
|
|
43440
43547
|
y: 0
|
|
43441
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);
|
|
43442
43597
|
// A transfer waiting for a travel that never happened — a controlled
|
|
43443
43598
|
// `current` the caller chose not to move: dropped, the focus has no
|
|
43444
43599
|
// business going anywhere. The one for a travel that DID happen stays,
|
|
@@ -43454,17 +43609,18 @@ const SlideContainer = ({
|
|
|
43454
43609
|
// and that one knows what was pressed.
|
|
43455
43610
|
const focusIsLeaving = !focusHandOverRef.current && slideElements.some(slideElement => slideElement !== currentElement && slideElement.contains(document.activeElement));
|
|
43456
43611
|
for (const slideElement of slideElements) {
|
|
43612
|
+
const area = readArea(slideElement);
|
|
43457
43613
|
const {
|
|
43458
43614
|
x,
|
|
43459
43615
|
y
|
|
43460
|
-
} =
|
|
43461
|
-
x: 0,
|
|
43462
|
-
y: 0
|
|
43463
|
-
};
|
|
43616
|
+
} = placeOfArea(area);
|
|
43464
43617
|
slideElement.style.setProperty("--slide-offset", `${x * 100}% ${y * 100}%`);
|
|
43465
43618
|
const isCurrent = slideElement === currentElement;
|
|
43466
43619
|
slideElement.toggleAttribute("data-current", isCurrent);
|
|
43467
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);
|
|
43468
43624
|
if (isCurrent) {
|
|
43469
43625
|
// Reachable again first, so the focus below has somewhere to land: an
|
|
43470
43626
|
// inert element cannot take it.
|
|
@@ -43491,7 +43647,6 @@ const SlideContainer = ({
|
|
|
43491
43647
|
// Where the track ends up, always — the animation below only covers the way
|
|
43492
43648
|
// there, and when it is over this is what holds.
|
|
43493
43649
|
track.style.setProperty("--slide-container-offset", offset);
|
|
43494
|
-
const durationMs = durationToMs(duration);
|
|
43495
43650
|
const travels = !noTravel && durationMs > 0 && offsetBefore !== undefined && offsetBefore !== offset;
|
|
43496
43651
|
if (travels) {
|
|
43497
43652
|
// The time it takes is the distance it has left to cover: a travel picked
|
|
@@ -43522,6 +43677,18 @@ const SlideContainer = ({
|
|
|
43522
43677
|
if (pendingRollsRef.current.length) {
|
|
43523
43678
|
hurryTravel(trackAnimationRef.current);
|
|
43524
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();
|
|
43525
43692
|
}
|
|
43526
43693
|
// A window waiting for its travel to be over (see goToArea's own loop
|
|
43527
43694
|
// branch): the animation says when, and says it about the move that just
|
|
@@ -43558,6 +43725,13 @@ const SlideContainer = ({
|
|
|
43558
43725
|
for (const slideElement of slideElements) {
|
|
43559
43726
|
slideElement.toggleAttribute("inert", slideElement !== currentElement);
|
|
43560
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
|
+
}
|
|
43561
43735
|
});
|
|
43562
43736
|
|
|
43563
43737
|
/**
|
|
@@ -43652,6 +43826,15 @@ const SlideContainer = ({
|
|
|
43652
43826
|
...cameFromRef.current,
|
|
43653
43827
|
[area]: readArea(currentElement)
|
|
43654
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;
|
|
43655
43838
|
if (loop) {
|
|
43656
43839
|
// A window does not change slide, it rolls: the travel plays, and once it
|
|
43657
43840
|
// is over the window is put back where it rests while whoever owns the
|