@jsenv/navi 0.29.483 → 0.29.484
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.
- package/README.md +4 -4
- package/dist/dev/jsenv_navi.js +3431 -1971
- package/dist/dev/jsenv_navi.js.map +64 -60
- package/dist/jsenv_navi.js +3286 -1907
- package/dist/jsenv_navi.js.map +64 -60
- package/docs/AI_INSTRUCTIONS.md +3 -2
- package/docs/actions.md +45 -0
- package/docs/control_value.md +73 -17
- package/docs/create_and_edit.md +26 -1
- package/docs/css_architecture.md +82 -2
- package/docs/data_states.md +3 -1
- package/docs/drag_interactions.md +30 -6
- package/docs/error_handling.md +19 -0
- package/docs/i18n.md +80 -1
- package/docs/interactions.md +50 -7
- package/docs/navigation.md +18 -0
- package/docs/network_policy.md +5 -2
- package/docs/pan_zoom.md +3 -0
- package/docs/popup_open.md +34 -2
- package/docs/resource.md +45 -5
- package/docs/route_transitions.md +20 -0
- package/docs/safe_area.md +52 -1
- package/docs/scroll.md +59 -5
- package/docs/state_binding.md +9 -0
- package/docs/testid.md +19 -1
- package/docs/typography.md +17 -0
- package/docs/view_transitions.md +13 -0
- package/package.json +3 -3
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -208,8 +208,9 @@ start when unsure which export solves a problem.
|
|
|
208
208
|
line height for everything; `BadgeList`. Before an overflowing label, and
|
|
209
209
|
before touching a `line-height`.
|
|
210
210
|
- `i18n.md` — `interpolateText`/`<Interpolate>` for one sentence, `createI18n`
|
|
211
|
-
for the app's texts, `naviI18n` for navi's own
|
|
212
|
-
sentence
|
|
211
|
+
for the app's texts, `naviI18n` for navi's own, and which helper writes a
|
|
212
|
+
number, a day or a time. Before writing a user-visible sentence, a number, a
|
|
213
|
+
date or a time.
|
|
213
214
|
- `testid.md` — role and accessible name first; where `data-testid` lands; what
|
|
214
215
|
not to target. Before a selector in a test.
|
|
215
216
|
|
package/docs/actions.md
CHANGED
|
@@ -84,6 +84,10 @@ const userAction = getUser.bindParams({ id: userIdSignal });
|
|
|
84
84
|
// and starts it, with useAsyncData(userAction, { run: true })
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
The params a signal or a getter hands over may be a fresh object each time:
|
|
88
|
+
params equal to the ones held are the same question, so the binding keeps the
|
|
89
|
+
object it has — nothing is asked again, and its readers do not render again.
|
|
90
|
+
|
|
87
91
|
`{ debounce }` puts a delay between the signal and the instance, for params that
|
|
88
92
|
move faster than a request should — a search box, a wheel, a slider:
|
|
89
93
|
|
|
@@ -104,6 +108,15 @@ params by hand. It follows where the signal **settles** (`A → B → A` sends
|
|
|
104
108
|
nothing for `B`). `actionRunEffect`, which takes the action itself and a params
|
|
105
109
|
getter, is for a request that must go out whether or not something draws it.
|
|
106
110
|
|
|
111
|
+
A request that follows typing — the suggestions under an address field — is
|
|
112
|
+
this binding, never the field's `action`: a field whose action runs is busy,
|
|
113
|
+
and a busy field is read-only, so the person could not go on typing.
|
|
114
|
+
`actionDebounce` delays an action that saves what was typed. Such a request,
|
|
115
|
+
written with a plain `createAction`, has no verb for the network policy to see
|
|
116
|
+
([network_policy.md](./network_policy.md)): offline, its params say there is
|
|
117
|
+
nothing to ask (`undefined`, [below](#nothing-to-ask-and-coming-back)).
|
|
118
|
+
`src/control/demos/integration/0_address_autocomplete_demo.html` is this shape.
|
|
119
|
+
|
|
107
120
|
### The answer is kept on the instance that ran
|
|
108
121
|
|
|
109
122
|
An action's state — `dataSignal`, `errorSignal`, `runningStateSignal` — belongs
|
|
@@ -479,6 +492,38 @@ stays on screen while it does, is in [list_refresh.md](./list_refresh.md); a
|
|
|
479
492
|
scope with reruns of its own, and a rerun after another resource writes, are
|
|
480
493
|
[`withParams()`](./resource.md#withparams-a-scope-with-reruns-of-its-own).
|
|
481
494
|
|
|
495
|
+
A read that is no row of a resource — a count, a summary — follows writes the
|
|
496
|
+
same way, with `dependencies` on the action:
|
|
497
|
+
|
|
498
|
+
```js
|
|
499
|
+
const SUMMARY = createAction(fetchSummary, {
|
|
500
|
+
dependencies: [ORDER, { resource: CART, on: ["POST"] }],
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
After a listed write, its instances that answered, or are still asking, ask
|
|
505
|
+
again; `on` is read as a resource's is
|
|
506
|
+
([resource.md](./resource.md#dependencies-rerun-after-another-resource-writes)).
|
|
507
|
+
|
|
508
|
+
What the server moves on its own — an email that may bounce, an account being
|
|
509
|
+
linked in another app — no write announces: its reader asks again on a timer.
|
|
510
|
+
|
|
511
|
+
```js
|
|
512
|
+
const [delivery] = useAsyncData(DELIVERY.GET.bindParams({ id }), {
|
|
513
|
+
run: true,
|
|
514
|
+
poll: {
|
|
515
|
+
every: 3000,
|
|
516
|
+
until: (delivery) => delivery.status !== "pending",
|
|
517
|
+
for: 60_000,
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
It ticks only while that reader is mounted and the document visible (once at
|
|
523
|
+
once when it shows again), never while a network policy answers reads from the
|
|
524
|
+
store, and each tick revalidates: the answer stays drawn and `loading` stays
|
|
525
|
+
false. `until`, `for` and a failure end it.
|
|
526
|
+
|
|
482
527
|
`rerunActions(actionSet)` reruns several at once;
|
|
483
528
|
`updateActions({ prerunSet, runSet, rerunSet, resetSet })` settles the four
|
|
484
529
|
kinds of request in one pass — a route change goes through it.
|
package/docs/control_value.md
CHANGED
|
@@ -18,6 +18,7 @@ somewhere else in the app.
|
|
|
18
18
|
- [Which controls take a `signal`](#which-controls-take-a-signal)
|
|
19
19
|
- [The PROP is what controls, not its value](#the-prop-is-what-controls-not-its-value)
|
|
20
20
|
- [A yes/no shown as two rows](#a-yesno-shown-as-two-rows)
|
|
21
|
+
- [A choice that can be given back](#a-choice-that-can-be-given-back)
|
|
21
22
|
- [Clearing, resetting, and what is shown meanwhile](#clearing-resetting-and-what-is-shown-meanwhile)
|
|
22
23
|
- [`value` and `signal` exclude each other](#value-and-signal-exclude-each-other)
|
|
23
24
|
- [A `stateSignal` brings more than a value](#a-statesignal-brings-more-than-a-value)
|
|
@@ -25,12 +26,12 @@ somewhere else in the app.
|
|
|
25
26
|
|
|
26
27
|
## The three answers
|
|
27
28
|
|
|
28
|
-
| what you pass | who holds the value | when the user acts
|
|
29
|
-
| ---------------------- | ------------------- |
|
|
30
|
-
| nothing | the control | it keeps it; `uiAction` tells you
|
|
31
|
-
| `defaultValue` | the control | same — the default is where it starts, and a new one is taken while nothing is edited
|
|
32
|
-
| `signal` | the signal | the control writes it back, both ways
|
|
33
|
-
| `value` (or `checked`) | you |
|
|
29
|
+
| what you pass | who holds the value | when the user acts |
|
|
30
|
+
| ---------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
|
|
31
|
+
| nothing | the control | it keeps it; `uiAction` tells you |
|
|
32
|
+
| `defaultValue` | the control | same — the default is where it starts, and a new one is taken while nothing is edited |
|
|
33
|
+
| `signal` | the signal | the control writes it back, both ways |
|
|
34
|
+
| `value` (or `checked`) | you | it holds what you hand down: the press shows its change, a failed `action` puts yours back |
|
|
34
35
|
|
|
35
36
|
A control given `value` and nothing to listen to it (`uiAction`, `action`, a
|
|
36
37
|
`signal`, a `command`, a form or group collecting it) is read-only, and says so
|
|
@@ -57,6 +58,12 @@ const minutesSignal = useSignal(0);
|
|
|
57
58
|
minutesSignal.value = 30;
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
A control moved by a gesture writes each value the gesture passes through: a
|
|
62
|
+
wheel dragged, flung or turned by the mouse wheel writes every row it crosses,
|
|
63
|
+
and its `action` runs once, on the row it settles on. Whatever reads the signal
|
|
64
|
+
re-renders at that rate, so read it in the smallest component that shows it, or
|
|
65
|
+
through a `computed` of what it needs.
|
|
66
|
+
|
|
60
67
|
"Every change" includes the ones the control did not decide: a group placing
|
|
61
68
|
its children, a picker filling its popup at open, and the same picker putting
|
|
62
69
|
back what it held when Escape cancels. So the signal always says where the
|
|
@@ -242,10 +249,22 @@ or taken back (the action failed, the popup was cancelled).
|
|
|
242
249
|
|
|
243
250
|
This holds for a control, for a group given a `defaultValue` (a selectable list,
|
|
244
251
|
a `ControlGroup`), and for everything inside a picker's popup, which stays
|
|
245
|
-
mounted while closed.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
252
|
+
mounted while closed.
|
|
253
|
+
|
|
254
|
+
Where the record goes depends on who sends it. A control that saves itself —
|
|
255
|
+
its own `action`, nothing around it to press — takes it as `defaultValue`
|
|
256
|
+
(`defaultChecked`). A picker whose `action` sends what its popup holds takes it
|
|
257
|
+
as its own `value`, the controls in the popup carrying only their `name`: a
|
|
258
|
+
default is a suggestion the picker does not hold, and closing on one untouched
|
|
259
|
+
confirms it ([form_changed.md](./form_changed.md#what-counts-as-already-held)),
|
|
260
|
+
so a popup opened and closed with defaults in it sends the record back. A group
|
|
261
|
+
holding nothing of its own counts as holding an answer as soon as one of its
|
|
262
|
+
controls does. A form editing a record has the same trap, and its own way out
|
|
263
|
+
([create_and_edit.md](./create_and_edit.md#the-edit-screen-opens-before-its-values)).
|
|
264
|
+
|
|
265
|
+
Either way, no `key` to remount the popup when the record changes: a key also
|
|
266
|
+
destroys whatever is being typed when the answer to ANOTHER field arrives. A
|
|
267
|
+
picker's cancel follows the same line: it undoes the person's edits
|
|
249
268
|
([popup_open.md](./popup_open.md#what-cancel-actually-undoes)), and what the
|
|
250
269
|
outside moved during the opening stays moved.
|
|
251
270
|
|
|
@@ -296,13 +315,13 @@ no field needs a url of its own — the fields then take nothing but their `name
|
|
|
296
315
|
What a control is worth when it holds nothing has the type of what it holds when
|
|
297
316
|
it holds something. A list of days nobody picked is `[]`, not `""`:
|
|
298
317
|
|
|
299
|
-
| control
|
|
300
|
-
|
|
|
301
|
-
| `Picker type="array"`, `List selectable multiple` | `[]` |
|
|
302
|
-
| `Picker type="object"`
|
|
303
|
-
| text/number/date `Input`, `Select`
|
|
304
|
-
| `Input type="checkbox" boolean`
|
|
305
|
-
| radio, any other checkbox
|
|
318
|
+
| control | empty is |
|
|
319
|
+
| ------------------------------------------------------------------ | -------- |
|
|
320
|
+
| `Picker type="array"`, `List selectable multiple`, `CheckboxGroup` | `[]` |
|
|
321
|
+
| `Picker type="object"` | `{}` |
|
|
322
|
+
| text/number/date `Input`, `Select` | `""` |
|
|
323
|
+
| `Input type="checkbox" boolean` | `false` |
|
|
324
|
+
| radio, `List selectable`, any other checkbox | absent |
|
|
306
325
|
|
|
307
326
|
This is what a clear (`--navi-clear`, a row's cross) leaves behind and what the
|
|
308
327
|
object around it carries, so `value.days || []` is not needed.
|
|
@@ -333,6 +352,24 @@ Choose by what the box asks:
|
|
|
333
352
|
value) takes its own `value`, or none for `"on"`. Unchecked carries nothing,
|
|
334
353
|
so the group gathers only the checked ones into an array.
|
|
335
354
|
|
|
355
|
+
A button that stays pressed — a mode ("Edit" / "Done"), show/hide, a setting —
|
|
356
|
+
is a checkbox too, drawn as a button: `<Input type="checkbox" variant="button"
|
|
357
|
+
boolean signal={editingSignal}>`, its children its words and its name. A
|
|
358
|
+
`<Button aria-pressed>` whose press writes the signal back is the binding done
|
|
359
|
+
by hand: the state it shows is the app's to keep in step, where the checkbox
|
|
360
|
+
announces it as checked and follows the signal whoever writes it.
|
|
361
|
+
|
|
362
|
+
```jsx
|
|
363
|
+
// ✗ a button wired to remember a state
|
|
364
|
+
<Button aria-pressed={editing} uiAction={() => (editingSignal.value = !editing)}>
|
|
365
|
+
{editing ? "Done" : "Edit"}
|
|
366
|
+
</Button>
|
|
367
|
+
// ✓
|
|
368
|
+
<Input type="checkbox" variant="button" boolean signal={editingSignal}>
|
|
369
|
+
{editingSignal.value ? "Done" : "Edit"}
|
|
370
|
+
</Input>
|
|
371
|
+
```
|
|
372
|
+
|
|
336
373
|
## Which controls take a `signal`
|
|
337
374
|
|
|
338
375
|
All of them: `Input` (every type), `Picker`, `Select`, `Wheel`, `Spin`,
|
|
@@ -389,6 +426,25 @@ What cannot be done is give a row `undefined` to mean "no value" — `undefined`
|
|
|
389
426
|
is what UNCHECKED means, here as in HTML, so such a row can never be ticked.
|
|
390
427
|
"Nothing chosen" is the absence of a row, not a row.
|
|
391
428
|
|
|
429
|
+
## A choice that can be given back
|
|
430
|
+
|
|
431
|
+
A single choice that may also be no choice at all — left, right, or neither — is
|
|
432
|
+
`<List selectable deselectable>`: the selected row, pressed again, lets go, and
|
|
433
|
+
the list holds nothing (absent). Without `deselectable` it is a radio group: a
|
|
434
|
+
choice moves to another row but never goes away.
|
|
435
|
+
|
|
436
|
+
```jsx
|
|
437
|
+
<List selectable deselectable name="side" defaultValue={me.side}>
|
|
438
|
+
<List.Item value="left">Gauche</List.Item>
|
|
439
|
+
<List.Item value="right">Droite</List.Item>
|
|
440
|
+
<List.Item value="both">Les deux</List.Item>
|
|
441
|
+
</List>
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
An answer that is the others together ("both") is a row of its own, not two
|
|
445
|
+
rows held at once; what it lights in the others hangs off `aria-selected` in
|
|
446
|
+
CSS.
|
|
447
|
+
|
|
392
448
|
## Clearing, resetting, and what is shown meanwhile
|
|
393
449
|
|
|
394
450
|
Three things that look alike and are not:
|
package/docs/create_and_edit.md
CHANGED
|
@@ -232,6 +232,24 @@ for a screen that can be blanked without the person losing their place.
|
|
|
232
232
|
Either way, cancelling is a link away: the screen is thrown away with what was
|
|
233
233
|
typed in it, and coming back re-reads the resource. There is nothing to restore.
|
|
234
234
|
|
|
235
|
+
### A screen that stays open after Save
|
|
236
|
+
|
|
237
|
+
Everything above assumes the screen is left on saving. One edited in place —
|
|
238
|
+
a settings panel, saved, then edited again — has no shape that answers all of
|
|
239
|
+
it yet, and each of the three leaves one thing out:
|
|
240
|
+
|
|
241
|
+
- `defaultValue={record.x}` follows the record and never undoes an edit, but is
|
|
242
|
+
not held: an untouched Save sends the record back.
|
|
243
|
+
- `onLoad` + `pristineKey` is held, and stops following: a record moved while
|
|
244
|
+
the screen is open stays as `onLoad` found it.
|
|
245
|
+
- `useSignalSync(record.x)` ([state_binding.md](./state_binding.md)) is held
|
|
246
|
+
and follows, but the record moving overwrites what is being typed — and it
|
|
247
|
+
compares by reference, so a record read again as a new array or object
|
|
248
|
+
counts as a move even when nothing in it changed.
|
|
249
|
+
|
|
250
|
+
What not to write: `useSignal(record.x)` with `key={record.updated_at}` on the
|
|
251
|
+
block. The key throws away what is being typed each time the record moves.
|
|
252
|
+
|
|
235
253
|
## A field that picks from a list too big to load
|
|
236
254
|
|
|
237
255
|
A place, a player, a category: the field is a picker whose popup holds **one
|
|
@@ -399,7 +417,14 @@ only its mechanisms change.
|
|
|
399
417
|
| the draft lives in the `searchParams` | it lives in the dialog, and dies with it |
|
|
400
418
|
| cancelling = leaving the screen, which is thrown away | `mount="while-opened"` (+ a `key` on the form), same effect |
|
|
401
419
|
| `RouteTravel` between the two screens | nothing: a dialog has no neighbour |
|
|
402
|
-
| `onLoad` + `pristineKey`: the resource arrives after | it is
|
|
420
|
+
| `onLoad` + `pristineKey`: the resource arrives after | it is in hand — the dialog is opened FROM the list, or re-reads it as it opens (below) |
|
|
421
|
+
|
|
422
|
+
A list's copy of a record can be minutes old, so a sheet that saves the whole
|
|
423
|
+
record reads it again as it opens, and that answer lands after the popup has
|
|
424
|
+
taken what it holds at open. A `Picker` given the record as its `value` (or its
|
|
425
|
+
signal) counts what the outside writes before the person changes anything as
|
|
426
|
+
what was there at open: a look and a close send nothing, and a cancel goes back
|
|
427
|
+
to the record as it arrived, not to the list's copy.
|
|
403
428
|
|
|
404
429
|
What does not change:
|
|
405
430
|
|
package/docs/css_architecture.md
CHANGED
|
@@ -50,6 +50,19 @@ along with the code; at module scope, it lands on every page importing the
|
|
|
50
50
|
module, and nothing can shake it out. Assign at module scope only for a module
|
|
51
51
|
that is a side effect by design — an app's tokens, say.
|
|
52
52
|
|
|
53
|
+
The same holds for an app's components, and the choice moves the cascade:
|
|
54
|
+
between unlayered sheets the last one adopted wins a tie. Assigned at module
|
|
55
|
+
scope in a module the entry imports, an app's sheet is adopted before any navi
|
|
56
|
+
component renders, and loses every tie with navi's rules, the same way every
|
|
57
|
+
session. Assigned in render, it lands after the navi sheets already adopted and
|
|
58
|
+
before the ones still to come, so a tie goes to whichever came last — which
|
|
59
|
+
depends on what the user opened first. Before moving a sheet from one to the
|
|
60
|
+
other, find its ties (a `.navi_*` class as a rule's subject, an app class on a
|
|
61
|
+
navi component setting what navi's one-class rule sets) and settle each by
|
|
62
|
+
weight, a prop or a variable rather than by order; what other modules use from
|
|
63
|
+
the sheet (a class they wear) goes in a shared sheet, installed by each of them
|
|
64
|
+
as below.
|
|
65
|
+
|
|
53
66
|
The setter is keyed by module: **two assignments in one module do not add up**,
|
|
54
67
|
the second replaces the first. A stylesheet shared between components is
|
|
55
68
|
therefore a module of its own, exposing an install function its callers run
|
|
@@ -258,6 +271,21 @@ A variant (`icon`, `discrete`, `bare`, `border`, `headless`…) describes what
|
|
|
258
271
|
the caller did not say: it only moves defaults, so a prop given next to it
|
|
259
272
|
always wins.
|
|
260
273
|
|
|
274
|
+
#### Where a prop lands
|
|
275
|
+
|
|
276
|
+
A `Button`, a range input and a `CardLayout` paint a frame inside themselves.
|
|
277
|
+
Their visual props — `background*`, `border*`, `borderRadius`, `boxShadow`,
|
|
278
|
+
`outline*`, `opacity`, `filter`, `cursor`, `overflow` — and their padding go to
|
|
279
|
+
that frame; size, position and margins stay on the control. A `style={{…}}` is
|
|
280
|
+
not sorted: all its keys paint the outer element, so a background or a shadow
|
|
281
|
+
written there lands around the frame rather than on it. Write them as props.
|
|
282
|
+
|
|
283
|
+
What navi's unlayered structure sets — a control's `position: relative` —
|
|
284
|
+
changes with the prop (`<Button absolute inset>`), or with the variable that
|
|
285
|
+
feeds it where there is one (`--nav-width`), never with a class: one class only
|
|
286
|
+
ties with navi's rule, and sheet order decides the tie (see
|
|
287
|
+
[Where CSS lives](#where-css-lives-importmetacss)).
|
|
288
|
+
|
|
261
289
|
### 2. CSS variables (for global or theme-level changes)
|
|
262
290
|
|
|
263
291
|
When the same change applies to many components (e.g. a design token update),
|
|
@@ -279,6 +307,32 @@ with `--navi-app-max-width` — never `--dialog-max-width`, a `--component-*`
|
|
|
279
307
|
token every dialog resets on itself: see
|
|
280
308
|
[safe_area.md](./safe_area.md#an-app-that-is-narrower-than-the-window).
|
|
281
309
|
|
|
310
|
+
#### The tokens a rule may read
|
|
311
|
+
|
|
312
|
+
What `:root` declares, and so what an app's own css may read with `var()`:
|
|
313
|
+
|
|
314
|
+
- spacing: `--navi-xxs` … `--navi-xxl` (`xxs`, `xs`, `s`, `m`, `l`, `xl`,
|
|
315
|
+
`xxl`), in `em` — the scale the spacing props take, and `borderRadius` too
|
|
316
|
+
(`borderRadius="s"` is `var(--navi-s)`): there is no radius scale of its own;
|
|
317
|
+
- type: `--navi-typo-xxs` … `--navi-typo-xxl`, the scale `size` takes, and
|
|
318
|
+
`--navi-line-height` ([typography.md](./typography.md#emoji));
|
|
319
|
+
- the paper: `--navi-surface-color`, `--navi-surface-text-color`,
|
|
320
|
+
`--navi-chrome-color` (see [below](#two-planes-the-paper-and-the-frame));
|
|
321
|
+
- `--navi-accent-color`; `--navi-error-color`, `--navi-info-color`,
|
|
322
|
+
`--navi-success-color` and `--navi-warning-color`, each with a `-light` twin;
|
|
323
|
+
- the stacking bands `--navi-z-index-*` ([z_index.md](./z_index.md)) and the
|
|
324
|
+
screen's `--navi-safe-area-inset-*` ([safe_area.md](./safe_area.md));
|
|
325
|
+
- the themable defaults of controls and surfaces: `--navi-control-*`,
|
|
326
|
+
`--navi-button-*` (the button grey is `--navi-button-background-color`),
|
|
327
|
+
`--navi-picker-*`, `--navi-link-*`, `--navi-popup-*`, `--navi-callout-*`,
|
|
328
|
+
`--navi-focus-outline-*`, `--navi-placeholder-*`.
|
|
329
|
+
|
|
330
|
+
The color keywords are not read this way: they go through the `color` prop
|
|
331
|
+
([typography.md](./typography.md)). A name found neither here nor in the doc of
|
|
332
|
+
what it belongs to is most likely declared nowhere, and `var()` on it draws its
|
|
333
|
+
fallback without a word — `--navi-radius-s`, `--navi-space-m`, `--navi-font-mono`
|
|
334
|
+
look plausible and are not tokens.
|
|
335
|
+
|
|
282
336
|
#### `--navi-*` vs `--component-*`: where the override has to go
|
|
283
337
|
|
|
284
338
|
The two families are **not** interchangeable, and layers have nothing to do
|
|
@@ -327,9 +381,16 @@ Scoping to an ancestor is not enough: `.my-sidebar { --link-color-pressed: blue
|
|
|
327
381
|
fails for the same reason `:root` does. The selector has to reach the link
|
|
328
382
|
element itself.
|
|
329
383
|
|
|
384
|
+
The rule is about declarations, not about the family. A `--component-*` navi
|
|
385
|
+
only reads with a fallback and declares nowhere — `--button-background`,
|
|
386
|
+
`--button-border`, `--button-padding*` — has nothing on the element to beat, so
|
|
387
|
+
an ancestor's value reaches it like any inherited custom property; one navi
|
|
388
|
+
declares — `--button-color`, `--nav-current-indicator-color` — stops at the
|
|
389
|
+
element.
|
|
390
|
+
|
|
330
391
|
When a component default deserves to be themed globally, promote it: declare a
|
|
331
|
-
`--navi-<component>-<thing>` in
|
|
332
|
-
make the component default read `var(--navi-…)`.
|
|
392
|
+
`--navi-<component>-<thing>` in `src/navi_css_vars.js` (in navi's repository)
|
|
393
|
+
and make the component default read `var(--navi-…)`.
|
|
333
394
|
|
|
334
395
|
#### Two planes: the paper and the frame
|
|
335
396
|
|
|
@@ -414,6 +475,24 @@ and it stops at the surface. Declaring the formulas on `*` to make container
|
|
|
414
475
|
ratios work is the thing not to do: it would overwrite the pinned keyword on
|
|
415
476
|
each of the card's children, and a paper could no longer say anything.
|
|
416
477
|
|
|
478
|
+
##### A dark surface inside a light page
|
|
479
|
+
|
|
480
|
+
`color` on a surface turns what is written against `currentColor` — the four
|
|
481
|
+
keyword formulas, and whatever an app writes the same way. It does not turn the
|
|
482
|
+
colors navi writes as `light-dark()` or as a system color: the paper
|
|
483
|
+
(`--navi-surface-color`) and its ink (`CanvasText`), the control border, the
|
|
484
|
+
button grey, the focus ring, a `Wheel`'s ink, glass and frame. A card saying
|
|
485
|
+
`color: white` and nothing else still holds a near-black wheel.
|
|
486
|
+
|
|
487
|
+
`color-scheme: dark` on the surface turns those: a `light-dark()` is resolved
|
|
488
|
+
where it is used, so every one below the surface takes its dark side — except a
|
|
489
|
+
token the app pinned to an absolute on `:root`. A dark card says both. A
|
|
490
|
+
component's own color token (`--wheel-color`) still tints it, set on the card
|
|
491
|
+
or any ancestor. `color-scheme` is inherited and no surface takes it back: a
|
|
492
|
+
popup opened from the card is drawn dark too. (A lowered `light-dark()` follows
|
|
493
|
+
`prefers-color-scheme` only: see
|
|
494
|
+
[Browser support](#browser-support-navis-css-and-your-target).)
|
|
495
|
+
|
|
417
496
|
### 3. Direct rule override (avoid unless necessary)
|
|
418
497
|
|
|
419
498
|
Overriding the actual CSS rules (not the variables) is intentionally hard. If
|
|
@@ -496,6 +575,7 @@ elements and the boxes before and after.
|
|
|
496
575
|
| A global design token | `--navi-*` on `:root` |
|
|
497
576
|
| The share of ink in `secondary`/`emphasis`/`discrete`/`hint` | `--navi-color-*-mix` on `:root` — never on a container |
|
|
498
577
|
| A container's own paper (a dark card) | `color` on it, plus a keyword pinned if its formula reads wrong; both stop at the next popup |
|
|
578
|
+
| A dark surface inside a light page | `color-scheme: dark` on it, beside its `color` — it turns every `light-dark()` below |
|
|
499
579
|
| How wide popups may ever get | `--navi-app-max-width` on `:root` |
|
|
500
580
|
| A structural layout rule | Expose a new CSS variable (contribute) |
|
|
501
581
|
| A rule counting children of a container holding navi controls | `:nth-child(… of S)`, `:not([navi-out-of-flow])`, or a class — a popup or a callout is a child too |
|
package/docs/data_states.md
CHANGED
|
@@ -119,7 +119,9 @@ told it shimmers until something runs that action.
|
|
|
119
119
|
|
|
120
120
|
navi's own placeholders take that flag rather than inventing it: `<Text loading>`
|
|
121
121
|
shimmers, `<Text skeleton>` is the same bar held still, and
|
|
122
|
-
`<Text skeleton loading={loading}>` is one that tells the truth either way.
|
|
122
|
+
`<Text skeleton loading={loading}>` is one that tells the truth either way. A
|
|
123
|
+
spinner beside a value is `<LoadingIndicator>` — never `<Loading>`, which is the
|
|
124
|
+
boundary a delegated wait suspends into and draws nothing of its own.
|
|
123
125
|
|
|
124
126
|
**Knowing the count beforehand beats guessing it.** A screen that already knows
|
|
125
127
|
how many rows are coming — a count carried by the parent resource, a total
|
|
@@ -60,17 +60,33 @@ carry began with, for the release as well (unlike a swipe's), except for
|
|
|
60
60
|
| `leave` | `{ id, x, y }` |
|
|
61
61
|
| `grab` | `{ pointerType, gestureInfo }` |
|
|
62
62
|
| `release` | `{ id, x, y, outcome }` |
|
|
63
|
+
| `settle` | `{ id, outcome, refused }` |
|
|
63
64
|
| `refuse` | `{ pointerType }` |
|
|
64
65
|
|
|
65
66
|
Items and places are named by their `id`. **What an outcome returns is waited
|
|
66
67
|
on**: the gesture holds what it carries until that promise settles, and a
|
|
67
68
|
rejection sends it home — a place the application would not accept must not stay
|
|
68
|
-
on screen as if it had. `moving`, `grab`, `release` and `refuse`
|
|
69
|
-
do not ask**: what they return is not waited on, and preventing
|
|
70
|
-
calls nothing off.
|
|
69
|
+
on screen as if it had. `moving`, `grab`, `release`, `settle` and `refuse`
|
|
70
|
+
**report, they do not ask**: what they return is not waited on, and preventing
|
|
71
|
+
their event calls nothing off.
|
|
72
|
+
|
|
73
|
+
**A refusal says why, on the element, once it is home**: an error callout, the
|
|
74
|
+
way a control says its action failed — a seat taken in the meantime is something
|
|
75
|
+
the person has to read, not guess from a card flying back. What the outcome
|
|
76
|
+
rejected with is read the way an `errorMapping` answer is
|
|
77
|
+
([error_handling.md](./error_handling.md#someone-just-acted--a-controls-action)):
|
|
78
|
+
an error, a string or an element is shown; `{ message, target }` is shown on
|
|
79
|
+
that target; rejecting with nothing (`return Promise.reject()`) refuses without
|
|
80
|
+
a word. An outcome answered by `"request_action"` is said by its control.
|
|
71
81
|
|
|
72
82
|
**Where the position is kept is the answer's.** Until the release navi carries
|
|
73
|
-
the element
|
|
83
|
+
the element on its `translate` property, added to the translate it already had.
|
|
84
|
+
The element's matrix is `translate`, then `rotate`, then `scale`, then
|
|
85
|
+
`transform`, so the hand's translate applies outside whatever the element is
|
|
86
|
+
drawn with — a shape turned to a bearing, a marker scaled with the zoom, by
|
|
87
|
+
property or inside `transform` — and the element stays under the hand. What it
|
|
88
|
+
is drawn with is left alone, a `var()` included, and a release kept in place
|
|
89
|
+
writes only `translate` back. An element drawn from state
|
|
74
90
|
(`left`/`top` computed from a position the application holds) is redrawn by the
|
|
75
91
|
`move` handler, and its layout owns the position from then on: navi sees it drawn
|
|
76
92
|
elsewhere and drops its translate, or it would land twice as far as the hand
|
|
@@ -90,20 +106,22 @@ whatever it is declared on, and a `data-drop-container` (a place outside is only
|
|
|
90
106
|
reached from outside).
|
|
91
107
|
|
|
92
108
|
```jsx
|
|
109
|
+
const startViewTransition = ensureDocumentStartViewTransition();
|
|
110
|
+
|
|
93
111
|
<List.Item
|
|
94
112
|
id={task.id}
|
|
95
113
|
data-view-transition-name={`task_${task.id}`}
|
|
96
114
|
interactions={{
|
|
97
115
|
reorder: (event) => {
|
|
98
116
|
const { fromId, toId, syncCloneWithDropTarget } = event.detail;
|
|
99
|
-
return
|
|
117
|
+
return startViewTransition(() => {
|
|
100
118
|
syncCloneWithDropTarget();
|
|
101
119
|
setOrder(moveBefore(order, fromId, toId));
|
|
102
120
|
}).finished;
|
|
103
121
|
},
|
|
104
122
|
toss: (event) => remove(event.detail.id),
|
|
105
123
|
}}
|
|
106
|
-
|
|
124
|
+
/>;
|
|
107
125
|
```
|
|
108
126
|
|
|
109
127
|
Every element declaring `reorder` marks itself, so the set of items IS the set of
|
|
@@ -406,6 +424,12 @@ up (a counter of what is in the hand) stays set up with nothing to take it down.
|
|
|
406
424
|
hand lets go, while still knowing whether something is on its way. Like `grab`,
|
|
407
425
|
it needs a drag to happen in, and a dev warning says so.
|
|
408
426
|
|
|
427
|
+
`settle` is when the carry is over, after the answer: the copy gone and the
|
|
428
|
+
original shown again (it is hidden, `navi-drag-clone-source`, while the copy
|
|
429
|
+
waits and flies), or the element at rest where it stays. `refused` says the
|
|
430
|
+
answer rejected. A copy taken back in hand on its way home has not settled — that
|
|
431
|
+
carry goes on, and its own end is told.
|
|
432
|
+
|
|
409
433
|
### The hand pulls and nothing follows: `refuse`
|
|
410
434
|
|
|
411
435
|
Something that can be carried is not always free to be: a court whose place on the
|
package/docs/error_handling.md
CHANGED
|
@@ -92,6 +92,25 @@ the page. Being an element, the message:
|
|
|
92
92
|
Reference: `src/control/demos/form_error_mapping_jsx/demo.html`, a mapping
|
|
93
93
|
answering with a different component per error kind.
|
|
94
94
|
|
|
95
|
+
### Someone acted, and no control asked — a gesture, an autosave
|
|
96
|
+
|
|
97
|
+
A card dropped on a place, a row swiped away, a draft saved as it is typed: the
|
|
98
|
+
person did something and the write runs from a callback, with no control's
|
|
99
|
+
action to draw its failure. The rule does not change — the answer belongs where
|
|
100
|
+
they acted — and what holds the callback says it there:
|
|
101
|
+
|
|
102
|
+
- **a drag outcome** (`land`, `reorder`, `toss`, `leave`, `move`) is said on the
|
|
103
|
+
element once it is home, in an error callout, and what the callback rejected
|
|
104
|
+
with is read like an `errorMapping` answer — rejecting with nothing refuses
|
|
105
|
+
without a word ([drag_interactions.md](./drag_interactions.md#carrying-something-move-reorder-land-toss-leave));
|
|
106
|
+
- **any other interaction's callback** that rejects is reported like every
|
|
107
|
+
error nothing displays (below): a swipe whose write failed comes back in
|
|
108
|
+
place, and the error is not lost with it. Reject with nothing for a refusal
|
|
109
|
+
that has nothing to say;
|
|
110
|
+
- **a write nobody's gesture is waiting on** (an autosave in an effect) has no
|
|
111
|
+
place to answer at: the screen reads the action that ran it
|
|
112
|
+
(`useAsyncData(action, { error: true })`) and says it where the draft is.
|
|
113
|
+
|
|
95
114
|
### A screen's data failed — a route action
|
|
96
115
|
|
|
97
116
|
Nobody clicked; the page simply cannot be drawn. It is the page, or a piece of
|
package/docs/i18n.md
CHANGED
|
@@ -69,6 +69,16 @@ import { i18n } from "@/app_i18n.js";
|
|
|
69
69
|
i18n("Deleting [name]…", { name: file.name });
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
A placeholder's value is data — a name, a number, a formatted date — or wording
|
|
73
|
+
the code picks: "tonight", a label from a table. Wording is looked up too, and
|
|
74
|
+
handed over translated; so is a unit word of the app's own, which `<Quantity>`
|
|
75
|
+
shows as given:
|
|
76
|
+
|
|
77
|
+
```jsx
|
|
78
|
+
i18n("Updated [when]", { when: i18n("tonight") });
|
|
79
|
+
<Quantity unit={[i18n("ticket"), i18n("tickets")]}>{count}</Quantity>;
|
|
80
|
+
```
|
|
81
|
+
|
|
72
82
|
### In an app, the text is the key
|
|
73
83
|
|
|
74
84
|
`keyLang: "en"` means "keys are already written in English". The key doubles as
|
|
@@ -101,6 +111,46 @@ to the key's name. That is why an instance with opaque keys wants a
|
|
|
101
111
|
happens to equal the key". Registering (`add`, `addAll`, `addLangKeys`) and how
|
|
102
112
|
the language is resolved key by key are in `createI18n`'s JSDoc.
|
|
103
113
|
|
|
114
|
+
## A sentence that depends on a number
|
|
115
|
+
|
|
116
|
+
A sentence whose words follow a count is one translation with several forms,
|
|
117
|
+
never two keys picked in code with one language's rule baked in (`count > 1 ?
|
|
118
|
+
plural : singular` is wrong in French at 0 and in English at 0, and Polish has
|
|
119
|
+
three forms where English has two). The forms go in place of the string, and
|
|
120
|
+
`count` picks one: an exact number written as a key first, then the form the
|
|
121
|
+
language gives that number (`Intl.PluralRules`), then `other`:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
const seats = {
|
|
125
|
+
0: "Full",
|
|
126
|
+
one: "[count] free seat",
|
|
127
|
+
other: "[count] free seats",
|
|
128
|
+
};
|
|
129
|
+
i18n(seats, { count: place.freeSeats });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The `other` string is the key the other languages register their forms under —
|
|
133
|
+
each language with the forms it has:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
i18n.add("[count] free seats", {
|
|
137
|
+
fr: {
|
|
138
|
+
0: "Complet",
|
|
139
|
+
one: "[count] place libre",
|
|
140
|
+
other: "[count] places libres",
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
With opaque keys, every language registers its forms under the key, and the
|
|
146
|
+
call passes `count`: `naviI18n` does this for its own messages. `<Quantity>`
|
|
147
|
+
picks its unit's form by the same rule, and `pluralCategory(count, { lang })`
|
|
148
|
+
answers it for a choice that is not a sentence.
|
|
149
|
+
|
|
150
|
+
A list of items read as one phrase is joined by `formatList` — the language's
|
|
151
|
+
"and" or "or" and its commas ("8h–10h, 12h et 18h–20h"), never a hand-written
|
|
152
|
+
`" et "`.
|
|
153
|
+
|
|
104
154
|
## Which language
|
|
105
155
|
|
|
106
156
|
Nothing needs configuring for navi and app texts to agree on a language: both
|
|
@@ -134,6 +184,33 @@ Because the language is read at lookup time, a component that reads `i18n(...)`
|
|
|
134
184
|
during render re-renders with the new wording when the language changes — as
|
|
135
185
|
long as the call happens in render, not in a value captured once outside it.
|
|
136
186
|
|
|
187
|
+
## Writing a value
|
|
188
|
+
|
|
189
|
+
A number, a day or a time on screen is wording as much as a sentence is, and
|
|
190
|
+
navi writes it: never `Intl` or `toLocale*` with a locale of the app's own,
|
|
191
|
+
which pins one region beside navi's dates and ignores `setSupportedLanguages`.
|
|
192
|
+
Each helper takes `lang` and otherwise reads the signal above; the component is
|
|
193
|
+
for JSX, the function for a string (a `title`, a notification).
|
|
194
|
+
|
|
195
|
+
| Value | Component | String |
|
|
196
|
+
| ----------------------- | -------------------------------------------------- | ------------------------------------------------ |
|
|
197
|
+
| a number | `<Quantity>` | `formatNumber` |
|
|
198
|
+
| a day | `<Time type="date">` | `formatDay` |
|
|
199
|
+
| a month | `<Time type="month">` | `formatMonth` |
|
|
200
|
+
| a day and its time | `<Time type="datetime">` | `formatDatetime` |
|
|
201
|
+
| a time of day | `<Time type="time">` | `formatTimeOfDay`, or `formatTime` from a `Date` |
|
|
202
|
+
| a clock from a count | `<Time type="hour"\|"minute" format="timestring">` | |
|
|
203
|
+
| two times of day | `<TimeRange>` | `formatTimeRange` |
|
|
204
|
+
| a day relative to today | `dayLabel` on `<Time type="date">`, beside the day | `formatDayRelative` |
|
|
205
|
+
| an instant, from now | `<Time type="relative">` | `formatTimeRelative` |
|
|
206
|
+
| a duration | `<Time type="duration">` | `formatDuration` and its per-unit siblings |
|
|
207
|
+
| items read as one list | | `formatList` |
|
|
208
|
+
|
|
209
|
+
A spelled day writes its year only when it is not in `now`'s year (`year:
|
|
210
|
+
"auto"`, see `formatDay`): appending the year by hand writes it twice for a
|
|
211
|
+
date in another year. A capital letter at the start of a value is
|
|
212
|
+
`<Text capitalize>`, not a sliced string.
|
|
213
|
+
|
|
137
214
|
## Changing what navi says
|
|
138
215
|
|
|
139
216
|
`naviI18n` holds navi's own texts. Register on it to override a default or to
|
|
@@ -156,4 +233,6 @@ so overriding either is the same call.
|
|
|
156
233
|
|
|
157
234
|
**Do not put application texts in `naviI18n`.** Key collisions aside, the two
|
|
158
235
|
have different lifecycles: navi's keys come with the package version, the app's
|
|
159
|
-
come with the app.
|
|
236
|
+
come with the app. The unit names it looks up are for rewording a unit navi or
|
|
237
|
+
`Intl` already knows ("hour", "kilometer"); a unit word of the app's own comes
|
|
238
|
+
from the app's registry, translated (see [above](#centralizing-texts-createi18n)).
|