@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
@@ -10,6 +10,7 @@ they are met once.
10
10
  - [Nested groups, and the fallback fade](#nested-groups-and-the-fallback-fade)
11
11
  - [Rendering is suspended for the whole callback](#rendering-is-suspended-for-the-whole-callback)
12
12
  - [`ready` rejects when the transition is skipped, `finished` when the update fails](#ready-rejects-when-the-transition-is-skipped-finished-when-the-update-fails)
13
+ - [An element captured in a view transition cannot be pointed at](#an-element-captured-in-a-view-transition-cannot-be-pointed-at)
13
14
  - [The top layer is painted through the root's picture](#the-top-layer-is-painted-through-the-roots-picture)
14
15
  - [Two frames show the live document](#two-frames-show-the-live-document)
15
16
  - [What makes a transition restyle the whole document](#what-makes-a-transition-restyle-the-whole-document)
@@ -20,18 +21,28 @@ they are met once.
20
21
  A `view-transition-name` must be unique per document: a duplicate aborts the
21
22
  transition, silently. Scope any name the app adds — per item id, per screen.
22
23
 
23
- The other half of the same fact is a bonus that costs nothing: any element given
24
- its own name (a tab underline, a header, a stamp) is animated by the browser
25
- from where it was to where it is during **any** transition, whoever started it.
26
- `Nav` does this for its current-tab indicator (`currentIndicator`), which is why
27
- the bar follows a `RouteTravel` swipe with no wiring between the two.
24
+ A named element is photographed on its own and animated by the browser from where
25
+ it was to where it is, during any transition, whoever started it. Hence the trap:
26
+ **an element named inside a page that moves does not move with it.** The name
27
+ lifts it out of its page's picture into a picture of its own, which stands where
28
+ it was captured, fading, while the pages slide away under it — right for a morph
29
+ between the two pages, wrong for anything that belongs to its page. navi's own
30
+ components drop their names for such movements: `Nav` names its current-tab bar
31
+ only for a movement between two tabs of its own row (`currentIndicatorSlides`),
32
+ and `List` unnames its rows for the length of any route transition or travel. A
33
+ name the app puts inside the pages is given only for the gesture it serves, or
34
+ dropped while they move: `view-transition-name: none` under
35
+ `:root:is([data-navi-route-transition], [data-navi-route-travel])`. Without nested
36
+ groups it also escapes the pages' clip, and navi warns about one inside a
37
+ transition area.
28
38
 
29
39
  ## Nested groups, and the fallback fade
30
40
 
31
41
  List and grid transitions rely on nested groups (`view-transition-group:
32
- contain`, Chrome/Edge 140+). On a browser without it nothing is named, so an
33
- unconditional `startViewTransition` falls back to a full-page cross-fade. If
34
- that fade is unwanted, the app — not a component — writes:
42
+ contain`, Chrome/Edge 140+). On a browser without them nothing is named, so an
43
+ unconditional `startViewTransition` falls back to a full-page cross-fade. Whether
44
+ that fade is a decent default or a glitch is the application's call, never a
45
+ component's, and turning it off is one rule (see `itemTransition` on `List`):
35
46
 
36
47
  ```css
37
48
  @supports not (view-transition-group: contain) {
@@ -41,9 +52,6 @@ that fade is unwanted, the app — not a component — writes:
41
52
  }
42
53
  ```
43
54
 
44
- Only the application knows whether a page-wide fade is a decent default or a
45
- glitch there.
46
-
47
55
  ## Rendering is suspended for the whole callback
48
56
 
49
57
  The document's rendering is suspended for the whole update callback, from the
@@ -53,103 +61,99 @@ callback awaits something that cannot happen, until the browser gives up on the
53
61
  transition entirely (`Transition was aborted because of timeout in DOM update`).
54
62
  Await a microtask, a task or a render — never a frame.
55
63
 
56
- That suspension lasts exactly as long as the callback, and **in a sub-document
57
- it takes the scrollbar with it**: an iframe's scrollbar is painted by the framed
58
- document, so it goes and comes back, shifting the layout by its width. A
59
- top-level page is spared — its root scrollbar is the compositor's. So a callback
60
- that waits on the network flickers every demo shown in an iframe while the same
61
- app, opened on its own, shows nothing. Keep the callback short, and suspect the
62
- frame before the code when a scrollbar blinks.
64
+ **In a sub-document the suspension takes the scrollbar with it**: an iframe's
65
+ scrollbar is painted by the framed document, so it goes and comes back for the
66
+ length of the callback, shifting the layout by its width — a top-level page is
67
+ spared, its root scrollbar being the compositor's. Keep the callback short, and
68
+ suspect the frame before the code when a scrollbar blinks.
63
69
 
64
70
  ## `ready` rejects when the transition is skipped, `finished` when the update fails
65
71
 
66
72
  There is only ever one transition per document, so another one starting SKIPS
67
73
  this one — as does the document going hidden or its snapshot containing block
68
- changing size mid-movement (a phone's address bar folding, its keyboard
69
- opening). A skip rejects `viewTransition.ready`, with a stackless DOMException
70
- (`AbortError` "skipped", or `InvalidStateError` "aborted because of invalid
71
- state"). `viewTransition.finished` still FULFILLS on a skip: the end state is
72
- reached, only the animation is not. `finished` (and `updateCallbackDone`)
73
- reject for one reason only, the update callback throwing.
74
-
75
- So ending a transition means handling both ways of `finished` — `.finally()`
76
- does not handle a rejection, an unhandled one is what it leaves behind, and
74
+ changing size mid-movement (a phone's address bar folding, its keyboard opening).
75
+ A skip rejects `viewTransition.ready`, with a stackless DOMException (`AbortError`
76
+ "skipped", or `InvalidStateError` "aborted because of invalid state").
77
+ `viewTransition.finished` still FULFILLS on a skip: the end state is reached, only
78
+ the animation is not. `finished` (and `updateCallbackDone`) reject for one reason
79
+ only, the update callback throwing.
80
+
81
+ So ending a transition means handling both ways of `finished` — `.finally()` does
82
+ not handle a rejection, an unhandled one is what it leaves behind, and
77
83
  `.then(done, done)` is the shape that ends it whichever way it went — while a
78
84
  `ready` waiter must expect the skip: `ready.then(onPictures, () => {})`.
79
85
 
86
+ ## An element captured in a view transition cannot be pointed at
87
+
88
+ A captured element is not painted where it stands, and nothing hit-tests to it: a
89
+ press over it falls through to the nearest ancestor still being painted, whatever
90
+ the pseudo-elements are told about `pointer-events`. Under the root's default
91
+ name, that is the whole page for the length of the movement. It is why a
92
+ `RouteTravel` keeps the root out of its pictures, so the page around the box stays
93
+ live, and catches a press on the travelling box at the document, handing it to the
94
+ box it fell inside; and why a door in a bar a route transition photographs needs
95
+ `pressableDuringRouteTransition` to stay pressable (see
96
+ [route_transitions.md](./route_transitions.md#pages-between-fixed-bars-the-transition-area)).
97
+
80
98
  ## The top layer is painted through the root's picture
81
99
 
82
- A modal `<dialog>`, its `::backdrop`, a popover: the browser paints the top
83
- layer during a transition only as part of the root's picture. With `:root {
84
- view-transition-name: none }`, the wall and the dialog go unpainted for the
85
- length of the movement, and a named element inside the dialog is photographed
86
- empty (Chrome 153, reproduced in a bare page; in a long page it showed only
87
- past a few thousand pixels of scroll, which is how it hid). A transition that
88
- opens or closes a top-layer surface keeps the root's default name and pays the
89
- frozen page — under a modal wall that costs nothing. navi's own lift
90
- (`popup_lift.md`) does exactly that.
91
-
92
- When the root cannot be kept — a route transition splits the bars from the
93
- pages, which only works with the root opted out — the top layer has to be
94
- **stood in for**. A dialog's box is an element, so it can be named and
95
- photographed on its own. Its wall cannot: `::backdrop` wears no name. What
96
- works is painting what the wall paints into the pictures themselves: a plain
97
- element appended inside each photographed box (the pages, each bar) before
98
- each capture, and one `position: fixed` element in the live document for what
99
- no picture covers, with a hole cut where each picture stands. The live one is
100
- not a picture, so nothing cross-fades it: it is animated on the movement's own
101
- clock. Where a picture stands changes over the movement — the frame the first
102
- picture is taken on shows the leaving state, the last frame the arriving one,
103
- and in between only the intersection of the two states' rectangles is under a
104
- picture at every moment — so the holes are cut three times. Reference:
105
- `paintTransitionWalls` and `paintRestWalls` in `nav/transition_furniture.js`;
106
- the measured story is in [route_transitions.md](./route_transitions.md#pages-between-fixed-bars-the-transition-area).
100
+ A modal `<dialog>`, its `::backdrop`, a popover: the browser paints the top layer
101
+ during a transition only as part of the root's picture. With
102
+ `:root { view-transition-name: none }`, the wall and the dialog go unpainted for
103
+ the length of the movement, and a named element inside the dialog is photographed
104
+ empty. A transition that opens or closes a top-layer surface keeps the root's
105
+ default name and pays the frozen page — under a modal wall that costs nothing.
106
+ navi's own lift ([popup_lift.md](./popup_lift.md)) does exactly that.
107
+
108
+ When the root cannot be kept — a route transition splits the bars from the pages,
109
+ which only works with the root opted out — the top layer has to be **stood in
110
+ for**. A dialog's box is an element, so it can be named and photographed on its
111
+ own; its wall cannot, `::backdrop` wears no name. What works is painting what the
112
+ wall paints into the pictures themselves, plus a live element in the document for
113
+ what no picture covers — not a picture, so animated on the movement's own clock,
114
+ and switched on by the same DOM write that switches the real wall off (see
115
+ [Two frames show the live document](#two-frames-show-the-live-document)).
107
116
 
108
117
  ## Two frames show the live document
109
118
 
110
119
  A view transition is not pictures from the call to `finished`. **The frame the
111
- first picture is taken on is rendered and shown**, with the top layer painted
112
- as usual, and `:active-view-transition` already matches on it (it matches from
113
- the `startViewTransition` call). **The frame after the pictures are dropped is
114
- shown too**, before the `finished` callbacks run — they are a frame late.
115
- Whatever is switched on for the movement is therefore on screen one frame
116
- before the pictures and one frame after; anything it stands in for must be
117
- switched off for exactly the same span, or that frame shows both (measured: a
118
- wall twice as dark under the press, read as a flash).
120
+ first picture is taken on is rendered and shown**, with the top layer painted as
121
+ usual, and `:active-view-transition` already matches on it (it matches from the
122
+ `startViewTransition` call). **The frame after the pictures are dropped is shown
123
+ too**, before the `finished` callbacks run — they are a frame late. Whatever is
124
+ switched on for the movement is therefore on screen one frame before the pictures
125
+ and one frame after; anything it stands in for must be switched off for exactly
126
+ the same span, or that frame shows both (a wall twice as dark: a flash).
119
127
 
120
128
  Two consequences for the switch itself:
121
129
 
122
130
  - **Switch both on the same DOM write**, at the same moment: an attribute set
123
131
  before the call and removed in `finished`. The late frame then shows the
124
- stand-in alone, which is what the real thing looked like — one frame nobody
125
- can see.
126
- - **`:active-view-transition` is the only same-frame signal at the end**: it
127
- stops matching in the rendering step that drops the pictures, before that
128
- frame is painted. It is what to use when the last frame must differ from the
129
- movement (see the holes above). It is NOT what to gate a stand-in's
130
- `display` on: hidden before the call, a stand-in measures as a zero rect and
131
- lands in the wrong place; and on the capture frame it is displayed while the
132
- real thing is still there.
132
+ stand-in alone, which is what the real thing looked like — one frame nobody can
133
+ see.
134
+ - **`:active-view-transition` is the only same-frame signal at the end**: it stops
135
+ matching in the rendering step that drops the pictures, before that frame is
136
+ painted. It is what to use when the last frame must differ from the movement. It
137
+ is NOT what to gate a stand-in's `display` on: hidden before the call, a
138
+ stand-in measures as a zero rect and lands in the wrong place; and on the
139
+ capture frame it is displayed while the real thing is still there.
133
140
 
134
141
  ## What makes a transition restyle the whole document
135
142
 
136
- Starting a transition restyles what changed and nothing more: in a bare page,
137
- the capture of the old state restyles a handful of elements, however long the
138
- page is. Two things make it restyle every one of them, in the frame the old
139
- state is photographed — on a page of ~4,700 elements, 17–21 ms at full speed
140
- and 60–90 ms with the CPU throttled 4×, added to every transition before
141
- anything moves:
143
+ Starting a transition restyles what changed and nothing more: in a bare page, the
144
+ capture of the old state restyles a handful of elements, however long the page
145
+ is. Two things make it restyle every element of the page, in the frame the old
146
+ state is photographed — added to every transition before anything moves:
142
147
 
143
148
  - **A `::highlight()` rule every element matches.** Written for the whole
144
- document, `::highlight(x) { … }` makes Chrome restyle every element when a
145
- view transition starts (Chrome 153, bare page: 10,009 of 10,009 elements; 6
146
- once the rule is scoped). Scope it to the elements that hold the ranges —
149
+ document, `::highlight(x) { … }` makes Chrome restyle every element when a view
150
+ transition starts. Scope it to the elements that hold the ranges —
147
151
  `.results::highlight(x), .results ::highlight(x)` (both: a browser without
148
152
  highlight inheritance only paints what the rule matches). navi's own search
149
153
  highlight is scoped to list rows.
150
- - **A custom property changed on `:root`.** Every element inherits it, so
151
- changing one restyles the document — and a movement changes its values as it
152
- starts. What the pictures read belongs on `::view-transition` instead: the
154
+ - **A custom property changed on `:root`.** Every element inherits it, so changing
155
+ one restyles the document — and a movement changes its values as it starts.
156
+ What the pictures read belongs on `::view-transition` instead: the
153
157
  pseudo-element tree inherits from it, and no element of the document does.
154
158
 
155
159
  ```css
@@ -160,41 +164,39 @@ anything moves:
160
164
  }
161
165
  ```
162
166
 
163
- A value computed in JS cannot get there through an inline style, which
164
- cannot target a pseudo-element: it goes in a rule of a sheet of its own,
165
- `:root::view-transition {}`, set with `rule.style.setProperty()` (navi does
166
- this in `nav/transition_values.js`).
167
+ A value computed in JS cannot get there through an inline style, which cannot
168
+ target a pseudo-element: it goes in a rule of a sheet of its own,
169
+ `:root::view-transition {}`, set with `rule.style.setProperty()`.
167
170
 
168
171
  A value JS has to read back is the exception: what JS reads off
169
172
  `::view-transition` is not reliable outside a live transition —
170
- `getComputedStyle(root, "::view-transition")` answers while none runs, but
171
- in Chrome and WebKit alike it went on answering empty for rules added after
172
- its first read. Such a value stays on `:root`, registered with `@property`
173
- and `inherits: false` so that the document does not inherit it, and is
174
- handed to the pictures explicitly: `--x: inherit` on `::view-transition` and
175
- on the pseudo-elements below it.
173
+ `getComputedStyle(root, "::view-transition")` answers while none runs, but in
174
+ Chrome and WebKit alike it went on answering empty for rules added after its
175
+ first read. Such a value stays on `:root`, registered with `@property` and
176
+ `inherits: false` so that the document does not inherit it, and is handed to the
177
+ pictures explicitly: `--x: inherit` on `::view-transition` and on the
178
+ pseudo-elements below it.
176
179
 
177
180
  Whatever does depend on the root is paid once, by the frame of the capture —
178
- unless a style read follows a write on the root, which forces the restyle on
179
- the spot, in the click. Read what the transition needs (names, rectangles)
180
- before writing anything on the root.
181
+ unless a style read follows a write on the root, which forces the restyle on the
182
+ spot, in the click. Read what the transition needs (names, rectangles) before
183
+ writing anything on the root.
181
184
 
182
185
  ## A hidden element is not photographed, even in the top layer
183
186
 
184
187
  A popover or a modal `<dialog>` whose ancestor is `display: none` still matches
185
- `:popover-open` or `:modal`, but it is not rendered: `checkVisibility()` is
186
- false, its rectangle is empty, and a transition taking its pictures then has it
187
- on the old side only — it leaves, as if it were gone. `isConnected` says
188
- nothing about it; `checkVisibility()` is what tells which of the two states an
189
- element is part of. navi relies on it to keep the page left by a route
190
- transition mounted but out of the second picture (see
188
+ `:popover-open` or `:modal`, but it is not rendered: `checkVisibility()` is false,
189
+ its rectangle is empty, and a transition taking its pictures then has it on the
190
+ old side only — it leaves, as if it were gone. `isConnected` says nothing about
191
+ it; `checkVisibility()` is what tells which of the two states an element is part
192
+ of. navi relies on it to keep the page left by a route transition mounted but out
193
+ of the second picture (see
191
194
  [route_transitions.md](./route_transitions.md#the-page-being-left-stays-until-its-movement-is-over)).
192
195
 
193
196
  ## See also
194
197
 
195
198
  - [route_transitions.md](./route_transitions.md) — pages moving against each
196
199
  other on navigation
197
- - [drag_to_travel.md](./drag_to_travel.md) — a transition held under a finger,
198
- and what the main thread cannot read of it
199
- - [drag_interactions.md](./drag_interactions.md#naming-what-travels) — naming
200
- what moves when something is dropped on a place
200
+ - [drag_to_travel.md](./drag_to_travel.md) — a transition held under a finger
201
+ - [drag_interactions.md](./drag_interactions.md#naming-what-travels) — naming what
202
+ moves when something is dropped on a place
package/docs/z_index.md CHANGED
@@ -11,15 +11,8 @@ Reach for the tools in this order.
11
11
 
12
12
  Between positioned elements that all have `z-index: auto`, the last one written
13
13
  paints in front. Moving a tag is the cheapest way to reorder, and it can never
14
- affect anything outside its parent.
15
-
16
- ```jsx
17
- // The stamp paints over the content because it comes after it. No z-index.
18
- <Box position="relative">
19
- <CardContent />
20
- <Stamp />
21
- </Box>
22
- ```
14
+ affect anything outside its parent (see
15
+ [the card at the end](#a-card-that-stacks-three-layers-with-no-z-index)).
23
16
 
24
17
  If the element that must be in front cannot move in the DOM (it is a slot, it
25
18
  is written by a consumer), that is a real reason to go further — "I did not
@@ -30,11 +23,8 @@ think about the order" is not.
30
23
  `z-index: 5` does not mean "in front of my siblings". It means "in front of
31
24
  everything painted lower **in the nearest stacking context**", and when no
32
25
  ancestor opens one, that context is the document root — including `FixedBar`,
33
- sticky list-group labels, and popups.
34
-
35
- This is the failure that keeps happening: a small `z-index` inside a card wins
36
- against a bar written at the other end of the page, because both are competing
37
- in the same, page-wide context.
26
+ sticky list-group labels, and popups. That is how a small `z-index` inside a
27
+ card wins against a bar written at the other end of the page.
38
28
 
39
29
  ## 3. If a `z-index` is genuinely needed, isolate
40
30
 
@@ -51,74 +41,51 @@ takes its place among its own siblings.
51
41
 
52
42
  A `z-index` inside a reusable component without this is a bug waiting for its
53
43
  call site: the component behaves differently depending on where it is dropped.
44
+ An app writing its own number without it is competing with navi's scale (§5).
54
45
 
55
46
  ## 4. What creates a stacking context without you asking
56
47
 
57
48
  `opacity` below 1, `transform`, `filter`, `backdrop-filter`, `will-change`,
58
49
  `contain: paint`, `mix-blend-mode`, and a positioned element with a `z-index`
59
- other than `auto` all open one. Two consequences:
50
+ other than `auto` all open one: something you faded or moved suddenly paints as
51
+ a block, and a `z-index` written deeper inside stops reaching where you
52
+ expected. The answer is still DOM order — write the layer that must be on top
53
+ last — not a `z-index` "to repair it": adding one on top of an unnoticed
54
+ stacking context is how a value ends up tuned to a symptom.
60
55
 
61
- - something you faded or moved suddenly paints as a block, in front of or
62
- behind a sibling it used to interleave with;
63
- - a `z-index` you wrote deeper inside stops reaching where you expected,
64
- because one of these ancestors now caps it.
56
+ ## 5. The values navi plays with
65
57
 
66
- The answer is still DOM order — write the layer that must be on top last —
67
- not a `z-index` "to repair it". Adding one on top of an unnoticed stacking
68
- context is how a value ends up tuned to a symptom.
58
+ They live in `src/navi_z_indexes.js`, as tokens, in bands a decade apart — one
59
+ can grow without reaching the next, and a value seen in devtools says which
60
+ band it came from. From the bottom:
69
61
 
70
- ## 5. The values navi plays with
62
+ 1, 2, 3 (a `Group` member hovered, focused, holding a popup open) < 10 (sticky
63
+ parts, while something scrolls under them) < 100 (`FixedBar`) < 1000
64
+ (`Dialog`/`Popover` with `layer="local"` and their backdrops, plus stack order)
65
+ < 10000 (a modal's wall painted into the page for the length of a route
66
+ transition, standing in for the top layer) < the top layer itself
67
+ (`Dialog`/`Popover` with `layer="top"`, callouts).
71
68
 
72
- They all live in `src/navi_z_indexes.js`, as tokens, in one ordered list — the
73
- file is the overview, this table is its summary. Bands are a decade apart so
74
- one can grow without reaching the next, and so a value seen in devtools says
75
- which band it came from.
76
-
77
- | Band | Token | Value |
78
- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------- |
79
- | Top layer (`Dialog`/`Popover` with `layer="top"`) | — | above everything |
80
- | A modal's wall painted into the page for the length of a route transition, standing in for the top layer | `--navi-z-index-top-layer` | 10000 |
81
- | `Dialog`/`Popover` with `layer="local"`, their backdrop, callouts | `--navi-z-index-popup`, `--navi-z-index-callout` | 1000 `+ stack order` |
82
- | `FixedBar` | `--navi-z-index-bar` | 100 |
83
- | Sticky while something scrolls under: `List` header/footer/group labels, `SidePanel` head/foot, `Box` header/footer, `<Box sticky>` | `--navi-z-index-sticky` | 10 |
84
- | A `Group` member under the pointer, then the one holding focus, then the one holding a popup open | `--navi-z-index-control-hovered`, `--navi-z-index-control-focused`, `--navi-z-index-control-expanded` | 1, 2, 3 |
85
- | `Table` sticky cells, drag, resize | `src/control/table/z_indexes.js` | 1–7, derived from each other |
86
-
87
- What to read from it:
88
-
89
- - **The order matters more than the numbers.** A bar is above anything the page
90
- scrolls, a popup above the bar, and a control raising itself above its
91
- neighbour is at the bottom — a hovered control crossing the top bar is the
92
- bug the gaps exist to make impossible.
93
- - **A z-index that only orders a component's own parts stays a literal** next to
94
- the rule that needs it. Tokens are for what is decided against another
95
- component; putting "above my own sibling" in the global list would only
96
- dilute it.
97
- - **An app writing its own number is competing with this scale.** Write
98
- `isolation: isolate` on the parent instead, and the number stops meaning
99
- anything outside it.
69
+ **The order matters more than the numbers.** A bar is above anything the page
70
+ scrolls, a popup above the bar, and a control raising itself above its
71
+ neighbour is at the bottom — a hovered control crossing the top bar is the bug
72
+ the gaps exist to make impossible. **A z-index that only orders a component's
73
+ own parts stays a literal** next to the rule that needs it (`Table` orders its
74
+ sticky cells, drag and resize on a 1–7 scale of its own).
100
75
 
101
76
  ### A sticky part is only in the band while it is stuck
102
77
 
103
78
  `--navi-z-index-sticky` says "kept stuck while something scrolls under it", and
104
- the second half of that sentence is a condition, not decoration. A `List` group
105
- label at rest is a block in the flow: nothing passes under it, and painting it
106
- at 10 there is what slices a focus ring, a badge or a stamp that a neighbouring
107
- row lets out of its box — including a `Group` member raising itself to 1, 2 or 3.
108
-
109
- CSS cannot express the condition. There is no `:stuck`, and
110
- `@container scroll-state(stuck: top)` styles a container's **descendants**, so
111
- an element can never read its own stuck state — which is the one that matters
112
- here. So `List` measures it and marks its three sticky parts
113
- (`.navi_list_item_header`, `.navi_list_item_footer`,
114
- `.navi_list_item_group_label`) with `navi-stuck`, against its own scroller and
115
- not the window, updated on scroll, on resize, and on every commit (a virtualized
116
- list changes which labels exist without anything scrolling).
117
-
118
- navi's own rule is the first reader: the band applies under `[navi-stuck]`,
119
- `auto` applies at rest. That is what puts the decision back within reach of an
120
- app — a card whose badge overflows into the label below it gets past it with a
121
- literal in the card, against its own neighbour, exactly as §1–3 above ask:
79
+ the second half of that sentence is a condition. A `List` group label at rest
80
+ is a block in the flow: nothing passes under it, and painting it at 10 there is
81
+ what slices a focus ring, a badge or a stamp that a neighbouring row lets out of
82
+ its box — including a `Group` member raising itself to 1, 2 or 3.
83
+
84
+ An element cannot read its own stuck state in CSS, so `List` measures it and
85
+ marks its header, footer and group labels `navi-stuck`: the band applies there,
86
+ `auto` at rest. That puts the decision back within reach of an app — a card
87
+ whose badge overflows into the label below it gets past it with a literal in
88
+ the card, against its own neighbour, exactly as §1–3 ask:
122
89
 
123
90
  ```jsx
124
91
  // Reaches past a label at rest (auto), loses to one that is stuck (10).
@@ -127,48 +94,32 @@ literal in the card, against its own neighbour, exactly as §1–3 above ask:
127
94
 
128
95
  For what a literal cannot reach, each part has a pair of variables —
129
96
  `--list-header-z-index`, `--list-footer-z-index`, `--list-group-label-z-index`,
130
- each with a `-stuck` counterpart defaulting to the band — settable on `<List>`
131
- with no selector and no navi class name. Reach for them last, and remember a
132
- negative value is compared against the page like any other: without a stacking
133
- context between the label and the nearest opaque background, `-1` does not put
134
- the label behind the rows, it puts it behind that background and out of sight.
135
-
136
- See the "Sticky parts" chapter of
97
+ each with a `-stuck` counterpart defaulting to the band — settable on `<List>`.
98
+ Reach for them last, and remember a negative value is compared against the page
99
+ like any other: without a stacking context between the label and the nearest
100
+ opaque background, `-1` does not put the label behind the rows, it puts it
101
+ behind that background and out of sight. See the "Sticky parts" chapter of
137
102
  [12_list_demo.html](../src/control/demos/12_list_demo.html).
138
103
 
139
- `<Box sticky>` and `Box`'s own `header`/`footer` take the opposite default, and for a reason worth
140
- knowing: they are in the band **always**, not only while stuck. `List` can tell
141
- — it measures its parts against its own scroller. A `Box` cannot: it is the
142
- generic scrolling area, its content is whatever the app puts in it, and a
143
- sticky part that drops to `auto` loses to anything that content positioned, a
144
- `transform` or an `opacity` below 1 included. So the band is the default,
145
- `isolation: isolate` on the scrolling box keeps it local, and
146
- `--box-header-z-index` / `--box-footer-z-index` write it back to `auto` at the
147
- one call site that knows nothing inside is positioned.
104
+ `Box`'s own sticky `header`/`footer` — the shape without a `body`; beside a
105
+ body they are plain blocks — and `<Box sticky>` take the opposite default: they
106
+ are in the band **always**, not only while stuck. A `Box` cannot tell: it is
107
+ the generic scrolling area, its content is whatever the app puts in it, and a
108
+ sticky part at `auto` loses to anything that content positioned, a `transform`
109
+ or an `opacity` below 1 included. The scrolling box is `isolation: isolate`, so
110
+ the band stays local to it. At the one call site that knows nothing inside is
111
+ positioned — a badge or a stamp overflowing a row is otherwise sliced by a
112
+ header it never scrolls under — `--box-header-z-index: auto`
113
+ (`--box-footer-z-index` likewise) writes it back.
148
114
  [9_scrollable_z_index_demo.html](../src/box/demos/9_scrollable_z_index_demo.html)
149
115
  shows the band, what `auto` would look like, and what the band costs, side by
150
116
  side.
151
117
 
152
- `<Box sticky>` — the one an app writes by hand — is that same generic case, so
153
- it carries the band too, from the `sticky` prop itself (`position="sticky"`
154
- included):
155
-
156
- ```jsx
157
- // position: sticky + z-index: var(--navi-z-index-sticky). Nothing to write.
158
- <Box sticky bottom>
159
- <SubmitButton />
160
- </Box>
161
- ```
162
-
163
- Without it the box is a positioned element at `z-index: auto`, and anything the
164
- page raised passes in front of it — a `Group` member holding focus is at 2, so
165
- it is seen crossing a submit bar the box was written to keep last. DOM order
166
- cannot answer that (2 beats `auto` whatever the order) and `isolation: isolate`
167
- neither (the common parent holds both, isolating it does not reorder them), so
168
- without the band the app would have no move left but a literal of its own.
169
-
170
- An explicit `zIndex` wins, `zIndex="auto"` included — the way
171
- `--box-header-z-index` writes the band back at a call site that knows better:
118
+ `<Box sticky>` carries the band from the prop itself. Without it, a `Group`
119
+ member holding focus (2) would be seen crossing a submit bar the box was written
120
+ to keep last, and neither DOM order nor `isolation` can answer that: 2 beats
121
+ `auto` whatever the order, and the common parent holds both. An explicit
122
+ `zIndex` wins, `"auto"` included:
172
123
 
173
124
  ```jsx
174
125
  // Back to auto: this one is meant to slide under the card that follows it.
@@ -183,10 +134,11 @@ member that comes after it in the DOM. DOM order cannot express "whichever one
183
134
  is hovered", so this is a legitimate `z-index`.
184
135
 
185
136
  `isolation: isolate` on the group would contain those values, but it would
186
- also contain the popup of a `Picker` held in the group: its 1000 would become
187
- local, and the popup would be capped inside the group instead of covering the
188
- page. So the group is deliberately not isolated, and what keeps its 1, 2 and 3
189
- harmless is the scale above them.
137
+ also contain the popup of a `Picker` held in the group when that popup stacks
138
+ in the page (`popupLayer="local"`): its 1000 would become local, and the popup
139
+ would be capped inside the group instead of covering the page. A top-layer
140
+ popup, the default, escapes any stacking context. So the group is deliberately
141
+ not isolated, and what keeps its 1, 2 and 3 harmless is the scale above them.
190
142
 
191
143
  ## A card that stacks three layers with no `z-index`
192
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.417",
3
+ "version": "0.29.419",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "preact": "^11.0.0-rc.2"
35
35
  },
36
36
  "dependencies": {
37
- "@jsenv/dom": "0.17.84",
37
+ "@jsenv/dom": "0.17.85",
38
38
  "@jsenv/humanize": "1.8.6",
39
39
  "@jsenv/validity": "0.4.5"
40
40
  },
@@ -1,83 +0,0 @@
1
- # BadgeList
2
-
3
- A row of badges that wraps. What it does at runtime depends on what it is asked
4
- for, and nothing is set up for a case that cannot happen:
5
-
6
- - **plain** — no `max`, no `maxLines`, no `fallback`, no `shrinkWrap`: one
7
- element holding its children as-is. No registry, no effect, no measurement.
8
- - **`max` / `fallback`** — the badges are counted (see below), nothing is
9
- measured, no DOM is watched.
10
- - **`shrinkWrap`** — a hidden clone of the list is laid out to find the widest
11
- wrapped row, and the list is narrowed to it so the last row isn't ragged.
12
- Opt-in outside a `Picker` (a picker draws a border around the list, so the
13
- ragged edge shows; elsewhere the work would often go unseen). With `maxLines`
14
- the rows are read first, at the full width, and the list is narrowed once the
15
- surplus is gone — in place, no clone.
16
- - **`maxLines`** — every badge is laid out once, hidden; where the rows fell is
17
- read; the list is rendered again with the badges that fit and a `+N more`
18
- badge for the rest. Both renders land in the same frame. A width change of
19
- the room around the list triggers another measure — not a change of the
20
- list's own width, which is what it produces, not what it is given.
21
-
22
- ## How badges are counted
23
-
24
- Once the list has something to decide — a `max`, a `fallback`, `maxLines` — a
25
- `Badge` inside it does not render itself: it hands its props to the list
26
- through `BadgeListContext` and renders nothing (a plain list leaves its badges
27
- to render themselves). Badges register in tree order, so by the time the list
28
- gets to its own content it holds them all
29
- and knows how many there are before deciding what to show — without walking
30
- children vnodes, and without rendering a badge it then has to take back.
31
-
32
- Two consequences:
33
-
34
- - a `Badge` must be a **direct** child of the list — its rendering is moved
35
- into the list, so `<div><Badge /></div>` takes the badge out of its wrapper;
36
- - a badge's own `key` goes to the registering vnode and not to the badge the
37
- list draws, which is keyed by position. Reordering recreates the nodes rather
38
- than moving them.
39
-
40
- ## `maxLines` in a Picker
41
-
42
- A picker clamps its value with `maxLines`, which is CSS line-clamp — it counts
43
- line boxes of inline text and never sees a wrapped flex row. So a `BadgeList`
44
- rendered as a picker's `ui` reads the number from `MaxLinesContext` instead
45
- (the picker provides it, default 1) and caps its own rows to it, and the picker
46
- turns its own clamp off (`.navi_picker_value:has(.navi_badge_list)`). Nothing
47
- to set on the list; `maxLines` on the picker is enough, and a `maxLines` on the
48
- list itself is a local override.
49
-
50
- `max` is a different cap: a number of badges, whatever the rows. The two
51
- compose — the `+N` badge takes one of the `max` slots.
52
-
53
- ## The `fallback`
54
-
55
- Without a `fallback`, an empty list renders **nothing**. Inside a picker there
56
- is one more thing to know: a picker given a `ui` draws that and only that, its
57
- own `placeholder` is **not** drawn next to it. An empty `BadgeList` in a picker
58
- is a blank picker — so the list's `fallback` is the placeholder, and the
59
- placeholder text is what to pass:
60
-
61
- ```jsx
62
- <BadgeList fallback="Select skills…">
63
- {selected.map((skill) => (
64
- <Badge key={skill}>{skill}</Badge>
65
- ))}
66
- </BadgeList>
67
- ```
68
-
69
- Plain text is the right shape for it. It reads at the picker's own size, in the
70
- placeholder color — the picker colors its value slot that way while it holds
71
- nothing — and the picker box stays the same height empty and filled: the box is
72
- held one line tall by the picker's own line (the right slot is `1lh`), whatever
73
- the slot inside holds. That slot is a fraction of a pixel taller with a line of
74
- text than with a row of badges (0.89px at the default font size — a line is set
75
- by the picker's font and line-height, a badge by its own padding and smaller
76
- font), which nothing shows.
77
-
78
- A transparent `Badge` as fallback matches the slot to the pixel instead, but its
79
- text is badge-sized — smaller than the picker's — and reads as a badge with
80
- nothing in it. Only worth it where the slot itself is what something else is
81
- sized on. Section 4 of `src/control/demos/picker/2_select_multiple_demo.html`
82
- measures the three cases (no fallback, plain text, transparent badge), box and
83
- slot side by side.