actual-css 0.11.1 → 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 +96 -0
- package/README.md +25 -6
- package/components.json +12 -18
- package/dist/actual.full.css +780 -479
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/brand/favicon.svg +5 -0
- package/docs/brand/lockup.svg +12 -0
- package/docs/brand/logo-dark.svg +9 -0
- package/docs/brand/logo-light.svg +9 -0
- package/docs/brand/logo.svg +11 -0
- package/docs/brand/mark.svg +11 -0
- package/docs/design-notes/architecture.md +67 -0
- package/docs/design-notes/steps.md +45 -28
- package/docs/navigation.json +3 -2
- package/docs/pages/components/alert.md +2 -10
- package/docs/pages/components/avatar.md +17 -11
- package/docs/pages/components/badge.md +3 -9
- package/docs/pages/components/breadcrumb.md +16 -2
- package/docs/pages/components/card.md +31 -2
- package/docs/pages/components/dialog.md +121 -33
- package/docs/pages/components/drawer.md +2 -2
- package/docs/pages/components/list.md +9 -13
- package/docs/pages/components/navbar.md +49 -30
- package/docs/pages/components/pagination.md +19 -0
- package/docs/pages/components/steps.md +24 -20
- package/docs/pages/components/table.md +3 -3
- package/docs/pages/enhancements/flyout.md +2 -2
- package/docs/pages/enhancements/scrollspy.md +7 -7
- package/docs/pages/enhancements/status-bar.md +2 -2
- package/docs/pages/enhancements/tabs.md +74 -3
- package/docs/pages/examples/overview.md +2 -1
- package/docs/pages/forms/date-picker.md +90 -0
- package/docs/pages/forms/detached-actions.md +37 -30
- package/docs/pages/forms/floating-field.md +21 -7
- package/docs/pages/forms/form-actions.md +58 -28
- package/docs/pages/forms/input-policy.md +1 -1
- package/docs/pages/forms/overview.md +6 -5
- package/docs/pages/forms/states-and-sizes.md +15 -15
- package/docs/pages/forms/sticky-actions.md +85 -83
- package/docs/pages/forms/validation.md +5 -5
- package/docs/pages/foundations/philosophy.md +102 -4
- package/docs/pages/foundations/theming.md +15 -4
- package/docs/pages/foundations/tokens.md +89 -36
- package/docs/pages/foundations/typography.md +4 -7
- package/docs/pages/guides/bootstrap.md +2 -2
- package/docs/pages/guides/design-tools.md +2 -2
- package/docs/pages/guides/integrating-actual-css.md +79 -9
- package/docs/pages/guides/introduction.md +5 -5
- package/docs/pages/guides/modular-import.md +3 -0
- package/docs/pages/guides/progressive-enhancement.md +27 -9
- package/docs/pages/guides/tailwind.md +70 -0
- package/docs/pages/guides/upgrading.md +151 -0
- package/docs/pages/layout/center.md +2 -1
- package/docs/pages/layout/cluster.md +37 -4
- package/docs/pages/layout/grid.md +2 -0
- package/docs/pages/layout/overview.md +1 -1
- package/docs/pages/layout/sidebar-layout.md +3 -0
- package/docs/pages/layout/stack.md +21 -16
- package/docs/pages/layout/switcher.md +2 -0
- package/docs/pages/layout/topbar.md +7 -5
- package/docs/pages/patterns/icon-slot.md +17 -1
- package/docs/pages/patterns/navigation-list.md +16 -46
- package/docs/pages/patterns/overview.md +3 -4
- package/docs/pages/utilities/flex.md +30 -20
- package/docs/pages/utilities/sizing-wrapping.md +52 -7
- package/docs/pages/utilities/spacing.md +4 -3
- package/package.json +6 -6
- package/reserved-classes.json +3 -3
- package/skills/actual-css/references/enhancements.md +10 -7
- package/src/cli/actual-css.js +39 -13
- package/src/css/components/alert.css +34 -34
- package/src/css/components/avatar.css +8 -17
- package/src/css/components/badge.css +1 -6
- package/src/css/components/breadcrumb.css +74 -8
- package/src/css/components/button.css +3 -9
- package/src/css/components/card.css +18 -10
- package/src/css/components/chat.css +4 -5
- package/src/css/components/drawer.css +42 -45
- package/src/css/components/flyout.css +3 -1
- package/src/css/components/index.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 +113 -134
- package/src/css/components/navbar.css +22 -31
- 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/base.css +1 -1
- package/src/css/core/focus.css +8 -6
- package/src/css/core/reset.css +2 -1
- package/src/css/core/theme.css +10 -1
- package/src/css/core/tokens.css +32 -12
- package/src/css/core/variants.css +41 -14
- 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 +14 -12
- 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 +25 -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/layout/topbar.css +12 -6
- package/src/css/utilities/base.css +5 -0
- package/src/css/utilities/extra.css +3 -7
- 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/color.js +65 -0
- package/src/tooling/design-export.js +1 -1
- package/src/tooling/design-tokens.js +53 -34
- package/src/tooling/theme-contrast.js +217 -0
- package/docs/pages/patterns/actions.md +0 -52
- package/src/css/components/actions.css +0 -27
package/src/cli/actual-css.js
CHANGED
|
@@ -8,10 +8,11 @@ function usage() {
|
|
|
8
8
|
return [
|
|
9
9
|
"actual-css bundle INPUT --out FILE [--minify]",
|
|
10
10
|
"actual-css design [--theme FILE] [--name NAME] --out DIR",
|
|
11
|
+
"actual-css contrast [--theme FILE] [--name NAME]",
|
|
11
12
|
"",
|
|
12
13
|
" -o, --out FILE write the bundle to FILE / the export to DIR (required)",
|
|
13
14
|
" --minify collapse comments and whitespace",
|
|
14
|
-
" --theme FILE
|
|
15
|
+
" --theme FILE read this theme file ([data-theme] or :root) over the default",
|
|
15
16
|
" --name NAME the data-theme name, when the file declares several",
|
|
16
17
|
" -h, --help show this message",
|
|
17
18
|
" --version print the actual-css version",
|
|
@@ -23,7 +24,13 @@ function usage() {
|
|
|
23
24
|
"",
|
|
24
25
|
"design: export a theme's tokens for Figma and Penpot, plus Button, Input, Badge,",
|
|
25
26
|
"Alert and Card measured in headless Chrome and a Penpot plugin that builds them.",
|
|
26
|
-
"Needs Bun (bunx actual-css design ...).",
|
|
27
|
+
"Needs Bun (bunx --bun actual-css design ...).",
|
|
28
|
+
"",
|
|
29
|
+
"contrast: measure the pairs a theme tunes by hand in headless Chrome, in every",
|
|
30
|
+
"scheme it declares: soft ink on its resting and hovered fill, the focus line on",
|
|
31
|
+
"--surface and --surface-solid, the invalid-field focus line, and the inverse",
|
|
32
|
+
"text. Exits 1 when a pair misses its threshold. Needs Bun (bunx --bun actual-css",
|
|
33
|
+
"contrast ...).",
|
|
27
34
|
].join("\n");
|
|
28
35
|
}
|
|
29
36
|
|
|
@@ -43,8 +50,8 @@ function parseArgs(argv) {
|
|
|
43
50
|
return { version: true };
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
if (command === "design") {
|
|
47
|
-
return
|
|
53
|
+
if (command === "design" || command === "contrast") {
|
|
54
|
+
return parseThemeArgs(command, rest);
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
if (command !== "bundle") {
|
|
@@ -101,9 +108,10 @@ function parseArgs(argv) {
|
|
|
101
108
|
return { help: false, command: "bundle", input, out, minify };
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
function
|
|
105
|
-
const options = { help: false, command
|
|
106
|
-
const valued = { "--theme": "themeFile", "--name": "name"
|
|
111
|
+
function parseThemeArgs(command, rest) {
|
|
112
|
+
const options = { help: false, command, themeFile: null, name: null, out: "" };
|
|
113
|
+
const valued = { "--theme": "themeFile", "--name": "name" };
|
|
114
|
+
if (command === "design") Object.assign(valued, { "--out": "out", "-o": "out" });
|
|
107
115
|
|
|
108
116
|
for (let i = 0; i < rest.length; i += 1) {
|
|
109
117
|
const arg = rest[i];
|
|
@@ -116,23 +124,40 @@ function parseDesignArgs(rest) {
|
|
|
116
124
|
i += 1;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
if (!options.out) throw new Error("Missing required --out DIR option.");
|
|
127
|
+
if (command === "design" && !options.out) throw new Error("Missing required --out DIR option.");
|
|
120
128
|
return options;
|
|
121
129
|
}
|
|
122
130
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
/* Checked before the tooling import: under Node those modules do not even
|
|
132
|
+
parse (`await using`), and `bundle` must stay usable there. The shebang is
|
|
133
|
+
Node for that reason, so plain `bunx` honors it and lands here: --bun is
|
|
134
|
+
what makes Bun run the script. */
|
|
135
|
+
function requireBun(command) {
|
|
126
136
|
if (typeof Bun === "undefined") {
|
|
127
137
|
throw new Error(
|
|
128
|
-
|
|
138
|
+
`actual-css ${command} needs Bun: it reads the theme in headless Chrome through Bun.WebView. Run it with bunx --bun actual-css ${command} ...`,
|
|
129
139
|
);
|
|
130
140
|
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function design({ themeFile, name, out }) {
|
|
144
|
+
requireBun("design");
|
|
131
145
|
const { exportDesign } = await import("../tooling/design-export.js");
|
|
132
146
|
const written = await exportDesign({ themeFile, name, out: resolve(out) });
|
|
133
147
|
console.log(`Exported ${written.length} files to ${resolve(out)}`);
|
|
134
148
|
}
|
|
135
149
|
|
|
150
|
+
async function contrast({ themeFile, name }) {
|
|
151
|
+
requireBun("contrast");
|
|
152
|
+
const { formatContrast, measureThemeFile } = await import("../tooling/theme-contrast.js");
|
|
153
|
+
const { text, misses } = formatContrast(await measureThemeFile({ themeFile, name }));
|
|
154
|
+
console.log(text);
|
|
155
|
+
if (misses > 0) {
|
|
156
|
+
console.error(`\n${misses} pair${misses === 1 ? "" : "s"} under threshold.`);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
async function bundle({ input, out, minify }) {
|
|
137
162
|
const inputPath = resolve(input);
|
|
138
163
|
const outputPath = resolve(out);
|
|
@@ -167,7 +192,8 @@ async function main() {
|
|
|
167
192
|
return;
|
|
168
193
|
}
|
|
169
194
|
|
|
170
|
-
|
|
195
|
+
const commands = { bundle, contrast, design };
|
|
196
|
+
await commands[args.command](args);
|
|
171
197
|
}
|
|
172
198
|
|
|
173
199
|
main().catch((error) => {
|
|
@@ -107,10 +107,11 @@
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/* Decorative leading icon — use .alert-icon on the icon element.
|
|
110
|
+
/* Decorative leading icon — use .alert-icon on the icon element. display and
|
|
111
|
+
vertical-align are the no-:has() layout (Degraded tier: the icon sits inline
|
|
112
|
+
before the text); the grid anatomy below replaces them. */
|
|
111
113
|
.alert > .alert-icon {
|
|
112
114
|
display: inline-block;
|
|
113
|
-
flex: none;
|
|
114
115
|
inline-size: var(--alert-icon-size);
|
|
115
116
|
block-size: var(--alert-icon-size);
|
|
116
117
|
font-size: var(--alert-icon-size);
|
|
@@ -142,45 +143,44 @@
|
|
|
142
143
|
|
|
143
144
|
/* Grid anatomy — activated by a leading icon, a trailing dismiss button, or
|
|
144
145
|
both. Icon and dismiss are pinned to the outer columns; anything else (the
|
|
145
|
-
message content) flows into the remaining text column.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.alert:has(> .alert-icon) {
|
|
155
|
-
grid-template-columns: auto minmax(0, 1fr);
|
|
156
|
-
}
|
|
146
|
+
message content) flows into the remaining text column. Without :has(), the
|
|
147
|
+
grid rules drop together and the alert keeps ordinary block flow. */
|
|
148
|
+
.alert:has(> .alert-icon, > .close) {
|
|
149
|
+
display: grid;
|
|
150
|
+
/* No grid-template-columns: the icon and close branches below always set
|
|
151
|
+
it, since one of them is what matched. */
|
|
152
|
+
align-items: var(--items-align, start);
|
|
153
|
+
column-gap: var(--space-30);
|
|
154
|
+
}
|
|
157
155
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
display: inline-grid;
|
|
162
|
-
place-items: center;
|
|
163
|
-
margin-block-start: 0.125em;
|
|
164
|
-
}
|
|
156
|
+
.alert:has(> .alert-icon) {
|
|
157
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
158
|
+
}
|
|
165
159
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
160
|
+
.alert:has(> .alert-icon) > .alert-icon {
|
|
161
|
+
grid-column: 1;
|
|
162
|
+
grid-row: 1 / -1;
|
|
163
|
+
display: inline-grid;
|
|
164
|
+
place-items: center;
|
|
165
|
+
margin-block-start: 0.125em;
|
|
166
|
+
}
|
|
169
167
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
.alert:has(> .alert-icon) > :not(.alert-icon, .close) {
|
|
169
|
+
grid-column: 2;
|
|
170
|
+
}
|
|
173
171
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
.alert:has(> .close) {
|
|
173
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
174
|
+
}
|
|
177
175
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
grid-row: 1;
|
|
181
|
-
}
|
|
176
|
+
.alert:has(> .alert-icon):has(> .close) {
|
|
177
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
182
178
|
}
|
|
183
179
|
|
|
180
|
+
.alert:has(> .close) > .close {
|
|
181
|
+
grid-column: -2 / -1;
|
|
182
|
+
grid-row: 1;
|
|
183
|
+
}
|
|
184
184
|
/* Links */
|
|
185
185
|
.alert :where(a:not(.btn)) {
|
|
186
186
|
color: inherit;
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
* --avatar-size
|
|
12
12
|
* --avatar-radius
|
|
13
13
|
* --avatar-font-size
|
|
14
|
+
* --avatar-bg
|
|
14
15
|
* --avatar-stack-size
|
|
15
16
|
* --avatar-stack-font-size
|
|
16
17
|
* --avatar-stack-overlap
|
|
17
18
|
* --avatar-stack-ring
|
|
18
19
|
*
|
|
19
20
|
* Internal:
|
|
20
|
-
* --avatar-bg
|
|
21
21
|
* --avatar-fg
|
|
22
22
|
* --avatar-status-translate-x
|
|
23
23
|
*/
|
|
@@ -95,16 +95,13 @@
|
|
|
95
95
|
|
|
96
96
|
.avatar > img,
|
|
97
97
|
.avatar > picture > img {
|
|
98
|
+
display: block;
|
|
98
99
|
inline-size: 100%;
|
|
99
100
|
block-size: 100%;
|
|
100
101
|
object-fit: cover;
|
|
101
102
|
border-radius: inherit;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
.avatar > picture > img {
|
|
105
|
-
display: block;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
105
|
button.avatar {
|
|
109
106
|
appearance: none;
|
|
110
107
|
cursor: pointer;
|
|
@@ -198,19 +195,13 @@ a.avatar[aria-disabled="true"] {
|
|
|
198
195
|
--avatar-stack-font-size: var(--font-size-lg);
|
|
199
196
|
}
|
|
200
197
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.avatar-stack:has(> .avatar.lg) {
|
|
208
|
-
--avatar-stack-size: 3rem;
|
|
209
|
-
--avatar-stack-font-size: var(--font-size-lg);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
198
|
+
/* Overlapping avatars hide their neighbour: a translucent fill (--surface-subtle
|
|
199
|
+
on a glass palette) would show the next avatar's edge through it, most of all
|
|
200
|
+
when hover raises one above the other. The fill is layered over the opaque
|
|
201
|
+
role; on an opaque fill the base never shows. */
|
|
213
202
|
.avatar-stack > .avatar {
|
|
203
|
+
background-color: var(--surface-opaque);
|
|
204
|
+
background-image: linear-gradient(var(--avatar-bg), var(--avatar-bg));
|
|
214
205
|
box-shadow: 0 0 0 var(--avatar-stack-ring) var(--surface-opaque);
|
|
215
206
|
}
|
|
216
207
|
|
|
@@ -137,7 +137,6 @@
|
|
|
137
137
|
--badge-size: var(--badge-dot-size);
|
|
138
138
|
--badge-bg: var(--intent, var(--neutral));
|
|
139
139
|
--badge-fg: var(--intent-fg, var(--neutral-fg));
|
|
140
|
-
--badge-border: transparent;
|
|
141
140
|
padding: 0;
|
|
142
141
|
border: 0;
|
|
143
142
|
inline-size: var(--badge-size);
|
|
@@ -169,10 +168,6 @@
|
|
|
169
168
|
}
|
|
170
169
|
|
|
171
170
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
172
|
-
:is(a.badge, button.badge):is(.solid, :empty) {
|
|
173
|
-
--ui-hover-bg: color-mix(in oklch, var(--intent, var(--neutral)) 88%, black);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
171
|
:is(a.badge, button.badge):where(:not(.solid, .outline, :empty)) {
|
|
177
172
|
--badge-default-hover-bg: color-mix(
|
|
178
173
|
in oklab,
|
|
@@ -199,7 +194,7 @@
|
|
|
199
194
|
:where(:disabled, [aria-disabled="true"])
|
|
200
195
|
):hover {
|
|
201
196
|
/* Huge inset spread paints the overlay over the pill radius (see button.css). */
|
|
202
|
-
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
197
|
+
box-shadow: inset 0 0 0 999px var(--hover-overlay-solid, var(--hover-overlay));
|
|
203
198
|
}
|
|
204
199
|
|
|
205
200
|
button.badge {
|
|
@@ -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 {
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
* --btn-bg
|
|
29
29
|
* --btn-fg
|
|
30
30
|
* --btn-border
|
|
31
|
-
* --btn-default-hover-bg
|
|
32
31
|
* --btn-border-block-end-color
|
|
33
32
|
* --btn-border-block-end-width
|
|
34
33
|
* --btn-border-block
|
|
@@ -54,9 +53,8 @@
|
|
|
54
53
|
--btn-bg: var(--ui-bg, var(--btn-default-bg, var(--intent, var(--neutral))));
|
|
55
54
|
--btn-fg: var(--ui-fg, var(--btn-default-fg, var(--intent-fg, var(--neutral-fg))));
|
|
56
55
|
--btn-border: var(--ui-border, var(--btn-default-border, transparent));
|
|
57
|
-
--btn-default-hover-bg: var(--btn-bg);
|
|
58
56
|
--btn-radius: var(--radius);
|
|
59
|
-
--btn-hover-overlay: var(--hover-overlay,
|
|
57
|
+
--btn-hover-overlay: var(--hover-overlay-solid, var(--hover-overlay));
|
|
60
58
|
--btn-border-block: calc(
|
|
61
59
|
var(--border-width) +
|
|
62
60
|
var(--btn-border-block-end-width, var(--border-width))
|
|
@@ -136,10 +134,6 @@
|
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
139
|
-
.btn {
|
|
140
|
-
--btn-default-hover-bg: color-mix(in oklch, var(--btn-bg) 88%, black);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
137
|
.btn.soft {
|
|
144
138
|
--ui-hover-bg: color-mix(
|
|
145
139
|
in oklab,
|
|
@@ -155,9 +149,9 @@
|
|
|
155
149
|
}
|
|
156
150
|
|
|
157
151
|
/* The .soft/.outline/.ghost hover recipe (--ui-hover-bg) is set locally above;
|
|
158
|
-
the overlay darkening for solid
|
|
152
|
+
the overlay darkening for solid is .btn-specific. */
|
|
159
153
|
.btn:not(:where(:disabled, [aria-disabled="true"])):hover {
|
|
160
|
-
background: var(--ui-hover-bg, var(--btn-
|
|
154
|
+
background: var(--ui-hover-bg, var(--btn-bg));
|
|
161
155
|
/* Huge inset spread paints the overlay over any border-radius without a
|
|
162
156
|
pseudo-element; a plain background would replace the fill instead of
|
|
163
157
|
darkening it. */
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Docs: docs/pages/components/card.md
|
|
5
5
|
*
|
|
6
6
|
* Surface, border, radius, padding, density, and elevation stay local here.
|
|
7
|
-
* Direct header/footer children are structural slots
|
|
8
|
-
* anchored to the card's bottom edge when extra block space
|
|
9
|
-
* price/action rows align across equal-height cards in a
|
|
10
|
-
* owns its direct-child rhythm
|
|
11
|
-
* over that layout. Page layout remains the job of markup and layout
|
|
7
|
+
* Direct header/footer children are structural slots with a default split row;
|
|
8
|
+
* a direct footer is anchored to the card's bottom edge when extra block space
|
|
9
|
+
* is available (so price/action rows align across equal-height cards in a
|
|
10
|
+
* grid). A bare card owns its direct-child rhythm and its slot rows; a layout
|
|
11
|
+
* primitive on the card or on a slot takes over that layout. Page layout remains the job of markup and layout
|
|
12
12
|
* primitives outside the card.
|
|
13
13
|
*
|
|
14
14
|
* Public hooks:
|
|
@@ -46,6 +46,9 @@
|
|
|
46
46
|
|
|
47
47
|
.card {
|
|
48
48
|
--card-pad: var(--space-40);
|
|
49
|
+
/* Local anatomy, like the padding. Never var(--gap): that token is
|
|
50
|
+
inherited, so the --gap of a parent grid became the rhythm inside every
|
|
51
|
+
card (+79px per card on a 38px grid). */
|
|
49
52
|
--card-gap: var(--space-30);
|
|
50
53
|
--card-radius: var(--radius-lg);
|
|
51
54
|
--card-max-inline-size: none;
|
|
@@ -87,7 +90,11 @@
|
|
|
87
90
|
margin-block: 0;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
|
|
93
|
+
/* The slot row is a convenience default, not anatomy: like the root box it
|
|
94
|
+
lives at zero specificity, so a primitive, role or utility on the slot
|
|
95
|
+
(.cluster, .media, .form-actions, .justify-content-*) replaces it instead of
|
|
96
|
+
losing to components/ on source order. */
|
|
97
|
+
:where(.card > header, .card > footer) {
|
|
91
98
|
display: flex;
|
|
92
99
|
flex-wrap: wrap;
|
|
93
100
|
align-items: var(--items-align, center);
|
|
@@ -96,15 +103,15 @@
|
|
|
96
103
|
min-inline-size: 0;
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
:where(.card > header) {
|
|
107
|
+
align-items: var(--items-align, flex-start);
|
|
108
|
+
}
|
|
109
|
+
|
|
99
110
|
.card > :where(header, footer) > * {
|
|
100
111
|
min-inline-size: 0;
|
|
101
112
|
margin-block: 0;
|
|
102
113
|
}
|
|
103
114
|
|
|
104
|
-
.card > header {
|
|
105
|
-
align-items: var(--items-align, flex-start);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
115
|
/* A direct footer is pinned to the bottom when the card has free space.
|
|
109
116
|
Specificity beats .stack > * so a .card.stack keeps this contract. */
|
|
110
117
|
.card > footer {
|
|
@@ -119,6 +126,7 @@
|
|
|
119
126
|
}
|
|
120
127
|
.card.compact {
|
|
121
128
|
--card-pad: var(--space-20);
|
|
129
|
+
--card-gap: var(--space-20);
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
@media print {
|
|
@@ -94,9 +94,8 @@
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/* Remove the reserved avatar gutter when a message has no avatar. This is a
|
|
97
|
-
cosmetic enhancement; the two-column baseline remains fully readable
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
97
|
+
cosmetic enhancement; the two-column baseline remains fully readable, and a
|
|
98
|
+
browser without :has() drops only this rule. */
|
|
99
|
+
.chat:not(:has(> .chat-avatar)) {
|
|
100
|
+
column-gap: 0;
|
|
102
101
|
}
|
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
* Internal:
|
|
23
23
|
* --drawer-closed-translate (side/RTL positioning state)
|
|
24
24
|
* --drawer-focus-bleed
|
|
25
|
+
* --drawer-close-reserve (header room for a corner close; zero without a close)
|
|
25
26
|
* --drawer-motion-duration (shared by panel and ::backdrop so exit duration
|
|
26
27
|
* changes in one place; entry state redefines it longer)
|
|
28
|
+
* --drawer-motion-ease (panel easing owned by the open/closed state)
|
|
27
29
|
*/
|
|
28
30
|
:where(dialog).drawer {
|
|
29
31
|
--drawer-size: 20rem;
|
|
@@ -31,9 +33,14 @@
|
|
|
31
33
|
/* The inset a direct .bleed child cancels (components/bleed.css). */
|
|
32
34
|
--surface-pad: var(--drawer-pad);
|
|
33
35
|
--drawer-closed-translate: -100%;
|
|
36
|
+
/* Header room for a corner close. Unconditional baseline: without :has()
|
|
37
|
+
(Degraded tier) a header keeps too much room rather than a title running
|
|
38
|
+
under the close. Zeroed when the drawer has no close. */
|
|
39
|
+
--drawer-close-reserve: calc(var(--close-size, var(--control-size)) + var(--space-30));
|
|
34
40
|
/* Exit is shorter than entry so the panel clears quickly; the scrim reads
|
|
35
41
|
the same variable through inheritance and never lags the panel. */
|
|
36
42
|
--drawer-motion-duration: var(--duration);
|
|
43
|
+
--drawer-motion-ease: var(--ease-exit);
|
|
37
44
|
|
|
38
45
|
/* Keep the surface anchored to the viewport in the top layer. This also
|
|
39
46
|
provides the containing block for the absolutely positioned close button.
|
|
@@ -65,21 +72,21 @@
|
|
|
65
72
|
box-shadow: var(--shadow);
|
|
66
73
|
opacity: 0;
|
|
67
74
|
transform: translateX(var(--drawer-closed-translate));
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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;
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
:where(dialog).drawer[open] {
|
|
74
83
|
--drawer-motion-duration: var(--duration-slow);
|
|
84
|
+
--drawer-motion-ease: var(--ease-enter);
|
|
75
85
|
display: grid;
|
|
76
86
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
77
87
|
gap: var(--space-40);
|
|
78
88
|
opacity: 1;
|
|
79
89
|
transform: translateX(0);
|
|
80
|
-
transition:
|
|
81
|
-
opacity var(--drawer-motion-duration) var(--ease-enter),
|
|
82
|
-
transform var(--drawer-motion-duration) var(--ease-enter);
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
/* A form is the drawer's content wrapper when actions need method="dialog".
|
|
@@ -99,7 +106,10 @@
|
|
|
99
106
|
opacity: 0;
|
|
100
107
|
/* Scrim curve stays neutral `ease`; only the panel's character curves
|
|
101
108
|
apply. Duration follows --drawer-motion-duration through inheritance. */
|
|
102
|
-
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;
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
:where(dialog).drawer[data-side="end"] {
|
|
@@ -133,47 +143,23 @@
|
|
|
133
143
|
exclusion list is easier to trust than its `position: absolute`. */
|
|
134
144
|
:where(dialog).drawer > :not(:is(header, footer, form, .close)),
|
|
135
145
|
:where(dialog).drawer > form > :not(:is(header, footer, .close)) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
);
|
|
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));
|
|
140
150
|
|
|
141
151
|
min-block-size: 0;
|
|
142
|
-
/* Keep focus paint inside the clip
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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);
|
|
148
159
|
overflow: auto;
|
|
149
160
|
overscroll-behavior: contain;
|
|
150
161
|
}
|
|
151
162
|
|
|
152
|
-
@supports (transition-behavior: allow-discrete) {
|
|
153
|
-
:where(dialog).drawer {
|
|
154
|
-
transition:
|
|
155
|
-
opacity var(--drawer-motion-duration) var(--ease-exit),
|
|
156
|
-
transform var(--drawer-motion-duration) var(--ease-exit),
|
|
157
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
158
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
:where(dialog).drawer[open] {
|
|
162
|
-
transition:
|
|
163
|
-
opacity var(--drawer-motion-duration) var(--ease-enter),
|
|
164
|
-
transform var(--drawer-motion-duration) var(--ease-enter),
|
|
165
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
166
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
:where(dialog).drawer::backdrop {
|
|
170
|
-
transition:
|
|
171
|
-
opacity var(--drawer-motion-duration) ease,
|
|
172
|
-
overlay var(--drawer-motion-duration) allow-discrete,
|
|
173
|
-
display var(--drawer-motion-duration) allow-discrete;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
163
|
/* Safari before 16.4 does not support :dir(); keep the ancestor-attribute
|
|
178
164
|
path as the Degraded-tier fallback. The :dir() rules are dropped on their
|
|
179
165
|
own there, so they need no wrapper. :dir() follows inherited HTML
|
|
@@ -210,9 +196,20 @@
|
|
|
210
196
|
}
|
|
211
197
|
}
|
|
212
198
|
|
|
199
|
+
/* No close anywhere: reclaim the reserve. Deliberately coarse — a close in
|
|
200
|
+
the body keeps the reserve, which costs only room, never overlap. */
|
|
201
|
+
:where(dialog).drawer:not(:has(.close)) {
|
|
202
|
+
--drawer-close-reserve: 0px;
|
|
203
|
+
}
|
|
204
|
+
|
|
213
205
|
/* The header reserves inline room so the title never runs under the close
|
|
214
|
-
control. */
|
|
206
|
+
control. A .bleed header keeps at least its band inset. */
|
|
215
207
|
:where(dialog).drawer > header,
|
|
216
|
-
:where(dialog).drawer > form > header {
|
|
217
|
-
padding-inline-end:
|
|
208
|
+
:where(dialog).drawer > :where(form) > header {
|
|
209
|
+
padding-inline-end: var(--drawer-close-reserve);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
:where(dialog).drawer > header:where(.bleed),
|
|
213
|
+
:where(dialog).drawer > :where(form) > header:where(.bleed) {
|
|
214
|
+
padding-inline-end: max(var(--bleed-pad, 0px), var(--drawer-close-reserve));
|
|
218
215
|
}
|