actual-css 0.14.1 → 0.15.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 +42 -0
- package/README.md +4 -4
- package/components.json +51 -6
- package/dist/actual.full.css +363 -31
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/navigation.json +4 -1
- package/docs/pages/components/accordion.md +15 -18
- package/docs/pages/components/avatar.md +1 -0
- package/docs/pages/components/badge.md +41 -5
- package/docs/pages/components/button.md +5 -0
- package/docs/pages/components/card.md +34 -0
- package/docs/pages/components/diagrams.md +96 -0
- package/docs/pages/components/steps.md +1 -1
- package/docs/pages/components/table.md +4 -0
- package/docs/pages/components/timeline.md +111 -0
- package/docs/pages/enhancements/tabs.md +48 -6
- package/docs/pages/examples/overview.md +3 -0
- package/docs/pages/forms/choice-cards.md +3 -0
- package/docs/pages/forms/combobox.md +34 -19
- package/docs/pages/forms/controls.md +2 -0
- package/docs/pages/forms/date-picker.md +34 -13
- package/docs/pages/forms/file-upload.md +120 -0
- package/docs/pages/forms/overview.md +8 -0
- package/docs/pages/foundations/philosophy.md +6 -0
- package/docs/pages/foundations/theming.md +57 -5
- package/docs/pages/foundations/typography.md +3 -0
- package/docs/pages/guides/introduction.md +1 -1
- package/docs/pages/guides/modular-import.md +124 -0
- package/docs/pages/guides/upgrading.md +63 -0
- package/package.json +2 -2
- package/reserved-classes.json +3 -0
- package/src/css/components/accordion.css +7 -3
- package/src/css/components/alert.css +9 -1
- package/src/css/components/badge.css +38 -11
- package/src/css/components/button.css +10 -2
- package/src/css/components/card.css +9 -0
- package/src/css/components/index.css +1 -0
- package/src/css/components/skeleton.css +11 -0
- package/src/css/components/tab.css +77 -0
- package/src/css/components/table.css +7 -1
- package/src/css/components/timeline.css +160 -0
- package/src/css/core/base.css +3 -1
- package/src/css/core/theme.css +6 -1
- package/src/css/core/tokens.css +3 -3
- package/src/css/forms/choice-card.css +5 -2
- package/src/css/forms/control.css +12 -3
- package/src/css/forms/custom-select.css +1 -1
- package/src/css/typography/prose.css +4 -2
- package/src/tooling/design-tokens.js +2 -0
- package/src/tooling/theme-contrast.js +1 -1
package/reserved-classes.json
CHANGED
|
@@ -232,9 +232,11 @@
|
|
|
232
232
|
"switch",
|
|
233
233
|
"switcher",
|
|
234
234
|
"tab",
|
|
235
|
+
"tab-panel",
|
|
235
236
|
"table",
|
|
236
237
|
"table-wrap",
|
|
237
238
|
"tabs",
|
|
239
|
+
"tabs-attached",
|
|
238
240
|
"text-balance",
|
|
239
241
|
"text-break",
|
|
240
242
|
"text-capitalize",
|
|
@@ -251,6 +253,7 @@
|
|
|
251
253
|
"text-wrap",
|
|
252
254
|
"textarea",
|
|
253
255
|
"tight",
|
|
256
|
+
"timeline",
|
|
254
257
|
"tooltip",
|
|
255
258
|
"topbar",
|
|
256
259
|
"touch-target",
|
|
@@ -18,12 +18,16 @@
|
|
|
18
18
|
* --accordion-pad
|
|
19
19
|
* --accordion-marker-color
|
|
20
20
|
* --accordion-marker-size
|
|
21
|
+
* --accordion-marker-icon
|
|
22
|
+
* --accordion-marker-open-rotate
|
|
21
23
|
*/
|
|
22
24
|
.accordion {
|
|
23
25
|
--accordion-radius: var(--radius);
|
|
24
26
|
--accordion-pad: var(--space-40);
|
|
25
27
|
--accordion-marker-color: var(--text-muted);
|
|
26
28
|
--accordion-marker-size: 1rem;
|
|
29
|
+
--accordion-marker-icon: var(--icon-chevron);
|
|
30
|
+
--accordion-marker-open-rotate: 180deg;
|
|
27
31
|
|
|
28
32
|
border: var(--border-width) solid var(--border);
|
|
29
33
|
border-radius: var(--accordion-radius);
|
|
@@ -68,13 +72,13 @@
|
|
|
68
72
|
inline-size: var(--accordion-marker-size);
|
|
69
73
|
block-size: var(--accordion-marker-size);
|
|
70
74
|
background-color: var(--accordion-marker-color);
|
|
71
|
-
-webkit-mask: var(--icon
|
|
72
|
-
mask: var(--icon
|
|
75
|
+
-webkit-mask: var(--accordion-marker-icon) center / contain no-repeat;
|
|
76
|
+
mask: var(--accordion-marker-icon) center / contain no-repeat;
|
|
73
77
|
transition: transform var(--duration);
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
.accordion details[open] summary::after {
|
|
77
|
-
transform: rotate(
|
|
81
|
+
transform: rotate(var(--accordion-marker-open-rotate));
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
/* The panel, not the item, owns the inset here — .separated paints a card on
|
|
@@ -172,6 +172,9 @@
|
|
|
172
172
|
/* No grid-template-columns: the icon and close branches below always set
|
|
173
173
|
it, since one of them is what matched. */
|
|
174
174
|
align-items: var(--items-align, start);
|
|
175
|
+
/* why: a stretched alert (equal-height grid row) spread its extra height
|
|
176
|
+
between the content rows, opening a gap under the title. */
|
|
177
|
+
align-content: start;
|
|
175
178
|
column-gap: var(--space-30);
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -179,9 +182,14 @@
|
|
|
179
182
|
grid-template-columns: auto minmax(0, 1fr);
|
|
180
183
|
}
|
|
181
184
|
|
|
185
|
+
/* why span 2, not 1 / -1: the rows are implicit, so -1 is line 1 and the
|
|
186
|
+
icon sat in the title row alone — any icon taller than one line inflated
|
|
187
|
+
that row and pushed the body down. Spanning the title and body rows lets
|
|
188
|
+
its height spread over both; only an icon taller than both together still
|
|
189
|
+
splits its excess between them. */
|
|
182
190
|
.alert:has(> .alert-icon) > .alert-icon {
|
|
183
191
|
grid-column: 1;
|
|
184
|
-
grid-row: 1 /
|
|
192
|
+
grid-row: 1 / span 2;
|
|
185
193
|
display: inline-grid;
|
|
186
194
|
place-items: center;
|
|
187
195
|
margin-block-start: 0.125em;
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* --badge-default-fg
|
|
21
21
|
* --badge-default-border
|
|
22
22
|
* --badge-default-hover-bg
|
|
23
|
+
* --badge-slot-inset
|
|
24
|
+
* --badge-slot-size
|
|
23
25
|
* --badge-bg
|
|
24
26
|
* --badge-fg
|
|
25
27
|
* --badge-border
|
|
@@ -44,9 +46,13 @@
|
|
|
44
46
|
--badge-font-size: var(--font-size-sm);
|
|
45
47
|
--badge-icon-size: 1em;
|
|
46
48
|
--badge-dot-size: 0.5rem;
|
|
49
|
+
/* A leading .avatar and a trailing .close are slots: a disc inside the pill
|
|
50
|
+
at the same inset from its block edges and its rounded end. */
|
|
51
|
+
--badge-slot-inset: calc(var(--badge-size) / 12);
|
|
52
|
+
--badge-slot-size: calc(var(--badge-size) - 2 * (var(--border-width) + var(--badge-slot-inset)));
|
|
47
53
|
/* Declared on the badge, not the button, so an author can still set them
|
|
48
54
|
on either. Same element as --badge-size, so .sm/.lg carry through. */
|
|
49
|
-
--close-size:
|
|
55
|
+
--close-size: var(--badge-slot-size);
|
|
50
56
|
--close-icon-size: 0.875em;
|
|
51
57
|
--badge-default-bg: var(--surface-subtle);
|
|
52
58
|
--badge-default-fg: var(--intent, var(--text));
|
|
@@ -97,9 +103,11 @@
|
|
|
97
103
|
--badge-dot-size: 0.625rem;
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
/* Decorative children follow the badge's optical size. Text
|
|
101
|
-
button retain their own boxes
|
|
102
|
-
.
|
|
106
|
+
/* Decorative children follow the badge's optical size. Text, the .close
|
|
107
|
+
button and an .avatar retain their own boxes — an aria-hidden avatar would
|
|
108
|
+
otherwise tie with .avatar on specificity and shrink to an icon whenever
|
|
109
|
+
badge.css loads last. */
|
|
110
|
+
.badge > :where(svg, img, [aria-hidden="true"]:not(.dot, .avatar)) {
|
|
103
111
|
flex: none;
|
|
104
112
|
/* The shared rule maps --badge-icon-size onto font-size. One em is therefore
|
|
105
113
|
the requested optical size; reusing the em-valued hook here would apply its
|
|
@@ -164,17 +172,36 @@
|
|
|
164
172
|
background-color: currentColor;
|
|
165
173
|
}
|
|
166
174
|
|
|
167
|
-
/*
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
/* Person chip (mention, assignee, contributor) — a leading .avatar follows the
|
|
176
|
+
pill instead of its own scale, so the badge keeps its height. */
|
|
177
|
+
.badge > .avatar {
|
|
178
|
+
--avatar-size: var(--badge-slot-size);
|
|
179
|
+
/* Initials scale from the label, not the diameter: a diameter ratio left
|
|
180
|
+
them under 6px on .sm. em resolves against the badge's font size here. */
|
|
181
|
+
--avatar-font-size: 0.7em;
|
|
182
|
+
margin-inline-start: calc(var(--badge-slot-inset) - var(--badge-pad-x));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Removable tag — a trailing .close (components/close.css), the avatar's
|
|
186
|
+
mirror: a fill or the hover wash stays a disc inside the pill. */
|
|
171
187
|
.badge > .close {
|
|
172
|
-
|
|
173
|
-
margin-inline-
|
|
174
|
-
margin-inline-end: -0.45em;
|
|
188
|
+
position: relative;
|
|
189
|
+
margin-inline-end: calc(var(--badge-slot-inset) - var(--badge-pad-x));
|
|
175
190
|
border-radius: var(--radius-full);
|
|
176
191
|
}
|
|
177
192
|
|
|
193
|
+
/* WCAG 2.5.8 floors pointer targets at 24 CSS px. The disc is smaller on the
|
|
194
|
+
default and .sm badges, so a transparent pseudo-element — part of the
|
|
195
|
+
button's hit area — extends the target to the floor around it, past the pill
|
|
196
|
+
if need be, without painting anything there. */
|
|
197
|
+
.badge > .close::after {
|
|
198
|
+
content: "";
|
|
199
|
+
position: absolute;
|
|
200
|
+
/* Square on purpose: nothing paints it, and a round one would miss the
|
|
201
|
+
corners of the 24px target. */
|
|
202
|
+
inset: min(0px, calc((var(--close-size) - 24px) / 2));
|
|
203
|
+
}
|
|
204
|
+
|
|
178
205
|
/* Hover recipes — sets --ui-hover-bg consumed by the hover rule below.
|
|
179
206
|
Declared outside :hover like button.css for the same reason. */
|
|
180
207
|
:is(a.badge, button.badge).outline {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* --btn-radius
|
|
20
20
|
* --btn-gap
|
|
21
21
|
* --btn-hover-overlay
|
|
22
|
+
* --btn-shadow
|
|
22
23
|
* --btn-default-bg
|
|
23
24
|
* --btn-default-fg
|
|
24
25
|
* --btn-default-border
|
|
@@ -85,6 +86,11 @@
|
|
|
85
86
|
border-radius: var(--btn-radius);
|
|
86
87
|
background: var(--btn-bg);
|
|
87
88
|
color: var(--btn-fg);
|
|
89
|
+
/* why: an unset hook rather than none, so a theme or container can give
|
|
90
|
+
every button an elevation the hover overlay composes with instead of
|
|
91
|
+
replacing (see the hover rule). The hook accepts shadow-list items:
|
|
92
|
+
use 0 0 #0000 to remove elevation, since none invalidates that list. */
|
|
93
|
+
box-shadow: var(--btn-shadow, 0 0 #0000);
|
|
88
94
|
font: inherit;
|
|
89
95
|
font-size: var(--btn-font-size);
|
|
90
96
|
font-weight: var(--font-weight-medium);
|
|
@@ -169,8 +175,10 @@
|
|
|
169
175
|
background: var(--ui-hover-bg, var(--btn-bg));
|
|
170
176
|
/* Huge inset spread paints the overlay over any border-radius without a
|
|
171
177
|
pseudo-element; a plain background would replace the fill instead of
|
|
172
|
-
darkening it. */
|
|
173
|
-
box-shadow:
|
|
178
|
+
darkening it. Listed after --btn-shadow, which it must not drop. */
|
|
179
|
+
box-shadow:
|
|
180
|
+
var(--btn-shadow, 0 0 #0000),
|
|
181
|
+
inset 0 0 0 999px var(--btn-hover-overlay);
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
.btn:not(:where(:disabled, [aria-disabled="true"])):active {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* --card-fg
|
|
29
29
|
* --heading
|
|
30
30
|
* --busy-overlay-bg
|
|
31
|
+
* --surface-link
|
|
31
32
|
*/
|
|
32
33
|
/* Intent boundary — inherited intents do not tint this component.
|
|
33
34
|
Local intent classes still win because :where() has zero specificity.
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
when they paint this surface, so an ancestor intent would otherwise leak
|
|
37
38
|
through a variant that has no local intent class. */
|
|
38
39
|
:where(.card) {
|
|
40
|
+
--surface-link: initial;
|
|
39
41
|
--intent: initial;
|
|
40
42
|
--intent-fg: initial;
|
|
41
43
|
--intent-soft-fg: initial;
|
|
@@ -71,6 +73,13 @@
|
|
|
71
73
|
border-radius: var(--card-radius);
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
/* Theme link ink is tuned for the page, not an intent fill. currentColor
|
|
77
|
+
follows the surrounding copy without freezing a parent's resolved ink;
|
|
78
|
+
the card and theme boundaries reset this internal relay above/in theme.css. */
|
|
79
|
+
.card.solid {
|
|
80
|
+
--surface-link: currentColor;
|
|
81
|
+
}
|
|
82
|
+
|
|
74
83
|
/* The box lives at zero specificity so a layout primitive applied to the same
|
|
75
84
|
element owns it. components/ is imported after layout/, so a plain .card
|
|
76
85
|
declaration would beat .media (and any other non-column primitive) on source
|
|
@@ -38,6 +38,17 @@
|
|
|
38
38
|
0 0 / 200% 100%;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/* Translucent ink tints show the surface they sit on. Fixed surfaces did
|
|
42
|
+
not: the --surface-raised highlight matched a card or a light page, so the
|
|
43
|
+
shape lost its edges mid-sweep, and a soft card or dark page made the track
|
|
44
|
+
inverted or invisible. The highlight keeps a tint so the edge never goes. */
|
|
45
|
+
@supports (color: color-mix(in oklab, red, blue)) {
|
|
46
|
+
.skeleton {
|
|
47
|
+
--skeleton-track: color-mix(in oklab, currentColor 10%, transparent);
|
|
48
|
+
--skeleton-highlight: color-mix(in oklab, currentColor 4%, transparent);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
@media (prefers-reduced-motion: no-preference) {
|
|
42
53
|
.skeleton {
|
|
43
54
|
animation: actual-skeleton var(--duration-shimmer) ease-in-out infinite;
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* Public hooks:
|
|
21
21
|
* --tab-flex
|
|
22
22
|
* --tab-gap
|
|
23
|
+
* --tab-panel-bg
|
|
24
|
+
* --tab-panel-pad
|
|
25
|
+
* --tab-panel-shadow
|
|
23
26
|
*
|
|
24
27
|
* Internal:
|
|
25
28
|
* --tab-edge
|
|
@@ -249,3 +252,77 @@
|
|
|
249
252
|
.tabs > li > .tab {
|
|
250
253
|
inline-size: 100%;
|
|
251
254
|
}
|
|
255
|
+
|
|
256
|
+
/* Attached horizontal tabs share a surface with their panels. The strip
|
|
257
|
+
draws the panel's top edge inside its scrollport: no overlapping layers
|
|
258
|
+
or outside ink that overflow clipping could cut off. Direct children
|
|
259
|
+
keep an ordinary or nested tablist inside a panel independent. */
|
|
260
|
+
.tabs-attached {
|
|
261
|
+
--tab-panel-bg: var(--surface-raised);
|
|
262
|
+
--tab-panel-pad: var(--space-40);
|
|
263
|
+
--tab-panel-shadow: none;
|
|
264
|
+
|
|
265
|
+
display: grid;
|
|
266
|
+
gap: 0;
|
|
267
|
+
min-inline-size: 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.tabs-attached > .tabs {
|
|
271
|
+
gap: 0;
|
|
272
|
+
border: 0;
|
|
273
|
+
background:
|
|
274
|
+
linear-gradient(var(--border), var(--border)) bottom / 100% var(--border-width) no-repeat;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.tabs-attached > .tabs > .tab {
|
|
278
|
+
border: var(--border-width) solid var(--border);
|
|
279
|
+
background: var(--surface-subtle);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.tabs-attached > .tabs > .tab + .tab {
|
|
283
|
+
margin-inline-start: calc(-1 * var(--border-width));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.tabs-attached > .tabs > .tab[aria-selected="true"] {
|
|
287
|
+
background: var(--tab-panel-bg);
|
|
288
|
+
color: var(--text);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.tabs-attached > .tabs > .tab[aria-selected="true"]::after {
|
|
292
|
+
/* Keep all four border colors equal: differently colored bottom borders
|
|
293
|
+
meet the sides diagonally. Erase only the inner bottom edge so the
|
|
294
|
+
vertical strokes reach the panel with square ends. */
|
|
295
|
+
inset-block-end: calc(-1 * var(--border-width));
|
|
296
|
+
block-size: var(--border-width);
|
|
297
|
+
border: 0;
|
|
298
|
+
background: var(--tab-panel-bg);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.tabs-attached > .tab-panel {
|
|
302
|
+
min-inline-size: 0;
|
|
303
|
+
margin: 0;
|
|
304
|
+
padding: var(--tab-panel-pad);
|
|
305
|
+
border: var(--border-width) solid var(--border);
|
|
306
|
+
border-block-start: 0;
|
|
307
|
+
background: var(--tab-panel-bg);
|
|
308
|
+
box-shadow: var(--tab-panel-shadow);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* Forced colors remove gradients and shadows. A real strip edge and the
|
|
312
|
+
user's selection colors preserve both the frame and the selected state. */
|
|
313
|
+
@media (forced-colors: active) {
|
|
314
|
+
.tabs-attached > .tabs {
|
|
315
|
+
border-block-end: var(--border-width) solid CanvasText;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.tabs-attached > .tabs > .tab[aria-selected="true"] {
|
|
319
|
+
forced-color-adjust: none;
|
|
320
|
+
border-color: CanvasText;
|
|
321
|
+
background: Highlight;
|
|
322
|
+
color: HighlightText;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.tabs-attached > .tabs > .tab[aria-selected="true"]::after {
|
|
326
|
+
content: none;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* Public hooks:
|
|
16
16
|
* --table-min
|
|
17
17
|
* --table-cell-pad
|
|
18
|
+
*
|
|
19
|
+
* Theme hooks (unset by default, read with a fallback, so a theme sets them
|
|
20
|
+
* once on its root and third-party grid bridges can read the same choice):
|
|
21
|
+
* --table-header-bg
|
|
22
|
+
* --table-header-fg
|
|
18
23
|
*/
|
|
19
24
|
.table-wrap {
|
|
20
25
|
/* Containing block for anything absolutely positioned inside the table —
|
|
@@ -65,11 +70,12 @@
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
.table thead th {
|
|
73
|
+
color: var(--table-header-fg, var(--text-muted));
|
|
68
74
|
vertical-align: bottom;
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
.table thead {
|
|
72
|
-
background: var(--surface-subtle);
|
|
78
|
+
background: var(--table-header-bg, var(--surface-subtle));
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
.table tbody tr:hover {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Timeline — a vertical history of dated events: activity feed, audit log,
|
|
3
|
+
* order tracking.
|
|
4
|
+
*
|
|
5
|
+
* Docs: docs/pages/components/timeline.md
|
|
6
|
+
*
|
|
7
|
+
* <ol class="timeline">
|
|
8
|
+
* <li class="success">
|
|
9
|
+
* <time datetime="2026-10-18T09:30">Today, 09:30</time>
|
|
10
|
+
* Appointment confirmed
|
|
11
|
+
* </li>
|
|
12
|
+
* </ol>
|
|
13
|
+
*
|
|
14
|
+
* Each event draws a dot on a rail; an intent class on the <li> colors it. An
|
|
15
|
+
* .avatar or an aria-hidden icon written first in the <li> takes the dot's
|
|
16
|
+
* place. A direct <time> child is the event's dateline. The content stays in
|
|
17
|
+
* block flow, so a sentence mixing text and links is never split into items.
|
|
18
|
+
*
|
|
19
|
+
* Progress through a process is .steps, not a timeline.
|
|
20
|
+
*
|
|
21
|
+
* Public hooks:
|
|
22
|
+
* --timeline-dot-size
|
|
23
|
+
* --timeline-slot-size
|
|
24
|
+
* --timeline-gap
|
|
25
|
+
* --timeline-inline-gap
|
|
26
|
+
* --timeline-line-size
|
|
27
|
+
*
|
|
28
|
+
* Internal:
|
|
29
|
+
* --timeline-rail-size
|
|
30
|
+
* --timeline-line-box
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/* Registered so the event's line box computes to a length on the <li> and
|
|
34
|
+
inherits as one: an avatar slot sets its own font size and line-height, and
|
|
35
|
+
an unregistered em would resolve against those. Unregistered, the dot still
|
|
36
|
+
lines up and the slot drifts by the font difference.
|
|
37
|
+
|
|
38
|
+
The initial value must be computationally independent: a rem or em there
|
|
39
|
+
invalidates the whole @property rule, silently. Every <li> sets its own. */
|
|
40
|
+
@property --timeline-line-box {
|
|
41
|
+
syntax: "<length>";
|
|
42
|
+
inherits: true;
|
|
43
|
+
initial-value: 0px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.timeline {
|
|
47
|
+
--timeline-dot-size: 0.625rem;
|
|
48
|
+
--timeline-slot-size: 1.75rem;
|
|
49
|
+
--timeline-gap: var(--density-space);
|
|
50
|
+
--timeline-inline-gap: var(--space-30);
|
|
51
|
+
--timeline-line-size: var(--border-width);
|
|
52
|
+
--timeline-rail-size: var(--timeline-dot-size);
|
|
53
|
+
|
|
54
|
+
display: grid;
|
|
55
|
+
/* A long URL or token in an event wraps instead of widening the list. */
|
|
56
|
+
grid-template-columns: minmax(0, 1fr);
|
|
57
|
+
/* A list stretched by its parent (a grid cell, an equal-height card) keeps
|
|
58
|
+
its rhythm instead of spreading the events over the extra height. */
|
|
59
|
+
align-content: start;
|
|
60
|
+
gap: var(--timeline-gap);
|
|
61
|
+
margin: 0;
|
|
62
|
+
padding: 0;
|
|
63
|
+
list-style: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* One slotted event widens the rail of the whole list, so every line and every
|
|
67
|
+
text column stays aligned when dots and avatars mix. Without :has() the rail
|
|
68
|
+
keeps the dot's width and a slot shrinks to it: the degraded list stays
|
|
69
|
+
aligned and readable, with a dot-sized avatar. */
|
|
70
|
+
.timeline:has(> li > :is(.avatar, [aria-hidden="true"]):first-child) {
|
|
71
|
+
--timeline-rail-size: var(--timeline-slot-size);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* The line box is a length, not the unitless ratio: a smaller <time> opening
|
|
75
|
+
the event keeps the same first line as text would, so the marker centred on
|
|
76
|
+
it lines up whichever element comes first. */
|
|
77
|
+
.timeline > li {
|
|
78
|
+
--timeline-line-box: calc(var(--line-height) * 1em);
|
|
79
|
+
|
|
80
|
+
position: relative;
|
|
81
|
+
padding-inline-start: calc(var(--timeline-rail-size) + var(--timeline-inline-gap));
|
|
82
|
+
line-height: var(--timeline-line-box);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* The event owns no rhythm beyond its dateline: block children drop their own
|
|
86
|
+
margins, and an author who needs more spacing composes a .stack inside. */
|
|
87
|
+
.timeline > li > * {
|
|
88
|
+
margin-block: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.timeline > li > time {
|
|
92
|
+
display: block;
|
|
93
|
+
color: var(--text-muted);
|
|
94
|
+
font-size: var(--font-size-sm);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.timeline > li::before {
|
|
98
|
+
content: "";
|
|
99
|
+
position: absolute;
|
|
100
|
+
inset-block-start: calc((var(--timeline-line-box) - var(--timeline-dot-size)) / 2);
|
|
101
|
+
inset-inline-start: calc((var(--timeline-rail-size) - var(--timeline-dot-size)) / 2);
|
|
102
|
+
inline-size: var(--timeline-dot-size);
|
|
103
|
+
block-size: var(--timeline-dot-size);
|
|
104
|
+
border-radius: var(--radius-full);
|
|
105
|
+
background: var(--intent, var(--neutral));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Stated as an undo, not as `:not(:has(...))` on the dot rule: an engine
|
|
109
|
+
without :has() would drop that whole rule and lose every dot, where this way
|
|
110
|
+
it only keeps a dot under the slot. */
|
|
111
|
+
.timeline > li:has(> :is(.avatar, [aria-hidden="true"]):first-child)::before {
|
|
112
|
+
content: none;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* The segment clears each marker by half the rail, so it never shows through
|
|
116
|
+
a translucent avatar fill, and a dot in a list widened by a slot keeps the
|
|
117
|
+
slot's clearance. A border rather than a background: forced colors keep a
|
|
118
|
+
border and wipe a background. */
|
|
119
|
+
.timeline > li:not(:last-child)::after {
|
|
120
|
+
content: "";
|
|
121
|
+
position: absolute;
|
|
122
|
+
inset-block-start: calc((var(--timeline-line-box) + var(--timeline-rail-size)) / 2);
|
|
123
|
+
inset-inline-start: calc((var(--timeline-rail-size) - var(--timeline-line-size)) / 2);
|
|
124
|
+
block-size: calc(100% + var(--timeline-gap) - var(--timeline-rail-size));
|
|
125
|
+
border-inline-start: var(--timeline-line-size) solid var(--border);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.timeline > li > :is(.avatar, [aria-hidden="true"]):first-child {
|
|
129
|
+
--avatar-size: var(--timeline-rail-size);
|
|
130
|
+
|
|
131
|
+
position: absolute;
|
|
132
|
+
inset-block-start: calc((var(--timeline-line-box) - var(--timeline-rail-size)) / 2);
|
|
133
|
+
inset-inline-start: 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* An icon slot gets a disc of the avatar's size, so both slot kinds read as one
|
|
137
|
+
rail. The event's intent colors the glyph. */
|
|
138
|
+
.timeline > li > [aria-hidden="true"]:first-child:not(.avatar) {
|
|
139
|
+
display: grid;
|
|
140
|
+
place-items: center;
|
|
141
|
+
inline-size: var(--timeline-rail-size);
|
|
142
|
+
block-size: var(--timeline-rail-size);
|
|
143
|
+
border: var(--border-width) solid var(--border);
|
|
144
|
+
border-radius: var(--radius-full);
|
|
145
|
+
background: var(--surface-opaque);
|
|
146
|
+
color: var(--intent, var(--text-muted));
|
|
147
|
+
font-size: var(--font-size-sm);
|
|
148
|
+
line-height: 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.timeline > li > [aria-hidden="true"]:first-child:not(.avatar) > svg {
|
|
152
|
+
inline-size: 1em;
|
|
153
|
+
block-size: 1em;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@media (forced-colors: active) {
|
|
157
|
+
.timeline > li::before {
|
|
158
|
+
background: CanvasText;
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/css/core/base.css
CHANGED
|
@@ -22,7 +22,7 @@ body {
|
|
|
22
22
|
opts in by setting --link. Because this is an element selector, any component
|
|
23
23
|
rule that colors its own anchors still wins. */
|
|
24
24
|
a {
|
|
25
|
-
color: var(--link, inherit);
|
|
25
|
+
color: var(--surface-link, var(--link, inherit));
|
|
26
26
|
text-underline-offset: 0.18em;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -91,4 +91,6 @@ small {
|
|
|
91
91
|
::selection {
|
|
92
92
|
background: var(--selection-bg, var(--primary));
|
|
93
93
|
color: var(--selection-fg, var(--primary-fg));
|
|
94
|
+
/* SVG text paints with fill, including labels with an explicit fill. */
|
|
95
|
+
fill: currentColor;
|
|
94
96
|
}
|
package/src/css/core/theme.css
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Exception: --surface-opaque and --surface-solid-fg are public; a theme with
|
|
11
11
|
* translucent surfaces overrides them with opaque values. --border-control is
|
|
12
12
|
* public too: the resting edge of a neutral control, apart from structural
|
|
13
|
-
* separators.
|
|
13
|
+
* separators. --control-default-bg is the inherited resting field fill.
|
|
14
14
|
* Internal: --hgroup-gap.
|
|
15
15
|
*/
|
|
16
16
|
:root {
|
|
@@ -103,6 +103,8 @@
|
|
|
103
103
|
their own tokens instead of inheriting :root's resolved values. */
|
|
104
104
|
:root,
|
|
105
105
|
[data-theme] {
|
|
106
|
+
/* A painted parent's link context must not cross into a new palette. */
|
|
107
|
+
--surface-link: initial;
|
|
106
108
|
--heading: var(--text);
|
|
107
109
|
--selection-bg: var(--primary);
|
|
108
110
|
--selection-fg: var(--primary-fg);
|
|
@@ -115,6 +117,9 @@
|
|
|
115
117
|
/* The edge that tells a control apart from the surface, where --border only
|
|
116
118
|
separates surfaces: fields, choices, switch, OTP cells, choice cards. */
|
|
117
119
|
--border-control: var(--border);
|
|
120
|
+
/* Inherited resting field fill, shared with external control bridges.
|
|
121
|
+
Component state rules still own readonly, disabled and invalid fills. */
|
|
122
|
+
--control-default-bg: var(--surface);
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
/* Soft foreground hooks — the soft ink is the intent rebated toward the local
|
package/src/css/core/tokens.css
CHANGED
|
@@ -125,14 +125,14 @@
|
|
|
125
125
|
Like --control-size, it is a scale plus a selection: themes set the
|
|
126
126
|
-sm/-md/-lg steps, and :root, .sm/.lg and .compact/.spacious (variants.css)
|
|
127
127
|
pick one. A theme that wrote --control-pad-x itself would be overwritten
|
|
128
|
-
by every size and density boundary —
|
|
129
|
-
|
|
128
|
+
by every size and density boundary — a theme's 12px became a wider 13px
|
|
129
|
+
inside .compact. The selection is rebound at each boundary because a
|
|
130
130
|
var() inside a custom property resolves where it is declared.
|
|
131
131
|
|
|
132
132
|
The boundaries also write --control-step-sm/-md/-lg: which step is
|
|
133
133
|
selected, as 0/1 weights that mean the same under every theme. A theme
|
|
134
134
|
that sets its own steps rebuilds the selection from them in its own block
|
|
135
|
-
(
|
|
135
|
+
(docs: Tokens · Controls), so a nested island of it reads its steps without
|
|
136
136
|
resetting the density it sits in. Never rebind on every [data-theme]: a
|
|
137
137
|
palette-only island then became a geometry boundary that an inherited
|
|
138
138
|
--control-size or --control-pad-x no longer crossed. */
|
|
@@ -225,14 +225,17 @@
|
|
|
225
225
|
clip-path: circle(0 at var(--choice-chip-dot));
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
/* The check keeps the card's scale-in, in flow ahead of the label.
|
|
228
|
+
/* The check keeps the card's scale-in, in flow ahead of the label. It is ink
|
|
229
|
+
on the chip, not a fill: a theme whose selected fill cannot be text (a
|
|
230
|
+
yellow or lime accent) lends its selected ink here, as for tabs and steps.
|
|
231
|
+
It outranks explicit intents too: their pastel fills are still not ink. */
|
|
229
232
|
.choice-card.chip::after {
|
|
230
233
|
order: -1;
|
|
231
234
|
flex: none;
|
|
232
235
|
align-self: center;
|
|
233
236
|
margin-block: 0;
|
|
234
237
|
margin-inline: 0 0.375em;
|
|
235
|
-
background: var(--intent, var(--state-selected));
|
|
238
|
+
background: var(--state-selected-text, var(--intent, var(--state-selected)));
|
|
236
239
|
-webkit-mask-size: contain;
|
|
237
240
|
mask-size: contain;
|
|
238
241
|
}
|
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
* Public hooks:
|
|
21
21
|
* --control-border
|
|
22
22
|
* --control-bg
|
|
23
|
+
* --control-default-bg
|
|
23
24
|
* --control-fg
|
|
24
25
|
* --control-cursor
|
|
26
|
+
* --control-shadow
|
|
25
27
|
* --control-border-block-end-color
|
|
26
28
|
* --control-border-block-end-width
|
|
27
29
|
* --control-hover-border (input and textarea)
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
.textarea,
|
|
32
34
|
.select {
|
|
33
35
|
--control-border: var(--border-control);
|
|
34
|
-
--control-bg: var(--surface);
|
|
36
|
+
--control-bg: var(--control-default-bg, var(--surface));
|
|
35
37
|
--control-fg: var(--text);
|
|
36
38
|
--control-cursor: auto;
|
|
37
39
|
|
|
@@ -49,6 +51,11 @@
|
|
|
49
51
|
border-radius: var(--radius);
|
|
50
52
|
background: var(--control-bg);
|
|
51
53
|
color: var(--control-fg);
|
|
54
|
+
/* why: an unset hook, so a theme can give every field a resting elevation
|
|
55
|
+
that focus keeps (see the focus rule). The fallback is none, the same as
|
|
56
|
+
the focus rule's: a transparent 0 0 shadow here made focus transition
|
|
57
|
+
between two values and expose a phantom shadow mid-way. */
|
|
58
|
+
box-shadow: var(--control-shadow, none);
|
|
52
59
|
font-size: var(--control-font-size);
|
|
53
60
|
cursor: var(--control-cursor);
|
|
54
61
|
/* No outline: none here — the core :focus fallback must survive for browsers
|
|
@@ -81,13 +88,15 @@
|
|
|
81
88
|
the control's border box (any smaller offset bleeds outward), so it covers
|
|
82
89
|
the border, never touches a .join neighbour or a scrollport edge, and needs
|
|
83
90
|
no group treatment. Width follows --focus-ring-width, including its
|
|
84
|
-
prefers-contrast step. Border = shape and validation, outline = focus.
|
|
91
|
+
prefers-contrast step. Border = shape and validation, outline = focus.
|
|
92
|
+
The box-shadow reset drops any halo but keeps the field's own elevation:
|
|
93
|
+
a plain none erased --control-shadow on keyboard focus. */
|
|
85
94
|
.input:focus-visible:not(:disabled),
|
|
86
95
|
.textarea:focus-visible:not(:disabled),
|
|
87
96
|
.select:focus-visible:not(:disabled) {
|
|
88
97
|
outline: var(--focus-ring-width) solid var(--form-invalid-border, var(--focus));
|
|
89
98
|
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
90
|
-
box-shadow: none;
|
|
99
|
+
box-shadow: var(--control-shadow, none);
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
.input[readonly],
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
--control-bg: var(--surface-raised);
|
|
39
39
|
outline: var(--focus-ring-width) solid var(--form-invalid-border, var(--focus));
|
|
40
40
|
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
41
|
-
box-shadow: none;
|
|
41
|
+
box-shadow: var(--control-shadow, none);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
&::picker-icon {
|