@jsenv/navi 0.29.418 → 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 +192 -43
  3. package/dist/dev/jsenv_navi.js.map +10 -10
  4. package/dist/jsenv_navi.js +190 -41
  5. package/dist/jsenv_navi.js.map +10 -10
  6. package/docs/AI_INSTRUCTIONS.md +177 -261
  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 +115 -174
  16. package/docs/drag_to_travel.md +10 -9
  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 +6 -4
  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 -482
  35. package/docs/safe_area.md +102 -89
  36. package/docs/scroll.md +175 -335
  37. package/docs/state_binding.md +2 -1
  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
package/docs/safe_area.md CHANGED
@@ -60,9 +60,8 @@ Declared in `src/layout/safe_area.js`.
60
60
  An app that never spans the whole window — a phone-shaped column centered in a
61
61
  wide one, bands on the sides — has one problem with popups: a dialog lives in
62
62
  the browser's top layer, so it is calibrated on the _viewport_, and would paint
63
- 1500px of modal over a 600px app. The top bar and the bottom nav have the same
64
- problem and solve it by repeating the app width by hand; popups must not need
65
- that, because the app would then have to know which components exist.
63
+ 1500px of modal over a 600px app. Popups must not need to be told, or the app
64
+ would have to know which components exist.
66
65
 
67
66
  So the app states its own screen once, and never names a component:
68
67
 
@@ -76,57 +75,28 @@ So the app states its own screen once, and never names a component:
76
75
  The bands fall out of it (centered), `--navi-app-width` follows, and `FixedBar`
77
76
  pins itself to the column's edges rather than the glass. An app wanting them
78
77
  uneven writes `--navi-app-inset-left` / `-right` directly instead; everything
79
- below follows those the same way, popup placement included.
80
-
81
- Every popup follows: `Dialog`, `Popover`, and everything built on them
82
- (`Picker`, `Select`…). It is a ceiling and nothing more — on a screen narrower
83
- than the app it never binds, and each popup still subtracts its own
84
- `marginWithContainer` from it, so the gap with the edges is kept either way.
85
- That gap is itself a share of the app's screen, not of the window (`"3appw"`,
86
- navi's own unit alongside `vvw`/`vvh`) — otherwise a 3% margin measured on a
87
- 1500px window would eat 90px out of a 600px app.
88
-
89
- Two ways NOT to get this:
90
-
91
- - mounting empty `FixedBar area="left"/"right"`: they would reserve the room,
92
- but the app's rectangle would still be the whole window, so dialogs and
93
- popovers would keep sizing themselves against 1500px;
94
- - setting `--dialog-max-width` on `.navi_dialog` from the app. It is a
95
- `--component-*` token, declared on the element (see
96
- [css_architecture.md](./css_architecture.md#--navi--vs---component--where-the-override-has-to-go)):
97
- every dialog resets it on itself in an unlayered rule, so that a nested
98
- dialog does not inherit its parent's size, and a `maxWidth` prop writes it
99
- inline (a `Picker` under `dialogSizeFromAnchor` does) — an app rule of the
100
- same specificity does not reliably win, and the cap silently disappears. It
101
- is also the knob a single popup uses to ask for a specific size, not a
102
- ceiling:
103
- `--navi-app-max-width` feeds `--dialog-maxmax-width`, the hard ceiling _under_
104
- that knob, so a popup that genuinely needs its own `maxWidth` can still say
105
- so without escaping the app's screen.
106
-
107
- An app can also get all of it by rendering itself in an iframe of the target
108
- width: the viewport then genuinely _is_ the app's screen and no token is needed.
109
- `--navi-app-max-width` is the answer for an app that does not want to pay that
110
- price.
111
-
112
- #### Placement follows the same rectangle
113
-
114
- The app's rectangle moves where a popup is placed, not only how big it may
115
- get. Placement is computed against the visual viewport narrowed to the level-1
116
- bands: `getAppInsets` (`src/layout/responsive.js`) reads `--navi-app-inset-*`
117
- back off the computed style — the bands `--navi-app-max-width` centers and the
118
- ones an app writes by hand alike, in any length unit — navi hands them to
119
- `@jsenv/dom` once (`setPlacementViewportInsets`, wired in
120
- `navi_css_vars.js`), and `pickPositionRelativeTo` reads them on every
121
- placement. With centered bands it is invisible for anything centered on its
122
- cross axis — `center`, `bottom`, `top`, which is what a dialog does nearly
123
- always — and with uneven ones it is what centers that dialog on the app column
124
- rather than on the window. Either way it is what puts anything anchored to an
125
- edge (a `positionArea` like `bottom-start`, a `SidePanel`) flush against the
126
- app column's edge rather than the window's.
127
- `FixedBar` reads the same description through CSS instead: it is pinned to
128
- `--navi-app-inset-*`, which says where the app's rectangle is in the window
129
- rather than how wide it may be.
78
+ below follows those the same way.
79
+
80
+ Every popup follows — `Dialog`, `Popover`, and everything built on them
81
+ (`Picker`, `Select`…) — as a ceiling and nothing more: on a screen narrower
82
+ than the app it never binds. A `Dialog` keeps its `marginWithContainer` inside
83
+ that screen, and its default gap is a share of the app's screen, not of the
84
+ window (see
85
+ [dialog_shape.md](./dialog_shape.md#marginwithcontainer-decides-the-gap-and-the-ceiling));
86
+ a `Popover` is capped at 95% of it. Placement uses the same rectangle: a dialog
87
+ centers on the app column, and anything anchored to an edge (a `positionArea`
88
+ like `bottom-start`, a `SidePanel`) sits flush with the column's edge rather
89
+ than the window's.
90
+
91
+ Two ways NOT to get this: empty `FixedBar area="left"/"right"` reserve the
92
+ room, but the app's rectangle stays the whole window and popups keep sizing
93
+ themselves against it; and `--dialog-max-width` set from the app is a
94
+ `--component-*` token every dialog resets on itself (a `maxWidth` prop writes it
95
+ inline), so the cap silently disappears — see
96
+ [css_architecture.md](./css_architecture.md#--navi--vs---component--where-the-override-has-to-go).
97
+ `--navi-app-max-width` feeds the hard ceiling _under_ that knob, so a popup that
98
+ genuinely needs its own `maxWidth` can still say so without escaping the app's
99
+ screen.
130
100
 
131
101
  ### Something that scrolls under the furniture
132
102
 
@@ -148,16 +118,15 @@ gets the `scroll-padding` unconditionally, since the document is the scrollport
148
118
  in the common case — and, while nothing is marked, the keyboard's room at its
149
119
  end (see below).
150
120
 
151
- Beware of making that container scrollable by accident — see
152
- [mobile_layout_pitfalls.md](./mobile_layout_pitfalls.md).
121
+ Beware of making that container a scroller by accident — see
122
+ [A document wider than the screen](#a-document-wider-than-the-screen).
153
123
 
154
124
  ### Reading it yourself
155
125
 
156
126
  `var(--navi-safe-area-inset-bottom)` in any rule. It is always declared, whether
157
127
  or not the app ever mounts a bar.
158
128
 
159
- From **JS**, both levels are registered as lengths (`@property`, in
160
- `safe_area.js`), so
129
+ From **JS**, both levels are registered as lengths (`@property`), so
161
130
  `getComputedStyle(document.documentElement).getPropertyValue("--navi-safe-area-inset-bottom")`
162
131
  gives pixels, and so does `--navi-app-inset-*`.
163
132
 
@@ -165,10 +134,7 @@ gives pixels, and so does `--navi-app-inset-*`.
165
134
 
166
135
  Level 2 has one slot per edge, `--navi-fixed-bar-space-*`, and it belongs to
167
136
  `FixedBar`: each bar measures its own border box (notch included), and navi
168
- writes the largest one on that edge inline on `<html>`
169
- (`src/layout/fixed_bar/fixed_bar_space.js`). The slot is combined with
170
- `env(safe-area-inset-*)` through `max()`, not added to it, since a bar measured
171
- with its notch already covers the notch.
137
+ writes the largest one on that edge inline on `<html>`.
172
138
 
173
139
  Something else taking an edge — a native banner, an OS strip — is published by
174
140
  being drawn as a `FixedBar`; every component reading the safe area then clears
@@ -176,19 +142,6 @@ it without learning it exists. Writing the slot by hand holds only while no bar
176
142
  takes that edge: the first bar mounting there replaces the value rather than
177
143
  adding to it.
178
144
 
179
- ## What already reads it
180
-
181
- Pointers, not a list to keep in sync — grep `--navi-safe-area-inset` for the
182
- truth:
183
-
184
- - `FixedBar` pins itself to `--navi-app-inset-*`.
185
- - `List` offsets its sticky group labels when `scroller="document"`, so a label
186
- comes to rest in front of the bar and not behind it.
187
- - `RouteTravel` clips the pictures of a travel to the safe area. It has to: a
188
- view transition paints in the top layer, where no `overflow` of the document
189
- reaches it, and the box pages travel in runs _under_ the bars by design — so
190
- a page scrolled by one pixel would be watched painting over them.
191
-
192
145
  ## The trap: which viewport
193
146
 
194
147
  Three heights are in play and they are not the same one — and which of them
@@ -203,24 +156,20 @@ the keyboard moves depends on the browser.
203
156
  By default the keyboard shrinks the visual viewport, on every browser, and
204
157
  `--navi-keyboard-inset-bottom` stays 0. Where the browser has the VirtualKeyboard
205
158
  API (Chromium), an app can call `enableVirtualKeyboardOverlay()` to make the
206
- keyboard overlay the page instead (`src/layout/virtual_keyboard.js`): no viewport
207
- shrinks, and the keyboard arrives as `--navi-keyboard-inset-bottom`
208
- (`env(keyboard-inset-height)`), which `--navi-app-inset-bottom` adds. Either way
209
- `--navi-app-height` and the popup ceilings answer the part of the screen left
210
- visible.
159
+ keyboard overlay the page instead: no viewport shrinks, and the keyboard arrives
160
+ as `--navi-keyboard-inset-bottom` (`env(keyboard-inset-height)`), which
161
+ `--navi-app-inset-bottom` adds. Either way `--navi-app-height` and the popup
162
+ ceilings answer the part of the screen left visible.
211
163
 
212
164
  An overlaying keyboard is one the browser no longer scrolls the focused field
213
165
  out from under, so navi does: when the keyboard rises or resizes, and when focus
214
166
  moves to another field with the keyboard up, a focused field that is not fully
215
- inside the document's `scroll-padding` box is centered in it. Chrome paints an
216
- autofill/suggestion strip above the keyboard that neither
217
- `env(keyboard-inset-height)` nor `geometrychange` counts, so
218
- `scroll-padding-bottom` adds `--navi-keyboard-strip-allowance` on top of the
219
- keyboard — which also keeps Chrome's own caret-following, as a textarea grows
220
- under typing, clear of the strip. The room to scroll into comes from the marked
221
- element's `padding-bottom`, or, on a page that marked nothing, from a block navi
222
- adds at the end of the document while the keyboard is up — both count the
223
- keyboard and the allowance.
167
+ inside the document's `scroll-padding` box is centered in it.
168
+ `scroll-padding-bottom` also adds `--navi-keyboard-strip-allowance`, for the
169
+ suggestion strip Chrome paints above the keyboard and no inset counts. The room
170
+ to scroll into comes from the marked element's `padding-bottom`, or, on a page
171
+ that marked nothing, from a block navi adds at the end of the document while
172
+ the keyboard is up.
224
173
 
225
174
  `position: fixed` — so every `FixedBar` — is laid out against the **layout**
226
175
  viewport. Where the visual viewport is what shrinks, a bottom bar therefore
@@ -238,3 +187,67 @@ inset) because what navi _sizes_ must fit what is actually visible.
238
187
  `src/layout/demos/fixed_bar/keyboard.html` puts all of these on screen at once
239
188
  and turns the bottom bar's number red when it goes under the keyboard. On a
240
189
  phone; a desktop has no keyboard to open.
190
+
191
+ ## A document wider than the screen
192
+
193
+ On Chrome Android, **anything that makes the document overflow horizontally
194
+ inflates the layout viewport** to the size of the content: a 2000px-wide grid
195
+ on a 412px phone takes `window.innerWidth` and `innerHeight` to about four times
196
+ the screen, while `visualViewport.width` still says 412. The document grows a
197
+ large empty area below the real content, and what is centered in the layout
198
+ viewport — a `<dialog>`'s `position: fixed; margin: auto` — lands far below the
199
+ visible area, miscentered or out of sight.
200
+
201
+ So the document itself never overflows in x. Wrap the app in a box that clips,
202
+ and clip `html` and `body` as a net for anything that escapes the wrapper:
203
+
204
+ ```html
205
+ <body>
206
+ <div style="overflow-x: clip">
207
+ <!-- all app content goes here -->
208
+ </div>
209
+ </body>
210
+ ```
211
+
212
+ ```css
213
+ html,
214
+ body {
215
+ overflow-x: clip;
216
+ }
217
+ ```
218
+
219
+ What it costs: content wider than the screen is cut off instead of reachable by
220
+ dragging. An element that genuinely needs to scroll horizontally (a wide table,
221
+ a carousel) gets its own `overflow-x: auto` — the wrapper stays `clip`.
222
+
223
+ **`clip`, never `auto`, `hidden` or `scroll`.** `clip` is the only value that
224
+ clips without turning the box into a scroll container (and it leaves
225
+ `overflow-y` at `visible`, where the others force it to `auto`). A wrapper that
226
+ scrolls, even by accident, takes every `position: sticky` in the app with it:
227
+ sticky resolves against the nearest scroll container, and this one grows with
228
+ its content and never scrolls, so sticky headers and `<List groupBy>` labels
229
+ scroll away with the content. Worse, a sticky element sticks within its scroll
230
+ container's box shrunk by that container's `scroll-padding`: a wrapper carrying
231
+ `data-navi-safe-area` has `scroll-padding-top: var(--navi-safe-area-inset-top)`,
232
+ so the labels come to rest that far below the bar, covering the content above
233
+ them.
234
+
235
+ **The wrapper is a net, not a fix.** Clipping makes the symptom disappear, and
236
+ with it the signal: something wider than the screen — a width in px, a
237
+ `min-width`, a grid of fixed columns, an unbreakable string coming from the
238
+ data — is still a layout bug. In dev, ask who overflows:
239
+
240
+ ```js
241
+ import { detectHorizontalOverflow } from "@jsenv/navi";
242
+
243
+ if (import.meta.dev) {
244
+ detectHorizontalOverflow({ root: document.querySelector("#main") });
245
+ }
246
+ ```
247
+
248
+ It outlines the culprits in red and names them in the console, at load and
249
+ whenever the layout changes. It reports the **outermost** box that sticks out
250
+ (its children stick out because it does), and stays quiet about what cannot
251
+ reach the document: anything inside a box that scrolls or clips on its own — a
252
+ wide table in its own `overflow-x: auto` container is doing the right thing —
253
+ and anything `position: fixed` or in the top layer.