@jsenv/navi 0.29.491 → 0.29.493

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/docs/actions.md CHANGED
@@ -456,9 +456,13 @@ on a read-only control — holds everywhere but on a control with
456
456
  button and a link hold no state to mirror: there, each press is one call). The
457
457
  user typing is one cause among several: a `value` prop coming back down after a
458
458
  render, a popup control propagating its choice up to the picker holding it, a
459
- group cascading a value into its children — all of them reach `uiAction`, so
460
- that a signal or a local variable listening to it never drifts out of sync.
461
- Reading it as "the user did something" is the natural mistake.
459
+ group cascading a value into its children, a group filling a control it builds
460
+ (a popup's fields at the opening), a new `defaultValue` taken by a field nobody
461
+ edited — all of them reach `uiAction`, so that a signal or a local variable
462
+ listening to it never drifts out of sync. The one value it does not hear is the
463
+ one a control starts on by itself, its own `defaultValue` or `value`: a mirror
464
+ reads that one at mount. Reading it as "the user did something" is the natural
465
+ mistake.
462
466
 
463
467
  The second argument says which one it was. Every event navi dispatches carries
464
468
  the event that caused it, and `findEvent(event, type)` walks that chain back:
@@ -291,7 +291,9 @@ or taken back (the action failed, the popup was cancelled).
291
291
 
292
292
  This holds for a control, for a group given a `defaultValue` (a selectable list,
293
293
  a `ControlGroup`), and for everything inside a picker's popup, which stays
294
- mounted while closed.
294
+ mounted while closed. An object — a `ControlGroup`, a `Form` — takes it key by
295
+ key: a field holding an edit keeps it, measured against its key of the new
296
+ default, while the fields around it follow.
295
297
 
296
298
  Where the record goes depends on who sends it. A control that saves itself —
297
299
  its own `action`, nothing around it to press — takes it as `defaultValue`
@@ -237,22 +237,40 @@ typed in it, and coming back re-reads the resource. There is nothing to restore.
237
237
  ### A screen that stays open after Save
238
238
 
239
239
  Everything above assumes the screen is left on saving. One edited in place —
240
- a settings panel, saved, then edited again — has no shape that answers all of
241
- it yet, and each of the three leaves one thing out:
242
-
243
- - `defaultValue={record.x}` follows the record and never undoes an edit, but is
244
- not held: an untouched Save sends the record back.
245
- - `onLoad` + `pristineKey` is held, and stops following: a record moved while
246
- the screen is open stays as `onLoad` found it.
247
- - `useSignalSync(record.x)` ([state_binding.md](./state_binding.md)) is held
248
- and follows, but the record moving overwrites what is being typed. It
249
- compares by content: a record read again as a new but equal array or object
250
- is not a move.
251
-
252
- Until one shape answers all three, a block that stays on screen takes
253
- `useSignalSync`, one per field: what it gives up — a move landing over a field
254
- being typed — needs someone else saving the same record at that moment, the
255
- rarest of the three on a settings block.
240
+ a settings panel, saved, then edited again — asks three things at once: the
241
+ form holds the record (an untouched Save sends nothing), follows it when it
242
+ moves (another admin, a run ending), and never overwrites what is being typed.
243
+ That is `<Form record>`:
244
+
245
+ ```jsx
246
+ <Form action={saveSetting} record={setting}>
247
+ <Input name="interval_minutes" />
248
+ <CheckboxGroup name="sexes" required>
249
+ …
250
+ </CheckboxGroup>
251
+ <Button type="submit" readOnlyWhileFormUnchanged>
252
+ Enregistrer
253
+ </Button>
254
+ </Form>
255
+ ```
256
+
257
+ Each named field starts on its key, held. A new record — compared by content,
258
+ so the same one read again is no move — is taken key by key: a field nobody
259
+ touched shows it, a field being typed in keeps what is typed, and the form,
260
+ measured against the new record from then on, reads changed exactly where an
261
+ edit differs from it. Save then sends the edit with the other keys as the
262
+ record now has them, not as they were when the screen opened. A send that went
263
+ through needs nothing more: its answer, landing as the next record, is what
264
+ the form holds. The record may be `undefined` while it loads — no
265
+ `pristineKey`, no `onLoad` copy. Nothing tells the person typing that the
266
+ record moved under their field: their Save is the answer.
267
+
268
+ A single control that saves itself has the same three with
269
+ `defaultValue={record.x}`, which follows and never undoes an edit; held only
270
+ matters to a form. A control bound to a signal of the screen's own takes
271
+ `useSignalSync(record.x)` ([state_binding.md](./state_binding.md)): held and
272
+ following, compared by content, but the record moving overwrites what is being
273
+ typed.
256
274
 
257
275
  What not to write: `useSignal(record.x)` with `key={record.updated_at}` on the
258
276
  block. The key throws away what is being typed each time the record moves.
@@ -435,9 +453,11 @@ only its mechanisms change.
435
453
  A list's copy of a record can be minutes old, so a sheet that saves the whole
436
454
  record reads it again as it opens, and that answer lands after the popup has
437
455
  taken what it holds at open. A `Picker` given the record as its `value` (or its
438
- signal) counts what the outside writes before the person changes anything as
439
- what was there at open: a look and a close send nothing, and a cancel goes back
440
- to the record as it arrived, not to the list's copy.
456
+ signal) counts what the outside writes before the person presses or types in
457
+ the popup as what was there at open: a look and a close send nothing, and a
458
+ cancel goes back to the record as it arrived, not to the list's copy. A record
459
+ landing after that press — a slow re-read, a sub-sheet opened only to look — is
460
+ an edit, and a keeping close sends it.
441
461
 
442
462
  What does not change:
443
463
 
@@ -25,8 +25,9 @@ whether a missing list is a skeleton, a sentence or an illustration.
25
25
 
26
26
  ## Asking for a state means receiving it as a value
27
27
 
28
- Without options, `useAsyncData` **delegates**: it suspends while the data is not
29
- there and throws when the action failed, so the states are drawn by the
28
+ Without options, `useAsyncData` **delegates**: it suspends while the action
29
+ runs — a rerun over data already held included — and throws when the action
30
+ failed, so the states are drawn by the
30
31
  `<Loading>` and `<ErrorBoundary>` above rather than by the component. Each
31
32
  option turns one of those from something done _to_ the component into a value
32
33
  the component reads:
@@ -557,6 +557,21 @@ sends the copy home like any refused carry. Nothing waits on a write still out:
557
557
  the next drop is answered on its own, so an answer that replays its moves over
558
558
  the state it was given keeps two drops in a row in order.
559
559
 
560
+ One outcome may answer two kinds of drop: a player put on another seat stands
561
+ there at once, the same player dropped on « Free the seat » is asked first, and
562
+ the copy has to wait over the zone while the question is open. `optimistic` is
563
+ then a function of the drop — the `event.detail` the answer reads, without the
564
+ event —, asked as the answer is about to run:
565
+
566
+ ```jsx
567
+ land: {
568
+ answer: (event) =>
569
+ event.detail.toId === releaseZoneId ? askRelease(event) : land(event),
570
+ verb: "PATCH",
571
+ optimistic: (detail) => detail.toId !== releaseZoneId,
572
+ },
573
+ ```
574
+
560
575
  Only `reorder` and `land`: a `move` hands its position over when the answer is
561
576
  in, and a throw has nothing to draw at once.
562
577
 
@@ -111,7 +111,8 @@ A value can also say what it is besides what it does, as
111
111
  happen, and the policy's words are said where the hand made it — before anything
112
112
  moves, for a gesture: a carried thing is not picked up, a swiped row resists like
113
113
  a wall. `optimistic` is for a `reorder` or a `land` drawn at once and written
114
- after ([drag_interactions.md](./drag_interactions.md#answered-at-once-optimistic)).
114
+ after — `true`, or a function of the drop deciding it per drop
115
+ ([drag_interactions.md](./drag_interactions.md#answered-at-once-optimistic)).
115
116
  A falsy `answer` is "not this one" too.
116
117
 
117
118
  ### The interactions navi detects
@@ -580,7 +580,9 @@ Five cases, and they are not a policy to configure but five different facts:
580
580
  further down than that page is tall.
581
581
  - **Replacing the entry** (`<Link replace>`, `route.redirectTo()`, a param
582
582
  settling) moves nothing: the same place said differently — except in a row of
583
- tabs, below.
583
+ tabs, below. A link to the address the document is at replaces too, as the
584
+ browser answers it: stacked, it would leave an entry that Back returns to
585
+ without moving.
584
586
  - **A reload** lands where one was, as it would have without navi, once the
585
587
  page is there ([below](#a-page-that-is-not-whole-yet)).
586
588
 
@@ -727,10 +729,19 @@ above the element has laid out, with nothing to tell it how long to wait.
727
729
  A mark that only designates — the row a notification points at, outlined until
728
730
  the reader moves on — is `:target` too, even though it restyles the element: it
729
731
  lasts until the page next writes its URL, which is the length of "this one". A
730
- search param would outlive it, as state the page does not act on. The alignment
731
- `setUrlTargetOptions({ block })` sets holds for the whole app; a target that wants
732
- to land with more room before it says so on itself, with a scroll-margin
733
- (`scrollMarginBlock` on a `Box`), which the landing honours.
732
+ search param would outlive it, as state the page does not act on.
733
+
734
+ Where the target lands is said for the app with `setUrlTargetOptions({ block })`,
735
+ and by a target for itself with `data-url-target-block` — on the element, or on a
736
+ box around it, a list saying it once for its items — read first. `"auto"` is the
737
+ word for a target the app cannot size ahead: one that fits the screen (less the
738
+ scroll-padding its bars reserve) lands in the middle, read with what is around
739
+ it — the row a notification points at —, one taller lands on its top, where
740
+ reading starts — an article of the terms. It is not the default: an `id` on a
741
+ heading is short, and a heading is read from where it starts. A target that
742
+ wants more room before it says so with a scroll-margin (`scrollMarginBlock` on a
743
+ `Box`), which every alignment honours — never a margin computed to push it
744
+ towards the middle, right for one height of row only.
734
745
 
735
746
  ### A row of tabs, where a replace IS an arrival
736
747
 
@@ -102,7 +102,7 @@ was asked:
102
102
 
103
103
  - **A write is refused before the press.** A control holding a write verb's
104
104
  instance — `GAME.DELETE`, `GAME.DELETE.bindParams({ id })` — or any control
105
- inside a `<Form>` holding one, is read-only while the policy holds, and
105
+ feeding a `<Form>` holding one, is read-only while the policy holds, and
106
106
  answers the press with `readOnlyMessage`.
107
107
 
108
108
  Which actions the policy sees: those declaring a verb (`meta.verb`) — every
@@ -131,6 +131,17 @@ anything moves: a carried thing is not picked up, a swiped row resists as a wall
131
131
  does. Undeclared, the gesture plays to the end and the write it calls fails
132
132
  after — said on the element, the way any refused answer is.
133
133
 
134
+ What a held form holds is what feeds its write: its fields, and a press that
135
+ sends or updates what it holds. A button or a link asking nothing of that write
136
+ stays pressable — a travel between slides or a step back in a wizard
137
+ (`--navi-left`, `--navi-go-to-slide:recap`, `--navi-back`), a page (a `Link`,
138
+ `--navi-nav-to`), a `uiAction`, a function of its own without `actionVerb` (a
139
+ "Cancel", `ButtonCopyToClipboard`) — and so is a way out (`--navi-close`,
140
+ `--navi-cancel`). The same goes for what a held control holds: a `Link` in the
141
+ popup of a held `Picker` still leads to its page. The form's own read-only and
142
+ busy state hold them all the same; `whenSelfInteractionsBlocked="ignore"` is what
143
+ takes a button out of those too.
144
+
134
145
  ## Holding writes while reads go out
135
146
 
136
147
  `reads: "network"` keeps the first half of the policy and drops the second: the
@@ -196,6 +207,15 @@ has nothing to decide which one says why.
196
207
  />;
197
208
  ```
198
209
 
210
+ A `<Picker>` whose popup is that form is held the same way with
211
+ `openWhileReadOnly={false}` beside `readOnly`. Read-only alone, a picker
212
+ still opens, so that what it holds can be read, and everything in its popup
213
+ is held in turn — the right answer when the sheet has something to show, a
214
+ step for nothing when it only fills a form. When that form is bound to a
215
+ write or declares its `actionVerb`, navi holds it anyway, and marking the
216
+ opener only decides where the refusal is said: on the opener, before
217
+ anything opens, or in the sheet.
218
+
199
219
  - **What a screen says on a `NetworkPolicyError`** — "no network" is a fact
200
220
  about the device, "offline mode" a decision, "we are only looking" neither;
201
221
  `error.reason` is there so the screen says the right one.
@@ -24,6 +24,7 @@ and everything below follows from that one fact.
24
24
  - [A row of cards: one popup that walks](#a-row-of-cards-one-popup-that-walks)
25
25
  - [Lifting on the way back only](#lifting-on-the-way-back-only)
26
26
  - [What it costs, and where the time goes](#what-it-costs-and-where-the-time-goes)
27
+ - [Less motion](#less-motion)
27
28
  - [The wall, and the frame before the movement](#the-wall-and-the-frame-before-the-movement)
28
29
  - [What the browser does around it](#what-the-browser-does-around-it)
29
30
 
@@ -286,9 +287,9 @@ and dev warns.
286
287
 
287
288
  **Tell the collecting close from the others.** Escape, the back button and a
288
289
  press on the wall close the dialog too, and usually mean "not now" rather than
289
- "collect". A `Dialog`'s or a `Popover`'s `onClose` receives who asked
290
- (`e.detail.requester`, the button of a `--navi-close`), and only the close that
291
- collects writes the state.
290
+ "collect". A `Dialog`'s, a `Popover`'s or a `Picker`'s `onClose` receives who
291
+ asked (`e.detail.requester`, the button of a `--navi-close`), and only the
292
+ close that collects writes the state.
292
293
  The others land nowhere: the fixed `liftAnchor` names nothing, so they wait out
293
294
  those 300 ms, fade out, and dev warns.
294
295
 
@@ -317,6 +318,19 @@ and one layout. So the lever is the content:
317
318
  The movement itself is the browser's: once it starts, the content's cost is
318
319
  over.
319
320
 
321
+ ## Less motion
322
+
323
+ Under `prefers-reduced-motion: reduce` nothing lifts: the popup opens or closes
324
+ where it stands. Only the movement is gone — the close is the same close,
325
+ `onClose` and `liftAnchor` are read as before, the wall still fades in. Not a
326
+ cross-fade in its place: that is still a view transition, and the page stays
327
+ frozen under its pictures for as long as it plays.
328
+
329
+ A popup's own opening and closing answer the setting too: `scaling` and
330
+ `expand-*` keep their fade and lose their growth, a `slide-from-*` lands at
331
+ once, `fading` is unchanged. So `{ open: "scaling", close: "lifting" }` closes
332
+ with a fade.
333
+
320
334
  ## The wall, and the frame before the movement
321
335
 
322
336
  The frame the dialog opens on is the picture the movement starts from. On it the
@@ -382,7 +382,10 @@ What the picker measures as "changed since open" is what its mirrored group
382
382
  holds. A piece of the answer living outside the controls — a seating rearranged
383
383
  by drag, kept in component state — has to be held by a control in the group too
384
384
  (a named control bound to that state), or a close over it reads as nothing
385
- changed and nothing runs.
385
+ changed and nothing runs. The app writing that state is the person's change
386
+ once they have pressed or typed in the popup (or in a sheet opened from it);
387
+ before that it is the outside's — the record re-read as the sheet opens
388
+ ([create_and_edit.md](./create_and_edit.md#the-same-loop-in-a-dialog)).
386
389
 
387
390
  A card in a list opens on `openOn="longpress"`, so a tap on it stays a tap. Two
388
391
  pickers on one card — the score sheet inside the edit card — are one wait: the
@@ -1104,7 +1107,9 @@ share sheet per card) pays one element per closed popup at every mount, not a
1104
1107
  popup: a row may carry the popups of its own actions. It is built at once when
1105
1108
  something needs it from the start — open at mount, a `navState`, an `anchor` (a
1106
1109
  hover builds the content ahead of the click), or a `mount` other than the
1107
- default. A `Picker`'s popup is the picker's own, built with it.
1110
+ default. A `Picker`'s popup is the picker's own, built with it: it is anchored
1111
+ on the picker's trigger, whose hover builds the content, and every way the
1112
+ picker opens asks its popup directly.
1108
1113
 
1109
1114
  "Closed" is two states, not one — never opened yet, and closed again after an
1110
1115
  opening — so the `mount` prop answers both at once:
package/docs/resource.md CHANGED
@@ -566,12 +566,19 @@ out with an example in its JSDoc:
566
566
  parent the store does not hold yet is left alone: its array comes with its
567
567
  next read.
568
568
  - **`.scopedOne(propertyName, callbacks)`** — every callback returns
569
- `[ownerId, props | null]`; the property is `null` until one does.
569
+ `[ownerId, props | null]`; the property is `null` until one does. An owner
570
+ the store does not hold yet is left alone: the answer is the action's value,
571
+ and the owner's next read brings its child.
570
572
  - **`.scopedMany(propertyName, { idKey, … })`** — `[ownerId, props]`; `PUT` (an
571
573
  id rename) `[ownerId, oldId, props]`; `DELETE` `[ownerId, childId]`; any
572
574
  `*_MANY` `[ownerId, itemArray]`, which replaces the whole collection;
573
575
  `DELETE_MANY` `[ownerId, [childId, …]]`. `idKey` names the child's own key
574
- inside its owner (`"id"` by default).
576
+ inside its owner (`"id"` by default). Where the collection arrives — a
577
+ `*_MANY` answer, an owner answer embedding the property — each child is set
578
+ to what it received: a field the held child has and the answer lacks becomes
579
+ `undefined`, since nothing else holds a part of a scoped child (a seat
580
+ changing hands keeps nothing of whoever sat there). A singular answer merges
581
+ into the child, as it may carry a part of it.
575
582
 
576
583
  `ownerId` may be `{ [uniqueKey]: value }` when the owner is known by an
577
584
  alternate key. A scoped child takes relations of its own —
@@ -637,7 +644,8 @@ Relationship mutations do **not** invalidate their parent by default. The rules:
637
644
  (`POST_MANY` included), rerun nothing on the owner: the callback result
638
645
  already carries the updated child, or the whole collection. Its own reads
639
646
  still follow its `rerunOn` (`POST` covers `POST_MANY` there).
640
- - `.scopedOne` mutations rerun nothing, ever — the result is the new value.
647
+ - `.scopedOne` mutations rerun nothing, ever — the result is the new value,
648
+ and whoever reads the property is drawn again with it.
641
649
  - `.one` / `.many` children live in an independent store; mutating them never
642
650
  reruns the parent. A `.many()` write changes the parent's array only through
643
651
  the answers naming it (`[parentId, …]`, see
@@ -775,6 +783,31 @@ written in place is; what fails throws into that first run. A cycle is fine: a
775
783
  rerun is one hop, and it reruns reads only. `createAction`'s `dependencies` take
776
784
  the same function form.
777
785
 
786
+ As written, that `USER` entry reruns every profile and every list of people
787
+ read in the session at each member added or removed — five more requests for
788
+ three profiles and two lists, growing with the session. A member written moves
789
+ the ranks of the group's list, read by slices, and changes no profile: name
790
+ the reads a heard write reaches with `reads`:
791
+
792
+ ```js
793
+ const USER = resource("user", {
794
+ dependencies: () => [
795
+ {
796
+ resource: PLAYER_GROUP,
797
+ on: ["addMember", "removeMember"],
798
+ reads: ["GET_RANGE"],
799
+ },
800
+ ],
801
+ GET,
802
+ GET_MANY,
803
+ GET_RANGE,
804
+ });
805
+ ```
806
+
807
+ `reads` takes `GET`, `GET_MANY` and `GET_RANGE`; left out, all three. Without
808
+ `on`, it applies to every write of the resource. `createAction`'s
809
+ `dependencies` refuse it: an action is one question, asked again whole.
810
+
778
811
  ## A function calling the verb, or the instance
779
812
 
780
813
  Both forms are real, and what separates them is narrower than it looks:
package/docs/scroll.md CHANGED
@@ -129,7 +129,9 @@ today's) — and it is compared by value, so it can be computed at every render.
129
129
  left and come back to, or the page reloaded, the way the page does (its `id`
130
130
  must name one list of the app, see [below](#a-list-that-comes-back-one-id-one-list));
131
131
  **`scrollResetOnNavigation`** opts out. A push to its screen is an arrival and
132
- forgets it, even when the address names the item one was reading: a link that is
132
+ forgets it — so is a document loaded fresh at its address, typed or reached from
133
+ outside the app, where only a reload or a back/forward load finds it again —
134
+ even when the address names the item one was reading: a link that is
133
135
  a return by meaning says `asReturn`, and the list comes back where that address
134
136
  was left (see
135
137
  [navigation.md](./navigation.md#where-a-navigation-lands-the-scroll)). What it draws there before its source
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.491",
3
+ "version": "0.29.493",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "preact": "^11.0.0-rc.2"
35
35
  },
36
36
  "dependencies": {
37
- "@jsenv/dom": "0.17.98",
37
+ "@jsenv/dom": "0.17.99",
38
38
  "@jsenv/humanize": "1.8.11",
39
39
  "@jsenv/validity": "0.4.7"
40
40
  },