@jsenv/navi 0.29.165 → 0.29.166

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.
@@ -331,9 +331,14 @@ consistency across the app, not from any single call site.
331
331
  restored on reload without a route and without an entry per step: a search
332
332
  param signal handed to a `SlideContainer`, walked to rather than jumped to,
333
333
  and `history: "push"` for the states whose values ARE places (with the write
334
- that says otherwise, `signal.set(v, { history })`). Read it before writing any
335
- routing code — the position of the user belongs in the URL by default, and
336
- that decision is not retrofittable.
334
+ that says otherwise, `signal.set(v, { history })`). It also holds the rule for
335
+ an OVERLAY — something opened from every screen and drawn over whatever the
336
+ reader was on: the URL may say what is drawn over the screen, it must never
337
+ name a place the reader is not at, which is why `/me/settings` is wrong for it
338
+ and `/places?settings` is right (the layer's address contains the address of
339
+ what it covers, so closing is exact even from a shared link). Read it before
340
+ writing any routing code — the position of the user belongs in the URL by
341
+ default, and that decision is not retrofittable.
337
342
  - Source code and demos on GitHub:
338
343
  https://github.com/jsenv/core/tree/main/packages/frontend/navi — where to go
339
344
  when the built export's JSDoc doesn't answer the question (see "Where the
@@ -19,6 +19,14 @@ linking to them, and turning them into tabs.
19
19
  - [Tabs that are not routes](#tabs-that-are-not-routes)
20
20
  - [A `SlideContainer` in the URL: a position that is not a place one came from](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
21
21
  - [A state whose values ARE places: `history: "push"`](#a-state-whose-values-are-places-history-push)
22
+ - [A layer over the screen: what its address may say](#a-layer-over-the-screen-what-its-address-may-say)
23
+ - [Why a layer is a routing question at all](#why-a-layer-is-a-routing-question-at-all)
24
+ - [`/me/settings` names a place the reader is not at](#mesettings-names-a-place-the-reader-is-not-at)
25
+ - [It is not "use a search param"](#it-is-not-use-a-search-param)
26
+ - [What the search param buys: the way back is in the address](#what-the-search-param-buys-the-way-back-is-in-the-address)
27
+ - [The wiring](#the-wiring)
28
+ - [Places inside the layer](#places-inside-the-layer)
29
+ - [What it costs](#what-it-costs)
22
30
 
23
31
  ## The rule that decides everything else: the position belongs in the URL
24
32
 
@@ -747,3 +755,162 @@ not one. What travels is the box, and the address is a label on where the box
747
755
  stands. A position several parts of the app must react to is still a route.
748
756
 
749
757
  Demo: [../src/layout/demos/8_slide_container_demo.html](../src/layout/demos/8_slide_container_demo.html).
758
+
759
+ ## A layer over the screen: what its address may say
760
+
761
+ Some things are opened from everywhere and drawn OVER whatever the reader was
762
+ looking at: the settings behind a gear in the top bar, a photo, a help sheet.
763
+ Closing one puts the reader back exactly where they were — not on a page the app
764
+ picked, on the one they had. And the address has to hold it, for the same reasons
765
+ every other position does: a reload lands on it, a link opens on it, the back
766
+ button closes it.
767
+
768
+ ### Why a layer is a routing question at all
769
+
770
+ Layout should not decide routing. But routing decides a great deal of what is on
771
+ screen: which page is mounted, whose data loads, which bar entry lights up, which
772
+ movement plays between two screens. An address is not a label — it is read by
773
+ everything — so a layer, which is a pure fact of layout, cannot be given just any
774
+ address.
775
+
776
+ The rule that falls out of it, and the whole of this section:
777
+
778
+ > The URL may say what is drawn OVER the screen. It must never name a place the
779
+ > reader is not at.
780
+
781
+ ### `/me/settings` names a place the reader is not at
782
+
783
+ Settings opened from anywhere and drawn over anything are not inside `/me`, even
784
+ when they conceptually belong to "me". Written `/me/settings`, the router
785
+ believes otherwise, and everything that reads the router inherits the belief:
786
+
787
+ - the bar entry for the "me" section lights up while the settings cover a game;
788
+ - `/me` matches, so the screen the reader was on is REPLACED. The covering is a
789
+ fiction that lasts as long as the animation, and the page beneath is gone;
790
+ - a route transition plays a crossing between two pages that never crossed, and
791
+ its back half plays on every way out — the sheet lifts to reveal a page that
792
+ was never underneath;
793
+ - and it is wrong on the way out too: closing does not return to `/me`, it
794
+ returns to wherever the reader was, which the address never said.
795
+
796
+ These do not get fixed one at a time. `currentExcept` stops one link from
797
+ claiming "you are here"; a `"none"` relation silences one crossing; a hand-rolled
798
+ `active` replaces one reading. Each patches a reader of the lie without removing
799
+ it, and the next reader arrives quietly wrong.
800
+
801
+ ### It is not "use a search param"
802
+
803
+ `/settings` at the top level does not claim to be inside `/me` — it is honest
804
+ about not being there, and every symptom above goes with it. What it still is,
805
+ though, is a PAGE: it names a screen, so the screen beneath is gone, and closing
806
+ has nowhere written to return to. `whatever/settings` is the same answer in a
807
+ longer word.
808
+
809
+ So the rule is not about search params. It is about not claiming a position. A
810
+ path can be honest; what a path cannot do is carry what is underneath.
811
+
812
+ ### What the search param buys: the way back is in the address
813
+
814
+ `/places?settings` says two things at once — which screen the reader is on, and
815
+ what is drawn over it. That is what makes closing exact from a cold start: a
816
+ reload, a link someone sent, a new tab. The layer's address CONTAINS the address
817
+ of what it covers, so "close" needs no memory at all.
818
+
819
+ `/settings` has to remember instead. In practice the browser often does — after a
820
+ reload the session's history is still there, so a back-press lands somewhere —
821
+ but that is the BROWSER's memory of this tab, not the address. A link someone
822
+ else opens has none of it, and neither does a bookmark. Only the address travels,
823
+ which is what makes the param shape the right model rather than the convenient
824
+ one.
825
+
826
+ ### The wiring
827
+
828
+ ```js
829
+ const settingsSignal = stateSignal(false, {
830
+ id: "settings",
831
+ type: "boolean",
832
+ weak: true,
833
+ history: "push",
834
+ });
835
+ export const ROOT_ROUTE = route("/", {
836
+ searchParams: { settings: settingsSignal },
837
+ });
838
+ ```
839
+
840
+ ```jsx
841
+ <Button command="--navi-toggle" commandFor="settings_panel">⚙</Button>
842
+ …
843
+ <SidePanel id="settings_panel" signal={settingsSignal} side="top" expandY />
844
+ ```
845
+
846
+ Every piece of it answers something:
847
+
848
+ - **declared on the ROOT route**, so the layer opens over every screen. Declared
849
+ on one route it would only exist there, and a door in the furniture is on every
850
+ screen ([Search params](#search-params));
851
+ - **`weak`**, so a link built to a page never inherits a layer that happens to be
852
+ open. A layer qualifies one visit; it is not part of anyone's address;
853
+ - **`history: "push"`**, so the layer is a place one came from and the back button
854
+ closes it. The closing itself is never an entry of its own — see
855
+ [popup_open.md](./popup_open.md), which owns what a `signal` bound to a URL
856
+ writes on open, on close, and on cancel;
857
+ - **the panel is rendered outside the page area**, next to the router rather than
858
+ inside it. It is drawn over the pages and the fixed bars alike, and it stays
859
+ mounted while closed so it has somewhere to animate from;
860
+ - **`expandY`**, because a sheet is content-tall by default and a layer covers
861
+ the screen. It is also what any tab row inside it must NOT be given: `expand`
862
+ is both axes, and in the panel's own column that makes the row eat the height
863
+ and push everything below it off screen;
864
+ - **no `defineRouteTransition` for it.** Nothing crosses: the page beneath does
865
+ not change, so there is no pair of pages and no movement between them. A layer
866
+ has an entrance of its own, and route transitions are for pages replacing pages
867
+ ([route_transitions.md](./route_transitions.md)).
868
+
869
+ ### Places inside the layer
870
+
871
+ A layer big enough to have tabs holds them the way a wizard does — a search param
872
+ of its own, `oneOf` its pages, replacing rather than pushing, so the back button
873
+ closes the layer instead of stepping back one tab. Everything in
874
+ [A `SlideContainer` in the URL](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
875
+ applies unchanged, and a layer is exactly the case that section names for
876
+ `SlideContainer` over `RouteTravel`: a popup lives in the browser's top layer,
877
+ where two pages cannot stand side by side.
878
+
879
+ ```js
880
+ const settingsTabSignal = stateSignal("account", {
881
+ id: "settings_tab",
882
+ oneOf: ["account", "alerts", "advanced"],
883
+ weak: true,
884
+ });
885
+ ```
886
+
887
+ The address then grows and shrinks with what the reader does: `/me` closed,
888
+ `/me?settings` open on the tab it opens on, `/me?settings&settings_tab=alerts`
889
+ one tab further, and back to `/me?settings` returning to the first.
890
+
891
+ Two params, because they answer two questions — is the layer there, and which of
892
+ its pages is shown. One param whose value is "which page of the layer, or none"
893
+ (`?settings=alerts`) is arguably the shape the address wants, and it is not
894
+ expressible today: a popup says its open state as a boolean, so it would
895
+ overwrite the tab on every open.
896
+
897
+ ### What it costs
898
+
899
+ This is a compromise, and it is worth stating rather than discovering:
900
+
901
+ - **the page beneath stays mounted and alive under an opaque layer.** Its actions
902
+ keep running, refreshing, retrying, for something nobody can see. Nothing tells
903
+ a route "you are covered", and nothing should be inferred from a layer being
904
+ open — the reader is still on that page, and will be back on it in a moment;
905
+ - **the router knows nothing about layers.** `<Route>` renders pages; the layer is
906
+ drawn beside them by the application. The address is honest about it, but "over"
907
+ is not a routing concept, and no `<Route>` shape expresses it;
908
+ - **the app really is on the page underneath**, which is right for `aria-current`,
909
+ for the bar, for a link built while the layer is open — and is exactly wrong for
910
+ anything wanting "the settings are what is current". If something needs to
911
+ answer that, it reads the layer's own state, not the router.
912
+
913
+ Demo:
914
+ [../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html](../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html)
915
+ — a bar holding both doors side by side: notifications, a page that replaces the
916
+ screen with a route transition, and settings, a layer that covers it.
@@ -533,7 +533,10 @@ is nav state by construction.
533
533
 
534
534
  The two meet when the signal IS a route's: a search-param `stateSignal` given to
535
535
  `signal` puts the open state in the address itself, where a link can point at
536
- it.
536
+ it. That is also the shape a popup takes when it is a LAYER over the screen —
537
+ settings opened from every screen and closed back onto the one the reader was on
538
+ — and what the address may and may not claim there is a decision of its own:
539
+ [navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say).
537
540
 
538
541
  ### `open`, and what it costs
539
542
 
@@ -65,6 +65,14 @@ happens to be everywhere. Write it for a page whose door really is furniture; a
65
65
  page reached from a screen has that screen to be paired with, and the pair says
66
66
  more.
67
67
 
68
+ **Such a page is still a PAGE**, and that is the question to settle before
69
+ writing the relation: it takes the screen's place, and the reader comes back to
70
+ whatever the router puts there — not necessarily to what they were on. A door in
71
+ the furniture whose destination must be drawn OVER the screen, and must give it
72
+ back exactly on closing, is not a page at all: it is a layer, its address says so
73
+ differently, and no relation is written for it. See
74
+ [navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say).
75
+
68
76
  ## Choosing a movement
69
77
 
70
78
  - **`slide-x`** — going INTO something: a list item opened, a card followed, a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.165",
3
+ "version": "0.29.166",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {