actual-css 0.13.0 → 0.14.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 +53 -0
- package/README.md +4 -4
- package/components.json +116 -10
- package/dist/actual.full.css +648 -77
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/navigation.json +2 -0
- package/docs/pages/components/alert.md +12 -2
- package/docs/pages/components/button.md +3 -2
- package/docs/pages/components/list.md +6 -3
- package/docs/pages/components/segmented.md +83 -0
- package/docs/pages/components/steps.md +4 -2
- package/docs/pages/enhancements/flyout.md +5 -0
- package/docs/pages/enhancements/tabs.md +7 -2
- package/docs/pages/examples/overview.md +3 -1
- package/docs/pages/forms/color-swatches.md +131 -0
- package/docs/pages/forms/combobox.md +2 -2
- package/docs/pages/forms/controls.md +6 -3
- package/docs/pages/forms/joined-controls.md +6 -2
- package/docs/pages/forms/overview.md +1 -0
- package/docs/pages/forms/switch.md +4 -2
- package/docs/pages/foundations/theming.md +1 -1
- package/docs/pages/foundations/tokens.md +54 -23
- package/docs/pages/guides/design-tools.md +13 -7
- package/docs/pages/guides/introduction.md +1 -1
- package/docs/pages/layout/scroll-snap.md +4 -2
- package/package.json +2 -2
- package/reserved-classes.json +3 -0
- package/src/cli/actual-css.js +3 -3
- package/src/css/components/alert.css +65 -14
- package/src/css/components/app-nav.css +17 -9
- package/src/css/components/badge.css +23 -9
- package/src/css/components/button.css +17 -2
- package/src/css/components/close.css +6 -3
- package/src/css/components/index.css +1 -0
- package/src/css/components/list.css +18 -3
- package/src/css/components/navbar.css +5 -2
- package/src/css/components/segmented.css +186 -0
- package/src/css/components/steps.css +6 -3
- package/src/css/components/tab.css +1 -1
- package/src/css/core/focus.css +3 -0
- package/src/css/core/tokens.css +20 -4
- package/src/css/core/variants.css +16 -5
- package/src/css/forms/color-swatch.css +159 -0
- package/src/css/forms/form-actions.css +9 -0
- package/src/css/forms/index.css +1 -0
- package/src/css/forms/native.css +50 -20
- package/src/css/forms/switch.css +5 -2
- package/src/css/layout/scroll-snap.css +30 -0
- package/src/css/layout/topbar.css +10 -0
- package/src/js/focus.js +16 -1
- package/src/js/surface.js +10 -0
- package/src/tooling/color.js +14 -1
- package/src/tooling/css-bundle.js +0 -5
- package/src/tooling/design-components.js +9 -5
- package/src/tooling/design-export.js +8 -0
- package/src/tooling/design-tokens.js +163 -43
- package/src/tooling/theme-contrast.js +151 -22
package/docs/navigation.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- Could have simple or complex html content.
|
|
13
13
|
- A title and a description are plain siblings: the alert owns the small space between its direct content blocks, so neither needs a wrapper. Nested content keeps its own rhythm — wrap several paragraphs in `.prose`, or a differently spaced group in `.stack`, when the message needs a rhythm of its own.
|
|
14
14
|
- Alerts may include a decorative leading icon. Use `.alert-icon` on the icon element; the rest of the content flows into the remaining text column.
|
|
15
|
-
- Use `.compact` or `.spacious`
|
|
15
|
+
- Use `.compact` or `.spacious` on the alert to step its block padding. The inline padding stays stable.
|
|
16
16
|
- Use a [`.close`](close.md) button to make the alert dismissible. It is a direct trailing child in standard alerts and lives inside `.alert-title` in admonitions; the alert makes it compact.
|
|
17
17
|
|
|
18
18
|
Because alerts are soft by default, adding `.soft` to an intent (`.alert.soft.primary`,
|
|
@@ -216,7 +216,7 @@ full-bleed notice.
|
|
|
216
216
|
|
|
217
217
|
## Admonition
|
|
218
218
|
|
|
219
|
-
An admonition is a structured box with a tinted title bar, an optional icon, and body content on the page surface — like the `!!! note` callouts in mkdocs. Use `.alert-title` for the header and `.alert-body` for the content. Intent classes tint the title bar background and the border.
|
|
219
|
+
An admonition is a structured box with a tinted title bar, an optional icon, and body content on the page surface — like the `!!! note` callouts in mkdocs. Use `.alert-title` for the header and `.alert-body` for the content. Intent classes tint the title bar background and the border; shared variants paint the title bar, so `.solid` fills it — and keeps the body's frame neutral — for a notification that needs to stand out.
|
|
220
220
|
|
|
221
221
|
```html demo
|
|
222
222
|
<div class="stack">
|
|
@@ -239,6 +239,16 @@ An admonition is a structured box with a tinted title bar, an optional icon, and
|
|
|
239
239
|
<p>The intent tints the title bar, the border, and the body uses <code>var(--surface)</code>.</p>
|
|
240
240
|
</div>
|
|
241
241
|
</div>
|
|
242
|
+
|
|
243
|
+
<div class="alert success admonition solid">
|
|
244
|
+
<div class="alert-title">
|
|
245
|
+
<i class="ti ti-circle-check" aria-hidden="true"></i>
|
|
246
|
+
Saved
|
|
247
|
+
</div>
|
|
248
|
+
<div class="alert-body">
|
|
249
|
+
<p>A solid title bar for an action response.</p>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
242
252
|
</div>
|
|
243
253
|
```
|
|
244
254
|
|
|
@@ -153,8 +153,9 @@ Keep one variant family inside a group. Because each button keeps its own
|
|
|
153
153
|
classes, nothing stops `.btn`, `.btn outline` and `.btn soft` from sitting side
|
|
154
154
|
by side — but three fills and three border treatments in one shape read as
|
|
155
155
|
unrelated controls that happen to touch, not as one unit. Change the variant
|
|
156
|
-
only to carry meaning: [Toggle](#toggle) marks
|
|
157
|
-
|
|
156
|
+
only to carry meaning: [Toggle](#toggle) marks a pressed button with a solid
|
|
157
|
+
fill among outlines. For one choice among a few on a shared track, use a
|
|
158
|
+
[segmented control](segmented.md) instead.
|
|
158
159
|
|
|
159
160
|
## Sizes
|
|
160
161
|
|
|
@@ -29,9 +29,12 @@ list markers when order or bullets carry meaning.
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Leading and trailing content are unconstrained: avatars, native choices,
|
|
32
|
-
badges, text, switches, and buttons retain their own component sizing.
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
badges, text, switches, and buttons retain their own component sizing. In a
|
|
33
|
+
narrow row, the trailing region moves under the content rather than breaking a
|
|
34
|
+
word of the title; supporting text still wraps beside it, and a token longer
|
|
35
|
+
than the row breaks instead of widening the page. A `.truncate` line inside
|
|
36
|
+
`.list-item-content` gives up its width, so it ellipsizes next to the trailing
|
|
37
|
+
region (wrap, responsive row, overflow).
|
|
35
38
|
|
|
36
39
|
Both outer regions are optional, and an absent one costs neither space nor a
|
|
37
40
|
gap: `.list-item-content` always takes the row's free space and
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Segmented control
|
|
2
|
+
|
|
3
|
+
> One compact choice among a few options, on a shared track with the selected segment raised.
|
|
4
|
+
|
|
5
|
+
**Related terms:** segmented control, toggle group, pill tabs, view switcher, button group.
|
|
6
|
+
|
|
7
|
+
`.segmented` is the track and each `.segment` a direct child. The look is
|
|
8
|
+
shared; the semantics come from the markup, so pick the anatomy by what the
|
|
9
|
+
choice does:
|
|
10
|
+
|
|
11
|
+
| Use | Markup | Selected | Keyboard |
|
|
12
|
+
| ------------- | ---------------------- | ---------------------- | ------------- |
|
|
13
|
+
| Pick a value | `<fieldset>` of radios | `:checked` | Native arrows |
|
|
14
|
+
| Switch panels | `role="tablist"` | `aria-selected="true"` | Tabs runtime |
|
|
15
|
+
| Navigate | `<nav>` of links | `aria-current="page"` | Tab, Enter |
|
|
16
|
+
|
|
17
|
+
Segments share the width equally and the track is at least `--control-size`
|
|
18
|
+
tall, so it lines up with `.btn` and `.input`; `.sm` and `.lg` size it like
|
|
19
|
+
any control. A long label wraps instead of truncating, and the row grows with
|
|
20
|
+
it. A parent that stretches its children (`.stack`) makes it full width; add
|
|
21
|
+
`.align-self-start` to keep it at its content width.
|
|
22
|
+
|
|
23
|
+
## Pick a value
|
|
24
|
+
|
|
25
|
+
Wrap each radio in its label. The radio is visually hidden but stays focusable
|
|
26
|
+
and submitted; in a browser without `:has()` it stays visible beside the
|
|
27
|
+
label.
|
|
28
|
+
|
|
29
|
+
```html demo
|
|
30
|
+
<fieldset class="segmented">
|
|
31
|
+
<legend class="sr-only">Range</legend>
|
|
32
|
+
<label class="segment"><input type="radio" name="range" value="day">Day</label>
|
|
33
|
+
<label class="segment"><input type="radio" name="range" value="week" checked>Week</label>
|
|
34
|
+
<label class="segment"><input type="radio" name="range" value="month">Month</label>
|
|
35
|
+
</fieldset>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Switch panels
|
|
39
|
+
|
|
40
|
+
A tablist with `data-enhance="tabs"` takes the [tabs](../enhancements/tabs.md)
|
|
41
|
+
runtime unchanged: it reads the roles, not the classes.
|
|
42
|
+
|
|
43
|
+
```html demo
|
|
44
|
+
<div class="segmented" role="tablist" aria-label="View" data-enhance="tabs">
|
|
45
|
+
<button class="segment" type="button" role="tab" id="view-list-tab" aria-selected="true" aria-controls="view-list">List</button>
|
|
46
|
+
<button class="segment" type="button" role="tab" id="view-grid-tab" aria-selected="false" aria-controls="view-grid" tabindex="-1">Grid</button>
|
|
47
|
+
</div>
|
|
48
|
+
<section role="tabpanel" id="view-list" aria-labelledby="view-list-tab">List view</section>
|
|
49
|
+
<section role="tabpanel" id="view-grid" aria-labelledby="view-grid-tab" hidden>Grid view</section>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Navigate
|
|
53
|
+
|
|
54
|
+
```html demo
|
|
55
|
+
<nav class="segmented" aria-label="Period">
|
|
56
|
+
<a class="segment" href="#day" aria-current="page">Day</a>
|
|
57
|
+
<a class="segment" href="#week">Week</a>
|
|
58
|
+
<a class="segment" href="#month">Month</a>
|
|
59
|
+
</nav>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## When to use something else
|
|
63
|
+
|
|
64
|
+
- Several options on at once (bold, italic): `.join` of `.btn` with
|
|
65
|
+
`aria-pressed`, see [joined controls](../forms/joined-controls.md).
|
|
66
|
+
- More options than fit on one row: [tabs](../enhancements/tabs.md), whose
|
|
67
|
+
strip scrolls.
|
|
68
|
+
- Long or descriptive options: [choice cards](../forms/choice-cards.md) or
|
|
69
|
+
plain radios.
|
|
70
|
+
|
|
71
|
+
## Hooks
|
|
72
|
+
|
|
73
|
+
- `--segmented-bg` — track fill.
|
|
74
|
+
- `--segmented-pad` — inset between the track and its segments, also the gap
|
|
75
|
+
between segments; the segment radius is the track radius minus it.
|
|
76
|
+
- `--segmented-radius` — track corner radius.
|
|
77
|
+
- `--segmented-fg` — label ink of the unselected segments; the selected one
|
|
78
|
+
takes `--text`.
|
|
79
|
+
- `--segmented-selected-bg` — fill of the selected segment. Its shadow follows
|
|
80
|
+
`--shadow`, so a flat theme removes it at the theme level.
|
|
81
|
+
- `--segmented-selected-border` — hairline around the selected segment. It is
|
|
82
|
+
what sets the segment apart where its fill is darker than the track; a theme
|
|
83
|
+
whose fill is lighter can fade it.
|
|
@@ -80,9 +80,11 @@ accent, override the tokens on the component:
|
|
|
80
80
|
}
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
`--state-selected` drives the completed fill
|
|
83
|
+
`--state-selected` drives the completed fill and connector;
|
|
84
84
|
`--state-selected-fg` corrects the number inside a completed marker when the
|
|
85
|
-
accent needs a lighter ink.
|
|
85
|
+
accent needs a lighter ink. The current marker sits on the surface, so its
|
|
86
|
+
number and ring read `--state-selected-text` first
|
|
87
|
+
([tokens](../foundations/tokens.md)).
|
|
86
88
|
|
|
87
89
|
## Horizontal steps
|
|
88
90
|
|
|
@@ -180,6 +180,11 @@ flyout when that specific control must close it regardless of the automatic
|
|
|
180
180
|
policy. This is useful for an Apply or Done action in an `outside` or `false`
|
|
181
181
|
panel. The trigger and Escape continue to close the flyout in every mode.
|
|
182
182
|
|
|
183
|
+
Arrow keys and Space in a radio group never close the flyout: they move
|
|
184
|
+
through the options, and a pointer pick closes it. Opening from the keyboard
|
|
185
|
+
focuses a group at its checked radio. The [palette picker](../forms/color-swatches.md#palette-in-a-flyout)
|
|
186
|
+
relies on both.
|
|
187
|
+
|
|
183
188
|
```html
|
|
184
189
|
<div class="flyout"
|
|
185
190
|
id="filters"
|
|
@@ -91,10 +91,15 @@ JavaScript involved:
|
|
|
91
91
|
|
|
92
92
|
## Equal-width tabs
|
|
93
93
|
|
|
94
|
-
To make a few tabs share the strip's width (
|
|
94
|
+
To make a few tabs share the strip's width (full-width, fill), set
|
|
95
95
|
`--tab-flex: 1 1 0` on the strip. `.switcher` does not apply here: a tab strip
|
|
96
96
|
never wraps, so it could never stack.
|
|
97
97
|
|
|
98
|
+
For a few tabs on a track with a raised selected tab (segmented control, pill
|
|
99
|
+
tabs), put the same tablist on [`.segmented`](../components/segmented.md):
|
|
100
|
+
the runtime is unchanged. Its labels wrap instead of scrolling, so keep it to
|
|
101
|
+
what fits on one row.
|
|
102
|
+
|
|
98
103
|
```html demo
|
|
99
104
|
<nav aria-label="Pricing sections">
|
|
100
105
|
<ul class="tabs" style="--tab-flex: 1 1 0">
|
|
@@ -158,5 +163,5 @@ Do not put a flyout trigger in a `role="tablist"`: a tablist holds only
|
|
|
158
163
|
## CSS hooks
|
|
159
164
|
|
|
160
165
|
- `--tab-flex` — flex of each item; set `1 1 0` on the strip for equal-width tabs
|
|
161
|
-
(
|
|
166
|
+
(fill the width). A strip that cannot fit its labels still scrolls.
|
|
162
167
|
- `--tab-gap` — space between an icon and the label text (default `0.375em`).
|
|
@@ -23,13 +23,15 @@ rest show it composed, isolated, or under stress.
|
|
|
23
23
|
Whole pages, composed the way a real product would.
|
|
24
24
|
|
|
25
25
|
- [Dashboard](../../demo/templates/dashboard.html) - data-heavy overview page
|
|
26
|
+
- [Order management](../../demo/templates/order-management.html) - one state at three levels (a badge in a selectable list, a message in context, a notification with its own action) composed on the `admin-neutral` business-interface theme
|
|
26
27
|
- [App](../../demo/templates/app.html) - application shell with sidebar
|
|
27
28
|
- [Workspaces](../../demo/templates/workspaces.html) - sectioned sidebar, master/detail list with native current state, and workspace cards
|
|
28
29
|
- [Marketing](../../demo/templates/marketing.html) - landing page with typography and fluid type
|
|
29
30
|
- [Blog](../../demo/templates/blog.html) - article page with prose
|
|
30
31
|
- [Settings modal](../../demo/templates/settings-modal.html) - two-pane preferences dialog, with the local CSS it needs audited rule by rule
|
|
31
32
|
- [Modal patterns](../../demo/templates/modal-patterns.html) - eight application modals, each the modal surface plus the smallest application CSS it needs
|
|
32
|
-
- [Workspace settings](../../demo/templates/settings-pricing.html) - plans with visible native choices, preferences and
|
|
33
|
+
- [Workspace settings](../../demo/templates/settings-pricing.html) - plans with visible native choices, preferences and segmented tabs
|
|
34
|
+
- [iOS-style settings](../../demo/templates/settings-ios.html) - iOS-like controls and grouped settings from unchanged framework markup, a local theme, and its findings listed rule by rule
|
|
33
35
|
- [Card compositions](../../demo/templates/card-compositions.html) - editorial, team, sign-in and horizontal cards composed from existing primitives
|
|
34
36
|
- [Register form](../../demo/templates/register-form.html) - registration page with choice cards, masks, the password toggle, `tel-prefix` validation, an OTP verification dialog, and status feedback
|
|
35
37
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Color swatches
|
|
2
|
+
|
|
3
|
+
> Pick a color from a palette the application defines, inline or from a flyout.
|
|
4
|
+
|
|
5
|
+
**Related terms:** color picker, palette, swatch, color chip, accent color, label color.
|
|
6
|
+
|
|
7
|
+
`.color-swatch` is a `<label>` wrapping a native radio, with the color in
|
|
8
|
+
`--color-swatch-value`. The radio stays the control: it submits, takes the
|
|
9
|
+
arrow keys and carries the name in `aria-label`. The checked swatch shows a
|
|
10
|
+
double ring that reads on any color. Use checkboxes for several colors at once.
|
|
11
|
+
|
|
12
|
+
For a free color, use the native [`.color`](controls.md) picker; a custom
|
|
13
|
+
spectrum or RGB sliders are outside Actual.
|
|
14
|
+
|
|
15
|
+
## Palette
|
|
16
|
+
|
|
17
|
+
Lay the swatches out with `.cluster` or `.grid`, and space them with `--gap`
|
|
18
|
+
(`.tight`, `.loose` or an inline value), not `gap`: the hit area reaches
|
|
19
|
+
half of `--gap` past the swatch, up to `--control-size`, so neighbours split
|
|
20
|
+
the gap between them. A tighter palette trades target size for density. Set
|
|
21
|
+
the column count with `--grid-columns: repeat(6, max-content)` on `.grid`.
|
|
22
|
+
|
|
23
|
+
```html demo
|
|
24
|
+
<fieldset class="field-group cluster">
|
|
25
|
+
<legend>Category color</legend>
|
|
26
|
+
<label class="color-swatch" style="--color-swatch-value: #2563eb"><input type="radio" name="category" value="#2563eb" aria-label="Blue" checked></label>
|
|
27
|
+
<label class="color-swatch" style="--color-swatch-value: #10b981"><input type="radio" name="category" value="#10b981" aria-label="Green"></label>
|
|
28
|
+
<label class="color-swatch" style="--color-swatch-value: #f59e0b"><input type="radio" name="category" value="#f59e0b" aria-label="Amber"></label>
|
|
29
|
+
<label class="color-swatch" style="--color-swatch-value: #e11d48"><input type="radio" name="category" value="#e11d48" aria-label="Rose"></label>
|
|
30
|
+
<label class="color-swatch" style="--color-swatch-value: #ffffff"><input type="radio" name="category" value="#ffffff" aria-label="White"></label>
|
|
31
|
+
</fieldset>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Palette in a flyout
|
|
35
|
+
|
|
36
|
+
A `<button class="color-swatch">` shows the current color and opens the
|
|
37
|
+
palette as a [flyout](../enhancements/flyout.md). A pointer pick closes it;
|
|
38
|
+
the arrow keys move through the colors with the panel open, and Escape returns
|
|
39
|
+
to the button. Give the panel `--flyout-pad` so the focus ring fits inside it.
|
|
40
|
+
Application code shows the pick on the button.
|
|
41
|
+
|
|
42
|
+
```html demo
|
|
43
|
+
<span class="field-label" id="accent-label">Accent color</span>
|
|
44
|
+
<div class="flyout-trigger">
|
|
45
|
+
<button class="color-swatch" type="button" data-enhance="flyout"
|
|
46
|
+
aria-controls="accent-palette" aria-expanded="false"
|
|
47
|
+
aria-labelledby="accent-label accent-name"
|
|
48
|
+
style="--color-swatch-value: #2563eb">
|
|
49
|
+
<span class="sr-only" id="accent-name">Blue</span>
|
|
50
|
+
</button>
|
|
51
|
+
<div class="flyout" id="accent-palette" style="--flyout-pad: var(--space-30)" hidden>
|
|
52
|
+
<div class="cluster" role="radiogroup" aria-labelledby="accent-label">
|
|
53
|
+
<label class="color-swatch" style="--color-swatch-value: #2563eb"><input type="radio" name="accent" value="#2563eb" aria-label="Blue" checked></label>
|
|
54
|
+
<label class="color-swatch" style="--color-swatch-value: #10b981"><input type="radio" name="accent" value="#10b981" aria-label="Green"></label>
|
|
55
|
+
<label class="color-swatch" style="--color-swatch-value: #f59e0b"><input type="radio" name="accent" value="#f59e0b" aria-label="Amber"></label>
|
|
56
|
+
<label class="color-swatch" style="--color-swatch-value: #e11d48"><input type="radio" name="accent" value="#e11d48" aria-label="Rose"></label>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<script>
|
|
61
|
+
(() => {
|
|
62
|
+
const preview = document.currentScript.closest(".docs-preview");
|
|
63
|
+
const trigger = preview.querySelector("button.color-swatch");
|
|
64
|
+
preview.querySelector(".flyout").addEventListener("change", (event) => {
|
|
65
|
+
trigger.style.setProperty("--color-swatch-value", event.target.value);
|
|
66
|
+
trigger.querySelector(".sr-only").textContent = event.target.getAttribute("aria-label");
|
|
67
|
+
});
|
|
68
|
+
})();
|
|
69
|
+
</script>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Palette and custom color
|
|
73
|
+
|
|
74
|
+
Add the native [`.color`](controls.md) picker to the panel for a color
|
|
75
|
+
outside the palette. Two changes to the flyout above:
|
|
76
|
+
|
|
77
|
+
- `data-flyout-auto-close="outside"` on the panel, so opening the color dialog
|
|
78
|
+
does not close it, and `data-flyout-close` on each swatch, so a pointer pick
|
|
79
|
+
still does. The arrow keys still move through the palette with the panel
|
|
80
|
+
open.
|
|
81
|
+
- The custom input sits in a `<label class="cluster">`, which centers it on
|
|
82
|
+
its text; `.choice` aligns checkboxes, radios and switches only.
|
|
83
|
+
- The custom input takes its own name. Application code unchecks the palette
|
|
84
|
+
when a custom color is chosen, so the form sends `brand`, or `brand-custom`
|
|
85
|
+
when no palette color is checked.
|
|
86
|
+
|
|
87
|
+
```html demo
|
|
88
|
+
<span class="field-label" id="brand-label">Brand color</span>
|
|
89
|
+
<div class="flyout-trigger">
|
|
90
|
+
<button class="color-swatch" type="button" data-enhance="flyout"
|
|
91
|
+
aria-controls="brand-palette" aria-expanded="false"
|
|
92
|
+
aria-labelledby="brand-label brand-name"
|
|
93
|
+
style="--color-swatch-value: #2563eb">
|
|
94
|
+
<span class="sr-only" id="brand-name">Blue</span>
|
|
95
|
+
</button>
|
|
96
|
+
<div class="flyout stack" id="brand-palette" data-flyout-auto-close="outside"
|
|
97
|
+
style="--flyout-pad: var(--space-30)" hidden>
|
|
98
|
+
<div class="cluster" role="radiogroup" aria-labelledby="brand-label">
|
|
99
|
+
<label class="color-swatch" data-flyout-close style="--color-swatch-value: #2563eb"><input type="radio" name="brand" value="#2563eb" aria-label="Blue" checked></label>
|
|
100
|
+
<label class="color-swatch" data-flyout-close style="--color-swatch-value: #10b981"><input type="radio" name="brand" value="#10b981" aria-label="Green"></label>
|
|
101
|
+
<label class="color-swatch" data-flyout-close style="--color-swatch-value: #f59e0b"><input type="radio" name="brand" value="#f59e0b" aria-label="Amber"></label>
|
|
102
|
+
<label class="color-swatch" data-flyout-close style="--color-swatch-value: #e11d48"><input type="radio" name="brand" value="#e11d48" aria-label="Rose"></label>
|
|
103
|
+
</div>
|
|
104
|
+
<label class="cluster"><input class="color" type="color" name="brand-custom" value="#7048e8"> Custom…</label>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<script>
|
|
108
|
+
(() => {
|
|
109
|
+
const preview = document.currentScript.closest(".docs-preview");
|
|
110
|
+
const trigger = preview.querySelector("button.color-swatch");
|
|
111
|
+
const palette = preview.querySelector(".flyout");
|
|
112
|
+
palette.addEventListener("change", (event) => {
|
|
113
|
+
const custom = event.target.type === "color";
|
|
114
|
+
if (custom) for (const radio of palette.querySelectorAll('[type="radio"]')) radio.checked = false;
|
|
115
|
+
trigger.style.setProperty("--color-swatch-value", event.target.value);
|
|
116
|
+
trigger.querySelector(".sr-only").textContent = custom
|
|
117
|
+
? `Custom ${event.target.value}`
|
|
118
|
+
: event.target.getAttribute("aria-label");
|
|
119
|
+
});
|
|
120
|
+
})();
|
|
121
|
+
</script>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The color dialog is the browser's own and looks different on each platform.
|
|
125
|
+
|
|
126
|
+
## Hooks
|
|
127
|
+
|
|
128
|
+
- `--color-swatch-value` — the color, set on each swatch.
|
|
129
|
+
- `--color-swatch-size` — visible swatch size, the height of a `.switch` track
|
|
130
|
+
by default. `.sm` and `.lg` scale it and the hit area.
|
|
131
|
+
- `--color-swatch-radius` — corner radius; round by default.
|
|
@@ -93,9 +93,9 @@ contrast contracts apply to the combobox for free:
|
|
|
93
93
|
|
|
94
94
|
.actual-combobox .cb-chip {
|
|
95
95
|
border: var(--border-width) solid
|
|
96
|
-
color-mix(in oklab, var(--
|
|
96
|
+
color-mix(in oklab, var(--primary) calc(100% - var(--soft-border-mix, 100%)), transparent);
|
|
97
97
|
border-radius: var(--radius-full);
|
|
98
|
-
background: color-mix(in oklab, var(--
|
|
98
|
+
background: color-mix(in oklab, var(--primary) calc(100% - var(--soft-bg-mix)), transparent);
|
|
99
99
|
color: color-mix(in oklab, var(--primary) var(--soft-fg-mix), var(--text));
|
|
100
100
|
font-size: var(--font-size-xs);
|
|
101
101
|
font-weight: var(--font-weight-semibold);
|
|
@@ -6,9 +6,12 @@ Native-only control chrome: the file input and color picker mostly defeat custom
|
|
|
6
6
|
|
|
7
7
|
> Small, theme-aware chrome for the native color picker.
|
|
8
8
|
|
|
9
|
-
Use `.color` on an `input[type="color"]` to
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Use `.color` on an `input[type="color"]` to show the native picker as a round
|
|
10
|
+
swatch the height of a [switch](switch.md), on a control-sized hit area, with
|
|
11
|
+
the framework's edge, disabled, and focus styles. `--color-size` sets the
|
|
12
|
+
swatch. The color dialog remains browser-native. To choose from a fixed
|
|
13
|
+
palette instead, use [color swatches](color-swatches.md), which share this
|
|
14
|
+
geometry.
|
|
12
15
|
|
|
13
16
|
```html demo
|
|
14
17
|
<label class="field">
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
> Use `.join` when controls need to be visually joined into a single unit.
|
|
4
4
|
|
|
5
|
-
**Related terms:**
|
|
5
|
+
**Related terms:** button group, input group, toolbar.
|
|
6
|
+
|
|
7
|
+
`.join` groups controls that keep their own identity and function. For one
|
|
8
|
+
choice among a few on a shared track, use a
|
|
9
|
+
[segmented control](../components/segmented.md).
|
|
6
10
|
|
|
7
11
|
## Class reference
|
|
8
12
|
|
|
@@ -13,7 +17,7 @@
|
|
|
13
17
|
|
|
14
18
|
Add `.join` to the wrapper that groups the controls. Use `.join-addon` for static prefix/suffix content such as currency symbols, units, or protocol text. Action buttons inside the group should use `.btn`. The wrapper should still carry `role="group"` with a label for accessibility.
|
|
15
19
|
|
|
16
|
-
`.join` reshapes corners and shared edges; it does not unify the segments' treatments, and the children keep whatever classes they carry. Give them one variant family so the group reads as a single unit, and change a segment's variant only to mark state — a solid button among outlines for
|
|
20
|
+
`.join` reshapes corners and shared edges; it does not unify the segments' treatments, and the children keep whatever classes they carry. Give them one variant family so the group reads as a single unit, and change a segment's variant only to mark state — a solid button among outlines for a pressed toggle. A group mixing `.btn`, `.btn outline` and `.btn soft` is valid markup that renders as three unrelated controls stuck together.
|
|
17
21
|
|
|
18
22
|
When composing source files manually, import `components/join.css` after the controls it groups. `.join` writes child border-radius longhands directly so joined corners win at equal specificity.
|
|
19
23
|
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
| `.range` | Component | Native range slider, theme-aware accent. |
|
|
29
29
|
| `.file` | Component | Native file input, themed picker button. |
|
|
30
30
|
| `.color` | Component | Native color picker in a control-sized box. |
|
|
31
|
+
| `.color-swatch` | Component | Palette color on a radio, or its trigger. |
|
|
31
32
|
| `.field` | Composition | Field wrapper, on `<label>` or `<div>`. |
|
|
32
33
|
| `.field-label` | Composition | Label text, element-agnostic. |
|
|
33
34
|
| `.field-help` | Composition | Supporting helper text under a control. |
|
|
@@ -132,8 +132,10 @@ same optical line. Change the type or the control size and they stay level:
|
|
|
132
132
|
- `--switch-width` — track width.
|
|
133
133
|
- `--switch-block-size` — track height.
|
|
134
134
|
- `--switch-knob-margin` — inset between knob and track.
|
|
135
|
+
- `--switch-knob-inline-size` — knob width; wider than its height makes a
|
|
136
|
+
capsule knob. Defaults to the knob height.
|
|
135
137
|
|
|
136
|
-
Knob size and travel distance are derived from
|
|
137
|
-
|
|
138
|
+
Knob size and travel distance are derived from these hooks; set them rather
|
|
139
|
+
than the derived values. The knob is concentric in its track: the
|
|
138
140
|
inset is measured inside the border on both axes, so it clears the rails by the
|
|
139
141
|
same distance it clears the ends.
|
|
@@ -56,7 +56,7 @@ Set `--form-actions-justify` (and `--form-actions-align`).
|
|
|
56
56
|
|
|
57
57
|
### Customize a switch's size
|
|
58
58
|
|
|
59
|
-
Change `--switch-width`, `--switch-block-size`, and `--switch-knob-margin`; knob size and travel are derived from them. To rescale the whole row of inline choices at once, use `.sm` / `.lg` on `.choice`, or set `--choice-control-size` directly — the checkbox, the radio, and the switch track all follow it. Density contexts leave inline choices alone.
|
|
59
|
+
Change `--switch-width`, `--switch-block-size`, and `--switch-knob-margin`; knob size and travel are derived from them. `--switch-knob-inline-size` widens the knob into a capsule. To rescale the whole row of inline choices at once, use `.sm` / `.lg` on `.choice`, or set `--choice-control-size` directly — the checkbox, the radio, and the switch track all follow it. Density contexts leave inline choices alone.
|
|
60
60
|
|
|
61
61
|
→ Forms · Switch
|
|
62
62
|
|
|
@@ -151,34 +151,42 @@ Color should reinforce structure and meaning, not carry them alone. Information
|
|
|
151
151
|
|
|
152
152
|
The default palette is **Ink & Terra**: a near-neutral aubergine `primary`, terracotta `secondary`, and pigment-toned statuses (sage, ochre, brick) tuned to equal perceived weight. The neutral ramp (text, borders, subtle surfaces) is tinted toward the primary hue so the whole page quietly carries the identity. Optional themes can re-introduce vivid intent palettes on top of this foundation.
|
|
153
153
|
|
|
154
|
-
Soft surfaces
|
|
154
|
+
Soft surfaces are generated from intent colors through `color-mix()`, in two
|
|
155
|
+
recipes that share one ink:
|
|
156
|
+
|
|
157
|
+
- **Tint** — the default `.badge` and `.alert`, and `.btn.soft`: a translucent
|
|
158
|
+
intent laid over whatever surface is underneath, with no rim. It keeps
|
|
159
|
+
separating on `--surface-subtle` and `--surface-raised`, where a fixed mix
|
|
160
|
+
with `--surface` would match the surface it sits on.
|
|
161
|
+
- **Mix** — the shared `.soft` on zones (`.card`, `.chat-bubble`, `.navbar`,
|
|
162
|
+
`.app-nav`): an opaque mix of `--surface` with the intent and a tinted rim.
|
|
163
|
+
A zone may sit over an image, where a translucent fill would hand the text's
|
|
164
|
+
contrast to whatever is behind it.
|
|
165
|
+
|
|
166
|
+
Themes tune both globally instead of rewriting component selectors:
|
|
155
167
|
|
|
156
168
|
```css
|
|
157
169
|
:root {
|
|
158
|
-
--soft-bg-mix: 88%;
|
|
159
|
-
--soft-
|
|
160
|
-
--soft-hover-alpha: 12%;
|
|
170
|
+
--soft-bg-mix: 88%; /* share of surface; the tint's opacity is 100% minus it */
|
|
171
|
+
--soft-hover-alpha: 6%; /* extra intent share on hover */
|
|
161
172
|
--soft-fg-mix: 100%;
|
|
162
173
|
}
|
|
163
174
|
```
|
|
164
175
|
|
|
165
|
-
`--soft-border-mix`
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
with no contour — the flatter language of a modern component set, from one
|
|
169
|
-
declaration:
|
|
176
|
+
`--soft-border-mix` has no default. Unset, the tint draws no rim and the mix
|
|
177
|
+
keeps a 65% one. A theme that sets it gives every soft treatment a tinted rim
|
|
178
|
+
of that weight — contoured badges and alerts, from one declaration:
|
|
170
179
|
|
|
171
180
|
```css
|
|
172
|
-
[data-theme="
|
|
173
|
-
--soft-border-mix:
|
|
181
|
+
[data-theme="contoured"] {
|
|
182
|
+
--soft-border-mix: 65%;
|
|
174
183
|
}
|
|
175
184
|
```
|
|
176
185
|
|
|
177
|
-
|
|
178
|
-
(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
borderless soft is a look, not a contract the Degraded tier upholds.
|
|
186
|
+
A rule that re-derives a soft color reads it with the recipe's own fallback:
|
|
187
|
+
`var(--soft-border-mix, 100%)` for a tint, `var(--soft-border-mix, 65%)` for a
|
|
188
|
+
mix. Both recipes are rendering-only — the baseline path below `color-mix()`
|
|
189
|
+
keeps `var(--surface-subtle)` and `var(--border)`.
|
|
182
190
|
|
|
183
191
|
`--soft-fg-mix` is the share of raw intent in soft *text*. At its `100%` default soft ink is the intent color itself, which is what a palette of dark, muted intents wants. A vivid or light palette cannot afford that: a soft badge then paints intent-tinted ink on an intent-tinted surface, and the two converge. Lowering the mix rebates the ink toward `--text`, which is the right direction in both schemes because `--text` is dark on a light theme and light on a dark one. Around `45%` a fully saturated palette recovers AA while the ink still reads as its intent.
|
|
184
192
|
|
|
@@ -193,17 +201,19 @@ hook, and the soft recipe reads it before falling back to the global mix:
|
|
|
193
201
|
- `--soft-fg-mix` — the **global fallback derivation**, unchanged.
|
|
194
202
|
|
|
195
203
|
**Soft foregrounds are calibrated against the darkest or closest interactive
|
|
196
|
-
surface they are expected to appear on.**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
the
|
|
200
|
-
|
|
201
|
-
|
|
204
|
+
surface they are expected to appear on.** The tint is translucent, so the same
|
|
205
|
+
pair reads differently on each surface — usually closest on `--surface-subtle`.
|
|
206
|
+
For interactive soft treatments the fill moves toward the intent on hover
|
|
207
|
+
(`--soft-hover-alpha`), so the binding pair is the *hovered* tint over
|
|
208
|
+
`--surface-subtle` and the resting fill simply carries more margin; the default
|
|
209
|
+
palette ships its own `--primary`/`--secondary`/… `-soft-fg` hooks to keep
|
|
210
|
+
every pair ≥ 4.5:1 on `--surface`, `--surface-raised` and `--surface-subtle` in
|
|
211
|
+
both color schemes, enforced by `tests/browser/soft-recipe.test.js`. The hard line stops at the default theme:
|
|
202
212
|
presets and custom `[data-theme]` islands keep the `--soft-fg-mix` derivation
|
|
203
213
|
unless they declare the hooks themselves (the framework resets them outside
|
|
204
214
|
the default boundary). A theme that overrides the soft palette is responsible
|
|
205
215
|
for preserving sufficient text contrast; `bunx --bun actual-css contrast --theme FILE`
|
|
206
|
-
measures its resting and hovered pairs.
|
|
216
|
+
measures its resting and hovered pairs, composited on each of those surfaces.
|
|
207
217
|
|
|
208
218
|
A theme whose primary should read more muted than its statuses sets only
|
|
209
219
|
`--primary-soft-fg`, and every soft `.primary` foreground follows it. Without a
|
|
@@ -599,12 +609,33 @@ A minimal recolor theme (illustrative, not a shipped theme):
|
|
|
599
609
|
|
|
600
610
|
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.
|
|
601
611
|
|
|
612
|
+
Selected state has three roles:
|
|
613
|
+
|
|
614
|
+
- `--state-selected` — the selected fill or accent: a pressed button, a checked switch, a completed step.
|
|
615
|
+
- `--state-selected-fg` — ink on that fill.
|
|
616
|
+
- `--state-selected-text` — selected ink on a regular surface: the current nav link, the selected tab, the current step's number and ring.
|
|
617
|
+
|
|
618
|
+
`--state-selected-text` has no default: the core resets it to the guaranteed-invalid value on every theme boundary, and each consumer reads `var(--state-selected-text, var(--state-selected))`. The reset is what lets a nested theme island reach its own `--state-selected`: a theme that resolves the hook to another token bakes the reference where it is declared, so an island that sets nothing would otherwise inherit the resolved value and never fall back. Set it when one color cannot be both a fill under its `-fg` and text on the page — typically a dark scheme, where a fill dark enough for white ink is too dark to read as text. Pulling the accent toward `--text` keeps its hue in both schemes:
|
|
619
|
+
|
|
620
|
+
```css
|
|
621
|
+
[data-theme="my-theme"] {
|
|
622
|
+
--state-selected-text: light-dark(
|
|
623
|
+
var(--primary),
|
|
624
|
+
color-mix(in oklab, var(--primary) 50%, var(--text))
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
`actual-css contrast` measures it on each surface (4.5:1 for text, 3:1 for the step ring).
|
|
630
|
+
|
|
602
631
|
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.
|
|
603
632
|
|
|
604
633
|
Because the native select chevron is an SVG background image and cannot inherit `currentColor`, the select hands the arrow back to the browser under `prefers-contrast: more` (same treatment as `forced-colors: active`) instead of shipping a recolored chevron per theme — one native fallback covers every theme without a high-contrast icon variant to maintain.
|
|
605
634
|
|
|
606
635
|
Under `prefers-contrast: more`, the user's contrast preference wins over a theme's palette by contract: the core collapses `--text-muted`/`--text-subtle` to `--text` and thickens borders and focus so no theme can defeat a legibility requirement. This is a deliberate decision, not a specificity quirk — the repeated `[data-theme][data-theme]` attribute only outranks a preset theme stylesheet loaded after the core.
|
|
607
636
|
|
|
637
|
+
Under `prefers-reduced-transparency: reduce`, `.topbar` and `.form-actions.sticky` paint `--surface` without a backdrop blur. A theme with translucent `--surface-*` redeclares them opaque inside the same media query; the glass demo theme shows the recipe.
|
|
638
|
+
|
|
608
639
|
Under `forced-colors: active`, the browser automatically remaps `color`, `background`, and `border` properties to system colors. The core only adjusts what the remapping cannot express on its own — `--state-disabled` becomes `GrayText` and `--disabled-opacity` becomes `1` — declared on `:root` and `[data-theme]` so every theme boundary follows. Keep local forced-color rules only for custom control geometry, native bar parts, or selected/checked states that would otherwise be communicated only by background color.
|
|
609
640
|
|
|
610
641
|
## Component mapping
|
|
@@ -10,16 +10,22 @@ bunx --bun actual-css design --theme src/theme.css --out design/
|
|
|
10
10
|
|
|
11
11
|
## What is exported
|
|
12
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
|
-
| `
|
|
18
|
-
| `
|
|
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
|
+
| `dtcg/foundations.tokens.json` | the portable profile, every foundation |
|
|
18
|
+
| `dtcg/light.tokens.json`, `dark.tokens.json` | colors, one file per scheme |
|
|
19
|
+
| `penpot/tokens.json` | all tokens, as sets and themes |
|
|
20
|
+
| `penpot/plugin/` | the plugin and component spec |
|
|
19
21
|
|
|
20
22
|
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
23
|
|
|
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.
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
## DTCG
|
|
27
|
+
|
|
28
|
+
`dtcg/` is the portable profile, for any reader that follows the [Design Tokens Format](https://www.designtokens.org/format/): every foundation carries the `$type` the spec defines (`dimension`, `fontWeight`, `fontFamily`, `duration`, `cubicBezier`, `number`), and each color carries a 6-digit `hex` with `alpha` separate. It keeps font stacks, line heights and motion, which `figma/` drops, and omits Figma's adaptations: a font weight is a `fontWeight` here, a `number` there. Shadows and z-index are not exported anywhere.
|
|
23
29
|
|
|
24
30
|
## Figma
|
|
25
31
|
|
|
@@ -53,7 +53,7 @@ optimizing — the bare `actual-css` entrypoint and `actual-css/full` both resol
|
|
|
53
53
|
to the complete compiled framework:
|
|
54
54
|
|
|
55
55
|
```html
|
|
56
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.
|
|
56
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.14/dist/actual.full.min.css">
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
The package does not maintain separate partial bundles. Modular entrypoints map
|