actual-css 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/README.md +7 -8
- package/components.json +124 -12
- package/dist/actual.full.css +1152 -309
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/design-notes/architecture.md +67 -0
- package/docs/design-notes/steps.md +45 -28
- package/docs/navigation.json +2 -0
- package/docs/pages/components/alert.md +12 -2
- package/docs/pages/components/avatar.md +17 -11
- package/docs/pages/components/breadcrumb.md +16 -2
- package/docs/pages/components/button.md +3 -2
- package/docs/pages/components/list.md +15 -16
- package/docs/pages/components/navbar.md +42 -22
- package/docs/pages/components/pagination.md +19 -0
- package/docs/pages/components/segmented.md +83 -0
- package/docs/pages/components/steps.md +28 -22
- package/docs/pages/enhancements/flyout.md +5 -0
- package/docs/pages/enhancements/scrollspy.md +6 -6
- package/docs/pages/enhancements/tabs.md +79 -3
- package/docs/pages/examples/overview.md +3 -1
- package/docs/pages/forms/color-swatches.md +131 -0
- package/docs/pages/forms/combobox.md +2 -2
- package/docs/pages/forms/controls.md +6 -3
- package/docs/pages/forms/floating-field.md +21 -7
- package/docs/pages/forms/form-actions.md +3 -6
- package/docs/pages/forms/joined-controls.md +6 -2
- package/docs/pages/forms/overview.md +1 -0
- package/docs/pages/forms/states-and-sizes.md +15 -15
- package/docs/pages/forms/switch.md +4 -2
- package/docs/pages/foundations/philosophy.md +97 -1
- package/docs/pages/foundations/theming.md +1 -1
- package/docs/pages/foundations/tokens.md +94 -27
- package/docs/pages/guides/design-tools.md +14 -8
- package/docs/pages/guides/integrating-actual-css.md +74 -8
- package/docs/pages/guides/introduction.md +3 -2
- package/docs/pages/guides/modular-import.md +1 -1
- package/docs/pages/guides/progressive-enhancement.md +25 -7
- package/docs/pages/guides/upgrading.md +86 -9
- package/docs/pages/layout/center.md +2 -1
- package/docs/pages/layout/cluster.md +11 -4
- package/docs/pages/layout/grid.md +2 -0
- package/docs/pages/layout/scroll-snap.md +4 -2
- package/docs/pages/layout/sidebar-layout.md +3 -0
- package/docs/pages/layout/switcher.md +2 -0
- package/docs/pages/layout/topbar.md +6 -4
- package/docs/pages/patterns/icon-slot.md +17 -1
- package/docs/pages/utilities/flex.md +1 -1
- package/docs/pages/utilities/sizing-wrapping.md +51 -6
- package/package.json +6 -6
- package/reserved-classes.json +3 -0
- package/src/cli/actual-css.js +4 -4
- package/src/css/components/alert.css +99 -48
- package/src/css/components/app-nav.css +17 -9
- package/src/css/components/avatar.css +2 -18
- package/src/css/components/badge.css +23 -10
- package/src/css/components/breadcrumb.css +74 -8
- package/src/css/components/button.css +17 -2
- package/src/css/components/close.css +6 -3
- package/src/css/components/drawer.css +23 -42
- package/src/css/components/flyout.css +0 -1
- package/src/css/components/index.css +1 -0
- package/src/css/components/join.css +2 -2
- package/src/css/components/list.css +28 -13
- package/src/css/components/modal.css +17 -10
- package/src/css/components/navbar.css +15 -10
- package/src/css/components/pagination.css +5 -0
- package/src/css/components/scrollspy.css +1 -1
- package/src/css/components/segmented.css +186 -0
- package/src/css/components/steps.css +72 -15
- package/src/css/components/tab.css +135 -5
- package/src/css/core/focus.css +11 -6
- package/src/css/core/theme.css +6 -1
- package/src/css/core/tokens.css +24 -6
- package/src/css/core/variants.css +16 -5
- package/src/css/forms/choice-card.css +1 -1
- package/src/css/forms/choice.css +1 -1
- package/src/css/forms/color-swatch.css +159 -0
- package/src/css/forms/control.css +1 -1
- package/src/css/forms/floating-field.css +20 -18
- package/src/css/forms/form-actions.css +14 -8
- package/src/css/forms/index.css +1 -0
- package/src/css/forms/native.css +51 -21
- package/src/css/forms/otp.css +4 -5
- package/src/css/forms/switch.css +6 -3
- package/src/css/forms/validation.css +23 -30
- package/src/css/layout/center.css +2 -0
- package/src/css/layout/cluster.css +14 -0
- package/src/css/layout/grid.css +7 -0
- package/src/css/layout/scroll-snap.css +30 -0
- package/src/css/layout/scroller.css +19 -3
- package/src/css/layout/sidebar-layout.css +3 -0
- package/src/css/layout/switcher.css +3 -0
- package/src/css/layout/topbar.css +10 -0
- package/src/css/utilities/extra.css +3 -3
- package/src/js/focus.js +16 -1
- package/src/js/full.js +4 -0
- package/src/js/inline-visible.js +54 -0
- package/src/js/parsed.js +21 -0
- package/src/js/surface.js +10 -0
- package/src/js/tab.js +33 -7
- package/src/tooling/color.js +14 -1
- package/src/tooling/css-bundle.js +0 -5
- package/src/tooling/design-components.js +9 -5
- package/src/tooling/design-export.js +9 -1
- package/src/tooling/design-tokens.js +207 -76
- package/src/tooling/theme-contrast.js +182 -45
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
.join-addon {
|
|
43
|
-
--control-border: var(--border);
|
|
43
|
+
--control-border: var(--border-control);
|
|
44
44
|
--control-bg: var(--surface-subtle);
|
|
45
45
|
--control-fg: var(--text-muted);
|
|
46
46
|
|
|
@@ -145,5 +145,5 @@
|
|
|
145
145
|
The second :has() covers a control nested one wrapper level deep. */
|
|
146
146
|
.join:has(> :is(.input, .textarea, .select)) > .btn.outline,
|
|
147
147
|
.join:has(> * > :is(.input, .textarea, .select)) > .btn.outline {
|
|
148
|
-
--btn-border-color: var(--intent, var(--border));
|
|
148
|
+
--btn-border-color: var(--intent, var(--border-control));
|
|
149
149
|
}
|
|
@@ -43,9 +43,13 @@
|
|
|
43
43
|
.list > :where(.list-item),
|
|
44
44
|
.list > li > .list-item {
|
|
45
45
|
display: flex;
|
|
46
|
-
/*
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
/* A trailing control that no longer fits beside the content's longest word
|
|
47
|
+
moves to its own line instead of breaking that word. */
|
|
48
|
+
flex-wrap: wrap;
|
|
49
|
+
/* Centered fits the short rows lists mostly hold. Read through
|
|
50
|
+
--items-align so .items-start on the row or the list wins without
|
|
51
|
+
outranking this selector — the opt-out for long supporting text. */
|
|
52
|
+
align-items: var(--items-align, center);
|
|
49
53
|
gap: var(--list-item-gap);
|
|
50
54
|
min-block-size: var(--list-item-min-size);
|
|
51
55
|
padding-block: var(--list-item-pad-block);
|
|
@@ -67,21 +71,20 @@ a.list-item:hover {
|
|
|
67
71
|
background: var(--surface-subtle);
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
/* Current row in a navigational list
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
…) stays free for assistive technology.
|
|
74
|
+
/* Current row in a navigational list: aria-current="page" when the row opens
|
|
75
|
+
a page, "true" when it is the current item of a master/detail view (a
|
|
76
|
+
conversation, a thread, a record) that is no page of its own. Any other
|
|
77
|
+
value, "false" included, is inert.
|
|
75
78
|
Calm by design: the row keeps its normal text metrics and color on a subtle
|
|
76
79
|
surface, and the selected accent lives on an inset inline-start trait only.
|
|
77
80
|
The trait is a real border, so it survives forced colors without a repaint
|
|
78
81
|
block. The positioning context lives on every row link, so becoming current
|
|
79
82
|
never moves an app-positioned decoration to a new containing block. */
|
|
80
|
-
a.list-item[aria-current] {
|
|
83
|
+
a.list-item:is([aria-current="page"], [aria-current="true"]) {
|
|
81
84
|
background: var(--surface-subtle);
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
a.list-item[aria-current]::before {
|
|
87
|
+
a.list-item:is([aria-current="page"], [aria-current="true"])::before {
|
|
85
88
|
content: "";
|
|
86
89
|
position: absolute;
|
|
87
90
|
inset-block: var(--list-item-pad-block);
|
|
@@ -89,17 +92,29 @@ a.list-item[aria-current]::before {
|
|
|
89
92
|
border-inline-start: 2px solid var(--state-selected);
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
/* The content region is the one that flexes, whichever position it holds.
|
|
95
|
+
/* The content region is the one that flexes, whichever position it holds.
|
|
96
|
+
Its automatic minimum is its longest word, which is what wraps the row; the
|
|
97
|
+
max size caps that minimum at the row, so a long token breaks rather than
|
|
98
|
+
widening the page. A zero minimum shrank it until words broke mid-way. */
|
|
93
99
|
.list-item-content {
|
|
94
100
|
flex: 1 1 0;
|
|
95
|
-
|
|
101
|
+
max-inline-size: 100%;
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
/* break-word, not anywhere: anywhere also lowers the min-content to a single
|
|
105
|
+
character, and the row would never wrap. */
|
|
98
106
|
.list-item-title,
|
|
99
107
|
.list-item-text {
|
|
100
108
|
display: block;
|
|
101
109
|
margin: 0;
|
|
102
|
-
overflow-wrap:
|
|
110
|
+
overflow-wrap: break-word;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Truncated text gives up its width by design: without containment its
|
|
114
|
+
unbreakable line would become the content's minimum and push the trailing
|
|
115
|
+
region onto a line of its own. */
|
|
116
|
+
.list-item-content .truncate {
|
|
117
|
+
contain: inline-size;
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
.list-item-text {
|
|
@@ -131,6 +131,12 @@
|
|
|
131
131
|
background: var(--surface-subtle);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/* The chrome header is a centered row, so prose margins on a bare title only
|
|
135
|
+
push the band taller and pull the title off the close axis. */
|
|
136
|
+
:where(dialog).modal > header > :is(h1, h2, h3, h4, h5, h6) {
|
|
137
|
+
margin-block: 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
134
140
|
:where(dialog).modal > header > .media {
|
|
135
141
|
flex: 1 1 auto;
|
|
136
142
|
min-inline-size: 0;
|
|
@@ -194,19 +200,20 @@
|
|
|
194
200
|
|
|
195
201
|
:where(dialog).modal.scrollable > form > :not(:is(header, footer)),
|
|
196
202
|
:where(dialog).modal.scrollable > .stack > :not(:is(header, footer)) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
203
|
+
/* Declared here, not as a :root alias: an inherited alias arrives already
|
|
204
|
+
computed, so a --focus-ring-width override on the dialog would not reach
|
|
205
|
+
it. */
|
|
206
|
+
--dialog-focus-bleed: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
201
207
|
|
|
202
208
|
flex: 1 1 auto;
|
|
203
209
|
min-block-size: 0;
|
|
204
|
-
/* Keep focus paint inside the clip
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
/* Keep focus paint inside the clip, on all four sides (overflow: auto clips
|
|
211
|
+
both axes), without moving the content. Every outside indicator is
|
|
212
|
+
--focus-ring-width wide (core/focus.css). On the block axis the reserve
|
|
213
|
+
reaches into the slot gap, which is wider, so the scrollport never slides
|
|
214
|
+
under the header or footer. */
|
|
215
|
+
margin: calc(var(--dialog-focus-bleed) * -1);
|
|
216
|
+
padding: var(--dialog-focus-bleed);
|
|
210
217
|
overflow: auto;
|
|
211
218
|
overscroll-behavior: contain;
|
|
212
219
|
}
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* links are a .cluster using .nav-link items. .nav-list is the vertical nav
|
|
8
8
|
* list shared with .drawer, fed by the shared --gap token.
|
|
9
9
|
*
|
|
10
|
+
* Public hooks:
|
|
11
|
+
* --state-selected-text (fallback-only: current-link ink, see tokens.css)
|
|
12
|
+
*
|
|
10
13
|
* Internal:
|
|
11
14
|
* --gap (local value for the shared gap token)
|
|
12
15
|
*/
|
|
@@ -71,8 +74,7 @@
|
|
|
71
74
|
|
|
72
75
|
/* nav-link serves two contracts: plain page navigation (aria-current="page")
|
|
73
76
|
and scrollspy, which toggles aria-current="location" on the active link.
|
|
74
|
-
|
|
75
|
-
removed everywhere else, never serialized as aria-current="false".
|
|
77
|
+
Those two values are the contract; any other, "false" included, is inert.
|
|
76
78
|
Selection never changes text metrics: the shared medium weight is kept and
|
|
77
79
|
the current link is carried by the selected accent. */
|
|
78
80
|
.nav-link:hover {
|
|
@@ -83,9 +85,9 @@
|
|
|
83
85
|
/* Current last, equal specificity to :hover (0,2,0): a hovered current link
|
|
84
86
|
keeps the selected accent instead of dropping to --text. The surface fill is
|
|
85
87
|
shared with the hover rule above. */
|
|
86
|
-
.nav-link[aria-current] {
|
|
88
|
+
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
87
89
|
background: var(--surface-subtle);
|
|
88
|
-
color: var(--state-selected);
|
|
90
|
+
color: var(--state-selected-text, var(--state-selected));
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
/* On a bar with its own --ui-bg/--ui-fg the fill is a tint of the link's own
|
|
@@ -96,7 +98,7 @@
|
|
|
96
98
|
above keeps the light chip where color-mix is missing, with ink that is
|
|
97
99
|
legible on it. */
|
|
98
100
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
99
|
-
.nav-link:is(:hover, [aria-current]) {
|
|
101
|
+
.nav-link:is(:hover, [aria-current="page"], [aria-current="location"]) {
|
|
100
102
|
background: color-mix(in oklch, currentColor 10%, transparent);
|
|
101
103
|
}
|
|
102
104
|
|
|
@@ -105,8 +107,8 @@
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
/* After :hover so a hovered current link keeps the selected accent. */
|
|
108
|
-
.nav-link[aria-current] {
|
|
109
|
-
color: var(--ui-fg, var(--state-selected));
|
|
110
|
+
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
111
|
+
color: var(--ui-fg, var(--state-selected-text, var(--state-selected)));
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
|
|
@@ -139,11 +141,14 @@
|
|
|
139
141
|
follows the forced palette without a forced-colors block and never touches
|
|
140
142
|
the row separators. Navbar horizontal links keep color + surface only. The
|
|
141
143
|
positioning context lives on every row link, so becoming current never moves
|
|
142
|
-
an app-positioned decoration to a new containing block.
|
|
143
|
-
|
|
144
|
+
an app-positioned decoration to a new containing block.
|
|
145
|
+
The trait sits centered in the link's inline padding with rounded ends: flush
|
|
146
|
+
on the edge, a square line read as pasted onto the rounded fill. */
|
|
147
|
+
.nav-list .nav-link:is([aria-current="page"], [aria-current="location"])::before {
|
|
144
148
|
content: "";
|
|
145
149
|
position: absolute;
|
|
146
150
|
inset-block: var(--space-20);
|
|
147
|
-
inset-inline-start:
|
|
151
|
+
inset-inline-start: calc((var(--space-20) - 2px) / 2);
|
|
148
152
|
border-inline-start: 2px solid currentColor;
|
|
153
|
+
border-radius: var(--radius-full);
|
|
149
154
|
}
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* Truncated ranges are plain text (`.muted`, `aria-hidden="true"`), not .btn
|
|
11
11
|
* — they are decorative, not actionable. They still get the same box sizing
|
|
12
12
|
* as .btn below so the row stays visually aligned.
|
|
13
|
+
*
|
|
14
|
+
* The row wraps when it does not fit: every page stays visible and
|
|
15
|
+
* clickable. A strip that scrolled hid pages behind a scrollbar that clicking
|
|
16
|
+
* cannot move, since a click changes the page. Keeping the rendered range
|
|
17
|
+
* compact is the application's job.
|
|
13
18
|
*/
|
|
14
19
|
.pagination {
|
|
15
20
|
display: flex;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* <nav class="scrollspy">
|
|
9
9
|
* containing <a href="#section"> links and toggles aria-current="location" on
|
|
10
10
|
* the link whose target is in view. Pair with .nav-list and .nav-link, or style
|
|
11
|
-
* [aria-current] yourself.
|
|
11
|
+
* [aria-current="location"] yourself.
|
|
12
12
|
*
|
|
13
13
|
* Progressive enhancement: on browsers that support :target-current,
|
|
14
14
|
* this file enables the native target group and marks the active link.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Segmented control — one compact choice among a few, on a shared track.
|
|
3
|
+
*
|
|
4
|
+
* Docs: docs/pages/components/segmented.md
|
|
5
|
+
*
|
|
6
|
+
* .segmented is the track, each .segment a direct child. The look is shared;
|
|
7
|
+
* the semantics stay with the markup, and the selected state is read from it:
|
|
8
|
+
* radios <label class="segment"><input type="radio"> :checked
|
|
9
|
+
* tabs <button class="segment" role="tab"> aria-selected="true"
|
|
10
|
+
* links <a class="segment"> aria-current="page"
|
|
11
|
+
* No script of its own: radios bring their arrow keys, a tablist takes
|
|
12
|
+
* data-enhance="tabs". Without :has() the radio anatomy loses its selected
|
|
13
|
+
* fill and focus ring, and the native radio stays visible to carry both.
|
|
14
|
+
*
|
|
15
|
+
* One row of equal shares (grid-auto-columns: 1fr), at least --control-size
|
|
16
|
+
* tall so it lines up with .btn and .input. Labels wrap instead of truncating;
|
|
17
|
+
* a word longer than its share breaks, so the control never overflows its
|
|
18
|
+
* container. A strip too long for one row is .tabs, which scrolls.
|
|
19
|
+
*
|
|
20
|
+
* Focus is drawn inside the segment, unlike other discrete actions
|
|
21
|
+
* (focus.css): an offset ring would cross the track's padding and cover the
|
|
22
|
+
* neighbouring segment.
|
|
23
|
+
*
|
|
24
|
+
* Public hooks:
|
|
25
|
+
* --segmented-bg
|
|
26
|
+
* --segmented-pad
|
|
27
|
+
* --segmented-radius
|
|
28
|
+
* --segmented-fg
|
|
29
|
+
* --segmented-selected-bg
|
|
30
|
+
* --segmented-selected-border
|
|
31
|
+
*/
|
|
32
|
+
.segmented {
|
|
33
|
+
--segmented-bg: var(--surface-subtle);
|
|
34
|
+
--segmented-pad: var(--space-10);
|
|
35
|
+
--segmented-radius: var(--radius);
|
|
36
|
+
--segmented-fg: var(--text-muted);
|
|
37
|
+
--segmented-selected-bg: var(--surface-raised);
|
|
38
|
+
/* The edge is what separates the selected segment where --surface-raised
|
|
39
|
+
is darker than --surface-subtle (the default dark palette). */
|
|
40
|
+
--segmented-selected-border: var(--border);
|
|
41
|
+
|
|
42
|
+
display: inline-grid;
|
|
43
|
+
grid-auto-flow: column;
|
|
44
|
+
grid-auto-columns: 1fr;
|
|
45
|
+
gap: var(--segmented-pad);
|
|
46
|
+
/* why: margin and border also reset <fieldset>, the radio anatomy's
|
|
47
|
+
element. The border is transparent, not 0: forced colors paint it, which
|
|
48
|
+
outlines the track once its fill is gone. */
|
|
49
|
+
margin: 0;
|
|
50
|
+
padding: var(--segmented-pad);
|
|
51
|
+
border: var(--border-width) solid transparent;
|
|
52
|
+
border-radius: var(--segmented-radius);
|
|
53
|
+
background: var(--segmented-bg);
|
|
54
|
+
font-size: var(--control-font-size);
|
|
55
|
+
vertical-align: middle;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.segment {
|
|
59
|
+
position: relative;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: center;
|
|
63
|
+
gap: 0.375em;
|
|
64
|
+
/* The track is --control-size tall through its segments, not through its
|
|
65
|
+
own min-block-size: a <fieldset> lays its content in an anonymous box
|
|
66
|
+
sized to the content, so a minimum on the track left the segments short.
|
|
67
|
+
A minimum, not a height: a wrapped label grows the whole row. */
|
|
68
|
+
min-block-size: calc(var(--control-size) - 2 * (var(--segmented-pad) + var(--border-width)));
|
|
69
|
+
padding: calc(var(--space-10) / 2) var(--space-30);
|
|
70
|
+
border: 0;
|
|
71
|
+
border-radius: calc(var(--segmented-radius) - var(--segmented-pad));
|
|
72
|
+
background: transparent;
|
|
73
|
+
color: var(--segmented-fg);
|
|
74
|
+
font: inherit;
|
|
75
|
+
/* Constant across states: a heavier selected label would resize its share. */
|
|
76
|
+
font-weight: var(--font-weight-medium);
|
|
77
|
+
line-height: 1.2;
|
|
78
|
+
text-align: center;
|
|
79
|
+
text-decoration: none;
|
|
80
|
+
/* why: also shrinks the min-content contribution, so 1fr keeps equal
|
|
81
|
+
shares under constraint instead of widening one around a long word. */
|
|
82
|
+
overflow-wrap: anywhere;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
transition:
|
|
85
|
+
background-color var(--duration),
|
|
86
|
+
color var(--duration),
|
|
87
|
+
box-shadow var(--duration);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.segment > input {
|
|
91
|
+
margin: 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* The radio is visually hidden but stays focusable, submitted and arrow-key
|
|
95
|
+
navigable (sr-only pattern). Guarded: without :has() the segment cannot
|
|
96
|
+
mirror :checked, so the visible native radio is the fallback. */
|
|
97
|
+
@supports selector(:has(*)) {
|
|
98
|
+
.segment > input {
|
|
99
|
+
position: absolute;
|
|
100
|
+
inline-size: 1px;
|
|
101
|
+
block-size: 1px;
|
|
102
|
+
padding: 0;
|
|
103
|
+
margin: -1px;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
clip: rect(0, 0, 0, 0);
|
|
106
|
+
white-space: nowrap;
|
|
107
|
+
border: 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.segment:hover {
|
|
112
|
+
color: var(--text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Separate rules from the :has() forms below: an engine without :has() would
|
|
116
|
+
discard a shared selector list, ARIA states included. */
|
|
117
|
+
.segment:is([aria-selected="true"], [aria-current="page"], [aria-current="true"]) {
|
|
118
|
+
background: var(--segmented-selected-bg);
|
|
119
|
+
color: var(--text);
|
|
120
|
+
box-shadow:
|
|
121
|
+
0 0 0 var(--border-width) var(--segmented-selected-border),
|
|
122
|
+
var(--shadow);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.segment:has(> input:checked) {
|
|
126
|
+
background: var(--segmented-selected-bg);
|
|
127
|
+
color: var(--text);
|
|
128
|
+
box-shadow:
|
|
129
|
+
0 0 0 var(--border-width) var(--segmented-selected-border),
|
|
130
|
+
var(--shadow);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.segment:focus-visible {
|
|
134
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
135
|
+
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.segment:has(> input:focus-visible) {
|
|
139
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
140
|
+
outline-offset: calc(var(--focus-ring-width) * -1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.segment:is(:disabled, [aria-disabled="true"]) {
|
|
144
|
+
opacity: var(--disabled-opacity);
|
|
145
|
+
cursor: not-allowed;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.segment:has(> input:disabled) {
|
|
149
|
+
opacity: var(--disabled-opacity);
|
|
150
|
+
cursor: not-allowed;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Forced colors drop the raised fill and its edge. The selected segment
|
|
154
|
+
takes the system highlight — a background, so the outline stays the focus
|
|
155
|
+
channel alone. forced-color-adjust: none is required, not cosmetic: the UA
|
|
156
|
+
otherwise paints a Canvas backplate behind the label, which hid the text on
|
|
157
|
+
the highlight. Opting out also stops the forcing of the rest, so the shadow
|
|
158
|
+
is dropped here and the focus line takes the highlight's text color. */
|
|
159
|
+
@media (forced-colors: active) {
|
|
160
|
+
.segment:is([aria-selected="true"], [aria-current="page"], [aria-current="true"]) {
|
|
161
|
+
--focus: HighlightText;
|
|
162
|
+
|
|
163
|
+
forced-color-adjust: none;
|
|
164
|
+
background: Highlight;
|
|
165
|
+
color: HighlightText;
|
|
166
|
+
box-shadow: none;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.segment:has(> input:checked) {
|
|
170
|
+
--focus: HighlightText;
|
|
171
|
+
|
|
172
|
+
forced-color-adjust: none;
|
|
173
|
+
background: Highlight;
|
|
174
|
+
color: HighlightText;
|
|
175
|
+
box-shadow: none;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* --disabled-opacity is 1 here (tokens.css): the dimming needs a color. */
|
|
179
|
+
.segment:is(:disabled, [aria-disabled="true"]) {
|
|
180
|
+
color: GrayText;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.segment:has(> input:disabled) {
|
|
184
|
+
color: GrayText;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
* destinations. Anything else focusable in a step is out of contract.
|
|
26
26
|
*
|
|
27
27
|
* The framework never hides an authored label to solve a layout problem. A
|
|
28
|
-
* labelled row
|
|
28
|
+
* labelled row wraps its labels, then scrolls if its markers or authored item
|
|
29
|
+
* minimum outgrow the available space. An `actual-container` size
|
|
29
30
|
* context buys it one thing: the wide representation, marker beside label.
|
|
30
31
|
*
|
|
31
32
|
* See docs/design-notes/steps.md for the threshold, the selector shapes, the
|
|
@@ -136,11 +137,14 @@
|
|
|
136
137
|
|
|
137
138
|
/* Complete owns the accented connector that reaches the current step, so the
|
|
138
139
|
connector leaving current goes back to neutral. Declared after
|
|
139
|
-
.step-complete at equal specificity: current wins on every channel.
|
|
140
|
+
.step-complete at equal specificity: current wins on every channel. The
|
|
141
|
+
current marker is a regular surface, so its number and ring are selected
|
|
142
|
+
ink on a surface, not a selected fill; they are held to different ratios
|
|
143
|
+
(4.5:1 for the number, 3:1 for the ring) by report:theme-contrast. */
|
|
140
144
|
.steps > [aria-current="step"] {
|
|
141
145
|
--step-marker-bg: var(--surface-opaque);
|
|
142
|
-
--step-marker-border: var(--state-selected);
|
|
143
|
-
--step-marker-fg: var(--state-selected);
|
|
146
|
+
--step-marker-border: var(--state-selected-text, var(--state-selected));
|
|
147
|
+
--step-marker-fg: var(--state-selected-text, var(--state-selected));
|
|
144
148
|
--step-line: var(--border);
|
|
145
149
|
|
|
146
150
|
color: var(--text);
|
|
@@ -161,10 +165,11 @@
|
|
|
161
165
|
/* Horizontal ------------------------------------------------------------- */
|
|
162
166
|
|
|
163
167
|
.steps-horizontal {
|
|
164
|
-
--step-min:
|
|
168
|
+
--step-min: var(--step-size);
|
|
165
169
|
--step-gap: var(--space-20);
|
|
166
170
|
|
|
167
171
|
inline-size: 100%;
|
|
172
|
+
column-gap: var(--step-gap);
|
|
168
173
|
overflow-x: auto;
|
|
169
174
|
/* Stated, never left to `overflow-x` alone: one scrolling axis computes the
|
|
170
175
|
other to `auto`, and a row hugs fractional content closely enough that a
|
|
@@ -173,11 +178,22 @@
|
|
|
173
178
|
overscroll-behavior-inline: contain;
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
/* A row that loads mid-flow starts scrolled to its current step; engines
|
|
182
|
+
without the property start at the first. */
|
|
183
|
+
.steps-horizontal > [aria-current="step"] {
|
|
184
|
+
/* biome-ignore lint/correctness/noUnknownProperty: CSS Scroll Snap 2 property that Biome's list predates; an engine without it drops the declaration. */
|
|
185
|
+
scroll-initial-target: nearest;
|
|
186
|
+
}
|
|
187
|
+
|
|
176
188
|
/* A scroll container clips at the padding edge, so a navigable row reserves the
|
|
177
|
-
focus ring's own width and offset
|
|
178
|
-
link has a ring to
|
|
189
|
+
focus ring's own width and offset, and focus scrolls a link in that far from
|
|
190
|
+
the inline clip. Only this composition scrolls, and only a link has a ring to
|
|
191
|
+
protect. */
|
|
179
192
|
.steps-horizontal:has(a[href]) {
|
|
193
|
+
/* Wrapped names can fill their cell right up to either inline clip edge. */
|
|
194
|
+
padding-inline: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
180
195
|
padding-block: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
196
|
+
scroll-padding-inline: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
181
197
|
}
|
|
182
198
|
|
|
183
199
|
/* One explicit row, the marker's: a `.step-label` is auto-placed into an
|
|
@@ -189,6 +205,8 @@
|
|
|
189
205
|
.steps-horizontal > li {
|
|
190
206
|
display: grid;
|
|
191
207
|
flex: 1 0 var(--step-min);
|
|
208
|
+
/* Bound each label to its share so it cannot change the marker pitch. */
|
|
209
|
+
grid-template-columns: minmax(0, 1fr);
|
|
192
210
|
grid-template-rows: var(--step-size);
|
|
193
211
|
gap: var(--step-gap);
|
|
194
212
|
justify-items: center;
|
|
@@ -196,9 +214,42 @@
|
|
|
196
214
|
text-align: center;
|
|
197
215
|
}
|
|
198
216
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
217
|
+
.steps-horizontal .step-label {
|
|
218
|
+
/* Even an unbroken name must fit its share instead of overlapping a peer.
|
|
219
|
+
Hyphenation, when `lang` allows it, is tried before an arbitrary break. */
|
|
220
|
+
overflow-wrap: anywhere;
|
|
221
|
+
hyphens: auto;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Half end cells put the outer markers on the row's edges without changing
|
|
225
|
+
their pitch, so the track lines up with the text around it, as the compact
|
|
226
|
+
and inline forms do. The marker's half-width keeps the first connector equal
|
|
227
|
+
to its peers.
|
|
228
|
+
|
|
229
|
+
Source order is load-bearing: these tie the `:empty` rules below and the
|
|
230
|
+
inline query on specificity, and both must win over them. */
|
|
231
|
+
.steps-horizontal > li:is(:first-child, :last-child) {
|
|
232
|
+
flex: 0.5 0 calc((var(--step-min) + var(--step-size)) / 2);
|
|
233
|
+
min-inline-size: max(var(--step-size), calc((var(--step-min) + var(--step-size)) / 2));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.steps-horizontal > li:first-child {
|
|
237
|
+
justify-items: start;
|
|
238
|
+
text-align: start;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.steps-horizontal > li:first-child::after {
|
|
242
|
+
inset-inline-start: var(--step-size);
|
|
243
|
+
inline-size: calc(200% - var(--step-size) * 2 + var(--step-gap));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.steps-horizontal > li:last-child {
|
|
247
|
+
justify-items: end;
|
|
248
|
+
text-align: end;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* An empty step ignores any authored reading-width floor and needs only its
|
|
252
|
+
marker. That lets five of them fit a phone without a size context.
|
|
202
253
|
|
|
203
254
|
`justify-items` is not an override of the row's centring: the marker is
|
|
204
255
|
centred because a label sits under it and the two share a centre, and with no
|
|
@@ -208,9 +259,8 @@
|
|
|
208
259
|
the text around it, the way the inline representation does.
|
|
209
260
|
|
|
210
261
|
`:empty` says exactly what the markup says, the way `.badge:empty` does, and
|
|
211
|
-
it reads the item's own content: bare text
|
|
212
|
-
|
|
213
|
-
floor and skew the row. `:empty` also keeps the compact form reachable on the
|
|
262
|
+
it reads the item's own content: bare text keeps the same authored floor as
|
|
263
|
+
its wrapped peers. `:empty` also keeps the compact form reachable on the
|
|
214
264
|
Degraded tier, where only the inline bonus needs `:has()`. */
|
|
215
265
|
.steps-horizontal > li:empty {
|
|
216
266
|
flex: 1 1 0;
|
|
@@ -220,6 +270,8 @@
|
|
|
220
270
|
|
|
221
271
|
.steps-horizontal > li:empty::after {
|
|
222
272
|
inset-inline-start: var(--step-size);
|
|
273
|
+
/* Marker-only end cells keep full shares, unlike labelled end cells. */
|
|
274
|
+
inline-size: calc(100% - var(--step-size) + var(--step-gap));
|
|
223
275
|
}
|
|
224
276
|
|
|
225
277
|
.steps-horizontal > li:empty:last-child {
|
|
@@ -227,12 +279,12 @@
|
|
|
227
279
|
}
|
|
228
280
|
|
|
229
281
|
/* The pseudo-element is marker-height so --step-connector may be any complete
|
|
230
|
-
background value. */
|
|
282
|
+
background value; its length also crosses the gap to the next equal track. */
|
|
231
283
|
.steps-horizontal > li::after {
|
|
232
284
|
position: absolute;
|
|
233
285
|
inset-block-start: 0;
|
|
234
286
|
inset-inline-start: calc(50% + var(--step-size) / 2);
|
|
235
|
-
inline-size: calc(100% - var(--step-size));
|
|
287
|
+
inline-size: calc(100% - var(--step-size) + var(--step-gap));
|
|
236
288
|
block-size: var(--step-size);
|
|
237
289
|
background: var(
|
|
238
290
|
--step-connector,
|
|
@@ -251,6 +303,11 @@
|
|
|
251
303
|
* written — a row out of contract is not owed a curated fallback.
|
|
252
304
|
*/
|
|
253
305
|
@container actual-container (inline-size >= 60rem) {
|
|
306
|
+
.steps-horizontal:has(.step-label) {
|
|
307
|
+
/* Inline connectors already own the spacing between marker-label groups. */
|
|
308
|
+
column-gap: 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
254
311
|
.steps-horizontal:has(.step-label) > li {
|
|
255
312
|
display: flex;
|
|
256
313
|
flex: 1 1 auto;
|