actual-css 0.12.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 +102 -0
- package/README.md +7 -8
- package/components.json +124 -12
- package/dist/actual.full.css +1152 -309
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/design-notes/architecture.md +67 -0
- package/docs/design-notes/steps.md +45 -28
- package/docs/navigation.json +2 -0
- package/docs/pages/components/alert.md +12 -2
- package/docs/pages/components/avatar.md +17 -11
- package/docs/pages/components/breadcrumb.md +16 -2
- package/docs/pages/components/button.md +3 -2
- package/docs/pages/components/list.md +15 -16
- package/docs/pages/components/navbar.md +42 -22
- package/docs/pages/components/pagination.md +19 -0
- package/docs/pages/components/segmented.md +83 -0
- package/docs/pages/components/steps.md +28 -22
- package/docs/pages/enhancements/flyout.md +5 -0
- package/docs/pages/enhancements/scrollspy.md +6 -6
- package/docs/pages/enhancements/tabs.md +79 -3
- 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/floating-field.md +21 -7
- package/docs/pages/forms/form-actions.md +3 -6
- package/docs/pages/forms/joined-controls.md +6 -2
- package/docs/pages/forms/overview.md +1 -0
- package/docs/pages/forms/states-and-sizes.md +15 -15
- package/docs/pages/forms/switch.md +4 -2
- package/docs/pages/foundations/philosophy.md +97 -1
- package/docs/pages/foundations/theming.md +1 -1
- package/docs/pages/foundations/tokens.md +94 -27
- package/docs/pages/guides/design-tools.md +14 -8
- package/docs/pages/guides/integrating-actual-css.md +74 -8
- package/docs/pages/guides/introduction.md +3 -2
- package/docs/pages/guides/modular-import.md +1 -1
- package/docs/pages/guides/progressive-enhancement.md +25 -7
- package/docs/pages/guides/upgrading.md +86 -9
- package/docs/pages/layout/center.md +2 -1
- package/docs/pages/layout/cluster.md +11 -4
- package/docs/pages/layout/grid.md +2 -0
- package/docs/pages/layout/scroll-snap.md +4 -2
- package/docs/pages/layout/sidebar-layout.md +3 -0
- package/docs/pages/layout/switcher.md +2 -0
- package/docs/pages/layout/topbar.md +6 -4
- package/docs/pages/patterns/icon-slot.md +17 -1
- package/docs/pages/utilities/flex.md +1 -1
- package/docs/pages/utilities/sizing-wrapping.md +51 -6
- package/package.json +6 -6
- package/reserved-classes.json +3 -0
- package/src/cli/actual-css.js +4 -4
- package/src/css/components/alert.css +99 -48
- package/src/css/components/app-nav.css +17 -9
- package/src/css/components/avatar.css +2 -18
- package/src/css/components/badge.css +23 -10
- package/src/css/components/breadcrumb.css +74 -8
- package/src/css/components/button.css +17 -2
- package/src/css/components/close.css +6 -3
- package/src/css/components/drawer.css +23 -42
- package/src/css/components/flyout.css +0 -1
- package/src/css/components/index.css +1 -0
- package/src/css/components/join.css +2 -2
- package/src/css/components/list.css +28 -13
- package/src/css/components/modal.css +17 -10
- package/src/css/components/navbar.css +15 -10
- package/src/css/components/pagination.css +5 -0
- package/src/css/components/scrollspy.css +1 -1
- package/src/css/components/segmented.css +186 -0
- package/src/css/components/steps.css +72 -15
- package/src/css/components/tab.css +135 -5
- package/src/css/core/focus.css +11 -6
- package/src/css/core/theme.css +6 -1
- package/src/css/core/tokens.css +24 -6
- package/src/css/core/variants.css +16 -5
- package/src/css/forms/choice-card.css +1 -1
- package/src/css/forms/choice.css +1 -1
- package/src/css/forms/color-swatch.css +159 -0
- package/src/css/forms/control.css +1 -1
- package/src/css/forms/floating-field.css +20 -18
- package/src/css/forms/form-actions.css +14 -8
- package/src/css/forms/index.css +1 -0
- package/src/css/forms/native.css +51 -21
- package/src/css/forms/otp.css +4 -5
- package/src/css/forms/switch.css +6 -3
- package/src/css/forms/validation.css +23 -30
- package/src/css/layout/center.css +2 -0
- package/src/css/layout/cluster.css +14 -0
- package/src/css/layout/grid.css +7 -0
- package/src/css/layout/scroll-snap.css +30 -0
- package/src/css/layout/scroller.css +19 -3
- package/src/css/layout/sidebar-layout.css +3 -0
- package/src/css/layout/switcher.css +3 -0
- package/src/css/layout/topbar.css +10 -0
- package/src/css/utilities/extra.css +3 -3
- package/src/js/focus.js +16 -1
- package/src/js/full.js +4 -0
- package/src/js/inline-visible.js +54 -0
- package/src/js/parsed.js +21 -0
- package/src/js/surface.js +10 -0
- package/src/js/tab.js +33 -7
- 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 +9 -1
- package/src/tooling/design-tokens.js +207 -76
- package/src/tooling/theme-contrast.js +182 -45
|
@@ -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,32 +95,45 @@
|
|
|
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
|
|
|
110
|
-
/*
|
|
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
|
+
|
|
132
|
+
/* Decorative leading icon — use .alert-icon on the icon element. display and
|
|
133
|
+
vertical-align are the no-:has() layout (Degraded tier: the icon sits inline
|
|
134
|
+
before the text); the grid anatomy below replaces them. */
|
|
111
135
|
.alert > .alert-icon {
|
|
112
136
|
display: inline-block;
|
|
113
|
-
flex: none;
|
|
114
137
|
inline-size: var(--alert-icon-size);
|
|
115
138
|
block-size: var(--alert-icon-size);
|
|
116
139
|
font-size: var(--alert-icon-size);
|
|
@@ -142,45 +165,44 @@
|
|
|
142
165
|
|
|
143
166
|
/* Grid anatomy — activated by a leading icon, a trailing dismiss button, or
|
|
144
167
|
both. Icon and dismiss are pinned to the outer columns; anything else (the
|
|
145
|
-
message content) flows into the remaining text column.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.alert:has(> .alert-icon) {
|
|
155
|
-
grid-template-columns: auto minmax(0, 1fr);
|
|
156
|
-
}
|
|
168
|
+
message content) flows into the remaining text column. Without :has(), the
|
|
169
|
+
grid rules drop together and the alert keeps ordinary block flow. */
|
|
170
|
+
.alert:has(> .alert-icon, > .close) {
|
|
171
|
+
display: grid;
|
|
172
|
+
/* No grid-template-columns: the icon and close branches below always set
|
|
173
|
+
it, since one of them is what matched. */
|
|
174
|
+
align-items: var(--items-align, start);
|
|
175
|
+
column-gap: var(--space-30);
|
|
176
|
+
}
|
|
157
177
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
display: inline-grid;
|
|
162
|
-
place-items: center;
|
|
163
|
-
margin-block-start: 0.125em;
|
|
164
|
-
}
|
|
178
|
+
.alert:has(> .alert-icon) {
|
|
179
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
180
|
+
}
|
|
165
181
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
182
|
+
.alert:has(> .alert-icon) > .alert-icon {
|
|
183
|
+
grid-column: 1;
|
|
184
|
+
grid-row: 1 / -1;
|
|
185
|
+
display: inline-grid;
|
|
186
|
+
place-items: center;
|
|
187
|
+
margin-block-start: 0.125em;
|
|
188
|
+
}
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
.alert:has(> .alert-icon) > :not(.alert-icon, .close) {
|
|
191
|
+
grid-column: 2;
|
|
192
|
+
}
|
|
173
193
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
194
|
+
.alert:has(> .close) {
|
|
195
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
196
|
+
}
|
|
177
197
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
grid-row: 1;
|
|
181
|
-
}
|
|
198
|
+
.alert:has(> .alert-icon):has(> .close) {
|
|
199
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
182
200
|
}
|
|
183
201
|
|
|
202
|
+
.alert:has(> .close) > .close {
|
|
203
|
+
grid-column: -2 / -1;
|
|
204
|
+
grid-row: 1;
|
|
205
|
+
}
|
|
184
206
|
/* Links */
|
|
185
207
|
.alert :where(a:not(.btn)) {
|
|
186
208
|
color: inherit;
|
|
@@ -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
|
}
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
* --avatar-size
|
|
12
12
|
* --avatar-radius
|
|
13
13
|
* --avatar-font-size
|
|
14
|
+
* --avatar-bg
|
|
14
15
|
* --avatar-stack-size
|
|
15
16
|
* --avatar-stack-font-size
|
|
16
17
|
* --avatar-stack-overlap
|
|
17
18
|
* --avatar-stack-ring
|
|
18
19
|
*
|
|
19
20
|
* Internal:
|
|
20
|
-
* --avatar-bg
|
|
21
21
|
* --avatar-fg
|
|
22
22
|
* --avatar-status-translate-x
|
|
23
23
|
*/
|
|
@@ -95,16 +95,13 @@
|
|
|
95
95
|
|
|
96
96
|
.avatar > img,
|
|
97
97
|
.avatar > picture > img {
|
|
98
|
+
display: block;
|
|
98
99
|
inline-size: 100%;
|
|
99
100
|
block-size: 100%;
|
|
100
101
|
object-fit: cover;
|
|
101
102
|
border-radius: inherit;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
.avatar > picture > img {
|
|
105
|
-
display: block;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
105
|
button.avatar {
|
|
109
106
|
appearance: none;
|
|
110
107
|
cursor: pointer;
|
|
@@ -198,19 +195,6 @@ a.avatar[aria-disabled="true"] {
|
|
|
198
195
|
--avatar-stack-font-size: var(--font-size-lg);
|
|
199
196
|
}
|
|
200
197
|
|
|
201
|
-
/* Infer the stack scale from its avatars. Progressive: without :has() the
|
|
202
|
-
explicit .avatar-stack.sm/.lg classes and the default size still size the
|
|
203
|
-
group, only the inference is lost. */
|
|
204
|
-
.avatar-stack:has(> .avatar.sm) {
|
|
205
|
-
--avatar-stack-size: 1.75rem;
|
|
206
|
-
--avatar-stack-font-size: var(--font-size-xs);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
.avatar-stack:has(> .avatar.lg) {
|
|
210
|
-
--avatar-stack-size: 3rem;
|
|
211
|
-
--avatar-stack-font-size: var(--font-size-lg);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
198
|
/* Overlapping avatars hide their neighbour: a translucent fill (--surface-subtle
|
|
215
199
|
on a glass palette) would show the next avatar's edge through it, most of all
|
|
216
200
|
when hover raises one above the other. The fill is layered over the opaque
|
|
@@ -111,33 +111,46 @@
|
|
|
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));
|
|
139
153
|
--badge-fg: var(--intent-fg, var(--neutral-fg));
|
|
140
|
-
--badge-border: transparent;
|
|
141
154
|
padding: 0;
|
|
142
155
|
border: 0;
|
|
143
156
|
inline-size: var(--badge-size);
|
|
@@ -172,8 +185,8 @@
|
|
|
172
185
|
:is(a.badge, button.badge):where(:not(.solid, .outline, :empty)) {
|
|
173
186
|
--badge-default-hover-bg: color-mix(
|
|
174
187
|
in oklab,
|
|
175
|
-
var(--
|
|
176
|
-
|
|
188
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix) + var(--soft-hover-alpha)),
|
|
189
|
+
transparent
|
|
177
190
|
);
|
|
178
191
|
}
|
|
179
192
|
|
|
@@ -4,14 +4,21 @@
|
|
|
4
4
|
* Docs: docs/pages/components/breadcrumb.md
|
|
5
5
|
*
|
|
6
6
|
* Inline list separated by "/". Mark the current page with `aria-current="page"`
|
|
7
|
-
* either on the item or on the current link.
|
|
7
|
+
* either on the item or on the current link. Ancestor labels sit in an <a> or a
|
|
8
|
+
* <span>, the box that truncates.
|
|
9
|
+
*
|
|
10
|
+
* Breadcrumbs are a single navigation hierarchy. Wrapping makes hierarchy look
|
|
11
|
+
* like separate rows and causes unstable header height, and scrolling hides the
|
|
12
|
+
* root behind a scrollbar. A trail that does not fit truncates instead:
|
|
13
|
+
* ancestors first, the current page last, each label behind an ellipsis.
|
|
8
14
|
*/
|
|
15
|
+
|
|
9
16
|
.breadcrumb {
|
|
10
17
|
display: flex;
|
|
11
|
-
flex-
|
|
12
|
-
flex-wrap: wrap;
|
|
18
|
+
flex-wrap: nowrap;
|
|
13
19
|
align-items: center;
|
|
14
20
|
gap: var(--space-10);
|
|
21
|
+
min-inline-size: 0;
|
|
15
22
|
margin: 0;
|
|
16
23
|
padding: 0;
|
|
17
24
|
color: var(--text-subtle);
|
|
@@ -19,15 +26,74 @@
|
|
|
19
26
|
list-style: none;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
/* The landmark around the trail must shrink with it in a flex or grid row, or
|
|
30
|
+
it keeps the trail's full width and the labels never get to truncate.
|
|
31
|
+
Without :has() the landmark keeps its width; min-inline-size: 0 on the
|
|
32
|
+
wrapper is the fallback. */
|
|
33
|
+
:where(nav:has(> .breadcrumb)) {
|
|
34
|
+
min-inline-size: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Grid, not flex: under a min-content constraint a grid track keeps its base
|
|
38
|
+
size, which is the label's minimum below, so the item's own minimum is
|
|
39
|
+
separator + label floor without anyone measuring a "/". A flex item would
|
|
40
|
+
report the full label as its minimum and never shrink.
|
|
41
|
+
|
|
42
|
+
Ancestors take the shrink before the current page, so the location is the
|
|
43
|
+
last thing to lose characters. The ratio is deliberately huge: at 1000 the
|
|
44
|
+
current page still took a fraction of a pixel, enough to trigger its
|
|
45
|
+
ellipsis while ancestors had room. Its own factor stays 1, because flex
|
|
46
|
+
distributes only a share of the deficit when the unfrozen factors sum below
|
|
47
|
+
one. Keyed on the last item, the current page by convention, so the order
|
|
48
|
+
holds without :has(). */
|
|
49
|
+
.breadcrumb > li {
|
|
50
|
+
display: inline-grid;
|
|
51
|
+
flex: 0 1000000 auto;
|
|
52
|
+
grid-auto-flow: column;
|
|
24
53
|
align-items: center;
|
|
25
|
-
|
|
54
|
+
white-space: nowrap;
|
|
26
55
|
}
|
|
27
56
|
|
|
28
|
-
.breadcrumb
|
|
29
|
-
|
|
57
|
+
.breadcrumb > li:last-child {
|
|
58
|
+
flex-shrink: 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* A margin rather than a grid gap, so the separator keeps its space when the
|
|
62
|
+
current page below lays it out inline. The weight stays normal there, where
|
|
63
|
+
it would inherit the current page's. */
|
|
64
|
+
.breadcrumb > li + li::before {
|
|
65
|
+
margin-inline-end: var(--space-10);
|
|
30
66
|
color: var(--text-subtle);
|
|
67
|
+
font-weight: var(--font-weight-normal);
|
|
68
|
+
content: "/";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* The ellipsis lives on the label, not on the item: an element never clips
|
|
72
|
+
its own outline, but an overflow-hidden item would clip its link's focus
|
|
73
|
+
line, which sits outside the link box.
|
|
74
|
+
|
|
75
|
+
The floor keeps a truncated label readable as a step ("Po…") instead of
|
|
76
|
+
collapsing to its separator. calc-size() lets a shorter label keep its own
|
|
77
|
+
width; without it, the plain 2em floor leaves a label under 2em a little
|
|
78
|
+
trailing room. */
|
|
79
|
+
.breadcrumb > li > :is(a, span) {
|
|
80
|
+
min-inline-size: 2em;
|
|
81
|
+
min-inline-size: calc-size(max-content, min(size, 2em));
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
text-overflow: ellipsis;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* The current page written as bare text has no label box, so its item
|
|
87
|
+
truncates itself. Only with no element inside: an aria-current item that
|
|
88
|
+
holds a link would clip that link's focus line, and keeps the label rule
|
|
89
|
+
above instead. Its own rule, never merged with the one above: a browser
|
|
90
|
+
without :has() drops it alone, and bare text then keeps its full width. */
|
|
91
|
+
.breadcrumb > li[aria-current="page"]:not(:has(> *)) {
|
|
92
|
+
display: block;
|
|
93
|
+
min-inline-size: 2em;
|
|
94
|
+
min-inline-size: calc-size(max-content, min(size, 2em));
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
text-overflow: ellipsis;
|
|
31
97
|
}
|
|
32
98
|
|
|
33
99
|
.breadcrumb a {
|
|
@@ -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
|
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* --drawer-close-reserve (header room for a corner close; zero without a close)
|
|
26
26
|
* --drawer-motion-duration (shared by panel and ::backdrop so exit duration
|
|
27
27
|
* changes in one place; entry state redefines it longer)
|
|
28
|
+
* --drawer-motion-ease (panel easing owned by the open/closed state)
|
|
28
29
|
*/
|
|
29
30
|
:where(dialog).drawer {
|
|
30
31
|
--drawer-size: 20rem;
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
/* Exit is shorter than entry so the panel clears quickly; the scrim reads
|
|
40
41
|
the same variable through inheritance and never lags the panel. */
|
|
41
42
|
--drawer-motion-duration: var(--duration);
|
|
43
|
+
--drawer-motion-ease: var(--ease-exit);
|
|
42
44
|
|
|
43
45
|
/* Keep the surface anchored to the viewport in the top layer. This also
|
|
44
46
|
provides the containing block for the absolutely positioned close button.
|
|
@@ -70,21 +72,21 @@
|
|
|
70
72
|
box-shadow: var(--shadow);
|
|
71
73
|
opacity: 0;
|
|
72
74
|
transform: translateX(var(--drawer-closed-translate));
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
/* Unsupported discrete behavior drops alone, preserving opacity/transform. */
|
|
76
|
+
transition-property: opacity, transform, overlay, display;
|
|
77
|
+
transition-duration: var(--drawer-motion-duration);
|
|
78
|
+
transition-timing-function: var(--drawer-motion-ease);
|
|
79
|
+
transition-behavior: allow-discrete;
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
:where(dialog).drawer[open] {
|
|
79
83
|
--drawer-motion-duration: var(--duration-slow);
|
|
84
|
+
--drawer-motion-ease: var(--ease-enter);
|
|
80
85
|
display: grid;
|
|
81
86
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
82
87
|
gap: var(--space-40);
|
|
83
88
|
opacity: 1;
|
|
84
89
|
transform: translateX(0);
|
|
85
|
-
transition:
|
|
86
|
-
opacity var(--drawer-motion-duration) var(--ease-enter),
|
|
87
|
-
transform var(--drawer-motion-duration) var(--ease-enter);
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
/* A form is the drawer's content wrapper when actions need method="dialog".
|
|
@@ -104,7 +106,10 @@
|
|
|
104
106
|
opacity: 0;
|
|
105
107
|
/* Scrim curve stays neutral `ease`; only the panel's character curves
|
|
106
108
|
apply. Duration follows --drawer-motion-duration through inheritance. */
|
|
107
|
-
transition: opacity
|
|
109
|
+
transition-property: opacity, overlay, display;
|
|
110
|
+
transition-duration: var(--drawer-motion-duration);
|
|
111
|
+
transition-timing-function: ease;
|
|
112
|
+
transition-behavior: allow-discrete;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
:where(dialog).drawer[data-side="end"] {
|
|
@@ -138,47 +143,23 @@
|
|
|
138
143
|
exclusion list is easier to trust than its `position: absolute`. */
|
|
139
144
|
:where(dialog).drawer > :not(:is(header, footer, form, .close)),
|
|
140
145
|
:where(dialog).drawer > form > :not(:is(header, footer, .close)) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
);
|
|
146
|
+
/* Declared here, not as a :root alias: an inherited alias arrives already
|
|
147
|
+
computed, so a --focus-ring-width override on the drawer would not reach
|
|
148
|
+
it. */
|
|
149
|
+
--drawer-focus-bleed: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
145
150
|
|
|
146
151
|
min-block-size: 0;
|
|
147
|
-
/* Keep focus paint inside the clip
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
/* Keep focus paint inside the clip, on all four sides (overflow: auto clips
|
|
153
|
+
both axes), without moving the content. Every outside indicator is
|
|
154
|
+
--focus-ring-width wide (core/focus.css). On the block axis the reserve
|
|
155
|
+
reaches into the slot gap, which is wider, so the scrollport never slides
|
|
156
|
+
under the header or footer. */
|
|
157
|
+
margin: calc(var(--drawer-focus-bleed) * -1);
|
|
158
|
+
padding: var(--drawer-focus-bleed);
|
|
153
159
|
overflow: auto;
|
|
154
160
|
overscroll-behavior: contain;
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
@supports (transition-behavior: allow-discrete) {
|
|
158
|
-
:where(dialog).drawer {
|
|
159
|
-
transition:
|
|
160
|
-
opacity var(--drawer-motion-duration) var(--ease-exit),
|
|
161
|
-
transform var(--drawer-motion-duration) var(--ease-exit),
|
|
162
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
163
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
:where(dialog).drawer[open] {
|
|
167
|
-
transition:
|
|
168
|
-
opacity var(--drawer-motion-duration) var(--ease-enter),
|
|
169
|
-
transform var(--drawer-motion-duration) var(--ease-enter),
|
|
170
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
171
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
:where(dialog).drawer::backdrop {
|
|
175
|
-
transition:
|
|
176
|
-
opacity var(--drawer-motion-duration) ease,
|
|
177
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
178
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
163
|
/* Safari before 16.4 does not support :dir(); keep the ancestor-attribute
|
|
183
164
|
path as the Degraded-tier fallback. The :dir() rules are dropped on their
|
|
184
165
|
own there, so they need no wrapper. :dir() follows inherited HTML
|