actual-css 0.1.0 → 0.3.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 (215) hide show
  1. package/CHANGELOG.md +90 -1
  2. package/README.md +25 -8
  3. package/dist/actual-themes.min.css +1 -0
  4. package/dist/actual.css +1492 -931
  5. package/dist/actual.js +1 -3
  6. package/dist/actual.min.css +1 -1
  7. package/dist/optional.css +1048 -0
  8. package/dist/optional.min.css +1 -0
  9. package/docs/design-notes/browser-support.md +95 -6
  10. package/docs/design-notes/cascade-layer.md +1 -1
  11. package/docs/design-notes/enhancement-contract.md +101 -0
  12. package/docs/design-notes/forced-colors.md +105 -0
  13. package/docs/design-notes/naming.md +3 -1
  14. package/docs/design-notes/platform-alignment.md +492 -0
  15. package/docs/design-notes/references.md +8 -0
  16. package/docs/design-notes/widget-primitives.md +119 -0
  17. package/docs/navigation.json +135 -0
  18. package/docs/pages/components/accordion.md +80 -0
  19. package/docs/pages/components/alert.md +203 -0
  20. package/docs/pages/components/aura.md +35 -0
  21. package/docs/pages/components/avatar.md +95 -0
  22. package/docs/pages/components/badge.md +102 -0
  23. package/docs/pages/components/breadcrumb.md +19 -0
  24. package/docs/pages/components/button.md +205 -0
  25. package/docs/pages/components/card.md +183 -0
  26. package/docs/pages/components/chat.md +50 -0
  27. package/docs/pages/components/dialog.md +367 -0
  28. package/docs/pages/components/drawer.md +113 -0
  29. package/docs/pages/components/fab.md +118 -0
  30. package/docs/pages/components/key.md +33 -0
  31. package/docs/pages/components/meter.md +23 -0
  32. package/docs/pages/components/navbar.md +92 -0
  33. package/docs/pages/components/pagination.md +55 -0
  34. package/docs/pages/components/progress.md +29 -0
  35. package/docs/pages/components/skeleton.md +38 -0
  36. package/docs/pages/components/spinner.md +93 -0
  37. package/docs/pages/components/table.md +102 -0
  38. package/docs/pages/enhancements/flyout.md +391 -0
  39. package/docs/pages/enhancements/overview.md +36 -0
  40. package/docs/pages/enhancements/scrollspy.md +71 -0
  41. package/docs/pages/enhancements/status-bar.md +72 -0
  42. package/docs/pages/enhancements/tabs.md +80 -0
  43. package/docs/pages/enhancements/tooltip.md +130 -0
  44. package/docs/pages/examples/overview.md +41 -0
  45. package/docs/pages/forms/choice-cards.md +102 -0
  46. package/docs/pages/forms/controls.md +32 -0
  47. package/docs/pages/forms/detached-actions.md +59 -0
  48. package/docs/pages/forms/floating-field.md +88 -0
  49. package/docs/pages/forms/form-actions.md +56 -0
  50. package/docs/pages/forms/input-policy.md +91 -0
  51. package/docs/pages/forms/joined-controls.md +37 -0
  52. package/docs/pages/forms/labels.md +42 -0
  53. package/docs/pages/forms/otp.md +50 -0
  54. package/docs/pages/forms/overview.md +188 -0
  55. package/docs/pages/forms/password.md +40 -0
  56. package/docs/pages/forms/range.md +17 -0
  57. package/docs/pages/forms/states-and-sizes.md +321 -0
  58. package/docs/pages/forms/sticky-actions.md +187 -0
  59. package/docs/pages/forms/switch.md +31 -0
  60. package/docs/pages/forms/validation.md +241 -0
  61. package/docs/{mental-model.md → pages/foundations/philosophy.md} +33 -18
  62. package/docs/pages/foundations/theming.md +92 -0
  63. package/docs/pages/foundations/tokens.md +421 -0
  64. package/docs/{typography.md → pages/foundations/typography.md} +27 -9
  65. package/docs/pages/guides/bootstrap.md +438 -0
  66. package/docs/pages/guides/introduction.md +157 -0
  67. package/docs/pages/guides/optional-components.md +33 -0
  68. package/docs/pages/guides/progressive-enhancement.md +686 -0
  69. package/docs/pages/guides/tailwind.md +455 -0
  70. package/docs/pages/layout/app-shell.md +25 -0
  71. package/docs/pages/layout/center.md +29 -0
  72. package/docs/pages/layout/cluster.md +57 -0
  73. package/docs/pages/layout/frame.md +30 -0
  74. package/docs/pages/layout/grid.md +142 -0
  75. package/docs/pages/layout/list.md +41 -0
  76. package/docs/pages/layout/media.md +21 -0
  77. package/docs/pages/layout/overview.md +37 -0
  78. package/docs/pages/layout/scroll-snap.md +54 -0
  79. package/docs/pages/layout/sidebar-layout.md +27 -0
  80. package/docs/pages/layout/stack.md +18 -0
  81. package/docs/pages/layout/switcher.md +36 -0
  82. package/docs/pages/layout/topbar.md +23 -0
  83. package/docs/pages/patterns/actions.md +52 -0
  84. package/docs/pages/patterns/code-block.md +36 -0
  85. package/docs/pages/patterns/exclusive-accordion.md +16 -0
  86. package/docs/pages/patterns/hierarchical-nav.md +26 -0
  87. package/docs/pages/patterns/icon-input.md +25 -0
  88. package/docs/pages/patterns/intent-nesting.md +3 -0
  89. package/docs/pages/patterns/navigation-list.md +235 -0
  90. package/docs/pages/patterns/overview.md +58 -0
  91. package/docs/pages/patterns/structured-lists.md +47 -0
  92. package/docs/pages/utilities/accessibility.md +34 -0
  93. package/docs/pages/utilities/flex.md +105 -0
  94. package/docs/pages/utilities/links.md +18 -0
  95. package/docs/pages/utilities/list.md +31 -0
  96. package/docs/pages/utilities/overflow.md +43 -0
  97. package/docs/pages/utilities/overview.md +18 -0
  98. package/docs/pages/utilities/shape.md +25 -0
  99. package/docs/pages/utilities/sizing-wrapping.md +115 -0
  100. package/docs/pages/utilities/spacing.md +79 -0
  101. package/docs/pages/utilities/text.md +43 -0
  102. package/llms.txt +10 -4
  103. package/package.json +30 -16
  104. package/src/css/actual.css +12 -1
  105. package/src/css/base.css +3 -9
  106. package/src/css/components/accordion.css +18 -30
  107. package/src/css/components/actions.css +4 -1
  108. package/src/css/components/alert.css +162 -49
  109. package/src/css/components/avatar.css +16 -10
  110. package/src/css/components/badge.css +82 -39
  111. package/src/css/components/breadcrumb.css +7 -15
  112. package/src/css/components/busy.css +7 -0
  113. package/src/css/components/button.css +82 -38
  114. package/src/css/components/card.css +17 -37
  115. package/src/css/components/drawer.css +30 -13
  116. package/src/css/components/flyout.css +39 -110
  117. package/src/css/components/index.css +1 -0
  118. package/src/css/components/join.css +14 -3
  119. package/src/css/components/key.css +2 -0
  120. package/src/css/components/menu.css +125 -0
  121. package/src/css/components/meter.css +0 -23
  122. package/src/css/components/modal.css +162 -7
  123. package/src/css/components/navbar.css +17 -18
  124. package/src/css/components/overline.css +0 -7
  125. package/src/css/components/pagination.css +1 -1
  126. package/src/css/components/progress.css +8 -26
  127. package/src/css/components/scrollspy.css +2 -2
  128. package/src/css/components/skeleton.css +7 -0
  129. package/src/css/components/spinner.css +10 -11
  130. package/src/css/components/status-bar.css +18 -28
  131. package/src/css/components/surface.css +2 -8
  132. package/src/css/components/tab.css +20 -48
  133. package/src/css/components/table.css +7 -5
  134. package/src/css/components/tooltip.css +14 -7
  135. package/src/css/focus.css +16 -3
  136. package/src/css/forms/choice-card.css +13 -15
  137. package/src/css/forms/choice.css +21 -28
  138. package/src/css/forms/control.css +16 -26
  139. package/src/css/forms/custom-select.css +9 -11
  140. package/src/css/forms/form-actions.css +14 -3
  141. package/src/css/forms/form.css +7 -4
  142. package/src/css/forms/index.css +5 -4
  143. package/src/css/forms/input-icon.css +30 -12
  144. package/src/css/forms/native.css +48 -33
  145. package/src/css/forms/range.css +103 -0
  146. package/src/css/forms/select.css +8 -8
  147. package/src/css/forms/switch.css +14 -13
  148. package/src/css/forms/validation.css +32 -5
  149. package/src/css/grid.css +19 -18
  150. package/src/css/intents.css +2 -2
  151. package/src/css/layout.css +29 -1
  152. package/src/css/optional/aura.css +93 -0
  153. package/src/css/optional/chat.css +105 -0
  154. package/src/css/optional/fab.css +104 -0
  155. package/src/css/optional/floating-field.css +87 -0
  156. package/src/css/optional/index.css +5 -0
  157. package/src/css/optional/layout-extra.css +7 -4
  158. package/src/css/optional/otp.css +115 -0
  159. package/src/css/optional/scroll-snap.css +6 -0
  160. package/src/css/optional/scroller.css +19 -7
  161. package/src/css/optional/typography-fluid.css +5 -0
  162. package/src/css/optional/utilities-extra.css +17 -2
  163. package/src/css/prose.css +18 -8
  164. package/src/css/reset.css +0 -10
  165. package/src/css/theme.css +29 -13
  166. package/src/css/themes/cyberpunk.css +1 -1
  167. package/src/css/themes/dim.css +3 -3
  168. package/src/css/themes/edge.css +11 -37
  169. package/src/css/themes/index.css +3 -3
  170. package/src/css/themes/{dark.css → indigo.css} +5 -4
  171. package/src/css/themes/neon.css +5 -5
  172. package/src/css/themes/square.css +0 -3
  173. package/src/css/tokens.css +81 -21
  174. package/src/css/utilities.css +8 -10
  175. package/src/css/variants.css +37 -65
  176. package/src/js/command.js +1 -3
  177. package/src/js/context-menu.js +72 -53
  178. package/src/js/dialog.js +68 -79
  179. package/src/js/dismiss.js +15 -0
  180. package/src/js/enhance.js +76 -19
  181. package/src/js/enhancement-loader.js +301 -0
  182. package/src/js/events.js +3 -0
  183. package/src/js/filter.js +3 -3
  184. package/src/js/floating.js +101 -131
  185. package/src/js/flyout.js +106 -96
  186. package/src/js/focus-group.js +116 -0
  187. package/src/js/focus.js +6 -1
  188. package/src/js/index.js +4 -2
  189. package/src/js/input.js +1 -1
  190. package/src/js/keys.js +27 -0
  191. package/src/js/mask.js +7 -6
  192. package/src/js/menu.js +89 -35
  193. package/src/js/parse-config.js +47 -0
  194. package/src/js/password.js +9 -1
  195. package/src/js/scrollspy.js +133 -62
  196. package/src/js/selectors.js +14 -31
  197. package/src/js/status.js +16 -14
  198. package/src/js/surface.js +219 -125
  199. package/src/js/tab.js +46 -66
  200. package/src/js/tooltip.js +53 -31
  201. package/src/js/validation.js +45 -51
  202. package/dist/actual.js.map +0 -31
  203. package/docs/README.md +0 -26
  204. package/docs/components.md +0 -860
  205. package/docs/design-notes/previous-work.md +0 -5
  206. package/docs/forms.md +0 -1265
  207. package/docs/javascript.md +0 -426
  208. package/docs/layout.md +0 -480
  209. package/docs/patterns.md +0 -415
  210. package/docs/reserved.md +0 -7
  211. package/docs/tokens.md +0 -455
  212. package/docs/ui.md +0 -937
  213. package/docs/utilities.md +0 -384
  214. package/src/css/dialog-fallback.css +0 -56
  215. package/src/js/dialog-fallback.js +0 -163
@@ -1,426 +0,0 @@
1
- # JavaScript
2
-
3
- ## Overview
4
-
5
- > Small progressive enhancers that attach to semantic HTML and clean themselves up when the DOM changes.
6
-
7
- Actual's JavaScript modules are optional. Importing `actual-css/js` registers the
8
- complete default runtime — every built-in enhancer, including validation and
9
- status; importing a specific module registers only that behavior.
10
-
11
- ```js
12
- import "actual-css/js";
13
- import "actual-css/js/dialog";
14
- import "actual-css/js/filter";
15
- import "actual-css/js/mask";
16
- ```
17
-
18
- The runtime does not require `DOMContentLoaded` or manual init calls. Components
19
- with a real element lifecycle use the shared `enhance()` helper: initial matching
20
- elements are connected, inserted matching elements are connected later, and
21
- removed elements run cleanup. Declarative `command` / `commandfor` actions use a
22
- single delegated click listener instead; triggers and targets are never tracked.
23
-
24
- `enhance()` observes DOM insertions and removals, not attribute changes. If an
25
- already-connected element receives a behavior attribute later, call the returned
26
- `refresh(node)` handle. Behavior attributes are setup-time contracts: removing
27
- one does not disconnect an active behavior. If an application needs a live
28
- enable/disable switch, keep that state inside its enhancer and clean it up when
29
- the element leaves the DOM.
30
-
31
- ### Side-effect modules
32
-
33
- All JS modules are side-effect modules: importing them registers behavior
34
- automatically. There is no init call, public global object, or configuration.
35
- This keeps imports declarative and tree-shakeable.
36
-
37
- ```js
38
- import "actual-css/js"; // complete default runtime (all enhancers)
39
- import "actual-css/js/dialog"; // single feature
40
- ```
41
-
42
- Use `actual-css/js` for demos, playgrounds, and smoke tests where one import
43
- enables every built-in behavior. Production apps can import individual modules
44
- such as `actual-css/js/validation` or `actual-css/js/status` when only those
45
- are needed.
46
-
47
- Loading the built `dist/actual.js` as a plain `<script>` (no `type="module"`,
48
- no bundler, no `import`) gets the full runtime — declarative triggers such as
49
- status's `command="--status"` work immediately. Modules never touch `window`;
50
- there is no global to call into. Static HTML that needs to trigger something
51
- from its own inline `<script>` dispatches the module's public event instead
52
- (see `actual-css/js/status`'s `actual:status`, for example):
53
-
54
- ```html
55
- <script src="actual.js"></script>
56
- <script>
57
- document.dispatchEvent(new CustomEvent("actual:status", {
58
- bubbles: true,
59
- detail: { message: "Saved.", intent: "success" },
60
- }));
61
- </script>
62
- ```
63
-
64
- Modules are safe to import during SSR — registration is a no-op when there is no
65
- DOM. The runtime remains active until the page unloads. There is no teardown
66
- handle; cleanup is per-element when an element leaves the DOM.
67
-
68
- ## Using The Runtime With Another CSS Framework
69
-
70
- The JS is designed to be usable on its own, without Actual's stylesheet. Two
71
- kinds of vocabulary appear in the modules, and only one of them is Actual's:
72
-
73
- - **data-\* attributes and ARIA** are each module's own behavior API —
74
- `data-mask`, `data-tooltip`, `aria-controls`, and so on. They are
75
- framework-neutral by construction and never need remapping. A name specific
76
- enough to the module (`data-mask`, `data-tooltip`) is safe to match alone.
77
- A generic domain word (`data-status` — order status, task status, …) is
78
- not: some unrelated element elsewhere in the app could carry the same
79
- attribute for its own reasons, so that selector is paired with a class (see
80
- `status.js` below) to scope the document-wide lookup down to the real target.
81
- - **CSS class names** (`.flyout`, `.tabs`, `is-open`, `has-modal-open`, …) are
82
- Actual's own vocabulary. Every module that matches against or writes one of
83
- these imports it from `actual-css/js/selectors` instead of hardcoding the
84
- string:
85
-
86
- ```js
87
- // src/js/selectors.js
88
- export const CLASSES = {
89
- flyout: "flyout",
90
- tabs: "tabs",
91
- open: "is-open",
92
- // …
93
- };
94
- ```
95
-
96
- To run this runtime against a different CSS framework, edit that one file (or
97
- alias it in a bundler) — no other module needs to change. `status.js` is the
98
- example that draws the line most clearly: its selector requires
99
- `.status-bar` (from `CLASSES.statusBar`, for the collision-safety reason
100
- above), but its `intent` option applies whatever class names the caller
101
- passes straight to the target's class list — `.danger`, `.success`, and the
102
- rest of Actual's intents.css vocabulary are never referenced by the module
103
- itself. Each call resets the target to the class list it had the first time
104
- `status()` ran, then adds the current call's intent classes, so nothing
105
- accumulates and classes present before JavaScript ever ran are left alone.
106
-
107
- ## Extending The Runtime
108
-
109
- ### Stateless commands
110
-
111
- Use `registerCommands()` for a target-oriented action that can be fully resolved
112
- when it happens. The command router keeps one handler per command name and one
113
- click listener per document; it does not scan, observe, or retain buttons and
114
- targets. Injected markup, changed `commandfor` values, and replacement targets
115
- therefore work immediately.
116
-
117
- Custom command names start with `--`, following the native invoker convention.
118
- Keep accessibility state that must exist before interaction in the HTML itself.
119
-
120
- ```html
121
- <button type="button" commandfor="details" command="--toggle-hidden"
122
- aria-controls="details">Toggle details</button>
123
- <section id="details" hidden>Details…</section>
124
- ```
125
-
126
- ```js
127
- import { registerCommands } from "actual-css/js/command";
128
-
129
- registerCommands("--toggle-hidden", {
130
- handle(event, trigger, target) {
131
- event.preventDefault();
132
- target.hidden = !target.hidden;
133
- trigger.setAttribute("aria-expanded", String(!target.hidden));
134
- },
135
- });
136
- ```
137
-
138
- The default resolver looks up `commandfor` by ID in the trigger's document or
139
- shadow root. Supply `resolve(trigger)` to validate or replace that lookup.
140
- An optional idempotent `prepare(trigger, target, command)` callback runs directly
141
- before `handle`; built-in behaviors use it for target-derived semantics.
142
- The returned handle has an idempotent `disconnect()` method for applications
143
- that unload the module owning the command.
144
-
145
- ### Stateful enhancers
146
-
147
- Use `enhance()` for project-specific behavior. It accepts a selector map where
148
- each function receives the matched element and may return a cleanup function.
149
-
150
- ```js
151
- import enhance from "actual-css/js/enhance";
152
-
153
- enhance({
154
- "[data-copy]": (button) => {
155
- const controller = new AbortController();
156
-
157
- button.addEventListener("click", async () => {
158
- const target = document.querySelector(button.dataset.copy);
159
- if (!target) return;
160
- await navigator.clipboard.writeText(target.textContent ?? "");
161
- }, { signal: controller.signal });
162
-
163
- return () => controller.abort();
164
- },
165
- });
166
- ```
167
-
168
- Prefer explicit app attributes such as `data-copy`, `data-autogrow`, or
169
- `data-ajax`. Actual's built-in attributes are intentionally small contracts; app
170
- behavior can be richer without overloading them.
171
-
172
- ## Lifecycle Rules
173
-
174
- - Keep selectors opt-in.
175
- - Return cleanup for every listener, observer, timer, or pending controller.
176
- - Use `AbortController` for event listeners whenever possible.
177
- - Make the unenhanced HTML useful first, then add behavior.
178
- - Dispatch a custom event when app code may need to react.
179
- - Keep modules safe to import without a DOM.
180
-
181
- ## Toggle Button Groups
182
-
183
- Actual styles `.btn[aria-pressed="true"]`; application code should update the attribute. Use grouped buttons when the state belongs to the current page, not for navigation between pages.
184
-
185
- ```html
186
- <div class="join" role="group" aria-label="Text style" data-toggle-group>
187
- <button class="btn outline" type="button" aria-pressed="false">Bold</button>
188
- <button class="btn outline" type="button" aria-pressed="false">Italic</button>
189
- <button class="btn outline" type="button" aria-pressed="false">Underline</button>
190
- </div>
191
-
192
- <div class="join" role="group" aria-label="Alignment" data-toggle-group="single">
193
- <button class="btn outline" type="button" aria-pressed="true">Left</button>
194
- <button class="btn outline" type="button" aria-pressed="false">Center</button>
195
- <button class="btn outline" type="button" aria-pressed="false">Right</button>
196
- </div>
197
- ```
198
-
199
- ```js
200
- import enhance from "actual-css/js/enhance";
201
-
202
- enhance({
203
- "[data-toggle-group]": (group) => {
204
- const controller = new AbortController();
205
-
206
- group.addEventListener("click", (event) => {
207
- if (!(event.target instanceof Element)) return;
208
-
209
- const button = event.target.closest("button[aria-pressed]");
210
- if (!button || !group.contains(button)) return;
211
-
212
- if (group.dataset.toggleGroup === "single") {
213
- for (const item of group.querySelectorAll("button[aria-pressed]")) {
214
- item.setAttribute("aria-pressed", String(item === button));
215
- }
216
- return;
217
- }
218
-
219
- const pressed = button.getAttribute("aria-pressed") === "true";
220
- button.setAttribute("aria-pressed", String(!pressed));
221
- }, { signal: controller.signal });
222
-
223
- return () => controller.abort();
224
- },
225
- });
226
- ```
227
-
228
- ## Custom Text Filters
229
-
230
- `data-filter` is intentionally limited to Actual's built-in filters. For domain
231
- rules such as currency, signed numbers, product codes, or time ranges, create an
232
- app-specific filter attribute.
233
-
234
- Use the input helpers when you need the same composition and caret behavior as
235
- Actual's built-in filters.
236
-
237
- ```js
238
- import enhance from "actual-css/js/enhance";
239
- import { dispatchInput, onTextInput, selectionStart, setCaret } from "actual-css/js/input";
240
-
241
- const filters = {
242
- currency(value) {
243
- const normalized = value.replace(/,/g, ".");
244
- const [whole, decimals = ""] = normalized.replace(/[^0-9.]/g, "").split(".");
245
- return decimals ? `${whole}.${decimals.slice(0, 2)}` : whole;
246
- },
247
- time(value) {
248
- const digits = value.replace(/\D/g, "").slice(0, 4);
249
- return digits.length > 2 ? `${digits.slice(0, 2)}:${digits.slice(2)}` : digits;
250
- },
251
- };
252
-
253
- enhance({
254
- "input[data-app-filter]": (input) => {
255
- const filter = filters[input.dataset.appFilter];
256
- if (!filter) return;
257
-
258
- const controller = new AbortController();
259
- let filtering = false;
260
-
261
- onTextInput(input, () => {
262
- if (filtering) return;
263
-
264
- const caret = selectionStart(input);
265
- const previous = input.value;
266
- const next = filter(previous);
267
- if (next === previous) return;
268
-
269
- input.value = next;
270
- setCaret(input, filter(previous.slice(0, caret)).length);
271
-
272
- try {
273
- filtering = true;
274
- dispatchInput(input);
275
- } finally {
276
- filtering = false;
277
- }
278
- }, controller.signal);
279
-
280
- return () => controller.abort();
281
- },
282
- });
283
- ```
284
-
285
- ```html
286
- <input inputmode="decimal" data-app-filter="currency" autocomplete="off" />
287
- <input inputmode="numeric" data-app-filter="time" autocomplete="off" />
288
- ```
289
-
290
- ## Textarea Autogrow
291
-
292
- Textarea autogrow is a good fit for a small local enhancer because product needs
293
- vary: minimum rows, maximum height, scroll behavior, and resize handles are all
294
- app decisions.
295
-
296
- ```js
297
- import enhance from "actual-css/js/enhance";
298
-
299
- enhance({
300
- "textarea[data-autogrow]": (textarea) => {
301
- const controller = new AbortController();
302
-
303
- function resize() {
304
- textarea.style.height = "auto";
305
- textarea.style.height = `${textarea.scrollHeight}px`;
306
- }
307
-
308
- textarea.addEventListener("input", resize, { signal: controller.signal });
309
- resize();
310
-
311
- return () => {
312
- controller.abort();
313
- textarea.style.height = "";
314
- };
315
- },
316
- });
317
- ```
318
-
319
- ## Ajax Forms
320
-
321
- Keep the native form as the baseline: set `action`, `method`, fields, and submit
322
- buttons normally. The enhancer can intercept submission only when JavaScript is
323
- available.
324
-
325
- ```js
326
- import enhance from "actual-css/js/enhance";
327
-
328
- enhance({
329
- "form[data-ajax]": (form) => {
330
- const controller = new AbortController();
331
-
332
- form.addEventListener("submit", async (event) => {
333
- event.preventDefault();
334
-
335
- const submitter = event.submitter;
336
- const data = new FormData(form, submitter);
337
- const response = await fetch(form.action, {
338
- method: form.method || "GET",
339
- body: form.method.toUpperCase() === "GET" ? null : data,
340
- signal: controller.signal,
341
- });
342
-
343
- form.dispatchEvent(new CustomEvent("actual:ajax", {
344
- bubbles: true,
345
- detail: { response },
346
- }));
347
- }, { signal: controller.signal });
348
-
349
- return () => controller.abort();
350
- },
351
- });
352
- ```
353
-
354
- For htmx-like fragment swaps, keep the first version narrow: one trigger, one
355
- target, one swap rule. If the behavior grows into history management, out-of-band
356
- swaps, request queues, and event policies, use a dedicated library.
357
-
358
- ```js
359
- import enhance from "actual-css/js/enhance";
360
-
361
- enhance({
362
- "[data-get][data-target]": (trigger) => {
363
- const controller = new AbortController();
364
-
365
- trigger.addEventListener("click", async () => {
366
- const target = document.querySelector(trigger.dataset.target);
367
- if (!target) return;
368
-
369
- const response = await fetch(trigger.dataset.get, {
370
- signal: controller.signal,
371
- });
372
- target.innerHTML = await response.text();
373
- }, { signal: controller.signal });
374
-
375
- return () => controller.abort();
376
- },
377
- });
378
- ```
379
-
380
- ## Form Validation
381
-
382
- `actual-css/js/validation` is part of the default runtime (`actual-css/js`)
383
- and also importable on its own — it is an enhancer, not a validation framework.
384
- Forms opt in with `.needs-validation`; the behavior registers automatically when
385
- the runtime loads. Native HTML constraint validation runs first, then the
386
- enhancer adds `aria-invalid="true"` to invalid fields on submit,
387
- focuses the first invalid field, and dispatches a bubbling `actual:invalid`
388
- event with `{ form, firstInvalid, message }`. Valid fields are not marked
389
- automatically.
390
-
391
- ```html
392
- <form class="needs-validation" data-validation-message="Please check the fields.">
393
- <input class="input" name="email" type="email" required
394
- aria-describedby="email-error" />
395
- <span class="field-error" id="email-error">Enter a valid email.</span>
396
- </form>
397
-
398
- <div class="status-bar" data-status role="status" aria-live="polite" aria-atomic="true"></div>
399
- ```
400
-
401
- The status bar auto-wires to `actual:invalid`: the form's `data-validation-message`
402
- appears with the `danger` intent, no manual listener. To handle the summary
403
- yourself instead, omit the status import and listen for the event:
404
-
405
- ```js
406
- document.addEventListener("actual:invalid", (event) => {
407
- const { firstInvalid, message } = event.detail;
408
- // route message to your own surface
409
- });
410
- ```
411
-
412
- Custom rules go in `data-validation-rules` and resolve through
413
- `FormValidator.registerRule`. The library never performs network validation;
414
- server errors flow back through `FormValidator.setErrors(form, { name: message })`
415
- and `FormValidator.clearFieldError(field)`.
416
-
417
- ```js
418
- import { FormValidator } from "actual-css/js/validation";
419
-
420
- FormValidator.registerRule("slug", (value) =>
421
- value.length === 0 || /^[a-z0-9-]+$/.test(value)
422
- );
423
-
424
- // after an AJAX submit the server reports field errors
425
- FormValidator.setErrors(form, { email: "Already taken" });
426
- ```