@jsenv/navi 0.29.67 → 0.29.69
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/dist/jsenv_navi.js +606 -185
- package/dist/jsenv_navi.js.map +38 -27
- package/docs/AI_INSTRUCTIONS.md +13 -7
- package/docs/list_refresh.md +86 -2
- package/docs/navigation.md +36 -0
- package/docs/route_transitions.md +126 -3
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -72,8 +72,11 @@ consistency across the app, not from any single call site.
|
|
|
72
72
|
what updates from a response without any request, `rerunOn` and its defaults,
|
|
73
73
|
and how a paginated `<List.Items>` re-reads its slices without disappearing,
|
|
74
74
|
including on its way back from a screen that unmounted it.
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
It also says who decides a re-read on the way back — the source the list
|
|
76
|
+
reads through, never the navigation and never a route transition.
|
|
77
|
+
Read it before adding verbs to `rerunOn`, hiding a list on `loading`,
|
|
78
|
+
remounting a list with a `key` to refresh it, or reporting that a page
|
|
79
|
+
stopped refreshing since a transition was defined on its pair.
|
|
77
80
|
- `docs/error_handling.md` — the two kinds of error and how navi keeps them
|
|
78
81
|
apart: where an error is shown depending on where it came from (a control's
|
|
79
82
|
action shows it on what was clicked, a route action replaces the page, a
|
|
@@ -162,14 +165,17 @@ consistency across the app, not from any single call site.
|
|
|
162
165
|
- `docs/route_transitions.md` — how pages move against each other on
|
|
163
166
|
navigation (`defineRouteTransition`): a transition states a relation the
|
|
164
167
|
user reads as a map, which movement fits which relation, when a global
|
|
165
|
-
default is right,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
default is right, how one link or one `navTo` overrides what the pair says
|
|
169
|
+
for the length of a single navigation (`<Link routeTransition>`), marking the page
|
|
170
|
+
area between fixed bars, and why a pair of routes is animated by
|
|
171
|
+
`RouteTravel` or by a route transition but never both. Read it before
|
|
172
|
+
animating any navigation.
|
|
168
173
|
- `docs/navigation.md` — how to build navigation: declaring routes
|
|
169
174
|
(`route()` / `setupRoutes()`), when a section is a route of its own rather
|
|
170
175
|
than a param, search params bound to signals, rendering with `<Route>`,
|
|
171
|
-
tab rows (`Nav` / `Link` / `RouteTravel`),
|
|
172
|
-
|
|
176
|
+
tab rows (`Nav` / `Link` / `RouteTravel`), where a navigation lands
|
|
177
|
+
(scroll: a push arrives at the top, a back or forward lands where the page
|
|
178
|
+
was left), and the few cases where tabs are legitimately not URLs. Read it before writing any routing code — the
|
|
173
179
|
position of the user belongs in the URL by default, and that decision is
|
|
174
180
|
not retrofittable.
|
|
175
181
|
- Source code and demos on GitHub:
|
package/docs/list_refresh.md
CHANGED
|
@@ -132,8 +132,9 @@ stale copy of anything.
|
|
|
132
132
|
A run that finds a composition takes the `refreshing` line of the table above
|
|
133
133
|
rather than the loading one: the rows are on screen while it asks again for the
|
|
134
134
|
window it draws. So the two lists an app cannot tell apart from the outside —
|
|
135
|
-
one reading `GET_MANY`, one reading `GET_RANGE` —
|
|
136
|
-
|
|
135
|
+
one reading `GET_MANY`, one reading `GET_RANGE` — look the same on the way back:
|
|
136
|
+
neither blanks, neither shows a first load. What they do behind that is not the
|
|
137
|
+
same, and the next section is about exactly that.
|
|
137
138
|
|
|
138
139
|
What a composition is about is the **values** its params hold, not the reader
|
|
139
140
|
instance: `GET_RANGE.bindParams({ scope: "thread" })` called from two places
|
|
@@ -145,6 +146,89 @@ The rest follows the rules already stated: a verb in `rerunOn.GET_RANGE`, or
|
|
|
145
146
|
gone — and `memoryBudget` (1000 ranks by default) trims the ranks far from any
|
|
146
147
|
window, which are asked for again if the user goes back to them.
|
|
147
148
|
|
|
149
|
+
### Who decides the re-read — and who does not
|
|
150
|
+
|
|
151
|
+
The navigation decides nothing. Neither the back button, nor the movement
|
|
152
|
+
playing between the two pages: the **source** the list reads through is what
|
|
153
|
+
answers, and it answers the same way whether the user arrived by a link, by
|
|
154
|
+
`history.back()`, or under a route transition.
|
|
155
|
+
|
|
156
|
+
| The list reads through | Coming back to it |
|
|
157
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `routeAction` over `GET_MANY` | **nothing goes out** — the action holds its response, and `.run()` on a `COMPLETED` action is a no-op |
|
|
159
|
+
| `<List.Items>` over `GET_RANGE` | **one ask goes out** — the reader kept ranks, not rows, and revalidates the window it draws |
|
|
160
|
+
|
|
161
|
+
Both are deliberate, and they are not in tension: an action that kept its answer
|
|
162
|
+
has the answer, while a composition is a claim about an order that any write
|
|
163
|
+
elsewhere may have made false. What a `GET_MANY` list wants on the way back it
|
|
164
|
+
has to say itself — `.rerun()` when the route becomes current again, or a verb
|
|
165
|
+
in `rerunOn` if a write is what makes it stale.
|
|
166
|
+
|
|
167
|
+
Nothing above changes when `defineRouteTransition` is written for the pair the
|
|
168
|
+
list is walked through. A transition states a relation between two pages (see
|
|
169
|
+
[route_transitions.md](./route_transitions.md)); it takes the document's
|
|
170
|
+
rendering hold for the one frame the browser needs to photograph it, and gives
|
|
171
|
+
it back. It never decides what the page arriving is allowed to ask for. Held by
|
|
172
|
+
`tests/route_transition_list_revisit/`, which mounts the same app twice — with
|
|
173
|
+
and without a relation on the pair — and walks the way back ten times on each,
|
|
174
|
+
counting what goes out at every revisit.
|
|
175
|
+
|
|
176
|
+
So the first thing to check, when a list stops refreshing after a transition was
|
|
177
|
+
added, is which row of the table above it is on: a `GET_MANY` list never
|
|
178
|
+
refreshed on its own, and what changed is whatever else in the app was doing the
|
|
179
|
+
re-read.
|
|
180
|
+
|
|
181
|
+
### Watching what the run asks for
|
|
182
|
+
|
|
183
|
+
A run that decides **not** to ask is invisible from the application's side: it
|
|
184
|
+
sends nothing and changes no state, so the network is silent and
|
|
185
|
+
`onRequestStateChange` — which reports what a request is doing — has no request
|
|
186
|
+
to report. A run that declined and a run that was never mounted look identical.
|
|
187
|
+
`debugScroll` is where that difference is visible; it carries the render window
|
|
188
|
+
and the run's asking, which are one subject.
|
|
189
|
+
|
|
190
|
+
```jsx
|
|
191
|
+
window.askLog = [];
|
|
192
|
+
<NaviDebug debugScroll={(...args) => window.askLog.push(args.join(" "))}>
|
|
193
|
+
<MyList />
|
|
194
|
+
</NaviDebug>;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
One line per pass of the run, whatever the outcome:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
ask 0-49: sent (revalidating=true holdPending=false count=412)
|
|
201
|
+
ask -1--1: nothing missing (revalidating=false holdPending=false count=412)
|
|
202
|
+
ask 0-49: held on a row not reached yet (revalidating=true holdPending=true count=412)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
| outcome | what it means |
|
|
206
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------------------- |
|
|
207
|
+
| `sent` | the range on the line went out |
|
|
208
|
+
| `nothing missing` | the run holds every row it draws and has nothing to revalidate |
|
|
209
|
+
| `held on a row not reached yet` | the list is on its way somewhere the window does not frame; only the row it is held on can be asked for |
|
|
210
|
+
| `already revalidating` | a revalidation for this window is in flight |
|
|
211
|
+
| `a request still covers this window` | what is in flight is still what the list would draw |
|
|
212
|
+
| `this range was asked for already` | asking again could only produce the same answer |
|
|
213
|
+
|
|
214
|
+
The state that decides is on the line rather than left to be inferred:
|
|
215
|
+
|
|
216
|
+
- **`revalidating`** — the run knows what it holds is from before. A revisit
|
|
217
|
+
showing `revalidating=false` never restored a composition, which is a
|
|
218
|
+
different problem from one showing `revalidating=true` and no `sent`.
|
|
219
|
+
- **`holdPending`** — the list is held somewhere it has not reached.
|
|
220
|
+
- **`count`** — how many rows the run stands for, `undefined` before its first
|
|
221
|
+
answer.
|
|
222
|
+
|
|
223
|
+
**Record, do not print.** The sink is called during rendering: push into an
|
|
224
|
+
array. Formatting an object in a console costs far more than what it measures,
|
|
225
|
+
and a timing-sensitive symptom moves under one — a list that fails to refresh on
|
|
226
|
+
the first revisit can start refreshing on the first two as soon as a
|
|
227
|
+
`console.log` is in the way, which makes the log a report about the log.
|
|
228
|
+
|
|
229
|
+
An absence in the trace is a fact too: no line for a revisit means the run never
|
|
230
|
+
rendered, which is about the list being mounted, not about what it asked for.
|
|
231
|
+
|
|
148
232
|
## `rerunOn`, verb by verb
|
|
149
233
|
|
|
150
234
|
`rerunOn` says which verbs invalidate this resource's `GET` / `GET_MANY` /
|
package/docs/navigation.md
CHANGED
|
@@ -294,6 +294,42 @@ settings page rising over whatever showed it — are animated with
|
|
|
294
294
|
`defineRouteTransition` instead, and a given pair must be animated by one of
|
|
295
295
|
the two, never both: see [route_transitions.md](./route_transitions.md).
|
|
296
296
|
|
|
297
|
+
## Where a navigation lands: the scroll
|
|
298
|
+
|
|
299
|
+
Three cases, and they are not a policy to configure but three different facts:
|
|
300
|
+
|
|
301
|
+
- **Going somewhere new** (a `<Link>`, anything that pushes) lands at the top.
|
|
302
|
+
It is an arrival: the offset one had elsewhere means nothing here, and left
|
|
303
|
+
alone the new entry would be born holding the previous page's offset — which
|
|
304
|
+
the browser would then hand back as if it were this page's own.
|
|
305
|
+
- **Going back or forward** (the browser's buttons, `navBack()`,
|
|
306
|
+
`history.back()`) lands where that page was left. navi keeps the position and
|
|
307
|
+
puts it back once the page is really rendered, which is what the browser
|
|
308
|
+
cannot do: it restores at the instant the entry changes, when the document
|
|
309
|
+
still holds the page being left, so anything further down than that page is
|
|
310
|
+
tall is clamped away.
|
|
311
|
+
- **A reload** lands where one was, as it would have without navi.
|
|
312
|
+
|
|
313
|
+
One consequence is softened where the browser exposes its stack (the
|
|
314
|
+
Navigation API — everywhere but Firefox today): **a `<Link>` whose destination
|
|
315
|
+
is the entry right next to the current one becomes a real traversal** instead
|
|
316
|
+
of a push. A "back" link to the page one just came from therefore behaves as a
|
|
317
|
+
back — the stack stays what the reader thinks it is (no A, B, A, B… growth)
|
|
318
|
+
and the scroll comes back; one step forward too, so returning to the page one
|
|
319
|
+
just left resumes it where it was. Only towards entries of this document (a
|
|
320
|
+
traversal to another one would be a full page load no link asked for), and
|
|
321
|
+
never when the push carries explicit state.
|
|
322
|
+
|
|
323
|
+
Everywhere else a `<Link>` is an arrival and lands at the top. A back arrow
|
|
324
|
+
that must ALWAYS behave as a back — even far from the entry it targets, even
|
|
325
|
+
in a browser with no Navigation API — is `navBack()`. Where there may be
|
|
326
|
+
nothing to go back to (a shared link opened cold), decide what the arrow does
|
|
327
|
+
from the history, not from the link.
|
|
328
|
+
|
|
329
|
+
What is not covered: a page whose height depends on something still loading is
|
|
330
|
+
not tall enough at the moment its position is put back, so a deep position is
|
|
331
|
+
clamped as it was before. Only the page knows when it is whole.
|
|
332
|
+
|
|
297
333
|
## Creating something, then editing it
|
|
298
334
|
|
|
299
335
|
The create screen, the page of what was created, the edit screen — three routes,
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# Route transitions
|
|
2
2
|
|
|
3
3
|
How pages of an app move against each other when the user navigates —
|
|
4
|
-
`defineRouteTransition`, `defineRouteDefaultTransition`,
|
|
5
|
-
|
|
4
|
+
`defineRouteTransition`, `defineRouteDefaultTransition`, what one link or one
|
|
5
|
+
`navTo` may ask for on top of them, and the thinking that decides which
|
|
6
|
+
movement (if any) a navigation deserves. The API grammar itself
|
|
6
7
|
(accepted forms, shipped type names) lives in the JSDoc of
|
|
7
8
|
`defineRouteTransition`; this file holds what a signature cannot say.
|
|
8
9
|
|
|
9
10
|
Demos: [the movements](../src/nav/demos/route_transition/route_transition.html),
|
|
10
11
|
[a default transition](../src/nav/demos/route_transition/route_transition_default.html),
|
|
11
|
-
[pages between fixed bars](../src/nav/demos/
|
|
12
|
+
[pages between fixed bars](../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html),
|
|
12
13
|
[with a RouteTravel inside](../src/nav/demos/route_transition/route_transition_with_travel.html)
|
|
13
14
|
|
|
14
15
|
## What a transition is for
|
|
@@ -77,6 +78,77 @@ A default has no direction (nothing says which of two arbitrary pages is
|
|
|
77
78
|
"before" the other), so only directionless movements make sense there. Written
|
|
78
79
|
relations, and `"none"`, always win over it.
|
|
79
80
|
|
|
81
|
+
## When one navigation knows better
|
|
82
|
+
|
|
83
|
+
A relation is written on a PAIR, so it holds for every way of reaching the
|
|
84
|
+
page — and some ways are walked against the map. Two pairs that are travelled
|
|
85
|
+
in both directions, with one direction common and one rare:
|
|
86
|
+
|
|
87
|
+
- a game ↔ a player's profile: the name of a player, tapped from the game, is
|
|
88
|
+
the common way in; a badge on a profile that leads back to the game it was
|
|
89
|
+
won in is the rare one;
|
|
90
|
+
- a profile ↔ the cards it describes: going down into the cards is the
|
|
91
|
+
structural descent; a card that leads up to the player it describes goes back
|
|
92
|
+
out.
|
|
93
|
+
|
|
94
|
+
Written for the common direction, the rare one plays backwards. And `"none"`
|
|
95
|
+
cannot fix it: navi does not tell a link from the back button, so silencing the
|
|
96
|
+
bad direction silences the good one too.
|
|
97
|
+
|
|
98
|
+
So the navigation itself may ask, and what it asks holds for **that navigation
|
|
99
|
+
and no other**:
|
|
100
|
+
|
|
101
|
+
```jsx
|
|
102
|
+
// The rare way round: the pair's movement, turned round.
|
|
103
|
+
<Link
|
|
104
|
+
route={GAME_ROUTE}
|
|
105
|
+
routeParams={{ id }}
|
|
106
|
+
transition={{ direction: "back" }}
|
|
107
|
+
>
|
|
108
|
+
{badge.gameName}
|
|
109
|
+
</Link>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
// The same thing said by a call rather than by an element.
|
|
114
|
+
navTo(GAME_ROUTE.buildUrl({ id }), { routeTransition: { direction: "back" } });
|
|
115
|
+
GAME_ROUTE.navTo({ id }, { routeTransition: { direction: "back" } });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The request is `"slide-x"`-style shorthand or `{ type, duration, direction }`,
|
|
119
|
+
the same forms `defineRouteTransition` takes, plus `direction`. It overrides
|
|
120
|
+
**field by field**: what it does not name, the relation (or the default) still
|
|
121
|
+
answers for. So:
|
|
122
|
+
|
|
123
|
+
- `{ direction: "back" }` keeps the pair's movement and only turns it round;
|
|
124
|
+
- `"zoom"` swaps the movement, keeping nothing else;
|
|
125
|
+
- `"none"` cuts, where the pair — or the default — would have played;
|
|
126
|
+
- `{ duration: 500 }` re-times what was already going to play.
|
|
127
|
+
|
|
128
|
+
A pair no relation was ever written for answers the same way: silence is what
|
|
129
|
+
the routes say, and a link that asks for a movement gets it, forward unless it
|
|
130
|
+
says otherwise. That is the whole shape of the control:
|
|
131
|
+
`defineRouteTransition` is what the app's map says and applies by default; a
|
|
132
|
+
link, or a programmatic `navTo`, overrides it for the length of one navigation.
|
|
133
|
+
Navigate again by any other means and the relation is back in charge — nothing
|
|
134
|
+
is remembered.
|
|
135
|
+
|
|
136
|
+
The link wears what it asks as an attribute, so a plain `<a>` says it too (a
|
|
137
|
+
type name, or the object as JSON):
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<a href="/game/42" data-navi-route-transition-request='{"direction":"back"}'
|
|
141
|
+
>…</a
|
|
142
|
+
>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Not yet: a movement chosen by HOW one navigated.** Playing one movement for
|
|
146
|
+
the back button and another for a link would be written on the same request —
|
|
147
|
+
`{ back: "slide-x", forward: "slide-y" }` — but the History API does not say
|
|
148
|
+
which way a traversal went, and navi runs on it today (see
|
|
149
|
+
`browser_integration/via_history.js`). The Navigation API does; the notation is
|
|
150
|
+
kept in mind for the day navi navigates through it.
|
|
151
|
+
|
|
80
152
|
## Pages between fixed bars: the transition area
|
|
81
153
|
|
|
82
154
|
By default the movement plays on the document itself — right when pages are the
|
|
@@ -110,6 +182,12 @@ through, not the page's own size: a page is as tall as its content, and a
|
|
|
110
182
|
vertical movement measured on the picture would send it several screens away —
|
|
111
183
|
off screen for most of the transition, flying past at the end.
|
|
112
184
|
|
|
185
|
+
A page that was **scrolled** is photographed where the reader was, and travels
|
|
186
|
+
from there: the document is not put back to its top until the picture has been
|
|
187
|
+
taken. Without that wait the picture keeps only the band the browser had
|
|
188
|
+
already painted at the new offset, and the movement carries a fragment of the
|
|
189
|
+
page instead of the page.
|
|
190
|
+
|
|
113
191
|
With an area marked, the page around it is left LIVE rather than photographed:
|
|
114
192
|
the bars keep answering the pointer for the whole movement, which a captured
|
|
115
193
|
element cannot do. The flip side is that anything around the area which must
|
|
@@ -145,6 +223,20 @@ movement against the view transition pseudo-elements — of the document, or of
|
|
|
145
223
|
the marked area. See the JSDoc of `defineRouteTransition` for the selector
|
|
146
224
|
shape, and the `spin` type in the demo for a working one.
|
|
147
225
|
|
|
226
|
+
What is left to write is the `animation-name`s and nothing else: navi attaches
|
|
227
|
+
to ANY named type what makes a movement look like one — each picture at the
|
|
228
|
+
size it was taken at (a page half the height of the one it crosses would
|
|
229
|
+
otherwise be seen inflating over the length of the movement), two solid pages
|
|
230
|
+
rather than two panes of glass, and the animation held where it ends. The
|
|
231
|
+
untyped cross-fade keeps the browser's defaults, since scaling one picture into
|
|
232
|
+
the other is the whole idea there.
|
|
233
|
+
|
|
234
|
+
The one knob a custom movement may want back: a movement that animates ONE of
|
|
235
|
+
its two sides leaves the other on the browser's fade, and a fade needs its two
|
|
236
|
+
half-transparent pictures to add up rather than cover each other
|
|
237
|
+
(`mix-blend-mode: plus-lighter`, as the shipped `zoom` and the demo's `spin`
|
|
238
|
+
both do). A movement where both pages move wants what navi poses.
|
|
239
|
+
|
|
148
240
|
## Route transitions and `RouteTravel` — one pair, one system
|
|
149
241
|
|
|
150
242
|
`RouteTravel` and `defineRouteTransition` answer different questions:
|
|
@@ -176,6 +268,37 @@ animates. A `<RouteTravel>` rendered inside the `element` of each of the routes
|
|
|
176
268
|
it travels between is destroyed mid-travel by the router. Give the row a single
|
|
177
269
|
branch — its tabs as params of one route is the usual shape.
|
|
178
270
|
|
|
271
|
+
## A transition says nothing about data
|
|
272
|
+
|
|
273
|
+
A relation is about the map of the app, and about nothing else. Defining one
|
|
274
|
+
does not change what the page arriving loads, reloads, or keeps:
|
|
275
|
+
|
|
276
|
+
- an action that `COMPLETED` still holds its response, on the way back as on the
|
|
277
|
+
way in — a page wanting fresh data says `.rerun()`, with or without a
|
|
278
|
+
movement;
|
|
279
|
+
- a `<List.Items>` reading through `GET_RANGE` still revalidates the window it
|
|
280
|
+
draws when it is mounted again.
|
|
281
|
+
|
|
282
|
+
What a transition takes is the document's **rendering** for the one frame the
|
|
283
|
+
browser needs to photograph the page being left (see `rendering_hold.js`), and
|
|
284
|
+
it gives it back in the same callback. The hold is about a picture, not about
|
|
285
|
+
data: nothing waits on it, nothing is skipped because of it.
|
|
286
|
+
|
|
287
|
+
That is the design, and it is held by `tests/route_transition_list_revisit/`:
|
|
288
|
+
one app mounted twice, the two mounts differing by a single
|
|
289
|
+
`defineRouteTransition` line, walked back and forth **ten times each** with what
|
|
290
|
+
goes to the network counted on every revisit. The loop is the point — a
|
|
291
|
+
difference that came and went would pass a single comparison often enough to
|
|
292
|
+
look like an invariant.
|
|
293
|
+
|
|
294
|
+
What the test holds is that shape: pages between fixed bars, a marked area, a
|
|
295
|
+
list virtualized against its scroller, held on a row the url names. It is
|
|
296
|
+
evidence about the mechanism, not a proof about every application; a page that
|
|
297
|
+
loses its revisit only under a movement is worth reporting with the loop above
|
|
298
|
+
run against it. See
|
|
299
|
+
[list_refresh.md](./list_refresh.md#who-decides-the-re-read--and-who-does-not)
|
|
300
|
+
for which source refreshes on a revisit and which does not.
|
|
301
|
+
|
|
179
302
|
## The rest, briefly
|
|
180
303
|
|
|
181
304
|
- Pace: `--navi-route-transition-duration` (CSS, default 300ms) for everyone;
|