actual-css 0.10.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +99 -1
  2. package/README.md +6 -2
  3. package/components.json +14 -3
  4. package/dist/actual.full.css +483 -128
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/enhancement-contract.md +16 -7
  8. package/docs/design-notes/platform-alignment.md +8 -4
  9. package/docs/design-notes/rich-text.md +61 -0
  10. package/docs/design-notes/steps.md +2 -2
  11. package/docs/navigation.json +4 -1
  12. package/docs/pages/components/aura.md +12 -13
  13. package/docs/pages/components/badge.md +10 -0
  14. package/docs/pages/components/bleed.md +5 -4
  15. package/docs/pages/components/button.md +1 -1
  16. package/docs/pages/components/chat.md +5 -0
  17. package/docs/pages/components/close.md +9 -3
  18. package/docs/pages/components/dialog.md +145 -13
  19. package/docs/pages/components/meter.md +2 -0
  20. package/docs/pages/components/pagination.md +15 -15
  21. package/docs/pages/components/shimmer.md +57 -0
  22. package/docs/pages/components/skeleton.md +2 -1
  23. package/docs/pages/components/spinner.md +11 -6
  24. package/docs/pages/components/table.md +13 -0
  25. package/docs/pages/enhancements/flyout.md +2 -0
  26. package/docs/pages/enhancements/tabs.md +4 -0
  27. package/docs/pages/examples/overview.md +1 -0
  28. package/docs/pages/forms/choice-cards.md +77 -4
  29. package/docs/pages/forms/combobox.md +16 -1
  30. package/docs/pages/forms/overview.md +4 -1
  31. package/docs/pages/forms/range.md +110 -5
  32. package/docs/pages/forms/rich-text.md +110 -0
  33. package/docs/pages/forms/validation.md +5 -1
  34. package/docs/pages/foundations/theming.md +49 -3
  35. package/docs/pages/foundations/tokens.md +15 -5
  36. package/docs/pages/guides/bootstrap.md +2 -1
  37. package/docs/pages/guides/design-tools.md +36 -0
  38. package/docs/pages/guides/introduction.md +1 -1
  39. package/docs/pages/guides/modular-import.md +1 -1
  40. package/docs/pages/guides/progressive-enhancement.md +13 -14
  41. package/docs/pages/guides/tailwind.md +4 -2
  42. package/llms.txt +4 -3
  43. package/package.json +6 -1
  44. package/reserved-classes.json +1 -0
  45. package/skills/actual-css/references/enhancements.md +35 -0
  46. package/src/cli/actual-css.js +49 -6
  47. package/src/css/components/avatar.css +1 -1
  48. package/src/css/components/bleed.css +5 -5
  49. package/src/css/components/button.css +14 -1
  50. package/src/css/components/close.css +17 -6
  51. package/src/css/components/flyout.css +4 -1
  52. package/src/css/components/meter.css +6 -3
  53. package/src/css/components/modal.css +123 -7
  54. package/src/css/components/pagination.css +3 -2
  55. package/src/css/components/rating.css +1 -1
  56. package/src/css/components/status-bar.css +2 -2
  57. package/src/css/components/steps.css +3 -3
  58. package/src/css/components/tab.css +3 -3
  59. package/src/css/components/table.css +6 -0
  60. package/src/css/components/tooltip.css +1 -1
  61. package/src/css/core/focus.css +14 -4
  62. package/src/css/core/theme.css +8 -0
  63. package/src/css/core/tokens.css +8 -4
  64. package/src/css/core/variants.css +2 -1
  65. package/src/css/effects/aura.css +51 -33
  66. package/src/css/effects/index.css +1 -0
  67. package/src/css/effects/shimmer.css +57 -0
  68. package/src/css/forms/choice-card.css +71 -23
  69. package/src/css/forms/control.css +2 -2
  70. package/src/css/forms/native.css +5 -1
  71. package/src/css/forms/otp.css +9 -1
  72. package/src/css/forms/range.css +64 -21
  73. package/src/js/command.js +61 -32
  74. package/src/js/dialog.js +8 -23
  75. package/src/js/dismiss.js +1 -2
  76. package/src/js/enhance.js +84 -123
  77. package/src/js/full.js +1 -0
  78. package/src/js/password.js +1 -8
  79. package/src/js/range.js +151 -0
  80. package/src/js/scrollspy.js +4 -5
  81. package/src/js/status.js +2 -10
  82. package/src/js/tooltip.js +11 -1
  83. package/src/js/validation.js +4 -10
  84. package/src/tooling/design-components.js +286 -0
  85. package/src/tooling/design-export.js +69 -0
  86. package/src/tooling/design-tokens.js +466 -0
  87. package/src/tooling/penpot-plugin/manifest.json +7 -0
  88. package/src/tooling/penpot-plugin/plugin.js +272 -0
  89. package/src/tooling/penpot-plugin/ui.html +58 -0
@@ -76,18 +76,25 @@ Surface tokens describe the canvas and elevation model. Components should prefer
76
76
  }
77
77
  ```
78
78
 
79
- The surface levels are: `surface` (canvas), `surface-raised` (cards, raised surfaces), `surface-subtle` (the working subtle level — for hover overlays, form fields, table headers), and `surface-solid` (the dark inverse surface, used for accent areas).
79
+ The surface levels are: `surface` (canvas), `surface-raised` (cards, raised surfaces), `surface-subtle` (the working subtle level — for hover overlays, form fields, table headers), and `surface-solid` (the inverse surface, used for accent areas).
80
+
81
+ Surfaces are painted material and may be translucent. Two roles must stay opaque, so they have their own tokens, both defaulting to `--surface`:
82
+
83
+ - `--surface-solid-fg` — the ink on `--surface-solid`: tooltips, the status bar, an inverse band.
84
+ - `--surface-opaque` — an opaque neutral used where a component must hide or separate what is painted beneath it: the status-dot and avatar-stack rings, range-thumb and meter separators, step markers, the mask under an `.aura` child. It is not a surface fill; paint panels with `--surface-*`.
85
+
86
+ A theme with translucent surfaces overrides both with opaque values.
80
87
 
81
88
  Use `*-fg` pairs only for solid backgrounds where the component controls both foreground and background. Do not require a foreground token for every surface token.
82
89
 
83
- Focus styling is outline-first. A global focus baseline applies `--focus-outline` and `--focus-outline-offset`: older browsers show it on `:focus`, while modern browsers limit it to `:focus-visible`. Components draw a solid `--focus-ring-width` line in `--focus`: inside the border box on text fields (`.input`, `.textarea`, `.select`), outside at `--focus-outline-offset` on actions (`.btn`, `.close`, choices, switches). The color never follows an intent or a local surface, so `--focus` must hold 3:1 against both `--surface` and `--surface-solid` — the page, fields and a solid band all rely on it. Under `forced-colors: active`, outlines map to the system focus color.
90
+ Focus styling is outline-first. A global focus baseline applies `--focus-outline` and `--focus-outline-offset`: older browsers show it on `:focus`, while modern browsers limit it to `:focus-visible`. The baseline is a `currentColor` line, so text-like elements (links, `summary`, nav and menu items) follow whatever surface the application painted. Boxed components draw a solid `--focus-ring-width` line in `--focus`: inside the border box on text fields (`.input`, `.textarea`, `.select`), outside at `--focus-outline-offset` on actions (`.btn`, `.close`, choices, switches). The color never follows an intent or a local surface, so `--focus` must hold 3:1 against both `--surface` and `--surface-solid` — the page, fields and a solid band all rely on it. Both lines are `--focus-ring-width` wide. Under `forced-colors: active`, outlines map to the system focus color.
84
91
 
85
92
  ```css
86
93
  :root {
87
94
  --focus: hsl(268 30% 55%);
88
95
 
89
96
  --focus-outline-color: currentColor;
90
- --focus-outline: calc(var(--border-width) * 2) solid var(--focus-outline-color);
97
+ --focus-outline: var(--focus-ring-width) solid var(--focus-outline-color);
91
98
  --focus-outline-offset: 2px;
92
99
  --focus-ring-width: 2px;
93
100
  }
@@ -381,7 +388,7 @@ A few cross-component helpers:
381
388
  ```css
382
389
  :root {
383
390
  --indicator-offset: 14.65%; /* circle-aware inset */
384
- --indicator-ring: var(--surface);
391
+ --indicator-ring: var(--surface-opaque);
385
392
  --backdrop-color: rgb(0 0 0);
386
393
  --backdrop-opacity: 0.45;
387
394
  --backdrop-fill: rgb(0 0 0 / var(--backdrop-opacity));
@@ -485,12 +492,15 @@ When overriding an intent color, review its paired `--*-fg` and `--*-soft-fg`. A
485
492
 
486
493
  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.
487
494
 
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.
496
+
488
497
  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.
489
498
 
490
499
  A full theme can override:
491
500
 
492
501
  - intent colors and foreground pairs
493
502
  - surface, text, border, focus, and overlay colors
503
+ - `--surface-opaque` and `--surface-solid-fg`, when surfaces are translucent
494
504
  - radius tokens
495
505
  - optional shadow, motion, typography, and soft-variant mix tokens
496
506
 
@@ -534,7 +544,7 @@ A minimal recolor theme (illustrative, not a shipped theme):
534
544
  }
535
545
  ```
536
546
 
537
- Several tokens are theme-derived aliases that reference other tokens — `--state-selected`/`--state-selected-fg`/`--state-disabled`, `--indicator-ring`, `--shadow`/`--shadow-popout`, `--heading`, and `--selection-bg`/`--selection-fg`. They are declared on `:root, [data-theme]` so they recompute on every theme boundary: a custom property resolves its `var()` references at computed-value time on the element that declares it, so an alias declared only on `:root` would be inherited as an already-resolved value and would not follow a `[data-theme]` island's overridden tokens. A theme that wants a distinct alias (e.g. a `--selection-bg` of its own) overrides it explicitly afterwards, which wins by cascade order.
547
+ Several tokens are theme-derived aliases that reference other tokens — `--state-selected`/`--state-selected-fg`/`--state-disabled`, `--indicator-ring`, `--shadow`/`--shadow-popout`, `--heading`, `--selection-bg`/`--selection-fg`, and `--surface-opaque`/`--surface-solid-fg`. They are declared on `:root, [data-theme]` so they recompute on every theme boundary: a custom property resolves its `var()` references at computed-value time on the element that declares it, so an alias declared only on `:root` would be inherited as an already-resolved value and would not follow a `[data-theme]` island's overridden tokens. A theme that wants a distinct alias (e.g. a `--selection-bg` of its own) overrides it explicitly afterwards, which wins by cascade order.
538
548
 
539
549
  Without `data-theme`, the default theme advertises `color-scheme: light dark` and follows the user's OS preference in browsers that support `light-dark()`. Dark themes should set `color-scheme: dark`. Light themes should set `color-scheme: light`. Browsers without `light-dark()` receive the light fallback.
540
550
 
@@ -18,7 +18,8 @@ For an incremental migration, use one of these approaches:
18
18
 
19
19
  * migrate isolated pages or application areas;
20
20
  * control precedence with cascade layers or import order;
21
- * use a project-side prefix transform for Actual while both frameworks coexist.
21
+ * use a project-side prefix transform for Actual while both frameworks coexist,
22
+ renaming the runtime's state classes in `actual-css/js/selectors` as well.
22
23
 
23
24
  The layered import is the cleanest way to keep both stylesheets during the
24
25
  migration:
@@ -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.
@@ -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.10/dist/actual.full.min.css">
55
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.11/dist/actual.full.min.css">
56
56
  ```
57
57
 
58
58
  The package does not maintain separate partial bundles. Modular entrypoints map
@@ -94,7 +94,7 @@ cells, so they read as a list:
94
94
  - **Components** — `actual-css/css/components`
95
95
  buttons, surfaces, overlays, feedback, navigation, data and composed controls
96
96
  - **Effects** — `actual-css/css/effects`
97
- aura
97
+ aura, shimmer
98
98
  - **Utilities** — `actual-css/css/utilities`
99
99
  base and extra utilities
100
100
 
@@ -92,6 +92,7 @@ example:
92
92
  | `actual-css/js/filter` | Input value filtering on `data-filter` |
93
93
  | `actual-css/js/mask` | Input masks on `data-mask` |
94
94
  | `actual-css/js/password` | Password reveal toggle |
95
+ | `actual-css/js/range` | Range fill and named values (token `range`) |
95
96
  | `actual-css/js/validation` | Form validation (token `validation`) |
96
97
  | `actual-css/js/status` | Singleton status bar on `[data-status][role="status"]` |
97
98
  | `actual-css/js/enhancement-loader` | Declared enhancement manifests |
@@ -108,11 +109,12 @@ needed.
108
109
 
109
110
  The JS is designed to run on its own, without Actual's stylesheet.
110
111
 
111
- - **`data-enhance` tokens** register root-controller behaviors. The runtime
112
- discovers components through `data-enhance="tabs"`, `data-enhance="flyout"`,
113
- `data-enhance="scrollspy"`, and `data-enhance="validation"` — no Actual
114
- presentation class needed. Self-describing attributes (`data-mask`,
115
- `data-tooltip`, `data-context-menu`) stay framework-neutral by construction.
112
+ - **`data-enhance` tokens** register named behaviors. The runtime discovers
113
+ components through `data-enhance="tabs"`, `data-enhance="flyout"`,
114
+ `data-enhance="range"`, `data-enhance="scrollspy"`, and
115
+ `data-enhance="validation"` — no Actual presentation class needed.
116
+ Self-describing attributes (`data-mask`, `data-tooltip`,
117
+ `data-context-menu`) stay framework-neutral by construction.
116
118
  - **`selectors.js`** lists the state classes the runtime *writes* (`is-open`,
117
119
  `was-validated`, …). Edit this file (or alias it in a bundler) to
118
120
  match a different CSS framework's state vocabulary. It is a read-side-only
@@ -326,7 +328,6 @@ import { registerCommands } from "actual-css/js/command";
326
328
 
327
329
  registerCommands("--toggle-hidden", {
328
330
  handle(event, trigger, target) {
329
- event.preventDefault();
330
331
  target.hidden = !target.hidden;
331
332
  trigger.setAttribute("aria-expanded", String(!target.hidden));
332
333
  },
@@ -335,10 +336,10 @@ registerCommands("--toggle-hidden", {
335
336
 
336
337
  The default resolver `targetFor(trigger)` looks up `commandfor` by ID in the
337
338
  trigger's document or shadow root. Supply `resolve(trigger)` to validate or
338
- replace that lookup. An optional idempotent `prepare(trigger, target, command)`
339
- callback runs directly before `handle`; built-in behaviors use it for
340
- target-derived semantics. `commandSelector(commands)` builds the matching button
341
- selector. The returned handle has an idempotent `disconnect()` method for
339
+ replace that lookup. Once a target resolves, the router cancels the click before
340
+ `handle` runs, so the native `command` event is never dispatched for a routed
341
+ command. Names must be a native keyword or start with `--`; anything else
342
+ throws. `commandSelector(commands)` builds the matching button selector. The returned handle has an idempotent `disconnect()` method for
342
343
  applications that unload the module owning the command.
343
344
 
344
345
  ### Dismiss command
@@ -414,10 +415,8 @@ registerEnhancement("autosubmit", (form) => {
414
415
  <form data-enhance="validation autosubmit">
415
416
  ```
416
417
 
417
- `enhancementSelector(name)` returns the matching `[data-enhance~="name"]`
418
- selector (names must match `[a-z][a-z0-9-]*`), and `hasEnhancement(el, name)`
419
- tests it. No core module list is modified — the regression test in
420
- `tests/enhance.test.js` proves the shape.
418
+ Names must match `[a-z][a-z0-9-]*`. To find opted-in elements from your own
419
+ script, query `[data-enhance~="name"]`; styles keep using presentation classes.
421
420
 
422
421
  A name is owned once per root: a second `registerEnhancement("autosubmit", …)`
423
422
  on the same root throws, and `disconnect()` releases the name so it can be
@@ -17,9 +17,11 @@ If you finish the migration with a new `.mt-4`, `.flex`, `.bg-primary`, and `md:
17
17
 
18
18
  Tailwind and Actual can temporarily coexist, but some short class names overlap.
19
19
 
20
- Examples include layout and flex-related names such as `.grid`, `.grow`, and `.items-center`.
20
+ The most dangerous is `.outline`: it is an Actual button variant (`.btn.outline`), and Tailwind's `outline` utility draws a real outline on it. Layer order does not save this one — the migration setup above puts Tailwind utilities over Actual on purpose.
21
21
 
22
- For a staged migration, use cascade layers, controlled import order, or a project-side Actual prefix transform where necessary.
22
+ Other overlapping names: `.table`, `.sticky`, `.truncate`, `.prose`, `.sr-only`, plus layout names such as `.grid`, `.grow`, and `.items-center`.
23
+
24
+ For a staged migration, use cascade layers, controlled import order, or a project-side Actual prefix transform where necessary. The transform lives in the consumer's own pipeline (a build-time rename on the Actual import), not in the Actual CLI, which only bundles. The JavaScript runtime writes state classes (`is-open`, `danger`, …): rename them in `actual-css/js/selectors` too, or flyouts and dialogs never open.
23
25
 
24
26
  The layered import is the cleanest way to keep both stylesheets during the
25
27
  migration:
package/llms.txt CHANGED
@@ -30,9 +30,10 @@ show supported CSS modules and individual enhancer imports without duplicating
30
30
  the full export list here.
31
31
 
32
32
  Presentation (`class`) and behavior are separate layers. `data-enhance` takes
33
- four tokens only — `tabs`, `flyout`, `scrollspy`, `validation`; leaf behaviors
34
- use their own attribute (`data-tooltip`, `data-mask`, `data-filter`,
35
- `data-context-menu`) and `<dialog>` needs none. See
33
+ five built-in tokens — `tabs`, `flyout`, `range`, `scrollspy`, `validation`.
34
+ Leaf behaviors whose opt-in also carries configuration use their own attribute
35
+ (`data-tooltip`, `data-mask`, `data-filter`, `data-context-menu`), and `<dialog>`
36
+ needs none. See
36
37
  [when a token, when a data-*](docs/design-notes/enhancement-contract.md).
37
38
 
38
39
  Behaviour primitives are independently importable for building custom widgets,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actual-css",
3
- "version": "0.10.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",
@@ -66,6 +66,7 @@
66
66
  "./js/mask": "./src/js/mask.js",
67
67
  "./js/menu": "./src/js/menu.js",
68
68
  "./js/password": "./src/js/password.js",
69
+ "./js/range": "./src/js/range.js",
69
70
  "./js/scrollspy": "./src/js/scrollspy.js",
70
71
  "./js/selectors": "./src/js/selectors.js",
71
72
  "./js/tab": "./src/js/tab.js",
@@ -130,6 +131,7 @@
130
131
  "build:js": "bun scripts/build-js.js",
131
132
  "build:themes": "bun scripts/build-themes.js",
132
133
  "build:size": "bun scripts/build-size.js",
134
+ "export:design": "bun scripts/export-design.js",
133
135
  "check:enhance": "bun run scripts/check-enhance.js",
134
136
  "generate:reserved": "bun scripts/generate-reserved-classes.js",
135
137
  "check:reserved": "bun scripts/check-reserved.js",
@@ -138,12 +140,15 @@
138
140
  "check:links": "bun scripts/check-doc-links.js",
139
141
  "check:templates": "bun scripts/check-templates.js",
140
142
  "check:markup": "bun scripts/check-markup.js",
143
+ "shot:crop": "bun scripts/shot-crop.js",
141
144
  "shot:forced": "bun scripts/forced-colors-shot.js",
142
145
  "shot:multi": "bun scripts/multi-shot.js",
143
146
  "shot:page": "bun scripts/page-shot.js",
147
+ "shot:states": "bun scripts/shot-states.js",
144
148
  "probe": "bun scripts/probe.js",
145
149
  "serve": "bun scripts/serve.js",
146
150
  "check:sync": "bun scripts/check-sync.js",
151
+ "release": "bun scripts/release.js",
147
152
  "release:notes": "bun scripts/release-notes.js",
148
153
  "test": "bun test",
149
154
  "test:changed": "bun test --changed=master",
@@ -203,6 +203,7 @@
203
203
  "separated",
204
204
  "shadow-context",
205
205
  "shadow-popout-context",
206
+ "shimmer",
206
207
  "sidebar-layout",
207
208
  "skeleton",
208
209
  "sm",
@@ -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) => {
@@ -211,7 +211,7 @@ a.avatar[aria-disabled="true"] {
211
211
  }
212
212
 
213
213
  .avatar-stack > .avatar {
214
- box-shadow: 0 0 0 var(--avatar-stack-ring) var(--surface);
214
+ box-shadow: 0 0 0 var(--avatar-stack-ring) var(--surface-opaque);
215
215
  }
216
216
 
217
217
  .avatar-stack > .avatar + .avatar {
@@ -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
 
@@ -14,6 +14,7 @@
14
14
  * Public hooks:
15
15
  * --flyout-inline-size
16
16
  * --flyout-max-inline-size
17
+ * --flyout-pad
17
18
  *
18
19
  * Internal:
19
20
  * --menu-item-size (derived from --control-size for menu rows)
@@ -66,7 +67,9 @@
66
67
  );
67
68
  overflow: auto;
68
69
  margin: 0;
69
- padding: var(--space-10);
70
+ /* Tight by default for action menus, whose items carry their own padding.
71
+ Rich panels (forms, pickers) opt into dialog air per instance. */
72
+ padding: var(--flyout-pad, var(--space-10));
70
73
  border: var(--border-width) solid var(--border);
71
74
  border-radius: var(--radius);
72
75
  background: var(--surface-raised);
@@ -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
@@ -43,7 +46,7 @@
43
46
  }
44
47
 
45
48
  .meter:not(meter) > * + * {
46
- border-inline-start: var(--border-width) solid var(--surface);
49
+ border-inline-start: var(--border-width) solid var(--surface-opaque);
47
50
  }
48
51
 
49
52
  .meter::-webkit-meter-bar {
@@ -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 {