@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
package/docs/actions.md CHANGED
@@ -22,7 +22,6 @@ them by hand — see [resource.md](./resource.md).
22
22
 
23
23
  - [Params: `bindParams`, and calling the action](#params-bindparams-and-calling-the-action)
24
24
  - [The answer is kept on the instance that ran](#the-answer-is-kept-on-the-instance-that-ran)
25
- - [A debounced signal asks where it settles, not where it passed](#a-debounced-signal-asks-where-it-settles-not-where-it-passed)
26
25
  - [The in-between states of a gesture are real params](#the-in-between-states-of-a-gesture-are-real-params)
27
26
  - [Running: `run`, `rerun`, `prerun`, `reset`](#running-run-rerun-prerun-reset)
28
27
  - [Aborting saves resources, it does not undo](#aborting-saves-resources-it-does-not-undo)
@@ -45,55 +44,43 @@ const getUser123 = getUser.bindParams({ id: 123 });
45
44
  await getUser123.run();
46
45
  ```
47
46
 
48
- Two `bindParams` with equal params give **the same instance** (deep equality),
49
- which is what makes state shared between two components asking for the same
50
- thing, and what deduplicates their requests.
51
-
52
- It is the params that are compared, never what produced them: four components
53
- deriving the same question through four `computed` of their own still ask it
54
- once, and read one state. So a derived params signal can be built wherever it is
55
- convenient — a `useMemo` per caller included — and nothing has to arrange for
56
- one instance of it. What this rests on is params being plain data: a `Set`, a
57
- `Map`, an element, a function or a signal inside them is compared by reference,
58
- and four callers then really do make four requests.
47
+ Two `bindParams` with equal params give **the same instance** (deep equality):
48
+ that is what shares state between two components asking for the same thing, and
49
+ deduplicates their requests, however each one derived its params — a `computed`
50
+ or a `useMemo` per caller included. What this rests on is params being plain
51
+ data: a `Set`, a `Map`, an element, a function, or a signal nested below a
52
+ param (`{ filter: { id: idSignal } }`) is compared by reference, and four
53
+ callers then really do make four requests. A signal given as a param's own
54
+ value (`{ id: idSignal }`) is read, and compared by what it holds.
59
55
 
60
56
  An action is callable, and calling it is the short way to bind and run in one
61
- go:
57
+ go — for a run that is a **gesture** (a click handler, an event, a step in a
58
+ flow), where the params are known at that moment and the run is the point:
62
59
 
63
60
  ```js
64
61
  getUser({ id: 123 }); // getUser.bindParams({ id: 123 }).rerun()
65
62
  getUser(); // getUser.rerun()
66
- ```
67
-
68
- Use it wherever a run is a **gesture** — a click handler, an event, a step in a
69
- flow — where the params are known at that moment and the run is the point:
70
-
71
- ```js
72
63
  const deleteGame = (game) => GAME.DELETE({ id: game.id });
73
64
  ```
74
65
 
75
- A run that fails hands the failure back — it **rejects**, or **throws
76
- synchronously** when the callback was synchronous — so a gesture behaves like any
77
- other call that can fail: an `await` on it throws, and an action awaiting it
78
- fails with it, which is what puts the error on the control that asked for the
79
- gesture, with nothing to write for it. See [error_handling.md](./error_handling.md#what-a-failing-action-does)
80
- for the runs that have nobody to reject at, and what they do instead.
66
+ Calling it `rerun()`s it: the run happens even if that instance already holds
67
+ data, which is what you want from a gesture and not from a component asking for
68
+ data. A run that fails **rejects**, or **throws synchronously** when the
69
+ callback was synchronous, so an action awaiting it fails with it — which is what
70
+ puts the error on the control that asked for the gesture (the runs with nobody
71
+ to reject at: [error_handling.md](./error_handling.md#what-a-failing-action-does)).
81
72
 
82
73
  Use `bindParams` when what you need is the **instance**, not the run: to keep a
83
- handle on it, or so that a run started in one place is visible from another —
84
- that shared identity is the whole of what it adds, and a control does not need it
85
- to draw its own busy and error states (see
74
+ handle on it, or so that a run started in one place is visible from another
75
+ (what a control gains from holding it:
86
76
  [resource.md](./resource.md#a-function-calling-the-verb-or-the-instance)).
87
- Note that calling the action `rerun()`s it — the run happens even if that
88
- instance already holds data, which is what you want from a gesture and not what
89
- you want from a component asking for data.
90
77
 
91
- Params may be signals, and then the action follows them:
78
+ Params may be signals, and then the instance follows them:
92
79
 
93
80
  ```js
94
81
  const userAction = getUser.bindParams({ id: userIdSignal });
95
- // a new id retargets it to the instance for that id; who runs the instance is
96
- // unchanged: `{ run: true }`, `actionRunEffect`, a press
82
+ // a new id retargets it to the instance for that id; a component reads it,
83
+ // and starts it, with useAsyncData(userAction, { run: true })
97
84
  ```
98
85
 
99
86
  `{ debounce }` puts a delay between the signal and the instance, for params that
@@ -109,20 +96,18 @@ const [found, searching] = useAsyncData(searchAction, {
109
96
  });
110
97
  ```
111
98
 
112
- There is no effect here and nothing to own: the delay lives in the binding, and
113
- two call sites passing the same signal and the same delay get the same instance,
114
- so the four components of one screen reading the same question ask it once. That
115
- is the shape to reach for whenever the screen owns its params signal.
116
- `actionRunEffect` is the other one, and what it adds is the run: use it when the request must go out whether or not something
117
- is drawing it. During the delay the instance is still the previous one, holding
118
- the previous answer — `loading` is what says a newer one is coming, don't
119
- compare params by hand to find out.
99
+ The delay lives in the binding — no effect, nothing to own — and two call sites
100
+ passing the same signal and delay share the instance. During the delay it holds
101
+ the previous answer, and `loading` says a newer one is coming: don't compare
102
+ params by hand. It follows where the signal **settles** (`A → B → A` sends
103
+ nothing for `B`). `actionRunEffect`, which takes the action itself and a params
104
+ getter, is for a request that must go out whether or not something draws it.
120
105
 
121
106
  ### The answer is kept on the instance that ran
122
107
 
123
- An action's state — `dataSignal`, `errorSignal`, `runningStateSignal` — belongs to
124
- the instance the params made, never to the action they were bound from. So the
125
- thing to read is whatever `bindParams` (or anything built on it) gave back:
108
+ An action's state — `dataSignal`, `errorSignal`, `runningStateSignal` — belongs
109
+ to the instance the params made, never to the action they were bound from. So
110
+ the thing to read is whatever `bindParams` (or anything built on it) gave back:
126
111
 
127
112
  ```js
128
113
  const COUNTS = routeAction(ANY_PAGE, ADMIN_COUNTS.GET, () => true);
@@ -132,62 +117,41 @@ ADMIN_COUNTS.GET.dataSignal.value; // undefined, for ever
132
117
  ```
133
118
 
134
119
  `routeAction` binds params — `() => true` is params too — so the answer lands on
135
- the child, and the verb it was declared from stays idle: nothing failed, nothing
136
- warns, the screen simply has no numbers in it. The same holds for the instance a
137
- control runs (see [below](#the-instance-a-control-runs)).
138
-
139
- A module-level signal derived from a verb — `computed(() => ME.GET.dataSignal.value)`
140
- — therefore only works for a verb something runs **directly**, with no params.
141
- Anything else derives from the instance:
142
-
143
- ```js
144
- export const countsSignal = computed(() => COUNTS.dataSignal.value);
145
- ```
146
-
147
- ### A debounced signal asks where it settles, not where it passed
148
-
149
- The delay publishes where the signal **settles**, never a value it only passed
150
- through on the way. A question that goes `A → B → A` inside the delay is the
151
- question `A`: no request goes out for `B`, and `loading` never went up. Only a
152
- signal that is somewhere new once the delay elapses moves the binding.
153
-
154
- That is load-bearing rather than a nicety, because settling is a **comparison,
155
- not a countdown**: `paramsSettlingSignal` is true while the signal and the
156
- binding differ and false when they agree. Move the binding to a value the signal
157
- is not on and nothing is left that could make them agree again — the screen is
158
- told a newer answer is coming, with no delay still running to bring one, and
159
- only another write to the source frees it. So a value the source never settled
160
- on does not cost a stale request, it costs a screen stuck on "loading".
120
+ the child, and the verb stays idle: nothing failed, nothing warns, the screen
121
+ simply has no numbers in it. The same holds for the instance a control runs
122
+ ([below](#the-instance-a-control-runs)). A module-level signal derived from a
123
+ verb — `computed(() => ME.GET.dataSignal.value)` — only works for a verb
124
+ something runs **directly**, with no params; anything else derives from the
125
+ instance: `computed(() => COUNTS.dataSignal.value)`.
161
126
 
162
127
  ### The in-between states of a gesture are real params
163
128
 
164
129
  The delay decides _when_ a question goes out, never _which_ one is coherent: it
165
130
  sees every write, including the ones a gesture only passes through. Moving an
166
- item from one slot to another by filling the new slot and then emptying the old
167
- one is two writes, and between them the item is in both — a state nobody meant
168
- to ask about, and a question the delay would happily send if it landed there.
169
-
170
- Make the intermediate not matter, rather than counting on the delay to hide it.
171
- Two ways, both worth having on their own:
131
+ item by filling the new slot and then emptying the old one is two writes, and
132
+ between them the item is in both — a question nobody meant to ask, sent if the
133
+ delay lands there. Make the intermediate not matter rather than counting on the
134
+ delay to hide it:
172
135
 
173
136
  - **`batch()` the writes that belong to one gesture**, so the intermediate state
174
- never exists at all — not for the delay, not for a render, not for a
175
- validation rule reading the same signals.
176
- - **Shape the question so states that are equivalent compare equal.** Params are
177
- compared deeply, so a list whose order carries nothing asks a new question
178
- every time it is rearranged; sent deduplicated and sorted, rearranging stops
179
- changing the question — better than asking it late.
137
+ never exists — not for the delay, not for a render, not for a validation rule
138
+ reading the same signals.
139
+ - **Shape the question so equivalent states compare equal.** Params are compared
140
+ deeply, so a list whose order carries nothing asks a new question every time
141
+ it is rearranged; sent deduplicated and sorted, it stops changing.
180
142
 
181
143
  ## Running: `run`, `rerun`, `prerun`, `reset`
182
144
 
183
145
  | Method | Does |
184
146
  | ---------- | ------------------------------------------------------------------------ |
185
- | `run()` | Asks for the data. An action already running or completed has it: no-op. |
147
+ | `run()` | Asks for the data; running or completed, it joins what is there. |
186
148
  | `rerun()` | Runs again whatever state it is in — a refresh, an explicit "check now". |
187
149
  | `prerun()` | Same as `run()`, in the background: nothing asked for it on screen yet. |
188
150
  | `reset()` | Aborts what is running and puts the action back to idle, data and all. |
189
151
  | `abort()` | Calls off the run in flight, keeping the data it had. |
190
152
 
153
+ A "check now" wired to `run()` therefore checks nothing, silently.
154
+
191
155
  ### Aborting saves resources, it does not undo
192
156
 
193
157
  `abort()` cancels what can still be cancelled — a `fetch` wired to the
@@ -205,83 +169,58 @@ const [user] = useAsyncData(userAction);
205
169
  ```
206
170
 
207
171
  `useAsyncData` suspends until the data is there and throws on failure, leaving
208
- both to the nearest `<Loading>` and `<ErrorBoundary>`; pass `{ loading: true }`
209
- or `{ error: true }` to receive either as a value instead, the component staying
210
- mounted throughout and keeping the data it already had. What it then draws for
211
- each state — the four combinations of `data` and `loading`, why a skeleton is
212
- told whether it is loading — is [data_states.md](./data_states.md). Where a
213
- failure goes when no screen takes it, and who is allowed to swallow the
214
- rejection, is [error_handling.md](./error_handling.md). `useActionStatus(action)` gives the whole
215
- state at once — `{ idle, loading, completed, aborted, error, data, params }` —
216
- for a component that needs to look at it rather than render it. It reads that
217
- very instance: to know what moves when a control is handed the action, see
218
- [the instance a control runs](#the-instance-a-control-runs).
172
+ both to the nearest `<Loading>` and `<ErrorBoundary>`; `{ loading: true }` or
173
+ `{ error: true }` hands either over as a value instead, the component staying
174
+ mounted — what it then draws is [data_states.md](./data_states.md), where a
175
+ failure nobody takes goes is [error_handling.md](./error_handling.md).
176
+ `useActionStatus(action)` gives the whole state of that very instance at once —
177
+ `{ idle, loading, completed, aborted, error, data, params }` — for a component
178
+ that needs to look at it rather than render it.
219
179
 
220
180
  **Reading does not run.** `useAsyncData` waits for data someone else asked for
221
181
  — for a page, the route, through `routeAction`. A component reading an action
222
- that nobody ran suspends, and `<Loading>` draws nothing for an idle action (no
223
- spinner for a request nobody sent): the whole subtree stays blank, for good. The
224
- obvious fix does not work — a `useEffect` in that same component that would
225
- `run()` it never fires, because a suspended component has no effects, and the
226
- run it was about to start is exactly what the suspension waits for. So either
227
- the action is started **before** anything reads it — bound where the screen is
228
- decided: a `routeAction`, a `<Button action>`, the `action` of the `<Link>` one
229
- came in by — or the component owns its request: a folding panel, a slice a
230
- button asks for. That second one is `{ run: true }`, said in the same line that
231
- reads the data:
182
+ nobody ran suspends, and `<Loading>` draws nothing for an idle action: the
183
+ subtree stays blank, for good. A `useEffect` that would `run()` it never fires,
184
+ since a suspended component has no effects. So either the action is started
185
+ **before** anything reads it — a `routeAction`, a `<Button action>`, the
186
+ `action` of the `<Link>` one came in by — or the component owns its request and
187
+ says so where it reads the data, which starts it from the render:
232
188
 
233
189
  ```jsx
234
190
  // asks for it if nobody did, from the render that reads it
235
191
  const [members] = useAsyncData(membersAction, { run: true });
236
192
  ```
237
193
 
238
- It starts the action from the render rather than from an effect, which is what
239
- makes it work at all: a suspended component has no effects, so a `run()` written
240
- in one would be waiting for itself. Everything else is unchanged — `loading` and
241
- `error` mean what they mean everywhere, delegated by default, handled inline
242
- when asked for — and running an action twice costs nothing, so nothing has to
243
- be guarded.
244
-
245
- Where that wait is caught is the caller's business, and a popup is no exception
246
- — see [popup_open.md](./popup_open.md#a-popup-that-loads-data) for what that
247
- costs when it is forgotten.
194
+ `loading` and `error` keep their meaning, and running an action twice costs
195
+ nothing, so nothing has to be guarded. Where that wait is caught is the caller's
196
+ business, a popup included
197
+ ([popup_open.md](./popup_open.md#a-popup-that-loads-data)).
248
198
 
249
199
  **`{ run: true }` is the fallback, not the shape to reach for.** A route action
250
200
  is asked for when the ADDRESS changes, with everything else that address needs;
251
- this one cannot start before the component that draws it exists, which is a
252
- render late at best and a gesture late in a popup. So it is for data nothing
253
- else can ask for — a parameter chosen inside the component and dying with it —
254
- and it says so at the call site, where a hand-written `useEffect` + `run()` said
255
- nothing at all.
256
-
257
- Outside a component — a request the app owns rather than a component —
258
- `actionRunEffect(action, () => …)` is that same run declared once: it runs on
259
- the first truthy params, reruns when they change, aborts when they go false. It
260
- is the very machinery `routeAction` is built on.
201
+ this one cannot start before the component that draws it exists — a render late
202
+ at best, a gesture late in a popup. It is for a parameter chosen inside the
203
+ component and dying with it. Outside a component,
204
+ `actionRunEffect(action, () => …)` is that same run declared once — it runs on
205
+ the first truthy params, reruns when they change, aborts when they go false —
206
+ and the machinery `routeAction` is built on.
261
207
 
262
208
  **Which of the two it is, is decided by the parameter, not by what draws it.**
263
209
  If the parameter is in the address — a path param, a search param bound to a
264
210
  `stateSignal` — the data belongs to the screen, and it is a `routeAction`, even
265
- when a popup is the only thing that draws it. The component owns its request
266
- only when the parameter is chosen inside it and dies with it. A popup is never
267
- by itself a reason to own one: being open can be bound too — to a signal, to the
268
- history entry, to the address — and the data of a popup that binds it is a route
269
- action like any other (see
270
- [popup_open.md](./popup_open.md#a-popup-that-loads-data)). Getting this wrong is
271
- invisible at the call site and expensive on screen: the request then leaves with
272
- the gesture instead of with the screen, one waterfall behind everything else the
273
- address needed.
274
-
275
- **A route action is a read.** The parameter says whose data it is; it does not
276
- say who may ask for it. An address asks again on every arrival — a reload, a
277
- pasted link, a step back — which is exactly what a read is for and what a write
278
- cannot survive: `POST /users/:id/invitations` hung on `/users/:id` mints a token
279
- per reload, for ever, and nothing at the call site says so. So a run that writes
280
- belongs to the component that decided to write, whatever its parameter, and
281
- `{ run: true }` is what says it. That is the shape of a request prepared for a
282
- gesture — a token a share button must already hold when it is pressed, since the
283
- OS share sheet only opens inside the gesture and never after an `await`: it runs
284
- at mount because the press cannot wait for it, not because the address changed.
211
+ when a popup is the only thing that draws it: being open can be bound too, and
212
+ the data of a popup that binds it is a route action like any other. Getting this
213
+ wrong is invisible at the call site and expensive on screen: the request leaves
214
+ with the gesture instead of with the screen, one waterfall behind everything
215
+ else the address needed.
216
+
217
+ **A route action is a read.** An address asks again on every arrival — a
218
+ reload, a pasted link, a step back — which is what a read is for and what a
219
+ write cannot survive: `POST /users/:id/invitations` hung on `/users/:id` mints a
220
+ token per reload, for ever. A run that writes belongs to the component that
221
+ decided to write, whatever its parameter, and `{ run: true }` says it — a token
222
+ a share button must already hold when pressed, for instance, since the OS share
223
+ sheet only opens inside the gesture, never after an `await`.
285
224
 
286
225
  `{ onLoad }` is what the screen does with the data **once, when it becomes
287
226
  known** — seed the fields someone is about to edit, focus something, remember
@@ -297,49 +236,41 @@ const [game] = useAsyncData(gameAction, {
297
236
  ```
298
237
 
299
238
  It fires once per set of params, never again for a rerun that brings the same
300
- thing back: a save, a refresh, a poll all hand the data over again, and copying
301
- it a second time would overwrite what the person is in the middle of writing.
302
- The action knows what it ran with, so nobody has to guess that dependency. It is
303
- called from a layout effect, so what it writes belongs to the same tick as the
304
- render that got the data — which is what lets a `<Form pristineKey>` take its
305
- reference on filled fields (see
306
- [create_and_edit.md](./create_and_edit.md#the-edit-screen-opens-before-its-values)).
307
-
308
- Controls take the action itself and wire the rest: `<Button action>` runs it on
309
- click, shows its loading state, and puts its error where the user can see it.
310
- That is the reason to pass an action instance rather than an arrow calling the
311
- callback:
312
-
313
- ```jsx
314
- // ✓ loading, error and disabled states come from the action
315
- <Button action={GAME_CANDIDATES.POST.bindParams({ id: game.id, user_id })}>
316
- Accept
317
- </Button>
318
- ```
239
+ thing back — a save, a refresh, a poll would otherwise overwrite what the person
240
+ is writing — and from a layout effect, so a `<Form pristineKey>` takes its
241
+ reference on filled fields
242
+ ([create_and_edit.md](./create_and_edit.md#the-edit-screen-opens-before-its-values)).
319
243
 
320
244
  ## The instance a control runs
321
245
 
322
- A control binds the action it is given to its own UI state and runs the result.
323
- Which instance that is depends on whether the control carries a value:
324
-
325
- - a control with **no value of its own** — a button — contributes no params, so
326
- what it runs **is** the instance it was handed. `useActionStatus` on that
327
- instance sees the click, the run, the data.
246
+ A control takes the action and wires the rest — `<Button action>` runs it on
247
+ click, draws its wait, puts its error where the user can see it — whether it is
248
+ handed an action or a plain function, which it wraps into one (what that wrapper
249
+ lacks against a resource verb's instance:
250
+ [resource.md](./resource.md#a-function-calling-the-verb-or-the-instance)). It
251
+ binds what it is given to its own UI state and runs the result, so which
252
+ instance runs depends on the value the control carries:
253
+
254
+ - a **button outside any form or group** contributes no params, so what it runs
255
+ **is** the instance it was handed: `useActionStatus` on that instance sees the
256
+ click, the run, the data.
257
+ - a **button inside a `<Form>` or another group**, with no `value` of its own,
258
+ carries the value around it — a submit carries its form's — and runs the
259
+ child instance bound to that value, as the controls below do.
328
260
  - a control that **carries a value** — an input, a form — runs the child
329
261
  instance bound to that value: the value merged over the params the caller
330
262
  bound, and a value that is not an object **replaces them outright**. A
331
- checkbox handed `ACTION.bindParams({ id })` runs `ACTION(true)`, and `id`
332
- is gone — the call fails wherever it needed it, far from the binding that
333
- lost it. Bind params to a control that holds a value only when the value
334
- completes them (a form's object over a bound `{ id }`); when it would
335
- replace them, pass a function and keep the params in its closure:
336
- `action={() => ACTION({ id, sure: true })}`. Those params are not the
337
- caller's, so neither is the status: the instance the caller holds stays idle
338
- while the control's run moves. Read the effect where it lands (the store,
339
- for a resource verb), or listen to the run itself, below.
340
-
341
- Every control with an `action` reports the run it performs, whichever instance
342
- that is:
263
+ checkbox handed `ACTION.bindParams({ id })` runs `ACTION(true)`, and `id` is
264
+ gone — the call fails wherever it needed it, far from the binding that lost
265
+ it. Bind params to a control that holds a value only when the value completes
266
+ them (a form's object over a bound `{ id }`); when it would replace them, pass
267
+ a function and keep the params in its closure:
268
+ `action={() => ACTION({ id, sure: true })}`.
269
+
270
+ In those last two cases the params are not the caller's, so neither is the
271
+ status: the instance the caller holds stays idle while the control's run moves.
272
+ Read the effect where it lands (the store, for a resource verb), or listen to
273
+ the run itself — every control with an `action` reports the run it performs:
343
274
 
344
275
  ```jsx
345
276
  <Button
@@ -358,25 +289,26 @@ displayed (see [error_handling.md](./error_handling.md)); an abort to
358
289
 
359
290
  ## `action` or `uiAction`
360
291
 
361
- Both fire when a control's value changes — and on a control that holds no
362
- value, a `<Button>` or a `<Link>`, the press itself is that occasion: each of
363
- them runs once per press, with no value to wait for. They are not two ways of
364
- writing the same thing:
292
+ Both fire when a control's value changes — and on a control that holds no value,
293
+ a `<Button>` or a `<Link>`, the press itself is that occasion: each runs once per
294
+ press. They are not two ways of writing the same thing:
295
+
296
+ | | `uiAction(value, event)` | `action` |
297
+ | ----------------- | ------------------------------------------ | ---------------------------------------------------- |
298
+ | what it is | a plain callback | an action bound to the control's value |
299
+ | while it runs | nothing | the control is busy (`aria-busy`, its loading state) |
300
+ | if it fails | an unhandled rejection — nothing on screen | an error callout on the control |
301
+ | a popup around it | closes | refuses to close until it is done |
365
302
 
366
- | | `uiAction(value, event)` | `action` |
367
- | ----------------- | ------------------------------------------ | -------------------------------------------------------------------------- |
368
- | what it is | a plain callback | an action bound to the control's value |
369
- | while it runs | nothing | the control is busy (`aria-busy`, its loading state) |
370
- | if it fails | an unhandled rejection — nothing on screen | an error callout on the control, and the control goes back to what it held |
371
- | a popup around it | closes | refuses to close until it is done |
303
+ A failed `action` also puts back what the control held where `resetOnError` is
304
+ on — by default a checkbox, a radio, their groups, a picker, a selectable list
305
+ and `<Details>`; a text input and a form keep the value that was refused.
372
306
 
373
307
  `uiAction` is a notification: the value has changed, here it is. Use it for what
374
308
  cannot fail — logging, moving something else on screen, keeping a local
375
- variable.
376
-
377
- Anything that can fail or take time is an `action`, and it does not have to be
378
- an action instance: a plain function is wrapped into one, bound to the control's
379
- value, so the callback receives what the control now holds.
309
+ variable. Anything that can fail or take time is an `action`, and it does not
310
+ have to be an action instance: a plain function is wrapped into one, bound to
311
+ the control's value, so the callback receives what the control now holds.
380
312
 
381
313
  ```jsx
382
314
  // ✓ the box shows it is saving, says so if the save fails, and goes back to
@@ -390,10 +322,8 @@ value, so the callback receives what the control now holds.
390
322
 
391
323
  The give-away is an `async` `uiAction`, or one that calls something that writes:
392
324
  `uiAction` never waits for what it starts, so nobody is left holding the result.
393
-
394
325
  To merely REMEMBER the value rather than send it, neither is the answer: bind a
395
- signal and drop the callback entirely — see
396
- [control_value.md](./control_value.md).
326
+ signal and drop the callback — see [control_value.md](./control_value.md).
397
327
 
398
328
  ## A press that opens something and waits for the answer
399
329
 
@@ -404,18 +334,21 @@ one that asks something of a control near it is a `command` (a value proposed is
404
334
  Reaching for a plain `onClick` usually means one of those was missed.
405
335
 
406
336
  An `action` and a `command` on the same press are one gesture in two halves, and
407
- the command is the second one: it waits for the action and runs only if it
408
- succeeded. "Leave the group, then go back to the list" therefore never leaves
337
+ the command is the second one: **it waits for the action and runs only if it
338
+ succeeded.** "Leave the group, then go back to the list" therefore never leaves
409
339
  anyone on a list they are still in — a failure or an abort drops the command and
410
340
  the error stays on the control, where the user is looking. `<Picker
411
- type="confirm">` follows the same rule with its `action` and `command`, and a
412
- form says it as `data-after-send`.
341
+ type="confirm">` follows the same rule with its `action` and `command`, and so
342
+ does `<Form command>`, which follows a submission only once it went through.
413
343
 
414
344
  ### Deleting something, then leaving the page it was on
415
345
 
416
- The everyday form of that pair, and the press is a `<Picker type="confirm">`
417
- rather than a `<Button>`: what is destroyed is asked about first, the question
418
- is a popup the picker already owns, and the work runs back on the trigger.
346
+ The everyday form of that pair, where what is destroyed is asked about first. A
347
+ short question is asked on the button itself: `<Button confirm>` replaces it with
348
+ the question and a Confirmer/Annuler pair, and the second press does what the
349
+ button was for, its wait and its error on that button. A question long enough to
350
+ want a popup, or a row with no room for it, is a `<Picker type="confirm">`, whose
351
+ work runs back on the trigger:
419
352
 
420
353
  ```jsx
421
354
  <Picker
@@ -430,23 +363,18 @@ is a popup the picker already owns, and the work runs back on the trigger.
430
363
  Yes closes the question and the request goes from the trigger — busy while it
431
364
  runs, an error callout on it if it fails — and the list is reached only once the
432
365
  group is really gone. A failure leaves the reader on the page of a group they
433
- are still in, which is the truth, with the reason in front of them.
434
-
435
- Navigating first and awaiting after — `navTo(...)` then `await leave(...)` — buys
436
- the frame where this page would render without what it displays, and pays for it
437
- with a reader standing in a list that still holds the group they think they left,
438
- told nothing. There is usually no frame to buy anyway: the response and the
439
- navigation land in the same task.
440
-
441
- Where to go afterwards is the app's decision, not navi's. Staying on the page to
442
- say it is done, or letting the route land on its empty state, are as good as
443
- leaving for the list; what the `command` settles is only that the success is
444
- what decides.
445
-
446
- A function calling the verb, not the instance: the trigger is busy and draws the
447
- error either way, and `bindParams` earns its place only when something else on
448
- screen must see this very run — see
449
- [resource.md](./resource.md#a-function-calling-the-verb-or-the-instance).
366
+ are still in, which is the truth, with the reason in front of them. Navigating
367
+ first and awaiting after — `navTo(...)` then `await leave(...)` — buys nothing
368
+ (the response and the navigation land in the same task) and, on a failure,
369
+ leaves the reader in a list that still holds the group they think they left,
370
+ told nothing.
371
+
372
+ Where to go afterwards is the app's decision: staying on the page to say it is
373
+ done, or letting the route land on its empty state, are as good as leaving for
374
+ the list. What the `command` settles is only that the success is what decides.
375
+ (The `action` above is a function calling the verb; what that gives up against
376
+ the verb's instance is in
377
+ [resource.md](./resource.md#a-function-calling-the-verb-or-the-instance).)
450
378
 
451
379
  On a link — a `Link`, a `<Button href>` or `<Button route>` — the three fire on
452
380
  the press, before the navigation, and the navigation waits for none of them: a
@@ -455,39 +383,24 @@ writes a draft synchronously is found by the next page, and a request goes on
455
383
  its own while the page changes. Work that decides the destination is not a
456
384
  link's: it navigates itself, from a `<Button action>`.
457
385
 
458
- The press that looks like a fourth case is the one that opens something and then
459
- does something with what came of it — "save this guest", pressed on a row,
460
- replacing the guest once the profile exists. It is not a fourth case and it is
461
- not a dialog plus a way home: it is a `Picker`, whose whole shape is a trigger,
462
- a popup, and an `action` that runs on what the popup settled.
463
-
464
- ```jsx
465
- <Picker variant="icon" rightSlotIcon={<DisketteSvg />} action={onCreated}>
466
- <GuestSavePrompt kind="player" name={guest.name} />
467
- </Picker>
468
- ```
469
-
470
- Written this way, what the popup needs to know travels as props rather than
471
- through the press, and the popup is built the first time it opens rather than
472
- once per row. See
473
- [popup_open.md](./popup_open.md#a-press-that-opens-a-popup-and-acts-on-it).
386
+ A press that opens something and then does something with what came of it —
387
+ "save this guest", pressed on a row — is not a fourth case: it is a `Picker`, a
388
+ trigger, a popup, and an `action` that runs on what the popup settled (see
389
+ [popup_open.md](./popup_open.md#a-press-that-opens-a-popup-and-acts-on-it)).
474
390
 
475
391
  What is left for an `onClick` is what no value can express — imperative work
476
- with nothing to open and nothing to send. And one nuance worth knowing: on a
477
- control with `selfInteractions`, a caller's `onClick` runs inside that control's
478
- own interaction gate rather than firing from the DOM (see
479
- [interactions.md](./interactions.md#an-affordance-inside-somebody-elses-box-selfinteractions)),
480
- so the usual objection — an `onClick` fires on a read-only control — does not
481
- apply there. Everywhere else it does.
392
+ with nothing to open and nothing to send. The usual objection to it — it fires
393
+ on a read-only control — holds everywhere but on a control with
394
+ `selfInteractions`, whose gate a caller's `onClick` runs inside
395
+ ([interactions.md](./interactions.md#an-affordance-inside-somebody-elses-box-selfinteractions)).
482
396
 
483
397
  ## `uiAction` mirrors the state, it does not report a gesture
484
398
 
485
- `uiAction` fires whenever the control's state changes, whoever changed it — a
486
- button and a link being the exception their shape makes plain: they hold no
487
- state to mirror, so each press is exactly one call. Everywhere else the user
488
- typing is one cause among several: a `value` prop coming back down after a
399
+ `uiAction` fires whenever the control's state changes, whoever changed it (a
400
+ button and a link hold no state to mirror: there, each press is one call). The
401
+ user typing is one cause among several: a `value` prop coming back down after a
489
402
  render, a popup control propagating its choice up to the picker holding it, a
490
- group cascading a value into its children — all of them reach `uiAction` too, so
403
+ group cascading a value into its children — all of them reach `uiAction`, so
491
404
  that a signal or a local variable listening to it never drifts out of sync.
492
405
  Reading it as "the user did something" is the natural mistake.
493
406