@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
@@ -1,9 +1,13 @@
1
1
  # Navi CSS Architecture
2
2
 
3
- - [Overview](#overview)
3
+ Navi components are styled through CSS custom properties and scoped rules. The
4
+ rules that paint and lay a component out are unlayered, so **navi wins by
5
+ default**; the defaults those rules read sit in `@layer navi`, so **any
6
+ unlayered app rule beats them** without `!important`; and the preferred
7
+ override surface is a **component prop**.
8
+
4
9
  - [Where CSS lives: `import.meta.css`](#where-css-lives-importmetacss)
5
10
  - [`${}` blinds the whole stylesheet](#-blinds-the-whole-stylesheet)
6
- - [Beware when moving a shared sheet out](#beware-when-moving-a-shared-sheet-out)
7
11
  - [Browser support: navi's css and your target](#browser-support-navis-css-and-your-target)
8
12
  - [Layer structure](#layer-structure)
9
13
  - [Why defaults go inside `@layer navi`](#why-defaults-go-inside-layer-navi)
@@ -17,14 +21,6 @@
17
21
  - [Say which children the rule means](#say-which-children-the-rule-means)
18
22
  - [Summary](#summary)
19
23
 
20
- ## Overview
21
-
22
- Navi components are styled through a combination of CSS custom properties (variables) and scoped CSS rules. The architecture is designed so that:
23
-
24
- 1. **Navi wins by default** — the rules that paint and lay a component out are not placed inside `@layer`, so they beat any layered global style. The exception is deliberate and always says so in a comment: a rule navi is happy to hand back sits in `@layer navi` next to the defaults — see [The exception](#the-exception-a-rule-navi-offers-back).
25
- 2. **Defaults are easy to override** — default values for CSS variables are declared inside `@layer navi`, which any unlayered rule on the same element beats whatever its specificity, making them trivially overridable from outside.
26
- 3. **The preferred override surface is component props** — props translate to inline styles or data attributes, not class names.
27
-
28
24
  ---
29
25
 
30
26
  ## Where CSS lives: `import.meta.css`
@@ -41,20 +37,23 @@ const css = /* css */ `
41
37
  import.meta.css = css;
42
38
  ```
43
39
 
44
- The `css` constant may be declared next to the assignment or anywhere in the same
45
- module — the build follows the name to where it is declared. What it cannot
46
- follow is a name coming from **another** module: `import.meta.css = importedCss`
47
- is read only when the constant belongs to the module doing the assignment.
40
+ The build reads the css where it is written: **an inline template, or a
41
+ `const` declared at the top level of the module doing the assignment.** It
42
+ follows nothing else — not a `let`, not a name imported from another module,
43
+ and not a `const` declared inside the component next to an assignment made in
44
+ render: that one ships unparsed, and without a warning.
48
45
 
49
- **Keep the assignment in a render, not at module scope.** A component's
50
- stylesheet is adopted when the component first renders, so a page that never
51
- renders it never carries it, and a bundler that sees no caller drops the css
52
- along with the code. `import.meta.css = css` at the top level of a module makes
53
- the sheet a module side effect: it lands on every page importing the module, for
54
- a component that may never appear, and nothing can shake it out.
46
+ **Keep the assignment in a render, not at module scope.** The sheet is adopted
47
+ when the assignment runs: from a render, a page that never renders the
48
+ component never carries it, and a bundler that sees no caller drops the css
49
+ along with the code; at module scope, it lands on every page importing the
50
+ module, and nothing can shake it out. Assign at module scope only for a module
51
+ that is a side effect by design — an app's tokens, say.
55
52
 
56
- That is also how a stylesheet shared between components is written — a module of
57
- its own, exposing an install function its callers run from their render:
53
+ The setter is keyed by module: **two assignments in one module do not add up**,
54
+ the second replaces the first. A stylesheet shared between components is
55
+ therefore a module of its own, exposing an install function its callers run
56
+ from their render:
58
57
 
59
58
  ```js
60
59
  // input_css.js
@@ -65,8 +64,6 @@ const inputCss = /* css */ `
65
64
  }
66
65
  }
67
66
  `;
68
- // Keyed on this module, so the controls drawn as a .navi_input box share one
69
- // stylesheet instead of each carrying a copy.
70
67
  export const installInputCss = () => {
71
68
  import.meta.css = inputCss;
72
69
  };
@@ -83,36 +80,48 @@ export const Textarea = (props) => {
83
80
  };
84
81
  ```
85
82
 
86
- Note the setter is keyed by module: **two assignments in one module do not add
87
- up**, the second replaces the first. One `import.meta.css` per module — which is
88
- why the shared sheet has to live in a module of its own rather than be
89
- concatenated into the component's.
83
+ Extracting a shared sheet changes **cascade order**: the install call runs
84
+ before the component's own assignment, so the extracted rules come _before_ the
85
+ component's sheet rather than where they were written. That matters only for
86
+ the same selector at the same specificity in the same layer — check for that
87
+ before splitting.
90
88
 
91
- The exception is a module that is a side effect by design — `navi_css_vars.js`
92
- and the sheets it pulls in are the app's tokens, always needed, and assign at
93
- module scope on purpose.
89
+ A module reached by `import()` carries its sheet in its chunk, adopted when the
90
+ chunk runs: see
91
+ [dynamic_import.md](./dynamic_import.md#importmetacss-on-the-far-side-of-a-split).
94
92
 
95
93
  ### `${}` blinds the whole stylesheet
96
94
 
97
95
  A substitution the build cannot read makes the **entire** template opaque, not
98
96
  just its line, and the stylesheet then ships verbatim: comments and whitespace
99
- included, nothing transpiled for the browsers the app targets (nesting,
100
- `light-dark()`, `color-mix()` reach them as written), a `url("./icon.svg")`
101
- neither copied nor hashed and resolved against the document — a silent 404 in
102
- production — nothing checked, nothing minified. The build warns about it; a css comment
103
- containing `jsenv-css-opaque` inside the template silences that one template when there
104
- is no way around it.
97
+ included, nothing lowered for the browsers the app targets, a
98
+ `url("./icon.svg")` neither copied nor hashed and resolved against the
99
+ document — a silent 404 in production — nothing checked, nothing minified.
105
100
 
106
101
  The one substitution the build reads stands exactly **where a css value
107
- stands** — inside a rule block, after the `:` of a declaration, not in a string,
108
- in `url()`, in a selector, a property name or an at-rule prelude. Everything
109
- else blinds the sheet. So write css without a `${}`: a value the JS knows is a
110
- custom property the css reads (`width: var(--panel-width, 300px)`, set from JS
111
- with `element.style.setProperty`); a selector or attribute name held in a JS
112
- constant is written out literally in the css, the constant staying on the JS
113
- side; a variant is a data attribute with both branches in the css; a repeated
114
- block is a selector list; a shared sheet is a module exposing an install
115
- function, as above.
102
+ stands**: inside a rule block, after the `:` of a declaration — not in a
103
+ string, in `url()`, in an at-rule prelude, in a selector or a property name —
104
+ and it must come out of the build's css transformation exactly once (a
105
+ declaration duplicated under a vendor prefix makes it twice). Anything else
106
+ ships the template as written, and the build warns
107
+ (`import.meta.css shipped as written`, naming the file and the `${}`). When
108
+ there is genuinely no way around it, a css comment containing
109
+ `jsenv-css-opaque` inside the template silences that one template — keep it
110
+ short: an opaque template ships its comments.
111
+
112
+ So write css without a `${}`:
113
+
114
+ - a value the JS knows → a custom property the css reads
115
+ (`width: var(--panel-width, 300px)`), set with `element.style.setProperty`;
116
+ - a class or attribute name held in a JS constant → written out literally in
117
+ the css, the constant staying on the JS side; a name you did not want to
118
+ repeat → css nesting (`&[data-loading]`);
119
+ - a variant → a data attribute, both branches written out in the css;
120
+ - an asset → a literal `url()`, which the build follows, copies and versions (a
121
+ truly dynamic url goes in a custom property);
122
+ - a block of declarations repeated in several rules → a selector list;
123
+ - a sheet shared between modules → a module exposing an install function, as
124
+ above.
116
125
 
117
126
  ```js
118
127
  const SWIPE_AXES_ATTRIBUTE = "data-swipe";
@@ -129,52 +138,36 @@ import.meta.css = /* css */ `
129
138
  element.setAttribute(SWIPE_AXES_ATTRIBUTE, "x");
130
139
  ```
131
140
 
132
- ### Beware when moving a shared sheet out
133
-
134
- Splitting a chunk out of a template changes **cascade order**: in the template
135
- it sits where it was written, while an extracted sheet is adopted by the install
136
- call, which runs before the component's own assignment — so its rules come
137
- _before_ the component's sheet. That matters only when the two carry
138
- the same selector at the same specificity and in the same layer — check for that
139
- before splitting. In navi, `popup_css.js` and `surface_text_css.js` overlap
140
- `.navi_dialog` and `.navi_popover` with dialog's and popover's own css, which is
141
- why dialog.jsx and popover.jsx interpolate them, and mark those two templates
142
- `jsenv-css-opaque`.
141
+ ### Browser support: navi's css and your target
143
142
 
144
- ### A sheet on the far side of a split
143
+ navi's css is written for current browsers — css nesting, `light-dark()`,
144
+ `color-mix()` on `currentColor` — and `dist/` ships it as authored, built for
145
+ Chrome 123.
145
146
 
146
- A module reached by `import()` carries its stylesheet in its chunk, adopted
147
- when the chunk runs: a fallback rendered before it cannot use its classes, and
148
- between unlayered sheets the last adopted wins. What follows for code loaded
149
- on demand is in [dynamic_import.md](./dynamic_import.md).
147
+ An app built with jsenv reads navi's stylesheets like its own and lowers them
148
+ to the target it declares. jsenv's default target is recent and touches
149
+ nothing; an app that must support older browsers says so once, and both its
150
+ dev server and its build follow — the declaration and what each step down
151
+ costs are in
152
+ [jsenv's browser support](https://github.com/jsenv/core/blob/main/docs/users/c_build/c_build.md#21-browser-support).
153
+ Most of navi's css then comes out lowered, not all of it:
150
154
 
151
- ### Browser support: navi's css and your target
155
+ - the `color-mix()` formulas built on `currentColor` or a `var()` are kept as
156
+ written — the color keywords and the hover, read-only and disabled shades
157
+ are made of them — and a browser without `color-mix()` drops them;
158
+ - a few of navi's stylesheets (`Dialog`'s, `Popover`'s, the callout's) are
159
+ opaque templates, shipped as written: they need native nesting;
160
+ - a lowered `light-dark()` is a switch keyed on `prefers-color-scheme`: navi's
161
+ tokens, resolved on `:root`, no longer follow a `color-scheme` set on one
162
+ element (the dark panel a `Wheel` relies on).
152
163
 
153
- **navi does not require recent browsers.** Its css is written with modern
154
- features — css nesting, `light-dark()`, `color-mix()` — and it ships that way:
155
- navi's own build targets a runtime where all of it is native, so what lands in
156
- `dist/` is what was authored, unrewritten.
157
-
158
- The runtime that decides is **yours**. An app's build reads navi's stylesheets
159
- out of `node_modules` like any other css and lowers them to the target the app
160
- declares. Targeting Chrome 89, everything above comes out as nesting-free rules
161
- and a pair of custom properties standing in for `light-dark()`; targeting the
162
- default, nothing is touched.
163
-
164
- Which is the part worth knowing: **jsenv's default target is recent**, and by
165
- default it assumes the modern form is fine. An app that must support older
166
- browsers says so, once, and both its dev server and its build follow — the
167
- declaration and what each step down costs are in
168
- [jsenv's browser support](https://github.com/jsenv/core/blob/main/docs/users/c_build/c_build.md#21-browser-support).
169
- Nothing in the app's own css has to change either: it is lowered by the same
170
- pass.
164
+ Under another bundler nothing is lowered: the css runs as navi's own build
165
+ wrote it.
171
166
 
172
- One thing is NOT lowered, and it is the trap the section above already names: a
173
- stylesheet the build cannot parse. A `${}` outside a value position makes the
174
- whole template opaque, and an opaque template is shipped verbatim — nesting
175
- included, `light-dark()` included, whatever the target. Such a stylesheet
176
- silently requires the browsers its own css requires, and no declaration can
177
- lower it. That is the strongest reason not to write one.
167
+ The same holds for the app's own sheets, with one hard stop: **an opaque
168
+ template is never lowered**, whatever the target. It silently requires the
169
+ browsers its own css requires, and no declaration can change that — the
170
+ strongest reason not to write a `${}`.
178
171
 
179
172
  ---
180
173
 
@@ -183,24 +176,24 @@ lower it. That is the strongest reason not to write one.
183
176
  ```
184
177
  @layer navi {
185
178
  /* CSS variable defaults — and, when navi says so, a rule it offers back */
186
- .navi_button {
187
- --button-font-size: var(--navi-control-font-size);
188
- --button-border-radius: var(--navi-control-border-radius);
179
+ .navi_thing {
180
+ --thing-border-radius: var(--navi-control-border-radius);
189
181
  }
190
182
  }
191
183
 
192
184
  /* The rules that paint and lay out — outside any layer */
193
- .navi_button {
194
- font-size: var(--button-font-size);
195
- border-radius: var(--button-border-radius);
185
+ .navi_thing {
186
+ box-sizing: border-box;
187
+ border-radius: var(--thing-border-radius);
196
188
  }
197
189
  ```
198
190
 
199
191
  ### Why defaults go inside `@layer navi`
200
192
 
201
- CSS cascade layers are ordered below unlayered styles. Anything declared in `@layer navi` is automatically beaten by any unlayered rule from the page or a design system, without needing `!important`.
202
-
203
- This means an app can override a Navi default from its own unlayered CSS, without `!important`, as long as its selector targets the same element the default was declared on:
193
+ Cascade layers are ordered below unlayered styles: anything declared in
194
+ `@layer navi` is beaten by any unlayered rule from the page or a design system,
195
+ without `!important`, as long as that rule targets the element the default was
196
+ declared on:
204
197
 
205
198
  ```css
206
199
  /* App CSS — no layer needed, automatically wins over @layer navi */
@@ -209,34 +202,29 @@ This means an app can override a Navi default from its own unlayered CSS, withou
209
202
  }
210
203
  ```
211
204
 
212
- Targeting the same element is not a detail — see [`--navi-*` vs `--component-*`](#--navi--vs---component--where-the-override-has-to-go) below.
205
+ Targeting the same element is not a detail — see
206
+ [`--navi-*` vs `--component-*`](#--navi--vs---component--where-the-override-has-to-go)
207
+ below.
213
208
 
214
209
  ### Why actual rules stay outside any layer
215
210
 
216
- Navi components often need to enforce specific values that global resets or utility libraries may clobber — for example `box-sizing: content-box`, `white-space: nowrap`, or `display: inline-flex`. If these rules were inside `@layer navi`, any unlayered global style (e.g. `* { box-sizing: border-box }`) would silently override them, breaking component layout.
217
-
218
- By keeping rules outside any layer, Navi wins by default without resorting to `!important`. An app that genuinely needs to change a structural rule should do so through the CSS variable surface, not by overriding the rule directly.
211
+ A component enforces values that global resets and utility libraries clobber —
212
+ `box-sizing: content-box`, `white-space: nowrap`, `display: inline-flex`.
213
+ Inside `@layer navi`, any unlayered global style
214
+ (`* { box-sizing: border-box }`) would silently override them and break the
215
+ layout. Unlayered, navi wins by default without `!important`; an app that needs
216
+ to change a structural rule does it through the CSS variable that feeds it, not
217
+ by overriding the rule.
219
218
 
220
219
  ### The exception: a rule navi offers back
221
220
 
222
- Some rules are not structure — they are what navi puts there in the absence of
223
- anything else, and an app (or another navi component) is meant to win over them
224
- plainly. Those go **inside** `@layer navi`, rules and all, and each one carries a
225
- comment naming who is supposed to win:
226
-
227
- - `[navi-aspect-ratio]`'s `min-width/min-height` and `.navi_icon`'s `display`
228
- (`box.jsx`, `text.jsx`), so box.jsx's own unlayered `[navi-box-flow]`
229
- attributes can still change the display;
230
- - the text properties a surface takes back from its opener
231
- (`surface_text_css.js`), so an app that wants one of them back says so;
232
- - the `[data-url-target]` mark, the document's `line-height` and
233
- `font-family`, a Field's spacing and a Label's dimmed color — appearance with
234
- no structural role;
235
- - `[data-navi-safe-area]`'s padding (`safe_area.js`), which is a suggestion for
236
- an element the **app** owns.
237
-
238
- So the layer is readable from the outside as a statement rather than an
239
- accident:
221
+ Some rules are not structure: they are what navi puts there when nothing else
222
+ says anything, and an app (or another navi component) is meant to win over them
223
+ plainly. Those go **inside** `@layer navi`, rules and all, and each carries a
224
+ comment naming who is supposed to win — `[data-navi-safe-area]`'s padding, a
225
+ suggestion for an element the **app** owns; a button's `display`, font and
226
+ line, so an app's own button typography wins without going through the
227
+ `--button-*` variables.
240
228
 
241
229
  | In `@layer navi` | Unlayered |
242
230
  | ------------------------------------------------ | -------------------------------- |
@@ -245,9 +233,8 @@ accident:
245
233
 
246
234
  Either way an app never has to inflate a selector: a layered rule loses to any
247
235
  unlayered rule of the app's, and an unlayered one is reached through the
248
- variable that feeds it — `--picker-border-radius` for the picker's corners,
249
- `--navi-control-border-radius` for every control's, both declared in the layer
250
- and both overridable from a single-class rule.
236
+ variable that feeds it (`--picker-border-radius` for a picker's corners,
237
+ `--navi-control-border-radius` for every control's).
251
238
 
252
239
  ---
253
240
 
@@ -255,7 +242,9 @@ and both overridable from a single-class rule.
255
242
 
256
243
  ### 1. Component props (preferred)
257
244
 
258
- Props are the primary way to customize appearance. They translate to inline `style` attributes or `data-*` attributes, both of which have higher specificity than class-based rules.
245
+ Props are the primary way to customize appearance. They translate to an inline
246
+ `style`, which outranks any selector, or to a `data-*` attribute navi's own
247
+ rules read (an attribute selector weighs what a class does).
259
248
 
260
249
  ```jsx
261
250
  // Size and color via props
@@ -265,60 +254,14 @@ Props are the primary way to customize appearance. They translate to inline `sty
265
254
  <Button style={{ "--button-font-size": "18px" }} />
266
255
  ```
267
256
 
268
- #### Variants set defaults, never resolved values
269
-
270
- A control resolves each styled property in two steps: the public variable
271
- holds what was asked for (`--picker-background-color`), and an internal
272
- `--x-` variable holds what is finally painted, per state:
273
-
274
- ```css
275
- .navi_picker {
276
- --x-picker-background-color: var(--picker-background-color);
277
-
278
- &[data-hover] {
279
- --x-picker-background-color: var(--picker-background-color-hover);
280
- }
281
- }
282
- ```
283
-
284
257
  A variant (`icon`, `discrete`, `bare`, `border`, `headless`…) describes what
285
- the caller did **not** say, so it writes the public variable — the default —
286
- and never the `--x-` one:
287
-
288
- ```css
289
- &[data-variant="icon"] {
290
- /* ✅ a default: a backgroundColor prop, being inline on this same element, wins */
291
- --picker-background-color: transparent;
292
- /* ❌ a verdict: the prop is read, translated, and then thrown away */
293
- --x-picker-background-color: transparent;
294
- }
295
- ```
296
-
297
- Writing `--x-` from a variant is the one failure mode that costs real time to
298
- diagnose: the prop is accepted, it reaches its variable with the right value,
299
- and nothing happens. A prop silently without effect is worse than a prop
300
- refused.
301
-
302
- Two things come with moving the default:
303
-
304
- - the **per-state** variables are derived from the base one by formula
305
- (`hover` = 5% black over the background, `disabled` = 5% grey), so a variant
306
- that clears the background must re-point them at the base
307
- (`--picker-background-color-hover: var(--picker-background-color)`), or a box
308
- reappears on hover under a control that is supposed to have none. When the
309
- variant does have a resting movement, express it as a mix **into** the
310
- background (`color-mix(in srgb, currentColor 8%, var(--picker-background-color))`)
311
- rather than a replacement, so it still composes with a color the caller gave.
312
- - a variable fed by another prop keeps that chain in its fallback:
313
- `--button-background-color: var(--button-background, transparent)` leaves both
314
- `background` and `backgroundColor` working.
315
-
316
- The same holds for sizing: a variant lowers `--picker-padding-x-default`, not
317
- `--x-picker-padding-left`.
258
+ the caller did not say: it only moves defaults, so a prop given next to it
259
+ always wins.
318
260
 
319
261
  ### 2. CSS variables (for global or theme-level changes)
320
262
 
321
- When the same change applies to many components (e.g. a design token update), set the variable at a higher scope:
263
+ When the same change applies to many components (e.g. a design token update),
264
+ set the variable at a higher scope:
322
265
 
323
266
  ```css
324
267
  /* Override Navi's default at the page or theme level */
@@ -327,20 +270,33 @@ When the same change applies to many components (e.g. a design token update), se
327
270
  }
328
271
  ```
329
272
 
330
- Because Navi's `--navi-*` defaults are themselves declared on `:root` inside `@layer navi`, this unlayered `:root` rule targets the same element and wins automatically.
273
+ Navi's `--navi-*` defaults are themselves declared on `:root` inside
274
+ `@layer navi`, so this unlayered `:root` rule targets the same element and wins
275
+ automatically.
276
+
277
+ How wide popups may ever get is the app's own screen, stated once on `:root`
278
+ with `--navi-app-max-width` — never `--dialog-max-width`, a `--component-*`
279
+ token every dialog resets on itself: see
280
+ [safe_area.md](./safe_area.md#an-app-that-is-narrower-than-the-window).
331
281
 
332
282
  #### `--navi-*` vs `--component-*`: where the override has to go
333
283
 
334
- The two families are **not** interchangeable, and layers have nothing to do with it:
284
+ The two families are **not** interchangeable, and layers have nothing to do
285
+ with it:
335
286
 
336
- | Token | Declared on | Overridable from `:root`? |
337
- | --------------- | ----------------------- | ------------------------------- |
338
- | `--navi-*` | `:root` (`@layer navi`) | Yes |
339
- | `--component-*` | `.navi_<component>` | **No** — must match the element |
287
+ | Token | Declared on | Overridable from `:root`? |
288
+ | --------------- | ----------------------- | ---------------------------------------------------------------------------- |
289
+ | `--navi-*` | `:root` (`@layer navi`) | Yes — but the five color keywords stop at each popup and callout (see below) |
290
+ | `--component-*` | `.navi_<component>` | **No** — must match the element |
340
291
 
341
- For a custom property, a declaration made **on the element itself** always beats the same property **inherited** from an ancestor. The cascade (specificity, layers, `!important`) only arbitrates between declarations targeting the same element — it never lets an ancestor win over the element's own declaration.
292
+ For a custom property, a declaration made **on the element itself** always
293
+ beats the same property **inherited** from an ancestor. The cascade
294
+ (specificity, layers, `!important`) only arbitrates between declarations
295
+ targeting the same element — it never lets an ancestor win over the element's
296
+ own declaration.
342
297
 
343
- So this does nothing, because `.navi_link` declares `--link-color-pressed` on itself:
298
+ So this does nothing, because `.navi_link` declares `--link-color-pressed` on
299
+ itself:
344
300
 
345
301
  ```css
346
302
  :root {
@@ -367,54 +323,47 @@ Three ways to actually change it:
367
323
  <Link style={{ "--link-color-pressed": "blue" }} />
368
324
  ```
369
325
 
370
- Note that scoping to an ancestor is not enough: `.my-sidebar { --link-color-pressed: blue }` fails for the same reason `:root` does. The selector has to reach the link element itself.
326
+ Scoping to an ancestor is not enough: `.my-sidebar { --link-color-pressed: blue }`
327
+ fails for the same reason `:root` does. The selector has to reach the link
328
+ element itself.
371
329
 
372
- When a component default deserves to be themed globally, promote it: declare a `--navi-<component>-<thing>` in [navi_css_vars.js](../src/navi_css_vars.js) and make the component default read `var(--navi-…)`.
330
+ When a component default deserves to be themed globally, promote it: declare a
331
+ `--navi-<component>-<thing>` in [navi_css_vars.js](../src/navi_css_vars.js) and
332
+ make the component default read `var(--navi-…)`.
373
333
 
374
334
  #### Two planes: the paper and the frame
375
335
 
376
- An app draws on two planes, and navi names both so that nothing has to invent
377
- the second one:
378
-
379
- - **the paper**, `--navi-surface-color`, what content sits on — a control's
380
- background, a card, a popup;
381
- - **the frame**, `--navi-chrome-color`, what surrounds a screen — a top bar, a
382
- side menu, a toolbar.
383
-
384
- ```jsx
385
- <Box background="chrome" />
386
- ```
387
-
388
- The frame is derived from the paper (a step toward the dark on a light page,
389
- toward the light on a dark one) rather than written as a literal, which is the
390
- whole point: `#f6f7f9` on a bar is a bar that stops being one in dark mode, and
391
- a bar painted in the paper's own color on a page of that color is not a bar, it
392
- is a line. An app that wants another frame sets the one token on `:root`.
336
+ The frame around a screen — a top bar, a side menu, a toolbar:
337
+ `--navi-chrome-color`, `<Box background="chrome" />` — is derived from the
338
+ paper content sits on (`--navi-surface-color`), never written as a literal: a
339
+ literal on a bar stops being a bar in dark mode. An app that wants another
340
+ frame sets the one token on `:root`.
393
341
 
394
342
  #### A surface is a new paper: what reaches a popup from its opener
395
343
 
396
344
  A popup (`Dialog`, `Popover`, everything built on them) and a callout are
397
- painted in the top layer but live in the DOM subtree of what opened them. For
398
- the cascade they are descendants of that element: every inherited property and
399
- every custom property declared on an ancestor reaches them, the top layer
400
- changing nothing about it. A dark card that writes in white, declared on the
401
- card, is the color a dialog opened from that card starts with.
345
+ painted in the top layer but live in the DOM subtree of what opened them — on
346
+ purpose: they inherit the opener's fonts, colors and custom properties, and
347
+ they leave with the screen that opened them. For the cascade they are
348
+ descendants of that element, the top layer changing nothing about it: a dark
349
+ card that writes in white, declared on the card, is the color a dialog opened
350
+ from that card starts with.
402
351
 
403
352
  Navi takes back what it knows a surface needs of its own — declared on the
404
353
  surface element itself, so it beats whatever an ancestor declared, whatever
405
- the layer (see the table above):
354
+ the layer:
406
355
 
407
- | taken back | where |
408
- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
409
- | the ink (`color`) | `--navi-popup-color` on `.navi_popover` / `.navi_dialog`; `revert` (the UA's `CanvasText`) on `.navi_callout` |
410
- | text properties that belong to the opener (alignment, transform, shadow, spacing, wrapping) | `surface_text_css.js`, which also says what is deliberately kept |
411
- | the five color keywords `--navi-color-primary/secondary/emphasis/discrete/hint` | re-declared on each surface in `navi_css_vars.js` |
356
+ | taken back | how |
357
+ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
358
+ | the ink (`color`) | `--navi-popup-color` on a popup; the UA's `CanvasText` on a callout |
359
+ | text properties that belong to the opener (alignment, transform, shadow, spacing, wrapping) | reset in `@layer navi`, so an app that wants one of them back says so on the surface |
360
+ | the five color keywords `--navi-color-primary/secondary/emphasis/discrete/hint` | re-declared on each surface, so a `:root` value of one stops there |
412
361
 
413
- Everything else declared on a container is inherited by the popup it opens.
414
- That is the shape of the bug to expect: a token an app pinned on a container
415
- for that container's paper — a background, a border color, a spacing, one of
416
- its own `--app-*` — arriving on a popup that has a different paper. When it
417
- hits, the answers are, in order:
362
+ Everything else declared on a container reaches the popup it opens. That is the
363
+ shape of the bug to expect: a token an app pinned on a container for that
364
+ container's paper — a background, a border color, a spacing, one of its own
365
+ `--app-*` — arriving on a popup that has a different paper. The answers, in
366
+ order:
418
367
 
419
368
  1. the token is about the paper and navi owns it: re-declare it on the surface,
420
369
  next to the color keywords;
@@ -443,14 +392,13 @@ ratio on `:root`, and the page and its popups agree:
443
392
  }
444
393
  ```
445
394
 
446
- The ratio is a `:root` knob only, and this is the trap to know about. A `var()`
447
- inside a custom property is substituted where **that** property is declared,
448
- not where it is read: `--navi-color-secondary` is declared on `:root` (and on
449
- each surface), so the `80%` is baked in there and a card inherits the
450
- already-mixed formula. A ratio set on a container changes nothing for the
451
- container's own text — and it _is_ read by the next surface opened from it,
452
- which re-declares the formula and resolves the `var()` against the inherited
453
- ratio:
395
+ The ratio is a `:root` knob only. A `var()` inside a custom property is
396
+ substituted where **that** property is declared, not where it is read:
397
+ `--navi-color-secondary` is declared on `:root` (and on each surface), so the
398
+ `80%` is baked in there and a card inherits the already-mixed formula. A ratio
399
+ set on a container changes nothing for the container's own text — and it _is_
400
+ read by the next surface opened from it, which re-declares the formula and
401
+ resolves the `var()` against the inherited ratio:
454
402
 
455
403
  ```css
456
404
  .card {
@@ -462,35 +410,26 @@ ratio:
462
410
  ```
463
411
 
464
412
  So a theme is a number on `:root`; a paper is a color, pinned on the container,
465
- and it stops at the surface. What must not be done to make container ratios
466
- work is declaring the formulas on `*` so they resolve on every element: the
467
- pinned keyword above would then be overwritten on each of the card's children,
468
- and a paper could no longer say anything.
469
-
470
- #### An app narrower than the screen
471
-
472
- A dialog lives in the browser's top layer and is calibrated on the viewport, so
473
- an app that is a column in a wide window states its own screen once, on `:root`,
474
- with `--navi-app-max-width` — and never caps popups through
475
- `--dialog-max-width`, a `--component-*` token every dialog resets on itself and
476
- a `maxWidth` prop writes inline. The whole of it, placement included, is in
477
- [safe_area.md](./safe_area.md#an-app-that-is-narrower-than-the-window).
413
+ and it stops at the surface. Declaring the formulas on `*` to make container
414
+ ratios work is the thing not to do: it would overwrite the pinned keyword on
415
+ each of the card's children, and a paper could no longer say anything.
478
416
 
479
417
  ### 3. Direct rule override (avoid unless necessary)
480
418
 
481
- Overriding the actual CSS rules (not the variables) is intentionally hard — that is by design. If you find yourself needing to do this, it usually means a CSS variable should be exposed for that property. Open an issue or add the variable yourself and contribute it back.
419
+ Overriding the actual CSS rules (not the variables) is intentionally hard. If
420
+ you find yourself needing to, a CSS variable should usually be exposed for that
421
+ property: open an issue, or add the variable and contribute it back.
482
422
 
483
423
  ---
484
424
 
485
425
  ## Counting children: what navi puts in your tree
486
426
 
487
- Some of what navi shows is rendered inside the tree you wrote. A popup renders
488
- in its opener's own subtree, so a `Dialog` or `Popover` written next to the
489
- button that opens it is a sibling of that button. A callout — the speech bubble
490
- a control pops to explain itself — is mounted on what it explains, or beside it
491
- when that element cannot hold it (a `<button>`, an `<input>`, anything inside a
492
- `<label>`). This is deliberate: they inherit your fonts, your colors, your
493
- custom properties, and they leave with the screen that opened them.
427
+ Some of what navi shows is rendered inside the tree you wrote (see
428
+ [A surface is a new paper](#a-surface-is-a-new-paper-what-reaches-a-popup-from-its-opener)):
429
+ a `Dialog` or `Popover` written next to the button that opens it is a sibling
430
+ of that button, and a callout — the speech bubble a control pops to explain
431
+ itself — is mounted on what it explains, or beside it when that element cannot
432
+ hold it (a `<button>`, an `<input>`, anything inside a `<label>`).
494
433
 
495
434
  None of them draws in the flow. They are `position: fixed`/`absolute`, most of
496
435
  them in the top layer: no grid track, no flex item, no line box, no gap. They
@@ -512,17 +451,14 @@ open:
512
451
  ```
513
452
 
514
453
  Press the third cell, its callout opens, and the cell is no longer the last
515
- child: it loses the span and halves. Close the callout and it comes back. The
516
- grid reserved nothing — the rule simply stopped matching. What makes this one
517
- nasty is that it is invisible from the app: the CSS is right, the JSX is right,
518
- and the trap only springs on an interaction, after the layout was reviewed.
454
+ child: it loses the span and halves. Close the callout and it comes back.
519
455
 
520
456
  ### Say which children the rule means
521
457
 
522
458
  A counting pseudo-class takes what to count: `:nth-child(An+B of S)` and
523
459
  `:nth-last-child(An+B of S)` walk only the children matching `S`, so name your
524
460
  own — either by what they are, or by what navi's out-of-flow elements all carry
525
- (`navi-out-of-flow`, the same marker navi's own `Group` reads):
461
+ (`navi-out-of-flow`, the same marker navi's own [`Group`](./group.md) reads):
526
462
 
527
463
  ```css
528
464
  .grid > .cell:nth-last-child(1 of .cell):nth-child(odd of .cell) {
@@ -553,14 +489,14 @@ elements and the boxes before and after.
553
489
 
554
490
  ## Summary
555
491
 
556
- | What you want to change | How to do it |
557
- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
558
- | One component instance | Component prop or `style` attribute |
559
- | All instances of a component | `--component-*` in unlayered app CSS, on a selector matching the component |
560
- | A global design token | `--navi-*` on `:root` |
561
- | The share of ink in `secondary`/`emphasis`/`discrete`/`hint` | `--navi-color-*-mix` on `:root` — never on a container |
562
- | A container's own paper (a dark card) | `color` on the container, plus a `--navi-color-*` keyword pinned if its formula reads wrong there; both stop at the next popup |
563
- | How wide popups may ever get | `--navi-app-max-width` on `:root` |
564
- | A structural layout rule | Expose a new CSS variable (contribute) |
565
- | A rule counting children of a container holding navi controls | `:nth-child(… of S)`, `:not([navi-out-of-flow])`, or a class — a popup or a callout is a child too |
566
- | What a variant decided | A prop — a variant only ever moves defaults, so props keep winning |
492
+ | What you want to change | How to do it |
493
+ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
494
+ | One component instance | Component prop or `style` attribute |
495
+ | All instances of a component | `--component-*` in unlayered app CSS, on a selector matching the component |
496
+ | A global design token | `--navi-*` on `:root` |
497
+ | The share of ink in `secondary`/`emphasis`/`discrete`/`hint` | `--navi-color-*-mix` on `:root` — never on a container |
498
+ | A container's own paper (a dark card) | `color` on it, plus a keyword pinned if its formula reads wrong; both stop at the next popup |
499
+ | How wide popups may ever get | `--navi-app-max-width` on `:root` |
500
+ | A structural layout rule | Expose a new CSS variable (contribute) |
501
+ | A rule counting children of a container holding navi controls | `:nth-child(… of S)`, `:not([navi-out-of-flow])`, or a class — a popup or a callout is a child too |
502
+ | What a variant decided | A prop — a variant only ever moves defaults, so props keep winning |