@jsenv/navi 0.29.418 → 0.29.419

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 +192 -43
  3. package/dist/dev/jsenv_navi.js.map +10 -10
  4. package/dist/jsenv_navi.js +190 -41
  5. package/dist/jsenv_navi.js.map +10 -10
  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 +159 -254
  12. package/docs/css_architecture.md +196 -260
  13. package/docs/data_states.md +41 -44
  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 +61 -78
  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
@@ -2,15 +2,16 @@
2
2
 
3
3
  How pages of an app move against each other when the user navigates —
4
4
  `defineRouteTransition`, `defineRouteDefaultTransition`, what one link or one
5
- `navTo` may ask for on top of them, and the thinking that decides which
6
- movement (if any) a navigation deserves. The API grammar itself
7
- (accepted forms, shipped type names) lives in the JSDoc of
8
- `defineRouteTransition`; this file holds what a signature cannot say.
5
+ `navTo` may ask for on top of them, and the thinking that decides which movement
6
+ (if any) a navigation deserves. The API grammar itself (accepted forms, shipped
7
+ type names) lives in the JSDoc of `defineRouteTransition`; this file holds what a
8
+ signature cannot say.
9
9
 
10
- Demos: [the movements](../src/nav/demos/route_transition/route_transition.html),
11
- [a default transition](../src/nav/demos/route_transition/route_transition_default.html),
12
- [pages between fixed bars](../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html),
13
- [with a RouteTravel inside](../src/nav/demos/route_transition/route_transition_with_travel.html)
10
+ Demos, in `src/nav/demos/`: `route_transition/route_transition.html` (the
11
+ movements), `route_transition/route_transition_default.html` (a default
12
+ transition), `route_transition_fixed_bars/route_transition_fixed_bars.html`
13
+ (pages between fixed bars), `route_transition/route_transition_with_travel.html`
14
+ (with a `RouteTravel` inside).
14
15
 
15
16
  - [What a transition is for](#what-a-transition-is-for)
16
17
  - [A page opened from anywhere](#a-page-opened-from-anywhere)
@@ -31,13 +32,12 @@ Demos: [the movements](../src/nav/demos/route_transition/route_transition.html),
31
32
  ## What a transition is for
32
33
 
33
34
  A transition is not decoration: it states a **relation** between two pages, and
34
- the user reads it as a map. A page sliding in from the right says "this place is
35
- deeper, the way back is to the left" — which is why the back arrow then feels
36
- inevitable rather than learned. A movement that states a relation the app does
37
- not actually have (a slide between two sibling tabs) teaches a false map, and a
38
- false map is worse than no animation at all.
39
-
40
- So the unit of declaration is the pair, not the app:
35
+ the user reads it as a map — a page sliding in from the right says "this place is
36
+ deeper, the way back is to the left", which is why the back arrow then feels
37
+ inevitable rather than learned. A movement stating a relation the app does not
38
+ have (a slide between two sibling tabs) teaches a false map, and a false map is
39
+ worse than no animation at all. So the unit of declaration is the pair, not the
40
+ app:
41
41
 
42
42
  ```js
43
43
  defineRouteTransition(MY_GAMES_PAGE, GAME_PAGE, "slide-x");
@@ -45,119 +45,88 @@ defineRouteTransition(RADAR_PAGE, GAME_PAGE, "slide-x");
45
45
  ```
46
46
 
47
47
  and two pages never written in the same relation play **nothing** between each
48
- other. That silence is a statement too: "Mes parties" and "Radars" are two tabs
49
- of a bottom bar, side by side, neither before the other — a cut is the honest
50
- rendering of that fact. Resist the urge to fill every navigation with movement;
51
- declare the relations that exist and let the rest cut.
48
+ other. That silence is a statement too: "Mes parties" and "Radars" are two tabs of
49
+ a bottom bar, side by side, neither before the other — a cut is the honest
50
+ rendering of that fact. Declare the relations that exist and let the rest cut.
52
51
 
53
52
  ## A page opened from anywhere
54
53
 
55
- One shape of page has no pair to write. Its door is in the fixed furniture — a
56
- gear in the top bar, a "+" in the tab bar — so it is opened from every screen of
57
- the app and closed back onto whichever one the reader was on. The relation is
58
- real and it is a single sentence ("the settings come down over the screen you
59
- were on, and go back up"), but there are twenty-five `from` pages and not one of
60
- them is the point.
61
-
62
- Leave the `from` out:
54
+ One shape of page has no pair to write: its door is in the fixed furniture — a
55
+ gear in the top bar, a "+" in the tab bar — so it is opened from every screen and
56
+ closed back onto whichever one the reader was on. Leave the `from` out:
63
57
 
64
58
  ```js
65
59
  defineRouteTransition(null, SETTINGS_PAGE, "cover-top");
66
60
  ```
67
61
 
68
- Arriving there plays forward from wherever, leaving plays back to wherever. A
69
- back out of it retraces the crossing that opened it (see
70
- [A traversal retraces its crossing](#a-traversal-retraces-its-crossing)); a link
71
- out of it, to wherever, finds the relation the same way the way in did.
72
-
73
- It is tried last, after every written pair, so a pair naming the same destination
74
- still owns its crossing — the map, where it was drawn, is more precise than "from
75
- wherever". And it is not `defineRouteDefaultTransition`: a default is about every
76
- navigation nothing was said about, this is about ONE destination whose door
77
- happens to be everywhere. Write it for a page whose door really is furniture; a
78
- page reached from a screen has that screen to be paired with, and the pair says
79
- more.
80
-
81
- **Such a page is still a PAGE**, and that is the question to settle before
82
- writing the relation: it takes the screen's place, and the reader comes back to
83
- whatever the router puts there — not necessarily to what they were on. A door in
84
- the furniture whose destination must be drawn OVER the screen, and must give it
85
- back exactly on closing, is not a page at all: it is a layer, its address says so
86
- differently, and no relation is written for it. See
87
- [navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say).
62
+ Arriving there plays forward from wherever, leaving plays back to wherever. It is
63
+ tried last — a pair naming the same destination still owns its crossing — and it
64
+ is not `defineRouteDefaultTransition`, which is about every navigation nothing
65
+ was said about. Write it only for a door that really is furniture: a page reached from a
66
+ screen has that screen to be paired with, and the pair says more.
67
+
68
+ **Such a page is still a PAGE**: it takes the screen's place, and the reader comes
69
+ back to whatever the router puts there. A destination that must be drawn OVER the
70
+ screen and give it back exactly on closing is a layer, not a page — its address
71
+ says so differently, and no relation is written for it (see
72
+ [navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say)).
88
73
 
89
74
  ## Choosing a movement
90
75
 
91
76
  - **`slide-x`** — going INTO something: a list item opened, a card followed, a
92
77
  notification tapped. The page is deeper on the same plane; leaving it slides
93
- back out. The most common relation in an app, and the one every phone has
94
- taught.
78
+ back out. The most common relation in an app.
95
79
  - **`slide-y`** — the same relation on a vertical arrangement, when the layout
96
80
  genuinely reads as a column.
97
81
  - **`cover-right` / `cover-left` / `cover-bottom` / `cover-top`** — a page that
98
- INTERRUPTS rather than continues: settings, a composer, anything modal-like
99
- that one returns from to find the page beneath unchanged. The covered page
100
- holding still is the point — it promises "you are not leaving, this is on
101
- top". The edge named is the one the page comes IN from, and the one to name is
102
- where its door is: a composer opened from a bottom bar covers from the bottom,
103
- settings pulled down from a top bar cover from the top.
82
+ INTERRUPTS rather than continues: settings, a composer, anything modal-like that
83
+ one returns from to find the page beneath unchanged — the covered page holding
84
+ still says "you are not leaving, this is on top". The edge named is the one the
85
+ page comes IN from, and the one to name is where its door is: a composer opened
86
+ from a bottom bar covers from the bottom.
104
87
  - **`zoom`** — a detail brought closer: a photo, a card expanded into a page.
105
- - **`cross-fade`** — a soft change with no spatial claim. Use it where a cut
106
- feels harsh but no direction would be true.
107
- - **`none`** — silence, written down. Needed only to override: one way of a
108
- pair, or the default.
88
+ - **`cross-fade`** — a soft change with no spatial claim, where a cut feels harsh
89
+ but no direction would be true.
90
+ - **`none`** — silence, written down. Needed only to override: one way of a pair,
91
+ or the default.
109
92
 
110
93
  Two recommendations that matter more than the individual choices:
111
94
 
112
- - **One movement per KIND of relation, app-wide.** If opening a game slides
113
- from the right, opening a profile should too — the user learns one grammar,
114
- not one rule per page.
95
+ - **One movement per KIND of relation, app-wide.** If opening a game slides from
96
+ the right, opening a profile should too — the user learns one grammar, not one
97
+ rule per page.
115
98
  - **Keep reciprocity.** The way back being the same movement reversed is what
116
- makes the map hold together; it is the default, and breaking it (a relation
99
+ makes the map hold together. It is the default, and breaking it (a relation
117
100
  written for the exact way travelled wins over being the reverse of another)
118
- should answer a real asymmetry in the app, not a styling whim. Write the way
119
- back only to say something DIFFERENT — another movement, or `"none"`. Written
120
- with the same one, both crossings find their own relation and both play
121
- forward, and the pair can never say "back" again, the back button included;
122
- navi warns when it sees that pair defined.
101
+ should answer a real asymmetry, not a styling whim: write the way back only to
102
+ say something DIFFERENT — another movement, or `"none"`. Written with the same
103
+ one, both crossings play forward and the pair can never say "back" again, the
104
+ back button included; navi warns when it sees that pair.
123
105
 
124
106
  ## A default transition — when
125
107
 
126
- `defineRouteDefaultTransition("cross-fade")` plays on every navigation no
127
- relation was written for. Two situations, two answers:
108
+ `defineRouteDefaultTransition("cross-fade")` plays on every navigation no relation
109
+ was written for. Two situations, two answers:
128
110
 
129
- - **App-shaped UI** (bars, tabs, pages one goes into): don't. The silence
130
- between sibling tabs is part of the grammar, and a default erases it. Declare
131
- the relations by hand.
132
- - **Content-shaped site** (documents, articles, browsing): a global cross-fade
133
- can be right — every navigation is a soft change of subject and no pair
134
- deserves a direction. This is the case the export exists for.
111
+ - **App-shaped UI** (bars, tabs, pages one goes into): don't. The silence between
112
+ sibling tabs is part of the grammar, and a default erases it.
113
+ - **Content-shaped site** (documents, articles, browsing): a global cross-fade can
114
+ be right — every navigation is a soft change of subject. This is the case the
115
+ export exists for.
135
116
 
136
117
  A default has no direction (nothing says which of two arbitrary pages is
137
- "before" the other), so only directionless movements make sense there: navi's
138
- `slide-*` and `cover-*` are written on the direction, and as the default they
139
- play nothing. Written relations, and `"none"`, always win over it.
118
+ "before" the other), so navi's `slide-*` and `cover-*`, written on the direction,
119
+ play nothing there. Written relations, and `"none"`, always win over it.
140
120
 
141
121
  ## When one navigation knows better
142
122
 
143
- A relation is written on a PAIR, so it holds for every way of reaching the
144
- page — and some ways are walked against the map. Two pairs that are travelled
145
- in both directions, with one direction common and one rare:
146
-
147
- - a game ↔ a player's profile: the name of a player, tapped from the game, is
148
- the common way in; a badge on a profile that leads back to the game it was
149
- won in is the rare one;
150
- - a profile ↔ the cards it describes: going down into the cards is the
151
- structural descent; a card that leads up to the player it describes goes back
152
- out.
153
-
154
- Written for the common direction, the rare one plays backwards. And `"none"`
155
- cannot fix it: a relation written for a way holds for every crossing of it,
156
- the back button's included (see below), so silencing the bad direction
157
- silences the good one too.
158
-
159
- So the navigation itself may ask, and what it asks holds for **that navigation
160
- and no other**:
123
+ A relation is written on a PAIR, so it holds for every way of reaching the page —
124
+ and some ways are walked against the map: a player's name tapped from a game is
125
+ the common way into a profile, a badge on that profile leading back to the game
126
+ it was won in is the rare one, and plays backwards. `"none"` cannot fix it: a
127
+ relation written for a way holds for every crossing of it, the back button's
128
+ included. So the navigation itself may ask, and what it asks holds for **that
129
+ navigation and no other**:
161
130
 
162
131
  ```jsx
163
132
  // The rare way round: the pair's movement, turned round.
@@ -171,56 +140,37 @@ and no other**:
171
140
  ```
172
141
 
173
142
  ```js
174
- // The same thing said by a call rather than by an element.
175
143
  navTo(GAME_ROUTE.buildUrl({ id }), { routeTransition: { direction: "back" } });
176
144
  GAME_ROUTE.navTo({ id }, { routeTransition: { direction: "back" } });
177
145
  ```
178
146
 
179
- The request is `"slide-x"`-style shorthand or `{ type, duration, direction }`,
180
- the same forms `defineRouteTransition` takes, plus `direction`. It overrides
181
- **field by field**: what it does not name, the relation (or the default) still
182
- answers for. So:
147
+ The request takes the forms `defineRouteTransition` takes, plus `direction`, and
148
+ overrides **field by field** — what it does not name, the relation (or the
149
+ default) still answers for:
183
150
 
184
151
  - `{ direction: "back" }` keeps the pair's movement and only turns it round;
185
- - `"zoom"` swaps the movement; the way round and the pace are still the
186
- pair's;
152
+ - `"zoom"` swaps the movement; the way round and the pace are still the pair's;
187
153
  - `"none"` cuts, where the pair — or the default — would have played;
188
154
  - `{ duration: 500 }` re-times what was already going to play.
189
155
 
190
- A pair no relation was ever written for answers the same way: silence is what
191
- the routes say, and a link that asks for a movement gets it, forward unless it
192
- says otherwise. That is the whole shape of the control:
193
- `defineRouteTransition` is what the app's map says and applies by default; a
194
- link, or a programmatic `navTo`, overrides it for the length of one navigation.
195
- Navigate again by any other means and the relation is back in charge — with one
196
- exception, which is what makes a request complete: a history traversal undoing
197
- that navigation plays what it asked for, reversed (next section).
198
-
199
- The link wears what it asks as an attribute, so a plain `<a>` says it too (a
200
- type name, or the object as JSON):
201
-
202
- ```html
203
- <a href="/game/42" data-navi-route-transition-request='{"direction":"back"}'
204
- >…</a
205
- >
206
- ```
207
-
208
- **Not yet: a movement chosen by HOW one navigated.** Playing one movement for
209
- the back button and another for a link would be written on the same request —
210
- `{ back: "slide-x", forward: "slide-y" }`. The notation is kept in mind for the
211
- day it is asked for.
156
+ A pair no relation was written for answers the same way: a link asking for a
157
+ movement gets it, forward unless it says otherwise. Navigate again by any other
158
+ means and the relations are back in charge — except the history traversal undoing
159
+ that navigation, which plays what it asked for, reversed (next section). The link
160
+ wears its request as an attribute, so a plain `<a>` says it too:
161
+ `data-navi-route-transition-request` holds a type name, or the object as JSON
162
+ (`'{"direction":"back"}'`).
212
163
 
213
164
  ## A traversal retraces its crossing
214
165
 
215
- The back button, `navBack()`, `history.back()`, the browser's "next": a
216
- traversal is not a walk on the map, it undoes one (or redoes one). The entry a
217
- push creates remembers the crossing that created it — what played, which way,
218
- from which url — in its own state. A back onto the page that crossing came
219
- from plays it reversed; a forward onto an entry whose crossing came from the
220
- page being left plays it again as it was.
166
+ The back button, `navBack()`, `history.back()`, the browser's "next": a traversal
167
+ is not a walk on the map, it undoes one (or redoes one). The entry a push creates
168
+ remembers the crossing that created it — what played, which way, from which url
169
+ — so a back onto the page that crossing came from plays it reversed, and a
170
+ forward plays it again as it was.
221
171
 
222
- That replay outranks everything the relations **deduce**, and nothing the
223
- author **wrote** for that exact way:
172
+ That replay outranks everything the relations **deduce**, and nothing the author
173
+ **wrote** for that exact way:
224
174
 
225
175
  | back from B to A | plays |
226
176
  | ----------------------------------------------- | ----------------------------- |
@@ -231,39 +181,27 @@ author **wrote** for that exact way:
231
181
  | `defineRouteTransition(B, A, "none")` | nothing |
232
182
  | `defineRouteTransition(B, A, "flip")` | flip, forward |
233
183
 
234
- The line is drawn there because the two halves answer two different needs.
235
- Reciprocity is the default, and the one tool for breaking it is writing the
236
- way back by hand — so that tool must reach the back button, which is the way
237
- back most often taken: a `"none"` written for `B → A` silences every return
238
- from B to A, whatever pressed it. Everything else `findRelation` answers is a
239
- deduction from a sentence about something else (the reverse of a pair, a
240
- destination reached from anywhere), and a traversal that knows the crossing it
241
- undoes knows better than a deduction.
242
-
243
- What this settles:
244
-
245
- - a page reached from anywhere whose links walk the map the other way (the
246
- author of a place's sheet, from the place): the link says
247
- `routeTransition={{ direction: "forward" }}`, and the back undoes exactly
248
- that — there is no reverse pair to write, and none that would collide with
249
- the other pages reaching the place;
250
- - a `routeTransition` request covers its own way back: a request is about one
251
- crossing, so nothing written for the pair outranks its replay;
252
- - the "two ways of a pair" trade-off above is about pairs written in one
253
- direction: the reverse is deduced, and the traversal wins over it.
184
+ The line is drawn there because writing the way back by hand is the one tool for
185
+ breaking reciprocity, so it must reach the back button: a `"none"` written for
186
+ `B → A` silences every return from B to A, whatever pressed it. Everything else
187
+ is a deduction from a sentence about something else, and a traversal that knows
188
+ the crossing it undoes knows better. So a link walking the map the other way from
189
+ a page reached from anywhere (the author of a place's sheet, from the place) says
190
+ `routeTransition={{ direction: "forward" }}`, and the back undoes exactly that —
191
+ no reverse pair to write, none to collide with the other pages reaching the
192
+ place.
254
193
 
255
194
  The relations alone answer a traversal that retraces no remembered crossing:
256
- several entries at once, or an entry another document wrote. And a link to the
257
- page one just came from is a traversal too where the browser exposes its stack
258
- (navi turns such a push into a back, see `browser_integration/via_history.js`),
259
- so it retraces as the button does — under the same rule.
195
+ several entries at once, or an entry another document wrote. A link to the page
196
+ one just came from is a traversal too, where the browser exposes its stack (see
197
+ [navigation.md](./navigation.md#where-a-navigation-lands-the-scroll)).
260
198
 
261
199
  ## Pages between fixed bars: the transition area
262
200
 
263
201
  By default the movement plays on the document itself — right when pages are the
264
- whole viewport. With fixed bars it is not: the root snapshot spans the viewport
265
- and the bars' regions are blank in it, so the moving picture drags a blank band
266
- across the screen where they stand. Wrap the pages instead:
202
+ whole viewport. With fixed bars it is not: the root picture spans the viewport and
203
+ the bars' regions are blank in it, so the moving picture drags a blank band across
204
+ the screen where they stand. Wrap the pages instead:
267
205
 
268
206
  ```jsx
269
207
  <RouteTransitionArea className="app">
@@ -271,157 +209,143 @@ across the screen where they stand. Wrap the pages instead:
271
209
  </RouteTransitionArea>
272
210
  ```
273
211
 
274
- The movement then plays on that region's own pictures, and the bars never move
275
- — without being named one by one. An app with fixed bars should consider this
276
- part of declaring transitions at all, not an option.
277
-
278
- The pages are cut twice: at the area's own bounds, and at the app's **safe
279
- area** (see [safe_area.md](./safe_area.md)). The second cut is not a detail. A
280
- fixed bar is fixed to the window while the area is a long box in the document —
281
- the room the bar gives back is padding, so the content runs under it by design.
282
- The pictures are drawn in the top layer, above everything the document can
283
- clip, so without that cut a page taller than the screen, or a scrolled one,
284
- would be watched sliding over the bars for the whole movement. The band is read
285
- from the safe area rather than from the bars, so every kind of furniture is
286
- covered at once and one that unmounts mid-movement is followed without anything
287
- being told.
288
-
289
- For the same reason, how far a page travels is the **window** it is seen
290
- through, not the page's own size: a page is as tall as its content, and a
291
- vertical movement measured on the picture would send it several screens away —
292
- off screen for most of the transition, flying past at the end.
293
-
294
- A page that was **scrolled** is photographed where the reader was, and travels
295
- from there: the document is not put back to its top until the picture has been
296
- taken. Without that wait the picture keeps only the band the browser had
297
- already painted at the new offset, and the movement carries a fragment of the
298
- page instead of the page.
299
-
300
- With an area marked, each **fixed bar around it is photographed on its own** for
301
- the length of the movement, and whether it is the frame or part of what changes
302
- is derived rather than declared — it is a fact about the pair of states, not
303
- about the bar:
304
-
305
- - a bar **both states have** is one element, so its two pictures pair into one
306
- group and it holds where it stands. The pages move behind it, and a bar whose
307
- content changes with the route cross-fades without being named by hand.
308
- - a bar **one state has** meets no counterpart. It belongs to the page that
309
- has it and travels with that page — leaving by the keyframes the page being
310
- left leaves by, arriving by the ones the page arriving arrives by — instead
311
- of appearing or vanishing in a frame. A page that takes the whole screen (a
312
- full-screen wizard whose banner is its own header) is this case, on the way
313
- in and on the way back.
314
-
315
- Every bar stands over the pages, as it does at rest: the page runs under its
316
- bar by design, and what a bar paints outside its box — a button standing up
317
- out of a tab bar, a shadow on the page — is seen for the length of the
318
- movement instead of being cut at the bar's edge. The one exception is a page
319
- **covered** by the other one (a `cover-*` movement): the sheet must come over
320
- that page's furniture too, so the bars of the covered page go under the pages
321
- and the covered page is cut at its own band instead. What such a bar paints
322
- outside its box is cut by its own page for those few hundred milliseconds — a
323
- bar cannot be both over its own page and under the other one. The price of
324
- being photographed is that a bar cannot answer the pointer for the length of
325
- the movement, which is what a route transition wants anyway: both pages are
326
- pictures too, and a press landing on either would be an accident. `RouteTravel`
327
- is the opposite case — a finger is on the box — and leaves the bars live.
328
-
329
- A bar the application names itself keeps its name and its own movement: navi
330
- names only what is unnamed.
331
-
332
- A **modal `Dialog` open on a page** is furniture of the same kind: its box is
333
- photographed on its own and travels with its page, or holds where it stands if
334
- both states have it. Its **wall** cannot be photographed (see
212
+ The movement then plays on that region's own pictures, and the bars never move —
213
+ without being named one by one. For an app with fixed bars this is part of
214
+ declaring transitions at all, not an option. It is a `Box`, so the layout the
215
+ pages need is written on it directly; an app that already has an element holding
216
+ its pages marks that one with `data-navi-route-transition-area` instead.
217
+
218
+ The pictures are drawn in the top layer, above everything the document can clip,
219
+ so they are cut: at the area's own bounds, at the app's rectangle (the glass
220
+ beside an app narrowed with `--navi-app-max-width`), and at the bands the
221
+ furniture keeps free — the area runs under the fixed bars by design, and uncut, a
222
+ page taller than the screen would be watched sliding over them. The bands are
223
+ read, never declared, so furniture that grows, shrinks or unmounts mid-movement
224
+ is followed: the app's **safe area** ([safe_area.md](./safe_area.md)) for
225
+ everything pinned to the window's edges, and **`useTransitionCover(ref, edge)`**
226
+ for what covers the pages from INSIDE the document, which the safe area does not
227
+ count — a sticky row of tabs above them, a header pinned to the top of a scroller
228
+ (`edge` is `"top"` by default). The pictures of a route transition or a travel
229
+ are then cut at that element instead of sliding over it.
230
+
231
+ A page travels the **window** it is seen through, not its own height, and a page
232
+ that was **scrolled** is photographed, and travels, from where the reader was.
233
+
234
+ Each **fixed bar around the area is photographed on its own**, and whether it is
235
+ the frame or part of what changes is derived from the pair of states: a bar
236
+ **both states have** holds where it stands while the pages move behind it (a bar
237
+ whose content changes with the route cross-fades, unnamed); a bar **one state
238
+ has** travels with the page that has it, by the keyframes that page leaves or
239
+ arrives by, instead of appearing or vanishing in a frame — a full-screen wizard
240
+ whose banner is its own header. Every bar stands over the pages, as at rest, so
241
+ what it paints outside its box (a button standing up out of a tab bar, a shadow)
242
+ is seen for the length of the movement — except over a page **covered** by the
243
+ other one (`cover-*`): the sheet comes over that page's furniture too, so its bars
244
+ go under the pages, and what they paint outside their box is cut by their page for
245
+ those few hundred milliseconds.
246
+
247
+ A bar the application names itself keeps its name and its own keyframes — navi
248
+ names only what is unnamed — but plays on the movement's clock, under the pages.
249
+ Its name must be worn before the movement starts: navi reads it before its
250
+ attributes go on the root, so a name written only under
251
+ `:root[data-navi-route-transition]` is not seen, and one of navi's takes its
252
+ place.
253
+
254
+ Everything photographed goes deaf to the pointer for the length of the movement
255
+ (see
256
+ [view_transitions.md](./view_transitions.md#an-element-captured-in-a-view-transition-cannot-be-pointed-at)),
257
+ which is what the pages want: a press on a picture would be an accident. The
258
+ **door** in a bar the two states share is the exception — it stands where its
259
+ picture is drawn, and the press that closes the page it is still opening is aimed
260
+ at what the reader sees. `pressableDuringRouteTransition` on its `<Link>` or
261
+ `<Button>` (`data-navi-route-transition-pressable` on anything else) keeps it
262
+ answering: the click is caught at the document and handed to it when it fell
263
+ inside its rectangle, and a navigation that is the exact way back of the movement
264
+ playing turns that movement round. Only on furniture that stands still: a control
265
+ travelling with the pages has a rectangle where it will be, not where it is seen
266
+ (navi warns about one inside the area). `RouteTravel` is the opposite case — a
267
+ finger is on the box — and leaves the bars live.
268
+
269
+ A **modal `Dialog` open on a page** is furniture of the same kind: it travels with
270
+ its page, or holds if both states have it. Its wall cannot be photographed (see
335
271
  [view_transitions.md](./view_transitions.md#the-top-layer-is-painted-through-the-roots-picture)),
336
- and dropping it for the movement makes the whole window blink under the press.
337
- So the wall is painted where it would be seen: into the pages' picture, into
338
- each bar's picture, and, for what no picture covers (the glass beside a
339
- narrowed app, the window below a short page), as a live element under the
340
- pictures that fades on the movement's clock. The outcome is derived from the
341
- pair of states like everything else here — a page leaving from under a wall
342
- leaves dimmed and the bars fade their dim with it; a page arriving under one
343
- arrives dimmed and the bars dim with it; a wall both states have holds. The real
344
- wall is switched off for exactly the length of the movement, so no frame shows
345
- two walls. Demo: [a dialog leaving with its page](../src/nav/demos/route_transition_dialog/route_transition_dialog.html).
346
-
347
- It is a `Box`, so the layout the pages need is written on it directly (`flex`,
348
- `className`, `style`, …). An app that already has an element holding its pages
349
- can mark that one with `data-navi-route-transition-area` rather than nesting
350
- another — the component does exactly that.
351
-
352
- **The area is a real box, and it has to be**: what gets photographed and
353
- clipped IS its rectangle. So `display: contents` cannot be used on it — an
354
- element with no box is never captured, and the movement then plays on nothing
355
- (the browser also aborts the transition). This is measured behaviour, not a
356
- precaution.
357
-
358
- Three misconfigurations are silent enough to be worth a console warning, each
359
- said once: an area that was not captured (the case above), several elements
360
- marked at once (they would share one `view-transition-name`, and the browser
361
- then refuses **every** view transition of the document), and pages travelling
362
- on the document while something else is captured on its own — the blank band.
363
- A warning here is a bug to fix, not a mechanism to lean on.
272
+ and dropping it for the movement makes the window blink under the press, so navi
273
+ paints it into the pictures: a page leaving from under a wall leaves dimmed, one
274
+ arriving under a wall arrives dimmed, a wall both states have holds — and no frame
275
+ shows two walls. Demo:
276
+ `src/nav/demos/route_transition_dialog/route_transition_dialog.html`.
277
+
278
+ **The area is a real box, and it has to be**: what gets photographed and clipped
279
+ IS its rectangle. An element with `display: contents` is never captured, and the
280
+ movement then plays on nothing.
281
+
282
+ Four misconfigurations are silent enough to be worth a console warning, each said
283
+ once: an area that was not captured (the case above); several elements marked at
284
+ once — they share one `view-transition-name`, and the browser refuses every route
285
+ transition while they do; pages travelling on the document while something else
286
+ is captured on its own — the blank band; and, on a browser without nested groups,
287
+ a `view-transition-name` written inside the area, whose element escapes the pages'
288
+ picture (see
289
+ [view_transitions.md](./view_transitions.md#a-name-is-unique-per-document)). A
290
+ warning here is a bug to fix, not a mechanism to lean on.
364
291
 
365
292
  ## Custom movements
366
293
 
367
- A type navi does not ship belongs to the application: the name is written on
368
- the root for the length of the transition
369
- (`data-navi-route-transition-type="<type>"`, next to
370
- `data-navi-route-transition="forward"|"back"`), and the app's CSS defines the
371
- movement against the view transition pseudo-elements — of the document, or of
372
- the marked area. See the JSDoc of `defineRouteTransition` for the selector
373
- shape, and the `spin` type in the demo for a working one.
374
-
375
- What is left to write is the `animation-name`s and nothing else: navi attaches
376
- to ANY named type what makes a movement look like one — each picture at the
377
- size it was taken at (a page half the height of the one it crosses would
378
- otherwise be seen inflating over the length of the movement), two solid pages
379
- rather than two panes of glass, and the animation held where it ends. The
380
- untyped cross-fade keeps the browser's defaults, since scaling one picture into
381
- the other is the whole idea there.
382
-
383
- The one knob a custom movement may want back: a movement that animates ONE of
384
- its two sides leaves the other on the browser's fade, and a fade needs its two
385
- half-transparent pictures to add up rather than cover each other
386
- (`mix-blend-mode: plus-lighter`, as the shipped `zoom` and the demo's `spin`
387
- both do). A movement where both pages move wants what navi poses.
388
-
389
- A custom type can also say its two keyframes **as values**, next to the rules
390
- that play them:
294
+ A type navi does not ship belongs to the application: the name is written on the
295
+ root for the length of the transition (`data-navi-route-transition-type="<type>"`,
296
+ next to `data-navi-route-transition="forward"|"back"`), and the app's CSS defines
297
+ the movement against the view transition pseudo-elements. Write it for both
298
+ groups, as navi does for its own: `root` when the document travels,
299
+ `navi-route-transition` when an area is marked — the root is then unnamed, and a
300
+ rule written for `(root)` alone never applies. The demo's `spin` type is one.
391
301
 
392
302
  ```css
393
303
  :root[data-navi-route-transition-type="spin"][data-navi-route-transition="forward"] {
394
- --navi-route-transition-leave: spin-out;
395
- --navi-route-transition-enter: spin-in;
304
+ &::view-transition-new(root),
305
+ &::view-transition-new(navi-route-transition) {
306
+ animation-name: spin-in;
307
+ }
396
308
  }
397
309
  ```
398
310
 
399
- That is what a fixed bar belonging to one of the two states is given to travel
400
- with the page it belongs to (see the transition area above). Nothing else reads
401
- them, and a type that publishes nothing leaves such a bar to the browser's fade.
311
+ What is left to write is the `animation-name`s: navi gives ANY named type what
312
+ makes a movement look like one — each picture at the size it was taken at, two
313
+ solid pages rather than two panes of glass, the animation held where it ends. The
314
+ untyped cross-fade keeps the browser's defaults.
402
315
 
403
- A type that plays one page **over** the other says which one is covered, the
404
- same way:
316
+ The one knob a custom movement may want back: a movement that animates ONE of its
317
+ two sides leaves the other on the browser's fade, and a fade needs its two
318
+ half-transparent pictures to add up rather than cover each other
319
+ (`mix-blend-mode: plus-lighter`, as the shipped `zoom` does). navi poses
320
+ `normal` from `:root[data-navi-route-transition][data-navi-route-transition-type]`,
321
+ in a sheet adopted after the document's own, so the rule taking it back must weigh
322
+ more than that selector
323
+ (`html:root[data-navi-route-transition][data-navi-route-transition-type="spin"]`
324
+ does); written on the type attribute alone, it is silently overruled.
325
+
326
+ A custom type can also say its two keyframes **as values** — what a fixed bar
327
+ belonging to one of the two states is given to travel with its page (a type that
328
+ publishes nothing leaves such a bar to the browser's fade) — and, when it plays
329
+ one page **over** the other, which one is covered: `old` when the page arriving
330
+ comes over the page being left, `new` when the page being left slides off the page
331
+ arriving. navi then draws the covered page under the other one, cut at its own
332
+ band, with its furniture under the pages, as the shipped `cover-*` types do:
405
333
 
406
334
  ```css
407
- :root[data-navi-route-transition-type="sheet"][data-navi-route-transition="forward"] {
408
- --navi-route-transition-covered: old;
335
+ :root[data-navi-route-transition-type="spin"][data-navi-route-transition="forward"] {
336
+ --navi-route-transition-leave: spin-out;
337
+ --navi-route-transition-enter: spin-in;
409
338
  }
410
- :root[data-navi-route-transition-type="sheet"][data-navi-route-transition="back"] {
411
- --navi-route-transition-covered: new;
339
+ :root[data-navi-route-transition-type="sheet"][data-navi-route-transition="forward"] {
340
+ --navi-route-transition-covered: old; /* and "new" on the way back */
412
341
  }
413
342
  ```
414
343
 
415
- `old` says the page arriving comes over the page being left, `new` that the
416
- page being left slides off the page arriving. Navi then draws the covered page
417
- under the other one, cuts it at its own band, and puts its furniture under the
418
- pages so the sheet comes over it (see the transition area above). The shipped
419
- `cover-*` types say exactly this; a movement where neither page covers the
420
- other publishes nothing.
421
-
422
- Those three values are read by navi from the root, so they are written there,
423
- and navi registers them as not inherited: writing them costs nothing. Any other
424
- value a custom movement's pictures read goes on `::view-transition` instead:
344
+ Those three values are read by navi from the root, so they are written there, and
345
+ navi registers them as not inherited: writing them costs nothing. Any other value
346
+ a custom movement's pictures read goes on `::view-transition`, never on the root
347
+ (see
348
+ [view_transitions.md](./view_transitions.md#what-makes-a-transition-restyle-the-whole-document)):
425
349
 
426
350
  ```css
427
351
  :root[data-navi-route-transition-type="spin"] {
@@ -431,17 +355,12 @@ value a custom movement's pictures read goes on `::view-transition` instead:
431
355
  }
432
356
  ```
433
357
 
434
- The pictures inherit it from there, and no element of the document does.
435
- Written on the root, every element would, and the page being left would be
436
- restyled in full in the frame it is photographed (see
437
- [What a transition costs](#what-a-transition-costs)).
438
-
439
358
  ## Two routes matching one url
440
359
 
441
- A relation is written between two pages, and it is resolved through **which page
442
- is current** — not through the url, and not through the branch the router
443
- renders. So a url claimed by two of the routes named in relations makes the
444
- movement depend on the order the relations were declared in:
360
+ A relation is resolved through **which page is current** — not through the url,
361
+ and not through the branch the router renders. So a url claimed by two of the
362
+ routes named in relations makes the movement depend on the order the relations
363
+ were declared in:
445
364
 
446
365
  ```js
447
366
  const ALERTS_ROUTE = route("/me/alerts");
@@ -453,12 +372,11 @@ defineRouteTransition(ALERTS_ROUTE, ALERT_CREATE_ROUTE, "cover-bottom");
453
372
  ```
454
373
 
455
374
  On `/me/alerts/create` both detail and create are current; the page mentioned
456
- first wins, and the composer slides in from the right instead of covering. It is
457
- the one place where declaration order is load-bearing, and nothing on screen says
458
- so — a wrong movement is still a movement, and reads as a deliberate choice.
459
-
460
- The fix is on the route, not on the relation: make one of them decline the url
461
- (see [navigation.md](./navigation.md#which-values-a-param-accepts)).
375
+ first wins, and the composer slides in from the right instead of covering.
376
+ Nothing on screen says so — a wrong movement is still a movement, and reads as a
377
+ deliberate choice. The fix is on the route, not on the relation: make one of them
378
+ decline the url (see
379
+ [navigation.md](./navigation.md#which-values-a-param-accepts)).
462
380
 
463
381
  ```js
464
382
  const ALERT_DETAIL_ROUTE = route("/me/alerts/:alertId", {
@@ -466,129 +384,100 @@ const ALERT_DETAIL_ROUTE = route("/me/alerts/:alertId", {
466
384
  });
467
385
  ```
468
386
 
469
- Which is worth doing whether or not a transition is involved: two routes matching
470
- one url is also two route actions running, and two branches competing for the
471
- `<Route>` the router renders. Left in place, navi says it once in the console,
472
- naming both routes.
387
+ Worth doing whether or not a transition is involved: two routes matching one url
388
+ also run two route actions, and compete for the branch the router renders (see
389
+ [navigation.md](./navigation.md#rendering-routes)). Left in place, navi says it
390
+ once in the console, naming both routes.
473
391
 
474
392
  ## Route transitions and `RouteTravel` — one pair, one system
475
393
 
476
- `RouteTravel` and `defineRouteTransition` answer different questions:
477
-
478
394
  - **`RouteTravel`** is a ROW: a total order of tabs, plus the drag gesture that
479
395
  walks it. Use it when the pages are genuinely a row the finger should push.
480
- - **`defineRouteTransition`** declares individual relations, with no gesture
481
- and no order beyond each pair.
396
+ - **`defineRouteTransition`** declares individual relations, with no gesture and
397
+ no order beyond each pair.
482
398
 
483
- A given PAIR of routes must be animated by one of the two, never both: a
484
- travel's pictures can be under a finger, and a transition starting on top would
485
- skip them mid-slide — and a page one can drag has promised a translation, which
486
- a cross-fade would break. The runtime enforces the priority (a travel in flight
487
- wins; the route transition is skipped with a console warning); the warning is
488
- the sign of a misconfiguration to fix, not a mechanism to rely on.
399
+ A given PAIR of routes must be animated by one of the two, never both: a travel's
400
+ pictures can be under a finger, and a transition starting on top would skip them
401
+ mid-slide — and a page one can drag has promised a translation, which a
402
+ cross-fade would break. A travel in flight wins and the route transition is
403
+ skipped with a console warning: the sign of a misconfiguration to fix, not a
404
+ mechanism to rely on.
489
405
 
490
406
  The two DO live together in one app, on different pairs, including a
491
407
  `<RouteTravel>` nested inside a `<RouteTransitionArea>` — a row of sections the
492
408
  thumb pushes, inside pages one goes into. Write the relations on the routes the
493
- row does not own: the tabs of the row travel, and opening something from any of
494
- them plays its own movement. A relation written on a bare route covers every
495
- one of its params at once, which is what makes "from any section" one line
496
- rather than one per tab. Demo:
497
- [../src/nav/demos/route_transition/route_transition_with_travel.html](../src/nav/demos/route_transition/route_transition_with_travel.html)
498
-
499
- One trap that belongs to `RouteTravel` rather than to transitions, but bites
500
- here first: the travelling box must stay MOUNTED across the changes it
501
- animates. A `<RouteTravel>` rendered inside the `element` of each of the routes
502
- it travels between is destroyed mid-travel by the router. Give the row a single
503
- branch — its tabs as params of one route is the usual shape.
409
+ row does not own: the tabs travel, and opening something from any of them plays
410
+ its own movement. A relation written on a bare route covers every one of its
411
+ params at once, so "from any section" is one line rather than one per tab. Demo:
412
+ `src/nav/demos/route_transition/route_transition_with_travel.html`.
413
+
414
+ One trap that belongs to `RouteTravel` but bites here first: the travelling box
415
+ must stay MOUNTED across the changes it animates. A `<RouteTravel>` rendered
416
+ inside the `element` of each of the routes it travels between is destroyed
417
+ mid-travel by the router. Give the row a single branch — its tabs as params of one
418
+ route is the usual shape.
504
419
 
505
420
  ## A transition says nothing about data
506
421
 
507
- A relation is about the map of the app, and about nothing else. Defining one
508
- does not change what the page arriving loads, reloads, or keeps:
509
-
510
- - an action that `COMPLETED` still holds its response, on the way back as on the
511
- way in — a page wanting fresh data says `.rerun()`, with or without a
512
- movement;
513
- - a `<List.Items>` reading through `GET_RANGE` still revalidates the window it
514
- draws when it is mounted again.
515
-
516
- What a transition takes is the document's **rendering** for the one frame the
517
- browser needs to photograph the page being left (see `rendering_hold.js`), and
518
- it gives it back in the same callback. The hold is about a picture, not about
519
- data: nothing waits on it, nothing is skipped because of it.
520
-
521
- That is the design, and it is held by `tests/route_transition_list_revisit/`:
522
- one app mounted twice, the two mounts differing by a single
523
- `defineRouteTransition` line, walked back and forth **ten times each** with what
524
- goes to the network counted on every revisit. The loop is the point — a
525
- difference that came and went would pass a single comparison often enough to
526
- look like an invariant. Three decors, chosen to be opposites: a long list
527
- virtualized against its scroller, the same one held on a row the url names, and
528
- a one-row list against the document scroller with its count known from
529
- elsewhere.
530
-
531
- One thing does make a page lose its revisit under a movement and not without
532
- one, and it is not about data either: a back taken before the page being opened
533
- has rendered, which returns to a page that never left. It has its own section
534
- below. See
535
- [list_refresh.md](./list_refresh.md#who-decides-the-re-read--and-who-does-not)
536
- for which source refreshes on a revisit and which does not.
422
+ A relation is about the map of the app and nothing else: it does not change what
423
+ the page arriving loads, reloads or keeps. An action that `COMPLETED` still holds
424
+ its response, on the way back as on the way in — a page wanting fresh data says
425
+ `.rerun()`, with or without a movement — and a `<List.Items>` reading through
426
+ `GET_RANGE` still revalidates the window it draws when mounted again (see
427
+ [list_refresh.md](./list_refresh.md#who-decides-the-re-read--and-who-does-not)).
428
+ What a transition holds is the document's **rendering**, while the page being
429
+ left is photographed, and it gives it back in the same callback: nothing waits on
430
+ it, nothing is skipped because of it. The one thing that makes a revisit differ
431
+ under a movement is a back taken before the page being opened has rendered — see
432
+ [below](#waiting-for-a-navigation-the-address-is-not-the-page).
537
433
 
538
434
  ## The page being left stays until its movement is over
539
435
 
540
- The picture of the page being left is taken before the change; the page
541
- arriving is built inside the transition's update callback, and everything done
542
- there delays the first frame of the movement. Taking a page down is a large
543
- part of it — every hook of every component in it cleaned up: on a page of
544
- thirty game cards, about 67 of the callback's 90 ms with the CPU throttled 4×,
545
- more than building the page arriving. Its picture already exists, so it only
546
- has to stay out of the second one: a container that changes page during the
547
- callback keeps the page it was showing, hidden (`display: none`), and takes it
548
- down once the movement is over (`keepLeavingPages` in `nav/route.jsx`).
549
- Measured on that page, first frame of the movement: 63 → 45 ms at full speed,
550
- 211 → 188 ms at 4×.
436
+ The picture of the page being left is taken before the change; the page arriving
437
+ is built inside the transition's update callback, and everything done there delays
438
+ the first frame of the movement. Taking a page down — every hook of every
439
+ component cleaned up — is a large part of it, and the page's picture already
440
+ exists, so it only has to stay out of the second one: a container that changes
441
+ page during the callback keeps the page it was showing, hidden (`display: none`),
442
+ and takes it down once the movement is over.
551
443
 
552
444
  What that means for a page and for the application around it:
553
445
 
554
446
  - **It is still mounted for the length of the movement.** Its effects keep
555
- running, and it still reads the address: a component reading the URL, a
556
- route or a signal directly re-renders in it too, hidden (see
557
- [What a transition costs](#what-a-transition-costs)). What it reads through
558
- `useAsyncData` does not: navi leaves a page alone as soon as its container
559
- would no longer show it.
447
+ running, and a component reading the URL, a route or a signal directly
448
+ re-renders in it too, hidden. What it reads through `useAsyncData` does not:
449
+ navi leaves a page alone as soon as its container would no longer show it.
560
450
  - **It is taken down after the page arriving is up.** A cleanup that puts back
561
- what it found — a document title, a class on `<body>`, a value in a shared
562
- store — puts a stale value back over the new page's. Such a registration is
563
- written as "the last to arrive wins": navi's `<Head>` keeps its titles as a
564
- stack, and a slot shows the last `SlotFill` to arrive.
565
- - **What it fills outside its own nodes leaves at once.** A `SlotFill` in a
566
- page kept while leaving steps out of its slot, so a bar each page fills shows
567
- the page arriving — or nothing, when that page fills none.
568
- - **Its popups leave with it.** A popup it holds open is no longer rendered: it
569
- is photographed on the old side only, leaves with its page, and is closed
570
- when the page is taken down (see
451
+ what it found — a document title, a class on `<body>`, a value in a shared store
452
+ — puts a stale value back over the new page's. Such a registration is written as
453
+ "the last to arrive wins": navi's `<Head>` keeps its titles as a stack, and a
454
+ slot shows the last `SlotFill` to arrive.
455
+ - **What it fills outside its own nodes leaves at once.** A `SlotFill` in a page
456
+ kept while leaving steps out of its slot, so a bar each page fills shows the
457
+ page arriving — or nothing, when that page fills none.
458
+ - **Its popups leave with it.** A popup it holds open is no longer rendered: it is
459
+ photographed on the old side only, leaves with its page, and is closed when the
460
+ page is taken down (see
571
461
  [view_transitions.md](./view_transitions.md#a-hidden-element-is-not-photographed-even-in-the-top-layer)).
572
- - **The focus it holds goes where its removal would have sent it** — to no
573
- element — when it is hidden: a hidden element keeps the focus otherwise,
574
- and would receive the keys pressed during the movement.
462
+ - **The focus it holds goes where its removal would have sent it** — to no element
463
+ — when it is hidden: a hidden element keeps the focus otherwise, and would
464
+ receive the keys pressed during the movement.
575
465
  - **Never while a modal dialog is open.** An open modal keeps everything but
576
- itself out of reach — the page arriving could not even take the focus — and
577
- one in a page kept while leaving would stay open for the whole movement. The
578
- page is then taken down at once, as without a movement.
466
+ itself out of reach — the page arriving could not even take the focus — and one
467
+ in a page kept while leaving would stay open for the whole movement. The page is
468
+ then taken down at once, as without a movement.
579
469
  - **Only where the page would have been taken down anyway.** A page rendered by
580
470
  the same element on both sides — two routes, one component — is the same
581
- instance carried over, as without a movement.
471
+ instance carried over.
582
472
 
583
473
  ## Waiting for a navigation: the address is not the page
584
474
 
585
475
  A navigation changes the URL first and the screen after — always. Under a
586
- transition the gap is wider on purpose: the rendering hold above spans the frame
587
- the picture is taken in, so for that frame the address says one page and the
588
- screen still shows the other. That is what makes the picture honest, and it is
589
- also long enough to be walked through by mistake.
590
-
591
- So a test that waits on the URL has not waited for anything to happen:
476
+ transition the gap is wider on purpose: the rendering hold spans the frame the
477
+ picture is taken in, so for that frame the address says one page and the screen
478
+ still shows the other. That is what makes the picture honest, and it is long
479
+ enough to be walked through by mistake. A test that waits on the URL has not
480
+ waited for anything to happen:
592
481
 
593
482
  ```js
594
483
  // ✗ resolves while the page being left is still the page on screen
@@ -598,12 +487,9 @@ await page.goBack();
598
487
  ```
599
488
 
600
489
  That back does not come back from anywhere. Nothing was unmounted, so nothing
601
- remounts — the list the user "returns to" is the element that never left, with
602
- no first load, no revisit, and no re-read (see
490
+ remounts — the list the user "returns to" is the element that never left, with no
491
+ first load, no revisit, and no re-read (see
603
492
  [list_refresh.md](./list_refresh.md#who-decides-the-re-read--and-who-does-not)).
604
- Every symptom of the arriving page being wrong follows from a walk that never
605
- took place.
606
-
607
493
  Wait for the page instead — anything only it can show:
608
494
 
609
495
  ```js
@@ -612,90 +498,55 @@ await expect(page.getByTestId("game_edit_link")).toBeVisible();
612
498
  await page.goBack();
613
499
  ```
614
500
 
615
- The window is **one frame**: the hold is given back inside the view
616
- transition's callback, which the browser runs at its next rendering
617
- opportunity. `tests/route_transition_list_revisit/` walks it the wrong way on
618
- purpose in its last case — a back pressed the instant the URL changes — and
619
- which side of that frame a cycle lands on belongs to the machine's load: under
620
- a movement the list that comes back is either the element that never left,
621
- asking for nothing, or one truly remounted, asking once, while the same walk
622
- without a movement is a real one every time. No thumb moves in one frame; an
623
- automated click continues in the same millisecond. This is a testing trap, not
624
- a user-facing behaviour.
501
+ The window is **one frame**: no thumb moves in one frame, but an automated click
502
+ continues in the same millisecond. A testing trap, not a user-facing behaviour.
625
503
 
626
504
  ## What a transition costs
627
505
 
628
506
  A transition adds one frame to a navigation: the browser photographs the page
629
- being left at its next rendering opportunity, and the page arriving is built
630
- only then, inside the update callback. Anything beyond that is avoidable, and
631
- each of these was measured doing it on a list of thirty cards (~4,700
632
- elements):
633
-
634
- - **Restyling the whole page being left in the frame it is photographed** —
635
- 17–21 ms at full speed, 60–90 ms at 4× CPU. Two browser facts cause it: a
636
- `::highlight()` rule every element matches, and a custom property changed on
637
- the root as the movement starts (see
507
+ being left at its next rendering opportunity, and the page arriving is built only
508
+ then, inside the update callback. Anything beyond that is avoidable:
509
+
510
+ - **Restyling the whole page being left in the frame it is photographed** — a
511
+ `::highlight()` rule every element matches, or a custom property changed on the
512
+ root as the movement starts (see
638
513
  [view_transitions.md](./view_transitions.md#what-makes-a-transition-restyle-the-whole-document)).
639
- navi's movements declare nothing inheritable on the root; a custom type
640
- publishing values of its own writes them on `::view-transition` (see
641
- [Custom movements](#custom-movements)).
514
+ navi's movements declare nothing inheritable on the root; a custom type writes
515
+ its values on `::view-transition`.
642
516
  - **The page being left reacting to what the navigation changed.** Every
643
517
  component in it that re-renders on the change does so inside the update
644
- callback, next to the page arriving being built. A link or a button pointing
645
- at a route (`<Link route>`, `<Button route>`) reads one flag — whether it is
646
- current — and stays still unless that flips: a profile opening leaves the
647
- links to a hundred other profiles exactly as they were (on the list of
648
- thirty cards: 90 re-renders gone, first frame 58 → 41 ms). A row read with
649
- `RESOURCE.useById` follows that row only, so the page arriving loading rows
650
- of its own into the same store leaves the cards of the page being left as
651
- they were. What moves last is the page's own reading of its route's action
652
- — its params, its data and its state all change as the route stops
653
- matching. `useAsyncData` gives no render to a page its container no longer
654
- shows: the page is about to be taken down, or hidden until the movement is
655
- over (on the list of thirty cards: about 450 component renders gone from the
656
- callback, first frame 10–20 ms sooner at 4× CPU, nothing measurable at full
657
- speed). A component reading the action's signals itself
658
- (`action.dataSignal.value`) does not get that, and re-renders in the hidden
659
- page.
518
+ callback, next to the page arriving being built. A `<Link route>` or
519
+ `<Button route>` reads one flag — whether it is current — and stays still unless
520
+ that flips; a row read with `RESOURCE.useById` follows that row only; and
521
+ `useAsyncData` gives no render to a page its container no longer shows. A
522
+ component reading an action's signals itself (`action.dataSignal.value`) gets
523
+ none of that, and re-renders in the hidden page.
660
524
  - **Restyling the page arriving more than once.** Built inside the update
661
525
  callback, it is styled as it lands; anything written afterwards that its
662
- elements inherit, or that re-selects them, restyles all of them again. On the
663
- list of thirty cards (~7,000 elements) three did, 16–19 ms each at full
664
- speed: the height of a list group's sticky label, written on the group once
665
- measured (navi keeps it from being inherited, which took the movement 17 ms
666
- sooner); the room a fixed bar takes (`--navi-fixed-bar-space-*`, feeding the
667
- inherited `--navi-safe-area-inset-*`), which changes on the root when a bar
668
- arrives or leaves with the page; and an app's `:has()` rule on the list
669
- container over its rows (`.navi_list_container:has(.card) …`), evaluated
670
- again as the rows land. An attribute the app sets on the container says the
671
- same thing as that `:has()`, for nothing.
672
- - **Building rows the first picture does not show.** A list coming back
673
- where it was draws, in the update callback, the rows that were on screen
674
- then — the `visibleCount` of the position it is handed back (see
675
- [scroll.md](./scroll.md#where-the-list-opens-and-where-it-is)) — and the
676
- rest of its window once the movement has started. The movement does not wait
677
- for them: it plays on the compositor, which keeps presenting frames while the
678
- main thread builds the rows. On the list of thirty cards, handed its whole
679
- position back: the movement starts 70–85 ms after the click instead of
680
- 185–270.
681
- - **Re-rendering what did not change, on the press itself.** A component that
682
- only wants a yes or a no reads a computed flag, not the signal it is derived
683
- from: a list whose rows each read the count of runs in flight re-rendered
684
- all thirty cards twice when a link in one of them was pressed — 774
685
- component renders before the navigation even started, 18 once the rows read
686
- whether the list is full instead.
687
- - **Nothing is gained by starting at the press.** The click's own work is the
688
- navigation, and the picture can only be taken on the frame after it; work
689
- moved to the pointer-down is paid again, for nothing, by every press that
690
- turns into a scroll.
526
+ elements inherit, or that re-selects them, restyles all of them again — the room
527
+ a fixed bar takes (`--navi-fixed-bar-space-*`, feeding the inherited
528
+ `--navi-safe-area-inset-*`) changing on the root as a bar arrives or leaves with
529
+ the page, an app's `:has()` rule on the list container over its rows (an
530
+ attribute the app sets on the container says the same thing for nothing).
531
+ - **Building rows the first picture does not show.** A list coming back where it
532
+ was draws, in the update callback, only the rows that were on screen then (the
533
+ `visibleCount` of the position it is handed back, see
534
+ [scroll.md](./scroll.md#where-the-list-opens-and-where-it-is)), and the rest of
535
+ its window once the movement has started — which plays on the compositor, and
536
+ does not wait for them.
537
+ - **Re-rendering what did not change, on the press itself.** A component that only
538
+ wants a yes or a no reads a computed flag, not the signal it is derived from.
539
+ - **Nothing is gained by starting at the press.** The picture can only be taken on
540
+ the frame after the click; work moved to the pointer-down is paid again, for
541
+ nothing, by every press that turns into a scroll.
691
542
 
692
543
  ## The rest, briefly
693
544
 
694
- - Pace: `--navi-route-transition-duration` (CSS, default 300ms) for everyone;
695
- a per-relation `{ type, duration }` for one relation, worn by that
696
- movement's pictures only.
545
+ - Pace: `--navi-route-transition-duration` (CSS, default 300ms) for everyone; a
546
+ per-relation `{ type, duration }` for one relation, worn by that movement's
547
+ pictures only.
697
548
  - The URL leads: transitions play on navigations somebody else started (a
698
549
  `<Link>`, the back button, `history.back()`). Nothing here navigates.
699
- - A browser without view transitions navigates with a cut. The app
700
- must remain fully usable that way — which it is, if the transitions state
701
- relations rather than carry information.
550
+ - A browser without view transitions navigates with a cut. The app must remain
551
+ fully usable that way — which it is, if the transitions state relations rather
552
+ than carry information.