@jsenv/navi 0.29.387 → 0.29.389

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 (43) hide show
  1. package/README.md +4 -4
  2. package/dist/dev/jsenv_navi.js +556 -392
  3. package/dist/dev/jsenv_navi.js.map +81 -81
  4. package/dist/dev/jsenv_navi_side_effects.js +87 -78
  5. package/dist/dev/jsenv_navi_side_effects.js.map +4 -4
  6. package/dist/jsenv_navi.js +551 -387
  7. package/dist/jsenv_navi.js.map +81 -81
  8. package/dist/jsenv_navi_side_effects.js +87 -78
  9. package/dist/jsenv_navi_side_effects.js.map +4 -4
  10. package/docs/AI_INSTRUCTIONS.md +39 -23
  11. package/docs/actions.md +6 -4
  12. package/docs/autofocus.md +2 -1
  13. package/docs/badge_list.md +5 -3
  14. package/docs/control_group.md +4 -3
  15. package/docs/control_object.md +1 -1
  16. package/docs/control_value.md +8 -9
  17. package/docs/css_architecture.md +16 -14
  18. package/docs/dialog_shape.md +15 -10
  19. package/docs/drag_interactions.md +49 -41
  20. package/docs/drag_to_travel.md +32 -24
  21. package/docs/dynamic_import.md +7 -5
  22. package/docs/error_handling.md +31 -30
  23. package/docs/field_validation.md +35 -6
  24. package/docs/i18n.md +16 -7
  25. package/docs/interactions.md +28 -23
  26. package/docs/list_action.md +8 -5
  27. package/docs/list_refresh.md +8 -1
  28. package/docs/mobile_layout_pitfalls.md +6 -5
  29. package/docs/navigation.md +7 -5
  30. package/docs/network_policy.md +22 -7
  31. package/docs/pan_zoom.md +16 -9
  32. package/docs/popup_backdrop.md +11 -5
  33. package/docs/popup_lift.md +5 -3
  34. package/docs/popup_open.md +39 -28
  35. package/docs/resource.md +31 -20
  36. package/docs/route_transitions.md +13 -10
  37. package/docs/safe_area.md +59 -39
  38. package/docs/scroll.md +11 -9
  39. package/docs/testid.md +10 -1
  40. package/docs/typography.md +2 -3
  41. package/docs/view_transitions.md +9 -9
  42. package/docs/z_index.md +6 -5
  43. package/package.json +2 -2
@@ -184,12 +184,16 @@ report — window `error`, the jsenv overlay in dev — and that is the right an
184
184
  for it: nothing had to guess whether someone was waiting.
185
185
 
186
186
  **Some runs have nobody to reject at**, and they say so where they start: one
187
- started from a signal effect because its params changed, one whose failure the
188
- control that started it already draws, a routing whose result every caller drops.
187
+ started from a signal effect because its params changed, one started by the
188
+ render that reads it (`{ run: true }`), one whose failure the control that
189
+ started it already draws, a routing whose result every caller drops.
189
190
  They pass through [`runUnwatched()`](../src/action/run_unwatched.js), which takes
190
- the rejection without hiding the error — it stays in `errorSignal`. That is the
191
- whole list of places navi swallows one, and taking it is what obliges that place
192
- to ask the next question: did anything display this error? (below).
191
+ the rejection without hiding the error — it stays in `errorSignal`. That is
192
+ every place navi swallows one but the prefetch on intent, dropped without a
193
+ word since nothing on screen asked for it and the arrival runs the action again
194
+ ([dynamic_import.md](./dynamic_import.md#ahead-of-the-render-intent)). Taking
195
+ it is what obliges that place to ask the next question: did anything display
196
+ this error? (below).
193
197
 
194
198
  Two things must never become a rejection, and do not. **A navigation**: the
195
199
  routing's promise is what the browser calls the navigation itself, and rejecting
@@ -212,24 +216,19 @@ Whatever displays the error sets `error.__handled_by__` (via
212
216
  one thing and only one: **this error is on screen somewhere.**
213
217
 
214
218
  It is the same mark the jsenv supervisor reads to keep its dev overlay out of
215
- the way. It has to exist because `preact/debug` re-throws, in a `setTimeout`,
216
- **every error a boundary handled** — deliberately, so that React-style dev
217
- overlays keep working:
218
-
219
- ```js
220
- // when an error was handled by an ErrorBoundary we will nonetheless emit an error
221
- // event on the window object. This is to make up for react compatibility in dev mode
222
- // and thus make the Next.js dev overlay work.
223
- ```
224
-
225
- Without the mark, an app calmly displaying "you are offline" gets a crash
226
- overlay thrown over it.
219
+ the way: a window `error` or `unhandledrejection` carrying it is not shown. An
220
+ error on screen can still reach window another way — a run started by hand and
221
+ never awaited — and without the mark an app calmly displaying "you are offline"
222
+ gets a crash overlay thrown over it. navi's own report reads the same mark
223
+ (below).
227
224
 
228
225
  Everything navi ships already marks: `<ErrorBoundary>` when it renders its
229
- fallback, `<ActionRenderer>` / `<Button action>` when they render their error
230
- branch, `useAsyncData({ error: true })`
231
- when it hands the error to the component, and a run given an `onError` — asking
232
- for the error is taking it.
226
+ fallback, `<ActionRenderer>` when it renders its error branch,
227
+ `useAsyncData({ error: true })` when it hands the error to the component, and a
228
+ run given an `onError` — asking for the error is taking it. Every control with
229
+ an `action` runs it that way, so the control claims the failure whatever it
230
+ then shows: a callout, a boundary (`actionErrorEffect="throw"`), or nothing
231
+ (`"none"`, a mapping returning nothing).
233
232
 
234
233
  ## Taken by nobody: the report
235
234
 
@@ -240,10 +239,9 @@ code that produced it.
240
239
  **Being read is enough to call the report off**, displayed or not. Once a render
241
240
  has the error, everything that can happen next is already covered without this
242
241
  module: it is displayed (and marked), or it is thrown — and a thrown error either
243
- finds a boundary that displays it, or reaches window on its own, since
244
- `preact/debug` re-throws what a boundary caught and an unbounded throw aborts the
245
- render loudly. Reporting it here too would be a second voice, and the wrong one:
246
- this module cannot see which of those happened.
242
+ finds a boundary that displays it, or reaches window on its own, since preact
243
+ re-throws what no boundary handled. Reporting it here too would be a second
244
+ voice, and the wrong one: this module cannot see which of those happened.
247
245
 
248
246
  **And a render stopped in the middle read nothing after it.** `useAsyncData`
249
247
  delegates a failure by throwing it out of the render — that is how the error
@@ -259,9 +257,11 @@ one, is not covered: nothing tells it apart from an action nobody reads.
259
257
 
260
258
  One error is never reported whoever looks at it: a **`NetworkPolicyError`**.
261
259
  The app declared the state that produced it and the request never left, so there
262
- is no bug to point at — it is data a screen shows. navi also cancels the window
263
- `error` event that displaying it produces in dev, which keeps the browser
264
- console and the jsenv overlay out of it
260
+ is no bug to point at — it is data a screen shows. When one reaches window
261
+ anyway, navi cancels the event, which keeps the browser console and the jsenv
262
+ overlay out of it. A boundary displaying one still leaves a `console.error` line
263
+ in dev: `preact/debug` logs every error a boundary catches, and reads nothing
264
+ that would tell it the error is handled
265
265
  ([network_policy.md](./network_policy.md)).
266
266
 
267
267
  So what reaches the report is an error **nothing looked at** — an action nobody
@@ -287,8 +287,9 @@ easy to miss when writing another one:
287
287
 
288
288
  - **Mark only what you actually render.** Marking on catch, before knowing
289
289
  whether anything is displayed, turns a boundary into a bug swallower: a
290
- `TypeError` in a component becomes a blank page AND a silent one, since the
291
- mark muted the overlay that would have pointed at it. navi's boundary marks
290
+ `TypeError` it then re-throws reaches window marked as on screen, so the
291
+ overlay that would have pointed at it stays out of the way — a blank page AND
292
+ a silent one. navi's boundary marks
292
293
  just before rendering its fallback, and re-throws untouched when it has no
293
294
  fallback — nothing to display means nothing handled.
294
295
  - **Reset on the document URL, not only on the rerun.** Re-running the failed
@@ -25,7 +25,8 @@ a validity rule, and the constraint is only its browser-side caller.
25
25
 
26
26
  ## Constraints
27
27
 
28
- A constraint is `{ name, check(field) }`. `check` returns `null` when the value
28
+ A constraint is `{ name, check(field) }` — a bare `check` function is accepted
29
+ too. `check` returns `null` when the value
29
30
  passes, or the message to show — a string, or `{ message, target }` when the
30
31
  callout belongs on another element than the control itself.
31
32
 
@@ -44,7 +45,7 @@ plain HTML.
44
45
  | ------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------- |
45
46
  | `required`, `pattern` | same | what the platform's attributes mean |
46
47
  | `minLength`, `maxLength` | same | a string too short or too long |
47
- | `min`, `max`, `step` | same | a number, date, time or duration out of range |
48
+ | `min`, `max`, `step` | same | a number, date, time or duration out of range, or off its step |
48
49
  | `singleSpace` | `data-single-space` | a leading or trailing space, two in a row |
49
50
  | `displayable` | `data-displayable` | zalgo, a value showing nothing, blank lines in series, a joiner joining nothing |
50
51
  | `maxStackedMarks` | `data-max-stacked-marks` | (parameter of `displayable`) |
@@ -64,9 +65,34 @@ A boolean switch (`displayable`, `singleSpace`, `noEmoji`) passed as `false` is
64
65
  off, so `noEmoji={settings.strictNames}` says what it looks like it says. The
65
66
  others carry a value — a count, a selector — and are off by being absent.
66
67
 
67
- Each constraint has a `<name>Message` prop (and a `data-<name>-message`
68
- attribute) to replace its sentence for one field. To change it everywhere,
69
- override the i18n key instead — see below.
68
+ **`singleSpace="autoFix"` corrects the value instead of refusing it.** A text
69
+ field ends with a space more often than anyone means it to — a pause after a
70
+ word, a mobile keyboard after a suggestion, a paste — and a refusal then asks
71
+ the person to find and delete a character they cannot see. With `"autoFix"`,
72
+ the leading and trailing spaces are removed and a run of spaces becomes one:
73
+
74
+ ```jsx
75
+ <Input name="title" singleSpace="autoFix" />
76
+ ```
77
+
78
+ - **At commit, never while typing.** The correction runs when the field is
79
+ left, and before an action reads the value. Correcting on each keystroke would
80
+ eat the space the person is about to follow with a word, so an action run
81
+ from typing (a search, debounced or not) reads the value as typed.
82
+ - **Nothing to say.** The correction is validity's own `SINGLE_SPACE_RULE`, so
83
+ the value always passes the rule: no callout, and a server re-checking with
84
+ `singleSpace: true` accepts what it receives.
85
+ - **It lands everywhere the value lives**: the field, its ui state, the bound
86
+ `signal`. A `disabled` or `readOnly` field is left alone, since its value is
87
+ the app's and not the person's.
88
+ - **`singleSpace` is the only constraint that offers it.** A correction is worth
89
+ it only where it cannot change what the person meant; a `maxLength` silently
90
+ cutting a text would.
91
+
92
+ Most constraints take a `<name>Message` prop to replace their sentence for one
93
+ field (`requiredMessage`, `singleSpaceMessage`, `oneOfMessage`…; `step` and the
94
+ time span constraints have none). To change it everywhere, override the i18n
95
+ key instead — see below.
70
96
 
71
97
  Two props sit beside them and belong to the browser alone, because they act
72
98
  before there is a value to validate: `charGuard` blocks a keystroke that is not
@@ -78,7 +104,10 @@ silently swallowing it.
78
104
  field can refuse the keystroke with the same knowledge the value is checked
79
105
  against: `charGuard="tel"`, `charGuard="slug"`, `charGuard="noEmoji"`. Refusing
80
106
  the keystroke and refusing the value are different jobs — the pair
81
- `maxLengthGuard`/`maxLength` is the same split — and a field usually wants both.
107
+ `maxLengthGuard`/`maxLength` is the same split — and a guard brings the value
108
+ check along: `maxLengthGuard` is checked at submit as `maxLength`, `charGuard`
109
+ sets an `Input`'s `pattern` from its class unless one is given. `maxLength`
110
+ alone is the submit-only half.
82
111
 
83
112
  **A guard answers for the gesture, never for what the field already holds.** A
84
113
  value can arrive already outside the class or already too long — a
package/docs/i18n.md CHANGED
@@ -117,10 +117,17 @@ resolved at registration time — so register `"fr"` before `"fr-CA"`.
117
117
  ### What comes out
118
118
 
119
119
  `i18n(key, values, { lang })` returns the translation with placeholders
120
- replaced. When nothing matches it returns the key itself, so a missing
121
- translation degrades to readable text rather than blank space or a crash. Pass
122
- `fallbackLang` at creation to fall through to another language first, per key —
123
- a half-translated language stays usable.
120
+ replaced. The language is resolved per key: the preferred languages in order
121
+ (`lang`, or the whole `languagesSignal` list by default), then the
122
+ `fallbackLang` given at creation, and the first one translating the key wins. A
123
+ half-translated language therefore shows its own words where it has them and the
124
+ next language's everywhere else.
125
+
126
+ When none of them translates the key, it comes back as is, so a missing
127
+ translation degrades to readable text rather than blank space or a crash. With
128
+ opaque keys that text is the key's name, which is why an instance with opaque
129
+ keys wants a `fallbackLang` holding every key — `naviI18n` falls back to
130
+ English.
124
131
 
125
132
  `i18n.has(key)` is the way to tell "no translation" apart from "translation that
126
133
  happens to equal the key".
@@ -139,9 +146,11 @@ setPreferredLanguage("fr"); // what this user picked, from a language picker
139
146
  ```
140
147
 
141
148
  `setSupportedLanguages` matters more than it looks: without it, a browser
142
- preferring German resolves to German, and every key missing in German falls back
143
- to its key. With it, that browser lands on the app's best available language
144
- instead.
149
+ preferring German reads German wherever German exists — navi's time wording,
150
+ every date and number — and the next language it lists, or English, for the
151
+ rest: navi's buttons and messages, the app's own texts. One page, two
152
+ languages. With it, that browser lands on the app's best available language for
153
+ all of them.
145
154
 
146
155
  It decides more than the wording: `<Time>` and the format helpers
147
156
  (`formatNumber`, `formatTime`, …) read the same signal, so **a document that never calls it follows the browser,
@@ -104,20 +104,21 @@ which the carrying interactions answer with).
104
104
  Two holds on one press — a `longpress` declared on something inside an
105
105
  element that declares one too — are answered by the nearer one, the way a
106
106
  click is the innermost target's: the inner hold fires, the outer wait is given
107
- up. Delays being equal; an outer hold made shorter than the inner one fires
108
- first. A press that lands on something carried inside the element (`move`,
109
- `moving`, `reorder`…) is that thing's the same way: the outer hold does not
110
- wait on it, whether the source drags by holding or by distance — except a
111
- source that says `"refuse"` for this press, which carries nothing.
107
+ up. That is with equal delays; an outer hold made shorter than the inner one
108
+ fires first. navi says so in dev, once per element, the first time it happens:
109
+ the outer hold is a declaration that can never fire, and a picker opened by
110
+ `openOn="longpress"` around a card that already holds one is exactly where it
111
+ happens. One hold has one meaning — two things on one card want two gestures
112
+ (a hold and a click), not two holds. A press that lands on something carried
113
+ inside the element (`move`, `moving`, `reorder`…) is that thing's the same
114
+ way: the outer hold does not wait on it, whether the source drags by holding
115
+ or by distance — except a source that says `"refuse"` for this press, which
116
+ carries nothing.
112
117
 
113
118
  There is no `dblclick` in that table, on purpose: the browser fires it for a
114
119
  mouse and never for a finger, so an element declaring it would answer half the
115
120
  hands that reach it. `double_click` is the same gesture counted from the
116
- pointer — see [Twice, whichever hand it is](#twice-whichever-hand-it-is). navi says so in dev, once per element, the first time it happens: the
117
- outer hold is a declaration that can never fire, and a picker opened by
118
- `openOn="longpress"` around a card that already holds one is exactly where it
119
- happens. One hold has one meaning — two things on one card want two gestures
120
- (a hold and a click), not two holds.
121
+ pointer — see [Twice, whichever hand it is](#twice-whichever-hand-it-is).
121
122
 
122
123
  A name nothing knows how to detect produces a dev warning naming the detectors
123
124
  that exist. The carrying family and the two surface streams each have a file of
@@ -148,8 +149,9 @@ ancestor can also listen for one, and `preventDefault()` on it means "not this
148
149
  time".
149
150
 
150
151
  The lower-level event the interaction was read from is reachable too:
151
- `interactionEvent.detail.event` is the `pointerdown` a swipe or a hold was made
152
- of — which is how a menu is opened at the point the press happened.
152
+ `interactionEvent.detail.event` is the `pointerdown` a hold or a drag was made
153
+ of (a drag's release included), and the `pointerup` that ended a swipe or a tap
154
+ — which is how a menu is opened at the point the press happened.
153
155
 
154
156
  ## Reaching the control
155
157
 
@@ -157,8 +159,8 @@ Everything goes through the interaction gate of the **nearest control** — itse
157
159
  an ancestor, or the one control it wraps, in that order. So a disabled, read-only
158
160
  or busy control answers a swipe the way it answers a click: it says why, where the
159
161
  interaction happened, and nothing runs. A `Box` with no control anywhere near it
160
- still answers a callback; only `"request_action"` has nothing to ask, and says so
161
- in dev.
162
+ still answers a callback; only the two requests (`"request_action"`,
163
+ `"request_ui_action"`) have nothing to ask, and say so in dev.
162
164
 
163
165
  A `Box` that lays out **several** controls — a row of badges, a toolbar — belongs
164
166
  to none of them: its interactions are its own, answered with no gate, exactly as
@@ -544,10 +546,11 @@ returns how to undo whatever it did. Listeners, attributes, anything: it is a
544
546
  plain setup and teardown, so a detector counts what it needs in its own closure
545
547
  and nothing has to hold state on its behalf.
546
548
 
547
- `claims` takes a **set** of names rather than one, because interactions sharing an
548
- input have to be arbitrated together — a swipe, a hold and a click dispute the
549
- same press, and read apart they walk over each other. `types` (third argument) is
550
- which of them were actually declared here.
549
+ `claims` is asked one name at a time, and a detector claims a **set** of them
550
+ rather than one, because interactions sharing an input have to be arbitrated
551
+ together — a swipe, a hold and a click dispute the same press, and read apart
552
+ they walk over each other. `types` (third argument) is which of them were
553
+ actually declared here.
551
554
 
552
555
  `trigger(type, originalEvent, detail)` says the interaction happened. Called with
553
556
  a single event — `trigger(event)` — the type is the detector's own, which only
@@ -577,17 +580,18 @@ container above it does not take the gesture:
577
580
  under the same finger reads it before answering: a list reordered vertically
578
581
  inside a row of slides swiped sideways leaves the sideways gesture alone, and a
579
582
  piece carried both ways inside a bottom sheet takes the press whole. Nothing to
580
- wire — see `docs/drag_to_travel.md`. A `Dialog` docked to the bottom edge goes
583
+ wire — see `docs/drag_to_travel.md`. A `Dialog` docked to an edge goes
581
584
  further and reads the press only on its header (plus anything carrying
582
585
  `data-swipe-grip`), so its body is free whatever is in it.
583
586
  - **A hold does not take the context menu.** Declaring `longpress` says what a
584
587
  held finger does; a right click comes from the other button and keeps opening
585
588
  the browser's menu. Declare `contextmenu` beside it to make the right click do
586
589
  the same thing. (A held _finger_ is the system's own context-menu gesture, and
587
- that one is refused while the wait runs.)
590
+ that one is refused for the length of the press, not just of the wait.)
588
591
  - **Where the press already means something, text is not selected.** An element
589
- declaring `longpress` or a swipe, and a drag source standing in a
590
- `data-drag-on-contact` place, keep their text unselectable: the browser answers
592
+ declaring `longpress`, a swipe or a counted tap (`double_click`,
593
+ `single_click`), and a drag source standing in a `data-drag-on-contact`
594
+ place, keep their text unselectable: the browser answers
591
595
  that same press with a selection of its own — the word under the thumb, blue,
592
596
  with handles — and nothing takes it back once the press is over. For every
593
597
  pointer, mouse included, which cannot finish a selection begun where the press
@@ -621,6 +625,7 @@ container above it does not take the gesture:
621
625
  - `src/control/interaction/interaction_press.js` — swipes, holds and the two
622
626
  taps, and what a swipe writes on the element.
623
627
  - `src/control/interaction/interaction_keyboard.js`,
624
- `interaction_native.js` — the other two detectors.
628
+ `interaction_native.js` — shortcuts, and the browser's own events. The
629
+ carrying and surface detectors are referenced from their own docs.
625
630
  - `src/control/demos/38_interactions_demo.html` — every case above, plus a
626
631
  mailbox, a board, a surface, and a custom gesture registered from the page.
@@ -29,7 +29,8 @@ refuses in the meantime, which is what you want when the rows are alternatives.
29
29
  ```
30
30
 
31
31
  A press on another row while it runs is refused and says so, in the selection's
32
- own words ("la sélection est en cours d'enregistrement") rather than in a row's.
32
+ own words ("le choix est en cours d'enregistrement", "la sélection…" in a
33
+ `multiple` list) rather than in a row's.
33
34
 
34
35
  ## On a row's control: that row waits alone
35
36
 
@@ -38,8 +39,9 @@ part of it: it holds no action, nothing about it is busy, and the other rows
38
39
  stay live. Two rows can be in flight at once.
39
40
 
40
41
  ```jsx
41
- <List selectable multiple>
42
+ <List id="the_list" selectable multiple>
42
43
  <List.Item
44
+ id={rowId}
43
45
  selectable
44
46
  value={person}
45
47
  readOnly={pending}
@@ -60,7 +62,7 @@ stay live. Two rows can be in flight at once.
60
62
 
61
63
  The command is what marks the row, and it runs **only if the action succeeded**
62
64
  — a button given both runs its action first and lets its command follow (see
63
- [actions.md](./actions.md)). So a refused invitation leaves the row unmarked
65
+ [actions.md](./actions.md#a-press-that-opens-something-and-waits-for-the-answer)). So a refused invitation leaves the row unmarked
64
66
  without anything to undo.
65
67
 
66
68
  `selectableArea="manual"` is about the row's own surface, not about the button:
@@ -149,8 +151,9 @@ nothing about a row's own action stops someone from starting one on every row it
149
151
  draws — a dozen requests in flight because the list happened to be long.
150
152
 
151
153
  So a list allows **four runs at once by default**. While that many are out,
152
- every row that is not running goes read-only and says how many it is waiting on;
153
- the next press is possible again the moment one comes back.
154
+ every control in it that would start another run goes read-only and says how
155
+ many it is waiting on; the next press is possible again the moment one comes
156
+ back.
154
157
 
155
158
  ```jsx
156
159
  <List parallelGuard={2}> // stricter
@@ -184,7 +184,8 @@ sends nothing and changes no state, so the network is silent and
184
184
  `onRequestStateChange` — which reports what a request is doing — has no request
185
185
  to report. A run that declined and a run that was never mounted look identical.
186
186
  `debugScroll` is where that difference is visible; it carries the render window
187
- and the run's asking, which are one subject.
187
+ and the run's asking, which are one subject. It logs in navi's development build
188
+ only.
188
189
 
189
190
  ```jsx
190
191
  window.askLog = [];
@@ -251,6 +252,12 @@ Defaults are `{ GET: false, GET_MANY: ["POST"], GET_RANGE: ["POST", "DELETE"] }`
251
252
  | `GET_MANY: ["POST"]` | Whether a new item belongs in this list depends on filters, pagination, sort — the backend knows, the client does not. `DELETE` is excluded because the store already removes the item from every list. |
252
253
  | `GET_RANGE: ["POST", "DELETE"]` | A slice is a range of places: a row leaving the collection shifts every place after it, which the store cannot do. Add the verb that moves an item in or out of the collection — a `PATCH` that archives, one that changes an item's tab. |
253
254
 
255
+ `rerunOn` is read key by key: a key it names replaces that default, a key it
256
+ leaves out keeps it. `rerunOn: { GET_RANGE: ["POST", "DELETE", "PATCH"] }`
257
+ changes what re-reads the slices and nothing else — `GET_MANY` still reruns after
258
+ `POST`. `false` turns a key off. A `withParams()` scope or a relation reads its
259
+ own `rerunOn` the same way, against the resource it is declared on.
260
+
254
261
  Adding `PUT`/`PATCH` to `GET_MANY` is the usual over-correction: it costs a
255
262
  request and a `loading` pass to obtain something the response already contained.
256
263
  When a `PATCH` "doesn't show", the fix is almost always server-side — return the
@@ -91,9 +91,9 @@ far from the wrapper:
91
91
  other axis to `auto`.
92
92
 
93
93
  Note: `overflow: hidden` on `<html>` would additionally create a new containing block,
94
- which could break `position: fixed` in edge cases. Any element that needs correct
95
- `position: fixed` behavior (like `<dialog>`) should be moved to `document.body` directly
96
- anyway (which `dialog.jsx` already does).
94
+ which could break `position: fixed` in edge cases. A dialog in the top layer is out of
95
+ reach of that: the top layer takes it out of every ancestor's containing block, which is
96
+ why navi's `Dialog` stays where it is written, in its opener's subtree.
97
97
 
98
98
  ### The wrapper is a net, not a fix: find what overflows
99
99
 
@@ -132,8 +132,9 @@ scrolls down to it when it opens.
132
132
  Fix: **place `<dialog>` as the first child of `<body>`**, before any scrollable content.
133
133
  `scrollIntoView` on an element already at the top of the document has no effect.
134
134
 
135
- In `dialog.jsx`, `showModal()` moves the dialog to `document.body` (prepending it)
136
- before opening for this reason.
135
+ This is advice for a `<dialog>` written by hand. navi's `Dialog` is not moved: it
136
+ renders in its opener's subtree (see
137
+ [css_architecture.md](./css_architecture.md#counting-children-what-navi-puts-in-your-tree)).
137
138
 
138
139
  ### Summary
139
140
 
@@ -713,12 +713,14 @@ too early and put back:
713
713
  hands out.
714
714
 
715
715
  **Only where the row owns the document**: nothing between the travelling box and
716
- the viewport may scroll or clip. A row inside a scroller of its own — a frame in
717
- an article, a panel beside other content — shares nothing with the document, and
716
+ the viewport may scroll. A row inside a scroller of its own — a frame in an
717
+ article, a panel beside other content — shares nothing with the document, and
718
718
  the offset there belongs to the page around it, which the reader never left; the
719
- travel leaves it alone. An `overflow: hidden` or `clip` on any ancestor is
720
- enough to put the row outside the document's scrollport, so a row that should
721
- give positions back and does not is worth looking at from that angle first.
719
+ travel leaves it alone. An `overflow: hidden` on any ancestor is enough to put
720
+ the row outside the document's scrollport — it holds an offset of its own even
721
+ without a scrollbar, whereas `overflow: clip` holds none and does not count — so
722
+ a row that should give positions back and does not is worth looking at from
723
+ that angle first.
722
724
 
723
725
  Pages that scroll inside themselves rather than scrolling the document are not
724
726
  concerned either way: each one brings its own scrollport, which goes away with
@@ -29,8 +29,11 @@ cache the app never has to write:
29
29
  - **A route action left behind is aborted, not reset.** Its value survives; only
30
30
  a run in flight is called off.
31
31
  - **Running a completed action is a no-op.** Coming back to `/games/abc` from
32
- the list sends nothing — `run()` on a `COMPLETED` action already has its data.
33
- Only `rerun()` goes back to the network (see [actions.md](./actions.md)).
32
+ the list sends nothing while the instance that read it is alive — `run()` on a
33
+ `COMPLETED` action already has its data. Only `rerun()` goes back to the
34
+ network (see [actions.md](./actions.md)). An instance nothing references any
35
+ more is asked again, with the store row drawn meanwhile; under the policy
36
+ that ask is answered from the store (below).
34
37
  - **A failed rerun keeps the previous value.** Only `errorSignal` and the
35
38
  running state move; what was on screen stays on screen.
36
39
 
@@ -97,10 +100,18 @@ was asked:
97
100
  the reason.
98
101
 
99
102
  It is an error only in the way it travels. Nothing treats it as a bug: navi
100
- never reports it as unhandled, and the window `error` event a boundary's
101
- display produces in dev is cancelled, so neither the browser console nor the
102
- jsenv overlay says anything. The person reads the screen; nobody else has to
103
- hear about it.
103
+ never reports it as unhandled, and when one reaches `window` anyway — a run
104
+ called by hand whose failure nobody catches, a render throw no boundary took
105
+ — navi cancels the event, so neither the browser console nor the jsenv
106
+ overlay says anything. The person reads the screen; nobody else has to hear
107
+ about it.
108
+
109
+ One line is left, in dev only. A boundary displaying it — `<ErrorBoundary>`,
110
+ a control with `actionErrorEffect="throw"` — makes `preact/debug` log it with
111
+ `console.error`, as it logs every error a boundary catches, 404s and failed
112
+ fetches included. `preact/debug` reads no option or flag that would say the
113
+ error is handled, so that line stays. The jsenv overlay stays quiet, and a
114
+ build does not load `preact/debug`.
104
115
 
105
116
  - **A write is refused before the press.** A control bound to a `POST`, `PUT`,
106
117
  `PATCH` or `DELETE` action — or any control inside a `<Form>` bound to one —
@@ -109,7 +120,11 @@ was asked:
109
120
 
110
121
  Which actions the policy sees: those declaring a verb (`meta.verb`) — every
111
122
  action a `resource()` makes. A plain `createAction` may not touch the network
112
- at all, so it is left alone; give it `meta: { verb: "GET" }` to opt in.
123
+ at all, so it is left alone. `createAction(callback, { meta: { verb } })` opts
124
+ it into what the action layer holds — a control bound to a write verb is
125
+ read-only, a completed `"GET"` is not rerun — but its callback is still called
126
+ when it runs: answering from the store and settling with a `NetworkPolicyError`
127
+ are done by the callbacks a `resource()` wraps.
113
128
 
114
129
  ## Holding writes while reads go out
115
130
 
package/docs/pan_zoom.md CHANGED
@@ -47,7 +47,9 @@ the release leaves behind. A pointer pans only once it has travelled a few px
47
47
  `pan` still gets its hold, and a `double_click` still gets its two presses.
48
48
 
49
49
  Declared alone, `zoom` takes two fingers and the wheel and leaves one pointer to
50
- whatever else reads it; `pan` alone leaves the wheel to the page.
50
+ whatever else reads it: nothing captures it, waits on it or swallows its click
51
+ until a second finger lands, so a `click` or a `longpress` declared beside `zoom`
52
+ answers it as it would anywhere. `pan` alone leaves the wheel to the page.
51
53
 
52
54
  ### A surface that scrolls past: `data-pan-after-hold`
53
55
 
@@ -73,8 +75,12 @@ Opt-in, and for the same reason its opposite is (see
73
75
  [`data-drag-on-contact`](./drag_interactions.md#a-finger-that-does-not-have-to-wait-data-drag-on-contact)):
74
76
  navi cannot see whether anything behind the surface scrolls. You know;
75
77
  say so. It spends the hold, though: a `longpress` declared beside a `pan` that
76
- waits asks one finger to answer two waits of the same length, and only one of
77
- them is answered.
78
+ waits asks one finger to answer two waits, and the surface's — the shorter one
79
+ — is the one answered.
80
+
81
+ On a surface that only zooms there is no pan to wait for: one finger is the
82
+ page's to scroll, two are the surface's, and `"kept"` (below) has nothing to
83
+ keep.
78
84
 
79
85
  The wait is then asked before every pan, which is right while each finger
80
86
  landing there is genuinely ambiguous. Once the user has settled into the plan it
@@ -152,11 +158,11 @@ place `data-pan-after-hold` is.
152
158
 
153
159
  ### When the surface has the hand: `grab`, `release`, `[data-grabbed]`
154
160
 
155
- A surface that is asked for — by a hold, or by the first few pixels of travel —
156
- has an instant where it becomes the hand's, and nothing on screen says it. The
157
- hand then moves too early and scrolls the page instead, or waits long past the
158
- moment out of doubt. So the surface says it, the same two words a carried element
159
- says (`grab`, `release`) and the same attribute:
161
+ A surface that is asked for — by a hold, by the first few pixels of travel, or
162
+ by a second finger — has an instant where it becomes the hand's, and nothing on
163
+ screen says it. The hand then moves too early and scrolls the page instead, or
164
+ waits long past the moment out of doubt. So the surface says it, the same two
165
+ words a carried element says (`grab`, `release`) and the same attribute:
160
166
 
161
167
  ```jsx
162
168
  <Box
@@ -180,7 +186,8 @@ says (`grab`, `release`) and the same attribute:
180
186
  }
181
187
  ```
182
188
 
183
- `[data-grabbed]` is on the element for as long as a hand is on the surface, so
189
+ `[data-grabbed]` is on the element for as long as a hand is on the surface (on
190
+ one that only zooms, from the second finger landing to the last one lifting), so
184
191
  a contour, a veil or a raised shadow needs no listener — which is what this is
185
192
  usually for. (`[data-hand-kept]` is the other one, and it outlasts the finger:
186
193
  see [`data-pan-after-hold`](#a-surface-that-scrolls-past-data-pan-after-hold).) `grab` and `release` are for the rest: a vibration, a state kept
@@ -62,9 +62,10 @@ dismissing click must not also press what is under it — without dimming the
62
62
  page for it. `backdrop={false}` is for a popup that must not absorb at all.
63
63
 
64
64
  **It is what decides whether a popup is modal.** `Popover` is never modal,
65
- either layer. A `Dialog` is modal exactly when it has a wall: with one, a
66
- top-layer dialog is `showModal()`'d and the browser makes everything behind
67
- genuinely inert; without one it goes to the same top layer through the Popover
65
+ either layer, and neither is a `layer="local"` dialog. A top-layer `Dialog` is
66
+ modal exactly when it has a wall: with one it is `showModal()`'d and the
67
+ browser makes everything behind genuinely inert; without one it goes to the
68
+ same top layer through the Popover
68
69
  API (`popover="manual"`), placed against the screen just the same, over a page
69
70
  that stays live. `Popup` forwards `backdrop` in both modes, so which of popover
70
71
  or dialog the small-screen resolution picks says nothing about whether one
@@ -179,7 +180,12 @@ panel that closes on that press then reopens on the click that follows is a
179
180
  panel meant to stay open, blinking.
180
181
 
181
182
  ```jsx
182
- <SidePanel id="error_panel" signal={openCardIdSignal} closeByPressOutside>
183
+ <SidePanel
184
+ id="error_panel"
185
+ signal={openCardIdSignal}
186
+ backdrop={false}
187
+ closeByPressOutside
188
+ >
183
189
  …
184
190
  </SidePanel>
185
191
 
@@ -198,7 +204,7 @@ adds a whole thing to the popup's ground — the card, its count, its buttons
198
204
  and nothing in it is a dismissal.
199
205
 
200
206
  It reads a press the page hears, so it belongs with a popup that has no wall
201
- (`backdrop={false}`, or a popover): behind a wall, no card is pressable.
207
+ (`backdrop={false}`): behind a wall, no card is pressable.
202
208
 
203
209
  ### `pointer-events: none` and `inert` are not this
204
210
 
@@ -132,8 +132,9 @@ corners itself, or on the card inside it, reads the same.
132
132
 
133
133
  ## Same width, or a wider box
134
134
 
135
- `dialogSizeFromAnchor` makes the trigger's box a floor for the dialog;
136
- `dialogMaxWidth="var(--anchor-width)"` makes it the ceiling too. Together they
135
+ On a `Picker`, `dialogSizeFromAnchor` makes the trigger's box a floor for the
136
+ dialog; `dialogMaxWidth="var(--anchor-width)"` makes it the ceiling too (on a
137
+ `Dialog` the same props are `sizeFromAnchor` and `maxWidth`). Together they
137
138
  say "the same card, brought forward": the box travels and may grow in height,
138
139
  never in width. Left out, `dialogExpandX` lets the sheet take the room it has,
139
140
  and the width change is animated — the card keeps its scale inside a box
@@ -334,7 +335,8 @@ into the picture of the state it closes into.
334
335
 
335
336
  ## What it costs, and where the time goes
336
337
 
337
- Measured at CPU ×6 on the demo bench (`12_picker_card_demo.html#lift-bench`),
338
+ Measured at CPU ×6 on the demo bench
339
+ (`src/control/demos/picker/12_picker_card_demo.html#lift-bench`),
338
340
  a card of a real edit sheet's weight (≈215 elements, 13 fields, 62 picker
339
341
  nodes):
340
342