Skip to content

Commit 18ad212

Browse files
committed
work
1 parent 66d67f7 commit 18ad212

9 files changed

Lines changed: 845 additions & 117 deletions

File tree

packages/frontend/navi/dist/jsenv_navi.js

Lines changed: 574 additions & 94 deletions
Large diffs are not rendered by default.

packages/frontend/navi/dist/jsenv_navi.js.map

Lines changed: 12 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/frontend/navi/docs/AI_INSTRUCTIONS.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,14 @@ consistency across the app, not from any single call site.
331331
restored on reload without a route and without an entry per step: a search
332332
param signal handed to a `SlideContainer`, walked to rather than jumped to,
333333
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.
337342
- Source code and demos on GitHub:
338343
https://github.com/jsenv/core/tree/main/packages/frontend/navi — where to go
339344
when the built export's JSDoc doesn't answer the question (see "Where the

packages/frontend/navi/docs/navigation.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ linking to them, and turning them into tabs.
1919
- [Tabs that are not routes](#tabs-that-are-not-routes)
2020
- [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)
2121
- [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)
2230

2331
## The rule that decides everything else: the position belongs in the URL
2432

@@ -747,3 +755,162 @@ not one. What travels is the box, and the address is a label on where the box
747755
stands. A position several parts of the app must react to is still a route.
748756

749757
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.

packages/frontend/navi/docs/popup_open.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,10 @@ is nav state by construction.
533533

534534
The two meet when the signal IS a route's: a search-param `stateSignal` given to
535535
`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).
537540

538541
### `open`, and what it costs
539542

packages/frontend/navi/docs/route_transitions.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ happens to be everywhere. Write it for a page whose door really is furniture; a
6565
page reached from a screen has that screen to be paired with, and the pair says
6666
more.
6767

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+
6876
## Choosing a movement
6977

7078
- **`slide-x`** — going INTO something: a list item opened, a card followed, a

packages/frontend/navi/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@jsenv/navi",
3-
"version": "0.29.165",
3+
"version": "0.29.166",
44
"type": "module",
55
"description": "Library of components including navigation to create frontend applications",
66
"repository": {

packages/frontend/navi/src/layout/side_panel.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,11 @@ export const SidePanel = ({
278278
return (
279279
<Popup
280280
mode={mode}
281-
open={open}
281+
// Spread rather than written: the collision warning between `signal` and
282+
// `open` asks whether the prop is THERE, not what it holds, so a panel
283+
// handed a signal and nothing else would be told off for an `open` only
284+
// this line ever put on it.
285+
{...(open === undefined ? null : { open })}
282286
signal={signal}
283287
defaultOpen={defaultOpen}
284288
onClose={onClose}

packages/frontend/navi/src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@
5959
background: #f3ecfb;
6060
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
6161
}
62+
.overlay_tabs {
63+
background: #f3ecfb;
64+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
65+
}
6266
.overlay_body {
6367
padding: 16px;
6468
background: #f3ecfb;
@@ -78,6 +82,8 @@
7882
Route,
7983
RouteTransitionArea,
8084
SidePanel,
85+
Slide,
86+
SlideContainer,
8187
defineRouteTransition,
8288
route,
8389
setupRoutes,
@@ -100,8 +106,20 @@
100106
weak: true,
101107
history: "push",
102108
});
109+
// Le calque a des places à lui. Elles amendent l'entrée d'histoire au
110+
// lieu d'en empiler une: le retour ferme les réglages, il ne recule pas
111+
// d'un onglet. Et le param disparaît de l'adresse tant que l'onglet est
112+
// celui d'origine — /me?reglages, puis /me?reglages&reglages_onglet=alertes.
113+
const reglagesOngletSignal = stateSignal("compte", {
114+
id: "reglages_onglet",
115+
oneOf: ["compte", "alertes", "avance"],
116+
weak: true,
117+
});
103118
const ROOT_ROUTE = route("/", {
104-
searchParams: { reglages: reglagesSignal },
119+
searchParams: {
120+
reglages: reglagesSignal,
121+
reglages_onglet: reglagesOngletSignal,
122+
},
105123
});
106124
setupRoutes([
107125
ME_EDIT_ROUTE,
@@ -279,12 +297,14 @@ <h1 className="page_title">{title}</h1>
279297

280298
// La croix tombe là où était la roue: le panneau prend tout l'écran,
281299
// barres comprises, et refait la ligne du haut à son compte.
300+
const REGLAGES_SLIDES_ID = "reglages_slides";
282301
const ReglagesOverlay = () => (
283302
<SidePanel
284303
id={REGLAGES_PANEL_ID}
285304
signal={reglagesSignal}
286305
side="top"
287-
height="100%"
306+
expandY
307+
style={{ background: "#f3ecfb" }}
288308
animation
289309
data-testid="reglages_panel"
290310
>
@@ -299,16 +319,53 @@ <h1 className="page_title">{title}</h1>
299319
300320
</Button>
301321
</SidePanel.Head>
302-
<div className="overlay_body">
303-
{Array.from({ length: 12 }, (_, index) => (
304-
<div key={index} className="page_row">
305-
Réglage {index + 1}
306-
</div>
307-
))}
308-
</div>
322+
{/* Des onglets, pas des routes: un calque est dans la couche du
323+
dessus, où deux pages ne peuvent pas tenir côte à côte. */}
324+
<Nav
325+
className="overlay_tabs"
326+
slideContainer={REGLAGES_SLIDES_ID}
327+
expandX
328+
currentIndicator
329+
>
330+
<Link slide="compte" variant="tab" padding="m">
331+
Compte
332+
</Link>
333+
<Link slide="alertes" variant="tab" padding="m">
334+
Alertes
335+
</Link>
336+
<Link slide="avance" variant="tab" padding="m">
337+
Avancé
338+
</Link>
339+
</Nav>
340+
<SlideContainer
341+
id={REGLAGES_SLIDES_ID}
342+
signal={reglagesOngletSignal}
343+
expandY
344+
data-testid="reglages_slides"
345+
>
346+
<Slide area="compte">
347+
<ReglagesSection title="Compte" rows={6} />
348+
</Slide>
349+
<Slide area="alertes">
350+
<ReglagesSection title="Alertes" rows={4} />
351+
</Slide>
352+
<Slide area="avance">
353+
<ReglagesSection title="Avancé" rows={8} />
354+
</Slide>
355+
</SlideContainer>
309356
</SidePanel>
310357
);
311358

359+
const ReglagesSection = ({ title, rows }) => (
360+
<div className="overlay_body">
361+
{Array.from({ length: rows }, (_, index) => (
362+
<div key={index} className="page_row">
363+
{title} — réglage {index + 1}
364+
</div>
365+
))}
366+
</div>
367+
);
368+
312369
const AppIndex = () => {
313370
useLayoutEffect(() => {
314371
ME_ROUTE.redirectTo();

0 commit comments

Comments
 (0)