actual-css 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +4 -4
- package/components.json +116 -10
- package/dist/actual.full.css +648 -77
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/navigation.json +2 -0
- package/docs/pages/components/alert.md +12 -2
- package/docs/pages/components/button.md +3 -2
- package/docs/pages/components/list.md +6 -3
- package/docs/pages/components/segmented.md +83 -0
- package/docs/pages/components/steps.md +4 -2
- package/docs/pages/enhancements/flyout.md +5 -0
- package/docs/pages/enhancements/tabs.md +7 -2
- package/docs/pages/examples/overview.md +3 -1
- package/docs/pages/forms/color-swatches.md +131 -0
- package/docs/pages/forms/combobox.md +2 -2
- package/docs/pages/forms/controls.md +6 -3
- package/docs/pages/forms/joined-controls.md +6 -2
- package/docs/pages/forms/overview.md +1 -0
- package/docs/pages/forms/switch.md +4 -2
- package/docs/pages/foundations/theming.md +1 -1
- package/docs/pages/foundations/tokens.md +54 -23
- package/docs/pages/guides/design-tools.md +13 -7
- package/docs/pages/guides/introduction.md +1 -1
- package/docs/pages/layout/scroll-snap.md +4 -2
- package/package.json +2 -2
- package/reserved-classes.json +3 -0
- package/src/cli/actual-css.js +3 -3
- package/src/css/components/alert.css +65 -14
- package/src/css/components/app-nav.css +17 -9
- package/src/css/components/badge.css +23 -9
- package/src/css/components/button.css +17 -2
- package/src/css/components/close.css +6 -3
- package/src/css/components/index.css +1 -0
- package/src/css/components/list.css +18 -3
- package/src/css/components/navbar.css +5 -2
- package/src/css/components/segmented.css +186 -0
- package/src/css/components/steps.css +6 -3
- package/src/css/components/tab.css +1 -1
- package/src/css/core/focus.css +3 -0
- package/src/css/core/tokens.css +20 -4
- package/src/css/core/variants.css +16 -5
- package/src/css/forms/color-swatch.css +159 -0
- package/src/css/forms/form-actions.css +9 -0
- package/src/css/forms/index.css +1 -0
- package/src/css/forms/native.css +50 -20
- package/src/css/forms/switch.css +5 -2
- package/src/css/layout/scroll-snap.css +30 -0
- package/src/css/layout/topbar.css +10 -0
- package/src/js/focus.js +16 -1
- package/src/js/surface.js +10 -0
- package/src/tooling/color.js +14 -1
- package/src/tooling/css-bundle.js +0 -5
- package/src/tooling/design-components.js +9 -5
- package/src/tooling/design-export.js +8 -0
- package/src/tooling/design-tokens.js +163 -43
- package/src/tooling/theme-contrast.js +151 -22
package/dist/actual.full.css
CHANGED
|
@@ -215,9 +215,20 @@ select {
|
|
|
215
215
|
[data-theme] {
|
|
216
216
|
/* State tokens — semantic colors for selected/pressed/current and disabled.
|
|
217
217
|
Components consume these instead of hard-wiring --primary/--text-subtle
|
|
218
|
-
so forced-colors can override them in one place.
|
|
218
|
+
so forced-colors can override them in one place. --state-selected is a
|
|
219
|
+
selected fill or accent, --state-selected-fg the ink on that fill.
|
|
220
|
+
--state-selected-text is selected ink on a regular surface (current nav
|
|
221
|
+
link, selected tab, current step number). It is reset to the
|
|
222
|
+
guaranteed-invalid value on every theme boundary, so a consumer reads its
|
|
223
|
+
local --state-selected through the fallback var(--state-selected-text,
|
|
224
|
+
var(--state-selected)): a theme that resolves it to var(--link) bakes that
|
|
225
|
+
reference where it is declared, so a nested island that does not set it
|
|
226
|
+
inherits an already-resolved value the local fallback can never reach. A
|
|
227
|
+
theme sets it when one color cannot be both a fill under its -fg and text
|
|
228
|
+
on the page, typically in a dark scheme. */
|
|
219
229
|
--state-selected: var(--primary);
|
|
220
230
|
--state-selected-fg: var(--primary-fg);
|
|
231
|
+
--state-selected-text: initial;
|
|
221
232
|
--state-disabled: var(--text-subtle);
|
|
222
233
|
|
|
223
234
|
/* Status dot ring — cuts the dot out of what it overlaps. */
|
|
@@ -226,10 +237,15 @@ select {
|
|
|
226
237
|
|
|
227
238
|
:root {
|
|
228
239
|
/* Variant tuning — themes may adjust generated soft surfaces without
|
|
229
|
-
replacing the component rules.
|
|
240
|
+
replacing the component rules. --soft-bg-mix is the share of surface in a
|
|
241
|
+
soft fill: an opaque mix on the shared .soft, an opacity (100% minus it)
|
|
242
|
+
on the translucent badge, alert and soft-button tint. --soft-hover-alpha
|
|
243
|
+
is the extra intent share on hover. --soft-border-mix has no default: it
|
|
244
|
+
is a fallback-only hook whose absence lets each recipe keep its own rim
|
|
245
|
+
(a tinted rim on .soft, none on the tint), and a theme that sets it gives
|
|
246
|
+
both a rim of that weight. */
|
|
230
247
|
--soft-bg-mix: 88%;
|
|
231
|
-
--soft-
|
|
232
|
-
--soft-hover-alpha: 12%;
|
|
248
|
+
--soft-hover-alpha: 6%;
|
|
233
249
|
/* Soft foreground: the share of raw intent in soft text. 100% keeps the
|
|
234
250
|
pure intent color; lower values rebate it toward --text, which vivid or
|
|
235
251
|
light intent palettes need to stay legible on their own soft surface.
|
|
@@ -718,6 +734,9 @@ small {
|
|
|
718
734
|
*
|
|
719
735
|
* Intent list source of truth: intents.css. Components consume --intent* tokens
|
|
720
736
|
* instead of re-listing intent classes.
|
|
737
|
+
*
|
|
738
|
+
* Public hooks:
|
|
739
|
+
* --soft-border-mix (fallback-only: a soft rim of that weight on every recipe)
|
|
721
740
|
*/
|
|
722
741
|
.solid {
|
|
723
742
|
--ui-bg: var(--intent, var(--neutral));
|
|
@@ -781,7 +800,7 @@ small {
|
|
|
781
800
|
|
|
782
801
|
/* Shared control scale. Wrappers are listed when their descendants form one
|
|
783
802
|
component (field, choice-card, floating-field, input-icon, join, pagination,
|
|
784
|
-
tabs, flyout). A local size declaration beats inherited density because it
|
|
803
|
+
tabs, segmented, flyout). A local size declaration beats inherited density because it
|
|
785
804
|
is declared on the sized component itself. Guarded by check:sync
|
|
786
805
|
(size-scale-participants): .sm and .lg must list the same components. */
|
|
787
806
|
:where(
|
|
@@ -794,6 +813,7 @@ small {
|
|
|
794
813
|
.field,
|
|
795
814
|
.choice,
|
|
796
815
|
.choice-card,
|
|
816
|
+
.color-swatch,
|
|
797
817
|
.input-icon,
|
|
798
818
|
.floating-field,
|
|
799
819
|
.otp,
|
|
@@ -801,6 +821,7 @@ small {
|
|
|
801
821
|
.pagination,
|
|
802
822
|
.tabs,
|
|
803
823
|
.tab,
|
|
824
|
+
.segmented,
|
|
804
825
|
.flyout
|
|
805
826
|
).sm {
|
|
806
827
|
--control-size: var(--control-size-sm);
|
|
@@ -821,6 +842,7 @@ small {
|
|
|
821
842
|
.field,
|
|
822
843
|
.choice,
|
|
823
844
|
.choice-card,
|
|
845
|
+
.color-swatch,
|
|
824
846
|
.input-icon,
|
|
825
847
|
.floating-field,
|
|
826
848
|
.otp,
|
|
@@ -828,6 +850,7 @@ small {
|
|
|
828
850
|
.pagination,
|
|
829
851
|
.tabs,
|
|
830
852
|
.tab,
|
|
853
|
+
.segmented,
|
|
831
854
|
.flyout
|
|
832
855
|
).lg {
|
|
833
856
|
--control-size: var(--control-size-lg);
|
|
@@ -839,21 +862,25 @@ small {
|
|
|
839
862
|
}
|
|
840
863
|
|
|
841
864
|
/* Modern color enhancement. Baseline variant values above remain usable
|
|
842
|
-
everywhere. Interactive states live in the components that consume them.
|
|
865
|
+
everywhere. Interactive states live in the components that consume them.
|
|
866
|
+
The shared .soft is opaque: it paints zones (.card, .navbar, .app-nav,
|
|
867
|
+
.chat-bubble) an author may lay over an image, where a translucent fill
|
|
868
|
+
would hand the text's contrast to whatever is behind it. The badge, alert
|
|
869
|
+
and soft button own a translucent tint instead (soft-tint). */
|
|
843
870
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
844
871
|
.soft {
|
|
845
|
-
/* @sync soft-recipe */
|
|
846
872
|
--ui-bg: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)));
|
|
847
873
|
--ui-border: color-mix(
|
|
848
874
|
in oklab,
|
|
849
|
-
var(--surface) var(--soft-border-mix),
|
|
875
|
+
var(--surface) var(--soft-border-mix, 65%),
|
|
850
876
|
var(--intent, var(--neutral))
|
|
851
877
|
);
|
|
878
|
+
/* @sync soft-ink */
|
|
852
879
|
--ui-fg: var(
|
|
853
880
|
--intent-soft-fg,
|
|
854
881
|
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
855
882
|
);
|
|
856
|
-
/* @sync soft-
|
|
883
|
+
/* @sync soft-ink */
|
|
857
884
|
}
|
|
858
885
|
}
|
|
859
886
|
|
|
@@ -880,6 +907,9 @@ small {
|
|
|
880
907
|
--surface-solid, so one token reads on the page, on a solid band and on
|
|
881
908
|
any nested surface.
|
|
882
909
|
|
|
910
|
+
A .segment is an action but takes the inset ring: its track's padding is
|
|
911
|
+
narrower than an offset ring, which would cover the neighbouring segment.
|
|
912
|
+
|
|
883
913
|
Documented exception: an invalid field focuses in --form-invalid-border
|
|
884
914
|
(danger), so the error stays visible at the point of correction. The border
|
|
885
915
|
carries the same hook; report:theme-contrast measures that line at 3:1
|
|
@@ -1927,16 +1957,36 @@ small {
|
|
|
1927
1957
|
* --scroll-snap-align
|
|
1928
1958
|
* --scroll-snap-item-size
|
|
1929
1959
|
* --scroll-snap-padding
|
|
1960
|
+
*
|
|
1961
|
+
* Internal:
|
|
1962
|
+
* --scroll-snap-focus-bleed
|
|
1930
1963
|
*/
|
|
1931
1964
|
.scroll-snap {
|
|
1932
1965
|
--scroll-snap-gap: var(--gap);
|
|
1933
1966
|
--scroll-snap-align: start;
|
|
1934
1967
|
--scroll-snap-item-size: auto;
|
|
1968
|
+
/* Declared here, not as a :root alias: an inherited alias arrives already
|
|
1969
|
+
computed, so a --focus-ring-width override on the rail would not reach it. */
|
|
1970
|
+
--scroll-snap-focus-bleed: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
1935
1971
|
|
|
1936
1972
|
display: flex;
|
|
1937
1973
|
flex-direction: row;
|
|
1938
1974
|
gap: var(--scroll-snap-gap);
|
|
1975
|
+
/* Both axes clip: `auto` on one computes the other to `auto`, so a ring on
|
|
1976
|
+
the top or bottom item would be lost too. Every side reserves the ring's
|
|
1977
|
+
own width; the block reserve returns to the flow through a negative
|
|
1978
|
+
margin, while the inline reserve stays as the rail's focus gutter.
|
|
1979
|
+
Bleeding the inline reserve outward instead would make the border box
|
|
1980
|
+
`containing block + 2 * reserve` wide, which at the viewport edge grows
|
|
1981
|
+
the page: a rail cannot be flush with arbitrary surrounding content and
|
|
1982
|
+
free of page overflow at once. */
|
|
1983
|
+
margin-block: calc(var(--scroll-snap-focus-bleed) * -1);
|
|
1984
|
+
padding-block: var(--scroll-snap-focus-bleed);
|
|
1985
|
+
padding-inline: var(--scroll-snap-focus-bleed);
|
|
1939
1986
|
overflow-x: auto;
|
|
1987
|
+
/* One scroll axis, stated: a fractional content height could otherwise raise
|
|
1988
|
+
a phantom vertical scrollbar under one font or zoom step. */
|
|
1989
|
+
overflow-y: hidden;
|
|
1940
1990
|
}
|
|
1941
1991
|
|
|
1942
1992
|
.scroll-snap > * {
|
|
@@ -1946,7 +1996,17 @@ small {
|
|
|
1946
1996
|
@supports (scroll-snap-type: x mandatory) {
|
|
1947
1997
|
.scroll-snap {
|
|
1948
1998
|
overscroll-behavior-inline: contain;
|
|
1999
|
+
/* Fallback only, never the ring's protector: padding-inline above already
|
|
2000
|
+
guarantees paint. A unitless `0` drops the clamp below but keeps this
|
|
2001
|
+
declaration, so the worst case is a snapped item flush with the gutter,
|
|
2002
|
+
not a clipped ring. */
|
|
1949
2003
|
scroll-padding-inline: var(--scroll-snap-padding, var(--scroll-snap-gap));
|
|
2004
|
+
/* A conforming <length> stays at least the focus reserve, so scroll and
|
|
2005
|
+
snap stop with the gutter's air. */
|
|
2006
|
+
scroll-padding-inline: max(
|
|
2007
|
+
var(--scroll-snap-padding, var(--scroll-snap-gap)),
|
|
2008
|
+
var(--scroll-snap-focus-bleed)
|
|
2009
|
+
);
|
|
1950
2010
|
/* x then inline: an engine that lacks the logical keyword keeps x.
|
|
1951
2011
|
proximity is the default, so a short rail cannot trap the pointer. */
|
|
1952
2012
|
scroll-snap-type: x proximity;
|
|
@@ -2043,6 +2103,16 @@ small {
|
|
|
2043
2103
|
}
|
|
2044
2104
|
}
|
|
2045
2105
|
|
|
2106
|
+
/* A reduce override, not a no-preference gate: a browser without the feature
|
|
2107
|
+
evaluates it false and keeps the frosted bar instead of losing it. */
|
|
2108
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
2109
|
+
.topbar {
|
|
2110
|
+
background: var(--surface);
|
|
2111
|
+
backdrop-filter: none;
|
|
2112
|
+
-webkit-backdrop-filter: none;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2046
2116
|
/* Readable content measure. Composes with .center for centering or with
|
|
2047
2117
|
margin-inline: auto for a one-off centered block.
|
|
2048
2118
|
*
|
|
@@ -2557,6 +2627,9 @@ small {
|
|
|
2557
2627
|
*
|
|
2558
2628
|
* Range has its own custom-styled file (range.css) so the focus ring can
|
|
2559
2629
|
* target the thumb pseudo-element rather than the rectangular CSS box.
|
|
2630
|
+
*
|
|
2631
|
+
* Public hooks:
|
|
2632
|
+
* --color-size
|
|
2560
2633
|
*/
|
|
2561
2634
|
.file {
|
|
2562
2635
|
color: var(--text-muted);
|
|
@@ -2602,16 +2675,24 @@ small {
|
|
|
2602
2675
|
outline-offset: var(--focus-outline-offset);
|
|
2603
2676
|
}
|
|
2604
2677
|
|
|
2605
|
-
/* Native color picker — keep the browser's dialog and selected-color UI.
|
|
2606
|
-
|
|
2607
|
-
|
|
2678
|
+
/* Native color picker — keep the browser's dialog and selected-color UI. Same
|
|
2679
|
+
geometry as .color-swatch: a --control-size hit area around a round swatch
|
|
2680
|
+
the height of a .switch track, so a picker, a palette and a switch compose.
|
|
2681
|
+
The input is the hit area; its padding leaves the swatch, which carries the
|
|
2682
|
+
edge and the focus ring. Vendor pseudos cannot share a rule. */
|
|
2608
2683
|
.color {
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2684
|
+
--color-size: calc(var(--choice-control-size) + 0.125em);
|
|
2685
|
+
|
|
2686
|
+
inline-size: max(var(--control-size), var(--color-size));
|
|
2687
|
+
block-size: max(var(--control-size), var(--color-size));
|
|
2688
|
+
padding: calc((max(var(--control-size), var(--color-size)) - var(--color-size)) / 2);
|
|
2689
|
+
/* Lay out at the visible width, as .color-swatch does: the hit area
|
|
2690
|
+
overflows on both sides so a label gap is measured from the swatch. */
|
|
2691
|
+
margin: 0 calc((var(--color-size) - max(var(--control-size), var(--color-size))) / 2);
|
|
2692
|
+
border: 0;
|
|
2693
|
+
background: transparent;
|
|
2694
|
+
/* Consumed so .sm/.lg reach the em-sized swatch. */
|
|
2695
|
+
font-size: var(--control-font-size);
|
|
2615
2696
|
cursor: pointer;
|
|
2616
2697
|
}
|
|
2617
2698
|
|
|
@@ -2620,13 +2701,21 @@ small {
|
|
|
2620
2701
|
}
|
|
2621
2702
|
|
|
2622
2703
|
.color::-webkit-color-swatch {
|
|
2623
|
-
border:
|
|
2624
|
-
border-radius:
|
|
2704
|
+
border: var(--border-width) solid var(--border-control);
|
|
2705
|
+
border-radius: var(--radius-full);
|
|
2625
2706
|
}
|
|
2626
2707
|
|
|
2627
2708
|
.color::-moz-color-swatch {
|
|
2628
|
-
border:
|
|
2629
|
-
border-radius:
|
|
2709
|
+
border: var(--border-width) solid var(--border-control);
|
|
2710
|
+
border-radius: var(--radius-full);
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
.color:hover::-webkit-color-swatch {
|
|
2714
|
+
border-color: var(--text-subtle);
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
.color:hover::-moz-color-swatch {
|
|
2718
|
+
border-color: var(--text-subtle);
|
|
2630
2719
|
}
|
|
2631
2720
|
|
|
2632
2721
|
.color:disabled {
|
|
@@ -2634,25 +2723,36 @@ small {
|
|
|
2634
2723
|
opacity: var(--disabled-opacity);
|
|
2635
2724
|
}
|
|
2636
2725
|
|
|
2726
|
+
/* The ring goes around the visible swatch, not the larger hit area. */
|
|
2637
2727
|
.color:focus-visible {
|
|
2728
|
+
outline: none;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
.color:focus-visible::-webkit-color-swatch {
|
|
2638
2732
|
outline: var(--focus-ring-width) solid var(--focus);
|
|
2639
2733
|
outline-offset: var(--focus-outline-offset);
|
|
2640
2734
|
}
|
|
2641
2735
|
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
}
|
|
2736
|
+
.color:focus-visible::-moz-color-swatch {
|
|
2737
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
2738
|
+
outline-offset: var(--focus-outline-offset);
|
|
2739
|
+
}
|
|
2647
2740
|
|
|
2648
|
-
|
|
2649
|
-
|
|
2741
|
+
@media (forced-colors: active) {
|
|
2742
|
+
/* The swatch color is content, so the swatch opts out of forcing and takes
|
|
2743
|
+
system colors for its edge and ring itself. */
|
|
2650
2744
|
.color::-webkit-color-swatch {
|
|
2745
|
+
--focus: CanvasText;
|
|
2746
|
+
|
|
2651
2747
|
forced-color-adjust: none;
|
|
2748
|
+
border-color: CanvasText;
|
|
2652
2749
|
}
|
|
2653
2750
|
|
|
2654
2751
|
.color::-moz-color-swatch {
|
|
2752
|
+
--focus: CanvasText;
|
|
2753
|
+
|
|
2655
2754
|
forced-color-adjust: none;
|
|
2755
|
+
border-color: CanvasText;
|
|
2656
2756
|
}
|
|
2657
2757
|
}
|
|
2658
2758
|
|
|
@@ -2802,6 +2902,15 @@ small {
|
|
|
2802
2902
|
}
|
|
2803
2903
|
}
|
|
2804
2904
|
|
|
2905
|
+
/* Same reduce override as .topbar (src/css/layout/topbar.css). */
|
|
2906
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
2907
|
+
.form-actions.sticky {
|
|
2908
|
+
background: var(--surface);
|
|
2909
|
+
-webkit-backdrop-filter: none;
|
|
2910
|
+
backdrop-filter: none;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2805
2914
|
/*
|
|
2806
2915
|
* Input Icon — decorative icon inside a relative wrapper, leading or trailing.
|
|
2807
2916
|
*
|
|
@@ -2881,6 +2990,7 @@ small {
|
|
|
2881
2990
|
* --switch-block-size
|
|
2882
2991
|
* --switch-knob-margin
|
|
2883
2992
|
* --switch-knob-size
|
|
2993
|
+
* --switch-knob-inline-size
|
|
2884
2994
|
* --switch-travel
|
|
2885
2995
|
*
|
|
2886
2996
|
* The track is sized from --choice-control-size (tokens.css), not
|
|
@@ -2914,9 +3024,11 @@ small {
|
|
|
2914
3024
|
2 *
|
|
2915
3025
|
var(--switch-knob-margin)
|
|
2916
3026
|
);
|
|
3027
|
+
/* A wider knob is a capsule; the travel follows its width. */
|
|
3028
|
+
--switch-knob-inline-size: var(--switch-knob-size);
|
|
2917
3029
|
--switch-travel: calc(
|
|
2918
3030
|
var(--switch-width) -
|
|
2919
|
-
var(--switch-knob-size) -
|
|
3031
|
+
var(--switch-knob-inline-size) -
|
|
2920
3032
|
2 *
|
|
2921
3033
|
var(--border-width) -
|
|
2922
3034
|
2 *
|
|
@@ -2952,7 +3064,7 @@ small {
|
|
|
2952
3064
|
position: absolute;
|
|
2953
3065
|
inset-block-start: var(--switch-knob-margin);
|
|
2954
3066
|
inset-inline-start: var(--switch-knob-margin);
|
|
2955
|
-
inline-size: var(--switch-knob-size);
|
|
3067
|
+
inline-size: var(--switch-knob-inline-size);
|
|
2956
3068
|
block-size: var(--switch-knob-size);
|
|
2957
3069
|
border-radius: var(--radius-full);
|
|
2958
3070
|
background: var(--switch-knob);
|
|
@@ -3464,6 +3576,166 @@ small {
|
|
|
3464
3576
|
}
|
|
3465
3577
|
}
|
|
3466
3578
|
|
|
3579
|
+
/*
|
|
3580
|
+
* Color swatch — a color chosen from a palette the application defines.
|
|
3581
|
+
*
|
|
3582
|
+
* Docs: docs/pages/forms/color-swatches.md
|
|
3583
|
+
*
|
|
3584
|
+
* A <label class="color-swatch"> wraps a native radio (one color) or checkbox
|
|
3585
|
+
* (several) and paints --color-swatch-value. The same class on a <button> is
|
|
3586
|
+
* the trigger of a palette flyout. The application owns the colors; Actual
|
|
3587
|
+
* owns the shape, the selection mark and the focus ring.
|
|
3588
|
+
*
|
|
3589
|
+
* Two sizes: the visible swatch (::before) is the height of a .switch track, so
|
|
3590
|
+
* a swatch and a switch in neighbouring rows match; the element is the hit
|
|
3591
|
+
* area around it. The hit area reaches half the inherited --gap past the
|
|
3592
|
+
* swatch, up to --control-size, and overflows its layout box: neighbours in a
|
|
3593
|
+
* .cluster or .grid meet in the middle of the gap instead of overlapping, and
|
|
3594
|
+
* rows sit one gap apart like columns. Edge, selection ring and focus ring all
|
|
3595
|
+
* belong to the visible swatch.
|
|
3596
|
+
*
|
|
3597
|
+
* The color is arbitrary, so nothing drawn on it may assume a contrast: the
|
|
3598
|
+
* selected mark is a double ring (--text outside, --surface-opaque inside) that
|
|
3599
|
+
* reads on any fill, and a hairline edge keeps a near-white color apart from
|
|
3600
|
+
* the page. ::before opts out of forced colors to keep the color, so it takes
|
|
3601
|
+
* system colors itself there.
|
|
3602
|
+
*
|
|
3603
|
+
* Without :has() the native radio stays visible in the swatch and carries the
|
|
3604
|
+
* state; the ring needs :has() to mirror it.
|
|
3605
|
+
*
|
|
3606
|
+
* Public hooks:
|
|
3607
|
+
* --color-swatch-value (fallback-only: the color, set per swatch)
|
|
3608
|
+
* --color-swatch-size
|
|
3609
|
+
* --color-swatch-radius
|
|
3610
|
+
*/
|
|
3611
|
+
.color-swatch {
|
|
3612
|
+
/* The .switch track height (switch.css); a browser test holds them equal. */
|
|
3613
|
+
--color-swatch-size: calc(var(--choice-control-size) + 0.125em);
|
|
3614
|
+
--color-swatch-radius: var(--radius-full);
|
|
3615
|
+
/* Internal: what the hit area adds to the swatch, half on each side. One gap
|
|
3616
|
+
at most, so two neighbours never claim the same point — a wider reach let
|
|
3617
|
+
the next swatch take a click on this one's center. */
|
|
3618
|
+
--color-swatch-reach: clamp(0px, var(--control-size) - var(--color-swatch-size), var(--gap));
|
|
3619
|
+
|
|
3620
|
+
position: relative;
|
|
3621
|
+
display: inline-grid;
|
|
3622
|
+
place-items: center;
|
|
3623
|
+
flex: none;
|
|
3624
|
+
inline-size: calc(var(--color-swatch-size) + var(--color-swatch-reach));
|
|
3625
|
+
block-size: calc(var(--color-swatch-size) + var(--color-swatch-reach));
|
|
3626
|
+
/* Lay out at the visible size: gaps are measured from the swatch, not from
|
|
3627
|
+
its invisible reach. */
|
|
3628
|
+
margin: calc(var(--color-swatch-reach) / -2);
|
|
3629
|
+
padding: 0;
|
|
3630
|
+
border: 0;
|
|
3631
|
+
background: transparent;
|
|
3632
|
+
color: inherit;
|
|
3633
|
+
/* Consumed so .sm/.lg reach the em-sized swatch, as on .choice-card. */
|
|
3634
|
+
font: inherit;
|
|
3635
|
+
font-size: var(--control-font-size);
|
|
3636
|
+
cursor: pointer;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
.color-swatch::before {
|
|
3640
|
+
content: "";
|
|
3641
|
+
grid-area: 1 / 1;
|
|
3642
|
+
inline-size: var(--color-swatch-size);
|
|
3643
|
+
block-size: var(--color-swatch-size);
|
|
3644
|
+
box-sizing: border-box;
|
|
3645
|
+
border: var(--border-width) solid var(--border-control);
|
|
3646
|
+
border-radius: var(--color-swatch-radius);
|
|
3647
|
+
background: var(--color-swatch-value, transparent) padding-box;
|
|
3648
|
+
/* The color is the content, as on .color (native.css). */
|
|
3649
|
+
forced-color-adjust: none;
|
|
3650
|
+
transition:
|
|
3651
|
+
border-color var(--duration),
|
|
3652
|
+
box-shadow var(--duration);
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
/* Shares the swatch's cell and paints after ::before, so a native radio left
|
|
3656
|
+
visible sits on the color. */
|
|
3657
|
+
.color-swatch > input {
|
|
3658
|
+
grid-area: 1 / 1;
|
|
3659
|
+
position: relative;
|
|
3660
|
+
margin: 0;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
/* Visually hidden but focusable, submitted and arrow-key navigable (sr-only
|
|
3664
|
+
pattern). Guarded: without :has() the swatch cannot mirror :checked, so the
|
|
3665
|
+
visible native radio is the fallback. */
|
|
3666
|
+
@supports selector(:has(*)) {
|
|
3667
|
+
.color-swatch > input {
|
|
3668
|
+
position: absolute;
|
|
3669
|
+
inline-size: 1px;
|
|
3670
|
+
block-size: 1px;
|
|
3671
|
+
padding: 0;
|
|
3672
|
+
margin: -1px;
|
|
3673
|
+
overflow: hidden;
|
|
3674
|
+
clip: rect(0, 0, 0, 0);
|
|
3675
|
+
white-space: nowrap;
|
|
3676
|
+
border: 0;
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
.color-swatch:hover::before {
|
|
3681
|
+
border-color: var(--text-subtle);
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
.color-swatch:has(> input:checked)::before {
|
|
3685
|
+
box-shadow:
|
|
3686
|
+
inset 0 0 0 2px var(--text),
|
|
3687
|
+
inset 0 0 0 4px var(--surface-opaque);
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
/* The ring goes around the visible swatch, not the larger hit area. */
|
|
3691
|
+
.color-swatch:focus-visible {
|
|
3692
|
+
outline: none;
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
.color-swatch:focus-visible::before {
|
|
3696
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
3697
|
+
outline-offset: var(--focus-outline-offset);
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
.color-swatch:has(> input:focus-visible)::before {
|
|
3701
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
3702
|
+
outline-offset: var(--focus-outline-offset);
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
.color-swatch:disabled {
|
|
3706
|
+
opacity: var(--disabled-opacity);
|
|
3707
|
+
cursor: not-allowed;
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
.color-swatch:has(> input:disabled) {
|
|
3711
|
+
opacity: var(--disabled-opacity);
|
|
3712
|
+
cursor: not-allowed;
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
/* A disabled swatch keeps its color: --disabled-opacity is 1 here
|
|
3716
|
+
(tokens.css), and the edge carries the state. */
|
|
3717
|
+
@media (forced-colors: active) {
|
|
3718
|
+
.color-swatch::before {
|
|
3719
|
+
--focus: CanvasText;
|
|
3720
|
+
|
|
3721
|
+
border-color: CanvasText;
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
.color-swatch:has(> input:checked)::before {
|
|
3725
|
+
box-shadow:
|
|
3726
|
+
inset 0 0 0 2px CanvasText,
|
|
3727
|
+
inset 0 0 0 4px Canvas;
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
.color-swatch:disabled::before {
|
|
3731
|
+
border-color: GrayText;
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
.color-swatch:has(> input:disabled)::before {
|
|
3735
|
+
border-color: GrayText;
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3467
3739
|
/*
|
|
3468
3740
|
* Customizable select — appearance: base-select picker enhancement.
|
|
3469
3741
|
*
|
|
@@ -3958,12 +4230,27 @@ small {
|
|
|
3958
4230
|
--ui-hover-bg: transparent;
|
|
3959
4231
|
}
|
|
3960
4232
|
|
|
4233
|
+
/* A soft button takes the badge and alert tint, not the shared opaque .soft:
|
|
4234
|
+
it carries a short label like them, and the translucent fill keeps
|
|
4235
|
+
separating on a subtle or raised surface. Ink stays the shared .soft one. */
|
|
3961
4236
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
3962
4237
|
.btn.soft {
|
|
4238
|
+
/* @sync soft-tint */
|
|
4239
|
+
--ui-bg: color-mix(
|
|
4240
|
+
in oklab,
|
|
4241
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
|
|
4242
|
+
transparent
|
|
4243
|
+
);
|
|
4244
|
+
--ui-border: color-mix(
|
|
4245
|
+
in oklab,
|
|
4246
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
|
|
4247
|
+
transparent
|
|
4248
|
+
);
|
|
4249
|
+
/* @sync soft-tint */
|
|
3963
4250
|
--ui-hover-bg: color-mix(
|
|
3964
4251
|
in oklab,
|
|
3965
|
-
var(--
|
|
3966
|
-
|
|
4252
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix) + var(--soft-hover-alpha)),
|
|
4253
|
+
transparent
|
|
3967
4254
|
);
|
|
3968
4255
|
}
|
|
3969
4256
|
|
|
@@ -4162,11 +4449,14 @@ a.btn[aria-disabled="true"] {
|
|
|
4162
4449
|
}
|
|
4163
4450
|
|
|
4164
4451
|
@media (forced-colors: active) {
|
|
4165
|
-
/* The X is a
|
|
4166
|
-
|
|
4167
|
-
|
|
4452
|
+
/* The X is a mask fill: without this the system remaps the background to
|
|
4453
|
+
Canvas and the glyph disappears. Kept on the pseudo so the host keeps its
|
|
4454
|
+
native color and focus adaptation. The fill is the system color, not
|
|
4455
|
+
currentColor: an unadjusted pseudo inherits the authored ink, so a white
|
|
4456
|
+
.solid ink painted a white X on Canvas. */
|
|
4168
4457
|
.close::before {
|
|
4169
4458
|
forced-color-adjust: none;
|
|
4459
|
+
background: ButtonText;
|
|
4170
4460
|
}
|
|
4171
4461
|
}
|
|
4172
4462
|
|
|
@@ -4283,28 +4573,42 @@ a.btn[aria-disabled="true"] {
|
|
|
4283
4573
|
}
|
|
4284
4574
|
|
|
4285
4575
|
/* Soft by default. Local defaults remain behind --ui-* in the resolution
|
|
4286
|
-
chain, so explicit shared treatments compose without selector exclusions.
|
|
4576
|
+
chain, so explicit shared treatments compose without selector exclusions.
|
|
4577
|
+
The fill is a translucent intent tint, so it shows the surface it sits on
|
|
4578
|
+
instead of a fixed mix with --surface: a mix stops separating on a subtle
|
|
4579
|
+
or raised surface of its own lightness. No rim unless a theme sets
|
|
4580
|
+
--soft-border-mix (tokens.css). */
|
|
4287
4581
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
4288
4582
|
.badge {
|
|
4289
|
-
/* @sync soft-
|
|
4583
|
+
/* @sync soft-tint */
|
|
4290
4584
|
--badge-default-bg: color-mix(
|
|
4291
4585
|
in oklab,
|
|
4292
|
-
var(--
|
|
4293
|
-
|
|
4586
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
|
|
4587
|
+
transparent
|
|
4294
4588
|
);
|
|
4295
4589
|
--badge-default-border: color-mix(
|
|
4296
4590
|
in oklab,
|
|
4297
|
-
var(--
|
|
4298
|
-
|
|
4591
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
|
|
4592
|
+
transparent
|
|
4299
4593
|
);
|
|
4594
|
+
/* @sync soft-tint */
|
|
4595
|
+
/* @sync soft-ink */
|
|
4300
4596
|
--badge-default-fg: var(
|
|
4301
4597
|
--intent-soft-fg,
|
|
4302
4598
|
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
4303
4599
|
);
|
|
4304
|
-
/* @sync soft-
|
|
4600
|
+
/* @sync soft-ink */
|
|
4305
4601
|
}
|
|
4306
4602
|
}
|
|
4307
4603
|
|
|
4604
|
+
/* Soft is the badge's own default: an explicit .soft must not swap the tint
|
|
4605
|
+
for the shared opaque .soft recipe, so it defers to the default. */
|
|
4606
|
+
.badge.soft {
|
|
4607
|
+
--ui-bg: initial;
|
|
4608
|
+
--ui-fg: initial;
|
|
4609
|
+
--ui-border: initial;
|
|
4610
|
+
}
|
|
4611
|
+
|
|
4308
4612
|
.badge:empty {
|
|
4309
4613
|
--badge-size: var(--badge-dot-size);
|
|
4310
4614
|
--badge-bg: var(--intent, var(--neutral));
|
|
@@ -4343,8 +4647,8 @@ a.btn[aria-disabled="true"] {
|
|
|
4343
4647
|
:is(a.badge, button.badge):where(:not(.solid, .outline, :empty)) {
|
|
4344
4648
|
--badge-default-hover-bg: color-mix(
|
|
4345
4649
|
in oklab,
|
|
4346
|
-
var(--
|
|
4347
|
-
|
|
4650
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix) + var(--soft-hover-alpha)),
|
|
4651
|
+
transparent
|
|
4348
4652
|
);
|
|
4349
4653
|
}
|
|
4350
4654
|
|
|
@@ -4462,6 +4766,16 @@ a.badge[aria-disabled="true"] {
|
|
|
4462
4766
|
font-size: var(--alert-font-size);
|
|
4463
4767
|
}
|
|
4464
4768
|
|
|
4769
|
+
/* Density steps the block padding only: the inline padding is the text
|
|
4770
|
+
column's edge and stays stable. Local to the alert, like .card.compact. */
|
|
4771
|
+
.alert.compact {
|
|
4772
|
+
--alert-pad-block: var(--space-20);
|
|
4773
|
+
}
|
|
4774
|
+
|
|
4775
|
+
.alert.spacious {
|
|
4776
|
+
--alert-pad-block: var(--space-40);
|
|
4777
|
+
}
|
|
4778
|
+
|
|
4465
4779
|
.alert > * {
|
|
4466
4780
|
min-inline-size: 0;
|
|
4467
4781
|
}
|
|
@@ -4486,28 +4800,40 @@ a.badge[aria-disabled="true"] {
|
|
|
4486
4800
|
}
|
|
4487
4801
|
|
|
4488
4802
|
/* Alerts are soft by default. The recipe writes local defaults rather than
|
|
4489
|
-
--ui-* so an explicit shared treatment always wins without exclusions.
|
|
4803
|
+
--ui-* so an explicit shared treatment always wins without exclusions. The
|
|
4804
|
+
fill is the translucent soft tint (see badge.css), with no rim unless a
|
|
4805
|
+
theme sets --soft-border-mix. */
|
|
4490
4806
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
4491
4807
|
.alert {
|
|
4492
|
-
/* @sync soft-
|
|
4808
|
+
/* @sync soft-tint */
|
|
4493
4809
|
--alert-default-bg: color-mix(
|
|
4494
4810
|
in oklab,
|
|
4495
|
-
var(--
|
|
4496
|
-
|
|
4811
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
|
|
4812
|
+
transparent
|
|
4497
4813
|
);
|
|
4498
4814
|
--alert-default-border: color-mix(
|
|
4499
4815
|
in oklab,
|
|
4500
|
-
var(--
|
|
4501
|
-
|
|
4816
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
|
|
4817
|
+
transparent
|
|
4502
4818
|
);
|
|
4819
|
+
/* @sync soft-tint */
|
|
4820
|
+
/* @sync soft-ink */
|
|
4503
4821
|
--alert-default-fg: var(
|
|
4504
4822
|
--intent-soft-fg,
|
|
4505
4823
|
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
4506
4824
|
);
|
|
4507
|
-
/* @sync soft-
|
|
4825
|
+
/* @sync soft-ink */
|
|
4508
4826
|
}
|
|
4509
4827
|
}
|
|
4510
4828
|
|
|
4829
|
+
/* Soft is the alert's own default: an explicit .soft must not swap the tint
|
|
4830
|
+
for the shared opaque .soft recipe, so it defers to the default. */
|
|
4831
|
+
.alert.soft {
|
|
4832
|
+
--ui-bg: initial;
|
|
4833
|
+
--ui-fg: initial;
|
|
4834
|
+
--ui-border: initial;
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4511
4837
|
/* Decorative leading icon — use .alert-icon on the icon element. display and
|
|
4512
4838
|
vertical-align are the no-:has() layout (Degraded tier: the icon sits inline
|
|
4513
4839
|
before the text); the grid anatomy below replaces them. */
|
|
@@ -4606,7 +4932,10 @@ a.badge[aria-disabled="true"] {
|
|
|
4606
4932
|
|
|
4607
4933
|
/* Admonition — structured static box with a tinted title bar and a body
|
|
4608
4934
|
that rests on the page surface. Not collapsible — use .accordion inside
|
|
4609
|
-
the body if the content needs to be expandable.
|
|
4935
|
+
the body if the content needs to be expandable. The title bar is what a
|
|
4936
|
+
shared treatment paints (.solid fills it, .surface/.outline clear it): the
|
|
4937
|
+
root's fill never shows behind the opaque title and body, and the title ink
|
|
4938
|
+
already follows --ui-fg, so a bar left on the soft tint is unreadable. */
|
|
4610
4939
|
.alert.admonition {
|
|
4611
4940
|
padding: 0;
|
|
4612
4941
|
display: block;
|
|
@@ -4620,6 +4949,7 @@ a.badge[aria-disabled="true"] {
|
|
|
4620
4949
|
gap: var(--space-20);
|
|
4621
4950
|
padding-block: var(--alert-pad-block);
|
|
4622
4951
|
padding-inline: var(--alert-pad-inline);
|
|
4952
|
+
font-weight: var(--font-weight-semibold);
|
|
4623
4953
|
border-start-start-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
|
|
4624
4954
|
border-start-end-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
|
|
4625
4955
|
}
|
|
@@ -4631,18 +4961,17 @@ a.badge[aria-disabled="true"] {
|
|
|
4631
4961
|
|
|
4632
4962
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
4633
4963
|
.alert.admonition > .alert-title {
|
|
4634
|
-
background:
|
|
4635
|
-
|
|
4636
|
-
var(--surface) var(--soft-bg-mix),
|
|
4637
|
-
var(--intent, var(--neutral))
|
|
4964
|
+
background: var(
|
|
4965
|
+
--ui-bg,
|
|
4966
|
+
color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)))
|
|
4638
4967
|
);
|
|
4639
4968
|
}
|
|
4640
4969
|
}
|
|
4641
4970
|
|
|
4642
4971
|
@supports not (color: color-mix(in oklch, red, white)) {
|
|
4643
4972
|
.alert.admonition > .alert-title {
|
|
4644
|
-
background: var(--intent, var(--neutral));
|
|
4645
|
-
color: var(--intent-fg, var(--neutral-fg));
|
|
4973
|
+
background: var(--ui-bg, var(--intent, var(--neutral)));
|
|
4974
|
+
color: var(--ui-fg, var(--intent-fg, var(--neutral-fg)));
|
|
4646
4975
|
}
|
|
4647
4976
|
}
|
|
4648
4977
|
|
|
@@ -4653,6 +4982,32 @@ a.badge[aria-disabled="true"] {
|
|
|
4653
4982
|
color: var(--text);
|
|
4654
4983
|
}
|
|
4655
4984
|
|
|
4985
|
+
/* Solid: the filled title bar already carries the intent, so the frame moves
|
|
4986
|
+
to the body and stays neutral. The root border goes because a transparent
|
|
4987
|
+
one would show the root's intent fill around the body. */
|
|
4988
|
+
.alert.admonition.solid {
|
|
4989
|
+
border: 0;
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
.alert.admonition.solid > .alert-body {
|
|
4993
|
+
border: var(--border-width) solid var(--border);
|
|
4994
|
+
border-block-start: 0;
|
|
4995
|
+
border-end-start-radius: var(--alert-radius, var(--radius-lg));
|
|
4996
|
+
border-end-end-radius: var(--alert-radius, var(--radius-lg));
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4999
|
+
/* Forced colors drop the title fill, so the bar would lose its edge: the
|
|
5000
|
+
frame returns to the root as one uniform system-colored border. */
|
|
5001
|
+
@media (forced-colors: active) {
|
|
5002
|
+
.alert.admonition.solid {
|
|
5003
|
+
border: var(--border-width) solid CanvasText;
|
|
5004
|
+
}
|
|
5005
|
+
|
|
5006
|
+
.alert.admonition.solid > .alert-body {
|
|
5007
|
+
border: 0;
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
|
|
4656
5011
|
.alert.admonition > .alert-body > :first-child {
|
|
4657
5012
|
margin-block-start: 0;
|
|
4658
5013
|
}
|
|
@@ -7362,11 +7717,14 @@ a.avatar[aria-disabled="true"] {
|
|
|
7362
7717
|
|
|
7363
7718
|
/* Complete owns the accented connector that reaches the current step, so the
|
|
7364
7719
|
connector leaving current goes back to neutral. Declared after
|
|
7365
|
-
.step-complete at equal specificity: current wins on every channel.
|
|
7720
|
+
.step-complete at equal specificity: current wins on every channel. The
|
|
7721
|
+
current marker is a regular surface, so its number and ring are selected
|
|
7722
|
+
ink on a surface, not a selected fill; they are held to different ratios
|
|
7723
|
+
(4.5:1 for the number, 3:1 for the ring) by report:theme-contrast. */
|
|
7366
7724
|
.steps > [aria-current="step"] {
|
|
7367
7725
|
--step-marker-bg: var(--surface-opaque);
|
|
7368
|
-
--step-marker-border: var(--state-selected);
|
|
7369
|
-
--step-marker-fg: var(--state-selected);
|
|
7726
|
+
--step-marker-border: var(--state-selected-text, var(--state-selected));
|
|
7727
|
+
--step-marker-fg: var(--state-selected-text, var(--state-selected));
|
|
7370
7728
|
--step-line: var(--border);
|
|
7371
7729
|
|
|
7372
7730
|
color: var(--text);
|
|
@@ -7667,6 +8025,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7667
8025
|
}
|
|
7668
8026
|
|
|
7669
8027
|
.app-nav > a {
|
|
8028
|
+
position: relative;
|
|
7670
8029
|
display: grid;
|
|
7671
8030
|
grid-template-rows: auto auto;
|
|
7672
8031
|
place-content: center;
|
|
@@ -7707,20 +8066,27 @@ a.avatar[aria-disabled="true"] {
|
|
|
7707
8066
|
}
|
|
7708
8067
|
|
|
7709
8068
|
/* Current tile is already a full selected surface. Selection never changes
|
|
7710
|
-
text metrics: the shared medium weight is kept
|
|
7711
|
-
|
|
8069
|
+
text metrics: the shared medium weight is kept. The forced-colors cue is a
|
|
8070
|
+
transparent border on a ::before overlay, not on the tile: an element border
|
|
8071
|
+
shrinks the content box, and the side layout starts its content at the inline
|
|
8072
|
+
edge, so the icon and label shifted by --border-width when a tile became
|
|
8073
|
+
current. The overlay costs no layout, yet forced colors still turn its
|
|
8074
|
+
transparent border into a system-color frame once --state-selected flattens
|
|
8075
|
+
to the bar's own Canvas. No forced-colors repaint: a Highlight fill put
|
|
8076
|
+
HighlightText on Chromium's Canvas backplate, which hid the label. The
|
|
8077
|
+
positioning context is on every link so it does not move when one becomes
|
|
8078
|
+
current or an application places its own decoration. */
|
|
7712
8079
|
.app-nav > a[aria-current="page"] {
|
|
7713
8080
|
background: var(--state-selected);
|
|
7714
8081
|
color: var(--state-selected-fg);
|
|
7715
8082
|
}
|
|
7716
8083
|
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
}
|
|
8084
|
+
.app-nav > a[aria-current="page"]::before {
|
|
8085
|
+
content: "";
|
|
8086
|
+
position: absolute;
|
|
8087
|
+
inset: 0;
|
|
8088
|
+
border: var(--border-width) solid transparent;
|
|
8089
|
+
border-radius: inherit;
|
|
7724
8090
|
}
|
|
7725
8091
|
|
|
7726
8092
|
/*
|
|
@@ -7732,6 +8098,9 @@ a.avatar[aria-disabled="true"] {
|
|
|
7732
8098
|
* links are a .cluster using .nav-link items. .nav-list is the vertical nav
|
|
7733
8099
|
* list shared with .drawer, fed by the shared --gap token.
|
|
7734
8100
|
*
|
|
8101
|
+
* Public hooks:
|
|
8102
|
+
* --state-selected-text (fallback-only: current-link ink, see tokens.css)
|
|
8103
|
+
*
|
|
7735
8104
|
* Internal:
|
|
7736
8105
|
* --gap (local value for the shared gap token)
|
|
7737
8106
|
*/
|
|
@@ -7809,7 +8178,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7809
8178
|
shared with the hover rule above. */
|
|
7810
8179
|
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
7811
8180
|
background: var(--surface-subtle);
|
|
7812
|
-
color: var(--state-selected);
|
|
8181
|
+
color: var(--state-selected-text, var(--state-selected));
|
|
7813
8182
|
}
|
|
7814
8183
|
|
|
7815
8184
|
/* On a bar with its own --ui-bg/--ui-fg the fill is a tint of the link's own
|
|
@@ -7830,7 +8199,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7830
8199
|
|
|
7831
8200
|
/* After :hover so a hovered current link keeps the selected accent. */
|
|
7832
8201
|
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
7833
|
-
color: var(--ui-fg, var(--state-selected));
|
|
8202
|
+
color: var(--ui-fg, var(--state-selected-text, var(--state-selected)));
|
|
7834
8203
|
}
|
|
7835
8204
|
}
|
|
7836
8205
|
|
|
@@ -8265,7 +8634,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
8265
8634
|
stays inert. */
|
|
8266
8635
|
.tab[aria-selected="true"],
|
|
8267
8636
|
.tab:is([aria-current="page"], [aria-current="true"]) {
|
|
8268
|
-
color: var(--intent, var(--state-selected));
|
|
8637
|
+
color: var(--intent, var(--state-selected-text, var(--state-selected)));
|
|
8269
8638
|
}
|
|
8270
8639
|
|
|
8271
8640
|
/* The selected indicator lives on a positioned pseudo so that only the active
|
|
@@ -8296,6 +8665,193 @@ a.avatar[aria-disabled="true"] {
|
|
|
8296
8665
|
inline-size: 100%;
|
|
8297
8666
|
}
|
|
8298
8667
|
|
|
8668
|
+
/*
|
|
8669
|
+
* Segmented control — one compact choice among a few, on a shared track.
|
|
8670
|
+
*
|
|
8671
|
+
* Docs: docs/pages/components/segmented.md
|
|
8672
|
+
*
|
|
8673
|
+
* .segmented is the track, each .segment a direct child. The look is shared;
|
|
8674
|
+
* the semantics stay with the markup, and the selected state is read from it:
|
|
8675
|
+
* radios <label class="segment"><input type="radio"> :checked
|
|
8676
|
+
* tabs <button class="segment" role="tab"> aria-selected="true"
|
|
8677
|
+
* links <a class="segment"> aria-current="page"
|
|
8678
|
+
* No script of its own: radios bring their arrow keys, a tablist takes
|
|
8679
|
+
* data-enhance="tabs". Without :has() the radio anatomy loses its selected
|
|
8680
|
+
* fill and focus ring, and the native radio stays visible to carry both.
|
|
8681
|
+
*
|
|
8682
|
+
* One row of equal shares (grid-auto-columns: 1fr), at least --control-size
|
|
8683
|
+
* tall so it lines up with .btn and .input. Labels wrap instead of truncating;
|
|
8684
|
+
* a word longer than its share breaks, so the control never overflows its
|
|
8685
|
+
* container. A strip too long for one row is .tabs, which scrolls.
|
|
8686
|
+
*
|
|
8687
|
+
* Focus is drawn inside the segment, unlike other discrete actions
|
|
8688
|
+
* (focus.css): an offset ring would cross the track's padding and cover the
|
|
8689
|
+
* neighbouring segment.
|
|
8690
|
+
*
|
|
8691
|
+
* Public hooks:
|
|
8692
|
+
* --segmented-bg
|
|
8693
|
+
* --segmented-pad
|
|
8694
|
+
* --segmented-radius
|
|
8695
|
+
* --segmented-fg
|
|
8696
|
+
* --segmented-selected-bg
|
|
8697
|
+
* --segmented-selected-border
|
|
8698
|
+
*/
|
|
8699
|
+
.segmented {
|
|
8700
|
+
--segmented-bg: var(--surface-subtle);
|
|
8701
|
+
--segmented-pad: var(--space-10);
|
|
8702
|
+
--segmented-radius: var(--radius);
|
|
8703
|
+
--segmented-fg: var(--text-muted);
|
|
8704
|
+
--segmented-selected-bg: var(--surface-raised);
|
|
8705
|
+
/* The edge is what separates the selected segment where --surface-raised
|
|
8706
|
+
is darker than --surface-subtle (the default dark palette). */
|
|
8707
|
+
--segmented-selected-border: var(--border);
|
|
8708
|
+
|
|
8709
|
+
display: inline-grid;
|
|
8710
|
+
grid-auto-flow: column;
|
|
8711
|
+
grid-auto-columns: 1fr;
|
|
8712
|
+
gap: var(--segmented-pad);
|
|
8713
|
+
/* why: margin and border also reset <fieldset>, the radio anatomy's
|
|
8714
|
+
element. The border is transparent, not 0: forced colors paint it, which
|
|
8715
|
+
outlines the track once its fill is gone. */
|
|
8716
|
+
margin: 0;
|
|
8717
|
+
padding: var(--segmented-pad);
|
|
8718
|
+
border: var(--border-width) solid transparent;
|
|
8719
|
+
border-radius: var(--segmented-radius);
|
|
8720
|
+
background: var(--segmented-bg);
|
|
8721
|
+
font-size: var(--control-font-size);
|
|
8722
|
+
vertical-align: middle;
|
|
8723
|
+
}
|
|
8724
|
+
|
|
8725
|
+
.segment {
|
|
8726
|
+
position: relative;
|
|
8727
|
+
display: flex;
|
|
8728
|
+
align-items: center;
|
|
8729
|
+
justify-content: center;
|
|
8730
|
+
gap: 0.375em;
|
|
8731
|
+
/* The track is --control-size tall through its segments, not through its
|
|
8732
|
+
own min-block-size: a <fieldset> lays its content in an anonymous box
|
|
8733
|
+
sized to the content, so a minimum on the track left the segments short.
|
|
8734
|
+
A minimum, not a height: a wrapped label grows the whole row. */
|
|
8735
|
+
min-block-size: calc(var(--control-size) - 2 * (var(--segmented-pad) + var(--border-width)));
|
|
8736
|
+
padding: calc(var(--space-10) / 2) var(--space-30);
|
|
8737
|
+
border: 0;
|
|
8738
|
+
border-radius: calc(var(--segmented-radius) - var(--segmented-pad));
|
|
8739
|
+
background: transparent;
|
|
8740
|
+
color: var(--segmented-fg);
|
|
8741
|
+
font: inherit;
|
|
8742
|
+
/* Constant across states: a heavier selected label would resize its share. */
|
|
8743
|
+
font-weight: var(--font-weight-medium);
|
|
8744
|
+
line-height: 1.2;
|
|
8745
|
+
text-align: center;
|
|
8746
|
+
text-decoration: none;
|
|
8747
|
+
/* why: also shrinks the min-content contribution, so 1fr keeps equal
|
|
8748
|
+
shares under constraint instead of widening one around a long word. */
|
|
8749
|
+
overflow-wrap: anywhere;
|
|
8750
|
+
cursor: pointer;
|
|
8751
|
+
transition:
|
|
8752
|
+
background-color var(--duration),
|
|
8753
|
+
color var(--duration),
|
|
8754
|
+
box-shadow var(--duration);
|
|
8755
|
+
}
|
|
8756
|
+
|
|
8757
|
+
.segment > input {
|
|
8758
|
+
margin: 0;
|
|
8759
|
+
}
|
|
8760
|
+
|
|
8761
|
+
/* The radio is visually hidden but stays focusable, submitted and arrow-key
|
|
8762
|
+
navigable (sr-only pattern). Guarded: without :has() the segment cannot
|
|
8763
|
+
mirror :checked, so the visible native radio is the fallback. */
|
|
8764
|
+
@supports selector(:has(*)) {
|
|
8765
|
+
.segment > input {
|
|
8766
|
+
position: absolute;
|
|
8767
|
+
inline-size: 1px;
|
|
8768
|
+
block-size: 1px;
|
|
8769
|
+
padding: 0;
|
|
8770
|
+
margin: -1px;
|
|
8771
|
+
overflow: hidden;
|
|
8772
|
+
clip: rect(0, 0, 0, 0);
|
|
8773
|
+
white-space: nowrap;
|
|
8774
|
+
border: 0;
|
|
8775
|
+
}
|
|
8776
|
+
}
|
|
8777
|
+
|
|
8778
|
+
.segment:hover {
|
|
8779
|
+
color: var(--text);
|
|
8780
|
+
}
|
|
8781
|
+
|
|
8782
|
+
/* Separate rules from the :has() forms below: an engine without :has() would
|
|
8783
|
+
discard a shared selector list, ARIA states included. */
|
|
8784
|
+
.segment:is([aria-selected="true"], [aria-current="page"], [aria-current="true"]) {
|
|
8785
|
+
background: var(--segmented-selected-bg);
|
|
8786
|
+
color: var(--text);
|
|
8787
|
+
box-shadow:
|
|
8788
|
+
0 0 0 var(--border-width) var(--segmented-selected-border),
|
|
8789
|
+
var(--shadow);
|
|
8790
|
+
}
|
|
8791
|
+
|
|
8792
|
+
.segment:has(> input:checked) {
|
|
8793
|
+
background: var(--segmented-selected-bg);
|
|
8794
|
+
color: var(--text);
|
|
8795
|
+
box-shadow:
|
|
8796
|
+
0 0 0 var(--border-width) var(--segmented-selected-border),
|
|
8797
|
+
var(--shadow);
|
|
8798
|
+
}
|
|
8799
|
+
|
|
8800
|
+
.segment:focus-visible {
|
|
8801
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
8802
|
+
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
8803
|
+
}
|
|
8804
|
+
|
|
8805
|
+
.segment:has(> input:focus-visible) {
|
|
8806
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
8807
|
+
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
8808
|
+
}
|
|
8809
|
+
|
|
8810
|
+
.segment:is(:disabled, [aria-disabled="true"]) {
|
|
8811
|
+
opacity: var(--disabled-opacity);
|
|
8812
|
+
cursor: not-allowed;
|
|
8813
|
+
}
|
|
8814
|
+
|
|
8815
|
+
.segment:has(> input:disabled) {
|
|
8816
|
+
opacity: var(--disabled-opacity);
|
|
8817
|
+
cursor: not-allowed;
|
|
8818
|
+
}
|
|
8819
|
+
|
|
8820
|
+
/* Forced colors drop the raised fill and its edge. The selected segment
|
|
8821
|
+
takes the system highlight — a background, so the outline stays the focus
|
|
8822
|
+
channel alone. forced-color-adjust: none is required, not cosmetic: the UA
|
|
8823
|
+
otherwise paints a Canvas backplate behind the label, which hid the text on
|
|
8824
|
+
the highlight. Opting out also stops the forcing of the rest, so the shadow
|
|
8825
|
+
is dropped here and the focus line takes the highlight's text color. */
|
|
8826
|
+
@media (forced-colors: active) {
|
|
8827
|
+
.segment:is([aria-selected="true"], [aria-current="page"], [aria-current="true"]) {
|
|
8828
|
+
--focus: HighlightText;
|
|
8829
|
+
|
|
8830
|
+
forced-color-adjust: none;
|
|
8831
|
+
background: Highlight;
|
|
8832
|
+
color: HighlightText;
|
|
8833
|
+
box-shadow: none;
|
|
8834
|
+
}
|
|
8835
|
+
|
|
8836
|
+
.segment:has(> input:checked) {
|
|
8837
|
+
--focus: HighlightText;
|
|
8838
|
+
|
|
8839
|
+
forced-color-adjust: none;
|
|
8840
|
+
background: Highlight;
|
|
8841
|
+
color: HighlightText;
|
|
8842
|
+
box-shadow: none;
|
|
8843
|
+
}
|
|
8844
|
+
|
|
8845
|
+
/* --disabled-opacity is 1 here (tokens.css): the dimming needs a color. */
|
|
8846
|
+
.segment:is(:disabled, [aria-disabled="true"]) {
|
|
8847
|
+
color: GrayText;
|
|
8848
|
+
}
|
|
8849
|
+
|
|
8850
|
+
.segment:has(> input:disabled) {
|
|
8851
|
+
color: GrayText;
|
|
8852
|
+
}
|
|
8853
|
+
}
|
|
8854
|
+
|
|
8299
8855
|
/*
|
|
8300
8856
|
* Scrollspy — behavior hook for navigation that marks the active section
|
|
8301
8857
|
* while the page scrolls.
|
|
@@ -8421,6 +8977,9 @@ a.avatar[aria-disabled="true"] {
|
|
|
8421
8977
|
.list > :where(.list-item),
|
|
8422
8978
|
.list > li > .list-item {
|
|
8423
8979
|
display: flex;
|
|
8980
|
+
/* A trailing control that no longer fits beside the content's longest word
|
|
8981
|
+
moves to its own line instead of breaking that word. */
|
|
8982
|
+
flex-wrap: wrap;
|
|
8424
8983
|
/* Centered fits the short rows lists mostly hold. Read through
|
|
8425
8984
|
--items-align so .items-start on the row or the list wins without
|
|
8426
8985
|
outranking this selector — the opt-out for long supporting text. */
|
|
@@ -8467,17 +9026,29 @@ a.list-item:is([aria-current="page"], [aria-current="true"])::before {
|
|
|
8467
9026
|
border-inline-start: 2px solid var(--state-selected);
|
|
8468
9027
|
}
|
|
8469
9028
|
|
|
8470
|
-
/* The content region is the one that flexes, whichever position it holds.
|
|
9029
|
+
/* The content region is the one that flexes, whichever position it holds.
|
|
9030
|
+
Its automatic minimum is its longest word, which is what wraps the row; the
|
|
9031
|
+
max size caps that minimum at the row, so a long token breaks rather than
|
|
9032
|
+
widening the page. A zero minimum shrank it until words broke mid-way. */
|
|
8471
9033
|
.list-item-content {
|
|
8472
9034
|
flex: 1 1 0;
|
|
8473
|
-
|
|
9035
|
+
max-inline-size: 100%;
|
|
8474
9036
|
}
|
|
8475
9037
|
|
|
9038
|
+
/* break-word, not anywhere: anywhere also lowers the min-content to a single
|
|
9039
|
+
character, and the row would never wrap. */
|
|
8476
9040
|
.list-item-title,
|
|
8477
9041
|
.list-item-text {
|
|
8478
9042
|
display: block;
|
|
8479
9043
|
margin: 0;
|
|
8480
|
-
overflow-wrap:
|
|
9044
|
+
overflow-wrap: break-word;
|
|
9045
|
+
}
|
|
9046
|
+
|
|
9047
|
+
/* Truncated text gives up its width by design: without containment its
|
|
9048
|
+
unbreakable line would become the content's minimum and push the trailing
|
|
9049
|
+
region onto a line of its own. */
|
|
9050
|
+
.list-item-content .truncate {
|
|
9051
|
+
contain: inline-size;
|
|
8481
9052
|
}
|
|
8482
9053
|
|
|
8483
9054
|
.list-item-text {
|