@jsenv/navi 0.29.489 → 0.29.491

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/resource.md CHANGED
@@ -66,6 +66,25 @@ it yourself is the point:
66
66
  (see [When the backend answers a sub-route with the whole
67
67
  parent](#when-the-backend-answers-a-sub-route-with-the-whole-parent)).
68
68
 
69
+ A pushed message is an answer — JSON, every object in it new — and enters
70
+ through `store.upsertAnswer(row)`, which compares it in depth to the row held:
71
+ the readers of a row that did not move stay still. `store.upsert()` compares
72
+ each field by reference, so a pushed row equal to the one held re-renders its
73
+ readers as soon as it carries a nested object. The store is not a list: a row
74
+ it did not hold joins no `GET_MANY` and no `GET_RANGE` composition. Read again
75
+ what it belongs to, as a `POST` of the resource would:
76
+
77
+ ```js
78
+ socket.addEventListener("message", (message) => {
79
+ const run = JSON.parse(message.data);
80
+ const known = RUN.store.select(run.id);
81
+ RUN.store.upsertAnswer(run);
82
+ if (!known) {
83
+ RUN.GET_RANGE.invalidate(); // or RUN.GET_MANY.rerun()
84
+ }
85
+ });
86
+ ```
87
+
69
88
  A list that loads its rows a slice at a time is **not** one of those cases —
70
89
  that is `GET_RANGE`, below.
71
90
 
@@ -129,6 +148,14 @@ What stays the app's, because only the app knows:
129
148
  the drawn row for the whole request. It waits on `data` being set — the
130
149
  emptiness test of data_states.md — and the refresh is shown as usual.
131
150
 
151
+ **An identity change is a fresh document.** Sign-out, switching to another
152
+ account, taking an invited account, leaving a "view as": write `undefined` into
153
+ the kept signals (`persist`, `keepPageOnScreen`), then
154
+ `window.location.replace(…)`. navi's `reload()` reruns the route actions only;
155
+ the stores, the component reads and the lists' compositions stay, and they are
156
+ the previous person's. A list of things to reset one by one is a list nobody
157
+ can tell is complete.
158
+
132
159
  What the copy is not:
133
160
 
134
161
  - **an answer.** The `GET` goes out every time; a screen must not read `data` as
@@ -170,11 +197,17 @@ keepPageOnScreen({
170
197
  screens: `GAME.GET` for any game opened, `USER.GET_MANY` for a tab and for
171
198
  every search. Kept per resource, that is every game ever opened. The page's
172
199
  reads are what navi already runs for it: the route actions asking something for
173
- the page (`activeRouteActionsSignal`; `activeRouteReadsSignal` is the same set
174
- without the page's code, what the app reruns when it comes back to the
175
- foreground), and the compositions the `<List.Items>`
200
+ the page (`activeRouteActionsSignal`), and the compositions the `<List.Items>`
176
201
  on screen read. A navigation replaces the slot with the next page's reads.
177
202
 
203
+ The same reads are what the app reads again when it comes back to the
204
+ foreground: `rerunActions(activeRouteReadsSignal.peek())`. That set is the
205
+ route actions without the page's code, plus the `GET_RANGE` readers of the
206
+ lists on screen; a reader handed to `rerunActions` revalidates — its lists ask
207
+ again for the window they draw, their rows kept, and every composition stays
208
+ (`reader.revalidate()`, where `invalidate()` would send the lists off screen
209
+ back to skeletons).
210
+
178
211
  What is kept, for the next document's first run of the same read with the same
179
212
  params:
180
213
 
@@ -481,6 +514,37 @@ for its verb. A dependent names it like a callback —
481
514
  `{ resource: ORDER, on: ["cancel"] }` reruns after a cancel, not after an
482
515
  archive — and a resource listed bare counts it with every other write.
483
516
 
517
+ A scoped relation takes `writes` as well. A sub-collection with several
518
+ writes — seat a player, move one, take one out — is a `.scopedMany()` naming
519
+ each of them, never `op`s dispatched inside the owner's `PATCH`. A
520
+ `.scopedMany()` write may also take a plural verb, for a route answering the
521
+ whole owner:
522
+
523
+ ```js
524
+ const GAME_SEATS = GAME.scopedMany("participants", {
525
+ idKey: "slot",
526
+ writes: {
527
+ add: {
528
+ verb: "POST_MANY",
529
+ callback: async ({ id, ...body }) => {
530
+ const game = await post(`/games/${id}/participants`, body);
531
+ return [game.id, game.participants];
532
+ },
533
+ },
534
+ move: { verb: "PATCH_MANY", callback: moveParticipant },
535
+ remove: { verb: "DELETE", callback: removeParticipant },
536
+ },
537
+ });
538
+ const ALERT = resource("alert", {
539
+ dependencies: [{ resource: GAME_SEATS, on: ["add", "remove"] }],
540
+ GET,
541
+ });
542
+ ```
543
+
544
+ Each answers in the relation's shape for its verb — `[ownerId, items]` for a
545
+ plural one, which replaces the collection and reruns nothing on the owner
546
+ ([Relations and autorerun](#relations-and-autorerun)).
547
+
484
548
  ## Callback return contracts
485
549
 
486
550
  These are not guessable — each relationship method has its own shape, written
@@ -492,9 +556,15 @@ out with an example in its JSDoc:
492
556
  `GET`/`POST` that embeds the child inline works too.
493
557
  - **`.many(propertyName, CHILD, callbacks)`** — `GET_MANY` returns the
494
558
  **parent** with the array nested inside; `GET`/`POST`/`PUT`/`PATCH` the
495
- **child**, upserted into the child store but not joining the parent's array,
496
- which only a `GET_MANY` rewrites; `DELETE` `[parentId, childId]`,
497
- `DELETE_MANY` `[parentId, [childId, …]]`.
559
+ **child**, upserted into the child store but not joining the parent's array;
560
+ `POST` `[parentId, child]` upserts the child **and** appends it to the
561
+ parent's array (putting someone in a group); `POST_MANY`/`PUT_MANY`/`PATCH_MANY`
562
+ an array of children, upserted only, or `[parentId, [child, …]]`, which also
563
+ replaces the parent's array; `DELETE` `[parentId, childId]`, `DELETE_MANY`
564
+ `[parentId, [childId, …]]`. A `POST` never renames, so its array is always the
565
+ join; a child is never an array, so a plural answer is never ambiguous. A
566
+ parent the store does not hold yet is left alone: its array comes with its
567
+ next read.
498
568
  - **`.scopedOne(propertyName, callbacks)`** — every callback returns
499
569
  `[ownerId, props | null]`; the property is `null` until one does.
500
570
  - **`.scopedMany(propertyName, { idKey, … })`** — `[ownerId, props]`; `PUT` (an
@@ -540,10 +610,12 @@ const GAME_CANDIDATES = GAME.scopedMany("candidates", {
540
610
  ```
541
611
 
542
612
  For `.many()`, the equivalent is its `GET_MANY` callback, which already takes
543
- the parent object with the array nested inside — return the response untouched.
544
- Deletion is the exception: `DELETE`/`DELETE_MANY` drop by id rather than
545
- replace, so return `[ownerId, deletedId]` from the id you already have in the
546
- params, and ignore the full parent the backend sent back.
613
+ the parent object with the array nested inside — return the response untouched
614
+ — and, for a write, a plural callback returning `[parentId, children]` (a
615
+ favourite toggled, answered with the whole list of favourites). Deletion is the
616
+ exception: `DELETE`/`DELETE_MANY` drop by id rather than replace, so return
617
+ `[ownerId, deletedId]` from the id you already have in the params, and ignore
618
+ the full parent the backend sent back.
547
619
 
548
620
  The parent's own fields in such a response (a `status`, a `score`) are applied
549
621
  only where the callback returns the parent: `.one()`'s `GET`/`PUT` and
@@ -557,17 +629,21 @@ explicitly.
557
629
  Relationship mutations do **not** invalidate their parent by default. The rules:
558
630
 
559
631
  - `.scopedMany` child **POST** reruns the owner's singular `GET`, whether it
560
- goes through the child or through a `withParams()` scope of it — but only when
561
- the last GET response actually embedded that property. GET_MANY on the parent
562
- is never rerun by a child POST (a list of parents is not stale because one of
563
- them gained a child).
564
- - `.scopedMany` child **PUT / PATCH / DELETE** rerun nothing: the callback result
565
- already carries the updated child.
632
+ goes through the child, a `withParams()` scope of it or a write named with
633
+ that verb — but only when the last GET response actually embedded that
634
+ property. GET_MANY on the parent is never rerun by a child POST (a list of
635
+ parents is not stale because one of them gained a child).
636
+ - `.scopedMany` child **PUT / PATCH / DELETE**, and every **`*_MANY`** write
637
+ (`POST_MANY` included), rerun nothing on the owner: the callback result
638
+ already carries the updated child, or the whole collection. Its own reads
639
+ still follow its `rerunOn` (`POST` covers `POST_MANY` there).
566
640
  - `.scopedOne` mutations rerun nothing, ever — the result is the new value.
567
641
  - `.one` / `.many` children live in an independent store; mutating them never
568
- reruns the parent. Declare it with
569
- [`dependencies`](#dependencies-rerun-after-another-resource-writes) if you
570
- need it.
642
+ reruns the parent. A `.many()` write changes the parent's array only through
643
+ the answers naming it (`[parentId, …]`, see
644
+ [Callback return contracts](#callback-return-contracts)); otherwise declare
645
+ it with [`dependencies`](#dependencies-rerun-after-another-resource-writes)
646
+ if you need it.
571
647
  - Within a relationship resource, the usual defaults still apply: its own
572
648
  `GET_MANY` reruns after its own `POST`. A `.one`/`.many` `GET` is reset (not
573
649
  rerun) by its own `DELETE`, as a root `GET` is; a scoped `GET` is not reset —
@@ -674,6 +750,31 @@ write, or that the resource does not define, throws at declaration — a
674
750
  dependency that never fires leaves a count on its old value with nothing to say
675
751
  so.
676
752
 
753
+ A resource can only list one declared before it: a `const` further down the
754
+ file is not there yet. When two resources each change what the other shows —
755
+ the members of a group are users, and starring a user writes into a group — or
756
+ when a file's order would decide it, give `dependencies` as a function:
757
+
758
+ ```js
759
+ const USER = resource("user", {
760
+ dependencies: () => [
761
+ { resource: PLAYER_GROUP, on: ["addMember", "removeMember"] },
762
+ ],
763
+ GET,
764
+ });
765
+ const PLAYER_GROUP = resource("player_group", {
766
+ dependencies: [USER],
767
+ GET,
768
+ writes: { addMember, removeMember },
769
+ });
770
+ ```
771
+
772
+ It is called once, before the first request of any resource leaves — every
773
+ module has been evaluated by then — and what it returns is checked as a list
774
+ written in place is; what fails throws into that first run. A cycle is fine: a
775
+ rerun is one hop, and it reruns reads only. `createAction`'s `dependencies` take
776
+ the same function form.
777
+
677
778
  ## A function calling the verb, or the instance
678
779
 
679
780
  Both forms are real, and what separates them is narrower than it looks:
package/docs/safe_area.md CHANGED
@@ -125,13 +125,16 @@ Beware of making that container a scroller by accident — see
125
125
 
126
126
  An app whose document scrolls still has screens that must fit the screen: a
127
127
  head that stays put over a part that scrolls, a wizard whose step fills what is
128
- left. The height of the free band is the visual viewport less both insets:
128
+ left. The height of the free band is the visual viewport less both insets, less
129
+ the strips that stick under a bar in the flow:
129
130
 
130
131
  ```css
131
132
  .wizard {
132
133
  height: calc(
133
134
  var(--navi-vvh) - var(--navi-safe-area-inset-top) -
134
- var(--navi-safe-area-inset-bottom)
135
+ var(--navi-safe-area-inset-bottom) -
136
+ var(--navi-transition-cover-top, 0px) -
137
+ var(--navi-transition-cover-bottom, 0px)
135
138
  );
136
139
  }
137
140
  ```
@@ -143,6 +146,13 @@ the app's own bands and an overlaying keyboard (see
143
146
  slot is how a `FixedBar` writes what it takes, and reading it for layout misses
144
147
  the notch on an edge with no bar, the band of a capped app, and the keyboard.
145
148
 
149
+ The covers are the strips of [the next section](#a-strip-under-a-bar): an
150
+ offline line above the page pushes it down by its height, and a page as tall as
151
+ the band would end that much under the bottom bar. `useTransitionCover`
152
+ publishes the tallest strip on each edge, in the frame the strip changes, and
153
+ removes it when the strip goes; the `0px` is for an app that has none, where the
154
+ variable does not exist.
155
+
146
156
  ### A strip under a bar
147
157
 
148
158
  A strip that stays under the top bar — a status banner, an "offline" line — is
@@ -162,7 +172,8 @@ useTransitionCover(bannerRef);
162
172
  ```
163
173
 
164
174
  `useTransitionCover` publishes the strip's height, so the pictures of a route
165
- movement are cut under it rather than sliding over it. An anchor's
175
+ movement are cut under it rather than sliding over it, and a page fitting the
176
+ free band leaves room for it ([above](#a-page-that-fits-the-free-band)). An anchor's
166
177
  `scroll-margin` adds only what `:root`'s `scroll-padding` does not hold already
167
178
  — the strip's own height, a few pixels of air — never the bar or the band.
168
179
 
package/docs/scroll.md CHANGED
@@ -128,7 +128,11 @@ followed — a place read from an answer that refreshes (yesterday's "today", th
128
128
  today's) — and it is compared by value, so it can be computed at every render. A list with an `id` also **comes back where it was** when its screen is
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
- **`scrollResetOnNavigation`** opts out. What it draws there before its source
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
133
+ a return by meaning says `asReturn`, and the list comes back where that address
134
+ was left (see
135
+ [navigation.md](./navigation.md#where-a-navigation-lands-the-scroll)). What it draws there before its source
132
136
  answers is another matter
133
137
  ([What a run knows when it opens](#what-a-run-knows-when-it-opens)). A list scrolling the document that
134
138
  opens on an item places the document itself: the offset the page kept for its url
package/docs/testid.md CHANGED
@@ -154,10 +154,13 @@ error message, a section, a row — put the testid on the surrounding component
154
154
  built on `Box` forwards it to its own element.
155
155
 
156
156
  A selectable `List.Item` is a control too, and the rule holds there: its host is
157
- the real checkbox or radio the row selects with, an input navi draws invisible
158
- and lays under the row's content. A `data-testid` on the item names that input —
159
- what `toBeChecked()` reads, not what the eye sees. A test that presses the row,
160
- or waits for it to show, targets what the row draws: a testid on its content.
157
+ the real checkbox or radio the row selects with, an input navi draws invisible,
158
+ as big as the row, under the row's content — content that lets the pointer
159
+ through to it (`selectableArea="all"`, the default). A `data-testid` on the item
160
+ names that input: it is what a press on the row reaches and what `toBeChecked()`
161
+ reads. A test that presses the row, or waits for it, targets the item's testid.
162
+ A testid on the content is for reading what the row says: Playwright refuses a
163
+ click on it, since the input is what the pointer meets there.
161
164
 
162
165
  What navi builds itself is named through props. A `data-testid` on a `<Picker>`
163
166
  names the trigger; `popupTestId` names the popup it builds (the callout, under
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.489",
3
+ "version": "0.29.491",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {
@@ -34,8 +34,8 @@
34
34
  "preact": "^11.0.0-rc.2"
35
35
  },
36
36
  "dependencies": {
37
- "@jsenv/dom": "0.17.97",
38
- "@jsenv/humanize": "1.8.10",
37
+ "@jsenv/dom": "0.17.98",
38
+ "@jsenv/humanize": "1.8.11",
39
39
  "@jsenv/validity": "0.4.7"
40
40
  },
41
41
  "devDependencies": {