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.
Files changed (58) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +4 -4
  3. package/components.json +116 -10
  4. package/dist/actual.full.css +648 -77
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/navigation.json +2 -0
  8. package/docs/pages/components/alert.md +12 -2
  9. package/docs/pages/components/button.md +3 -2
  10. package/docs/pages/components/list.md +6 -3
  11. package/docs/pages/components/segmented.md +83 -0
  12. package/docs/pages/components/steps.md +4 -2
  13. package/docs/pages/enhancements/flyout.md +5 -0
  14. package/docs/pages/enhancements/tabs.md +7 -2
  15. package/docs/pages/examples/overview.md +3 -1
  16. package/docs/pages/forms/color-swatches.md +131 -0
  17. package/docs/pages/forms/combobox.md +2 -2
  18. package/docs/pages/forms/controls.md +6 -3
  19. package/docs/pages/forms/joined-controls.md +6 -2
  20. package/docs/pages/forms/overview.md +1 -0
  21. package/docs/pages/forms/switch.md +4 -2
  22. package/docs/pages/foundations/theming.md +1 -1
  23. package/docs/pages/foundations/tokens.md +54 -23
  24. package/docs/pages/guides/design-tools.md +13 -7
  25. package/docs/pages/guides/introduction.md +1 -1
  26. package/docs/pages/layout/scroll-snap.md +4 -2
  27. package/package.json +2 -2
  28. package/reserved-classes.json +3 -0
  29. package/src/cli/actual-css.js +3 -3
  30. package/src/css/components/alert.css +65 -14
  31. package/src/css/components/app-nav.css +17 -9
  32. package/src/css/components/badge.css +23 -9
  33. package/src/css/components/button.css +17 -2
  34. package/src/css/components/close.css +6 -3
  35. package/src/css/components/index.css +1 -0
  36. package/src/css/components/list.css +18 -3
  37. package/src/css/components/navbar.css +5 -2
  38. package/src/css/components/segmented.css +186 -0
  39. package/src/css/components/steps.css +6 -3
  40. package/src/css/components/tab.css +1 -1
  41. package/src/css/core/focus.css +3 -0
  42. package/src/css/core/tokens.css +20 -4
  43. package/src/css/core/variants.css +16 -5
  44. package/src/css/forms/color-swatch.css +159 -0
  45. package/src/css/forms/form-actions.css +9 -0
  46. package/src/css/forms/index.css +1 -0
  47. package/src/css/forms/native.css +50 -20
  48. package/src/css/forms/switch.css +5 -2
  49. package/src/css/layout/scroll-snap.css +30 -0
  50. package/src/css/layout/topbar.css +10 -0
  51. package/src/js/focus.js +16 -1
  52. package/src/js/surface.js +10 -0
  53. package/src/tooling/color.js +14 -1
  54. package/src/tooling/css-bundle.js +0 -5
  55. package/src/tooling/design-components.js +9 -5
  56. package/src/tooling/design-export.js +8 -0
  57. package/src/tooling/design-tokens.js +163 -43
  58. package/src/tooling/theme-contrast.js +151 -22
@@ -69,6 +69,7 @@
69
69
  "pagination",
70
70
  "progress",
71
71
  "rating",
72
+ "segmented",
72
73
  "shimmer",
73
74
  "skeleton",
74
75
  "spinner",
@@ -91,6 +92,7 @@
91
92
  "form-actions",
92
93
  "joined-controls",
93
94
  "choice-cards",
95
+ "color-swatches",
94
96
  "switch",
95
97
  "range",
96
98
  "otp",
@@ -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` for density changes. The inline padding stays stable.
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 the active segment with a solid
157
- button among outlines, which is the pattern a segmented control wants.
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
- `.list-item-content` has no minimum content floor, so supporting text and long
34
- tokens wrap instead of widening the page.
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, connector and the current ring;
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 (segmented, full-width, fill), set
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
- (segmented, fill the width). A strip that cannot fit its labels still scrolls.
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 a local segmented tab treatment
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(--surface) var(--soft-border-mix), var(--primary));
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(--surface) var(--soft-bg-mix), var(--primary));
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 give the native picker a control-sized
10
- box with the framework's border, radius, surface, disabled, and focus styles.
11
- The color dialog and selected-color swatch remain browser-native.
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:** segmented control, button group, input group.
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 the active segment of a segmented control. A group mixing `.btn`, `.btn outline` and `.btn soft` is valid markup that renders as three unrelated controls stuck together.
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 the three `--switch-*` hooks; set
137
- those rather than the derived values. The knob is concentric in its track: the
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 (`.btn.soft`, `.badge.soft`, and the default `.alert`) are generated from intent colors through `color-mix()`. Themes can tune the mix globally instead of rewriting component selectors.
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-border-mix: 65%;
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` is the same lever for the rim. The `65%` default keeps a
166
- visibly tinted border around every soft surface; pointing it at `--soft-bg-mix`
167
- resolves the rim to the fill, and soft treatments become a tint carrying text
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="flat"] {
173
- --soft-border-mix: var(--soft-bg-mix);
181
+ [data-theme="contoured"] {
182
+ --soft-border-mix: 65%;
174
183
  }
175
184
  ```
176
185
 
177
- It reaches the default `.badge` and `.alert` plus every explicit `.soft`
178
- (`.btn`, `.card`, `.chat`, `.navbar`, `.app-nav`), and nothing else: a component
179
- on its own surface keeps its `--border` rim. The dial is also
180
- rendering-only — the baseline path below `color-mix()` keeps `var(--border)`, so
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.** For interactive soft treatments the
197
- fill moves toward the intent on hover (`--soft-hover-alpha`), so the binding
198
- surface is the *hovered* one and the resting fill simply carries more margin;
199
- the default palette ships its own `--primary`/`--secondary`/… `-soft-fg` hooks
200
- to keep that pair ≥ 4.5:1 in both color schemes, enforced by
201
- `tests/browser/soft-recipe.test.js`. The hard line stops at the default theme:
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
- | `penpot/tokens.json` | all tokens, as sets and themes |
18
- | `penpot/plugin/` | the plugin and component spec |
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. Font stacks, line heights, shadows, motion and z-index have no design-tool equivalent and are not exported.
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.13/dist/actual.full.min.css">
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