@jsenv/navi 0.29.119 → 0.29.120

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.
@@ -38845,7 +38845,6 @@ installImportMetaCssBuild(import.meta);const css$W = /* css */`
38845
38845
  --link-text-decoration: underline;
38846
38846
  --link-text-decoration-hover: var(--link-text-decoration);
38847
38847
  --link-cursor: pointer;
38848
- --link-loading-outline-size: 1px;
38849
38848
  --link-outline-width: 2px;
38850
38849
 
38851
38850
  --link-current-indicator-size: 2px;
@@ -38904,12 +38903,7 @@ installImportMetaCssBuild(import.meta);const css$W = /* css */`
38904
38903
  aspect-ratio: inherit;
38905
38904
  padding-top: var(--x-link-padding-top);
38906
38905
  padding-right: var(--x-link-padding-right);
38907
- /* The loading underline is drawn inside the box: its own room is kept at
38908
- the bottom on top of whatever padding was asked for, so a link that
38909
- starts loading never reflows. */
38910
- padding-bottom: calc(
38911
- var(--x-link-padding-bottom) + var(--link-loading-outline-size)
38912
- );
38906
+ padding-bottom: var(--x-link-padding-bottom);
38913
38907
  padding-left: var(--x-link-padding-left);
38914
38908
  color: var(--x-link-color);
38915
38909
  text-decoration: var(--x-link-text-decoration);
@@ -49856,7 +49850,7 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
49856
49850
  * it says where one starts, not where one is — say `current` for that.
49857
49851
  * Without it the first slide is the one shown, the way a stack of pages opens
49858
49852
  * on its first page.
49859
- * @param {(area: string, detail: {cause: "drag"|"keyboard"|"command"|"code", event: Event}) => void|false|Promise<void|false>} [props.onCurrentChange]
49853
+ * @param {(area: string, detail: {cause: "drag"|"keyboard"|"command"|"code"|"url", event: Event}) => void|false|Promise<void|false>} [props.onCurrentChange]
49860
49854
  * - the slide being shown has changed. `cause` says what asked for it, which
49861
49855
  * is what tells a place browsed past from a place aimed at: a caller writing
49862
49856
  * this into the URL pushes a history entry for a tab that was pressed and
@@ -49866,6 +49860,8 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
49866
49860
  * from — a guard that says no, a session that is gone. A promise refuses it
49867
49861
  * late, once whatever it had to ask has answered; the travel plays meanwhile
49868
49862
  * and is undone if the answer is no.
49863
+ * `cause: "url"` is the address asking (see `urlParam`) — a load on a link, a
49864
+ * traversal — rather than anything done inside the box.
49869
49865
  * @param {"now"|"rest"} [props.commit="now"] - when the change is told.
49870
49866
  * "rest" waits for the travel to be over, and lets the container hold the
49871
49867
  * slide it is going to meanwhile: the picture moves with the finger and the
@@ -49883,6 +49879,33 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
49883
49879
  * one that just travelled there. Called once the travel is over, and in the
49884
49880
  * same render as the return to rest — anything later shows the old content
49885
49881
  * for a frame.
49882
+ * @param {string|{name: string, history?: "replace"|"push"}} [props.urlParam]
49883
+ * - the search param this container owns, and where it stands is written
49884
+ * into: `urlParam="step"` puts `?step=<area>` in the address on every travel,
49885
+ * and opens on the area it names on a load, a bookmark, a traversal, a link
49886
+ * from anywhere else in the application.
49887
+ * It is the answer to "the step should be readable and should survive a
49888
+ * reload" WITHOUT one route per slide: no route to declare, no route guard to
49889
+ * re-express the walk's own rules in, and no page transition — what travels
49890
+ * is the box, and the address is a label on where the box stands. Say it on a
49891
+ * container whose slides are places one may be sent to; a walk nobody links
49892
+ * into keeps its position to itself.
49893
+ * Written by REPLACEMENT: four steps that each stacked an entry would turn
49894
+ * one back-press into four, and the back arrow of a page means "leave this
49895
+ * page", not "one question back". `history: "push"` says the opposite for the
49896
+ * containers that mean it — slides that ARE places one came from, a gallery
49897
+ * one browses — and even there a slide reached by dragging replaces, so
49898
+ * swiping back and forth does not bury the way out.
49899
+ * The address is READ through the walk, not jumped to: it comes from outside
49900
+ * the box, so every slide between here and there is asked to let go the way a
49901
+ * key going that way would ask it, and the first one that holds
49902
+ * (`preventNav`, or a `required` step still unanswered) is where one stops —
49903
+ * `?step=done` cannot open a confirmation screen for something nobody sent.
49904
+ * Whatever comes of it, the address is then rewritten with the area actually
49905
+ * shown: it says where one IS, never where one asked to be. A step the app
49906
+ * knows is already answered on a reload is told so by its own `required`
49907
+ * (`required={!alreadyFilled}`) — the container remembers nothing across a
49908
+ * load, and cannot.
49886
49909
  * Each way of travelling can be shut off, or narrowed to one axis: `true`
49887
49910
  * (every axis the map has), `false`, `"x"`, `"y"`, `"xy"`.
49888
49911
  *
@@ -49928,6 +49951,7 @@ const SlideContainer = ({
49928
49951
  commit = "now",
49929
49952
  loop,
49930
49953
  onLoop,
49954
+ urlParam,
49931
49955
  travelByKeyboard = true,
49932
49956
  travelByDrag = true,
49933
49957
  travelByScroll = "x",
@@ -50023,6 +50047,51 @@ const SlideContainer = ({
50023
50047
  const followerElementsRef = useRef([]);
50024
50048
  const currentFromCaller = currentSignal ? currentSignal.value : currentProp;
50025
50049
  const current = rollingArea ?? provisionalArea ?? currentFromCaller ?? currentAreaState;
50050
+ // The search param this container owns, when it owns one: the name it is
50051
+ // written under, and whether going somewhere is somewhere one CAME from.
50052
+ const urlParamName = typeof urlParam === "string" ? urlParam : urlParam?.name;
50053
+ const urlParamHistory = urlParam?.history || "replace";
50054
+ // What the address says right now. Read from the signal rather than from
50055
+ // window.location so that a traversal — the browser's own back and forward —
50056
+ // is a render like any other: the address moved, and the slides follow it.
50057
+ // Read only when there is a param to read, so a container that owns none is
50058
+ // not re-rendered by every navigation in the application.
50059
+ const areaInUrl = urlParamName ? new URL(documentUrlSignal.value).searchParams.get(urlParamName) : null;
50060
+ // The last thing this container knows the address said, whether it wrote it
50061
+ // or read it. What makes a two-way binding stop turning: the address is only
50062
+ // an INSTRUCTION when it changed by itself (a load, a traversal, a link), and
50063
+ // what this container put there is not news. Without it, a container ahead of
50064
+ // its own URL (commit="rest", where the picture arrives before the address is
50065
+ // written) would read its own lateness as an order to go back.
50066
+ const areaInUrlSeenRef = useRef(undefined);
50067
+ // Where the container stands, said in the address. `replace` amends the entry
50068
+ // one is on: a walk of four steps must not turn one back-press into four, and
50069
+ // the back arrow of a page keeps meaning "leave this page". `push` is for
50070
+ // slides that ARE places one came from — and even there a slide reached by
50071
+ // DRAGGING replaces, because browsing back and forth with a thumb is not a
50072
+ // trail one wants to walk home along.
50073
+ const writeUrlParam = (area, cause, {
50074
+ replace = false
50075
+ } = {}) => {
50076
+ if (!urlParamName) {
50077
+ return;
50078
+ }
50079
+ areaInUrlSeenRef.current = area;
50080
+ const urlObject = new URL(window.location.href);
50081
+ if (urlObject.searchParams.get(urlParamName) === area) {
50082
+ return;
50083
+ }
50084
+ urlObject.searchParams.set(urlParamName, area);
50085
+ navTo(urlObject.href, {
50086
+ replace: replace || urlParamHistory !== "push" || cause === "drag" ||
50087
+ // The address already says it: reading it back is not a place one went.
50088
+ cause === "url",
50089
+ // What travels is the box, not the page: the address is a label on where
50090
+ // the box stands, and a route transition would move the whole document
50091
+ // for a slide that has already travelled by itself.
50092
+ routeTransition: "none"
50093
+ });
50094
+ };
50026
50095
  const vertical = layout === "column";
50027
50096
  // What the map has, and what each way of asking is allowed to use of it.
50028
50097
  const mapAxes = travelAxesOf(layout);
@@ -50625,7 +50694,7 @@ const SlideContainer = ({
50625
50694
  }
50626
50695
  const leftArea = readArea(currentElement);
50627
50696
  setCurrentAreaState(area);
50628
- if (!onCurrentChange && !currentSignal) {
50697
+ if (!onCurrentChange && !currentSignal && !urlParamName) {
50629
50698
  return true;
50630
50699
  }
50631
50700
  // What asked for this, read off the interaction rather than carried down
@@ -50660,6 +50729,12 @@ const SlideContainer = ({
50660
50729
  // `onCurrentChange` is called, in that order, so a caller reading the signal
50661
50730
  // from inside its own handler reads where it now is.
50662
50731
  const tellCurrentChange = (area, detail, leftArea) => {
50732
+ // The address first, because it is the one thing that must never disagree
50733
+ // with the picture — and it is told here rather than by the caller so that
50734
+ // it is told about the travels that HAPPENED and about no others: the ones
50735
+ // a lock refused never reach this point, and one refused late is written
50736
+ // back below (see goBackToRefusedArea).
50737
+ writeUrlParam(area, detail.cause);
50663
50738
  if (currentSignal) {
50664
50739
  currentSignal.value = area;
50665
50740
  }
@@ -50693,8 +50768,62 @@ const SlideContainer = ({
50693
50768
  if (currentSignal) {
50694
50769
  currentSignal.value = leftArea;
50695
50770
  }
50771
+ // Written over rather than stacked on, whatever this container does with
50772
+ // the history otherwise: the entry the refused travel wrote is the one
50773
+ // being corrected, and a refusal is not a place one was.
50774
+ writeUrlParam(leftArea, "code", {
50775
+ replace: true
50776
+ });
50696
50777
  };
50697
50778
 
50779
+ // The address asks for a slide. Read on every render it changes on rather
50780
+ // than at mount alone, because the browser's own arrows are exactly that —
50781
+ // the address moving by itself — and a link from elsewhere in the application
50782
+ // is too. What follows is a travel like any other: the slides move, the
50783
+ // caller is told, and the address ends up saying where the box actually
50784
+ // stands.
50785
+ useLayoutEffect(() => {
50786
+ if (!urlParamName) {
50787
+ return;
50788
+ }
50789
+ if (areaInUrl === areaInUrlSeenRef.current) {
50790
+ // Not news: either nothing moved, or this container is reading back what
50791
+ // it wrote itself.
50792
+ return;
50793
+ }
50794
+ areaInUrlSeenRef.current = areaInUrl;
50795
+ if (areaInUrl === null) {
50796
+ // An address that says nothing is not an address saying "the first
50797
+ // slide": a container opened without the param opens where it would have
50798
+ // opened anyway, and the param appears the first time one travels.
50799
+ return;
50800
+ }
50801
+ // Where the box IS, read off the DOM: `current` is undefined until someone
50802
+ // names a slide, and the container standing on its first one is a fact only
50803
+ // the map knows (see the layout effect that paints it).
50804
+ const areaOnScreen = containerRef.current?.getAttribute("data-slide-current");
50805
+ if (!areaOnScreen || areaInUrl === areaOnScreen) {
50806
+ return;
50807
+ }
50808
+ const reached = reachableTowards(areaOnScreen, areaInUrl);
50809
+ if (reached && reached !== areaOnScreen) {
50810
+ // Told rather than asked: the walk above has already put every lock on
50811
+ // the way the question goToArea would have put the first one, and this
50812
+ // travel has no interaction behind it to hand a focus to — the slide
50813
+ // arriving is handed the keyboard by the layout effect, the way it is for
50814
+ // any travel nobody pressed anything for.
50815
+ setCurrentAreaState(reached);
50816
+ tellCurrentChange(reached, {
50817
+ cause: "url"
50818
+ }, areaOnScreen);
50819
+ return;
50820
+ }
50821
+ // The param names nowhere this map knows, or somewhere the walk is not
50822
+ // allowed to reach: the address is put back on the slide one is actually
50823
+ // looking at, rather than left saying one is somewhere one is not.
50824
+ writeUrlParam(areaOnScreen, "url");
50825
+ }, [urlParamName, areaInUrl]);
50826
+
50698
50827
  // The press kept during a roll, taken once the window rests and the travel is
50699
50828
  // given back (noTravel off): by direction when there was one, so it is read
50700
50829
  // against the map as it is NOW — the content moved one step under the window
@@ -50788,20 +50917,25 @@ const SlideContainer = ({
50788
50917
  };
50789
50918
 
50790
50919
  /**
50920
+ * @param {number} dx
50921
+ * @param {number} dy
50922
+ * @param {string} [fromArea] - where to step from. The slide on screen when
50923
+ * nothing says otherwise; named only by a walk that is not standing there
50924
+ * (see reachableTowards).
50791
50925
  * @returns {string|undefined} the area one step that way, if there is one.
50792
50926
  * Nothing there means the direction is simply not offered — no wrapping, no
50793
50927
  * nearest-match: a map is read as a map, and a move landing nowhere would
50794
50928
  * break that reading. Walks over its own cells first, so a spanning area
50795
50929
  * leaves by its far edge rather than onto itself.
50796
50930
  */
50797
- const areaTowards = (dx, dy) => {
50931
+ const areaTowards = (dx, dy, fromArea) => {
50798
50932
  const {
50799
50933
  slideElements,
50800
50934
  areaAt,
50801
50935
  placeOf
50802
50936
  } = readMap();
50803
50937
  const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
50804
- const currentArea = readArea(currentElement);
50938
+ const currentArea = fromArea ?? readArea(currentElement);
50805
50939
  let {
50806
50940
  x,
50807
50941
  y
@@ -50860,6 +50994,64 @@ const SlideContainer = ({
50860
50994
  const moveNext = (event, options) => vertical ? move(0, 1, event, options) || move(1, 0, event, options) : move(1, 0, event, options) || move(0, 1, event, options);
50861
50995
  const movePrevious = (event, options) => vertical ? move(0, -1, event, options) || move(-1, 0, event, options) : move(-1, 0, event, options) || move(0, -1, event, options);
50862
50996
 
50997
+ /**
50998
+ * How far the map lets one get towards an area, walking from where the
50999
+ * container stands.
51000
+ *
51001
+ * What the ADDRESS asks for goes through here rather than straight to
51002
+ * goToArea: a URL comes from OUTSIDE the walk — typed, shared, kept from a
51003
+ * session that has moved on — so every slide on the way is asked to let go,
51004
+ * exactly as a hand or a key going that way would ask it, and the first one
51005
+ * that holds is where one stops. Jumped to instead, an address would open the
51006
+ * one screen the walk itself cannot reach: the confirmation of something
51007
+ * nobody sent. Stopping short is not a failure either — a wizard reopens as
51008
+ * far along as it is allowed to, which is where the reader left off.
51009
+ *
51010
+ * @returns {string|undefined} the area one ends up on, undefined for a name
51011
+ * this map does not know.
51012
+ */
51013
+ const reachableTowards = (fromArea, targetArea) => {
51014
+ const {
51015
+ slideElements,
51016
+ placeOf
51017
+ } = readMap();
51018
+ const target = placeOf.get(targetArea);
51019
+ if (!target || !placeOf.has(fromArea)) {
51020
+ return undefined;
51021
+ }
51022
+ if (loop) {
51023
+ // A window has no walls to walk into: its slides are one endless line,
51024
+ // and what they show is whoever owns the content to place.
51025
+ return targetArea;
51026
+ }
51027
+ const holds = (area, forward) => {
51028
+ const slideElement = slideElements.find(slideElement => readArea(slideElement) === area);
51029
+ return slideElement?.hasAttribute(forward ? "data-prevent-nav-next" : "data-prevent-nav-previous");
51030
+ };
51031
+ let area = fromArea;
51032
+ // A walk crosses each slide at most once — beyond that a map is reading
51033
+ // itself in circles.
51034
+ let stepsLeft = slideElements.length;
51035
+ while (area !== targetArea && stepsLeft--) {
51036
+ const place = placeOf.get(area);
51037
+ // Along the row first, then down: the order a map is read in.
51038
+ const dx = Math.sign(target.x - place.x);
51039
+ const dy = dx ? 0 : Math.sign(target.y - place.y);
51040
+ if (!dx && !dy) {
51041
+ break;
51042
+ }
51043
+ if (holds(area, dx > 0 || dy > 0)) {
51044
+ break;
51045
+ }
51046
+ const next = areaTowards(dx, dy, area);
51047
+ if (!next) {
51048
+ break;
51049
+ }
51050
+ area = next;
51051
+ }
51052
+ return area;
51053
+ };
51054
+
50863
51055
  // Where the track is right now, as the gesture left it: the resting place of
50864
51056
  // the slide being dragged, plus what the pointer has pulled since.
50865
51057
  const paintDrag = () => {
@@ -51113,8 +51305,8 @@ const SlideContainer = ({
51113
51305
  sign,
51114
51306
  target
51115
51307
  }) => {
51116
- const areaBack = axis === "x" ? areaTowards(-1, 0) : areaTowards(0, -1);
51117
- const areaOn = axis === "x" ? areaTowards(1, 0) : areaTowards(0, 1);
51308
+ let areaBack = axis === "x" ? areaTowards(-1, 0) : areaTowards(0, -1);
51309
+ let areaOn = axis === "x" ? areaTowards(1, 0) : areaTowards(0, 1);
51118
51310
  // Everything positional is read HERE rather than when the pointer
51119
51311
  // landed: the travel that was playing then may have arrived since, and
51120
51312
  // it is what the slides are doing at the moment the gesture takes them
@@ -51125,6 +51317,24 @@ const SlideContainer = ({
51125
51317
  placeOf
51126
51318
  } = readMap();
51127
51319
  const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
51320
+ // The hold goToArea reads at the release, read again HERE, off the same
51321
+ // slide and the same attribute: a slide that will refuse the arrival
51322
+ // must not offer the journey. A locked direction simply has nowhere to
51323
+ // go for the length of this gesture — the one case the gesture already
51324
+ // knows, being the last slide of a walk. The hand then gets the wall it
51325
+ // can lean on and never walk through (see drag_to_travel), the slide
51326
+ // behind it stays offstage instead of being read on the way, and the
51327
+ // release has nothing left to refuse.
51328
+ // Nothing here about `released` (--navi-done): that is one particular
51329
+ // departure letting go, decided as it happens, and a gesture armed
51330
+ // before it has no such thing to read — the attribute as rendered is
51331
+ // what the hand is answered from.
51332
+ if (currentElement?.hasAttribute("data-prevent-nav-previous")) {
51333
+ areaBack = undefined;
51334
+ }
51335
+ if (currentElement?.hasAttribute("data-prevent-nav-next")) {
51336
+ areaOn = undefined;
51337
+ }
51128
51338
  const box = track.getBoundingClientRect();
51129
51339
  if (!areaBack && !areaOn || !currentElement || !box.width || !box.height ||
51130
51340
  // Something else with a better claim on the gesture: a scroller