actual-css 0.12.0 → 0.13.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 +49 -0
- package/README.md +7 -8
- package/components.json +8 -2
- package/dist/actual.full.css +505 -233
- 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/pages/components/avatar.md +17 -11
- package/docs/pages/components/breadcrumb.md +16 -2
- package/docs/pages/components/list.md +9 -13
- package/docs/pages/components/navbar.md +42 -22
- package/docs/pages/components/pagination.md +19 -0
- package/docs/pages/components/steps.md +24 -20
- package/docs/pages/enhancements/scrollspy.md +6 -6
- package/docs/pages/enhancements/tabs.md +74 -3
- package/docs/pages/forms/floating-field.md +21 -7
- package/docs/pages/forms/form-actions.md +3 -6
- package/docs/pages/forms/states-and-sizes.md +15 -15
- package/docs/pages/foundations/philosophy.md +97 -1
- package/docs/pages/foundations/tokens.md +40 -4
- package/docs/pages/guides/design-tools.md +1 -1
- 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/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 +5 -5
- package/src/cli/actual-css.js +1 -1
- package/src/css/components/alert.css +34 -34
- package/src/css/components/avatar.css +2 -18
- package/src/css/components/badge.css +0 -1
- package/src/css/components/breadcrumb.css +74 -8
- package/src/css/components/drawer.css +23 -42
- package/src/css/components/flyout.css +0 -1
- package/src/css/components/join.css +2 -2
- package/src/css/components/list.css +10 -10
- package/src/css/components/modal.css +17 -10
- package/src/css/components/navbar.css +10 -8
- package/src/css/components/pagination.css +5 -0
- package/src/css/components/scrollspy.css +1 -1
- package/src/css/components/steps.css +66 -12
- package/src/css/components/tab.css +134 -4
- package/src/css/core/focus.css +8 -6
- package/src/css/core/theme.css +6 -1
- package/src/css/core/tokens.css +4 -2
- package/src/css/forms/choice-card.css +1 -1
- package/src/css/forms/choice.css +1 -1
- package/src/css/forms/control.css +1 -1
- package/src/css/forms/floating-field.css +20 -18
- package/src/css/forms/form-actions.css +5 -8
- package/src/css/forms/native.css +2 -2
- package/src/css/forms/otp.css +4 -5
- package/src/css/forms/switch.css +1 -1
- 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/scroller.css +19 -3
- package/src/css/layout/sidebar-layout.css +3 -0
- package/src/css/layout/switcher.css +3 -0
- package/src/css/utilities/extra.css +3 -3
- 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/tab.js +33 -7
- package/src/tooling/design-export.js +1 -1
- package/src/tooling/design-tokens.js +44 -33
- package/src/tooling/theme-contrast.js +32 -24
|
@@ -12,8 +12,18 @@
|
|
|
12
12
|
* author class can then adjust a vertical strip without restating the
|
|
13
13
|
* attribute to reach its specificity.
|
|
14
14
|
*
|
|
15
|
+
* A horizontal strip is one axis of navigation, so it never wraps: a second
|
|
16
|
+
* line no longer matches the Left/Right model and reads as a nested level.
|
|
17
|
+
* When the tabs do not fit, the strip scrolls without a scrollbar; focus
|
|
18
|
+
* scrolls the active tab into view.
|
|
19
|
+
*
|
|
15
20
|
* Public hooks:
|
|
21
|
+
* --tab-flex
|
|
16
22
|
* --tab-gap
|
|
23
|
+
*
|
|
24
|
+
* Internal:
|
|
25
|
+
* --tab-edge
|
|
26
|
+
* --tab-fade-dir (read inside @keyframes, so it resolves per element)
|
|
17
27
|
*/
|
|
18
28
|
/* Intent boundary — inherited intents do not tint this component.
|
|
19
29
|
Local intent classes still win because :where() has zero specificity. */
|
|
@@ -24,27 +34,129 @@
|
|
|
24
34
|
.tabs {
|
|
25
35
|
display: flex;
|
|
26
36
|
flex-direction: row;
|
|
27
|
-
flex-wrap:
|
|
37
|
+
flex-wrap: nowrap;
|
|
28
38
|
gap: var(--space-10);
|
|
29
39
|
margin: 0;
|
|
30
40
|
padding: 0;
|
|
41
|
+
/* Block overflow is hidden, not auto: overflow-x: auto alone computes the
|
|
42
|
+
block axis to auto, so a sub-pixel line box would add a vertical
|
|
43
|
+
scrollbar inside the strip. Nothing paints outside it: the focus outline
|
|
44
|
+
is inset and the indicator sits inside the tab. */
|
|
45
|
+
overflow: auto hidden;
|
|
46
|
+
overscroll-behavior-inline: contain;
|
|
47
|
+
/* why: a visible bar sits on the indicator and the hairline and reads as a
|
|
48
|
+
second underline. The strip has its own scroll controls: selecting the
|
|
49
|
+
clipped tab, or focusing it, brings it into view with the next one
|
|
50
|
+
hinted by scroll-padding; arrow keys and swipes do the same. */
|
|
51
|
+
scrollbar-width: none;
|
|
52
|
+
/* Focus scrolls a tab in with room to spare, hinting at the next one; the
|
|
53
|
+
edge fade below spans the same inset, so a revealed tab stays clear of it. */
|
|
54
|
+
--tab-edge: var(--space-60);
|
|
55
|
+
|
|
56
|
+
scroll-padding-inline: var(--tab-edge);
|
|
31
57
|
border-block-end: var(--border-width) solid var(--border);
|
|
32
58
|
list-style: none;
|
|
33
59
|
}
|
|
34
60
|
|
|
61
|
+
/* The landmark around link tabs is not a scroll container, so as a flex or
|
|
62
|
+
grid item it would keep the strip's full width and push the page into
|
|
63
|
+
scrolling. Letting it shrink hands the overflow back to the strip.
|
|
64
|
+
Without :has() the landmark keeps its width; min-inline-size: 0 on the
|
|
65
|
+
wrapper is the fallback. */
|
|
66
|
+
:where(nav:has(> .tabs)) {
|
|
67
|
+
min-inline-size: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Items keep their size and the strip scrolls instead. Covers both
|
|
71
|
+
anatomies: .tab directly in a tablist, or <li> in a navigation list.
|
|
72
|
+
--tab-flex: 1 1 0 shares the width equally; an item still never shrinks
|
|
73
|
+
below its label (flex items keep min-content), so a strip that cannot fit
|
|
74
|
+
scrolls as before. */
|
|
75
|
+
.tabs > * {
|
|
76
|
+
flex: var(--tab-flex, none);
|
|
77
|
+
}
|
|
78
|
+
|
|
35
79
|
.tabs:where([aria-orientation="vertical"]) {
|
|
36
80
|
display: grid;
|
|
81
|
+
overflow: visible;
|
|
37
82
|
align-content: start;
|
|
38
83
|
justify-items: stretch;
|
|
39
84
|
border-block-end: 0;
|
|
40
85
|
border-inline-end: var(--border-width) solid var(--border);
|
|
41
86
|
}
|
|
42
87
|
|
|
88
|
+
/* Edge fade: with no scrollbar, the strip fades the edges it can still scroll
|
|
89
|
+
toward. It runs on the strip's own inline timeline, which is inactive on a
|
|
90
|
+
strip that fits or a vertical rail, so those stay unmasked. Keep the
|
|
91
|
+
@supports: without scroll timelines the animation would end at once and pin
|
|
92
|
+
its last mask. It follows scroll, not time, so reduced motion keeps it. */
|
|
93
|
+
@supports (animation-timeline: scroll()) {
|
|
94
|
+
.tabs {
|
|
95
|
+
--tab-fade-dir: to right;
|
|
96
|
+
|
|
97
|
+
animation: actual-tabs-fade linear both;
|
|
98
|
+
animation-timeline: scroll(self inline);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Gradient keywords are physical; :dir() follows the strip's own
|
|
102
|
+
direction, so an LTR island in an RTL page keeps its fade on the right. */
|
|
103
|
+
.tabs:dir(rtl) {
|
|
104
|
+
--tab-fade-dir: to left;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* A strip that loads on a current link tab starts scrolled to it. Horizontal
|
|
109
|
+
only: the strip is then its nearest scroll container, while a vertical
|
|
110
|
+
rail would hand the target to the page. Engines without the property start
|
|
111
|
+
at the first tab, and the fade says there is more. */
|
|
112
|
+
:where(.tabs:not([aria-orientation="vertical"]))
|
|
113
|
+
.tab:is([aria-current="page"], [aria-current="true"]) {
|
|
114
|
+
/* biome-ignore lint/correctness/noUnknownProperty: CSS Scroll Snap 2 property that Biome's list predates; an engine without it drops the declaration. */
|
|
115
|
+
scroll-initial-target: nearest;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Forced colors keep every label at full contrast. */
|
|
119
|
+
@media (forced-colors: active) {
|
|
120
|
+
.tabs {
|
|
121
|
+
animation: none;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* mask-image does not interpolate, so each mask holds until halfway to the
|
|
126
|
+
next keyframe: the start fade appears within the first pixels of scroll and
|
|
127
|
+
the end fade drops at the very end. */
|
|
128
|
+
@keyframes actual-tabs-fade {
|
|
129
|
+
0% {
|
|
130
|
+
mask-image: linear-gradient(
|
|
131
|
+
var(--tab-fade-dir),
|
|
132
|
+
#000 calc(100% - var(--tab-edge)),
|
|
133
|
+
transparent
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
1%,
|
|
138
|
+
99% {
|
|
139
|
+
mask-image: linear-gradient(
|
|
140
|
+
var(--tab-fade-dir),
|
|
141
|
+
transparent,
|
|
142
|
+
#000 var(--tab-edge) calc(100% - var(--tab-edge)),
|
|
143
|
+
transparent
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
100% {
|
|
148
|
+
mask-image: linear-gradient(var(--tab-fade-dir), transparent, #000 var(--tab-edge));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
43
152
|
.tab {
|
|
44
153
|
/* Positioning context for the selected indicator pseudo. */
|
|
45
154
|
position: relative;
|
|
46
155
|
display: inline-flex;
|
|
47
156
|
align-items: center;
|
|
157
|
+
/* A tab wider than its label (--tab-flex) centers it; at natural width this
|
|
158
|
+
changes nothing, and a vertical rail resets it to the start below. */
|
|
159
|
+
justify-content: center;
|
|
48
160
|
gap: var(--tab-gap, 0.375em);
|
|
49
161
|
min-block-size: var(--control-size);
|
|
50
162
|
padding-inline: var(--space-30);
|
|
@@ -58,12 +170,18 @@
|
|
|
58
170
|
the strip. The active state is carried by color and the indicator line. */
|
|
59
171
|
font-weight: var(--font-weight-semibold);
|
|
60
172
|
text-decoration: none;
|
|
173
|
+
/* A label breaking inside a one-line strip is the same defect as the strip
|
|
174
|
+
wrapping. */
|
|
175
|
+
white-space: nowrap;
|
|
61
176
|
cursor: pointer;
|
|
62
177
|
}
|
|
63
178
|
|
|
179
|
+
/* A vertical strip has no inline axis to protect: long labels wrap within the
|
|
180
|
+
rail instead of overflowing it. */
|
|
64
181
|
.tabs:where([aria-orientation="vertical"]) .tab {
|
|
65
182
|
justify-content: flex-start;
|
|
66
183
|
padding-inline-end: var(--space-40);
|
|
184
|
+
white-space: normal;
|
|
67
185
|
}
|
|
68
186
|
|
|
69
187
|
.tab:is(:disabled, [aria-disabled="true"]) {
|
|
@@ -94,8 +212,13 @@
|
|
|
94
212
|
border-end-end-radius: 0;
|
|
95
213
|
}
|
|
96
214
|
|
|
215
|
+
/* A link tab is current on aria-current="page". "true" is for the one place
|
|
216
|
+
where no page token fits: a navigation flyout trigger whose panel holds the
|
|
217
|
+
current page. State selectors name their values, never the attribute's
|
|
218
|
+
presence, so "false" (what React and Vue serialize for a false binding)
|
|
219
|
+
stays inert. */
|
|
97
220
|
.tab[aria-selected="true"],
|
|
98
|
-
.tab[aria-current="page"] {
|
|
221
|
+
.tab:is([aria-current="page"], [aria-current="true"]) {
|
|
99
222
|
color: var(--intent, var(--state-selected));
|
|
100
223
|
}
|
|
101
224
|
|
|
@@ -104,7 +227,7 @@
|
|
|
104
227
|
forced colors and erase the distinction. currentColor follows the palette the
|
|
105
228
|
UA forces, so no forced-colors block is needed. */
|
|
106
229
|
.tab[aria-selected="true"]::after,
|
|
107
|
-
.tab[aria-current="page"]::after {
|
|
230
|
+
.tab:is([aria-current="page"], [aria-current="true"])::after {
|
|
108
231
|
content: "";
|
|
109
232
|
position: absolute;
|
|
110
233
|
inset-inline: 0;
|
|
@@ -113,9 +236,16 @@
|
|
|
113
236
|
}
|
|
114
237
|
|
|
115
238
|
.tabs:where([aria-orientation="vertical"]) .tab[aria-selected="true"]::after,
|
|
116
|
-
.tabs:where([aria-orientation="vertical"])
|
|
239
|
+
.tabs:where([aria-orientation="vertical"])
|
|
240
|
+
.tab:is([aria-current="page"], [aria-current="true"])::after {
|
|
117
241
|
inset-block: 0;
|
|
118
242
|
inset-inline: auto 0;
|
|
119
243
|
border-block-end: 0;
|
|
120
244
|
border-inline-end: 2px solid currentColor;
|
|
121
245
|
}
|
|
246
|
+
|
|
247
|
+
/* A shared width (--tab-flex) lands on the <li> of a link tab; the tab fills
|
|
248
|
+
it, so the indicator and the hit area span the whole share. */
|
|
249
|
+
.tabs > li > .tab {
|
|
250
|
+
inline-size: 100%;
|
|
251
|
+
}
|
package/src/css/core/focus.css
CHANGED
|
@@ -21,14 +21,16 @@
|
|
|
21
21
|
Documented exception: an invalid field focuses in --form-invalid-border
|
|
22
22
|
(danger), so the error stays visible at the point of correction. The border
|
|
23
23
|
carries the same hook; report:theme-contrast measures that line at 3:1
|
|
24
|
-
against the field's background alongside --focus.
|
|
24
|
+
against the field's background alongside --focus.
|
|
25
|
+
|
|
26
|
+
Public hooks:
|
|
27
|
+
--focus-outline (fallback-only: replaces the whole generic line)
|
|
28
|
+
*/
|
|
25
29
|
:focus {
|
|
26
|
-
outline: var(--focus-outline);
|
|
30
|
+
outline: var(--focus-outline, var(--focus-ring-width) solid var(--focus-outline-color));
|
|
27
31
|
outline-offset: var(--focus-outline-offset);
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
:
|
|
32
|
-
outline: none;
|
|
33
|
-
}
|
|
34
|
+
:focus:not(:focus-visible) {
|
|
35
|
+
outline: none;
|
|
34
36
|
}
|
package/src/css/core/theme.css
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* Derived: "Theme-derived aliases", Tone subsystem, Backdrop fill — reference
|
|
9
9
|
* other tokens; hard-coded overrides on a custom theme will break the intent.
|
|
10
10
|
* Exception: --surface-opaque and --surface-solid-fg are public; a theme with
|
|
11
|
-
* translucent surfaces overrides them with opaque values.
|
|
11
|
+
* translucent surfaces overrides them with opaque values. --border-control is
|
|
12
|
+
* public too: the resting edge of a neutral control, apart from structural
|
|
13
|
+
* separators.
|
|
12
14
|
* Internal: --hgroup-gap.
|
|
13
15
|
*/
|
|
14
16
|
:root {
|
|
@@ -110,6 +112,9 @@
|
|
|
110
112
|
never a fill. --surface-solid-fg is the ink on --surface-solid. */
|
|
111
113
|
--surface-opaque: var(--surface);
|
|
112
114
|
--surface-solid-fg: var(--surface);
|
|
115
|
+
/* The edge that tells a control apart from the surface, where --border only
|
|
116
|
+
separates surfaces: fields, choices, switch, OTP cells, choice cards. */
|
|
117
|
+
--border-control: var(--border);
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
/* Soft foreground hooks — the soft ink is the intent rebated toward the local
|
package/src/css/core/tokens.css
CHANGED
|
@@ -81,9 +81,11 @@
|
|
|
81
81
|
without a component ring (links, summary, plain items). Components draw
|
|
82
82
|
the themed ring instead: --focus-ring-width solid --focus. See focus.css
|
|
83
83
|
for the two contracts. Both share --focus-ring-width: they differ in
|
|
84
|
-
color only.
|
|
84
|
+
color only. No --focus-outline alias here: declared on :root it would be
|
|
85
|
+
inherited already computed, so a theme island or a region setting
|
|
86
|
+
--focus-ring-width would not reach it. focus.css composes the line on the
|
|
87
|
+
focused element instead. */
|
|
85
88
|
--focus-outline-color: currentColor;
|
|
86
|
-
--focus-outline: var(--focus-ring-width) solid var(--focus-outline-color);
|
|
87
89
|
--focus-outline-offset: 2px;
|
|
88
90
|
--focus-ring-width: 2px;
|
|
89
91
|
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
--choice-card-pad: var(--density-space, var(--space-40));
|
|
39
39
|
--choice-card-gap: var(--space-30);
|
|
40
40
|
--choice-card-radius: var(--radius);
|
|
41
|
-
--choice-card-border: var(--border);
|
|
41
|
+
--choice-card-border: var(--border-control);
|
|
42
42
|
--choice-card-bg: var(--surface);
|
|
43
43
|
--choice-card-check-size: 1.25em;
|
|
44
44
|
|
package/src/css/forms/choice.css
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* messages stay in the surrounding .field, not inside the cell, so the
|
|
10
10
|
* resting label can center on the control alone.
|
|
11
11
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* The cell reads its control's state through :has(), so the label may come
|
|
13
|
+
* before or after the control (a form theme rendering label then widget needs
|
|
14
|
+
* no reordering). Injected sibling nodes are tolerated (password managers,
|
|
15
|
+
* extension wrappers) as long as the control stays a direct child; the cell
|
|
16
|
+
* contract stays a single control plus one label, since a second control
|
|
17
|
+
* would float the label on either one's state.
|
|
18
|
+
* Every .input/.textarea needs
|
|
16
19
|
* placeholder=" " to expose a reliable empty state — without it the label
|
|
17
20
|
* only reacts to focus.
|
|
18
21
|
*
|
|
@@ -62,23 +65,22 @@
|
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/* Single-line controls rest in the middle until they receive a value. */
|
|
65
|
-
.floating-field
|
|
68
|
+
.floating-field:has(> :is(.input, .select)) > .field-label {
|
|
66
69
|
inset-block-start: 50%;
|
|
67
70
|
transform: translateY(-50%);
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
/* An empty textarea label follows the control's normal first-line position;
|
|
71
74
|
the control itself keeps the extra headroom reserved for its floated state. */
|
|
72
|
-
.floating-field
|
|
75
|
+
.floating-field:has(> .textarea) > .field-label {
|
|
73
76
|
inset-block-start: calc(1em + var(--border-width));
|
|
74
77
|
transform: none;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
/* :autofill covers prefilled values before interaction, when the browser may
|
|
78
81
|
not have propagated the value to the placeholder state yet. */
|
|
79
|
-
.floating-field
|
|
80
|
-
|
|
81
|
-
~ .field-label {
|
|
82
|
+
.floating-field:has(> :is(.input, .textarea):is(:focus, :autofill, :not(:placeholder-shown)))
|
|
83
|
+
> .field-label {
|
|
82
84
|
inset-block-start: var(--floating-label-top);
|
|
83
85
|
transform: none;
|
|
84
86
|
font-size: var(--font-size-xs);
|
|
@@ -88,16 +90,16 @@
|
|
|
88
90
|
/* Controls without a dependable :placeholder-shown empty state stay floated:
|
|
89
91
|
select always, and date/time inputs whose placeholder rendering is
|
|
90
92
|
inconsistent across engines (Firefox shows none). */
|
|
91
|
-
.floating-field
|
|
93
|
+
.floating-field:has(
|
|
92
94
|
> :is(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
.select,
|
|
96
|
+
.input[type="date"],
|
|
97
|
+
.input[type="time"],
|
|
98
|
+
.input[type="datetime-local"],
|
|
99
|
+
.input[type="month"],
|
|
100
|
+
.input[type="week"]
|
|
101
|
+
)
|
|
102
|
+
) > .field-label {
|
|
101
103
|
inset-block-start: var(--floating-label-top);
|
|
102
104
|
transform: none;
|
|
103
105
|
font-size: var(--font-size-xs);
|
|
@@ -21,15 +21,12 @@
|
|
|
21
21
|
* --form-actions-sticky-padding (owned by .form-actions.sticky)
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
/* The policy
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
forms load after layout in the official bundle (tests/css-audit.test.js
|
|
28
|
-
guards the order); utilities load later and keep the author's last word. On
|
|
29
|
-
this row --cluster-justify/--cluster-align have no effect. */
|
|
24
|
+
/* The role relays its policy into the row's cluster hooks. Cluster hooks stop
|
|
25
|
+
at every nested .cluster, so the relay cannot leak, and this class beats the
|
|
26
|
+
cluster's zero-specificity reset in any import order. */
|
|
30
27
|
.form-actions {
|
|
31
|
-
align
|
|
32
|
-
justify
|
|
28
|
+
--cluster-align: var(--form-actions-align, center);
|
|
29
|
+
--cluster-justify: var(--form-actions-justify, flex-start);
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
/* Opt-in sticky variant. When the bar is not stuck (at rest) the translucent
|
package/src/css/forms/native.css
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
calc((var(--control-size) - 1lh - 2 * var(--border-width)) / 2)
|
|
26
26
|
);
|
|
27
27
|
padding-inline: var(--space-20);
|
|
28
|
-
border: var(--border-width) solid var(--border);
|
|
28
|
+
border: var(--border-width) solid var(--border-control);
|
|
29
29
|
border-radius: var(--radius-sm);
|
|
30
30
|
background: var(--surface-subtle);
|
|
31
31
|
color: var(--text);
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
inline-size: var(--control-size);
|
|
60
60
|
block-size: var(--control-size);
|
|
61
61
|
padding: var(--border-width);
|
|
62
|
-
border: var(--border-width) solid var(--border);
|
|
62
|
+
border: var(--border-width) solid var(--border-control);
|
|
63
63
|
border-radius: var(--radius);
|
|
64
64
|
background: var(--surface);
|
|
65
65
|
cursor: pointer;
|
package/src/css/forms/otp.css
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
--otp-cell-size: var(--control-size);
|
|
22
22
|
--otp-gap: var(--space-10);
|
|
23
23
|
--otp-font-size: 1.5em;
|
|
24
|
-
--otp-cell-border: var(--border);
|
|
24
|
+
--otp-cell-border: var(--border-control);
|
|
25
25
|
--otp-cell-bg: var(--surface);
|
|
26
26
|
--otp-caret-space: calc((var(--otp-cell-size) - 1ch) / 2 + var(--otp-gap) + 1px);
|
|
27
27
|
|
|
@@ -107,10 +107,9 @@
|
|
|
107
107
|
--otp-cell-border: var(--danger);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
110
|
+
/* biome-ignore lint/style/noDescendingSpecificity: all invalid routes write the same border, so their order cannot change the state. Keep :user-invalid separate so unsupported engines retain the other routes. */
|
|
111
|
+
.needs-validation .otp > input:user-invalid ~ span {
|
|
112
|
+
--otp-cell-border: var(--danger);
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
.otp > input:disabled {
|
package/src/css/forms/switch.css
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
.switch {
|
|
31
31
|
--switch-bg: var(--border);
|
|
32
|
-
--switch-border: var(--border);
|
|
32
|
+
--switch-border: var(--border-control);
|
|
33
33
|
--switch-knob: var(--surface-raised);
|
|
34
34
|
--switch-block-size: calc(var(--choice-control-size) + 0.125em);
|
|
35
35
|
--switch-width: calc(var(--switch-block-size) * 1.8);
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Imported by default through forms/index.css. Custom builds may omit this
|
|
7
7
|
* file when an app wants to own validation styling completely.
|
|
8
|
+
* Without :has(), automatic error display and choice-card borders drop;
|
|
9
|
+
* native control states and the explicit .field.danger route stay available.
|
|
8
10
|
*
|
|
9
11
|
* State coverage (which states each component ships) is audited in
|
|
10
12
|
* tests/validation.test.js and summarized in docs/pages/forms/validation.md —
|
|
@@ -26,11 +28,14 @@
|
|
|
26
28
|
display: block;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
.field:has([aria-invalid="true"]) > .field-error,
|
|
32
|
+
.needs-validation.was-validated .field:has(:invalid) > .field-error {
|
|
33
|
+
display: block;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* biome-ignore lint/style/noDescendingSpecificity: every invalid route reveals the error; keep :user-invalid separate so unsupported engines retain the other routes. */
|
|
37
|
+
.needs-validation .field:has(:user-invalid) > .field-error {
|
|
38
|
+
display: block;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
:where(.input, .textarea, .select, .check, .radio, .switch)[aria-invalid="true"],
|
|
@@ -54,39 +59,27 @@
|
|
|
54
59
|
--switch-bg: var(--danger);
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
+
.needs-validation .check:is(:checked, :indeterminate):user-invalid,
|
|
63
|
+
.needs-validation .radio:checked:user-invalid {
|
|
64
|
+
--choice-bg: var(--danger);
|
|
65
|
+
}
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
67
|
+
.needs-validation .switch:checked:user-invalid {
|
|
68
|
+
--switch-bg: var(--danger);
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
/* Choice cards hide their native input, so invalid state must surface on the
|
|
69
72
|
card itself. Sets the same --form-invalid-border hook the card's border
|
|
70
73
|
consumes. */
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
--form-invalid-border: var(--danger);
|
|
75
|
-
}
|
|
74
|
+
.choice-card:has([aria-invalid="true"]),
|
|
75
|
+
.needs-validation.was-validated .choice-card:has(:invalid) {
|
|
76
|
+
--form-invalid-border: var(--danger);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
--form-invalid-border: var(--danger);
|
|
81
|
-
}
|
|
79
|
+
.needs-validation :is(.input, .textarea, .select, .check, .radio, .switch):user-invalid {
|
|
80
|
+
--form-invalid-border: var(--danger);
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
display: block;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.needs-validation .choice-card:has(:user-invalid) {
|
|
90
|
-
--form-invalid-border: var(--danger);
|
|
91
|
-
}
|
|
83
|
+
.needs-validation .choice-card:has(:user-invalid) {
|
|
84
|
+
--form-invalid-border: var(--danger);
|
|
92
85
|
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* --center-size
|
|
8
8
|
* --center-pad
|
|
9
9
|
*/
|
|
10
|
+
/* The hooks inherit on purpose: a site's content width is page policy, so a
|
|
11
|
+
theme sets them once on :root for every .center. */
|
|
10
12
|
.center {
|
|
11
13
|
box-sizing: content-box;
|
|
12
14
|
inline-size: calc(100% - var(--center-pad, 1rem) * 2);
|
|
@@ -10,7 +10,21 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Child contract: direct-child margins are reset, spacing comes from --gap.
|
|
12
12
|
* A ul/ol/menu keeps its semantics but loses list chrome (see below).
|
|
13
|
+
*
|
|
14
|
+
* The hooks describe this row's own geometry, so they stop at each cluster:
|
|
15
|
+
* set them on the cluster itself, or target nested rows with a selector. The
|
|
16
|
+
* gap token is context rhythm and keeps inheriting.
|
|
13
17
|
*/
|
|
18
|
+
|
|
19
|
+
/* Instance boundary: an ancestor's value never reaches a nested cluster.
|
|
20
|
+
Zero specificity, so any class or inline style on the cluster wins without
|
|
21
|
+
depending on import order; `initial` makes the var() fallbacks apply. */
|
|
22
|
+
:where(.cluster) {
|
|
23
|
+
--cluster-align: initial;
|
|
24
|
+
--cluster-justify: initial;
|
|
25
|
+
--cluster-wrap: initial;
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
.cluster {
|
|
15
29
|
display: flex;
|
|
16
30
|
flex-direction: row;
|
package/src/css/layout/grid.css
CHANGED
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
* the thresholds were arrived at: docs/design-notes/grid.md.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
/* Instance boundary: a nested grid never takes its parent's track template.
|
|
26
|
+
Zero specificity, so a class or inline style on the grid itself wins. */
|
|
27
|
+
:where(.grid) {
|
|
28
|
+
--grid-columns: initial;
|
|
29
|
+
--grid-min: initial;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
/* --grid-columns replaces the intrinsic auto-fit template: once set, the grid
|
|
26
33
|
no longer collapses on its own and the author owns narrow-container
|
|
27
34
|
collapse. */
|
|
@@ -13,7 +13,16 @@
|
|
|
13
13
|
* draw their native scrollbar — the intended fallback, not a broken state.
|
|
14
14
|
*
|
|
15
15
|
* Styles the scrollbar only; it does not create the overflow. Pair it with
|
|
16
|
-
* .overflow-auto or a component that already scrolls
|
|
16
|
+
* .overflow-auto or a component that already scrolls (horizontal steps, a
|
|
17
|
+
* table wrap; tabs hide their bar and fade their own edges). No edge fade
|
|
18
|
+
* here: a mask would dictate the strip's scroll-padding and focus geometry for
|
|
19
|
+
* a decorative cue, and an item clipped at the edge already says the strip
|
|
20
|
+
* continues.
|
|
21
|
+
*
|
|
22
|
+
* Not on touch: Chromium trades the overlay scrollbar of a touch screen,
|
|
23
|
+
* hidden at rest, for a classic always-visible one as soon as scrollbar-color
|
|
24
|
+
* or scrollbar-width is set. On a phone that painted a permanent bar under
|
|
25
|
+
* every strip; the native overlay is the better cue there.
|
|
17
26
|
*
|
|
18
27
|
* Public hooks:
|
|
19
28
|
* --scroller-track
|
|
@@ -22,9 +31,16 @@
|
|
|
22
31
|
.scroller {
|
|
23
32
|
--scroller-track: transparent;
|
|
24
33
|
--scroller-thumb: var(--border);
|
|
34
|
+
}
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
/* Any primary pointer but touch: a keyboard-only or pointerless device
|
|
37
|
+
(pointer: none) draws classic scrollbars too. `all and` keeps the negation
|
|
38
|
+
valid in Media Queries 3 parsers. */
|
|
39
|
+
@media not all and (pointer: coarse) {
|
|
40
|
+
.scroller {
|
|
41
|
+
scrollbar-color: var(--scroller-thumb) var(--scroller-track);
|
|
42
|
+
scrollbar-width: thin;
|
|
43
|
+
}
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
/* Opt-in gutter: reserves space so the layout does not shift when
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* visual, reading and keyboard order all agree — the role rules swap so the
|
|
18
18
|
* aside still takes its preferred width and the main still reclaims the row.
|
|
19
19
|
*/
|
|
20
|
+
/* No instance reset, unlike .cluster and .grid: the regions read the hooks,
|
|
21
|
+
and a layout nested as a region takes its own size from its parent's
|
|
22
|
+
values, so the hooks must reach it. */
|
|
20
23
|
.sidebar-layout {
|
|
21
24
|
display: flex;
|
|
22
25
|
flex-direction: row;
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Child contract: every direct child is a peer; the flex-basis applies to all.
|
|
14
14
|
*/
|
|
15
|
+
/* No instance reset, unlike .cluster and .grid: the children read the hook,
|
|
16
|
+
and a switcher nested as a direct child takes its own flex-basis from its
|
|
17
|
+
parent's value, so the hook must reach it. */
|
|
15
18
|
.switcher {
|
|
16
19
|
display: flex;
|
|
17
20
|
flex-direction: row;
|
|
@@ -84,9 +84,9 @@
|
|
|
84
84
|
flex-wrap: nowrap;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/* The property is written directly,
|
|
88
|
-
a
|
|
89
|
-
|
|
87
|
+
/* The property is written directly, so the utility works on any flex row, not
|
|
88
|
+
only a .cluster. Utilities load last, so the direct declaration beats
|
|
89
|
+
.cluster at equal specificity. */
|
|
90
90
|
.justify-content-start {
|
|
91
91
|
justify-content: flex-start;
|
|
92
92
|
}
|
package/src/js/full.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Each module self-registers via observer: importing this bundle enables
|
|
5
5
|
* common behaviors. No DOMContentLoaded ceremony, no init calls.
|
|
6
6
|
* Injected content wires automatically; removal cleans up automatically.
|
|
7
|
+
* The entry also exposes the public enhancement API from the same module
|
|
8
|
+
* graph, so adopters extend the runtime they already run.
|
|
7
9
|
*
|
|
8
10
|
* A plain `<script src="actual.full.js"></script>` (no bundler, no import) still
|
|
9
11
|
* gets the full runtime, including declarative triggers like status's
|
|
@@ -25,3 +27,5 @@ import "./range.js";
|
|
|
25
27
|
import "./validation.js";
|
|
26
28
|
import "./status.js";
|
|
27
29
|
import "./index.js";
|
|
30
|
+
|
|
31
|
+
export { applyEnhancement, default as enhance, registerEnhancement } from "./enhance.js";
|