@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
@@ -2,12 +2,12 @@
2
2
 
3
3
  `animation="lifting"` on a `Dialog` (and on a `Picker` in `mode="dialog"`) is
4
4
  for one situation: the thing the user pressed is the thing the popup shows,
5
- brought to the front to be looked at or written in. A card in a feed becomes
6
- its edit sheet; a drawing in a corner becomes the drawing full width. The page
5
+ brought to the front to be looked at or written in. A card in a feed becomes its
6
+ edit sheet; a drawing in a corner becomes the drawing full width. The page
7
7
  recedes behind a wall, the box leaves its place, travels and grows, and comes
8
- back into its place on close — or, with `animation={{ open: "scaling", close: "lifting" }}`, only
9
- lands somewhere on close (see [lifting on the way back
10
- only](#lifting-on-the-way-back-only)).
8
+ back into its place on close — or, with
9
+ `animation={{ open: "scaling", close: "lifting" }}`, only lands somewhere on
10
+ close (see [lifting on the way back only](#lifting-on-the-way-back-only)).
11
11
 
12
12
  It is not a way to open a dialog with a nicer entrance. A dialog that shows
13
13
  something else than what was pressed — a menu, a confirmation, a form the
@@ -25,14 +25,14 @@ and everything below follows from that one fact.
25
25
  - [What it costs, and where the time goes](#what-it-costs-and-where-the-time-goes)
26
26
  - [The wall, and the frame before the movement](#the-wall-and-the-frame-before-the-movement)
27
27
  - [What the browser does around it](#what-the-browser-does-around-it)
28
- - [Measuring](#measuring)
29
28
 
30
29
  ## What is lifted is named
31
30
 
32
31
  `data-lift` marks the one node that IS the trigger once in front: a node inside
33
- the popup, or the popup itself when the whole dialog is that node. Nothing is
34
- inferred: a dialog holding the lifted card plus a badge above and buttons below
35
- lifts the card; a dialog that is the card wears `data-lift` itself.
32
+ the popup, or the popup itself when the whole dialog is that node. A dialog
33
+ holding the lifted card plus a badge above and buttons below lifts the card; a
34
+ dialog that is the card wears `data-lift` itself. The lift takes the first
35
+ `data-lift` the popup holds.
36
36
 
37
37
  ```jsx
38
38
  <Picker mode="dialog" animation="lifting" ui={<GameCard game={game} />} …>
@@ -46,31 +46,27 @@ That names one end. The other is the anchor — the box the opening came out of,
46
46
  and the box the closing goes back into unless `liftAnchor` names another (see
47
47
  [a row of cards](#a-row-of-cards-one-popup-that-walks)).
48
48
 
49
- The opening waits for it. What a popup holds often arrives after the tap —
50
- code fetched for the address, a row fetched for the popup — and a movement
51
- started before the lifted node exists would carry the card into an empty box.
52
- So the dialog is opened at once, held unpainted over a page left untouched,
53
- and the lift starts the moment `data-lift` is in the DOM. Past a second without it, the dialog is shown where it stands,
54
- with no movement, and dev warns.
49
+ The opening waits for the lifted node: a movement started before it exists would
50
+ carry the card into an empty box. The dialog is opened at once and held
51
+ unpainted — its wall already fading in, the page's scroll already held — and the
52
+ lift starts the moment `data-lift` is in the DOM. Past a second without it, the
53
+ dialog is shown where it stands, with no movement, and dev warns.
55
54
 
56
55
  **Render the lifted node at once.** Waiting is a safety net, not the design: a
57
56
  sheet whose card only exists once its data has landed pays that latency on
58
57
  every opening. Render the card immediately from what the trigger already holds
59
58
  (a read-only copy, a skeleton with the card's box), and let the fields fill in
60
- inside it. `mount="from-first-open"` keeps it built across closes;
61
- `mount="while-opened"` rebuilds it on every opening — the right choice for a
62
- form seeded from a `defaultValue`, and the most expensive one (see
63
- [costs](#what-it-costs-and-where-the-time-goes)).
59
+ inside it — and keep it built across closes unless it must be rebuilt on every
60
+ opening (see [costs](#what-it-costs-and-where-the-time-goes)).
64
61
 
65
62
  ## The trigger's box is the card's box
66
63
 
67
64
  The movement starts from the trigger's box — the `Picker`'s root, or a
68
- `Dialog`'s `anchor`. That box must be the card's: a trigger wider than the
69
- card it shows (a bare picker stretched by a column flex, an `expandX` whose
70
- `ui` does not fill it) starts the lift from a box the card does not fill. The
71
- painted box then wears the card's width from the first frame while the card
72
- sits in its corner, and the width never reads as changing. Measured on the
73
- demo bench: the picker root at 364px around a 260px card.
65
+ `Dialog`'s `anchor`. That box must be the card's: a trigger wider than the card
66
+ it shows (a bare picker stretched by a column flex, an `expandX` whose `ui` does
67
+ not fill it) starts the lift from a box the card does not fill. The painted box
68
+ then wears the card's width from the first frame while the card sits in its
69
+ corner, and the width never reads as changing.
74
70
 
75
71
  Check it once, at rest: the picker root's `getBoundingClientRect()` equals the
76
72
  card's. `variant="bare"` with a `ui` that is the card, in a container that does
@@ -79,76 +75,69 @@ usual shape.
79
75
 
80
76
  ## Two kinds: a card, or a scene
81
77
 
82
- `lift="box"` (default) is one object at two sizes — a card gaining fields, a
83
- row becoming a sheet. Each picture keeps its own size, from the box's top-left
84
- corner: the card's text does not zoom, the box gains room, and the picture of
85
- what it became is uncovered as the box grows. The picture being left stays
86
- whole for most of the way; the arriving one only shows near the end.
87
-
88
- `lift="scene"` is one scene through two frames — a drawing enlarged. Both
89
- pictures are drawn at the box's width and centred in it, so the content does
90
- scale with the box, and a thumbnail lands on the middle of the whole at every
91
- width.
78
+ `lift="box"` (default) is one object at two sizes — each picture keeps its own
79
+ size, and the box gains room; `lift="scene"` is one scene through two frames —
80
+ both pictures are drawn at the box's width, centred. Choose the kind by what
81
+ grows: text and controls keep their scale (`box`), a drawing does not (`scene`).
92
82
 
93
83
  Two traps, both about the thumbnail:
94
84
 
95
85
  - **A scene under `lift="box"`** keeps the thumbnail at its own size in the
96
- corner of a growing, empty box, then swaps. Choose the kind by what grows:
97
- text and controls keep their scale (`box`), a drawing does not (`scene`).
98
- Dev warns when the lifted node is, box for box, an `<svg>`, `<img>`,
99
- `<picture>`, `<canvas>` or `<video>` under `box`.
100
- - **A thumbnail that is not the scene framed.** `scene` assumes the thumbnail
101
- is either the whole drawing smaller, or a band cut from its middle as wide as
102
- it. A thumbnail letterboxed inside a box of another aspect ratio (an SVG
86
+ corner of a growing, empty box, then swaps. Dev warns when the lifted node is,
87
+ box for box, an `<svg>`, `<img>`, `<picture>`, `<canvas>` or `<video>` under
88
+ `box`.
89
+ - **A thumbnail that is not the scene framed.** `scene` assumes the thumbnail is
90
+ either the whole drawing smaller, or a band cut from its middle as wide as it.
91
+ A thumbnail letterboxed inside a box of another aspect ratio (an SVG
103
92
  `meet`-fitted into a 2:1 frame), or a crop taken off-centre, cannot land on
104
- the whole: the drawing seems to slide to one side, then reappear centred.
105
- Give the trigger's frame the scene's own aspect ratio, and crop from the
106
- middle if you crop.
93
+ the whole: the drawing seems to slide to one side, then reappear centred. Give
94
+ the trigger's frame the scene's own aspect ratio, and crop from the middle if
95
+ you crop.
107
96
 
108
97
  ## The lifted node paints itself
109
98
 
110
99
  The moving box wears the lifted node's background for the length of the
111
- movement, so where the box has grown past the picture it carries, it is the
112
- card that has grown. That paint is read off `data-lift` — and, when that node
113
- paints nothing, off the first descendant that has its box and paints.
114
-
115
- So a transparent wrapper around the card is fine, and a card whose colour
116
- lives on a nested element of a different size is not: publish the colour on
117
- the box that is the card. A gradient or an image travels as well
118
- (`background-image`). A shadow or a halo (`box-shadow`, `filter:
119
- drop-shadow()`) is in the picture but cut at the box's edge, with the content
120
- the box uncovers — except when the two boxes are the same size (`box`) or the
121
- same aspect ratio (`scene`): both pictures then fit the box the whole way,
122
- nothing needs uncovering, and the ink travels with the box.
100
+ movement, so where the box has grown past the picture it carries, it is the card
101
+ that has grown. That paint is read off `data-lift` — and, when that node paints
102
+ nothing, off the first descendant that has its box and paints. So a transparent
103
+ wrapper around the card is fine, and a card whose colour lives on a nested
104
+ element of a different size is not: publish the colour on the box that is the
105
+ card (a gradient or an image travels as well). A shadow or a halo is in the
106
+ picture but cut at the box's edge, except when the two boxes are the same size
107
+ (`box`) or the same aspect ratio (`scene`): both pictures then fit the box the
108
+ whole way, and the ink travels with it.
123
109
 
124
110
  Corners are not paint: they are written per box, on purpose — the same card at
125
- two sizes does not want the same round, a 6px corner stops showing on a big
126
- card. The moving box leaves with the corners of the box it leaves and arrives
127
- with those of the box it arrives on, the two authored values interpolated on
128
- the movement's own clock. Each end's corners are the first round found going
129
- down through the nodes that are that box, so a bare trigger carrying its
130
- corners itself, or on the card inside it, reads the same.
111
+ two sizes does not want the same round. The moving box leaves with the corners
112
+ of the box it leaves and arrives with those of the box it arrives on, each read
113
+ as the first round found going down through the nodes that are that box — so a
114
+ bare trigger carrying its corners itself, or on the card inside it, reads the
115
+ same.
131
116
 
132
117
  ## Same width, or a wider box
133
118
 
134
- On a `Picker`, `dialogSizeFromAnchor` makes the trigger's box a floor for the
135
- dialog; `dialogMaxWidth="var(--anchor-width)"` makes it the ceiling too (on a
136
- `Dialog` the same props are `sizeFromAnchor` and `maxWidth`). Together they
137
- say "the same card, brought forward": the box travels and may grow in height,
138
- never in width. Left out, `dialogExpandX` lets the sheet take the room it has,
139
- and the width change is animated — the card keeps its scale inside a box
140
- gaining room on the right, then the wider layout arrives.
119
+ On a `Picker`, `dialogSizeFromAnchor` alone makes the trigger's width both the
120
+ dialog's floor and its ceiling (a `dialogMaxWidth` of the caller's still wins
121
+ over the ceiling): the same card, brought forward — the box travels and may grow
122
+ in height, never in width. On a `Dialog`, `sizeFromAnchor` is only the floor,
123
+ and `maxWidth="var(--anchor-width)"` adds the ceiling. Left out, the sheet can
124
+ take the room it has (`dialogExpandX`), and the width change is animated — the
125
+ card keeps its scale inside a box gaining room on the right, then the wider
126
+ layout arrives.
141
127
 
142
128
  Both read well; what does not is a box that is neither: a card whose width the
143
129
  sheet changes by a few pixels for no reason the eye can name. Decide.
144
130
 
145
131
  ## A row of cards: one popup that walks
146
132
 
147
- A row of small drawings — trophies on a profile, photos, badges — where
148
- pressing one brings it to the front, big, and from there the next one is
149
- reached without going back to the row. The popup is then about the whole row,
150
- and the press only says where it opens: one `Dialog` for the row, lifting,
151
- holding a `SlideContainer` the walk moves through.
133
+ A row of small drawings — trophies on a profile, photos, badges — where pressing
134
+ one brings it to the front, big, and from there the next one is reached without
135
+ going back to the row. The popup is then about the whole row, and the press only
136
+ says where it opens: one `Dialog` for the row, lifting, holding a
137
+ `SlideContainer` the walk moves through. A picker per drawing is the first thing
138
+ one writes, and a dead end — each would have to hold the whole row to be
139
+ walkable (the third case of
140
+ [a shared popup](./popup_open.md#when-a-shared-popup-is-still-the-right-answer)).
152
141
 
153
142
  ```jsx
154
143
  const currentKeySignal = useSignal(undefined);
@@ -190,88 +179,55 @@ const currentKeySignal = useSignal(undefined);
190
179
  </Dialog>
191
180
  ```
192
181
 
193
- **One popup, because the popup is about the row.** A picker per drawing is the
194
- first thing one writes, and it is a dead end: each popup would have to hold the
195
- whole row to be walkable, so a row of N costs N×N slides, and a walk opened on
196
- the second drawing ends in a popup whose trigger is somewhere else. That is a
197
- reason of its own to share a popup, beside the two in
198
- [popup_open.md](./popup_open.md#when-a-shared-popup-is-still-the-right-answer):
199
- what the popup shows is more than what was pressed.
200
-
201
- **Which drawing it opens on is the command's `value`.** The press says it the
202
- way it says it everywhere ([opening it ON
203
- something](./popup_open.md#opening-it-on-something)), and `onOpen` writes it
204
- into the container's own signal — the press seeds the walk rather than keeping
205
- a second copy of it, and from then on the chevrons, the arrows and a thumb
206
- write the same signal (see [state_binding.md](./state_binding.md)).
207
-
208
- **`data-lift` moves with the walk, and has to be right on the first frame.**
209
- There is one lifted node per document, and here it is the current slide's
210
- drawing — a condition on the signal, not a mark written once on the popup. The
211
- lift takes the first `data-lift` it finds on the frame the popup opens:
212
- `mount="while-opened"` is what makes that frame the right one, since the
213
- content is built after `onOpen`, on the drawing the open named. Content kept
214
- across openings still carries the mark of the drawing the walk was left on, and
215
- the lift takes that one. The closing reads it the same way, and a mark left on
216
- a slide the walk moved off is a picture taken where that slide stands — off
217
- screen — so the box flies in from outside the surface. The bill for rebuilding
218
- is the row's, not one card's — every slide is built on every opening (see
219
- [costs](#what-it-costs-and-where-the-time-goes)).
220
-
221
- **The trigger's box is what travels, so the button is the drawing and nothing
222
- else.** That is [the trigger's box is the card's
223
- box](#the-triggers-box-is-the-cards-box) read backwards: everything inside the
224
- button is stretched into the popup's box on the way. A tile is usually more
225
- than its drawing — a count floating in a corner, a level written
226
- underneath — and those belong outside the button, positioned against the tile
227
- or placed under it. Layout, not a prop.
228
-
229
- **The anchor comes with the press.** A button opening a popup names itself as
230
- the anchor ([the anchor](./popup_open.md#the-anchor), third rule), so the lift
231
- starts on the drawing that was pressed, with no `anchor` prop and no
232
- `triggerNaviCommand`. Writing an `anchor` prop is how to lose that: the prop is
233
- what answers when no press does, and it wins over the press.
234
-
235
- **The arrows reach the walk from anywhere on the surface.** Only slides go in a
236
- `SlideContainer`, so the chevrons pinned to the edges of a full-screen surface
237
- are outside it, and the keyboard, once it lands on one of them, walks nothing.
238
- `data-slide-container-follows={SLIDES_ID}` on the `Dialog` — the outermost
239
- element, which is what holds the keyboard when nothing in it does — makes the
240
- whole surface a follower.
241
-
242
- **Where it comes back to is named too.** The opening's anchor is the drawing
243
- that was pressed, and a walk that moved on has something else in front by the
244
- time it closes: left alone, the silver cup flies home into the gold cup's
245
- tile. `liftAnchor` says the other end — same grammar as `anchor` (element, ref
246
- or id), read at the close rather than kept from the opening, so what names the
247
- tile currently in front is read then. An id built from the signal the walk is
248
- bound to is the shortest way to say it, the row's tiles carrying the matching
249
- ids. Left out, the box comes back where it came from, which is right exactly
250
- as long as nothing walked.
251
-
252
- When the walk signal is the address's — the popup's `signal`, so the sheet
253
- comes back open on the same card after leaving the page (see
254
- [popup_open.md](./popup_open.md#signal--the-app-holds-it-both-ways)) — a close
255
- that comes through the address, the back button, empties it before the popup
256
- knows it is closing, and a name built from it at that render says nothing:
257
- the box flies home into the first tile. Name the tile from the last key the
258
- walk held — a ref written while the signal holds one — not from the signal
259
- itself.
260
-
261
- **A press on the surface that dismisses is `data-navi-popup-outside`.** Marking
262
- the see-through box as backdrop (see
263
- [popup_backdrop.md](./popup_backdrop.md)) is read on the press itself. A close
264
- written by hand on a click has to tell a click from the end of a swipe — and
265
- that guard is the sign the marker was missed.
182
+ The drawing it opens on is the command's `value`
183
+ ([opening it ON something](./popup_open.md#opening-it-on-something)), and
184
+ `onOpen` writes it into the container's own signal: the press seeds the walk,
185
+ and from then on the chevrons, the arrows and a thumb write the same signal.
186
+
187
+ **`data-lift` moves with the walk, and has to be right on the first frame.** The
188
+ lifted node is the current slide's drawing — a condition on the signal — and the
189
+ lift takes the first `data-lift` on the frame the popup opens.
190
+ `mount="while-opened"` is what makes that the drawing the open named, the
191
+ content being built after `onOpen`: content kept across openings still carries
192
+ the mark of the drawing the walk was left on. The closing reads it the same way,
193
+ and a mark left on a slide the walk moved off is a picture taken off screen: the
194
+ box flies in from outside the surface.
195
+
196
+ **The button is the drawing and nothing else**: its box is what travels (see
197
+ [above](#the-triggers-box-is-the-cards-box)), so a count floating in a corner or
198
+ a level written underneath belongs outside the button, positioned against the
199
+ tile. **No `anchor` prop** either: the button pressed is the anchor
200
+ ([the anchor](./popup_open.md#the-anchor), third rule), and an `anchor` prop
201
+ wins over the press.
202
+
203
+ **The arrows reach the walk from anywhere on the surface.** The chevrons pinned
204
+ to the edges of a full-screen surface are outside the `SlideContainer` — only
205
+ slides go in it — so the keyboard, once it lands on one of them, walks nothing.
206
+ `data-slide-container-follows={SLIDES_ID}` on the `Dialog`, the outermost
207
+ element and what holds the keyboard when nothing in it does, makes the whole
208
+ surface a follower.
209
+
210
+ **Where it comes back to is named too.** A walk that moved on has something else
211
+ in front by the time it closes: left alone, the silver cup flies home into the
212
+ gold cup's tile. `liftAnchor` says the other end, read at the close — an id
213
+ built from the walk's signal, the tiles carrying the matching ids. When that
214
+ signal is the address's (the popup's `signal`, see
215
+ [popup_open.md](./popup_open.md#signal--the-app-holds-it-both-ways)), a close
216
+ through the address, the back button, empties it before the popup knows it is
217
+ closing, and a name built from it at that render names no tile: build it from
218
+ the last key the walk held — a ref written while the signal holds one.
219
+
220
+ **A press on the surface that dismisses is `data-navi-popup-outside`** (see
221
+ [popup_backdrop.md](./popup_backdrop.md#where-the-outside-begins)), read on the
222
+ press itself. A close written by hand on a click has to tell a click from the
223
+ end of a swipe — and that guard is the sign the marker was missed.
266
224
 
267
225
  ## Lifting on the way back only
268
226
 
269
- Sometimes the opening is not a lift and the closing is. A banner says "your
270
- level is computed"; pressing it opens a full-screen reveal, the crest big in a
271
- halo; collecting it sends the crest down into its place on the rank plate,
272
- which replaces the banner at that moment. The banner is not the reveal, so
273
- nothing morphs on the way in. But on the way out one box does travel into
274
- another, and that other box did not exist when the reveal opened.
227
+ Sometimes the opening is not a lift and the closing is. A banner opens a
228
+ full-screen reveal, the crest big in a halo; collecting it sends the crest down
229
+ into its place on the rank plate that replaces the banner — a box that did not
230
+ exist when the reveal opened.
275
231
 
276
232
  ```jsx
277
233
  <Dialog
@@ -292,143 +248,98 @@ another, and that other box did not exist when the reveal opened.
292
248
  </Dialog>
293
249
  ```
294
250
 
295
- **The opening is whatever `open` says** (`"auto"` included). Only the close lifts, so none of
296
- what a lifting opening brings is there: no wait for `data-lift`, no opaque
297
- wall. `data-lift` is read at the close alone.
298
-
299
- **The box it lands in can be rendered by the close.** `liftAnchor` is read
300
- once the close has been made, `onClose` included, inside the transition. A
301
- state written in `onClose` has rendered by the time the landing is looked up,
302
- so the landing box can come from that state. This is also the one place where
303
- that state can be written: before the close, the reveal would disappear from
304
- the picture being left; after it, the picture of the arrival is already taken.
305
- Do not write it by hand around the dialog either. Unmounting the dialog with
306
- the component that holds it takes it off screen without a close, and the lift
307
- never happens.
251
+ **The opening is whatever `open` says** (`"auto"` included): no wait for
252
+ `data-lift`, no opaque wall, and `data-lift` is read at the close alone. Nor
253
+ does the opening's own exit play: while a closing lift runs, the dialog's
254
+ transitions are off, or `scaling`'s exit would keep the dialog painted into the
255
+ picture of the state it closes into.
256
+
257
+ **The box it lands in is rendered by the close.** `liftAnchor` is read once the
258
+ close has been made, `onClose` included, inside the transition: a state written
259
+ in `onClose` has rendered by the time the landing is looked up. It is also the
260
+ one place that state can be written — before the close, the reveal would
261
+ disappear from the picture being left; after it, the picture of the arrival is
262
+ already taken — and not by unmounting the dialog with the component that holds
263
+ it, which takes it off screen without a close, and without a lift. The dialog
264
+ may go away with the state it writes: the picture of the reveal was taken
265
+ before. The id goes on the element that is exactly the small crest, not on the
266
+ plate around it, for the reason
267
+ [the trigger's box is the card's box](#the-triggers-box-is-the-cards-box).
308
268
 
309
269
  **It is waited for, briefly.** When `liftAnchor` names nothing yet, navi waits
310
- for it to appear, up to 300 ms, before it takes the new picture. The screen is
311
- frozen on the reveal meanwhile, so this covers a render and not a fetch: the
312
- landing must be drawable from what the page already holds. Past the wait, the
313
- dialog closes without landing (its picture fades out) and dev warns.
270
+ up to 300 ms for it to appear, the screen frozen on the reveal meanwhile — a
271
+ render, not a fetch: the landing must be drawable from what the page already
272
+ holds. Past the wait, the dialog closes without landing (its picture fades out)
273
+ and dev warns.
314
274
 
315
275
  **Tell the collecting close from the others.** Escape, the back button and a
316
- press on the wall also close the dialog, and they usually mean "not now" rather
317
- than "collect". `onClose` receives who asked (`e.detail.requester`, the button
318
- of a `--navi-close`). Only the close that collects writes the state, and the
319
- others close the dialog without writing it, back into the element it opened from.
320
-
321
- **The dialog may go away with the state.** A reveal shown in place of the
322
- plate usually lives in the same branch as the banner and is unmounted by the
323
- state it writes. That is fine: the picture of the reveal was taken before the
324
- close.
325
-
326
- **The landing box is the crest's box**, for the same reason the trigger's box
327
- is the card's box (see [above](#the-triggers-box-is-the-cards-box)): the id
328
- goes on the element that is exactly the small crest, not on the plate around
329
- it.
330
-
331
- **The opening's own exit does not play.** While a closing lift runs, the
332
- dialog's transitions are off. `scaling`'s exit would keep the dialog painted
333
- into the picture of the state it closes into.
276
+ press on the wall close the dialog too, and usually mean "not now" rather than
277
+ "collect". A `Dialog`'s `onClose` receives who asked (`e.detail.requester`, the
278
+ button of a `--navi-close`), and only the close that collects writes the state.
279
+ The others land nowhere: the fixed `liftAnchor` names nothing, so they wait out
280
+ those 300 ms, fade out, and dev warns.
334
281
 
335
282
  ## What it costs, and where the time goes
336
283
 
337
- Measured at CPU ×6 on the demo bench
338
- (`src/control/demos/picker/12_picker_card_demo.html#lift-bench`),
339
- a card of a real edit sheet's weight (≈215 elements, 13 fields, 62 picker
340
- nodes):
341
-
342
- | opening | tap → first moving frame |
343
- | ------------------------------------------- | ------------------------ |
344
- | the sheet rebuilt on every opening | 355–416 ms |
345
- | the sheet already built (`from-first-open`) | 63–94 ms |
346
- | a drawing (9 elements) | ≈140 ms |
347
-
348
284
  Navi's own share of an opening — showing the dialog, placing it, the two
349
- pictures — is the second line. Everything above it is the content: preact and
350
- the components of the sheet, spread thin over ~100 components with no single
351
- hot spot, plus `showModal()` and one layout. So the lever is the content:
285
+ pictures — is the smaller part. The rest is the content: preact and the
286
+ components of the sheet, spread thin with no single hot spot, plus `showModal()`
287
+ and one layout. So the lever is the content:
352
288
 
353
289
  - **Build less, or earlier.** A sheet that is the same across openings keeps
354
- `mount="from-first-open"`. A sheet that must be rebuilt (`while-opened`)
355
- pays its build on every tap; keep it as light as the page allows. A popup
356
- whose lifted node changes from one opening to the next has no choice — see
357
- [a row of cards](#a-row-of-cards-one-popup-that-walks).
290
+ `mount="from-first-open"`. A sheet that must be rebuilt (`while-opened`) pays
291
+ its build on every tap; keep it as light as the page allows. A popup whose
292
+ lifted node changes from one opening to the next has no choice, and
293
+ [a row of cards](#a-row-of-cards-one-popup-that-walks) rebuilds every slide
294
+ on every opening.
358
295
  - **Do not rebuild by accident.** A sheet whose code or data is tied to the
359
- address (`?edit=<id>` driving a route action) is thrown away when the
360
- address clears and rebuilt through a `null` render on the next opening —
296
+ address (`?edit=<id>` driving a route action) is thrown away when the address
297
+ clears and rebuilt through a `null` render on the next opening —
361
298
  `from-first-open` then rebuilds anyway. Keep the code loaded across closes,
362
299
  and seed the sheet from what the trigger holds.
363
300
  - **Warm what a first opening creates.** Intl formatters (a day spin's labels)
364
- are created on the first opening and cached; a page that will lift a card
365
- can render one such label at idle.
301
+ are created on the first opening and cached; a page that will lift a card can
302
+ render one such label at idle.
366
303
 
367
- The movement itself is the browser's, on the compositor: once it starts, the
368
- content's cost is over. Measured per frame at ×6: ≈23 ms, the wall's fade
369
- included.
304
+ The movement itself is the browser's: once it starts, the content's cost is
305
+ over.
370
306
 
371
307
  ## The wall, and the frame before the movement
372
308
 
373
- The frame the dialog opens on is the picture the movement starts from, and it
374
- lasts as long as the pictures take: 40–60 ms with nothing left to build, the
375
- sheet's build with a sheet to render. On it the card is still an element of
376
- the page, under the wall, and the movement then draws it on its own, sharp,
377
- above the wall. So the wall arrives from nothing, over the movement: that
378
- frame shows the page untouched, card included.
379
-
380
- Any wall on that frame is worn by the card for its whole length, then taken
381
- off at once when the movement starts: a flash. A wall at half strength, to
382
- answer a slow tap before the movement, leaves the card dim and blurred for the
383
- length of the build (≈130 ms at ×6), then sharp; at full strength the card is
384
- gone.
385
-
386
- What that frame does not do is say a tap was taken. That answer belongs to
387
- the trigger, which is what the movement starts from anyway: a hold has the
388
- finger still on it, and a `Picker`'s trigger wears `aria-expanded="true"` from
389
- the open on, for an app that wants it pressed in while the sheet builds.
390
-
391
- `animation="lifting"` brings its own wall — opaque and blurred
392
- (`--navi-backdrop-lift-*`), the page it came out of being what the movement
393
- leaves; `backdropVariant="discrete"` asks for the light wash back, and
394
- `backdropVariant="lift"` asks for that wall on a popup that does not move. See
395
- `popup_backdrop.md`.
309
+ The frame the dialog opens on is the picture the movement starts from. On it the
310
+ card is still an element of the page, under the wall, and the movement then
311
+ draws it on its own, sharp, above the wall — so any wall on that frame is worn
312
+ by the card for the length of the build, then taken off at once when the
313
+ movement starts: a flash. The wall arrives from nothing, over the movement.
314
+
315
+ What that frame does not do is say a tap was taken. That answer belongs to the
316
+ trigger, which is what the movement starts from anyway: a hold has the finger
317
+ still on it, and a `Picker`'s trigger wears `aria-expanded="true"` from the open
318
+ on, for an app that wants it pressed in while the sheet builds. The wall's own
319
+ paint is
320
+ [popup_backdrop.md](./popup_backdrop.md#the-one-animation-that-decides-its-own-backdrop)'s.
396
321
 
397
322
  ## What the browser does around it
398
323
 
399
- - **The page is photographed too**, deliberately: a modal dialog and its wall
400
- live in the top layer, and the browser paints the top layer during a
401
- transition only as part of the root's picture — opted out, wall and dialog
402
- go unpainted for the length of the movement (Chrome 153, reproduced in a
403
- bare page; in an app it showed only past a few thousand pixels of scroll).
324
+ - **The page is photographed too**, deliberately: the dialog and its wall live
325
+ in the top layer, which the browser paints during a transition only as part of
326
+ the root's picture (see
327
+ [view_transitions.md](./view_transitions.md#the-top-layer-is-painted-through-the-roots-picture)).
404
328
  The price is a page frozen and unpressable while the movement plays: under a
405
- modal wall at the opening, and a quarter of a second at the closing.
329
+ modal wall at the opening, and for the popup's animation duration at the
330
+ closing.
406
331
  - **Fixed bars stay in the page's picture**, under the wall like everything
407
- else. The popup is placed in the room between them, and its picture keeps
408
- to that room on the way as well: the moving box is clipped to it, so a card
409
- half under the bottom bar leaves from under it and comes back under it.
410
- Naming the bars to draw them over the box was tried and taken out: a bar
411
- photographed on its own has no wall over it, and compositing the wall onto
412
- its picture by hand never quite matched the wall itself.
413
- - **The page is held still for the movement.** Its picture is frozen for
414
- the length of the transition, while the browser keeps following the live
415
- anchor: a scroll would carry the arriving box along under a page that does
416
- not move, and past the clip to the room between the bars. Scroll gestures
417
- on the background are cancelled until the movement is over, the way
418
- `scrollCapture` cancels them while a dialog is open.
332
+ else. The popup is placed in the room between them, and the moving box is
333
+ clipped to that room on the way, so a card half under the bottom bar leaves
334
+ from under it and comes back under it. Naming the bars to draw them over the
335
+ box was tried: a bar photographed on its own has no wall over it.
336
+ - **The page is held still for the movement.** Its picture is frozen for the
337
+ length of the transition while the browser keeps following the live anchor,
338
+ so a scroll would carry the arriving box along under a page that does not
339
+ move. Scroll gestures on the background are cancelled until the movement is
340
+ over, the way `scrollCapture` cancels them while a dialog is open.
419
341
  - **The closing is photographed whole**, the picture of the popup taken before
420
- the close takes it off screen. Escape, the wall, `--navi-close`, `navBack`
421
- all go through it; the native `cancel` of a `<dialog>` is prevented so the
422
- browser does not close it before the picture.
342
+ the close takes it off screen: Escape, the wall, `--navi-close` and `navBack`
343
+ all go through it.
423
344
  - **One lift at a time.** A document has one view transition; a lift started
424
345
  while another plays replaces it, and the replaced one is released cleanly.
425
-
426
- ## Measuring
427
-
428
- Movement is measured, never eyeballed (see the animations skill). The bench in
429
- `12_picker_card_demo.html#lift-bench` reports, per opening, tap → dialog open
430
- and tap → first moving frame, plus a census of what the dialog holds, next to
431
- the numbers of a real app's sheet; throttle the CPU in devtools to read a
432
- phone's. A re-rasterized screenshot lies about a running transition: to see
433
- what the screen shows, record a video (`recordVideo`, then frames through
434
- ffmpeg) and, for a size, track the colour of the card frame by frame.