actual-css 0.11.1 → 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 (80) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +26 -6
  3. package/components.json +4 -16
  4. package/dist/actual.full.css +296 -267
  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 +3 -2
  13. package/docs/pages/components/alert.md +2 -10
  14. package/docs/pages/components/badge.md +3 -9
  15. package/docs/pages/components/card.md +31 -2
  16. package/docs/pages/components/dialog.md +121 -33
  17. package/docs/pages/components/drawer.md +2 -2
  18. package/docs/pages/components/navbar.md +8 -9
  19. package/docs/pages/components/table.md +3 -3
  20. package/docs/pages/enhancements/flyout.md +2 -2
  21. package/docs/pages/enhancements/scrollspy.md +1 -1
  22. package/docs/pages/enhancements/status-bar.md +2 -2
  23. package/docs/pages/examples/overview.md +2 -1
  24. package/docs/pages/forms/date-picker.md +90 -0
  25. package/docs/pages/forms/detached-actions.md +37 -30
  26. package/docs/pages/forms/form-actions.md +61 -28
  27. package/docs/pages/forms/input-policy.md +1 -1
  28. package/docs/pages/forms/overview.md +6 -5
  29. package/docs/pages/forms/sticky-actions.md +85 -83
  30. package/docs/pages/forms/validation.md +5 -5
  31. package/docs/pages/foundations/philosophy.md +5 -3
  32. package/docs/pages/foundations/theming.md +15 -4
  33. package/docs/pages/foundations/tokens.md +50 -33
  34. package/docs/pages/foundations/typography.md +4 -7
  35. package/docs/pages/guides/bootstrap.md +2 -2
  36. package/docs/pages/guides/design-tools.md +2 -2
  37. package/docs/pages/guides/integrating-actual-css.md +5 -1
  38. package/docs/pages/guides/introduction.md +3 -4
  39. package/docs/pages/guides/modular-import.md +3 -0
  40. package/docs/pages/guides/progressive-enhancement.md +2 -2
  41. package/docs/pages/guides/tailwind.md +70 -0
  42. package/docs/pages/guides/upgrading.md +74 -0
  43. package/docs/pages/layout/cluster.md +28 -2
  44. package/docs/pages/layout/overview.md +1 -1
  45. package/docs/pages/layout/stack.md +21 -16
  46. package/docs/pages/layout/topbar.md +1 -1
  47. package/docs/pages/patterns/navigation-list.md +16 -46
  48. package/docs/pages/patterns/overview.md +3 -4
  49. package/docs/pages/utilities/flex.md +30 -20
  50. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  51. package/docs/pages/utilities/spacing.md +4 -3
  52. package/package.json +2 -2
  53. package/reserved-classes.json +3 -3
  54. package/skills/actual-css/references/enhancements.md +10 -7
  55. package/src/cli/actual-css.js +39 -13
  56. package/src/css/components/avatar.css +16 -9
  57. package/src/css/components/badge.css +1 -5
  58. package/src/css/components/button.css +3 -9
  59. package/src/css/components/card.css +18 -10
  60. package/src/css/components/chat.css +4 -5
  61. package/src/css/components/drawer.css +19 -3
  62. package/src/css/components/flyout.css +3 -0
  63. package/src/css/components/index.css +0 -1
  64. package/src/css/components/modal.css +96 -124
  65. package/src/css/components/navbar.css +14 -25
  66. package/src/css/core/base.css +1 -1
  67. package/src/css/core/reset.css +2 -1
  68. package/src/css/core/theme.css +4 -0
  69. package/src/css/core/tokens.css +28 -10
  70. package/src/css/core/variants.css +41 -14
  71. package/src/css/forms/form-actions.css +15 -10
  72. package/src/css/layout/cluster.css +11 -0
  73. package/src/css/layout/topbar.css +12 -6
  74. package/src/css/utilities/base.css +5 -0
  75. package/src/css/utilities/extra.css +3 -7
  76. package/src/tooling/color.js +65 -0
  77. package/src/tooling/design-tokens.js +9 -1
  78. package/src/tooling/theme-contrast.js +209 -0
  79. package/docs/pages/patterns/actions.md +0 -52
  80. package/src/css/components/actions.css +0 -27
@@ -1,14 +1,15 @@
1
1
  # Navigation List
2
2
 
3
- Use `.nav-list` for a semantic list of navigation links.
4
-
5
- It removes native list chrome but does not force a direction. Combine it with layout primitives depending on context.
3
+ Use `.nav-list` for a vertical list of navigation links: it removes native list
4
+ chrome, stacks the rows and stretches each `.nav-link` across the row. For a
5
+ horizontal row of links, use `.cluster` on the list, which removes the list
6
+ chrome too.
6
7
 
7
8
  ```html demo
8
9
  <div class="stack">
9
10
  <h2>Navigation</h2>
10
11
  <nav aria-label="Main navigation">
11
- <ul class="nav-list cluster">
12
+ <ul class="cluster">
12
13
  <li><a href="/docs">Docs</a></li>
13
14
  <li><a href="/components">Components</a></li>
14
15
  <li><a href="/examples">Examples</a></li>
@@ -16,7 +17,7 @@ It removes native list chrome but does not force a direction. Combine it with la
16
17
  </nav>
17
18
  <h2>Documentation</h2>
18
19
  <nav aria-label="Documentation">
19
- <ul class="nav-list stack">
20
+ <ul class="nav-list">
20
21
  <li><a href="/docs/getting-started" aria-current="page">Getting started</a></li>
21
22
  <li><a href="/docs/tokens">Tokens</a></li>
22
23
  <li><a href="/docs/layout">Layout</a></li>
@@ -26,14 +27,6 @@ It removes native list chrome but does not force a direction. Combine it with la
26
27
  </div>
27
28
  ```
28
29
 
29
- Use `.nav-list` for:
30
-
31
- - header navigation
32
- - side navigation
33
- - footer navigation
34
- - tab-like navigation lists
35
- - documentation navigation
36
-
37
30
  Do not reset every list inside `nav`.
38
31
 
39
32
  ```css
@@ -51,7 +44,7 @@ Prefer an explicit pattern class.
51
44
 
52
45
  ```html demo
53
46
  <nav aria-label="Footer">
54
- <ul class="nav-list cluster">
47
+ <ul class="cluster">
55
48
  <li><a href="/privacy">Privacy</a></li>
56
49
  <li><a href="/terms">Terms</a></li>
57
50
  <li><a href="/contact">Contact</a></li>
@@ -69,7 +62,7 @@ Header navigation is usually a composition of landmarks, layout primitives, and
69
62
  <a class="brand" href="/">Actual CSS</a>
70
63
 
71
64
  <nav aria-label="Main navigation">
72
- <ul class="nav-list cluster">
65
+ <ul class="cluster">
73
66
  <li><a href="/docs">Docs</a></li>
74
67
  <li><a href="/components">Components</a></li>
75
68
  <li><a href="/examples">Examples</a></li>
@@ -85,8 +78,7 @@ In this example:
85
78
  - `.center` constrains the content.
86
79
  - `.cluster` arranges the brand and navigation.
87
80
  - `nav` provides navigation semantics.
88
- - `.nav-list` normalizes the navigation list.
89
- - the inner `.cluster` arranges navigation items horizontally.
81
+ - the inner `.cluster` removes the list chrome and arranges the links horizontally.
90
82
 
91
83
  A project may add `.site-header` when it needs a visual shell.
92
84
 
@@ -106,7 +98,7 @@ A project may add `.site-header` when it needs a visual shell.
106
98
  <aside class="shell-sidebar-nav" aria-label="Primary">
107
99
  <a class="navbar-brand" href="#"><span class="avatar primary"><abbr>AC</abbr></span> Product</a>
108
100
  <nav aria-label="Primary">
109
- <ul class="nav-list stack">
101
+ <ul class="nav-list">
110
102
  <li><a class="nav-link" href="#" aria-current="page">Overview</a></li>
111
103
  <li><a class="nav-link" href="#">Settings</a></li>
112
104
  </ul>
@@ -167,7 +159,7 @@ Side navigation is usually a `.nav-list` composed with `.stack`.
167
159
  ```html demo
168
160
  <aside>
169
161
  <nav aria-label="Documentation">
170
- <ul class="nav-list stack">
162
+ <ul class="nav-list">
171
163
  <li><a href="/docs/getting-started" aria-current="page">Getting started</a></li>
172
164
  <li><a href="/docs/tokens">Tokens</a></li>
173
165
  <li><a href="/docs/layout">Layout</a></li>
@@ -182,7 +174,7 @@ A project may add `.side-nav` only when it needs specific visual behavior such a
182
174
  ```html demo
183
175
  <aside class="side-nav">
184
176
  <nav aria-label="Documentation">
185
- <ul class="nav-list stack">
177
+ <ul class="nav-list">
186
178
  <li><a href="/docs/getting-started" aria-current="page">Getting started</a></li>
187
179
  <li><a href="/docs/tokens">Tokens</a></li>
188
180
  <li><a href="/docs/layout">Layout</a></li>
@@ -206,30 +198,8 @@ A project may add `.side-nav` only when it needs specific visual behavior such a
206
198
  }
207
199
  ```
208
200
 
209
- ### Optional Navigation Link Styling
210
-
211
- `.nav-list` can stay purely structural. If the framework wants navigation links to have a default hit area and active state, this can be added as a lightweight visual pattern.
212
-
213
- ```css
214
- .nav-list a {
215
- display: flex;
216
- align-items: center;
217
- gap: var(--space-20);
218
- min-block-size: 2rem;
219
- padding-inline: var(--space-20);
220
- border-radius: var(--radius-sm);
221
- color: inherit;
222
- text-decoration: none;
223
- }
224
-
225
- .nav-list a:hover {
226
- background: var(--surface-subtle);
227
- }
228
-
229
- .nav-list a[aria-current="page"] {
230
- background: var(--surface-subtle);
231
- font-weight: 600;
232
- }
233
- ```
201
+ ### Link Styling
234
202
 
235
- Keep this separate from layout primitives. `.cluster` and `.stack` should not style links.
203
+ `.nav-link` gives a link its hit area, hover fill and `aria-current` state, in a
204
+ `.cluster` row or a `.nav-list` (see [Navbar](../components/navbar.md)). Set
205
+ `--gap` on a `.nav-list` to space its rows differently.
@@ -22,9 +22,8 @@ Use patterns when a semantic element needs a small amount of normalization befor
22
22
  Patterns often compose with layout primitives.
23
23
 
24
24
  ```html
25
- <menu class="actions cluster">...</menu>
26
- <ul class="nav-list cluster">...</ul>
27
- <ul class="nav-list stack">...</ul>
25
+ <menu class="cluster">...</menu>
26
+ <ul class="nav-list">...</ul>
28
27
  <section class="grid">...</section>
29
28
  ```
30
29
 
@@ -51,7 +50,7 @@ Avoid putting semantic resets into layout primitives.
51
50
  If list chrome should be removed, use an explicit pattern.
52
51
 
53
52
  ```html demo
54
- <ul class="nav-list stack">
53
+ <ul class="nav-list">
55
54
  <li><a href="/docs">Docs</a></li>
56
55
  <li><a href="/components">Components</a></li>
57
56
  </ul>
@@ -6,24 +6,25 @@ Small flex helpers for grow behavior and other one-line needs.
6
6
 
7
7
  Base utilities:
8
8
 
9
- | Class | Description |
10
- | --------------- | -------------------------------------------------------------- |
11
- | `.grow` | Makes an item take available flex space without overflowing. |
12
- | `.items-start` | Aligns children to the block-start edge; sets `--items-align`. |
13
- | `.items-center` | Centers children on the cross axis; sets `--items-align`. |
14
- | `.items-end` | Aligns children to the block-end edge; sets `--items-align`. |
15
- | `.dot` | Inline separator dot for metadata rows. |
9
+ | Class | Description |
10
+ | ----------------- | ------------------------------------------------------------------- |
11
+ | `.grow` | Makes an item take available flex space without overflowing. |
12
+ | `.items-start` | Aligns children to the block-start edge; sets `--items-align`. |
13
+ | `.items-center` | Centers children on the cross axis; sets `--items-align`. |
14
+ | `.items-end` | Aligns children to the block-end edge; sets `--items-align`. |
15
+ | `.items-baseline` | Aligns children on their first text baseline; sets `--items-align`. |
16
+ | `.dot` | Inline separator dot for metadata rows. |
16
17
 
17
18
  Extra utilities:
18
19
 
19
- | Class | Description |
20
- | -------------------------------- | --------------------------------------------------------- |
21
- | `.justify-content-start` | `justify-content: flex-start` and `--cluster-justify`. |
22
- | `.justify-content-center` | `justify-content: center` and `--cluster-justify`. |
23
- | `.justify-content-end` | `justify-content: flex-end` and `--cluster-justify`. |
24
- | `.justify-content-space-between` | `justify-content: space-between` and `--cluster-justify`. |
25
- | `.flex-wrap` | `flex-wrap: wrap` on any flex container. |
26
- | `.flex-nowrap` | `flex-wrap: nowrap` on any flex container. |
20
+ | Class | Description |
21
+ | -------------------------------- | ------------------------------------------------------- |
22
+ | `.justify-content-start` | `justify-content: flex-start` on the element itself. |
23
+ | `.justify-content-center` | `justify-content: center` on the element itself. |
24
+ | `.justify-content-end` | `justify-content: flex-end` on the element itself. |
25
+ | `.justify-content-space-between` | `justify-content: space-between` on the element itself. |
26
+ | `.flex-wrap` | `flex-wrap: wrap` on any flex container. |
27
+ | `.flex-nowrap` | `flex-wrap: nowrap` on any flex container. |
27
28
 
28
29
  ## Grow
29
30
 
@@ -66,14 +67,24 @@ Use `.items-start` on flex or grid layouts when children should keep their natur
66
67
 
67
68
  Use `.items-center` when a flex or grid layout should center its children on the cross axis. Use `.items-end` when children should align to the block-end edge.
68
69
 
70
+ Use `.items-baseline` when a row mixes text sizes — a price and its period, a name and a timestamp — so the words share one line instead of their boxes sharing a center.
71
+
72
+ ```html demo
73
+ <p class="cluster items-baseline">
74
+ <strong style="font-size: 2rem">$29</strong>
75
+ <span class="muted">/ month</span>
76
+ </p>
77
+ ```
78
+
69
79
  These helpers work by setting a custom property that participating components read,
70
80
  so they also apply inside a component's own grid anatomy rather than only on plain
71
81
  flex containers:
72
82
 
73
- - `.items-start` / `.items-center` / `.items-end` set `--items-align`, read by `.alert` and `.card`.
83
+ - `.items-start` / `.items-center` / `.items-end` / `.items-baseline` set `--items-align`, read by `.alert` and `.card`.
74
84
  - `.text-start` / `.text-center` / `.text-end` set `--text-align`, read by `.table` cells.
75
- - `.justify-content-*` (utilities/extra) set `--cluster-justify`, read by `.cluster`.
76
- - `.flex-wrap` / `.flex-nowrap` (utilities/extra) set `flex-wrap` directly on any flex container.
85
+
86
+ `.justify-content-*` and `.flex-wrap` / `.flex-nowrap` (utilities/extra) set the property
87
+ directly, so they never reach a nested `.cluster`.
77
88
 
78
89
  Set the property directly when you need a value the helpers do not ship.
79
90
 
@@ -107,6 +118,5 @@ Always mark it `aria-hidden="true"` — it is a visual separator, not content.
107
118
 
108
119
  ## CSS hooks
109
120
 
110
- - `--items-align` — cross-axis alignment, set by `.items-start` / `.items-center` / `.items-end`.
121
+ - `--items-align` — cross-axis alignment, set by the `.items-*` helpers.
111
122
  - `--text-align` — text alignment, set by `.text-start` / `.text-center` / `.text-end`.
112
- - `--cluster-justify` — main-axis distribution, set by the optional `.justify-content-*` helpers and read by `.cluster`.
@@ -29,7 +29,7 @@ one line (`.badge`). To stop a layout primitive from wrapping — for example to
29
29
  force a `.cluster` onto a single row — set `--cluster-wrap: nowrap`.
30
30
 
31
31
  ```html demo
32
- <div class="cluster compact" style="--cluster-wrap: nowrap;">
32
+ <div class="cluster tight" style="--cluster-wrap: nowrap;">
33
33
  <select class="select sm fit" aria-label="Segment">
34
34
  <option>All segments</option>
35
35
  </select>
@@ -48,9 +48,10 @@ Override the default gap from layout primitives (`.stack`, `.cluster`, `.grid`):
48
48
  - `.gap-none` → `gap: 0`
49
49
 
50
50
  `.gap-none` sets the element's own gap directly and does not change `--gap`
51
- for nested layouts. For a denser or roomier rhythm, use the `.compact` /
52
- `.spacious` density contexts, or set the `gap` property on the instance when the
53
- change must stop there.
51
+ for nested layouts. For a tighter or looser rhythm alone — closer items, without
52
+ smaller controls — use `.tight` / `.loose`; `.compact` / `.spacious` also change
53
+ padding and control size. Set the `gap` property on the instance when the change
54
+ must stop there.
54
55
 
55
56
  `--gap` defines the contextual rhythm; `.gap-context` applies that rhythm to a
56
57
  container that does not already consume `--gap`. The layout primitives
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actual-css",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
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",
@@ -10,7 +10,7 @@
10
10
  "type": "git",
11
11
  "url": "https://github.com/lekoala/actual-css.git"
12
12
  },
13
- "homepage": "https://github.com/lekoala/actual-css",
13
+ "homepage": "https://lekoala.github.io/actual-css/",
14
14
  "bugs": {
15
15
  "url": "https://github.com/lekoala/actual-css/issues"
16
16
  },
@@ -1,6 +1,5 @@
1
1
  [
2
2
  "accordion",
3
- "actions",
4
3
  "admonition",
5
4
  "alert",
6
5
  "alert-body",
@@ -56,7 +55,6 @@
56
55
  "cursor-pointer",
57
56
  "danger",
58
57
  "data-list",
59
- "dialog-confirmation",
60
58
  "dialog-icon",
61
59
  "display-block",
62
60
  "display-flex",
@@ -110,6 +108,7 @@
110
108
  "intent-color",
111
109
  "is-open",
112
110
  "is-static",
111
+ "items-baseline",
113
112
  "items-center",
114
113
  "items-end",
115
114
  "items-start",
@@ -132,6 +131,7 @@
132
131
  "list-item-title",
133
132
  "list-item-trailing",
134
133
  "list-reset",
134
+ "loose",
135
135
  "margin-block-end-auto",
136
136
  "margin-block-start-auto",
137
137
  "margin-inline-auto",
@@ -159,7 +159,6 @@
159
159
  "nav-list",
160
160
  "navbar",
161
161
  "navbar-brand",
162
- "navbar-nav",
163
162
  "needs-validation",
164
163
  "neutral",
165
164
  "otp",
@@ -248,6 +247,7 @@
248
247
  "text-uppercase",
249
248
  "text-wrap",
250
249
  "textarea",
250
+ "tight",
251
251
  "tooltip",
252
252
  "topbar",
253
253
  "touch-target",
@@ -40,16 +40,19 @@ the title in the content wrapper's direct `<header>`. The close button is then
40
40
  anchored to the surface, and the header reserves room so the title cannot run
41
41
  under it. A `.close` that is a direct child of the chrome header is that same
42
42
  corner overlay, and that header reserves room for it too. Put actions in the
43
- wrapper's `<footer>`.
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.
44
46
 
45
47
  Use `.bleed` on that direct header when it needs a contrasting full-width band;
46
48
  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.
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.
53
56
 
54
57
  Do not turn the dialog heading into an `.alert` to obtain icon or surface
55
58
  styling. Compose `.dialog-icon` with `.media` for a leading title icon; reserve
@@ -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 export this [data-theme] file instead of the default theme",
15
+ " --theme FILE read this [data-theme] file instead of the default theme",
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 parseDesignArgs(rest);
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 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" };
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
- 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.
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
- "actual-css design needs Bun: it reads the theme in headless Chrome through Bun.WebView. Run it with bunx.",
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
- await (args.command === "design" ? design(args) : bundle(args));
195
+ const commands = { bundle, contrast, design };
196
+ await commands[args.command](args);
171
197
  }
172
198
 
173
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 {
@@ -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, rgb(0 0 0 / 0.08));
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/soft is .btn-specific. */
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-default-hover-bg));
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; 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
  }