actual-css 0.11.0 → 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.
- package/CHANGELOG.md +73 -0
- package/README.md +29 -5
- package/components.json +15 -17
- package/dist/actual.full.css +403 -227
- package/dist/actual.full.min.css +1 -1
- package/docs/brand/favicon.svg +5 -0
- package/docs/brand/lockup.svg +12 -0
- package/docs/brand/logo-dark.svg +9 -0
- package/docs/brand/logo-light.svg +9 -0
- package/docs/brand/logo.svg +11 -0
- package/docs/brand/mark.svg +11 -0
- package/docs/navigation.json +4 -2
- package/docs/pages/components/alert.md +2 -10
- package/docs/pages/components/badge.md +3 -9
- package/docs/pages/components/bleed.md +5 -4
- package/docs/pages/components/card.md +31 -2
- package/docs/pages/components/close.md +9 -3
- package/docs/pages/components/dialog.md +248 -31
- package/docs/pages/components/drawer.md +2 -2
- package/docs/pages/components/meter.md +2 -0
- package/docs/pages/components/navbar.md +8 -9
- package/docs/pages/components/table.md +3 -3
- package/docs/pages/enhancements/flyout.md +2 -2
- package/docs/pages/enhancements/scrollspy.md +1 -1
- package/docs/pages/enhancements/status-bar.md +2 -2
- package/docs/pages/examples/overview.md +3 -1
- package/docs/pages/forms/date-picker.md +90 -0
- package/docs/pages/forms/detached-actions.md +37 -30
- package/docs/pages/forms/form-actions.md +61 -28
- package/docs/pages/forms/input-policy.md +1 -1
- package/docs/pages/forms/overview.md +6 -5
- package/docs/pages/forms/sticky-actions.md +85 -83
- package/docs/pages/forms/validation.md +5 -5
- package/docs/pages/foundations/philosophy.md +5 -3
- package/docs/pages/foundations/theming.md +15 -4
- package/docs/pages/foundations/tokens.md +50 -33
- package/docs/pages/foundations/typography.md +4 -7
- package/docs/pages/guides/bootstrap.md +2 -2
- package/docs/pages/guides/design-tools.md +36 -0
- package/docs/pages/guides/integrating-actual-css.md +5 -1
- package/docs/pages/guides/introduction.md +3 -4
- package/docs/pages/guides/modular-import.md +3 -0
- package/docs/pages/guides/progressive-enhancement.md +2 -2
- package/docs/pages/guides/tailwind.md +70 -0
- package/docs/pages/guides/upgrading.md +74 -0
- package/docs/pages/layout/cluster.md +28 -2
- package/docs/pages/layout/overview.md +1 -1
- package/docs/pages/layout/stack.md +21 -16
- package/docs/pages/layout/topbar.md +1 -1
- package/docs/pages/patterns/navigation-list.md +16 -46
- package/docs/pages/patterns/overview.md +3 -4
- package/docs/pages/utilities/flex.md +30 -20
- package/docs/pages/utilities/sizing-wrapping.md +1 -1
- package/docs/pages/utilities/spacing.md +4 -3
- package/package.json +3 -2
- package/reserved-classes.json +3 -3
- package/skills/actual-css/references/enhancements.md +38 -0
- package/src/cli/actual-css.js +75 -6
- package/src/css/components/avatar.css +16 -9
- package/src/css/components/badge.css +1 -5
- package/src/css/components/bleed.css +5 -5
- package/src/css/components/button.css +17 -10
- package/src/css/components/card.css +18 -10
- package/src/css/components/chat.css +4 -5
- package/src/css/components/close.css +17 -6
- package/src/css/components/drawer.css +19 -3
- package/src/css/components/flyout.css +3 -0
- package/src/css/components/index.css +0 -1
- package/src/css/components/meter.css +5 -2
- package/src/css/components/modal.css +157 -69
- package/src/css/components/navbar.css +14 -25
- package/src/css/core/base.css +1 -1
- package/src/css/core/reset.css +2 -1
- package/src/css/core/theme.css +4 -0
- package/src/css/core/tokens.css +28 -10
- package/src/css/core/variants.css +41 -14
- package/src/css/forms/form-actions.css +15 -10
- package/src/css/forms/native.css +5 -1
- package/src/css/layout/cluster.css +11 -0
- package/src/css/layout/topbar.css +12 -6
- package/src/css/utilities/base.css +5 -0
- package/src/css/utilities/extra.css +3 -7
- package/src/tooling/color.js +65 -0
- package/src/tooling/design-components.js +286 -0
- package/src/tooling/design-export.js +69 -0
- package/src/tooling/design-tokens.js +474 -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
- package/src/tooling/theme-contrast.js +209 -0
- package/docs/pages/patterns/actions.md +0 -52
- package/src/css/components/actions.css +0 -27
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sticky Actions
|
|
2
2
|
|
|
3
|
-
> Use `.form-actions
|
|
3
|
+
> Use `.cluster.form-actions.sticky` when long forms need reachable submit actions while scrolling.
|
|
4
4
|
|
|
5
5
|
## Inside the form
|
|
6
6
|
|
|
@@ -8,8 +8,8 @@ The `<form>` is the bounding parent. The sticky stays pinned to the viewport bot
|
|
|
8
8
|
|
|
9
9
|
Scroll the page to see the actions stay reachable.
|
|
10
10
|
|
|
11
|
-
```html demo
|
|
12
|
-
<form novalidate>
|
|
11
|
+
```html demo sticky
|
|
12
|
+
<form class="stack" novalidate>
|
|
13
13
|
<div class="stack">
|
|
14
14
|
<label class="field">
|
|
15
15
|
<span class="field-label">Full name <span class="required-mark" aria-hidden="true">*</span></span>
|
|
@@ -57,7 +57,7 @@ Scroll the page to see the actions stay reachable.
|
|
|
57
57
|
|
|
58
58
|
<fieldset class="field-group">
|
|
59
59
|
<legend class="field-label">Theme</legend>
|
|
60
|
-
<div class="stack
|
|
60
|
+
<div class="stack tight">
|
|
61
61
|
<label class="choice">
|
|
62
62
|
<input class="radio" type="radio" name="theme" value="light" checked />
|
|
63
63
|
<span>Light</span>
|
|
@@ -79,7 +79,7 @@ Scroll the page to see the actions stay reachable.
|
|
|
79
79
|
</label>
|
|
80
80
|
</div>
|
|
81
81
|
|
|
82
|
-
<div class="form-actions sticky">
|
|
82
|
+
<div class="cluster form-actions sticky">
|
|
83
83
|
<button type="button" class="btn outline">Cancel</button>
|
|
84
84
|
<button type="submit" class="btn primary">Save changes</button>
|
|
85
85
|
</div>
|
|
@@ -92,84 +92,86 @@ For a sticky page footer, dialog footer, or card footer, detach the actions and
|
|
|
92
92
|
|
|
93
93
|
Scroll the page to see the detached footer stay reachable.
|
|
94
94
|
|
|
95
|
-
```html demo
|
|
96
|
-
<
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
<
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
</
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
</
|
|
95
|
+
```html demo sticky
|
|
96
|
+
<div class="stack">
|
|
97
|
+
<form id="account-form" novalidate>
|
|
98
|
+
<div class="stack">
|
|
99
|
+
<label class="field">
|
|
100
|
+
<span class="field-label">Full name <span class="required-mark" aria-hidden="true">*</span></span>
|
|
101
|
+
<input class="input" type="text" autocomplete="name" required placeholder="Jane Doe" />
|
|
102
|
+
</label>
|
|
103
|
+
|
|
104
|
+
<label class="field">
|
|
105
|
+
<span class="field-label">Email <span class="required-mark" aria-hidden="true">*</span></span>
|
|
106
|
+
<input class="input" type="email" autocomplete="email" required />
|
|
107
|
+
</label>
|
|
108
|
+
|
|
109
|
+
<label class="field">
|
|
110
|
+
<span class="field-label">Phone</span>
|
|
111
|
+
<input class="input" type="tel" autocomplete="tel" />
|
|
112
|
+
</label>
|
|
113
|
+
|
|
114
|
+
<label class="field">
|
|
115
|
+
<span class="field-label">Bio</span>
|
|
116
|
+
<textarea class="textarea" rows="4" placeholder="Tell us about yourself…"></textarea>
|
|
117
|
+
</label>
|
|
118
|
+
|
|
119
|
+
<label class="field">
|
|
120
|
+
<span class="field-label">Website</span>
|
|
121
|
+
<input class="input" type="url" autocomplete="url" placeholder="https://example.com" />
|
|
122
|
+
</label>
|
|
123
|
+
|
|
124
|
+
<label class="field">
|
|
125
|
+
<span class="field-label">Language</span>
|
|
126
|
+
<select class="select">
|
|
127
|
+
<option>English</option>
|
|
128
|
+
<option>Français</option>
|
|
129
|
+
<option>Deutsch</option>
|
|
130
|
+
</select>
|
|
131
|
+
</label>
|
|
132
|
+
|
|
133
|
+
<label class="field">
|
|
134
|
+
<span class="field-label">Experience level</span>
|
|
135
|
+
<input class="range" type="range" min="0" max="10" value="5" />
|
|
136
|
+
</label>
|
|
137
|
+
|
|
138
|
+
<label class="field">
|
|
139
|
+
<span class="field-label">Profile picture</span>
|
|
140
|
+
<input class="file" type="file" accept="image/*" />
|
|
141
|
+
</label>
|
|
142
|
+
|
|
143
|
+
<fieldset class="field-group">
|
|
144
|
+
<legend class="field-label">Theme</legend>
|
|
145
|
+
<div class="stack tight">
|
|
146
|
+
<label class="choice">
|
|
147
|
+
<input class="radio" type="radio" name="theme-detached" value="light" checked />
|
|
148
|
+
<span>Light</span>
|
|
149
|
+
</label>
|
|
150
|
+
<label class="choice">
|
|
151
|
+
<input class="radio" type="radio" name="theme-detached" value="dark" />
|
|
152
|
+
<span>Dark</span>
|
|
153
|
+
</label>
|
|
154
|
+
<label class="choice">
|
|
155
|
+
<input class="radio" type="radio" name="theme-detached" value="auto" />
|
|
156
|
+
<span>System</span>
|
|
157
|
+
</label>
|
|
158
|
+
</div>
|
|
159
|
+
</fieldset>
|
|
160
|
+
|
|
161
|
+
<label class="choice">
|
|
162
|
+
<input class="check" type="checkbox" />
|
|
163
|
+
<span>Email notifications</span>
|
|
164
|
+
</label>
|
|
165
|
+
</div>
|
|
166
|
+
</form>
|
|
167
|
+
|
|
168
|
+
<footer class="cluster form-actions sticky">
|
|
169
|
+
<button type="button" class="btn outline">Cancel</button>
|
|
170
|
+
<button type="submit" class="btn primary" form="account-form">
|
|
171
|
+
Save changes
|
|
172
|
+
</button>
|
|
173
|
+
</footer>
|
|
174
|
+
</div>
|
|
173
175
|
```
|
|
174
176
|
|
|
175
177
|
## CSS hooks
|
|
@@ -62,7 +62,7 @@ there is no init call. To opt in forms you cannot mark up, call
|
|
|
62
62
|
`actual-css/js/enhance`.
|
|
63
63
|
|
|
64
64
|
```html demo
|
|
65
|
-
<form class="needs-validation" data-enhance="validation" data-validation-message="Please check the highlighted fields.">
|
|
65
|
+
<form class="stack needs-validation" data-enhance="validation" data-validation-message="Please check the highlighted fields.">
|
|
66
66
|
<div class="stack">
|
|
67
67
|
<label class="field">
|
|
68
68
|
<span class="field-label">Password</span>
|
|
@@ -81,7 +81,7 @@ there is no init call. To opt in forms you cannot mark up, call
|
|
|
81
81
|
</label>
|
|
82
82
|
</div>
|
|
83
83
|
|
|
84
|
-
<div class="form-actions">
|
|
84
|
+
<div class="cluster form-actions">
|
|
85
85
|
<button class="btn primary" type="submit">Submit</button>
|
|
86
86
|
<button class="btn neutral outline" type="reset">Reset</button>
|
|
87
87
|
</div>
|
|
@@ -139,7 +139,7 @@ The status bar (`actual-css/js/status`) auto-wires to that event: import it and
|
|
|
139
139
|
Switch this on to submit.
|
|
140
140
|
</span>
|
|
141
141
|
</div>
|
|
142
|
-
<div class="form-actions">
|
|
142
|
+
<div class="cluster form-actions">
|
|
143
143
|
<button class="btn primary" type="submit">Submit</button>
|
|
144
144
|
<button class="btn neutral outline" type="reset">Reset</button>
|
|
145
145
|
</div>
|
|
@@ -197,7 +197,7 @@ Add your own with `FormValidator.registerRule(name, (value, el, ...opts) => bool
|
|
|
197
197
|
<span class="field-error" id="attachment-error">Choose a .pdf or .csv file.</span>
|
|
198
198
|
</label>
|
|
199
199
|
|
|
200
|
-
<div class="form-actions">
|
|
200
|
+
<div class="cluster form-actions">
|
|
201
201
|
<button class="btn primary" type="submit">Submit</button>
|
|
202
202
|
<button class="btn neutral outline" type="reset">Reset</button>
|
|
203
203
|
</div>
|
|
@@ -246,7 +246,7 @@ The `date` rule pairs naturally with `data-mask` — the mask structures input,
|
|
|
246
246
|
</span>
|
|
247
247
|
</label>
|
|
248
248
|
|
|
249
|
-
<div class="form-actions">
|
|
249
|
+
<div class="cluster form-actions">
|
|
250
250
|
<button class="btn primary" type="submit">Submit</button>
|
|
251
251
|
<button class="btn neutral outline" type="reset">Reset</button>
|
|
252
252
|
</div>
|
|
@@ -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
|
|
84
|
-
icon size stay unchanged.
|
|
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`,
|
|
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. `
|
|
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.
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
171
|
-
|
|
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-
|
|
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`
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
333
|
+
--control-pad-x: var(--control-pad-x-lg);
|
|
314
334
|
}
|
|
315
335
|
```
|
|
316
336
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
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. `
|
|
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 `
|
|
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:
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 `.
|
|
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 `.
|
|
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
|
|
|
@@ -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 --bun 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, 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
|
+
|
|
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.
|
|
@@ -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.
|
|
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
|
|
76
|
-
|
|
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>
|