@jsenv/navi 0.29.165 → 0.29.167
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 +604 -104
- package/dist/jsenv_navi.js.map +14 -10
- package/docs/AI_INSTRUCTIONS.md +19 -3
- package/docs/actions.md +6 -4
- package/docs/data_states.md +147 -0
- package/docs/error_handling.md +9 -0
- package/docs/list_refresh.md +16 -1
- package/docs/navigation.md +167 -0
- package/docs/popup_open.md +4 -1
- package/docs/route_transitions.md +8 -0
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -69,6 +69,17 @@ consistency across the app, not from any single call site.
|
|
|
69
69
|
collection (searching it is that collection's own `GET_MANY`). Companion
|
|
70
70
|
files: `docs/actions.md`, `docs/resource_with_params.md`,
|
|
71
71
|
`docs/resource_dependencies.md`.
|
|
72
|
+
- `docs/data_states.md` — what a screen draws while its data is loading,
|
|
73
|
+
missing, stale or failed: the three questions `data`, `loading` and `error`
|
|
74
|
+
answer separately, the four combinations of the first two and what each one
|
|
75
|
+
means, and the invariant that `loading: true` never suspends — so a component
|
|
76
|
+
drawing its own states is never taken away, with the dialog, the scroll
|
|
77
|
+
position and the retry button it was holding. Also why a skeleton is TOLD
|
|
78
|
+
whether it is loading rather than deducing it from emptiness, why an error
|
|
79
|
+
arrives beside the data rather than in place of it, and what `dismissError()`
|
|
80
|
+
does and does not do. Read it before drawing a skeleton on `!data`, before
|
|
81
|
+
hiding content because `error` is set, and before adding a state flag of your
|
|
82
|
+
own next to what the hook already returns.
|
|
72
83
|
- `docs/list_refresh.md` — what a write sends back to the network and what stays
|
|
73
84
|
on screen meanwhile: stale data returned by `useAsyncData({ loading: true })`,
|
|
74
85
|
what updates from a response without any request, `rerunOn` and its defaults,
|
|
@@ -331,9 +342,14 @@ consistency across the app, not from any single call site.
|
|
|
331
342
|
restored on reload without a route and without an entry per step: a search
|
|
332
343
|
param signal handed to a `SlideContainer`, walked to rather than jumped to,
|
|
333
344
|
and `history: "push"` for the states whose values ARE places (with the write
|
|
334
|
-
that says otherwise, `signal.set(v, { history })`).
|
|
335
|
-
|
|
336
|
-
|
|
345
|
+
that says otherwise, `signal.set(v, { history })`). It also holds the rule for
|
|
346
|
+
an OVERLAY — something opened from every screen and drawn over whatever the
|
|
347
|
+
reader was on: the URL may say what is drawn over the screen, it must never
|
|
348
|
+
name a place the reader is not at, which is why `/me/settings` is wrong for it
|
|
349
|
+
and `/places?settings` is right (the layer's address contains the address of
|
|
350
|
+
what it covers, so closing is exact even from a shared link). Read it before
|
|
351
|
+
writing any routing code — the position of the user belongs in the URL by
|
|
352
|
+
default, and that decision is not retrofittable.
|
|
337
353
|
- Source code and demos on GitHub:
|
|
338
354
|
https://github.com/jsenv/core/tree/main/packages/frontend/navi — where to go
|
|
339
355
|
when the built export's JSDoc doesn't answer the question (see "Where the
|
package/docs/actions.md
CHANGED
|
@@ -130,10 +130,12 @@ const [user] = useAsyncData(userAction);
|
|
|
130
130
|
|
|
131
131
|
`useAsyncData` suspends until the data is there and throws on failure, leaving
|
|
132
132
|
both to the nearest `<Loading>` and `<ErrorBoundary>`; pass `{ loading: true }`
|
|
133
|
-
or `{ error: true }` to
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
[
|
|
133
|
+
or `{ error: true }` to receive either as a value instead, the component staying
|
|
134
|
+
mounted throughout and keeping the data it already had. What it then draws for
|
|
135
|
+
each state — the four combinations of `data` and `loading`, why a skeleton is
|
|
136
|
+
told whether it is loading — is [data_states.md](./data_states.md). Where a
|
|
137
|
+
failure goes when no screen takes it — and why a run settles with its error
|
|
138
|
+
rather than rejecting — is [error_handling.md](./error_handling.md). `useActionStatus(action)` gives the whole
|
|
137
139
|
state at once — `{ idle, loading, completed, aborted, error, data, params }` —
|
|
138
140
|
for a component that needs to look at it rather than render it. It reads that
|
|
139
141
|
very instance: to know what moves when a control is handed the action, see
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# The states of a screen's data
|
|
2
|
+
|
|
3
|
+
A screen reading async data draws one of a handful of states, and there are two
|
|
4
|
+
ways to get it wrong. Both are common, and both come from the same shortcut.
|
|
5
|
+
|
|
6
|
+
- **A wait announced over something that is not coming.** A skeleton drawn on
|
|
7
|
+
"there is no data" alone keeps shimmering after the load failed, and after the
|
|
8
|
+
params turned out to be false. It says "hold on", and there is nothing to hold
|
|
9
|
+
on for.
|
|
10
|
+
- **Content taken away because something behind it went wrong.** A list that
|
|
11
|
+
disappears because its refresh failed; a page replaced by a message while the
|
|
12
|
+
rows it had are still perfectly good, and still true a second ago.
|
|
13
|
+
|
|
14
|
+
The shortcut behind both: collapsing "loading", "empty" and "failed" into one
|
|
15
|
+
state — _not ready_ — and drawing one thing for it. They are three separate
|
|
16
|
+
questions, and a screen that draws honestly answers them separately:
|
|
17
|
+
|
|
18
|
+
- **is there something to show?** → `data`
|
|
19
|
+
- **is anything on its way?** → `loading`
|
|
20
|
+
- **did the last attempt fail?** → `error`
|
|
21
|
+
|
|
22
|
+
navi's part is to answer all three, at every render, and to keep answering them.
|
|
23
|
+
What is drawn for each is the component's, always: navi has no opinion on
|
|
24
|
+
whether a missing list is a skeleton, a sentence or an illustration.
|
|
25
|
+
|
|
26
|
+
## Asking for a state means receiving it as a value
|
|
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
|
|
30
|
+
`<Loading>` and `<ErrorBoundary>` above rather than by the component.
|
|
31
|
+
|
|
32
|
+
Each option turns one of those from something done _to_ the component into a
|
|
33
|
+
value the component reads:
|
|
34
|
+
|
|
35
|
+
| passed | the wait | the failure |
|
|
36
|
+
| --------------- | ------------------------- | ---------------------------- |
|
|
37
|
+
| nothing | suspends into `<Loading>` | throws to `<ErrorBoundary>` |
|
|
38
|
+
| `loading: true` | `loading`, a boolean | throws |
|
|
39
|
+
| `error: true` | suspends | `error`, plus `dismissError` |
|
|
40
|
+
| both | `loading` | `error` |
|
|
41
|
+
|
|
42
|
+
The invariant that makes this worth relying on: **`loading: true` never
|
|
43
|
+
suspends.** Not while the action runs, not while a debounced binding is
|
|
44
|
+
settling, not while it holds params nobody has started yet, not after a failure
|
|
45
|
+
whose message was dismissed. Every one of those comes back as a value.
|
|
46
|
+
|
|
47
|
+
That is not a detail of the wait — it is what keeps the component alive.
|
|
48
|
+
Suspending does not draw a spinner over a subtree: it takes the subtree away and
|
|
49
|
+
puts the boundary's fallback in its place, and everything under it is remounted
|
|
50
|
+
when it comes back. A dialog open inside it closes, a scroll position is lost,
|
|
51
|
+
half-typed text is gone, and the retry button the component was offering
|
|
52
|
+
disappears along with the component. A component that said it draws its own
|
|
53
|
+
states is never taken away from the person looking at it.
|
|
54
|
+
|
|
55
|
+
`error: true` says the same thing about failure, and it has the same
|
|
56
|
+
consequence: the component stays, so it is still there to offer the way out.
|
|
57
|
+
|
|
58
|
+
A component that delegates nothing needs no `<Loading>` above it. One that
|
|
59
|
+
delegates its wait does, since that is where the suspension lands, and navi says
|
|
60
|
+
so rather than leaving the subtree blank.
|
|
61
|
+
|
|
62
|
+
## `data` and `loading` are independent
|
|
63
|
+
|
|
64
|
+
They answer different questions, so all four combinations happen and each one
|
|
65
|
+
means something different:
|
|
66
|
+
|
|
67
|
+
| `data` | `loading` | what it is | what a screen draws |
|
|
68
|
+
| ----------- | --------- | ----------------------------- | ------------------------ |
|
|
69
|
+
| `undefined` | `true` | first load | a skeleton, animated |
|
|
70
|
+
| set | `true` | a refresh over a known answer | the content, marked busy |
|
|
71
|
+
| set | `false` | settled | the content |
|
|
72
|
+
| `undefined` | `false` | nothing here, nothing coming | an empty state |
|
|
73
|
+
|
|
74
|
+
Two consequences worth stating out loud.
|
|
75
|
+
|
|
76
|
+
**The emptiness test is `data === undefined`, never `loading`.** Reading
|
|
77
|
+
`loading` as "there is nothing to display" blanks the screen on every refresh —
|
|
78
|
+
for a checkbox ticked on one row of a list, the whole list goes.
|
|
79
|
+
|
|
80
|
+
**A skeleton is told whether it is loading; it does not deduce it.** The last
|
|
81
|
+
row of the table is the one that gets forgotten, and it is reachable: a first
|
|
82
|
+
load that failed — its message on screen or dismissed — and a route action whose
|
|
83
|
+
params getter returns false. A skeleton drawn on emptiness alone shimmers there
|
|
84
|
+
forever, claiming a load nobody started.
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
const [items, loading] = useAsyncData(ACTION, { loading: true });
|
|
88
|
+
if (items === undefined) {
|
|
89
|
+
return <ItemsSkeleton loading={loading} />;
|
|
90
|
+
}
|
|
91
|
+
return <ItemList items={items} busy={loading} />;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
navi's own placeholders take that flag rather than inventing it: `<Text loading>`
|
|
95
|
+
shimmers, `<Text skeleton>` is the same bar held still, and `<Text skeleton
|
|
96
|
+
loading={loading}>` is one that tells the truth either way.
|
|
97
|
+
|
|
98
|
+
**Knowing the count beforehand beats guessing it.** How many skeleton rows to
|
|
99
|
+
draw is a question the response answers too late; a screen that already knows —
|
|
100
|
+
a count carried by the parent resource, a total returned alongside a previous
|
|
101
|
+
page — says so, and `<List loading loadingSkeletonCount={0}>` draws the empty
|
|
102
|
+
state right away instead of three rows that will vanish. Nothing jumps when the
|
|
103
|
+
response lands.
|
|
104
|
+
|
|
105
|
+
## An error is a message on the screen, not the screen
|
|
106
|
+
|
|
107
|
+
`error: true` hands the failure over **beside** the data the action already had,
|
|
108
|
+
not in place of it. A refresh that failed does not unmake the previous answer,
|
|
109
|
+
so a screen that had something to show keeps it and says the failure over it:
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
const [items, loading, error, dismissError] = useAsyncData(ACTION, {
|
|
113
|
+
loading: true,
|
|
114
|
+
error: true,
|
|
115
|
+
});
|
|
116
|
+
if (error && !items) {
|
|
117
|
+
return <ErrorScreen error={error} />; // nothing behind it: the message is the screen
|
|
118
|
+
}
|
|
119
|
+
return (
|
|
120
|
+
<>
|
|
121
|
+
{error && <ErrorStrip error={error} onClose={dismissError} />}
|
|
122
|
+
<ItemList items={items} busy={loading} />
|
|
123
|
+
</>
|
|
124
|
+
);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`dismissError()` removes the message and nothing else — nothing is asked again,
|
|
128
|
+
because nothing had to be: what closing it reveals was already there. What comes
|
|
129
|
+
back is the same state minus the error, which is why the two branches above stay
|
|
130
|
+
honest afterwards.
|
|
131
|
+
|
|
132
|
+
Taking the whole subtree away is `<ErrorBoundary>`'s job and it is a different
|
|
133
|
+
decision: reach for it when the failure means the screen genuinely cannot be
|
|
134
|
+
drawn, not as a stronger version of the same thing. Which of the two, and where
|
|
135
|
+
an error goes when neither takes it, is
|
|
136
|
+
[error_handling.md](./error_handling.md).
|
|
137
|
+
|
|
138
|
+
## Reference
|
|
139
|
+
|
|
140
|
+
- [error_handling.md](./error_handling.md) — the two kinds of error, where each
|
|
141
|
+
is shown, and what happens to one nobody displays.
|
|
142
|
+
- [list_refresh.md](./list_refresh.md) — the same states for a list, plus what a
|
|
143
|
+
write puts back on screen with no request at all.
|
|
144
|
+
- [actions.md](./actions.md#reading-an-action) — reading an action versus
|
|
145
|
+
running one, `{ run: true }`, `{ onLoad }`.
|
|
146
|
+
- `src/state/demos/use_async_data_demo.html` — one card per shape: delegated,
|
|
147
|
+
loading inline, both inline, owning its request.
|
package/docs/error_handling.md
CHANGED
|
@@ -140,6 +140,15 @@ const [game, loading, error, dismissError] = useAsyncData(gameAction, {
|
|
|
140
140
|
});
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
`game` and `error` arrive **together**: a refresh that failed does not unmake
|
|
144
|
+
the answer the action already had, so the screen keeps what it has and says the
|
|
145
|
+
failure on it, and `dismissError()` takes that message away without asking
|
|
146
|
+
anything again. That is the whole difference between the two shapes — a boundary
|
|
147
|
+
replaces the subtree by definition, this one writes on it — so reach for the
|
|
148
|
+
boundary when the failure means the screen cannot be drawn at all, and for this
|
|
149
|
+
when it means one thing about it is out of date. How the component draws the
|
|
150
|
+
states it is handed is [data_states.md](./data_states.md).
|
|
151
|
+
|
|
143
152
|
And one row rather than the whole list: `<List.Item error>` replaces the row's
|
|
144
153
|
content with the message — what the row stood for did not happen, so drawing it
|
|
145
154
|
as if it had would be a lie.
|
package/docs/list_refresh.md
CHANGED
|
@@ -27,13 +27,15 @@ The short answer:
|
|
|
27
27
|
| never completed yet | `undefined` | `true` |
|
|
28
28
|
| running after success | previous | `true` |
|
|
29
29
|
| completed | fresh | `false` |
|
|
30
|
+
| failed after success | previous | `false` |
|
|
31
|
+
| failed, error closed | `undefined` | `false` |
|
|
30
32
|
|
|
31
33
|
So the emptiness test is `data === undefined`, never `loading`:
|
|
32
34
|
|
|
33
35
|
```js
|
|
34
36
|
const [items, loading] = useAsyncData(ACTION, { loading: true });
|
|
35
37
|
if (items === undefined) {
|
|
36
|
-
return
|
|
38
|
+
return <RadarListSkeleton loading={loading} />; // nothing to show yet
|
|
37
39
|
}
|
|
38
40
|
return <RadarList radars={items} busy={loading} />; // re-read: stay on screen
|
|
39
41
|
```
|
|
@@ -48,10 +50,23 @@ if (loading) {
|
|
|
48
50
|
Read `loading` as "what you are displaying is from before", not "there is
|
|
49
51
|
nothing to display".
|
|
50
52
|
|
|
53
|
+
The two answer different questions — _is there anything to show_ and _is
|
|
54
|
+
anything on its way_ — so a screen reads both, and a skeleton is given `loading`
|
|
55
|
+
rather than deducing it from emptiness. The four combinations, and what each one
|
|
56
|
+
draws, are in [data_states.md](./data_states.md).
|
|
57
|
+
|
|
51
58
|
`<List loading>` is the first-load answer, not the refresh one: it replaces the
|
|
52
59
|
rows with skeletons. Pass it while stale rows exist and they disappear — same
|
|
53
60
|
mistake as `loading ? null :`, one level down.
|
|
54
61
|
|
|
62
|
+
The `failed after success` row is the same rule at the other end: a refresh that
|
|
63
|
+
failed does not unmake the rows either. With `error: true` the failure comes
|
|
64
|
+
back **beside** them, so the list stays and the failure is said over it — a
|
|
65
|
+
strip above the rows, a retry, `dismissError()` to close the strip without
|
|
66
|
+
asking anything again. Taking the rows away is `<ErrorBoundary>`'s job, and only
|
|
67
|
+
when the screen genuinely cannot be drawn — see
|
|
68
|
+
[data_states.md](./data_states.md).
|
|
69
|
+
|
|
55
70
|
## What updates without a request
|
|
56
71
|
|
|
57
72
|
An action's `data` is a computation over the resource store, not a snapshot of
|
package/docs/navigation.md
CHANGED
|
@@ -19,6 +19,14 @@ linking to them, and turning them into tabs.
|
|
|
19
19
|
- [Tabs that are not routes](#tabs-that-are-not-routes)
|
|
20
20
|
- [A `SlideContainer` in the URL: a position that is not a place one came from](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
|
|
21
21
|
- [A state whose values ARE places: `history: "push"`](#a-state-whose-values-are-places-history-push)
|
|
22
|
+
- [A layer over the screen: what its address may say](#a-layer-over-the-screen-what-its-address-may-say)
|
|
23
|
+
- [Why a layer is a routing question at all](#why-a-layer-is-a-routing-question-at-all)
|
|
24
|
+
- [`/me/settings` names a place the reader is not at](#mesettings-names-a-place-the-reader-is-not-at)
|
|
25
|
+
- [It is not "use a search param"](#it-is-not-use-a-search-param)
|
|
26
|
+
- [What the search param buys: the way back is in the address](#what-the-search-param-buys-the-way-back-is-in-the-address)
|
|
27
|
+
- [The wiring](#the-wiring)
|
|
28
|
+
- [Places inside the layer](#places-inside-the-layer)
|
|
29
|
+
- [What it costs](#what-it-costs)
|
|
22
30
|
|
|
23
31
|
## The rule that decides everything else: the position belongs in the URL
|
|
24
32
|
|
|
@@ -747,3 +755,162 @@ not one. What travels is the box, and the address is a label on where the box
|
|
|
747
755
|
stands. A position several parts of the app must react to is still a route.
|
|
748
756
|
|
|
749
757
|
Demo: [../src/layout/demos/8_slide_container_demo.html](../src/layout/demos/8_slide_container_demo.html).
|
|
758
|
+
|
|
759
|
+
## A layer over the screen: what its address may say
|
|
760
|
+
|
|
761
|
+
Some things are opened from everywhere and drawn OVER whatever the reader was
|
|
762
|
+
looking at: the settings behind a gear in the top bar, a photo, a help sheet.
|
|
763
|
+
Closing one puts the reader back exactly where they were — not on a page the app
|
|
764
|
+
picked, on the one they had. And the address has to hold it, for the same reasons
|
|
765
|
+
every other position does: a reload lands on it, a link opens on it, the back
|
|
766
|
+
button closes it.
|
|
767
|
+
|
|
768
|
+
### Why a layer is a routing question at all
|
|
769
|
+
|
|
770
|
+
Layout should not decide routing. But routing decides a great deal of what is on
|
|
771
|
+
screen: which page is mounted, whose data loads, which bar entry lights up, which
|
|
772
|
+
movement plays between two screens. An address is not a label — it is read by
|
|
773
|
+
everything — so a layer, which is a pure fact of layout, cannot be given just any
|
|
774
|
+
address.
|
|
775
|
+
|
|
776
|
+
The rule that falls out of it, and the whole of this section:
|
|
777
|
+
|
|
778
|
+
> The URL may say what is drawn OVER the screen. It must never name a place the
|
|
779
|
+
> reader is not at.
|
|
780
|
+
|
|
781
|
+
### `/me/settings` names a place the reader is not at
|
|
782
|
+
|
|
783
|
+
Settings opened from anywhere and drawn over anything are not inside `/me`, even
|
|
784
|
+
when they conceptually belong to "me". Written `/me/settings`, the router
|
|
785
|
+
believes otherwise, and everything that reads the router inherits the belief:
|
|
786
|
+
|
|
787
|
+
- the bar entry for the "me" section lights up while the settings cover a game;
|
|
788
|
+
- `/me` matches, so the screen the reader was on is REPLACED. The covering is a
|
|
789
|
+
fiction that lasts as long as the animation, and the page beneath is gone;
|
|
790
|
+
- a route transition plays a crossing between two pages that never crossed, and
|
|
791
|
+
its back half plays on every way out — the sheet lifts to reveal a page that
|
|
792
|
+
was never underneath;
|
|
793
|
+
- and it is wrong on the way out too: closing does not return to `/me`, it
|
|
794
|
+
returns to wherever the reader was, which the address never said.
|
|
795
|
+
|
|
796
|
+
These do not get fixed one at a time. `currentExcept` stops one link from
|
|
797
|
+
claiming "you are here"; a `"none"` relation silences one crossing; a hand-rolled
|
|
798
|
+
`active` replaces one reading. Each patches a reader of the lie without removing
|
|
799
|
+
it, and the next reader arrives quietly wrong.
|
|
800
|
+
|
|
801
|
+
### It is not "use a search param"
|
|
802
|
+
|
|
803
|
+
`/settings` at the top level does not claim to be inside `/me` — it is honest
|
|
804
|
+
about not being there, and every symptom above goes with it. What it still is,
|
|
805
|
+
though, is a PAGE: it names a screen, so the screen beneath is gone, and closing
|
|
806
|
+
has nowhere written to return to. `whatever/settings` is the same answer in a
|
|
807
|
+
longer word.
|
|
808
|
+
|
|
809
|
+
So the rule is not about search params. It is about not claiming a position. A
|
|
810
|
+
path can be honest; what a path cannot do is carry what is underneath.
|
|
811
|
+
|
|
812
|
+
### What the search param buys: the way back is in the address
|
|
813
|
+
|
|
814
|
+
`/places?settings` says two things at once — which screen the reader is on, and
|
|
815
|
+
what is drawn over it. That is what makes closing exact from a cold start: a
|
|
816
|
+
reload, a link someone sent, a new tab. The layer's address CONTAINS the address
|
|
817
|
+
of what it covers, so "close" needs no memory at all.
|
|
818
|
+
|
|
819
|
+
`/settings` has to remember instead. In practice the browser often does — after a
|
|
820
|
+
reload the session's history is still there, so a back-press lands somewhere —
|
|
821
|
+
but that is the BROWSER's memory of this tab, not the address. A link someone
|
|
822
|
+
else opens has none of it, and neither does a bookmark. Only the address travels,
|
|
823
|
+
which is what makes the param shape the right model rather than the convenient
|
|
824
|
+
one.
|
|
825
|
+
|
|
826
|
+
### The wiring
|
|
827
|
+
|
|
828
|
+
```js
|
|
829
|
+
const settingsSignal = stateSignal(false, {
|
|
830
|
+
id: "settings",
|
|
831
|
+
type: "boolean",
|
|
832
|
+
weak: true,
|
|
833
|
+
history: "push",
|
|
834
|
+
});
|
|
835
|
+
export const ROOT_ROUTE = route("/", {
|
|
836
|
+
searchParams: { settings: settingsSignal },
|
|
837
|
+
});
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
```jsx
|
|
841
|
+
<Button command="--navi-toggle" commandFor="settings_panel">⚙</Button>
|
|
842
|
+
…
|
|
843
|
+
<SidePanel id="settings_panel" signal={settingsSignal} side="top" expandY />
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
Every piece of it answers something:
|
|
847
|
+
|
|
848
|
+
- **declared on the ROOT route**, so the layer opens over every screen. Declared
|
|
849
|
+
on one route it would only exist there, and a door in the furniture is on every
|
|
850
|
+
screen ([Search params](#search-params));
|
|
851
|
+
- **`weak`**, so a link built to a page never inherits a layer that happens to be
|
|
852
|
+
open. A layer qualifies one visit; it is not part of anyone's address;
|
|
853
|
+
- **`history: "push"`**, so the layer is a place one came from and the back button
|
|
854
|
+
closes it. The closing itself is never an entry of its own — see
|
|
855
|
+
[popup_open.md](./popup_open.md), which owns what a `signal` bound to a URL
|
|
856
|
+
writes on open, on close, and on cancel;
|
|
857
|
+
- **the panel is rendered outside the page area**, next to the router rather than
|
|
858
|
+
inside it. It is drawn over the pages and the fixed bars alike, and it stays
|
|
859
|
+
mounted while closed so it has somewhere to animate from;
|
|
860
|
+
- **`expandY`**, because a sheet is content-tall by default and a layer covers
|
|
861
|
+
the screen. It is also what any tab row inside it must NOT be given: `expand`
|
|
862
|
+
is both axes, and in the panel's own column that makes the row eat the height
|
|
863
|
+
and push everything below it off screen;
|
|
864
|
+
- **no `defineRouteTransition` for it.** Nothing crosses: the page beneath does
|
|
865
|
+
not change, so there is no pair of pages and no movement between them. A layer
|
|
866
|
+
has an entrance of its own, and route transitions are for pages replacing pages
|
|
867
|
+
([route_transitions.md](./route_transitions.md)).
|
|
868
|
+
|
|
869
|
+
### Places inside the layer
|
|
870
|
+
|
|
871
|
+
A layer big enough to have tabs holds them the way a wizard does — a search param
|
|
872
|
+
of its own, `oneOf` its pages, replacing rather than pushing, so the back button
|
|
873
|
+
closes the layer instead of stepping back one tab. Everything in
|
|
874
|
+
[A `SlideContainer` in the URL](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
|
|
875
|
+
applies unchanged, and a layer is exactly the case that section names for
|
|
876
|
+
`SlideContainer` over `RouteTravel`: a popup lives in the browser's top layer,
|
|
877
|
+
where two pages cannot stand side by side.
|
|
878
|
+
|
|
879
|
+
```js
|
|
880
|
+
const settingsTabSignal = stateSignal("account", {
|
|
881
|
+
id: "settings_tab",
|
|
882
|
+
oneOf: ["account", "alerts", "advanced"],
|
|
883
|
+
weak: true,
|
|
884
|
+
});
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
The address then grows and shrinks with what the reader does: `/me` closed,
|
|
888
|
+
`/me?settings` open on the tab it opens on, `/me?settings&settings_tab=alerts`
|
|
889
|
+
one tab further, and back to `/me?settings` returning to the first.
|
|
890
|
+
|
|
891
|
+
Two params, because they answer two questions — is the layer there, and which of
|
|
892
|
+
its pages is shown. One param whose value is "which page of the layer, or none"
|
|
893
|
+
(`?settings=alerts`) is arguably the shape the address wants, and it is not
|
|
894
|
+
expressible today: a popup says its open state as a boolean, so it would
|
|
895
|
+
overwrite the tab on every open.
|
|
896
|
+
|
|
897
|
+
### What it costs
|
|
898
|
+
|
|
899
|
+
This is a compromise, and it is worth stating rather than discovering:
|
|
900
|
+
|
|
901
|
+
- **the page beneath stays mounted and alive under an opaque layer.** Its actions
|
|
902
|
+
keep running, refreshing, retrying, for something nobody can see. Nothing tells
|
|
903
|
+
a route "you are covered", and nothing should be inferred from a layer being
|
|
904
|
+
open — the reader is still on that page, and will be back on it in a moment;
|
|
905
|
+
- **the router knows nothing about layers.** `<Route>` renders pages; the layer is
|
|
906
|
+
drawn beside them by the application. The address is honest about it, but "over"
|
|
907
|
+
is not a routing concept, and no `<Route>` shape expresses it;
|
|
908
|
+
- **the app really is on the page underneath**, which is right for `aria-current`,
|
|
909
|
+
for the bar, for a link built while the layer is open — and is exactly wrong for
|
|
910
|
+
anything wanting "the settings are what is current". If something needs to
|
|
911
|
+
answer that, it reads the layer's own state, not the router.
|
|
912
|
+
|
|
913
|
+
Demo:
|
|
914
|
+
[../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html](../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html)
|
|
915
|
+
— a bar holding both doors side by side: notifications, a page that replaces the
|
|
916
|
+
screen with a route transition, and settings, a layer that covers it.
|
package/docs/popup_open.md
CHANGED
|
@@ -533,7 +533,10 @@ is nav state by construction.
|
|
|
533
533
|
|
|
534
534
|
The two meet when the signal IS a route's: a search-param `stateSignal` given to
|
|
535
535
|
`signal` puts the open state in the address itself, where a link can point at
|
|
536
|
-
it.
|
|
536
|
+
it. That is also the shape a popup takes when it is a LAYER over the screen —
|
|
537
|
+
settings opened from every screen and closed back onto the one the reader was on
|
|
538
|
+
— and what the address may and may not claim there is a decision of its own:
|
|
539
|
+
[navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say).
|
|
537
540
|
|
|
538
541
|
### `open`, and what it costs
|
|
539
542
|
|
|
@@ -65,6 +65,14 @@ happens to be everywhere. Write it for a page whose door really is furniture; a
|
|
|
65
65
|
page reached from a screen has that screen to be paired with, and the pair says
|
|
66
66
|
more.
|
|
67
67
|
|
|
68
|
+
**Such a page is still a PAGE**, and that is the question to settle before
|
|
69
|
+
writing the relation: it takes the screen's place, and the reader comes back to
|
|
70
|
+
whatever the router puts there — not necessarily to what they were on. A door in
|
|
71
|
+
the furniture whose destination must be drawn OVER the screen, and must give it
|
|
72
|
+
back exactly on closing, is not a page at all: it is a layer, its address says so
|
|
73
|
+
differently, and no relation is written for it. See
|
|
74
|
+
[navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say).
|
|
75
|
+
|
|
68
76
|
## Choosing a movement
|
|
69
77
|
|
|
70
78
|
- **`slide-x`** — going INTO something: a list item opened, a card followed, a
|