actual-css 0.9.2 → 0.11.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 (136) hide show
  1. package/CHANGELOG.md +169 -1
  2. package/README.md +65 -44
  3. package/components.json +35 -14
  4. package/dist/actual.full.css +877 -583
  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/enhancement-contract.md +16 -7
  10. package/docs/design-notes/forced-colors.md +12 -1
  11. package/docs/design-notes/naming.md +1 -1
  12. package/docs/design-notes/platform-alignment.md +8 -4
  13. package/docs/design-notes/rich-text.md +61 -0
  14. package/docs/design-notes/steps.md +2 -2
  15. package/docs/design-notes/surface-reparenting.md +8 -9
  16. package/docs/navigation.json +4 -0
  17. package/docs/pages/components/accordion.md +55 -1
  18. package/docs/pages/components/alert.md +9 -15
  19. package/docs/pages/components/aura.md +12 -13
  20. package/docs/pages/components/badge.md +16 -7
  21. package/docs/pages/components/bleed.md +42 -0
  22. package/docs/pages/components/button.md +30 -4
  23. package/docs/pages/components/card.md +21 -86
  24. package/docs/pages/components/chat.md +5 -0
  25. package/docs/pages/components/close.md +48 -0
  26. package/docs/pages/components/data-list.md +26 -1
  27. package/docs/pages/components/dialog.md +21 -18
  28. package/docs/pages/components/drawer.md +9 -9
  29. package/docs/pages/components/navbar.md +5 -5
  30. package/docs/pages/components/pagination.md +15 -15
  31. package/docs/pages/components/shimmer.md +57 -0
  32. package/docs/pages/components/skeleton.md +2 -1
  33. package/docs/pages/components/spinner.md +14 -9
  34. package/docs/pages/components/table.md +13 -0
  35. package/docs/pages/enhancements/flyout.md +2 -0
  36. package/docs/pages/enhancements/overview.md +5 -0
  37. package/docs/pages/enhancements/tabs.md +4 -0
  38. package/docs/pages/enhancements/tooltip.md +3 -1
  39. package/docs/pages/examples/overview.md +2 -1
  40. package/docs/pages/forms/choice-cards.md +116 -4
  41. package/docs/pages/forms/combobox.md +39 -12
  42. package/docs/pages/forms/joined-controls.md +2 -5
  43. package/docs/pages/forms/overview.md +10 -1
  44. package/docs/pages/forms/range.md +110 -5
  45. package/docs/pages/forms/rich-text.md +110 -0
  46. package/docs/pages/forms/validation.md +5 -1
  47. package/docs/pages/foundations/theming.md +110 -18
  48. package/docs/pages/foundations/tokens.md +41 -13
  49. package/docs/pages/guides/bootstrap.md +2 -1
  50. package/docs/pages/guides/introduction.md +15 -23
  51. package/docs/pages/guides/modular-import.md +54 -7
  52. package/docs/pages/guides/progressive-enhancement.md +19 -17
  53. package/docs/pages/guides/tailwind.md +21 -7
  54. package/docs/pages/layout/column-layout.md +92 -54
  55. package/docs/pages/layout/media.md +1 -1
  56. package/docs/pages/layout/overview.md +1 -1
  57. package/docs/pages/utilities/overview.md +1 -1
  58. package/docs/pages/utilities/sizing-wrapping.md +5 -3
  59. package/llms.txt +25 -4
  60. package/package.json +10 -6
  61. package/reserved-classes.json +6 -30
  62. package/skills/actual-css/PROJECT.md.example +65 -0
  63. package/skills/actual-css/README.md +64 -0
  64. package/skills/actual-css/SKILL.md +234 -0
  65. package/skills/actual-css/references/accessibility.md +44 -0
  66. package/skills/actual-css/references/api-and-vocabulary.md +94 -0
  67. package/skills/actual-css/references/enhancements.md +59 -0
  68. package/skills/actual-css/references/forms.md +49 -0
  69. package/skills/actual-css/references/layout-and-theming.md +82 -0
  70. package/skills/actual-css/references/review-checklist.md +51 -0
  71. package/skills/actual-css/scripts/inspect-actual-css.mjs +171 -0
  72. package/src/css/components/accordion.css +44 -7
  73. package/src/css/components/alert.css +23 -43
  74. package/src/css/components/app-nav.css +9 -0
  75. package/src/css/components/avatar.css +1 -1
  76. package/src/css/components/badge.css +11 -39
  77. package/src/css/components/bleed.css +79 -0
  78. package/src/css/components/button.css +6 -17
  79. package/src/css/components/card.css +7 -40
  80. package/src/css/components/close.css +91 -0
  81. package/src/css/components/data-list.css +18 -0
  82. package/src/css/components/drawer.css +15 -56
  83. package/src/css/components/fab.css +10 -0
  84. package/src/css/components/flyout.css +5 -1
  85. package/src/css/components/index.css +2 -0
  86. package/src/css/components/join.css +36 -20
  87. package/src/css/components/menu.css +12 -0
  88. package/src/css/components/meter.css +1 -1
  89. package/src/css/components/modal.css +10 -57
  90. package/src/css/components/navbar.css +27 -0
  91. package/src/css/components/pagination.css +3 -2
  92. package/src/css/components/rating.css +1 -1
  93. package/src/css/components/status-bar.css +2 -2
  94. package/src/css/components/steps.css +3 -3
  95. package/src/css/components/tab.css +3 -3
  96. package/src/css/components/table.css +6 -0
  97. package/src/css/components/tooltip.css +1 -1
  98. package/src/css/core/focus.css +19 -3
  99. package/src/css/core/theme.css +33 -43
  100. package/src/css/core/tokens.css +9 -5
  101. package/src/css/core/variants.css +5 -34
  102. package/src/css/effects/aura.css +51 -33
  103. package/src/css/effects/index.css +1 -0
  104. package/src/css/effects/shimmer.css +57 -0
  105. package/src/css/forms/choice-card.css +142 -24
  106. package/src/css/forms/choice.css +2 -3
  107. package/src/css/forms/control.css +17 -10
  108. package/src/css/forms/custom-select.css +4 -4
  109. package/src/css/forms/native.css +4 -4
  110. package/src/css/forms/otp.css +9 -1
  111. package/src/css/forms/range.css +72 -14
  112. package/src/css/forms/switch.css +2 -3
  113. package/src/css/layout/app-layout.css +1 -1
  114. package/src/css/layout/column-layout.css +26 -111
  115. package/src/css/utilities/extra.css +7 -6
  116. package/src/js/command.js +61 -32
  117. package/src/js/dialog.js +103 -47
  118. package/src/js/dismiss.js +1 -2
  119. package/src/js/enhance.js +91 -126
  120. package/src/js/escape.js +1 -0
  121. package/src/js/flyout.js +10 -1
  122. package/src/js/focus-group.js +2 -1
  123. package/src/js/full.js +1 -0
  124. package/src/js/keys.js +14 -0
  125. package/src/js/menu.js +2 -1
  126. package/src/js/password.js +1 -8
  127. package/src/js/range.js +151 -0
  128. package/src/js/scrollspy.js +4 -5
  129. package/src/js/status.js +21 -10
  130. package/src/js/surface.js +12 -8
  131. package/src/js/tab.js +2 -1
  132. package/src/js/tooltip.js +29 -2
  133. package/src/js/validation.js +8 -15
  134. package/dist/actual.css +0 -876
  135. package/dist/actual.js +0 -1
  136. package/dist/actual.min.css +0 -1
@@ -96,27 +96,119 @@ 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-solid-fg);
126
+ background: var(--surface-solid);
127
+ color: var(--surface-solid-fg);
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
+ ### Scope a theme to one section / microtheme / theme island
139
+
140
+ Any `data-theme` value is an island, not only `light` and `dark`:
141
+ `<aside data-theme="promo">` paints its subtree with that theme, nested inside
142
+ the page theme, without changing the markup. The same painting rule applies as
143
+ for a dark section. A theme can change radius, typography, shadow and motion as
144
+ well as color.
145
+
146
+ → Foundations · Tokens (theme contract)
147
+
148
+ ### Dark mode toggle / theme switcher / remember the user's choice
149
+
150
+ Actual CSS ships the mechanism, not the control. Without `data-theme` the page
151
+ follows the OS; `data-theme="light"` or `"dark"` on `<html>` forces a scheme,
152
+ and removing the attribute returns to the OS. The switcher UI and where the
153
+ choice is stored belong to the application; a page with no setting already
154
+ follows the OS. Apply a stored choice from an inline script in `<head>` so the
155
+ first paint does not flash the OS scheme. The demo's theme picker is an
156
+ inspection tool, not a component to copy.
157
+
158
+ ### Force light or dark on a theme that has both / theme and color scheme
159
+
160
+ With a single brand theme, `data-theme="light"` and `"dark"` already are that
161
+ choice once you redefine them as your brand's two schemes (see the dark section
162
+ above).
163
+
164
+ A named theme that pairs both schemes through `light-dark()` follows the OS. To
165
+ pin one scheme, set `color-scheme` on the element that carries `data-theme`,
166
+ inline or in a rule more specific than the theme's own. `color-scheme` is the
167
+ scheme axis and `data-theme` the design axis, so there is no separate
168
+ `data-color-scheme` attribute.
169
+
170
+ ```html
171
+ <html data-theme="ocean" style="color-scheme: dark">
172
+ ```
173
+
174
+ On an ancestor it has no effect: the theme declares its own `color-scheme` on
175
+ its boundary. Leave single-scheme themes (`color-scheme: dark` only) alone —
176
+ their tokens have one value, so forcing the other scheme only flips native
177
+ controls and scrollbars.
178
+
179
+ → Foundations · Tokens (theme contract)
180
+
181
+ ### Replace the palette with my own brand
182
+
183
+ Override the whole minimal recolor set (Tokens · Theme contract), not just
184
+ `--primary`. Every neutral of the default theme — surfaces, text, border,
185
+ `--neutral`, `--hover-overlay`, `--shadow-color` — is tinted toward the default
186
+ primary and never derived from `--primary` at runtime, so a partial override
187
+ leaves the default identity in the greys. `report:theme-contrast` checks
188
+ contrast, not a leftover tint.
189
+
190
+ Soft ink is the other half. The default palette ships calibrated
191
+ `--*-soft-fg` hooks; any other theme falls back to the `--soft-fg-mix`
192
+ derivation, which is a single global percentage and therefore guarantees
193
+ nothing about your intents. Verify with `bun run report:theme-contrast`, then
194
+ lower `--soft-fg-mix` or declare the per-role hooks for the intents that miss.
195
+
196
+ → Foundations · Tokens (theme contract)
114
197
 
115
198
  ### Use or adapt a preset palette
116
199
 
117
200
  `src/css/themes/` holds example palettes (`ocean`, `spruce`, `neon`, `brutalist`, …) as
118
201
  reference material to copy into your own `[data-theme]` island — they are demo
119
202
  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.
203
+ lets the core derive everything else.
204
+
205
+ The `bootstrap-v6` example follows Bootstrap 6's blue palette, stronger subtle
206
+ surfaces, filled badges, and steady select hover; its focus is the core solid
207
+ line. Use `.badge.soft` for a subtle badge. It is a visual theme, not a
208
+ Bootstrap compatibility layer. The `gradient` example keeps a Bootstrap 4 style
209
+ outer halo on focused fields, for comparison. The `material` example restyles
210
+ fields as filled underlines with pill buttons — tokens plus three small rules,
211
+ and `.join` still needs no adaptation. The `glass` example paints translucent
212
+ surfaces over its own backdrop: it sets `--surface-opaque` and
213
+ `--surface-solid-fg` to opaque values, raises `--soft-bg-mix` so soft fills stay
214
+ light over glass, and adds the blur and an opaque busy veil as small rules.
@@ -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.
@@ -58,21 +76,27 @@ Surface tokens describe the canvas and elevation model. Components should prefer
58
76
  }
59
77
  ```
60
78
 
61
- The surface levels are: `surface` (canvas), `surface-raised` (cards, raised surfaces), `surface-subtle` (the working subtle level — for hover overlays, form fields, table headers), and `surface-solid` (the dark inverse surface, used for accent areas).
79
+ The surface levels are: `surface` (canvas), `surface-raised` (cards, raised surfaces), `surface-subtle` (the working subtle level — for hover overlays, form fields, table headers), and `surface-solid` (the inverse surface, used for accent areas).
80
+
81
+ Surfaces are painted material and may be translucent. Two roles must stay opaque, so they have their own tokens, both defaulting to `--surface`:
82
+
83
+ - `--surface-solid-fg` — the ink on `--surface-solid`: tooltips, the status bar, an inverse band.
84
+ - `--surface-opaque` — an opaque neutral used where a component must hide or separate what is painted beneath it: the status-dot and avatar-stack rings, range-thumb and meter separators, step markers, the mask under an `.aura` child. It is not a surface fill; paint panels with `--surface-*`.
85
+
86
+ A theme with translucent surfaces overrides both with opaque values.
62
87
 
63
88
  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
89
 
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.
90
+ 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`. The baseline is a `currentColor` line, so text-like elements (links, `summary`, nav and menu items) follow whatever surface the application painted. Boxed 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. Both lines are `--focus-ring-width` wide. Under `forced-colors: active`, outlines map to the system focus color.
66
91
 
67
92
  ```css
68
93
  :root {
69
- --focus: var(--neutral);
94
+ --focus: hsl(268 30% 55%);
70
95
 
71
96
  --focus-outline-color: currentColor;
72
- --focus-outline: calc(var(--border-width) * 2) solid var(--focus-outline-color);
97
+ --focus-outline: var(--focus-ring-width) solid var(--focus-outline-color);
73
98
  --focus-outline-offset: 2px;
74
- --focus-ring-width: 3px;
75
- --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
99
+ --focus-ring-width: 2px;
76
100
  }
77
101
  ```
78
102
 
@@ -364,7 +388,7 @@ A few cross-component helpers:
364
388
  ```css
365
389
  :root {
366
390
  --indicator-offset: 14.65%; /* circle-aware inset */
367
- --indicator-ring: var(--surface);
391
+ --indicator-ring: var(--surface-opaque);
368
392
  --backdrop-color: rgb(0 0 0);
369
393
  --backdrop-opacity: 0.45;
370
394
  --backdrop-fill: rgb(0 0 0 / var(--backdrop-opacity));
@@ -445,9 +469,7 @@ Text controls (`.input`/`.textarea`/`.select`) expose the same lower-edge extens
445
469
 
446
470
  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
471
 
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`).
472
+ 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
473
 
452
474
  Rules:
453
475
 
@@ -464,9 +486,13 @@ Rules:
464
486
 
465
487
  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
488
 
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.
489
+ A minimal recolor theme overrides the intent pairs, surfaces, text colors, border, focus, hover overlay, and shadow color. Pick `--focus` as a mid-tone that holds 3:1 against both `--surface` and `--surface-solid` in every scheme the theme declares; a pastel dark-scheme primary usually fails on the light solid surface. `bun run report:theme-contrast` measures both pairs.
490
+
491
+ When overriding an intent color, review its paired `--*-fg` and `--*-soft-fg`. A theme that declares no hook falls back to the `--soft-fg-mix` derivation: one global percentage applied to every role, so it carries no contrast guarantee for a palette it was not tuned against. `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`.
492
+
493
+ 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.
468
494
 
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.
495
+ Contrast is measured between opaque colors. A translucent `--surface` has no ratio until it is composited over its backdrop, so `report:theme-contrast` refuses such a theme, and `.soft` mixes over it come out more opaque and more saturated than the surface. A translucent theme owns both: it measures against its real backdrop and replaces any recipe that does not hold there.
470
496
 
471
497
  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
498
 
@@ -474,6 +500,7 @@ A full theme can override:
474
500
 
475
501
  - intent colors and foreground pairs
476
502
  - surface, text, border, focus, and overlay colors
503
+ - `--surface-opaque` and `--surface-solid-fg`, when surfaces are translucent
477
504
  - radius tokens
478
505
  - optional shadow, motion, typography, and soft-variant mix tokens
479
506
 
@@ -507,6 +534,7 @@ A minimal recolor theme (illustrative, not a shipped theme):
507
534
  --border: hsl(260 24% 84%);
508
535
  --focus: var(--primary);
509
536
  --hover-overlay: hsl(260 30% 10% / 0.04);
537
+ --shadow-color: hsl(260 30% 10%);
510
538
  --soft-bg-mix: 84%;
511
539
  --soft-border-mix: 58%;
512
540
 
@@ -516,7 +544,7 @@ A minimal recolor theme (illustrative, not a shipped theme):
516
544
  }
517
545
  ```
518
546
 
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.
547
+ 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.
520
548
 
521
549
  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
550
 
@@ -18,7 +18,8 @@ For an incremental migration, use one of these approaches:
18
18
 
19
19
  * migrate isolated pages or application areas;
20
20
  * control precedence with cascade layers or import order;
21
- * use a project-side prefix transform for Actual while both frameworks coexist.
21
+ * use a project-side prefix transform for Actual while both frameworks coexist,
22
+ renaming the runtime's state classes in `actual-css/js/selectors` as well.
22
23
 
23
24
  The layered import is the cleanest way to keep both stylesheets during the
24
25
  migration:
@@ -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.11/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.
@@ -1,19 +1,26 @@
1
1
  # Modular imports
2
2
 
3
- Actual CSS ships one minimal core (`actual-css`) and one all-in entrypoint (`actual-css/full`). Projects can also compose the framework from family manifests or individual modules.
3
+ Actual CSS has two kinds of entrypoint. Without `/css`, you get a compiled artifact ready to serve. With `/css`, you compose the sources.
4
4
 
5
5
  ## Entrypoints
6
6
 
7
- ```css
8
- @import "actual-css"; /* minimal core */
9
- @import "actual-css/full"; /* complete framework */
7
+ ```text
8
+ Composable source CSS (recommended)
9
+ actual-css/css core
10
+ actual-css/css/<family> one family, e.g. css/layout, css/forms/all
11
+ actual-css/css/<family>/* one module, e.g. css/components/button
12
+ actual-css/css/full every family, as source
13
+
14
+ Zero-config distribution
15
+ actual-css complete compiled framework
16
+ actual-css/full the same bundle, named explicitly
10
17
  ```
11
18
 
12
- `actual-css` contains the shared baseline: reset, tokens, theme, document defaults, intents, universal variants, focus, and print.
19
+ `actual-css/css` contains the shared baseline: reset, tokens, theme, document defaults, intents, universal variants, focus, and print.
13
20
 
14
21
  It contains no typography, layout, forms, components, effects, or utilities.
15
22
 
16
- `actual-css/full` starts from the core and adds every family in cascade order:
23
+ The full framework starts from the core and adds every family in cascade order:
17
24
 
18
25
  ```text
19
26
  core → typography → layout → forms → components → effects → utilities
@@ -27,6 +34,46 @@ Components may enhance or replace that treatment in interactive states, but thei
27
34
 
28
35
  When an interactive state replaces the outline, it must preserve a visible focus indicator in forced-colors, either with an outline-based fallback or by keeping the replacement out of forced-colors.
29
36
 
37
+ ## Start from the full entry
38
+
39
+ Deleting is easier than adding, and it keeps the cascade order right. Copy the
40
+ package's full entry into your project, rewrite its relative imports to
41
+ package paths, then delete what you do not need:
42
+
43
+ - CSS: `src/css/actual.full.css`. Prefix each import with `actual-css/css/`
44
+ and drop `.css` and `/index`; `./actual.css` is `actual-css/css`. To go below
45
+ family level, replace a family line with the imports of its `index.css`,
46
+ rewritten the same way.
47
+ - JS: `src/js/full.js`. Prefix each import with `actual-css/js/` and drop
48
+ `.js`; `./index.js` is `actual-css/js`.
49
+
50
+ ```css
51
+ /* app/index.css — actual.full.css, rewritten, components expanded and pruned */
52
+ @import "actual-css/css";
53
+ @import "actual-css/css/typography";
54
+ @import "actual-css/css/layout";
55
+ @import "actual-css/css/forms/all";
56
+ @import "actual-css/css/components/button";
57
+ @import "actual-css/css/components/close";
58
+ @import "actual-css/css/components/card";
59
+ @import "actual-css/css/components/modal";
60
+ @import "actual-css/css/utilities";
61
+ ```
62
+
63
+ ```js
64
+ // app/index.js — full.js, rewritten and pruned
65
+ import "actual-css/js/flyout";
66
+ import "actual-css/js/dialog";
67
+ import "actual-css/js/dismiss";
68
+ import "actual-css/js/tooltip";
69
+ import "actual-css/js";
70
+ ```
71
+
72
+ JS modules import what they depend on. CSS modules do not, and three are
73
+ shared: keep `components/close` with modal, drawer, alert, or a removable
74
+ badge; `components/bleed` with a `.bleed` child of card, drawer, or accordion;
75
+ `components/spinner` with `components/busy`.
76
+
30
77
  ## Family manifests
31
78
 
32
79
  Each family is one import. The contents are enumerations rather than table
@@ -47,7 +94,7 @@ cells, so they read as a list:
47
94
  - **Components** — `actual-css/css/components`
48
95
  buttons, surfaces, overlays, feedback, navigation, data and composed controls
49
96
  - **Effects** — `actual-css/css/effects`
50
- aura
97
+ aura, shimmer
51
98
  - **Utilities** — `actual-css/css/utilities`
52
99
  base and extra utilities
53
100
 
@@ -49,7 +49,10 @@ never tracked.
49
49
 
50
50
  `enhance()` observes DOM insertions and removals, not attribute changes. If an
51
51
  already-connected element receives a behavior attribute later, call the returned
52
- `refresh(node)` handle. Behavior attributes are setup-time contracts: removing
52
+ `refresh(node)` handle of your own `enhance()`, or `applyEnhancement()` for a
53
+ `data-enhance` token. The built-in `data-*` behaviors (`data-mask`,
54
+ `data-filter`, …) expose no handle: insert the element with the attribute
55
+ already set. Behavior attributes are setup-time contracts: removing
53
56
  one does not disconnect an active behavior. If an application needs a live
54
57
  enable/disable switch, keep that state inside its enhancer and clean it up when
55
58
  the element leaves the DOM.
@@ -89,6 +92,7 @@ example:
89
92
  | `actual-css/js/filter` | Input value filtering on `data-filter` |
90
93
  | `actual-css/js/mask` | Input masks on `data-mask` |
91
94
  | `actual-css/js/password` | Password reveal toggle |
95
+ | `actual-css/js/range` | Range fill and named values (token `range`) |
92
96
  | `actual-css/js/validation` | Form validation (token `validation`) |
93
97
  | `actual-css/js/status` | Singleton status bar on `[data-status][role="status"]` |
94
98
  | `actual-css/js/enhancement-loader` | Declared enhancement manifests |
@@ -105,11 +109,12 @@ needed.
105
109
 
106
110
  The JS is designed to run on its own, without Actual's stylesheet.
107
111
 
108
- - **`data-enhance` tokens** register root-controller behaviors. The runtime
109
- discovers components through `data-enhance="tabs"`, `data-enhance="flyout"`,
110
- `data-enhance="scrollspy"`, and `data-enhance="validation"` — no Actual
111
- presentation class needed. Self-describing attributes (`data-mask`,
112
- `data-tooltip`, `data-context-menu`) stay framework-neutral by construction.
112
+ - **`data-enhance` tokens** register named behaviors. The runtime discovers
113
+ components through `data-enhance="tabs"`, `data-enhance="flyout"`,
114
+ `data-enhance="range"`, `data-enhance="scrollspy"`, and
115
+ `data-enhance="validation"` — no Actual presentation class needed.
116
+ Self-describing attributes (`data-mask`, `data-tooltip`,
117
+ `data-context-menu`) stay framework-neutral by construction.
113
118
  - **`selectors.js`** lists the state classes the runtime *writes* (`is-open`,
114
119
  `was-validated`, …). Edit this file (or alias it in a bundler) to
115
120
  match a different CSS framework's state vocabulary. It is a read-side-only
@@ -229,8 +234,8 @@ Three non-obvious requirements:
229
234
 
230
235
  Lifecycle: `prepareSurface` → `openSurface` → `closeSurface` →
231
236
  `disconnectSurface`. `retainSurface(panel)` reference-counts a surface shared by
232
- several triggers and returns a release function; `isSurfaceOpen` and
233
- `getSurfaceAutoClose` read current state. The separate `escape` primitive adds
237
+ several triggers and returns a release function; `isSurfaceOpen` reads the
238
+ current state. The separate `escape` primitive adds
234
239
  visible dismissable UI to the shared per-document Escape stack.
235
240
 
236
241
  Actual-managed surfaces are mutually exclusive within a document. Opening a
@@ -323,7 +328,6 @@ import { registerCommands } from "actual-css/js/command";
323
328
 
324
329
  registerCommands("--toggle-hidden", {
325
330
  handle(event, trigger, target) {
326
- event.preventDefault();
327
331
  target.hidden = !target.hidden;
328
332
  trigger.setAttribute("aria-expanded", String(!target.hidden));
329
333
  },
@@ -332,10 +336,10 @@ registerCommands("--toggle-hidden", {
332
336
 
333
337
  The default resolver `targetFor(trigger)` looks up `commandfor` by ID in the
334
338
  trigger's document or shadow root. Supply `resolve(trigger)` to validate or
335
- replace that lookup. An optional idempotent `prepare(trigger, target, command)`
336
- callback runs directly before `handle`; built-in behaviors use it for
337
- target-derived semantics. `commandSelector(commands)` builds the matching button
338
- selector. The returned handle has an idempotent `disconnect()` method for
339
+ replace that lookup. Once a target resolves, the router cancels the click before
340
+ `handle` runs, so the native `command` event is never dispatched for a routed
341
+ command. Names must be a native keyword or start with `--`; anything else
342
+ throws. `commandSelector(commands)` builds the matching button selector. The returned handle has an idempotent `disconnect()` method for
339
343
  applications that unload the module owning the command.
340
344
 
341
345
  ### Dismiss command
@@ -411,10 +415,8 @@ registerEnhancement("autosubmit", (form) => {
411
415
  <form data-enhance="validation autosubmit">
412
416
  ```
413
417
 
414
- `enhancementSelector(name)` returns the matching `[data-enhance~="name"]`
415
- selector (names must match `[a-z][a-z0-9-]*`), and `hasEnhancement(el, name)`
416
- tests it. No core module list is modified — the regression test in
417
- `tests/enhance.test.js` proves the shape.
418
+ Names must match `[a-z][a-z0-9-]*`. To find opted-in elements from your own
419
+ script, query `[data-enhance~="name"]`; styles keep using presentation classes.
418
420
 
419
421
  A name is owned once per root: a second `registerEnhancement("autosubmit", …)`
420
422
  on the same root throws, and `disconnect()` releases the name so it can be
@@ -17,25 +17,39 @@ If you finish the migration with a new `.mt-4`, `.flex`, `.bg-primary`, and `md:
17
17
 
18
18
  Tailwind and Actual can temporarily coexist, but some short class names overlap.
19
19
 
20
- Examples include layout and flex-related names such as `.grid`, `.grow`, and `.items-center`.
20
+ The most dangerous is `.outline`: it is an Actual button variant (`.btn.outline`), and Tailwind's `outline` utility draws a real outline on it. Layer order does not save this one — the migration setup above puts Tailwind utilities over Actual on purpose.
21
21
 
22
- For a staged migration, use cascade layers, controlled import order, or a project-side Actual prefix transform where necessary.
22
+ Other overlapping names: `.table`, `.sticky`, `.truncate`, `.prose`, `.sr-only`, plus layout names such as `.grid`, `.grow`, and `.items-center`.
23
+
24
+ For a staged migration, use cascade layers, controlled import order, or a project-side Actual prefix transform where necessary. The transform lives in the consumer's own pipeline (a build-time rename on the Actual import), not in the Actual CLI, which only bundles. The JavaScript runtime writes state classes (`is-open`, `danger`, …): rename them in `actual-css/js/selectors` too, or flyouts and dialogs never open.
23
25
 
24
26
  The layered import is the cleanest way to keep both stylesheets during the
25
27
  migration:
26
28
 
27
29
  ```css
30
+ /* Tailwind v4 emits its own layers (theme, base, components, utilities),
31
+ so declare the order explicitly: Actual sits above Tailwind components
32
+ but below Tailwind utilities during the migration. */
33
+ @layer theme, base, components, actual, utilities;
34
+
28
35
  @import "tailwind.css"; /* your compiled Tailwind output */
29
- @layer actual;
30
36
  @import "actual-css/full" layer(actual);
37
+
31
38
  /* Unlayered project overrides stay on top */
32
39
  @import "app.css";
33
40
  ```
34
41
 
35
- The full import places every Actual rule in a single `actual` layer, so
36
- unlayered Tailwind output and project CSS keep precedence while the migration
37
- runs. See [Cascade layer strategy](https://github.com/lekoala/actual-css/blob/master/docs/design-notes/cascade-layer.md) for the
38
- limitations of the approach.
42
+ The full import places every Actual rule in a single `actual` layer. The
43
+ explicit `@layer` order is what keeps Tailwind utilities above Actual while
44
+ the migration runs — not the unlayered status of either output. See
45
+ [Cascade layer strategy](https://github.com/lekoala/actual-css/blob/master/docs/design-notes/cascade-layer.md)
46
+ for the limitations of the approach.
47
+
48
+ Layers do not scope custom properties: both frameworks define
49
+ `--font-sans`, `--font-mono`, `--font-weight-*`, and `--radius-sm` /
50
+ `--radius-lg`. With `actual` after `theme`, `font-sans` takes Actual's font
51
+ stack and `rounded-lg` resolves to Actual's `--radius-lg`. Expect these
52
+ token collisions on migrated screens until Tailwind is removed.
39
53
 
40
54
  Do not assume that loading both complete stylesheets is automatically safe.
41
55