@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
@@ -37,9 +37,8 @@ something nobody can change.
37
37
 
38
38
  ## A bound signal works in both directions
39
39
 
40
- This is the part that does not show in a call site: `signal` is not a seed. The
41
- control writes every change into it, **and follows it when something else
42
- writes it**.
40
+ `signal` is not a seed. The control writes every change into it, **and follows
41
+ it when something else writes it**:
43
42
 
44
43
  ```jsx
45
44
  const minutesSignal = useSignal(0);
@@ -58,32 +57,35 @@ minutesSignal.value = 30;
58
57
 
59
58
  "Every change" includes the ones the control did not decide: a group placing
60
59
  its children, a picker filling its popup at open, and the same picker putting
61
- back what it held when Escape cancels. The signal mirrors the control, so it
62
- always says where the control actually is — which is what lets a settings sheet
63
- reopen on the tab it was left on (see
64
- [control_object.md](./control_object.md#a-settings-sheet)). What it does NOT
65
- mirror is a picker's popup being played with: a picker's own signal is written
66
- when the picker commits, on close.
67
-
68
- Both halves are worth knowing about, because each replaces a habit:
69
-
70
- - the write-back replaces `uiAction={(v) => (mySignal.value = v)}`;
71
- - the follow replaces the `key` or the `value`/`uiAction` pair used to push an
72
- outside change into a control.
73
-
74
- The follow goes all the way up: a bound control that lives inside a group — two
75
- wheels in a `WheelGroup`, a field in a `ControlGroup` — makes that group
76
- re-aggregate when its signal is written, and the form above sees the new value.
77
- A value pushed in from anywhere is an answer like any other: the wheels roll,
60
+ back what it held when Escape cancels. So the signal always says where the
61
+ control actually is — which is what lets a settings sheet reopen on the tab it
62
+ was left on ([control_object.md](./control_object.md#a-settings-sheet)). A
63
+ picker's own signal follows its popup the same way, gesture by gesture, and so
64
+ does its `uiAction`. What waits for the close is the picker's `action` — and a
65
+ suggestion nobody touched, which becomes the answer when the popup closes on it.
66
+
67
+ The write-back replaces the `uiAction` that copies the value into a signal:
68
+
69
+ ```jsx
70
+ // ✗ the control is told what it now holds, and the app writes it back down
71
+ <Picker value={side} uiAction={(value) => (sideSignal.value = value)} />
72
+ // ✓
73
+ <Picker signal={sideSignal} />
74
+ ```
75
+
76
+ The follow goes all the way up: a bound control inside a group — two wheels in
77
+ a `WheelGroup`, a field in a `ControlGroup` — makes that group re-aggregate when
78
+ its signal is written, and the form above sees the new value: the wheels roll,
78
79
  and the submit lights up. Which is why a **button** offering such a value is not
79
- a hand-written signal write — see the next section.
80
+ a hand-written signal write — see
81
+ [below](#a-button-that-proposes-a-value-is---navi-update).
80
82
 
81
83
  ## A picker fills its popup: the control inside already knows
82
84
 
83
85
  A picker mirrors the control in its popup, both ways
84
- ([popup_open.md](./popup_open.md#composing-a-value-or-doing-work)) — and the
85
- first half of that mirroring happens **at open**: the picker fills the control
86
- with the value it holds, before anything is shown.
86
+ ([popup_open.md](./popup_open.md#composing-a-value-or-doing-work)), and the
87
+ first half happens **at open**: the picker fills the control with the value it
88
+ holds, before anything is shown.
87
89
 
88
90
  ```jsx
89
91
  // the row matching the picker's value is already selected, painted and
@@ -101,20 +103,16 @@ with the value it holds, before anything is shown.
101
103
 
102
104
  So a `current`, a `selected` per row, or a `value` computed from what the picker
103
105
  holds is not "being explicit": it is a second answer to a question already
104
- answered, and it is the one that goes stale — the picker's value moves on a
105
- cancel, on a `--navi-update`, on a signal written elsewhere, and none of those
106
- pass through the prop the app is computing.
107
-
108
- What is left for the app is what to make of the selection, not what it is:
109
- a row is marked (`aria-selected`) and painted by the list itself, so anything
110
- extra hangs off that marker in CSS rather than off a prop.
106
+ answered, and the one that goes stale — the picker's value moves on a cancel, a
107
+ `--navi-update`, a signal written elsewhere, and none of those pass through that
108
+ prop. The list marks the selected row itself (`aria-selected`); anything extra
109
+ hangs off that marker in CSS.
111
110
 
112
111
  ## A button that proposes a value is `--navi-update`
113
112
 
114
- A shortcut beside a control — "Tous niveaux" / "Aucun niveau" next to a list of
115
- levels, "1h / 1h30 / 2h" next to a pair of wheels, a suggestion under a field —
116
- is a value being offered to that control. It is not an action, and it is not a
117
- signal to write by hand:
113
+ A shortcut beside a control — "Tous niveaux" next to a list of levels, "1h /
114
+ 1h30 / 2h" next to a pair of wheels, a suggestion under a field — is a value
115
+ offered to that control. It is not an action, and not a signal to write by hand:
118
116
 
119
117
  ```jsx
120
118
  <WheelGroup id="duration" signal={durationSignal}>
@@ -131,25 +129,21 @@ signal to write by hand:
131
129
  </Button>
132
130
  ```
133
131
 
134
- - the **value** is the button's own `value`, whatever shape it has — a string, an
132
+ - the **value** is the button's own `value`, whatever its shape — a string, an
135
133
  array of levels, an object of two wheels;
136
- - the **target** is `commandFor`, naming the control's id — left out, the nearest
137
- control around the button is used, which is what a button placed inside the
134
+ - the **target** is `commandFor`, naming the control's id — left out, the
135
+ nearest control around the button, which is what a button placed inside the
138
136
  control it proposes to wants;
139
- - and the press goes through the same gate as every other interaction, so a
137
+ - the press goes through the same gate as every other interaction, so a
140
138
  read-only, disabled or busy control **refuses it and says why**.
141
139
 
142
- That last point is the whole reason, and the counter-example is what everybody
143
- writes first:
140
+ That last point is the reason, and the counter-example is what everybody writes
141
+ first:
144
142
 
145
143
  ```jsx
146
144
  // ✗ not gated — plain DOM. On a read-only sheet the button greys out and fires
147
145
  // all the same, rewriting a value nobody is allowed to change.
148
- <Button
149
- onClick={() => {
150
- durationSignal.value = { hours: 1, minutes: 30 };
151
- }}
152
- >
146
+ <Button onClick={() => (durationSignal.value = { hours: 1, minutes: 30 })}>
153
147
  1h30
154
148
  </Button>
155
149
  ```
@@ -159,20 +153,17 @@ to a control: moving something else on screen, seeding state before anything is
159
153
  drawn.
160
154
 
161
155
  The id goes **on the control**, and a group is one — `ControlGroup`, `Form`,
162
- `WheelGroup`, `List selectable`. Put it on a layout box around the control and
163
- the command finds an element that holds no value; navi says so in dev rather
164
- than letting the press do nothing at all. An id that matches nothing is a dev
165
- warning too, naming the id it looked for.
156
+ `WheelGroup`, `List selectable`. Put on a layout box around the control, the
157
+ command finds an element that holds no value, and navi says so in dev rather
158
+ than letting the press do nothing; an id that matches nothing is a dev warning
159
+ too, naming the id it looked for.
166
160
 
167
161
  ### `--navi-update:smooth`: the control is seen answering
168
162
 
169
- A shortcut sets the value at once, and on a phone the thumb covers the button
170
- while the eye is on the control: with two wheels above four shortcuts, « soir »
171
- swaps the digits and nothing shows which wheel changed, or by how much. The
172
- control should be the thing that answers — seen moving to the value the way it
173
- moves under a finger.
174
-
175
- That is what the `:smooth` argument asks for:
163
+ On a phone the thumb covers the shortcut while the eye is on the control, and a
164
+ value swapped at once shows neither which control changed nor by how much.
165
+ `:smooth` asks for the control to be seen moving to the value, the way it moves
166
+ under a finger:
176
167
 
177
168
  ```jsx
178
169
  <Button command="--navi-update:smooth" commandFor={hoursId} value={evening}>
@@ -180,37 +171,26 @@ That is what the `:smooth` argument asks for:
180
171
  </Button>
181
172
  ```
182
173
 
183
- - **The control decides how it moves, and when the value lands.** The
184
- argument says nothing about pixels or duration, and the two controls that
185
- know it answer it differently because their movement is a different thing. A
186
- wheel is a value one lands ON: it takes the value at once — whoever reads the
187
- control right after the press (a form, `--navi-send`, a signal) gets it — and
188
- scrolls to the row the way it glides after an arrow key, the short way round
189
- when it loops. A spin is a value one GOES TO: it plays the travel a chevron
190
- would play, one travel whatever the distance, and the value lands with the
191
- slides, 250 ms later (`duration`). A slider would slide its thumb; a control
192
- with nothing to move sets its value and that is all. A control that does not
193
- know the argument is not broken — it answers like a plain `--navi-update`.
194
- - **A spin says no the way its chevron does.** A value past `min`/`max` — or
195
- one whose first step would be — is not travelled to at all: the same message
196
- the chevron that way gives, said on the spin, and the value is left alone.
197
- - **A gesture on the control itself is never fought.** A wheel being dragged
198
- or flung keeps reporting its own rows; the requested value is where it goes
199
- once the finger's movement is over. A value a wheel merely caught up with is
200
- not a choice either: arriving on it fires no settle, no `action` — those
201
- belong to the user's own inputs. A spin is the other way round, and for the
202
- same reason: the travel IS the answer being given, so landing tells
203
- `uiAction` about it, with the press that asked for it.
204
- - **`prefers-reduced-motion` keeps the instant swap.** The option describes
205
- how the change is shown, and whoever asked to see less motion is answered
206
- first.
207
-
208
- The same request is available from JS, for what is not a button —
209
- `dispatchRequestSetUIState(el, value, { behavior: "smooth" })` — and it
210
- survives a group: sent to a `TimeRangeWheel`, it reaches each of its wheels.
211
-
212
- _Reference: the wheel (`wheel.jsx`, `pendingBehaviorRef`) and the spin
213
- (`picker_spin.jsx`, `travelsPending`) are the controls honouring it._
174
+ - **The control decides how it moves, and when the value lands.** A wheel takes
175
+ the value at once — whoever reads the control right after the press (a form,
176
+ `--navi-send`, a signal) gets it — and glides to the row, the short way round
177
+ when it loops. A spin plays the one travel a chevron would play, whatever the
178
+ distance, and the value lands with the slides (`duration`, 250 ms by
179
+ default); a value past `min`/`max` is refused the way that chevron refuses
180
+ it, and left alone. An `editable` spin (a `NumberSpin`, so a `TimeSpin`) is a
181
+ field, and nothing travels around a field: the value is swapped in place. A
182
+ control that does not know the argument answers like a plain `--navi-update`.
183
+ - **A gesture on the control itself is never fought.** A wheel being dragged or
184
+ flung keeps reporting its own rows, and goes to the requested value once the
185
+ finger's movement is over. A value a wheel merely caught up with is not a
186
+ choice: it fires no settle, no `action`. A spin's landing IS the answer being
187
+ given, so it tells `uiAction`, with the press that asked for it.
188
+ - **`prefers-reduced-motion` keeps the instant swap**: whoever asked to see less
189
+ motion is answered first.
190
+
191
+ From JS, for what is not a button:
192
+ `dispatchRequestSetUIState(el, value, { behavior: "smooth" })` — sent to a
193
+ group such as `TimeRangeWheel`, it reaches each of its wheels.
214
194
 
215
195
  ## `signal` + `defaultValue`: the answer and where it starts
216
196
 
@@ -242,14 +222,12 @@ or taken back (the action failed, the popup was cancelled).
242
222
 
243
223
  This holds for a control, for a group given a `defaultValue` (a selectable list,
244
224
  a `ControlGroup`), and for everything inside a picker's popup, which stays
245
- mounted while closed. So the record goes in as `defaultValue` and nothing
246
- else: no `key` to remount the popup when it changes. A key is worse than
247
- unnecessary here, since it also destroys whatever is being typed when the
248
- answer to ANOTHER field arrives.
249
-
250
- A picker's cancel follows the same line. It puts back what the picker held at
251
- open, which undoes the person's edits, but what the outside moved during the
252
- opening stays moved.
225
+ mounted while closed. So the record goes in as `defaultValue` and nothing else —
226
+ no `key` to remount the popup when it changes: a key also destroys whatever is
227
+ being typed when the answer to ANOTHER field arrives. A picker's cancel follows
228
+ the same line: it undoes the person's edits
229
+ ([popup_open.md](./popup_open.md#what-cancel-actually-undoes)), and what the
230
+ outside moved during the opening stays moved.
253
231
 
254
232
  ## Drawing what a control holds
255
233
 
@@ -257,6 +235,8 @@ A picker's trigger usually shows the choice (a label for a code, a summary of
257
235
  an object). Read it from the picker rather than keeping a copy:
258
236
 
259
237
  ```jsx
238
+ // a `ui` component is handed `value`, `loading` and `interactive`;
239
+ // a `ui` element reads the same three with usePickerState()
260
240
  const VisibilityLabel = ({ value }) => <Text>{labelOf(value)}</Text>;
261
241
 
262
242
  <Picker name="visibility" defaultValue={user.visibility} ui={VisibilityLabel}>
@@ -264,13 +244,10 @@ const VisibilityLabel = ({ value }) => <Text>{labelOf(value)}</Text>;
264
244
  </Picker>;
265
245
  ```
266
246
 
267
- `ui={Component}` is handed `value`, `loading` and `interactive`. A `ui` given as
268
- an element reads the same three with `usePickerState()`.
269
-
270
247
  What not to write: `const s = useSignal(defaultValue)` bound as `signal={s}`,
271
248
  just to have something to read. `useSignal` reads its argument once, so the
272
249
  signal never hears the record move, and a bound signal takes precedence over the
273
- control's own `defaultValue`. The field is stuck on its first value. A signal is
250
+ control's own `defaultValue`: the field is stuck on its first value. A signal is
274
251
  for a value the app owns and writes itself (a url param, state shared between
275
252
  screens), not a mirror kept for drawing.
276
253
 
@@ -287,13 +264,12 @@ attribute:
287
264
  | `List selectable multiple`, checkbox group | the array of selected values |
288
265
 
289
266
  A group (a selectable list, a checkbox group) writes its whole selection into
290
- the signal, not one item's value — its children put it together between them.
291
-
292
- A `<Form>` (or a `<ControlGroup>`) takes one the same way, holding the whole
293
- object: its named children are filled from it, they move when something else
294
- writes it, and what they change is written back into it. One signal for a screen
295
- whose values arrive together — see
296
- [create_and_edit.md](./create_and_edit.md#two-screens-two-states).
267
+ the signal, which its children put together between them. A `<Form>` (or a
268
+ `<ControlGroup>`) takes one the same way, holding the whole object: its named
269
+ children are filled from it, move when something else writes it, and write back
270
+ what they change. One signal for a screen whose values arrive together and where
271
+ no field needs a url of its own — the fields then take nothing but their `name`
272
+ (see [create_and_edit.md](./create_and_edit.md#two-screens-two-states)).
297
273
 
298
274
  ## Empty keeps the shape of the question
299
275
 
@@ -309,36 +285,28 @@ it holds something. A list of days nobody picked is `[]`, not `""`:
309
285
  | radio, checkbox holding a value of its own | absent |
310
286
 
311
287
  This is what a clear (`--navi-clear`, a row's cross) leaves behind and what the
312
- object around it carries, so the conversion nobody writes — `value.days || []` —
313
- is not needed, and not needed only after having watched the wrong shape reach
314
- the server.
288
+ object around it carries, so `value.days || []` is not needed.
315
289
 
316
290
  A checkbox is a member of a set, the way HTML has it: checked it carries its
317
291
  `value` (`"on"` when it was given none), unchecked it carries nothing at all —
318
292
  which is what lets several checkboxes sharing a name aggregate into an array. A
319
- checkbox that is a yes/no rather than a member says so with `value={true}`, and
320
- is then `true` or `false`. Its bound `signal` holds the boolean either way (see
321
- the table above): what a signal on a checkbox is about is whether it is checked.
293
+ checkbox that is a yes/no says so with `value={true}`, and is then `true` or
294
+ `false`. Its bound `signal` holds the boolean either way: what a signal on a
295
+ checkbox is about is whether it is checked.
322
296
 
323
297
  ## Which controls take a `signal`
324
298
 
325
299
  All of them: `Input` (every type), `Picker`, `Select`, `Wheel`, `Spin`,
326
- `List selectable` (single and multiple), and control groups in general. Anything
327
- that is a navi control goes through the same state controller, and the same
328
- `signal` prop.
329
-
330
- `SlideContainer` takes one too, though it is layout rather than a control: the
331
- area it shows is a piece of state like any other, and binding it is what lets
332
- something else read where the slides are — or move them by writing it. Bind it
333
- to a `stateSignal` a route declares as a search param and the area is in the
334
- address, with no route per slide — see
335
- [navigation.md](./navigation.md#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from).
300
+ `List selectable` (single and multiple), and control groups in general — they
301
+ go through the same state controller. `SlideContainer` takes one too, for the
302
+ area it shows (see
303
+ [navigation.md](./navigation.md#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)).
336
304
 
337
305
  Inside a `List selectable` you can bind the list, or give each `List.Item` its
338
306
  own `selected` — but not expect the two to arbitrate. An item that declares
339
307
  `selected` is answering for itself, and the list's signal does not reposition
340
- it. On screen that reads as a list where clicking does nothing, so navi says it
341
- in dev the moment the two claims meet: bind one end or the other, not both.
308
+ it: a list where clicking does nothing. navi says it in dev the moment the two
309
+ claims meet — bind one end or the other, not both.
342
310
 
343
311
  ## The PROP is what controls, not its value
344
312
 
@@ -364,9 +332,9 @@ tries to place a child that has claimed itself this way.
364
332
 
365
333
  ## A yes/no shown as two rows
366
334
 
367
- A checkbox is one way to ask a yes/no; two rows one can compare — "Publique, we
368
- propose it to players looking for this kind of game" against "Privée, it travels
369
- only by the link you send" — is another, and it is the same value:
335
+ A checkbox is one way to ask a yes/no; two rows one can compare — "Publique" and
336
+ "Privée", each with the sentence that tells them apart — is another, and it is
337
+ the same value:
370
338
 
371
339
  ```jsx
372
340
  <Picker name="visibility" signal={isPublicSignal}>
@@ -380,8 +348,7 @@ only by the link you send" — is another, and it is the same value:
380
348
  Nothing translates: the row holds the boolean, the form carries the boolean.
381
349
  What cannot be done is give a row `undefined` to mean "no value" — `undefined`
382
350
  is what UNCHECKED means, here as in HTML, so such a row can never be ticked.
383
- A value the control can hold is a value one can see; "nothing chosen" is the
384
- absence of a row, not a row.
351
+ "Nothing chosen" is the absence of a row, not a row.
385
352
 
386
353
  ## Clearing, resetting, and what is shown meanwhile
387
354
 
@@ -396,84 +363,53 @@ Three things that look alike and are not:
396
363
 
397
364
  So a row whose cross means "back to the one from my profile" needs nothing of
398
365
  its own: clearing empties it, and `placeholder` is where that sentence is
399
- written.
366
+ written. The app then reads `undefined` (or the empty of the row's type), which
367
+ is what "nothing chosen here, use the usual answer" means everywhere else.
400
368
 
401
369
  ```jsx
402
370
  <Picker clearable placeholder="Celui de mon profil" signal={sideSignal} />
403
371
  ```
404
372
 
405
- What the app then reads is `undefined` (or the empty of the row's type), which
406
- is what "nothing chosen here, use the usual answer" already means everywhere
407
- else — the same rule as an emptied signal falling back on its default.
408
-
409
373
  A control that cannot show emptiness — a pair of wheels has no blank row to land
410
374
  on — takes the same `placeholder` as a POSITION instead of a word (see
411
- `TimeWheel`): shown, and still not an answer.
375
+ `TimeWheel`): shown, and still not an answer. A clear, or a value written as
376
+ `undefined`, puts it back on that position and back to answering nothing.
412
377
 
413
378
  ## `value` and `signal` exclude each other
414
379
 
415
380
  `value` (or `checked`) says "you hold it", `signal` says "the signal holds it".
416
381
  Passing both is a call site to fix: **the signal wins and the other prop is
417
382
  ignored** — on a leaf control as on a group (a selectable list, a checkbox
418
- group) — and navi says so in dev. One owner, whichever half of the binding you
419
- look at.
420
-
421
- Replacing `value` with `signal` also means dropping the `uiAction` that wrote
422
- the signal by hand — writing it is exactly what the binding does. Keep
423
- `uiAction` only for what is not "remember the value": logging, a side effect,
424
- something else moving with it.
383
+ group) — and navi says so in dev. What is left for a `uiAction` once the signal
384
+ is bound: [state_binding.md](./state_binding.md#what-is-left-for-the-callback).
425
385
 
426
386
  ## A `stateSignal` brings more than a value
427
387
 
428
- A plain signal (`useSignal`, `signal()`) is enough to bind a control. A
429
- `stateSignal` also carries its own `options`, and a control reads them so it
388
+ A plain signal (`useSignal`, `signal()`) is enough to bind a control, both ways.
389
+ A `stateSignal` also carries its own `options`, and a control reads them so it
430
390
  does not have to be told twice: `type` (which decides the input type and the
431
391
  validation messages), `min`, `max`, `step`, and its **default**, which seeds
432
392
  `defaultValue`/`defaultChecked` — so a reset goes back to the signal's original
433
393
  default rather than to whatever it happened to hold at the last render.
434
394
 
435
- That is the only difference. A plain signal binds the same way in both
436
- directions; it just has nothing extra to say.
437
-
438
395
  ## A time of day: typed, or turned
439
396
 
440
- A time of day, and a span between two of them, come as a pair of components,
441
- and the choice between them is about the GESTURE: `TimeSpin`/`TimeRangeSpin`
442
- are fields one types in, `TimeWheel`/`TimeRangeWheel` are wheels one turns.
443
- Both carry a single `"HH:MM"` (or `{ start, end }` for a span), so a form holds
444
- one field either way.
445
-
446
- Prefer the wheels whenever a half-written value would be nonsense: a time typed
447
- digit by digit goes through states that are not times ("1" on its way to "18"),
448
- each of them bounded and corrected under the fingers, while a wheel only ever
449
- shows values that exist. Two things only the wheels have:
450
-
451
- - the bounds of a span PUSH each other while they turn (`minDuration`) instead
452
- of being refused at send;
453
- - `placeholder` is a position shown without being an answer — for a span that
454
- is optional ("any time of day") on wheels that have no blank row to land on. A
455
- clear, or a value written as `undefined`, puts such a pair back on its
456
- placeholder and back to answering nothing, so a row's cross means what it says
457
- (the rule of [clearing](#clearing-resetting-and-what-is-shown-meanwhile)).
458
-
459
- `hours` bounds what the wheels offer (`{ min: 7, max: 21 }`, or the list
460
- itself): rows nobody can land on are rows in the way.
461
-
462
- Reference: `src/control/picker/preset/spin_time.jsx`,
463
- `src/control/wheel/wheel_time.jsx`.
397
+ `TimeSpin`/`TimeRangeSpin` are fields one types in, `TimeWheel`/`TimeRangeWheel`
398
+ are wheels one turns; both carry a single `"HH:MM"` (or `{ start, end }` for a
399
+ span), so a form holds one field either way. Prefer the wheels whenever a
400
+ half-written value would be nonsense: a time typed digit by digit goes through
401
+ states that are not times ("1" on its way to "18"), while a wheel only ever
402
+ shows values that exist. Only the wheels push the bounds of a span apart as they
403
+ turn (`minDuration`, which the spins check at send) and take a `placeholder` as
404
+ a position. Their props are in their JSDoc
405
+ (`src/control/picker/preset/spin_time.jsx`, `src/control/wheel/wheel_time.jsx`).
464
406
 
465
407
  ## See also
466
408
 
467
- - [state_binding.md](./state_binding.md) — the same rule beyond controls:
468
- a `SlideContainer`'s area, a popup being open, the position in the app — all
469
- bound rather than copied back by a callback
470
- - [form_changed.md](./form_changed.md) — what a form makes of each of these:
471
- which fields it counts as already answered, and when it sends nothing
472
- - [control_object.md](./control_object.md) — several controls reading as one
473
- object: which group aggregates them, and how the value travels down by name
474
- - [control_group.md](./control_group.md) — several controls reading as one
475
- framed object
476
- - [actions.md](./actions.md#action-or-uiaction) — `action` or `uiAction`: which
477
- one carries loading and error, and which one carries nothing
478
- - [popup_open.md](./popup_open.md#escape-cancels-the-other-gestures-keep) — what
479
- a cancelled popup does to the value inside it
409
+ - [state_binding.md](./state_binding.md) — the same rule beyond controls
410
+ - [form_changed.md](./form_changed.md) — which fields a form counts as already
411
+ answered, and when it sends nothing
412
+ - [control_object.md](./control_object.md) — several controls answering one
413
+ object
414
+ - [group.md](./group.md) — several controls drawn as one frame
415
+ - [actions.md](./actions.md#action-or-uiaction) — `action` or `uiAction`