actual-css 0.11.0 → 0.11.1

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.
@@ -9,7 +9,8 @@
9
9
  "modular-import",
10
10
  "progressive-enhancement",
11
11
  "bootstrap",
12
- "tailwind"
12
+ "tailwind",
13
+ "design-tools"
13
14
  ]
14
15
  },
15
16
  {
@@ -3,10 +3,11 @@
3
3
  `.bleed` lets a direct child of a padded surface reach its edges. Use it for a
4
4
  full-width image, a colored header or footer, or a notice band.
5
5
 
6
- It works inside `.card`, `.drawer`, and an accordion panel. The accordion panel
7
- owns the padding, so place `.bleed` directly inside the panel, after the summary.
8
- A deeper descendant stays within its wrapper. A modular build imports
9
- `actual-css/css/components/bleed` alongside the surface component.
6
+ It works inside `.card`, `.drawer`, a modal's direct `<form>` or `.stack`, and
7
+ an accordion panel. The accordion panel owns the padding, so place `.bleed`
8
+ directly inside the panel, after the summary. A deeper descendant stays within
9
+ its wrapper. A modular build imports `actual-css/css/components/bleed`
10
+ alongside the surface component.
10
11
 
11
12
  ```html demo
12
13
  <article class="card stack" style="--card-max-inline-size: 24rem">
@@ -28,7 +28,9 @@
28
28
  In a `.modal` or a `.drawer`, the close sits out of the content flow at the
29
29
  panel's top inline-end corner. Place it as a direct child of the dialog, of its
30
30
  top-level `form` or `header`, or of a `form` inside that header; a `.close`
31
- deeper in the content keeps its own place. See [Dialog](dialog.md) and
31
+ deeper in the content keeps its own place, so wrapping it in a `.cluster` keeps
32
+ it in the header's row. A header that hosts the corner close reserves inline
33
+ room so a title never runs under it. See [Dialog](dialog.md) and
32
34
  [Drawer](drawer.md).
33
35
 
34
36
  ```html
@@ -43,6 +45,10 @@ deeper in the content keeps its own place. See [Dialog](dialog.md) and
43
45
 
44
46
  - `--close-size` — inline and block size of the button; `--control-size` unless the container sets it.
45
47
  - `--close-icon-size` — size of the X.
48
+ - `--close-bg` — background of the dialog corner close; `--surface-subtle` unless the dialog or a painted band sets it, e.g. `transparent` for a bare X.
49
+ - `--close-color` — ink of the X; `--text-muted` at the dialog corner, otherwise inherited from the context.
50
+ - `--close-hover-color` — ink of the dialog corner close on hover; `--text` unless a band sets its own foreground.
51
+ - `--close-hover-overlay` — hover and active wash that tints the fill the context gave the control; the dialog corner sets it `transparent` so a hover changes only the ink.
46
52
 
47
- Alerts and badges declare both hooks on themselves, so set them on the container
48
- or on the button.
53
+ Alerts and badges declare both size hooks on themselves, so set them on the
54
+ container or on the button.
@@ -14,9 +14,37 @@
14
14
  | `.dialog-icon` | Component | Circular intent-aware icon well. |
15
15
  | `.close` | Composition | Icon-only close button, at the top end. |
16
16
 
17
- A [`.close`](close.md) placed as a direct child of the dialog or of its form
18
- sits outside the content flow, so it does not take part in the dialog's own
19
- layout.
17
+ ## Anatomy
18
+
19
+ `.modal` is the outer surface and deliberately has no inner padding. Wrap its
20
+ content in a direct `<form>` or `.stack`; that wrapper owns the padding, rhythm,
21
+ and overflow. Put `.stack` on the wrapper, never on `dialog.modal` itself.
22
+
23
+ For a header band, place `<header>` directly under `dialog.modal`, before the
24
+ body wrapper. Put a flexible `.media` title region first and a `.cluster` of
25
+ controls second. The title has the close control's minimum height; its leading
26
+ icon and every trailing control share the same center axis. A wrapped title
27
+ simply makes the row taller.
28
+
29
+ A direct `dialog > header` is dialog chrome and forms a distinct header band.
30
+ Put an ordinary content heading inside the padded `form` or `.stack` instead.
31
+
32
+ Keep a corner [`.close`](close.md) as a direct child of the dialog and put the
33
+ title in the wrapper's direct `<header>`. The button then stays outside the
34
+ content flow while the header reserves enough inline space to keep its title
35
+ clear.
36
+
37
+ A `.close` placed as a direct child of the chrome header is still the corner
38
+ overlay. That header reserves the same inline room for it, never below its own
39
+ `--modal-pad`, so a wrapping title stays clear either way.
40
+
41
+ The corner close overlays the panel edge, so a body tall enough to scroll would
42
+ run its scrollbar under the button. Give such a dialog the `scrollable` anatomy:
43
+ the close then sits over the fixed header and the scrollbar starts below it.
44
+
45
+ Add `.bleed` to that header when it should form a full-width band. It reaches
46
+ the dialog edges, keeps the modal's inner padding, and can take a surface token
47
+ such as `background: var(--surface-subtle)`.
20
48
 
21
49
  ## Usage
22
50
 
@@ -85,7 +113,8 @@ For a compact destructive confirmation with a leading status icon, compose
85
113
  `dialog-confirmation` with the media object. The icon well accepts the shared
86
114
  intent and emphasis classes; `dialog-icon danger soft` creates the tinted red
87
115
  circle while keeping the glyph centered. The footer becomes a separate action
88
- band without changing the semantics of the form or its buttons.
116
+ band whose two actions sit at its edges, so the cancel-style control never sits
117
+ next to the committing one.
89
118
 
90
119
  ```html demo
91
120
  <button class="btn danger"
@@ -121,6 +150,49 @@ band without changing the semantics of the form or its buttons.
121
150
  </dialog>
122
151
  ```
123
152
 
153
+ For a trailing illustration, use a direct `<header>` as the message region and
154
+ compose it as a non-wrapping split cluster. `.grow` keeps the text column
155
+ flexible while `.dialog-icon` stays at the inline end. The confirmation variant
156
+ pads either this header or the leading-icon `.media` shape above.
157
+
158
+ Use the same anatomy in a `.card` instead when the surface belongs in page
159
+ content and does not need modal focus or a backdrop.
160
+
161
+ ```html demo
162
+ <button class="btn"
163
+ type="button"
164
+ commandfor="customize-dialog"
165
+ command="show-modal"
166
+ aria-haspopup="dialog"
167
+ aria-controls="customize-dialog">
168
+ Customize workspace
169
+ </button>
170
+
171
+ <dialog class="modal dialog-confirmation"
172
+ id="customize-dialog"
173
+ closedby="none"
174
+ aria-labelledby="customize-dialog-title">
175
+ <form method="dialog">
176
+ <header class="cluster"
177
+ style="--cluster-align: start; --cluster-justify: space-between; --cluster-wrap: nowrap">
178
+ <hgroup class="grow">
179
+ <h3 id="customize-dialog-title">Make this workspace your own</h3>
180
+ <p class="muted">Choose colors and defaults now, or continue with the current settings.</p>
181
+ </hgroup>
182
+
183
+ <span class="dialog-icon success soft" aria-hidden="true">
184
+ <i class="ti ti-check"></i>
185
+ </span>
186
+ </header>
187
+
188
+ <footer>
189
+ <button class="btn primary" value="customize">Customize</button>
190
+ <button class="btn ghost" value="later">Later</button>
191
+ </footer>
192
+ </form>
193
+ </dialog>
194
+ ```
195
+
124
196
  ## Information modal
125
197
 
126
198
  Use this shape for contextual information or lightweight secondary content. It
@@ -163,6 +235,67 @@ has no action button; the header close button dismisses the dialog.
163
235
  </dialog>
164
236
  ```
165
237
 
238
+ ## Action modal
239
+
240
+ Use this shape when contextual information leads to one non-destructive next
241
+ step, such as reviewing a setup before continuing. Unlike an alert dialog, it
242
+ can be dismissed without choosing the action; unlike an information modal, it
243
+ has a primary action in a footer.
244
+
245
+ Use `.dialog-icon` with the media object for a leading title icon. An `.alert`
246
+ belongs in the body only when the dialog contains a distinct status message;
247
+ the dialog title itself is not an alert.
248
+
249
+ ```html demo
250
+ <button class="btn primary"
251
+ type="button"
252
+ commandfor="setup-dialog"
253
+ command="show-modal"
254
+ aria-haspopup="dialog"
255
+ aria-controls="setup-dialog">
256
+ Continue setup
257
+ </button>
258
+
259
+ <dialog class="modal"
260
+ id="setup-dialog"
261
+ closedby="any"
262
+ data-dialog-dismissible
263
+ aria-labelledby="setup-dialog-title">
264
+ <header>
265
+ <div class="media items-center grow">
266
+ <span class="dialog-icon warning soft" aria-hidden="true">
267
+ <i class="ti ti-alert-triangle"></i>
268
+ </span>
269
+
270
+ <h2 id="setup-dialog-title">Finish workspace setup</h2>
271
+ </div>
272
+
273
+ <div class="cluster">
274
+ <button class="close"
275
+ type="button"
276
+ commandfor="setup-dialog"
277
+ command="request-close"
278
+ aria-controls="setup-dialog"
279
+ aria-label="Close dialog"></button>
280
+ </div>
281
+ </header>
282
+
283
+ <form method="dialog" class="stack">
284
+ <div class="card stack">
285
+ <p><strong>Acme workspace</strong></p>
286
+ <p>Team plan</p>
287
+ <p>Starts October 1</p>
288
+ </div>
289
+
290
+ <p>Confirm these settings to finish setting up the workspace.</p>
291
+
292
+ <footer>
293
+ <button class="btn primary inline-size-full" value="continue">Continue</button>
294
+ </footer>
295
+ </form>
296
+ </dialog>
297
+ ```
298
+
166
299
  ## Scrollable modal
167
300
 
168
301
  Use `modal scrollable` when the header and footer should stay visible while the
@@ -371,20 +504,16 @@ Prefer native dialog behavior whenever possible. The framework runtime should
371
504
  not replace the platform modal system; it should only make dialogs declarative,
372
505
  animation-friendly, and consistent across supported browsers.
373
506
 
374
- The close button is anchored to the dialog surface, not the content flow: it
375
- sits at the top `inline-end`, inside the panel. The button itself is out of
376
- flow, while the header reserves enough inline padding to keep its title clear.
377
- Keep the button a direct child of `dialog.modal` so the scrolling content
378
- (`> form` or `> .stack`) never competes with it.
379
-
380
507
  ## CSS hooks
381
508
 
382
509
  - `--modal-size` — maximum dialog width. Fallback-only, so a class on the
383
510
  dialog or an inherited value both reach it. A dialog is shrink-to-fit: a
384
511
  composition that must fill this width sets `inline-size` as well.
512
+ - `--modal-pad` — padding of the direct `<form>` or `.stack` content wrapper.
513
+ - `--modal-header-bg` — background of a direct header band.
385
514
  - `--dialog-viewport-gap` — distance kept between the dialog and the viewport edges.
386
515
  - `--dialog-icon-size` — diameter of the `.dialog-icon` circle.
387
516
  - `--dialog-icon-glyph-size` — size of the glyph centered inside `.dialog-icon`.
388
517
  - `--close-size` / `--close-icon-size` — size of the corner `.close` and of its
389
- X. The header reserves the close size plus `--space-30` on its inline end so
518
+ X. The header reserves the close size, its edge inset, and a content gap so
390
519
  the title never runs under it.
@@ -70,6 +70,8 @@ marker to that background rather than to the neutral intent.
70
70
 
71
71
  ### Hooks
72
72
 
73
+ - `--meter-track` — track color; raise it when the meter sits on a
74
+ `--surface-subtle` card, where the default track would disappear.
73
75
  - `--bar-height` — bar thickness, shared with `.progress`.
74
76
  - `--meter-value` — relative weight of a direct child in a segmented meter.
75
77
 
@@ -28,6 +28,7 @@ Whole pages, composed the way a real product would.
28
28
  - [Marketing](../../demo/templates/marketing.html) - landing page with typography and fluid type
29
29
  - [Blog](../../demo/templates/blog.html) - article page with prose
30
30
  - [Settings modal](../../demo/templates/settings-modal.html) - two-pane preferences dialog, with the local CSS it needs audited rule by rule
31
+ - [Modal patterns](../../demo/templates/modal-patterns.html) - eight application modals, each the modal surface plus the smallest application CSS it needs
31
32
  - [Workspace settings](../../demo/templates/settings-pricing.html) - plans with visible native choices, preferences and a local segmented tab treatment
32
33
  - [Card compositions](../../demo/templates/card-compositions.html) - editorial, team, sign-in and horizontal cards composed from existing primitives
33
34
  - [Register form](../../demo/templates/register-form.html) - registration page with choice cards, masks, the password toggle, `tel-prefix` validation, an OTP verification dialog, and status feedback
@@ -0,0 +1,36 @@
1
+ # Design tools (Figma, Penpot)
2
+
3
+ `actual-css design` exports a theme to design tools: its tokens for Figma and Penpot, and a Penpot plugin that builds Button, Input, Badge, Alert and Card bound to those tokens. Use it to hand designers the same palette, scale and components the code ships — a design kit, a UI kit, a Figma or Penpot library generated from your theme.
4
+
5
+ ```sh
6
+ bunx actual-css design --theme src/theme.css --out design/
7
+ ```
8
+
9
+ `--theme` is a file declaring one `[data-theme="…"]` block (see [Theming](../foundations/theming.md)); without it the default theme is exported. The command needs Bun: it reads the theme in headless Chrome, so every `light-dark()`, `color-mix()` and `var()` chain resolves exactly as the browser paints it.
10
+
11
+ ## What is exported
12
+
13
+ | File | Content |
14
+ | --------------------------------------------- | ------------------------------------ |
15
+ | `figma/foundations.tokens.json` | spacing, radius, borders, type sizes |
16
+ | `figma/light.tokens.json`, `dark.tokens.json` | colors, one file per scheme |
17
+ | `penpot/tokens.json` | all tokens, as sets and themes |
18
+ | `penpot/plugin/` | the plugin and component spec |
19
+
20
+ Token names are the CSS names without `--`: `surface-raised` in the design tool is `var(--surface-raised)` in code. A scheme exists when the theme declares it: `color-scheme: light dark` exports both, anything else one. Lengths are exported in px, with the CSS value kept in `$extensions["actual-css"]`.
21
+
22
+ Colors no theme token paints — the soft fills and borders of `.soft` — are exported as recipe tokens named `<variant>-<intent>-<role>` (`soft-danger-bg`, `soft-danger-border`, `soft-danger-fg`), next to the theme colors so they switch with the scheme. Font stacks, line heights, shadows, motion and z-index have no design-tool equivalent and are not exported.
23
+
24
+ ## Figma
25
+
26
+ Import each file from the Variables panel. Figma makes one collection per file: import `light`, then add `dark` to that collection as a second mode (a paid-plan feature; the free plan allows one mode per collection).
27
+
28
+ ## Penpot
29
+
30
+ 1. Tokens tab → Tools → Import `penpot/tokens.json`. It replaces the file's tokens.
31
+ 2. Serve `penpot/plugin/` over http with CORS, and add its `manifest.json` URL in the plugin manager (`Ctrl + Alt + P`).
32
+ 3. Run **Build components**.
33
+
34
+ The components are built on their own page, "Actual CSS components"; drag instances from Assets onto any page. Each is a variant set whose properties are the classes an author stacks — `Intent`, `Variant` and `Size` on Button and Badge, `Intent` and `Variant` on Alert, `Size` on Input — defaulting to the bare class (`.btn`, `.badge`). Fills, borders and text colors are bound to tokens, so switching the Penpot theme between Light and Dark repaints every variant. Radius, padding, height and type are the measured values: run the export again after changing them in the theme.
35
+
36
+ The components are measured, not modelled: the browser renders the classes and the plugin replays the result, so a theme's own radius or padding shows up without configuration. States (hover, focus, disabled) stay a code concern.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actual-css",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Plain CSS component framework with semantic classes, universal variants, small tokens, theme hooks, and progressive enhancements.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.4.2",
@@ -131,6 +131,7 @@
131
131
  "build:js": "bun scripts/build-js.js",
132
132
  "build:themes": "bun scripts/build-themes.js",
133
133
  "build:size": "bun scripts/build-size.js",
134
+ "export:design": "bun scripts/export-design.js",
134
135
  "check:enhance": "bun run scripts/check-enhance.js",
135
136
  "generate:reserved": "bun scripts/generate-reserved-classes.js",
136
137
  "check:reserved": "bun scripts/check-reserved.js",
@@ -21,6 +21,41 @@ 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>`.
44
+
45
+ Use `.bleed` on that direct header when it needs a contrasting full-width band;
46
+ the wrapper relays the modal padding so the band reaches the surface edges while
47
+ its contents keep the normal inset. In `.dialog-confirmation`, keep the icon and
48
+ the text header as the media object's two children; the component centers the
49
+ header's first heading on the icon while following copy stays in that text
50
+ column. For a trailing icon, use the confirmation's direct header as a
51
+ non-wrapping split `.cluster`, with `.grow` on the text group. Do not compensate
52
+ heading alignment with a literal margin.
53
+
54
+ Do not turn the dialog heading into an `.alert` to obtain icon or surface
55
+ styling. Compose `.dialog-icon` with `.media` for a leading title icon; reserve
56
+ `.alert` for a distinct status message in the dialog body. Inspect
57
+ `docs/pages/components/dialog.md` in the installed package for current recipes.
58
+
24
59
  ## Custom widgets
25
60
 
26
61
  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,23 @@ 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",
10
11
  "",
11
- " -o, --out FILE write the bundle to FILE (required)",
12
+ " -o, --out FILE write the bundle to FILE / the export to DIR (required)",
12
13
  " --minify collapse comments and whitespace",
14
+ " --theme FILE export this [data-theme] file instead of the default theme",
15
+ " --name NAME the data-theme name, when the file declares several",
13
16
  " -h, --help show this message",
14
17
  " --version print the actual-css version",
15
18
  "",
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.",
19
+ "bundle: flatten plain CSS @import chains from relative files and actual-css/css/*",
20
+ "subpaths. A layer or layer(name) import is flattened into the @layer block it",
21
+ "stands for. Remote imports are kept and hoisted to the top of the bundle.",
19
22
  "Leaves modern CSS syntax untouched; this is a bundler, not a transpiler.",
23
+ "",
24
+ "design: export a theme's tokens for Figma and Penpot, plus Button, Input, Badge,",
25
+ "Alert and Card measured in headless Chrome and a Penpot plugin that builds them.",
26
+ "Needs Bun (bunx actual-css design ...).",
20
27
  ].join("\n");
21
28
  }
22
29
 
@@ -36,6 +43,10 @@ function parseArgs(argv) {
36
43
  return { version: true };
37
44
  }
38
45
 
46
+ if (command === "design") {
47
+ return parseDesignArgs(rest);
48
+ }
49
+
39
50
  if (command !== "bundle") {
40
51
  throw new Error(`Unknown command: ${command}`);
41
52
  }
@@ -87,7 +98,39 @@ function parseArgs(argv) {
87
98
  throw new Error("Missing required --out FILE option.");
88
99
  }
89
100
 
90
- return { help: false, input, out, minify };
101
+ return { help: false, command: "bundle", input, out, minify };
102
+ }
103
+
104
+ function parseDesignArgs(rest) {
105
+ const options = { help: false, command: "design", themeFile: null, name: null, out: "" };
106
+ const valued = { "--theme": "themeFile", "--name": "name", "--out": "out", "-o": "out" };
107
+
108
+ for (let i = 0; i < rest.length; i += 1) {
109
+ const arg = rest[i];
110
+ if (arg === "--help" || arg === "-h") return { help: true };
111
+ const key = valued[arg];
112
+ if (!key) throw new Error(`Unknown option: ${arg}`);
113
+ const value = rest[i + 1];
114
+ if (!value) throw new Error(`Missing value for ${arg}.`);
115
+ options[key] = value;
116
+ i += 1;
117
+ }
118
+
119
+ if (!options.out) throw new Error("Missing required --out DIR option.");
120
+ return options;
121
+ }
122
+
123
+ async function design({ themeFile, name, out }) {
124
+ // Checked before the import: under Node the module does not even parse
125
+ // (`await using`), and `bundle` must stay usable there.
126
+ if (typeof Bun === "undefined") {
127
+ throw new Error(
128
+ "actual-css design needs Bun: it reads the theme in headless Chrome through Bun.WebView. Run it with bunx.",
129
+ );
130
+ }
131
+ const { exportDesign } = await import("../tooling/design-export.js");
132
+ const written = await exportDesign({ themeFile, name, out: resolve(out) });
133
+ console.log(`Exported ${written.length} files to ${resolve(out)}`);
91
134
  }
92
135
 
93
136
  async function bundle({ input, out, minify }) {
@@ -124,7 +167,7 @@ async function main() {
124
167
  return;
125
168
  }
126
169
 
127
- await bundle(args);
170
+ await (args.command === "design" ? design(args) : bundle(args));
128
171
  }
129
172
 
130
173
  main().catch((error) => {
@@ -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
 
@@ -31,6 +31,7 @@
31
31
  * --btn-default-hover-bg
32
32
  * --btn-border-block-end-color
33
33
  * --btn-border-block-end-width
34
+ * --btn-border-block
34
35
  * --spinner-width
35
36
  */
36
37
 
@@ -56,6 +57,10 @@
56
57
  --btn-default-hover-bg: var(--btn-bg);
57
58
  --btn-radius: var(--radius);
58
59
  --btn-hover-overlay: var(--hover-overlay, rgb(0 0 0 / 0.08));
60
+ --btn-border-block: calc(
61
+ var(--border-width) +
62
+ var(--btn-border-block-end-width, var(--border-width))
63
+ );
59
64
 
60
65
  /* Positioning context for indicators, overlays, and grouped states. */
61
66
  position: relative;
@@ -65,7 +70,13 @@
65
70
  gap: var(--btn-gap, 0.375em);
66
71
  min-block-size: var(--btn-min-size);
67
72
  padding-block: var(--space-20);
68
- padding-block: max(var(--space-10), calc((var(--btn-min-size) - 1lh) / 2));
73
+ /* why: centres one line in --btn-min-size and keeps that rhythm when the
74
+ label wraps. min-block-size is a border-box length, so both borders come
75
+ out of it; without them .btn stood 2px taller than .input. */
76
+ padding-block: max(
77
+ var(--space-10),
78
+ calc((var(--btn-min-size) - 1lh - var(--btn-border-block)) / 2)
79
+ );
69
80
  padding-inline: var(--btn-pad-x);
70
81
  border: var(--border-width) solid var(--btn-border-color, var(--btn-border));
71
82
  border-block-end-color: var(
@@ -162,6 +173,8 @@
162
173
  --ui-fg: var(--intent, currentColor);
163
174
  --ui-border: transparent;
164
175
  --btn-radius: var(--radius-sm);
176
+ /* border: 0 below; the padding must not subtract a border it lacks. */
177
+ --btn-border-block: 0px;
165
178
 
166
179
  min-block-size: auto;
167
180
  padding-inline: 0;
@@ -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
 
@@ -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 {