actual-css 0.10.0 → 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 (71) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/README.md +2 -2
  3. package/components.json +2 -1
  4. package/dist/actual.full.css +314 -106
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/enhancement-contract.md +16 -7
  8. package/docs/design-notes/platform-alignment.md +8 -4
  9. package/docs/design-notes/rich-text.md +61 -0
  10. package/docs/design-notes/steps.md +2 -2
  11. package/docs/navigation.json +2 -0
  12. package/docs/pages/components/aura.md +12 -13
  13. package/docs/pages/components/badge.md +10 -0
  14. package/docs/pages/components/button.md +1 -1
  15. package/docs/pages/components/chat.md +5 -0
  16. package/docs/pages/components/dialog.md +5 -2
  17. package/docs/pages/components/pagination.md +15 -15
  18. package/docs/pages/components/shimmer.md +57 -0
  19. package/docs/pages/components/skeleton.md +2 -1
  20. package/docs/pages/components/spinner.md +11 -6
  21. package/docs/pages/components/table.md +13 -0
  22. package/docs/pages/enhancements/flyout.md +2 -0
  23. package/docs/pages/enhancements/tabs.md +4 -0
  24. package/docs/pages/forms/choice-cards.md +77 -4
  25. package/docs/pages/forms/combobox.md +16 -1
  26. package/docs/pages/forms/overview.md +4 -1
  27. package/docs/pages/forms/range.md +110 -5
  28. package/docs/pages/forms/rich-text.md +110 -0
  29. package/docs/pages/forms/validation.md +5 -1
  30. package/docs/pages/foundations/theming.md +49 -3
  31. package/docs/pages/foundations/tokens.md +15 -5
  32. package/docs/pages/guides/bootstrap.md +2 -1
  33. package/docs/pages/guides/introduction.md +1 -1
  34. package/docs/pages/guides/modular-import.md +1 -1
  35. package/docs/pages/guides/progressive-enhancement.md +13 -14
  36. package/docs/pages/guides/tailwind.md +4 -2
  37. package/llms.txt +4 -3
  38. package/package.json +5 -1
  39. package/reserved-classes.json +1 -0
  40. package/src/css/components/avatar.css +1 -1
  41. package/src/css/components/flyout.css +4 -1
  42. package/src/css/components/meter.css +1 -1
  43. package/src/css/components/pagination.css +3 -2
  44. package/src/css/components/rating.css +1 -1
  45. package/src/css/components/status-bar.css +2 -2
  46. package/src/css/components/steps.css +3 -3
  47. package/src/css/components/tab.css +3 -3
  48. package/src/css/components/table.css +6 -0
  49. package/src/css/components/tooltip.css +1 -1
  50. package/src/css/core/focus.css +14 -4
  51. package/src/css/core/theme.css +8 -0
  52. package/src/css/core/tokens.css +8 -4
  53. package/src/css/core/variants.css +2 -1
  54. package/src/css/effects/aura.css +51 -33
  55. package/src/css/effects/index.css +1 -0
  56. package/src/css/effects/shimmer.css +57 -0
  57. package/src/css/forms/choice-card.css +71 -23
  58. package/src/css/forms/control.css +2 -2
  59. package/src/css/forms/otp.css +9 -1
  60. package/src/css/forms/range.css +64 -21
  61. package/src/js/command.js +61 -32
  62. package/src/js/dialog.js +8 -23
  63. package/src/js/dismiss.js +1 -2
  64. package/src/js/enhance.js +84 -123
  65. package/src/js/full.js +1 -0
  66. package/src/js/password.js +1 -8
  67. package/src/js/range.js +151 -0
  68. package/src/js/scrollspy.js +4 -5
  69. package/src/js/status.js +2 -10
  70. package/src/js/tooltip.js +11 -1
  71. package/src/js/validation.js +4 -10
@@ -10,6 +10,9 @@
10
10
  - `.field-group` and `.choice` provide layout without depending on parent scope.
11
11
  - Customizable select is progressive enhancement only — the native select remains the baseline.
12
12
  - Searchable selects, tags and autocomplete are a JavaScript widget concern; theme an external combobox with Actual tokens — see the [combobox template](../../demo/templates/combobox.html).
13
+ - Formatted text (bold, lists, links) is a hosted-editor concern; the native textarea stays the baseline and an external engine is themed the same way — see the [rich-text template](../../demo/templates/rich-text.html) and the [composer template](../../demo/templates/composer.html) for the plain-text case.
14
+
15
+ **Related terms:** form controls, select, custom select, dropdown, input, textarea, checkbox, radio.
13
16
 
14
17
  ## Class reference
15
18
 
@@ -181,7 +184,7 @@ it a stronger fill.
181
184
  - Use native `<optgroup>` and `disabled` options in `.select` controls when applicable; the customizable picker preserves both as a progressive enhancement.
182
185
  - `.choice` is the choice-label API. Use `.check` or `.radio` on the nested control. The control goes first, the label group second, so multi-line labels align the control with the first line.
183
186
  - For a switch, use `class="switch"` and `role="switch"`.
184
- - Core range controls stay native and are enhanced by `accent-color`. An optional richer range skin may come later if real projects need it.
187
+ - `.range` skins the native range control (`appearance: none` track, thumb, and focus ring). Add `data-enhance="range"` for the fill share, named `datalist` values, and output sync.
185
188
  - `.form-actions` carries a default top margin. Override it with `--form-actions-margin-block-start`, `--form-actions-align`, or `--form-actions-justify`. It is class-only and may live inside or outside `<form>`. See Detached Actions below.
186
189
  - `.join` visually joins adjacent controls into a single unit. Use `.join-addon` for static prefix/suffix content. The container still carries `role="group"` for accessibility.
187
190
 
@@ -12,14 +12,117 @@ rather than the CSS box.
12
12
  ```html demo
13
13
  <label class="field">
14
14
  <span class="field-label">Volume</span>
15
- <input class="range" type="range" min="0" max="100" value="50" />
15
+ <input class="range" data-enhance="range" type="range" min="0" max="100" value="50" />
16
16
  </label>
17
17
  ```
18
18
 
19
- Actual deliberately does not emulate filled tracks, tick marks, value labels,
20
- or multi-thumb ranges. Those still require engine-specific pseudo-elements or a
21
- value duplicated out of the HTML into CSS. Prefer the platform as richer range
22
- styling becomes interoperable — see
19
+ ## Fill
20
+
21
+ `data-enhance="range"` opts into the `actual-css/js/range` behavior. It writes
22
+ `--range-progress`, the fill share the track gradient reads, and keeps it in
23
+ sync on input and after a form reset, including inputs associated through
24
+ `form="…"`. Without it the variable stays at its `0%` default and the track
25
+ renders plain — the control stays fully usable without JavaScript. The fill
26
+ follows the writing direction, and a bare `input[type="range"]` without the
27
+ `.range` class can opt in the same way.
28
+
29
+ ## Named values
30
+
31
+ A `datalist` is the source of truth for discrete named steps: positions plus
32
+ names. Browsers render its ticks and labels inconsistently, so Actual never
33
+ depends on their presentation — the enhancement only reads `option` values and
34
+ their `label`. A matching label sets `aria-valuetext` on the input and mirrors
35
+ into each `<output for="…">`. The output is the visible value, not the input's
36
+ name — give the input its own accessible name, since `for` only declares the
37
+ output's operands.
38
+
39
+ ```html demo
40
+ <div class="field">
41
+ <output class="field-label" for="effort">High</output>
42
+ <input
43
+ class="range"
44
+ data-enhance="range"
45
+ id="effort"
46
+ type="range"
47
+ min="0"
48
+ max="3"
49
+ step="1"
50
+ value="2"
51
+ list="effort-levels"
52
+ aria-label="Effort"
53
+ />
54
+ <datalist id="effort-levels">
55
+ <option value="0" label="Minimal"></option>
56
+ <option value="1" label="Standard"></option>
57
+ <option value="2" label="High"></option>
58
+ <option value="3" label="Maximum"></option>
59
+ </datalist>
60
+ </div>
61
+ ```
62
+
63
+ `aria-valuetext` is the announced value when a number really means a text; keep
64
+ it even if the visible `output` changes shape. An `output` is a live region,
65
+ so verify with a screen reader that the two do not double-announce.
66
+
67
+ ## In a flyout
68
+
69
+ The range stays an `input`, the flyout stays a flyout — together they read as
70
+ a richer picker with no new component. `data-flyout-auto-close="outside"` keeps
71
+ dragging the slider from closing the panel.
72
+
73
+ ```html demo
74
+ <div class="flyout-trigger">
75
+ <button
76
+ class="btn outline"
77
+ type="button"
78
+ data-enhance="flyout"
79
+ aria-expanded="false"
80
+ aria-controls="effort-picker"
81
+ >
82
+ Reasoning effort
83
+ </button>
84
+
85
+ <div
86
+ class="flyout"
87
+ id="effort-picker"
88
+ data-flyout-auto-close="outside"
89
+ style="--flyout-inline-size: 16rem; --flyout-pad: var(--space-40)"
90
+ hidden
91
+ >
92
+ <div class="field">
93
+ <output class="field-label text-center" for="effort-flyout">High</output>
94
+
95
+ <input
96
+ class="range"
97
+ data-enhance="range"
98
+ id="effort-flyout"
99
+ type="range"
100
+ min="0"
101
+ max="3"
102
+ step="1"
103
+ value="2"
104
+ list="effort-flyout-levels"
105
+ aria-label="Reasoning effort"
106
+ >
107
+
108
+ <datalist id="effort-flyout-levels">
109
+ <option value="0" label="Minimal"></option>
110
+ <option value="1" label="Standard"></option>
111
+ <option value="2" label="High"></option>
112
+ <option value="3" label="Maximum"></option>
113
+ </datalist>
114
+ </div>
115
+
116
+ <div class="cluster" style="--cluster-justify: center">
117
+ <button class="btn primary sm" type="button" data-flyout-close>Done</button>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ ```
122
+
123
+ Actual deliberately does not emulate tick marks or multi-thumb ranges. Ticks
124
+ still need engine-specific pseudo-elements; the fill above is designed for
125
+ deletion once `::slider-fill` is interoperable — see
23
126
  [Enhanced range controls](https://github.com/lekoala/actual-css/blob/master/docs/design-notes/platform-alignment.md).
24
127
 
25
128
  ## CSS hooks
@@ -28,3 +131,5 @@ styling becomes interoperable — see
28
131
  - `--range-track-height` — track thickness.
29
132
  - `--range-thumb-bg` — thumb color. Defaults to the local intent, then `--primary`.
30
133
  - `--range-track-bg` — track color.
134
+ - `--range-fill-bg` — filled portion color. Defaults to the local intent, then `--primary`.
135
+ - `--range-progress` — fill share. Written by `actual-css/js/range`, never set by hand.
@@ -0,0 +1,110 @@
1
+ # Rich text
2
+
3
+ > A rich-text editor is not shipped by the framework — the native `<textarea>` stays the baseline. Enhance it with a dedicated engine, themed with Actual tokens.
4
+
5
+ Use a `<textarea>` while editing text, and a hosted editing engine as soon as
6
+ editing means a document (formatting, lists, links, mentions). The
7
+ [rich-text template](../../demo/templates/rich-text.html) enhances a textarea
8
+ with [@lekoala/rich-text](https://github.com/lekoala/rich-text) (Squire
9
+ underneath): the package generates the toolbar with its roving tabindex,
10
+ states, commands and lifecycle, while Actual maps its visual language onto the
11
+ package's `--rt-*` tokens. No adapter, no compatibility layer.
12
+
13
+ **Related terms:** rich text, richtext, WYSIWYG, HTML editor, formatted text, Squire, ProseMirror, Tiptap, text formatting, bold italic, mentions, slash commands.
14
+
15
+ ## Theme bridge
16
+
17
+ The template loads the package JS and CSS from a CDN (`@0.2` caret range) and
18
+ declares one editor per field — Actual never builds toolbar buttons or calls
19
+ into the editing engine:
20
+
21
+ ```html
22
+ <div class="field actual-rich-text">
23
+ <label class="field-label" for="note">Team note</label>
24
+ <rich-text id="note-editor">
25
+ <textarea class="textarea" id="note" name="note" required placeholder="Write a note…"></textarea>
26
+ </rich-text>
27
+ <span class="field-help">Bold, italic, lists, quotes and links.</span>
28
+ </div>
29
+ ```
30
+
31
+ ```js
32
+ editor.options = {
33
+ buttons, // Tabler icons as Nodes, never HTML strings
34
+ requestLink, // an Actual <dialog> replacing window.prompt()
35
+ suggestions, // static @people and /snippets providers
36
+ };
37
+ ```
38
+
39
+ The bridge maps tokens; it does not restyle the editor's internals:
40
+
41
+ ```css
42
+ .actual-rich-text .rt-shell {
43
+ --rt-bg: var(--surface);
44
+ --rt-fg: var(--text);
45
+ --rt-border: var(--form-invalid-border, var(--control-border, var(--border)));
46
+ --rt-divider: var(--border);
47
+ --rt-focus: var(--form-invalid-border, var(--focus));
48
+ --rt-toolbar-bg: var(--surface-subtle);
49
+ --rt-button-pressed-bg: var(--state-selected);
50
+ --rt-link: var(--primary);
51
+ --rt-suggestion-bg: var(--surface-raised);
52
+ --rt-suggestion-shadow: var(--shadow-popout);
53
+ }
54
+ ```
55
+
56
+ The card in the template documents the full token map of this recipe. The
57
+ suggestion popover is a child of `.rt-shell`, so it inherits the tokens like
58
+ the rest of the editor. `--rt-border` paints only the field frame, so the
59
+ invalid and hover hooks never reach the toolbar separators or the quote bar,
60
+ which follow `--rt-divider`.
61
+
62
+ ## Bridge contract
63
+
64
+ `@lekoala/rich-text` owns the editor UI semantics — progressive enhancement,
65
+ sanitization, toolbar behaviour and keyboard, links, `@mentions`, `/`
66
+ suggestions, form/reset/readonly/disabled integration, selection and undo
67
+ lifecycle. Actual owns presentation and composer chrome — tokens, visual
68
+ states, dialogs, attachments, send, `Ctrl/Cmd+Enter`.
69
+
70
+ Formatting buttons stay local to the editor instance. The stateless `command`
71
+ router never drives bold/italic: those depend on selection, schema and history,
72
+ which only the engine knows. Reserve `command` for the chrome around the
73
+ editor (attach, template, dictate, send).
74
+
75
+ A rich composer reuses `.composer` and `.composer-actions` for the outer
76
+ surface; the bridge only lifts the editor and toolbar into the composer grid
77
+ with `display: contents` and grid areas. `composer.css` itself never knows
78
+ about `.rt-*`. The plain-text [composer template](../../demo/templates/composer.html)
79
+ stays the canonical pattern when editing means text, not a document.
80
+
81
+ People lookup and templates are application concerns: the demo wires static
82
+ arrays into the package's suggestion seam, and the composer buttons type
83
+ `@`/`/` so the providers do the rest. Attachments stay composer-owned —
84
+ logged, never serialized into the HTML.
85
+
86
+ ## Storage and sanitization
87
+
88
+ The form submits limited HTML, not engine state: portable, readable,
89
+ editor-independent, reversible. Sanitization is mandatory inside the package
90
+ and not configurable; the client-side sanitizer is a UI boundary, not the
91
+ persistence security boundary — sanitize again on the server before storing or
92
+ rendering untrusted content. An editor holding only the empty block serializes
93
+ to `""`, so native `required` keeps working.
94
+
95
+ If the product later needs truly structural content (custom document nodes,
96
+ annotations, collaboration), a schema-based engine becomes interesting — see
97
+ the [rich-text design note](https://github.com/lekoala/actual-css/blob/master/docs/design-notes/rich-text.md).
98
+
99
+ ## Baseline traps
100
+
101
+ The textarea is the value owner and the no-JS fallback, holding raw HTML. Two
102
+ details keep it honest:
103
+
104
+ - Without the integration script the page remains a plain textarea — the link
105
+ dialog wiring and the suggestion providers only exist once the package
106
+ loads, and the composer falls back to plain-text send.
107
+ - `required` cannot sit usefully on a hidden textarea: mirror validity with
108
+ `aria-invalid` on submit (the package emits `richtext:invalid` and focuses
109
+ the editor when it is the form's first invalid control), and let the bridge
110
+ re-own the danger hook from there.
@@ -55,7 +55,11 @@ Native validation first; the enhancer only adds state and focus behavior.
55
55
 
56
56
  Actual CSS ships validation *styles*. Invalid fields are marked with `aria-invalid="true"` (or the manual `.field.danger` wrapper class). The default forms bundle imports `forms/validation.css`; custom builds may omit that file when validation styling is app-owned. A small enhancer (`actual-css/js/validation`), included in the full runtime (`actual-css/js/full`) and also importable on its own, prevents premature error display, marks invalid fields on blur and submit, focuses the first invalid field on submit, and supports a few custom rules. It is not a validation framework — server and AJAX validation stay in app code.
57
57
 
58
- Opt in with the `.needs-validation` class. Importing the module registers the behavior; there is no init call.
58
+ Opt in with `data-enhance="validation"` on the form; `.needs-validation` alone
59
+ only enables the CSS feedback. Importing the module registers the behavior;
60
+ there is no init call. To opt in forms you cannot mark up, call
61
+ `applyEnhancement("validation", "form.needs-validation")` from
62
+ `actual-css/js/enhance`.
59
63
 
60
64
  ```html demo
61
65
  <form class="needs-validation" data-enhance="validation" data-validation-message="Please check the highlighted fields.">
@@ -122,9 +122,9 @@ application CSS and keep forms and intent ink out of it:
122
122
 
123
123
  ```css
124
124
  .site-band {
125
- --heading: var(--surface);
125
+ --heading: var(--surface-solid-fg);
126
126
  background: var(--surface-solid);
127
- color: var(--surface);
127
+ color: var(--surface-solid-fg);
128
128
  }
129
129
  ```
130
130
 
@@ -135,6 +135,49 @@ there: the theme contract keeps `--focus` at 3:1 on `--surface-solid`.
135
135
 
136
136
  → Foundations · Tokens (theme contract) · Examples · Contrast contexts
137
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
+
138
181
  ### Replace the palette with my own brand
139
182
 
140
183
  Override the whole minimal recolor set (Tokens · Theme contract), not just
@@ -165,4 +208,7 @@ line. Use `.badge.soft` for a subtle badge. It is a visual theme, not a
165
208
  Bootstrap compatibility layer. The `gradient` example keeps a Bootstrap 4 style
166
209
  outer halo on focused fields, for comparison. The `material` example restyles
167
210
  fields as filled underlines with pill buttons — tokens plus three small rules,
168
- and `.join` still needs no adaptation.
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.
@@ -76,18 +76,25 @@ Surface tokens describe the canvas and elevation model. Components should prefer
76
76
  }
77
77
  ```
78
78
 
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 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.
80
87
 
81
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.
82
89
 
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.
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.
84
91
 
85
92
  ```css
86
93
  :root {
87
94
  --focus: hsl(268 30% 55%);
88
95
 
89
96
  --focus-outline-color: currentColor;
90
- --focus-outline: calc(var(--border-width) * 2) solid var(--focus-outline-color);
97
+ --focus-outline: var(--focus-ring-width) solid var(--focus-outline-color);
91
98
  --focus-outline-offset: 2px;
92
99
  --focus-ring-width: 2px;
93
100
  }
@@ -381,7 +388,7 @@ A few cross-component helpers:
381
388
  ```css
382
389
  :root {
383
390
  --indicator-offset: 14.65%; /* circle-aware inset */
384
- --indicator-ring: var(--surface);
391
+ --indicator-ring: var(--surface-opaque);
385
392
  --backdrop-color: rgb(0 0 0);
386
393
  --backdrop-opacity: 0.45;
387
394
  --backdrop-fill: rgb(0 0 0 / var(--backdrop-opacity));
@@ -485,12 +492,15 @@ When overriding an intent color, review its paired `--*-fg` and `--*-soft-fg`. A
485
492
 
486
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.
487
494
 
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.
496
+
488
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.
489
498
 
490
499
  A full theme can override:
491
500
 
492
501
  - intent colors and foreground pairs
493
502
  - surface, text, border, focus, and overlay colors
503
+ - `--surface-opaque` and `--surface-solid-fg`, when surfaces are translucent
494
504
  - radius tokens
495
505
  - optional shadow, motion, typography, and soft-variant mix tokens
496
506
 
@@ -534,7 +544,7 @@ A minimal recolor theme (illustrative, not a shipped theme):
534
544
  }
535
545
  ```
536
546
 
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.
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.
538
548
 
539
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.
540
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:
@@ -52,7 +52,7 @@ optimizing — the bare `actual-css` entrypoint and `actual-css/full` both resol
52
52
  to the complete compiled framework:
53
53
 
54
54
  ```html
55
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.10/dist/actual.full.min.css">
55
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/actual-css@0.11/dist/actual.full.min.css">
56
56
  ```
57
57
 
58
58
  The package does not maintain separate partial bundles. Modular entrypoints map
@@ -94,7 +94,7 @@ cells, so they read as a list:
94
94
  - **Components** — `actual-css/css/components`
95
95
  buttons, surfaces, overlays, feedback, navigation, data and composed controls
96
96
  - **Effects** — `actual-css/css/effects`
97
- aura
97
+ aura, shimmer
98
98
  - **Utilities** — `actual-css/css/utilities`
99
99
  base and extra utilities
100
100
 
@@ -92,6 +92,7 @@ example:
92
92
  | `actual-css/js/filter` | Input value filtering on `data-filter` |
93
93
  | `actual-css/js/mask` | Input masks on `data-mask` |
94
94
  | `actual-css/js/password` | Password reveal toggle |
95
+ | `actual-css/js/range` | Range fill and named values (token `range`) |
95
96
  | `actual-css/js/validation` | Form validation (token `validation`) |
96
97
  | `actual-css/js/status` | Singleton status bar on `[data-status][role="status"]` |
97
98
  | `actual-css/js/enhancement-loader` | Declared enhancement manifests |
@@ -108,11 +109,12 @@ needed.
108
109
 
109
110
  The JS is designed to run on its own, without Actual's stylesheet.
110
111
 
111
- - **`data-enhance` tokens** register root-controller behaviors. The runtime
112
- discovers components through `data-enhance="tabs"`, `data-enhance="flyout"`,
113
- `data-enhance="scrollspy"`, and `data-enhance="validation"` — no Actual
114
- presentation class needed. Self-describing attributes (`data-mask`,
115
- `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.
116
118
  - **`selectors.js`** lists the state classes the runtime *writes* (`is-open`,
117
119
  `was-validated`, …). Edit this file (or alias it in a bundler) to
118
120
  match a different CSS framework's state vocabulary. It is a read-side-only
@@ -326,7 +328,6 @@ import { registerCommands } from "actual-css/js/command";
326
328
 
327
329
  registerCommands("--toggle-hidden", {
328
330
  handle(event, trigger, target) {
329
- event.preventDefault();
330
331
  target.hidden = !target.hidden;
331
332
  trigger.setAttribute("aria-expanded", String(!target.hidden));
332
333
  },
@@ -335,10 +336,10 @@ registerCommands("--toggle-hidden", {
335
336
 
336
337
  The default resolver `targetFor(trigger)` looks up `commandfor` by ID in the
337
338
  trigger's document or shadow root. Supply `resolve(trigger)` to validate or
338
- replace that lookup. An optional idempotent `prepare(trigger, target, command)`
339
- callback runs directly before `handle`; built-in behaviors use it for
340
- target-derived semantics. `commandSelector(commands)` builds the matching button
341
- 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
342
343
  applications that unload the module owning the command.
343
344
 
344
345
  ### Dismiss command
@@ -414,10 +415,8 @@ registerEnhancement("autosubmit", (form) => {
414
415
  <form data-enhance="validation autosubmit">
415
416
  ```
416
417
 
417
- `enhancementSelector(name)` returns the matching `[data-enhance~="name"]`
418
- selector (names must match `[a-z][a-z0-9-]*`), and `hasEnhancement(el, name)`
419
- tests it. No core module list is modified — the regression test in
420
- `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.
421
420
 
422
421
  A name is owned once per root: a second `registerEnhancement("autosubmit", …)`
423
422
  on the same root throws, and `disconnect()` releases the name so it can be
@@ -17,9 +17,11 @@ 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:
package/llms.txt CHANGED
@@ -30,9 +30,10 @@ show supported CSS modules and individual enhancer imports without duplicating
30
30
  the full export list here.
31
31
 
32
32
  Presentation (`class`) and behavior are separate layers. `data-enhance` takes
33
- four tokens only — `tabs`, `flyout`, `scrollspy`, `validation`; leaf behaviors
34
- use their own attribute (`data-tooltip`, `data-mask`, `data-filter`,
35
- `data-context-menu`) and `<dialog>` needs none. See
33
+ five built-in tokens — `tabs`, `flyout`, `range`, `scrollspy`, `validation`.
34
+ Leaf behaviors whose opt-in also carries configuration use their own attribute
35
+ (`data-tooltip`, `data-mask`, `data-filter`, `data-context-menu`), and `<dialog>`
36
+ needs none. See
36
37
  [when a token, when a data-*](docs/design-notes/enhancement-contract.md).
37
38
 
38
39
  Behaviour primitives are independently importable for building custom widgets,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actual-css",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Plain CSS component framework with semantic classes, universal variants, small tokens, theme hooks, and progressive enhancements.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.4.2",
@@ -66,6 +66,7 @@
66
66
  "./js/mask": "./src/js/mask.js",
67
67
  "./js/menu": "./src/js/menu.js",
68
68
  "./js/password": "./src/js/password.js",
69
+ "./js/range": "./src/js/range.js",
69
70
  "./js/scrollspy": "./src/js/scrollspy.js",
70
71
  "./js/selectors": "./src/js/selectors.js",
71
72
  "./js/tab": "./src/js/tab.js",
@@ -138,12 +139,15 @@
138
139
  "check:links": "bun scripts/check-doc-links.js",
139
140
  "check:templates": "bun scripts/check-templates.js",
140
141
  "check:markup": "bun scripts/check-markup.js",
142
+ "shot:crop": "bun scripts/shot-crop.js",
141
143
  "shot:forced": "bun scripts/forced-colors-shot.js",
142
144
  "shot:multi": "bun scripts/multi-shot.js",
143
145
  "shot:page": "bun scripts/page-shot.js",
146
+ "shot:states": "bun scripts/shot-states.js",
144
147
  "probe": "bun scripts/probe.js",
145
148
  "serve": "bun scripts/serve.js",
146
149
  "check:sync": "bun scripts/check-sync.js",
150
+ "release": "bun scripts/release.js",
147
151
  "release:notes": "bun scripts/release-notes.js",
148
152
  "test": "bun test",
149
153
  "test:changed": "bun test --changed=master",
@@ -203,6 +203,7 @@
203
203
  "separated",
204
204
  "shadow-context",
205
205
  "shadow-popout-context",
206
+ "shimmer",
206
207
  "sidebar-layout",
207
208
  "skeleton",
208
209
  "sm",
@@ -211,7 +211,7 @@ a.avatar[aria-disabled="true"] {
211
211
  }
212
212
 
213
213
  .avatar-stack > .avatar {
214
- box-shadow: 0 0 0 var(--avatar-stack-ring) var(--surface);
214
+ box-shadow: 0 0 0 var(--avatar-stack-ring) var(--surface-opaque);
215
215
  }
216
216
 
217
217
  .avatar-stack > .avatar + .avatar {
@@ -14,6 +14,7 @@
14
14
  * Public hooks:
15
15
  * --flyout-inline-size
16
16
  * --flyout-max-inline-size
17
+ * --flyout-pad
17
18
  *
18
19
  * Internal:
19
20
  * --menu-item-size (derived from --control-size for menu rows)
@@ -66,7 +67,9 @@
66
67
  );
67
68
  overflow: auto;
68
69
  margin: 0;
69
- padding: var(--space-10);
70
+ /* Tight by default for action menus, whose items carry their own padding.
71
+ Rich panels (forms, pickers) opt into dialog air per instance. */
72
+ padding: var(--flyout-pad, var(--space-10));
70
73
  border: var(--border-width) solid var(--border);
71
74
  border-radius: var(--radius);
72
75
  background: var(--surface-raised);
@@ -43,7 +43,7 @@
43
43
  }
44
44
 
45
45
  .meter:not(meter) > * + * {
46
- border-inline-start: var(--border-width) solid var(--surface);
46
+ border-inline-start: var(--border-width) solid var(--surface-opaque);
47
47
  }
48
48
 
49
49
  .meter::-webkit-meter-bar {
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Docs: docs/pages/components/pagination.md
5
5
  *
6
- * Render .btn elements inside a .pagination list. The active page is marked
7
- * with aria-current="page"; .btn owns the active state styling.
6
+ * Render .btn.ghost elements inside a .pagination list. The active page is
7
+ * marked with aria-current="page"; .btn owns the active state styling, so the
8
+ * current page is the only filled item.
8
9
  *
9
10
  * Truncated ranges are plain text (`.muted`, `aria-hidden="true"`), not .btn
10
11
  * — they are decorative, not actionable. They still get the same box sizing
@@ -56,7 +56,7 @@
56
56
  }
57
57
 
58
58
  .rating > input[type="radio"]:focus-visible {
59
- outline: var(--focus-outline);
59
+ outline: var(--focus-ring-width) solid var(--focus);
60
60
  outline-offset: var(--focus-outline-offset);
61
61
  background: var(--rating-color);
62
62
  }