actual-css 0.14.0 → 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 +56 -0
- package/README.md +8 -8
- package/components.json +51 -6
- package/dist/actual.full.css +367 -31
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/design-notes/widget-primitives.md +4 -0
- package/docs/navigation.json +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/flyout.md +4 -3
- package/docs/pages/enhancements/overview.md +2 -1
- package/docs/pages/enhancements/tabs.md +48 -6
- package/docs/pages/enhancements/tooltip.md +3 -1
- package/docs/pages/examples/overview.md +5 -2
- 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 +4 -3
- 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/color-swatch.css +4 -0
- 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/js/enhance.js +14 -0
- package/src/js/floating-space.js +26 -0
- package/src/js/flyout.js +3 -1
- package/src/js/parse-config.js +5 -2
- package/src/js/tooltip.js +2 -2
- package/src/tooling/design-tokens.js +2 -0
- package/src/tooling/theme-contrast.js +1 -1
package/dist/actual.full.css
CHANGED
|
@@ -178,14 +178,14 @@ select {
|
|
|
178
178
|
Like --control-size, it is a scale plus a selection: themes set the
|
|
179
179
|
-sm/-md/-lg steps, and :root, .sm/.lg and .compact/.spacious (variants.css)
|
|
180
180
|
pick one. A theme that wrote --control-pad-x itself would be overwritten
|
|
181
|
-
by every size and density boundary —
|
|
182
|
-
|
|
181
|
+
by every size and density boundary — a theme's 12px became a wider 13px
|
|
182
|
+
inside .compact. The selection is rebound at each boundary because a
|
|
183
183
|
var() inside a custom property resolves where it is declared.
|
|
184
184
|
|
|
185
185
|
The boundaries also write --control-step-sm/-md/-lg: which step is
|
|
186
186
|
selected, as 0/1 weights that mean the same under every theme. A theme
|
|
187
187
|
that sets its own steps rebuilds the selection from them in its own block
|
|
188
|
-
(
|
|
188
|
+
(docs: Tokens · Controls), so a nested island of it reads its steps without
|
|
189
189
|
resetting the density it sits in. Never rebind on every [data-theme]: a
|
|
190
190
|
palette-only island then became a geometry boundary that an inherited
|
|
191
191
|
--control-size or --control-pad-x no longer crossed. */
|
|
@@ -335,7 +335,7 @@ select {
|
|
|
335
335
|
* Exception: --surface-opaque and --surface-solid-fg are public; a theme with
|
|
336
336
|
* translucent surfaces overrides them with opaque values. --border-control is
|
|
337
337
|
* public too: the resting edge of a neutral control, apart from structural
|
|
338
|
-
* separators.
|
|
338
|
+
* separators. --control-default-bg is the inherited resting field fill.
|
|
339
339
|
* Internal: --hgroup-gap.
|
|
340
340
|
*/
|
|
341
341
|
:root {
|
|
@@ -428,6 +428,8 @@ select {
|
|
|
428
428
|
their own tokens instead of inheriting :root's resolved values. */
|
|
429
429
|
:root,
|
|
430
430
|
[data-theme] {
|
|
431
|
+
/* A painted parent's link context must not cross into a new palette. */
|
|
432
|
+
--surface-link: initial;
|
|
431
433
|
--heading: var(--text);
|
|
432
434
|
--selection-bg: var(--primary);
|
|
433
435
|
--selection-fg: var(--primary-fg);
|
|
@@ -440,6 +442,9 @@ select {
|
|
|
440
442
|
/* The edge that tells a control apart from the surface, where --border only
|
|
441
443
|
separates surfaces: fields, choices, switch, OTP cells, choice cards. */
|
|
442
444
|
--border-control: var(--border);
|
|
445
|
+
/* Inherited resting field fill, shared with external control bridges.
|
|
446
|
+
Component state rules still own readonly, disabled and invalid fills. */
|
|
447
|
+
--control-default-bg: var(--surface);
|
|
443
448
|
}
|
|
444
449
|
|
|
445
450
|
/* Soft foreground hooks — the soft ink is the intent rebated toward the local
|
|
@@ -576,7 +581,7 @@ body {
|
|
|
576
581
|
opts in by setting --link. Because this is an element selector, any component
|
|
577
582
|
rule that colors its own anchors still wins. */
|
|
578
583
|
a {
|
|
579
|
-
color: var(--link, inherit);
|
|
584
|
+
color: var(--surface-link, var(--link, inherit));
|
|
580
585
|
text-underline-offset: 0.18em;
|
|
581
586
|
}
|
|
582
587
|
|
|
@@ -645,6 +650,8 @@ small {
|
|
|
645
650
|
::selection {
|
|
646
651
|
background: var(--selection-bg, var(--primary));
|
|
647
652
|
color: var(--selection-fg, var(--primary-fg));
|
|
653
|
+
/* SVG text paints with fill, including labels with an explicit fill. */
|
|
654
|
+
fill: currentColor;
|
|
648
655
|
}
|
|
649
656
|
|
|
650
657
|
/*
|
|
@@ -1040,7 +1047,7 @@ small {
|
|
|
1040
1047
|
/* --link is a theme-level token (core/base.css also reads it for bare links);
|
|
1041
1048
|
prose falls back to --primary so a theme that never sets it is unaffected. */
|
|
1042
1049
|
.prose :where(a) {
|
|
1043
|
-
color: var(--link, var(--primary));
|
|
1050
|
+
color: var(--surface-link, var(--link, var(--primary)));
|
|
1044
1051
|
text-decoration-line: underline;
|
|
1045
1052
|
text-decoration-thickness: 0.08em;
|
|
1046
1053
|
text-underline-offset: 0.18em;
|
|
@@ -1052,7 +1059,9 @@ small {
|
|
|
1052
1059
|
|
|
1053
1060
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
1054
1061
|
.prose :where(a:hover, a:focus-visible) {
|
|
1055
|
-
|
|
1062
|
+
/* A solid surface owns its contrast pair in every state; mixing in the
|
|
1063
|
+
page's --text would pull its links toward the wrong background. */
|
|
1064
|
+
color: var(--surface-link, color-mix(in oklab, var(--link, var(--primary)), var(--text) 15%));
|
|
1056
1065
|
}
|
|
1057
1066
|
}
|
|
1058
1067
|
|
|
@@ -2221,8 +2230,10 @@ small {
|
|
|
2221
2230
|
* Public hooks:
|
|
2222
2231
|
* --control-border
|
|
2223
2232
|
* --control-bg
|
|
2233
|
+
* --control-default-bg
|
|
2224
2234
|
* --control-fg
|
|
2225
2235
|
* --control-cursor
|
|
2236
|
+
* --control-shadow
|
|
2226
2237
|
* --control-border-block-end-color
|
|
2227
2238
|
* --control-border-block-end-width
|
|
2228
2239
|
* --control-hover-border (input and textarea)
|
|
@@ -2232,7 +2243,7 @@ small {
|
|
|
2232
2243
|
.textarea,
|
|
2233
2244
|
.select {
|
|
2234
2245
|
--control-border: var(--border-control);
|
|
2235
|
-
--control-bg: var(--surface);
|
|
2246
|
+
--control-bg: var(--control-default-bg, var(--surface));
|
|
2236
2247
|
--control-fg: var(--text);
|
|
2237
2248
|
--control-cursor: auto;
|
|
2238
2249
|
|
|
@@ -2250,6 +2261,11 @@ small {
|
|
|
2250
2261
|
border-radius: var(--radius);
|
|
2251
2262
|
background: var(--control-bg);
|
|
2252
2263
|
color: var(--control-fg);
|
|
2264
|
+
/* why: an unset hook, so a theme can give every field a resting elevation
|
|
2265
|
+
that focus keeps (see the focus rule). The fallback is none, the same as
|
|
2266
|
+
the focus rule's: a transparent 0 0 shadow here made focus transition
|
|
2267
|
+
between two values and expose a phantom shadow mid-way. */
|
|
2268
|
+
box-shadow: var(--control-shadow, none);
|
|
2253
2269
|
font-size: var(--control-font-size);
|
|
2254
2270
|
cursor: var(--control-cursor);
|
|
2255
2271
|
/* No outline: none here — the core :focus fallback must survive for browsers
|
|
@@ -2282,13 +2298,15 @@ small {
|
|
|
2282
2298
|
the control's border box (any smaller offset bleeds outward), so it covers
|
|
2283
2299
|
the border, never touches a .join neighbour or a scrollport edge, and needs
|
|
2284
2300
|
no group treatment. Width follows --focus-ring-width, including its
|
|
2285
|
-
prefers-contrast step. Border = shape and validation, outline = focus.
|
|
2301
|
+
prefers-contrast step. Border = shape and validation, outline = focus.
|
|
2302
|
+
The box-shadow reset drops any halo but keeps the field's own elevation:
|
|
2303
|
+
a plain none erased --control-shadow on keyboard focus. */
|
|
2286
2304
|
.input:focus-visible:not(:disabled),
|
|
2287
2305
|
.textarea:focus-visible:not(:disabled),
|
|
2288
2306
|
.select:focus-visible:not(:disabled) {
|
|
2289
2307
|
outline: var(--focus-ring-width) solid var(--form-invalid-border, var(--focus));
|
|
2290
2308
|
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
2291
|
-
box-shadow: none;
|
|
2309
|
+
box-shadow: var(--control-shadow, none);
|
|
2292
2310
|
}
|
|
2293
2311
|
|
|
2294
2312
|
.input[readonly],
|
|
@@ -3532,14 +3550,17 @@ small {
|
|
|
3532
3550
|
clip-path: circle(0 at var(--choice-chip-dot));
|
|
3533
3551
|
}
|
|
3534
3552
|
|
|
3535
|
-
/* The check keeps the card's scale-in, in flow ahead of the label.
|
|
3553
|
+
/* The check keeps the card's scale-in, in flow ahead of the label. It is ink
|
|
3554
|
+
on the chip, not a fill: a theme whose selected fill cannot be text (a
|
|
3555
|
+
yellow or lime accent) lends its selected ink here, as for tabs and steps.
|
|
3556
|
+
It outranks explicit intents too: their pastel fills are still not ink. */
|
|
3536
3557
|
.choice-card.chip::after {
|
|
3537
3558
|
order: -1;
|
|
3538
3559
|
flex: none;
|
|
3539
3560
|
align-self: center;
|
|
3540
3561
|
margin-block: 0;
|
|
3541
3562
|
margin-inline: 0 0.375em;
|
|
3542
|
-
background: var(--intent, var(--state-selected));
|
|
3563
|
+
background: var(--state-selected-text, var(--intent, var(--state-selected)));
|
|
3543
3564
|
-webkit-mask-size: contain;
|
|
3544
3565
|
mask-size: contain;
|
|
3545
3566
|
}
|
|
@@ -3627,6 +3648,10 @@ small {
|
|
|
3627
3648
|
its invisible reach. */
|
|
3628
3649
|
margin: calc(var(--color-swatch-reach) / -2);
|
|
3629
3650
|
padding: 0;
|
|
3651
|
+
/* No in-flow text, so the baseline would be the box's bottom edge and an
|
|
3652
|
+
inline swatch would ride above the line (a flyout trigger beside its
|
|
3653
|
+
label). A palette in .cluster or .grid ignores it. */
|
|
3654
|
+
vertical-align: middle;
|
|
3630
3655
|
border: 0;
|
|
3631
3656
|
background: transparent;
|
|
3632
3657
|
color: inherit;
|
|
@@ -3776,7 +3801,7 @@ small {
|
|
|
3776
3801
|
--control-bg: var(--surface-raised);
|
|
3777
3802
|
outline: var(--focus-ring-width) solid var(--form-invalid-border, var(--focus));
|
|
3778
3803
|
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
3779
|
-
box-shadow: none;
|
|
3804
|
+
box-shadow: var(--control-shadow, none);
|
|
3780
3805
|
}
|
|
3781
3806
|
|
|
3782
3807
|
&::picker-icon {
|
|
@@ -4116,6 +4141,7 @@ small {
|
|
|
4116
4141
|
* --btn-radius
|
|
4117
4142
|
* --btn-gap
|
|
4118
4143
|
* --btn-hover-overlay
|
|
4144
|
+
* --btn-shadow
|
|
4119
4145
|
* --btn-default-bg
|
|
4120
4146
|
* --btn-default-fg
|
|
4121
4147
|
* --btn-default-border
|
|
@@ -4182,6 +4208,11 @@ small {
|
|
|
4182
4208
|
border-radius: var(--btn-radius);
|
|
4183
4209
|
background: var(--btn-bg);
|
|
4184
4210
|
color: var(--btn-fg);
|
|
4211
|
+
/* why: an unset hook rather than none, so a theme or container can give
|
|
4212
|
+
every button an elevation the hover overlay composes with instead of
|
|
4213
|
+
replacing (see the hover rule). The hook accepts shadow-list items:
|
|
4214
|
+
use 0 0 #0000 to remove elevation, since none invalidates that list. */
|
|
4215
|
+
box-shadow: var(--btn-shadow, 0 0 #0000);
|
|
4185
4216
|
font: inherit;
|
|
4186
4217
|
font-size: var(--btn-font-size);
|
|
4187
4218
|
font-weight: var(--font-weight-medium);
|
|
@@ -4266,8 +4297,10 @@ small {
|
|
|
4266
4297
|
background: var(--ui-hover-bg, var(--btn-bg));
|
|
4267
4298
|
/* Huge inset spread paints the overlay over any border-radius without a
|
|
4268
4299
|
pseudo-element; a plain background would replace the fill instead of
|
|
4269
|
-
darkening it. */
|
|
4270
|
-
box-shadow:
|
|
4300
|
+
darkening it. Listed after --btn-shadow, which it must not drop. */
|
|
4301
|
+
box-shadow:
|
|
4302
|
+
var(--btn-shadow, 0 0 #0000),
|
|
4303
|
+
inset 0 0 0 999px var(--btn-hover-overlay);
|
|
4271
4304
|
}
|
|
4272
4305
|
|
|
4273
4306
|
.btn:not(:where(:disabled, [aria-disabled="true"])):active {
|
|
@@ -4482,6 +4515,8 @@ a.btn[aria-disabled="true"] {
|
|
|
4482
4515
|
* --badge-default-fg
|
|
4483
4516
|
* --badge-default-border
|
|
4484
4517
|
* --badge-default-hover-bg
|
|
4518
|
+
* --badge-slot-inset
|
|
4519
|
+
* --badge-slot-size
|
|
4485
4520
|
* --badge-bg
|
|
4486
4521
|
* --badge-fg
|
|
4487
4522
|
* --badge-border
|
|
@@ -4506,9 +4541,13 @@ a.btn[aria-disabled="true"] {
|
|
|
4506
4541
|
--badge-font-size: var(--font-size-sm);
|
|
4507
4542
|
--badge-icon-size: 1em;
|
|
4508
4543
|
--badge-dot-size: 0.5rem;
|
|
4544
|
+
/* A leading .avatar and a trailing .close are slots: a disc inside the pill
|
|
4545
|
+
at the same inset from its block edges and its rounded end. */
|
|
4546
|
+
--badge-slot-inset: calc(var(--badge-size) / 12);
|
|
4547
|
+
--badge-slot-size: calc(var(--badge-size) - 2 * (var(--border-width) + var(--badge-slot-inset)));
|
|
4509
4548
|
/* Declared on the badge, not the button, so an author can still set them
|
|
4510
4549
|
on either. Same element as --badge-size, so .sm/.lg carry through. */
|
|
4511
|
-
--close-size:
|
|
4550
|
+
--close-size: var(--badge-slot-size);
|
|
4512
4551
|
--close-icon-size: 0.875em;
|
|
4513
4552
|
--badge-default-bg: var(--surface-subtle);
|
|
4514
4553
|
--badge-default-fg: var(--intent, var(--text));
|
|
@@ -4559,9 +4598,11 @@ a.btn[aria-disabled="true"] {
|
|
|
4559
4598
|
--badge-dot-size: 0.625rem;
|
|
4560
4599
|
}
|
|
4561
4600
|
|
|
4562
|
-
/* Decorative children follow the badge's optical size. Text
|
|
4563
|
-
button retain their own boxes
|
|
4564
|
-
.
|
|
4601
|
+
/* Decorative children follow the badge's optical size. Text, the .close
|
|
4602
|
+
button and an .avatar retain their own boxes — an aria-hidden avatar would
|
|
4603
|
+
otherwise tie with .avatar on specificity and shrink to an icon whenever
|
|
4604
|
+
badge.css loads last. */
|
|
4605
|
+
.badge > :where(svg, img, [aria-hidden="true"]:not(.dot, .avatar)) {
|
|
4565
4606
|
flex: none;
|
|
4566
4607
|
/* The shared rule maps --badge-icon-size onto font-size. One em is therefore
|
|
4567
4608
|
the requested optical size; reusing the em-valued hook here would apply its
|
|
@@ -4626,17 +4667,36 @@ a.btn[aria-disabled="true"] {
|
|
|
4626
4667
|
background-color: currentColor;
|
|
4627
4668
|
}
|
|
4628
4669
|
|
|
4629
|
-
/*
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4670
|
+
/* Person chip (mention, assignee, contributor) — a leading .avatar follows the
|
|
4671
|
+
pill instead of its own scale, so the badge keeps its height. */
|
|
4672
|
+
.badge > .avatar {
|
|
4673
|
+
--avatar-size: var(--badge-slot-size);
|
|
4674
|
+
/* Initials scale from the label, not the diameter: a diameter ratio left
|
|
4675
|
+
them under 6px on .sm. em resolves against the badge's font size here. */
|
|
4676
|
+
--avatar-font-size: 0.7em;
|
|
4677
|
+
margin-inline-start: calc(var(--badge-slot-inset) - var(--badge-pad-x));
|
|
4678
|
+
}
|
|
4679
|
+
|
|
4680
|
+
/* Removable tag — a trailing .close (components/close.css), the avatar's
|
|
4681
|
+
mirror: a fill or the hover wash stays a disc inside the pill. */
|
|
4633
4682
|
.badge > .close {
|
|
4634
|
-
|
|
4635
|
-
margin-inline-
|
|
4636
|
-
margin-inline-end: -0.45em;
|
|
4683
|
+
position: relative;
|
|
4684
|
+
margin-inline-end: calc(var(--badge-slot-inset) - var(--badge-pad-x));
|
|
4637
4685
|
border-radius: var(--radius-full);
|
|
4638
4686
|
}
|
|
4639
4687
|
|
|
4688
|
+
/* WCAG 2.5.8 floors pointer targets at 24 CSS px. The disc is smaller on the
|
|
4689
|
+
default and .sm badges, so a transparent pseudo-element — part of the
|
|
4690
|
+
button's hit area — extends the target to the floor around it, past the pill
|
|
4691
|
+
if need be, without painting anything there. */
|
|
4692
|
+
.badge > .close::after {
|
|
4693
|
+
content: "";
|
|
4694
|
+
position: absolute;
|
|
4695
|
+
/* Square on purpose: nothing paints it, and a round one would miss the
|
|
4696
|
+
corners of the 24px target. */
|
|
4697
|
+
inset: min(0px, calc((var(--close-size) - 24px) / 2));
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4640
4700
|
/* Hover recipes — sets --ui-hover-bg consumed by the hover rule below.
|
|
4641
4701
|
Declared outside :hover like button.css for the same reason. */
|
|
4642
4702
|
:is(a.badge, button.badge).outline {
|
|
@@ -4877,6 +4937,9 @@ a.badge[aria-disabled="true"] {
|
|
|
4877
4937
|
/* No grid-template-columns: the icon and close branches below always set
|
|
4878
4938
|
it, since one of them is what matched. */
|
|
4879
4939
|
align-items: var(--items-align, start);
|
|
4940
|
+
/* why: a stretched alert (equal-height grid row) spread its extra height
|
|
4941
|
+
between the content rows, opening a gap under the title. */
|
|
4942
|
+
align-content: start;
|
|
4880
4943
|
column-gap: var(--space-30);
|
|
4881
4944
|
}
|
|
4882
4945
|
|
|
@@ -4884,9 +4947,14 @@ a.badge[aria-disabled="true"] {
|
|
|
4884
4947
|
grid-template-columns: auto minmax(0, 1fr);
|
|
4885
4948
|
}
|
|
4886
4949
|
|
|
4950
|
+
/* why span 2, not 1 / -1: the rows are implicit, so -1 is line 1 and the
|
|
4951
|
+
icon sat in the title row alone — any icon taller than one line inflated
|
|
4952
|
+
that row and pushed the body down. Spanning the title and body rows lets
|
|
4953
|
+
its height spread over both; only an icon taller than both together still
|
|
4954
|
+
splits its excess between them. */
|
|
4887
4955
|
.alert:has(> .alert-icon) > .alert-icon {
|
|
4888
4956
|
grid-column: 1;
|
|
4889
|
-
grid-row: 1 /
|
|
4957
|
+
grid-row: 1 / span 2;
|
|
4890
4958
|
display: inline-grid;
|
|
4891
4959
|
place-items: center;
|
|
4892
4960
|
margin-block-start: 0.125em;
|
|
@@ -5054,6 +5122,7 @@ a.badge[aria-disabled="true"] {
|
|
|
5054
5122
|
* --card-fg
|
|
5055
5123
|
* --heading
|
|
5056
5124
|
* --busy-overlay-bg
|
|
5125
|
+
* --surface-link
|
|
5057
5126
|
*/
|
|
5058
5127
|
/* Intent boundary — inherited intents do not tint this component.
|
|
5059
5128
|
Local intent classes still win because :where() has zero specificity.
|
|
@@ -5062,6 +5131,7 @@ a.badge[aria-disabled="true"] {
|
|
|
5062
5131
|
when they paint this surface, so an ancestor intent would otherwise leak
|
|
5063
5132
|
through a variant that has no local intent class. */
|
|
5064
5133
|
:where(.card) {
|
|
5134
|
+
--surface-link: initial;
|
|
5065
5135
|
--intent: initial;
|
|
5066
5136
|
--intent-fg: initial;
|
|
5067
5137
|
--intent-soft-fg: initial;
|
|
@@ -5097,6 +5167,13 @@ a.badge[aria-disabled="true"] {
|
|
|
5097
5167
|
border-radius: var(--card-radius);
|
|
5098
5168
|
}
|
|
5099
5169
|
|
|
5170
|
+
/* Theme link ink is tuned for the page, not an intent fill. currentColor
|
|
5171
|
+
follows the surrounding copy without freezing a parent's resolved ink;
|
|
5172
|
+
the card and theme boundaries reset this internal relay above/in theme.css. */
|
|
5173
|
+
.card.solid {
|
|
5174
|
+
--surface-link: currentColor;
|
|
5175
|
+
}
|
|
5176
|
+
|
|
5100
5177
|
/* The box lives at zero specificity so a layout primitive applied to the same
|
|
5101
5178
|
element owns it. components/ is imported after layout/, so a plain .card
|
|
5102
5179
|
declaration would beat .media (and any other non-column primitive) on source
|
|
@@ -6339,12 +6416,16 @@ picture.bleed > img {
|
|
|
6339
6416
|
* --accordion-pad
|
|
6340
6417
|
* --accordion-marker-color
|
|
6341
6418
|
* --accordion-marker-size
|
|
6419
|
+
* --accordion-marker-icon
|
|
6420
|
+
* --accordion-marker-open-rotate
|
|
6342
6421
|
*/
|
|
6343
6422
|
.accordion {
|
|
6344
6423
|
--accordion-radius: var(--radius);
|
|
6345
6424
|
--accordion-pad: var(--space-40);
|
|
6346
6425
|
--accordion-marker-color: var(--text-muted);
|
|
6347
6426
|
--accordion-marker-size: 1rem;
|
|
6427
|
+
--accordion-marker-icon: var(--icon-chevron);
|
|
6428
|
+
--accordion-marker-open-rotate: 180deg;
|
|
6348
6429
|
|
|
6349
6430
|
border: var(--border-width) solid var(--border);
|
|
6350
6431
|
border-radius: var(--accordion-radius);
|
|
@@ -6389,13 +6470,13 @@ picture.bleed > img {
|
|
|
6389
6470
|
inline-size: var(--accordion-marker-size);
|
|
6390
6471
|
block-size: var(--accordion-marker-size);
|
|
6391
6472
|
background-color: var(--accordion-marker-color);
|
|
6392
|
-
-webkit-mask: var(--icon
|
|
6393
|
-
mask: var(--icon
|
|
6473
|
+
-webkit-mask: var(--accordion-marker-icon) center / contain no-repeat;
|
|
6474
|
+
mask: var(--accordion-marker-icon) center / contain no-repeat;
|
|
6394
6475
|
transition: transform var(--duration);
|
|
6395
6476
|
}
|
|
6396
6477
|
|
|
6397
6478
|
.accordion details[open] summary::after {
|
|
6398
|
-
transform: rotate(
|
|
6479
|
+
transform: rotate(var(--accordion-marker-open-rotate));
|
|
6399
6480
|
}
|
|
6400
6481
|
|
|
6401
6482
|
/* The panel, not the item, owns the inset here — .separated paints a card on
|
|
@@ -6592,6 +6673,17 @@ picture.bleed > img {
|
|
|
6592
6673
|
0 0 / 200% 100%;
|
|
6593
6674
|
}
|
|
6594
6675
|
|
|
6676
|
+
/* Translucent ink tints show the surface they sit on. Fixed surfaces did
|
|
6677
|
+
not: the --surface-raised highlight matched a card or a light page, so the
|
|
6678
|
+
shape lost its edges mid-sweep, and a soft card or dark page made the track
|
|
6679
|
+
inverted or invisible. The highlight keeps a tint so the edge never goes. */
|
|
6680
|
+
@supports (color: color-mix(in oklab, red, blue)) {
|
|
6681
|
+
.skeleton {
|
|
6682
|
+
--skeleton-track: color-mix(in oklab, currentColor 10%, transparent);
|
|
6683
|
+
--skeleton-highlight: color-mix(in oklab, currentColor 4%, transparent);
|
|
6684
|
+
}
|
|
6685
|
+
}
|
|
6686
|
+
|
|
6595
6687
|
@media (prefers-reduced-motion: no-preference) {
|
|
6596
6688
|
.skeleton {
|
|
6597
6689
|
animation: actual-skeleton var(--duration-shimmer) ease-in-out infinite;
|
|
@@ -7981,6 +8073,167 @@ a.avatar[aria-disabled="true"] {
|
|
|
7981
8073
|
no-repeat;
|
|
7982
8074
|
}
|
|
7983
8075
|
|
|
8076
|
+
/*
|
|
8077
|
+
* Timeline — a vertical history of dated events: activity feed, audit log,
|
|
8078
|
+
* order tracking.
|
|
8079
|
+
*
|
|
8080
|
+
* Docs: docs/pages/components/timeline.md
|
|
8081
|
+
*
|
|
8082
|
+
* <ol class="timeline">
|
|
8083
|
+
* <li class="success">
|
|
8084
|
+
* <time datetime="2026-10-18T09:30">Today, 09:30</time>
|
|
8085
|
+
* Appointment confirmed
|
|
8086
|
+
* </li>
|
|
8087
|
+
* </ol>
|
|
8088
|
+
*
|
|
8089
|
+
* Each event draws a dot on a rail; an intent class on the <li> colors it. An
|
|
8090
|
+
* .avatar or an aria-hidden icon written first in the <li> takes the dot's
|
|
8091
|
+
* place. A direct <time> child is the event's dateline. The content stays in
|
|
8092
|
+
* block flow, so a sentence mixing text and links is never split into items.
|
|
8093
|
+
*
|
|
8094
|
+
* Progress through a process is .steps, not a timeline.
|
|
8095
|
+
*
|
|
8096
|
+
* Public hooks:
|
|
8097
|
+
* --timeline-dot-size
|
|
8098
|
+
* --timeline-slot-size
|
|
8099
|
+
* --timeline-gap
|
|
8100
|
+
* --timeline-inline-gap
|
|
8101
|
+
* --timeline-line-size
|
|
8102
|
+
*
|
|
8103
|
+
* Internal:
|
|
8104
|
+
* --timeline-rail-size
|
|
8105
|
+
* --timeline-line-box
|
|
8106
|
+
*/
|
|
8107
|
+
|
|
8108
|
+
/* Registered so the event's line box computes to a length on the <li> and
|
|
8109
|
+
inherits as one: an avatar slot sets its own font size and line-height, and
|
|
8110
|
+
an unregistered em would resolve against those. Unregistered, the dot still
|
|
8111
|
+
lines up and the slot drifts by the font difference.
|
|
8112
|
+
|
|
8113
|
+
The initial value must be computationally independent: a rem or em there
|
|
8114
|
+
invalidates the whole @property rule, silently. Every <li> sets its own. */
|
|
8115
|
+
@property --timeline-line-box {
|
|
8116
|
+
syntax: "<length>";
|
|
8117
|
+
inherits: true;
|
|
8118
|
+
initial-value: 0px;
|
|
8119
|
+
}
|
|
8120
|
+
|
|
8121
|
+
.timeline {
|
|
8122
|
+
--timeline-dot-size: 0.625rem;
|
|
8123
|
+
--timeline-slot-size: 1.75rem;
|
|
8124
|
+
--timeline-gap: var(--density-space);
|
|
8125
|
+
--timeline-inline-gap: var(--space-30);
|
|
8126
|
+
--timeline-line-size: var(--border-width);
|
|
8127
|
+
--timeline-rail-size: var(--timeline-dot-size);
|
|
8128
|
+
|
|
8129
|
+
display: grid;
|
|
8130
|
+
/* A long URL or token in an event wraps instead of widening the list. */
|
|
8131
|
+
grid-template-columns: minmax(0, 1fr);
|
|
8132
|
+
/* A list stretched by its parent (a grid cell, an equal-height card) keeps
|
|
8133
|
+
its rhythm instead of spreading the events over the extra height. */
|
|
8134
|
+
align-content: start;
|
|
8135
|
+
gap: var(--timeline-gap);
|
|
8136
|
+
margin: 0;
|
|
8137
|
+
padding: 0;
|
|
8138
|
+
list-style: none;
|
|
8139
|
+
}
|
|
8140
|
+
|
|
8141
|
+
/* One slotted event widens the rail of the whole list, so every line and every
|
|
8142
|
+
text column stays aligned when dots and avatars mix. Without :has() the rail
|
|
8143
|
+
keeps the dot's width and a slot shrinks to it: the degraded list stays
|
|
8144
|
+
aligned and readable, with a dot-sized avatar. */
|
|
8145
|
+
.timeline:has(> li > :is(.avatar, [aria-hidden="true"]):first-child) {
|
|
8146
|
+
--timeline-rail-size: var(--timeline-slot-size);
|
|
8147
|
+
}
|
|
8148
|
+
|
|
8149
|
+
/* The line box is a length, not the unitless ratio: a smaller <time> opening
|
|
8150
|
+
the event keeps the same first line as text would, so the marker centred on
|
|
8151
|
+
it lines up whichever element comes first. */
|
|
8152
|
+
.timeline > li {
|
|
8153
|
+
--timeline-line-box: calc(var(--line-height) * 1em);
|
|
8154
|
+
|
|
8155
|
+
position: relative;
|
|
8156
|
+
padding-inline-start: calc(var(--timeline-rail-size) + var(--timeline-inline-gap));
|
|
8157
|
+
line-height: var(--timeline-line-box);
|
|
8158
|
+
}
|
|
8159
|
+
|
|
8160
|
+
/* The event owns no rhythm beyond its dateline: block children drop their own
|
|
8161
|
+
margins, and an author who needs more spacing composes a .stack inside. */
|
|
8162
|
+
.timeline > li > * {
|
|
8163
|
+
margin-block: 0;
|
|
8164
|
+
}
|
|
8165
|
+
|
|
8166
|
+
.timeline > li > time {
|
|
8167
|
+
display: block;
|
|
8168
|
+
color: var(--text-muted);
|
|
8169
|
+
font-size: var(--font-size-sm);
|
|
8170
|
+
}
|
|
8171
|
+
|
|
8172
|
+
.timeline > li::before {
|
|
8173
|
+
content: "";
|
|
8174
|
+
position: absolute;
|
|
8175
|
+
inset-block-start: calc((var(--timeline-line-box) - var(--timeline-dot-size)) / 2);
|
|
8176
|
+
inset-inline-start: calc((var(--timeline-rail-size) - var(--timeline-dot-size)) / 2);
|
|
8177
|
+
inline-size: var(--timeline-dot-size);
|
|
8178
|
+
block-size: var(--timeline-dot-size);
|
|
8179
|
+
border-radius: var(--radius-full);
|
|
8180
|
+
background: var(--intent, var(--neutral));
|
|
8181
|
+
}
|
|
8182
|
+
|
|
8183
|
+
/* Stated as an undo, not as `:not(:has(...))` on the dot rule: an engine
|
|
8184
|
+
without :has() would drop that whole rule and lose every dot, where this way
|
|
8185
|
+
it only keeps a dot under the slot. */
|
|
8186
|
+
.timeline > li:has(> :is(.avatar, [aria-hidden="true"]):first-child)::before {
|
|
8187
|
+
content: none;
|
|
8188
|
+
}
|
|
8189
|
+
|
|
8190
|
+
/* The segment clears each marker by half the rail, so it never shows through
|
|
8191
|
+
a translucent avatar fill, and a dot in a list widened by a slot keeps the
|
|
8192
|
+
slot's clearance. A border rather than a background: forced colors keep a
|
|
8193
|
+
border and wipe a background. */
|
|
8194
|
+
.timeline > li:not(:last-child)::after {
|
|
8195
|
+
content: "";
|
|
8196
|
+
position: absolute;
|
|
8197
|
+
inset-block-start: calc((var(--timeline-line-box) + var(--timeline-rail-size)) / 2);
|
|
8198
|
+
inset-inline-start: calc((var(--timeline-rail-size) - var(--timeline-line-size)) / 2);
|
|
8199
|
+
block-size: calc(100% + var(--timeline-gap) - var(--timeline-rail-size));
|
|
8200
|
+
border-inline-start: var(--timeline-line-size) solid var(--border);
|
|
8201
|
+
}
|
|
8202
|
+
|
|
8203
|
+
.timeline > li > :is(.avatar, [aria-hidden="true"]):first-child {
|
|
8204
|
+
--avatar-size: var(--timeline-rail-size);
|
|
8205
|
+
|
|
8206
|
+
position: absolute;
|
|
8207
|
+
inset-block-start: calc((var(--timeline-line-box) - var(--timeline-rail-size)) / 2);
|
|
8208
|
+
inset-inline-start: 0;
|
|
8209
|
+
}
|
|
8210
|
+
|
|
8211
|
+
/* An icon slot gets a disc of the avatar's size, so both slot kinds read as one
|
|
8212
|
+
rail. The event's intent colors the glyph. */
|
|
8213
|
+
.timeline > li > [aria-hidden="true"]:first-child:not(.avatar) {
|
|
8214
|
+
display: grid;
|
|
8215
|
+
place-items: center;
|
|
8216
|
+
inline-size: var(--timeline-rail-size);
|
|
8217
|
+
block-size: var(--timeline-rail-size);
|
|
8218
|
+
border: var(--border-width) solid var(--border);
|
|
8219
|
+
border-radius: var(--radius-full);
|
|
8220
|
+
background: var(--surface-opaque);
|
|
8221
|
+
color: var(--intent, var(--text-muted));
|
|
8222
|
+
font-size: var(--font-size-sm);
|
|
8223
|
+
line-height: 1;
|
|
8224
|
+
}
|
|
8225
|
+
|
|
8226
|
+
.timeline > li > [aria-hidden="true"]:first-child:not(.avatar) > svg {
|
|
8227
|
+
inline-size: 1em;
|
|
8228
|
+
block-size: 1em;
|
|
8229
|
+
}
|
|
8230
|
+
|
|
8231
|
+
@media (forced-colors: active) {
|
|
8232
|
+
.timeline > li::before {
|
|
8233
|
+
background: CanvasText;
|
|
8234
|
+
}
|
|
8235
|
+
}
|
|
8236
|
+
|
|
7984
8237
|
|
|
7985
8238
|
/* Navigation */
|
|
7986
8239
|
/*
|
|
@@ -8435,6 +8688,9 @@ a.avatar[aria-disabled="true"] {
|
|
|
8435
8688
|
* Public hooks:
|
|
8436
8689
|
* --tab-flex
|
|
8437
8690
|
* --tab-gap
|
|
8691
|
+
* --tab-panel-bg
|
|
8692
|
+
* --tab-panel-pad
|
|
8693
|
+
* --tab-panel-shadow
|
|
8438
8694
|
*
|
|
8439
8695
|
* Internal:
|
|
8440
8696
|
* --tab-edge
|
|
@@ -8665,6 +8921,80 @@ a.avatar[aria-disabled="true"] {
|
|
|
8665
8921
|
inline-size: 100%;
|
|
8666
8922
|
}
|
|
8667
8923
|
|
|
8924
|
+
/* Attached horizontal tabs share a surface with their panels. The strip
|
|
8925
|
+
draws the panel's top edge inside its scrollport: no overlapping layers
|
|
8926
|
+
or outside ink that overflow clipping could cut off. Direct children
|
|
8927
|
+
keep an ordinary or nested tablist inside a panel independent. */
|
|
8928
|
+
.tabs-attached {
|
|
8929
|
+
--tab-panel-bg: var(--surface-raised);
|
|
8930
|
+
--tab-panel-pad: var(--space-40);
|
|
8931
|
+
--tab-panel-shadow: none;
|
|
8932
|
+
|
|
8933
|
+
display: grid;
|
|
8934
|
+
gap: 0;
|
|
8935
|
+
min-inline-size: 0;
|
|
8936
|
+
}
|
|
8937
|
+
|
|
8938
|
+
.tabs-attached > .tabs {
|
|
8939
|
+
gap: 0;
|
|
8940
|
+
border: 0;
|
|
8941
|
+
background:
|
|
8942
|
+
linear-gradient(var(--border), var(--border)) bottom / 100% var(--border-width) no-repeat;
|
|
8943
|
+
}
|
|
8944
|
+
|
|
8945
|
+
.tabs-attached > .tabs > .tab {
|
|
8946
|
+
border: var(--border-width) solid var(--border);
|
|
8947
|
+
background: var(--surface-subtle);
|
|
8948
|
+
}
|
|
8949
|
+
|
|
8950
|
+
.tabs-attached > .tabs > .tab + .tab {
|
|
8951
|
+
margin-inline-start: calc(-1 * var(--border-width));
|
|
8952
|
+
}
|
|
8953
|
+
|
|
8954
|
+
.tabs-attached > .tabs > .tab[aria-selected="true"] {
|
|
8955
|
+
background: var(--tab-panel-bg);
|
|
8956
|
+
color: var(--text);
|
|
8957
|
+
}
|
|
8958
|
+
|
|
8959
|
+
.tabs-attached > .tabs > .tab[aria-selected="true"]::after {
|
|
8960
|
+
/* Keep all four border colors equal: differently colored bottom borders
|
|
8961
|
+
meet the sides diagonally. Erase only the inner bottom edge so the
|
|
8962
|
+
vertical strokes reach the panel with square ends. */
|
|
8963
|
+
inset-block-end: calc(-1 * var(--border-width));
|
|
8964
|
+
block-size: var(--border-width);
|
|
8965
|
+
border: 0;
|
|
8966
|
+
background: var(--tab-panel-bg);
|
|
8967
|
+
}
|
|
8968
|
+
|
|
8969
|
+
.tabs-attached > .tab-panel {
|
|
8970
|
+
min-inline-size: 0;
|
|
8971
|
+
margin: 0;
|
|
8972
|
+
padding: var(--tab-panel-pad);
|
|
8973
|
+
border: var(--border-width) solid var(--border);
|
|
8974
|
+
border-block-start: 0;
|
|
8975
|
+
background: var(--tab-panel-bg);
|
|
8976
|
+
box-shadow: var(--tab-panel-shadow);
|
|
8977
|
+
}
|
|
8978
|
+
|
|
8979
|
+
/* Forced colors remove gradients and shadows. A real strip edge and the
|
|
8980
|
+
user's selection colors preserve both the frame and the selected state. */
|
|
8981
|
+
@media (forced-colors: active) {
|
|
8982
|
+
.tabs-attached > .tabs {
|
|
8983
|
+
border-block-end: var(--border-width) solid CanvasText;
|
|
8984
|
+
}
|
|
8985
|
+
|
|
8986
|
+
.tabs-attached > .tabs > .tab[aria-selected="true"] {
|
|
8987
|
+
forced-color-adjust: none;
|
|
8988
|
+
border-color: CanvasText;
|
|
8989
|
+
background: Highlight;
|
|
8990
|
+
color: HighlightText;
|
|
8991
|
+
}
|
|
8992
|
+
|
|
8993
|
+
.tabs-attached > .tabs > .tab[aria-selected="true"]::after {
|
|
8994
|
+
content: none;
|
|
8995
|
+
}
|
|
8996
|
+
}
|
|
8997
|
+
|
|
8668
8998
|
/*
|
|
8669
8999
|
* Segmented control — one compact choice among a few, on a shared track.
|
|
8670
9000
|
*
|
|
@@ -9088,6 +9418,11 @@ a.list-item:is([aria-current="page"], [aria-current="true"])::before {
|
|
|
9088
9418
|
* Public hooks:
|
|
9089
9419
|
* --table-min
|
|
9090
9420
|
* --table-cell-pad
|
|
9421
|
+
*
|
|
9422
|
+
* Theme hooks (unset by default, read with a fallback, so a theme sets them
|
|
9423
|
+
* once on its root and third-party grid bridges can read the same choice):
|
|
9424
|
+
* --table-header-bg
|
|
9425
|
+
* --table-header-fg
|
|
9091
9426
|
*/
|
|
9092
9427
|
.table-wrap {
|
|
9093
9428
|
/* Containing block for anything absolutely positioned inside the table —
|
|
@@ -9138,11 +9473,12 @@ a.list-item:is([aria-current="page"], [aria-current="true"])::before {
|
|
|
9138
9473
|
}
|
|
9139
9474
|
|
|
9140
9475
|
.table thead th {
|
|
9476
|
+
color: var(--table-header-fg, var(--text-muted));
|
|
9141
9477
|
vertical-align: bottom;
|
|
9142
9478
|
}
|
|
9143
9479
|
|
|
9144
9480
|
.table thead {
|
|
9145
|
-
background: var(--surface-subtle);
|
|
9481
|
+
background: var(--table-header-bg, var(--surface-subtle));
|
|
9146
9482
|
}
|
|
9147
9483
|
|
|
9148
9484
|
.table tbody tr:hover {
|