@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.
- package/README.md +1 -1
- package/dist/dev/jsenv_navi.js +347 -83
- package/dist/dev/jsenv_navi.js.map +12 -12
- package/dist/jsenv_navi.js +345 -81
- package/dist/jsenv_navi.js.map +12 -12
- package/docs/AI_INSTRUCTIONS.md +177 -258
- package/docs/actions.md +164 -251
- package/docs/autofocus.md +66 -83
- package/docs/control_object.md +117 -214
- package/docs/control_value.md +121 -185
- package/docs/create_and_edit.md +159 -254
- package/docs/css_architecture.md +196 -260
- package/docs/data_states.md +41 -44
- package/docs/dialog_shape.md +91 -134
- package/docs/drag_interactions.md +291 -454
- package/docs/drag_to_travel.md +152 -748
- package/docs/dynamic_import.md +119 -136
- package/docs/error_handling.md +75 -127
- package/docs/field_validation.md +49 -89
- package/docs/form_changed.md +61 -78
- package/docs/group.md +141 -0
- package/docs/i18n.md +31 -76
- package/docs/interactions.md +170 -326
- package/docs/list_action.md +36 -51
- package/docs/list_refresh.md +73 -115
- package/docs/mobile_touch.md +56 -62
- package/docs/navigation.md +377 -723
- package/docs/network_policy.md +61 -75
- package/docs/pan_zoom.md +62 -103
- package/docs/popup_backdrop.md +128 -168
- package/docs/popup_lift.md +179 -268
- package/docs/popup_open.md +380 -651
- package/docs/resource.md +228 -408
- package/docs/route_transitions.md +333 -473
- package/docs/safe_area.md +102 -89
- package/docs/scroll.md +175 -317
- package/docs/state_binding.md +40 -84
- package/docs/testid.md +18 -23
- package/docs/typography.md +111 -141
- package/docs/view_transitions.md +106 -104
- package/docs/z_index.md +61 -109
- package/package.json +2 -2
- package/docs/badge_list.md +0 -83
- package/docs/control_group.md +0 -168
- package/docs/mobile_layout_pitfalls.md +0 -145
package/docs/css_architecture.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# Navi CSS Architecture
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
along with the code
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
100
|
-
`
|
|
101
|
-
|
|
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
|
|
108
|
-
in `url()`, in
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
.
|
|
187
|
-
--
|
|
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
|
-
.
|
|
194
|
-
|
|
195
|
-
border-radius: var(--
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
223
|
-
anything
|
|
224
|
-
plainly. Those go **inside** `@layer navi`, rules and all, and each
|
|
225
|
-
comment naming who is supposed to win
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
249
|
-
`--navi-control-border-radius` for every control's
|
|
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
|
|
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
|
|
286
|
-
|
|
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),
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
card, is the color a dialog opened
|
|
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
|
|
354
|
+
the layer:
|
|
406
355
|
|
|
407
|
-
| taken back |
|
|
408
|
-
| ------------------------------------------------------------------------------------------- |
|
|
409
|
-
| the ink (`color`) | `--navi-popup-color` on
|
|
410
|
-
| text properties that belong to the opener (alignment, transform, shadow, spacing, wrapping) |
|
|
411
|
-
| the five color keywords `--navi-color-primary/secondary/emphasis/discrete/hint` | re-declared on each surface
|
|
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
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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.
|
|
466
|
-
work is
|
|
467
|
-
|
|
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
|
|
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
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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.
|
|
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
|
|
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 |
|