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.
Files changed (92) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +29 -5
  3. package/components.json +15 -17
  4. package/dist/actual.full.css +403 -227
  5. package/dist/actual.full.min.css +1 -1
  6. package/docs/brand/favicon.svg +5 -0
  7. package/docs/brand/lockup.svg +12 -0
  8. package/docs/brand/logo-dark.svg +9 -0
  9. package/docs/brand/logo-light.svg +9 -0
  10. package/docs/brand/logo.svg +11 -0
  11. package/docs/brand/mark.svg +11 -0
  12. package/docs/navigation.json +4 -2
  13. package/docs/pages/components/alert.md +2 -10
  14. package/docs/pages/components/badge.md +3 -9
  15. package/docs/pages/components/bleed.md +5 -4
  16. package/docs/pages/components/card.md +31 -2
  17. package/docs/pages/components/close.md +9 -3
  18. package/docs/pages/components/dialog.md +248 -31
  19. package/docs/pages/components/drawer.md +2 -2
  20. package/docs/pages/components/meter.md +2 -0
  21. package/docs/pages/components/navbar.md +8 -9
  22. package/docs/pages/components/table.md +3 -3
  23. package/docs/pages/enhancements/flyout.md +2 -2
  24. package/docs/pages/enhancements/scrollspy.md +1 -1
  25. package/docs/pages/enhancements/status-bar.md +2 -2
  26. package/docs/pages/examples/overview.md +3 -1
  27. package/docs/pages/forms/date-picker.md +90 -0
  28. package/docs/pages/forms/detached-actions.md +37 -30
  29. package/docs/pages/forms/form-actions.md +61 -28
  30. package/docs/pages/forms/input-policy.md +1 -1
  31. package/docs/pages/forms/overview.md +6 -5
  32. package/docs/pages/forms/sticky-actions.md +85 -83
  33. package/docs/pages/forms/validation.md +5 -5
  34. package/docs/pages/foundations/philosophy.md +5 -3
  35. package/docs/pages/foundations/theming.md +15 -4
  36. package/docs/pages/foundations/tokens.md +50 -33
  37. package/docs/pages/foundations/typography.md +4 -7
  38. package/docs/pages/guides/bootstrap.md +2 -2
  39. package/docs/pages/guides/design-tools.md +36 -0
  40. package/docs/pages/guides/integrating-actual-css.md +5 -1
  41. package/docs/pages/guides/introduction.md +3 -4
  42. package/docs/pages/guides/modular-import.md +3 -0
  43. package/docs/pages/guides/progressive-enhancement.md +2 -2
  44. package/docs/pages/guides/tailwind.md +70 -0
  45. package/docs/pages/guides/upgrading.md +74 -0
  46. package/docs/pages/layout/cluster.md +28 -2
  47. package/docs/pages/layout/overview.md +1 -1
  48. package/docs/pages/layout/stack.md +21 -16
  49. package/docs/pages/layout/topbar.md +1 -1
  50. package/docs/pages/patterns/navigation-list.md +16 -46
  51. package/docs/pages/patterns/overview.md +3 -4
  52. package/docs/pages/utilities/flex.md +30 -20
  53. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  54. package/docs/pages/utilities/spacing.md +4 -3
  55. package/package.json +3 -2
  56. package/reserved-classes.json +3 -3
  57. package/skills/actual-css/references/enhancements.md +38 -0
  58. package/src/cli/actual-css.js +75 -6
  59. package/src/css/components/avatar.css +16 -9
  60. package/src/css/components/badge.css +1 -5
  61. package/src/css/components/bleed.css +5 -5
  62. package/src/css/components/button.css +17 -10
  63. package/src/css/components/card.css +18 -10
  64. package/src/css/components/chat.css +4 -5
  65. package/src/css/components/close.css +17 -6
  66. package/src/css/components/drawer.css +19 -3
  67. package/src/css/components/flyout.css +3 -0
  68. package/src/css/components/index.css +0 -1
  69. package/src/css/components/meter.css +5 -2
  70. package/src/css/components/modal.css +157 -69
  71. package/src/css/components/navbar.css +14 -25
  72. package/src/css/core/base.css +1 -1
  73. package/src/css/core/reset.css +2 -1
  74. package/src/css/core/theme.css +4 -0
  75. package/src/css/core/tokens.css +28 -10
  76. package/src/css/core/variants.css +41 -14
  77. package/src/css/forms/form-actions.css +15 -10
  78. package/src/css/forms/native.css +5 -1
  79. package/src/css/layout/cluster.css +11 -0
  80. package/src/css/layout/topbar.css +12 -6
  81. package/src/css/utilities/base.css +5 -0
  82. package/src/css/utilities/extra.css +3 -7
  83. package/src/tooling/color.js +65 -0
  84. package/src/tooling/design-components.js +286 -0
  85. package/src/tooling/design-export.js +69 -0
  86. package/src/tooling/design-tokens.js +474 -0
  87. package/src/tooling/penpot-plugin/manifest.json +7 -0
  88. package/src/tooling/penpot-plugin/plugin.js +272 -0
  89. package/src/tooling/penpot-plugin/ui.html +58 -0
  90. package/src/tooling/theme-contrast.js +209 -0
  91. package/docs/pages/patterns/actions.md +0 -52
  92. 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.
@@ -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
- "Bundle plain CSS @import chains from relative files and actual-css/css/* subpaths.",
17
- "A layer or layer(name) import is flattened into the @layer block it stands for.",
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
- await bundle(args);
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
- @supports selector(:has(> .avatar)) {
202
- .avatar-stack:has(> .avatar.sm) {
203
- --avatar-stack-size: 1.75rem;
204
- --avatar-stack-font-size: var(--font-size-xs);
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
- .avatar-stack:has(> .avatar.lg) {
208
- --avatar-stack-size: 3rem;
209
- --avatar-stack-font-size: var(--font-size-lg);
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, so they own the token
9
- * there; the accordion pads its summary and its panel instead, so the panel
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, rgb(0 0 0 / 0.08));
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
- padding-block: max(var(--space-10), calc((var(--btn-min-size) - 1lh) / 2));
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/soft is .btn-specific. */
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-default-hover-bg));
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; a direct footer is
8
- * anchored to the card's bottom edge when extra block space is available (so
9
- * price/action rows align across equal-height cards in a grid). A bare card
10
- * owns its direct-child rhythm; a layout primitive on the same element takes
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
- .card > :where(header, footer) {
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
- @supports selector(:has(*)) {
99
- .chat:not(:has(> .chat-avatar)) {
100
- column-gap: 0;
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: calc(var(--close-size, var(--control-size)) + var(--space-30));
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);
@@ -3,7 +3,6 @@
3
3
  @import "./close.css";
4
4
  @import "./badge.css";
5
5
  @import "./alert.css";
6
- @import "./actions.css";
7
6
 
8
7
  /* Surfaces */
9
8
  @import "./card.css";
@@ -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(--surface-subtle);
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(--surface-subtle);
57
+ background: var(--meter-track);
55
58
  }
56
59
 
57
60
  .meter::-webkit-meter-optimum-value {