@jsenv/navi 0.29.52 → 0.29.53

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.
@@ -73,9 +73,28 @@ consistency across the app, not from any single call site.
73
73
  and how a paginated `<List.Items>` re-reads its slices without disappearing.
74
74
  Read it before adding verbs to `rerunOn`, hiding a list on `loading`, or
75
75
  remounting a list with a `key` to refresh it.
76
+ - `docs/error_handling.md` — the two kinds of error and how navi keeps them
77
+ apart: where an error is shown depending on where it came from (a control's
78
+ action shows it on what was clicked, a route action replaces the page, a
79
+ refused value is validation and not an error at all), why a run never rejects,
80
+ the `__handled_by__` mark that means "this error is on screen somewhere" (and
81
+ what `preact/debug` throws over your app without it), what becomes of an error
82
+ nobody displays, and the two rules any error boundary must follow — mark only
83
+ what you render, reset on the document URL and not only on the rerun. Read it
84
+ before displaying an error by hand, before writing an error boundary of your
85
+ own, and before concluding that a dev overlay over a page that already shows
86
+ its error is a crash.
76
87
  - `docs/css_architecture.md` — how Navi's CSS layering works, and the
77
88
  supported ways to override component styles (props > CSS variables > direct
78
89
  rule overrides, in that preference order).
90
+ - `docs/safe_area.md` — where the app is in the window and what covers it:
91
+ the two inset families (`--navi-app-inset-*` for what is pinned to an edge,
92
+ `--navi-safe-area-inset-*` for what flows inside), how an app declares itself
93
+ narrower than the window, `data-navi-safe-area`, how something new joins the
94
+ sum, and which viewport is which once a virtual keyboard is open. Read it
95
+ before hand-writing an offset to clear a `FixedBar`, before reaching for
96
+ `env(safe-area-inset-*)` directly, and before making an app simulate a phone
97
+ screen.
79
98
  - `docs/scroll.md` — where scrolling happens: what turns `Box`
80
99
  `header`/`body`/`footer` on, `FixedBar` space, `List`'s `scroller`, scroll
81
100
  inside a `Dialog`/`Popover`, and what a scroll does to hover
@@ -185,6 +204,25 @@ consistency across the app, not from any single call site.
185
204
  does this for its current-tab indicator automatically
186
205
  (`currentIndicator`), which is why the bar follows a `RouteTravel` swipe
187
206
  with no wiring.
207
+ - the document's rendering is SUSPENDED for the whole update callback, from
208
+ the capture of the old state to the capture of the new one. Nothing paints,
209
+ and `requestAnimationFrame` does not tick in there — awaiting a frame
210
+ inside the callback awaits something that cannot happen, until the browser
211
+ gives up on the transition entirely (`Transition was aborted because of
212
+ timeout in DOM update`). Await a microtask, a task or a render; never a
213
+ frame.
214
+ - that suspension lasts exactly as long as the callback, and **in a
215
+ sub-document it takes the scrollbar with it**: an iframe's scrollbar is
216
+ painted by the framed document, so it goes and comes back, shifting the
217
+ layout by its width. A top-level page is spared — its root scrollbar is the
218
+ compositor's. So a callback that waits on the network flickers every demo
219
+ shown in an iframe while the same app, opened on its own, shows nothing.
220
+ Keep the callback short, and suspect the frame before the code when a
221
+ scrollbar blinks.
222
+ - `viewTransition.finished` REJECTS when another transition replaces this one
223
+ — there is only ever one per document. `.finally()` does not handle a
224
+ rejection, so an unhandled one is what it leaves behind; `.then(done, done)`
225
+ is the shape that ends a transition whichever way it went.
188
226
 
189
227
  If unsure which export solves a problem, check `README.md` first: it names what
190
228
  navi provides, area by area. From a name, the built export tells you the API and
@@ -82,8 +82,8 @@ far from the wrapper:
82
82
  - **Worse than not sticking: the sticky element is offset downwards.** The rectangle a
83
83
  sticky element sticks within is the scroll container's box shrunk by its
84
84
  `scroll-padding` (CSS Position L3), and Chromium applies that for an element scroll
85
- container. A wrapper carrying `data-navi-fixed-bar-space` has
86
- `scroll-padding-top: var(--navi-fixed-bar-space-top)`, so labels come to rest at
85
+ container. A wrapper carrying `data-navi-safe-area` has
86
+ `scroll-padding-top: var(--navi-safe-area-inset-top)`, so labels come to rest at
87
87
  `scroll-padding-top + top` — a group label floating a hundred pixels below the bar,
88
88
  covering the content above it.
89
89
 
package/docs/actions.md CHANGED
@@ -81,7 +81,9 @@ const [user] = useAsyncData(userAction);
81
81
  `useAsyncData` suspends until the data is there and throws on failure, leaving
82
82
  both to the nearest `<Loading>` and `<ErrorBoundary>`; pass `{ loading: true }`
83
83
  or `{ error: true }` to handle either inside the component (stale data stays
84
- available while a rerun is in flight). `useActionStatus(action)` gives the whole
84
+ available while a rerun is in flight). Where a failure goes when no screen takes
85
+ it — and why a run settles with its error rather than rejecting — is
86
+ [error_handling.md](./error_handling.md). `useActionStatus(action)` gives the whole
85
87
  state at once — `{ idle, loading, completed, aborted, error, data, params }` —
86
88
  for a component that needs to look at it rather than render it.
87
89
 
@@ -138,7 +138,9 @@ Two things to know or the screen stays empty:
138
138
  - **Handle the error where it happens**, or hand it to an `<ErrorBoundary>`:
139
139
  `useAsyncData(action, { loading: true, error: true })` returns
140
140
  `[data, loading, error]`, which is what lets a page draw its own "the server
141
- refused" with a "try again" that calls `action.rerun()`.
141
+ refused" with a "try again" that calls `action.rerun()`. Which of the two, and
142
+ what happens to an error no screen takes:
143
+ [error_handling.md](./error_handling.md).
142
144
 
143
145
  ## Two screens, two states
144
146
 
@@ -222,14 +222,16 @@ is placed. Placement is still computed against the real viewport
222
222
  (`pickPositionRelativeTo`, in `@jsenv/dom`). That is invisible for anything
223
223
  centered on its cross axis — `center`, `bottom`, `top`, which is what a dialog
224
224
  does nearly always — but shows for anything anchored to an edge: a
225
- `positionArea` like `bottom-start`, a `SidePanel`, a fixed bar. Those sit
226
- against the window's edge rather than the app column's, so they stay on the real
227
- viewport for now (`side_panel.jsx` restates `--dialog-maxmax-width` as the full
228
- viewport on purpose).
229
-
230
- Making them follow the app column too means narrowing the container rect
231
- placement is computed against, inside `pickPositionRelativeTo` worth doing the
232
- day a side panel or a fixed bar has to live inside a simulated screen.
225
+ `positionArea` like `bottom-start`, a `SidePanel`. Those sit against the
226
+ window's edge rather than the app column's, so they stay on the real viewport
227
+ for now (`side_panel.jsx` restates `--dialog-maxmax-width` as the full viewport
228
+ on purpose).
229
+
230
+ `FixedBar` is the exception, and shows the shape of the fix: it is pinned to
231
+ `--navi-app-inset-*` (see `docs/safe_area.md`), which describes where the app's
232
+ rectangle is in the window rather than how wide it may be. Making the rest
233
+ follow means narrowing the container rect placement is computed against, inside
234
+ `pickPositionRelativeTo`, to that same rectangle.
233
235
 
234
236
  Note that an app can already get all of it, placement included, by rendering
235
237
  itself in an iframe of the target width: the viewport then genuinely _is_ the
@@ -0,0 +1,174 @@
1
+ # Error handling
2
+
3
+ There are two kinds of error in an app, and everything here exists to keep them
4
+ apart:
5
+
6
+ - **what the person can see and act on** — offline, a 404, the server refusing
7
+ a value. That is data. It must be displayed, and displayed where it makes
8
+ sense for what just happened;
9
+ - **a bug** — a `TypeError`, a contract broken. That must be as loud as
10
+ possible, and must never be swallowed by the machinery built for the first
11
+ kind.
12
+
13
+ The rule the whole thing rests on: **displaying an error is claiming it.**
14
+ Whatever puts an error on screen says so, and an error nobody claimed is
15
+ treated as a bug. Nothing has to guess which kind it is, and nothing has to
16
+ guess at the wrong moment — the answer is given after the fact by what actually
17
+ rendered.
18
+
19
+ ## Where an error appears depends on where it came from
20
+
21
+ ### Someone just acted — a control's action
22
+
23
+ A `<Button action>`, a `<Form>` submit: the person did something and is waiting
24
+ for the answer, so the answer belongs where they acted. navi shows it as a
25
+ validation message on the **requester** (the submit button, not the form):
26
+
27
+ ```jsx
28
+ <Button action={GAME.POST.bindParams(values)}>Créer</Button>
29
+ ```
30
+
31
+ - `errorMapping(error)` turns the raw error into what to show: a string, an
32
+ `Error`, an element, or `{ message, target }` to point the message at another
33
+ element.
34
+ - The message is a constraint on the control (`navi_action_error`) rather than a
35
+ render of its own, and it **auto-resets on the next action**: hitting submit
36
+ again clears it and re-submits, instead of the form being stuck invalid.
37
+ - `actionErrorEffect` changes that: `"throw"` re-throws the error from a layout
38
+ effect so the nearest `<ErrorBoundary>` takes it (a control cannot throw from
39
+ an event handler and be caught — see the comment in
40
+ [use_execute_action.js](../src/action/use_execute_action.js)), and anything
41
+ else — `"none"` — means the control displays nothing because something else
42
+ does. `<Details>` uses that: its `<ActionRenderer>` shows the error inside the
43
+ open panel.
44
+
45
+ ### A screen's data failed — a route action
46
+
47
+ Nobody clicked; the page simply cannot be drawn. It is the page, or a piece of
48
+ it, that is replaced — by an ancestor, never by a prop on the page. That is what
49
+ `useAsyncData`'s default means: **delegate**. The component says what it renders
50
+ when it has data, and what it cannot render is somebody else's job, up the tree:
51
+
52
+ ```jsx
53
+ <Route>
54
+ <ErrorBoundary
55
+ fallback={({ error, resetError }) => (
56
+ <ErrorScreen error={error} onRetry={resetError} />
57
+ )}
58
+ >
59
+ <Loading fallback={<GameSkeleton />}>
60
+ <Route route={GAME_ROUTE} element={GamePage} />
61
+ <Route route={GAMES_ROUTE} element={GamesPage} />
62
+ </Loading>
63
+ </ErrorBoundary>
64
+ </Route>
65
+ ```
66
+
67
+ Two things about that shape:
68
+
69
+ - The boundary goes **outside** the `<Loading>`. A page suspends first and fails
70
+ second; a boundary placed under the `Suspense` it suspended in is part of the
71
+ tree being held.
72
+ - It is written **between** a container `<Route>` and its branches, and the
73
+ container reads through it — the selected branch keeps whatever was written
74
+ around it. So a boundary can bracket a section of the pages instead of the
75
+ whole router, without the router having to know it exists.
76
+
77
+ Inside the component when the error is part of what the screen draws rather than
78
+ a screen of its own:
79
+
80
+ ```jsx
81
+ const [game, loading, error, dismissError] = useAsyncData(gameAction, {
82
+ loading: true,
83
+ error: true,
84
+ });
85
+ ```
86
+
87
+ And one row rather than the whole list: `<List.Item error>` replaces the row's
88
+ content with the message — what the row stood for did not happen, so drawing it
89
+ as if it had would be a lie.
90
+
91
+ ### A value is refused — validation
92
+
93
+ Not an error at all, and not to be modelled as one: a value the app or the
94
+ browser refuses is a **status on the control**, carried by navi's constraint
95
+ validation. Never throw to reject a value — see `src/control/rules/`.
96
+
97
+ ## What a failing action does
98
+
99
+ It writes the error into `errorSignal`, moves to `FAILED`, and stops.
100
+
101
+ **A run never rejects.** `run()`, `rerun()`, `prerun()` settle with the error as
102
+ their value. Nothing about a failed action arrives as a rejected promise, so
103
+ there is no floating rejection to catch and no navigation that has to await one
104
+ to stay quiet. Code that wants the error asks for it — `useAsyncData`,
105
+ `errorSignal`, or an `onError` passed to the run.
106
+
107
+ Why it does no more than that: **at the instant an action fails, nothing can
108
+ know whether a screen will display it.** A route action runs before its page
109
+ renders — often before that page exists — so any decision made there about "will
110
+ someone show this?" is a guess, and it is wrong for the most common case in the
111
+ app. So no decision is made there.
112
+
113
+ ## Claiming an error: `__handled_by__`
114
+
115
+ Whatever displays the error sets `error.__handled_by__` (via
116
+ `markErrorAsDisplayedBy` in
117
+ [action_error_report.js](../src/action/action_error_report.js)). That mark means
118
+ one thing and only one: **this error is on screen somewhere.**
119
+
120
+ It is the same mark the jsenv supervisor reads to keep its dev overlay out of
121
+ the way. It has to exist because `preact/debug` re-throws, in a `setTimeout`,
122
+ **every error a boundary handled** — deliberately, so that React-style dev
123
+ overlays keep working:
124
+
125
+ ```js
126
+ // when an error was handled by an ErrorBoundary we will nonetheless emit an error
127
+ // event on the window object. This is to make up for react compatibility in dev mode
128
+ // and thus make the Next.js dev overlay work.
129
+ ```
130
+
131
+ Without the mark, an app calmly displaying "you are offline" gets a crash
132
+ overlay thrown over it.
133
+
134
+ Everything navi ships already marks: `<ErrorBoundary>` when it renders its
135
+ fallback, `<ActionRenderer>` / `<Button action>` when they render their error
136
+ branch, `useAsyncData({ error: true })`
137
+ when it hands the error to the component, and a run given an `onError` — asking
138
+ for the error is taking it.
139
+
140
+ ## Claimed by nobody: the report
141
+
142
+ Unclaimed errors are re-thrown from a macrotask, which makes them ordinary
143
+ unhandled errors — window `error` event, jsenv overlay in dev — pointing at the
144
+ code that produced them.
145
+
146
+ The delay is the whole mechanism: every render that could display the error
147
+ (Preact's queue, a `Suspense` boundary settling on the failure, the boundary
148
+ above it) happens in microtasks, so one macrotask later the answer is final. A
149
+ screen that mounts much later than that — mounted by something slower than a
150
+ render — gets its error reported anyway; the report is then a duplicate of what
151
+ it shows, never a lie about it. The same error reaching the reporter twice is
152
+ reported once.
153
+
154
+ ## Writing your own boundary
155
+
156
+ navi's `<ErrorBoundary>` is what a boundary must be, and both of its rules are
157
+ easy to miss when writing another one:
158
+
159
+ - **Mark only what you actually render.** Marking on catch, before knowing
160
+ whether anything is displayed, turns a boundary into a bug swallower: a
161
+ `TypeError` in a component becomes a blank page AND a silent one, since the
162
+ mark muted the overlay that would have pointed at it. navi's boundary marks
163
+ just before rendering its fallback, and re-throws untouched when it has no
164
+ fallback — nothing to display means nothing handled.
165
+ - **Reset on the document URL, not only on the rerun.** Re-running the failed
166
+ action is one way out; going somewhere else is the common one. A boundary that
167
+ only watches the action stays in place of every page after the failure,
168
+ including the ones that would render fine.
169
+
170
+ The one reason to write your own: **filtering what you take.** navi's takes
171
+ everything its subtree throws. An app that wants its own bugs to stay visible
172
+ displays only what is data to it — an error carrying an HTTP status, its own
173
+ `OfflineError` — and re-throws the rest **unmarked**, so the overlay still does
174
+ its job.
@@ -190,16 +190,37 @@ Two shapes for a section, and which one applies is decided by the URL:
190
190
 
191
191
  ### Loading data
192
192
 
193
- A branch loads with `action`, and shows its states with the usual boundaries:
193
+ A page reads its data from a route action through `useAsyncData` and says only
194
+ what it renders; what it cannot render is delegated to an ancestor — waiting to
195
+ `<Loading>`, failing to `<ErrorBoundary>`. Both are written **between** the
196
+ container and its branches, and the container reads through them, so a whole
197
+ section of pages shares one:
194
198
 
195
199
  ```jsx
196
- <ErrorBoundary fallback={(error, { resetError }) => …}>
197
- <Suspense fallback={<p>Loading…</p>}>
198
- <Route route={GAME_ROUTE} action={loadGame} element={(game) => <GamePage game={game} />} />
199
- </Suspense>
200
- </ErrorBoundary>
200
+ <Route>
201
+ <ErrorBoundary
202
+ fallback={({ error, resetError }) => (
203
+ <ErrorScreen error={error} onRetry={resetError} />
204
+ )}
205
+ >
206
+ <Loading fallback={<GameSkeleton />}>
207
+ <Route route={GAME_ROUTE} element={GamePage} />
208
+ <Route route={GAMES_ROUTE} element={GamesPage} />
209
+ </Loading>
210
+ </ErrorBoundary>
211
+ <Route fallback element={NotFoundPage} />
212
+ </Route>
201
213
  ```
202
214
 
215
+ The order matters: the boundary goes **outside** the `<Loading>`. A page
216
+ suspends first and fails second, and a boundary placed under the `Suspense` it
217
+ suspended in is part of the tree being held.
218
+
219
+ A branch selected inside a wrapper keeps it — the container renders the active
220
+ branch alone, wrapper included, so `<Loading>`/`<ErrorBoundary>` can bracket a
221
+ subset of the branches rather than the whole router. What happens to a failure
222
+ no boundary takes: [error_handling.md](./error_handling.md).
223
+
203
224
  ## Links and tab rows
204
225
 
205
226
  `<Link route={…}>` builds its href from the route and knows on its own whether it
@@ -0,0 +1,161 @@
1
+ # The safe area: where the app is, and what covers it
2
+
3
+ An app is rarely given the whole window. A bar is pinned over it, the device
4
+ eats a corner, the app itself pretends to be a phone inside a desktop window.
5
+ Every component that must stay clear of all that would otherwise have to learn
6
+ what "all that" is — and each one would learn a different subset.
7
+
8
+ So navi publishes it, once, as CSS variables on `<html>`. Whoever reduces the
9
+ visible region says so; whoever must avoid it reads the sum and never learns
10
+ what is covering it.
11
+
12
+ ## The two levels
13
+
14
+ There are two rectangles, and confusing them is the mistake this file exists to
15
+ prevent.
16
+
17
+ ```
18
+ ┌────────────────────── window ───────────────────────┐
19
+ │ │ │ │
20
+ │ band │ ┌───────── FixedBar top ─────────┐ │ band │ ← --navi-app-inset-*
21
+ │ │ ├────────────────────────────────┤ │ │
22
+ │ │ │ │ │ │
23
+ │ │ │ the safe area │ │ │ ← --navi-safe-area-inset-*
24
+ │ │ │ │ │ │
25
+ │ │ └──────── FixedBar bottom ───────┘ │ │
26
+ └─────────────────────────────────────────────────────┘
27
+ ```
28
+
29
+ **`--navi-app-inset-{top,right,bottom,left}`** — from the window's edges to the
30
+ app's own rectangle. What is _pinned to an edge_ is pinned to this.
31
+
32
+ **`--navi-safe-area-inset-{top,right,bottom,left}`** — from the window's edges
33
+ to the band left free _inside_ that rectangle. What _flows, scrolls or gets
34
+ painted_ keeps to this.
35
+
36
+ Two and not one, because a fixed bar is one of the things that reduce the free
37
+ band: placed against the band it contributes to, it would push itself off its
38
+ own edge. What is anchored and what is anchored-inside cannot be the same
39
+ number.
40
+
41
+ Level 2 is level 1 plus everything on that edge:
42
+
43
+ ```css
44
+ --navi-safe-area-inset-top: calc(
45
+ var(--navi-app-inset-top) +
46
+ max(env(safe-area-inset-top), var(--navi-fixed-bar-space-top))
47
+ );
48
+ ```
49
+
50
+ `max()` and not a sum between the notch and the bars: a bar pinned to an edge
51
+ already reaches under the notch and counts it in its own height, so adding both
52
+ would reserve it twice.
53
+
54
+ Declared in `src/layout/safe_area.js`.
55
+
56
+ ## Using it
57
+
58
+ ### An app that is narrower than the window
59
+
60
+ One line, and nothing names a component:
61
+
62
+ ```css
63
+ :root {
64
+ --navi-app-max-width: 600px;
65
+ }
66
+ ```
67
+
68
+ The bands fall out of it (centered), `--navi-app-width` follows, and `FixedBar`
69
+ pins itself to the column's edges rather than the glass. An app wanting them
70
+ uneven writes `--navi-app-inset-left` / `-right` directly instead.
71
+
72
+ Do **not** get this by mounting empty `FixedBar area="left"/"right"`: they would
73
+ reserve the room, but the app's rectangle would still be the whole window, so
74
+ dialogs and popovers would keep sizing themselves against 1500px.
75
+
76
+ ### Something that scrolls under the furniture
77
+
78
+ Mark it, and it gets both paddings:
79
+
80
+ ```html
81
+ <div id="main" data-navi-safe-area>…</div>
82
+ ```
83
+
84
+ Two distinct things must be given back, and forgetting the second is the classic
85
+ bug — `padding`, or the last screenful stays unreachable under the bar; and
86
+ `scroll-padding`, or everything the browser scrolls _to_ (an anchor,
87
+ `scrollIntoView()`, a field taking focus, a restored position) lands _behind_ it.
88
+ The padding does not help there: it moves the content, not the place the browser
89
+ brings its target to.
90
+
91
+ Which element scrolls is the app's business, so navi never picks one. `:root`
92
+ gets the `scroll-padding` unconditionally, since the document is the scrollport
93
+ in the common case.
94
+
95
+ Beware of making that container scrollable by accident — see
96
+ `MOBILE_LAYOUT_PITFALLS.md`.
97
+
98
+ ### Reading it yourself
99
+
100
+ `var(--navi-safe-area-inset-bottom)` in any rule. It is always declared, whether
101
+ or not the app ever mounts a bar.
102
+
103
+ Reading it from **JS** takes a probe: an unregistered custom property keeps its
104
+ `calc()` unresolved through `getComputedStyle`. Give a hidden box
105
+ `height: var(--navi-safe-area-inset-bottom)` and measure it — see
106
+ `src/layout/demos/fixed_bar/keyboard.html`.
107
+
108
+ ### Putting something new into it
109
+
110
+ Publish what you take on one edge, into that edge's slot. That is the whole
111
+ contract — a native banner, an OS strip, anything an app invents joins the sum
112
+ without a single component learning it exists. `FixedBar` is the worked example:
113
+ it measures its own border box (notch included) and writes
114
+ `--navi-fixed-bar-space-*` (`src/layout/fixed_bar/fixed_bar_space.js`).
115
+
116
+ ## What already reads it
117
+
118
+ Pointers, not a list to keep in sync — grep `--navi-safe-area-inset` for the
119
+ truth:
120
+
121
+ - `FixedBar` pins itself to `--navi-app-inset-*`.
122
+ - `List` offsets its sticky group labels when `scroller="document"`, so a label
123
+ comes to rest in front of the bar and not behind it.
124
+ - `RouteTravel` clips the pictures of a travel to the safe area. It has to: a
125
+ view transition paints in the top layer, where no `overflow` of the document
126
+ reaches it, and the box pages travel in runs _under_ the bars by design — so
127
+ a page scrolled by one pixel would be watched painting over them.
128
+
129
+ ## The trap: which viewport
130
+
131
+ Three heights are in play and they are not the same one.
132
+
133
+ | what | shrinks when the keyboard opens |
134
+ | -------------------------------- | ------------------------------- |
135
+ | `window.innerHeight` / `100dvh` | no |
136
+ | `visualViewport.height` | yes |
137
+ | `--navi-vvh` (tracks the visual) | yes |
138
+
139
+ `position: fixed` — so every `FixedBar` — is laid out against the **layout**
140
+ viewport. A bottom bar therefore stays at the bottom of a window the keyboard is
141
+ covering: it ends up _behind_ the keyboard, and no inset says so, because
142
+ nothing reduced the layout viewport.
143
+
144
+ The consequence for anything measuring against the insets: mix the two families
145
+ and you get a drift that only appears with a keyboard open. `getBoundingClientRect`
146
+ is in layout-viewport coordinates, so what is compared to it must be too
147
+ (`100dvh`), while `--navi-app-*` derives from `--navi-vvh` because what navi
148
+ _sizes_ must fit what is actually visible.
149
+
150
+ `src/layout/demos/fixed_bar/keyboard.html` puts all of these on screen at once
151
+ and turns the bottom bar's number red when it goes under the keyboard. On a
152
+ phone; a desktop has no keyboard to open.
153
+
154
+ ## Current limitation
155
+
156
+ Popup **placement** does not follow level 1 yet: `pickPositionRelativeTo` (in
157
+ `@jsenv/dom`) still computes against the real viewport. Invisible for anything
158
+ centered on its cross axis — which is what a dialog does nearly always — but a
159
+ `positionArea` like `bottom-start` or a `SidePanel` sits against the window's
160
+ edge rather than the app column's. See "Current limitations" in
161
+ `css_architecture.md`.
package/docs/scroll.md CHANGED
@@ -71,14 +71,19 @@ Reference: `src/box/box.jsx` (the `[data-scrollable]` CSS),
71
71
 
72
72
  The default case: nothing to do, the document scrolls.
73
73
 
74
- The case that needs wiring is **fixed bars** a top bar, a bottom nav, the
75
- normal shape of a mobile app. `FixedBar` measures its own height (safe area
76
- included) and publishes it on `<html>`:
74
+ The case that needs wiring is whatever covers the viewport — **fixed bars** (a
75
+ top bar, a bottom nav, the normal shape of a mobile app), the device's own
76
+ notch, a band an app reserves for itself. Each publishes what it takes, navi
77
+ adds them up on `<html>`, and the content reads the sum:
77
78
 
78
79
  ```
79
- --navi-fixed-bar-space-top / -bottom / -left / -right
80
+ --navi-safe-area-inset-top / -right / -bottom / -left
80
81
  ```
81
82
 
83
+ `docs/safe_area.md` holds the concept: the two levels, how an app declares
84
+ itself narrower than the window, and how something other than a bar joins the
85
+ sum.
86
+
82
87
  Two distinct things must be given back to the content, and forgetting the
83
88
  second one is the classic bug:
84
89
 
@@ -94,7 +99,7 @@ scrolls — which element that is, is the app's business, so navi does not pick:
94
99
 
95
100
  ```html
96
101
  <!-- on the container that scrolls under the bars -->
97
- <div id="main" data-navi-fixed-bar-space>…</div>
102
+ <div id="main" data-navi-safe-area>…</div>
98
103
  ```
99
104
 
100
105
  **Do not make that container scrollable by accident.** An `overflow-x: auto`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.52",
3
+ "version": "0.29.53",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {