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
|
@@ -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
|
+
}
|
|
@@ -137,11 +137,14 @@
|
|
|
137
137
|
|
|
138
138
|
/* Complete owns the accented connector that reaches the current step, so the
|
|
139
139
|
connector leaving current goes back to neutral. Declared after
|
|
140
|
-
.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. */
|
|
141
144
|
.steps > [aria-current="step"] {
|
|
142
145
|
--step-marker-bg: var(--surface-opaque);
|
|
143
|
-
--step-marker-border: var(--state-selected);
|
|
144
|
-
--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));
|
|
145
148
|
--step-line: var(--border);
|
|
146
149
|
|
|
147
150
|
color: var(--text);
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
stays inert. */
|
|
220
220
|
.tab[aria-selected="true"],
|
|
221
221
|
.tab:is([aria-current="page"], [aria-current="true"]) {
|
|
222
|
-
color: var(--intent, var(--state-selected));
|
|
222
|
+
color: var(--intent, var(--state-selected-text, var(--state-selected)));
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
/* The selected indicator lives on a positioned pseudo so that only the active
|
package/src/css/core/focus.css
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
--surface-solid, so one token reads on the page, on a solid band and on
|
|
19
19
|
any nested surface.
|
|
20
20
|
|
|
21
|
+
A .segment is an action but takes the inset ring: its track's padding is
|
|
22
|
+
narrower than an offset ring, which would cover the neighbouring segment.
|
|
23
|
+
|
|
21
24
|
Documented exception: an invalid field focuses in --form-invalid-border
|
|
22
25
|
(danger), so the error stays visible at the point of correction. The border
|
|
23
26
|
carries the same hook; report:theme-contrast measures that line at 3:1
|
package/src/css/core/tokens.css
CHANGED
|
@@ -162,9 +162,20 @@
|
|
|
162
162
|
[data-theme] {
|
|
163
163
|
/* State tokens — semantic colors for selected/pressed/current and disabled.
|
|
164
164
|
Components consume these instead of hard-wiring --primary/--text-subtle
|
|
165
|
-
so forced-colors can override them in one place.
|
|
165
|
+
so forced-colors can override them in one place. --state-selected is a
|
|
166
|
+
selected fill or accent, --state-selected-fg the ink on that fill.
|
|
167
|
+
--state-selected-text is selected ink on a regular surface (current nav
|
|
168
|
+
link, selected tab, current step number). It is reset to the
|
|
169
|
+
guaranteed-invalid value on every theme boundary, so a consumer reads its
|
|
170
|
+
local --state-selected through the fallback var(--state-selected-text,
|
|
171
|
+
var(--state-selected)): a theme that resolves it to var(--link) bakes that
|
|
172
|
+
reference where it is declared, so a nested island that does not set it
|
|
173
|
+
inherits an already-resolved value the local fallback can never reach. A
|
|
174
|
+
theme sets it when one color cannot be both a fill under its -fg and text
|
|
175
|
+
on the page, typically in a dark scheme. */
|
|
166
176
|
--state-selected: var(--primary);
|
|
167
177
|
--state-selected-fg: var(--primary-fg);
|
|
178
|
+
--state-selected-text: initial;
|
|
168
179
|
--state-disabled: var(--text-subtle);
|
|
169
180
|
|
|
170
181
|
/* Status dot ring — cuts the dot out of what it overlaps. */
|
|
@@ -173,10 +184,15 @@
|
|
|
173
184
|
|
|
174
185
|
:root {
|
|
175
186
|
/* Variant tuning — themes may adjust generated soft surfaces without
|
|
176
|
-
replacing the component rules.
|
|
187
|
+
replacing the component rules. --soft-bg-mix is the share of surface in a
|
|
188
|
+
soft fill: an opaque mix on the shared .soft, an opacity (100% minus it)
|
|
189
|
+
on the translucent badge, alert and soft-button tint. --soft-hover-alpha
|
|
190
|
+
is the extra intent share on hover. --soft-border-mix has no default: it
|
|
191
|
+
is a fallback-only hook whose absence lets each recipe keep its own rim
|
|
192
|
+
(a tinted rim on .soft, none on the tint), and a theme that sets it gives
|
|
193
|
+
both a rim of that weight. */
|
|
177
194
|
--soft-bg-mix: 88%;
|
|
178
|
-
--soft-
|
|
179
|
-
--soft-hover-alpha: 12%;
|
|
195
|
+
--soft-hover-alpha: 6%;
|
|
180
196
|
/* Soft foreground: the share of raw intent in soft text. 100% keeps the
|
|
181
197
|
pure intent color; lower values rebate it toward --text, which vivid or
|
|
182
198
|
light intent palettes need to stay legible on their own soft surface.
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
*
|
|
35
35
|
* Intent list source of truth: intents.css. Components consume --intent* tokens
|
|
36
36
|
* instead of re-listing intent classes.
|
|
37
|
+
*
|
|
38
|
+
* Public hooks:
|
|
39
|
+
* --soft-border-mix (fallback-only: a soft rim of that weight on every recipe)
|
|
37
40
|
*/
|
|
38
41
|
.solid {
|
|
39
42
|
--ui-bg: var(--intent, var(--neutral));
|
|
@@ -97,7 +100,7 @@
|
|
|
97
100
|
|
|
98
101
|
/* Shared control scale. Wrappers are listed when their descendants form one
|
|
99
102
|
component (field, choice-card, floating-field, input-icon, join, pagination,
|
|
100
|
-
tabs, flyout). A local size declaration beats inherited density because it
|
|
103
|
+
tabs, segmented, flyout). A local size declaration beats inherited density because it
|
|
101
104
|
is declared on the sized component itself. Guarded by check:sync
|
|
102
105
|
(size-scale-participants): .sm and .lg must list the same components. */
|
|
103
106
|
:where(
|
|
@@ -110,6 +113,7 @@
|
|
|
110
113
|
.field,
|
|
111
114
|
.choice,
|
|
112
115
|
.choice-card,
|
|
116
|
+
.color-swatch,
|
|
113
117
|
.input-icon,
|
|
114
118
|
.floating-field,
|
|
115
119
|
.otp,
|
|
@@ -117,6 +121,7 @@
|
|
|
117
121
|
.pagination,
|
|
118
122
|
.tabs,
|
|
119
123
|
.tab,
|
|
124
|
+
.segmented,
|
|
120
125
|
.flyout
|
|
121
126
|
).sm {
|
|
122
127
|
--control-size: var(--control-size-sm);
|
|
@@ -137,6 +142,7 @@
|
|
|
137
142
|
.field,
|
|
138
143
|
.choice,
|
|
139
144
|
.choice-card,
|
|
145
|
+
.color-swatch,
|
|
140
146
|
.input-icon,
|
|
141
147
|
.floating-field,
|
|
142
148
|
.otp,
|
|
@@ -144,6 +150,7 @@
|
|
|
144
150
|
.pagination,
|
|
145
151
|
.tabs,
|
|
146
152
|
.tab,
|
|
153
|
+
.segmented,
|
|
147
154
|
.flyout
|
|
148
155
|
).lg {
|
|
149
156
|
--control-size: var(--control-size-lg);
|
|
@@ -155,21 +162,25 @@
|
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
/* Modern color enhancement. Baseline variant values above remain usable
|
|
158
|
-
everywhere. Interactive states live in the components that consume them.
|
|
165
|
+
everywhere. Interactive states live in the components that consume them.
|
|
166
|
+
The shared .soft is opaque: it paints zones (.card, .navbar, .app-nav,
|
|
167
|
+
.chat-bubble) an author may lay over an image, where a translucent fill
|
|
168
|
+
would hand the text's contrast to whatever is behind it. The badge, alert
|
|
169
|
+
and soft button own a translucent tint instead (soft-tint). */
|
|
159
170
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
160
171
|
.soft {
|
|
161
|
-
/* @sync soft-recipe */
|
|
162
172
|
--ui-bg: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)));
|
|
163
173
|
--ui-border: color-mix(
|
|
164
174
|
in oklab,
|
|
165
|
-
var(--surface) var(--soft-border-mix),
|
|
175
|
+
var(--surface) var(--soft-border-mix, 65%),
|
|
166
176
|
var(--intent, var(--neutral))
|
|
167
177
|
);
|
|
178
|
+
/* @sync soft-ink */
|
|
168
179
|
--ui-fg: var(
|
|
169
180
|
--intent-soft-fg,
|
|
170
181
|
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
171
182
|
);
|
|
172
|
-
/* @sync soft-
|
|
183
|
+
/* @sync soft-ink */
|
|
173
184
|
}
|
|
174
185
|
}
|
|
175
186
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Color swatch — a color chosen from a palette the application defines.
|
|
3
|
+
*
|
|
4
|
+
* Docs: docs/pages/forms/color-swatches.md
|
|
5
|
+
*
|
|
6
|
+
* A <label class="color-swatch"> wraps a native radio (one color) or checkbox
|
|
7
|
+
* (several) and paints --color-swatch-value. The same class on a <button> is
|
|
8
|
+
* the trigger of a palette flyout. The application owns the colors; Actual
|
|
9
|
+
* owns the shape, the selection mark and the focus ring.
|
|
10
|
+
*
|
|
11
|
+
* Two sizes: the visible swatch (::before) is the height of a .switch track, so
|
|
12
|
+
* a swatch and a switch in neighbouring rows match; the element is the hit
|
|
13
|
+
* area around it. The hit area reaches half the inherited --gap past the
|
|
14
|
+
* swatch, up to --control-size, and overflows its layout box: neighbours in a
|
|
15
|
+
* .cluster or .grid meet in the middle of the gap instead of overlapping, and
|
|
16
|
+
* rows sit one gap apart like columns. Edge, selection ring and focus ring all
|
|
17
|
+
* belong to the visible swatch.
|
|
18
|
+
*
|
|
19
|
+
* The color is arbitrary, so nothing drawn on it may assume a contrast: the
|
|
20
|
+
* selected mark is a double ring (--text outside, --surface-opaque inside) that
|
|
21
|
+
* reads on any fill, and a hairline edge keeps a near-white color apart from
|
|
22
|
+
* the page. ::before opts out of forced colors to keep the color, so it takes
|
|
23
|
+
* system colors itself there.
|
|
24
|
+
*
|
|
25
|
+
* Without :has() the native radio stays visible in the swatch and carries the
|
|
26
|
+
* state; the ring needs :has() to mirror it.
|
|
27
|
+
*
|
|
28
|
+
* Public hooks:
|
|
29
|
+
* --color-swatch-value (fallback-only: the color, set per swatch)
|
|
30
|
+
* --color-swatch-size
|
|
31
|
+
* --color-swatch-radius
|
|
32
|
+
*/
|
|
33
|
+
.color-swatch {
|
|
34
|
+
/* The .switch track height (switch.css); a browser test holds them equal. */
|
|
35
|
+
--color-swatch-size: calc(var(--choice-control-size) + 0.125em);
|
|
36
|
+
--color-swatch-radius: var(--radius-full);
|
|
37
|
+
/* Internal: what the hit area adds to the swatch, half on each side. One gap
|
|
38
|
+
at most, so two neighbours never claim the same point — a wider reach let
|
|
39
|
+
the next swatch take a click on this one's center. */
|
|
40
|
+
--color-swatch-reach: clamp(0px, var(--control-size) - var(--color-swatch-size), var(--gap));
|
|
41
|
+
|
|
42
|
+
position: relative;
|
|
43
|
+
display: inline-grid;
|
|
44
|
+
place-items: center;
|
|
45
|
+
flex: none;
|
|
46
|
+
inline-size: calc(var(--color-swatch-size) + var(--color-swatch-reach));
|
|
47
|
+
block-size: calc(var(--color-swatch-size) + var(--color-swatch-reach));
|
|
48
|
+
/* Lay out at the visible size: gaps are measured from the swatch, not from
|
|
49
|
+
its invisible reach. */
|
|
50
|
+
margin: calc(var(--color-swatch-reach) / -2);
|
|
51
|
+
padding: 0;
|
|
52
|
+
/* No in-flow text, so the baseline would be the box's bottom edge and an
|
|
53
|
+
inline swatch would ride above the line (a flyout trigger beside its
|
|
54
|
+
label). A palette in .cluster or .grid ignores it. */
|
|
55
|
+
vertical-align: middle;
|
|
56
|
+
border: 0;
|
|
57
|
+
background: transparent;
|
|
58
|
+
color: inherit;
|
|
59
|
+
/* Consumed so .sm/.lg reach the em-sized swatch, as on .choice-card. */
|
|
60
|
+
font: inherit;
|
|
61
|
+
font-size: var(--control-font-size);
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.color-swatch::before {
|
|
66
|
+
content: "";
|
|
67
|
+
grid-area: 1 / 1;
|
|
68
|
+
inline-size: var(--color-swatch-size);
|
|
69
|
+
block-size: var(--color-swatch-size);
|
|
70
|
+
box-sizing: border-box;
|
|
71
|
+
border: var(--border-width) solid var(--border-control);
|
|
72
|
+
border-radius: var(--color-swatch-radius);
|
|
73
|
+
background: var(--color-swatch-value, transparent) padding-box;
|
|
74
|
+
/* The color is the content, as on .color (native.css). */
|
|
75
|
+
forced-color-adjust: none;
|
|
76
|
+
transition:
|
|
77
|
+
border-color var(--duration),
|
|
78
|
+
box-shadow var(--duration);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Shares the swatch's cell and paints after ::before, so a native radio left
|
|
82
|
+
visible sits on the color. */
|
|
83
|
+
.color-swatch > input {
|
|
84
|
+
grid-area: 1 / 1;
|
|
85
|
+
position: relative;
|
|
86
|
+
margin: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Visually hidden but focusable, submitted and arrow-key navigable (sr-only
|
|
90
|
+
pattern). Guarded: without :has() the swatch cannot mirror :checked, so the
|
|
91
|
+
visible native radio is the fallback. */
|
|
92
|
+
@supports selector(:has(*)) {
|
|
93
|
+
.color-swatch > input {
|
|
94
|
+
position: absolute;
|
|
95
|
+
inline-size: 1px;
|
|
96
|
+
block-size: 1px;
|
|
97
|
+
padding: 0;
|
|
98
|
+
margin: -1px;
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
clip: rect(0, 0, 0, 0);
|
|
101
|
+
white-space: nowrap;
|
|
102
|
+
border: 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.color-swatch:hover::before {
|
|
107
|
+
border-color: var(--text-subtle);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.color-swatch:has(> input:checked)::before {
|
|
111
|
+
box-shadow:
|
|
112
|
+
inset 0 0 0 2px var(--text),
|
|
113
|
+
inset 0 0 0 4px var(--surface-opaque);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* The ring goes around the visible swatch, not the larger hit area. */
|
|
117
|
+
.color-swatch:focus-visible {
|
|
118
|
+
outline: none;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.color-swatch:focus-visible::before {
|
|
122
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
123
|
+
outline-offset: var(--focus-outline-offset);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.color-swatch:has(> input:focus-visible)::before {
|
|
127
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
128
|
+
outline-offset: var(--focus-outline-offset);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.color-swatch:disabled {
|
|
132
|
+
opacity: var(--disabled-opacity);
|
|
133
|
+
cursor: not-allowed;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.color-swatch:has(> input:disabled) {
|
|
137
|
+
opacity: var(--disabled-opacity);
|
|
138
|
+
cursor: not-allowed;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* A disabled swatch keeps its color: --disabled-opacity is 1 here
|
|
142
|
+
(tokens.css), and the edge carries the state. */
|
|
143
|
+
@media (forced-colors: active) {
|
|
144
|
+
.color-swatch::before {
|
|
145
|
+
--focus: CanvasText;
|
|
146
|
+
|
|
147
|
+
border-color: CanvasText;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.color-swatch:has(> input:checked)::before {
|
|
151
|
+
box-shadow:
|
|
152
|
+
inset 0 0 0 2px CanvasText,
|
|
153
|
+
inset 0 0 0 4px Canvas;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.color-swatch:disabled::before {
|
|
157
|
+
border-color: GrayText;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.color-swatch:has(> input:disabled)::before {
|
|
161
|
+
border-color: GrayText;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -57,3 +57,12 @@
|
|
|
57
57
|
background: color-mix(in oklch, var(--surface) 88%, transparent);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
/* Same reduce override as .topbar (src/css/layout/topbar.css). */
|
|
62
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
63
|
+
.form-actions.sticky {
|
|
64
|
+
background: var(--surface);
|
|
65
|
+
-webkit-backdrop-filter: none;
|
|
66
|
+
backdrop-filter: none;
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/css/forms/index.css
CHANGED
package/src/css/forms/native.css
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Range has its own custom-styled file (range.css) so the focus ring can
|
|
9
9
|
* target the thumb pseudo-element rather than the rectangular CSS box.
|
|
10
|
+
*
|
|
11
|
+
* Public hooks:
|
|
12
|
+
* --color-size
|
|
10
13
|
*/
|
|
11
14
|
.file {
|
|
12
15
|
color: var(--text-muted);
|
|
@@ -52,16 +55,24 @@
|
|
|
52
55
|
outline-offset: var(--focus-outline-offset);
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
/* Native color picker — keep the browser's dialog and selected-color UI.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
/* Native color picker — keep the browser's dialog and selected-color UI. Same
|
|
59
|
+
geometry as .color-swatch: a --control-size hit area around a round swatch
|
|
60
|
+
the height of a .switch track, so a picker, a palette and a switch compose.
|
|
61
|
+
The input is the hit area; its padding leaves the swatch, which carries the
|
|
62
|
+
edge and the focus ring. Vendor pseudos cannot share a rule. */
|
|
58
63
|
.color {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
--color-size: calc(var(--choice-control-size) + 0.125em);
|
|
65
|
+
|
|
66
|
+
inline-size: max(var(--control-size), var(--color-size));
|
|
67
|
+
block-size: max(var(--control-size), var(--color-size));
|
|
68
|
+
padding: calc((max(var(--control-size), var(--color-size)) - var(--color-size)) / 2);
|
|
69
|
+
/* Lay out at the visible width, as .color-swatch does: the hit area
|
|
70
|
+
overflows on both sides so a label gap is measured from the swatch. */
|
|
71
|
+
margin: 0 calc((var(--color-size) - max(var(--control-size), var(--color-size))) / 2);
|
|
72
|
+
border: 0;
|
|
73
|
+
background: transparent;
|
|
74
|
+
/* Consumed so .sm/.lg reach the em-sized swatch. */
|
|
75
|
+
font-size: var(--control-font-size);
|
|
65
76
|
cursor: pointer;
|
|
66
77
|
}
|
|
67
78
|
|
|
@@ -70,13 +81,21 @@
|
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
.color::-webkit-color-swatch {
|
|
73
|
-
border:
|
|
74
|
-
border-radius:
|
|
84
|
+
border: var(--border-width) solid var(--border-control);
|
|
85
|
+
border-radius: var(--radius-full);
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
.color::-moz-color-swatch {
|
|
78
|
-
border:
|
|
79
|
-
border-radius:
|
|
89
|
+
border: var(--border-width) solid var(--border-control);
|
|
90
|
+
border-radius: var(--radius-full);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.color:hover::-webkit-color-swatch {
|
|
94
|
+
border-color: var(--text-subtle);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.color:hover::-moz-color-swatch {
|
|
98
|
+
border-color: var(--text-subtle);
|
|
80
99
|
}
|
|
81
100
|
|
|
82
101
|
.color:disabled {
|
|
@@ -84,24 +103,35 @@
|
|
|
84
103
|
opacity: var(--disabled-opacity);
|
|
85
104
|
}
|
|
86
105
|
|
|
106
|
+
/* The ring goes around the visible swatch, not the larger hit area. */
|
|
87
107
|
.color:focus-visible {
|
|
108
|
+
outline: none;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.color:focus-visible::-webkit-color-swatch {
|
|
88
112
|
outline: var(--focus-ring-width) solid var(--focus);
|
|
89
113
|
outline-offset: var(--focus-outline-offset);
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
116
|
+
.color:focus-visible::-moz-color-swatch {
|
|
117
|
+
outline: var(--focus-ring-width) solid var(--focus);
|
|
118
|
+
outline-offset: var(--focus-outline-offset);
|
|
119
|
+
}
|
|
97
120
|
|
|
98
|
-
|
|
99
|
-
|
|
121
|
+
@media (forced-colors: active) {
|
|
122
|
+
/* The swatch color is content, so the swatch opts out of forcing and takes
|
|
123
|
+
system colors for its edge and ring itself. */
|
|
100
124
|
.color::-webkit-color-swatch {
|
|
125
|
+
--focus: CanvasText;
|
|
126
|
+
|
|
101
127
|
forced-color-adjust: none;
|
|
128
|
+
border-color: CanvasText;
|
|
102
129
|
}
|
|
103
130
|
|
|
104
131
|
.color::-moz-color-swatch {
|
|
132
|
+
--focus: CanvasText;
|
|
133
|
+
|
|
105
134
|
forced-color-adjust: none;
|
|
135
|
+
border-color: CanvasText;
|
|
106
136
|
}
|
|
107
137
|
}
|
package/src/css/forms/switch.css
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* --switch-block-size
|
|
15
15
|
* --switch-knob-margin
|
|
16
16
|
* --switch-knob-size
|
|
17
|
+
* --switch-knob-inline-size
|
|
17
18
|
* --switch-travel
|
|
18
19
|
*
|
|
19
20
|
* The track is sized from --choice-control-size (tokens.css), not
|
|
@@ -47,9 +48,11 @@
|
|
|
47
48
|
2 *
|
|
48
49
|
var(--switch-knob-margin)
|
|
49
50
|
);
|
|
51
|
+
/* A wider knob is a capsule; the travel follows its width. */
|
|
52
|
+
--switch-knob-inline-size: var(--switch-knob-size);
|
|
50
53
|
--switch-travel: calc(
|
|
51
54
|
var(--switch-width) -
|
|
52
|
-
var(--switch-knob-size) -
|
|
55
|
+
var(--switch-knob-inline-size) -
|
|
53
56
|
2 *
|
|
54
57
|
var(--border-width) -
|
|
55
58
|
2 *
|
|
@@ -85,7 +88,7 @@
|
|
|
85
88
|
position: absolute;
|
|
86
89
|
inset-block-start: var(--switch-knob-margin);
|
|
87
90
|
inset-inline-start: var(--switch-knob-margin);
|
|
88
|
-
inline-size: var(--switch-knob-size);
|
|
91
|
+
inline-size: var(--switch-knob-inline-size);
|
|
89
92
|
block-size: var(--switch-knob-size);
|
|
90
93
|
border-radius: var(--radius-full);
|
|
91
94
|
background: var(--switch-knob);
|