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
|
@@ -4,14 +4,21 @@
|
|
|
4
4
|
* Docs: docs/pages/components/breadcrumb.md
|
|
5
5
|
*
|
|
6
6
|
* Inline list separated by "/". Mark the current page with `aria-current="page"`
|
|
7
|
-
* either on the item or on the current link.
|
|
7
|
+
* either on the item or on the current link. Ancestor labels sit in an <a> or a
|
|
8
|
+
* <span>, the box that truncates.
|
|
9
|
+
*
|
|
10
|
+
* Breadcrumbs are a single navigation hierarchy. Wrapping makes hierarchy look
|
|
11
|
+
* like separate rows and causes unstable header height, and scrolling hides the
|
|
12
|
+
* root behind a scrollbar. A trail that does not fit truncates instead:
|
|
13
|
+
* ancestors first, the current page last, each label behind an ellipsis.
|
|
8
14
|
*/
|
|
15
|
+
|
|
9
16
|
.breadcrumb {
|
|
10
17
|
display: flex;
|
|
11
|
-
flex-
|
|
12
|
-
flex-wrap: wrap;
|
|
18
|
+
flex-wrap: nowrap;
|
|
13
19
|
align-items: center;
|
|
14
20
|
gap: var(--space-10);
|
|
21
|
+
min-inline-size: 0;
|
|
15
22
|
margin: 0;
|
|
16
23
|
padding: 0;
|
|
17
24
|
color: var(--text-subtle);
|
|
@@ -19,15 +26,74 @@
|
|
|
19
26
|
list-style: none;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
/* The landmark around the trail must shrink with it in a flex or grid row, or
|
|
30
|
+
it keeps the trail's full width and the labels never get to truncate.
|
|
31
|
+
Without :has() the landmark keeps its width; min-inline-size: 0 on the
|
|
32
|
+
wrapper is the fallback. */
|
|
33
|
+
:where(nav:has(> .breadcrumb)) {
|
|
34
|
+
min-inline-size: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Grid, not flex: under a min-content constraint a grid track keeps its base
|
|
38
|
+
size, which is the label's minimum below, so the item's own minimum is
|
|
39
|
+
separator + label floor without anyone measuring a "/". A flex item would
|
|
40
|
+
report the full label as its minimum and never shrink.
|
|
41
|
+
|
|
42
|
+
Ancestors take the shrink before the current page, so the location is the
|
|
43
|
+
last thing to lose characters. The ratio is deliberately huge: at 1000 the
|
|
44
|
+
current page still took a fraction of a pixel, enough to trigger its
|
|
45
|
+
ellipsis while ancestors had room. Its own factor stays 1, because flex
|
|
46
|
+
distributes only a share of the deficit when the unfrozen factors sum below
|
|
47
|
+
one. Keyed on the last item, the current page by convention, so the order
|
|
48
|
+
holds without :has(). */
|
|
49
|
+
.breadcrumb > li {
|
|
50
|
+
display: inline-grid;
|
|
51
|
+
flex: 0 1000000 auto;
|
|
52
|
+
grid-auto-flow: column;
|
|
24
53
|
align-items: center;
|
|
25
|
-
|
|
54
|
+
white-space: nowrap;
|
|
26
55
|
}
|
|
27
56
|
|
|
28
|
-
.breadcrumb
|
|
29
|
-
|
|
57
|
+
.breadcrumb > li:last-child {
|
|
58
|
+
flex-shrink: 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* A margin rather than a grid gap, so the separator keeps its space when the
|
|
62
|
+
current page below lays it out inline. The weight stays normal there, where
|
|
63
|
+
it would inherit the current page's. */
|
|
64
|
+
.breadcrumb > li + li::before {
|
|
65
|
+
margin-inline-end: var(--space-10);
|
|
30
66
|
color: var(--text-subtle);
|
|
67
|
+
font-weight: var(--font-weight-normal);
|
|
68
|
+
content: "/";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* The ellipsis lives on the label, not on the item: an element never clips
|
|
72
|
+
its own outline, but an overflow-hidden item would clip its link's focus
|
|
73
|
+
line, which sits outside the link box.
|
|
74
|
+
|
|
75
|
+
The floor keeps a truncated label readable as a step ("Po…") instead of
|
|
76
|
+
collapsing to its separator. calc-size() lets a shorter label keep its own
|
|
77
|
+
width; without it, the plain 2em floor leaves a label under 2em a little
|
|
78
|
+
trailing room. */
|
|
79
|
+
.breadcrumb > li > :is(a, span) {
|
|
80
|
+
min-inline-size: 2em;
|
|
81
|
+
min-inline-size: calc-size(max-content, min(size, 2em));
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
text-overflow: ellipsis;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* The current page written as bare text has no label box, so its item
|
|
87
|
+
truncates itself. Only with no element inside: an aria-current item that
|
|
88
|
+
holds a link would clip that link's focus line, and keeps the label rule
|
|
89
|
+
above instead. Its own rule, never merged with the one above: a browser
|
|
90
|
+
without :has() drops it alone, and bare text then keeps its full width. */
|
|
91
|
+
.breadcrumb > li[aria-current="page"]:not(:has(> *)) {
|
|
92
|
+
display: block;
|
|
93
|
+
min-inline-size: 2em;
|
|
94
|
+
min-inline-size: calc-size(max-content, min(size, 2em));
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
text-overflow: ellipsis;
|
|
31
97
|
}
|
|
32
98
|
|
|
33
99
|
.breadcrumb a {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* --drawer-close-reserve (header room for a corner close; zero without a close)
|
|
26
26
|
* --drawer-motion-duration (shared by panel and ::backdrop so exit duration
|
|
27
27
|
* changes in one place; entry state redefines it longer)
|
|
28
|
+
* --drawer-motion-ease (panel easing owned by the open/closed state)
|
|
28
29
|
*/
|
|
29
30
|
:where(dialog).drawer {
|
|
30
31
|
--drawer-size: 20rem;
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
/* Exit is shorter than entry so the panel clears quickly; the scrim reads
|
|
40
41
|
the same variable through inheritance and never lags the panel. */
|
|
41
42
|
--drawer-motion-duration: var(--duration);
|
|
43
|
+
--drawer-motion-ease: var(--ease-exit);
|
|
42
44
|
|
|
43
45
|
/* Keep the surface anchored to the viewport in the top layer. This also
|
|
44
46
|
provides the containing block for the absolutely positioned close button.
|
|
@@ -70,21 +72,21 @@
|
|
|
70
72
|
box-shadow: var(--shadow);
|
|
71
73
|
opacity: 0;
|
|
72
74
|
transform: translateX(var(--drawer-closed-translate));
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
/* Unsupported discrete behavior drops alone, preserving opacity/transform. */
|
|
76
|
+
transition-property: opacity, transform, overlay, display;
|
|
77
|
+
transition-duration: var(--drawer-motion-duration);
|
|
78
|
+
transition-timing-function: var(--drawer-motion-ease);
|
|
79
|
+
transition-behavior: allow-discrete;
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
:where(dialog).drawer[open] {
|
|
79
83
|
--drawer-motion-duration: var(--duration-slow);
|
|
84
|
+
--drawer-motion-ease: var(--ease-enter);
|
|
80
85
|
display: grid;
|
|
81
86
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
82
87
|
gap: var(--space-40);
|
|
83
88
|
opacity: 1;
|
|
84
89
|
transform: translateX(0);
|
|
85
|
-
transition:
|
|
86
|
-
opacity var(--drawer-motion-duration) var(--ease-enter),
|
|
87
|
-
transform var(--drawer-motion-duration) var(--ease-enter);
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
/* A form is the drawer's content wrapper when actions need method="dialog".
|
|
@@ -104,7 +106,10 @@
|
|
|
104
106
|
opacity: 0;
|
|
105
107
|
/* Scrim curve stays neutral `ease`; only the panel's character curves
|
|
106
108
|
apply. Duration follows --drawer-motion-duration through inheritance. */
|
|
107
|
-
transition: opacity
|
|
109
|
+
transition-property: opacity, overlay, display;
|
|
110
|
+
transition-duration: var(--drawer-motion-duration);
|
|
111
|
+
transition-timing-function: ease;
|
|
112
|
+
transition-behavior: allow-discrete;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
:where(dialog).drawer[data-side="end"] {
|
|
@@ -138,47 +143,23 @@
|
|
|
138
143
|
exclusion list is easier to trust than its `position: absolute`. */
|
|
139
144
|
:where(dialog).drawer > :not(:is(header, footer, form, .close)),
|
|
140
145
|
:where(dialog).drawer > form > :not(:is(header, footer, .close)) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
);
|
|
146
|
+
/* Declared here, not as a :root alias: an inherited alias arrives already
|
|
147
|
+
computed, so a --focus-ring-width override on the drawer would not reach
|
|
148
|
+
it. */
|
|
149
|
+
--drawer-focus-bleed: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
145
150
|
|
|
146
151
|
min-block-size: 0;
|
|
147
|
-
/* Keep focus paint inside the clip
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
/* Keep focus paint inside the clip, on all four sides (overflow: auto clips
|
|
153
|
+
both axes), without moving the content. Every outside indicator is
|
|
154
|
+
--focus-ring-width wide (core/focus.css). On the block axis the reserve
|
|
155
|
+
reaches into the slot gap, which is wider, so the scrollport never slides
|
|
156
|
+
under the header or footer. */
|
|
157
|
+
margin: calc(var(--drawer-focus-bleed) * -1);
|
|
158
|
+
padding: var(--drawer-focus-bleed);
|
|
153
159
|
overflow: auto;
|
|
154
160
|
overscroll-behavior: contain;
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
@supports (transition-behavior: allow-discrete) {
|
|
158
|
-
:where(dialog).drawer {
|
|
159
|
-
transition:
|
|
160
|
-
opacity var(--drawer-motion-duration) var(--ease-exit),
|
|
161
|
-
transform var(--drawer-motion-duration) var(--ease-exit),
|
|
162
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
163
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
:where(dialog).drawer[open] {
|
|
167
|
-
transition:
|
|
168
|
-
opacity var(--drawer-motion-duration) var(--ease-enter),
|
|
169
|
-
transform var(--drawer-motion-duration) var(--ease-enter),
|
|
170
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
171
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
:where(dialog).drawer::backdrop {
|
|
175
|
-
transition:
|
|
176
|
-
opacity var(--drawer-motion-duration) ease,
|
|
177
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
178
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
163
|
/* Safari before 16.4 does not support :dir(); keep the ancestor-attribute
|
|
183
164
|
path as the Degraded-tier fallback. The :dir() rules are dropped on their
|
|
184
165
|
own there, so they need no wrapper. :dir() follows inherited HTML
|
|
@@ -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,10 @@
|
|
|
43
43
|
.list > :where(.list-item),
|
|
44
44
|
.list > li > .list-item {
|
|
45
45
|
display: flex;
|
|
46
|
-
/*
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
/* Centered fits the short rows lists mostly hold. Read through
|
|
47
|
+
--items-align so .items-start on the row or the list wins without
|
|
48
|
+
outranking this selector — the opt-out for long supporting text. */
|
|
49
|
+
align-items: var(--items-align, center);
|
|
49
50
|
gap: var(--list-item-gap);
|
|
50
51
|
min-block-size: var(--list-item-min-size);
|
|
51
52
|
padding-block: var(--list-item-pad-block);
|
|
@@ -67,21 +68,20 @@ a.list-item:hover {
|
|
|
67
68
|
background: var(--surface-subtle);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
/* Current row in a navigational list
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
…) stays free for assistive technology.
|
|
71
|
+
/* Current row in a navigational list: aria-current="page" when the row opens
|
|
72
|
+
a page, "true" when it is the current item of a master/detail view (a
|
|
73
|
+
conversation, a thread, a record) that is no page of its own. Any other
|
|
74
|
+
value, "false" included, is inert.
|
|
75
75
|
Calm by design: the row keeps its normal text metrics and color on a subtle
|
|
76
76
|
surface, and the selected accent lives on an inset inline-start trait only.
|
|
77
77
|
The trait is a real border, so it survives forced colors without a repaint
|
|
78
78
|
block. The positioning context lives on every row link, so becoming current
|
|
79
79
|
never moves an app-positioned decoration to a new containing block. */
|
|
80
|
-
a.list-item[aria-current] {
|
|
80
|
+
a.list-item:is([aria-current="page"], [aria-current="true"]) {
|
|
81
81
|
background: var(--surface-subtle);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
a.list-item[aria-current]::before {
|
|
84
|
+
a.list-item:is([aria-current="page"], [aria-current="true"])::before {
|
|
85
85
|
content: "";
|
|
86
86
|
position: absolute;
|
|
87
87
|
inset-block: var(--list-item-pad-block);
|
|
@@ -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
|
}
|
|
@@ -71,8 +71,7 @@
|
|
|
71
71
|
|
|
72
72
|
/* nav-link serves two contracts: plain page navigation (aria-current="page")
|
|
73
73
|
and scrollspy, which toggles aria-current="location" on the active link.
|
|
74
|
-
|
|
75
|
-
removed everywhere else, never serialized as aria-current="false".
|
|
74
|
+
Those two values are the contract; any other, "false" included, is inert.
|
|
76
75
|
Selection never changes text metrics: the shared medium weight is kept and
|
|
77
76
|
the current link is carried by the selected accent. */
|
|
78
77
|
.nav-link:hover {
|
|
@@ -83,7 +82,7 @@
|
|
|
83
82
|
/* Current last, equal specificity to :hover (0,2,0): a hovered current link
|
|
84
83
|
keeps the selected accent instead of dropping to --text. The surface fill is
|
|
85
84
|
shared with the hover rule above. */
|
|
86
|
-
.nav-link[aria-current] {
|
|
85
|
+
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
87
86
|
background: var(--surface-subtle);
|
|
88
87
|
color: var(--state-selected);
|
|
89
88
|
}
|
|
@@ -96,7 +95,7 @@
|
|
|
96
95
|
above keeps the light chip where color-mix is missing, with ink that is
|
|
97
96
|
legible on it. */
|
|
98
97
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
99
|
-
.nav-link:is(:hover, [aria-current]) {
|
|
98
|
+
.nav-link:is(:hover, [aria-current="page"], [aria-current="location"]) {
|
|
100
99
|
background: color-mix(in oklch, currentColor 10%, transparent);
|
|
101
100
|
}
|
|
102
101
|
|
|
@@ -105,7 +104,7 @@
|
|
|
105
104
|
}
|
|
106
105
|
|
|
107
106
|
/* After :hover so a hovered current link keeps the selected accent. */
|
|
108
|
-
.nav-link[aria-current] {
|
|
107
|
+
.nav-link:is([aria-current="page"], [aria-current="location"]) {
|
|
109
108
|
color: var(--ui-fg, var(--state-selected));
|
|
110
109
|
}
|
|
111
110
|
}
|
|
@@ -139,11 +138,14 @@
|
|
|
139
138
|
follows the forced palette without a forced-colors block and never touches
|
|
140
139
|
the row separators. Navbar horizontal links keep color + surface only. The
|
|
141
140
|
positioning context lives on every row link, so becoming current never moves
|
|
142
|
-
an app-positioned decoration to a new containing block.
|
|
143
|
-
|
|
141
|
+
an app-positioned decoration to a new containing block.
|
|
142
|
+
The trait sits centered in the link's inline padding with rounded ends: flush
|
|
143
|
+
on the edge, a square line read as pasted onto the rounded fill. */
|
|
144
|
+
.nav-list .nav-link:is([aria-current="page"], [aria-current="location"])::before {
|
|
144
145
|
content: "";
|
|
145
146
|
position: absolute;
|
|
146
147
|
inset-block: var(--space-20);
|
|
147
|
-
inset-inline-start:
|
|
148
|
+
inset-inline-start: calc((var(--space-20) - 2px) / 2);
|
|
148
149
|
border-inline-start: 2px solid currentColor;
|
|
150
|
+
border-radius: var(--radius-full);
|
|
149
151
|
}
|
|
@@ -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.
|
|
@@ -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
|
|
@@ -161,10 +162,11 @@
|
|
|
161
162
|
/* Horizontal ------------------------------------------------------------- */
|
|
162
163
|
|
|
163
164
|
.steps-horizontal {
|
|
164
|
-
--step-min:
|
|
165
|
+
--step-min: var(--step-size);
|
|
165
166
|
--step-gap: var(--space-20);
|
|
166
167
|
|
|
167
168
|
inline-size: 100%;
|
|
169
|
+
column-gap: var(--step-gap);
|
|
168
170
|
overflow-x: auto;
|
|
169
171
|
/* Stated, never left to `overflow-x` alone: one scrolling axis computes the
|
|
170
172
|
other to `auto`, and a row hugs fractional content closely enough that a
|
|
@@ -173,11 +175,22 @@
|
|
|
173
175
|
overscroll-behavior-inline: contain;
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
/* A row that loads mid-flow starts scrolled to its current step; engines
|
|
179
|
+
without the property start at the first. */
|
|
180
|
+
.steps-horizontal > [aria-current="step"] {
|
|
181
|
+
/* biome-ignore lint/correctness/noUnknownProperty: CSS Scroll Snap 2 property that Biome's list predates; an engine without it drops the declaration. */
|
|
182
|
+
scroll-initial-target: nearest;
|
|
183
|
+
}
|
|
184
|
+
|
|
176
185
|
/* 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
|
|
186
|
+
focus ring's own width and offset, and focus scrolls a link in that far from
|
|
187
|
+
the inline clip. Only this composition scrolls, and only a link has a ring to
|
|
188
|
+
protect. */
|
|
179
189
|
.steps-horizontal:has(a[href]) {
|
|
190
|
+
/* Wrapped names can fill their cell right up to either inline clip edge. */
|
|
191
|
+
padding-inline: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
180
192
|
padding-block: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
193
|
+
scroll-padding-inline: calc(var(--focus-outline-offset) + var(--focus-ring-width));
|
|
181
194
|
}
|
|
182
195
|
|
|
183
196
|
/* One explicit row, the marker's: a `.step-label` is auto-placed into an
|
|
@@ -189,6 +202,8 @@
|
|
|
189
202
|
.steps-horizontal > li {
|
|
190
203
|
display: grid;
|
|
191
204
|
flex: 1 0 var(--step-min);
|
|
205
|
+
/* Bound each label to its share so it cannot change the marker pitch. */
|
|
206
|
+
grid-template-columns: minmax(0, 1fr);
|
|
192
207
|
grid-template-rows: var(--step-size);
|
|
193
208
|
gap: var(--step-gap);
|
|
194
209
|
justify-items: center;
|
|
@@ -196,9 +211,42 @@
|
|
|
196
211
|
text-align: center;
|
|
197
212
|
}
|
|
198
213
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
214
|
+
.steps-horizontal .step-label {
|
|
215
|
+
/* Even an unbroken name must fit its share instead of overlapping a peer.
|
|
216
|
+
Hyphenation, when `lang` allows it, is tried before an arbitrary break. */
|
|
217
|
+
overflow-wrap: anywhere;
|
|
218
|
+
hyphens: auto;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Half end cells put the outer markers on the row's edges without changing
|
|
222
|
+
their pitch, so the track lines up with the text around it, as the compact
|
|
223
|
+
and inline forms do. The marker's half-width keeps the first connector equal
|
|
224
|
+
to its peers.
|
|
225
|
+
|
|
226
|
+
Source order is load-bearing: these tie the `:empty` rules below and the
|
|
227
|
+
inline query on specificity, and both must win over them. */
|
|
228
|
+
.steps-horizontal > li:is(:first-child, :last-child) {
|
|
229
|
+
flex: 0.5 0 calc((var(--step-min) + var(--step-size)) / 2);
|
|
230
|
+
min-inline-size: max(var(--step-size), calc((var(--step-min) + var(--step-size)) / 2));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.steps-horizontal > li:first-child {
|
|
234
|
+
justify-items: start;
|
|
235
|
+
text-align: start;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.steps-horizontal > li:first-child::after {
|
|
239
|
+
inset-inline-start: var(--step-size);
|
|
240
|
+
inline-size: calc(200% - var(--step-size) * 2 + var(--step-gap));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.steps-horizontal > li:last-child {
|
|
244
|
+
justify-items: end;
|
|
245
|
+
text-align: end;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* An empty step ignores any authored reading-width floor and needs only its
|
|
249
|
+
marker. That lets five of them fit a phone without a size context.
|
|
202
250
|
|
|
203
251
|
`justify-items` is not an override of the row's centring: the marker is
|
|
204
252
|
centred because a label sits under it and the two share a centre, and with no
|
|
@@ -208,9 +256,8 @@
|
|
|
208
256
|
the text around it, the way the inline representation does.
|
|
209
257
|
|
|
210
258
|
`: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
|
|
259
|
+
it reads the item's own content: bare text keeps the same authored floor as
|
|
260
|
+
its wrapped peers. `:empty` also keeps the compact form reachable on the
|
|
214
261
|
Degraded tier, where only the inline bonus needs `:has()`. */
|
|
215
262
|
.steps-horizontal > li:empty {
|
|
216
263
|
flex: 1 1 0;
|
|
@@ -220,6 +267,8 @@
|
|
|
220
267
|
|
|
221
268
|
.steps-horizontal > li:empty::after {
|
|
222
269
|
inset-inline-start: var(--step-size);
|
|
270
|
+
/* Marker-only end cells keep full shares, unlike labelled end cells. */
|
|
271
|
+
inline-size: calc(100% - var(--step-size) + var(--step-gap));
|
|
223
272
|
}
|
|
224
273
|
|
|
225
274
|
.steps-horizontal > li:empty:last-child {
|
|
@@ -227,12 +276,12 @@
|
|
|
227
276
|
}
|
|
228
277
|
|
|
229
278
|
/* The pseudo-element is marker-height so --step-connector may be any complete
|
|
230
|
-
background value. */
|
|
279
|
+
background value; its length also crosses the gap to the next equal track. */
|
|
231
280
|
.steps-horizontal > li::after {
|
|
232
281
|
position: absolute;
|
|
233
282
|
inset-block-start: 0;
|
|
234
283
|
inset-inline-start: calc(50% + var(--step-size) / 2);
|
|
235
|
-
inline-size: calc(100% - var(--step-size));
|
|
284
|
+
inline-size: calc(100% - var(--step-size) + var(--step-gap));
|
|
236
285
|
block-size: var(--step-size);
|
|
237
286
|
background: var(
|
|
238
287
|
--step-connector,
|
|
@@ -251,6 +300,11 @@
|
|
|
251
300
|
* written — a row out of contract is not owed a curated fallback.
|
|
252
301
|
*/
|
|
253
302
|
@container actual-container (inline-size >= 60rem) {
|
|
303
|
+
.steps-horizontal:has(.step-label) {
|
|
304
|
+
/* Inline connectors already own the spacing between marker-label groups. */
|
|
305
|
+
column-gap: 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
254
308
|
.steps-horizontal:has(.step-label) > li {
|
|
255
309
|
display: flex;
|
|
256
310
|
flex: 1 1 auto;
|