actual-css 0.7.0 → 0.9.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 +217 -0
- package/README.md +13 -11
- package/components.json +668 -0
- package/dist/actual.css +166 -37
- package/dist/actual.full.css +1087 -643
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/dist/actual.js +1 -1
- package/dist/actual.min.css +1 -1
- package/docs/design-notes/accessibility-decisions.md +33 -0
- package/docs/design-notes/browser-support.md +62 -30
- package/docs/design-notes/column-layout.md +3 -3
- package/docs/design-notes/css-anchor.md +75 -21
- package/docs/design-notes/enhancement-contract.md +14 -6
- package/docs/design-notes/grid.md +4 -4
- package/docs/design-notes/naming.md +6 -1
- package/docs/design-notes/platform-alignment.md +53 -7
- package/docs/design-notes/popover-manual-poc.md +105 -25
- package/docs/design-notes/surface-reparenting.md +56 -47
- package/docs/design-notes/tooltip-coordinate-space.md +132 -0
- package/docs/design-notes/widget-primitives.md +7 -4
- package/docs/pages/components/accordion.md +41 -2
- package/docs/pages/components/alert.md +85 -30
- package/docs/pages/components/app-navigation.md +6 -0
- package/docs/pages/components/avatar.md +6 -6
- package/docs/pages/components/badge.md +101 -16
- package/docs/pages/components/button.md +16 -8
- package/docs/pages/components/card.md +10 -10
- package/docs/pages/components/dialog.md +21 -6
- package/docs/pages/components/drawer.md +13 -0
- package/docs/pages/components/fab.md +4 -3
- package/docs/pages/components/list.md +42 -4
- package/docs/pages/components/navbar.md +17 -1
- package/docs/pages/components/pagination.md +2 -2
- package/docs/pages/components/skeleton.md +3 -1
- package/docs/pages/components/steps.md +19 -0
- package/docs/pages/components/table.md +3 -2
- package/docs/pages/enhancements/flyout.md +82 -38
- package/docs/pages/enhancements/overview.md +25 -0
- package/docs/pages/enhancements/status-bar.md +1 -1
- package/docs/pages/enhancements/tabs.md +45 -37
- package/docs/pages/enhancements/tooltip.md +36 -10
- package/docs/pages/examples/overview.md +38 -5
- package/docs/pages/forms/choice-cards.md +11 -6
- package/docs/pages/forms/combobox.md +1 -1
- package/docs/pages/forms/floating-field.md +4 -3
- package/docs/pages/forms/joined-controls.md +22 -0
- package/docs/pages/forms/otp.md +2 -1
- package/docs/pages/forms/overview.md +1 -1
- package/docs/pages/forms/states-and-sizes.md +16 -18
- package/docs/pages/forms/sticky-actions.md +2 -2
- package/docs/pages/forms/switch.md +1 -1
- package/docs/pages/foundations/philosophy.md +57 -26
- package/docs/pages/foundations/theming.md +2 -2
- package/docs/pages/foundations/tokens.md +118 -21
- package/docs/pages/foundations/typography.md +35 -14
- package/docs/pages/guides/introduction.md +11 -9
- package/docs/pages/guides/progressive-enhancement.md +115 -30
- package/docs/pages/guides/tailwind.md +3 -2
- package/docs/pages/layout/media.md +1 -1
- package/docs/pages/layout/overview.md +11 -1
- package/docs/pages/layout/scroll-snap.md +3 -3
- package/docs/pages/layout/stack.md +17 -9
- package/docs/pages/patterns/icon-input.md +4 -2
- package/docs/pages/patterns/icon-slot.md +2 -0
- package/docs/pages/patterns/intent-nesting.md +1 -1
- package/docs/pages/utilities/overview.md +23 -2
- package/docs/pages/utilities/sizing-wrapping.md +1 -1
- package/docs/pages/utilities/spacing.md +10 -8
- package/llms.txt +2 -1
- package/package.json +159 -149
- package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
- package/src/css/components/accordion.css +42 -3
- package/src/css/components/alert.css +25 -13
- package/src/css/components/app-nav.css +18 -8
- package/src/css/components/avatar.css +13 -1
- package/src/css/components/badge.css +78 -12
- package/src/css/components/breadcrumb.css +2 -3
- package/src/css/components/button.css +9 -16
- package/src/css/components/card.css +9 -2
- package/src/css/components/chat.css +1 -0
- package/src/css/components/data-list.css +1 -1
- package/src/css/components/drawer.css +101 -32
- package/src/css/components/fab.css +0 -6
- package/src/css/components/flyout.css +35 -98
- package/src/css/components/index.css +0 -1
- package/src/css/components/indicator.css +21 -0
- package/src/css/components/key.css +10 -0
- package/src/css/components/list.css +40 -4
- package/src/css/components/menu.css +2 -1
- package/src/css/components/modal.css +48 -33
- package/src/css/components/navbar.css +37 -4
- package/src/css/components/pagination.css +1 -0
- package/src/css/components/skeleton.css +15 -4
- package/src/css/components/spinner.css +6 -6
- package/src/css/components/status-bar.css +0 -5
- package/src/css/components/steps.css +45 -59
- package/src/css/components/tab.css +29 -9
- package/src/css/components/table.css +13 -5
- package/src/css/components/tooltip.css +36 -40
- package/src/css/core/base.css +23 -1
- package/src/css/core/intents.css +17 -6
- package/src/css/core/theme.css +27 -0
- package/src/css/core/tokens.css +35 -15
- package/src/css/core/variants.css +64 -15
- package/src/css/forms/choice-card.css +4 -4
- package/src/css/forms/choice.css +76 -29
- package/src/css/forms/control.css +2 -1
- package/src/css/forms/floating-field.css +3 -3
- package/src/css/forms/form.css +4 -3
- package/src/css/forms/input-icon.css +20 -14
- package/src/css/forms/native.css +6 -1
- package/src/css/forms/otp.css +9 -1
- package/src/css/forms/range.css +2 -0
- package/src/css/forms/select.css +29 -9
- package/src/css/forms/switch.css +28 -8
- package/src/css/layout/app-layout.css +1 -1
- package/src/css/layout/grid.css +50 -76
- package/src/css/layout/scroll-snap.css +11 -7
- package/src/css/layout/scroller.css +3 -4
- package/src/css/layout/stack.css +2 -2
- package/src/css/typography/fluid.css +4 -3
- package/src/css/typography/prose.css +6 -5
- package/src/css/utilities/base.css +2 -1
- package/src/css/utilities/extra.css +20 -17
- package/src/js/context-menu.js +27 -26
- package/src/js/dialog.js +2 -1
- package/src/js/dismiss.js +5 -0
- package/src/js/enhance.js +34 -2
- package/src/js/enhancement-loader.js +36 -43
- package/src/js/events.js +0 -3
- package/src/js/filter.js +0 -1
- package/src/js/floating-space.js +42 -0
- package/src/js/flyout.js +9 -44
- package/src/js/focus-group.js +7 -0
- package/src/js/focus.js +6 -0
- package/src/js/menu.js +10 -1
- package/src/js/selectors.js +5 -9
- package/src/js/status.js +6 -5
- package/src/js/surface.js +97 -139
- package/src/js/tab.js +6 -1
- package/src/js/tooltip.js +150 -51
- package/src/js/validation.js +1 -1
- package/src/css/components/surface.css +0 -49
|
@@ -75,7 +75,7 @@ contrast contracts apply to the combobox for free:
|
|
|
75
75
|
background: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--primary));
|
|
76
76
|
color: color-mix(in oklab, var(--primary) var(--soft-fg-mix), var(--text));
|
|
77
77
|
font-size: var(--font-size-xs);
|
|
78
|
-
font-weight: var(--font-weight-
|
|
78
|
+
font-weight: var(--font-weight-semibold);
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
@@ -68,11 +68,12 @@ every state, so their label always sits at the top.
|
|
|
68
68
|
</div>
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
##
|
|
71
|
+
## Size and density
|
|
72
72
|
|
|
73
73
|
The reserved headroom responds to control density through `--control-size`:
|
|
74
|
-
`.sm` and `.lg` on the cell
|
|
75
|
-
|
|
74
|
+
`.sm` and `.lg` on the cell resize the control geometry and typography. A
|
|
75
|
+
`.compact` or `.spacious` ancestor changes geometry only, including the label
|
|
76
|
+
spacing with it; typography stays unchanged under density.
|
|
76
77
|
The floating label does add height — the headroom reserves a second vertical
|
|
77
78
|
line on top of the control's minimum size.
|
|
78
79
|
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
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
15
|
|
|
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.
|
|
17
|
+
|
|
16
18
|
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.
|
|
17
19
|
|
|
18
20
|
```html demo
|
|
@@ -33,6 +35,26 @@ the focus ring surrounds the complete joined field. Attached buttons keep their
|
|
|
33
35
|
own focus indicator when reached directly, so the actionable segment remains
|
|
34
36
|
identifiable.
|
|
35
37
|
|
|
38
|
+
## Linting `role="group"`
|
|
39
|
+
|
|
40
|
+
Biome's `a11y/useSemanticElements` reports the wrapper and offers `<fieldset>`
|
|
41
|
+
as the replacement. That is the only element it suggests, and it does not fit:
|
|
42
|
+
`<fieldset>` groups form controls under a `<legend>`, while ARIA's `group` is
|
|
43
|
+
any labelled set of related objects. Suppress it on the element:
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<!-- biome-ignore lint/a11y/useSemanticElements: ARIA group has no HTML
|
|
47
|
+
equivalent here; fieldset expects form controls under a legend. -->
|
|
48
|
+
<div class="join" role="group" aria-label="Date navigation">
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Unlike the menu rule in
|
|
52
|
+
[flyout](../enhancements/flyout.html#linting-rolemenu), do not switch this one
|
|
53
|
+
off globally: it correctly catches `<div role="navigation">`,
|
|
54
|
+
`<div role="button">` and `<div role="separator">`. And drop `role="group"`
|
|
55
|
+
rather than suppressing it when the wrapper has no label to give — an unnamed
|
|
56
|
+
group adds a boundary and nothing else.
|
|
57
|
+
|
|
36
58
|
## CSS hooks
|
|
37
59
|
|
|
38
60
|
- `--join-radius` — outer corner radius of the joined group. The per-corner
|
package/docs/pages/forms/otp.md
CHANGED
|
@@ -36,7 +36,8 @@ Add exactly one visual `span` per expected character. Their count must match
|
|
|
36
36
|
a numeric keyboard but does not enforce numeric input; validate the submitted
|
|
37
37
|
value as usual.
|
|
38
38
|
|
|
39
|
-
The component follows shared `.sm` and `.lg`
|
|
39
|
+
The component follows shared `.sm` and `.lg` size through `--control-size` and
|
|
40
|
+
`--control-font-size`.
|
|
40
41
|
Use `aria-invalid="true"` for explicit invalid state, or place it in the
|
|
41
42
|
standard `.needs-validation.was-validated` form flow.
|
|
42
43
|
|
|
@@ -98,7 +98,7 @@ A full form using the control classes, field wrappers, and form actions:
|
|
|
98
98
|
|
|
99
99
|
<fieldset class="field-group">
|
|
100
100
|
<legend class="field-label">Availability</legend>
|
|
101
|
-
<div class="stack
|
|
101
|
+
<div class="stack compact">
|
|
102
102
|
<label class="choice">
|
|
103
103
|
<input class="radio" type="radio" name="availability" value="available" checked />
|
|
104
104
|
<span>Available for work</span>
|
|
@@ -4,23 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
## Class reference
|
|
6
6
|
|
|
7
|
-
| Class | Kind | Description
|
|
8
|
-
| ----- | -------- |
|
|
9
|
-
| `.sm` | Modifier |
|
|
10
|
-
| `.lg` | Modifier |
|
|
7
|
+
| Class | Kind | Description |
|
|
8
|
+
| ----- | -------- | ---------------------------------------- |
|
|
9
|
+
| `.sm` | Modifier | Smaller control geometry and typography. |
|
|
10
|
+
| `.lg` | Modifier | Larger control geometry and typography. |
|
|
11
11
|
|
|
12
|
-
Each
|
|
12
|
+
Each maps `--control-size` and `--control-font-size` to the matching step.
|
|
13
13
|
|
|
14
14
|
Use `readonly` for text-like values that can still be focused, selected, and submitted. Use `disabled` for unavailable controls that should not receive focus or submit a value.
|
|
15
15
|
|
|
16
|
-
The `.sm` and `.lg` modifiers set the shared control tokens used by inputs,
|
|
16
|
+
The `.sm` and `.lg` modifiers set the shared control tokens used by inputs,
|
|
17
|
+
selects, textareas, buttons, and inline choices. Put the modifier on the control
|
|
18
|
+
itself, or on a `.field` wrapper when the whole field should share that size.
|
|
17
19
|
|
|
18
|
-
Inline choices — `.check`, `.radio`, `.switch` —
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
without its label would only break the alignment. Scale them by scaling their
|
|
23
|
-
type instead.
|
|
20
|
+
Inline choices — `.check`, `.radio`, `.switch` — sit on a line of text, so their
|
|
21
|
+
geometry follows the local control typography through `--choice-control-size`.
|
|
22
|
+
Apply `.sm` or `.lg` to the `.choice` wrapper so its label and control scale
|
|
23
|
+
together. Density contexts leave both unchanged.
|
|
24
24
|
|
|
25
25
|
```html demo
|
|
26
26
|
<form class="stack" novalidate>
|
|
@@ -276,12 +276,11 @@ type instead.
|
|
|
276
276
|
<button class="btn outline lg" type="button">Large</button>
|
|
277
277
|
</div>
|
|
278
278
|
|
|
279
|
-
<!--
|
|
280
|
-
identical even though the modifiers differ. -->
|
|
279
|
+
<!-- Size belongs on the choice wrapper so label and control move together. -->
|
|
281
280
|
<div class="grid items-start">
|
|
282
281
|
<label class="choice sm">
|
|
283
282
|
<input class="check" type="checkbox" checked />
|
|
284
|
-
<span
|
|
283
|
+
<span>Small checkbox</span>
|
|
285
284
|
</label>
|
|
286
285
|
<label class="choice">
|
|
287
286
|
<input class="radio" type="radio" name="demo-radio-size" checked />
|
|
@@ -289,7 +288,7 @@ type instead.
|
|
|
289
288
|
</label>
|
|
290
289
|
<label class="choice lg">
|
|
291
290
|
<input class="switch" type="checkbox" role="switch" checked />
|
|
292
|
-
<span
|
|
291
|
+
<span>Large switch</span>
|
|
293
292
|
</label>
|
|
294
293
|
<label class="choice">
|
|
295
294
|
<input class="check" type="checkbox" id="indet-size" />
|
|
@@ -297,8 +296,7 @@ type instead.
|
|
|
297
296
|
</label>
|
|
298
297
|
</div>
|
|
299
298
|
|
|
300
|
-
<!--
|
|
301
|
-
together, so the alignment survives. -->
|
|
299
|
+
<!-- The underlying typography hook remains available for custom scales. -->
|
|
302
300
|
<div class="grid items-start" style="--control-font-size: var(--font-size-sm);">
|
|
303
301
|
<label class="choice">
|
|
304
302
|
<input class="check" type="checkbox" checked />
|
|
@@ -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 compact">
|
|
61
61
|
<label class="choice">
|
|
62
62
|
<input class="radio" type="radio" name="theme" value="light" checked />
|
|
63
63
|
<span>Light</span>
|
|
@@ -141,7 +141,7 @@ Scroll the page to see the detached footer stay reachable.
|
|
|
141
141
|
|
|
142
142
|
<fieldset class="field-group">
|
|
143
143
|
<legend class="field-label">Theme</legend>
|
|
144
|
-
<div class="stack
|
|
144
|
+
<div class="stack compact">
|
|
145
145
|
<label class="choice">
|
|
146
146
|
<input class="radio" type="radio" name="theme-detached" value="light" checked />
|
|
147
147
|
<span>Light</span>
|
|
@@ -62,7 +62,7 @@ naturally or via an explicit line break — still sit cleanly.
|
|
|
62
62
|
|
|
63
63
|
A switch is sized off the control typography, not field density. Its track
|
|
64
64
|
comes from `--choice-control-size` — the same token a checkbox and a radio use
|
|
65
|
-
— so the three sit on a line of text at matching weight.
|
|
65
|
+
— so the three sit on a line of text at matching weight. Density contexts change
|
|
66
66
|
`--control-size`, which is field height, and deliberately leave inline choices
|
|
67
67
|
alone.
|
|
68
68
|
|
|
@@ -29,12 +29,15 @@ There is no `.btn-primary` or `.badge-success`. Intents are generic and reusable
|
|
|
29
29
|
|
|
30
30
|
Components fall back to their own default intent when no intent class is set. Buttons, badges, and alerts use the neutral palette (`--neutral` / `--neutral-fg`) by default.
|
|
31
31
|
|
|
32
|
-
Custom intents need no framework support — define `--intent` and `--intent-fg`
|
|
32
|
+
Custom intents need no framework support — define `--intent` and `--intent-fg`
|
|
33
|
+
(and `--intent-soft-fg` when the custom role needs its own soft foreground) on a
|
|
34
|
+
class:
|
|
33
35
|
|
|
34
36
|
```css
|
|
35
37
|
.tertiary {
|
|
36
38
|
--intent: var(--tertiary);
|
|
37
39
|
--intent-fg: var(--tertiary-fg);
|
|
40
|
+
--intent-soft-fg: var(--tertiary-soft-fg);
|
|
38
41
|
}
|
|
39
42
|
```
|
|
40
43
|
|
|
@@ -52,34 +55,60 @@ Custom intents need no framework support — define `--intent` and `--intent-fg`
|
|
|
52
55
|
`.ghost` and `.link` are button-only variants.
|
|
53
56
|
|
|
54
57
|
```html demo
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
+
<div class="stack">
|
|
59
|
+
<div class="cluster">
|
|
60
|
+
<button class="btn soft primary">Soft primary</button>
|
|
61
|
+
<span class="badge outline success">Outline success</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="alert solid warning">Solid warning</div>
|
|
64
|
+
</div>
|
|
58
65
|
```
|
|
59
66
|
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
`.sm` and `.lg`
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
## Size and density
|
|
68
|
+
|
|
69
|
+
`.sm` and `.lg` mean one size role below or above a component's natural size.
|
|
70
|
+
Size names are shared roles, not shared measurements. Each participating
|
|
71
|
+
component family maps them to its own optical scale: badges use `xs / sm / md`,
|
|
72
|
+
while controls use `sm / md / lg`. Components without an explicit mapping do
|
|
73
|
+
not change.
|
|
74
|
+
|
|
75
|
+
A size role is applied to the component that participates in the scale and
|
|
76
|
+
never styles arbitrary descendants. It propagates through token inheritance,
|
|
77
|
+
though: components in the shared control scale set local `--control-size` and
|
|
78
|
+
`--control-font-size`, and descendants consume them by their own rules. That is
|
|
79
|
+
how `.field sm`, `.input-icon sm`, `.join sm`, and `.flyout sm` size the
|
|
80
|
+
controls inside them, while a bare `.sm` on a plain element does nothing.
|
|
81
|
+
|
|
82
|
+
`.compact` and `.spacious` establish inherited density tokens. Density arranges
|
|
83
|
+
how tightly UI sits — spacing and component geometry — while typography and
|
|
84
|
+
icon size stay unchanged. Participation is opt-in, and a component only joins
|
|
85
|
+
where density has an effect independent from its optical size: if the result is
|
|
86
|
+
merely that the component looks smaller or larger, that is size, not density.
|
|
87
|
+
|
|
88
|
+
Density participation takes three levels:
|
|
89
|
+
|
|
90
|
+
- **Geometry + rhythm** — controls and their wrappers (`.field`, `.join`,
|
|
91
|
+
`.flyout`, …). Height, padding, and gap respond; typography is untouched.
|
|
92
|
+
- **Rhythm only** — content rows and shells such as `.list`. Spacing tightens
|
|
93
|
+
(`--list-item-gap: var(--gap)`), but the structural size stays stable.
|
|
94
|
+
- **No density effect** — components whose geometry is intrinsic to their
|
|
95
|
+
content (`.badge`, `.avatar`, `.spinner`, `.rating`, `.key`, prose, inline
|
|
96
|
+
`.choice`/`.switch`). Their optical size belongs to `sm`/`lg` or local hooks.
|
|
67
97
|
|
|
68
98
|
As inherited contexts, one class tightens or loosens the whole subtree:
|
|
69
99
|
|
|
70
100
|
```html demo
|
|
71
|
-
<div class="
|
|
101
|
+
<div class="compact stack">
|
|
72
102
|
<button class="btn">Compact button</button>
|
|
73
103
|
<input class="input">
|
|
74
104
|
<span class="badge">Compact badge</span>
|
|
75
105
|
</div>
|
|
76
106
|
```
|
|
77
107
|
|
|
78
|
-
Application chrome can use the same scope
|
|
79
|
-
needed:
|
|
108
|
+
Application chrome can use the same scope:
|
|
80
109
|
|
|
81
110
|
```html demo
|
|
82
|
-
<header class="navbar
|
|
111
|
+
<header class="navbar compact">
|
|
83
112
|
<a class="navbar-brand" href="/">Workspace</a>
|
|
84
113
|
<nav class="cluster" aria-label="Workspace actions">
|
|
85
114
|
<button class="btn ghost">Search</button>
|
|
@@ -88,22 +117,24 @@ needed:
|
|
|
88
117
|
</header>
|
|
89
118
|
```
|
|
90
119
|
|
|
91
|
-
|
|
120
|
+
Component geometry remains local and wins over inherited density when both
|
|
121
|
+
affect the same dimension:
|
|
92
122
|
|
|
93
123
|
```html demo
|
|
94
|
-
<
|
|
95
|
-
<
|
|
124
|
+
<div class="compact">
|
|
125
|
+
<button class="btn lg">Large button in a compact context</button>
|
|
126
|
+
</div>
|
|
96
127
|
```
|
|
97
128
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
by density scopes.
|
|
129
|
+
The density tokens are `--gap`, `--density-space`, and `--control-size`.
|
|
130
|
+
Components consume the dimensions that make sense for them and opt out of the
|
|
131
|
+
rest. The absolute `--space-*` scale is not rebound by density scopes.
|
|
132
|
+
Components with bespoke geometry (`.badge`, `.avatar`,
|
|
133
|
+
`.spinner`, `.rating`) own their local `.sm`/`.lg` mappings.
|
|
104
134
|
|
|
105
|
-
|
|
106
|
-
|
|
135
|
+
`.compact` also remains a meaningful local modifier for `.card` and `.table`.
|
|
136
|
+
On those components it tightens their own padding and establishes the compact
|
|
137
|
+
density context for descendants.
|
|
107
138
|
|
|
108
139
|
## Child semantics
|
|
109
140
|
|
|
@@ -20,7 +20,7 @@ Set `--cluster-align`.
|
|
|
20
20
|
|
|
21
21
|
### Change spacing without changing control size
|
|
22
22
|
|
|
23
|
-
Set `--gap`
|
|
23
|
+
Set `--gap` on the layout primitive to retune it and everything nested. To change only that element's rhythm without reaching nested primitives, set the `gap` property instead.
|
|
24
24
|
|
|
25
25
|
→ Layout
|
|
26
26
|
|
|
@@ -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, set `--choice-control-size`
|
|
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.
|
|
60
60
|
|
|
61
61
|
→ Forms · Switch
|
|
62
62
|
|
|
@@ -76,6 +76,18 @@ Focus styling is outline-first. `--focus` names the theme's focus color, and a r
|
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
### Palette coherence
|
|
80
|
+
|
|
81
|
+
A coherent palette treats surfaces, text, borders, intents, and shadows as related roles rather than isolated colors.
|
|
82
|
+
|
|
83
|
+
Coherence does not mean that every token must be chromatic. Pure or near-pure whites and blacks remain legitimate where their role calls for them; what matters is that the surrounding neutral ramp and derived treatments belong to the same visual family.
|
|
84
|
+
|
|
85
|
+
Neutral roles may lean toward the palette's ambient hue — by default the primary's — while lightness and chroma stay curated per role. Chroma runs strongest through the ramp's mid-lightness rungs, often around `--border` and `--text-muted` depending on the color scheme, and tapers toward both ends. A palette whose accent is deliberately warmer or cooler than its paper picks an ambient hue of its own.
|
|
86
|
+
|
|
87
|
+
Semantic intents are curated colors, not mechanical derivatives of `--primary`. Danger, warning, and success stay recognizable for their function, but they are chosen as members of the theme palette rather than copied independently from a generic scale. Neither intents nor neutrals are derived from `--primary` at runtime: the relationship is a design decision recorded in the theme's own values.
|
|
88
|
+
|
|
89
|
+
Color should reinforce structure and meaning, not carry them alone. Information hierarchy and semantic states should remain understandable when hue is removed or unavailable. Desaturating a composition is a useful review heuristic, not a conformance test.
|
|
90
|
+
|
|
79
91
|
### Default theme philosophy
|
|
80
92
|
|
|
81
93
|
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.
|
|
@@ -91,8 +103,62 @@ Soft surfaces (`.btn.soft`, `.badge.soft`, and the default `.alert`) are generat
|
|
|
91
103
|
}
|
|
92
104
|
```
|
|
93
105
|
|
|
106
|
+
`--soft-border-mix` is the same lever for the rim. The `65%` default keeps a
|
|
107
|
+
visibly tinted border around every soft surface; pointing it at `--soft-bg-mix`
|
|
108
|
+
resolves the rim to the fill, and soft treatments become a tint carrying text
|
|
109
|
+
with no contour — the flatter language of a modern component set, from one
|
|
110
|
+
declaration:
|
|
111
|
+
|
|
112
|
+
```css
|
|
113
|
+
[data-theme="flat"] {
|
|
114
|
+
--soft-border-mix: var(--soft-bg-mix);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
It reaches the default `.badge` and `.alert` plus every explicit `.soft`
|
|
119
|
+
(`.btn`, `.card`, `.chat`, `.navbar`, `.app-nav`), and nothing else: a component
|
|
120
|
+
on its own surface keeps its `--border` rim. The dial is also
|
|
121
|
+
rendering-only — the baseline path below `color-mix()` keeps `var(--border)`, so
|
|
122
|
+
borderless soft is a look, not a contract the Degraded tier upholds.
|
|
123
|
+
|
|
94
124
|
`--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.
|
|
95
125
|
|
|
126
|
+
Soft ink can also be corrected per role. The intent classes relay a per-role
|
|
127
|
+
hook, and the soft recipe reads it before falling back to the global mix:
|
|
128
|
+
|
|
129
|
+
- `--primary-soft-fg`, `--secondary-soft-fg`, `--success-soft-fg`,
|
|
130
|
+
`--warning-soft-fg`, `--danger-soft-fg`, `--neutral-soft-fg` — **theme hooks
|
|
131
|
+
per role**; the ones whose palette needs a softer ink than the others.
|
|
132
|
+
- `--intent-soft-fg` — the **current relay** (framework plumbing), set by each
|
|
133
|
+
intent class to its own `--*-soft-fg`.
|
|
134
|
+
- `--soft-fg-mix` — the **global fallback derivation**, unchanged.
|
|
135
|
+
|
|
136
|
+
**Soft foregrounds are calibrated against the darkest or closest interactive
|
|
137
|
+
surface they are expected to appear on.** For interactive soft treatments the
|
|
138
|
+
fill moves toward the intent on hover (`--soft-hover-alpha`), so the binding
|
|
139
|
+
surface is the *hovered* one and the resting fill simply carries more margin;
|
|
140
|
+
the default palette ships its own `--primary`/`--secondary`/… `-soft-fg` hooks
|
|
141
|
+
to keep that pair ≥ 4.5:1 in both color schemes, enforced by
|
|
142
|
+
`tests/browser/soft-recipe.test.js`. The hard line stops at the default theme:
|
|
143
|
+
presets and custom `[data-theme]` islands keep the `--soft-fg-mix` derivation
|
|
144
|
+
unless they declare the hooks themselves (the framework resets them outside
|
|
145
|
+
the default boundary). A theme that overrides the soft palette is responsible
|
|
146
|
+
for preserving sufficient text contrast; `bun run report:theme-contrast`
|
|
147
|
+
reports every preset's resting and hovered pairs without failing the build.
|
|
148
|
+
|
|
149
|
+
A theme whose primary should read more muted than its statuses sets only
|
|
150
|
+
`--primary-soft-fg`, and every soft `.primary` foreground follows it. Without a
|
|
151
|
+
`--*-soft-fg` the recipe resolves byte-identically to the old `--soft-fg-mix`
|
|
152
|
+
derivation. A custom intent class provides the relay directly:
|
|
153
|
+
|
|
154
|
+
```css
|
|
155
|
+
.tertiary {
|
|
156
|
+
--intent: var(--tertiary);
|
|
157
|
+
--intent-fg: var(--tertiary-fg);
|
|
158
|
+
--intent-soft-fg: var(--tertiary-soft-fg);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
96
162
|
**When an intent is tinted against a theme-controlled surface or border, the mix interpolates in `oklab`.** That covers the soft recipe, the checked `.choice-card` tint, and the `.overline.pill` border.
|
|
97
163
|
|
|
98
164
|
Not because rectangular interpolation is more faithful to the intent hue — across the shipped presets it is a few degrees *less* faithful. A browser treats hue as powerless below a small chroma epsilon, so for the low-chroma surfaces and borders the presets actually use, a polar mix snaps cleanly onto the intent and wins.
|
|
@@ -147,8 +213,10 @@ Typography tokens cover the document baseline and common component needs.
|
|
|
147
213
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
148
214
|
|
|
149
215
|
--line-height: 1.5;
|
|
150
|
-
--font-weight: 400;
|
|
151
|
-
--font-weight-
|
|
216
|
+
--font-weight-normal: 400;
|
|
217
|
+
--font-weight-medium: 500;
|
|
218
|
+
--font-weight-semibold: 600;
|
|
219
|
+
--font-weight-bold: 700;
|
|
152
220
|
|
|
153
221
|
--font-size-xs: 0.8125rem;
|
|
154
222
|
--font-size-sm: 0.875rem;
|
|
@@ -161,7 +229,9 @@ Components can use local font variables for their own mapping, but should inheri
|
|
|
161
229
|
|
|
162
230
|
### Controls
|
|
163
231
|
|
|
164
|
-
Controls share
|
|
232
|
+
Controls share geometry so buttons, inputs, selects, and compact app UI align.
|
|
233
|
+
Local `.sm`/`.lg` sizes map `--control-size`, `--control-font-size`, and
|
|
234
|
+
`--control-pad-x`.
|
|
165
235
|
|
|
166
236
|
```css
|
|
167
237
|
:root {
|
|
@@ -169,41 +239,53 @@ Controls share sizing so buttons, inputs, selects, and compact app UI always ali
|
|
|
169
239
|
--control-size-md: 2.375rem; /* 38px → default */
|
|
170
240
|
--control-size-lg: 2.75rem; /* 44px → .lg */
|
|
171
241
|
--control-size: var(--control-size-md);
|
|
172
|
-
--control-pad-x:
|
|
242
|
+
--control-pad-x: var(--font-size-md);
|
|
173
243
|
--control-font-size: var(--font-size-md);
|
|
174
244
|
--disabled-opacity: 0.65;
|
|
175
245
|
}
|
|
176
246
|
```
|
|
177
247
|
|
|
248
|
+
`--control-pad-x` is a resolved geometry length, stated in the typographic
|
|
249
|
+
tokens rather than an `em`. Every part of one control must see the same
|
|
250
|
+
horizontal distance; an `em` would change value on a descendant with its own
|
|
251
|
+
font size, which is exactly what `--input-icon-size` does to the icon glyph. It
|
|
252
|
+
mirrors `--control-font-size` and is rebound beside it at the `.sm`/`.lg`
|
|
253
|
+
boundaries.
|
|
254
|
+
|
|
178
255
|
- `.lg` — 44px control height
|
|
179
256
|
- default — 38px control height
|
|
180
257
|
- `.sm` — 32px control height
|
|
181
258
|
|
|
182
|
-
The font size
|
|
259
|
+
The default font size is `--font-size-md`; `.sm` maps it to `--font-size-sm` and
|
|
260
|
+
`.lg` to `--font-size-lg`. Density contexts change `--control-size` without
|
|
261
|
+
changing the font size.
|
|
183
262
|
|
|
184
263
|
Disabled controls and disabled-prone components read `--disabled-opacity` for their dimmed state — buttons, tabs, menu items, badges, avatars, choice cards, native inputs, and range sliders.
|
|
185
264
|
|
|
186
265
|
### Density
|
|
187
266
|
|
|
188
|
-
Density
|
|
267
|
+
Density contexts swap shared spacing and geometry:
|
|
189
268
|
|
|
190
269
|
```css
|
|
191
|
-
.
|
|
270
|
+
.compact {
|
|
192
271
|
--gap: var(--space-20);
|
|
193
272
|
--density-space: var(--space-20);
|
|
194
273
|
--control-size: var(--control-size-sm);
|
|
195
|
-
--density-compact-size: 1.25rem;
|
|
196
274
|
}
|
|
197
275
|
|
|
198
|
-
.
|
|
276
|
+
.spacious {
|
|
199
277
|
--gap: var(--space-50);
|
|
200
278
|
--density-space: var(--space-50);
|
|
201
279
|
--control-size: var(--control-size-lg);
|
|
202
|
-
--density-compact-size: 1.875rem;
|
|
203
280
|
}
|
|
204
281
|
```
|
|
205
282
|
|
|
206
|
-
Density covers spacing and geometry only — it never touches typography or icon
|
|
283
|
+
Density covers spacing and geometry only — it never touches typography or icon
|
|
284
|
+
size. Participation is opt-in and selective: controls consume `--control-size`
|
|
285
|
+
for geometry; spacing helpers and rhythms consume `--density-space` and `--gap`;
|
|
286
|
+
components whose size is intrinsic (badge, avatar, spinner, rating, key, prose,
|
|
287
|
+
inline choice/switch) keep their own scales. A local component size overrides
|
|
288
|
+
inherited density for the dimensions they share.
|
|
207
289
|
|
|
208
290
|
### Elevation
|
|
209
291
|
|
|
@@ -258,7 +340,7 @@ A single icon token can serve both `background-image` (native select) and `mask-
|
|
|
258
340
|
A few cross-component helpers:
|
|
259
341
|
|
|
260
342
|
- `--indicator-offset` / `--indicator-ring` — positioning and contrast ring for a status dot attached to `.avatar > .badge:empty`.
|
|
261
|
-
- `--backdrop-color` / `--backdrop-opacity` / `--backdrop-fill` — scrim color and opacity shared by `dialog.modal::backdrop
|
|
343
|
+
- `--backdrop-color` / `--backdrop-opacity` / `--backdrop-fill` — scrim color and opacity shared by `dialog.modal::backdrop` and `dialog.drawer::backdrop`. Override `--backdrop-opacity` per theme for a denser or lighter scrim.
|
|
262
344
|
|
|
263
345
|
```css
|
|
264
346
|
:root {
|
|
@@ -274,18 +356,30 @@ Under `forced-colors: active`, `--backdrop-fill` maps to `Canvas`.
|
|
|
274
356
|
|
|
275
357
|
### Layering
|
|
276
358
|
|
|
277
|
-
|
|
359
|
+
Two systems order overlays, and only one of them uses numbers.
|
|
360
|
+
|
|
361
|
+
**The platform top layer** holds native `<dialog>` (modal, drawer) and every element currently showing as a popover — which, since the runtime drives flyouts, context menus and tooltips with `popover="manual"`, is all of Actual's anchored overlays. Top-layer ordering does not use `z-index`. Such an element always paints above the document, and its order among the others is its top-layer promotion order — the most recently promoted on top. A `z-index` declaration on it still computes; it just does not decide where it lands.
|
|
362
|
+
|
|
363
|
+
**The document layer** is everything else, and the scale below is what orders it. Nothing in it can reach the other system: `z-index: 999999` on a document-layer element still paints under any open dialog or popover.
|
|
278
364
|
|
|
279
|
-
| Token
|
|
280
|
-
|
|
|
281
|
-
| `--z-sticky`
|
|
282
|
-
| `--z-menu`
|
|
283
|
-
| `--z-
|
|
284
|
-
| `--z-status` | 60 | `status-bar` |
|
|
365
|
+
| Token | Value | Used by |
|
|
366
|
+
| ------------ | ----- | ------------------------------- |
|
|
367
|
+
| `--z-sticky` | 10 | `form-actions.sticky`, `topbar` |
|
|
368
|
+
| `--z-menu` | 20 | `fab`, static `.flyout` |
|
|
369
|
+
| `--z-status` | 60 | `status-bar` |
|
|
285
370
|
|
|
286
|
-
`.
|
|
371
|
+
`.flyout` keeps the `--z-menu` declaration for a panel the runtime does not
|
|
372
|
+
manage — one the platform has not promoted still stacks by number.
|
|
287
373
|
|
|
288
|
-
|
|
374
|
+
`status-bar` is at the top of the document layer: above sticky bars and the FAB,
|
|
375
|
+
and below any open dialog, menu or tooltip. That is the boundary between the
|
|
376
|
+
two systems rather than a gap in the scale. Status messages are transient,
|
|
377
|
+
non-critical feedback and their real contract is the live-region announcement
|
|
378
|
+
— which is why `status.js` moves the singleton into an open modal dialog's
|
|
379
|
+
subtree, since a modal inerts the rest of the document and promotion would not
|
|
380
|
+
undo that.
|
|
381
|
+
|
|
382
|
+
Stacking-context traps: these apply to overlays painted in the page, not to promoted ones. An element that is a DOM descendant of a container with `transform`, `filter`, `contain`, `isolation`, or `will-change` is confined to that ancestor's stacking context, and its z-index token only orders it against siblings within that context. Promotion escapes all of it — but do not move the markup to chase the effect: a panel or tooltip used from inside a modal dialog must be authored *inside* that dialog, because the top layer does not lift an element out of a modal's inertness.
|
|
289
383
|
|
|
290
384
|
Scrollbar gutter: modal open/close can shift page layout when the classic scrollbar disappears. When a measured scrollbar is present, `modal.css` applies `scrollbar-gutter: stable` to the viewport so the page doesn't jump.
|
|
291
385
|
|
|
@@ -342,6 +436,7 @@ Rules:
|
|
|
342
436
|
- Prefix shared variant plumbing with `--ui-*`.
|
|
343
437
|
- The shared relays `--ui-*`, `--intent*`, and `--density-*` are framework plumbing: they are classified once at framework level, not restated in every component file.
|
|
344
438
|
- Keep component-specific variants in component code when they do not generalize.
|
|
439
|
+
- `flush` is a shared modifier: it removes the component's outer chrome so the component sits directly inside an existing surface (`.accordion.flush`). Reuse the term one component at a time when the same integration need appears elsewhere; `separated` stays component-local until a second real consumer exists.
|
|
345
440
|
- Do not require users to override internal tokens for ordinary theming.
|
|
346
441
|
- Promote an internal token to public only when there is a repeated, reasonable customization need.
|
|
347
442
|
- A custom property used only in a fallback position (`var(--x, default)`) and never declared is an unset extension point and must carry a classification — public, framework plumbing, or internal. `check:css-api` fails on any unclassified fallback-only property; it does not force an artificial declaration to satisfy the audit.
|
|
@@ -352,6 +447,8 @@ Themes override tokens, not selectors. The themes in `src/css/themes/` are repos
|
|
|
352
447
|
|
|
353
448
|
A minimal recolor theme overrides the intent pairs, surfaces, text colors, border, focus, and hover overlay. In browsers with `color-mix()` support, the core derives `--focus-ring` from the island's `--focus`; override the ring only for a deliberate visual treatment or when a matching pre-`color-mix()` fallback is required.
|
|
354
449
|
|
|
450
|
+
When overriding an intent color, review its paired `--*-fg` and `--*-soft-fg`. The generic soft fallback (`--soft-fg-mix` derivation, above) may already be sufficient.
|
|
451
|
+
|
|
355
452
|
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.
|
|
356
453
|
|
|
357
454
|
A full theme can override:
|