@jsenv/navi 0.29.417 → 0.29.419

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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/dev/jsenv_navi.js +347 -83
  3. package/dist/dev/jsenv_navi.js.map +12 -12
  4. package/dist/jsenv_navi.js +345 -81
  5. package/dist/jsenv_navi.js.map +12 -12
  6. package/docs/AI_INSTRUCTIONS.md +177 -258
  7. package/docs/actions.md +164 -251
  8. package/docs/autofocus.md +66 -83
  9. package/docs/control_object.md +117 -214
  10. package/docs/control_value.md +121 -185
  11. package/docs/create_and_edit.md +159 -254
  12. package/docs/css_architecture.md +196 -260
  13. package/docs/data_states.md +41 -44
  14. package/docs/dialog_shape.md +91 -134
  15. package/docs/drag_interactions.md +291 -454
  16. package/docs/drag_to_travel.md +152 -748
  17. package/docs/dynamic_import.md +119 -136
  18. package/docs/error_handling.md +75 -127
  19. package/docs/field_validation.md +49 -89
  20. package/docs/form_changed.md +61 -78
  21. package/docs/group.md +141 -0
  22. package/docs/i18n.md +31 -76
  23. package/docs/interactions.md +170 -326
  24. package/docs/list_action.md +36 -51
  25. package/docs/list_refresh.md +73 -115
  26. package/docs/mobile_touch.md +56 -62
  27. package/docs/navigation.md +377 -723
  28. package/docs/network_policy.md +61 -75
  29. package/docs/pan_zoom.md +62 -103
  30. package/docs/popup_backdrop.md +128 -168
  31. package/docs/popup_lift.md +179 -268
  32. package/docs/popup_open.md +380 -651
  33. package/docs/resource.md +228 -408
  34. package/docs/route_transitions.md +333 -473
  35. package/docs/safe_area.md +102 -89
  36. package/docs/scroll.md +175 -317
  37. package/docs/state_binding.md +40 -84
  38. package/docs/testid.md +18 -23
  39. package/docs/typography.md +111 -141
  40. package/docs/view_transitions.md +106 -104
  41. package/docs/z_index.md +61 -109
  42. package/package.json +2 -2
  43. package/docs/badge_list.md +0 -83
  44. package/docs/control_group.md +0 -168
  45. package/docs/mobile_layout_pitfalls.md +0 -145
@@ -27,10 +27,9 @@ whether a missing list is a skeleton, a sentence or an illustration.
27
27
 
28
28
  Without options, `useAsyncData` **delegates**: it suspends while the data is not
29
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:
30
+ `<Loading>` and `<ErrorBoundary>` above rather than by the component. Each
31
+ option turns one of those from something done _to_ the component into a value
32
+ the component reads:
34
33
 
35
34
  | passed | the wait | the failure |
36
35
  | --------------- | ------------------------- | ---------------------------- |
@@ -44,24 +43,19 @@ suspends.** Not while the action runs, not while a debounced binding is
44
43
  settling, not while it holds params nobody has started yet, not after a failure
45
44
  whose message was dismissed. Every one of those comes back as a value.
46
45
 
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 leaves with it and is reopened when it returns, 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.
46
+ That is what keeps the component alive. Suspending does not draw a spinner over
47
+ a subtree: it takes the subtree away and puts the boundary's fallback in its
48
+ place, and everything under it is remounted when it comes back — a dialog open
49
+ inside it leaves and is reopened, a scroll position is lost, half-typed text is
50
+ gone, and the retry button the component was offering disappears with it.
51
+ `error: true` says the same thing about failure: the component stays, so it is
52
+ still there to offer the way out.
57
53
 
58
54
  A component that delegates nothing needs no `<Loading>` above it. One that
59
55
  delegates its wait does, since that is where the suspension lands, and navi says
60
56
  so rather than leaving the subtree blank. Inside a popup that boundary is the
61
- popup's own — `{ loading: true }`, or a `<Loading>` inside the popup — never
62
- the one above the page: a suspension landing there takes the popup away with
63
- the page it copies, and the popup does not open at all (see
64
- [popup_open.md](./popup_open.md#a-popup-that-loads-data)).
57
+ popup's own, never the one above the page
58
+ ([popup_open.md](./popup_open.md#a-popup-that-loads-data)).
65
59
 
66
60
  ## `data` and `loading` are independent
67
61
 
@@ -75,28 +69,28 @@ means something different:
75
69
  | set | `false` | settled | the content |
76
70
  | `undefined` | `false` | nothing here, nothing coming | an empty state |
77
71
 
78
- Two consequences worth stating out loud.
79
-
80
72
  **A first load is only a first load when nothing is known.** A resource `GET`
81
73
  whose params name a row the store already holds — read earlier by a `GET_MANY`,
82
- a relation, a list — starts on the second line, not the first: the row is drawn
83
- and the request goes out behind it. The response replaces it, and a failure
84
- lands beside it like any refresh failure. A `GET` of a row the store lacks
85
- still starts on the first line, so what is drawn is never a different row. A
86
- row known from the previous page load counts too, when the resource asks for
87
- it: [`persist`](./resource.md#persist-the-last-answer-drawn-again-after-a-reload)
88
- puts a reload on the second line as well.
74
+ a relation, a list — starts on the second line, not the first: the row is
75
+ drawn, the request goes out behind it, and a failure lands beside it like any
76
+ refresh failure. A row known from the previous page load counts too, when the
77
+ resource asks for it:
78
+ [`persist`](./resource.md#persist-the-last-answer-drawn-again-after-a-reload)
79
+ puts a reload on the second line as well. A `GET` bound through a signal — a
80
+ route action, a `bindParams` holding one — is on the second line when the signal
81
+ moves to params it has not answered yet, with the previous params' row: its
82
+ `data` stays that row until the new answer lands, whether or not the store holds
83
+ the new one.
89
84
 
90
85
  **The emptiness test is `data === undefined`, never `loading`.** Reading
91
86
  `loading` as "there is nothing to display" blanks the screen on every refresh —
92
- for a checkbox ticked on one row of a list, the whole list goes.
93
-
94
- A list is no exception. A plural action — `GET_MANY`, a `.many()` or
95
- `.scopedMany()` read — has no data until its first answer: `undefined`, not
96
- `[]`. An empty array is an answer ("the collection is empty"), and the two are
97
- different facts drawn differently: a skeleton for the first, an empty state for
98
- the second. So a component reading a plural action must accept `undefined`
99
- before dereferencing (`items?.map`, `items ?? []`, or the branch above).
87
+ for a checkbox ticked on one row of a list, the whole list goes. A list is no
88
+ exception: a plural action — `GET_MANY`, a `.many()` or `.scopedMany()` read —
89
+ has no data until its first answer, `undefined`, not `[]`. An empty array is an
90
+ answer ("the collection is empty"), and the two are drawn differently: a
91
+ skeleton for the first, an empty state for the second. So a component reading a
92
+ plural action accepts `undefined` before dereferencing (`items?.map`,
93
+ `items ?? []`, or the branch below).
100
94
 
101
95
  **A skeleton is told whether it is loading; it does not deduce it.** The last
102
96
  row of the table is the one that gets forgotten, and it is reachable: a first
@@ -112,16 +106,19 @@ if (items === undefined) {
112
106
  return <ItemList items={items} busy={loading} />;
113
107
  ```
114
108
 
109
+ `loading` is not proof that a request is out, either: an action holding params
110
+ that nothing started — idle, or aborted — reads `loading: true`, so a skeleton
111
+ told it shimmers until something runs that action.
112
+
115
113
  navi's own placeholders take that flag rather than inventing it: `<Text loading>`
116
- shimmers, `<Text skeleton>` is the same bar held still, and `<Text skeleton
117
- loading={loading}>` is one that tells the truth either way.
114
+ shimmers, `<Text skeleton>` is the same bar held still, and
115
+ `<Text skeleton loading={loading}>` is one that tells the truth either way.
118
116
 
119
- **Knowing the count beforehand beats guessing it.** How many skeleton rows to
120
- draw is a question the response answers too late; a screen that already knows —
121
- a count carried by the parent resource, a total returned alongside a previous
122
- page — says so, and `<List loading loadingSkeletonCount={0}>` draws the empty
123
- state right away instead of three rows that will vanish. Nothing jumps when the
124
- response lands.
117
+ **Knowing the count beforehand beats guessing it.** A screen that already knows
118
+ how many rows are coming — a count carried by the parent resource, a total
119
+ returned alongside a previous page — says so, and
120
+ `<List loading loadingSkeletonCount={0}>` draws the empty state right away
121
+ instead of three rows that will vanish.
125
122
 
126
123
  ## An error is a message on the screen, not the screen
127
124
 
@@ -156,7 +153,7 @@ drawn, not as a stronger version of the same thing. Which of the two, and where
156
153
  an error goes when neither takes it, is
157
154
  [error_handling.md](./error_handling.md).
158
155
 
159
- ## Reference
156
+ ## See also
160
157
 
161
158
  - [error_handling.md](./error_handling.md) — the two kinds of error, where each
162
159
  is shown, and what happens to one nobody displays.
@@ -16,11 +16,10 @@ Where a `Dialog` sits and how big it gets. What happens _inside_ it (scrolling,
16
16
 
17
17
  ## A dialog is sized by its content, never by a `width`
18
18
 
19
- `Dialog` has no `width` prop of its own (one handed to it is `Box`'s, still
20
- under the ceiling below), and that is the whole design: a dialog is a surface
21
- laid over the app, and what it holds is what knows how wide it should be. What
22
- a caller states are **bounds** — a floor under a dialog too narrow for its
23
- content, a ceiling over one that would sprawl:
19
+ `Dialog` has no `width` prop of its own, and that is the whole design: a dialog
20
+ is a surface laid over the app, and what it holds is what knows how wide it
21
+ should be. What a caller states are **bounds** — a floor under a dialog too
22
+ narrow for its content, a ceiling over one that would sprawl:
24
23
 
25
24
  | what you want to say | how |
26
25
  | --------------------------------------- | ------------------------- |
@@ -29,32 +28,26 @@ content, a ceiling over one that would sprawl:
29
28
  | "as wide as the container allows" | `expandX` / `expandY` |
30
29
  | "as wide as the control that opened it" | `sizeFromAnchor` (opt-in) |
31
30
 
32
- `sizeFromAnchor` is off by default and that is deliberate: unlike a `Popover`,
33
- a dialog is not attached to what opened it, so following that element's box is
34
- a choice, not the norm. It only ever sets a **floor**
31
+ `sizeFromAnchor` is off by default and that is deliberate: unlike a `Popover`, a
32
+ dialog is not attached to what opened it, so following that element's box is a
33
+ choice, not the norm. It only ever sets a **floor**
35
34
  (`--anchor-width`/`--anchor-height`), never a width.
36
35
 
37
36
  ## The ceiling nobody sets
38
37
 
39
38
  Above every bound a caller passes there is one navi always applies: the
40
- container, minus `marginWithContainer` on both sides
41
- (`--dialog-maxmax-width` / `--dialog-maxmax-height`). It is not a default that
42
- a larger `maxWidth` overrides — it wins, always. A `minWidth` too large for the
43
- screen is clamped by it too, so **no combination of props can produce a dialog
44
- that overflows its container.** Stop trying to defend against that case.
45
-
46
- Two consequences worth knowing before writing CSS of your own:
47
-
48
- - the caps are applied to the dialog's **size**, not merely to its position.
49
- That is what makes a centered dialog follow the mobile virtual keyboard for
50
- free: the ceiling is expressed against the app's live screen
51
- (`--navi-app-width`/`--navi-app-height`, which track the visual viewport), so
52
- the browser reflows the dialog as the keyboard opens. Nothing to wire.
53
- - "the container" is the **app's own screen** for `layer="top"` — the visual
54
- viewport minus the app's bands, whether `--navi-app-max-width` centers them
55
- or the app writes `--navi-app-inset-*` itself (see
56
- [`safe_area.md`](./safe_area.md)) — and the positioned ancestor for
57
- `layer="local"`.
39
+ container, minus `marginWithContainer` on both sides. It is not a default that a
40
+ larger `maxWidth` overrides — it wins, always, and a `minWidth` too large for
41
+ the screen is clamped by it too, so **no combination of props can produce a
42
+ dialog that overflows its container.** Stop trying to defend against that case.
43
+
44
+ The caps are applied to the dialog's **size**, not merely to its position, and
45
+ expressed against the app's live screen (`--navi-app-width`/`--navi-app-height`,
46
+ which track the visual viewport): the browser reflows a centered dialog as the
47
+ mobile keyboard opens, with nothing to wire. "The container" is the **app's own
48
+ screen** for `layer="top"` — the visual viewport minus the app's bands (see
49
+ [`safe_area.md`](./safe_area.md)) — and the positioned ancestor for
50
+ `layer="local"`.
58
51
 
59
52
  ## One dialog, two shapes
60
53
 
@@ -69,9 +62,9 @@ below it, and the question and the field stay where the finger found them.
69
62
  reads and taps (a list, a couple of buttons, a confirmation) and that holds
70
63
  nothing raising a keyboard. A bottom sheet one types into is reflowed into the
71
64
  strip left above the keyboard at every keystroke, and the mistake is silent on
72
- the desktop the author tests on — which is why the safe edge is the default
73
- and the thumb-first one is the opt-in, and why a `"bottom"` sheet found holding
74
- a field is warned about in dev, on any screen.
65
+ the desktop the author tests on — which is why the safe edge is the default, and
66
+ why a `"bottom"` sheet found holding a field is warned about in dev, on any
67
+ screen.
75
68
 
76
69
  ```jsx
77
70
  <Dialog dockedOnSmallTouchScreen /> // top: the keyboard cannot reach it
@@ -82,36 +75,32 @@ The edge is the prop's own value rather than `positionArea` because
82
75
  `positionArea` places _both_ shapes: `positionArea="bottom"` also pins the
83
76
  centered box to the bottom of a desktop window.
84
77
 
85
- Both halves of the name matter. Touch alone would dock a tablet or a kiosk
86
- panel — a whole screen away from where the finger just tapped. Size alone would
87
- dock a narrow desktop window, which is still a mouse. `smallTouchScreenSignal`
88
- answers both, by shape rather than by a box of maximum dimensions (a phone is a
89
- narrow slab, in either orientation), and it is **live**: turning the phone
90
- re-resolves the dialog.
91
-
92
- What docking supplies — defaults only, so any single axis of the sheet can be
93
- adjusted without giving up the rest:
94
-
95
- | supplied | value | why |
96
- | --------------------- | -------- | ---------------------------------------------------- |
97
- | `positionArea` | the edge | `"top"` unless the prop says `"bottom"`, see above |
98
- | `marginWithContainer` | `0` | a sheet is flush, or it is not a sheet |
99
- | `expandX` | `true` | container-wide, same reason |
100
- | `scrollCapture` | `true` | a drag past the sheet's edge must not reach the page |
101
-
102
- A `layer="top"` dialog flush with an edge of the screen — a docked sheet,
103
- anything with `marginWithContainer={0}` — keeps the band the device
104
- reserves there (`env(safe-area-inset-*)`: the notch, the home indicator,
105
- Safari's floating bar). The surface still reaches the edge; what it holds stops
106
- at the band. Only the device's own inset: the dialog is in front of the app's
107
- fixed bars, so `--navi-safe-area-inset-*`, which counts them too (see
108
- [`safe_area.md`](./safe_area.md)), is not what it reads.
109
-
110
- Plus a swipe-to-close through the edge the sheet rests on (down for a bottom
111
- sheet, up for a top one), held by the sheet's `header` (a `Box` with the
112
- `header` prop) and by anything carrying `data-swipe-grip` — never by the whole
113
- sheet, so a board something is dragged across keeps its own gestures. See
114
- [`drag_to_travel.md`](./drag_to_travel.md).
78
+ Both halves of the name matter: touch alone would dock a tablet or a kiosk panel,
79
+ size alone a narrow desktop window, which is still a mouse.
80
+ `smallTouchScreenSignal` answers both, by shape rather than by a box of maximum
81
+ dimensions (a phone is a narrow slab, in either orientation). A `Dialog` follows
82
+ it live: turning the phone re-resolves it. A `Picker` decides popover or dialog
83
+ again at each opening, but a `Popup` decides once, for its lifetime — one
84
+ mounted as a popover never docks.
85
+
86
+ What docking supplies are defaults — `positionArea` (the edge),
87
+ `marginWithContainer={0}` (a sheet is flush, or it is not a sheet), `expandX`
88
+ and `scrollCapture` — so any single axis of the sheet can be adjusted without
89
+ giving up the rest.
90
+
91
+ A `layer="top"` dialog flush with an edge of the screen — `marginWithContainer`
92
+ at `0` with a `positionArea` on that edge or an `expand` across it, as a docked
93
+ sheet is — keeps the band the device reserves there (`env(safe-area-inset-*)`:
94
+ the notch, the home indicator, Safari's floating bar): the surface reaches the
95
+ edge, what it holds stops at the band. A centered dialog with a `0` margin
96
+ touches no edge, and gets no band. Only the device's own inset: the dialog is in
97
+ front of the app's fixed bars, so `--navi-safe-area-inset-*`, which counts them
98
+ too (see [`safe_area.md`](./safe_area.md)), is not what it reads.
99
+
100
+ A docked sheet closes by a swipe through the edge it rests on, held by its
101
+ `header` and by anything carrying `data-swipe-grip` — never by the whole sheet,
102
+ so a board something is dragged across keeps its own gestures (see
103
+ [`drag_to_travel.md`](./drag_to_travel.md)).
115
104
 
116
105
  **`expandY` (or `expand`) cancels docking outright.** A dialog already filling
117
106
  the height touches both edges; all docking could still do is take away the
@@ -131,17 +120,12 @@ Both halves are written together, and each applies where it means something:
131
120
  <Dialog dockedOnSmallTouchScreen minWidth="12rem" maxWidth="16rem">
132
121
  ```
133
122
 
134
- | prop | centered box | docked sheet |
135
- | ----------- | ------------ | ----------------------------------------------------- |
136
- | `maxWidth` | applies | **withdrawn** — the sheet is container-wide |
137
- | `minWidth` | applies | stops mattering (the floor is below full width) |
138
- | `maxHeight` | applies | applies — a sheet is content-tall, not container-tall |
139
- | `minHeight` | applies | applies |
140
-
141
123
  `maxWidth` is an answer about the _centered_ shape: "do not sprawl on a wide
142
- window". A sheet spanning its container's full width, flush against the two
143
- side edges, **is** the docked mode — so docking withdraws that ceiling rather
144
- than capping the sheet with it. The container ceiling still holds, as always.
124
+ window". A sheet spanning its container's full width **is** the docked mode, so
125
+ docking withdraws that ceiling rather than capping the sheet with it; the
126
+ container ceiling still holds. `minWidth` stops mattering there (the floor is
127
+ below full width), and `maxHeight`/`minHeight` keep applying — a sheet is
128
+ content-tall, not container-tall.
145
129
 
146
130
  > **Trap: do not re-derive the docking condition in the app.** This looks like
147
131
  > the way to say it and is subtly wrong:
@@ -156,18 +140,15 @@ than capping the sheet with it. The container ceiling still holds, as always.
156
140
  > dialog that also sets `expandY` never docks, so the cap must never be
157
141
  > withdrawn there, and the line above withdraws it anyway. Beyond being wrong,
158
142
  > it duplicates a condition navi owns at every call site (it drifts the day
159
- > "docked" gains a rule), and it reads as a bug: it says nothing about docked
160
- > sheets to the next person. State both bounds plainly and let the dialog
161
- > resolve its own shape.
143
+ > "docked" gains a rule), and it says nothing about docked sheets to the next
144
+ > person. State both bounds plainly and let the dialog resolve its own shape.
162
145
 
163
146
  ## `expand` is not "docked", and `expandX={false}` is not "do not sprawl"
164
147
 
165
148
  `expandX`/`expandY` mean "grow to the ceiling" — the container ceiling above,
166
149
  capped in turn by `maxWidth`/`maxHeight` when they apply. `expand` is the
167
- shorthand for both.
168
-
169
- Since docking _supplies_ `expandX`, passing it explicitly takes the caller out
170
- of that default:
150
+ shorthand for both. Since docking _supplies_ `expandX`, passing it explicitly
151
+ takes the caller out of that default:
171
152
 
172
153
  ```jsx
173
154
  // The sheet stops being flush: a floating box against its edge of the screen.
@@ -177,8 +158,7 @@ of that default:
177
158
  That is consistent — an explicitly passed prop wins over a docked default — and
178
159
  it is still the trap, because the two props read as answering different
179
160
  questions (one about the centered shape, one about the phone) when they answer
180
- the same one. If what you meant was "cap the centered box", that is `maxWidth`,
181
- and docking withdraws it on its own.
161
+ the same one. "Cap the centered box" is `maxWidth`.
182
162
 
183
163
  ## `marginWithContainer` decides the gap AND the ceiling
184
164
 
@@ -190,74 +170,51 @@ It defaults to a share of whatever holds the dialog (`3appw` for `layer="top"`,
190
170
  `3cqw` for `layer="local"`) and accepts a number of pixels, a viewport length —
191
171
  `appw`/`apph` being the app's own screen, `vvw`/`vvh` the visual viewport, which
192
172
  shrinks when the keyboard opens — or a container length (`cqw`/`cqh`). A
193
- spacing token (`"s"`, `"m"`…) cannot be resolved to pixels here: navi warns
194
- and places the dialog flush.
195
- Pass `0` for something meant to sit flush (a side panel), and note that docking
196
- already passes `0` for you.
173
+ spacing token (`"s"`, `"m"`…) cannot be resolved to pixels here: navi warns and
174
+ places the dialog flush. Pass `0` for something meant to sit flush (a side
175
+ panel); docking already passes it.
197
176
 
198
177
  ## Holding a size while the dialog is open
199
178
 
200
- `sizing="frozen"` measures the dialog once and holds it until it closes; what
201
- no longer fits — or no longer fills it — becomes the scroll's business. It is
202
- for a surface acted upon while it is open: marking a notification read,
203
- emptying a queue, swapping between two slides of different heights. The row
204
- being aimed at must not move under the finger.
205
-
206
- Two things make it safe to reach for:
207
-
208
- - the freeze writes a `width`/`height`, never a `min-*` — the caps above keep
209
- winning, so a frozen dialog still fits when the phone is turned;
210
- - the measure is taken at the first render where the prop says `"frozen"`, so a
211
- dialog opening on skeletons says `sizing={loading ? "auto" : "frozen"}` and
212
- is measured once the real content has arrived. Closing releases it.
179
+ `sizing="frozen"` measures the dialog once and holds it until it closes — for a
180
+ surface acted upon while it is open (emptying a queue, swapping between two
181
+ slides of different heights), where the row being aimed at must not move under
182
+ the finger. The caps above keep winning; a dialog opening on skeletons says
183
+ `sizing={loading ? "auto" : "frozen"}`, to be measured once the real content has
184
+ arrived.
213
185
 
214
186
  ## A `layer="local"` dialog answers to its container
215
187
 
216
- `layer="top"` (the default) is a real `<dialog>` in the browser's top layer,
217
- placed against the screen and clipped by nothing — modal, with everything
218
- behind made inert, unless `backdrop={false}` says otherwise (see
219
- [`popup_backdrop.md`](./popup_backdrop.md)). `layer="local"` stays in normal
220
- document flow, confined to and clipped by its own positioned ancestor.
188
+ `layer="top"` (the default) is placed against the screen and clipped by nothing;
189
+ `layer="local"` stays in normal document flow, confined to and clipped by its
190
+ own positioned ancestor, which every bound is then measured against (the
191
+ default margin in container units). Its container's scroll is locked while it
192
+ is open — its backdrop covers the scrollport, not the scrolled content — and
193
+ `scrollCapture` extends that lock to the whole page.
221
194
 
222
- For a **docked** dialog that is the choice of which edge the sheet rests on:
223
- `layer` names the container, and docking makes the sheet flush and full width
224
- against it. A sheet meant for the bottom of the screen is `layer="top"`, even
225
- when the page behind it must stay live — that is `backdrop={false}`, not
226
- `layer="local"`.
227
-
228
- For the shape, that swaps what every bound is measured against: the container
229
- becomes that ancestor's box, read through
230
- `--container-position-remaining-width`/`-height`, and the default margin is
231
- read in container units. Its own container's scroll is always locked while it
232
- is open — its backdrop covers the scrollport, not the scrolled content, so
233
- scrolling would slide the dialog away and reveal what the backdrop does not
234
- cover. `scrollCapture` is what extends that lock to the whole page.
235
-
236
- One accepted limitation, not an oversight: a dialog that is not modal —
237
- `layer="local"`, or `backdrop={false}` — **cannot** be dismissed by the
238
- hardware/gesture back button. No web API hooks into that outside the browser's
239
- own modal-dismissal stack, which only a genuine `showModal()` element joins.
195
+ For a **docked** dialog, `layer` is the choice of what the sheet rests against.
196
+ A sheet meant for the bottom of the screen is `layer="top"`, even when the page
197
+ behind it must stay live — that is `backdrop={false}`, not `layer="local"`.
198
+ Whether a dialog is modal, and what one that is not gives up (the back button),
199
+ is [`popup_backdrop.md`](./popup_backdrop.md#is-there-a-backdrop-at-all)'s.
240
200
 
241
201
  ## Reaching all of this through a `Picker` or a `SplitButton`
242
202
 
243
- A picker's popup is a popover or a dialog depending on the screen, so it
244
- exposes both sets of bounds under prefixed names, and they mean exactly what
245
- they mean on `Dialog` itself:
246
-
247
- `dialogMinWidth`, `dialogMinHeight`, `dialogMaxWidth`, `dialogMaxHeight`,
248
- `dialogExpand`, `dialogExpandX`, `dialogExpandY`, `dialogSizeFromAnchor`,
249
- `dockedOnSmallTouchScreen`, `marginWithContainer` — plus `popoverMaxHeight` and
250
- `popupWidthFitContent` for the other shape.
203
+ A picker's popup is a popover or a dialog depending on the screen, so it exposes
204
+ the dialog's bounds under prefixed names (`dialogMinWidth`, `dialogMaxWidth`,
205
+ `dialogExpandX`…), next to `dockedOnSmallTouchScreen` and `marginWithContainer`.
206
+ They mean what they mean on `Dialog`, with one exception: `dialogSizeFromAnchor`
207
+ makes the trigger's width the dialog's ceiling as well as its floor (see
208
+ [popup_lift.md](./popup_lift.md#same-width-or-a-wider-box)).
251
209
 
252
- `SplitButton` forwards the same set (minus `dialogSizeFromAnchor`) to the picker
253
- it wraps. Anything not in
254
- that set lands on the split button's own box instead — so a prop that seems to
255
- do nothing to the menu is worth checking against that list first (see
256
- `POPUP_PROP_SET` in `src/control/input/split_button.jsx`).
210
+ `SplitButton` forwards its popup props to the picker it wraps, and its menu is a
211
+ popover unless it says `mode="dialog"` — its `dialog*` props do nothing until
212
+ then. Anything not in its `POPUP_PROP_SET` (`src/control/input/split_button.jsx`)
213
+ lands on the split button's own box instead, so a prop that seems to do nothing
214
+ to the menu is worth checking against that list first.
257
215
 
258
216
  ---
259
217
 
260
218
  Reference: `src/layout/dialog.jsx` (the stylesheet at the top of the file holds
261
- the cap arithmetic), `src/layout/responsive.js` (`smallTouchScreenSignal`),
262
- `src/control/picker/picker_custom.jsx` (how the picker hands `dialogMin*`/
263
- `dialogMax*` to the dialog element), and `src/layout/demos/1_dialog_demo.html`.
219
+ the cap arithmetic), `src/layout/responsive.js` (`smallTouchScreenSignal`), and
220
+ `src/layout/demos/1_dialog_demo.html`.