actual-css 0.10.0 → 0.11.1

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 (89) hide show
  1. package/CHANGELOG.md +99 -1
  2. package/README.md +6 -2
  3. package/components.json +14 -3
  4. package/dist/actual.full.css +483 -128
  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 +4 -1
  12. package/docs/pages/components/aura.md +12 -13
  13. package/docs/pages/components/badge.md +10 -0
  14. package/docs/pages/components/bleed.md +5 -4
  15. package/docs/pages/components/button.md +1 -1
  16. package/docs/pages/components/chat.md +5 -0
  17. package/docs/pages/components/close.md +9 -3
  18. package/docs/pages/components/dialog.md +145 -13
  19. package/docs/pages/components/meter.md +2 -0
  20. package/docs/pages/components/pagination.md +15 -15
  21. package/docs/pages/components/shimmer.md +57 -0
  22. package/docs/pages/components/skeleton.md +2 -1
  23. package/docs/pages/components/spinner.md +11 -6
  24. package/docs/pages/components/table.md +13 -0
  25. package/docs/pages/enhancements/flyout.md +2 -0
  26. package/docs/pages/enhancements/tabs.md +4 -0
  27. package/docs/pages/examples/overview.md +1 -0
  28. package/docs/pages/forms/choice-cards.md +77 -4
  29. package/docs/pages/forms/combobox.md +16 -1
  30. package/docs/pages/forms/overview.md +4 -1
  31. package/docs/pages/forms/range.md +110 -5
  32. package/docs/pages/forms/rich-text.md +110 -0
  33. package/docs/pages/forms/validation.md +5 -1
  34. package/docs/pages/foundations/theming.md +49 -3
  35. package/docs/pages/foundations/tokens.md +15 -5
  36. package/docs/pages/guides/bootstrap.md +2 -1
  37. package/docs/pages/guides/design-tools.md +36 -0
  38. package/docs/pages/guides/introduction.md +1 -1
  39. package/docs/pages/guides/modular-import.md +1 -1
  40. package/docs/pages/guides/progressive-enhancement.md +13 -14
  41. package/docs/pages/guides/tailwind.md +4 -2
  42. package/llms.txt +4 -3
  43. package/package.json +6 -1
  44. package/reserved-classes.json +1 -0
  45. package/skills/actual-css/references/enhancements.md +35 -0
  46. package/src/cli/actual-css.js +49 -6
  47. package/src/css/components/avatar.css +1 -1
  48. package/src/css/components/bleed.css +5 -5
  49. package/src/css/components/button.css +14 -1
  50. package/src/css/components/close.css +17 -6
  51. package/src/css/components/flyout.css +4 -1
  52. package/src/css/components/meter.css +6 -3
  53. package/src/css/components/modal.css +123 -7
  54. package/src/css/components/pagination.css +3 -2
  55. package/src/css/components/rating.css +1 -1
  56. package/src/css/components/status-bar.css +2 -2
  57. package/src/css/components/steps.css +3 -3
  58. package/src/css/components/tab.css +3 -3
  59. package/src/css/components/table.css +6 -0
  60. package/src/css/components/tooltip.css +1 -1
  61. package/src/css/core/focus.css +14 -4
  62. package/src/css/core/theme.css +8 -0
  63. package/src/css/core/tokens.css +8 -4
  64. package/src/css/core/variants.css +2 -1
  65. package/src/css/effects/aura.css +51 -33
  66. package/src/css/effects/index.css +1 -0
  67. package/src/css/effects/shimmer.css +57 -0
  68. package/src/css/forms/choice-card.css +71 -23
  69. package/src/css/forms/control.css +2 -2
  70. package/src/css/forms/native.css +5 -1
  71. package/src/css/forms/otp.css +9 -1
  72. package/src/css/forms/range.css +64 -21
  73. package/src/js/command.js +61 -32
  74. package/src/js/dialog.js +8 -23
  75. package/src/js/dismiss.js +1 -2
  76. package/src/js/enhance.js +84 -123
  77. package/src/js/full.js +1 -0
  78. package/src/js/password.js +1 -8
  79. package/src/js/range.js +151 -0
  80. package/src/js/scrollspy.js +4 -5
  81. package/src/js/status.js +2 -10
  82. package/src/js/tooltip.js +11 -1
  83. package/src/js/validation.js +4 -10
  84. package/src/tooling/design-components.js +286 -0
  85. package/src/tooling/design-export.js +69 -0
  86. package/src/tooling/design-tokens.js +466 -0
  87. package/src/tooling/penpot-plugin/manifest.json +7 -0
  88. package/src/tooling/penpot-plugin/plugin.js +272 -0
  89. package/src/tooling/penpot-plugin/ui.html +58 -0
@@ -0,0 +1,57 @@
1
+ # Shimmer
2
+
3
+ > **Module** — import `actual-css/css/effects/shimmer` or `actual-css/css/effects`.
4
+
5
+ Shimmer sweeps a highlight across a text label to show an activity in
6
+ progress: an agent working, a chatbot composing, a job processing.
7
+
8
+ ```html demo
9
+ <div class="stack">
10
+ <span class="shimmer" role="status">Working…</span>
11
+ <p class="cluster">
12
+ <span class="spinner" aria-hidden="true"></span>
13
+ <span class="shimmer" role="status">Generating the report…</span>
14
+ </p>
15
+ </div>
16
+ ```
17
+
18
+ **Related terms:** shimmering text, loading text, thinking indicator, pending
19
+ label, typing indicator.
20
+
21
+ Shimmer is presentation only and selects no ARIA attribute. Put
22
+ `role="status"` on a message that should be announced, and `aria-busy="true"`
23
+ on the region whose content is being updated — not on the message because it
24
+ looks busy. Assistive technology may hold back changes inside a busy region
25
+ until it is released.
26
+
27
+ The label rests on `--shimmer-color` and the band passes in
28
+ `--shimmer-highlight`; one pass takes `--duration-shimmer`, the pace shared
29
+ with `.skeleton` and indeterminate `.progress`. With reduced motion, or in a
30
+ browser without `background-clip: text`, the label is plain
31
+ `--shimmer-color` text. Recolor a label through these hooks: its own `color`
32
+ shows only where the sweep does not run.
33
+
34
+ ## Choosing a waiting signal
35
+
36
+ | Primitive | Signals |
37
+ | -------------------------- | -------------------------------------------- |
38
+ | [`.spinner`](spinner.md) | An operation takes time |
39
+ | [`.skeleton`](skeleton.md) | Content is not available yet |
40
+ | `.shimmer` | This label describes an activity in progress |
41
+ | [`.aura`](aura.md) | This object is emphasized |
42
+
43
+ They compose rather than imply one another: `aria-busy` never adds an aura or
44
+ a shimmer. A card being processed can carry several, each for its own reason:
45
+
46
+ ```html demo
47
+ <span class="aura primary">
48
+ <article class="card stack" style="inline-size: 18rem">
49
+ <p><strong>Quarterly import</strong></p>
50
+ <p class="shimmer" role="status">Processing 1,204 rows…</p>
51
+ <div class="stack" aria-busy="true">
52
+ <div class="skeleton" data-shape="text" aria-hidden="true"></div>
53
+ <div class="skeleton" data-shape="text" aria-hidden="true"></div>
54
+ </div>
55
+ </article>
56
+ </span>
57
+ ```
@@ -8,7 +8,8 @@
8
8
  - Use layout utilities for placeholder arrangement.
9
9
  - Only relevant for simple placeholders - use more structural solutions like phantom-ui for complex cases.
10
10
 
11
- **Related terms:** skeleton screen, shimmer, loading placeholder.
11
+ **Related terms:** skeleton screen, shimmer, loading placeholder. For a text
12
+ label showing an activity in progress, see [Shimmer](shimmer.md).
12
13
 
13
14
  ## Class reference
14
15
 
@@ -12,11 +12,15 @@
12
12
 
13
13
  ## Class reference
14
14
 
15
- | Class | Kind | Description |
16
- | -------------- | --------- | ------------------------------------------------------------------ |
17
- | `.spinner` | Component | Circular loading indicator using `currentColor`. |
18
- | `.sm` / `.lg` | Size | Smaller (`0.75em`) or larger (`2rem`) spinner. |
19
- | Shared intents | Intent | `.primary`, `.secondary`, `.success`, `.warning`, `.danger` color. |
15
+ | Class | Kind | Description |
16
+ | -------------------- | --------- | ------------------------------------------------ |
17
+ | `.spinner` | Component | Circular loading indicator using `currentColor`. |
18
+ | `[aria-busy="true"]` | State | Loading region; see Busy state below. |
19
+ | `.sm` / `.lg` | Size | Smaller (`0.75em`) or larger (`2rem`) spinner. |
20
+ | Shared intents | Intent | `.primary`, `.secondary`, `.success`, … |
21
+
22
+ The shared intents are `.primary`, `.secondary`, `.success`, `.warning` and
23
+ `.danger`.
20
24
 
21
25
  ## Basic usage
22
26
 
@@ -68,7 +72,8 @@ Size follows the font (`1em`); use `.sm` / `.lg` rather than sizing it directly.
68
72
 
69
73
  ## Busy state
70
74
 
71
- A container-wide loading state that keeps the underlying content in place. It is
75
+ The spinner is the indicator; `aria-busy` is the state. A container-wide
76
+ loading state keeps the underlying content in place. It is
72
77
  driven by `aria-busy="true"` plus a direct last-child `.spinner` — there is no
73
78
  separate `.busy` class. The spinner is shown as a centered overlay over a faded
74
79
  surface; buttons keep their spinners inline instead (see Button).
@@ -124,6 +124,19 @@ same vocabulary as `.card.compact`.
124
124
  </div>
125
125
  ```
126
126
 
127
+ ## When a table stops reading as a table
128
+
129
+ Responsive strategy is horizontal overflow: the wrap scrolls, the table keeps
130
+ its columns and its column scanability. No `data-label` restructuring, no
131
+ second representation of the data.
132
+
133
+ For datasets that no longer read well as a table on narrow screens, compose a
134
+ different view instead of restructuring the table: a `.list` / `.media` row
135
+ with badges and actions for a CRUD collection, [`.data-list`](data-list.md)
136
+ for one object's properties, or a dedicated data grid for interactive tables
137
+ (sorting, filtering, selection). Two views for two contexts is a feature, not
138
+ a duplication.
139
+
127
140
  ## CSS hooks
128
141
 
129
142
  - `--table-cell-pad` — cell padding; `.compact` re-declares it tighter.
@@ -522,6 +522,8 @@ delay; a number sets the delay in milliseconds.
522
522
 
523
523
  The repetition is the point: the two hooks are different contracts, and only
524
524
  the cap may enter the runtime's `min()`.
525
+ - `--flyout-pad` — panel padding. Defaults to `var(--space-10)`, tight for
526
+ action menus; rich panels opt into dialog air per instance.
525
527
  - `--menu-item-size` — minimum row height of `.menu-item`.
526
528
  - `--menu-item-icon-size` — shared leading-column width for icons and checked-state indicators.
527
529
 
@@ -22,6 +22,10 @@
22
22
  - JavaScript owns roving `tabindex`, `aria-selected`, `hidden`, and keyboard behavior.
23
23
  - Left/Right select tabs and wrap at the ends. Home/End jump to first/last. Down moves focus into the selected panel.
24
24
  - A tab list needs both `.tabs` and `role="tablist"`; `.tab` styles each trigger.
25
+ - Naming follows the container/item convention (like `menu` / `menu-item`):
26
+ `.tabs` is the strip, `.tab` is one trigger. The JS module
27
+ (`actual-css/js/tab`) is named for the file; the behavior token
28
+ (`data-enhance="tabs"`) is named for what it wires.
25
29
  - A `.tab` with an icon uses `--tab-gap` (default `0.375em`) for the space between icon and label.
26
30
  - A tab that is `hidden`, `disabled`, `aria-disabled`, or has no panel is skipped
27
31
  by the arrow keys. An application filtering a tab strip only has to set
@@ -28,6 +28,7 @@ Whole pages, composed the way a real product would.
28
28
  - [Marketing](../../demo/templates/marketing.html) - landing page with typography and fluid type
29
29
  - [Blog](../../demo/templates/blog.html) - article page with prose
30
30
  - [Settings modal](../../demo/templates/settings-modal.html) - two-pane preferences dialog, with the local CSS it needs audited rule by rule
31
+ - [Modal patterns](../../demo/templates/modal-patterns.html) - eight application modals, each the modal surface plus the smallest application CSS it needs
31
32
  - [Workspace settings](../../demo/templates/settings-pricing.html) - plans with visible native choices, preferences and a local segmented tab treatment
32
33
  - [Card compositions](../../demo/templates/card-compositions.html) - editorial, team, sign-in and horizontal cards composed from existing primitives
33
34
  - [Register form](../../demo/templates/register-form.html) - registration page with choice cards, masks, the password toggle, `tel-prefix` validation, an OTP verification dialog, and status feedback
@@ -11,9 +11,31 @@
11
11
  | `.primary` / `.secondary` | Intent | Accent for the selected state. |
12
12
 
13
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.
14
- - Selected items show a primary border, subtle background tint, and a checkmark badge at the top-right corner — round for radio, rounded square for checkbox.
15
- - Pair with grid or flex utilities for group layout. The badge overhangs the top-right corner, so avoid `overflow: hidden` on the direct container.
16
- - Supports disabled, checked, focus-visible, and invalid states.
14
+ - The control shows before any choice: a ring (radio) or a rounded box (checkbox) on the leading edge, level with the first line. Checked, it fills with a check, and the card takes a primary border and a subtle tint.
15
+ - Each child element of the label sits on its own row beside the indicator — a title and a `.muted` detail need no wrapper. Wrap loose text in an element.
16
+ - Stack cards for a list of options (a person, an address, a delivery method), or pair them with grid utilities for options compared side by side.
17
+ - Supports disabled, checked, focus-visible, and invalid states; the resting ring turns danger with the border.
18
+
19
+ ```html demo
20
+ <fieldset class="field-group stack">
21
+ <legend class="field-label">Deliver to</legend>
22
+ <label class="choice-card">
23
+ <input type="radio" name="address" value="home" checked />
24
+ <strong>Home</strong>
25
+ <span class="muted">12 Rue des Tanneurs, 1000 Brussels</span>
26
+ </label>
27
+ <label class="choice-card">
28
+ <input type="radio" name="address" value="work" />
29
+ <strong>Work</strong>
30
+ <span class="muted">Avenue Louise 54, 1050 Ixelles</span>
31
+ </label>
32
+ <label class="choice-card">
33
+ <input type="radio" name="address" value="pickup" />
34
+ <strong>Pickup point</strong>
35
+ <span class="muted">Collect within 7 days</span>
36
+ </label>
37
+ </fieldset>
38
+ ```
17
39
 
18
40
  ```html demo
19
41
  <div class="grid-3">
@@ -94,6 +116,52 @@ follows through its `em` sizing. Padding is density, so use a `.compact` or
94
116
  `.spacious` context to change it. Intent classes (`.primary`, `.secondary`) are
95
117
  supported on the `.choice-card` element.
96
118
 
119
+ ## Corner badge
120
+
121
+ A tile whose content is the choice itself — a product image, a centred plan
122
+ name — may want no indicator column, and a check badge over its corner once
123
+ selected (radio/checkbox tile, selected tick, check in the corner). That is a
124
+ recipe on the card's own `::before` (disc) and `::after` (check) layers,
125
+ written in application CSS after the framework:
126
+
127
+ ```css
128
+ .choice-tile {
129
+ --choice-card-check-size: 1.5em;
130
+
131
+ display: block;
132
+ }
133
+
134
+ .choice-tile::before,
135
+ .choice-tile::after {
136
+ position: absolute;
137
+ inset-inline-end: -0.375em;
138
+ inset-block-start: -0.375em;
139
+ margin: 0;
140
+ }
141
+
142
+ .choice-tile::before {
143
+ border: 0;
144
+ box-shadow: 0 0 0 2px var(--surface-opaque);
145
+ transform: scale(0);
146
+ }
147
+
148
+ .choice-tile:has(:checked)::before {
149
+ transform: scale(1);
150
+ }
151
+ ```
152
+
153
+ ```html
154
+ <label class="choice-card choice-tile">
155
+ <input type="radio" name="cpu" value="1" checked />
156
+ <strong>8-core CPU</strong><br />
157
+ <span class="muted">32 GB RAM</span>
158
+ </label>
159
+ ```
160
+
161
+ The badge overhangs the corner, so no ancestor up to the grid may clip with
162
+ `overflow: hidden`. The [Neon Ramen item page](../../demo/sites/neon-ramen/item.html)
163
+ uses it for its sizes and toppings.
164
+
97
165
  ## Chips
98
166
 
99
167
  Add `.chip` for the compact pill form: filter chips, selectable tags, toggle
@@ -101,6 +169,9 @@ pills, multi-select facets. A radio group gives single-choice chips. A leading
101
169
  dot turns into the check in a fixed slot, so checking never moves the label.
102
170
  Use `.cluster` for a wrapping row. A chip is sized like a control: `.btn` and
103
171
  `.input` height, `--control-pad-x` inline padding, `.sm` / `.lg` scale.
172
+ A chip is a `<label>` around its checkbox/radio — never nest a `.close` or any
173
+ other button inside it. A removable value (combobox tag) belongs to the widget
174
+ that produced it, or to a sibling `.badge` + `.close`, not to the chip label.
104
175
 
105
176
  ```html demo
106
177
  <fieldset class="field-group">
@@ -129,8 +200,10 @@ by widening the clip, as the `material` theme does.
129
200
  ## CSS hooks
130
201
 
131
202
  - `--choice-card-pad` — inner padding.
203
+ - `--choice-card-gap` — space between the indicator column and the label.
132
204
  - `--choice-card-radius` — corner radius.
133
- - `--choice-card-check-size` — size of the check indicator.
205
+ - `--choice-card-check-size` — size of the indicator, and the width of its
206
+ column.
134
207
  - `--choice-card-border` / `--choice-card-bg` — resting border and background;
135
208
  the checked state and hover rebind them, so a checked override needs
136
209
  `:has(:checked)`.
@@ -10,7 +10,7 @@ real product needs a JavaScript combobox (searchable select, removable tags,
10
10
  autocomplete suggestions), load a dedicated component and bridge its custom
11
11
  properties onto Actual's tokens.
12
12
 
13
- **Related terms:** combo box, combobox, autocomplete, datalist, searchable select, enhanced select, multiple select, tags, chips, tag input, pill input.
13
+ **Related terms:** combo box, combobox, autocomplete, datalist, select, custom select, native select, searchable select, enhanced select, multiple select, dropdown, form controls, tags, chips, tag input, pill input.
14
14
 
15
15
  ## Theme bridge
16
16
 
@@ -36,6 +36,21 @@ mix tokens.
36
36
 
37
37
  The card in the template documents the full token map of this recipe.
38
38
 
39
+ ## Bridge contract
40
+
41
+ Actual owns the visual language; the widget owns the behavior. Actual provides
42
+ surface, border, radius, inset focus outline, danger state, `.join` corners,
43
+ and the contrast contracts those tokens carry. The widget provides search,
44
+ keyboard navigation, multi-value state, removable tags, and option rendering.
45
+ The bridge CSS stays in docs/demo — it is a recipe, not package API, so no
46
+ `actual-combobox.css` ships with the framework and no widget version is pinned
47
+ by it.
48
+
49
+ Multi-value and i18n (for example a "n selected" summary string) are widget
50
+ configuration: pass locale strings and tag rendering through the widget's own
51
+ options, and keep skinning on the `--cb-*` tokens so light/dark mode and preset
52
+ themes keep applying for free.
53
+
39
54
  ## Lightweight pass-through
40
55
 
41
56
  Most of the skin is three one-line token bridges. The visible vocabulary —
@@ -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.