@jsenv/navi 0.29.417 → 0.29.419

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 +347 -83
  3. package/dist/dev/jsenv_navi.js.map +12 -12
  4. package/dist/jsenv_navi.js +345 -81
  5. package/dist/jsenv_navi.js.map +12 -12
  6. package/docs/AI_INSTRUCTIONS.md +177 -258
  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 +159 -254
  12. package/docs/css_architecture.md +196 -260
  13. package/docs/data_states.md +41 -44
  14. package/docs/dialog_shape.md +91 -134
  15. package/docs/drag_interactions.md +291 -454
  16. package/docs/drag_to_travel.md +152 -748
  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 +61 -78
  21. package/docs/group.md +141 -0
  22. package/docs/i18n.md +31 -76
  23. package/docs/interactions.md +170 -326
  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 -473
  35. package/docs/safe_area.md +102 -89
  36. package/docs/scroll.md +175 -317
  37. package/docs/state_binding.md +40 -84
  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
@@ -42,24 +42,20 @@ validation message on the **requester** (the submit button, not the form):
42
42
  - The message is a constraint on the control (`navi_action_error`) rather than a
43
43
  render of its own, and it **auto-resets on the next action**: hitting submit
44
44
  again clears it and re-submits, instead of the form being stuck invalid.
45
- - `actionErrorEffect` changes that: `"throw"` re-throws the error from a layout
46
- effect so the nearest `<ErrorBoundary>` takes it (a control cannot throw from
47
- an event handler and be caught — see the comment in
48
- [use_execute_action.js](../src/action/use_execute_action.js)), and anything
49
- else — `"none"` — means the control displays nothing because something else
50
- does. `<Details>` uses that: its `<ActionRenderer>` shows the error inside the
51
- open panel.
45
+ - `actionErrorEffect="throw"` re-throws the error from a layout effect so the
46
+ nearest `<ErrorBoundary>` takes it — an error thrown from an event handler
47
+ reaches no boundary. `"none"` means the control displays nothing because
48
+ something else does: `<Details>` uses it, its `<ActionRenderer>` showing the
49
+ error inside the open panel.
52
50
  - `onActionError(error, event)` fires alongside whichever display was chosen —
53
51
  it is for reacting to the failure, not for showing it (showing stays claimed
54
52
  by the message or the boundary). Its siblings on every control with an
55
- `action` — `onActionStart`, `onActionEnd`, `onActionAborted` — are in
56
- [actions.md](./actions.md#the-instance-a-control-runs).
53
+ `action` are in [actions.md](./actions.md#the-instance-a-control-runs).
57
54
 
58
55
  #### A refusal that carries a link
59
56
 
60
- The callout is not limited to a sentence. When the refusal has somewhere to go —
61
- "this slot is already taken by that game" — the mapping returns an element and
62
- the callout holds it, link included:
57
+ When the refusal has somewhere to go — "this slot is already taken by that game"
58
+ — the mapping returns an element and the callout holds it, link included:
63
59
 
64
60
  ```jsx
65
61
  <Form
@@ -79,55 +75,39 @@ the callout holds it, link included:
79
75
  >
80
76
  ```
81
77
 
82
- That is the whole thing: the message arrives on the button that was pressed, it
83
- survives whatever ran before it (a `command` waits for its own action), and it
84
- clears itself on the next submit. A refusal with a destination does **not** need
85
- a render of its own in the page.
78
+ The message arrives on the button that was pressed, survives whatever ran before
79
+ it (a `command` waits for its own action), and clears itself on the next
80
+ submit: a refusal with a destination does **not** need a render of its own in
81
+ the page. Being an element, the message:
86
82
 
87
- Two consequences of the message being an element:
88
-
89
- - it is rendered as a **preact tree of its own** into the callout, not as part
90
- of the app tree, so it receives no context from the app. Anything reading
91
- global signals — a navi `<Link route>`, a store — works; anything expecting an
92
- app `Context.Provider` above it does not, and has to be given what it needs
83
+ - is rendered as a **preact tree of its own** into the callout, not as part of
84
+ the app tree, so it receives no context from the app. Anything reading global
85
+ signals — a navi `<Link route>`, a store — works; anything expecting an app
86
+ `Context.Provider` above it does not, and has to be given what it needs
93
87
  through props;
94
- - it has no one-line form, so nothing derives a `title` from it — the callout is
88
+ - has no one-line form, so nothing derives a `title` from it — the callout is
95
89
  the only place it is shown. A string or an `Error` message still writes the
96
90
  control's `title` when the control has none of its own.
97
91
 
98
- [form_error_mapping_jsx demo](../src/control/demos/form_error_mapping_jsx/demo.html)
99
- shows a mapping answering with a different component per error kind.
92
+ Reference: `src/control/demos/form_error_mapping_jsx/demo.html`, a mapping
93
+ answering with a different component per error kind.
100
94
 
101
95
  ### A screen's data failed — a route action
102
96
 
103
97
  Nobody clicked; the page simply cannot be drawn. It is the page, or a piece of
104
98
  it, that is replaced — by an ancestor, never by a prop on the page. That is what
105
- `useAsyncData`'s default means: **delegate**. The component says what it renders
106
- when it has data, and what it cannot render is somebody else's job, up the tree:
107
- `<Loading>` for the wait, `<ErrorBoundary>` for the failure, both written
108
- between a container `<Route>` and its branches — the boundary OUTSIDE the
109
- `<Loading>`, since a page suspends first and fails second. The shape of that
110
- tree, and why a wrapper brackets a subset of the branches, is in
99
+ `useAsyncData`'s default means: **delegate**, the wait to `<Loading>` and the
100
+ failure to `<ErrorBoundary>`, the boundary OUTSIDE the `<Loading>` since a page
101
+ suspends first and fails second. The shape of that tree is in
111
102
  [navigation.md](./navigation.md#loading-data).
112
103
 
113
- Inside the component when the error is part of what the screen draws rather than
114
- a screen of its own:
115
-
116
- ```jsx
117
- const [game, loading, error, dismissError] = useAsyncData(gameAction, {
118
- loading: true,
119
- error: true,
120
- });
121
- ```
122
-
123
- `game` and `error` arrive **together**: a refresh that failed does not unmake
124
- the answer the action already had, so the screen keeps what it has and says the
125
- failure on it, and `dismissError()` takes that message away without asking
126
- anything again. That is the whole difference between the two shapes — a boundary
127
- replaces the subtree by definition, this one writes on it — so reach for the
128
- boundary when the failure means the screen cannot be drawn at all, and for this
129
- when it means one thing about it is out of date. How the component draws the
130
- states it is handed is [data_states.md](./data_states.md).
104
+ When the error is part of what the screen draws rather than a screen of its own,
105
+ `useAsyncData(action, { loading: true, error: true })` hands it over **beside**
106
+ the answer the action already had: the screen keeps what it has, says the
107
+ failure on it, and `dismissError()` takes the message away without asking
108
+ anything again. Reach for the boundary when the failure means the screen cannot
109
+ be drawn at all, for this when it means one thing about it is out of date (see
110
+ [data_states.md](./data_states.md#an-error-is-a-message-on-the-screen-not-the-screen)).
131
111
 
132
112
  And one row rather than the whole list: `<List.Item error>` replaces the row's
133
113
  content with the message — what the row stood for did not happen, so drawing it
@@ -137,24 +117,10 @@ as if it had would be a lie.
137
117
 
138
118
  Not an error at all, and not to be modelled as one: a value the app or the
139
119
  browser refuses is a **status on the control**, carried by navi's constraint
140
- validation. Never throw to reject a value — see `src/control/rules/`.
141
-
142
- An app's own rule joins that set as a constraint — `{ name, check }`, where
143
- `check(field)` reads `field.uiState` and returns a message or `null`:
144
-
145
- ```jsx
146
- <Input constraints={[TEXT_SHAPE_CONSTRAINT]} />; // this control
147
- registerGlobalConstraint(TEXT_SHAPE_CONSTRAINT); // every control
148
- ```
149
-
150
- A constraint is an object, so it carries whatever it needs to decide — there is
151
- nothing to pass through an attribute. The attributes the shipped constraints
152
- read (`required`, `data-single-space`, `data-displayable`…) exist because those
153
- constraints are global and need a per-field switch; a constraint written for
154
- one call site does not.
155
-
156
- Where such a rule belongs — a navi constraint, a @jsenv/validity rule the
157
- server runs too, or both — is `docs/field_validation.md`.
120
+ validation. Never throw to reject a value. An app's own rule joins that set as a
121
+ constraint — `constraints` on one control, `registerGlobalConstraint` for every
122
+ control — and where such a rule belongs, a navi constraint, a @jsenv/validity
123
+ rule the server runs too, or both, is [field_validation.md](./field_validation.md).
158
124
 
159
125
  ## What a failing action does
160
126
 
@@ -176,24 +142,21 @@ awaiting another one fails with it:
176
142
  ```
177
143
 
178
144
  The error is in `errorSignal` all the same: a screen reads it there whether or
179
- not anything was waiting on the promise. `useAsyncData`, `errorSignal` and an
180
- `onError` passed to the run are still how a render asks for it.
181
-
182
- Code that lets such a rejection float gets the runtime's own unhandled-error
183
- report — window `error`, the jsenv overlay in dev — and that is the right answer
184
- for it: nothing had to guess whether someone was waiting.
185
-
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 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.
190
- They pass through [`runUnwatched()`](../src/action/run_unwatched.js), which takes
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).
145
+ not anything was waiting on the promise — `useAsyncData`, `errorSignal` and an
146
+ `onError` passed to the run are how a render asks for it. Code that lets such a
147
+ rejection float gets the runtime's own report — an `unhandledrejection`, the
148
+ jsenv overlay in dev — and that is the right answer for it: nothing had to guess
149
+ whether someone was waiting.
150
+
151
+ **Some runs have nobody to reject at**: one started from a signal effect because
152
+ its params changed, one started by the render that reads it (`{ run: true }`),
153
+ one whose failure the control that started it already draws, a routing whose
154
+ result every caller drops. navi takes their rejection itself without hiding the
155
+ error — it stays in `errorSignal` — and taking it is what obliges navi to ask
156
+ the next question: did anything display this error? (below). The prefetch on
157
+ intent is dropped without a word, since nothing on screen asked for it and the
158
+ arrival runs the action again
159
+ ([dynamic_import.md](./dynamic_import.md#ahead-of-the-render-intent)).
197
160
 
198
161
  Two things must never become a rejection, and do not. **A navigation**: the
199
162
  routing's promise is what the browser calls the navigation itself, and rejecting
@@ -202,18 +165,15 @@ of an update**: several actions run for one update, so their outcomes are
202
165
  gathered with `allSettled` — one failure is an outcome to record, not something
203
166
  that takes the other five down.
204
167
 
205
- Why the failure does no more than that: **at the instant an action fails, nothing can
206
- know whether a screen will display it.** A route action runs before its page
207
- renders — often before that page exists — so any decision made there about "will
208
- someone show this?" is a guess, and it is wrong for the most common case in the
209
- app. So no decision is made there.
168
+ The failure does no more than that because, at the instant an action fails,
169
+ nothing can know whether a screen will display it: a route action runs before
170
+ its page renders, often before that page exists.
210
171
 
211
172
  ## Claiming an error: `__handled_by__`
212
173
 
213
174
  Whatever displays the error sets `error.__handled_by__` (via
214
- `markErrorAsDisplayedBy` in
215
- [action_error_report.js](../src/action/action_error_report.js)). That mark means
216
- one thing and only one: **this error is on screen somewhere.**
175
+ `markErrorAsDisplayedBy`). That mark means one thing and only one: **this error
176
+ is on screen somewhere.**
217
177
 
218
178
  It is the same mark the jsenv supervisor reads to keep its dev overlay out of
219
179
  the way: a window `error` or `unhandledrejection` carrying it is not shown. An
@@ -237,32 +197,18 @@ unhandled error — window `error` event, jsenv overlay in dev — pointing at t
237
197
  code that produced it.
238
198
 
239
199
  **Being read is enough to call the report off**, displayed or not. Once a render
240
- has the error, everything that can happen next is already covered without this
241
- module: it is displayed (and marked), or it is thrown — and a thrown error either
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.
245
-
246
- **And a render stopped in the middle read nothing after it.** `useAsyncData`
247
- delegates a failure by throwing it out of the render — that is how the error
248
- reaches the boundary. The render ends on that line, so a second `useAsyncData`
249
- below it, even one asking for `error: true`, is never called and never claims
250
- its own error. "Nobody read it" would then be true and meaningless: the order of
251
- two hook calls would decide whether the app is told it has a bug. So the throw
252
- speaks for the failures waiting for the same answer alongside it — a url change
253
- fails its route actions together — and none of them is reported. What is on
254
- screen is the failure the render was stopped at, which is the same story. A
255
- failure arriving _later_, after the page was replaced by what displays the first
256
- one, is not covered: nothing tells it apart from an action nobody reads.
257
-
258
- One error is never reported whoever looks at it: a **`NetworkPolicyError`**.
259
- The app declared the state that produced it and the request never left, so there
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
- ([network_policy.md](./network_policy.md)).
200
+ has the error, everything that can happen next is already covered: it is
201
+ displayed (and marked), or it is thrown — and a thrown error either finds a
202
+ boundary that displays it, or reaches window on its own, since preact re-throws
203
+ what no boundary handled. A render stopped by a delegated throw also speaks for
204
+ the failures waiting on the same answer alongside it — a url change fails its
205
+ route actions together — so none of them is reported; a failure arriving later,
206
+ after the page was replaced by what displays the first one, still is.
207
+
208
+ A **`NetworkPolicyError`** is never reported, whoever looks at it: the app
209
+ declared the state that produced it and the request never left, so there is no
210
+ bug to point at (see [network_policy.md](./network_policy.md), with the one
211
+ `console.error` line `preact/debug` still writes in dev).
266
212
 
267
213
  So what reaches the report is an error **nothing looked at** — an action nobody
268
214
  reads, a prerun for a page never opened. And _when_ it is reported follows from
@@ -272,9 +218,8 @@ that:
272
218
  - but a route action fails ON the url change, before its page exists — it is the
273
219
  routing itself that brings what will display the error. So the deadline waits
274
220
  for the document to stop moving and for the frame that paints what the routing
275
- brought. Anything faster tells an app that is displaying "you are offline" that
276
- it displayed nothing (measured: the screen arrived ~12ms after a plain
277
- macrotask deadline).
221
+ brought: anything faster tells an app displaying "you are offline" that it
222
+ displayed nothing.
278
223
 
279
224
  Waiting longer costs nothing, precisely because a read is enough to call it off:
280
225
  what is still unread by then was going to stay unread. The same error reaching
@@ -289,9 +234,9 @@ easy to miss when writing another one:
289
234
  whether anything is displayed, turns a boundary into a bug swallower: a
290
235
  `TypeError` it then re-throws reaches window marked as on screen, so the
291
236
  overlay that would have pointed at it stays out of the way — a blank page AND
292
- a silent one. navi's boundary marks
293
- just before rendering its fallback, and re-throws untouched when it has no
294
- fallback — nothing to display means nothing handled.
237
+ a silent one. navi's boundary marks just before rendering its fallback, and
238
+ re-throws untouched when it has no fallback — nothing to display means nothing
239
+ handled.
295
240
  - **Reset on the document URL, not only on the rerun.** Re-running the failed
296
241
  action is one way out; going somewhere else is the common one. A boundary that
297
242
  only watches the action stays in place of every page after the failure,
@@ -300,12 +245,15 @@ easy to miss when writing another one:
300
245
  The one reason to write your own: **filtering what you take.** navi's takes
301
246
  everything its subtree throws. An app that wants its own bugs to stay visible
302
247
  displays only what is data to it — an error carrying an HTTP status, a
303
- `NetworkPolicyError` — and re-throws the rest **unmarked**, so the overlay still does
304
- its job. `markErrorAsDisplayedBy` and `errorIsDisplayed` are exported for that:
248
+ `NetworkPolicyError` — and re-throws the rest **unmarked**, so the overlay still
249
+ does its job. `markErrorAsDisplayedBy` and `errorIsDisplayed` are exported for
250
+ that:
305
251
 
306
252
  ```jsx
307
253
  const PageErrorBoundary = ({ children, fallback }) => {
308
254
  const [error, resetError] = useErrorBoundary();
255
+ // + the second rule: resetError() once the document URL (useDocumentUrl())
256
+ // is no longer the one the error was caught on
309
257
  if (!error) {
310
258
  return children;
311
259
  }
@@ -10,8 +10,7 @@ them apart is the whole subject.
10
10
  **What only a browser can answer.** A keystroke blocked before the value exists,
11
11
  a callout placed next to the field, `required` on a radio group, `data-one-of`
12
12
  reading an option list out of the document, the moment a message appears
13
- (typing? blur? submit?).
14
- This is navi's, and it stays navi's.
13
+ (typing? blur? submit?). This is navi's, and it stays navi's.
15
14
 
16
15
  **« Is this value acceptable ».** A length, a set of allowed characters, a
17
16
  value that renders nothing, a business rule. This is not a DOM question — a
@@ -26,20 +25,17 @@ a validity rule, and the constraint is only its browser-side caller.
26
25
  ## Constraints
27
26
 
28
27
  A constraint is `{ name, check(field) }` — a bare `check` function is accepted
29
- too. `check` returns `null` when the value
30
- passes, or the message to show — a string, or `{ message, target }` when the
31
- callout belongs on another element than the control itself.
28
+ too. `check` returns `null` when the value passes, or the message to show — a
29
+ string, or `{ message, target }` when the callout belongs on another element
30
+ than the control itself.
32
31
 
33
32
  Navi's own constraints are switched on by an attribute on the control —
34
- standard when the platform has one, `data-*` when it does not.
35
-
36
- **They are written as props, in camelCase.** A constraint declares the attribute
37
- it reads (`data-no-emoji`) and a control accepts the prop that stands for it
38
- (`noEmoji`), putting it on the control host under the attribute name — the same
39
- conversion `element.dataset` does. So `singleSpace` is what you write and
40
- `data-single-space` is what ends up in the DOM (and in the devtools, and in a
41
- test selector). The attribute form is accepted too, for a control written in
42
- plain HTML.
33
+ standard when the platform has one, `data-*` when it does not. **They are
34
+ written as props, in camelCase**, which land on the control host as the
35
+ attribute the constraint reads — the conversion `element.dataset` does:
36
+ `singleSpace` is what you write, `data-single-space` is what ends up in the DOM
37
+ (the devtools, a test selector). The attribute form is accepted too, for a
38
+ control written in plain HTML.
43
39
 
44
40
  | prop | attribute | refuses |
45
41
  | ------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------- |
@@ -52,9 +48,9 @@ plain HTML.
52
48
  | `noEmoji` | `data-no-emoji` | an emoji, where a name, an identifier or a title does not want one |
53
49
  | `maxLineBreaks` | `data-max-line-breaks` | a value holding more line breaks than that |
54
50
  | `oneOf` | `data-one-of` | a value outside the option list its CSS selector points at |
55
- | `sameAs` | `data-same-as` | a value differing from the field it names |
51
+ | `sameAs` | `data-same-as` | a value differing from the field its CSS selector points at |
56
52
  | `minDigit`, `minUpperLetter`, … | `data-min-digit`, … | a password missing a kind of character |
57
- | `timeAfter`, `timeMinDuration` | `data-time-after`, `data-time-min-duration` | a time span that ends before it starts, or is too short |
53
+ | `minDuration` | `data-time-after`, `data-time-min-duration` | a `TimeRangeSpin`/`TimeRangeWheel` span ending before it starts, or too short |
58
54
 
59
55
  ```jsx
60
56
  <Input required singleSpace noEmoji maxLength={80} />
@@ -75,39 +71,33 @@ the leading and trailing spaces are removed and a run of spaces becomes one:
75
71
  <Input name="title" singleSpace="autoFix" />
76
72
  ```
77
73
 
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.
74
+ - **At commit, never while typing**: when the field is left, and before an
75
+ action reads the value. Correcting on each keystroke would eat the space the
76
+ person is about to follow with a word, so an action run from typing (a
77
+ search, debounced or not) reads the value as typed.
78
+ - **Nothing to say**: the correction is validity's own rule, so no callout, and
79
+ a server re-checking with `singleSpace: true` accepts what it receives.
80
+ - **It lands everywhere the value lives** — the field, its ui state, the bound
81
+ `signal` — except in a `disabled` or `readOnly` field, whose value is the
82
+ app's and not the person's.
83
+ - **`singleSpace` is the only constraint that offers it**: a correction is worth
84
+ it only where it cannot change what the person meant, which a `maxLength`
85
+ silently cutting a text would.
91
86
 
92
87
  Most constraints take a `<name>Message` prop to replace their sentence for one
93
88
  field (`requiredMessage`, `singleSpaceMessage`, `oneOfMessage`…; `step` and the
94
89
  time span constraints have none). To change it everywhere, override the i18n
95
- key instead — see below.
90
+ key instead — see [Messages](#messages).
96
91
 
97
92
  Two props sit beside them and belong to the browser alone, because they act
98
93
  before there is a value to validate: `charGuard` blocks a keystroke that is not
99
- in its character class, `maxLengthGuard` blocks the one that would overflow (and
100
- truncates a paste). Both show what they refused in a callout rather than
101
- silently swallowing it.
102
-
103
- `charGuard` takes a character class or one of validity's preset names, so a
104
- field can refuse the keystroke with the same knowledge the value is checked
105
- against: `charGuard="tel"`, `charGuard="slug"`, `charGuard="noEmoji"`. Refusing
106
- the keystroke and refusing the value are different jobs — the pair
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.
94
+ in its character class (or one of validity's preset names: `charGuard="tel"`,
95
+ `"slug"`, `"noEmoji"`), `maxLengthGuard` blocks the one that would overflow and
96
+ truncates a paste. Both show what they refused in a callout rather than
97
+ silently swallowing it. Refusing the keystroke and refusing the value are
98
+ different jobs, and a guard brings the value check along: `maxLengthGuard` is
99
+ checked at submit as `maxLength`, `charGuard` sets an `Input`'s `pattern` from
100
+ its class unless one is given. `maxLength` alone is the submit-only half.
111
101
 
112
102
  **A guard answers for the gesture, never for what the field already holds.** A
113
103
  value can arrive already outside the class or already too long — a
@@ -119,21 +109,12 @@ is already there is the constraint's business, and it says so at submit.
119
109
 
120
110
  ## Reading the validity without submitting
121
111
 
122
- `useConstraintValidityState(ref)` gives the control's validity as it stands,
123
- re-read whenever it changes:
124
-
125
- ```js
126
- const state = useConstraintValidityState(inputRef);
127
- state.valid; // false
128
- state.single_space.messageString; // the sentence
129
- state.reported; // "max_length" — the one the callout says
130
- ```
131
-
132
- Several constraints fail at once and only one sentence is shown: the one with
133
- the highest priority — an `error` status first, then `required`, then the
134
- platform's own constraints, then navi's and the app's, ties going to the first
135
- registered. `reported` names it, so a summary drawn beside the field says the
136
- same thing as the callout rather than picking a second one.
112
+ `useConstraintValidityState(ref)` gives the control's validity as it stands
113
+ (see its JSDoc). When several constraints fail at once, only one sentence is
114
+ shown: the one with the highest priority — an `error` status first, then
115
+ `required`, then the platform's own constraints, then navi's and the app's, ties
116
+ going to the first registered. `reported` names it, so a summary drawn beside
117
+ the field says the same thing as the callout rather than picking a second one.
137
118
 
138
119
  `src/control/demos/validation/text_rules_demo.html` is that, one rule per row: a
139
120
  value that breaks it, its message read live, and a submit to see the callout.
@@ -142,46 +123,25 @@ value that breaks it, its message read live, and a submit to see the callout.
142
123
 
143
124
  Every sentence navi says is a key in `naviI18n`, and the validation ones are
144
125
  `constraint.*`. An app changes one for its whole app by registering over it:
145
-
146
- ```js
147
- import { naviI18n } from "@jsenv/navi";
148
-
149
- naviI18n.add("constraint.single_space.consecutive", {
150
- fr: "Pas deux espaces d'affilée.",
151
- });
152
- ```
126
+ `naviI18n.add("constraint.single_space.consecutive", { fr: "…" })`.
153
127
 
154
128
  The keys of everything validity owns are validity's own key prefixed with
155
129
  `constraint.` — `single_space.start` is `constraint.single_space.start`,
156
130
  `char_class.slug` is `constraint.char_class.slug`. That is deliberate: the
157
131
  sentence a server returns and the sentence the field shows are looked up under
158
132
  one name, so making them agree is registering one key, not maintaining a
159
- translation table. See `src/control/rules/validity_bridge.js`, and `i18n.md` for
160
- how the registry itself works.
133
+ translation table. See [i18n.md](./i18n.md) for how the registry itself works.
161
134
 
162
135
  ## An app's own rules
163
136
 
164
137
  An app puts its rules in the package its server reads too, and then decides, per
165
- rule, who runs it.
138
+ rule, who runs it — see validity's
139
+ [Rules of your own](../../../tooling/validity/README.md#rules-of-your-own) and
140
+ [Front and back](../../../tooling/validity/README.md#front-and-back-sharing-what-is-worth-sharing).
141
+ The navi side:
166
142
 
167
- **Both sides.** The rule is a validity rule; navi wears it as a constraint.
168
-
169
- ```js
170
- // shared/src/text_rules.js — read by the server and by the front
171
- export const MAX_WORDS_RULE = {
172
- name: "maxWords",
173
- applyOn: (maxWords, value) => {
174
- if (maxWords === undefined || typeof value !== "string") {
175
- return null;
176
- }
177
- const count = value.trim().split(/\s+/).length;
178
- if (count <= maxWords) {
179
- return null;
180
- }
181
- return { key: "max_words", params: { max: maxWords, count } };
182
- },
183
- };
184
- ```
143
+ **Both sides.** The rule is a validity rule; navi wears it as a constraint,
144
+ built once:
185
145
 
186
146
  ```js
187
147
  // front — once, at module level: a constraint rebuilt on every render is a new
@@ -195,9 +155,9 @@ const MAX_WORDS_CONSTRAINT = constraintFromValidityRule(MAX_WORDS_RULE, {
195
155
  <Textarea constraints={[MAX_WORDS_CONSTRAINT]} />;
196
156
  ```
197
157
 
198
- The rule's key is looked up in `naviI18n` under `constraint.max_words`, so
199
- register it there — or pass `formatMessage` beside the parameters to say it
200
- through the app's own i18n instead.
158
+ The rule's key is looked up in `naviI18n` under `constraint.<key>`
159
+ (`constraint.max_words`), so register it there — or pass `formatMessage` beside
160
+ the parameters to say it through the app's own i18n instead.
201
161
 
202
162
  **The server alone.** A rule needing the database, another user's data or a
203
163
  secret stays in `createValidity({ rules })` and never reaches the front; its