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.
- package/CHANGELOG.md +90 -1
- package/README.md +25 -8
- package/dist/actual-themes.min.css +1 -0
- package/dist/actual.css +1492 -931
- package/dist/actual.js +1 -3
- package/dist/actual.min.css +1 -1
- package/dist/optional.css +1048 -0
- package/dist/optional.min.css +1 -0
- package/docs/design-notes/browser-support.md +95 -6
- package/docs/design-notes/cascade-layer.md +1 -1
- package/docs/design-notes/enhancement-contract.md +101 -0
- package/docs/design-notes/forced-colors.md +105 -0
- package/docs/design-notes/naming.md +3 -1
- package/docs/design-notes/platform-alignment.md +492 -0
- package/docs/design-notes/references.md +8 -0
- package/docs/design-notes/widget-primitives.md +119 -0
- package/docs/navigation.json +135 -0
- package/docs/pages/components/accordion.md +80 -0
- package/docs/pages/components/alert.md +203 -0
- package/docs/pages/components/aura.md +35 -0
- package/docs/pages/components/avatar.md +95 -0
- package/docs/pages/components/badge.md +102 -0
- package/docs/pages/components/breadcrumb.md +19 -0
- package/docs/pages/components/button.md +205 -0
- package/docs/pages/components/card.md +183 -0
- package/docs/pages/components/chat.md +50 -0
- package/docs/pages/components/dialog.md +367 -0
- package/docs/pages/components/drawer.md +113 -0
- package/docs/pages/components/fab.md +118 -0
- package/docs/pages/components/key.md +33 -0
- package/docs/pages/components/meter.md +23 -0
- package/docs/pages/components/navbar.md +92 -0
- package/docs/pages/components/pagination.md +55 -0
- package/docs/pages/components/progress.md +29 -0
- package/docs/pages/components/skeleton.md +38 -0
- package/docs/pages/components/spinner.md +93 -0
- package/docs/pages/components/table.md +102 -0
- package/docs/pages/enhancements/flyout.md +391 -0
- package/docs/pages/enhancements/overview.md +36 -0
- package/docs/pages/enhancements/scrollspy.md +71 -0
- package/docs/pages/enhancements/status-bar.md +72 -0
- package/docs/pages/enhancements/tabs.md +80 -0
- package/docs/pages/enhancements/tooltip.md +130 -0
- package/docs/pages/examples/overview.md +41 -0
- package/docs/pages/forms/choice-cards.md +102 -0
- package/docs/pages/forms/controls.md +32 -0
- package/docs/pages/forms/detached-actions.md +59 -0
- package/docs/pages/forms/floating-field.md +88 -0
- package/docs/pages/forms/form-actions.md +56 -0
- package/docs/pages/forms/input-policy.md +91 -0
- package/docs/pages/forms/joined-controls.md +37 -0
- package/docs/pages/forms/labels.md +42 -0
- package/docs/pages/forms/otp.md +50 -0
- package/docs/pages/forms/overview.md +188 -0
- package/docs/pages/forms/password.md +40 -0
- package/docs/pages/forms/range.md +17 -0
- package/docs/pages/forms/states-and-sizes.md +321 -0
- package/docs/pages/forms/sticky-actions.md +187 -0
- package/docs/pages/forms/switch.md +31 -0
- package/docs/pages/forms/validation.md +241 -0
- package/docs/{mental-model.md → pages/foundations/philosophy.md} +33 -18
- package/docs/pages/foundations/theming.md +92 -0
- package/docs/pages/foundations/tokens.md +421 -0
- package/docs/{typography.md → pages/foundations/typography.md} +27 -9
- package/docs/pages/guides/bootstrap.md +438 -0
- package/docs/pages/guides/introduction.md +157 -0
- package/docs/pages/guides/optional-components.md +33 -0
- package/docs/pages/guides/progressive-enhancement.md +686 -0
- package/docs/pages/guides/tailwind.md +455 -0
- package/docs/pages/layout/app-shell.md +25 -0
- package/docs/pages/layout/center.md +29 -0
- package/docs/pages/layout/cluster.md +57 -0
- package/docs/pages/layout/frame.md +30 -0
- package/docs/pages/layout/grid.md +142 -0
- package/docs/pages/layout/list.md +41 -0
- package/docs/pages/layout/media.md +21 -0
- package/docs/pages/layout/overview.md +37 -0
- package/docs/pages/layout/scroll-snap.md +54 -0
- package/docs/pages/layout/sidebar-layout.md +27 -0
- package/docs/pages/layout/stack.md +18 -0
- package/docs/pages/layout/switcher.md +36 -0
- package/docs/pages/layout/topbar.md +23 -0
- package/docs/pages/patterns/actions.md +52 -0
- package/docs/pages/patterns/code-block.md +36 -0
- package/docs/pages/patterns/exclusive-accordion.md +16 -0
- package/docs/pages/patterns/hierarchical-nav.md +26 -0
- package/docs/pages/patterns/icon-input.md +25 -0
- package/docs/pages/patterns/intent-nesting.md +3 -0
- package/docs/pages/patterns/navigation-list.md +235 -0
- package/docs/pages/patterns/overview.md +58 -0
- package/docs/pages/patterns/structured-lists.md +47 -0
- package/docs/pages/utilities/accessibility.md +34 -0
- package/docs/pages/utilities/flex.md +105 -0
- package/docs/pages/utilities/links.md +18 -0
- package/docs/pages/utilities/list.md +31 -0
- package/docs/pages/utilities/overflow.md +43 -0
- package/docs/pages/utilities/overview.md +18 -0
- package/docs/pages/utilities/shape.md +25 -0
- package/docs/pages/utilities/sizing-wrapping.md +115 -0
- package/docs/pages/utilities/spacing.md +79 -0
- package/docs/pages/utilities/text.md +43 -0
- package/llms.txt +10 -4
- package/package.json +30 -16
- package/src/css/actual.css +12 -1
- package/src/css/base.css +3 -9
- package/src/css/components/accordion.css +18 -30
- package/src/css/components/actions.css +4 -1
- package/src/css/components/alert.css +162 -49
- package/src/css/components/avatar.css +16 -10
- package/src/css/components/badge.css +82 -39
- package/src/css/components/breadcrumb.css +7 -15
- package/src/css/components/busy.css +7 -0
- package/src/css/components/button.css +82 -38
- package/src/css/components/card.css +17 -37
- package/src/css/components/drawer.css +30 -13
- package/src/css/components/flyout.css +39 -110
- package/src/css/components/index.css +1 -0
- package/src/css/components/join.css +14 -3
- package/src/css/components/key.css +2 -0
- package/src/css/components/menu.css +125 -0
- package/src/css/components/meter.css +0 -23
- package/src/css/components/modal.css +162 -7
- package/src/css/components/navbar.css +17 -18
- package/src/css/components/overline.css +0 -7
- package/src/css/components/pagination.css +1 -1
- package/src/css/components/progress.css +8 -26
- package/src/css/components/scrollspy.css +2 -2
- package/src/css/components/skeleton.css +7 -0
- package/src/css/components/spinner.css +10 -11
- package/src/css/components/status-bar.css +18 -28
- package/src/css/components/surface.css +2 -8
- package/src/css/components/tab.css +20 -48
- package/src/css/components/table.css +7 -5
- package/src/css/components/tooltip.css +14 -7
- package/src/css/focus.css +16 -3
- package/src/css/forms/choice-card.css +13 -15
- package/src/css/forms/choice.css +21 -28
- package/src/css/forms/control.css +16 -26
- package/src/css/forms/custom-select.css +9 -11
- package/src/css/forms/form-actions.css +14 -3
- package/src/css/forms/form.css +7 -4
- package/src/css/forms/index.css +5 -4
- package/src/css/forms/input-icon.css +30 -12
- package/src/css/forms/native.css +48 -33
- package/src/css/forms/range.css +103 -0
- package/src/css/forms/select.css +8 -8
- package/src/css/forms/switch.css +14 -13
- package/src/css/forms/validation.css +32 -5
- package/src/css/grid.css +19 -18
- package/src/css/intents.css +2 -2
- package/src/css/layout.css +29 -1
- package/src/css/optional/aura.css +93 -0
- package/src/css/optional/chat.css +105 -0
- package/src/css/optional/fab.css +104 -0
- package/src/css/optional/floating-field.css +87 -0
- package/src/css/optional/index.css +5 -0
- package/src/css/optional/layout-extra.css +7 -4
- package/src/css/optional/otp.css +115 -0
- package/src/css/optional/scroll-snap.css +6 -0
- package/src/css/optional/scroller.css +19 -7
- package/src/css/optional/typography-fluid.css +5 -0
- package/src/css/optional/utilities-extra.css +17 -2
- package/src/css/prose.css +18 -8
- package/src/css/reset.css +0 -10
- package/src/css/theme.css +29 -13
- package/src/css/themes/cyberpunk.css +1 -1
- package/src/css/themes/dim.css +3 -3
- package/src/css/themes/edge.css +11 -37
- package/src/css/themes/index.css +3 -3
- package/src/css/themes/{dark.css → indigo.css} +5 -4
- package/src/css/themes/neon.css +5 -5
- package/src/css/themes/square.css +0 -3
- package/src/css/tokens.css +81 -21
- package/src/css/utilities.css +8 -10
- package/src/css/variants.css +37 -65
- package/src/js/command.js +1 -3
- package/src/js/context-menu.js +72 -53
- package/src/js/dialog.js +68 -79
- package/src/js/dismiss.js +15 -0
- package/src/js/enhance.js +76 -19
- package/src/js/enhancement-loader.js +301 -0
- package/src/js/events.js +3 -0
- package/src/js/filter.js +3 -3
- package/src/js/floating.js +101 -131
- package/src/js/flyout.js +106 -96
- package/src/js/focus-group.js +116 -0
- package/src/js/focus.js +6 -1
- package/src/js/index.js +4 -2
- package/src/js/input.js +1 -1
- package/src/js/keys.js +27 -0
- package/src/js/mask.js +7 -6
- package/src/js/menu.js +89 -35
- package/src/js/parse-config.js +47 -0
- package/src/js/password.js +9 -1
- package/src/js/scrollspy.js +133 -62
- package/src/js/selectors.js +14 -31
- package/src/js/status.js +16 -14
- package/src/js/surface.js +219 -125
- package/src/js/tab.js +46 -66
- package/src/js/tooltip.js +53 -31
- package/src/js/validation.js +45 -51
- package/dist/actual.js.map +0 -31
- package/docs/README.md +0 -26
- package/docs/components.md +0 -860
- package/docs/design-notes/previous-work.md +0 -5
- package/docs/forms.md +0 -1265
- package/docs/javascript.md +0 -426
- package/docs/layout.md +0 -480
- package/docs/patterns.md +0 -415
- package/docs/reserved.md +0 -7
- package/docs/tokens.md +0 -455
- package/docs/ui.md +0 -937
- package/docs/utilities.md +0 -384
- package/src/css/dialog-fallback.css +0 -56
- package/src/js/dialog-fallback.js +0 -163
package/docs/javascript.md
DELETED
|
@@ -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
|
-
```
|