@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
@@ -31,13 +31,13 @@ its own `action`/`uiAction`/`command` if you want one.
31
31
  // worth { street, city, zip }
32
32
  ```
33
33
 
34
- It is also the brick composite controls are built from:
35
- `aggregateChildStates` / `distributeChildUIState` replace "one key per child"
36
- with a value of your own — two wheels that are one number of minutes, three
37
- fields that are one date. A group with them takes and hands back a single
38
- value, so it can be driven by one `value`/`signal` like any other control. It is
39
- what `TimeSpin` is made of, and the same mechanism `InputDuration`, `SpinGroup`
40
- and `WheelGroup` use for their own members.
34
+ It is also the brick composite controls are built from: with
35
+ `aggregateChildStates` / `distributeChildUIState` (see its JSDoc) a group takes
36
+ and hands back a single value of your own — two wheels that are one number of
37
+ minutes, three fields that are one date — and is driven by one `value`/`signal`
38
+ like any other control. `TimeRangeSpin` is a `ControlGroup` of two `TimeSpin`s;
39
+ `SpinGroup` (which a `TimeSpin` is), `WheelGroup` and `InputDuration` use the
40
+ same mechanism for their own members.
41
41
 
42
42
  ## `<Form>`: the shape, plus a send
43
43
 
@@ -45,10 +45,8 @@ A `<Form>` aggregates exactly the same way. What it adds is everything about
45
45
  **sending**: the reference it measures against, the refusal to act when nothing
46
46
  changed (see [form_changed.md](./form_changed.md)), the submit button and its
47
47
  `readOnlyWhileFormUnchanged`, what follows a successful send (`command`), and a
48
- `<form>` element with the browser's own submit/reset.
49
-
50
- So the choice is not about the value, it is about whether this cluster is a
51
- **question with a send**:
48
+ `<form>` element with the browser's own submit/reset. So the choice is not
49
+ about the value, it is about whether this cluster is a **question with a send**:
52
50
 
53
51
  - a shape inside a bigger whole → `ControlGroup`;
54
52
  - something the user sends → `Form`.
@@ -58,11 +56,9 @@ the `<form>` element — and it is a **second question**, not a sub-object of th
58
56
  first: what it holds is its own, and the form around it never sees it. Not in
59
57
  the value it sends, not in its "nothing changed", and not in the constraints it
60
58
  checks before sending — a required field in a popup nobody opened cannot refuse
61
- the outer submit and point at something the screen is not showing.
62
-
63
- So the inner form carries all of the above with it: its own reference, its own
64
- "nothing changed", its own submit story. Grouping three fields into a sub-object
65
- should cost none of that — that is a `ControlGroup`.
59
+ the outer submit and point at something the screen is not showing. Grouping
60
+ three fields into a sub-object should cost none of that — that is a
61
+ `ControlGroup`.
66
62
 
67
63
  ## Naming, and what a nameless group does
68
64
 
@@ -70,10 +66,9 @@ A group's `name` is the key its value lands under, in the group above it — a
70
66
  `ControlGroup`, or a `Form` inside something that is not a form (a picker, say).
71
67
  A form inside a form lands nowhere: it answers for itself, named or not.
72
68
 
73
- Left nameless, a group is a **grouping**: it exists to hold its children
74
- together (shared navigation, a visual cluster) without claiming a key, and what
75
- it holds is merged into the object around it as if its children had been written
76
- there.
69
+ Left nameless, a group is a **grouping**: it holds its children together (shared
70
+ navigation, a visual cluster) without claiming a key, and what it holds is
71
+ merged into the object around it as if its children had been written there.
77
72
 
78
73
  ```jsx
79
74
  <ControlGroup name="when">
@@ -86,11 +81,10 @@ there.
86
81
  // worth { day, hours, minutes } — the WheelGroup adds no key of its own
87
82
  ```
88
83
 
89
- A nameless **leaf** is a different story: it is a control whose value has
90
- nowhere to go, and it is warned about. A control that is not answering this
91
- group's question at all — one that only opens something, one that commits on
92
- its own — says so with `standalone` and leaves the group entirely (see
93
- [form_changed.md](./form_changed.md#a-control-that-answers-for-itself)).
84
+ A nameless **leaf** is a different story: a control whose value has nowhere to
85
+ go, and it is warned about — unless it is not answering this group's question at
86
+ all, which it says with `standalone`
87
+ ([form_changed.md](./form_changed.md#a-control-that-answers-for-itself)).
94
88
 
95
89
  ## A picker whose value is an object
96
90
 
@@ -98,39 +92,33 @@ its own — says so with `standalone` and leaves the group entirely (see
98
92
  aggregates (next to `type="array"`, whose value is a selection):
99
93
 
100
94
  ```jsx
101
- <Picker name="when" type="object" value={{ day, hours, minutes }}>
95
+ <Picker name="when" type="object" signal={whenSignal}>
102
96
  <ControlGroup>
103
- <DaySpin name="day" signal={daySignal} />
97
+ <DaySpin name="day" />
104
98
  <WheelGroup>
105
- <Wheel name="hours" signal={hoursSignal} />
106
- <Wheel name="minutes" signal={minutesSignal} />
99
+ <Wheel name="hours" />
100
+ <Wheel name="minutes" />
107
101
  </WheelGroup>
108
102
  </ControlGroup>
109
103
  </Picker>
104
+ // whenSignal holds { day, hours, minutes }
110
105
  ```
111
106
 
112
- Two things to get right:
113
-
114
107
  - **One control in the popup, and it must be the group.** A picker syncs with a
115
108
  single control: the first one receives the picker's whole value and is the
116
- only one read back. Two controls side by side in a popup is the shape to
117
- avoid — the second is neither filled nor collected, and navi says so in dev.
118
- Wrap them in one `ControlGroup` (or one `Form`, when the popup has a send of
119
- its own — but only while the picker itself is not inside a form, since a form
120
- inside a form answers for itself and the picker would be left with nothing to
121
- fill).
122
- - **The value travels by name.** What the picker was given goes down into the
123
- group, which hands each named child its own key; a nameless grouping inside
124
- receives the whole object and picks out what it names. Give a picker of this
125
- kind a scalar type and there is no group to distribute anything — the whole
126
- object lands on one control, which is how `"[object Object]"` ends up in a
127
- url.
128
- - **A control that helps FIND the answer is not the answer.** A search box above
129
- a long list, a "select all" beside it: they are tools, and a tool says so with
130
- `standalone`. The picker then walks past it and talks to the list, which is
131
- what a popup made of one choice and the means to reach it needs. Never put it
132
- on the control that IS the value — the picker would have nobody left to fill,
133
- and the popup would open blank on a value it holds.
109
+ only one read back; a second one beside it is neither filled nor collected,
110
+ and navi says so in dev. Wrap them in one `ControlGroup` (or one `Form`, for a
111
+ popup with a send of its own — only while the picker is not inside a form: a
112
+ form inside a form answers for itself, leaving the picker nothing to fill).
113
+ - **The value travels by name.** The group hands each named child its own key;
114
+ a nameless grouping inside receives the whole object and picks out what it
115
+ names. Give such a picker a scalar type and the whole object lands on one
116
+ control — which is how `"[object Object]"` ends up in a url.
117
+ - **A control that helps FIND the answer is not the answer.** A search box
118
+ above a long list, a "select all" beside it are tools, marked `standalone`:
119
+ the picker walks past them to the list. Never put it on the control that IS
120
+ the value — the picker would have nobody left to fill, and the popup would
121
+ open blank on a value it holds.
134
122
 
135
123
  ```jsx
136
124
  <Picker name="place_ids" type="array">
@@ -143,109 +131,56 @@ Two things to get right:
143
131
 
144
132
  A popup can also hold **no** answer for the picker: several lists that are the
145
133
  same one choice, each acting on every touch, nothing read back on close. Mark
146
- them all `standalone` and the picker talks to none of them — the shape to reach
147
- for when one `ControlGroup` around them would be wrong, because its value would
148
- be one key per list where the answer is a single value.
134
+ them all `standalone` and the picker talks to none of them — rather than one
135
+ `ControlGroup` around them, whose value would be one key per list where the
136
+ answer is a single value.
149
137
 
150
138
  ## A group holds what it was given
151
139
 
152
140
  A group's value looks like it is made of its children, and mostly it is — but
153
- the two are not the same thing, and the difference shows the moment the children
154
- are not all there yet:
141
+ not while the children are not all there yet:
155
142
 
156
143
  - a group **told** a value holds it whole, before any child has registered to
157
- show it. A list whose items are still loading, a popup built at open, a row
158
- scrolled out of a virtualized list — none of them make the value smaller;
144
+ show it: items still loading, a popup built at open, a row scrolled out of a
145
+ virtualized list do not make the value smaller;
159
146
  - **a child mounting or unmounting is not somebody answering.** While children
160
- are arriving, their aggregate is a partial reading; a group takes it for its
161
- own value only once it has derived that value itself (nobody handed it one),
162
- or once a child really acts;
163
- - a child arriving **after** the value did is placed from what the group holds,
164
- so it shows its part of it. A child arriving with something of its own to show
165
- is answering, and keeps it. A child bound to a `signal` is placed like any
166
- other — bound is not frozen — and the placement writes the signal, so an
167
- `<Input type="hidden" signal>` carrying a piece of the answer shows what the
168
- group holds. Only a child controlled by a `value`/`checked` prop is left
169
- alone: its owner decides.
170
-
171
- `distributeChildUIState` is asked child by child, which is fine while each child
172
- can be placed on its own. When it cannot — four seats where who sits down
173
- decides who moves, a row whose first answer changes what the others may show —
174
- `distributeChildStates` is the same way down asked ONCE for all of them:
175
-
176
- ```js
177
- distributeChildStates: (groupValue, children) => new Map([[child, state], …])
178
- ```
179
-
180
- the mirror of `aggregateChildStates`, which already sees every child. A child
181
- the Map does not name is left where it is. Given both, the plural one wins.
182
-
183
- **It closes the loop, which the per-child one does not.** A group with a plural
184
- distribute holds ONE answer its children are views OF — a list saying who plays,
185
- four seats saying who sits where — rather than a value that IS what they said.
186
- So when one view speaks, the answer moves and **the other views are placed
187
- again**; the one that just acted is left where the user put it. Without that, a
188
- seat keeps showing somebody the list no longer holds, and nothing says so.
189
-
190
- This is the same rule as "a child arriving after the value did is placed from
191
- what the group holds", for a child that was there when the value moved without
192
- it. It matters most inside a picker's popup, where nothing else would bring the
193
- value back down: a picker's value IS what the control in its popup holds, so the
194
- façade never echoes it back.
195
-
196
- Five things follow, and each of them was paid for twice before being written
197
- here:
198
-
199
- - **the group's value has to be the view that carries the MOST.** Four seats say
200
- who plays AND who sits where; a list of who plays says half of that. Make the
201
- seating the value and the list is derived from it (`value.filter(Boolean)`);
202
- make the list the value and every gesture on a seat has to be guessed at. The
203
- rule to apply: between two views of one answer, the value is the one the other
204
- can be computed from;
205
- - **the Map must name EVERY child**, the derived view included. A child the Map
206
- does not name is left where it is — which reads as "the list never fills";
207
- - **name the children you WANT, never exclude the one you don't.** A group holds
208
- more than the views: a search box, a filter, whatever the popup needs. Reading
209
- the seats as "every child except the list" collects those too, and the value
210
- ends up with five slots for four seats. Finding them by name says what it
211
- means;
212
- - **a gesture that moves two children must never leave the value in an
213
- in-between.** Dragging somebody from one seat to another is one intention and
214
- two writes, and the group aggregates between them: for one instant the person
215
- is in the list and in no seat, which a value that IS the seating has no room
216
- for. Measured, that instant costs the person — the aggregate drops them and
217
- the distribute unticks them before the second write lands. Two ways out, and
218
- the first is the one to prefer:
219
-
220
- 1. **write the group's value once.** `rowSignal.value = slots` with both seats
221
- already moved: there is no in-between at all. Available when the row is
222
- where the placement lives;
223
- 2. **write the destination first**, when the seats are the truth and must stay
224
- separate signals. The person is somewhere at every observable moment (in
225
- two seats for an instant, which the rule then resolves), instead of nowhere.
226
- Source-first loses them — measured, and silently.
227
-
228
- Wrapping the two writes in a signal `batch()` does NOT help: the group
229
- aggregates on each child's change, not on the render that follows.
230
-
231
- - **and when the in-between cannot be avoided, the aggregate says so.** Two
232
- people swapping seats means one leaves before the other arrives, whichever
233
- order the writes take. What is avoidable is PUBLISHING that half-state: an
234
- aggregate that returns **what the group already holds** says "not yet, ask me
235
- again" — nothing is placed, nothing is handed to the row above, and the next
236
- assembly publishes the whole answer. Returning `undefined` says the opposite
237
- ("there is no answer"), and wipes the row.
238
-
239
- ```js
240
- // "not yet": the moving person is picked and seated nowhere, and it is not
241
- // somebody who was just ticked — so this is a gesture in flight
242
- const midMove = picked.some(
243
- (id) => !seatedNow.includes(id) && lastAnswer.includes(id),
244
- );
245
- if (midMove) {
246
- return lastAnswer; // what the group already holds
247
- }
248
- ```
147
+ arrive, their aggregate is a partial reading; the group takes it for its value
148
+ only once it derived that value itself (nobody handed it one), or once a child
149
+ really acts;
150
+ - a child arriving **after** the value did is placed from what the group holds;
151
+ one arriving with something of its own to show is answering, and keeps it. A
152
+ child bound to a `signal` is placed like any other — bound is not frozen — and
153
+ the placement writes the signal, so an `<Input type="hidden" signal>` carrying
154
+ a piece of the answer shows what the group holds. Only a child controlled by a
155
+ `value`/`checked` prop is left alone: its owner decides.
156
+
157
+ When children cannot be placed one at a time — four seats where who sits down
158
+ decides who moves — `distributeChildStates(groupValue, children) => Map` places
159
+ them all at once (see the `ControlGroup` JSDoc). It closes the loop: the group
160
+ holds ONE answer its children are views OF, so when one speaks the others are
161
+ placed again — without it a seat keeps showing somebody the list no longer
162
+ holds, above all in a picker's popup, where the façade never echoes the value
163
+ back down. What follows:
164
+
165
+ - **the group's value is the view that carries the MOST.** Four seats say who
166
+ plays AND who sits where, a list of who plays says half of that: make the
167
+ seating the value and derive the list (`value.filter(Boolean)`). Between two
168
+ views of one answer, the value is the one the other can be computed from;
169
+ - **the Map names EVERY child**, the derived view included (one it does not name
170
+ is left where it is: "the list never fills"), and **by name** — "every child
171
+ except the list" also collects the search box the popup holds;
172
+ - **a gesture that moves two children never leaves the value in an
173
+ in-between.** A drag from one seat to another is two writes, and the group
174
+ aggregates between them: for an instant the person is in no seat, and is
175
+ dropped. Write the group's value once, both seats already moved; or, when the
176
+ seats must stay separate signals, write the destination first (two seats for
177
+ an instant, never none) — source-first loses them silently. A signal `batch()`
178
+ does NOT help: the group aggregates on each child's change, not on the render;
179
+ - **an in-between that cannot be avoided** (two people swapping seats) is not
180
+ published: an aggregate returning what the group already holds says "not
181
+ yet" — nothing is placed or handed up, and the next assembly publishes the
182
+ whole answer. Returning `undefined` says "there is no answer", and wipes the
183
+ row.
249
184
 
250
185
  ```jsx
251
186
  // the seats are found by name, never as "everything that is not the list"
@@ -286,25 +221,22 @@ AND which levels, when it starts AND when it ends — hands back a sub-object:
286
221
  ```
287
222
 
288
223
  Flattening those into the sent object is the caller's business
289
- (`{ ...value.level, ...value.hours }`), and it is the only reasonable place for
290
- it: a picker with no name of its own could not be collected, and one merging its
291
- keys into the object around it would take the row's own identity away — nothing
292
- would say which row a key came from, nor which row to put an incoming value
293
- back on. A `<ControlGroup>` with no name IS that merge, and it exists for the
294
- case where there is no row: several controls in one screen, no door between
295
- them.
224
+ (`{ ...value.level, ...value.hours }`): a picker with no name of its own could
225
+ not be collected, and one merging its keys into the object around it would take
226
+ the row's identity away — nothing would say which row a key came from, nor which
227
+ row to put an incoming value back on. A `<ControlGroup>` with no name IS that
228
+ merge, for the case where there is no row: several controls in one screen, no
229
+ door between them.
296
230
 
297
231
  ## A settings sheet
298
232
 
299
233
  A popup that is not one choice but a handful of settings — four tabs, a select,
300
234
  a field, a button that answers with a place — and ONE answer, which must reach
301
- the app only when the popup closes. The list behind it must not move while it is
302
- open, and Escape must leave things exactly as they were found.
303
-
304
- Nothing new is needed for that: it is the object picker above, with the group in
305
- its popup. A picker's `action` runs on close and only on close (its `uiAction`
306
- follows every gesture, which is what the popup shows), so what is inside acts on
307
- nothing until the user is done.
235
+ the app only when the popup closes: the list behind it must not move while it is
236
+ open, and Escape must leave things exactly as they were found. It is the object
237
+ picker above, with the group in its popup: a picker's `action` runs on close and
238
+ only on close (its `uiAction` follows every gesture, which is what the popup
239
+ shows).
308
240
 
309
241
  ```jsx
310
242
  <Picker
@@ -327,70 +259,41 @@ nothing until the user is done.
327
259
  </SlideContainer>
328
260
  </ControlGroup>
329
261
  </Picker>
262
+ // `save` receives, on close and once:
263
+ // { origin: "city", city: "Antibes", radius: "30", department: "" }
330
264
  ```
331
265
 
332
- ```js
333
- // what `save` receives, on close and once
334
- { origin: "city", city: "Antibes", radius: "30", department: "" }
335
- ```
336
-
337
- **Which tab is showing is part of the answer.** The same fields mean different
338
- things depending on the tab they were filled on, so the tab is a key of the
339
- value like the rest — a sheet handing back `{ city: "Antibes" }` without saying
340
- which tab it was left on has not said what was chosen. A tab bar is a navigation
341
- (`<Nav slideContainer>` + `<Link slide>`), so nothing in it is a field: the
342
- current area reaches the value through an `<Input type="hidden">` bound to the
343
- same signal as the container. That is what a hidden field is for — a
344
- piece of the answer with no control to be read from, here because the tabs are
345
- places rather than choices. A tab bar made of radios needs none: it is a field
346
- already, under its own name.
347
-
348
- That signal is written in both directions, which is what makes the sheet reopen
349
- where it was left: a value handed DOWN to a control writes it too — on open, and
350
- when Escape puts back what the picker held — so the tabs go back to the answer
351
- rather than staying on the one that was being tried.
352
-
353
- **Every field answers, including the ones the current tab does not use.** A
354
- field on another tab stays mounted, keeps what was typed in it, and comes back
355
- in the object. Which of them count is read from `origin` by whoever receives the
356
- value — the group has no opinion about it, and does not need one. Deriving
357
- something narrower (a stored string, an id) is that reader's business too:
358
- `action` receives the object and stores whatever it wants, `value` hands the
359
- object back.
360
-
361
- A group CAN be worth a single value of its own — `aggregateChildStates` and
362
- `distributeChildUIState`, see the top of this file — and a sheet is precisely
363
- where that is the wrong reach: it throws away the state that makes the answer
364
- readable, starting with the tab.
365
-
366
- **A button inside is not a field.** The one asking for a position acts on the
367
- press, like anywhere else; what it answers goes into a control that IS a field
368
- (a read-only one showing the place it found), and the group picks it up from
369
- there.
370
-
371
- The façade stays on what is saved by reading the app's own state
372
- (`value={zone}` + a `ui` rendered from `zone`), while the picker holds the
373
- draft: the summary behind the open dialog then shows the answer, not the
374
- attempt.
375
-
376
- Seen working — the tabs, the geolocation button, the cancel — in
266
+ **Which tab is showing is part of the answer**: the same fields mean different
267
+ things depending on the tab they were filled on. A tab bar is a navigation
268
+ (`<Nav slideContainer>` + `<Link slide>`), not a field, so the current area
269
+ reaches the value through an `<Input type="hidden">` bound to the container's
270
+ signal (a tab bar made of radios is a field already). That signal is written
271
+ both ways — a value handed DOWN writes it too, on open and when Escape puts back
272
+ what the picker held — so the sheet reopens on the tab of the answer, not on the
273
+ one being tried.
274
+
275
+ **Every field answers**, including those on another tab: they stay mounted and
276
+ come back in the object, and whoever receives it reads `origin` to know which
277
+ count. Aggregating the sheet to a single value of its own
278
+ (`aggregateChildStates`) throws away what makes the answer readable, starting
279
+ with the tab. **A button inside is not a field**: the one asking for a position
280
+ acts on the press, and what it answers goes into a control that IS one (a
281
+ read-only field showing the place found).
282
+
283
+ The façade reads the app's own state (`value={zone}` + a `ui` rendered from
284
+ `zone`) while the picker holds the draft, so the summary behind the open dialog
285
+ shows the answer, not the attempt. Demo:
377
286
  `control/demos/picker/9_picker_settings_sheet_demo.html`.
378
287
 
379
288
  ## `Group` is not `ControlGroup`
380
289
 
381
- Two names, two subjects, no relation:
382
-
383
- - `<Group>` is about the **frame** — several controls reading as one object to
384
- the eye, one border per seam. See [control_group.md](./control_group.md).
385
- - `<ControlGroup>` is about the **value** — several controls reading as one
386
- object to the action.
387
-
388
- They compose: a `Group` around controls that also happen to be a
389
- `ControlGroup`'s children is fine, and either can exist without the other.
290
+ `<Group>` is about the **frame** — several controls reading as one object to the
291
+ eye ([group.md](./group.md)); `<ControlGroup>` is about the **value**. They
292
+ compose, and either can exist without the other.
390
293
 
391
294
  ## See also
392
295
 
393
296
  - [form_changed.md](./form_changed.md) — what a form sends, and what it measures
394
297
  against
395
298
  - [control_value.md](./control_value.md) — who holds a single control's value
396
- - [control_group.md](./control_group.md) — the visual `<Group>`
299
+ - [group.md](./group.md) — the visual `<Group>`