actual-css 0.13.0 → 0.14.1
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 +67 -0
- package/README.md +8 -8
- package/components.json +116 -10
- package/dist/actual.full.css +652 -77
- 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 +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 +9 -3
- package/docs/pages/enhancements/overview.md +2 -1
- package/docs/pages/enhancements/tabs.md +7 -2
- package/docs/pages/enhancements/tooltip.md +3 -1
- package/docs/pages/examples/overview.md +5 -3
- 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 +5 -4
- 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 +163 -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/enhance.js +14 -0
- package/src/js/floating-space.js +26 -0
- package/src/js/flyout.js +3 -1
- package/src/js/focus.js +16 -1
- package/src/js/parse-config.js +5 -2
- package/src/js/surface.js +10 -0
- package/src/js/tooltip.js +2 -2
- 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,170 @@ 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
|
+
/* No in-flow text, so the baseline would be the box's bottom edge and an
|
|
3631
|
+
inline swatch would ride above the line (a flyout trigger beside its
|
|
3632
|
+
label). A palette in .cluster or .grid ignores it. */
|
|
3633
|
+
vertical-align: middle;
|
|
3634
|
+
border: 0;
|
|
3635
|
+
background: transparent;
|
|
3636
|
+
color: inherit;
|
|
3637
|
+
/* Consumed so .sm/.lg reach the em-sized swatch, as on .choice-card. */
|
|
3638
|
+
font: inherit;
|
|
3639
|
+
font-size: var(--control-font-size);
|
|
3640
|
+
cursor: pointer;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
.color-swatch::before {
|
|
3644
|
+
content: "";
|
|
3645
|
+
grid-area: 1 / 1;
|
|
3646
|
+
inline-size: var(--color-swatch-size);
|
|
3647
|
+
block-size: var(--color-swatch-size);
|
|
3648
|
+
box-sizing: border-box;
|
|
3649
|
+
border: var(--border-width) solid var(--border-control);
|
|
3650
|
+
border-radius: var(--color-swatch-radius);
|
|
3651
|
+
background: var(--color-swatch-value, transparent) padding-box;
|
|
3652
|
+
/* The color is the content, as on .color (native.css). */
|
|
3653
|
+
forced-color-adjust: none;
|
|
3654
|
+
transition:
|
|
3655
|
+
border-color var(--duration),
|
|
3656
|
+
box-shadow var(--duration);
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
/* Shares the swatch's cell and paints after ::before, so a native radio left
|
|
3660
|
+
visible sits on the color. */
|
|
3661
|
+
.color-swatch > input {
|
|
3662
|
+
grid-area: 1 / 1;
|
|
3663
|
+
position: relative;
|
|
3664
|
+
margin: 0;
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
/* Visually hidden but focusable, submitted and arrow-key navigable (sr-only
|
|
3668
|
+
pattern). Guarded: without :has() the swatch cannot mirror :checked, so the
|
|
3669
|
+
visible native radio is the fallback. */
|
|
3670
|
+
@supports selector(:has(*)) {
|
|
3671
|
+
.color-swatch > input {
|
|
3672
|
+
position: absolute;
|
|
3673
|
+
inline-size: 1px;
|
|
3674
|
+
block-size: 1px;
|
|
3675
|
+
padding: 0;
|
|
3676
|
+
margin: -1px;
|
|
3677
|
+
overflow: hidden;
|
|
3678
|
+
clip: rect(0, 0, 0, 0);
|
|
3679
|
+
white-space: nowrap;
|
|
3680
|
+
border: 0;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
.color-swatch:hover::before {
|
|
3685
|
+
border-color: var(--text-subtle);
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
.color-swatch:has(> input:checked)::before {
|
|
3689
|
+
box-shadow:
|
|
3690
|
+
inset 0 0 0 2px var(--text),
|
|
3691
|
+
inset 0 0 0 4px var(--surface-opaque);
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
/* The ring goes around the visible swatch, not the larger hit area. */
|
|
3695
|
+
.color-swatch:focus-visible {
|
|
3696
|
+
outline: none;
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
.color-swatch:focus-visible::before {
|
|
3700
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
3701
|
+
outline-offset: var(--focus-outline-offset);
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
.color-swatch:has(> input:focus-visible)::before {
|
|
3705
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
3706
|
+
outline-offset: var(--focus-outline-offset);
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
.color-swatch:disabled {
|
|
3710
|
+
opacity: var(--disabled-opacity);
|
|
3711
|
+
cursor: not-allowed;
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
.color-swatch:has(> input:disabled) {
|
|
3715
|
+
opacity: var(--disabled-opacity);
|
|
3716
|
+
cursor: not-allowed;
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
/* A disabled swatch keeps its color: --disabled-opacity is 1 here
|
|
3720
|
+
(tokens.css), and the edge carries the state. */
|
|
3721
|
+
@media (forced-colors: active) {
|
|
3722
|
+
.color-swatch::before {
|
|
3723
|
+
--focus: CanvasText;
|
|
3724
|
+
|
|
3725
|
+
border-color: CanvasText;
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
.color-swatch:has(> input:checked)::before {
|
|
3729
|
+
box-shadow:
|
|
3730
|
+
inset 0 0 0 2px CanvasText,
|
|
3731
|
+
inset 0 0 0 4px Canvas;
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
.color-swatch:disabled::before {
|
|
3735
|
+
border-color: GrayText;
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
.color-swatch:has(> input:disabled)::before {
|
|
3739
|
+
border-color: GrayText;
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3467
3743
|
/*
|
|
3468
3744
|
* Customizable select — appearance: base-select picker enhancement.
|
|
3469
3745
|
*
|
|
@@ -3958,12 +4234,27 @@ small {
|
|
|
3958
4234
|
--ui-hover-bg: transparent;
|
|
3959
4235
|
}
|
|
3960
4236
|
|
|
4237
|
+
/* A soft button takes the badge and alert tint, not the shared opaque .soft:
|
|
4238
|
+
it carries a short label like them, and the translucent fill keeps
|
|
4239
|
+
separating on a subtle or raised surface. Ink stays the shared .soft one. */
|
|
3961
4240
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
3962
4241
|
.btn.soft {
|
|
4242
|
+
/* @sync soft-tint */
|
|
4243
|
+
--ui-bg: color-mix(
|
|
4244
|
+
in oklab,
|
|
4245
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
|
|
4246
|
+
transparent
|
|
4247
|
+
);
|
|
4248
|
+
--ui-border: color-mix(
|
|
4249
|
+
in oklab,
|
|
4250
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
|
|
4251
|
+
transparent
|
|
4252
|
+
);
|
|
4253
|
+
/* @sync soft-tint */
|
|
3963
4254
|
--ui-hover-bg: color-mix(
|
|
3964
4255
|
in oklab,
|
|
3965
|
-
var(--
|
|
3966
|
-
|
|
4256
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix) + var(--soft-hover-alpha)),
|
|
4257
|
+
transparent
|
|
3967
4258
|
);
|
|
3968
4259
|
}
|
|
3969
4260
|
|
|
@@ -4162,11 +4453,14 @@ a.btn[aria-disabled="true"] {
|
|
|
4162
4453
|
}
|
|
4163
4454
|
|
|
4164
4455
|
@media (forced-colors: active) {
|
|
4165
|
-
/* The X is a
|
|
4166
|
-
|
|
4167
|
-
|
|
4456
|
+
/* The X is a mask fill: without this the system remaps the background to
|
|
4457
|
+
Canvas and the glyph disappears. Kept on the pseudo so the host keeps its
|
|
4458
|
+
native color and focus adaptation. The fill is the system color, not
|
|
4459
|
+
currentColor: an unadjusted pseudo inherits the authored ink, so a white
|
|
4460
|
+
.solid ink painted a white X on Canvas. */
|
|
4168
4461
|
.close::before {
|
|
4169
4462
|
forced-color-adjust: none;
|
|
4463
|
+
background: ButtonText;
|
|
4170
4464
|
}
|
|
4171
4465
|
}
|
|
4172
4466
|
|
|
@@ -4283,28 +4577,42 @@ a.btn[aria-disabled="true"] {
|
|
|
4283
4577
|
}
|
|
4284
4578
|
|
|
4285
4579
|
/* Soft by default. Local defaults remain behind --ui-* in the resolution
|
|
4286
|
-
chain, so explicit shared treatments compose without selector exclusions.
|
|
4580
|
+
chain, so explicit shared treatments compose without selector exclusions.
|
|
4581
|
+
The fill is a translucent intent tint, so it shows the surface it sits on
|
|
4582
|
+
instead of a fixed mix with --surface: a mix stops separating on a subtle
|
|
4583
|
+
or raised surface of its own lightness. No rim unless a theme sets
|
|
4584
|
+
--soft-border-mix (tokens.css). */
|
|
4287
4585
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
4288
4586
|
.badge {
|
|
4289
|
-
/* @sync soft-
|
|
4587
|
+
/* @sync soft-tint */
|
|
4290
4588
|
--badge-default-bg: color-mix(
|
|
4291
4589
|
in oklab,
|
|
4292
|
-
var(--
|
|
4293
|
-
|
|
4590
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
|
|
4591
|
+
transparent
|
|
4294
4592
|
);
|
|
4295
4593
|
--badge-default-border: color-mix(
|
|
4296
4594
|
in oklab,
|
|
4297
|
-
var(--
|
|
4298
|
-
|
|
4595
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
|
|
4596
|
+
transparent
|
|
4299
4597
|
);
|
|
4598
|
+
/* @sync soft-tint */
|
|
4599
|
+
/* @sync soft-ink */
|
|
4300
4600
|
--badge-default-fg: var(
|
|
4301
4601
|
--intent-soft-fg,
|
|
4302
4602
|
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
4303
4603
|
);
|
|
4304
|
-
/* @sync soft-
|
|
4604
|
+
/* @sync soft-ink */
|
|
4305
4605
|
}
|
|
4306
4606
|
}
|
|
4307
4607
|
|
|
4608
|
+
/* Soft is the badge's own default: an explicit .soft must not swap the tint
|
|
4609
|
+
for the shared opaque .soft recipe, so it defers to the default. */
|
|
4610
|
+
.badge.soft {
|
|
4611
|
+
--ui-bg: initial;
|
|
4612
|
+
--ui-fg: initial;
|
|
4613
|
+
--ui-border: initial;
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4308
4616
|
.badge:empty {
|
|
4309
4617
|
--badge-size: var(--badge-dot-size);
|
|
4310
4618
|
--badge-bg: var(--intent, var(--neutral));
|
|
@@ -4343,8 +4651,8 @@ a.btn[aria-disabled="true"] {
|
|
|
4343
4651
|
:is(a.badge, button.badge):where(:not(.solid, .outline, :empty)) {
|
|
4344
4652
|
--badge-default-hover-bg: color-mix(
|
|
4345
4653
|
in oklab,
|
|
4346
|
-
var(--
|
|
4347
|
-
|
|
4654
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix) + var(--soft-hover-alpha)),
|
|
4655
|
+
transparent
|
|
4348
4656
|
);
|
|
4349
4657
|
}
|
|
4350
4658
|
|
|
@@ -4462,6 +4770,16 @@ a.badge[aria-disabled="true"] {
|
|
|
4462
4770
|
font-size: var(--alert-font-size);
|
|
4463
4771
|
}
|
|
4464
4772
|
|
|
4773
|
+
/* Density steps the block padding only: the inline padding is the text
|
|
4774
|
+
column's edge and stays stable. Local to the alert, like .card.compact. */
|
|
4775
|
+
.alert.compact {
|
|
4776
|
+
--alert-pad-block: var(--space-20);
|
|
4777
|
+
}
|
|
4778
|
+
|
|
4779
|
+
.alert.spacious {
|
|
4780
|
+
--alert-pad-block: var(--space-40);
|
|
4781
|
+
}
|
|
4782
|
+
|
|
4465
4783
|
.alert > * {
|
|
4466
4784
|
min-inline-size: 0;
|
|
4467
4785
|
}
|
|
@@ -4486,28 +4804,40 @@ a.badge[aria-disabled="true"] {
|
|
|
4486
4804
|
}
|
|
4487
4805
|
|
|
4488
4806
|
/* Alerts are soft by default. The recipe writes local defaults rather than
|
|
4489
|
-
--ui-* so an explicit shared treatment always wins without exclusions.
|
|
4807
|
+
--ui-* so an explicit shared treatment always wins without exclusions. The
|
|
4808
|
+
fill is the translucent soft tint (see badge.css), with no rim unless a
|
|
4809
|
+
theme sets --soft-border-mix. */
|
|
4490
4810
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
4491
4811
|
.alert {
|
|
4492
|
-
/* @sync soft-
|
|
4812
|
+
/* @sync soft-tint */
|
|
4493
4813
|
--alert-default-bg: color-mix(
|
|
4494
4814
|
in oklab,
|
|
4495
|
-
var(--
|
|
4496
|
-
|
|
4815
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-bg-mix)),
|
|
4816
|
+
transparent
|
|
4497
4817
|
);
|
|
4498
4818
|
--alert-default-border: color-mix(
|
|
4499
4819
|
in oklab,
|
|
4500
|
-
var(--
|
|
4501
|
-
|
|
4820
|
+
var(--intent, var(--neutral)) calc(100% - var(--soft-border-mix, 100%)),
|
|
4821
|
+
transparent
|
|
4502
4822
|
);
|
|
4823
|
+
/* @sync soft-tint */
|
|
4824
|
+
/* @sync soft-ink */
|
|
4503
4825
|
--alert-default-fg: var(
|
|
4504
4826
|
--intent-soft-fg,
|
|
4505
4827
|
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
4506
4828
|
);
|
|
4507
|
-
/* @sync soft-
|
|
4829
|
+
/* @sync soft-ink */
|
|
4508
4830
|
}
|
|
4509
4831
|
}
|
|
4510
4832
|
|
|
4833
|
+
/* Soft is the alert's own default: an explicit .soft must not swap the tint
|
|
4834
|
+
for the shared opaque .soft recipe, so it defers to the default. */
|
|
4835
|
+
.alert.soft {
|
|
4836
|
+
--ui-bg: initial;
|
|
4837
|
+
--ui-fg: initial;
|
|
4838
|
+
--ui-border: initial;
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4511
4841
|
/* Decorative leading icon — use .alert-icon on the icon element. display and
|
|
4512
4842
|
vertical-align are the no-:has() layout (Degraded tier: the icon sits inline
|
|
4513
4843
|
before the text); the grid anatomy below replaces them. */
|
|
@@ -4606,7 +4936,10 @@ a.badge[aria-disabled="true"] {
|
|
|
4606
4936
|
|
|
4607
4937
|
/* Admonition — structured static box with a tinted title bar and a body
|
|
4608
4938
|
that rests on the page surface. Not collapsible — use .accordion inside
|
|
4609
|
-
the body if the content needs to be expandable.
|
|
4939
|
+
the body if the content needs to be expandable. The title bar is what a
|
|
4940
|
+
shared treatment paints (.solid fills it, .surface/.outline clear it): the
|
|
4941
|
+
root's fill never shows behind the opaque title and body, and the title ink
|
|
4942
|
+
already follows --ui-fg, so a bar left on the soft tint is unreadable. */
|
|
4610
4943
|
.alert.admonition {
|
|
4611
4944
|
padding: 0;
|
|
4612
4945
|
display: block;
|
|
@@ -4620,6 +4953,7 @@ a.badge[aria-disabled="true"] {
|
|
|
4620
4953
|
gap: var(--space-20);
|
|
4621
4954
|
padding-block: var(--alert-pad-block);
|
|
4622
4955
|
padding-inline: var(--alert-pad-inline);
|
|
4956
|
+
font-weight: var(--font-weight-semibold);
|
|
4623
4957
|
border-start-start-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
|
|
4624
4958
|
border-start-end-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
|
|
4625
4959
|
}
|
|
@@ -4631,18 +4965,17 @@ a.badge[aria-disabled="true"] {
|
|
|
4631
4965
|
|
|
4632
4966
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
4633
4967
|
.alert.admonition > .alert-title {
|
|
4634
|
-
background:
|
|
4635
|
-
|
|
4636
|
-
var(--surface) var(--soft-bg-mix),
|
|
4637
|
-
var(--intent, var(--neutral))
|
|
4968
|
+
background: var(
|
|
4969
|
+
--ui-bg,
|
|
4970
|
+
color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)))
|
|
4638
4971
|
);
|
|
4639
4972
|
}
|
|
4640
4973
|
}
|
|
4641
4974
|
|
|
4642
4975
|
@supports not (color: color-mix(in oklch, red, white)) {
|
|
4643
4976
|
.alert.admonition > .alert-title {
|
|
4644
|
-
background: var(--intent, var(--neutral));
|
|
4645
|
-
color: var(--intent-fg, var(--neutral-fg));
|
|
4977
|
+
background: var(--ui-bg, var(--intent, var(--neutral)));
|
|
4978
|
+
color: var(--ui-fg, var(--intent-fg, var(--neutral-fg)));
|
|
4646
4979
|
}
|
|
4647
4980
|
}
|
|
4648
4981
|
|
|
@@ -4653,6 +4986,32 @@ a.badge[aria-disabled="true"] {
|
|
|
4653
4986
|
color: var(--text);
|
|
4654
4987
|
}
|
|
4655
4988
|
|
|
4989
|
+
/* Solid: the filled title bar already carries the intent, so the frame moves
|
|
4990
|
+
to the body and stays neutral. The root border goes because a transparent
|
|
4991
|
+
one would show the root's intent fill around the body. */
|
|
4992
|
+
.alert.admonition.solid {
|
|
4993
|
+
border: 0;
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4996
|
+
.alert.admonition.solid > .alert-body {
|
|
4997
|
+
border: var(--border-width) solid var(--border);
|
|
4998
|
+
border-block-start: 0;
|
|
4999
|
+
border-end-start-radius: var(--alert-radius, var(--radius-lg));
|
|
5000
|
+
border-end-end-radius: var(--alert-radius, var(--radius-lg));
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
/* Forced colors drop the title fill, so the bar would lose its edge: the
|
|
5004
|
+
frame returns to the root as one uniform system-colored border. */
|
|
5005
|
+
@media (forced-colors: active) {
|
|
5006
|
+
.alert.admonition.solid {
|
|
5007
|
+
border: var(--border-width) solid CanvasText;
|
|
5008
|
+
}
|
|
5009
|
+
|
|
5010
|
+
.alert.admonition.solid > .alert-body {
|
|
5011
|
+
border: 0;
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
|
|
4656
5015
|
.alert.admonition > .alert-body > :first-child {
|
|
4657
5016
|
margin-block-start: 0;
|
|
4658
5017
|
}
|
|
@@ -7362,11 +7721,14 @@ a.avatar[aria-disabled="true"] {
|
|
|
7362
7721
|
|
|
7363
7722
|
/* Complete owns the accented connector that reaches the current step, so the
|
|
7364
7723
|
connector leaving current goes back to neutral. Declared after
|
|
7365
|
-
.step-complete at equal specificity: current wins on every channel.
|
|
7724
|
+
.step-complete at equal specificity: current wins on every channel. The
|
|
7725
|
+
current marker is a regular surface, so its number and ring are selected
|
|
7726
|
+
ink on a surface, not a selected fill; they are held to different ratios
|
|
7727
|
+
(4.5:1 for the number, 3:1 for the ring) by report:theme-contrast. */
|
|
7366
7728
|
.steps > [aria-current="step"] {
|
|
7367
7729
|
--step-marker-bg: var(--surface-opaque);
|
|
7368
|
-
--step-marker-border: var(--state-selected);
|
|
7369
|
-
--step-marker-fg: var(--state-selected);
|
|
7730
|
+
--step-marker-border: var(--state-selected-text, var(--state-selected));
|
|
7731
|
+
--step-marker-fg: var(--state-selected-text, var(--state-selected));
|
|
7370
7732
|
--step-line: var(--border);
|
|
7371
7733
|
|
|
7372
7734
|
color: var(--text);
|
|
@@ -7667,6 +8029,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7667
8029
|
}
|
|
7668
8030
|
|
|
7669
8031
|
.app-nav > a {
|
|
8032
|
+
position: relative;
|
|
7670
8033
|
display: grid;
|
|
7671
8034
|
grid-template-rows: auto auto;
|
|
7672
8035
|
place-content: center;
|
|
@@ -7707,20 +8070,27 @@ a.avatar[aria-disabled="true"] {
|
|
|
7707
8070
|
}
|
|
7708
8071
|
|
|
7709
8072
|
/* Current tile is already a full selected surface. Selection never changes
|
|
7710
|
-
text metrics: the shared medium weight is kept
|
|
7711
|
-
|
|
8073
|
+
text metrics: the shared medium weight is kept. The forced-colors cue is a
|
|
8074
|
+
transparent border on a ::before overlay, not on the tile: an element border
|
|
8075
|
+
shrinks the content box, and the side layout starts its content at the inline
|
|
8076
|
+
edge, so the icon and label shifted by --border-width when a tile became
|
|
8077
|
+
current. The overlay costs no layout, yet forced colors still turn its
|
|
8078
|
+
transparent border into a system-color frame once --state-selected flattens
|
|
8079
|
+
to the bar's own Canvas. No forced-colors repaint: a Highlight fill put
|
|
8080
|
+
HighlightText on Chromium's Canvas backplate, which hid the label. The
|
|
8081
|
+
positioning context is on every link so it does not move when one becomes
|
|
8082
|
+
current or an application places its own decoration. */
|
|
7712
8083
|
.app-nav > a[aria-current="page"] {
|
|
7713
8084
|
background: var(--state-selected);
|
|
7714
8085
|
color: var(--state-selected-fg);
|
|
7715
8086
|
}
|
|
7716
8087
|
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
}
|
|
8088
|
+
.app-nav > a[aria-current="page"]::before {
|
|
8089
|
+
content: "";
|
|
8090
|
+
position: absolute;
|
|
8091
|
+
inset: 0;
|
|
8092
|
+
border: var(--border-width) solid transparent;
|
|
8093
|
+
border-radius: inherit;
|
|
7724
8094
|
}
|
|
7725
8095
|
|
|
7726
8096
|
/*
|
|
@@ -7732,6 +8102,9 @@ a.avatar[aria-disabled="true"] {
|
|
|
7732
8102
|
* links are a .cluster using .nav-link items. .nav-list is the vertical nav
|
|
7733
8103
|
* list shared with .drawer, fed by the shared --gap token.
|
|
7734
8104
|
*
|
|
8105
|
+
* Public hooks:
|
|
8106
|
+
* --state-selected-text (fallback-only: current-link ink, see tokens.css)
|
|
8107
|
+
*
|
|
7735
8108
|
* Internal:
|
|
7736
8109
|
* --gap (local value for the shared gap token)
|
|
7737
8110
|
*/
|
|
@@ -7809,7 +8182,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7809
8182
|
shared with the hover rule above. */
|
|
7810
8183
|
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
7811
8184
|
background: var(--surface-subtle);
|
|
7812
|
-
color: var(--state-selected);
|
|
8185
|
+
color: var(--state-selected-text, var(--state-selected));
|
|
7813
8186
|
}
|
|
7814
8187
|
|
|
7815
8188
|
/* On a bar with its own --ui-bg/--ui-fg the fill is a tint of the link's own
|
|
@@ -7830,7 +8203,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
7830
8203
|
|
|
7831
8204
|
/* After :hover so a hovered current link keeps the selected accent. */
|
|
7832
8205
|
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
7833
|
-
color: var(--ui-fg, var(--state-selected));
|
|
8206
|
+
color: var(--ui-fg, var(--state-selected-text, var(--state-selected)));
|
|
7834
8207
|
}
|
|
7835
8208
|
}
|
|
7836
8209
|
|
|
@@ -8265,7 +8638,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
8265
8638
|
stays inert. */
|
|
8266
8639
|
.tab[aria-selected="true"],
|
|
8267
8640
|
.tab:is([aria-current="page"], [aria-current="true"]) {
|
|
8268
|
-
color: var(--intent, var(--state-selected));
|
|
8641
|
+
color: var(--intent, var(--state-selected-text, var(--state-selected)));
|
|
8269
8642
|
}
|
|
8270
8643
|
|
|
8271
8644
|
/* The selected indicator lives on a positioned pseudo so that only the active
|
|
@@ -8296,6 +8669,193 @@ a.avatar[aria-disabled="true"] {
|
|
|
8296
8669
|
inline-size: 100%;
|
|
8297
8670
|
}
|
|
8298
8671
|
|
|
8672
|
+
/*
|
|
8673
|
+
* Segmented control — one compact choice among a few, on a shared track.
|
|
8674
|
+
*
|
|
8675
|
+
* Docs: docs/pages/components/segmented.md
|
|
8676
|
+
*
|
|
8677
|
+
* .segmented is the track, each .segment a direct child. The look is shared;
|
|
8678
|
+
* the semantics stay with the markup, and the selected state is read from it:
|
|
8679
|
+
* radios <label class="segment"><input type="radio"> :checked
|
|
8680
|
+
* tabs <button class="segment" role="tab"> aria-selected="true"
|
|
8681
|
+
* links <a class="segment"> aria-current="page"
|
|
8682
|
+
* No script of its own: radios bring their arrow keys, a tablist takes
|
|
8683
|
+
* data-enhance="tabs". Without :has() the radio anatomy loses its selected
|
|
8684
|
+
* fill and focus ring, and the native radio stays visible to carry both.
|
|
8685
|
+
*
|
|
8686
|
+
* One row of equal shares (grid-auto-columns: 1fr), at least --control-size
|
|
8687
|
+
* tall so it lines up with .btn and .input. Labels wrap instead of truncating;
|
|
8688
|
+
* a word longer than its share breaks, so the control never overflows its
|
|
8689
|
+
* container. A strip too long for one row is .tabs, which scrolls.
|
|
8690
|
+
*
|
|
8691
|
+
* Focus is drawn inside the segment, unlike other discrete actions
|
|
8692
|
+
* (focus.css): an offset ring would cross the track's padding and cover the
|
|
8693
|
+
* neighbouring segment.
|
|
8694
|
+
*
|
|
8695
|
+
* Public hooks:
|
|
8696
|
+
* --segmented-bg
|
|
8697
|
+
* --segmented-pad
|
|
8698
|
+
* --segmented-radius
|
|
8699
|
+
* --segmented-fg
|
|
8700
|
+
* --segmented-selected-bg
|
|
8701
|
+
* --segmented-selected-border
|
|
8702
|
+
*/
|
|
8703
|
+
.segmented {
|
|
8704
|
+
--segmented-bg: var(--surface-subtle);
|
|
8705
|
+
--segmented-pad: var(--space-10);
|
|
8706
|
+
--segmented-radius: var(--radius);
|
|
8707
|
+
--segmented-fg: var(--text-muted);
|
|
8708
|
+
--segmented-selected-bg: var(--surface-raised);
|
|
8709
|
+
/* The edge is what separates the selected segment where --surface-raised
|
|
8710
|
+
is darker than --surface-subtle (the default dark palette). */
|
|
8711
|
+
--segmented-selected-border: var(--border);
|
|
8712
|
+
|
|
8713
|
+
display: inline-grid;
|
|
8714
|
+
grid-auto-flow: column;
|
|
8715
|
+
grid-auto-columns: 1fr;
|
|
8716
|
+
gap: var(--segmented-pad);
|
|
8717
|
+
/* why: margin and border also reset <fieldset>, the radio anatomy's
|
|
8718
|
+
element. The border is transparent, not 0: forced colors paint it, which
|
|
8719
|
+
outlines the track once its fill is gone. */
|
|
8720
|
+
margin: 0;
|
|
8721
|
+
padding: var(--segmented-pad);
|
|
8722
|
+
border: var(--border-width) solid transparent;
|
|
8723
|
+
border-radius: var(--segmented-radius);
|
|
8724
|
+
background: var(--segmented-bg);
|
|
8725
|
+
font-size: var(--control-font-size);
|
|
8726
|
+
vertical-align: middle;
|
|
8727
|
+
}
|
|
8728
|
+
|
|
8729
|
+
.segment {
|
|
8730
|
+
position: relative;
|
|
8731
|
+
display: flex;
|
|
8732
|
+
align-items: center;
|
|
8733
|
+
justify-content: center;
|
|
8734
|
+
gap: 0.375em;
|
|
8735
|
+
/* The track is --control-size tall through its segments, not through its
|
|
8736
|
+
own min-block-size: a <fieldset> lays its content in an anonymous box
|
|
8737
|
+
sized to the content, so a minimum on the track left the segments short.
|
|
8738
|
+
A minimum, not a height: a wrapped label grows the whole row. */
|
|
8739
|
+
min-block-size: calc(var(--control-size) - 2 * (var(--segmented-pad) + var(--border-width)));
|
|
8740
|
+
padding: calc(var(--space-10) / 2) var(--space-30);
|
|
8741
|
+
border: 0;
|
|
8742
|
+
border-radius: calc(var(--segmented-radius) - var(--segmented-pad));
|
|
8743
|
+
background: transparent;
|
|
8744
|
+
color: var(--segmented-fg);
|
|
8745
|
+
font: inherit;
|
|
8746
|
+
/* Constant across states: a heavier selected label would resize its share. */
|
|
8747
|
+
font-weight: var(--font-weight-medium);
|
|
8748
|
+
line-height: 1.2;
|
|
8749
|
+
text-align: center;
|
|
8750
|
+
text-decoration: none;
|
|
8751
|
+
/* why: also shrinks the min-content contribution, so 1fr keeps equal
|
|
8752
|
+
shares under constraint instead of widening one around a long word. */
|
|
8753
|
+
overflow-wrap: anywhere;
|
|
8754
|
+
cursor: pointer;
|
|
8755
|
+
transition:
|
|
8756
|
+
background-color var(--duration),
|
|
8757
|
+
color var(--duration),
|
|
8758
|
+
box-shadow var(--duration);
|
|
8759
|
+
}
|
|
8760
|
+
|
|
8761
|
+
.segment > input {
|
|
8762
|
+
margin: 0;
|
|
8763
|
+
}
|
|
8764
|
+
|
|
8765
|
+
/* The radio is visually hidden but stays focusable, submitted and arrow-key
|
|
8766
|
+
navigable (sr-only pattern). Guarded: without :has() the segment cannot
|
|
8767
|
+
mirror :checked, so the visible native radio is the fallback. */
|
|
8768
|
+
@supports selector(:has(*)) {
|
|
8769
|
+
.segment > input {
|
|
8770
|
+
position: absolute;
|
|
8771
|
+
inline-size: 1px;
|
|
8772
|
+
block-size: 1px;
|
|
8773
|
+
padding: 0;
|
|
8774
|
+
margin: -1px;
|
|
8775
|
+
overflow: hidden;
|
|
8776
|
+
clip: rect(0, 0, 0, 0);
|
|
8777
|
+
white-space: nowrap;
|
|
8778
|
+
border: 0;
|
|
8779
|
+
}
|
|
8780
|
+
}
|
|
8781
|
+
|
|
8782
|
+
.segment:hover {
|
|
8783
|
+
color: var(--text);
|
|
8784
|
+
}
|
|
8785
|
+
|
|
8786
|
+
/* Separate rules from the :has() forms below: an engine without :has() would
|
|
8787
|
+
discard a shared selector list, ARIA states included. */
|
|
8788
|
+
.segment:is([aria-selected="true"], [aria-current="page"], [aria-current="true"]) {
|
|
8789
|
+
background: var(--segmented-selected-bg);
|
|
8790
|
+
color: var(--text);
|
|
8791
|
+
box-shadow:
|
|
8792
|
+
0 0 0 var(--border-width) var(--segmented-selected-border),
|
|
8793
|
+
var(--shadow);
|
|
8794
|
+
}
|
|
8795
|
+
|
|
8796
|
+
.segment:has(> input:checked) {
|
|
8797
|
+
background: var(--segmented-selected-bg);
|
|
8798
|
+
color: var(--text);
|
|
8799
|
+
box-shadow:
|
|
8800
|
+
0 0 0 var(--border-width) var(--segmented-selected-border),
|
|
8801
|
+
var(--shadow);
|
|
8802
|
+
}
|
|
8803
|
+
|
|
8804
|
+
.segment:focus-visible {
|
|
8805
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
8806
|
+
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
8807
|
+
}
|
|
8808
|
+
|
|
8809
|
+
.segment:has(> input:focus-visible) {
|
|
8810
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
8811
|
+
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
8812
|
+
}
|
|
8813
|
+
|
|
8814
|
+
.segment:is(:disabled, [aria-disabled="true"]) {
|
|
8815
|
+
opacity: var(--disabled-opacity);
|
|
8816
|
+
cursor: not-allowed;
|
|
8817
|
+
}
|
|
8818
|
+
|
|
8819
|
+
.segment:has(> input:disabled) {
|
|
8820
|
+
opacity: var(--disabled-opacity);
|
|
8821
|
+
cursor: not-allowed;
|
|
8822
|
+
}
|
|
8823
|
+
|
|
8824
|
+
/* Forced colors drop the raised fill and its edge. The selected segment
|
|
8825
|
+
takes the system highlight — a background, so the outline stays the focus
|
|
8826
|
+
channel alone. forced-color-adjust: none is required, not cosmetic: the UA
|
|
8827
|
+
otherwise paints a Canvas backplate behind the label, which hid the text on
|
|
8828
|
+
the highlight. Opting out also stops the forcing of the rest, so the shadow
|
|
8829
|
+
is dropped here and the focus line takes the highlight's text color. */
|
|
8830
|
+
@media (forced-colors: active) {
|
|
8831
|
+
.segment:is([aria-selected="true"], [aria-current="page"], [aria-current="true"]) {
|
|
8832
|
+
--focus: HighlightText;
|
|
8833
|
+
|
|
8834
|
+
forced-color-adjust: none;
|
|
8835
|
+
background: Highlight;
|
|
8836
|
+
color: HighlightText;
|
|
8837
|
+
box-shadow: none;
|
|
8838
|
+
}
|
|
8839
|
+
|
|
8840
|
+
.segment:has(> input:checked) {
|
|
8841
|
+
--focus: HighlightText;
|
|
8842
|
+
|
|
8843
|
+
forced-color-adjust: none;
|
|
8844
|
+
background: Highlight;
|
|
8845
|
+
color: HighlightText;
|
|
8846
|
+
box-shadow: none;
|
|
8847
|
+
}
|
|
8848
|
+
|
|
8849
|
+
/* --disabled-opacity is 1 here (tokens.css): the dimming needs a color. */
|
|
8850
|
+
.segment:is(:disabled, [aria-disabled="true"]) {
|
|
8851
|
+
color: GrayText;
|
|
8852
|
+
}
|
|
8853
|
+
|
|
8854
|
+
.segment:has(> input:disabled) {
|
|
8855
|
+
color: GrayText;
|
|
8856
|
+
}
|
|
8857
|
+
}
|
|
8858
|
+
|
|
8299
8859
|
/*
|
|
8300
8860
|
* Scrollspy — behavior hook for navigation that marks the active section
|
|
8301
8861
|
* while the page scrolls.
|
|
@@ -8421,6 +8981,9 @@ a.avatar[aria-disabled="true"] {
|
|
|
8421
8981
|
.list > :where(.list-item),
|
|
8422
8982
|
.list > li > .list-item {
|
|
8423
8983
|
display: flex;
|
|
8984
|
+
/* A trailing control that no longer fits beside the content's longest word
|
|
8985
|
+
moves to its own line instead of breaking that word. */
|
|
8986
|
+
flex-wrap: wrap;
|
|
8424
8987
|
/* Centered fits the short rows lists mostly hold. Read through
|
|
8425
8988
|
--items-align so .items-start on the row or the list wins without
|
|
8426
8989
|
outranking this selector — the opt-out for long supporting text. */
|
|
@@ -8467,17 +9030,29 @@ a.list-item:is([aria-current="page"], [aria-current="true"])::before {
|
|
|
8467
9030
|
border-inline-start: 2px solid var(--state-selected);
|
|
8468
9031
|
}
|
|
8469
9032
|
|
|
8470
|
-
/* The content region is the one that flexes, whichever position it holds.
|
|
9033
|
+
/* The content region is the one that flexes, whichever position it holds.
|
|
9034
|
+
Its automatic minimum is its longest word, which is what wraps the row; the
|
|
9035
|
+
max size caps that minimum at the row, so a long token breaks rather than
|
|
9036
|
+
widening the page. A zero minimum shrank it until words broke mid-way. */
|
|
8471
9037
|
.list-item-content {
|
|
8472
9038
|
flex: 1 1 0;
|
|
8473
|
-
|
|
9039
|
+
max-inline-size: 100%;
|
|
8474
9040
|
}
|
|
8475
9041
|
|
|
9042
|
+
/* break-word, not anywhere: anywhere also lowers the min-content to a single
|
|
9043
|
+
character, and the row would never wrap. */
|
|
8476
9044
|
.list-item-title,
|
|
8477
9045
|
.list-item-text {
|
|
8478
9046
|
display: block;
|
|
8479
9047
|
margin: 0;
|
|
8480
|
-
overflow-wrap:
|
|
9048
|
+
overflow-wrap: break-word;
|
|
9049
|
+
}
|
|
9050
|
+
|
|
9051
|
+
/* Truncated text gives up its width by design: without containment its
|
|
9052
|
+
unbreakable line would become the content's minimum and push the trailing
|
|
9053
|
+
region onto a line of its own. */
|
|
9054
|
+
.list-item-content .truncate {
|
|
9055
|
+
contain: inline-size;
|
|
8481
9056
|
}
|
|
8482
9057
|
|
|
8483
9058
|
.list-item-text {
|