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
@@ -80,8 +80,9 @@ how `.field sm`, `.input-icon sm`, `.join sm`, and `.flyout sm` size the
80
80
  controls inside them, while a bare `.sm` on a plain element does nothing.
81
81
 
82
82
  `.compact` and `.spacious` establish inherited density tokens. Density arranges
83
- how tightly UI sits — spacing and component geometry — while typography and
84
- icon size stay unchanged. Participation is opt-in, and a component only joins
83
+ how tightly UI sits — spacing and component geometry — while font size and
84
+ icon size stay unchanged. `.tight` and `.loose` change the rhythm (`--gap`)
85
+ alone. Participation is opt-in, and a component only joins
85
86
  where density has an effect independent from its optical size: if the result is
86
87
  merely that the component looks smaller or larger, that is size, not density.
87
88
 
@@ -126,7 +127,8 @@ affect the same dimension:
126
127
  </div>
127
128
  ```
128
129
 
129
- The density tokens are `--gap`, `--density-space`, and `--control-size`.
130
+ The density tokens are `--gap`, `--density-space`, `--control-size`, and
131
+ `--control-pad-x`.
130
132
  Components consume the dimensions that make sense for them and opt out of the
131
133
  rest. The absolute `--space-*` scale is not rebound by density scopes.
132
134
  Components with bespoke geometry (`.badge`, `.avatar`,
@@ -184,16 +184,27 @@ Override the whole minimal recolor set (Tokens · Theme contract), not just
184
184
  `--primary`. Every neutral of the default theme — surfaces, text, border,
185
185
  `--neutral`, `--hover-overlay`, `--shadow-color` — is tinted toward the default
186
186
  primary and never derived from `--primary` at runtime, so a partial override
187
- leaves the default identity in the greys. `report:theme-contrast` checks
187
+ leaves the default identity in the greys. `actual-css contrast` checks
188
188
  contrast, not a leftover tint.
189
189
 
190
190
  Soft ink is the other half. The default palette ships calibrated
191
191
  `--*-soft-fg` hooks; any other theme falls back to the `--soft-fg-mix`
192
192
  derivation, which is a single global percentage and therefore guarantees
193
- nothing about your intents. Verify with `bun run report:theme-contrast`, then
194
- lower `--soft-fg-mix` or declare the per-role hooks for the intents that miss.
193
+ nothing about your intents. Measure it, then lower `--soft-fg-mix` or declare
194
+ the per-role hooks for the intents that miss:
195
195
 
196
- → Foundations · Tokens (theme contract)
196
+ ```sh
197
+ bunx --bun actual-css contrast --theme src/theme.css
198
+ ```
199
+
200
+ It reads the theme in headless Chrome, in every scheme it declares, and exits 1
201
+ when a soft, focus, invalid-focus or inverse pair misses its threshold, so it
202
+ can gate a CI job.
203
+
204
+ Starting from a color scale (Tailwind `--color-zinc-*`, a brand ramp): map one
205
+ step per role rather than importing the scale.
206
+
207
+ → Foundations · Tokens (theme contract) · Guides · Tailwind (Map the palette to theme roles)
197
208
 
198
209
  ### Use or adapt a preset palette
199
210
 
@@ -167,8 +167,8 @@ to keep that pair ≥ 4.5:1 in both color schemes, enforced by
167
167
  presets and custom `[data-theme]` islands keep the `--soft-fg-mix` derivation
168
168
  unless they declare the hooks themselves (the framework resets them outside
169
169
  the default boundary). A theme that overrides the soft palette is responsible
170
- for preserving sufficient text contrast; `bun run report:theme-contrast`
171
- reports every preset's resting and hovered pairs without failing the build.
170
+ for preserving sufficient text contrast; `bunx --bun actual-css contrast --theme FILE`
171
+ measures its resting and hovered pairs.
172
172
 
173
173
  A theme whose primary should read more muted than its statuses sets only
174
174
  `--primary-soft-fg`, and every soft `.primary` foreground follows it. Without a
@@ -236,7 +236,6 @@ Typography tokens cover the document baseline and common component needs.
236
236
  --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
237
237
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
238
238
  --font-width: 100%;
239
- --font-width-dense: 95%;
240
239
 
241
240
  --line-height: 1.5;
242
241
  --font-weight-normal: 400;
@@ -265,18 +264,45 @@ Local `.sm`/`.lg` sizes map `--control-size`, `--control-font-size`, and
265
264
  --control-size-md: 2.375rem; /* 38px → default */
266
265
  --control-size-lg: 2.75rem; /* 44px → .lg */
267
266
  --control-size: var(--control-size-md);
268
- --control-pad-x: var(--font-size-md);
267
+ --control-pad-x-sm: var(--font-size-xs); /* .sm, .compact */
268
+ --control-pad-x-md: var(--font-size-md); /* default */
269
+ --control-pad-x-lg: var(--font-size-lg); /* .lg, .spacious */
270
+ --control-pad-x: var(--control-pad-x-md);
269
271
  --control-font-size: var(--font-size-md);
270
272
  --disabled-opacity: 0.65;
271
273
  }
272
274
  ```
273
275
 
274
- `--control-pad-x` is a resolved geometry length, stated in the typographic
275
- tokens rather than an `em`. Every part of one control must see the same
276
- horizontal distance; an `em` would change value on a descendant with its own
277
- font size, which is exactly what `--input-icon-size` does to the icon glyph. It
278
- mirrors `--control-font-size` and is rebound beside it at the `.sm`/`.lg`
279
- boundaries.
276
+ A theme sets the steps (`--control-size-*`, `--control-pad-x-*`); `.sm`/`.lg`
277
+ and `.compact`/`.spacious` select one. `--control-size` and `--control-pad-x`
278
+ are the selected values: override them on one instance, never in a theme,
279
+ where every size and density boundary would overwrite them.
280
+
281
+ A `data-theme` island inherits the selected values, so an override set above it
282
+ crosses it. A theme that changes the steps and is used as a nested island must
283
+ re-select them in its own block, from the step weights that size and density
284
+ classes set, so the island keeps the density around it:
285
+
286
+ ```css
287
+ [data-theme="brand"] {
288
+ --control-pad-x-sm: 0.5rem;
289
+ --control-pad-x-md: 0.75rem;
290
+ --control-pad-x-lg: 1rem;
291
+ --control-pad-x: calc(
292
+ (var(--control-pad-x-sm) * var(--control-step-sm)) +
293
+ (var(--control-pad-x-md) * var(--control-step-md)) +
294
+ (var(--control-pad-x-lg) * var(--control-step-lg))
295
+ );
296
+ }
297
+ ```
298
+
299
+ Re-select only the token whose steps the theme changes: the same rule for
300
+ `--control-size` would stop an inherited `--control-size` at the island.
301
+
302
+ `--control-pad-x` is a length, not an `em`: every part of one control must see
303
+ the same horizontal distance, and an `em` would change value on a descendant
304
+ with its own font size, which is exactly what `--input-icon-size` does to the
305
+ icon glyph.
280
306
 
281
307
  - `.lg` — 44px control height
282
308
  - default — 38px control height
@@ -290,42 +316,33 @@ Disabled controls and disabled-prone components read `--disabled-opacity` for th
290
316
 
291
317
  ### Density
292
318
 
293
- Density contexts swap shared spacing and geometry, plus a slight typographic
294
- width bonus:
319
+ Density contexts swap shared rhythm and geometry, and never typography:
295
320
 
296
321
  ```css
297
- :root {
298
- --font-width: 100%;
299
- --font-width-dense: 95%;
300
- }
301
-
302
322
  .compact {
303
323
  --gap: var(--space-20);
304
324
  --density-space: var(--space-20);
305
325
  --control-size: var(--control-size-sm);
306
- font-stretch: var(--font-width-dense);
326
+ --control-pad-x: var(--control-pad-x-sm);
307
327
  }
308
328
 
309
329
  .spacious {
310
330
  --gap: var(--space-50);
311
331
  --density-space: var(--space-50);
312
332
  --control-size: var(--control-size-lg);
313
- font-stretch: var(--font-width);
333
+ --control-pad-x: var(--control-pad-x-lg);
314
334
  }
315
335
  ```
316
336
 
317
- Density never changes font size or icon size — it may only use the denser font
318
- width. `font-stretch` uses a variable `wdth` axis when available, or
319
- participates in normal width-face matching for families that expose multiple
320
- widths. Layout must never depend on the narrowing being available. Compact
321
- density may slightly narrow variable typefaces when they support a width axis.
322
- This is a density enhancement, not an overflow strategy: never condense a
323
- component just because its label overflows at some viewport — that hides a
324
- composition problem. A theme whose typeface has no useful width axis opts out
325
- with `--font-width-dense: 100%`.
337
+ - **Size** (`.sm`, `.lg`) — typography + geometry, on one component.
338
+ - **Density** (`.compact`, `.spacious`) — rhythm + geometry, on a region.
339
+ - **Rhythm** (`.tight`, `.loose`) — `--gap` only, on a region.
340
+
341
+ Font size, icon size and font width stay put under density. A condensed table
342
+ or panel is a typographic choice: set `font-stretch` on it in the application.
326
343
 
327
344
  Participation is opt-in and selective: controls consume `--control-size`
328
- for geometry; spacing helpers and rhythms consume `--density-space` and `--gap`;
345
+ and `--control-pad-x` for geometry; spacing helpers and rhythms consume `--density-space` and `--gap`;
329
346
  components whose size is intrinsic (badge, avatar, spinner, rating, key, prose,
330
347
  inline choice/switch) keep their own scales. A local component size overrides
331
348
  inherited density for the dimensions they share.
@@ -486,13 +503,13 @@ Rules:
486
503
 
487
504
  Themes override tokens, not selectors. The themes in `src/css/themes/` are repository-only demo examples, not included in the default stylesheet or the npm package; they exist to show valid ways to use this contract, such as `ocean`, `square`, `cyberpunk`, and `brutalist`.
488
505
 
489
- A minimal recolor theme overrides the intent pairs, surfaces, text colors, border, focus, hover overlay, and shadow color. Pick `--focus` as a mid-tone that holds 3:1 against both `--surface` and `--surface-solid` in every scheme the theme declares; a pastel dark-scheme primary usually fails on the light solid surface. `bun run report:theme-contrast` measures both pairs.
506
+ A minimal recolor theme overrides the intent pairs, surfaces, text colors, border, focus, hover overlay, and shadow color. Pick `--focus` as a mid-tone that holds 3:1 against both `--surface` and `--surface-solid` in every scheme the theme declares; a pastel dark-scheme primary usually fails on the light solid surface. `bunx --bun actual-css contrast --theme FILE` measures both pairs, and exits 1 when a pair misses.
490
507
 
491
- When overriding an intent color, review its paired `--*-fg` and `--*-soft-fg`. A theme that declares no hook falls back to the `--soft-fg-mix` derivation: one global percentage applied to every role, so it carries no contrast guarantee for a palette it was not tuned against. `bun run report:theme-contrast` prints the resting and hovered soft pair for every intent of every island, which is how a theme finds the roles that need a hook or a lower `--soft-fg-mix`.
508
+ When overriding an intent color, review its paired `--*-fg` and `--*-soft-fg`. A theme that declares no hook falls back to the `--soft-fg-mix` derivation: one global percentage applied to every role, so it carries no contrast guarantee for a palette it was not tuned against. `actual-css contrast` prints the resting and hovered soft pair for every intent in every scheme the theme declares, which is how a theme finds the roles that need a hook or a lower `--soft-fg-mix`.
492
509
 
493
510
  The default palette's hooks apply to `:root` and to the `light`/`dark` boundaries only. Any other `data-theme` value resets them to the derivation, whether the attribute sits on `<html>` or on a nested island.
494
511
 
495
- Contrast is measured between opaque colors. A translucent `--surface` has no ratio until it is composited over its backdrop, so `report:theme-contrast` refuses such a theme, and `.soft` mixes over it come out more opaque and more saturated than the surface. A translucent theme owns both: it measures against its real backdrop and replaces any recipe that does not hold there.
512
+ Contrast is measured between opaque colors. A translucent `--surface` has no ratio until it is composited over its backdrop, so `actual-css contrast` reports its pairs as n/a, and `.soft` mixes over it come out more opaque and more saturated than the surface. A translucent theme owns both: it measures against its real backdrop and replaces any recipe that does not hold there.
496
513
 
497
514
  Shape, shadow, motion, typography, and soft-variant mix tokens are optional knobs. Override them only when the theme actually changes that part of the system.
498
515
 
@@ -532,7 +549,7 @@ A minimal recolor theme (illustrative, not a shipped theme):
532
549
  --text-muted: hsl(260 10% 42%);
533
550
  --text-subtle: hsl(260 8% 56%);
534
551
  --border: hsl(260 24% 84%);
535
- --focus: var(--primary);
552
+ --focus: hsl(260 60% 62%);
536
553
  --hover-overlay: hsl(260 30% 10% / 0.04);
537
554
  --shadow-color: hsl(260 30% 10%);
538
555
  --soft-bg-mix: 84%;
@@ -115,13 +115,10 @@ baseline stays sans so app UI inherits predictably.
115
115
 
116
116
  ### Font width
117
117
 
118
- Two public hooks carry the typographic side of density: `--font-width` (normal
119
- width) and `--font-width-dense` (compact width). The document reads
120
- `--font-width` through `font-stretch`; `.compact` applies
121
- `--font-width-dense` and `.spacious` restores `--font-width`. A theme whose
122
- typeface has no useful width axis sets `--font-width-dense: 100%`. Width is a
123
- density token, not a responsive escape hatch — never condense text to fix an
124
- overflowing label.
118
+ `--font-width` sets the document's `font-stretch`: a theme whose typeface has a
119
+ `wdth` axis or several width faces picks its width there. Density does not
120
+ change it. To condense one dense table or panel, set `font-stretch` on that
121
+ element; never condense text to fix an overflowing label.
125
122
 
126
123
  ## Font weights
127
124
 
@@ -361,12 +361,12 @@ Actual's navbar is intentionally a simpler navigation pattern.
361
361
  Keep the desktop navigation as a navbar and compose the mobile experience from a
362
362
  drawer or flyout rather than looking for a `.navbar-expand-lg` equivalent:
363
363
 
364
- * desktop → `.navbar` with `.navbar-nav`;
364
+ * desktop → `.navbar` with a `.cluster` link list;
365
365
  * mobile → `.drawer` with `.nav-list`;
366
366
  * trigger → a `command="show-modal"` / `commandfor` button.
367
367
 
368
368
  The [Navbar component page](../components/navbar.md) shows this composition
369
- as *the Actual way*: a horizontal `.navbar-nav` on the desktop bar and a vertical
369
+ as *the Actual way*: a horizontal `.cluster` on the desktop bar and a vertical
370
370
  `.nav-list` inside the mobile drawer, opened with the native dialog command
371
371
  pattern. There is no collapse/toggler state to keep in sync.
372
372
 
@@ -3,10 +3,10 @@
3
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
4
 
5
5
  ```sh
6
- bunx actual-css design --theme src/theme.css --out design/
6
+ bunx --bun actual-css design --theme src/theme.css --out design/
7
7
  ```
8
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.
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, and `--bun` makes `bunx` run it under Bun instead of the CLI's Node shebang: it reads the theme in headless Chrome, so every `light-dark()`, `color-mix()` and `var()` chain resolves exactly as the browser paints it.
10
10
 
11
11
  ## What is exported
12
12
 
@@ -76,11 +76,15 @@ Prefer:
76
76
  --card-pad: var(--space-50);
77
77
  }
78
78
 
79
- .actions {
79
+ .product-actions {
80
80
  --cluster-justify: space-between;
81
81
  }
82
82
  ```
83
83
 
84
+ `.product-actions` is an application class composed onto the primitive:
85
+ `<div class="cluster product-actions">`. The hook is read by `.cluster`, so the
86
+ CSS stays a local adjustment, not a reimplementation.
87
+
84
88
  over reimplementing their layout:
85
89
 
86
90
  ```css
@@ -52,7 +52,7 @@ optimizing — the bare `actual-css` entrypoint and `actual-css/full` both resol
52
52
  to the complete compiled framework:
53
53
 
54
54
  ```html
55
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.11/dist/actual.full.min.css">
55
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.12/dist/actual.full.min.css">
56
56
  ```
57
57
 
58
58
  The package does not maintain separate partial bundles. Modular entrypoints map
@@ -72,9 +72,8 @@ shared by buttons, badges, alerts, and cards; a subtree that needs another
72
72
  palette is a `data-theme` island, not a class. `.ghost` and `.link` are
73
73
  button-only. Size variants `.sm` and `.lg` scale typography and
74
74
  participating component geometry consistently. Density contexts `.compact` and
75
- `.spacious` change geometry and spacing without changing font size or icons;
76
- `.compact` may slightly narrow typefaces that support a width axis.
77
- Undocumented `is-*` classes are runtime internals.
75
+ `.spacious` change geometry and spacing without changing font size, font width
76
+ or icons. Undocumented `is-*` classes are runtime internals.
78
77
 
79
78
  ```html
80
79
  <button class="btn">Click me</button>
@@ -98,6 +98,9 @@ cells, so they read as a list:
98
98
  - **Utilities** — `actual-css/css/utilities`
99
99
  base and extra utilities
100
100
 
101
+ `.form-actions` composes with `.cluster`: a build that takes forms without the
102
+ layout family imports `actual-css/css/layout/cluster` before the forms.
103
+
101
104
  Individual modules can be imported through their domain path:
102
105
 
103
106
  ```css
@@ -760,12 +760,12 @@ field, and dispatches a bubbling `actual:invalid` event with
760
760
  `{ form, firstInvalid, message }`. Valid fields are not marked automatically.
761
761
 
762
762
  ```html demo
763
- <form class="needs-validation" data-enhance="validation"
763
+ <form class="stack needs-validation" data-enhance="validation"
764
764
  data-validation-message="Please check the fields.">
765
765
  <input class="input" name="email" type="email" required
766
766
  aria-describedby="email-error" />
767
767
  <span class="field-error" id="email-error">Enter a valid email.</span>
768
- <div class="form-actions">
768
+ <div class="cluster form-actions">
769
769
  <button class="btn primary" type="submit">Submit</button>
770
770
  </div>
771
771
  </form>
@@ -380,6 +380,76 @@ Actual theme tokens should handle shared theme behavior. Local CSS remains appro
380
380
 
381
381
  This is particularly important when migrating a Tailwind application with a large custom color palette.
382
382
 
383
+ ### Map the Tailwind palette to theme roles
384
+
385
+ Tailwind's theme is a set of ramps: `--color-zinc-50` … `--color-zinc-950`,
386
+ one per hue. Actual's theme contract is a set of roles: `--surface`,
387
+ `--border`, `--text-muted`, `--primary`, `--focus`. Each element picks a
388
+ step in Tailwind; Actual picks once per role, in the theme, and every
389
+ component reads the role. Map one neutral family and one brand hue, then
390
+ stop: the rest of the ramp has no consumer.
391
+
392
+ A starting point for a zinc and blue application:
393
+
394
+ | Role | Light | Dark |
395
+ | ------------------ | ---------- | ---------- |
396
+ | `--surface` | `white` | `zinc-950` |
397
+ | `--surface-raised` | `white` | `zinc-900` |
398
+ | `--surface-subtle` | `zinc-100` | `zinc-800` |
399
+ | `--surface-solid` | `zinc-900` | `zinc-100` |
400
+ | `--border` | `zinc-200` | `zinc-700` |
401
+ | `--text` | `zinc-950` | `zinc-50` |
402
+ | `--text-muted` | `zinc-600` | `zinc-400` |
403
+ | `--text-subtle` | `zinc-500` | `zinc-500` |
404
+ | `--neutral` | `zinc-600` | `zinc-400` |
405
+ | `--primary` | `blue-600` | `blue-400` |
406
+ | `--focus` | `blue-500` | `blue-500` |
407
+
408
+ Each scheme is one `light-dark()` pair on the role, instead of a `dark:`
409
+ variant on every element:
410
+
411
+ ```css
412
+ [data-theme="app"] {
413
+ color-scheme: light dark;
414
+
415
+ --surface: light-dark(var(--color-white), var(--color-zinc-950));
416
+ --border: light-dark(var(--color-zinc-200), var(--color-zinc-700));
417
+ --primary: light-dark(var(--color-blue-600), var(--color-blue-400));
418
+ --primary-fg: light-dark(var(--color-white), var(--color-zinc-950));
419
+ --focus: var(--color-blue-500);
420
+ /* … the other roles of the table */
421
+ }
422
+ ```
423
+
424
+ Tailwind v4 emits only the theme variables your sources use; declare the
425
+ palette with `@theme static` while the theme reads them, and replace them with
426
+ their literal values before Tailwind leaves the build.
427
+
428
+ What the ramp does not decide for you:
429
+
430
+ - **Foreground pairs.** Tailwind has no `-fg` step. White on `blue-600` holds
431
+ 4.5:1, white on `blue-500` does not, so a dark-scheme primary lightened to
432
+ `blue-400` needs a dark `--primary-fg`. Every intent needs its pair:
433
+ `--warning-fg` on an amber is dark ink.
434
+ - **Focus.** `--focus` must hold 3:1 on `--surface` and `--surface-solid` in
435
+ both schemes, so it is a mid-tone, not the primary step. `blue-500` holds
436
+ on `white`, `zinc-900`, `zinc-950` and `zinc-100`: one value covers both
437
+ schemes.
438
+ - **Neutral temperature.** `slate`, `gray`, `zinc`, `neutral` and `stone` are
439
+ the same ramp at different temperatures; `neutral` is pure grey. Pick the
440
+ family that leans toward your primary, and take every neutral role from it.
441
+ - **No subtle intent steps.** Do not map `blue-50` / `blue-100` to tinted
442
+ surfaces: `.soft` mixes the intent into the local surface, so there is no
443
+ `--primary-subtle` to fill.
444
+ - **Shadow and hover ink.** Tailwind shadows are black; Actual derives
445
+ `--shadow` and `--shadow-popout` from one `--shadow-color`. Take it from the
446
+ neutral family's darkest step, and `--hover-overlay` as a few percent of
447
+ `--text`.
448
+
449
+ `bunx --bun actual-css contrast --theme FILE` measures the focus, soft and inverse
450
+ pairs in both schemes. Check the text tiers on `--surface-subtle` by hand:
451
+ `zinc-500` holds 4.5:1 on `white` but not on `zinc-100`.
452
+
383
453
  ## Forms
384
454
 
385
455
  Tailwind applications often construct form controls from utility combinations or use an additional forms plugin.
@@ -0,0 +1,74 @@
1
+ # Upgrading to 0.12
2
+
3
+ Each step below is a search for a removed class or hook and its replacement.
4
+ The [changelog](https://github.com/lekoala/actual-css/blob/master/CHANGELOG.md)
5
+ lists every change; this page keeps the ones that need an edit.
6
+
7
+ **Related terms:** migration, breaking changes, update, version.
8
+
9
+ ## Action rows
10
+
11
+ `.actions`, `.navbar-nav` and the layout of `.form-actions` are gone. A row of
12
+ actions is a `.cluster`; a vertical list is `stack list-reset`. Replace
13
+ `class="actions"` and `class="navbar-nav"` with `class="cluster"`, and
14
+ `class="form-actions"` with `class="cluster form-actions"`:
15
+
16
+ ```html
17
+ <menu class="cluster">…</menu>
18
+ <ul class="cluster">…</ul>
19
+ <div class="cluster form-actions">…</div>
20
+ <ul class="stack list-reset">…</ul>
21
+ ```
22
+
23
+ - The row takes the framework `--gap` (0.75rem). The former `.actions` used
24
+ 0.5rem: add `.tight` to keep it.
25
+ - The former `.navbar-nav` used `--space-10` (0.25rem), which no utility
26
+ reproduces. If the tighter navbar rhythm matters, set
27
+ `gap: var(--space-10)` on that row in application CSS.
28
+ - `.form-actions` has no top margin and no `--form-actions-margin-block-start`:
29
+ put `.stack` on the form. `--form-actions-justify` and `--form-actions-align`
30
+ still apply. In a modular build, import `layout/cluster` before
31
+ `forms/form-actions`.
32
+ - `.justify-content-*` aligns only its own element and no longer reaches a
33
+ nested `.cluster`. Put the utility on the row it should align.
34
+
35
+ ## Dialogs
36
+
37
+ - `.dialog-confirmation` is removed. Compose `.media` with `.dialog-icon`, then
38
+ a `<footer class="bleed background-subtle cluster justify-content-space-between">`;
39
+ the recipe is in [Dialog](../components/dialog.md).
40
+ - A `dialog.modal` body `<footer>` has no layout of its own: write
41
+ `<footer class="cluster justify-content-end">`.
42
+ - `--modal-header-bg` is removed: put a `.background-*` utility on the
43
+ `dialog.modal > header` band. Only that default fill has zero specificity;
44
+ the header's layout keeps its structural rule.
45
+ - The confirmation recipe uses the regular 1rem modal inset where the former
46
+ `.dialog-confirmation` used 1.5rem. Set `--modal-pad: var(--space-50)` on
47
+ the dialog to keep the roomier treatment.
48
+
49
+ ## Cards
50
+
51
+ The layout of a `.card` header or footer row is a zero-specificity default: any
52
+ author rule that targets the slot replaces it, not only a `.media`, `.cluster`,
53
+ `.form-actions` or `.justify-content-*` class. Check application CSS for bare
54
+ element rules such as `header { … }` or `footer { … }`, which now win over the
55
+ card's slot row.
56
+
57
+ ## Themes and density
58
+
59
+ - A theme sets `--control-pad-x-sm`, `--control-pad-x-md` and
60
+ `--control-pad-x-lg` instead of `--control-pad-x`. Used as a nested island,
61
+ it re-selects them in its own block; see
62
+ [Tokens](../foundations/tokens.md).
63
+ - `--font-width-dense` is removed, and `.compact` no longer condenses type: set
64
+ `font-stretch` on the element that should render condensed.
65
+
66
+ ## Rendering changes without an edit
67
+
68
+ These need no markup change; compare the affected screens.
69
+
70
+ - `.sm` controls have 13px of inline padding instead of 14px.
71
+ - `.compact` and `.spacious` controls get narrower or wider, not only shorter
72
+ or taller.
73
+ - `.nav-list` rows sit `--space-10` apart instead of `--space-20`.
74
+ - `.card.compact` lowers the gap between its children to `--space-20`.
@@ -17,6 +17,30 @@ between items always comes from `--gap`. Unlike `.stack`, which resets only the
17
17
  block axis, cluster resets margins on both axes — a wrapping cluster's `gap`
18
18
  controls inline spacing between items and block spacing between wrapped rows.
19
19
 
20
+ ## Action rows
21
+
22
+ Actions are content; the cluster owns the geometry. A semantic action list is a
23
+ `<menu>` (or `<ul>`) with `.cluster`:
24
+
25
+ ```html demo
26
+ <menu class="cluster">
27
+ <li><button class="btn primary" type="button">Save</button></li>
28
+ <li><button class="btn outline" type="button">Cancel</button></li>
29
+ </menu>
30
+ ```
31
+
32
+ There is no `.actions` class. A dense row is `cluster tight`; a vertical action
33
+ list is `stack list-reset`.
34
+
35
+ ## List chrome
36
+
37
+ `.cluster` neutralizes the native chrome of a `<ul>`, `<ol>` or `<menu>` it is
38
+ applied to. The element stays a list in the DOM and for assistive technology,
39
+ but a horizontal, wrapping group cannot carry markers and indentation
40
+ meaningfully, so they are removed. `.stack` makes no such change — an
41
+ `<ol class="stack">` keeps its numbers. Use `.list-reset` alone for a plain list
42
+ with no layout.
43
+
20
44
  ## CSS hooks
21
45
 
22
46
  - `--cluster-justify` — main-axis distribution (`justify-content`).
@@ -48,8 +72,10 @@ wraps to several lines — set `--cluster-align`.
48
72
  ```
49
73
 
50
74
  The optional utility layer ships `.justify-content-start`, `.justify-content-center`,
51
- `.justify-content-end` and `.justify-content-space-between`, which set
52
- `--cluster-justify` for you. See the utilities page.
75
+ `.justify-content-end` and `.justify-content-space-between`. They set
76
+ `justify-content` on that one row only; a nested `.cluster` keeps its own
77
+ alignment. `--cluster-justify` is inherited, so setting it on a region reaches
78
+ nested clusters too. See the utilities page.
53
79
 
54
80
  ## Controls inside a wrapping cluster
55
81
 
@@ -57,4 +57,4 @@ Prefer a parent `gap` for relationships between peers instead of encoding siblin
57
57
 
58
58
  Before adding a divider or other separating chrome, consider whether additional spacing already expresses the boundary clearly.
59
59
 
60
- Density changes the amount of space available, not the relationships that space communicates. When a composition contains local, group, and section rhythms, preserve their relative hierarchy with nested or local `--gap` values rather than relying on one inherited gap at every level. See [Choosing a density](./stack.md#choosing-a-density) for the nesting and density mechanics.
60
+ Density changes the amount of space available, not the relationships that space communicates. When a composition contains local, group, and section rhythms, preserve their relative hierarchy with nested or local `--gap` values rather than relying on one inherited gap at every level. See [Choosing a rhythm](./stack.md#choosing-a-rhythm) for the nesting mechanics, and `.tight` / `.loose` for a gap-only change.
@@ -39,19 +39,24 @@ content, keep them in normal block flow instead:
39
39
  Use `.stack` for the vertical rhythm *between* related items inside one region,
40
40
  not as the wrapper that lays out independent full-width regions of a page.
41
41
 
42
- ## Choosing a density
42
+ ## Choosing a rhythm
43
43
 
44
- `--gap` is the primitive's channel, and the density context variants set it at
45
- the point of use. Apply the density class on the stack itself to size one
46
- instance, or on an ancestor to reach nested layouts. Pick the one that matches
47
- how tightly the children belong together:
44
+ `--gap` is the primitive's channel. `.tight` and `.loose` set it and nothing
45
+ else; apply one on the stack to space one instance, or on an ancestor to reach
46
+ nested layouts. Pick the one that matches how tightly the children belong
47
+ together:
48
48
 
49
49
  | Markup | Gap | Use for |
50
50
  | ----------------- | ---- | ------------------------------------------ |
51
51
  | `.stack gap-none` | 0 | Lines forming one typographic unit |
52
- | `.stack compact` | 8px | A homogeneous series of controls |
52
+ | `.stack tight` | 8px | A homogeneous series of controls |
53
53
  | `.stack` | 12px | Normal flow between distinct elements |
54
- | `.stack spacious` | 24px | Sections, or blocks that read as separated |
54
+ | `.stack loose` | 24px | Sections, or blocks that read as separated |
55
+
56
+ **Size** changes a component's scale. **Density** (`.compact`, `.spacious`)
57
+ changes UI geometry and rhythm together: gap, padding, control size. **Rhythm**
58
+ (`.tight`, `.loose`) changes the gap only — closer or farther items, tighter
59
+ spacing without smaller buttons. Use one rhythm class per element.
55
60
 
56
61
  One typographic unit means a name and a job title, or a figure and its caption.
57
62
  A homogeneous series means a radio list, a checkbox list, or a compact vertical
@@ -60,13 +65,13 @@ menu.
60
65
  The distinction between the first two matters. `42` above `Open issues` is
61
66
  almost a single block of text, so it takes `gap-none`. Three `.choice` labels
62
67
  stay three separate controls that happen to form one group, so they take
63
- `compact` — tighter than the default, but still spaced.
68
+ `tight` — tighter than the default, but still spaced.
64
69
 
65
- `.compact` and `.spacious` rebind `--gap` to `--space-20` and `--space-50`.
66
- Because the token is inherited, the same two classes also tighten the inner gap
67
- of any nested stack, cluster, and grid. For a one-off gap that must *not* follow
68
- density, set the `gap` property directly on the instance (the next section
69
- explains why).
70
+ `.tight` and `.loose` rebind `--gap` to `--space-20` and `--space-50`, the same
71
+ steps `.compact` and `.spacious` use. Because the token is inherited, they also
72
+ tighten the inner gap of any nested stack, cluster, and grid. For a one-off gap
73
+ that must *not* reach nested layouts, set the `gap` property directly on the
74
+ instance (the next section explains why).
70
75
 
71
76
  A stack that mixes kinds keeps the default: five `.field` wrappers and one lone
72
77
  `.choice` is a form that contains a checkbox, not a list of options, and
@@ -92,7 +97,7 @@ depth:
92
97
  ```
93
98
 
94
99
  Both give the region the same spacing; only the second stops there. This is what
95
- makes a nested `.stack compact` a real density choice — 12px down to 8px — rather
100
+ makes a nested `.stack tight` a real rhythm choice — 12px down to 8px — rather
96
101
  than a patch cancelling a rhythm inherited from three levels up.
97
102
 
98
103
  Setting `--gap` is still the right move on the element that *consumes* it: a
@@ -100,8 +105,8 @@ one-off `--gap` on a card body or an actions row tunes that primitive and
100
105
  nothing below it, because the setter is the consumer. The leak only appears when
101
106
  a container sets the token and its children read it.
102
107
 
103
- `.compact` and `.spacious` are the deliberate exception: they set `--gap`
104
- precisely so a density context reaches nested layouts.
108
+ The rhythm and density classes are the deliberate exception: they set `--gap`
109
+ precisely so the context reaches nested layouts.
105
110
 
106
111
  ## Grouping items tightly
107
112
 
@@ -4,7 +4,7 @@ Sticky, frosted top bar shell for app/dashboard layouts with a persistent sideba
4
4
 
5
5
  > **Module** — import `actual-css/css/layout/topbar` or `actual-css/css/layout`.
6
6
 
7
- Use `.topbar` for the structural shell of an app-shell header: sticky positioning, stacking, and a frosted background. Row content — search, breadcrumb, actions — stays local to the page.
7
+ Use `.topbar` for the structural shell of an app-shell header: sticky positioning, stacking, and a frosted background. Its children sit in a centered row by default; a layout primitive on the same element, such as `topbar cluster`, replaces that row. Row content — search, breadcrumb, actions — stays local to the page.
8
8
 
9
9
  ```html demo
10
10
  <header class="topbar">