@jsenv/navi 0.29.387 → 0.29.388
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/README.md +4 -4
- package/dist/dev/jsenv_navi.js +400 -310
- package/dist/dev/jsenv_navi.js.map +72 -72
- package/dist/dev/jsenv_navi_side_effects.js.map +1 -1
- package/dist/jsenv_navi.js +396 -306
- package/dist/jsenv_navi.js.map +72 -72
- package/dist/jsenv_navi_side_effects.js.map +1 -1
- package/docs/AI_INSTRUCTIONS.md +39 -23
- package/docs/actions.md +6 -4
- package/docs/autofocus.md +2 -1
- package/docs/badge_list.md +5 -3
- package/docs/control_group.md +4 -3
- package/docs/control_object.md +1 -1
- package/docs/control_value.md +8 -9
- package/docs/css_architecture.md +16 -14
- package/docs/dialog_shape.md +12 -8
- package/docs/drag_interactions.md +44 -36
- package/docs/drag_to_travel.md +29 -23
- package/docs/dynamic_import.md +7 -5
- package/docs/error_handling.md +26 -27
- package/docs/field_validation.md +35 -6
- package/docs/interactions.md +28 -23
- package/docs/list_action.md +8 -5
- package/docs/list_refresh.md +7 -1
- package/docs/mobile_layout_pitfalls.md +6 -5
- package/docs/navigation.md +7 -5
- package/docs/network_policy.md +10 -3
- package/docs/pan_zoom.md +2 -2
- package/docs/popup_backdrop.md +11 -5
- package/docs/popup_lift.md +5 -3
- package/docs/popup_open.md +39 -28
- package/docs/resource.md +24 -17
- package/docs/route_transitions.md +13 -10
- package/docs/safe_area.md +38 -22
- package/docs/scroll.md +11 -9
- package/docs/testid.md +10 -1
- package/docs/typography.md +2 -3
- package/docs/view_transitions.md +9 -9
- package/docs/z_index.md +6 -5
- package/package.json +2 -2
package/docs/list_refresh.md
CHANGED
|
@@ -184,7 +184,8 @@ sends nothing and changes no state, so the network is silent and
|
|
|
184
184
|
`onRequestStateChange` — which reports what a request is doing — has no request
|
|
185
185
|
to report. A run that declined and a run that was never mounted look identical.
|
|
186
186
|
`debugScroll` is where that difference is visible; it carries the render window
|
|
187
|
-
and the run's asking, which are one subject.
|
|
187
|
+
and the run's asking, which are one subject. It logs in navi's development build
|
|
188
|
+
only.
|
|
188
189
|
|
|
189
190
|
```jsx
|
|
190
191
|
window.askLog = [];
|
|
@@ -251,6 +252,11 @@ Defaults are `{ GET: false, GET_MANY: ["POST"], GET_RANGE: ["POST", "DELETE"] }`
|
|
|
251
252
|
| `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. |
|
|
252
253
|
| `GET_RANGE: ["POST", "DELETE"]` | A slice is a range of places: a row leaving the collection shifts every place after it, which the store cannot do. Add the verb that moves an item in or out of the collection — a `PATCH` that archives, one that changes an item's tab. |
|
|
253
254
|
|
|
255
|
+
A `rerunOn` given replaces the `GET` and `GET_MANY` defaults whole: a key it
|
|
256
|
+
leaves out reruns nothing, so `rerunOn: { GET_RANGE: ["POST", "DELETE", "PATCH"] }`
|
|
257
|
+
alone also turns off `GET_MANY`'s `["POST"]`. Only `GET_RANGE`, left out, keeps
|
|
258
|
+
its default. Restate every key the resource is read through.
|
|
259
|
+
|
|
254
260
|
Adding `PUT`/`PATCH` to `GET_MANY` is the usual over-correction: it costs a
|
|
255
261
|
request and a `loading` pass to obtain something the response already contained.
|
|
256
262
|
When a `PATCH` "doesn't show", the fix is almost always server-side — return the
|
|
@@ -91,9 +91,9 @@ far from the wrapper:
|
|
|
91
91
|
other axis to `auto`.
|
|
92
92
|
|
|
93
93
|
Note: `overflow: hidden` on `<html>` would additionally create a new containing block,
|
|
94
|
-
which could break `position: fixed` in edge cases.
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
which could break `position: fixed` in edge cases. A dialog in the top layer is out of
|
|
95
|
+
reach of that: the top layer takes it out of every ancestor's containing block, which is
|
|
96
|
+
why navi's `Dialog` stays where it is written, in its opener's subtree.
|
|
97
97
|
|
|
98
98
|
### The wrapper is a net, not a fix: find what overflows
|
|
99
99
|
|
|
@@ -132,8 +132,9 @@ scrolls down to it when it opens.
|
|
|
132
132
|
Fix: **place `<dialog>` as the first child of `<body>`**, before any scrollable content.
|
|
133
133
|
`scrollIntoView` on an element already at the top of the document has no effect.
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
This is advice for a `<dialog>` written by hand. navi's `Dialog` is not moved: it
|
|
136
|
+
renders in its opener's subtree (see
|
|
137
|
+
[css_architecture.md](./css_architecture.md#counting-children-what-navi-puts-in-your-tree)).
|
|
137
138
|
|
|
138
139
|
### Summary
|
|
139
140
|
|
package/docs/navigation.md
CHANGED
|
@@ -713,12 +713,14 @@ too early and put back:
|
|
|
713
713
|
hands out.
|
|
714
714
|
|
|
715
715
|
**Only where the row owns the document**: nothing between the travelling box and
|
|
716
|
-
the viewport may scroll
|
|
717
|
-
|
|
716
|
+
the viewport may scroll. A row inside a scroller of its own — a frame in an
|
|
717
|
+
article, a panel beside other content — shares nothing with the document, and
|
|
718
718
|
the offset there belongs to the page around it, which the reader never left; the
|
|
719
|
-
travel leaves it alone. An `overflow: hidden`
|
|
720
|
-
|
|
721
|
-
|
|
719
|
+
travel leaves it alone. An `overflow: hidden` on any ancestor is enough to put
|
|
720
|
+
the row outside the document's scrollport — it holds an offset of its own even
|
|
721
|
+
without a scrollbar, whereas `overflow: clip` holds none and does not count — so
|
|
722
|
+
a row that should give positions back and does not is worth looking at from
|
|
723
|
+
that angle first.
|
|
722
724
|
|
|
723
725
|
Pages that scroll inside themselves rather than scrolling the document are not
|
|
724
726
|
concerned either way: each one brings its own scrollport, which goes away with
|
package/docs/network_policy.md
CHANGED
|
@@ -29,8 +29,11 @@ cache the app never has to write:
|
|
|
29
29
|
- **A route action left behind is aborted, not reset.** Its value survives; only
|
|
30
30
|
a run in flight is called off.
|
|
31
31
|
- **Running a completed action is a no-op.** Coming back to `/games/abc` from
|
|
32
|
-
the list sends nothing — `run()` on a
|
|
33
|
-
Only `rerun()` goes back to the
|
|
32
|
+
the list sends nothing while the instance that read it is alive — `run()` on a
|
|
33
|
+
`COMPLETED` action already has its data. Only `rerun()` goes back to the
|
|
34
|
+
network (see [actions.md](./actions.md)). An instance nothing references any
|
|
35
|
+
more is asked again, with the store row drawn meanwhile; under the policy
|
|
36
|
+
that ask is answered from the store (below).
|
|
34
37
|
- **A failed rerun keeps the previous value.** Only `errorSignal` and the
|
|
35
38
|
running state move; what was on screen stays on screen.
|
|
36
39
|
|
|
@@ -109,7 +112,11 @@ was asked:
|
|
|
109
112
|
|
|
110
113
|
Which actions the policy sees: those declaring a verb (`meta.verb`) — every
|
|
111
114
|
action a `resource()` makes. A plain `createAction` may not touch the network
|
|
112
|
-
at all, so it is left alone
|
|
115
|
+
at all, so it is left alone. `createAction(callback, { meta: { verb } })` opts
|
|
116
|
+
it into what the action layer holds — a control bound to a write verb is
|
|
117
|
+
read-only, a completed `"GET"` is not rerun — but its callback is still called
|
|
118
|
+
when it runs: answering from the store and settling with a `NetworkPolicyError`
|
|
119
|
+
are done by the callbacks a `resource()` wraps.
|
|
113
120
|
|
|
114
121
|
## Holding writes while reads go out
|
|
115
122
|
|
package/docs/pan_zoom.md
CHANGED
|
@@ -73,8 +73,8 @@ Opt-in, and for the same reason its opposite is (see
|
|
|
73
73
|
[`data-drag-on-contact`](./drag_interactions.md#a-finger-that-does-not-have-to-wait-data-drag-on-contact)):
|
|
74
74
|
navi cannot see whether anything behind the surface scrolls. You know;
|
|
75
75
|
say so. It spends the hold, though: a `longpress` declared beside a `pan` that
|
|
76
|
-
waits asks one finger to answer two waits
|
|
77
|
-
|
|
76
|
+
waits asks one finger to answer two waits, and the surface's — the shorter one
|
|
77
|
+
— is the one answered.
|
|
78
78
|
|
|
79
79
|
The wait is then asked before every pan, which is right while each finger
|
|
80
80
|
landing there is genuinely ambiguous. Once the user has settled into the plan it
|
package/docs/popup_backdrop.md
CHANGED
|
@@ -62,9 +62,10 @@ dismissing click must not also press what is under it — without dimming the
|
|
|
62
62
|
page for it. `backdrop={false}` is for a popup that must not absorb at all.
|
|
63
63
|
|
|
64
64
|
**It is what decides whether a popup is modal.** `Popover` is never modal,
|
|
65
|
-
either layer
|
|
66
|
-
|
|
67
|
-
genuinely inert; without one it goes to the
|
|
65
|
+
either layer, and neither is a `layer="local"` dialog. A top-layer `Dialog` is
|
|
66
|
+
modal exactly when it has a wall: with one it is `showModal()`'d and the
|
|
67
|
+
browser makes everything behind genuinely inert; without one it goes to the
|
|
68
|
+
same top layer through the Popover
|
|
68
69
|
API (`popover="manual"`), placed against the screen just the same, over a page
|
|
69
70
|
that stays live. `Popup` forwards `backdrop` in both modes, so which of popover
|
|
70
71
|
or dialog the small-screen resolution picks says nothing about whether one
|
|
@@ -179,7 +180,12 @@ panel that closes on that press then reopens on the click that follows is a
|
|
|
179
180
|
panel meant to stay open, blinking.
|
|
180
181
|
|
|
181
182
|
```jsx
|
|
182
|
-
<SidePanel
|
|
183
|
+
<SidePanel
|
|
184
|
+
id="error_panel"
|
|
185
|
+
signal={openCardIdSignal}
|
|
186
|
+
backdrop={false}
|
|
187
|
+
closeByPressOutside
|
|
188
|
+
>
|
|
183
189
|
…
|
|
184
190
|
</SidePanel>
|
|
185
191
|
|
|
@@ -198,7 +204,7 @@ adds a whole thing to the popup's ground — the card, its count, its buttons
|
|
|
198
204
|
and nothing in it is a dismissal.
|
|
199
205
|
|
|
200
206
|
It reads a press the page hears, so it belongs with a popup that has no wall
|
|
201
|
-
(`backdrop={false}
|
|
207
|
+
(`backdrop={false}`): behind a wall, no card is pressable.
|
|
202
208
|
|
|
203
209
|
### `pointer-events: none` and `inert` are not this
|
|
204
210
|
|
package/docs/popup_lift.md
CHANGED
|
@@ -132,8 +132,9 @@ corners itself, or on the card inside it, reads the same.
|
|
|
132
132
|
|
|
133
133
|
## Same width, or a wider box
|
|
134
134
|
|
|
135
|
-
`dialogSizeFromAnchor` makes the trigger's box a floor for the
|
|
136
|
-
`dialogMaxWidth="var(--anchor-width)"` makes it the ceiling too
|
|
135
|
+
On a `Picker`, `dialogSizeFromAnchor` makes the trigger's box a floor for the
|
|
136
|
+
dialog; `dialogMaxWidth="var(--anchor-width)"` makes it the ceiling too (on a
|
|
137
|
+
`Dialog` the same props are `sizeFromAnchor` and `maxWidth`). Together they
|
|
137
138
|
say "the same card, brought forward": the box travels and may grow in height,
|
|
138
139
|
never in width. Left out, `dialogExpandX` lets the sheet take the room it has,
|
|
139
140
|
and the width change is animated — the card keeps its scale inside a box
|
|
@@ -334,7 +335,8 @@ into the picture of the state it closes into.
|
|
|
334
335
|
|
|
335
336
|
## What it costs, and where the time goes
|
|
336
337
|
|
|
337
|
-
Measured at CPU ×6 on the demo bench
|
|
338
|
+
Measured at CPU ×6 on the demo bench
|
|
339
|
+
(`src/control/demos/picker/12_picker_card_demo.html#lift-bench`),
|
|
338
340
|
a card of a real edit sheet's weight (≈215 elements, 13 fields, 62 picker
|
|
339
341
|
nodes):
|
|
340
342
|
|
package/docs/popup_open.md
CHANGED
|
@@ -91,7 +91,7 @@ where that state lives:
|
|
|
91
91
|
|
|
92
92
|
| what opens it | how |
|
|
93
93
|
| ---------------------------------- | ------------------------------------------------------ |
|
|
94
|
-
| a button | `command` / `
|
|
94
|
+
| a button | `command` / `commandFor` props, no `open` |
|
|
95
95
|
| a gesture, an event, a JS decision | `triggerNaviCommand(...)`, still no `open` |
|
|
96
96
|
| a state the app holds | `signal` — [below](#when-the-app-holds-the-open-state) |
|
|
97
97
|
| where the user is | `navState`, or a route `stateSignal` given to `signal` |
|
|
@@ -99,7 +99,7 @@ where that state lives:
|
|
|
99
99
|
## A button opens it: the attributes
|
|
100
100
|
|
|
101
101
|
```jsx
|
|
102
|
-
<Button command="--navi-open"
|
|
102
|
+
<Button command="--navi-open" commandFor="note-dialog">
|
|
103
103
|
Read the note
|
|
104
104
|
</Button>
|
|
105
105
|
<Dialog id="note-dialog">…</Dialog>
|
|
@@ -123,7 +123,7 @@ press:
|
|
|
123
123
|
## Something else opens it: `triggerNaviCommand`
|
|
124
124
|
|
|
125
125
|
The attributes fire on every click of the element that carries them, and they
|
|
126
|
-
cover more than "this button opens that dialog": `
|
|
126
|
+
cover more than "this button opens that dialog": `commandFor` says to whom,
|
|
127
127
|
`value` says what it is about, `--navi-x:argument` says how. Before writing any
|
|
128
128
|
JS here, check none of those is the answer — `triggerNaviCommand` is the last
|
|
129
129
|
resort, not the general way to run a command.
|
|
@@ -209,7 +209,7 @@ closes the popup as usual.
|
|
|
209
209
|
|
|
210
210
|
```jsx
|
|
211
211
|
<Row
|
|
212
|
-
|
|
212
|
+
commandFor="row_menu"
|
|
213
213
|
interactions={{
|
|
214
214
|
longpress: (event) =>
|
|
215
215
|
triggerNaviCommand(event.target, "--navi-open", event),
|
|
@@ -225,7 +225,8 @@ own opening press from somebody else's comes from that gesture.
|
|
|
225
225
|
The first argument is the command's **source** — the element it is triggered
|
|
226
226
|
_from_. The target is resolved from it, in this order:
|
|
227
227
|
|
|
228
|
-
1. `
|
|
228
|
+
1. `commandFor="someId"` on the source (written to the DOM as the HTML
|
|
229
|
+
`commandfor` attribute, which is what is read),
|
|
229
230
|
2. `navi-command-target="parent-control" | "child-control"`,
|
|
230
231
|
3. the command's own fallback — for the popup commands, `closest("[aria-expanded]")`.
|
|
231
232
|
|
|
@@ -234,7 +235,7 @@ element itself as the source resolves to that popup: `closest()` starts at the
|
|
|
234
235
|
element itself. That is the short form used above, and it is enough whenever the
|
|
235
236
|
JS that decides already holds the popup's ref.
|
|
236
237
|
|
|
237
|
-
To trigger from another element instead, give that element a `
|
|
238
|
+
To trigger from another element instead, give that element a `commandFor`
|
|
238
239
|
pointing at the popup's `id` — attribute-driven target resolution, JS-driven
|
|
239
240
|
timing.
|
|
240
241
|
|
|
@@ -250,10 +251,11 @@ names none, and on whoever asked when nothing else says. In that order:
|
|
|
250
251
|
3. **`detail.source`** — who asked. A button therefore opens the popup on
|
|
251
252
|
itself, and a popup passed as its own source is its own anchor.
|
|
252
253
|
|
|
253
|
-
For `Dialog` the anchor does nothing at all unless `sizeFromAnchor`
|
|
254
|
-
(
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
For `Dialog` the anchor does nothing at all unless `sizeFromAnchor` asks for
|
|
255
|
+
it (it then feeds the `--anchor-width`/`--anchor-height` CSS vars) or
|
|
256
|
+
`animation="lifting"` does (the box the dialog comes out of, see
|
|
257
|
+
[popup_lift.md](./popup_lift.md)); for `Popover`, which really is positioned
|
|
258
|
+
relative to its anchor, say what the anchor is:
|
|
257
259
|
|
|
258
260
|
```jsx
|
|
259
261
|
<Popover ref={popoverRef} anchor={rowRef}>
|
|
@@ -261,8 +263,9 @@ anchor is:
|
|
|
261
263
|
|
|
262
264
|
`anchorCustomEventDetail="ignore"` drops the open's side of that order
|
|
263
265
|
entirely, leaving the `anchor` prop alone — for a popover that must never be
|
|
264
|
-
anchored to whatever opened it (`SidePanel` does this), and for a
|
|
265
|
-
|
|
266
|
+
anchored to whatever opened it (`SidePanel` does this), and for a dialog that
|
|
267
|
+
must never be sized from it (`sizeFromAnchor`) or lifted out of it
|
|
268
|
+
(`animation="lifting"`).
|
|
266
269
|
|
|
267
270
|
## Opening it ON something
|
|
268
271
|
|
|
@@ -272,8 +275,8 @@ row is one dialog with two modes, and the press is the only thing that knows
|
|
|
272
275
|
which one — so the press says it, with its own value:
|
|
273
276
|
|
|
274
277
|
```jsx
|
|
275
|
-
<Button command="--navi-open"
|
|
276
|
-
<Button value={radar.id} command="--navi-open"
|
|
278
|
+
<Button command="--navi-open" commandFor="radar-dialog">Nouveau radar</Button>
|
|
279
|
+
<Button value={radar.id} command="--navi-open" commandFor="radar-dialog" />
|
|
277
280
|
|
|
278
281
|
<Dialog
|
|
279
282
|
id="radar-dialog"
|
|
@@ -629,7 +632,7 @@ command once it succeeded.
|
|
|
629
632
|
```jsx
|
|
630
633
|
// Stays open while save() runs, closes when it resolves, stays open if it
|
|
631
634
|
// throws — with what was typed still there and the error on the button.
|
|
632
|
-
<Button command="--navi-close"
|
|
635
|
+
<Button command="--navi-close" commandFor="note-dialog" action={save}>
|
|
633
636
|
Save
|
|
634
637
|
</Button>
|
|
635
638
|
```
|
|
@@ -689,12 +692,12 @@ the popup without closing it, and the error callout stays where the press was.
|
|
|
689
692
|
The gestures that close a popup do not all mean the same thing, and that is on
|
|
690
693
|
purpose:
|
|
691
694
|
|
|
692
|
-
| gesture | what it means | who decides
|
|
693
|
-
| ------------------------------ | ------------- |
|
|
694
|
-
| Escape | cancel | `escapeEffect="cancel"` (default) |
|
|
695
|
-
| a press outside | close, keep | `pressOutside="close"`
|
|
696
|
-
| a close cross (`--navi-close`) | close, keep | [the cross](#the-close-cross)
|
|
697
|
-
| `--navi-cancel` on a button | cancel | the button
|
|
695
|
+
| gesture | what it means | who decides |
|
|
696
|
+
| ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------ |
|
|
697
|
+
| Escape | cancel | always, for a `Dialog`/`Popover`; `escapeEffect="cancel"` (the default) for a `Picker` |
|
|
698
|
+
| a press outside | close, keep | `pressOutside="close"` — the default of a `Dialog` and of a `Picker`; a bare `Popover` says `"ignore"` |
|
|
699
|
+
| a close cross (`--navi-close`) | close, keep | [the cross](#the-close-cross) |
|
|
700
|
+
| `--navi-cancel` on a button | cancel | the button |
|
|
698
701
|
|
|
699
702
|
Escape says "forget it". It is the one gesture that has meant that everywhere,
|
|
700
703
|
for as long as there have been dialogs, and navi keeps it that way. **A popup
|
|
@@ -757,7 +760,8 @@ empty.
|
|
|
757
760
|
|
|
758
761
|
### `escapeEffect="close"`, and why it is a last resort
|
|
759
762
|
|
|
760
|
-
`escapeEffect="close"`
|
|
763
|
+
`escapeEffect="close"` — a `Picker` prop; a `Dialog`/`Popover` has no such
|
|
764
|
+
switch — makes Escape say what a click outside says. It exists,
|
|
761
765
|
and it is almost never what you want: it takes away the only key that undoes,
|
|
762
766
|
and a popup with no way back is one people stop opening. Reach for a close
|
|
763
767
|
cross first.
|
|
@@ -996,7 +1000,8 @@ one fill above.
|
|
|
996
1000
|
|
|
997
1001
|
A press on another card is not an outside press once the card names the panel
|
|
998
1002
|
with `data-navi-popup-inside` (see [popup_backdrop.md](./popup_backdrop.md)).
|
|
999
|
-
The full example is `src/layout/demos/7_slot_demo.html`, "
|
|
1003
|
+
The full example is `src/layout/demos/7_slot_demo.html`, "Two side-panel
|
|
1004
|
+
slots".
|
|
1000
1005
|
|
|
1001
1006
|
## A popup that loads data
|
|
1002
1007
|
|
|
@@ -1116,14 +1121,20 @@ half-typed form where it left them.
|
|
|
1116
1121
|
"Closed" is two states, not one — never opened yet, and closed again after an
|
|
1117
1122
|
opening — so the `mount` prop answers both at once:
|
|
1118
1123
|
|
|
1119
|
-
| `mount` | before the first open
|
|
1120
|
-
| ----------------------------- |
|
|
1121
|
-
| `"always"` | mounted
|
|
1122
|
-
| `"
|
|
1123
|
-
| `"
|
|
1124
|
+
| `mount` | before the first open | after a close |
|
|
1125
|
+
| ----------------------------- | --------------------------- | ------------- |
|
|
1126
|
+
| `"always"` | mounted | mounted |
|
|
1127
|
+
| `"idle"` | mounted once the page idles | mounted |
|
|
1128
|
+
| `"from-first-open"` (default) | not mounted | mounted |
|
|
1129
|
+
| `"while-opened"` | not mounted | not mounted |
|
|
1124
1130
|
|
|
1125
1131
|
`"always"` is for content something depends on before any opening: a value read
|
|
1126
1132
|
off it, fields a surrounding form submits, a size measured from outside.
|
|
1133
|
+
`"idle"` is `"always"` minus the cost on the render that draws the page.
|
|
1134
|
+
|
|
1135
|
+
Whatever the value but `"while-opened"`, intent on the anchor — a pointer
|
|
1136
|
+
entering it, focus landing in it — builds the content ahead of the click that
|
|
1137
|
+
will open it.
|
|
1127
1138
|
|
|
1128
1139
|
`"while-opened"` is what an uncontrolled field seeded from a `defaultValue`
|
|
1129
1140
|
needs: without it, a popup reopened after the underlying value changed still
|
package/docs/resource.md
CHANGED
|
@@ -93,14 +93,15 @@ const GAME = resource("game", {
|
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
The callback receives the bound params merged with the range the list asks for
|
|
96
|
-
(`start`, `end`, `limit`, `before`, `after`, `around`, `count`), and
|
|
97
|
-
as second argument — aborted when the list stops wanting those rows.
|
|
98
|
-
a range the way a `Content-Range` does: **`{ items, start, count }`**
|
|
99
|
-
rows, at this place, out of that many. `start` may be omitted when the
|
|
100
|
-
asked for
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
(`start`, `end`, `limit`, `before`, `after`, `around`, `count`), and `{ signal }`
|
|
97
|
+
as second argument — a signal aborted when the list stops wanting those rows.
|
|
98
|
+
It returns a range the way a `Content-Range` does: **`{ items, start, count }`**
|
|
99
|
+
— these rows, at this place, out of that many. `start` may be omitted when the
|
|
100
|
+
list asked for one at or after `0` (a negative `start` counts back from the end,
|
|
101
|
+
and the source must then say where the slice landed); `count` defaults to
|
|
102
|
+
`start + items.length` (a source that does not know its total). The items are
|
|
103
|
+
upserted on their way in, so the list draws store items — never copies of the
|
|
104
|
+
JSON — and a request sent from a row is read back on that row.
|
|
104
105
|
|
|
105
106
|
A row that must follow its **own fields** through a write reads them from the
|
|
106
107
|
store rather than from the object it was handed:
|
|
@@ -131,9 +132,12 @@ this collection again after its screen went away finds it drawn and revalidates
|
|
|
131
132
|
it rather than starting over
|
|
132
133
|
([list_refresh.md](./list_refresh.md#leaving-the-screen-and-coming-back)).
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
Membership is what a re-read answers: a verb listed in `rerunOn.GET_RANGE`
|
|
136
|
+
(`["POST", "DELETE"]` by default) tells the run the collection moved, and it
|
|
137
|
+
asks again for the window it draws, rows staying on screen meanwhile. A write
|
|
138
|
+
that moves an item in or out through another verb — a `PATCH` that archives —
|
|
139
|
+
is added there, or `reader.invalidate()` is called; a `key` on the run is not the
|
|
140
|
+
way ([list_refresh.md](./list_refresh.md#a-paginated-list-stays-on-screen-too)).
|
|
137
141
|
|
|
138
142
|
It reads a collection, so it lives on the resource itself (or on a
|
|
139
143
|
`withParams()` of it), not on a relation.
|
|
@@ -411,14 +415,17 @@ rules, verified by `src/state/rest/tests/resource_graph_parent_rerun.test.js`:
|
|
|
411
415
|
reruns the parent. Declare it explicitly with `dependencies` if you need it
|
|
412
416
|
(see [`dependencies`](#dependencies-rerun-after-another-resource-writes)).
|
|
413
417
|
- Within a relationship resource, the usual defaults still apply: its own
|
|
414
|
-
`GET_MANY` reruns after its own `POST
|
|
415
|
-
`DELETE
|
|
416
|
-
|
|
418
|
+
`GET_MANY` reruns after its own `POST`. A `.one`/`.many` `GET` is reset (not
|
|
419
|
+
rerun) by its own `DELETE`, as a root `GET` is; a scoped `GET` is not reset —
|
|
420
|
+
its data simply resolves to nothing once the child is dropped. Override per
|
|
421
|
+
relation with `rerunOn`/`dependencies`, which every relationship method
|
|
422
|
+
accepts.
|
|
417
423
|
|
|
418
424
|
Splitting a sub-resource out of a parent `PATCH` therefore changes the refresh
|
|
419
|
-
graph:
|
|
420
|
-
only by these rules. When a parent field
|
|
421
|
-
say so with `dependencies` rather than
|
|
425
|
+
graph: a parent field the parent's own response kept fresh is, once the relation
|
|
426
|
+
is a resource of its own, refreshed only by these rules. When a parent field
|
|
427
|
+
genuinely depends on a child mutation, say so with `dependencies` rather than
|
|
428
|
+
relying on a rerun that will not happen.
|
|
422
429
|
|
|
423
430
|
## `withParams()`: a scope with reruns of its own
|
|
424
431
|
|
|
@@ -132,8 +132,9 @@ relation was written for. Two situations, two answers:
|
|
|
132
132
|
deserves a direction. This is the case the export exists for.
|
|
133
133
|
|
|
134
134
|
A default has no direction (nothing says which of two arbitrary pages is
|
|
135
|
-
"before" the other), so only directionless movements make sense there
|
|
136
|
-
|
|
135
|
+
"before" the other), so only directionless movements make sense there: navi's
|
|
136
|
+
`slide-*` and `cover-*` are written on the direction, and as the default they
|
|
137
|
+
play nothing. Written relations, and `"none"`, always win over it.
|
|
137
138
|
|
|
138
139
|
## When one navigation knows better
|
|
139
140
|
|
|
@@ -179,7 +180,8 @@ the same forms `defineRouteTransition` takes, plus `direction`. It overrides
|
|
|
179
180
|
answers for. So:
|
|
180
181
|
|
|
181
182
|
- `{ direction: "back" }` keeps the pair's movement and only turns it round;
|
|
182
|
-
- `"zoom"` swaps the movement
|
|
183
|
+
- `"zoom"` swaps the movement; the way round and the pace are still the
|
|
184
|
+
pair's;
|
|
183
185
|
- `"none"` cuts, where the pair — or the default — would have played;
|
|
184
186
|
- `{ duration: 500 }` re-times what was already going to play.
|
|
185
187
|
|
|
@@ -525,12 +527,13 @@ await page.goBack();
|
|
|
525
527
|
The window is **one frame**: the hold is given back inside the view
|
|
526
528
|
transition's callback, which the browser runs at its next rendering
|
|
527
529
|
opportunity. `tests/route_transition_list_revisit/` walks it the wrong way on
|
|
528
|
-
purpose in its last case —
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
same millisecond. This is a testing trap, not
|
|
530
|
+
purpose in its last case — a back pressed the instant the URL changes — and
|
|
531
|
+
which side of that frame a cycle lands on belongs to the machine's load: under
|
|
532
|
+
a movement the list that comes back is either the element that never left,
|
|
533
|
+
asking for nothing, or one truly remounted, asking once, while the same walk
|
|
534
|
+
without a movement is a real one every time. No thumb moves in one frame; an
|
|
535
|
+
automated click continues in the same millisecond. This is a testing trap, not
|
|
536
|
+
a user-facing behaviour.
|
|
534
537
|
|
|
535
538
|
## The rest, briefly
|
|
536
539
|
|
|
@@ -538,6 +541,6 @@ same millisecond. This is a testing trap, not a user-facing behaviour.
|
|
|
538
541
|
a per-relation `{ type, duration }` for one relation.
|
|
539
542
|
- The URL leads: transitions play on navigations somebody else started (a
|
|
540
543
|
`<Link>`, the back button, `history.back()`). Nothing here navigates.
|
|
541
|
-
- A browser without view transitions
|
|
544
|
+
- A browser without view transitions navigates with a cut. The app
|
|
542
545
|
must remain fully usable that way — which it is, if the transitions state
|
|
543
546
|
relations rather than carry information.
|
package/docs/safe_area.md
CHANGED
|
@@ -80,8 +80,8 @@ uneven writes `--navi-app-inset-left` / `-right` directly instead.
|
|
|
80
80
|
In pixels: popup placement reads this value back from CSS to compute its own
|
|
81
81
|
margins, and a custom property computes to a token stream rather than to a
|
|
82
82
|
length, so `40rem` would arrive there as the string `"40rem"`. A non-px value
|
|
83
|
-
still caps the popup's size (that part is pure CSS) but leaves
|
|
84
|
-
viewport-sized, and says so in the console.
|
|
83
|
+
still caps the popup's size (that part is pure CSS) but leaves its margins and
|
|
84
|
+
its placement viewport-sized, and says so in the console.
|
|
85
85
|
|
|
86
86
|
Every popup follows: `Dialog`, `Popover`, and everything built on them
|
|
87
87
|
(`Picker`, `Select`…). It is a ceiling and nothing more — on a screen narrower
|
|
@@ -98,11 +98,13 @@ Two ways NOT to get this:
|
|
|
98
98
|
popovers would keep sizing themselves against 1500px;
|
|
99
99
|
- setting `--dialog-max-width` on `.navi_dialog` from the app. It is a
|
|
100
100
|
`--component-*` token, declared on the element (see
|
|
101
|
-
[css_architecture.md](./css_architecture.md#--navi--vs---component--where-the-override-has-to-go))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
[css_architecture.md](./css_architecture.md#--navi--vs---component--where-the-override-has-to-go)):
|
|
102
|
+
every dialog resets it on itself in an unlayered rule, so that a nested
|
|
103
|
+
dialog does not inherit its parent's size, and a `maxWidth` prop writes it
|
|
104
|
+
inline (a `Picker` under `dialogSizeFromAnchor` does) — an app rule of the
|
|
105
|
+
same specificity does not reliably win, and the cap silently disappears. It
|
|
106
|
+
is also the knob a single popup uses to ask for a specific size, not a
|
|
107
|
+
ceiling:
|
|
106
108
|
`--navi-app-max-width` feeds `--dialog-maxmax-width`, the hard ceiling _under_
|
|
107
109
|
that knob, so a popup that genuinely needs its own `maxWidth` can still say
|
|
108
110
|
so without escaping the app's screen.
|
|
@@ -154,10 +156,12 @@ Beware of making that container scrollable by accident — see
|
|
|
154
156
|
`var(--navi-safe-area-inset-bottom)` in any rule. It is always declared, whether
|
|
155
157
|
or not the app ever mounts a bar.
|
|
156
158
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
`
|
|
160
|
-
|
|
159
|
+
From **JS**, the four `--navi-safe-area-inset-*` are registered as lengths
|
|
160
|
+
(`@property`, in `safe_area.js`), so
|
|
161
|
+
`getComputedStyle(document.documentElement).getPropertyValue("--navi-safe-area-inset-bottom")`
|
|
162
|
+
gives pixels. The level-1 `--navi-app-inset-*` are not registered and keep their
|
|
163
|
+
`calc()` unresolved there: give a hidden box `height: var(…)` and measure it —
|
|
164
|
+
see `src/layout/demos/fixed_bar/keyboard.html`.
|
|
161
165
|
|
|
162
166
|
### Putting something new into it
|
|
163
167
|
|
|
@@ -182,24 +186,36 @@ truth:
|
|
|
182
186
|
|
|
183
187
|
## The trap: which viewport
|
|
184
188
|
|
|
185
|
-
Three heights are in play and they are not the same one
|
|
189
|
+
Three heights are in play and they are not the same one — and which of them
|
|
190
|
+
the keyboard moves depends on the browser.
|
|
186
191
|
|
|
187
|
-
| what | shrinks when the keyboard opens
|
|
188
|
-
| -------------------------------- |
|
|
189
|
-
| `window.innerHeight` / `100dvh` | no
|
|
190
|
-
| `visualViewport.height` | yes
|
|
191
|
-
| `--navi-vvh` (tracks the visual) |
|
|
192
|
+
| what | shrinks when the keyboard opens |
|
|
193
|
+
| -------------------------------- | ----------------------------------------------- |
|
|
194
|
+
| `window.innerHeight` / `100dvh` | no |
|
|
195
|
+
| `visualViewport.height` | yes, except where the keyboard overlays (below) |
|
|
196
|
+
| `--navi-vvh` (tracks the visual) | same as `visualViewport.height` |
|
|
197
|
+
|
|
198
|
+
Wherever the browser has the VirtualKeyboard API (Chromium), navi makes the
|
|
199
|
+
keyboard overlay the page rather than shrink it (`src/layout/virtual_keyboard.js`):
|
|
200
|
+
no viewport shrinks, and the keyboard arrives as `--navi-keyboard-inset-bottom`
|
|
201
|
+
(`env(keyboard-inset-height)`), which `--navi-app-inset-bottom` adds. Firefox,
|
|
202
|
+
Safari, and an app that called `disableVirtualKeyboardOverlay()` shrink the
|
|
203
|
+
visual viewport instead, and `--navi-keyboard-inset-bottom` stays 0. Either way
|
|
204
|
+
`--navi-app-height` and the popup ceilings answer the part of the screen left
|
|
205
|
+
visible.
|
|
192
206
|
|
|
193
207
|
`position: fixed` — so every `FixedBar` — is laid out against the **layout**
|
|
194
|
-
viewport.
|
|
195
|
-
|
|
196
|
-
nothing reduced the layout viewport.
|
|
208
|
+
viewport. Where the visual viewport is what shrinks, a bottom bar therefore
|
|
209
|
+
stays at the bottom of a window the keyboard is covering: it ends up _behind_
|
|
210
|
+
the keyboard, and no inset says so, because nothing reduced the layout viewport.
|
|
211
|
+
Where the keyboard overlays, the bar is pinned to `--navi-app-inset-bottom`,
|
|
212
|
+
which counts it, and sits above it.
|
|
197
213
|
|
|
198
214
|
The consequence for anything measuring against the insets: mix the two families
|
|
199
215
|
and you get a drift that only appears with a keyboard open. `getBoundingClientRect`
|
|
200
216
|
is in layout-viewport coordinates, so what is compared to it must be too
|
|
201
|
-
(`100dvh`), while `--navi-app-*` derives from `--navi-vvh`
|
|
202
|
-
_sizes_ must fit what is actually visible.
|
|
217
|
+
(`100dvh`), while `--navi-app-*` derives from `--navi-vvh` (plus the keyboard
|
|
218
|
+
inset) because what navi _sizes_ must fit what is actually visible.
|
|
203
219
|
|
|
204
220
|
`src/layout/demos/fixed_bar/keyboard.html` puts all of these on screen at once
|
|
205
221
|
and turns the bottom bar's number red when it goes under the keyboard. On a
|
package/docs/scroll.md
CHANGED
|
@@ -47,7 +47,7 @@ Two shapes, and they do not behave the same:
|
|
|
47
47
|
| `header` / `footer` alone | the container itself scrolls, and they are `position: sticky` at its edges — the content scrolls under them |
|
|
48
48
|
| a `body` as well | the container becomes a flex column, its own overflow turns to `hidden`, and the **body is the only thing that scrolls**; header and footer sit outside it (`position: static`, `flex-shrink: 0`) |
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Three consequences worth knowing before fighting them:
|
|
51
51
|
|
|
52
52
|
- the body is `flex: 0 1 auto` — **it shrinks, it never grows**. A short body
|
|
53
53
|
leaves the footer right under it rather than pushed to the bottom of a box it
|
|
@@ -61,7 +61,8 @@ Two consequences worth knowing before fighting them:
|
|
|
61
61
|
them — positioned or not. Write `style={{ "--box-header-z-index": "auto" }}`
|
|
62
62
|
(`--box-footer-z-index` likewise) at the call site that needs the opposite: a
|
|
63
63
|
badge or a stamp overflowing a row is otherwise sliced by a header it never
|
|
64
|
-
scrolls under. `isolation: isolate`
|
|
64
|
+
scrolls under. The box is `isolation: isolate` already, so either value stays
|
|
65
|
+
local to it.
|
|
65
66
|
See `docs/z_index.md` and `src/box/demos/9_scrollable_z_index_demo.html`.
|
|
66
67
|
|
|
67
68
|
Padding belongs on the parts, not on the scrolling box: padding on a scroller
|
|
@@ -237,10 +238,11 @@ A dialog is already bounded by the room its container leaves it
|
|
|
237
238
|
(`--dialog-maxmax-height`), so a `maxHeight` is only for making it smaller than
|
|
238
239
|
that.
|
|
239
240
|
|
|
240
|
-
`dialog.jsx` deliberately declares no `overflow` of its own:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
`dialog.jsx` deliberately declares no `overflow` of its own: the
|
|
242
|
+
`[data-scrollable]` the dialog carries is its scrolling rule. A modal dialog
|
|
243
|
+
would get `auto` from the UA stylesheet anyway, but a `layer="local"` one gets
|
|
244
|
+
nothing, and without that rule its `max-height` would only decide how big the
|
|
245
|
+
box looks while the content kept painting straight through it.
|
|
244
246
|
|
|
245
247
|
### `scrollCapture`
|
|
246
248
|
|
|
@@ -309,7 +311,7 @@ So: nothing scrollable between the cap and the slides. A `<Box body>` around
|
|
|
309
311
|
them is a scroller (see the table at the top of this file) — and so is a bare
|
|
310
312
|
`overflow="auto"` on a wrapper. The dialog keeps a shared `header` if the tabs
|
|
311
313
|
are shared, with an explicit `flexShrink="0"` since the rule that gives it for
|
|
312
|
-
free
|
|
314
|
+
free applies only next to a `body`.
|
|
313
315
|
|
|
314
316
|
**Padding goes on the slide** — or on its parts, since the slide is now the
|
|
315
317
|
scroller (see the top of this file) — but never on the container nor on
|
|
@@ -393,8 +395,8 @@ browser paints first — what a phone screen shows, plus a few — and `after` f
|
|
|
393
395
|
the paint on. The switch waits for the paint itself, not for an effect: preact
|
|
394
396
|
runs a component's pending effects early when that component renders again,
|
|
395
397
|
and something always re-renders before a popup has painted. Do not rebuild
|
|
396
|
-
this by hand with a `useEffect` that widens a slice
|
|
397
|
-
|
|
398
|
+
this by hand with a `useEffect` that widens a slice: that effect is one preact
|
|
399
|
+
runs early, and it fails for that reason. The runs ask their source for `after`
|
|
398
400
|
rows from the start, so the smaller first window costs no second request.
|
|
399
401
|
|
|
400
402
|
### Doing it well
|