actual-css 0.9.2 → 0.10.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 (101) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +65 -44
  3. package/components.json +33 -13
  4. package/dist/actual.full.css +585 -499
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/app-ui.md +1 -1
  8. package/docs/design-notes/column-layout.md +103 -520
  9. package/docs/design-notes/forced-colors.md +12 -1
  10. package/docs/design-notes/naming.md +1 -1
  11. package/docs/design-notes/surface-reparenting.md +8 -9
  12. package/docs/navigation.json +2 -0
  13. package/docs/pages/components/accordion.md +55 -1
  14. package/docs/pages/components/alert.md +9 -15
  15. package/docs/pages/components/badge.md +6 -7
  16. package/docs/pages/components/bleed.md +42 -0
  17. package/docs/pages/components/button.md +30 -4
  18. package/docs/pages/components/card.md +21 -86
  19. package/docs/pages/components/close.md +48 -0
  20. package/docs/pages/components/data-list.md +26 -1
  21. package/docs/pages/components/dialog.md +16 -16
  22. package/docs/pages/components/drawer.md +9 -9
  23. package/docs/pages/components/navbar.md +5 -5
  24. package/docs/pages/components/spinner.md +3 -3
  25. package/docs/pages/enhancements/overview.md +5 -0
  26. package/docs/pages/enhancements/tooltip.md +3 -1
  27. package/docs/pages/examples/overview.md +2 -1
  28. package/docs/pages/forms/choice-cards.md +39 -0
  29. package/docs/pages/forms/combobox.md +23 -11
  30. package/docs/pages/forms/joined-controls.md +2 -5
  31. package/docs/pages/forms/overview.md +6 -0
  32. package/docs/pages/foundations/theming.md +64 -18
  33. package/docs/pages/foundations/tokens.md +28 -10
  34. package/docs/pages/guides/introduction.md +15 -23
  35. package/docs/pages/guides/modular-import.md +53 -6
  36. package/docs/pages/guides/progressive-enhancement.md +6 -3
  37. package/docs/pages/guides/tailwind.md +17 -5
  38. package/docs/pages/layout/column-layout.md +92 -54
  39. package/docs/pages/layout/media.md +1 -1
  40. package/docs/pages/layout/overview.md +1 -1
  41. package/docs/pages/utilities/overview.md +1 -1
  42. package/docs/pages/utilities/sizing-wrapping.md +5 -3
  43. package/llms.txt +24 -4
  44. package/package.json +6 -6
  45. package/reserved-classes.json +5 -30
  46. package/skills/actual-css/PROJECT.md.example +65 -0
  47. package/skills/actual-css/README.md +64 -0
  48. package/skills/actual-css/SKILL.md +234 -0
  49. package/skills/actual-css/references/accessibility.md +44 -0
  50. package/skills/actual-css/references/api-and-vocabulary.md +94 -0
  51. package/skills/actual-css/references/enhancements.md +59 -0
  52. package/skills/actual-css/references/forms.md +49 -0
  53. package/skills/actual-css/references/layout-and-theming.md +82 -0
  54. package/skills/actual-css/references/review-checklist.md +51 -0
  55. package/skills/actual-css/scripts/inspect-actual-css.mjs +171 -0
  56. package/src/css/components/accordion.css +44 -7
  57. package/src/css/components/alert.css +23 -43
  58. package/src/css/components/app-nav.css +9 -0
  59. package/src/css/components/badge.css +11 -39
  60. package/src/css/components/bleed.css +79 -0
  61. package/src/css/components/button.css +6 -17
  62. package/src/css/components/card.css +7 -40
  63. package/src/css/components/close.css +91 -0
  64. package/src/css/components/data-list.css +18 -0
  65. package/src/css/components/drawer.css +15 -56
  66. package/src/css/components/fab.css +10 -0
  67. package/src/css/components/flyout.css +1 -0
  68. package/src/css/components/index.css +2 -0
  69. package/src/css/components/join.css +36 -20
  70. package/src/css/components/menu.css +12 -0
  71. package/src/css/components/modal.css +10 -57
  72. package/src/css/components/navbar.css +27 -0
  73. package/src/css/core/focus.css +9 -3
  74. package/src/css/core/theme.css +25 -43
  75. package/src/css/core/tokens.css +1 -1
  76. package/src/css/core/variants.css +3 -33
  77. package/src/css/forms/choice-card.css +74 -4
  78. package/src/css/forms/choice.css +2 -3
  79. package/src/css/forms/control.css +15 -8
  80. package/src/css/forms/custom-select.css +4 -4
  81. package/src/css/forms/native.css +4 -4
  82. package/src/css/forms/range.css +23 -8
  83. package/src/css/forms/switch.css +2 -3
  84. package/src/css/layout/app-layout.css +1 -1
  85. package/src/css/layout/column-layout.css +26 -111
  86. package/src/css/utilities/extra.css +7 -6
  87. package/src/js/dialog.js +95 -24
  88. package/src/js/enhance.js +7 -3
  89. package/src/js/escape.js +1 -0
  90. package/src/js/flyout.js +10 -1
  91. package/src/js/focus-group.js +2 -1
  92. package/src/js/keys.js +14 -0
  93. package/src/js/menu.js +2 -1
  94. package/src/js/status.js +19 -0
  95. package/src/js/surface.js +12 -8
  96. package/src/js/tab.js +2 -1
  97. package/src/js/tooltip.js +18 -1
  98. package/src/js/validation.js +5 -6
  99. package/dist/actual.css +0 -876
  100. package/dist/actual.js +0 -1
  101. package/dist/actual.min.css +0 -1
@@ -6,16 +6,17 @@
6
6
 
7
7
  ## Class reference
8
8
 
9
- | Class | Kind | Description |
10
- | ---------------------- | --------- | ---------------------------------------------- |
11
- | `.modal` | Component | Centered surface on the native `<dialog>`. |
12
- | `.scrollable` | Variant | Header and footer stay while the body scrolls. |
13
- | `.dialog-confirmation` | Variant | Message above a full-width action band. |
14
- | `.dialog-icon` | Component | Circular intent-aware icon well. |
15
- | `.dialog-close` | Component | Icon-only close button, at the top end. |
16
-
17
- `.dialog-close` sits outside the content flow, so it does not take part in the
18
- dialog's own layout.
9
+ | Class | Kind | Description |
10
+ | ---------------------- | ----------- | ---------------------------------------------- |
11
+ | `.modal` | Component | Centered surface on the native `<dialog>`. |
12
+ | `.scrollable` | Variant | Header and footer stay while the body scrolls. |
13
+ | `.dialog-confirmation` | Variant | Message above a full-width action band. |
14
+ | `.dialog-icon` | Component | Circular intent-aware icon well. |
15
+ | `.close` | Composition | Icon-only close button, at the top end. |
16
+
17
+ A [`.close`](close.md) placed as a direct child of the dialog or of its form
18
+ sits outside the content flow, so it does not take part in the dialog's own
19
+ layout.
19
20
 
20
21
  ## Usage
21
22
 
@@ -138,7 +139,7 @@ has no action button; the header close button dismisses the dialog.
138
139
  id="details-dialog"
139
140
  closedby="any"
140
141
  data-dialog-dismissible>
141
- <button class="dialog-close"
142
+ <button class="close"
142
143
  type="button"
143
144
  commandfor="details-dialog"
144
145
  command="request-close"
@@ -243,7 +244,7 @@ promoted above it and still unclickable.
243
244
  id="overlay-dialog"
244
245
  closedby="any"
245
246
  data-dialog-dismissible>
246
- <button class="dialog-close"
247
+ <button class="close"
247
248
  type="button"
248
249
  commandfor="overlay-dialog"
249
250
  command="request-close"
@@ -381,7 +382,6 @@ Keep the button a direct child of `dialog.modal` so the scrolling content
381
382
  - `--dialog-viewport-gap` — distance kept between the dialog and the viewport edges.
382
383
  - `--dialog-icon-size` — diameter of the `.dialog-icon` circle.
383
384
  - `--dialog-icon-glyph-size` — size of the glyph centered inside `.dialog-icon`.
384
- - `--control-size` — inline and block size of the `.dialog-close` button. The
385
- header reserves `calc(var(--control-size) + var(--space-30))` on its inline
386
- end so the title never runs under the close.
387
- - `--dialog-close-icon-size` — size of the close glyph.
385
+ - `--close-size` / `--close-icon-size` — size of the corner `.close` and of its
386
+ X. The header reserves the close size plus `--space-30` on its inline end so
387
+ the title never runs under it.
@@ -6,10 +6,10 @@
6
6
 
7
7
  ## Class reference
8
8
 
9
- | Class | Kind | Description |
10
- | --------------- | --------- | ---------------------------------------------------------- |
11
- | `.drawer` | Component | Modal side-sheet built on the native `<dialog>` element. |
12
- | `.drawer-close` | Component | Icon-only close button that sits inside the drawer header. |
9
+ | Class | Kind | Description |
10
+ | --------- | ----------- | -------------------------------------------------------- |
11
+ | `.drawer` | Component | Modal side-sheet built on the native `<dialog>` element. |
12
+ | `.close` | Composition | Icon-only close button, at the top end. |
13
13
 
14
14
  ## Usage
15
15
 
@@ -56,7 +56,7 @@ your intended target `autofocus`; otherwise the body claims initial focus.
56
56
  <strong>Menu</strong>
57
57
 
58
58
  <form method="dialog">
59
- <button class="drawer-close" type="submit" aria-label="Close navigation"></button>
59
+ <button class="close" type="submit" aria-label="Close navigation"></button>
60
60
  </form>
61
61
  </header>
62
62
 
@@ -96,7 +96,7 @@ actions and the close button can close the drawer.
96
96
  <form method="dialog">
97
97
  <header>
98
98
  <strong>Settings</strong>
99
- <button class="drawer-close" type="submit" aria-label="Close settings"></button>
99
+ <button class="close" type="submit" aria-label="Close settings"></button>
100
100
  </header>
101
101
 
102
102
  <div class="stack">
@@ -123,6 +123,6 @@ useful for drawers that should not be dismissed accidentally.
123
123
 
124
124
  - `--drawer-size` — panel width.
125
125
  - `--drawer-pad` — panel padding.
126
- - `--control-size` — size of the `.drawer-close` button. The drawer header
127
- reserves `calc(var(--control-size) + var(--space-30))` on its inline end so
128
- the title never runs under the close.
126
+ - `--close-size` / `--close-icon-size` — size of the corner [`.close`](close.md)
127
+ and of its X. The header reserves the close size plus `--space-30` on its
128
+ inline end so the title never runs under it.
@@ -51,7 +51,7 @@ icons. See [App Navigation](app-navigation.md).
51
51
 
52
52
  For a vertical sidebar nav, use `.nav-list` inside the drawer or sidebar.
53
53
  Links inside `.nav-list` stretch to the available inline size, providing a
54
- full-row hit target for vertical navigation. The [Workspaces example](../examples/overview.md)
54
+ full-row hit target for vertical navigation. The [Workspaces example](../examples/overview.md#page-shapes)
55
55
  shows the sectioned-sidebar composition: grouped `.nav-list` blocks with
56
56
  section titles, a pinned account footer, and the native current state.
57
57
 
@@ -94,15 +94,15 @@ accessible name.
94
94
  <header>
95
95
  <strong>Menu</strong>
96
96
  <form method="dialog">
97
- <button class="drawer-close" type="submit" aria-label="Close navigation"></button>
97
+ <button class="close" type="submit" aria-label="Close navigation"></button>
98
98
  </form>
99
99
  </header>
100
100
 
101
101
  <nav>
102
102
  <ul class="nav-list stack">
103
- <li><a href="#" aria-current="page">Home</a></li>
104
- <li><a href="#">Docs</a></li>
105
- <li><a href="#">Components</a></li>
103
+ <li><a class="nav-link" href="#" aria-current="page">Home</a></li>
104
+ <li><a class="nav-link" href="#">Docs</a></li>
105
+ <li><a class="nav-link" href="#">Components</a></li>
106
106
  </ul>
107
107
  </nav>
108
108
  </dialog>
@@ -74,7 +74,7 @@ separate `.busy` class. The spinner is shown as a centered overlay over a faded
74
74
  surface; buttons keep their spinners inline instead (see Button).
75
75
 
76
76
  - Set `aria-busy="true"` on the container and add `<span class="spinner" aria-hidden="true"></span>` as its last direct child.
77
- - The overlay uses `--busy-overlay-bg`; an inverted surface (`.inverted`) already composes with it.
77
+ - The overlay uses `--busy-overlay-bg`; a card derives it from its own surface.
78
78
  - Decorative spinners stay `aria-hidden="true"`; announce progress on the region with `role="status"` and a label when needed.
79
79
 
80
80
  ```html demo
@@ -89,5 +89,5 @@ surface; buttons keep their spinners inline instead (see Button).
89
89
 
90
90
  - `--spinner-track` — the ring color.
91
91
  - `--spinner-value` — the moving gap; swap the two for an inverted spinner.
92
- - `--busy-overlay-bg` — background of the busy overlay. An inverted surface
93
- (`.inverted`) overrides it to blend with its own surface.
92
+ - `--busy-overlay-bg` — background of the busy overlay. Set it on a surface you
93
+ paint yourself so the overlay blends with that surface.
@@ -53,6 +53,11 @@ Enhancer modules self-register when imported. They do not require init calls and
53
53
  are safe to import during server-side rendering: outside a browser, registration
54
54
  is a no-op until the module is loaded again with a DOM.
55
55
 
56
+ Load the full bundle **or** the source modules, never both: each evaluation
57
+ owns its registry and document handlers, so a dist bundle mixed with
58
+ `actual-css/js/*` source subpaths (or two dist bundles) registers every
59
+ behavior twice and a flyout opens then immediately closes.
60
+
56
61
  ## Component pages
57
62
 
58
63
  Each interactive component has its own page documenting the exact markup its
@@ -8,6 +8,8 @@ and arrow position are written by JavaScript; the element is shown and hidden
8
8
  by promoting it to the top layer with `popover="manual"`.
9
9
 
10
10
  - Use `data-tooltip` on the trigger. With text (`data-tooltip="Help"`), the tooltip element is generated. Empty (`data-tooltip`) marks an explicit tooltip connected via `aria-describedby`.
11
+ - Generated text is a snapshot at show time: changing `data-tooltip` never rewrites a tooltip that is already shown, and an always-visible generated tooltip keeps its initial text. For content that must update dynamically, use an explicit tooltip. See the [tooltips bench](../examples/overview.md) (`demo/templates/tooltips.html`) for all six cases side by side.
12
+ - Generated text is always plain text (`textContent` only), so a shorthand value is safe for untrusted strings once they are in the DOM. An explicit tooltip is author-owned DOM: its sanitization belongs to whoever produces it. Escape the attribute server-side — a DOM already compromised at render cannot be repaired at read time. There is no `data-tooltip-html`, by design.
11
13
  - Tooltips are supplemental. Do not put required information or interactive controls inside them.
12
14
  - Show on hover and focus. Hide on Escape, blur, or pointer leave.
13
15
  - A trigger scrolled out of view takes its tooltip down and brings it back when it returns; only a dismissal ends the tooltip.
@@ -35,7 +37,7 @@ runtime when Actual should own showing and hiding.
35
37
 
36
38
  **Where the element lives is part of the contract.** An explicit tooltip stays
37
39
  exactly where you wrote it, so it keeps every scope that reaches it by
38
- inheritance — a theme island, a density scope, `.inverted`, your own scoped
40
+ inheritance — a theme island, a density scope, your own scoped
39
41
  custom properties. It also means placement is yours to get right: a tooltip
40
42
  used from inside a modal dialog must be authored inside that dialog, because
41
43
  the top layer does not lift an element out of a modal's inertness.
@@ -47,12 +47,13 @@ One mechanism at a time, with its contract and its pitfalls.
47
47
  Contracts you cannot read off a static screenshot: state, motion, focus, and
48
48
  extension points.
49
49
 
50
- - [Surface contracts](../../demo/templates/surfaces.html) - `.inverted`, cascade precedence, and the intent boundary
50
+ - [Contrast contexts](../../demo/templates/contrast-contexts.html) - the page, `data-theme` islands, a card island, a named theme and an application-painted band side by side, with every intent variant, a full form (combobox and date picker included) and focus line measured live, plus joined fields that need no focus adaptation
51
51
  - [Presence motion](../../demo/templates/motion.html) - status bar, tooltip, flyout, modal and drawer replayed side by side, with easing swaps and a reduced-motion simulation
52
52
  - [Keyboard controls](../../demo/templates/keyboard-controls.html) - hands-on keyboard support matrix and focus test bench
53
53
  - [Adaptive filter surface](../../demo/templates/adaptive-surface.html) - app-level flyout/drawer composition with one synchronized filter state
54
54
  - [Select intents](../../demo/templates/select-intents.html) - extending the enhancer with a project's own `data-enhance` behavior, injected markup included, with no framework change
55
55
  - [Popover transport probe](../../demo/templates/popover-transport.html) - self-scoring bench for the native popover transport go/no-go
56
+ - [Tooltips](../../demo/templates/tooltips.html) - generated text is a snapshot at show time: what updates, what stays, and the explicit-tip way for dynamic content
56
57
 
57
58
  ### Third-party components on Actual tokens
58
59
 
@@ -7,6 +7,7 @@
7
7
  | Class | Kind | Description |
8
8
  | ------------------------- | --------- | --------------------------------------------- |
9
9
  | `.choice-card` | Component | Card-style `<label>` around a native control. |
10
+ | `.chip` | Modifier | Compact pill form: filter chips, tags. |
10
11
  | `.primary` / `.secondary` | Intent | Accent for the selected state. |
11
12
 
12
13
  - Use `.choice-card` on a `<label>` wrapping a native `<input type="radio">` or `<input type="checkbox">`. The input is visually hidden; in browsers without `:has()` it stays visible so selection state never disappears.
@@ -93,11 +94,49 @@ follows through its `em` sizing. Padding is density, so use a `.compact` or
93
94
  `.spacious` context to change it. Intent classes (`.primary`, `.secondary`) are
94
95
  supported on the `.choice-card` element.
95
96
 
97
+ ## Chips
98
+
99
+ Add `.chip` for the compact pill form: filter chips, selectable tags, toggle
100
+ pills, multi-select facets. A radio group gives single-choice chips. A leading
101
+ dot turns into the check in a fixed slot, so checking never moves the label.
102
+ Use `.cluster` for a wrapping row. A chip is sized like a control: `.btn` and
103
+ `.input` height, `--control-pad-x` inline padding, `.sm` / `.lg` scale.
104
+
105
+ ```html demo
106
+ <fieldset class="field-group">
107
+ <legend class="field-label">Fruit</legend>
108
+ <div class="cluster">
109
+ <label class="choice-card chip">
110
+ <input type="checkbox" name="fruit" value="pear" checked />
111
+ Pear
112
+ </label>
113
+ <label class="choice-card chip">
114
+ <input type="checkbox" name="fruit" value="apple" />
115
+ Apple
116
+ </label>
117
+ <label class="choice-card chip">
118
+ <input type="checkbox" name="fruit" value="plum" disabled />
119
+ Plum
120
+ </label>
121
+ </div>
122
+ </fieldset>
123
+ ```
124
+
125
+ The dot is `::before`: a full-chip layer clipped to a `circle()` centred on
126
+ the check slot (`--choice-chip-dot`). A theme can grow it into a selected fill
127
+ by widening the clip, as the `material` theme does.
128
+
96
129
  ## CSS hooks
97
130
 
98
131
  - `--choice-card-pad` — inner padding.
99
132
  - `--choice-card-radius` — corner radius.
100
133
  - `--choice-card-check-size` — size of the check indicator.
134
+ - `--choice-card-border` / `--choice-card-bg` — resting border and background;
135
+ the checked state and hover rebind them, so a checked override needs
136
+ `:has(:checked)`.
137
+ - `--choice-chip-dot` — read-only: the chip's check-slot centre as a `circle()`
138
+ position. It is declared on the chip's `::before`, so read it in a
139
+ `.choice-card.chip…::before` rule.
101
140
 
102
141
  Plain `.check` / `.radio` controls expose two hooks:
103
142
 
@@ -47,7 +47,7 @@ contrast contracts apply to the combobox for free:
47
47
  .actual-combobox {
48
48
  --cb-bg: var(--surface);
49
49
  --cb-color: var(--text);
50
- --cb-border-color: var(--border);
50
+ --cb-border-color: var(--form-invalid-border, var(--border));
51
51
  --cb-border-radius: var(--radius);
52
52
  --cb-focus-color: var(--focus);
53
53
  --cb-muted-color: var(--text-muted);
@@ -57,15 +57,23 @@ contrast contracts apply to the combobox for free:
57
57
  --cb-option-min-height: var(--control-size);
58
58
  }
59
59
 
60
- .actual-combobox .cb-control:focus-within,
61
- .actual-combobox .cb-text-control:focus {
62
- outline: 2px solid transparent;
63
- box-shadow: var(--focus-ring-shadow);
60
+ .actual-combobox :is(.cb-text-control:focus-visible, .cb-control:has(.cb-input:focus-visible)) {
61
+ border-color: var(--form-invalid-border, var(--cb-border-color));
62
+ outline: var(--focus-ring-width) solid var(--form-invalid-border, var(--focus));
63
+ outline-offset: calc(var(--focus-ring-width) * -1);
64
+ box-shadow: none;
64
65
  }
65
66
 
66
- .actual-combobox .cb-chip-remove:focus-visible {
67
- outline: 2px solid transparent;
68
- box-shadow: 0 0 0 2px var(--focus-ring);
67
+ .actual-combobox :is(.cb-text-control[aria-invalid="true"], .cb-control:has([aria-invalid="true"])) {
68
+ --form-invalid-border: var(--danger);
69
+ }
70
+
71
+ :where(.actual-combobox) combo-box {
72
+ border-radius: var(--cb-border-radius);
73
+ }
74
+
75
+ .actual-combobox :is(.cb-control, .cb-text-control) {
76
+ border-radius: inherit;
69
77
  }
70
78
 
71
79
  .actual-combobox .cb-chip {
@@ -79,9 +87,13 @@ contrast contracts apply to the combobox for free:
79
87
  }
80
88
  ```
81
89
 
82
- The composite focus ring follows any internal focus, not just the search
83
- input: focus a chip or its remove × and the control shows the same field ring,
84
- with the local `:focus-visible` indicator on the chip or its × on top.
90
+ While the search input is focused, the control takes the same inset outline as
91
+ `.input` in place of the library's accent border and outer halo; a focused chip
92
+ or remove × keeps the library's own inner ring. Being inset, the outline needs
93
+ no adaptation inside `.join`: `combo-box` owns the radius at zero specificity,
94
+ so the join gives it the group corners and the visible control inherits them. The invalid rule sets `--form-invalid-border`,
95
+ the hook `.field.danger` and `aria-invalid` already use, so border and outline
96
+ turn danger together.
85
97
 
86
98
  The same approach works for any widget that exposes a custom-property skin —
87
99
  keep the widget's behavior, restyle its tokens through the shared vocabulary.
@@ -29,11 +29,8 @@ When composing source files manually, import `components/join.css` after the con
29
29
  </div>
30
30
  ```
31
31
 
32
- `.join` handles border-radius and border collapsing between adjacent children. It works with any direct child — `.input`, `.btn`, `.select`, or `.join-addon`.
33
- When an `.input`, `.textarea`, or `.select` receives keyboard-relevant focus,
34
- the focus ring surrounds the complete joined field. Attached buttons keep their
35
- own focus indicator when reached directly, so the actionable segment remains
36
- identifiable.
32
+ `.join` handles border-radius and border collapsing between adjacent children. It works with any direct child — `.input`, `.btn`, `.select`, or `.join-addon` — plus one wrapper level (e.g. a combobox element around the native control): the wrapper receives the group corners and the inner control inherits them via `border-radius: inherit`.
33
+ When an `.input`, `.textarea`, or `.select` receives keyboard focus, it keeps its own inset focus outline; the group never moves the ring onto itself, so the focused segment remains identifiable.
37
34
 
38
35
  ## Linting `role="group"`
39
36
 
@@ -165,6 +165,12 @@ A full form using the control classes, field wrappers, and form actions:
165
165
  </form>
166
166
  ```
167
167
 
168
+ ## Hooks
169
+
170
+ Set `--control-hover-border` to tune the hover border of `.input` and `.textarea`.
171
+ Set `--control-hover-bg` to keep a `.select` background steady on hover, or give
172
+ it a stronger fill.
173
+
168
174
  ## Notes
169
175
 
170
176
  - `required` is enough on native inputs. `aria-required="true"` is redundant and not used in these examples.
@@ -96,27 +96,73 @@ Set `--avatar-size` / `--avatar-radius`; tune overlap with `--avatar-stack-overl
96
96
 
97
97
  → Components · Avatar
98
98
 
99
- ### Make an inverted / contrasting surface
100
-
101
- Apply `.inverted` to create one contrasting surface context. It paints a bare
102
- block (`<header class="inverted">`, `<section class="inverted">`) with
103
- `--surface-solid` and `--surface`; contextual content and transparent treatments
104
- follow that foreground. Apply it directly to a component that consumes the
105
- shared `--ui-*` contract when that component itself is the contrasting surface.
106
-
107
- Surface-owning descendants keep their own component surface and establish a new
108
- foreground context. Use `data-theme="dark"`, `data-theme="light"`, or a named
109
- theme instead when an entire subtree — including forms, states, and nested
110
- surfaces — must use another palette. More specific component states or explicit
111
- surface variants (`.card.subtle`) still take precedence.
112
-
113
- → Components · Card · Foundations · Tokens (theme contract)
99
+ ### Make a dark (or light) section / inverted, inverse, contrasting band
100
+
101
+ Put `data-theme="dark"` (or `"light"`) on the section, footer, navbar or card.
102
+ The island recomputes every token — surfaces, text, fields, validation,
103
+ intents, focus and native controls — so a form or a `.primary.outline` inside
104
+ it reads correctly. In a branded application, `[data-theme="light"]` and
105
+ `[data-theme="dark"]` are the scheme roles of your brand: redefine them after
106
+ Actual CSS and the island follows your palette.
107
+
108
+ ```html
109
+ <footer data-theme="dark">…</footer>
110
+ <article class="card" data-theme="dark">…</article>
111
+ ```
112
+
113
+ The island recomputes tokens but paints nothing. A `.card` paints its own
114
+ surface; any other element needs `background: var(--surface)` and
115
+ `color: var(--text)`, or its ink lands on the page theme's background. A
116
+ `<legend>` straddles its fieldset's border, outside the fieldset's surface, so
117
+ put the island on a wrapper, not on the `<fieldset>`.
118
+
119
+ Actual has no class that inverts only part of a subtree. When all you need is
120
+ a painted band with neutral content and filled buttons, write it in
121
+ application CSS and keep forms and intent ink out of it:
122
+
123
+ ```css
124
+ .site-band {
125
+ --heading: var(--surface);
126
+ background: var(--surface-solid);
127
+ color: var(--surface);
128
+ }
129
+ ```
130
+
131
+ Transparent treatments (`.btn.outline`, `.ghost`, `.link`) and headings follow
132
+ that ink; fields, their labels and help text, and intent colors keep the page
133
+ palette, which nothing tunes for `--surface-solid`. Focus lines stay readable
134
+ there: the theme contract keeps `--focus` at 3:1 on `--surface-solid`.
135
+
136
+ → Foundations · Tokens (theme contract) · Examples · Contrast contexts
137
+
138
+ ### Replace the palette with my own brand
139
+
140
+ Override the whole minimal recolor set (Tokens · Theme contract), not just
141
+ `--primary`. Every neutral of the default theme — surfaces, text, border,
142
+ `--neutral`, `--hover-overlay`, `--shadow-color` — is tinted toward the default
143
+ primary and never derived from `--primary` at runtime, so a partial override
144
+ leaves the default identity in the greys. `report:theme-contrast` checks
145
+ contrast, not a leftover tint.
146
+
147
+ Soft ink is the other half. The default palette ships calibrated
148
+ `--*-soft-fg` hooks; any other theme falls back to the `--soft-fg-mix`
149
+ derivation, which is a single global percentage and therefore guarantees
150
+ nothing about your intents. Verify with `bun run report:theme-contrast`, then
151
+ lower `--soft-fg-mix` or declare the per-role hooks for the intents that miss.
152
+
153
+ → Foundations · Tokens (theme contract)
114
154
 
115
155
  ### Use or adapt a preset palette
116
156
 
117
157
  `src/css/themes/` holds example palettes (`ocean`, `spruce`, `neon`, `brutalist`, …) as
118
158
  reference material to copy into your own `[data-theme]` island — they are demo
119
159
  assets, not package entrypoints. Each sets the intent/surface/text tokens and
120
- lets the core derive everything else. In browsers with `color-mix()` support,
121
- `--focus-ring` follows each island's `--focus` automatically; re-declare it only
122
- for a deliberate visual override or a matching legacy fallback.
160
+ lets the core derive everything else.
161
+
162
+ The `bootstrap-v6` example follows Bootstrap 6's blue palette, stronger subtle
163
+ surfaces, filled badges, and steady select hover; its focus is the core solid
164
+ line. Use `.badge.soft` for a subtle badge. It is a visual theme, not a
165
+ Bootstrap compatibility layer. The `gradient` example keeps a Bootstrap 4 style
166
+ outer halo on focused fields, for comparison. The `material` example restyles
167
+ fields as filled underlines with pill buttons — tokens plus three small rules,
168
+ and `.join` still needs no adaptation.
@@ -17,6 +17,24 @@ Component custom properties fall into three header categories in the source: **P
17
17
 
18
18
  As a convention, component-prefixed properties owned by the component's base rule are usually author hooks. Properties owned by states/variants, derived from other hooks, or written by JavaScript are not. This convention is indicative, not algorithmic: runtime-written, derived, and state-relay properties stay internal even when their form resembles a hook.
19
19
 
20
+ ### Where a token is declared
21
+
22
+ A hook is only defined inside the scope that declares it. Reading one outside that scope is not an error a browser reports: `var()` resolves to nothing, the whole declaration becomes invalid at computed-value time, and the property falls back to its initial value — `padding: var(--card-pad)` outside a card computes to `0`, silently.
23
+
24
+ | Scope | Declared on | Examples |
25
+ | --------- | --------------------------- | -------------------------------------- |
26
+ | Global | `:root`, every theme island | `--space-40`, `--radius`, `--primary` |
27
+ | Component | the component's base rule | `--card-pad`, `--drawer-size` |
28
+ | Relay | a variant, state, or script | `--ui-bg`, `--intent`, `--surface-pad` |
29
+
30
+ Global tokens are the ones to reach for in application CSS. Write a relay only to override what a variant sets on one element (Button · Variants); read one only where its owner documents it (`--bleed-pad`). When you do want a component token outside its component — matching a bare block to the card rhythm, say — give `var()` the default explicitly, so the declaration survives where the hook is absent:
31
+
32
+ ```css
33
+ .panel {
34
+ padding: var(--card-pad, var(--space-40));
35
+ }
36
+ ```
37
+
20
38
  ### Color
21
39
 
22
40
  Intent tokens are used by `.primary`, `.secondary`, `.success`, `.warning`, `.danger`, and `.neutral`. They are curated color/foreground pairs. Actual CSS does not compute foregrounds automatically in the core theme contract.
@@ -62,17 +80,16 @@ The surface levels are: `surface` (canvas), `surface-raised` (cards, raised surf
62
80
 
63
81
  Use `*-fg` pairs only for solid backgrounds where the component controls both foreground and background. Do not require a foreground token for every surface token.
64
82
 
65
- Focus styling is outline-first. `--focus` names the theme's focus color, and a ring is derived from it with `color-mix()`. A global focus baseline applies `--focus-outline` and `--focus-outline-offset`: older browsers show it on `:focus`, while modern browsers limit it to `:focus-visible`. Components add `--focus-ring-shadow` only where a softer halo helps; `--focus-ring-width` sets its thickness. Under `forced-colors: active`, the outline maps to `Highlight` and the shadow is disabled.
83
+ Focus styling is outline-first. A global focus baseline applies `--focus-outline` and `--focus-outline-offset`: older browsers show it on `:focus`, while modern browsers limit it to `:focus-visible`. Components draw a solid `--focus-ring-width` line in `--focus`: inside the border box on text fields (`.input`, `.textarea`, `.select`), outside at `--focus-outline-offset` on actions (`.btn`, `.close`, choices, switches). The color never follows an intent or a local surface, so `--focus` must hold 3:1 against both `--surface` and `--surface-solid` — the page, fields and a solid band all rely on it. Under `forced-colors: active`, outlines map to the system focus color.
66
84
 
67
85
  ```css
68
86
  :root {
69
- --focus: var(--neutral);
87
+ --focus: hsl(268 30% 55%);
70
88
 
71
89
  --focus-outline-color: currentColor;
72
90
  --focus-outline: calc(var(--border-width) * 2) solid var(--focus-outline-color);
73
91
  --focus-outline-offset: 2px;
74
- --focus-ring-width: 3px;
75
- --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
92
+ --focus-ring-width: 2px;
76
93
  }
77
94
  ```
78
95
 
@@ -445,9 +462,7 @@ Text controls (`.input`/`.textarea`/`.select`) expose the same lower-edge extens
445
462
 
446
463
  Alerts expose the same idea on the leading edge instead of the lower one: `--alert-border-inline-start-color` and `--alert-border-inline-start-width` are unset by default and fall back to the regular `--ui-border`, so a theme can turn an alert into a callout with a colored flag on the inline-start side, matching the classic notice pattern, without a per-variant override.
447
464
 
448
- A theme is not limited to the tokens above; it can restate any component-local token under a state selector to change the interaction recipe itself, not just the palette. The `edge` theme does this for focus: the base input recipe (forms/control.css) flips `--control-border` to `--focus` on top of the shared outline + ring, which would stack with a lower-edge accent. Edge instead restores `--control-border` under `:focus-visible` and thickens the accent edge so the edge, not a second ring, carries the focus.
449
-
450
- Buttons don't consume `--focus-outline` — button.css computes its own ring inline so the color can follow `--btn-focus-color` (intent-aware, unlike the plain `--focus`). It reuses the shared `--focus-ring-width` for the ring thickness and derives the color through `--btn-focus-ring-color` (a `color-mix()` of `--btn-focus-color` when supported, falling back to the shared `--focus-ring`).
465
+ A theme is not limited to the tokens above; it can restate any component-local token or rule under a state selector to change the interaction recipe itself, not just the palette. The `gradient` theme does this for field focus: it restores a `--focus` border plus an outer translucent halo in place of the core inset outline.
451
466
 
452
467
  Rules:
453
468
 
@@ -464,9 +479,11 @@ Rules:
464
479
 
465
480
  Themes override tokens, not selectors. The themes in `src/css/themes/` are repository-only demo examples, not included in the default stylesheet or the npm package; they exist to show valid ways to use this contract, such as `ocean`, `square`, `cyberpunk`, and `brutalist`.
466
481
 
467
- 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.
482
+ A minimal recolor theme overrides the intent pairs, surfaces, text colors, border, focus, hover overlay, and shadow color. Pick `--focus` as a mid-tone that holds 3:1 against both `--surface` and `--surface-solid` in every scheme the theme declares; a pastel dark-scheme primary usually fails on the light solid surface. `bun run report:theme-contrast` measures both pairs.
483
+
484
+ When overriding an intent color, review its paired `--*-fg` and `--*-soft-fg`. A theme that declares no hook falls back to the `--soft-fg-mix` derivation: one global percentage applied to every role, so it carries no contrast guarantee for a palette it was not tuned against. `bun run report:theme-contrast` prints the resting and hovered soft pair for every intent of every island, which is how a theme finds the roles that need a hook or a lower `--soft-fg-mix`.
468
485
 
469
- 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.
486
+ The default palette's hooks apply to `:root` and to the `light`/`dark` boundaries only. Any other `data-theme` value resets them to the derivation, whether the attribute sits on `<html>` or on a nested island.
470
487
 
471
488
  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.
472
489
 
@@ -507,6 +524,7 @@ A minimal recolor theme (illustrative, not a shipped theme):
507
524
  --border: hsl(260 24% 84%);
508
525
  --focus: var(--primary);
509
526
  --hover-overlay: hsl(260 30% 10% / 0.04);
527
+ --shadow-color: hsl(260 30% 10%);
510
528
  --soft-bg-mix: 84%;
511
529
  --soft-border-mix: 58%;
512
530
 
@@ -516,7 +534,7 @@ A minimal recolor theme (illustrative, not a shipped theme):
516
534
  }
517
535
  ```
518
536
 
519
- 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 `--focus-ring-shadow`. 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.
537
+ Several tokens are theme-derived aliases that reference other tokens — `--state-selected`/`--state-selected-fg`/`--state-disabled`, `--indicator-ring`, `--shadow`/`--shadow-popout`, `--heading`, and `--selection-bg`/`--selection-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.
520
538
 
521
539
  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.
522
540
 
@@ -30,37 +30,29 @@ ARIA and native HTML, and per-widget configuration uses self-describing
30
30
  npm install actual-css
31
31
  ```
32
32
 
33
- Import the full framework in your CSS:
33
+ Import what you use. The simplest start is the package's full entry, copied
34
+ into your project and pruned — see
35
+ [Start from the full entry](modular-import.md#start-from-the-full-entry):
34
36
 
35
37
  ```css
36
- @import "actual-css/full";
37
- ```
38
-
39
- `actual-css/full` ships every functional family. The bare `actual-css` entrypoint
40
- is the minimal core (reset, tokens, theme, base, intents, variants, focus,
41
- print).
42
-
43
- Or compose only the pieces you use:
44
-
45
- ```css
46
- @import "actual-css/css";
38
+ @import "actual-css/css"; /* core */
47
39
  @import "actual-css/css/layout";
40
+ @import "actual-css/css/forms/all";
48
41
  @import "actual-css/css/components/button";
49
42
  @import "actual-css/css/components/card";
50
- @import "actual-css/css/forms";
51
- @import "actual-css/css/components/flyout";
52
43
  @import "actual-css/css/utilities";
53
44
  ```
54
45
 
55
- `actual-css/css` is the minimal core. Family manifests (`css/layout`,
56
- `css/forms`, `css/components`, `css/typography`, `css/effects`,
57
- `css/utilities`) and their modules map one-to-one to `src/css/`. See the
58
- [modular import guide](modular-import.md) for the full map.
46
+ `actual-css/css` is the core (reset, tokens, theme, base, intents, variants,
47
+ focus, print). Each family and module is its own import; the
48
+ [modular import guide](modular-import.md) has the full map.
59
49
 
60
- You can also use the compiled full bundle directly:
50
+ For zero-config use — prototypes, CDN, or when bundle size is not worth
51
+ optimizing — the bare `actual-css` entrypoint and `actual-css/full` both resolve
52
+ to the complete compiled framework:
61
53
 
62
54
  ```html
63
- <link rel="stylesheet" href="actual.full.min.css">
55
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.10/dist/actual.full.min.css">
64
56
  ```
65
57
 
66
58
  The package does not maintain separate partial bundles. Modular entrypoints map
@@ -76,9 +68,9 @@ Actual CSS uses a small unprefixed class grammar:
76
68
 
77
69
  Intents are `.primary`, `.secondary`, `.success`, `.warning`, and `.danger`, and
78
70
  work across components. Variants such as `.solid`, `.soft`, `.outline`, and `.surface` are
79
- shared by buttons, badges, alerts, and cards; `.inverted` is a shared surface
80
- modifier that paints any block with the inverse surface. `.ghost` and `.link`
81
- are button-only. Size variants `.sm` and `.lg` scale typography and
71
+ shared by buttons, badges, alerts, and cards; a subtree that needs another
72
+ palette is a `data-theme` island, not a class. `.ghost` and `.link` are
73
+ button-only. Size variants `.sm` and `.lg` scale typography and
82
74
  participating component geometry consistently. Density contexts `.compact` and
83
75
  `.spacious` change geometry and spacing without changing font size or icons;
84
76
  `.compact` may slightly narrow typefaces that support a width axis.