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
@@ -8,6 +8,8 @@ and arrow position are written by JavaScript; the element is shown and hidden
8
8
  by promoting it to the top layer with `popover="manual"`.
9
9
 
10
10
  - Use `data-tooltip` on the trigger. With text (`data-tooltip="Help"`), the tooltip element is generated. Empty (`data-tooltip`) marks an explicit tooltip connected via `aria-describedby`.
11
+ - Generated text is a snapshot at show time: changing `data-tooltip` never rewrites a tooltip that is already shown, and an always-visible generated tooltip keeps its initial text. For content that must update dynamically, use an explicit tooltip. See the [tooltips bench](../examples/overview.md) (`demo/templates/tooltips.html`) for all six cases side by side.
12
+ - Generated text is always plain text (`textContent` only), so a shorthand value is safe for untrusted strings once they are in the DOM. An explicit tooltip is author-owned DOM: its sanitization belongs to whoever produces it. Escape the attribute server-side — a DOM already compromised at render cannot be repaired at read time. There is no `data-tooltip-html`, by design.
11
13
  - Tooltips are supplemental. Do not put required information or interactive controls inside them.
12
14
  - Show on hover and focus. Hide on Escape, blur, or pointer leave.
13
15
  - A trigger scrolled out of view takes its tooltip down and brings it back when it returns; only a dismissal ends the tooltip.
@@ -35,7 +37,7 @@ runtime when Actual should own showing and hiding.
35
37
 
36
38
  **Where the element lives is part of the contract.** An explicit tooltip stays
37
39
  exactly where you wrote it, so it keeps every scope that reaches it by
38
- inheritance — a theme island, a density scope, `.inverted`, your own scoped
40
+ inheritance — a theme island, a density scope, your own scoped
39
41
  custom properties. It also means placement is yours to get right: a tooltip
40
42
  used from inside a modal dialog must be authored inside that dialog, because
41
43
  the top layer does not lift an element out of a modal's inertness.
@@ -47,12 +47,13 @@ One mechanism at a time, with its contract and its pitfalls.
47
47
  Contracts you cannot read off a static screenshot: state, motion, focus, and
48
48
  extension points.
49
49
 
50
- - [Surface contracts](../../demo/templates/surfaces.html) - `.inverted`, cascade precedence, and the intent boundary
50
+ - [Contrast contexts](../../demo/templates/contrast-contexts.html) - the page, `data-theme` islands, a card island, a named theme and an application-painted band side by side, with every intent variant, a full form (combobox and date picker included) and focus line measured live, plus joined fields that need no focus adaptation
51
51
  - [Presence motion](../../demo/templates/motion.html) - status bar, tooltip, flyout, modal and drawer replayed side by side, with easing swaps and a reduced-motion simulation
52
52
  - [Keyboard controls](../../demo/templates/keyboard-controls.html) - hands-on keyboard support matrix and focus test bench
53
53
  - [Adaptive filter surface](../../demo/templates/adaptive-surface.html) - app-level flyout/drawer composition with one synchronized filter state
54
54
  - [Select intents](../../demo/templates/select-intents.html) - extending the enhancer with a project's own `data-enhance` behavior, injected markup included, with no framework change
55
55
  - [Popover transport probe](../../demo/templates/popover-transport.html) - self-scoring bench for the native popover transport go/no-go
56
+ - [Tooltips](../../demo/templates/tooltips.html) - generated text is a snapshot at show time: what updates, what stays, and the explicit-tip way for dynamic content
56
57
 
57
58
  ### Third-party components on Actual tokens
58
59
 
@@ -7,12 +7,35 @@
7
7
  | Class | Kind | Description |
8
8
  | ------------------------- | --------- | --------------------------------------------- |
9
9
  | `.choice-card` | Component | Card-style `<label>` around a native control. |
10
+ | `.chip` | Modifier | Compact pill form: filter chips, tags. |
10
11
  | `.primary` / `.secondary` | Intent | Accent for the selected state. |
11
12
 
12
13
  - Use `.choice-card` on a `<label>` wrapping a native `<input type="radio">` or `<input type="checkbox">`. The input is visually hidden; in browsers without `:has()` it stays visible so selection state never disappears.
13
- - 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.
14
- - Pair with grid or flex utilities for group layout. The badge overhangs the top-right corner, so avoid `overflow: hidden` on the direct container.
15
- - 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
+ ```
16
39
 
17
40
  ```html demo
18
41
  <div class="grid-3">
@@ -93,11 +116,100 @@ follows through its `em` sizing. Padding is density, so use a `.compact` or
93
116
  `.spacious` context to change it. Intent classes (`.primary`, `.secondary`) are
94
117
  supported on the `.choice-card` element.
95
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
+
165
+ ## Chips
166
+
167
+ Add `.chip` for the compact pill form: filter chips, selectable tags, toggle
168
+ pills, multi-select facets. A radio group gives single-choice chips. A leading
169
+ dot turns into the check in a fixed slot, so checking never moves the label.
170
+ Use `.cluster` for a wrapping row. A chip is sized like a control: `.btn` and
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.
175
+
176
+ ```html demo
177
+ <fieldset class="field-group">
178
+ <legend class="field-label">Fruit</legend>
179
+ <div class="cluster">
180
+ <label class="choice-card chip">
181
+ <input type="checkbox" name="fruit" value="pear" checked />
182
+ Pear
183
+ </label>
184
+ <label class="choice-card chip">
185
+ <input type="checkbox" name="fruit" value="apple" />
186
+ Apple
187
+ </label>
188
+ <label class="choice-card chip">
189
+ <input type="checkbox" name="fruit" value="plum" disabled />
190
+ Plum
191
+ </label>
192
+ </div>
193
+ </fieldset>
194
+ ```
195
+
196
+ The dot is `::before`: a full-chip layer clipped to a `circle()` centred on
197
+ the check slot (`--choice-chip-dot`). A theme can grow it into a selected fill
198
+ by widening the clip, as the `material` theme does.
199
+
96
200
  ## CSS hooks
97
201
 
98
202
  - `--choice-card-pad` — inner padding.
203
+ - `--choice-card-gap` — space between the indicator column and the label.
99
204
  - `--choice-card-radius` — corner radius.
100
- - `--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.
207
+ - `--choice-card-border` / `--choice-card-bg` — resting border and background;
208
+ the checked state and hover rebind them, so a checked override needs
209
+ `:has(:checked)`.
210
+ - `--choice-chip-dot` — read-only: the chip's check-slot centre as a `circle()`
211
+ position. It is declared on the chip's `::before`, so read it in a
212
+ `.choice-card.chip…::before` rule.
101
213
 
102
214
  Plain `.check` / `.radio` controls expose two hooks:
103
215
 
@@ -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 —
@@ -47,7 +62,7 @@ contrast contracts apply to the combobox for free:
47
62
  .actual-combobox {
48
63
  --cb-bg: var(--surface);
49
64
  --cb-color: var(--text);
50
- --cb-border-color: var(--border);
65
+ --cb-border-color: var(--form-invalid-border, var(--border));
51
66
  --cb-border-radius: var(--radius);
52
67
  --cb-focus-color: var(--focus);
53
68
  --cb-muted-color: var(--text-muted);
@@ -57,15 +72,23 @@ contrast contracts apply to the combobox for free:
57
72
  --cb-option-min-height: var(--control-size);
58
73
  }
59
74
 
60
- .actual-combobox .cb-control:focus-within,
61
- .actual-combobox .cb-text-control:focus {
62
- outline: 2px solid transparent;
63
- box-shadow: var(--focus-ring-shadow);
75
+ .actual-combobox :is(.cb-text-control:focus-visible, .cb-control:has(.cb-input:focus-visible)) {
76
+ border-color: var(--form-invalid-border, var(--cb-border-color));
77
+ outline: var(--focus-ring-width) solid var(--form-invalid-border, var(--focus));
78
+ outline-offset: calc(var(--focus-ring-width) * -1);
79
+ box-shadow: none;
80
+ }
81
+
82
+ .actual-combobox :is(.cb-text-control[aria-invalid="true"], .cb-control:has([aria-invalid="true"])) {
83
+ --form-invalid-border: var(--danger);
84
+ }
85
+
86
+ :where(.actual-combobox) combo-box {
87
+ border-radius: var(--cb-border-radius);
64
88
  }
65
89
 
66
- .actual-combobox .cb-chip-remove:focus-visible {
67
- outline: 2px solid transparent;
68
- box-shadow: 0 0 0 2px var(--focus-ring);
90
+ .actual-combobox :is(.cb-control, .cb-text-control) {
91
+ border-radius: inherit;
69
92
  }
70
93
 
71
94
  .actual-combobox .cb-chip {
@@ -79,9 +102,13 @@ contrast contracts apply to the combobox for free:
79
102
  }
80
103
  ```
81
104
 
82
- The composite focus ring follows any internal focus, not just the search
83
- input: focus a chip or its remove × and the control shows the same field ring,
84
- with the local `:focus-visible` indicator on the chip or its × on top.
105
+ While the search input is focused, the control takes the same inset outline as
106
+ `.input` in place of the library's accent border and outer halo; a focused chip
107
+ or remove × keeps the library's own inner ring. Being inset, the outline needs
108
+ no adaptation inside `.join`: `combo-box` owns the radius at zero specificity,
109
+ so the join gives it the group corners and the visible control inherits them. The invalid rule sets `--form-invalid-border`,
110
+ the hook `.field.danger` and `aria-invalid` already use, so border and outline
111
+ turn danger together.
85
112
 
86
113
  The same approach works for any widget that exposes a custom-property skin —
87
114
  keep the widget's behavior, restyle its tokens through the shared vocabulary.
@@ -29,11 +29,8 @@ When composing source files manually, import `components/join.css` after the con
29
29
  </div>
30
30
  ```
31
31
 
32
- `.join` handles border-radius and border collapsing between adjacent children. It works with any direct child — `.input`, `.btn`, `.select`, or `.join-addon`.
33
- When an `.input`, `.textarea`, or `.select` receives keyboard-relevant focus,
34
- the focus ring surrounds the complete joined field. Attached buttons keep their
35
- own focus indicator when reached directly, so the actionable segment remains
36
- identifiable.
32
+ `.join` handles border-radius and border collapsing between adjacent children. It works with any direct child — `.input`, `.btn`, `.select`, or `.join-addon` — plus one wrapper level (e.g. a combobox element around the native control): the wrapper receives the group corners and the inner control inherits them via `border-radius: inherit`.
33
+ When an `.input`, `.textarea`, or `.select` receives keyboard focus, it keeps its own inset focus outline; the group never moves the ring onto itself, so the focused segment remains identifiable.
37
34
 
38
35
  ## Linting `role="group"`
39
36
 
@@ -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
 
@@ -165,6 +168,12 @@ A full form using the control classes, field wrappers, and form actions:
165
168
  </form>
166
169
  ```
167
170
 
171
+ ## Hooks
172
+
173
+ Set `--control-hover-border` to tune the hover border of `.input` and `.textarea`.
174
+ Set `--control-hover-bg` to keep a `.select` background steady on hover, or give
175
+ it a stronger fill.
176
+
168
177
  ## Notes
169
178
 
170
179
  - `required` is enough on native inputs. `aria-required="true"` is redundant and not used in these examples.
@@ -175,7 +184,7 @@ A full form using the control classes, field wrappers, and form actions:
175
184
  - Use native `<optgroup>` and `disabled` options in `.select` controls when applicable; the customizable picker preserves both as a progressive enhancement.
176
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.
177
186
  - For a switch, use `class="switch"` and `role="switch"`.
178
- - 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.
179
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.
180
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.
181
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.">