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.
- package/CHANGELOG.md +99 -1
- package/README.md +6 -2
- package/components.json +14 -3
- package/dist/actual.full.css +483 -128
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/design-notes/enhancement-contract.md +16 -7
- package/docs/design-notes/platform-alignment.md +8 -4
- package/docs/design-notes/rich-text.md +61 -0
- package/docs/design-notes/steps.md +2 -2
- package/docs/navigation.json +4 -1
- package/docs/pages/components/aura.md +12 -13
- package/docs/pages/components/badge.md +10 -0
- package/docs/pages/components/bleed.md +5 -4
- package/docs/pages/components/button.md +1 -1
- package/docs/pages/components/chat.md +5 -0
- package/docs/pages/components/close.md +9 -3
- package/docs/pages/components/dialog.md +145 -13
- package/docs/pages/components/meter.md +2 -0
- package/docs/pages/components/pagination.md +15 -15
- package/docs/pages/components/shimmer.md +57 -0
- package/docs/pages/components/skeleton.md +2 -1
- package/docs/pages/components/spinner.md +11 -6
- package/docs/pages/components/table.md +13 -0
- package/docs/pages/enhancements/flyout.md +2 -0
- package/docs/pages/enhancements/tabs.md +4 -0
- package/docs/pages/examples/overview.md +1 -0
- package/docs/pages/forms/choice-cards.md +77 -4
- package/docs/pages/forms/combobox.md +16 -1
- package/docs/pages/forms/overview.md +4 -1
- package/docs/pages/forms/range.md +110 -5
- package/docs/pages/forms/rich-text.md +110 -0
- package/docs/pages/forms/validation.md +5 -1
- package/docs/pages/foundations/theming.md +49 -3
- package/docs/pages/foundations/tokens.md +15 -5
- package/docs/pages/guides/bootstrap.md +2 -1
- package/docs/pages/guides/design-tools.md +36 -0
- package/docs/pages/guides/introduction.md +1 -1
- package/docs/pages/guides/modular-import.md +1 -1
- package/docs/pages/guides/progressive-enhancement.md +13 -14
- package/docs/pages/guides/tailwind.md +4 -2
- package/llms.txt +4 -3
- package/package.json +6 -1
- package/reserved-classes.json +1 -0
- package/skills/actual-css/references/enhancements.md +35 -0
- package/src/cli/actual-css.js +49 -6
- package/src/css/components/avatar.css +1 -1
- package/src/css/components/bleed.css +5 -5
- package/src/css/components/button.css +14 -1
- package/src/css/components/close.css +17 -6
- package/src/css/components/flyout.css +4 -1
- package/src/css/components/meter.css +6 -3
- package/src/css/components/modal.css +123 -7
- package/src/css/components/pagination.css +3 -2
- package/src/css/components/rating.css +1 -1
- package/src/css/components/status-bar.css +2 -2
- package/src/css/components/steps.css +3 -3
- package/src/css/components/tab.css +3 -3
- package/src/css/components/table.css +6 -0
- package/src/css/components/tooltip.css +1 -1
- package/src/css/core/focus.css +14 -4
- package/src/css/core/theme.css +8 -0
- package/src/css/core/tokens.css +8 -4
- package/src/css/core/variants.css +2 -1
- package/src/css/effects/aura.css +51 -33
- package/src/css/effects/index.css +1 -0
- package/src/css/effects/shimmer.css +57 -0
- package/src/css/forms/choice-card.css +71 -23
- package/src/css/forms/control.css +2 -2
- package/src/css/forms/native.css +5 -1
- package/src/css/forms/otp.css +9 -1
- package/src/css/forms/range.css +64 -21
- package/src/js/command.js +61 -32
- package/src/js/dialog.js +8 -23
- package/src/js/dismiss.js +1 -2
- package/src/js/enhance.js +84 -123
- package/src/js/full.js +1 -0
- package/src/js/password.js +1 -8
- package/src/js/range.js +151 -0
- package/src/js/scrollspy.js +4 -5
- package/src/js/status.js +2 -10
- package/src/js/tooltip.js +11 -1
- package/src/js/validation.js +4 -10
- package/src/tooling/design-components.js +286 -0
- package/src/tooling/design-export.js +69 -0
- package/src/tooling/design-tokens.js +466 -0
- package/src/tooling/penpot-plugin/manifest.json +7 -0
- package/src/tooling/penpot-plugin/plugin.js +272 -0
- 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
|
|
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`.
|
|
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:
|
|
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`,
|
|
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.
|
|
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
|
|
112
|
-
|
|
113
|
-
`data-enhance="
|
|
114
|
-
presentation class needed.
|
|
115
|
-
`data-
|
|
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.
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
418
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
use their own attribute
|
|
35
|
-
`data-context-menu`) and `<dialog>`
|
|
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.
|
|
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",
|
package/reserved-classes.json
CHANGED
|
@@ -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.
|
package/src/cli/actual-css.js
CHANGED
|
@@ -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
|
-
"
|
|
17
|
-
"A layer or layer(name) import is flattened into the @layer block it
|
|
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,
|
|
9
|
-
*
|
|
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
|
-
|
|
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
|
|
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(--
|
|
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(--
|
|
57
|
+
background: var(--meter-track);
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
.meter::-webkit-meter-optimum-value {
|