@jsenv/navi 0.29.19 → 0.29.21

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.
@@ -33,6 +33,10 @@ consistency across the app, not from any single call site.
33
33
  - `docs/resource.md` — REST state: `resource()`, relationships, callback return
34
34
  contracts, autorerun rules. Companion files: `docs/actions.md`,
35
35
  `docs/resource_with_params.md`, `docs/resource_dependencies.md`.
36
+ - `docs/list_refresh.md` — what a write sends back to the network and what stays
37
+ on screen meanwhile: stale data returned by `useAsyncData({ loading: true })`,
38
+ what updates from a response without any request, `rerunOn` and its defaults.
39
+ Read it before adding verbs to `rerunOn` or hiding a list on `loading`.
36
40
  - `docs/css_architecture.md` — how Navi's CSS layering works, and the
37
41
  supported ways to override component styles (props > CSS variables > direct
38
42
  rule overrides, in that preference order).
package/docs/actions.md CHANGED
@@ -1,250 +1,117 @@
1
- /\*\*
2
-
3
- - # Actions System - Declarative Resource Management for Frontend Applications
4
- -
5
- - This module provides a comprehensive system for managing asynchronous resources (API calls, data fetching)
6
- - in a declarative, signal-based architecture. It's designed for complex frontend applications that need
7
- - fine-grained control over loading states, caching, and resource lifecycle management.
8
- -
9
- - ## Core Concepts
10
- -
11
- - ### 🔧 **Action Templates**
12
- - Factory functions that define how to load resources. Templates are pure and reusable.
13
- - ```js
14
-
15
- ```
16
-
17
- - const getUserTemplate = createActionTemplate(async ({ userId }) => {
18
- - const response = await fetch(`/api/users/${userId}`);
19
- - return response.json();
20
- - });
21
- - ```
22
-
23
- ```
24
-
25
- -
26
- - ### 🎯 **Action Instances**
27
- - Stateful objects created from templates with specific parameters. Each unique parameter set
28
- - gets its own cached instance (automatic memoization).
29
- - ```js
30
-
31
- ```
32
-
33
- - const userAction = getUserTemplate.instantiate({ userId: 123 });
34
- - const status = useActionStatus(userAction); // { pending, data, error, ... }
35
- - ```
36
-
37
- ```
38
-
39
- -
40
- - ### 🔄 **Action Proxies**
41
- - Dynamic actions that react to signal changes, automatically reloading when parameters change.
42
- - ```js
43
-
44
- ```
45
-
46
- - const userProxy = createActionProxy(getUserTemplate, {
47
- - userId: userIdSignal, // Signal - reactive
48
- - includeProfile: true // Static - not reactive
49
- - });
50
- - // Automatically reloads when userIdSignal changes
51
- - ```
52
-
53
- ```
54
-
55
- -
56
- - ## Loading States & Lifecycle
57
- -
58
- - ### 📊 **State Management**
59
- - Each action has a well-defined state machine:
60
- - - `IDLE` → `LOADING` → `LOADED` (success)
61
- - - `IDLE` `LOADING` `FAILED` (error)
62
- - - `IDLE` `LOADING` `ABORTED` (cancelled)
63
- -
64
- - ### ⚡ **Load Types**
65
- - - **`.load()`** - Load with user intent (sets `loadRequested: true`)
66
- - - **`.preload()`** - Background loading (sets `loadRequested: false`)
67
- - - **`.reload()`** - Force reload even if already loaded
68
- - - **`.unload()`** - Cancel loading and reset state
69
- -
70
- - ### 🛡️ **Preload Protection**
71
- - Preloaded actions are protected from garbage collection for 5 minutes to ensure
72
- - they remain available for components that may load later (e.g., via dynamic imports).
73
- -
74
- - ## Key Features
75
- -
76
- - ### 🧠 **Intelligent Memoization**
77
- - - Actions with identical parameters share the same instance
78
- - - Uses deep equality comparison with `compareTwoJsValues`
79
- - - Supports `SYMBOL_IDENTITY` for fast recognition of "conceptually same" objects
80
- - - Memory-efficient with automatic garbage collection
81
- -
82
- - ### 🔗 **Parameter Binding & Composition**
83
- - ```js
84
-
85
- ```
86
-
87
- - const baseAction = getUserTemplate.instantiate({ userId: 123 });
88
- - const enrichedAction = baseAction.bindParams({ includeProfile: true });
89
- - // Result: { userId: 123, includeProfile: true }
90
- -
91
- - // Supports objects, primitives, and signals
92
- - const dynamicAction = baseAction.bindParams(filtersSignal);
93
- - ```
94
-
95
- ```
96
-
97
- -
98
- - ### 🎮 **Concurrent Loading Control**
99
- - - Prevents duplicate requests for same resource
100
- - - Smart request deduplication and racing condition handling
101
- - - Coordinated loading/unloading of multiple actions via `updateActions()`
102
- -
103
- - ### 🔧 **Side Effects & Cleanup**
104
- - ```js
105
-
106
- ```
107
-
108
- - const actionTemplate = createActionTemplate(callback, {
109
- - sideEffect: (params, loadParams) => {
110
- - // Setup logic (analytics, subscriptions, etc.)
111
- - return () => {
112
- - // Cleanup logic - called on unload/abort
113
- - };
114
- - }
115
- - });
116
- - ```
117
-
118
- ```
119
-
120
- -
121
- - ## Usage Patterns
122
- -
123
- - ### 🏗️ **Basic Resource Loading**
124
- - ```js
125
-
126
- ```
127
-
128
- - const getUserAction = createActionTemplate(async ({ userId }) => {
129
- - return await api.getUser(userId);
130
- - });
131
- -
132
- - // In component
133
- - const userAction = getUserAction.instantiate({ userId: 123 });
134
- - const { pending, data, error } = useActionStatus(userAction);
135
- -
136
- - useEffect(() => {
137
- - userAction.load();
138
- - }, []);
139
- - ```
140
-
141
- ```
142
-
143
- -
144
- - ### 🔄 **Reactive Data Loading**
145
- - ```js
146
-
147
- ```
148
-
149
- - const searchProxy = createActionProxy(searchTemplate, {
150
- - query: searchSignal,
151
- - filters: filtersSignal
152
- - });
153
- - // Automatically reloads when signals change
154
- - ```
155
-
156
- ```
157
-
158
- -
159
- - ### 📋 **Master-Detail Pattern**
160
- - ```js
161
-
162
- ```
163
-
164
- - const usersAction = getUsersTemplate.instantiate();
165
- - const selectedUser = signal(null);
166
- -
167
- - const userDetailsProxy = createActionProxy(getUserTemplate, {
168
- - userId: computed(() => selectedUser.value?.id)
169
- - });
170
- - ```
171
-
172
- ```
173
-
174
- -
175
- - ### 🏃 **Progressive Loading**
176
- - ```js
177
-
178
- ```
179
-
180
- - // Preload on hover, load on click
181
- - <button
182
- - onMouseEnter={() => action.preload()}
183
- - onClick={() => action.load()}
184
- - >
185
- - Load User
186
- - </button>
187
- - ```
188
-
189
- ```
190
-
191
- -
192
- - ## Advanced Features
193
- -
194
- - ### 🎭 **Custom Data Transformation**
195
- - ```js
196
-
197
- ```
198
-
199
- - const actionTemplate = createActionTemplate(fetchUser, {
200
- - computedDataSignal: computed(() => {
201
- - const rawData = dataSignal.value;
202
- - return rawData ? transformUser(rawData) : null;
203
- - })
204
- - });
205
- - ```
206
-
207
- ```
208
-
209
- -
210
- - ### 🎨 **Async Rendering Support**
211
- - ```js
212
-
213
- ```
214
-
215
- - const actionTemplate = createActionTemplate(fetchData, {
216
- - renderLoadedAsync: async () => {
217
- - const { UserComponent } = await import('./UserComponent.js');
218
- - return (user) => <UserComponent user={user} />;
219
- - }
220
- - });
221
- - ```
222
-
223
- ```
224
-
225
- -
226
- - ### 🛠️ **Debugging & Observability**
227
- - Built-in debug mode with detailed logging of state transitions, loading coordination,
228
- - and memory management. Enable with `debug = true`.
229
- -
230
- - ## Integration Points
231
- -
232
- - - **Signals**: Built on @preact/signals for reactive state management
233
- - - **Navigation**: Integrates with navigation systems for route-based loading
234
- - - **Components**: Use `useActionStatus()` hook for component integration
235
- - - **Memory Management**: Automatic cleanup with WeakMap-based private properties
236
- -
237
- - ## Performance Characteristics
238
- -
239
- - - **Memory Efficient**: Weak references prevent memory leaks
240
- - - **Request Deduplication**: Identical requests are automatically merged
241
- - - **Minimal Re-renders**: Signal-based updates only trigger when data actually changes
242
- - - **Lazy Loading**: Actions only created when needed, with intelligent memoization
243
- -
244
- - This system is particularly well-suited for:
245
- - - SPAs with complex data fetching requirements
246
- - - Applications needing fine-grained loading state control
247
- - - Systems requiring request coordination and deduplication
248
- - - Progressive loading and preloading scenarios
249
- - - Master-detail interfaces with dynamic parameter binding
250
- \*/
1
+ # Actions
2
+
3
+ An action is an async callback plus the state of its last run, held in signals:
4
+ running or not, the error it failed with, the data it produced. Components read
5
+ that state instead of keeping their own.
6
+
7
+ ```js
8
+ import { createAction } from "@jsenv/navi";
9
+
10
+ const getUser = createAction(async ({ id }, { signal }) => {
11
+ const response = await fetch(`/users/${id}`, { signal });
12
+ return response.json();
13
+ });
14
+ ```
15
+
16
+ The callback receives `(params, { reason, event, signal, isPrerun })`. `signal`
17
+ is aborted when the run is called off pass it to `fetch`.
18
+
19
+ `resource()` creates one action per REST callback rather than having you write
20
+ them by hand — see [resource.md](./resource.md).
21
+
22
+ ## Params: `bindParams`, and calling the action
23
+
24
+ `createAction` gives one action for the callback; the params make instances of
25
+ it, each with its own state:
26
+
27
+ ```js
28
+ const getUser123 = getUser.bindParams({ id: 123 });
29
+ await getUser123.run();
30
+ ```
31
+
32
+ Two `bindParams` with equal params give **the same instance** (deep equality),
33
+ which is what makes state shared between two components asking for the same
34
+ thing, and what deduplicates their requests.
35
+
36
+ An action is callable, and calling it is the short way to bind and run in one
37
+ go:
38
+
39
+ ```js
40
+ getUser({ id: 123 }); // getUser.bindParams({ id: 123 }).rerun()
41
+ getUser(); // getUser.rerun()
42
+ ```
43
+
44
+ Use it wherever a run is a **gesture** — a click handler, an event, a step in a
45
+ flow — where the params are known at that moment and the run is the point:
46
+
47
+ ```js
48
+ const deleteGame = (game) => GAME.DELETE({ id: game.id });
49
+ ```
50
+
51
+ Use `bindParams` when what you need is the **instance**, not the run: to hand it
52
+ to a component that will run it and read its state
53
+ (`<Button action={GAME.DELETE.bindParams({ id })}>`), or to keep a handle on it.
54
+ Note that calling the action `rerun()`s it — the run happens even if that
55
+ instance already holds data, which is what you want from a gesture and not what
56
+ you want from a component asking for data.
57
+
58
+ Params may be signals, and then the action follows them:
59
+
60
+ ```js
61
+ const userAction = getUser.bindParams({ id: userIdSignal });
62
+ // a new params value reruns it
63
+ ```
64
+
65
+ ## Running: `run`, `rerun`, `prerun`, `reset`
66
+
67
+ | Method | Does |
68
+ | ---------- | ------------------------------------------------------------------------ |
69
+ | `run()` | Asks for the data. An action already running or completed has it: no-op. |
70
+ | `rerun()` | Runs again whatever state it is in — a refresh, an explicit "check now". |
71
+ | `prerun()` | Same as `run()`, in the background: nothing asked for it on screen yet. |
72
+ | `reset()` | Aborts what is running and puts the action back to idle, data and all. |
73
+ | `abort()` | Calls off the run in flight, keeping the data it had. |
74
+
75
+ ## Reading an action
76
+
77
+ ```jsx
78
+ const [user] = useAsyncData(userAction);
79
+ ```
80
+
81
+ `useAsyncData` suspends until the data is there and throws on failure, leaving
82
+ both to the nearest `<Loading>` and `<ErrorBoundary>`; pass `{ loading: true }`
83
+ or `{ error: true }` to handle either inside the component (stale data stays
84
+ available while a rerun is in flight). `useActionStatus(action)` gives the whole
85
+ state at once — `{ idle, loading, completed, aborted, error, data, params }` —
86
+ for a component that needs to look at it rather than render it.
87
+
88
+ Controls take the action itself and wire the rest: `<Button action>` runs it on
89
+ click, shows its loading state, and puts its error where the user can see it.
90
+ That is the reason to pass an action instance rather than an arrow calling the
91
+ callback:
92
+
93
+ ```jsx
94
+ // ✓ loading, error and disabled states come from the action
95
+ <Button action={GAME_CANDIDATES.POST.bindParams({ id: game.id, user_id })}>
96
+ Accept
97
+ </Button>
98
+ ```
99
+
100
+ ## Reruns
101
+
102
+ Actions do not stay stale on their own: a resource's `POST` reruns the
103
+ `GET_MANY` that lists it, a `DELETE` resets the `GET` that loaded the item, and
104
+ `dependencies`/`rerunOn` declare the rest. What re-runs after a write, and what
105
+ stays on screen while it does, is in [list_refresh.md](./list_refresh.md) and
106
+ [resource_dependencies.md](./resource_dependencies.md).
107
+
108
+ `rerunActions(actions)` / `updateActions(actions)` drive several at once (route
109
+ changes do exactly that).
110
+
111
+ ## See also
112
+
113
+ - [resource.md](./resource.md) — actions created from REST callbacks, and the
114
+ store behind them
115
+ - [resource_with_params.md](./resource_with_params.md) — `withParams()` and
116
+ isolated rerun scopes
117
+ - [list_refresh.md](./list_refresh.md) — what a write refreshes
@@ -0,0 +1,131 @@
1
+ # A list while it refreshes
2
+
3
+ When a write touches one item of a list, two questions decide what the user
4
+ sees: **what goes back to the network**, and **what stays on screen meanwhile**.
5
+ Getting either wrong turns "pause one row" into a full page reload.
6
+
7
+ The short answer:
8
+
9
+ - A write that returns the modified item fixes every list containing it, with
10
+ no request and no loading state at all.
11
+ - `useAsyncData(action, { loading: true })` keeps returning the previous data
12
+ while the action re-runs — the list is never taken away unless the component
13
+ throws it away.
14
+
15
+ ## `loading: true` returns the previous value
16
+
17
+ `useAsyncData` returns `[data, loading, error]`. During a re-run, `data` is the
18
+ **previous data**, not `undefined`:
19
+
20
+ | Moment | `data` | `loading` |
21
+ | --------------------- | ----------- | --------- |
22
+ | never completed yet | `undefined` | `true` |
23
+ | running after success | previous | `true` |
24
+ | completed | fresh | `false` |
25
+
26
+ So the emptiness test is `data === undefined`, never `loading`:
27
+
28
+ ```js
29
+ const [items, loading] = useAsyncData(ACTION, { loading: true });
30
+ if (items === undefined) {
31
+ return null; // first load: there is nothing to show yet
32
+ }
33
+ return <RadarList radars={items} busy={loading} />; // re-read: stay on screen
34
+ ```
35
+
36
+ ```js
37
+ // ✗ blanks the page on every re-run, for a checkbox on one row
38
+ if (loading) {
39
+ return null;
40
+ }
41
+ ```
42
+
43
+ Read `loading` as "what you are displaying is from before", not "there is
44
+ nothing to display".
45
+
46
+ `<List loading>` is the first-load answer, not the refresh one: it replaces the
47
+ rows with skeletons. Pass it while stale rows exist and they disappear — same
48
+ mistake as `loading ? null :`, one level down.
49
+
50
+ ## What updates without a request
51
+
52
+ An action's `data` is a computation over the resource store, not a snapshot of
53
+ its own last response. Any write that puts an item into the store recomputes
54
+ every list holding it, while the `GET_MANY` action stays `COMPLETED` — no
55
+ request, no loading state, no flicker.
56
+
57
+ The condition is the whole hinge of the system:
58
+
59
+ > **the write's callback must return the item, with its key** — `{ id, … }`.
60
+
61
+ Partial props are fine (`{ id, paused: true }` merges into the stored item); the
62
+ key is what cannot be missing. The corollary matters just as much: a callback
63
+ that returns nothing — a `204`, or a `fetch` whose result is dropped — writes
64
+ nothing to the store, and the change never reaches the screen. There, the only
65
+ way back is a re-read.
66
+
67
+ `DELETE` is symmetric: returning the id drops the item from the store, and every
68
+ list containing it drops it too.
69
+
70
+ ## `rerunOn`, verb by verb
71
+
72
+ `rerunOn` says which verbs invalidate this resource's `GET` / `GET_MANY`:
73
+
74
+ ```js
75
+ const GAME_RADAR = resource("game_radar", {
76
+ rerunOn: { GET_MANY: ["POST", "DELETE"] },
77
+
78
+ });
79
+ ```
80
+
81
+ Defaults are `{ GET: false, GET_MANY: ["POST"] }`:
82
+
83
+ | Default | Why |
84
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
85
+ | `GET: false` | `PUT`/`PATCH` already update the UI through the store; `DELETE` resets the `GET` rather than re-running it, so a deleted item shows nothing instead of a spinner then a 404. Give the deleted case its own UI (an "item not found" panel, a redirect) instead of `GET: ["DELETE"]`. |
86
+ | `GET_MANY: ["POST"]` | Whether a new item belongs in this list depends on filters, pagination, sort — the backend knows, the client does not. `DELETE` is excluded because the store already removes the item from every list. |
87
+
88
+ Adding `PUT`/`PATCH` to `GET_MANY` is the usual over-correction: it costs a
89
+ request and a `loading` pass to obtain something the response already contained.
90
+ When a `PATCH` "doesn't show", the fix is almost always server-side — return the
91
+ updated item — not client-side refreshing.
92
+
93
+ ## Decision table
94
+
95
+ | What changed | Re-read the list? |
96
+ | ------------------------- | ---------------------------------------------------- |
97
+ | a field of one item | no — the write's response is enough |
98
+ | membership of the list | yes (`POST`) — the backend decides who belongs |
99
+ | the ORDER of the list | yes — the store stores, it does not sort (see below) |
100
+ | nothing came back (`204`) | yes — there is nothing to put in the store |
101
+
102
+ ## The store stores, it does not sort
103
+
104
+ Each `*_MANY` action holds its own array of ids; its data is those ids resolved
105
+ against the store, in that order. A `PUT_MANY` returning the collection in a new
106
+ order updates every item, and sets the order **of the `PUT_MANY` action** — the
107
+ `GET_MANY` list keeps the order it already had.
108
+
109
+ That is the design (a store is not an index), but it has a sharp edge: a
110
+ drag-and-drop that "works" on screen — because local state holds the order —
111
+ comes back in the old order on the next visit. After a reorder, re-read the list
112
+ explicitly:
113
+
114
+ ```js
115
+ await REORDER_ACTION.run({ … });
116
+ GET_MANY_ACTION.rerun();
117
+ ```
118
+
119
+ ## `.rerun()`, not `.run()`, to refresh
120
+
121
+ `.run()` on an action that already `COMPLETED` does nothing: it is a request to
122
+ have the data, and the data is there. Wiring a "check now" button to `.run()`
123
+ therefore checks nothing, silently. Use `.rerun()`, which resets the action and
124
+ runs it again.
125
+
126
+ ## See also
127
+
128
+ - [resource.md](./resource.md) — `resource()`, relations, callback return
129
+ contracts
130
+ - [resource_dependencies.md](./resource_dependencies.md) — invalidating a
131
+ resource from another one
package/docs/resource.md CHANGED
@@ -25,10 +25,94 @@ Each callback returns the data to upsert into the store:
25
25
  | GET / POST / PUT / PATCH | the full item object, `{ id, … }` |
26
26
  | DELETE | the id, or `{ id }` |
27
27
  | GET_MANY / POST_MANY / … | an array of item objects |
28
+ | GET_PAGE | `{ items, start, count }` (below) |
28
29
 
29
30
  Actions are read in components through the action system (`useAsyncData`,
30
31
  `<Button action>`, …) — see [actions.md](./actions.md).
31
32
 
33
+ ## `store.upsert()` is not how data enters the store
34
+
35
+ navi writes the store. Declare the resource and its relations, return the shape
36
+ each callback owes (tables above and below), and the write happens. A hand
37
+ written `store.upsert()` in the path of a normal request of the resource is the
38
+ sign that something is missing: a relation that is not declared, or a callback
39
+ that does not return what it should.
40
+
41
+ `store` is exposed for what is **not** a request of the resource, where writing
42
+ it yourself is the point:
43
+
44
+ - seeding it with data that came from elsewhere — state rendered by the server,
45
+ a local cache, a websocket message;
46
+ - absorbing the parent object a relation route answered with: the relation
47
+ callback only writes the relation, so the parent's own fields have nowhere
48
+ else to land (see [When the backend answers a sub-route with the whole
49
+ parent](#when-the-backend-answers-a-sub-route-with-the-whole-parent)).
50
+
51
+ A list that loads its rows page by page is **not** one of those cases — that is
52
+ `GET_PAGE`, right below.
53
+
54
+ ## `GET_PAGE`: feeding a list that loads as it scrolls
55
+
56
+ A `<List.Items>` asks for the rows it is about to draw and keeps what it gets.
57
+ `GET_PAGE` is the resource's answer to that question — one slice at a time:
58
+
59
+ ```js
60
+ const GAME = resource("game", {
61
+ GET: ({ id }) => fetchJson(`/games/${id}`),
62
+ GET_PAGE: ({ radar, start, limit }) =>
63
+ fetchJson(`/radars/${radar.id}/games?start=${start}&limit=${limit}`),
64
+ // { items: [{ id, … }, …], start: 20, count: 137 }
65
+ });
66
+ ```
67
+
68
+ ```jsx
69
+ <List.Items
70
+ count={radar.match_count}
71
+ itemsAction={GAME.GET_PAGE.bindParams({ radar })}
72
+ renderItem={(game) => <GameCard game={game} />}
73
+ />
74
+ ```
75
+
76
+ The callback receives the bound params merged with the range the list asks for
77
+ (`start`, `end`, `limit`, `before`, `after`, `around`, `count`), and a `signal`
78
+ as second argument — aborted when the list stops wanting those rows. It returns
79
+ a page the way a `Content-Range` does: **`{ items, start, count }`** — these
80
+ rows, at this place, out of that many. `start` may be omitted when the list
81
+ asked for a positive one; `count` defaults to `start + items.length` (a source
82
+ that does not know its total). The items are upserted on their way in, so the
83
+ list draws store items — never copies of the JSON — and a request sent from a
84
+ row is read back on that row.
85
+
86
+ A row that must follow its **own fields** through a write reads them from the
87
+ store rather than from the object it was handed:
88
+
89
+ ```jsx
90
+ const GameCard = ({ id }) => {
91
+ const game = GAME.useById(id); // the item as it is now
92
+
93
+ };
94
+ ```
95
+
96
+ An update replaces the item object (the store holds values, and that is what
97
+ makes a change detectable), so the one the list is holding is the one it was
98
+ given. Relations are not concerned: they are keyed by owner, and a row reading
99
+ `game.candidates` reads the shared collection whatever object carries it.
100
+
101
+ `GET_PAGE` is a **reader, not an action**. It keeps no value and takes no place
102
+ in the rerun graph, which is what makes it usable per slice:
103
+
104
+ - the list already holds the pages it received and glues them back together —
105
+ a second memory holding one of them would fight it;
106
+ - a `POST` invalidating "the collection" would otherwise send every page ever
107
+ loaded back to the network at once.
108
+
109
+ What it does not give is membership: an item that leaves the collection stays on
110
+ screen until the rows are asked for again. Give the screen its own way to ask
111
+ (a refresh gesture, a `key` on the run).
112
+
113
+ It reads a collection, so it lives on the resource itself (or on a
114
+ `withParams()` of it), not on a relation.
115
+
32
116
  ## Relations: pick one of the four methods
33
117
 
34
118
  A backend sub-route (`/games/:id/candidates`, `/games/:id/candidates/:userId/seen`)
@@ -258,10 +342,16 @@ The arrow works, but nothing tracks it: no per-row spinner, no error surfaced on
258
342
  the button that caused it, no deduplication of concurrent runs, no autorerun of
259
343
  the actions this mutation should invalidate. See [actions.md](./actions.md).
260
344
 
345
+ Away from a component, where the run is a gesture and not something to render,
346
+ an action is callable: `GAME.DELETE({ id })` is
347
+ `GAME.DELETE.bindParams({ id }).rerun()`.
348
+
261
349
  ## See also
262
350
 
263
351
  - [resource_with_params.md](./resource_with_params.md) — `withParams()` and
264
352
  isolated lifecycles
265
353
  - [resource_dependencies.md](./resource_dependencies.md) — cross-resource
266
354
  autorerun
355
+ - [list_refresh.md](./list_refresh.md) — what re-runs after a write, and what
356
+ stays on screen while it does
267
357
  - [actions.md](./actions.md) — action lifecycle, `bindParams`, `useAsyncData`
package/docs/scroll.md CHANGED
@@ -229,7 +229,7 @@ is a trade, not a leak.
229
229
  own body) but never grows on its own. Growing is the caller's decision —
230
230
  `expandY`.
231
231
 
232
- Pass `keyboardTravel={false}` when the arrow keys belong to the content (a list
232
+ Pass `travelByKeyboard={false}` when the arrow keys belong to the content (a list
233
233
  one walks through, a picker whose slides are steps): otherwise the right arrow
234
234
  changes screen mid-reading.
235
235
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.19",
3
+ "version": "0.29.21",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {