actual-css 0.11.0 → 0.12.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 +73 -0
- package/README.md +29 -5
- package/components.json +15 -17
- package/dist/actual.full.css +403 -227
- 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/navigation.json +4 -2
- package/docs/pages/components/alert.md +2 -10
- package/docs/pages/components/badge.md +3 -9
- package/docs/pages/components/bleed.md +5 -4
- package/docs/pages/components/card.md +31 -2
- package/docs/pages/components/close.md +9 -3
- package/docs/pages/components/dialog.md +248 -31
- package/docs/pages/components/drawer.md +2 -2
- package/docs/pages/components/meter.md +2 -0
- package/docs/pages/components/navbar.md +8 -9
- package/docs/pages/components/table.md +3 -3
- package/docs/pages/enhancements/flyout.md +2 -2
- package/docs/pages/enhancements/scrollspy.md +1 -1
- package/docs/pages/enhancements/status-bar.md +2 -2
- package/docs/pages/examples/overview.md +3 -1
- package/docs/pages/forms/date-picker.md +90 -0
- package/docs/pages/forms/detached-actions.md +37 -30
- package/docs/pages/forms/form-actions.md +61 -28
- package/docs/pages/forms/input-policy.md +1 -1
- package/docs/pages/forms/overview.md +6 -5
- package/docs/pages/forms/sticky-actions.md +85 -83
- package/docs/pages/forms/validation.md +5 -5
- package/docs/pages/foundations/philosophy.md +5 -3
- package/docs/pages/foundations/theming.md +15 -4
- package/docs/pages/foundations/tokens.md +50 -33
- package/docs/pages/foundations/typography.md +4 -7
- package/docs/pages/guides/bootstrap.md +2 -2
- package/docs/pages/guides/design-tools.md +36 -0
- package/docs/pages/guides/integrating-actual-css.md +5 -1
- package/docs/pages/guides/introduction.md +3 -4
- package/docs/pages/guides/modular-import.md +3 -0
- package/docs/pages/guides/progressive-enhancement.md +2 -2
- package/docs/pages/guides/tailwind.md +70 -0
- package/docs/pages/guides/upgrading.md +74 -0
- package/docs/pages/layout/cluster.md +28 -2
- package/docs/pages/layout/overview.md +1 -1
- package/docs/pages/layout/stack.md +21 -16
- package/docs/pages/layout/topbar.md +1 -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 +1 -1
- package/docs/pages/utilities/spacing.md +4 -3
- package/package.json +3 -2
- package/reserved-classes.json +3 -3
- package/skills/actual-css/references/enhancements.md +38 -0
- package/src/cli/actual-css.js +75 -6
- package/src/css/components/avatar.css +16 -9
- package/src/css/components/badge.css +1 -5
- package/src/css/components/bleed.css +5 -5
- package/src/css/components/button.css +17 -10
- package/src/css/components/card.css +18 -10
- package/src/css/components/chat.css +4 -5
- package/src/css/components/close.css +17 -6
- package/src/css/components/drawer.css +19 -3
- package/src/css/components/flyout.css +3 -0
- package/src/css/components/index.css +0 -1
- package/src/css/components/meter.css +5 -2
- package/src/css/components/modal.css +157 -69
- package/src/css/components/navbar.css +14 -25
- package/src/css/core/base.css +1 -1
- package/src/css/core/reset.css +2 -1
- package/src/css/core/theme.css +4 -0
- package/src/css/core/tokens.css +28 -10
- package/src/css/core/variants.css +41 -14
- package/src/css/forms/form-actions.css +15 -10
- package/src/css/forms/native.css +5 -1
- package/src/css/layout/cluster.css +11 -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/tooling/color.js +65 -0
- package/src/tooling/design-components.js +286 -0
- package/src/tooling/design-export.js +69 -0
- package/src/tooling/design-tokens.js +474 -0
- package/src/tooling/penpot-plugin/manifest.json +7 -0
- package/src/tooling/penpot-plugin/plugin.js +272 -0
- package/src/tooling/penpot-plugin/ui.html +58 -0
- package/src/tooling/theme-contrast.js +209 -0
- package/docs/pages/patterns/actions.md +0 -52
- package/src/css/components/actions.css +0 -27
|
@@ -21,6 +21,44 @@ Self-describing leaf behavior should use the feature's own documented attribute,
|
|
|
21
21
|
|
|
22
22
|
Native `<dialog>` is semantically self-describing and does not need a generic enhancement token merely to be a dialog.
|
|
23
23
|
|
|
24
|
+
## Dialog anatomy
|
|
25
|
+
|
|
26
|
+
Treat `.modal` as the outer surface, not as its content layout. It deliberately
|
|
27
|
+
has no padding. Wrap modal content in a direct `<form>` or `.stack`; do not put
|
|
28
|
+
`.stack` on `dialog.modal` itself.
|
|
29
|
+
|
|
30
|
+
For a header band, put `<header>` directly under the dialog before the body
|
|
31
|
+
wrapper. Compose a flexible `.media` title region and a trailing `.cluster` of
|
|
32
|
+
controls. Nest `.close` in that cluster so it remains in flow; the title,
|
|
33
|
+
leading `.dialog-icon`, close, and any additional control then share one center
|
|
34
|
+
axis and the title uses the close control's minimum height. A direct
|
|
35
|
+
`dialog > header` is dialog chrome and forms a distinct band; put an ordinary
|
|
36
|
+
content heading in the wrapper's `<header>` instead.
|
|
37
|
+
|
|
38
|
+
For a corner close button, keep `.close` as a direct child of the dialog and put
|
|
39
|
+
the title in the content wrapper's direct `<header>`. The close button is then
|
|
40
|
+
anchored to the surface, and the header reserves room so the title cannot run
|
|
41
|
+
under it. A `.close` that is a direct child of the chrome header is that same
|
|
42
|
+
corner overlay, and that header reserves room for it too. Put actions in the
|
|
43
|
+
wrapper's `<footer>` and compose its row in markup
|
|
44
|
+
(`<footer class="cluster justify-content-end">`); the modal and drawer footers
|
|
45
|
+
have no layout of their own.
|
|
46
|
+
|
|
47
|
+
Use `.bleed` on that direct header when it needs a contrasting full-width band;
|
|
48
|
+
the wrapper relays the modal padding so the band reaches the surface edges while
|
|
49
|
+
its contents keep the normal inset. A confirmation is composition, not a
|
|
50
|
+
variant: a `.media` row with a leading `.dialog-icon` and the text column (its
|
|
51
|
+
first heading centers on the icon, following copy stays in that column), then a
|
|
52
|
+
`<footer class="bleed background-subtle cluster justify-content-space-between">` action
|
|
53
|
+
band with the cancel-style action first. For a trailing icon, use the wrapper's
|
|
54
|
+
direct header as a non-wrapping split `.cluster`, with `.grow` on the text
|
|
55
|
+
group. Do not compensate heading alignment with a literal margin.
|
|
56
|
+
|
|
57
|
+
Do not turn the dialog heading into an `.alert` to obtain icon or surface
|
|
58
|
+
styling. Compose `.dialog-icon` with `.media` for a leading title icon; reserve
|
|
59
|
+
`.alert` for a distinct status message in the dialog body. Inspect
|
|
60
|
+
`docs/pages/components/dialog.md` in the installed package for current recipes.
|
|
61
|
+
|
|
24
62
|
## Custom widgets
|
|
25
63
|
|
|
26
64
|
Before implementing lifecycle, focus, Escape handling, keyboard navigation, input rewriting, or floating-surface policy from scratch, inspect the exported Actual JS primitives.
|
package/src/cli/actual-css.js
CHANGED
|
@@ -7,16 +7,30 @@ import { inlineImports, minifyCss } from "../tooling/css-bundle.js";
|
|
|
7
7
|
function usage() {
|
|
8
8
|
return [
|
|
9
9
|
"actual-css bundle INPUT --out FILE [--minify]",
|
|
10
|
+
"actual-css design [--theme FILE] [--name NAME] --out DIR",
|
|
11
|
+
"actual-css contrast [--theme FILE] [--name NAME]",
|
|
10
12
|
"",
|
|
11
|
-
" -o, --out FILE write the bundle to FILE (required)",
|
|
13
|
+
" -o, --out FILE write the bundle to FILE / the export to DIR (required)",
|
|
12
14
|
" --minify collapse comments and whitespace",
|
|
15
|
+
" --theme FILE read this [data-theme] file instead of the default theme",
|
|
16
|
+
" --name NAME the data-theme name, when the file declares several",
|
|
13
17
|
" -h, --help show this message",
|
|
14
18
|
" --version print the actual-css version",
|
|
15
19
|
"",
|
|
16
|
-
"
|
|
17
|
-
"A layer or layer(name) import is flattened into the @layer block it
|
|
18
|
-
"Remote imports are kept and hoisted to the top of the bundle.",
|
|
20
|
+
"bundle: flatten plain CSS @import chains from relative files and actual-css/css/*",
|
|
21
|
+
"subpaths. A layer or layer(name) import is flattened into the @layer block it",
|
|
22
|
+
"stands for. Remote imports are kept and hoisted to the top of the bundle.",
|
|
19
23
|
"Leaves modern CSS syntax untouched; this is a bundler, not a transpiler.",
|
|
24
|
+
"",
|
|
25
|
+
"design: export a theme's tokens for Figma and Penpot, plus Button, Input, Badge,",
|
|
26
|
+
"Alert and Card measured in headless Chrome and a Penpot plugin that builds them.",
|
|
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 ...).",
|
|
20
34
|
].join("\n");
|
|
21
35
|
}
|
|
22
36
|
|
|
@@ -36,6 +50,10 @@ function parseArgs(argv) {
|
|
|
36
50
|
return { version: true };
|
|
37
51
|
}
|
|
38
52
|
|
|
53
|
+
if (command === "design" || command === "contrast") {
|
|
54
|
+
return parseThemeArgs(command, rest);
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
if (command !== "bundle") {
|
|
40
58
|
throw new Error(`Unknown command: ${command}`);
|
|
41
59
|
}
|
|
@@ -87,7 +105,57 @@ function parseArgs(argv) {
|
|
|
87
105
|
throw new Error("Missing required --out FILE option.");
|
|
88
106
|
}
|
|
89
107
|
|
|
90
|
-
return { help: false, input, out, minify };
|
|
108
|
+
return { help: false, command: "bundle", input, out, minify };
|
|
109
|
+
}
|
|
110
|
+
|
|
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" });
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
117
|
+
const arg = rest[i];
|
|
118
|
+
if (arg === "--help" || arg === "-h") return { help: true };
|
|
119
|
+
const key = valued[arg];
|
|
120
|
+
if (!key) throw new Error(`Unknown option: ${arg}`);
|
|
121
|
+
const value = rest[i + 1];
|
|
122
|
+
if (!value) throw new Error(`Missing value for ${arg}.`);
|
|
123
|
+
options[key] = value;
|
|
124
|
+
i += 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (command === "design" && !options.out) throw new Error("Missing required --out DIR option.");
|
|
128
|
+
return options;
|
|
129
|
+
}
|
|
130
|
+
|
|
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) {
|
|
136
|
+
if (typeof Bun === "undefined") {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`actual-css ${command} needs Bun: it reads the theme in headless Chrome through Bun.WebView. Run it with bunx --bun actual-css ${command} ...`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function design({ themeFile, name, out }) {
|
|
144
|
+
requireBun("design");
|
|
145
|
+
const { exportDesign } = await import("../tooling/design-export.js");
|
|
146
|
+
const written = await exportDesign({ themeFile, name, out: resolve(out) });
|
|
147
|
+
console.log(`Exported ${written.length} files to ${resolve(out)}`);
|
|
148
|
+
}
|
|
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
|
+
}
|
|
91
159
|
}
|
|
92
160
|
|
|
93
161
|
async function bundle({ input, out, minify }) {
|
|
@@ -124,7 +192,8 @@ async function main() {
|
|
|
124
192
|
return;
|
|
125
193
|
}
|
|
126
194
|
|
|
127
|
-
|
|
195
|
+
const commands = { bundle, contrast, design };
|
|
196
|
+
await commands[args.command](args);
|
|
128
197
|
}
|
|
129
198
|
|
|
130
199
|
main().catch((error) => {
|
|
@@ -198,19 +198,26 @@ a.avatar[aria-disabled="true"] {
|
|
|
198
198
|
--avatar-stack-font-size: var(--font-size-lg);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
/* Infer the stack scale from its avatars. Progressive: without :has() the
|
|
202
|
+
explicit .avatar-stack.sm/.lg classes and the default size still size the
|
|
203
|
+
group, only the inference is lost. */
|
|
204
|
+
.avatar-stack:has(> .avatar.sm) {
|
|
205
|
+
--avatar-stack-size: 1.75rem;
|
|
206
|
+
--avatar-stack-font-size: var(--font-size-xs);
|
|
207
|
+
}
|
|
206
208
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
209
|
+
.avatar-stack:has(> .avatar.lg) {
|
|
210
|
+
--avatar-stack-size: 3rem;
|
|
211
|
+
--avatar-stack-font-size: var(--font-size-lg);
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
/* Overlapping avatars hide their neighbour: a translucent fill (--surface-subtle
|
|
215
|
+
on a glass palette) would show the next avatar's edge through it, most of all
|
|
216
|
+
when hover raises one above the other. The fill is layered over the opaque
|
|
217
|
+
role; on an opaque fill the base never shows. */
|
|
213
218
|
.avatar-stack > .avatar {
|
|
219
|
+
background-color: var(--surface-opaque);
|
|
220
|
+
background-image: linear-gradient(var(--avatar-bg), var(--avatar-bg));
|
|
214
221
|
box-shadow: 0 0 0 var(--avatar-stack-ring) var(--surface-opaque);
|
|
215
222
|
}
|
|
216
223
|
|
|
@@ -169,10 +169,6 @@
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
@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
172
|
:is(a.badge, button.badge):where(:not(.solid, .outline, :empty)) {
|
|
177
173
|
--badge-default-hover-bg: color-mix(
|
|
178
174
|
in oklab,
|
|
@@ -199,7 +195,7 @@
|
|
|
199
195
|
:where(:disabled, [aria-disabled="true"])
|
|
200
196
|
):hover {
|
|
201
197
|
/* Huge inset spread paints the overlay over the pill radius (see button.css). */
|
|
202
|
-
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
198
|
+
box-shadow: inset 0 0 0 999px var(--hover-overlay-solid, var(--hover-overlay));
|
|
203
199
|
}
|
|
204
200
|
|
|
205
201
|
button.badge {
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* A surface that insets its direct children relays that inset to them as
|
|
7
7
|
* --surface-pad; a direct .bleed child cancels it and reaches the surface
|
|
8
|
-
* edge. Card and drawer pad at the container level,
|
|
9
|
-
*
|
|
10
|
-
* owns it and the item does not.
|
|
8
|
+
* edge. Card and drawer pad at the container level, while modal pads its inner
|
|
9
|
+
* wrapper; each owns the token where that inset lives. The accordion pads its
|
|
10
|
+
* summary and panel instead, so the panel owns it and the item does not.
|
|
11
11
|
*
|
|
12
12
|
* Why the token is handed to the children rather than declared on the surface:
|
|
13
13
|
* custom properties inherit, so a surface-level declaration would also reach a
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
* Framework plumbing:
|
|
29
29
|
* --surface-pad (declared by the surface)
|
|
30
30
|
*/
|
|
31
|
-
:where(.card, .drawer, .accordion details > summary + *) > * * {
|
|
31
|
+
:where(.card, .drawer, dialog.modal > :is(form, .stack), .accordion details > summary + *) > * * {
|
|
32
32
|
--bleed-pad: initial;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
:where(.card, .drawer, .accordion details > summary + *) > * {
|
|
35
|
+
:where(.card, .drawer, dialog.modal > :is(form, .stack), .accordion details > summary + *) > * {
|
|
36
36
|
--bleed-pad: var(--surface-pad);
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -28,9 +28,9 @@
|
|
|
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
|
|
33
|
+
* --btn-border-block
|
|
34
34
|
* --spinner-width
|
|
35
35
|
*/
|
|
36
36
|
|
|
@@ -53,9 +53,12 @@
|
|
|
53
53
|
--btn-bg: var(--ui-bg, var(--btn-default-bg, var(--intent, var(--neutral))));
|
|
54
54
|
--btn-fg: var(--ui-fg, var(--btn-default-fg, var(--intent-fg, var(--neutral-fg))));
|
|
55
55
|
--btn-border: var(--ui-border, var(--btn-default-border, transparent));
|
|
56
|
-
--btn-default-hover-bg: var(--btn-bg);
|
|
57
56
|
--btn-radius: var(--radius);
|
|
58
|
-
--btn-hover-overlay: var(--hover-overlay,
|
|
57
|
+
--btn-hover-overlay: var(--hover-overlay-solid, var(--hover-overlay));
|
|
58
|
+
--btn-border-block: calc(
|
|
59
|
+
var(--border-width) +
|
|
60
|
+
var(--btn-border-block-end-width, var(--border-width))
|
|
61
|
+
);
|
|
59
62
|
|
|
60
63
|
/* Positioning context for indicators, overlays, and grouped states. */
|
|
61
64
|
position: relative;
|
|
@@ -65,7 +68,13 @@
|
|
|
65
68
|
gap: var(--btn-gap, 0.375em);
|
|
66
69
|
min-block-size: var(--btn-min-size);
|
|
67
70
|
padding-block: var(--space-20);
|
|
68
|
-
|
|
71
|
+
/* why: centres one line in --btn-min-size and keeps that rhythm when the
|
|
72
|
+
label wraps. min-block-size is a border-box length, so both borders come
|
|
73
|
+
out of it; without them .btn stood 2px taller than .input. */
|
|
74
|
+
padding-block: max(
|
|
75
|
+
var(--space-10),
|
|
76
|
+
calc((var(--btn-min-size) - 1lh - var(--btn-border-block)) / 2)
|
|
77
|
+
);
|
|
69
78
|
padding-inline: var(--btn-pad-x);
|
|
70
79
|
border: var(--border-width) solid var(--btn-border-color, var(--btn-border));
|
|
71
80
|
border-block-end-color: var(
|
|
@@ -125,10 +134,6 @@
|
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
128
|
-
.btn {
|
|
129
|
-
--btn-default-hover-bg: color-mix(in oklch, var(--btn-bg) 88%, black);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
137
|
.btn.soft {
|
|
133
138
|
--ui-hover-bg: color-mix(
|
|
134
139
|
in oklab,
|
|
@@ -144,9 +149,9 @@
|
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
/* The .soft/.outline/.ghost hover recipe (--ui-hover-bg) is set locally above;
|
|
147
|
-
the overlay darkening for solid
|
|
152
|
+
the overlay darkening for solid is .btn-specific. */
|
|
148
153
|
.btn:not(:where(:disabled, [aria-disabled="true"])):hover {
|
|
149
|
-
background: var(--ui-hover-bg, var(--btn-
|
|
154
|
+
background: var(--ui-hover-bg, var(--btn-bg));
|
|
150
155
|
/* Huge inset spread paints the overlay over any border-radius without a
|
|
151
156
|
pseudo-element; a plain background would replace the fill instead of
|
|
152
157
|
darkening it. */
|
|
@@ -162,6 +167,8 @@
|
|
|
162
167
|
--ui-fg: var(--intent, currentColor);
|
|
163
168
|
--ui-border: transparent;
|
|
164
169
|
--btn-radius: var(--radius-sm);
|
|
170
|
+
/* border: 0 below; the padding must not subtract a border it lacks. */
|
|
171
|
+
--btn-border-block: 0px;
|
|
165
172
|
|
|
166
173
|
min-block-size: auto;
|
|
167
174
|
padding-inline: 0;
|
|
@@ -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
|
}
|
|
@@ -16,8 +16,17 @@
|
|
|
16
16
|
* Public hooks:
|
|
17
17
|
* --close-size
|
|
18
18
|
* --close-icon-size
|
|
19
|
+
* --close-bg
|
|
20
|
+
* --close-color
|
|
21
|
+
* --close-hover-color
|
|
22
|
+
* --close-hover-overlay
|
|
19
23
|
*/
|
|
20
24
|
.close {
|
|
25
|
+
/* Routes the hover/active wash, as --btn-hover-overlay does for .btn. A
|
|
26
|
+
context that wants ink-only feedback (the dialog corner) sets it
|
|
27
|
+
transparent. */
|
|
28
|
+
--close-hover-overlay: var(--hover-overlay);
|
|
29
|
+
|
|
21
30
|
display: inline-grid;
|
|
22
31
|
place-items: center;
|
|
23
32
|
flex: none;
|
|
@@ -44,12 +53,12 @@
|
|
|
44
53
|
the control (the dialog corner's --surface-subtle) instead of replacing it. */
|
|
45
54
|
@media (hover: hover) {
|
|
46
55
|
.close:hover {
|
|
47
|
-
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
56
|
+
box-shadow: inset 0 0 0 999px var(--close-hover-overlay);
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
.close:active {
|
|
52
|
-
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
61
|
+
box-shadow: inset 0 0 0 999px var(--close-hover-overlay);
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
.close:focus-visible {
|
|
@@ -61,23 +70,25 @@
|
|
|
61
70
|
surface rather than to the header or a scroll container inside it. Matched
|
|
62
71
|
on the documented positions (the dialog, its form, its header, or a form
|
|
63
72
|
inside that header) so a .close inside content — an alert in the body —
|
|
64
|
-
keeps its own place. */
|
|
73
|
+
keeps its own place. The corner drops the wash: only the ink changes. */
|
|
65
74
|
:where(dialog):is(.modal, .drawer) > .close,
|
|
66
75
|
:where(dialog):is(.modal, .drawer) > :is(form, header) > .close,
|
|
67
76
|
:where(dialog):is(.modal, .drawer) > :is(form, header) > :is(form, header) > .close {
|
|
77
|
+
--close-hover-overlay: transparent;
|
|
78
|
+
|
|
68
79
|
position: absolute;
|
|
69
80
|
z-index: 1;
|
|
70
81
|
inset-block-start: var(--space-20);
|
|
71
82
|
inset-inline-end: var(--space-20);
|
|
72
|
-
background: var(--surface-subtle);
|
|
73
|
-
color: var(--text-muted);
|
|
83
|
+
background: var(--close-bg, var(--surface-subtle));
|
|
84
|
+
color: var(--close-color, var(--text-muted));
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
@media (hover: hover) {
|
|
77
88
|
:where(dialog):is(.modal, .drawer) > .close:hover,
|
|
78
89
|
:where(dialog):is(.modal, .drawer) > :is(form, header) > .close:hover,
|
|
79
90
|
:where(dialog):is(.modal, .drawer) > :is(form, header) > :is(form, header) > .close:hover {
|
|
80
|
-
color: var(--text);
|
|
91
|
+
color: var(--close-hover-color, var(--text));
|
|
81
92
|
}
|
|
82
93
|
}
|
|
83
94
|
|
|
@@ -22,6 +22,7 @@
|
|
|
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)
|
|
27
28
|
*/
|
|
@@ -31,6 +32,10 @@
|
|
|
31
32
|
/* The inset a direct .bleed child cancels (components/bleed.css). */
|
|
32
33
|
--surface-pad: var(--drawer-pad);
|
|
33
34
|
--drawer-closed-translate: -100%;
|
|
35
|
+
/* Header room for a corner close. Unconditional baseline: without :has()
|
|
36
|
+
(Degraded tier) a header keeps too much room rather than a title running
|
|
37
|
+
under the close. Zeroed when the drawer has no close. */
|
|
38
|
+
--drawer-close-reserve: calc(var(--close-size, var(--control-size)) + var(--space-30));
|
|
34
39
|
/* Exit is shorter than entry so the panel clears quickly; the scrim reads
|
|
35
40
|
the same variable through inheritance and never lags the panel. */
|
|
36
41
|
--drawer-motion-duration: var(--duration);
|
|
@@ -210,9 +215,20 @@
|
|
|
210
215
|
}
|
|
211
216
|
}
|
|
212
217
|
|
|
218
|
+
/* No close anywhere: reclaim the reserve. Deliberately coarse — a close in
|
|
219
|
+
the body keeps the reserve, which costs only room, never overlap. */
|
|
220
|
+
:where(dialog).drawer:not(:has(.close)) {
|
|
221
|
+
--drawer-close-reserve: 0px;
|
|
222
|
+
}
|
|
223
|
+
|
|
213
224
|
/* The header reserves inline room so the title never runs under the close
|
|
214
|
-
control. */
|
|
225
|
+
control. A .bleed header keeps at least its band inset. */
|
|
215
226
|
:where(dialog).drawer > header,
|
|
216
|
-
:where(dialog).drawer > form > header {
|
|
217
|
-
padding-inline-end:
|
|
227
|
+
:where(dialog).drawer > :where(form) > header {
|
|
228
|
+
padding-inline-end: var(--drawer-close-reserve);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
:where(dialog).drawer > header:where(.bleed),
|
|
232
|
+
:where(dialog).drawer > :where(form) > header:where(.bleed) {
|
|
233
|
+
padding-inline-end: max(var(--bleed-pad, 0px), var(--drawer-close-reserve));
|
|
218
234
|
}
|
|
@@ -50,6 +50,9 @@
|
|
|
50
50
|
position: absolute;
|
|
51
51
|
inset-block-start: calc(100% + var(--space-10));
|
|
52
52
|
inset-inline-start: 0;
|
|
53
|
+
/* Anatomy, not a default to replace: `flyout grid-3` (mega menu) takes its
|
|
54
|
+
columns from the grid preset and keeps this tight gap, because each
|
|
55
|
+
section pads itself — the visible gutter is section padding + gap. */
|
|
53
56
|
display: grid;
|
|
54
57
|
gap: var(--space-10);
|
|
55
58
|
inline-size: var(--flyout-inline-size, max-content);
|
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
* .sm/.lg are deliberately not supported.
|
|
12
12
|
*
|
|
13
13
|
* Public hooks:
|
|
14
|
+
* --meter-track
|
|
14
15
|
* --meter-value
|
|
15
16
|
* --bar-height
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
.meter {
|
|
20
|
+
--meter-track: var(--surface-subtle);
|
|
21
|
+
|
|
19
22
|
appearance: none;
|
|
20
23
|
-webkit-appearance: none;
|
|
21
24
|
display: block;
|
|
@@ -24,7 +27,7 @@
|
|
|
24
27
|
overflow: hidden;
|
|
25
28
|
border: 0;
|
|
26
29
|
border-radius: var(--radius-full);
|
|
27
|
-
background: var(--
|
|
30
|
+
background: var(--meter-track);
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/* A non-native meter is a weighted flex container. The element type keeps the
|
|
@@ -51,7 +54,7 @@
|
|
|
51
54
|
block-size: var(--bar-height);
|
|
52
55
|
border: 0;
|
|
53
56
|
border-radius: var(--radius-full);
|
|
54
|
-
background: var(--
|
|
57
|
+
background: var(--meter-track);
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
.meter::-webkit-meter-optimum-value {
|