@jsenv/navi 0.29.489 → 0.29.490

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.
@@ -27,9 +27,12 @@ what follows the send still follows it — the popup closes, the slide moves on,
27
27
  the page changes. The user is done either way; there was simply nothing to
28
28
  send.
29
29
 
30
- `canSendWhileUnchanged` turns that off, for a form where sending the same thing
31
- twice is the point — a single button firing a notification, an action whose
32
- duplicates are fine.
30
+ `canSendWhileUnchanged` turns that off, for a form whose send is the act itself
31
+ rather than its fields: a request that exists even empty (join, ask, raise a
32
+ hand), a step that moves on, a creation whose screen opens already filled —
33
+ from the url, from last time's answers, from the defaults a `stateSignal`
34
+ declares — and a send whose duplicates are the point (a single button firing a
35
+ notification).
33
36
 
34
37
  ```jsx
35
38
  <Form action={notify} canSendWhileUnchanged>
@@ -89,11 +92,13 @@ are what keep an empty one from being sent.
89
92
 
90
93
  A creation lands on the page the server just made, and its id comes back with
91
94
  the response — too late for a prop. Do it in the action, which is where the
92
- answer is (a creation always has something to send, so there is no "the press
93
- did nothing" case for `command` to cover):
95
+ answer is. A creation that may open already filled — a draft restored from the
96
+ url — takes `canSendWhileUnchanged`, or its press does nothing at all: no send,
97
+ and the navigation that lives in the action never runs:
94
98
 
95
99
  ```jsx
96
100
  <Form
101
+ canSendWhileUnchanged
97
102
  action={async (value) => {
98
103
  const game = await createGame(value);
99
104
  navTo(`/games/${game.id}`);
@@ -201,6 +206,16 @@ does not register with the group around it. Nothing goes up (its value is not
201
206
  collected, the form stays unchanged whatever it does) and nothing comes down
202
207
  (the form's reset, its validation cascade).
203
208
 
209
+ Enter is the exception. In a text field, standalone or not, Enter sends the
210
+ group around it (`--navi-send`), the way Enter submits an HTML form; in a
211
+ picker's popup it sends the picker — its own value, not the field's — and
212
+ closes it. A field with an `action` of its own commits that action instead. A
213
+ field that helps find the answer says which list it drives with `navi-list`:
214
+ Enter then picks the row under the arrows, and does nothing while none is. A
215
+ field whose Enter must send nothing at all — a street typed before its
216
+ suggestion filled the city — says `enterSendsNothing`; the browser's own Enter
217
+ (a datalist choice) still happens.
218
+
204
219
  What it does **not** say is "ignore everything around me". `disabled`,
205
220
  `readOnly` and `loading` travel on their own contexts and go on reaching it,
206
221
  deliberately: a door with no value of its own can still write into the form
package/docs/group.md CHANGED
@@ -43,7 +43,11 @@ Live examples: `src/control/demos/15_group_demo.html`.
43
43
  - **Seams**: a member with a member before it is pulled back by one border
44
44
  width, so the two borders along a seam become one line — the member's own
45
45
  `--border-width` when it declares one, else the group's `--group-border-width`
46
- (default: `--navi-control-border-width`).
46
+ (default: `--navi-control-border-width`). Members that draw no border have
47
+ no line to share: `seamColor` (and `seamWidth`, 1px by default) sets them
48
+ that far apart and paints the gap — `<SplitButton seamColor="…">` for two
49
+ borderless halves. In css, `--group-seam-color` and `--group-seam-width`,
50
+ set on the group or around it.
47
51
  - **Corners**: a member loses the radius on each side where it faces another
48
52
  member. A single member keeps its own radius — a group of one looks like the
49
53
  control alone.
package/docs/i18n.md CHANGED
@@ -192,25 +192,40 @@ which pins one region beside navi's dates and ignores `setSupportedLanguages`.
192
192
  Each helper takes `lang` and otherwise reads the signal above; the component is
193
193
  for JSX, the function for a string (a `title`, a notification).
194
194
 
195
- | Value | Component | String |
196
- | ----------------------- | -------------------------------------------------- | ------------------------------------------------ |
197
- | a number | `<Quantity>` | `formatNumber` |
198
- | a day | `<Time type="date">` | `formatDay` |
199
- | a month | `<Time type="month">` | `formatMonth` |
200
- | a day and its time | `<Time type="datetime">` | `formatDatetime` |
201
- | a time of day | `<Time type="time">` | `formatTimeOfDay`, or `formatTime` from a `Date` |
202
- | a clock from a count | `<Time type="hour"\|"minute" format="timestring">` | |
203
- | two times of day | `<TimeRange>` | `formatTimeRange` |
204
- | a day relative to today | `dayLabel` on `<Time type="date">`, beside the day | `formatDayRelative` |
205
- | an instant, from now | `<Time type="relative">` | `formatTimeRelative` |
206
- | a duration | `<Time type="duration">` | `formatDuration` and its per-unit siblings |
207
- | items read as one list | | `formatList` |
195
+ | Value | Component | String |
196
+ | ------------------------------- | -------------------------------------------------- | ------------------------------------------------ |
197
+ | a number | `<Quantity>` | `formatNumber` |
198
+ | a size in bytes | | `formatByteSize` |
199
+ | a day | `<Time type="date">` | `formatDay` |
200
+ | a month | `<Time type="month">` | `formatMonth` |
201
+ | a day and its time | `<Time type="datetime">` | `formatDatetime` |
202
+ | a time of day | `<Time type="time">` | `formatTimeOfDay`, or `formatTime` from a `Date` |
203
+ | a clock from a count | `<Time type="hour"\|"minute" format="timestring">` | |
204
+ | two times of day | `<TimeRange>` | `formatTimeRange` |
205
+ | a day relative to today | `dayLabel` on `<Time type="date">`, beside the day | `formatDayRelative` |
206
+ | an instant, from now | `<Time type="relative">` | `formatTimeRelative` |
207
+ | a duration | `<Time type="duration">` | `formatDuration` and its per-unit siblings |
208
+ | the time elapsed since, ticking | `<Time type="duration" since={instant}>` | |
209
+ | items read as one list | `formatList` over elements | `formatList` |
208
210
 
209
211
  A spelled day writes its year only when it is not in `now`'s year (`year:
210
212
  "auto"`, see `formatDay`): appending the year by hand writes it twice for a
211
213
  date in another year. A capital letter at the start of a value is
212
214
  `<Text capitalize>`, not a sliced string.
213
215
 
216
+ The shape of a number is asked of `formatNumber` too: it takes
217
+ `Intl.NumberFormat`'s options in the reader's language — a fixed precision
218
+ (`formatNumber(seconds, { minimumFractionDigits: 1, maximumFractionDigits: 1 })`
219
+ → "1,0"), a sign (`signDisplay: "exceptZero"` → "+3") — and `<Quantity>`
220
+ forwards the fraction digits and the sign. A time written to the second (a log
221
+ line, a "checked at") is `precision: "second"` on `formatTime`,
222
+ `formatDatetime`, `formatTimeOfDay` and `<Time type="time"|"datetime">`.
223
+
224
+ A list whose items are elements — a `<TimeRange>` per span — goes through
225
+ `formatList` as well: given an element, it returns a fragment with each element
226
+ at its place between the language's words, where a hand-written " et " would
227
+ join them in one language only.
228
+
214
229
  ## Changing what navi says
215
230
 
216
231
  `naviI18n` holds navi's own texts. Register on it to override a default or to
@@ -93,6 +93,27 @@ turned down where it is read (see
93
93
  [`refuse`](./drag_interactions.md#the-hand-pulls-and-nothing-follows-refuse),
94
94
  which the carrying interactions answer with).
95
95
 
96
+ A value can also say what it is besides what it does, as
97
+ `{ answer, verb, optimistic }`, `answer` being one of the four:
98
+
99
+ ```jsx
100
+ <List.Item
101
+ interactions={{
102
+ reorder: { answer: saveOrder, verb: "PUT" },
103
+ toss: { answer: remove, verb: "DELETE" },
104
+ swipe_left: { answer: archive, verb: "PATCH" },
105
+ }}
106
+ />
107
+ ```
108
+
109
+ `verb` says the answer writes, which a function cannot say about itself. Under a
110
+ [network policy](./network_policy.md) holding writes the interaction does not
111
+ happen, and the policy's words are said where the hand made it — before anything
112
+ moves, for a gesture: a carried thing is not picked up, a swiped row resists like
113
+ a wall. `optimistic` is for a `reorder` or a `land` drawn at once and written
114
+ after ([drag_interactions.md](./drag_interactions.md#answered-at-once-optimistic)).
115
+ A falsy `answer` is "not this one" too.
116
+
96
117
  ### The interactions navi detects
97
118
 
98
119
  | Key | Read from |
@@ -199,9 +220,15 @@ finger was — is `findEvent(event, ["pointerdown", "pointerup", "click",
199
220
  Everything goes through the interaction gate of the **nearest control** — itself,
200
221
  an ancestor, or the one control it wraps, in that order. So a disabled, read-only
201
222
  or busy control answers a swipe the way it answers a click: it says why, where the
202
- interaction happened, and nothing runs. A `Box` with no control anywhere near it
203
- still answers a callback; only the two requests (`"request_action"`,
204
- `"request_ui_action"`) have nothing to ask, and say so in dev.
223
+ interaction happened, and nothing runs. A gesture asks the gate where the hand
224
+ meets it, not once it has played: a carried thing is not picked up — the gate
225
+ speaks at the instant the grab would have been felt — and a swiped row resists
226
+ the way a side nothing is declared for does, the gate speaking the first time the
227
+ hand pulls that way. A row that goes all the way out before being told it was
228
+ never free to go is a press that looks accepted and fails after. A `Box` with no
229
+ control anywhere near it still answers a callback; only the two requests
230
+ (`"request_action"`, `"request_ui_action"`) have nothing to ask, and say so in
231
+ dev.
205
232
 
206
233
  A `Box` that lays out **several** controls — a row of badges, a toolbar — belongs
207
234
  to none of them: its interactions are its own, answered with no gate. A click on
@@ -276,8 +303,11 @@ element that declares the gesture.
276
303
 
277
304
  While the answer takes time, the element **stays where the gesture left it**, and
278
305
  comes back once it settles — a failure leaves the row in place to be tried
279
- again. What a success does to it is yours (a list that redemands its rows, a row
280
- that leaves): navi does not make it disappear.
306
+ again, and says why on it once it is back, read the way a refused drag's answer
307
+ is ([drag_interactions.md](./drag_interactions.md#carrying-something-move-reorder-land-toss-leave)):
308
+ rejecting with nothing comes back without a word. What a success does to it is
309
+ yours (a list that redemands its rows, a row that leaves): navi does not make it
310
+ disappear.
281
311
 
282
312
  ## Twice, whichever hand it is
283
313
 
@@ -540,7 +570,9 @@ teardown, so a travelling container above it does not take the gesture (see
540
570
  ## Reference
541
571
 
542
572
  - `src/control/interaction/interaction_registry.js` — the prop, the four values,
543
- the registry.
573
+ `{ answer, verb, optimistic }`, the registry.
574
+ - `src/control/interaction/interaction_refusal.js` — how a refusal is said,
575
+ before and after.
544
576
  - `src/control/interaction/interaction_press.js` — swipes, holds and the two
545
577
  taps, and what a swipe writes on the element.
546
578
  - `src/control/interaction/interaction_keyboard.js`,
@@ -125,7 +125,10 @@ same (next section).
125
125
  A composition is about the **values** its params hold, not the reader instance:
126
126
  `GET_RANGE.bindParams({ scope: "thread" })` called from two places reads and
127
127
  writes the same one. A verb in `rerunOn.GET_RANGE`, or `reader.invalidate()`,
128
- drops the compositions — they stand for an order that is gone — and
128
+ drops the compositions — they stand for an order that is gone;
129
+ `reader.revalidate()` keeps them and has the lists on screen ask again for
130
+ their window, for a collection that may have moved (the app back from the
131
+ background) — and
129
132
  `memoryBudget` (1000 ranks by default) trims the ranks far from any window,
130
133
  which are asked for again if the user goes back to them.
131
134
 
@@ -318,6 +318,12 @@ param that qualifies a screen is not a place, and one entry per write turns a
318
318
  single back-press into as many as the user moved. A state whose values ARE places
319
319
  says so — see [`history: "push"`](#a-state-whose-values-are-places-history-push).
320
320
 
321
+ One answer may be kept as several params — a level and its reach,
322
+ `?levels=…&level_exact=…` — and drawn by one control whose value is the object.
323
+ `stateSignalGroup({ levels: levelsSignal, level_exact: levelExactSignal })` is
324
+ that object: read from the params, following each of them, and written back
325
+ into them as one move — one history entry.
326
+
321
327
  #### A value written per frame
322
328
 
323
329
  Every write reaches the address, synchronously, and browsers refuse an address
@@ -380,6 +386,13 @@ be written about it (see
380
386
  [route_transitions.md](./route_transitions.md#a-page-opened-from-anywhere)), and
381
387
  its `matchingSignal` read outside the tree.
382
388
 
389
+ Which page is on screen is the containers' answer, not the routes': several can
390
+ match one address, and only the order of the branches says which renders.
391
+ `renderedPageSignal` holds it — `{ route, params }` of the deepest page the
392
+ routers render, a section's own sub-router included, the fallback's
393
+ `routeFallback()` when nothing matches. Naming the screen being left, counting
394
+ where people go: read it there rather than walking a copy of the route table.
395
+
383
396
  A section with a shared prefix owns its own sub-router: one leaf on
384
397
  `/dashboard/` whose element renders its own `<Route>` tree and chrome, handing its
385
398
  local state down through `elementProps`. Pages sharing a layout but no prefix
@@ -578,6 +591,16 @@ A, B, A, B… growth, and the scroll comes back — and one step forward too. On
578
591
  towards entries of this document, and never when the push carries explicit
579
592
  state. An arrow that must ALWAYS behave as a back is `navBack()`.
580
593
 
594
+ Further back, a push is an arrival even when it means a return: a bar entry
595
+ leading back to a thread whose address names the item one was reading
596
+ (`/games?at=<id>`) is, by its path, a new arrival at the top — the same push from
597
+ a notification would be one. Only the link knows, and it says so:
598
+ `<Link asReturn>`, `navTo(url, { asReturn: true })`. The push still stacks its
599
+ entry, and lands like a back: the document where that address was last left,
600
+ its lists on the positions they kept there
601
+ ([scroll.md](./scroll.md#where-the-list-opens-and-where-it-is)). An address never
602
+ left opens at its top as usual.
603
+
581
604
  ### A page that is not whole yet
582
605
 
583
606
  A position is put back into whatever the page shows at that moment. When that is
@@ -111,15 +111,26 @@ all, so it is left alone, and so is a function handed to a control's `action`:
111
111
  it is wrapped into an action with no verb, even when it calls one, so the
112
112
  control is not held and the write fails after the press with a
113
113
  `NetworkPolicyError` ([resource.md](./resource.md#a-function-calling-the-verb-or-the-instance)).
114
- Such a control says what its function does to the server with `actionVerb` —
115
- `<Input type="checkbox" boolean action={(on) => saveSetting("weather", on)}
116
- actionVerb="PATCH" />` — and is then held up front, with the policy's message.
114
+ Such a control, or a `<Form>`, says what its function does to the server with
115
+ `actionVerb` — `<Input type="checkbox" boolean action={(on) => saveSetting("weather", on)}
116
+ actionVerb="PATCH" />`, `<Form action={sendDraft} actionVerb="POST">` — and is
117
+ then held up front, with the policy's message: a form's fields and its submit
118
+ alike.
117
119
  `createAction(callback, { meta: { verb } })` says the same of an action declared
118
120
  once, and opts it into what the action layer holds — a control bound to a write verb is read-only, a completed `"GET"`
119
121
  is not rerun — but its callback is still called when it runs: answering from
120
122
  the store and settling with a `NetworkPolicyError` are done by the callbacks a
121
123
  `resource()` wraps.
122
124
 
125
+ A gesture's answer is a function navi cannot see into either, and says what it
126
+ writes in its declaration —
127
+ `interactions={{ reorder: { answer: saveOrder, verb: "PUT" }, toss: { answer: remove, verb: "DELETE" } }}`
128
+ ([interactions.md](./interactions.md#the-four-values)). Under the policy it does
129
+ not happen, and the policy's message is said where the hand made it, before
130
+ anything moves: a carried thing is not picked up, a swiped row resists as a wall
131
+ does. Undeclared, the gesture plays to the end and the write it calls fails
132
+ after — said on the element, the way any refused answer is.
133
+
123
134
  ## Holding writes while reads go out
124
135
 
125
136
  `reads: "network"` keeps the first half of the policy and drops the second: the
@@ -7,7 +7,8 @@ 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
8
  back into its place on close — or, with
9
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)).
10
+ close, which a `Popover` does too (see
11
+ [lifting on the way back only](#lifting-on-the-way-back-only)).
11
12
 
12
13
  It is not a way to open a dialog with a nicer entrance. A dialog that shows
13
14
  something else than what was pressed — a menu, a confirmation, a form the
@@ -255,6 +256,16 @@ does the opening's own exit play: while a closing lift runs, the dialog's
255
256
  transitions are off, or `scaling`'s exit would keep the dialog painted into the
256
257
  picture of the state it closes into.
257
258
 
259
+ **A popover lands the same way.** A `Popover` — a `Popup` or a `Picker` in
260
+ popover mode — takes the same `animation={{ open, close: "lifting" }}` and
261
+ `liftAnchor`: an invitation shown out of the top bar, whose "Later" puts the
262
+ app's tile away in the settings wheel, flies the tile into the wheel as it
263
+ closes. Left without `liftAnchor`, the lifted node goes back into the anchor the
264
+ popover was placed against. The popover itself leaves with the page's picture,
265
+ fading as the node travels; its own exit is off for the same reason as the
266
+ dialog's. Only the close lifts: a popover opens placed against its anchor,
267
+ never out of it, so a plain `"lifting"` gives it no movement either way.
268
+
258
269
  **The box it lands in is rendered by the close.** `liftAnchor` is read once the
259
270
  close has been made, `onClose` included, inside the transition: a state written
260
271
  in `onClose` has rendered by the time the landing is looked up. It is also the
@@ -275,8 +286,9 @@ and dev warns.
275
286
 
276
287
  **Tell the collecting close from the others.** Escape, the back button and a
277
288
  press on the wall close the dialog too, and usually mean "not now" rather than
278
- "collect". A `Dialog`'s `onClose` receives who asked (`e.detail.requester`, the
279
- button of a `--navi-close`), and only the close that collects writes the state.
289
+ "collect". A `Dialog`'s or a `Popover`'s `onClose` receives who asked
290
+ (`e.detail.requester`, the button of a `--navi-close`), and only the close that
291
+ collects writes the state.
280
292
  The others land nowhere: the fixed `liftAnchor` names nothing, so they wait out
281
293
  those 300 ms, fade out, and dev warns.
282
294
 
@@ -206,7 +206,12 @@ names none, and on whoever asked when nothing else says. In that order:
206
206
  the press is the only thing that knows that point.
207
207
  2. **the `anchor` prop** — where this popup opens when nobody says.
208
208
  3. **`detail.source`** — who asked. A button therefore opens the popup on
209
- itself, and a popup passed as its own source is its own anchor.
209
+ itself, and a popup passed as its own source is its own anchor. A source
210
+ bigger than what the popup comes out of — a whole row pressed, a crest
211
+ inside it — names that part: `navi-command-proxy-for="<id>"` on the button
212
+ makes the element with that id the source the popup reads, what a Popover
213
+ is placed against and what a lifting Dialog lifts from and lands back into.
214
+ The press, the value and the focus stay the button's.
210
215
 
211
216
  A `Popover` is positioned against it; a `Dialog` only reads it for
212
217
  `sizeFromAnchor` and `animation="lifting"` (see [popup_lift.md](./popup_lift.md)).
@@ -471,6 +476,40 @@ can veto: `onClose` is the close happening, not a request to close. The refusals
471
476
  are navi's: a close over a control mid-action, and a `Picker`'s close over a
472
477
  value that does not validate.
473
478
 
479
+ ### Awaiting the answer: `askPopup`
480
+
481
+ A gesture whose outcome is a question waits for it where the gesture is: a card
482
+ thrown off the list asks "delete it?" and either goes or flies home, a player
483
+ dropped on the release zone asks "remove them from the game?". The question is a
484
+ popup, and the answer is how it closed. `askPopup` opens it as `--navi-open`
485
+ does and settles once it has closed:
486
+
487
+ ```js
488
+ import { askPopup } from "@jsenv/navi";
489
+
490
+ toss: async (event) => {
491
+ const { confirmed } = await askPopup(deleteDialogRef.current, event, {
492
+ value: alert.id,
493
+ });
494
+ if (!confirmed) {
495
+ throw undefined; // the card goes home, nothing said
496
+ }
497
+ },
498
+ ```
499
+
500
+ `confirmed` is a yes: a `--navi-confirm`, or a send the popup closed on — a
501
+ `<Form>` inside it, a row's `--navi-send`. Everything else is a no, every way
502
+ out included: Escape, the backdrop, the cross, a link leaving, Back, the popup
503
+ leaving the page. It settles after `onClose`, and after the yes's own action:
504
+ a button's command waits for its action, so a confirm that deletes has deleted.
505
+ Asked again while the popup is still open, the earlier question is answered no
506
+ and the new one waits for the close. Nothing opening — no popup found, a press
507
+ that closed it a moment ago — rejects with `undefined`, which a gesture's answer
508
+ reads as a refusal to say nothing about.
509
+
510
+ The popup stays the popup: uncontrolled, with its own say over closing, and
511
+ nothing in it knows it was asked rather than opened.
512
+
474
513
  ### Closing when a button also runs an action
475
514
 
476
515
  A button that carries both runs them in that order: the action first, the
@@ -800,6 +839,54 @@ it — the shape of a popup that is a LAYER over the screen, whose address is
800
839
  [navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say)'s
801
840
  decision.
802
841
 
842
+ To land on a page with such a popup open — "see the details" leading to a
843
+ profile with its results sheet over it — the link says which, the way the
844
+ popup's `navState` says it:
845
+
846
+ ```jsx
847
+ <Link
848
+ route={PROFILE}
849
+ routeParams={{ id }}
850
+ open={{ id: "results", type: "push" }}
851
+ >
852
+ Voir le détail du calcul
853
+ </Link>
854
+ // or navTo(PROFILE.buildUrl({ id }), { open: { id: "results", type: "push" } })
855
+ ```
856
+
857
+ The page's entry is pushed, then the popup's own, as its opening would write
858
+ it: closing the sheet leaves the reader on the profile, and Back then leaves
859
+ the profile. The popup need not be mounted yet — it reads its key when the
860
+ page mounts it. A popup whose open state is a url param needs none of this:
861
+ its `buildUrl` already says it.
862
+
863
+ ### State that belongs to the popup: `ownSignals`
864
+
865
+ What a popup was opened on beside its `value` — the seat of a game a share
866
+ sheet is about — and what is chosen inside it — the result slide shown in a
867
+ level sheet — often lives in the address too, so a reload or a return finds
868
+ it. It means nothing once the popup is closed, and navi writes it away with
869
+ the open state:
870
+
871
+ ```jsx
872
+ <Dialog
873
+ signal={gameShareSignal}
874
+ value={game.id}
875
+ ownSignals={[gameShareSlotSignal]}
876
+ >
877
+ ```
878
+
879
+ Every close empties them (writes `undefined`, a state signal's default, out of
880
+ the url), whatever closes the popup — its cross, a keep-close landing back,
881
+ Escape, Back — and at the moment that close says: a keep-close lands on an
882
+ address without them, a cancel going back empties what the entry behind still
883
+ held, written by an `onOpen` before the push. A `navState` popup closed by a
884
+ link leaving keeps them on the entry left, where Back finds the popup open on
885
+ what it was showing. A signal two popups share (a sheet and the
886
+ plate opening it both reading `?rank_result=`) is emptied by whichever
887
+ closes. `Dialog`, `Popover`, `SidePanel`, `Popup` and `Expandable` take it; a
888
+ `Picker` writes its own in `onClose`, which runs before its close lands.
889
+
803
890
  ### `open`, and what it costs
804
891
 
805
892
  `open` is the one-way half of `signal` — the parent re-renders with a boolean
@@ -980,6 +1067,17 @@ the gesture that opens the popup; `useAsyncData` reads it (see
980
1067
  and aborts of a screen left ([network_policy.md](./network_policy.md)) apply; a
981
1068
  reload keeps the open state, so it keeps the request too.
982
1069
 
1070
+ **The request leaves at the press, the content is built at the hover.** A press
1071
+ on what opens the popup — a `--navi-open` or `--navi-toggle` button — preruns
1072
+ the route actions its opening changes, read as if the popup were already open:
1073
+ the request leaves at the pointerdown, not when the open state is written. The
1074
+ content may be built earlier: a `Picker`, or a popup given an `anchor`, builds
1075
+ it when the pointer rests there (see `mount`, below). On a computer the rows
1076
+ therefore arrive a press later than the content they fill. That is the price of
1077
+ a read keyed by the popup — one sheet per card, one request per card: a pointer
1078
+ crossing a list of cards has chosen none of them, and a hover that asked would
1079
+ ask for each card it crossed.
1080
+
983
1081
  The popup owning its request — `useAsyncData(action, { run: true })` — is the
984
1082
  fallback, for a parameter chosen inside the popup and dying with it: a filter
985
1083
  the sheet itself holds and nothing else remembers. Hand-written, with `run()`
@@ -996,15 +1094,17 @@ not leave with the close. A `Picker` told no value whose value something reads
996
1094
  is the exception: it builds its popup as soon as that value is read (see
997
1095
  [above](#a-press-that-opens-a-popup-and-acts-on-it)).
998
1096
 
999
- A `Dialog` nobody has asked for yet is not even built. It stands in as its bare
1000
- `<dialog>` — the id a command finds, the element a press announces itself on —
1001
- and the dialog proper is built at the first request: an open, a press on what
1002
- opens it, its `signal` or `open` saying open. From then on it stays built. So a
1003
- list whose rows each hold a few dialogs (a sheet per seat, a share sheet per
1004
- card) pays one element per closed dialog at every mount, not a dialog: a row
1005
- may carry the dialogs of its own actions. It is built at once when something
1006
- needs it from the start — open at mount, a `navState`, an `anchor` (a hover
1007
- builds the content ahead of the click), or a `mount` other than the default.
1097
+ A `Dialog` or a `Popover` nobody has asked for yet is not even built — nor a
1098
+ `Popup` or a `SidePanel`, which render one of the two. It stands in as one bare
1099
+ element (a `<dialog>`, a `<div>`) — the id a command finds, the element a press
1100
+ announces itself on — and the popup proper is built at the first request: an
1101
+ open, a press on what opens it, its `signal` or `open` saying open. From then on
1102
+ it stays built. So a list whose rows each hold a few popups (a sheet per seat, a
1103
+ share sheet per card) pays one element per closed popup at every mount, not a
1104
+ popup: a row may carry the popups of its own actions. It is built at once when
1105
+ something needs it from the start — open at mount, a `navState`, an `anchor` (a
1106
+ hover builds the content ahead of the click), or a `mount` other than the
1107
+ default. A `Picker`'s popup is the picker's own, built with it.
1008
1108
 
1009
1109
  "Closed" is two states, not one — never opened yet, and closed again after an
1010
1110
  opening — so the `mount` prop answers both at once: