actual-css 0.2.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 (214) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +24 -9
  3. package/dist/actual-themes.min.css +1 -0
  4. package/dist/actual.css +1244 -915
  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 +94 -14
  10. package/docs/design-notes/cascade-layer.md +1 -1
  11. package/docs/design-notes/enhancement-contract.md +11 -14
  12. package/docs/design-notes/forced-colors.md +105 -0
  13. package/docs/design-notes/naming.md +2 -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 +37 -1
  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 +4 -4
  103. package/package.json +20 -12
  104. package/src/css/actual.css +12 -1
  105. package/src/css/base.css +3 -5
  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 +126 -99
  109. package/src/css/components/avatar.css +16 -10
  110. package/src/css/components/badge.css +55 -35
  111. package/src/css/components/breadcrumb.css +7 -15
  112. package/src/css/components/busy.css +6 -0
  113. package/src/css/components/button.css +78 -32
  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 -109
  117. package/src/css/components/index.css +1 -0
  118. package/src/css/components/join.css +14 -2
  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/skeleton.css +7 -0
  128. package/src/css/components/spinner.css +6 -7
  129. package/src/css/components/status-bar.css +15 -24
  130. package/src/css/components/surface.css +2 -8
  131. package/src/css/components/tab.css +19 -48
  132. package/src/css/components/table.css +7 -5
  133. package/src/css/components/tooltip.css +14 -7
  134. package/src/css/focus.css +14 -38
  135. package/src/css/forms/choice-card.css +13 -14
  136. package/src/css/forms/choice.css +21 -27
  137. package/src/css/forms/control.css +16 -17
  138. package/src/css/forms/custom-select.css +9 -11
  139. package/src/css/forms/form-actions.css +14 -3
  140. package/src/css/forms/form.css +7 -4
  141. package/src/css/forms/index.css +4 -4
  142. package/src/css/forms/input-icon.css +30 -12
  143. package/src/css/forms/native.css +50 -11
  144. package/src/css/forms/range.css +17 -9
  145. package/src/css/forms/select.css +8 -8
  146. package/src/css/forms/switch.css +14 -12
  147. package/src/css/forms/validation.css +32 -5
  148. package/src/css/grid.css +19 -18
  149. package/src/css/intents.css +2 -2
  150. package/src/css/layout.css +29 -1
  151. package/src/css/optional/aura.css +93 -0
  152. package/src/css/optional/chat.css +105 -0
  153. package/src/css/optional/fab.css +104 -0
  154. package/src/css/optional/floating-field.css +87 -0
  155. package/src/css/optional/index.css +5 -0
  156. package/src/css/optional/layout-extra.css +7 -4
  157. package/src/css/optional/otp.css +115 -0
  158. package/src/css/optional/scroll-snap.css +6 -0
  159. package/src/css/optional/scroller.css +13 -7
  160. package/src/css/optional/typography-fluid.css +5 -0
  161. package/src/css/optional/utilities-extra.css +17 -2
  162. package/src/css/prose.css +18 -8
  163. package/src/css/reset.css +0 -10
  164. package/src/css/theme.css +21 -5
  165. package/src/css/themes/cyberpunk.css +1 -1
  166. package/src/css/themes/dim.css +3 -3
  167. package/src/css/themes/edge.css +8 -10
  168. package/src/css/themes/index.css +3 -3
  169. package/src/css/themes/{dark.css → indigo.css} +5 -4
  170. package/src/css/themes/neon.css +5 -5
  171. package/src/css/themes/square.css +0 -3
  172. package/src/css/tokens.css +74 -20
  173. package/src/css/utilities.css +8 -10
  174. package/src/css/variants.css +37 -65
  175. package/src/js/command.js +1 -3
  176. package/src/js/context-menu.js +67 -35
  177. package/src/js/dialog.js +68 -79
  178. package/src/js/dismiss.js +15 -0
  179. package/src/js/enhance.js +54 -23
  180. package/src/js/enhancement-loader.js +301 -0
  181. package/src/js/events.js +3 -0
  182. package/src/js/filter.js +3 -3
  183. package/src/js/floating.js +101 -131
  184. package/src/js/flyout.js +104 -87
  185. package/src/js/focus-group.js +116 -0
  186. package/src/js/focus.js +6 -1
  187. package/src/js/index.js +4 -2
  188. package/src/js/input.js +1 -1
  189. package/src/js/keys.js +27 -0
  190. package/src/js/mask.js +7 -6
  191. package/src/js/menu.js +87 -34
  192. package/src/js/parse-config.js +47 -0
  193. package/src/js/password.js +9 -1
  194. package/src/js/scrollspy.js +12 -4
  195. package/src/js/selectors.js +0 -7
  196. package/src/js/status.js +3 -1
  197. package/src/js/surface.js +185 -136
  198. package/src/js/tab.js +36 -53
  199. package/src/js/tooltip.js +53 -31
  200. package/src/js/validation.js +34 -8
  201. package/dist/actual.js.map +0 -31
  202. package/docs/README.md +0 -28
  203. package/docs/components.md +0 -860
  204. package/docs/design-notes/previous-work.md +0 -5
  205. package/docs/forms.md +0 -1265
  206. package/docs/javascript.md +0 -496
  207. package/docs/layout.md +0 -480
  208. package/docs/patterns.md +0 -504
  209. package/docs/reserved.md +0 -7
  210. package/docs/tokens.md +0 -455
  211. package/docs/ui.md +0 -929
  212. package/docs/utilities.md +0 -384
  213. package/src/css/dialog-fallback.css +0 -56
  214. package/src/js/dialog-fallback.js +0 -163
@@ -1,496 +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.
71
-
72
- - **data-enhance tokens** register root-controller behaviors. The runtime
73
- discovers components through `data-enhance="tabs"`, `data-enhance="flyout"`,
74
- `data-enhance="scrollspy"`, and `data-enhance="validation"` — no Actual
75
- presentation class needed. Self-describing attributes (`data-mask`,
76
- `data-tooltip`, `data-context-menu`) stay framework-neutral by construction.
77
- - **`selectors.js`** lists state classes the runtime *writes* (`is-open`,
78
- `was-validated`, `is-sheet`, …). Edit this file (or alias it in a bundler)
79
- to match a different CSS framework's state vocabulary. Discovery selectors
80
- were removed in 0.2 — the file no longer bridges CSS class names to JS
81
- initialization.
82
- - **One documented exception:** validation reads `.field-error` (or its alias
83
- `[data-field-error]`) and the optional `.field` ancestor to connect error
84
- slots and toggle danger state. Both are read-side presentation adapters that
85
- degrade to no-op under foreign CSS — `aria-invalid`, focus management,
86
- custom rules, and server errors all keep working without them.
87
- - **status.js** uses `[data-status][role="status"]` — semantics, not a class.
88
- Its `intent` option applies whatever class names the caller passes, so
89
- `.danger`, `.success`, and the rest of intents.css are never hardcoded.
90
-
91
- ## The Runtime as a Primitive Kit
92
-
93
- For building custom widgets (select, tags, date picker) on Actual's primitives,
94
- see [Widget primitives](design-notes/widget-primitives.md). Every primitive
95
- subpath is published in `package.json#exports`:
96
-
97
- ```
98
- actual-css/js/enhance actual-css/js/events actual-css/js/floating
99
- actual-css/js/focus actual-css/js/keys actual-css/js/menu
100
- actual-css/js/surface
101
- ```
102
-
103
- The exports map is explicit — it *is* the statement of what is public.
104
-
105
- ## Floating Contract
106
-
107
- `floating` is a positioning-only primitive with zero CSS dependency: no
108
- `classList`, no `selectors.js` import, no Actual class anywhere. It computes and
109
- exposes `left`/`top`, `data-placement`, `--available-height`,
110
- `--arrow-x`/`--arrow-y`. The caller owns the floating element's presentation
111
- entirely.
112
-
113
- A third-party widget importing only `floating` with no Actual stylesheet must
114
- be able to position a listbox under an input — that is the test.
115
-
116
- ## Surface Contract
117
-
118
- `surface` has documented presentation coupling. A foreign consumer must supply
119
- or account for:
120
-
121
- ```css
122
- .my-panel { position: absolute; z-index: 100; }
123
- .my-panel[hidden] { display: none; }
124
- /* surface.js sets position:fixed + inline left/top on open —
125
- do not override with !important */
126
- /* .is-open / .is-sheet / .surface-backdrop are written by surface.js
127
- — style or ignore them */
128
- /* --flyout-trigger-width is set while an open surface is positioned */
129
- /* data-actual-surface is written by surface.js for teardown
130
- — never select on it */
131
- ```
132
-
133
- Two non-obvious requirements:
134
- 1. `[hidden] { display: none }` must not be defeated by a higher-specificity
135
- `display` rule — `surface.js` toggles `hidden`, not `display`.
136
- 2. The open state switches to `position: fixed` with measured inline
137
- coordinates.
138
-
139
- Lifecycle: `prepareSurface` → `openSurface` → `closeSurface` →
140
- `disconnectSurface`. `openSurface` dispatches a cancelable
141
- `actual:surface-open` event: widgets can veto or decorate opens, and context
142
- menus inject their own source/restoreFocusTo through it.
143
-
144
- Extension seam: a `[role="listbox"]` surface with `[role="option"]` children
145
- gets no keyboard handling and no click-autoclose from `surface.js`.
146
- `[role="option"]` is deliberately not in the menu-item vocabulary — a combobox
147
- reuses the surface lifecycle while retaining full control of its own keyboard
148
- and selection.
149
-
150
- ## Extending The Runtime
151
-
152
- ### Stateless commands
153
-
154
- Use `registerCommands()` for a target-oriented action that can be fully resolved
155
- when it happens. The command router keeps one handler per command name and one
156
- click listener per document; it does not scan, observe, or retain buttons and
157
- targets. Injected markup, changed `commandfor` values, and replacement targets
158
- therefore work immediately.
159
-
160
- Custom command names start with `--`, following the native invoker convention.
161
- Keep accessibility state that must exist before interaction in the HTML itself.
162
-
163
- ```html
164
- <button type="button" commandfor="details" command="--toggle-hidden"
165
- aria-controls="details">Toggle details</button>
166
- <section id="details" hidden>Details…</section>
167
- ```
168
-
169
- ```js
170
- import { registerCommands } from "actual-css/js/command";
171
-
172
- registerCommands("--toggle-hidden", {
173
- handle(event, trigger, target) {
174
- event.preventDefault();
175
- target.hidden = !target.hidden;
176
- trigger.setAttribute("aria-expanded", String(!target.hidden));
177
- },
178
- });
179
- ```
180
-
181
- The default resolver looks up `commandfor` by ID in the trigger's document or
182
- shadow root. Supply `resolve(trigger)` to validate or replace that lookup.
183
- An optional idempotent `prepare(trigger, target, command)` callback runs directly
184
- before `handle`; built-in behaviors use it for target-derived semantics.
185
- The returned handle has an idempotent `disconnect()` method for applications
186
- that unload the module owning the command.
187
-
188
- ### Stateful enhancers
189
-
190
- Use `enhance()` for project-specific behavior. It accepts a selector map where
191
- each function receives the matched element and may return a cleanup function.
192
-
193
- ```js
194
- import enhance from "actual-css/js/enhance";
195
-
196
- enhance({
197
- "[data-copy]": (button) => {
198
- const controller = new AbortController();
199
-
200
- button.addEventListener("click", async () => {
201
- const target = document.querySelector(button.dataset.copy);
202
- if (!target) return;
203
- await navigator.clipboard.writeText(target.textContent ?? "");
204
- }, { signal: controller.signal });
205
-
206
- return () => controller.abort();
207
- },
208
- });
209
- ```
210
-
211
- Prefer explicit app attributes such as `data-copy`, `data-autogrow`, or
212
- `data-ajax`. Actual's built-in attributes are intentionally small contracts; app
213
- behavior can be richer without overloading them.
214
-
215
- ### Named behavior registration
216
-
217
- Use `registerEnhancement()` when author markup opts in with a `data-enhance`
218
- token instead of an app-specific attribute. A third-party behavior registers
219
- exactly like a built-in one:
220
-
221
- ```js
222
- import { registerEnhancement } from "actual-css/js/enhance";
223
-
224
- registerEnhancement("autosubmit", (form) => {
225
- const controller = new AbortController();
226
-
227
- form.addEventListener("change", () => form.requestSubmit(), {
228
- signal: controller.signal,
229
- });
230
-
231
- return () => controller.abort();
232
- });
233
- ```
234
-
235
- ```html
236
- <form data-enhance="validation autosubmit">
237
- ```
238
-
239
- No core module list is modified. The test case in `tests/enhance.test.js`
240
- (*registers a third-party behavior without touching core*) proves the shape.
241
-
242
- ## Lifecycle Rules
243
-
244
- - Keep selectors opt-in.
245
- - Return cleanup for every listener, observer, timer, or pending controller.
246
- - Use `AbortController` for event listeners whenever possible.
247
- - Make the unenhanced HTML useful first, then add behavior.
248
- - Dispatch a custom event when app code may need to react.
249
- - Keep modules safe to import without a DOM.
250
-
251
- ## Toggle Button Groups
252
-
253
- 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.
254
-
255
- ```html
256
- <div class="join" role="group" aria-label="Text style" data-toggle-group>
257
- <button class="btn outline" type="button" aria-pressed="false">Bold</button>
258
- <button class="btn outline" type="button" aria-pressed="false">Italic</button>
259
- <button class="btn outline" type="button" aria-pressed="false">Underline</button>
260
- </div>
261
-
262
- <div class="join" role="group" aria-label="Alignment" data-toggle-group="single">
263
- <button class="btn outline" type="button" aria-pressed="true">Left</button>
264
- <button class="btn outline" type="button" aria-pressed="false">Center</button>
265
- <button class="btn outline" type="button" aria-pressed="false">Right</button>
266
- </div>
267
- ```
268
-
269
- ```js
270
- import enhance from "actual-css/js/enhance";
271
-
272
- enhance({
273
- "[data-toggle-group]": (group) => {
274
- const controller = new AbortController();
275
-
276
- group.addEventListener("click", (event) => {
277
- if (!(event.target instanceof Element)) return;
278
-
279
- const button = event.target.closest("button[aria-pressed]");
280
- if (!button || !group.contains(button)) return;
281
-
282
- if (group.dataset.toggleGroup === "single") {
283
- for (const item of group.querySelectorAll("button[aria-pressed]")) {
284
- item.setAttribute("aria-pressed", String(item === button));
285
- }
286
- return;
287
- }
288
-
289
- const pressed = button.getAttribute("aria-pressed") === "true";
290
- button.setAttribute("aria-pressed", String(!pressed));
291
- }, { signal: controller.signal });
292
-
293
- return () => controller.abort();
294
- },
295
- });
296
- ```
297
-
298
- ## Custom Text Filters
299
-
300
- `data-filter` is intentionally limited to Actual's built-in filters. For domain
301
- rules such as currency, signed numbers, product codes, or time ranges, create an
302
- app-specific filter attribute.
303
-
304
- Use the input helpers when you need the same composition and caret behavior as
305
- Actual's built-in filters.
306
-
307
- ```js
308
- import enhance from "actual-css/js/enhance";
309
- import { dispatchInput, onTextInput, selectionStart, setCaret } from "actual-css/js/input";
310
-
311
- const filters = {
312
- currency(value) {
313
- const normalized = value.replace(/,/g, ".");
314
- const [whole, decimals = ""] = normalized.replace(/[^0-9.]/g, "").split(".");
315
- return decimals ? `${whole}.${decimals.slice(0, 2)}` : whole;
316
- },
317
- time(value) {
318
- const digits = value.replace(/\D/g, "").slice(0, 4);
319
- return digits.length > 2 ? `${digits.slice(0, 2)}:${digits.slice(2)}` : digits;
320
- },
321
- };
322
-
323
- enhance({
324
- "input[data-app-filter]": (input) => {
325
- const filter = filters[input.dataset.appFilter];
326
- if (!filter) return;
327
-
328
- const controller = new AbortController();
329
- let filtering = false;
330
-
331
- onTextInput(input, () => {
332
- if (filtering) return;
333
-
334
- const caret = selectionStart(input);
335
- const previous = input.value;
336
- const next = filter(previous);
337
- if (next === previous) return;
338
-
339
- input.value = next;
340
- setCaret(input, filter(previous.slice(0, caret)).length);
341
-
342
- try {
343
- filtering = true;
344
- dispatchInput(input);
345
- } finally {
346
- filtering = false;
347
- }
348
- }, controller.signal);
349
-
350
- return () => controller.abort();
351
- },
352
- });
353
- ```
354
-
355
- ```html
356
- <input inputmode="decimal" data-app-filter="currency" autocomplete="off" />
357
- <input inputmode="numeric" data-app-filter="time" autocomplete="off" />
358
- ```
359
-
360
- ## Textarea Autogrow
361
-
362
- Textarea autogrow is a good fit for a small local enhancer because product needs
363
- vary: minimum rows, maximum height, scroll behavior, and resize handles are all
364
- app decisions.
365
-
366
- ```js
367
- import enhance from "actual-css/js/enhance";
368
-
369
- enhance({
370
- "textarea[data-autogrow]": (textarea) => {
371
- const controller = new AbortController();
372
-
373
- function resize() {
374
- textarea.style.height = "auto";
375
- textarea.style.height = `${textarea.scrollHeight}px`;
376
- }
377
-
378
- textarea.addEventListener("input", resize, { signal: controller.signal });
379
- resize();
380
-
381
- return () => {
382
- controller.abort();
383
- textarea.style.height = "";
384
- };
385
- },
386
- });
387
- ```
388
-
389
- ## Ajax Forms
390
-
391
- Keep the native form as the baseline: set `action`, `method`, fields, and submit
392
- buttons normally. The enhancer can intercept submission only when JavaScript is
393
- available.
394
-
395
- ```js
396
- import enhance from "actual-css/js/enhance";
397
-
398
- enhance({
399
- "form[data-ajax]": (form) => {
400
- const controller = new AbortController();
401
-
402
- form.addEventListener("submit", async (event) => {
403
- event.preventDefault();
404
-
405
- const submitter = event.submitter;
406
- const data = new FormData(form, submitter);
407
- const response = await fetch(form.action, {
408
- method: form.method || "GET",
409
- body: form.method.toUpperCase() === "GET" ? null : data,
410
- signal: controller.signal,
411
- });
412
-
413
- form.dispatchEvent(new CustomEvent("actual:ajax", {
414
- bubbles: true,
415
- detail: { response },
416
- }));
417
- }, { signal: controller.signal });
418
-
419
- return () => controller.abort();
420
- },
421
- });
422
- ```
423
-
424
- For htmx-like fragment swaps, keep the first version narrow: one trigger, one
425
- target, one swap rule. If the behavior grows into history management, out-of-band
426
- swaps, request queues, and event policies, use a dedicated library.
427
-
428
- ```js
429
- import enhance from "actual-css/js/enhance";
430
-
431
- enhance({
432
- "[data-get][data-target]": (trigger) => {
433
- const controller = new AbortController();
434
-
435
- trigger.addEventListener("click", async () => {
436
- const target = document.querySelector(trigger.dataset.target);
437
- if (!target) return;
438
-
439
- const response = await fetch(trigger.dataset.get, {
440
- signal: controller.signal,
441
- });
442
- target.innerHTML = await response.text();
443
- }, { signal: controller.signal });
444
-
445
- return () => controller.abort();
446
- },
447
- });
448
- ```
449
-
450
- ## Form Validation
451
-
452
- `actual-css/js/validation` is part of the default runtime (`actual-css/js`)
453
- and also importable on its own — it is an enhancer, not a validation framework.
454
- Forms opt in with `.needs-validation`; the behavior registers automatically when
455
- the runtime loads. Native HTML constraint validation runs first, then the
456
- enhancer adds `aria-invalid="true"` to invalid fields on submit,
457
- focuses the first invalid field, and dispatches a bubbling `actual:invalid`
458
- event with `{ form, firstInvalid, message }`. Valid fields are not marked
459
- automatically.
460
-
461
- ```html
462
- <form class="needs-validation" data-enhance="validation" data-validation-message="Please check the fields.">
463
- <input class="input" name="email" type="email" required
464
- aria-describedby="email-error" />
465
- <span class="field-error" id="email-error">Enter a valid email.</span>
466
- </form>
467
-
468
- <div class="status-bar" data-status role="status" aria-live="polite" aria-atomic="true"></div>
469
- ```
470
-
471
- The status bar auto-wires to `actual:invalid`: the form's `data-validation-message`
472
- appears with the `danger` intent, no manual listener. To handle the summary
473
- yourself instead, omit the status import and listen for the event:
474
-
475
- ```js
476
- document.addEventListener("actual:invalid", (event) => {
477
- const { firstInvalid, message } = event.detail;
478
- // route message to your own surface
479
- });
480
- ```
481
-
482
- Custom rules go in `data-validation-rules` and resolve through
483
- `FormValidator.registerRule`. The library never performs network validation;
484
- server errors flow back through `FormValidator.setErrors(form, { name: message })`
485
- and `FormValidator.clearFieldError(field)`.
486
-
487
- ```js
488
- import { FormValidator } from "actual-css/js/validation";
489
-
490
- FormValidator.registerRule("slug", (value) =>
491
- value.length === 0 || /^[a-z0-9-]+$/.test(value)
492
- );
493
-
494
- // after an AJAX submit the server reports field errors
495
- FormValidator.setErrors(form, { email: "Already taken" });
496
- ```