@jsenv/navi 0.29.418 → 0.29.420

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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/dev/jsenv_navi.js +584 -196
  3. package/dist/dev/jsenv_navi.js.map +35 -35
  4. package/dist/jsenv_navi.js +563 -187
  5. package/dist/jsenv_navi.js.map +35 -35
  6. package/docs/AI_INSTRUCTIONS.md +177 -261
  7. package/docs/actions.md +164 -251
  8. package/docs/autofocus.md +66 -83
  9. package/docs/control_object.md +117 -214
  10. package/docs/control_value.md +121 -185
  11. package/docs/create_and_edit.md +169 -254
  12. package/docs/css_architecture.md +196 -260
  13. package/docs/data_states.md +39 -43
  14. package/docs/dialog_shape.md +91 -134
  15. package/docs/drag_interactions.md +115 -174
  16. package/docs/drag_to_travel.md +10 -9
  17. package/docs/dynamic_import.md +119 -136
  18. package/docs/error_handling.md +75 -127
  19. package/docs/field_validation.md +49 -89
  20. package/docs/form_changed.md +66 -81
  21. package/docs/group.md +141 -0
  22. package/docs/i18n.md +31 -76
  23. package/docs/interactions.md +6 -4
  24. package/docs/list_action.md +36 -51
  25. package/docs/list_refresh.md +73 -115
  26. package/docs/mobile_touch.md +56 -62
  27. package/docs/navigation.md +377 -723
  28. package/docs/network_policy.md +61 -75
  29. package/docs/pan_zoom.md +62 -103
  30. package/docs/popup_backdrop.md +128 -168
  31. package/docs/popup_lift.md +179 -268
  32. package/docs/popup_open.md +380 -651
  33. package/docs/resource.md +228 -408
  34. package/docs/route_transitions.md +333 -482
  35. package/docs/safe_area.md +102 -89
  36. package/docs/scroll.md +175 -335
  37. package/docs/state_binding.md +2 -1
  38. package/docs/testid.md +18 -23
  39. package/docs/typography.md +111 -141
  40. package/docs/view_transitions.md +106 -104
  41. package/docs/z_index.md +61 -109
  42. package/package.json +2 -2
  43. package/docs/badge_list.md +0 -83
  44. package/docs/control_group.md +0 -168
  45. package/docs/mobile_layout_pitfalls.md +0 -145
@@ -22,9 +22,7 @@ linking to them, and turning them into tabs.
22
22
  - [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)
23
23
  - [A state whose values ARE places: `history: "push"`](#a-state-whose-values-are-places-history-push)
24
24
  - [A layer over the screen: what its address may say](#a-layer-over-the-screen-what-its-address-may-say)
25
- - [Why a layer is a routing question at all](#why-a-layer-is-a-routing-question-at-all)
26
25
  - [`/me/settings` names a place the reader is not at](#mesettings-names-a-place-the-reader-is-not-at)
27
- - [It is not "use a search param"](#it-is-not-use-a-search-param)
28
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)
29
27
  - [The wiring](#the-wiring)
30
28
  - [Places inside the layer](#places-inside-the-layer)
@@ -34,30 +32,23 @@ linking to them, and turning them into tabs.
34
32
 
35
33
  Where the user is — which section, which tab, which sub-page — is state. Put it
36
34
  in the URL unless there is a reason not to. What that buys, none of which can be
37
- retrofitted later:
38
-
39
- - the browser's back and forward buttons work, because each place is a history
40
- entry;
41
- - the place is shareable and bookmarkable — someone can send a link to exactly
42
- what they are looking at;
43
- - the place is **targetable**: anything, anywhere in the app, can send the user
44
- there with a `<Link route={…}>`, without knowing anything about the component
45
- that displays it;
46
- - a reload lands where the user was.
35
+ retrofitted later: back and forward work, each place being a history entry; the
36
+ place is shareable and bookmarkable; it is **targetable** — anything in the app
37
+ can send the user there with a `<Link route={…}>`, knowing nothing of the
38
+ component that displays it; and a reload lands where the user was.
47
39
 
48
40
  So the default shape of a tab row is routes: `<Nav>` + `<Link route>` +
49
- `<RouteTravel>`. `SlideContainer` is the exception, not the starting point — see
50
- [Tabs that are not routes](#tabs-that-are-not-routes) for the cases that
51
- genuinely are one, and
52
- [A `SlideContainer` in the URL](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
53
- for the middle answer: a position READ from the URL and restored on reload,
54
- without a route and without a history entry per step.
41
+ `<RouteTravel>`. `SlideContainer` is the exception, not the starting point (see
42
+ [Tabs that are not routes](#tabs-that-are-not-routes)), with a middle answer: a
43
+ position READ from the URL and restored on reload, without a history entry per
44
+ step
45
+ ([A `SlideContainer` in the URL](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)).
55
46
 
56
47
  ## Declaring routes
57
48
 
58
- Every route is created with `route()` and they are all declared to `setupRoutes()`
59
- in one call — the routing system resolves specificity and signal ownership across
60
- the whole set, so it has to see the whole set.
49
+ Every route is created with `route()` and all are declared to `setupRoutes()` in
50
+ one call: the routing system resolves specificity and signal ownership across the
51
+ whole set, so it has to see the whole set.
61
52
 
62
53
  ```js
63
54
  // routes.js
@@ -71,50 +62,23 @@ setupRoutes([HOME_ROUTE, GAMES_ROUTE, GAME_ROUTE]);
71
62
  ```
72
63
 
73
64
  Named exports from one module, on purpose: the file is the map of the
74
- application, and an import line says which places a component deals with.
75
- Routes are plain objects usable outside of any component — `route.buildUrl()`,
76
- `route.navTo()`, `route.redirectTo()`, `route.matching` — which is why they are
77
- declared apart from the JSX that renders them.
65
+ application, and an import line says which places a component deals with. Routes
66
+ are plain objects usable outside any component (`buildUrl()`, `navTo()`,
67
+ `redirectTo()`, `matching`), which is why they are declared apart from the JSX.
78
68
 
79
69
  ### A document that is not at the root: `setBaseUrl`
80
70
 
81
71
  A second document in the same site — an admin panel at `/admin/admin.html`
82
- alongside the app at `/` — owns the addresses below its own directory, and its
83
- routes are written as if that directory were the root:
84
-
85
- ```js
86
- // admin/routes.js
87
- import { route, setBaseUrl, setupRoutes } from "@jsenv/navi";
88
-
89
- setBaseUrl("/admin/admin.html");
90
-
91
- export const PLACES_ROUTE = route("/places");
92
- export const PLACE_ROUTE = route("/places/:placeId");
93
-
94
- setupRoutes([PLACES_ROUTE, PLACE_ROUTE]);
95
- ```
96
-
97
- `/admin/places/42` matches `PLACE_ROUTE`, and `buildUrl` puts the prefix back —
98
- nothing else in the document ever spells `/admin` again, so the panel can be
99
- moved by changing this one line.
100
-
101
- Two things about it:
102
-
103
- - **it belongs in the routes module, above `setupRoutes()`** — the base url
104
- is read whenever an address is matched or built, and the first match happens
105
- right there, as the routes are set up against the current address. In the
106
- entry point it would be too late: the import of the routes module runs
107
- first, `setupRoutes()` with it, and an import sorter would put the call after
108
- anyway;
109
- - **the server has to answer the document for every address below it.** jsenv's
110
- dev server already does: an address it finds no file for is answered with the
111
- closest html file above it, `<dirname>.html` included, so `/admin/places/42`
112
- is served `/admin/admin.html`. Any other host needs the same rule.
72
+ beside the app at `/` — writes its routes as if its directory were the root,
73
+ after `setBaseUrl("/admin/admin.html")` in its routes module, above
74
+ `setupRoutes()` (see its JSDoc). **The server has to answer the document for
75
+ every address below it.** jsenv's dev server already does — an address it finds
76
+ no file for is answered with the closest html file above it, `<dirname>.html`
77
+ included, so `/admin/places/42` is served `/admin/admin.html` — and any other
78
+ host needs the same rule.
113
79
 
114
80
  ### A section is allowed to be a route of its own
115
81
 
116
- This is the most commonly missed point.
117
-
118
82
  When a segment can take a **finite, known set of values**, declare one literal
119
83
  route per value rather than one parameterized route you pass params to:
120
84
 
@@ -125,49 +89,24 @@ export const CANDIDATE_GAMES_ROUTE = route("/games/candidates");
125
89
  export const FINISHED_GAMES_ROUTE = route("/games/finished");
126
90
  ```
127
91
 
128
- A literal route may sit alongside a parameterized one on the same segment
129
- (`/games/:section` and `/games/my_games`). Both match, and the literal one is
130
- taken as the more specific — so declaring the sections costs nothing and takes
131
- nothing away.
92
+ The routes are then listable — `routes.js` shows the places the application has,
93
+ where `/games/:section` shows one and hides three; call sites need no
94
+ `routeParams`, so a wrong section is a missing import rather than a string nobody
95
+ checks; and each section can carry search params of its own. Params stay for what
96
+ the code cannot enumerate (an id, a date), and for a finite set handled
97
+ **uniformly** — a row built by `.map()` over the sections — bound to a signal for
98
+ validation and a default.
132
99
 
133
- Why prefer it:
134
-
135
- - **The routes are listable.** `routes.js` shows the places the application has.
136
- A single `/games/:section` shows one place and hides three.
137
- - **No `routeParams` at the call sites.** `<Link route={MY_GAMES_ROUTE}>` instead
138
- of `<Link route={GAMES_ROUTE} routeParams={{ section: "my_games" }}>`, and the
139
- same for `<Route>`. A wrong section is then a missing import rather than a
140
- string nobody checks.
141
- - **Each section can carry its own search params.** `/games/finished` may have a
142
- `sort` the other sections have no business knowing about.
143
-
144
- Params stay for what is genuinely dynamic — a value the code cannot enumerate:
145
-
146
- ```js
147
- export const GAME_ROUTE = route("/games/:gameId"); // ✅ an id
148
- export const DAY_ROUTE = route("/planning/:day"); // ✅ any date
149
- ```
150
-
151
- A parameterized route also remains right for a finite set that must be handled
152
- **uniformly** — a row built by `.map()` over a list of sections, where writing
153
- one branch per section would be writing the same branch N times. Bind the param
154
- to a signal to get validation and a default:
155
-
156
- ```js
157
- import { stateSignal } from "@jsenv/navi";
158
-
159
- const sectionSignal = stateSignal("to_come", {
160
- id: "games_section",
161
- oneOf: ["candidate", "to_come", "done"],
162
- autoFix: true,
163
- });
164
- export const GAMES_SECTION_ROUTE = route(`/games/:section=${sectionSignal}`);
165
- ```
100
+ A literal and a parameterized route can sit on the same segment, and both then
101
+ match: `/games/my_games` is also a `/games/:section`, whose signal is written
102
+ `"my_games"`. Where both exist,
103
+ the literal branch goes first (see [Rendering routes](#rendering-routes)) and the
104
+ param is constrained to the values it really takes (see
105
+ [Which values a param accepts](#which-values-a-param-accepts)).
166
106
 
167
107
  #### Declaring the sections is what makes them places
168
108
 
169
- Both forms can be written together, and they say different things — which is
170
- why declaring the literals is not decoration:
109
+ Both forms written together say different things:
171
110
 
172
111
  ```js
173
112
  export const MY_GAMES_ROUTE = route(`/games/me/:section=${sectionSignal}`);
@@ -176,50 +115,29 @@ export const MY_GAMES_CANDIDATE_ROUTE = route("/games/me/candidate");
176
115
  export const MY_GAMES_DONE_ROUTE = route("/games/me/done");
177
116
  ```
178
117
 
179
- Standing on `/games/me/done`:
118
+ On `/games/me/done`, `MY_GAMES_ROUTE.buildUrl()` gives `/games/me/done`: the
119
+ parameterized route reads its signal, so a link to "my games" from the bottom bar
120
+ **reopens the section you were looking at**. `MY_GAMES_TO_COME_ROUTE.buildUrl()`
121
+ gives `/games/me`, always: a tab must point at its own section, never at the one
122
+ already open — a tab pointing at the current page cannot be clicked. The default
123
+ section has no segment, so its literal route is the **parent** of the
124
+ parameterized one, and still does not inherit the param.
180
125
 
181
- - `MY_GAMES_ROUTE.buildUrl()` → `/games/me/done`. The parameterized route reads
182
- its signal, so a link to "my games" from the bottom bar **reopens the section
183
- you were looking at**. That is what the signal is for, and `persists` makes it
184
- survive the night;
185
- - `MY_GAMES_TO_COME_ROUTE.buildUrl()` → `/games/me`, always. A tab must point at
186
- its own section, never at the one already open — a tab pointing at the current
187
- page is a tab that cannot be clicked.
188
-
189
- The default section is the delicate one: it has no segment of its own, so its
190
- literal route is the **parent** of the parameterized one. It still means the
191
- default section and does not inherit the param.
192
-
193
- What tells navi these values name pages rather than qualify one is precisely
194
- that the literal routes exist. Where no literal is declared, the value stays a
195
- qualifier and an ancestor url keeps it:
126
+ That the literals exist is what tells navi these values name pages. Where none is
127
+ declared, the value stays a qualifier, carried by an ancestor url:
196
128
 
197
129
  ```js
198
- const tabSignal = stateSignal("general", { id: "settings_tab" });
199
130
  export const ADMIN_ROUTE = route(`/admin/:section=${sectionSignal}/`);
200
131
  export const ADMIN_SETTINGS_ROUTE = route(`/admin/settings/:tab=${tabSignal}`);
201
132
  // nobody declared /admin/settings/advanced, so on tab "advanced":
202
133
  // ADMIN_ROUTE.buildUrl() → /admin/settings/advanced — "admin, where you left it"
203
134
  ```
204
135
 
205
- So the rule is the one you would want: name a section and it becomes a place;
206
- leave it unnamed and it stays a setting carried along.
207
-
208
136
  ### Which values a param accepts
209
137
 
210
138
  A param says which segments it accepts, and a segment it declines is not a
211
139
  half-match to be sorted out later — the route simply does not match:
212
140
 
213
- ```js
214
- export const GAME_ROUTE = route(`/:gameId=${gameIdSignal}`, {
215
- params: { gameId: /^W-[A-Z0-9]{8}$/i },
216
- });
217
- ```
218
-
219
- A constraint is a regexp, the list of accepted values, or a `(value) => boolean`
220
- — the list is compared as strings, so it can be the very `oneOf` given to the
221
- signal bound to that param:
222
-
223
141
  ```js
224
142
  const SECTIONS = ["candidate", "to_come", "done"];
225
143
  const sectionSignal = stateSignal("to_come", {
@@ -229,27 +147,25 @@ const sectionSignal = stateSignal("to_come", {
229
147
  export const GAMES_SECTION_ROUTE = route(`/games/:section=${sectionSignal}`, {
230
148
  params: { section: SECTIONS },
231
149
  });
150
+ export const GAME_ROUTE = route(`/:gameId=${gameIdSignal}`, {
151
+ params: { gameId: /^W-[A-Z0-9]{8}$/i },
152
+ });
232
153
  ```
233
154
 
234
- This is what makes a param usable at the root, where it would otherwise swallow
235
- every single-segment address: `/cgu` and `/me` stay other routes' urls,
236
- `<Route fallback>` is reachable for `/whatever`, and no signal is written for a
237
- url this route has nothing to do with.
238
-
239
- A constrained param is also **required** — no segment is not one of the values
240
- it accepts — so `/:gameId` does not match `/`. The address with no segment is a
241
- route of its own, which is the shape you want anyway.
155
+ This is what lets a param sit at the root without swallowing every single-segment
156
+ address: `/cgu` stays another route's url, `<Route fallback>` is reachable, and
157
+ no signal is written for a url this route has nothing to do with. A constrained
158
+ param is also **required**: `/:gameId` does not match `/`. The accepted forms are
159
+ in `route()`'s JSDoc.
242
160
 
243
161
  #### Constrain the shape, never the existence
244
162
 
245
163
  A constraint answers one question: **is this segment addressed to this route?**
246
164
  It is decided on the url alone, before anything is written, so it can only be
247
165
  about shape — that a segment looks like a game code, not that the game exists.
248
-
249
- Whether the value is any good is a different question, asked later and answered
250
- by different things: the signal's own validation (`oneOf`, `autoFix`) and the
251
- route action's data. That question belongs to a route that **did** match, with a
252
- page free to repair itself, show a not-found screen, offer a way out:
166
+ Whether the value is any good is asked later, by the signal's validation and the
167
+ route action's data, on a route that **did** match, with a page free to show a
168
+ not-found screen and offer a way out:
253
169
 
254
170
  ```js
255
171
  // ✅ /W-ZZZZZZZZ matches, the action 404s, the page says so
@@ -257,122 +173,52 @@ page free to repair itself, show a not-found screen, offer a way out:
257
173
  // and "no route matched" is a worse answer than "this game is gone"
258
174
  ```
259
175
 
260
- So the signal never takes part in matching. It knows what to make of a value;
261
- the route decides whether the url is its own.
262
-
263
- #### Why order stops being load-bearing
264
-
265
- When several routes match one url and bind the **same signal** on a param of the
266
- same name, they all write it, in declaration order — the last one wins:
267
-
268
- ```js
269
- route(`/games/:gameId=${gameIdSignal}`); // declared first
270
- route(`/:gameId=${gameIdSignal}/:state`); // declared later
271
- // on /games/W-ABC234PQ the second one matches too and writes "games"
272
- ```
273
-
274
- Constraining `gameId` removes that second match entirely, which is the fix.
275
- Where a param genuinely cannot be constrained, the routes must not share a
276
- signal.
176
+ Constraining is also the fix when several routes match one url and bind the
177
+ **same signal** on a param of the same name: they all write it, in declaration
178
+ order, the last one winning (`/:gameId/:state` matches `/games/W-ABC234PQ` and
179
+ writes `"games"` into a `gameIdSignal` it shares). Where a param genuinely cannot
180
+ be constrained, the routes must not share a signal.
277
181
 
278
182
  ### An address that only sends elsewhere
279
183
 
280
- Some addresses are not pages: the root of an app whose home screen is « my
281
- games », the old address of a section that moved, the share link of a game
282
- carrying a segment only WhatsApp cares about. They exist to be resolved, and a
283
- route says so itself:
184
+ Some addresses are not pages — the root of an app whose home screen is « my
185
+ games », the old address of a section that moved, a share link carrying a
186
+ segment only WhatsApp cares about — and the route says so itself:
284
187
 
285
188
  ```js
286
189
  export const HOME_ROUTE = route("/", { redirectRoute: MY_GAMES_ROUTE });
287
- export const GAME_SHARED_ROUTE = route("/:gameId/:shareState", {
288
- redirectRoute: GAME_ROUTE,
289
- });
290
190
  ```
291
191
 
292
- The params found in the url carry over to the ones the target route declares
293
- under the same name — `gameId` above needs no help — and what it cannot place
294
- is left behind, `shareState` included. `redirectRouteParams` says the rest:
295
-
296
- ```js
297
- // renaming, when the two routes do not call it the same thing
298
- route("/partie/:id", {
299
- redirectRoute: GAME_ROUTE,
300
- redirectRouteParams: ({ id }) => ({ gameId: id }),
301
- });
302
- // dropping one, keeping the others
303
- route("/:gameId/invite", {
304
- redirectRoute: MY_GAMES_ROUTE,
305
- redirectRouteParams: { gameId: undefined },
306
- });
307
- // carrying nothing over
308
- route("/tri", { redirectRoute: MY_GAMES_ROUTE, redirectRouteParams: null });
309
- ```
310
-
311
- #### Why it is not a page rendering `null`
312
-
313
192
  The redirection is resolved at the door of the navigation, before the url is
314
- written anywhere. Nothing about that address ever happens: no history entry, no
315
- route matching, no route action loading data for a screen nobody will see, no
316
- element mounted, nothing painted — and going back lands on the page before it
317
- rather than replaying the redirection forever.
318
-
319
- A page doing it in an effect gets none of that. It has to be routed to first,
320
- which means the address exists, its action runs, and the app is on a screen
321
- nobody should see for one paint — one a route transition can even animate _to_.
322
- Anything reached by rendering is already too late, so a redirection is declared
323
- with the address and never appears in the `<Route>` tree at all.
324
-
325
- It fires on the route's own address only. `/` catches everything below it when
326
- it renders a container, and would carry `/cgu` away with it if redirecting
327
- followed the same reading — so redirecting asks the stricter question: is this
328
- url exactly that route's address?
329
-
330
- Where several redirecting routes answer for one url, the more specific wins —
331
- `/:gameId/invite` over `/:gameId/:shareState`, the same reading the rest of the
332
- router uses. Chains collapse into one navigation, and a cycle throws naming the
333
- addresses it goes through.
193
+ written anywhere: no history entry, no route action, nothing mounted, and going
194
+ back lands on the page before it. A page redirecting in an effect gets none of
195
+ that — the address exists, its action runs, and a screen nobody should see is
196
+ painted, one a route transition can even animate _to_ — so a redirection is
197
+ declared on the route, **never as a page rendering `null`**. It fires on the
198
+ route's own address only (`/` matches everything below it for rendering, and
199
+ would carry `/cgu` away); of several redirecting routes the more specific wins,
200
+ chains collapse into one navigation, and a cycle throws. Which params carry over:
201
+ the JSDoc of `redirectRoute` and `redirectRouteParams`.
334
202
 
335
203
  #### A search param only the link carries: `dropSearchParams`
336
204
 
337
- A share link can carry a param that is not for the app at all: `?v=k3f9x2`, a
338
- fingerprint of what the link preview shows, there because WhatsApp caches one
339
- preview per address. Once the link is opened it has done its job, and left in
340
- the bar it gets copied and shared again, stale. The route that owns the address
341
- declares it:
342
-
343
- ```js
344
- export const GAME_ROUTE = route(`/games/:gameId=${gameIdSignal}`, {
345
- searchParams: { weather: weatherSignal },
346
- dropSearchParams: ["v"],
347
- });
348
- ```
349
-
350
- `/games/W-ABC234PQ?weather&v=k3f9x2` becomes `/games/W-ABC234PQ?weather` at the
351
- door, exactly like a redirection: no history entry, no route action, no signal
352
- written. The other params stay as they were written. Like a redirection, it
353
- applies to the route's own address only.
354
-
355
- A redirecting route on `/games/:gameId` cannot do this. A search param never
356
- makes a pattern fail to match, so that route also matches the address without
357
- `v`. It then redirects the address to itself, which does nothing, and it
358
- reports `matching` next to `GAME_ROUTE` on every game page.
205
+ A share link can carry a param that is not for the app at all — `?v=k3f9x2`,
206
+ there because WhatsApp caches one link preview per address — and left in the bar
207
+ it gets copied and shared again, stale. The route that owns the address drops it
208
+ at the door, like a redirection, with `dropSearchParams: ["v"]`. A redirecting
209
+ route cannot do this: a search param never makes a pattern fail to match, so it
210
+ would match the address without `v` too, redirect it to itself, and report
211
+ `matching` next to the real route on every page.
359
212
 
360
213
  #### When the destination depends on data
361
214
 
362
- `/admin` sends the reader to the first section their permissions allow: the
363
- target is not known until `GET /me` has answered. That is not a redirection at
364
- all — the door resolves an address from the url alone, and there is nothing to
365
- resolve yet.
366
-
367
- What it is, is a **landing that loads**, so it is declared the way every load is:
368
- a route action, and the redirection once the answer is there.
215
+ `/admin` sends the reader to the first section their permissions allow: a target
216
+ not known until `GET /me` has answered. That is not a redirection — the door
217
+ resolves an address from the url alone — it is a **landing that loads**: a route
218
+ action, and the redirection once the answer is there.
369
219
 
370
220
  ```js
371
- export const ADMIN_ROUTE = route("/admin");
372
- const anySectionMatchingSignal = anyMatchingRouteSignal([
373
- PLACES_ROUTE,
374
- USERS_ROUTE,
375
- ]);
221
+ export const ADMIN_ROUTE = route("/admin"); // no trailing slash: the landing alone
376
222
 
377
223
  routeAction(
378
224
  ADMIN_ROUTE,
@@ -380,31 +226,19 @@ routeAction(
380
226
  const me = await ME.GET.run();
381
227
  firstSectionAllowed(me).redirectTo();
382
228
  },
383
- // only on the landing itself: "/admin" also matches every section below it
384
- () => !anySectionMatchingSignal.value,
229
+ // params, even constant ones: an action without them is prerun on a hover
230
+ () => true,
385
231
  );
386
232
  ```
387
233
 
388
- `redirectTo()` replaces the entry rather than stacking onto it, so the back
389
- button still leaves by where the reader came in — the one property of a
390
- redirection worth keeping here.
391
-
392
- The params are not optional here, for two reasons. A container address matches
393
- everything below it, so without the gate the landing would fire on every
394
- section and send the reader back to the first one. And a route action declared
395
- without params is fetched ahead of the arrival — a link to it preruns it when
396
- the pointer reaches the link (see [dynamic_import.md](./dynamic_import.md)),
397
- which for a read is a head start and for a navigation is the navigation
398
- happening on a hover. Params say the action asks something of the address, and
399
- that is what keeps it from being prerun.
400
-
401
- What it costs, and there is no way around it: the address exists, and something
402
- is on screen while the request is out. That is honest — the reader IS waiting —
403
- so give `/admin` the screen a wait deserves (the section frame, a `<Loading>`)
404
- rather than a page rendering `null`. What must not happen is the rest of the
405
- list: the wait belongs to the route action, so no page below it runs, and the
406
- destination is decided in one place instead of in an effect that fires again on
407
- every render.
234
+ `redirectTo()` replaces the entry, so the back button still leaves by where the
235
+ reader came in. The params function is not optional: a route action declared
236
+ without one is prerun when the pointer reaches a link to it (see
237
+ [dynamic_import.md](./dynamic_import.md#ahead-of-the-render-intent)) — for a read
238
+ a head start, for a navigation the navigation happening on a hover. The cost: the
239
+ address exists, and something is on screen while the request is out — give it
240
+ the screen a wait deserves (the section frame, a `<Loading>`), not a page
241
+ rendering `null`.
408
242
 
409
243
  ### Search params
410
244
 
@@ -419,44 +253,29 @@ const vueSignal = stateSignal("liste", {
419
253
  export const HOME_ROUTE = route("/", { searchParams: { vue: vueSignal } });
420
254
  ```
421
255
 
422
- The signal and the URL are the same state: writing the signal rewrites the URL,
423
- and a URL arriving from outside writes the signal. Never keep a `useState`
424
- beside a route param for the same fact.
425
-
426
- Declared on the **root route**, a search param is a position that holds wherever
427
- one is in the application — a view mode that survives moving from page to page.
428
- Declared on one route, it exists only there.
256
+ The signal and the URL are the same state: never keep a `useState` beside a route
257
+ param for the same fact. Declared on the **root route**, a search param holds
258
+ wherever one is in the application; declared on one route, it exists only there.
429
259
 
430
260
  Writing it AMENDS the history entry one is on rather than stacking a new one: a
431
261
  param that qualifies a screen is not a place, and one entry per write turns a
432
- single back-press into as many as the user moved. A state whose values ARE
433
- places says so — see [`history: "push"`](#a-state-whose-values-are-places-history-push).
262
+ single back-press into as many as the user moved. A state whose values ARE places
263
+ says so — see [`history: "push"`](#a-state-whose-values-are-places-history-push).
434
264
 
435
265
  #### A value written per frame
436
266
 
437
- Every write of the signal reaches the address, synchronously, and browsers
438
- refuse an address written too often: Safari throws a `SecurityError` past 100
439
- writes per 10 seconds, and the state then runs ahead of an address that stopped
440
- following. A value dragged at 60 Hz gets there in under two seconds. Chromium
441
- drops the writes silently, so a developer working there never sees it; navi
442
- warns in dev when the budget is reached.
443
-
444
- **An address is not a recording of a gesture.** What the address records is
445
- where the value was PUT DOWN, not the path the finger took. So the value one
446
- drags belongs to the gesture, and the state bound to the url is written when the
447
- gesture ends:
448
-
449
- - **A gesture with a release** — a disc turned, a slider dragged: hold the value
450
- in the component while the pointer is down, and write the signal on release.
451
- Re-reading the signal meanwhile would pull the control back mid-gesture, so
452
- the held value is the one drawn.
453
- - **A movement with no release** — a wheel zooming, a fly-to animation: write
454
- the signal when the movement has settled (a short idle timer, or the end of
455
- the animation).
456
-
457
- When every intermediate value IS a valid address — the centre of a panned map,
458
- where each frame is a place one could reload into — the state can ask navi to
459
- debounce the browser's address instead, in milliseconds:
267
+ Every write reaches the address, synchronously, and browsers refuse an address
268
+ written too often: Safari throws a `SecurityError` past 100 writes per 10
269
+ seconds, and the state then runs ahead of an address that stopped following
270
+ (Chromium and Firefox drop the writes silently; navi warns in dev).
271
+
272
+ **An address is not a recording of a gesture.** It records where the value was
273
+ PUT DOWN, not the path the finger took: a value one drags is held in the
274
+ component while the pointer is down and written on release — re-reading the
275
+ signal meanwhile would pull the control back mid-gesture — and a movement with no
276
+ release (a wheel zooming, a fly-to) is written once it has settled. When every
277
+ intermediate value IS a valid address — the centre of a panned map — the state
278
+ can ask navi to debounce the browser's copy of the address instead:
460
279
 
461
280
  ```js
462
281
  const eastSignal = stateSignal(0, {
@@ -466,12 +285,9 @@ const eastSignal = stateSignal(0, {
466
285
  });
467
286
  ```
468
287
 
469
- The routes, the document url and the renders still follow every write; only the
470
- browser's copy of the address (`window.location`) waits for the state to stay
471
- still that long, and is then written with the last value. A push is never
472
- debounced, and a push or a state written onto the entry flushes what is pending
473
- first. A back pressed before the write keeps the entry at the last address
474
- written: the drag in progress, and nothing else.
288
+ The routes, the document url and the renders still follow every write; only
289
+ `window.location` waits for the state to stay still that long, and a back pressed
290
+ before the write keeps the last address written. A push is never debounced.
475
291
 
476
292
  ## Rendering routes
477
293
 
@@ -479,6 +295,15 @@ written: the drag in progress, and nothing else.
479
295
  the branch matching the URL; with a `route` it is a branch; with `fallback` it is
480
296
  the branch taken when no sibling matches.
481
297
 
298
+ A route matches its own address and nothing below it — `/games/:gameId` does not
299
+ match `/games/2/edit` — except `/` and a pattern ending in `/`, which match
300
+ everything below them too (`route("/games/")` matches `/games/2/edit`): that is
301
+ what keeps a section's container on screen while one is inside it. Two patterns
302
+ can still match one url — `/games/my_games` is also a `/games/:gameId` — and a
303
+ container renders the **first** branch that matches, in the order written, not
304
+ the most specific. So the branches go from the most precise to the widest, and
305
+ the pages of a `RouteTravel` row alike:
306
+
482
307
  ```jsx
483
308
  <Route>
484
309
  <Route route={MY_GAMES_ROUTE} element={MyGamesPage} />
@@ -488,18 +313,11 @@ the branch taken when no sibling matches.
488
313
  </Route>
489
314
  ```
490
315
 
491
- `elementProps` passes props to the element, which is how a section hands its own
492
- local state down to its sub-pages.
493
-
494
- Two shapes for a section, and which one applies is decided by the URL:
495
-
496
- - **A section with a shared prefix owns its own sub-router.** One leaf
497
- `<Route route={DASHBOARD_SECTION_ROUTE} element={DashboardSection} />` at the
498
- top, and `DashboardSection` renders its own `<Route>` tree plus whatever chrome
499
- it has. Everything about the section is in one file.
500
- - **Pages sharing a layout but no prefix** (`/profile` and `/settings` inside an
501
- authenticated shell) use a container `<Route element={AuthLayout}>`: the active
502
- child is injected into the layout as its children.
316
+ A section with a shared prefix owns its own sub-router: one leaf on
317
+ `/dashboard/` whose element renders its own `<Route>` tree and chrome, handing its
318
+ local state down through `elementProps`. Pages sharing a layout but no prefix
319
+ (`/profile`, `/settings`) use a container `<Route element={AuthLayout}>`, which
320
+ injects the active child as its children.
503
321
 
504
322
  ### Loading data
505
323
 
@@ -525,115 +343,82 @@ section of pages shares one:
525
343
  </Route>
526
344
  ```
527
345
 
528
- The order matters: the boundary goes **outside** the `<Loading>`. A page
529
- suspends first and fails second, and a boundary placed under the `Suspense` it
530
- suspended in is part of the tree being held.
346
+ The order matters: the boundary goes **outside** the `<Loading>`. A page suspends
347
+ first and fails second, and a boundary placed under the `Suspense` it suspended
348
+ in is part of the tree being held.
531
349
 
532
350
  A branch selected inside a wrapper keeps it — the container renders the active
533
- branch alone, wrapper included, so `<Loading>`/`<ErrorBoundary>` can bracket a
534
- subset of the branches rather than the whole router. What happens to a failure
535
- no boundary takes: [error_handling.md](./error_handling.md).
351
+ branch alone, wrapper included — so `<Loading>`/`<ErrorBoundary>` can bracket a
352
+ subset of the branches rather than the whole router. What happens to a failure no
353
+ boundary takes: [error_handling.md](./error_handling.md).
536
354
 
537
355
  ## Links and tab rows
538
356
 
539
- `<Link route={…}>` builds its href from the route and knows on its own whether it
540
- is the current one — that is what draws the current-tab state. It also fetches
541
- the code of where it leads when the pointer or the focus reaches it, for a page
542
- loaded on demand (see [dynamic_import.md](./dynamic_import.md)). `<Nav>` says once,
543
- for the whole row, where the bar that marks the current tab goes:
357
+ `<Link route={…}>` builds its href from the route, knows on its own whether it is
358
+ the current one — that is what draws the current-tab state — and fetches the code
359
+ of where it leads when the pointer or the focus reaches it (see
360
+ [dynamic_import.md](./dynamic_import.md#ahead-of-the-render-intent)). `<Nav>`
361
+ says once, for the whole row, where the bar that marks the current tab goes:
544
362
 
545
363
  ```jsx
546
364
  <Nav currentIndicator>
547
- <Link route={MY_GAMES_ROUTE} variant="tab">
365
+ <Link route={MY_GAMES_ROUTE} variant="tab" replace>
548
366
  Mes parties
549
367
  </Link>
550
- <Link route={CANDIDATE_GAMES_ROUTE} variant="tab">
368
+ <Link route={CANDIDATE_GAMES_ROUTE} variant="tab" replace>
551
369
  Candidatures
552
370
  </Link>
553
371
  </Nav>
554
372
  ```
555
373
 
556
- The bar travels from one tab to the next rather than blinking, because `<Nav>`
557
- gives it a `view-transition-name` of its own: the browser then moves it on the
558
- same clock as any transition playing — including a `RouteTravel` swipe, with no
559
- wiring between the two.
560
-
561
- An entry of the row usually stands for a whole section, not for the single page
562
- it opens: "Lieux" must stay lit while the reader is on `/places/42`. That is
563
- `currentAlso` — the routes, other than the link's own, that the entry also
564
- stands for:
565
-
566
- ```jsx
567
- <Link route={PLACES_ROUTE} currentAlso={[PLACE_ROUTE]} variant="tab">
568
- Lieux
569
- </Link>
570
- ```
571
-
572
- `currentExcept` is the other side: a place under the section the entry does NOT
573
- stand for. Both amend what a link claims about itself and nothing else — which
574
- is why neither of them is an answer to a route that lies about where the reader
374
+ The bar glides from one tab to the next when the change between them plays as a
375
+ view transition — a `RouteTravel` swipe, a route transition — because `<Nav>`
376
+ names it for that movement (`currentIndicatorSlides`, on by default), and only
377
+ for a movement between two tabs of its own row: during any other route movement
378
+ the bar leaves or arrives with its row, a named element inside a page that moves
379
+ standing still (see
380
+ [view_transitions.md](./view_transitions.md#a-name-is-unique-per-document)).
381
+ With no transition at all, it jumps.
382
+
383
+ `replace` is what a row of tabs wants: the neighbour is a lateral move, not a step
384
+ deeper, so the destination takes the place of the current history entry — the
385
+ whole row weighs one entry, and the back button (the arrow at the top, the
386
+ phone's own) leaves by where the reader came in. The link stays a link (an
387
+ address, a middle click, `aria-current`); the same word is
388
+ `navTo(url, { replace: true })`, `route.redirectTo()`, `<Button replace>`. A
389
+ replaced entry inherits the state of the one it takes the place of: **an entry's
390
+ state does not say how it arrived** — only the navigation being applied does, and
391
+ navi is the one applying it.
392
+
393
+ An entry usually stands for a whole section: "Lieux" stays lit on `/places/42`
394
+ with `currentAlso={[PLACE_ROUTE]}`, and `currentExcept` names a place under the
395
+ section it does NOT stand for. Both amend what a link claims about itself and
396
+ nothing else — neither is an answer to a route that lies about where the reader
575
397
  is (see [what its address may say](#a-layer-over-the-screen-what-its-address-may-say)).
576
398
 
577
- A row of tabs is a lateral move: the neighbour is one finger away, and going
578
- there is not going one step deeper. `replace` says exactly that — the
579
- destination takes the place of the current history entry instead of stacking
580
- onto it, so the whole row weighs one entry and the back button (the arrow at the
581
- top, the phone's own) leaves by where the reader came in:
582
-
583
- ```jsx
584
- <Link route={CANDIDATE_GAMES_ROUTE} variant="tab" replace>
585
- Candidatures
586
- </Link>
587
- ```
588
-
589
- The link stays a link — an address, a middle click, the keyboard, `aria-current`
590
- — only the way there changes. It is the same word as `navTo(url, { replace:
591
- true })` and `route.redirectTo()` — and as `<Button replace>`, on an `href` or
592
- on a `--navi-nav-to` command.
593
-
594
- A replaced entry inherits the state of the one it takes the place of (so does
595
- `route.redirectTo()`): **an entry's state does not say how it arrived**. Only
596
- the navigation being applied says that, and navi is the one applying it — see
597
- the back arrow below, which is what that fact is usually needed for.
598
-
599
399
  ## The back arrow: `navBack`
600
400
 
601
401
  An arrow drawn inside the app promises the screen the reader came from — never
602
- the page they were on before the app. Both cases are real for the same url: one
603
- descends into a profile from a list, or one opens it cold from a shared link, a
604
- bookmark, a notification. `history.back()` answers the first and, on the second,
605
- gives back the conversation the link came from.
606
-
607
- `window.history.length` cannot tell them apart — it counts the whole tab — and
608
- neither can an entry's state, for the reason just above. What answers is a count
609
- of how many entries of THIS document stand underneath, kept as the navigations
610
- are applied and written into each entry so it survives a reload mid-stack. navi
611
- keeps it: an app that kept its own would have to be told about every single
612
- `replace` it performs, and the one it forgets shows up only on a cold-opened
613
- screen after a precise gesture.
402
+ the page before the app, which is what `history.back()` gives on a screen opened
403
+ cold from a shared link, a bookmark, a notification. `window.history.length`
404
+ counts the whole tab and cannot tell the two cases apart, and neither can an
405
+ entry's state (above). navi counts the entries of THIS document underneath,
406
+ through every push and replace it applies — an app keeping its own count forgets
407
+ one, and it shows only on a cold-opened screen after a precise gesture.
614
408
 
615
409
  ```jsx
616
- const BackButton = () => {
617
- const canNavBack = useCanNavBack();
618
- ...
619
- };
410
+ const canNavBack = useCanNavBack(); // reactive; canNavBackSignal outside components
620
411
  ```
621
412
 
622
- `useCanNavBack()` (or `canNavBackSignal` outside a component) is reactive: the
623
- arrow appears and disappears as the stack moves, it is not decided at mount.
624
-
625
413
  ```js
626
414
  navBack({ fallback: USER_ME_ROUTE.buildUrl() });
627
415
  ```
628
416
 
629
417
  The `fallback` takes the place of the current entry rather than stacking on it:
630
418
  pushed, it would put the screen just left one press ahead, and the phone's own
631
- back button would walk straight back into it — a loop with no way out of the
632
- app. Without a `fallback`, a `navBack()` with nothing of ours behind does
633
- nothing.
634
-
635
- Said by a button, it is a command, the fallback being its argument the way
636
- `--navi-nav-to` carries its destination:
419
+ back button would walk straight back into it — a loop with no way out of the app.
420
+ Without a `fallback`, a `navBack()` with nothing of ours behind does nothing. Said
421
+ by a button, it is a command carrying the fallback:
637
422
 
638
423
  ```jsx
639
424
  <Button command={`--navi-nav-back:${USER_ME_ROUTE.buildUrl()}`}>←</Button>
@@ -643,7 +428,7 @@ Said by a button, it is a command, the fallback being its argument the way
643
428
 
644
429
  `<RouteTravel>` wraps the `<Route>` tree of a row of tabs and makes every change
645
430
  between them a movement — a tab pressed, a key, the back button, and a thumb
646
- dragging the pages.
431
+ dragging the pages:
647
432
 
648
433
  ```jsx
649
434
  <SectionNav />
@@ -656,215 +441,144 @@ dragging the pages.
656
441
  </RouteTravel>
657
442
  ```
658
443
 
659
- The router still mounts only the branch that matches; the page being left is
660
- shown from the picture the browser keeps of it. The page arriving mounts during
661
- the gesture and fills in under the finger, as its own loading state.
662
-
663
- The order of the tabs — what "one step that way" means, which no URL says — is
664
- read from the children in the order they are written. Pass `routes` only to say
665
- another order, or when the pages are not children of the box. An entry is a route,
666
- or `{ route, params }` when the tabs are params of one route.
667
-
668
- The row is on the **first** of its pages that matches, the way a `<Route>` shows
669
- its first matching branch — several routes match at once when one is a case of
670
- another. A page left out of the row does not travel: reaching it is a change of
671
- place, not a step along the row, and it plays no movement. `axis="y"` lays the
672
- pages out as a column instead: forward is then the page rising and the next one
673
- coming up from below.
674
-
675
- A swipe **replaces** the current history entry, and a tab pressed says the same
676
- thing when its link asks for it (`<Link replace>`, see above) — the two gestures
677
- towards the same neighbour must not write two different histories. `onTravel`
678
- decides otherwise. A replace normally leaves the scroll where it is; a row of
679
- tabs is the exception, and gives each tab back the offset it was read at — see
680
- [A row of tabs, where a replace IS an arrival](#a-row-of-tabs-where-a-replace-is-an-arrival).
681
-
682
- Several `RouteTravel` boxes may live on one page — a section of the path and a
683
- search param of the root route are two rows of tabs, both live — and only the one
684
- actually travelling is captured.
685
-
686
- Demo: [../src/nav/demos/route_travel/route_travel.html](../src/nav/demos/route_travel/route_travel.html)
687
- and [../src/nav/demos/tabs/tabs.html](../src/nav/demos/tabs/tabs.html). The full
688
- spec of the gesture is [drag_to_travel.md](./drag_to_travel.md).
689
-
690
- `RouteTravel` is for pages that form a ROW the finger can push. Pages related
691
- pair by pair without being a row — a game opened from several places, a
692
- settings page rising over whatever showed it — are animated with
693
- `defineRouteTransition` instead, and a given pair must be animated by one of
694
- the two, never both: see [route_transitions.md](./route_transitions.md).
444
+ The router still mounts only the branch that matches: the page being left is the
445
+ picture the browser keeps of it, and the page arriving mounts during the gesture,
446
+ filling in under the finger as its own loading state. The order of the tabs —
447
+ "one step that way", which no URL says — is the order the children are written;
448
+ the row is on the **first** of its pages that matches, like a `<Route>`; a page
449
+ left out of the row does not travel. A swipe **replaces** the current entry, as
450
+ `<Link replace>` does for a tab pressed: two gestures towards the same neighbour
451
+ must not write two different histories — and a row of tabs is the one place a
452
+ replace moves the scroll
453
+ ([below](#a-row-of-tabs-where-a-replace-is-an-arrival)). Several `RouteTravel`
454
+ boxes may live on one page, and only the one travelling is captured.
455
+
456
+ `RouteTravel` is for pages that form a ROW the finger can push; pages related
457
+ pair by pair are animated with `defineRouteTransition`, and a given pair by one
458
+ of the two, never both (see
459
+ [route_transitions.md](./route_transitions.md#route-transitions-and-routetravel--one-pair-one-system)).
460
+ The gesture itself: [drag_to_travel.md](./drag_to_travel.md). Demos:
461
+ `src/nav/demos/route_travel/route_travel.html`, `src/nav/demos/tabs/tabs.html`.
695
462
 
696
463
  ## Where a navigation lands: the scroll
697
464
 
698
465
  Five cases, and they are not a policy to configure but five different facts:
699
466
 
700
- - **Going somewhere new** (a `<Link>`, anything that pushes to another path)
701
- lands at the top. It is an arrival: the offset one had elsewhere means nothing
702
- here, and left alone the new entry would be born holding the previous page's
703
- offset — which the browser would then hand back as if it were this page's own.
704
- A push that keeps the path is not one: it stacks an entry over the page the
705
- reader is scrolled in — a [layer](#a-layer-over-the-screen-what-its-address-may-say)
467
+ - **Going somewhere new** (a push to another path) lands at the top. It is an
468
+ arrival: left alone, the new entry would be born holding the previous page's
469
+ offset, which the browser would then hand back as this page's own. A push that
470
+ keeps the path — a [layer](#a-layer-over-the-screen-what-its-address-may-say)
706
471
  opening, a [state whose values are places](#a-state-whose-values-are-places-history-push)
707
- written — and moves nothing, like a replace.
708
- - **Going somewhere new, to an element** (`/places/le-set#tournaments`) lands
709
- on the element whose `id` is the fragment, once it is rendered — even when it
710
- arrives with the data a second later. See
711
- [Landing on an element](#landing-on-an-element-the-fragment).
712
- - **Going back or forward** (the browser's buttons, `navBack()`,
713
- `history.back()`) lands where that page was left. navi keeps the position and
714
- puts it back once the page is really rendered, which is what the browser
715
- cannot do: it restores at the instant the entry changes, when the document
716
- still holds the page being left, so anything further down than that page is
717
- tall is clamped away.
472
+ — stacks an entry over the page the reader is scrolled in, and moves nothing.
473
+ - **Going somewhere new, to an element** (`/places/le-set#tournaments`) lands on
474
+ the element once it is rendered (see
475
+ [Landing on an element](#landing-on-an-element-the-fragment)).
476
+ - **Going back or forward** lands where that page was left, put back once the
477
+ page is really rendered. The browser restores at the instant the entry changes,
478
+ while the document still holds the page being left, and clamps away anything
479
+ further down than that page is tall.
718
480
  - **Replacing the entry** (`<Link replace>`, `route.redirectTo()`, a param
719
- settling, a state written) moves nothing. It is the same place said
720
- differently, and the reader is still in the page they were reading — a row of
721
- tabs is the one shape where that reading is wrong, see below.
481
+ settling) moves nothing: the same place said differently — except in a row of
482
+ tabs, below.
722
483
  - **A reload** lands where one was, as it would have without navi.
723
484
 
724
- One consequence is softened where the browser exposes its stack (the
725
- Navigation API — everywhere but Firefox today): **a `<Link>` whose destination
726
- is the entry right next to the current one becomes a real traversal** instead
727
- of a push. A "back" link to the page one just came from therefore behaves as a
728
- back — the stack stays what the reader thinks it is (no A, B, A, B… growth)
729
- and the scroll comes back; one step forward too, so returning to the page one
730
- just left resumes it where it was. Only towards entries of this document (a
731
- traversal to another one would be a full page load no link asked for), and
732
- never when the push carries explicit state.
733
-
734
- Everywhere else a `<Link>` is an arrival and lands at the top. A back arrow
735
- that must ALWAYS behave as a back — even far from the entry it targets, even
736
- in a browser with no Navigation API — is `navBack()`. Where there may be
737
- nothing to go back to (a shared link opened cold), decide what the arrow does
738
- from the history, not from the link.
485
+ Where the browser exposes its stack (the Navigation API), **a `<Link>` whose
486
+ destination is the entry right next to the current one becomes a real
487
+ traversal**: a "back" link to the page one just came from behaves as a back — no
488
+ A, B, A, B… growth, and the scroll comes back — and one step forward too. Only
489
+ towards entries of this document, and never when the push carries explicit
490
+ state. An arrow that must ALWAYS behave as a back is `navBack()`.
739
491
 
740
492
  ### Landing on an element: the fragment
741
493
 
742
494
  A link meant to bring one element under the reader's eyes — a notification
743
- pointing at a section several screens down, a shared link to a comment, a row
744
- in a list — is an `id` on the element and a `#id` in the link. Nothing else:
495
+ pointing at a section several screens down, a shared link to a comment, a row in
496
+ a list — is an `id` on the element and a `#id` in the link. Nothing else:
745
497
 
746
498
  ```jsx
747
499
  <Box id="tournaments">…</Box>
748
500
 
749
- // anywhere else
501
+ // anywhere else — <Link route> builds no fragment, so the link spells its href
750
502
  <Link href={`${PLACE_ROUTE.buildUrl({ slug })}#tournaments`}>…</Link>
751
503
  ```
752
504
 
753
- `<Link route>` builds no fragment, so the link spells its `href`.
754
-
755
- The browser alone is not enough in an app. It answers a fragment when the
756
- document finishes loading and on a fragment navigation within the page — both
757
- before the data has drawn the element — and a navigation navi routes is not a
758
- fragment navigation at all. So navi answers every URL carrying a hash itself:
759
-
760
- - **It waits for the element, and for the page.** The element is reached when
761
- it exists and shows something (`checkVisibility`): one still loading, or
762
- rendered inside a closed tab or a folded `<details>`, has not arrived yet.
763
- navi opens nothing to reach it, and reaches it as soon as it shows — the
764
- reader opening the `<details>`, switching to the tab — while the wait lasts.
765
- And it is reached once no route or action is loading: until then, what sits
766
- above it may still be drawn and push it down — and a page reusing the node of
767
- the page being left (`/places/a#tournaments` → `/places/b#tournaments`) holds
768
- it before its own data has come.
769
- - **It gives up.** Once the document has had no route or action loading for
770
- `graceAfterIdle` (1 s). A link to an element that is gone lands where a link
771
- without a fragment would — at the top of a page one arrives at — with no
772
- mark. A document that never stops working is answered after `maxWait`
773
- (10 s), with the element as it stands.
774
- - **A return is not an arrival.** Back, forward and a reload land where the
775
- page was left, as they do without a fragment; the fragment then only puts
776
- `:target` on its element — no scroll, no ring, no focus.
777
- - **It answers once per arrival.** What is watched is the path and the hash: a
778
- search param written while the reader is there (a filter, a page) does not
779
- throw them back to the element. Pressing the very link one is on answers
780
- again.
781
- - **What the reader gets**: the element against the top edge, instantly (what
782
- the browser does with a fragment); the keyboard focus on it when it is itself
783
- focusable — an `id` on the link or the button, not on a box around one; and a
784
- fading ring, the element carrying `data-url-target` for `markDuration` (2 s).
785
- The ring is coloured by `--navi-url-target-color` and lives in `@layer navi`:
786
- an unlayered `[data-url-target] { … }` replaces it, `animation: none` removes
787
- it. Its length is `markDuration`, which also publishes
788
- `--navi-url-target-duration` to CSS — setting only the variable makes the ring
789
- and the attribute disagree.
790
- - **Adjusting it**: `setUrlTargetOptions()`, once, at the app's start — the
791
- alignment (`block`), `behavior`, `markDuration`, `graceAfterIdle`, `maxWait`.
792
-
793
- **`:target` is the lasting "this one"**, and `data-url-target` the moment it
794
- arrived. navi sets `:target` on the element as it lands on it — the browser
795
- alone never would after a navigation navi routes, nor for an element that
796
- arrived after the load — and takes it off once the URL stops naming the
797
- element, which the browser alone would not either: a search param written on
798
- the page rebuilds the address without its fragment, a back returns to the page
799
- without one. It stays on the element navi landed on; a node rendered in its
800
- place later is another one. In JS, `useUrlTargetId()` is the id the hash
801
- designates, `""` when there is none, re-rendering when it changes.
802
-
803
- #### A fragment, or a search param
505
+ The browser answers a fragment when the document finishes loading and on a
506
+ fragment navigation within the page — both before the data has drawn the element
507
+ — and a navigation navi routes is not a fragment navigation at all. So navi
508
+ answers every navigation it routes to a URL carrying a hash (a `#id` link to the
509
+ page one is already on stays the browser's own, without the wait or the ring):
510
+
511
+ - **it waits for the element, and for the page.** The element must exist and
512
+ show something (`checkVisibility`): one inside a closed tab or a folded
513
+ `<details>` has not arrived, and navi opens nothing to reach it. And no route
514
+ or action may be loading: what sits above it may still push it down, and a
515
+ node reused from the page being left is there before its own data;
516
+ - **it gives up** once the document has stopped working for a moment (or after a
517
+ maximum wait, for one that never does): a link to an element that is gone lands
518
+ where a link without a fragment would, with no mark;
519
+ - **a return is not an arrival**: back, forward and a reload land where the page
520
+ was left, and only put `:target` on the element — no scroll, no ring, no focus;
521
+ - **it answers once per arrival**: a search param written while the reader is
522
+ there does not throw them back to the element; pressing the very link one is on
523
+ answers again.
524
+
525
+ What the reader gets: the element against the top edge, instantly; the keyboard
526
+ focus on it when it is itself focusable — an `id` on the link or the button, not
527
+ on a box around one; and a fading ring, the element carrying `data-url-target`.
528
+ The ring is coloured by `--navi-url-target-color` and lives in `@layer navi`: an
529
+ unlayered `[data-url-target] { … }` replaces it, `animation: none` removes it.
530
+ Its length is `markDuration`, set with `setUrlTargetOptions()` along with the
531
+ alignment and the waits; it also publishes `--navi-url-target-duration`, and
532
+ setting only the variable makes the ring and the attribute disagree.
533
+
534
+ **`:target` is the lasting "this one"**, `data-url-target` the moment it arrived.
535
+ navi sets `:target` as it lands on the element — the browser never would after a
536
+ navigation navi routes, nor for an element arriving after the load — and takes
537
+ it off once the URL stops naming it: a search param a route writes rebuilds the
538
+ address without its fragment, a back returns to the page without one. A node
539
+ rendered in its place later is another element. `useUrlTargetId()` is the id the
540
+ hash designates, reactive.
804
541
 
805
542
  A fragment says where to look; a search param says what to show. When the page
806
543
  draws something differently for the value — a filter, a selected row, an open
807
- panel — it is a [search param](#search-params). When the page is the same and
808
- only the reader's eyes should go somewhere, it is a fragment, down to a single
809
- row (`#tournament_42`).
810
-
811
- A search param plus a `scrollIntoView` in an effect rebuilds this, less well:
812
- the effect fires at mount, before what sits above the element has laid out,
813
- and nothing ever tells it to stop waiting.
544
+ panel — it is a [search param](#search-params); when only the reader's eyes
545
+ should go somewhere, a fragment, down to a single row (`#tournament_42`) — never
546
+ a search param plus a `scrollIntoView` in an effect, which fires before what sits
547
+ above the element has laid out, with nothing to tell it how long to wait.
814
548
 
815
549
  ### A row of tabs, where a replace IS an arrival
816
550
 
817
551
  The tabs of a `<RouteTravel>` navigate by replacing, and yet each one is another
818
- route. They also share a single scrollport — the document — and the tab on
819
- screen is what makes it tall. So leaving the offset alone does not keep it: the
820
- moment the arriving tab is shorter, the browser clamps, and the reader's place
821
- is gone before anything of navi's is asked.
822
-
823
- The row is the only thing that knows this, so the row is what says it. On every
824
- travel — a tab pressed, a thumb dragging the pages, a wheel, a travel let go of
825
- too early and put back:
826
-
827
- - the arriving tab is given back the offset it was read at, once it is really
828
- rendered;
829
- - a tab never read opens at its **top**, rather than wherever its neighbour
830
- happened to be;
831
- - the clamp itself is never recorded. It is not the reader scrolling, and the
832
- url it would be written against is already the arriving tab's — recorded, it
833
- destroys that tab's own position, which is then what a later back or forward
834
- hands out.
552
+ route, sharing one scrollport — the document — that the tab on screen makes
553
+ tall: the moment the arriving tab is shorter, the browser clamps, and the
554
+ reader's place is gone. The row is the only thing that knows this, so on every
555
+ travel — a tab pressed, a thumb, a wheel, a travel put back — it gives the
556
+ arriving tab the offset it was read at, once it is really rendered; opens a tab
557
+ never read at its **top**, not wherever its neighbour happened to be; and never
558
+ records the clamp, which, recorded against the arriving tab's url, would destroy
559
+ that tab's own position.
835
560
 
836
561
  **Only where the row owns the document**: nothing between the travelling box and
837
- the viewport may scroll. A row inside a scroller of its own — a frame in an
838
- article, a panel beside other content — shares nothing with the document, and
839
- the offset there belongs to the page around it, which the reader never left; the
840
- travel leaves it alone. An `overflow: hidden` on any ancestor is enough to put
841
- the row outside the document's scrollport — it holds an offset of its own even
842
- without a scrollbar, whereas `overflow: clip` holds none and does not count — so
843
- a row that should give positions back and does not is worth looking at from
844
- that angle first.
845
-
846
- Pages that scroll inside themselves rather than scrolling the document are not
847
- concerned either way: each one brings its own scrollport, which goes away with
848
- the page and has nothing to give back.
849
-
850
- What is not covered: a page whose height depends on something still loading is
851
- not tall enough at the moment its position is put back, so a deep position is
852
- clamped as it was before. Only the page knows when it is whole.
562
+ the viewport may scroll, and a row inside a scroller of its own leaves that
563
+ offset alone. An `overflow: hidden` on any ancestor is enough to take the row out
564
+ of the document's scrollport — it holds an offset even without a scrollbar, where
565
+ `overflow: clip` holds none — so it is the first thing to look for when a row
566
+ does not give positions back. And a page whose height depends on something still
567
+ loading is not tall enough when its position is put back: only the page knows
568
+ when it is whole.
853
569
 
854
570
  ## Creating something, then editing it
855
571
 
856
572
  The create screen, the page of what was created, the edit screen — three routes,
857
- one form, and a movement between them. It is assembled in
858
- [create_and_edit.md](./create_and_edit.md), which is also where the two matching
859
- rules that decide the shape of the `<Route>` tree are spelled out (several routes
860
- match at once; the first matching branch wins).
573
+ one form, and a movement between them — are assembled in
574
+ [create_and_edit.md](./create_and_edit.md).
861
575
 
862
576
  ## Tabs that are not routes
863
577
 
864
578
  `SlideContainer` holds slides that replace one another in one box, with the same
865
- gestures and the same travelling bar, and — unless the signal it is bound to is
866
- one the URL holds, see below — nothing written to the URL. Use it when the
867
- position genuinely is not a place one should be able to link to:
579
+ gestures and the same travelling bar, and — unless its signal is one the URL
580
+ holds, see below — nothing written to the URL. Use it when the position genuinely
581
+ is not a place one should be able to link to:
868
582
 
869
583
  - the steps of a wizard, or the screens of a picker, inside a dialog or a popover
870
584
  — a popup is promoted to the browser's top layer, so no container can hold two
@@ -886,33 +600,29 @@ route.
886
600
  </SlideContainer>
887
601
  ```
888
602
 
889
- `<Nav slideContainer>` names the container by id — the row can sit anywhere on the
890
- page. It reads which slide is on screen from the container itself, and its bar
891
- follows the slides, a finger dragging them included. `<Link slide>` has no href
892
- and behaves like a button: this is not a link to anywhere.
603
+ The row names its container by id, so it can sit anywhere on the page, and its
604
+ bar follows the slides, a finger dragging them included. `<Link slide>` has no
605
+ href and behaves like a button: this is not a link to anywhere.
893
606
 
894
607
  ### A `SlideContainer` in the URL: a position that is not a place one came from
895
608
 
896
609
  "Should a link be able to open the app on this?" has a third answer, and a wizard
897
- is exactly it: **yes for reading and for reloading, no for history.** The step one
898
- is on should be legible in the address bar and should survive a reload —
899
- `/alerts/W-123/edit` reopening on "Lieu" because that is where the reader was —
900
- and it should NOT stack an entry per step, because the back arrow of a form means
901
- "leave this form", not "one question back". Four steps that each push turn one
902
- back-press into four, and walk the reader backwards through a form they thought
903
- they had left.
904
-
905
- There is nothing to invent for it: a search param already IS a position in the
906
- URL that replaces rather than pushes ([Search params](#search-params)). Declare
907
- the step as one, and hand its signal to the container:
610
+ is exactly it: **yes for reading and for reloading, no for history.** The step
611
+ should be legible in the address bar and survive a reload, and should NOT stack
612
+ an entry per step: the back arrow of a form means "leave this form", not "one
613
+ question back", and four steps that each push walk the reader backwards through a
614
+ form they thought they had left. A search param already IS a position in the URL
615
+ that replaces rather than pushes; declare the step as one, and hand its signal to
616
+ the container:
908
617
 
909
618
  ```js
910
619
  const stepSignal = stateSignal("when", {
911
620
  id: "step",
912
621
  oneOf: ["when", "where", "who", "recap", "done"],
622
+ autoFix: true, // a value outside oneOf is repaired, and the address with it
913
623
  // the step qualifies THIS visit, not the screen: a link built to the editor
914
- // does not inherit the step one happens to be on, and it goes back to the
915
- // default when the route stops matching
624
+ // does not inherit it, and it goes back to the default when the route stops
625
+ // matching
916
626
  weak: true,
917
627
  });
918
628
  export const ALERT_EDIT_ROUTE = route("/alerts/:alertId/edit", {
@@ -924,31 +634,20 @@ export const ALERT_EDIT_ROUTE = route("/alerts/:alertId/edit", {
924
634
  <SlideContainer signal={stepSignal}>
925
635
  ```
926
636
 
927
- That is the whole wiring, and every half of it is the piece that already
928
- existed. Worth naming, because each answers a question a wizard actually has:
929
-
930
- - **where it opens is the state's own default** — the first argument of
931
- `stateSignal`. The container has a `defaultCurrent` for when it owns its
932
- position, but a bound container does not own it: one place says where the step
933
- starts, and it is the same place a reset goes back to;
934
- - **the param is absent while the step IS that default** (route.js prunes it), so
935
- `/alerts/W-123/edit` is clean on the first step, gains `?step=where` on the
936
- second, and loses it again coming back. The address carries what differs from
937
- the usual answer and nothing else;
938
- - **the container walks to the step rather than jumping to it.** The address comes
939
- from outside the box (typed, shared, kept from a session that has moved on), so
940
- every slide between here and there is asked to let go the way a key going that
941
- way would ask it, and the first one that holds is where one stops. `?step=done`
942
- cannot open a confirmation screen for something nobody sent. The signal is then
943
- written with the area actually shown, so the address never says one is
944
- somewhere one is not;
945
- - **only the travels that HAPPENED are written.** A travel a lock refused, or one
946
- `onCurrentChange` refused late, never reaches the signal — or is written back
947
- when it does.
948
-
949
- A container remembers nothing across a reload, so a step whose `required` the app
950
- knows is already satisfied says so itself (`required={!alreadyFilled}`); the same
951
- holds for a hold that a finished job lifts (`preventNavNext={!published}`).
637
+ Where it opens is the state's own default — a bound container does not own its
638
+ position, so one place says where the step starts, the same place a reset goes
639
+ back to — and the param is absent from the address while the step IS that
640
+ default. The container **walks** to the step the address asks for rather than
641
+ jumping to it: the address comes from outside the box (typed, shared, kept from a
642
+ session that has moved on), so every slide on the way is asked to let go the way
643
+ a key would ask it, and the first one that holds is where one stops —
644
+ `?step=done` cannot open a confirmation for something nobody sent. The signal is
645
+ then written with the area actually shown, and only the travels that HAPPENED are
646
+ written (one a lock or `onCurrentChange` refused never reaches it, or is written
647
+ back). A value no slide carries is left as asked: refusing it is the state's job,
648
+ `oneOf` + `autoFix`. A container remembers nothing across a reload, so a step the
649
+ app knows is already satisfied says so itself (`required={!alreadyFilled}`,
650
+ `preventNavNext={!published}`).
952
651
 
953
652
  **A start that depends on the page one is on** — a wizard creating something
954
653
  opens on its first question, the same wizard editing something opens on its
@@ -963,72 +662,44 @@ route("/alerts/:alertId/edit", {
963
662
  ```
964
663
 
965
664
  Everything that asks "is this the default" then asks the route one is on: both
966
- addresses stay clean on the step they open on — `/alerts/create` on "when",
967
- `/alerts/W-123/edit` on "recap" — an address naming no step puts the state back
968
- on that page's own, and going from one to the other moves the step with it.
969
-
970
- Not a `computed` over the route's `matchingSignal`: a state and the route that
971
- declares it cannot name each other — whichever is written first, the other is
972
- not initialised yet, and a dynamic default is read while `stateSignal` is being
973
- built. The route is the right place regardless; it is the thing that knows which
974
- page one is on.
975
-
976
- Two containers on one screen are two signals, and that is the whole answer to
977
- "which one owns the param": the one holding the route's signal. A gallery of
978
- seven wizards side by side hands each of them a `useSignal` of its own, and
979
- nothing goes into the address.
665
+ addresses stay clean on the step they open on, and going from one to the other
666
+ moves the step with it. Two containers on one screen are two signals — the one
667
+ holding the route's signal owns the param; a gallery of wizards side by side
668
+ hands each a `useSignal` of its own, and nothing goes into the address.
980
669
 
981
670
  ### A state whose values ARE places: `history: "push"`
982
671
 
983
672
  Replacement is the default because most URL-held state qualifies the screen one
984
673
  is on. A state whose values are places one came from — the photo being looked at
985
- in a gallery — says so where the state is declared:
674
+ in a gallery — says so where it is declared, and one write can still say
675
+ otherwise:
986
676
 
987
677
  ```js
988
678
  const photoSignal = stateSignal(undefined, { id: "photo", history: "push" });
989
- ```
990
-
991
- Every write of it then stacks an entry, wherever the write comes from. Except
992
- where the writer knows this particular move is not one: a slide reached by
993
- DRAGGING replaces even in a container that pushes, because swiping back and forth
994
- with a thumb is browsing, not a trail one wants to walk home along. That is said
995
- at the write rather than declared:
996
-
997
- ```js
998
679
  photoSignal.set(nextPhoto, { history: "replace" });
999
680
  ```
1000
681
 
1001
- `SlideContainer` already does exactly that for its own drags, so a gallery gets
1002
- it by declaring the state and nothing else.
682
+ A slide reached by DRAGGING is such a write — swiping back and forth with a thumb
683
+ is browsing, not a trail one wants to walk home along — and `SlideContainer`
684
+ replaces for its own drags even when its state pushes.
1003
685
 
1004
686
  ### What this is not
1005
687
 
1006
- It is not a route. Nothing is declared for the steps themselves, nothing matches
1007
- on them, and no page transition plays — a transition needs the page to change
1008
- ([route_transitions.md](./route_transitions.md)), and a search param moving is
1009
- not one. What travels is the box, and the address is a label on where the box
1010
- stands. A position several parts of the app must react to is still a route.
1011
-
1012
- Demo: [../src/layout/demos/8_slide_container_demo.html](../src/layout/demos/8_slide_container_demo.html).
688
+ It is not a route. A search param bound to a `SlideContainer` moves the box, not
689
+ a page: nothing is declared for the steps, nothing matches on them, and no route
690
+ transition is written for them. A position several parts of the app must react
691
+ to is still a route. Demo: `src/layout/demos/8_slide_container_demo.html`.
1013
692
 
1014
693
  ## A layer over the screen: what its address may say
1015
694
 
1016
695
  Some things are opened from everywhere and drawn OVER whatever the reader was
1017
696
  looking at: the settings behind a gear in the top bar, a photo, a help sheet.
1018
- Closing one puts the reader back exactly where they were — not on a page the app
1019
- picked, on the one they had. And the address has to hold it, for the same reasons
697
+ Closing one puts the reader back exactly where they were — on the page they had,
698
+ not one the app picked — and the address has to hold it, for the same reasons
1020
699
  every other position does: a reload lands on it, a link opens on it, the back
1021
- button closes it.
1022
-
1023
- ### Why a layer is a routing question at all
1024
-
1025
- Layout should not decide routing. But routing decides a great deal of what is on
1026
- screen: which page is mounted, whose data loads, which bar entry lights up, which
1027
- movement plays between two screens. An address is not a label — it is read by
1028
- everything — so a layer, which is a pure fact of layout, cannot be given just any
1029
- address.
1030
-
1031
- The rule that falls out of it, and the whole of this section:
700
+ button closes it. Routing decides a great deal of what is on screen — which page
701
+ is mounted, whose data loads, which bar entry lights up, which movement plays —
702
+ so a layer, a pure fact of layout, cannot be given just any address:
1032
703
 
1033
704
  > The URL may say what is drawn OVER the screen. It must never name a place the
1034
705
  > reader is not at.
@@ -1040,43 +711,29 @@ when they conceptually belong to "me". Written `/me/settings`, the router
1040
711
  believes otherwise, and everything that reads the router inherits the belief:
1041
712
 
1042
713
  - the bar entry for the "me" section lights up while the settings cover a game;
1043
- - `/me` matches, so the screen the reader was on is REPLACED. The covering is a
1044
- fiction that lasts as long as the animation, and the page beneath is gone;
714
+ - the screen the reader was on is REPLACED by a page of the "me" section: the
715
+ covering is a fiction that lasts as long as the animation;
1045
716
  - a route transition plays a crossing between two pages that never crossed, and
1046
717
  its back half plays on every way out — the sheet lifts to reveal a page that
1047
718
  was never underneath;
1048
- - and it is wrong on the way out too: closing does not return to `/me`, it
1049
- returns to wherever the reader was, which the address never said.
1050
-
1051
- These do not get fixed one at a time. `currentExcept` stops one link from
1052
- claiming "you are here"; a `"none"` relation silences one crossing; a hand-rolled
1053
- `active` replaces one reading. Each patches a reader of the lie without removing
1054
- it, and the next reader arrives quietly wrong.
719
+ - closing does not return to `/me`, it returns to wherever the reader was, which
720
+ the address never said.
1055
721
 
1056
- ### It is not "use a search param"
1057
-
1058
- `/settings` at the top level does not claim to be inside `/me` — it is honest
1059
- about not being there, and every symptom above goes with it. What it still is,
1060
- though, is a PAGE: it names a screen, so the screen beneath is gone, and closing
1061
- has nowhere written to return to. `whatever/settings` is the same answer in a
1062
- longer word.
1063
-
1064
- So the rule is not about search params. It is about not claiming a position. A
1065
- path can be honest; what a path cannot do is carry what is underneath.
722
+ These do not get fixed one at a time: `currentExcept`, a `"none"` relation, a
723
+ hand-rolled `active` each patch one reader of the lie, and the next reader
724
+ arrives quietly wrong.
1066
725
 
1067
726
  ### What the search param buys: the way back is in the address
1068
727
 
1069
- `/places?settings` says two things at once — which screen the reader is on, and
1070
- what is drawn over it. That is what makes closing exact from a cold start: a
1071
- reload, a link someone sent, a new tab. The layer's address CONTAINS the address
1072
- of what it covers, so "close" needs no memory at all.
1073
-
1074
- `/settings` has to remember instead. In practice the browser often does — after a
1075
- reload the session's history is still there, so a back-press lands somewhere —
1076
- but that is the BROWSER's memory of this tab, not the address. A link someone
1077
- else opens has none of it, and neither does a bookmark. Only the address travels,
1078
- which is what makes the param shape the right model rather than the convenient
1079
- one.
728
+ `/settings` at the top level is honest about not being inside `/me`, but it is
729
+ still a PAGE: the screen beneath is gone, and closing has nowhere written to
730
+ return to — so the rule is about not claiming a position, not about search
731
+ params. `/places?settings` says both which screen the reader is on and what is
732
+ drawn over it, so closing needs no memory at all: exact after a reload, from a
733
+ link someone sent, in a new tab. `/settings` has to remember instead — the
734
+ browser's history of the tab often covers for it after a reload, but a link
735
+ someone else opens has none of it, and neither does a bookmark. Only the address
736
+ travels.
1080
737
 
1081
738
  ### The wiring
1082
739
 
@@ -1095,31 +752,35 @@ export const ROOT_ROUTE = route("/", {
1095
752
  ```jsx
1096
753
  <Button command="--navi-toggle" commandFor="settings_panel">⚙</Button>
1097
754
  …
1098
- <SidePanel id="settings_panel" signal={settingsSignal} side="top" expandY />
755
+ <SidePanel
756
+ id="settings_panel"
757
+ signal={settingsSignal}
758
+ side="top"
759
+ expandY
760
+ animation
761
+ />
1099
762
  ```
1100
763
 
1101
764
  Every piece of it answers something:
1102
765
 
1103
- - **declared on the ROOT route**, so the layer opens over every screen. Declared
1104
- on one route it would only exist there, and a door in the furniture is on every
1105
- screen ([Search params](#search-params));
1106
- - **`weak`**, so a link built to a page never inherits a layer that happens to be
1107
- open. A layer qualifies one visit; it is not part of anyone's address;
1108
- - **`history: "push"`**, so the layer is a place one came from and the back button
1109
- closes it. The closing itself is never an entry of its own — see
1110
- [popup_open.md](./popup_open.md), which owns what a `signal` bound to a URL
1111
- writes on open, on close, and on cancel;
1112
- - **the panel is rendered outside the page area**, next to the router rather than
1113
- inside it. It is drawn over the pages and the fixed bars alike, and it stays
1114
- mounted while closed so it has somewhere to animate from;
766
+ - **declared on the ROOT route**, so the layer opens over every screen, where its
767
+ door in the furniture is;
768
+ - **`weak`**: a link built to a page never inherits a layer that happens to be
769
+ open — a layer qualifies one visit, it is not part of anyone's address;
770
+ - **`history: "push"`**: the layer is a place one came from, and the back button
771
+ closes it. The closing is never an entry of its own —
772
+ [popup_open.md](./popup_open.md#signal--the-app-holds-it-both-ways) owns what a
773
+ `signal` bound to a URL writes on open, on close, and on cancel;
774
+ - **the panel is rendered outside the page area**, next to the router: over the
775
+ pages and the fixed bars alike, mounted while closed, `animation` giving it an
776
+ entrance of its own (a `SidePanel` has none by default);
1115
777
  - **`expandY`**, because a sheet is content-tall by default and a layer covers
1116
- the screen. It is also what any tab row inside it must NOT be given: `expand`
1117
- is both axes, and in the panel's own column that makes the row eat the height
1118
- and push everything below it off screen;
1119
- - **no `defineRouteTransition` for it.** Nothing crosses: the page beneath does
1120
- not change, so there is no pair of pages and no movement between them. A layer
1121
- has an entrance of its own, and route transitions are for pages replacing pages
1122
- ([route_transitions.md](./route_transitions.md)).
778
+ the screen. A tab row inside it must NOT get `expand`: that is both axes, and
779
+ in the panel's column the row eats the height and pushes everything below it
780
+ off screen;
781
+ - **no `defineRouteTransition` for it.** The page beneath does not change, so
782
+ there is no pair of pages to move between — route transitions are for pages
783
+ replacing pages ([route_transitions.md](./route_transitions.md)).
1123
784
 
1124
785
  ### Places inside the layer
1125
786
 
@@ -1127,9 +788,9 @@ A layer big enough to have tabs holds them the way a wizard does — a search pa
1127
788
  of its own, `oneOf` its pages, replacing rather than pushing, so the back button
1128
789
  closes the layer instead of stepping back one tab. Everything in
1129
790
  [A `SlideContainer` in the URL](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
1130
- applies unchanged, and a layer is exactly the case that section names for
1131
- `SlideContainer` over `RouteTravel`: a popup lives in the browser's top layer,
1132
- where two pages cannot stand side by side.
791
+ applies unchanged, and a layer is exactly the case
792
+ [Tabs that are not routes](#tabs-that-are-not-routes) names for `SlideContainer`
793
+ over `RouteTravel`.
1133
794
 
1134
795
  ```js
1135
796
  const settingsTabSignal = stateSignal("account", {
@@ -1141,16 +802,11 @@ const settingsTabSignal = stateSignal("account", {
1141
802
 
1142
803
  The address then grows and shrinks with what the reader does: `/me` closed,
1143
804
  `/me?settings` open on the tab it opens on, `/me?settings&settings_tab=alerts`
1144
- one tab further, and back to `/me?settings` returning to the first.
1145
-
1146
- Two params, because they answer two questions — is the layer there, and which of
1147
- its pages is shown. One param whose value is "which page of the layer, or none"
1148
- (`?settings=alerts`) is arguably the shape the address wants, and a popup cannot
1149
- hold it: it is open on exactly one value of its signal (`value`, see
1150
- [popup_open.md](./popup_open.md#signal--the-app-holds-it-both-ways)), so a tab
1151
- changing under it would read as the layer closing. `value` is for the other
1152
- question — WHICH of several popups is open, one sheet per card under a single
1153
- `?seat=<gameId>` — not for what varies inside one.
805
+ one tab further, and back to `/me?settings` returning to the first. Two params,
806
+ because they answer two questions — is the layer there, and which of its pages
807
+ is shown. A popup's `value` answers another one, WHICH of several popups is open
808
+ (one sheet per card under a single `?seat=<gameId>`), not what varies inside one
809
+ (see [popup_open.md](./popup_open.md#signal--the-app-holds-it-both-ways)).
1154
810
 
1155
811
  ### What it costs
1156
812
 
@@ -1161,14 +817,12 @@ This is a compromise, and it is worth stating rather than discovering:
1161
817
  a route "you are covered", and nothing should be inferred from a layer being
1162
818
  open — the reader is still on that page, and will be back on it in a moment;
1163
819
  - **the router knows nothing about layers.** `<Route>` renders pages; the layer is
1164
- drawn beside them by the application. The address is honest about it, but "over"
1165
- is not a routing concept, and no `<Route>` shape expresses it;
1166
- - **the app really is on the page underneath**, which is right for `aria-current`,
1167
- for the bar, for a link built while the layer is open — and is exactly wrong for
1168
- anything wanting "the settings are what is current". If something needs to
1169
- answer that, it reads the layer's own state, not the router.
1170
-
1171
- Demo:
1172
- [../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html](../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html)
820
+ drawn beside them by the application, and "over" is not a routing concept;
821
+ - **the app really is on the page underneath**, which is right for
822
+ `aria-current`, for the bar, for a link built while the layer is open — and
823
+ wrong for anything wanting "the settings are what is current": that reads the
824
+ layer's own state, not the router.
825
+
826
+ Demo: `src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html`
1173
827
  — a bar holding both doors side by side: notifications, a page that replaces the
1174
828
  screen with a route transition, and settings, a layer that covers it.