actual-css 0.13.0 → 0.14.1
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/CHANGELOG.md +67 -0
- package/README.md +8 -8
- package/components.json +116 -10
- package/dist/actual.full.css +652 -77
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/design-notes/widget-primitives.md +4 -0
- package/docs/navigation.json +2 -0
- package/docs/pages/components/alert.md +12 -2
- package/docs/pages/components/button.md +3 -2
- package/docs/pages/components/list.md +6 -3
- package/docs/pages/components/segmented.md +83 -0
- package/docs/pages/components/steps.md +4 -2
- package/docs/pages/enhancements/flyout.md +9 -3
- package/docs/pages/enhancements/overview.md +2 -1
- package/docs/pages/enhancements/tabs.md +7 -2
- package/docs/pages/enhancements/tooltip.md +3 -1
- package/docs/pages/examples/overview.md +5 -3
- package/docs/pages/forms/color-swatches.md +131 -0
- package/docs/pages/forms/combobox.md +2 -2
- package/docs/pages/forms/controls.md +6 -3
- package/docs/pages/forms/joined-controls.md +6 -2
- package/docs/pages/forms/overview.md +1 -0
- package/docs/pages/forms/switch.md +4 -2
- package/docs/pages/foundations/theming.md +1 -1
- package/docs/pages/foundations/tokens.md +54 -23
- package/docs/pages/guides/design-tools.md +13 -7
- package/docs/pages/guides/introduction.md +1 -1
- package/docs/pages/layout/scroll-snap.md +4 -2
- package/package.json +5 -4
- package/reserved-classes.json +3 -0
- package/src/cli/actual-css.js +3 -3
- package/src/css/components/alert.css +65 -14
- package/src/css/components/app-nav.css +17 -9
- package/src/css/components/badge.css +23 -9
- package/src/css/components/button.css +17 -2
- package/src/css/components/close.css +6 -3
- package/src/css/components/index.css +1 -0
- package/src/css/components/list.css +18 -3
- package/src/css/components/navbar.css +5 -2
- package/src/css/components/segmented.css +186 -0
- package/src/css/components/steps.css +6 -3
- package/src/css/components/tab.css +1 -1
- package/src/css/core/focus.css +3 -0
- package/src/css/core/tokens.css +20 -4
- package/src/css/core/variants.css +16 -5
- package/src/css/forms/color-swatch.css +163 -0
- package/src/css/forms/form-actions.css +9 -0
- package/src/css/forms/index.css +1 -0
- package/src/css/forms/native.css +50 -20
- package/src/css/forms/switch.css +5 -2
- package/src/css/layout/scroll-snap.css +30 -0
- package/src/css/layout/topbar.css +10 -0
- package/src/js/enhance.js +14 -0
- package/src/js/floating-space.js +26 -0
- package/src/js/flyout.js +3 -1
- package/src/js/focus.js +16 -1
- package/src/js/parse-config.js +5 -2
- package/src/js/surface.js +10 -0
- package/src/js/tooltip.js +2 -2
- package/src/tooling/color.js +14 -1
- package/src/tooling/css-bundle.js +0 -5
- package/src/tooling/design-components.js +9 -5
- package/src/tooling/design-export.js +8 -0
- package/src/tooling/design-tokens.js +163 -43
- package/src/tooling/theme-contrast.js +151 -22
|
@@ -151,34 +151,42 @@ Color should reinforce structure and meaning, not carry them alone. Information
|
|
|
151
151
|
|
|
152
152
|
The default palette is **Ink & Terra**: a near-neutral aubergine `primary`, terracotta `secondary`, and pigment-toned statuses (sage, ochre, brick) tuned to equal perceived weight. The neutral ramp (text, borders, subtle surfaces) is tinted toward the primary hue so the whole page quietly carries the identity. Optional themes can re-introduce vivid intent palettes on top of this foundation.
|
|
153
153
|
|
|
154
|
-
Soft surfaces
|
|
154
|
+
Soft surfaces are generated from intent colors through `color-mix()`, in two
|
|
155
|
+
recipes that share one ink:
|
|
156
|
+
|
|
157
|
+
- **Tint** — the default `.badge` and `.alert`, and `.btn.soft`: a translucent
|
|
158
|
+
intent laid over whatever surface is underneath, with no rim. It keeps
|
|
159
|
+
separating on `--surface-subtle` and `--surface-raised`, where a fixed mix
|
|
160
|
+
with `--surface` would match the surface it sits on.
|
|
161
|
+
- **Mix** — the shared `.soft` on zones (`.card`, `.chat-bubble`, `.navbar`,
|
|
162
|
+
`.app-nav`): an opaque mix of `--surface` with the intent and a tinted rim.
|
|
163
|
+
A zone may sit over an image, where a translucent fill would hand the text's
|
|
164
|
+
contrast to whatever is behind it.
|
|
165
|
+
|
|
166
|
+
Themes tune both globally instead of rewriting component selectors:
|
|
155
167
|
|
|
156
168
|
```css
|
|
157
169
|
:root {
|
|
158
|
-
--soft-bg-mix: 88%;
|
|
159
|
-
--soft-
|
|
160
|
-
--soft-hover-alpha: 12%;
|
|
170
|
+
--soft-bg-mix: 88%; /* share of surface; the tint's opacity is 100% minus it */
|
|
171
|
+
--soft-hover-alpha: 6%; /* extra intent share on hover */
|
|
161
172
|
--soft-fg-mix: 100%;
|
|
162
173
|
}
|
|
163
174
|
```
|
|
164
175
|
|
|
165
|
-
`--soft-border-mix`
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
with no contour — the flatter language of a modern component set, from one
|
|
169
|
-
declaration:
|
|
176
|
+
`--soft-border-mix` has no default. Unset, the tint draws no rim and the mix
|
|
177
|
+
keeps a 65% one. A theme that sets it gives every soft treatment a tinted rim
|
|
178
|
+
of that weight — contoured badges and alerts, from one declaration:
|
|
170
179
|
|
|
171
180
|
```css
|
|
172
|
-
[data-theme="
|
|
173
|
-
--soft-border-mix:
|
|
181
|
+
[data-theme="contoured"] {
|
|
182
|
+
--soft-border-mix: 65%;
|
|
174
183
|
}
|
|
175
184
|
```
|
|
176
185
|
|
|
177
|
-
|
|
178
|
-
(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
borderless soft is a look, not a contract the Degraded tier upholds.
|
|
186
|
+
A rule that re-derives a soft color reads it with the recipe's own fallback:
|
|
187
|
+
`var(--soft-border-mix, 100%)` for a tint, `var(--soft-border-mix, 65%)` for a
|
|
188
|
+
mix. Both recipes are rendering-only — the baseline path below `color-mix()`
|
|
189
|
+
keeps `var(--surface-subtle)` and `var(--border)`.
|
|
182
190
|
|
|
183
191
|
`--soft-fg-mix` is the share of raw intent in soft *text*. At its `100%` default soft ink is the intent color itself, which is what a palette of dark, muted intents wants. A vivid or light palette cannot afford that: a soft badge then paints intent-tinted ink on an intent-tinted surface, and the two converge. Lowering the mix rebates the ink toward `--text`, which is the right direction in both schemes because `--text` is dark on a light theme and light on a dark one. Around `45%` a fully saturated palette recovers AA while the ink still reads as its intent.
|
|
184
192
|
|
|
@@ -193,17 +201,19 @@ hook, and the soft recipe reads it before falling back to the global mix:
|
|
|
193
201
|
- `--soft-fg-mix` — the **global fallback derivation**, unchanged.
|
|
194
202
|
|
|
195
203
|
**Soft foregrounds are calibrated against the darkest or closest interactive
|
|
196
|
-
surface they are expected to appear on.**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
the
|
|
200
|
-
|
|
201
|
-
|
|
204
|
+
surface they are expected to appear on.** The tint is translucent, so the same
|
|
205
|
+
pair reads differently on each surface — usually closest on `--surface-subtle`.
|
|
206
|
+
For interactive soft treatments the fill moves toward the intent on hover
|
|
207
|
+
(`--soft-hover-alpha`), so the binding pair is the *hovered* tint over
|
|
208
|
+
`--surface-subtle` and the resting fill simply carries more margin; the default
|
|
209
|
+
palette ships its own `--primary`/`--secondary`/… `-soft-fg` hooks to keep
|
|
210
|
+
every pair ≥ 4.5:1 on `--surface`, `--surface-raised` and `--surface-subtle` in
|
|
211
|
+
both color schemes, enforced by `tests/browser/soft-recipe.test.js`. The hard line stops at the default theme:
|
|
202
212
|
presets and custom `[data-theme]` islands keep the `--soft-fg-mix` derivation
|
|
203
213
|
unless they declare the hooks themselves (the framework resets them outside
|
|
204
214
|
the default boundary). A theme that overrides the soft palette is responsible
|
|
205
215
|
for preserving sufficient text contrast; `bunx --bun actual-css contrast --theme FILE`
|
|
206
|
-
measures its resting and hovered pairs.
|
|
216
|
+
measures its resting and hovered pairs, composited on each of those surfaces.
|
|
207
217
|
|
|
208
218
|
A theme whose primary should read more muted than its statuses sets only
|
|
209
219
|
`--primary-soft-fg`, and every soft `.primary` foreground follows it. Without a
|
|
@@ -599,12 +609,33 @@ A minimal recolor theme (illustrative, not a shipped theme):
|
|
|
599
609
|
|
|
600
610
|
Several tokens are theme-derived aliases that reference other tokens — `--state-selected`/`--state-selected-fg`/`--state-disabled`, `--indicator-ring`, `--shadow`/`--shadow-popout`, `--heading`, `--selection-bg`/`--selection-fg`, and `--surface-opaque`/`--surface-solid-fg`. They are declared on `:root, [data-theme]` so they recompute on every theme boundary: a custom property resolves its `var()` references at computed-value time on the element that declares it, so an alias declared only on `:root` would be inherited as an already-resolved value and would not follow a `[data-theme]` island's overridden tokens. A theme that wants a distinct alias (e.g. a `--selection-bg` of its own) overrides it explicitly afterwards, which wins by cascade order.
|
|
601
611
|
|
|
612
|
+
Selected state has three roles:
|
|
613
|
+
|
|
614
|
+
- `--state-selected` — the selected fill or accent: a pressed button, a checked switch, a completed step.
|
|
615
|
+
- `--state-selected-fg` — ink on that fill.
|
|
616
|
+
- `--state-selected-text` — selected ink on a regular surface: the current nav link, the selected tab, the current step's number and ring.
|
|
617
|
+
|
|
618
|
+
`--state-selected-text` has no default: the core resets it to the guaranteed-invalid value on every theme boundary, and each consumer reads `var(--state-selected-text, var(--state-selected))`. The reset is what lets a nested theme island reach its own `--state-selected`: a theme that resolves the hook to another token bakes the reference where it is declared, so an island that sets nothing would otherwise inherit the resolved value and never fall back. Set it when one color cannot be both a fill under its `-fg` and text on the page — typically a dark scheme, where a fill dark enough for white ink is too dark to read as text. Pulling the accent toward `--text` keeps its hue in both schemes:
|
|
619
|
+
|
|
620
|
+
```css
|
|
621
|
+
[data-theme="my-theme"] {
|
|
622
|
+
--state-selected-text: light-dark(
|
|
623
|
+
var(--primary),
|
|
624
|
+
color-mix(in oklab, var(--primary) 50%, var(--text))
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
`actual-css contrast` measures it on each surface (4.5:1 for text, 3:1 for the step ring).
|
|
630
|
+
|
|
602
631
|
Without `data-theme`, the default theme advertises `color-scheme: light dark` and follows the user's OS preference in browsers that support `light-dark()`. Dark themes should set `color-scheme: dark`. Light themes should set `color-scheme: light`. Browsers without `light-dark()` receive the light fallback.
|
|
603
632
|
|
|
604
633
|
Because the native select chevron is an SVG background image and cannot inherit `currentColor`, the select hands the arrow back to the browser under `prefers-contrast: more` (same treatment as `forced-colors: active`) instead of shipping a recolored chevron per theme — one native fallback covers every theme without a high-contrast icon variant to maintain.
|
|
605
634
|
|
|
606
635
|
Under `prefers-contrast: more`, the user's contrast preference wins over a theme's palette by contract: the core collapses `--text-muted`/`--text-subtle` to `--text` and thickens borders and focus so no theme can defeat a legibility requirement. This is a deliberate decision, not a specificity quirk — the repeated `[data-theme][data-theme]` attribute only outranks a preset theme stylesheet loaded after the core.
|
|
607
636
|
|
|
637
|
+
Under `prefers-reduced-transparency: reduce`, `.topbar` and `.form-actions.sticky` paint `--surface` without a backdrop blur. A theme with translucent `--surface-*` redeclares them opaque inside the same media query; the glass demo theme shows the recipe.
|
|
638
|
+
|
|
608
639
|
Under `forced-colors: active`, the browser automatically remaps `color`, `background`, and `border` properties to system colors. The core only adjusts what the remapping cannot express on its own — `--state-disabled` becomes `GrayText` and `--disabled-opacity` becomes `1` — declared on `:root` and `[data-theme]` so every theme boundary follows. Keep local forced-color rules only for custom control geometry, native bar parts, or selected/checked states that would otherwise be communicated only by background color.
|
|
609
640
|
|
|
610
641
|
## Component mapping
|
|
@@ -10,16 +10,22 @@ bunx --bun actual-css design --theme src/theme.css --out design/
|
|
|
10
10
|
|
|
11
11
|
## What is exported
|
|
12
12
|
|
|
13
|
-
| File | Content
|
|
14
|
-
| --------------------------------------------- |
|
|
15
|
-
| `figma/foundations.tokens.json` | spacing, radius, borders, type sizes
|
|
16
|
-
| `figma/light.tokens.json`, `dark.tokens.json` | colors, one file per scheme
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
13
|
+
| File | Content |
|
|
14
|
+
| --------------------------------------------- | -------------------------------------- |
|
|
15
|
+
| `figma/foundations.tokens.json` | spacing, radius, borders, type sizes |
|
|
16
|
+
| `figma/light.tokens.json`, `dark.tokens.json` | colors, one file per scheme |
|
|
17
|
+
| `dtcg/foundations.tokens.json` | the portable profile, every foundation |
|
|
18
|
+
| `dtcg/light.tokens.json`, `dark.tokens.json` | colors, one file per scheme |
|
|
19
|
+
| `penpot/tokens.json` | all tokens, as sets and themes |
|
|
20
|
+
| `penpot/plugin/` | the plugin and component spec |
|
|
19
21
|
|
|
20
22
|
Token names are the CSS names without `--`: `surface-raised` in the design tool is `var(--surface-raised)` in code. A scheme exists when the theme declares it: `color-scheme: light dark` exports both, anything else one. Lengths are exported in px, with the CSS value kept in `$extensions["actual-css"]`.
|
|
21
23
|
|
|
22
|
-
Colors no theme token paints — the soft fills and borders of `.soft` — are exported as recipe tokens named `<variant>-<intent>-<role>` (`soft-danger-bg`, `soft-danger-border`, `soft-danger-fg`), next to the theme colors so they switch with the scheme.
|
|
24
|
+
Colors no theme token paints — the soft fills and borders of `.soft` — are exported as recipe tokens named `<variant>-<intent>-<role>` (`soft-danger-bg`, `soft-danger-border`, `soft-danger-fg`), next to the theme colors so they switch with the scheme.
|
|
25
|
+
|
|
26
|
+
## DTCG
|
|
27
|
+
|
|
28
|
+
`dtcg/` is the portable profile, for any reader that follows the [Design Tokens Format](https://www.designtokens.org/format/): every foundation carries the `$type` the spec defines (`dimension`, `fontWeight`, `fontFamily`, `duration`, `cubicBezier`, `number`), and each color carries a 6-digit `hex` with `alpha` separate. It keeps font stacks, line heights and motion, which `figma/` drops, and omits Figma's adaptations: a font weight is a `fontWeight` here, a `number` there. Shadows and z-index are not exported anywhere.
|
|
23
29
|
|
|
24
30
|
## Figma
|
|
25
31
|
|
|
@@ -53,7 +53,7 @@ optimizing — the bare `actual-css` entrypoint and `actual-css/full` both resol
|
|
|
53
53
|
to the complete compiled framework:
|
|
54
54
|
|
|
55
55
|
```html
|
|
56
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.
|
|
56
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.14/dist/actual.full.min.css">
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
The package does not maintain separate partial bundles. Modular entrypoints map
|
|
@@ -40,8 +40,10 @@ Items snap to the inline start edge by default, with `proximity` snapping so a s
|
|
|
40
40
|
|
|
41
41
|
- `--scroll-snap-item-size` — item `flex` basis; set it for card rails.
|
|
42
42
|
- `--scroll-snap-gap` — space between items.
|
|
43
|
-
- `--scroll-snap-padding` — inline scroll
|
|
44
|
-
|
|
43
|
+
- `--scroll-snap-padding` — preferred inline scroll inset; defaults to
|
|
44
|
+
`--scroll-snap-gap`. Use a length (`0px` for zero); conforming values stay at
|
|
45
|
+
least large enough for the focus gutter, and the rail's edge items keep that
|
|
46
|
+
gutter even at rest.
|
|
45
47
|
- `--scroll-snap-align` — item snap alignment. Prefer `data-snap-align="center"`
|
|
46
48
|
over setting this directly.
|
|
47
49
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-css",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Plain CSS component framework with semantic classes, universal variants, small tokens, theme hooks, and progressive enhancements.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"packageManager": "bun@1.4.
|
|
6
|
+
"packageManager": "bun@1.4.3",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "Thomas <thomas@lekoala.be>",
|
|
9
9
|
"repository": {
|
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
"build:js": "bun scripts/build-js.js",
|
|
133
133
|
"build:themes": "bun scripts/build-themes.js",
|
|
134
134
|
"build:size": "bun scripts/build-size.js",
|
|
135
|
+
"build:pages": "bun scripts/pages.js",
|
|
135
136
|
"export:design": "bun scripts/export-design.js",
|
|
136
137
|
"check:enhance": "bun run scripts/check-enhance.js",
|
|
137
138
|
"generate:reserved": "bun scripts/generate-reserved-classes.js",
|
|
@@ -157,9 +158,9 @@
|
|
|
157
158
|
},
|
|
158
159
|
"devDependencies": {
|
|
159
160
|
"@biomejs/biome": "^2.5.15",
|
|
160
|
-
"happy-dom": "^
|
|
161
|
+
"happy-dom": "^21.0.1"
|
|
161
162
|
},
|
|
162
163
|
"dependencies": {
|
|
163
|
-
"@lekoala/floating": "^0.
|
|
164
|
+
"@lekoala/floating": "^0.3.0"
|
|
164
165
|
}
|
|
165
166
|
}
|
package/reserved-classes.json
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"close",
|
|
49
49
|
"cluster",
|
|
50
50
|
"color",
|
|
51
|
+
"color-swatch",
|
|
51
52
|
"column-gap-context",
|
|
52
53
|
"column-layout",
|
|
53
54
|
"compact",
|
|
@@ -198,6 +199,8 @@
|
|
|
198
199
|
"scroller",
|
|
199
200
|
"scrollspy",
|
|
200
201
|
"secondary",
|
|
202
|
+
"segment",
|
|
203
|
+
"segmented",
|
|
201
204
|
"select",
|
|
202
205
|
"separated",
|
|
203
206
|
"shadow-context",
|
package/src/cli/actual-css.js
CHANGED
|
@@ -22,9 +22,9 @@ function usage() {
|
|
|
22
22
|
"stands for. Remote imports are kept and hoisted to the top of the bundle.",
|
|
23
23
|
"Leaves modern CSS syntax untouched; this is a bundler, not a transpiler.",
|
|
24
24
|
"",
|
|
25
|
-
"design: export a theme's tokens for Figma and
|
|
26
|
-
"Alert and Card measured in headless Chrome and a
|
|
27
|
-
"Needs Bun (bunx --bun actual-css design ...).",
|
|
25
|
+
"design: export a theme's tokens for Figma, Penpot and a portable DTCG profile,",
|
|
26
|
+
"plus Button, Input, Badge, Alert and Card measured in headless Chrome and a",
|
|
27
|
+
"Penpot plugin that builds them. Needs Bun (bunx --bun actual-css design ...).",
|
|
28
28
|
"",
|
|
29
29
|
"contrast: measure the pairs a theme tunes by hand in headless Chrome, in every",
|
|
30
30
|
"scheme it declares: soft ink on its resting and hovered fill, the focus line on",
|
|
@@ -61,6 +61,16 @@
|
|
|
61
61
|
font-size: var(--alert-font-size);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/* Density steps the block padding only: the inline padding is the text
|
|
65
|
+
column's edge and stays stable. Local to the alert, like .card.compact. */
|
|
66
|
+
.alert.compact {
|
|
67
|
+
--alert-pad-block: var(--space-20);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.alert.spacious {
|
|
71
|
+
--alert-pad-block: var(--space-40);
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
.alert > * {
|
|
65
75
|
min-inline-size: 0;
|
|
66
76
|
}
|
|
@@ -85,28 +95,40 @@
|
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
/* Alerts are soft by default. The recipe writes local defaults rather than
|
|
88
|
-
--ui-* so an explicit shared treatment always wins without exclusions.
|
|
98
|
+
--ui-* so an explicit shared treatment always wins without exclusions. The
|
|
99
|
+
fill is the translucent soft tint (see badge.css), with no rim unless a
|
|
100
|
+
theme sets --soft-border-mix. */
|
|
89
101
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
90
102
|
.alert {
|
|
91
|
-
/* @sync soft-
|
|
103
|
+
/* @sync soft-tint */
|
|
92
104
|
--alert-default-bg: color-mix(
|
|
93
105
|
in oklab,
|
|
94
|
-
var(--
|
|
95
|
-
|
|
106
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
|
|
107
|
+
transparent
|
|
96
108
|
);
|
|
97
109
|
--alert-default-border: color-mix(
|
|
98
110
|
in oklab,
|
|
99
|
-
var(--
|
|
100
|
-
|
|
111
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
|
|
112
|
+
transparent
|
|
101
113
|
);
|
|
114
|
+
/* @sync soft-tint */
|
|
115
|
+
/* @sync soft-ink */
|
|
102
116
|
--alert-default-fg: var(
|
|
103
117
|
--intent-soft-fg,
|
|
104
118
|
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
105
119
|
);
|
|
106
|
-
/* @sync soft-
|
|
120
|
+
/* @sync soft-ink */
|
|
107
121
|
}
|
|
108
122
|
}
|
|
109
123
|
|
|
124
|
+
/* Soft is the alert's own default: an explicit .soft must not swap the tint
|
|
125
|
+
for the shared opaque .soft recipe, so it defers to the default. */
|
|
126
|
+
.alert.soft {
|
|
127
|
+
--ui-bg: initial;
|
|
128
|
+
--ui-fg: initial;
|
|
129
|
+
--ui-border: initial;
|
|
130
|
+
}
|
|
131
|
+
|
|
110
132
|
/* Decorative leading icon — use .alert-icon on the icon element. display and
|
|
111
133
|
vertical-align are the no-:has() layout (Degraded tier: the icon sits inline
|
|
112
134
|
before the text); the grid anatomy below replaces them. */
|
|
@@ -205,7 +227,10 @@
|
|
|
205
227
|
|
|
206
228
|
/* Admonition — structured static box with a tinted title bar and a body
|
|
207
229
|
that rests on the page surface. Not collapsible — use .accordion inside
|
|
208
|
-
the body if the content needs to be expandable.
|
|
230
|
+
the body if the content needs to be expandable. The title bar is what a
|
|
231
|
+
shared treatment paints (.solid fills it, .surface/.outline clear it): the
|
|
232
|
+
root's fill never shows behind the opaque title and body, and the title ink
|
|
233
|
+
already follows --ui-fg, so a bar left on the soft tint is unreadable. */
|
|
209
234
|
.alert.admonition {
|
|
210
235
|
padding: 0;
|
|
211
236
|
display: block;
|
|
@@ -219,6 +244,7 @@
|
|
|
219
244
|
gap: var(--space-20);
|
|
220
245
|
padding-block: var(--alert-pad-block);
|
|
221
246
|
padding-inline: var(--alert-pad-inline);
|
|
247
|
+
font-weight: var(--font-weight-semibold);
|
|
222
248
|
border-start-start-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
|
|
223
249
|
border-start-end-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
|
|
224
250
|
}
|
|
@@ -230,18 +256,17 @@
|
|
|
230
256
|
|
|
231
257
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
232
258
|
.alert.admonition > .alert-title {
|
|
233
|
-
background:
|
|
234
|
-
|
|
235
|
-
var(--surface) var(--soft-bg-mix),
|
|
236
|
-
var(--intent, var(--neutral))
|
|
259
|
+
background: var(
|
|
260
|
+
--ui-bg,
|
|
261
|
+
color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)))
|
|
237
262
|
);
|
|
238
263
|
}
|
|
239
264
|
}
|
|
240
265
|
|
|
241
266
|
@supports not (color: color-mix(in oklch, red, white)) {
|
|
242
267
|
.alert.admonition > .alert-title {
|
|
243
|
-
background: var(--intent, var(--neutral));
|
|
244
|
-
color: var(--intent-fg, var(--neutral-fg));
|
|
268
|
+
background: var(--ui-bg, var(--intent, var(--neutral)));
|
|
269
|
+
color: var(--ui-fg, var(--intent-fg, var(--neutral-fg)));
|
|
245
270
|
}
|
|
246
271
|
}
|
|
247
272
|
|
|
@@ -252,6 +277,32 @@
|
|
|
252
277
|
color: var(--text);
|
|
253
278
|
}
|
|
254
279
|
|
|
280
|
+
/* Solid: the filled title bar already carries the intent, so the frame moves
|
|
281
|
+
to the body and stays neutral. The root border goes because a transparent
|
|
282
|
+
one would show the root's intent fill around the body. */
|
|
283
|
+
.alert.admonition.solid {
|
|
284
|
+
border: 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.alert.admonition.solid > .alert-body {
|
|
288
|
+
border: var(--border-width) solid var(--border);
|
|
289
|
+
border-block-start: 0;
|
|
290
|
+
border-end-start-radius: var(--alert-radius, var(--radius-lg));
|
|
291
|
+
border-end-end-radius: var(--alert-radius, var(--radius-lg));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* Forced colors drop the title fill, so the bar would lose its edge: the
|
|
295
|
+
frame returns to the root as one uniform system-colored border. */
|
|
296
|
+
@media (forced-colors: active) {
|
|
297
|
+
.alert.admonition.solid {
|
|
298
|
+
border: var(--border-width) solid CanvasText;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.alert.admonition.solid > .alert-body {
|
|
302
|
+
border: 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
255
306
|
.alert.admonition > .alert-body > :first-child {
|
|
256
307
|
margin-block-start: 0;
|
|
257
308
|
}
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
.app-nav > a {
|
|
43
|
+
position: relative;
|
|
43
44
|
display: grid;
|
|
44
45
|
grid-template-rows: auto auto;
|
|
45
46
|
place-content: center;
|
|
@@ -80,18 +81,25 @@
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/* Current tile is already a full selected surface. Selection never changes
|
|
83
|
-
text metrics: the shared medium weight is kept
|
|
84
|
-
|
|
84
|
+
text metrics: the shared medium weight is kept. The forced-colors cue is a
|
|
85
|
+
transparent border on a ::before overlay, not on the tile: an element border
|
|
86
|
+
shrinks the content box, and the side layout starts its content at the inline
|
|
87
|
+
edge, so the icon and label shifted by --border-width when a tile became
|
|
88
|
+
current. The overlay costs no layout, yet forced colors still turn its
|
|
89
|
+
transparent border into a system-color frame once --state-selected flattens
|
|
90
|
+
to the bar's own Canvas. No forced-colors repaint: a Highlight fill put
|
|
91
|
+
HighlightText on Chromium's Canvas backplate, which hid the label. The
|
|
92
|
+
positioning context is on every link so it does not move when one becomes
|
|
93
|
+
current or an application places its own decoration. */
|
|
85
94
|
.app-nav > a[aria-current="page"] {
|
|
86
95
|
background: var(--state-selected);
|
|
87
96
|
color: var(--state-selected-fg);
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
99
|
+
.app-nav > a[aria-current="page"]::before {
|
|
100
|
+
content: "";
|
|
101
|
+
position: absolute;
|
|
102
|
+
inset: 0;
|
|
103
|
+
border: var(--border-width) solid transparent;
|
|
104
|
+
border-radius: inherit;
|
|
97
105
|
}
|
|
@@ -111,28 +111,42 @@
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/* Soft by default. Local defaults remain behind --ui-* in the resolution
|
|
114
|
-
chain, so explicit shared treatments compose without selector exclusions.
|
|
114
|
+
chain, so explicit shared treatments compose without selector exclusions.
|
|
115
|
+
The fill is a translucent intent tint, so it shows the surface it sits on
|
|
116
|
+
instead of a fixed mix with --surface: a mix stops separating on a subtle
|
|
117
|
+
or raised surface of its own lightness. No rim unless a theme sets
|
|
118
|
+
--soft-border-mix (tokens.css). */
|
|
115
119
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
116
120
|
.badge {
|
|
117
|
-
/* @sync soft-
|
|
121
|
+
/* @sync soft-tint */
|
|
118
122
|
--badge-default-bg: color-mix(
|
|
119
123
|
in oklab,
|
|
120
|
-
var(--
|
|
121
|
-
|
|
124
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
|
|
125
|
+
transparent
|
|
122
126
|
);
|
|
123
127
|
--badge-default-border: color-mix(
|
|
124
128
|
in oklab,
|
|
125
|
-
var(--
|
|
126
|
-
|
|
129
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
|
|
130
|
+
transparent
|
|
127
131
|
);
|
|
132
|
+
/* @sync soft-tint */
|
|
133
|
+
/* @sync soft-ink */
|
|
128
134
|
--badge-default-fg: var(
|
|
129
135
|
--intent-soft-fg,
|
|
130
136
|
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
131
137
|
);
|
|
132
|
-
/* @sync soft-
|
|
138
|
+
/* @sync soft-ink */
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
|
142
|
+
/* Soft is the badge's own default: an explicit .soft must not swap the tint
|
|
143
|
+
for the shared opaque .soft recipe, so it defers to the default. */
|
|
144
|
+
.badge.soft {
|
|
145
|
+
--ui-bg: initial;
|
|
146
|
+
--ui-fg: initial;
|
|
147
|
+
--ui-border: initial;
|
|
148
|
+
}
|
|
149
|
+
|
|
136
150
|
.badge:empty {
|
|
137
151
|
--badge-size: var(--badge-dot-size);
|
|
138
152
|
--badge-bg: var(--intent, var(--neutral));
|
|
@@ -171,8 +185,8 @@
|
|
|
171
185
|
:is(a.badge, button.badge):where(:not(.solid, .outline, :empty)) {
|
|
172
186
|
--badge-default-hover-bg: color-mix(
|
|
173
187
|
in oklab,
|
|
174
|
-
var(--
|
|
175
|
-
|
|
188
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix) + var(--soft-hover-alpha)),
|
|
189
|
+
transparent
|
|
176
190
|
);
|
|
177
191
|
}
|
|
178
192
|
|
|
@@ -133,12 +133,27 @@
|
|
|
133
133
|
--ui-hover-bg: transparent;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/* A soft button takes the badge and alert tint, not the shared opaque .soft:
|
|
137
|
+
it carries a short label like them, and the translucent fill keeps
|
|
138
|
+
separating on a subtle or raised surface. Ink stays the shared .soft one. */
|
|
136
139
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
137
140
|
.btn.soft {
|
|
141
|
+
/* @sync soft-tint */
|
|
142
|
+
--ui-bg: color-mix(
|
|
143
|
+
in oklab,
|
|
144
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
|
|
145
|
+
transparent
|
|
146
|
+
);
|
|
147
|
+
--ui-border: color-mix(
|
|
148
|
+
in oklab,
|
|
149
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
|
|
150
|
+
transparent
|
|
151
|
+
);
|
|
152
|
+
/* @sync soft-tint */
|
|
138
153
|
--ui-hover-bg: color-mix(
|
|
139
154
|
in oklab,
|
|
140
|
-
var(--
|
|
141
|
-
|
|
155
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix) + var(--soft-hover-alpha)),
|
|
156
|
+
transparent
|
|
142
157
|
);
|
|
143
158
|
}
|
|
144
159
|
|
|
@@ -93,10 +93,13 @@
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
@media (forced-colors: active) {
|
|
96
|
-
/* The X is a
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
/* The X is a mask fill: without this the system remaps the background to
|
|
97
|
+
Canvas and the glyph disappears. Kept on the pseudo so the host keeps its
|
|
98
|
+
native color and focus adaptation. The fill is the system color, not
|
|
99
|
+
currentColor: an unadjusted pseudo inherits the authored ink, so a white
|
|
100
|
+
.solid ink painted a white X on Canvas. */
|
|
99
101
|
.close::before {
|
|
100
102
|
forced-color-adjust: none;
|
|
103
|
+
background: ButtonText;
|
|
101
104
|
}
|
|
102
105
|
}
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
.list > :where(.list-item),
|
|
44
44
|
.list > li > .list-item {
|
|
45
45
|
display: flex;
|
|
46
|
+
/* A trailing control that no longer fits beside the content's longest word
|
|
47
|
+
moves to its own line instead of breaking that word. */
|
|
48
|
+
flex-wrap: wrap;
|
|
46
49
|
/* Centered fits the short rows lists mostly hold. Read through
|
|
47
50
|
--items-align so .items-start on the row or the list wins without
|
|
48
51
|
outranking this selector — the opt-out for long supporting text. */
|
|
@@ -89,17 +92,29 @@ a.list-item:is([aria-current="page"], [aria-current="true"])::before {
|
|
|
89
92
|
border-inline-start: 2px solid var(--state-selected);
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
/* The content region is the one that flexes, whichever position it holds.
|
|
95
|
+
/* The content region is the one that flexes, whichever position it holds.
|
|
96
|
+
Its automatic minimum is its longest word, which is what wraps the row; the
|
|
97
|
+
max size caps that minimum at the row, so a long token breaks rather than
|
|
98
|
+
widening the page. A zero minimum shrank it until words broke mid-way. */
|
|
93
99
|
.list-item-content {
|
|
94
100
|
flex: 1 1 0;
|
|
95
|
-
|
|
101
|
+
max-inline-size: 100%;
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
/* break-word, not anywhere: anywhere also lowers the min-content to a single
|
|
105
|
+
character, and the row would never wrap. */
|
|
98
106
|
.list-item-title,
|
|
99
107
|
.list-item-text {
|
|
100
108
|
display: block;
|
|
101
109
|
margin: 0;
|
|
102
|
-
overflow-wrap:
|
|
110
|
+
overflow-wrap: break-word;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Truncated text gives up its width by design: without containment its
|
|
114
|
+
unbreakable line would become the content's minimum and push the trailing
|
|
115
|
+
region onto a line of its own. */
|
|
116
|
+
.list-item-content .truncate {
|
|
117
|
+
contain: inline-size;
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
.list-item-text {
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* links are a .cluster using .nav-link items. .nav-list is the vertical nav
|
|
8
8
|
* list shared with .drawer, fed by the shared --gap token.
|
|
9
9
|
*
|
|
10
|
+
* Public hooks:
|
|
11
|
+
* --state-selected-text (fallback-only: current-link ink, see tokens.css)
|
|
12
|
+
*
|
|
10
13
|
* Internal:
|
|
11
14
|
* --gap (local value for the shared gap token)
|
|
12
15
|
*/
|
|
@@ -84,7 +87,7 @@
|
|
|
84
87
|
shared with the hover rule above. */
|
|
85
88
|
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
86
89
|
background: var(--surface-subtle);
|
|
87
|
-
color: var(--state-selected);
|
|
90
|
+
color: var(--state-selected-text, var(--state-selected));
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/* On a bar with its own --ui-bg/--ui-fg the fill is a tint of the link's own
|
|
@@ -105,7 +108,7 @@
|
|
|
105
108
|
|
|
106
109
|
/* After :hover so a hovered current link keeps the selected accent. */
|
|
107
110
|
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
108
|
-
color: var(--ui-fg, var(--state-selected));
|
|
111
|
+
color: var(--ui-fg, var(--state-selected-text, var(--state-selected)));
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
|