actual-css 0.1.0 → 0.2.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 (51) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/README.md +2 -0
  3. package/dist/actual.css +356 -124
  4. package/dist/actual.js +2 -2
  5. package/dist/actual.js.map +13 -13
  6. package/dist/actual.min.css +1 -1
  7. package/docs/README.md +2 -0
  8. package/docs/components.md +2 -2
  9. package/docs/design-notes/browser-support.md +9 -0
  10. package/docs/design-notes/enhancement-contract.md +104 -0
  11. package/docs/design-notes/naming.md +1 -0
  12. package/docs/design-notes/widget-primitives.md +83 -0
  13. package/docs/forms.md +4 -4
  14. package/docs/javascript.md +104 -34
  15. package/docs/patterns.md +89 -0
  16. package/docs/ui.md +22 -30
  17. package/llms.txt +6 -0
  18. package/package.json +14 -8
  19. package/src/css/base.css +0 -4
  20. package/src/css/components/alert.css +88 -2
  21. package/src/css/components/badge.css +30 -7
  22. package/src/css/components/busy.css +1 -0
  23. package/src/css/components/button.css +6 -8
  24. package/src/css/components/flyout.css +0 -1
  25. package/src/css/components/join.css +1 -2
  26. package/src/css/components/scrollspy.css +2 -2
  27. package/src/css/components/spinner.css +4 -4
  28. package/src/css/components/status-bar.css +3 -4
  29. package/src/css/components/tab.css +1 -0
  30. package/src/css/focus.css +37 -0
  31. package/src/css/forms/choice-card.css +1 -2
  32. package/src/css/forms/choice.css +1 -2
  33. package/src/css/forms/control.css +1 -10
  34. package/src/css/forms/index.css +1 -0
  35. package/src/css/forms/native.css +8 -32
  36. package/src/css/forms/range.css +95 -0
  37. package/src/css/forms/switch.css +1 -2
  38. package/src/css/optional/scroller.css +6 -0
  39. package/src/css/theme.css +9 -9
  40. package/src/css/themes/edge.css +3 -27
  41. package/src/css/tokens.css +9 -3
  42. package/src/js/context-menu.js +21 -34
  43. package/src/js/enhance.js +26 -0
  44. package/src/js/flyout.js +12 -19
  45. package/src/js/menu.js +15 -14
  46. package/src/js/scrollspy.js +125 -62
  47. package/src/js/selectors.js +14 -24
  48. package/src/js/status.js +13 -13
  49. package/src/js/surface.js +57 -12
  50. package/src/js/tab.js +10 -13
  51. package/src/js/validation.js +11 -43
@@ -0,0 +1,83 @@
1
+ # Widget primitives
2
+
3
+ The JS runtime doubles as a primitive kit for building custom widgets.
4
+
5
+ ## Primitives
6
+
7
+ | Need | Primitive | Import |
8
+ |------|-----------|--------|
9
+ | Opt-in discovery + lifecycle + cleanup | `enhance`, `registerEnhancement` | `actual-css/js/enhance` |
10
+ | Positioning, flip/shift, `--available-height`, `data-placement` | `floating` (`track`, `reposition`, `repositionAt`) | `actual-css/js/floating` |
11
+ | Open/close, sheet on mobile, backdrop, outside-click, Escape, focus restore | `surface` | `actual-css/js/surface` |
12
+ | Roving focus over a list | `keys` (`firstItem`, `lastItem`, `nextItem`) | `actual-css/js/keys` |
13
+ | Menu-item vocabulary, usable-item filtering | `menu` | `actual-css/js/menu` |
14
+ | Focusable lookup, visibility | `focus` | `actual-css/js/focus` |
15
+ | Caret-safe value rewriting | `input` (`onTextInput`, `setCaret`, `dispatchInput`) | `actual-css/js/input` |
16
+ | Character policies | `filter` | `actual-css/js/filter` |
17
+ | Event namespace | `events` (`EVENTS`, `ACTUAL_EVENT_PREFIX`) | `actual-css/js/events` |
18
+ | Declarative triggers | `command` (`registerCommands`) | `actual-css/js/command` |
19
+ | State classes the runtime writes | `selectors` (`CLASSES`) | `actual-css/js/selectors` |
20
+
21
+ ## Compositions
22
+
23
+ ### Custom select — A. Owns its popup
24
+
25
+ ```
26
+ enhance
27
+ floating
28
+ keys
29
+ ```
30
+
31
+ Full control. Everything re-implemented. Use when the widget must own dismissal (e.g.
32
+ an inline autocomplete that should survive an outside click).
33
+
34
+ ### Custom select — B. Reuses the surface lifecycle
35
+
36
+ ```
37
+ enhance
38
+ surface (Escape, outside click, focus restore, sheet mode)
39
+ floating
40
+ keys
41
+ ```
42
+
43
+ Less code. Inherits the runtime's dismissal semantics and mobile sheet. Use for
44
+ anything that behaves like a popup.
45
+
46
+ > A `[role="listbox"]` surface with `[role="option"]` children gets no key handling and
47
+ > no click-autoclose from `surface.js` — that is the extension seam. `[role="option"]`
48
+ > is deliberately not in the menu-item vocabulary.
49
+
50
+ ### Tags input
51
+
52
+ ```
53
+ enhance + input + keys + filter (optional character policy)
54
+ ```
55
+
56
+ No floating, no surface.
57
+
58
+ ### Date picker
59
+
60
+ ```
61
+ enhance + surface + floating + keys + events
62
+ ```
63
+
64
+ A calendar popup wants Escape/outside-click/sheet. `actual:surface-open` to veto or
65
+ decorate.
66
+
67
+ ## What "no Actual class" actually promises
68
+
69
+ > No Actual presentation or component class is required in authored markup for behavior to
70
+ > work. Runtime state classes are still written by the primitives, and remapping them is
71
+ > what `selectors.js` is for.
72
+
73
+ Concretely: no `.flyout`, `.tabs`, `.field`, `.btn`, `.needs-validation`, `.status-bar`
74
+ anywhere in the author's HTML — while `.is-open` may appear in the author's *CSS*, as a
75
+ state to style or ignore.
76
+
77
+ ## Honest gaps
78
+
79
+ - No typeahead helper — filter, debounce, render are author concerns.
80
+ - No `aria-activedescendant` helper — the author manages the composite-widget pattern.
81
+ - No virtual-list support.
82
+ - No `<select>` replacement in core — the deliverable is the *surface*, not the widget.
83
+ `<select data-enhance="select">` is the intended shape for an external package.
package/docs/forms.md CHANGED
@@ -7,7 +7,7 @@
7
7
  - Form controls are styled with explicit classes, not by parent scope.
8
8
  - Control classes: `.input`, `.textarea`, `.select`, `.check`, `.radio`, `.switch`, `.range`, `.file`.
9
9
  - Reusable layout classes: `.field`, `.field-label`, `.field-help`, `.field-error`, `.field-group`, `.choice`, `.form-actions`, `.form-actions.sticky`.
10
- - No floating labels.
10
+ - No floating labels shipped — the blocks demo shows a userland recipe for teams that want them.
11
11
  - Proper focus style that preserves keyboard navigation.
12
12
  - `.field-group` and `.choice` provide layout without depending on parent scope.
13
13
  - Customizable select is progressive enhancement only — the native select remains the baseline.
@@ -1085,7 +1085,7 @@ Actual CSS ships validation *styles*. Invalid fields are marked with `aria-inval
1085
1085
  Opt in with the `.needs-validation` class. Importing the module registers the behavior; there is no init call.
1086
1086
 
1087
1087
  ```html
1088
- <form class="needs-validation" data-validation-message="Please check the highlighted fields.">
1088
+ <form class="needs-validation" data-enhance="validation" data-validation-message="Please check the highlighted fields.">
1089
1089
  <div class="stack">
1090
1090
  <label class="field">
1091
1091
  <span class="field-label">Password</span>
@@ -1116,7 +1116,7 @@ On blur, the enhancer sets `aria-invalid="true"` on the invalid field so visual
1116
1116
  The status bar (`actual-css/js/status`) auto-wires to that event: import it and add one status element, and the form's `data-validation-message` appears automatically on invalid submit — no manual listener.
1117
1117
 
1118
1118
  ```html
1119
- <form class="needs-validation" data-validation-message="Please check the highlighted fields.">
1119
+ <form class="needs-validation" data-enhance="validation" data-validation-message="Please check the highlighted fields.">
1120
1120
  <div class="stack">
1121
1121
  <label class="field">
1122
1122
  <span class="field-label">Email</span>
@@ -1196,7 +1196,7 @@ Add your own with `FormValidator.registerRule(name, (value, el, ...opts) => bool
1196
1196
  The `date` rule pairs naturally with `data-mask` — the mask structures input, the rule validates meaning. Same rule, different formats:
1197
1197
 
1198
1198
  ```html
1199
- <form class="stack needs-validation" novalidate>
1199
+ <form class="stack needs-validation" data-enhance="validation" novalidate>
1200
1200
  <label class="field">
1201
1201
  <span class="field-label">Date (ISO)</span>
1202
1202
  <input class="input" name="date-iso"
@@ -67,42 +67,85 @@ handle; cleanup is per-element when an element leaves the DOM.
67
67
 
68
68
  ## Using The Runtime With Another CSS Framework
69
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:
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`:
85
96
 
86
- ```js
87
- // src/js/selectors.js
88
- export const CLASSES = {
89
- flyout: "flyout",
90
- tabs: "tabs",
91
- open: "is-open",
92
- // …
93
- };
94
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.
95
143
 
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.
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.
106
149
 
107
150
  ## Extending The Runtime
108
151
 
@@ -169,6 +212,33 @@ Prefer explicit app attributes such as `data-copy`, `data-autogrow`, or
169
212
  `data-ajax`. Actual's built-in attributes are intentionally small contracts; app
170
213
  behavior can be richer without overloading them.
171
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
+
172
242
  ## Lifecycle Rules
173
243
 
174
244
  - Keep selectors opt-in.
@@ -389,7 +459,7 @@ event with `{ form, firstInvalid, message }`. Valid fields are not marked
389
459
  automatically.
390
460
 
391
461
  ```html
392
- <form class="needs-validation" data-validation-message="Please check the fields.">
462
+ <form class="needs-validation" data-enhance="validation" data-validation-message="Please check the fields.">
393
463
  <input class="input" name="email" type="email" required
394
464
  aria-describedby="email-error" />
395
465
  <span class="field-error" id="email-error">Enter a valid email.</span>
package/docs/patterns.md CHANGED
@@ -413,3 +413,92 @@ If list chrome should be removed, use an explicit pattern.
413
413
  <li><a href="/components">Components</a></li>
414
414
  </ul>
415
415
  ```
416
+
417
+ ## Hierarchical scrollable nav
418
+
419
+ App shell with a collapsible nav tree. Uses existing primitives only — no
420
+ treeview component.
421
+
422
+ ```html
423
+ <aside class="sidebar">
424
+ <nav class="nav-list scroller" data-enhance="scrollspy" aria-label="Docs">
425
+ <details open>
426
+ <summary class="nav-heading">Getting started</summary>
427
+ <a class="nav-link" href="#intro">Introduction</a>
428
+ <a class="nav-link" href="#install">Installation</a>
429
+ </details>
430
+ <details>
431
+ <summary class="nav-heading">Components</summary>
432
+ <a class="nav-link" href="#alert">Alert</a>
433
+ <a class="nav-link" href="#badge">Badge</a>
434
+ </details>
435
+ </nav>
436
+ </aside>
437
+ ```
438
+
439
+ - `.nav-list` + `.nav-link`: Actual's nav chrome
440
+ - Native `<details>`: exclusive accordion with `<details name>` (no JS engine)
441
+ - `data-enhance="scrollspy"`: scroll-driven `aria-current`
442
+ - `.scroller`: thin, theme-aware scrollbar
443
+ - No `.nav-heading` / `.nav-sublist` classes — the existing primitives are enough
444
+
445
+ ## Code block with copy button
446
+
447
+ ```html
448
+ <div class="code-block">
449
+ <pre><code>npm install actual-css</code></pre>
450
+ <button class="btn sm ghost" data-copy>Copy</button>
451
+ </div>
452
+ ```
453
+
454
+ ```css
455
+ .code-block { position: relative; }
456
+ .code-block > [data-copy] {
457
+ position: absolute;
458
+ inset-block-start: var(--space-2);
459
+ inset-inline-end: var(--space-2);
460
+ }
461
+ ```
462
+
463
+ ```js
464
+ import enhance from "actual-css/js/enhance";
465
+
466
+ enhance({
467
+ "[data-copy]": (button) => {
468
+ const controller = new AbortController();
469
+ button.addEventListener("click", async () => {
470
+ const code = button.closest(".code-block")?.querySelector("code");
471
+ if (code) await navigator.clipboard.writeText(code.textContent);
472
+ }, { signal: controller.signal });
473
+ return () => controller.abort();
474
+ },
475
+ });
476
+ ```
477
+
478
+ No core JS, no syntax highlighter — the recipe is the documentation.
479
+
480
+ ## `<details name>` exclusive accordion
481
+
482
+ Browsers that support the `name` attribute on `<details>` get native exclusive
483
+ accordions — no JS engine needed. Omit `name` to allow several open at once.
484
+
485
+ ```html
486
+ <details name="faq" open>
487
+ <summary>What is this?</summary>
488
+ <p>A CSS framework.</p>
489
+ </details>
490
+ <details name="faq">
491
+ <summary>Does it need JS?</summary>
492
+ <p>Only the progressive enhancers you import.</p>
493
+ </details>
494
+ ```
495
+
496
+ Not shipping an accordion JS engine is a design decision, not a gap.
497
+
498
+ ## Intent nesting
499
+
500
+ Intent on an ancestor must not tint nested components, and a local intent inside
501
+ an intent-carrying ancestor must win. Both directions are verified in
502
+ `tests/css-audit.test.js` and the kitchen-sink demo template. Every
503
+ intent-consuming component must declare its `@sync intent-boundary` block —
504
+ that is what prevents inheritance from leaking across components.
package/docs/ui.md CHANGED
@@ -65,13 +65,7 @@ Use this shape when the dialog interrupts the flow and asks for a decision. It h
65
65
  </div>
66
66
 
67
67
  <footer>
68
- <button class="btn outline"
69
- type="button"
70
- commandfor="delete-dialog"
71
- command="request-close"
72
- aria-controls="delete-dialog">
73
- Cancel
74
- </button>
68
+ <button class="btn outline" value="cancel">Cancel</button>
75
69
 
76
70
  <button class="btn danger"
77
71
  value="delete">
@@ -161,13 +155,7 @@ Use `modal scrollable` when the header and footer should stay visible while the
161
155
  </div>
162
156
 
163
157
  <footer>
164
- <button class="btn outline"
165
- type="button"
166
- commandfor="scroll-dialog"
167
- command="request-close"
168
- aria-controls="scroll-dialog">
169
- Cancel
170
- </button>
158
+ <button class="btn outline" value="cancel">Cancel</button>
171
159
 
172
160
  <button class="btn primary" value="accept">
173
161
  Accept
@@ -227,9 +215,7 @@ Flyouts and tooltips opened from inside a modal dialog are mounted inside the di
227
215
  </button>
228
216
  </div>
229
217
 
230
- <menu class="flyout"
231
- id="dialog-actions-menu"
232
- hidden>
218
+ <menu class="flyout" data-enhance="flyout" id="dialog-actions-menu" hidden>
233
219
  <li><button type="button">Archive</button></li>
234
220
  <li><button type="button">Duplicate</button></li>
235
221
  <li><button type="button">Share</button></li>
@@ -311,13 +297,7 @@ The effect is progressive: it only runs when the browser supports `document.star
311
297
  </header>
312
298
 
313
299
  <footer>
314
- <button class="btn outline"
315
- type="button"
316
- commandfor="vt-dialog"
317
- command="request-close"
318
- aria-controls="vt-dialog">
319
- Cancel
320
- </button>
300
+ <button class="btn outline" value="cancel">Cancel</button>
321
301
  <button class="btn primary" value="confirm">Confirm</button>
322
302
  </footer>
323
303
  </form>
@@ -478,6 +458,7 @@ Flyout covers two distinct patterns:
478
458
  </button>
479
459
 
480
460
  <menu class="flyout sm"
461
+ data-enhance="flyout"
481
462
  id="account-actions"
482
463
  aria-labelledby="account-flyout-trigger"
483
464
  hidden>
@@ -501,7 +482,7 @@ Flyout covers two distinct patterns:
501
482
  <i class="ti ti-chevron-down" aria-hidden="true"></i>
502
483
  </button>
503
484
 
504
- <div class="flyout"
485
+ <div class="flyout" data-enhance="flyout"
505
486
  id="products-panel"
506
487
  aria-label="Products"
507
488
  data-flyout-mobile="auto"
@@ -551,6 +532,7 @@ Use `class="flyout grid-3"` when a nav panel needs multiple link groups. Keep li
551
532
  </button>
552
533
 
553
534
  <div class="flyout grid-3"
535
+ data-enhance="flyout"
554
536
  id="product-mega-menu"
555
537
  aria-label="Platform"
556
538
  data-flyout-mobile="auto"
@@ -628,6 +610,7 @@ Long press is opt-in with `data-context-menu-long-press`. Empty uses the default
628
610
  <output id="context-result" class="text-sm text-muted" aria-live="polite">No context selected.</output>
629
611
 
630
612
  <menu class="flyout"
613
+ data-enhance="flyout"
631
614
  id="file-actions"
632
615
  aria-labelledby="file-actions-trigger"
633
616
  hidden>
@@ -662,7 +645,7 @@ Long press is opt-in with `data-context-menu-long-press`. Empty uses the default
662
645
 
663
646
  ```html
664
647
  <div class="tabset">
665
- <div class="tabs" role="tablist" aria-label="Settings">
648
+ <div class="tabs" data-enhance="tabs" role="tablist" aria-label="Settings">
666
649
  <button class="tab primary"
667
650
  type="button"
668
651
  role="tab"
@@ -832,12 +815,21 @@ or style `[aria-current]` yourself.
832
815
 
833
816
  - `.scrollspy` marks the region for JS detection. It does not add visual styles.
834
817
  - `.nav-list` and `.nav-link` provide the visible list and active link styling.
835
- - The JS enhancer uses `IntersectionObserver` to toggle `aria-current="location"`
836
- on the active link. Without JavaScript, the links remain regular anchor links.
818
+ - The JS enhancer toggles `aria-current="location"` on the active link. Without
819
+ JavaScript, the links remain regular anchor links.
820
+ - Detection is geometric and deterministic: on each scroll, the active section is
821
+ the last one whose top has crossed an activation line. Reaching the end of the
822
+ scroll always activates the last section, and sitting above the first activates
823
+ none.
824
+ - `data-scrollspy-root="#container"` measures a scroll container instead of the
825
+ viewport.
826
+ - `data-scrollspy-offset` moves the activation line: a bare number or `px` value
827
+ is pixels, a `%` value is a share of the root's visible height. Invalid values
828
+ fall back to `20%`, and negatives clamp to `0`.
837
829
  - The same markup also works with the CSS-native scroll markers path (see below).
838
830
 
839
831
  ```html
840
- <nav class="scrollspy" aria-label="Page sections">
832
+ <nav class="scrollspy" data-enhance="scrollspy" aria-label="Page sections">
841
833
  <ol class="nav-list stack">
842
834
  <li><a class="nav-link" href="#overview" aria-current="location">Overview</a></li>
843
835
  <li><a class="nav-link" href="#tokens">Tokens</a></li>
@@ -892,7 +884,7 @@ portable fallback — the same markup serves both paths.
892
884
  - Intents: `danger`, `success`, `warning`, `neutral`. The default (no intent) is a neutral dark pill.
893
885
 
894
886
  ```html
895
- <form class="needs-validation" data-validation-message="Please check the highlighted fields.">
887
+ <form class="needs-validation" data-enhance="validation" data-validation-message="Please check the highlighted fields.">
896
888
  <label class="field">
897
889
  <span class="field-label">Email</span>
898
890
  <input class="input" type="email" name="email" required
package/llms.txt CHANGED
@@ -15,3 +15,9 @@ full CSS framework and `actual-css/js` for the complete runtime. The composition
15
15
  guides in [README](README.md) and [JavaScript](docs/javascript.md) show supported
16
16
  CSS modules and individual enhancer imports without duplicating the full export
17
17
  list here.
18
+
19
+ In 0.2, presentation (`class`) and behavior (`data-enhance`) are separate layers.
20
+ Behaviour primitives (`enhance`, `floating`, `surface`, `keys`, `menu`, `focus`,
21
+ `events`) are independently importable for building custom widgets.
22
+ See [enhancement contract](docs/design-notes/enhancement-contract.md) and
23
+ [widget primitives](docs/design-notes/widget-primitives.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actual-css",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Plain CSS component framework with semantic classes, universal variants, small tokens, theme hooks, and progressive enhancements.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.14",
@@ -8,11 +8,11 @@
8
8
  "author": "Thomas <thomas@lekoala.be>",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://onedev.kalyptus.ovh/actual-css.git"
11
+ "url": "https://github.com/lekoala/actual-css.git"
12
12
  },
13
- "homepage": "https://onedev.kalyptus.ovh/actual-css",
13
+ "homepage": "https://github.com/lekoala/actual-css",
14
14
  "bugs": {
15
- "url": "https://onedev.kalyptus.ovh/actual-css/issues"
15
+ "url": "https://github.com/lekoala/actual-css/issues"
16
16
  },
17
17
  "keywords": [
18
18
  "css",
@@ -40,11 +40,15 @@
40
40
  "./js/dialog": "./src/js/dialog.js",
41
41
  "./js/dialog-fallback": "./src/js/dialog-fallback.js",
42
42
  "./js/enhance": "./src/js/enhance.js",
43
+ "./js/events": "./src/js/events.js",
43
44
  "./js/filter": "./src/js/filter.js",
44
45
  "./js/floating": "./src/js/floating.js",
45
46
  "./js/flyout": "./src/js/flyout.js",
47
+ "./js/focus": "./src/js/focus.js",
46
48
  "./js/input": "./src/js/input.js",
49
+ "./js/keys": "./src/js/keys.js",
47
50
  "./js/mask": "./src/js/mask.js",
51
+ "./js/menu": "./src/js/menu.js",
48
52
  "./js/password": "./src/js/password.js",
49
53
  "./js/scrollspy": "./src/js/scrollspy.js",
50
54
  "./js/selectors": "./src/js/selectors.js",
@@ -52,6 +56,7 @@
52
56
  "./js/tooltip": "./src/js/tooltip.js",
53
57
  "./js/validation": "./src/js/validation.js",
54
58
  "./js/status": "./src/js/status.js",
59
+ "./js/surface": "./src/js/surface.js",
55
60
  "./package.json": "./package.json"
56
61
  },
57
62
  "main": "./dist/actual.js",
@@ -83,6 +88,7 @@
83
88
  "build:dist": "bun scripts/build-dist.js",
84
89
  "build:js": "bun scripts/build-js.js",
85
90
  "build:size": "bun scripts/build-size.js",
91
+ "check:enhance": "bun run scripts/check-enhance.js",
86
92
  "check:reserved": "bun scripts/check-reserved.js",
87
93
  "check:links": "bun scripts/check-doc-links.js",
88
94
  "check:templates": "bun scripts/check-templates.js",
@@ -90,12 +96,12 @@
90
96
  "shot:page": "bun scripts/page-shot.js",
91
97
  "check:sync": "bun scripts/check-sync.js",
92
98
  "test": "bun test",
93
- "build:all": "bun run build:demo && bun run build:dist && bun run check:reserved && bun run check:sync && bun run check:links && bun run check:templates && bun run test && bun run build:js && bun run build:size",
99
+ "build:all": "bun run build:demo && bun run build:dist && bun run check:enhance && bun run check:reserved && bun run check:sync && bun run check:links && bun run check:templates && bun run test && bun run build:js && bun run build:size",
94
100
  "prepublishOnly": "bun run build:all"
95
101
  },
96
102
  "devDependencies": {
97
- "@biomejs/biome": "^2.4.0",
98
- "happy-dom": "^20.10.6",
99
- "prismjs": "^1.30.0"
103
+ "@biomejs/biome": "^2.5",
104
+ "happy-dom": "^20.11",
105
+ "prismjs": "^1.30"
100
106
  }
101
107
  }
package/src/css/base.css CHANGED
@@ -17,10 +17,6 @@ a {
17
17
  text-underline-offset: 0.18em;
18
18
  }
19
19
 
20
- a:focus-visible {
21
- box-shadow: var(--focus-ring-shadow);
22
- }
23
-
24
20
  @media (hover: hover) {
25
21
  a:hover {
26
22
  text-decoration-thickness: 0.12em;