actual-css 0.13.0 → 0.14.0
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 +53 -0
- package/README.md +4 -4
- package/components.json +116 -10
- package/dist/actual.full.css +648 -77
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- 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 +5 -0
- package/docs/pages/enhancements/tabs.md +7 -2
- package/docs/pages/examples/overview.md +3 -1
- 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 +2 -2
- 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 +159 -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/focus.js +16 -1
- package/src/js/surface.js +10 -0
- 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
|
@@ -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.0",
|
|
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": {
|
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
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Segmented control — one compact choice among a few, on a shared track.
|
|
3
|
+
*
|
|
4
|
+
* Docs: docs/pages/components/segmented.md
|
|
5
|
+
*
|
|
6
|
+
* .segmented is the track, each .segment a direct child. The look is shared;
|
|
7
|
+
* the semantics stay with the markup, and the selected state is read from it:
|
|
8
|
+
* radios <label class="segment"><input type="radio"> :checked
|
|
9
|
+
* tabs <button class="segment" role="tab"> aria-selected="true"
|
|
10
|
+
* links <a class="segment"> aria-current="page"
|
|
11
|
+
* No script of its own: radios bring their arrow keys, a tablist takes
|
|
12
|
+
* data-enhance="tabs". Without :has() the radio anatomy loses its selected
|
|
13
|
+
* fill and focus ring, and the native radio stays visible to carry both.
|
|
14
|
+
*
|
|
15
|
+
* One row of equal shares (grid-auto-columns: 1fr), at least --control-size
|
|
16
|
+
* tall so it lines up with .btn and .input. Labels wrap instead of truncating;
|
|
17
|
+
* a word longer than its share breaks, so the control never overflows its
|
|
18
|
+
* container. A strip too long for one row is .tabs, which scrolls.
|
|
19
|
+
*
|
|
20
|
+
* Focus is drawn inside the segment, unlike other discrete actions
|
|
21
|
+
* (focus.css): an offset ring would cross the track's padding and cover the
|
|
22
|
+
* neighbouring segment.
|
|
23
|
+
*
|
|
24
|
+
* Public hooks:
|
|
25
|
+
* --segmented-bg
|
|
26
|
+
* --segmented-pad
|
|
27
|
+
* --segmented-radius
|
|
28
|
+
* --segmented-fg
|
|
29
|
+
* --segmented-selected-bg
|
|
30
|
+
* --segmented-selected-border
|
|
31
|
+
*/
|
|
32
|
+
.segmented {
|
|
33
|
+
--segmented-bg: var(--surface-subtle);
|
|
34
|
+
--segmented-pad: var(--space-10);
|
|
35
|
+
--segmented-radius: var(--radius);
|
|
36
|
+
--segmented-fg: var(--text-muted);
|
|
37
|
+
--segmented-selected-bg: var(--surface-raised);
|
|
38
|
+
/* The edge is what separates the selected segment where --surface-raised
|
|
39
|
+
is darker than --surface-subtle (the default dark palette). */
|
|
40
|
+
--segmented-selected-border: var(--border);
|
|
41
|
+
|
|
42
|
+
display: inline-grid;
|
|
43
|
+
grid-auto-flow: column;
|
|
44
|
+
grid-auto-columns: 1fr;
|
|
45
|
+
gap: var(--segmented-pad);
|
|
46
|
+
/* why: margin and border also reset <fieldset>, the radio anatomy's
|
|
47
|
+
element. The border is transparent, not 0: forced colors paint it, which
|
|
48
|
+
outlines the track once its fill is gone. */
|
|
49
|
+
margin: 0;
|
|
50
|
+
padding: var(--segmented-pad);
|
|
51
|
+
border: var(--border-width) solid transparent;
|
|
52
|
+
border-radius: var(--segmented-radius);
|
|
53
|
+
background: var(--segmented-bg);
|
|
54
|
+
font-size: var(--control-font-size);
|
|
55
|
+
vertical-align: middle;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.segment {
|
|
59
|
+
position: relative;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: center;
|
|
63
|
+
gap: 0.375em;
|
|
64
|
+
/* The track is --control-size tall through its segments, not through its
|
|
65
|
+
own min-block-size: a <fieldset> lays its content in an anonymous box
|
|
66
|
+
sized to the content, so a minimum on the track left the segments short.
|
|
67
|
+
A minimum, not a height: a wrapped label grows the whole row. */
|
|
68
|
+
min-block-size: calc(var(--control-size) - 2 * (var(--segmented-pad) + var(--border-width)));
|
|
69
|
+
padding: calc(var(--space-10) / 2) var(--space-30);
|
|
70
|
+
border: 0;
|
|
71
|
+
border-radius: calc(var(--segmented-radius) - var(--segmented-pad));
|
|
72
|
+
background: transparent;
|
|
73
|
+
color: var(--segmented-fg);
|
|
74
|
+
font: inherit;
|
|
75
|
+
/* Constant across states: a heavier selected label would resize its share. */
|
|
76
|
+
font-weight: var(--font-weight-medium);
|
|
77
|
+
line-height: 1.2;
|
|
78
|
+
text-align: center;
|
|
79
|
+
text-decoration: none;
|
|
80
|
+
/* why: also shrinks the min-content contribution, so 1fr keeps equal
|
|
81
|
+
shares under constraint instead of widening one around a long word. */
|
|
82
|
+
overflow-wrap: anywhere;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
transition:
|
|
85
|
+
background-color var(--duration),
|
|
86
|
+
color var(--duration),
|
|
87
|
+
box-shadow var(--duration);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.segment > input {
|
|
91
|
+
margin: 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* The radio is visually hidden but stays focusable, submitted and arrow-key
|
|
95
|
+
navigable (sr-only pattern). Guarded: without :has() the segment cannot
|
|
96
|
+
mirror :checked, so the visible native radio is the fallback. */
|
|
97
|
+
@supports selector(:has(*)) {
|
|
98
|
+
.segment > input {
|
|
99
|
+
position: absolute;
|
|
100
|
+
inline-size: 1px;
|
|
101
|
+
block-size: 1px;
|
|
102
|
+
padding: 0;
|
|
103
|
+
margin: -1px;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
clip: rect(0, 0, 0, 0);
|
|
106
|
+
white-space: nowrap;
|
|
107
|
+
border: 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.segment:hover {
|
|
112
|
+
color: var(--text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Separate rules from the :has() forms below: an engine without :has() would
|
|
116
|
+
discard a shared selector list, ARIA states included. */
|
|
117
|
+
.segment:is([aria-selected="true"], [aria-current="page"], [aria-current="true"]) {
|
|
118
|
+
background: var(--segmented-selected-bg);
|
|
119
|
+
color: var(--text);
|
|
120
|
+
box-shadow:
|
|
121
|
+
0 0 0 var(--border-width) var(--segmented-selected-border),
|
|
122
|
+
var(--shadow);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.segment:has(> input:checked) {
|
|
126
|
+
background: var(--segmented-selected-bg);
|
|
127
|
+
color: var(--text);
|
|
128
|
+
box-shadow:
|
|
129
|
+
0 0 0 var(--border-width) var(--segmented-selected-border),
|
|
130
|
+
var(--shadow);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.segment:focus-visible {
|
|
134
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
135
|
+
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.segment:has(> input:focus-visible) {
|
|
139
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
140
|
+
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.segment:is(:disabled, [aria-disabled="true"]) {
|
|
144
|
+
opacity: var(--disabled-opacity);
|
|
145
|
+
cursor: not-allowed;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.segment:has(> input:disabled) {
|
|
149
|
+
opacity: var(--disabled-opacity);
|
|
150
|
+
cursor: not-allowed;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Forced colors drop the raised fill and its edge. The selected segment
|
|
154
|
+
takes the system highlight — a background, so the outline stays the focus
|
|
155
|
+
channel alone. forced-color-adjust: none is required, not cosmetic: the UA
|
|
156
|
+
otherwise paints a Canvas backplate behind the label, which hid the text on
|
|
157
|
+
the highlight. Opting out also stops the forcing of the rest, so the shadow
|
|
158
|
+
is dropped here and the focus line takes the highlight's text color. */
|
|
159
|
+
@media (forced-colors: active) {
|
|
160
|
+
.segment:is([aria-selected="true"], [aria-current="page"], [aria-current="true"]) {
|
|
161
|
+
--focus: HighlightText;
|
|
162
|
+
|
|
163
|
+
forced-color-adjust: none;
|
|
164
|
+
background: Highlight;
|
|
165
|
+
color: HighlightText;
|
|
166
|
+
box-shadow: none;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.segment:has(> input:checked) {
|
|
170
|
+
--focus: HighlightText;
|
|
171
|
+
|
|
172
|
+
forced-color-adjust: none;
|
|
173
|
+
background: Highlight;
|
|
174
|
+
color: HighlightText;
|
|
175
|
+
box-shadow: none;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* --disabled-opacity is 1 here (tokens.css): the dimming needs a color. */
|
|
179
|
+
.segment:is(:disabled, [aria-disabled="true"]) {
|
|
180
|
+
color: GrayText;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.segment:has(> input:disabled) {
|
|
184
|
+
color: GrayText;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -137,11 +137,14 @@
|
|
|
137
137
|
|
|
138
138
|
/* Complete owns the accented connector that reaches the current step, so the
|
|
139
139
|
connector leaving current goes back to neutral. Declared after
|
|
140
|
-
.step-complete at equal specificity: current wins on every channel.
|
|
140
|
+
.step-complete at equal specificity: current wins on every channel. The
|
|
141
|
+
current marker is a regular surface, so its number and ring are selected
|
|
142
|
+
ink on a surface, not a selected fill; they are held to different ratios
|
|
143
|
+
(4.5:1 for the number, 3:1 for the ring) by report:theme-contrast. */
|
|
141
144
|
.steps > [aria-current="step"] {
|
|
142
145
|
--step-marker-bg: var(--surface-opaque);
|
|
143
|
-
--step-marker-border: var(--state-selected);
|
|
144
|
-
--step-marker-fg: var(--state-selected);
|
|
146
|
+
--step-marker-border: var(--state-selected-text, var(--state-selected));
|
|
147
|
+
--step-marker-fg: var(--state-selected-text, var(--state-selected));
|
|
145
148
|
--step-line: var(--border);
|
|
146
149
|
|
|
147
150
|
color: var(--text);
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
stays inert. */
|
|
220
220
|
.tab[aria-selected="true"],
|
|
221
221
|
.tab:is([aria-current="page"], [aria-current="true"]) {
|
|
222
|
-
color: var(--intent, var(--state-selected));
|
|
222
|
+
color: var(--intent, var(--state-selected-text, var(--state-selected)));
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
/* The selected indicator lives on a positioned pseudo so that only the active
|
package/src/css/core/focus.css
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
--surface-solid, so one token reads on the page, on a solid band and on
|
|
19
19
|
any nested surface.
|
|
20
20
|
|
|
21
|
+
A .segment is an action but takes the inset ring: its track's padding is
|
|
22
|
+
narrower than an offset ring, which would cover the neighbouring segment.
|
|
23
|
+
|
|
21
24
|
Documented exception: an invalid field focuses in --form-invalid-border
|
|
22
25
|
(danger), so the error stays visible at the point of correction. The border
|
|
23
26
|
carries the same hook; report:theme-contrast measures that line at 3:1
|