actual-css 0.6.0 → 0.8.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 (143) hide show
  1. package/CHANGELOG.md +214 -1
  2. package/README.md +13 -11
  3. package/components.json +668 -0
  4. package/dist/actual.css +166 -37
  5. package/dist/actual.full.css +1005 -550
  6. package/dist/actual.full.js +1 -1
  7. package/dist/actual.full.min.css +1 -1
  8. package/dist/actual.js +1 -1
  9. package/dist/actual.min.css +1 -1
  10. package/docs/design-notes/accessibility-decisions.md +33 -0
  11. package/docs/design-notes/browser-support.md +82 -25
  12. package/docs/design-notes/column-layout.md +3 -3
  13. package/docs/design-notes/css-anchor.md +75 -21
  14. package/docs/design-notes/enhancement-contract.md +14 -6
  15. package/docs/design-notes/grid.md +4 -4
  16. package/docs/design-notes/naming.md +6 -1
  17. package/docs/design-notes/platform-alignment.md +99 -2
  18. package/docs/design-notes/popover-manual-poc.md +328 -0
  19. package/docs/design-notes/surface-reparenting.md +131 -0
  20. package/docs/design-notes/tooltip-coordinate-space.md +132 -0
  21. package/docs/design-notes/widget-primitives.md +9 -6
  22. package/docs/pages/components/accordion.md +41 -2
  23. package/docs/pages/components/alert.md +80 -30
  24. package/docs/pages/components/app-navigation.md +6 -0
  25. package/docs/pages/components/avatar.md +6 -6
  26. package/docs/pages/components/badge.md +101 -16
  27. package/docs/pages/components/button.md +16 -8
  28. package/docs/pages/components/card.md +10 -10
  29. package/docs/pages/components/dialog.md +16 -6
  30. package/docs/pages/components/fab.md +4 -3
  31. package/docs/pages/components/list.md +42 -4
  32. package/docs/pages/components/navbar.md +17 -1
  33. package/docs/pages/components/pagination.md +2 -2
  34. package/docs/pages/components/steps.md +19 -0
  35. package/docs/pages/components/table.md +3 -2
  36. package/docs/pages/enhancements/flyout.md +144 -24
  37. package/docs/pages/enhancements/overview.md +25 -0
  38. package/docs/pages/enhancements/status-bar.md +20 -0
  39. package/docs/pages/enhancements/tabs.md +45 -37
  40. package/docs/pages/enhancements/tooltip.md +40 -2
  41. package/docs/pages/examples/overview.md +38 -5
  42. package/docs/pages/forms/choice-cards.md +6 -2
  43. package/docs/pages/forms/combobox.md +1 -1
  44. package/docs/pages/forms/floating-field.md +4 -3
  45. package/docs/pages/forms/joined-controls.md +22 -0
  46. package/docs/pages/forms/otp.md +2 -1
  47. package/docs/pages/forms/overview.md +1 -1
  48. package/docs/pages/forms/states-and-sizes.md +16 -18
  49. package/docs/pages/forms/sticky-actions.md +2 -2
  50. package/docs/pages/forms/switch.md +1 -1
  51. package/docs/pages/foundations/philosophy.md +67 -22
  52. package/docs/pages/foundations/theming.md +10 -2
  53. package/docs/pages/foundations/tokens.md +118 -21
  54. package/docs/pages/foundations/typography.md +35 -14
  55. package/docs/pages/guides/introduction.md +11 -9
  56. package/docs/pages/guides/progressive-enhancement.md +136 -49
  57. package/docs/pages/guides/tailwind.md +3 -2
  58. package/docs/pages/layout/media.md +1 -1
  59. package/docs/pages/layout/overview.md +11 -1
  60. package/docs/pages/layout/scroll-snap.md +3 -3
  61. package/docs/pages/layout/stack.md +17 -9
  62. package/docs/pages/patterns/icon-input.md +4 -2
  63. package/docs/pages/patterns/icon-slot.md +2 -0
  64. package/docs/pages/patterns/intent-nesting.md +1 -1
  65. package/docs/pages/utilities/overview.md +23 -2
  66. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  67. package/docs/pages/utilities/spacing.md +10 -8
  68. package/llms.txt +2 -1
  69. package/package.json +19 -8
  70. package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
  71. package/src/css/components/accordion.css +42 -3
  72. package/src/css/components/alert.css +25 -13
  73. package/src/css/components/app-nav.css +18 -8
  74. package/src/css/components/avatar.css +13 -1
  75. package/src/css/components/badge.css +78 -12
  76. package/src/css/components/breadcrumb.css +2 -3
  77. package/src/css/components/button.css +9 -16
  78. package/src/css/components/card.css +9 -2
  79. package/src/css/components/chat.css +1 -0
  80. package/src/css/components/data-list.css +1 -1
  81. package/src/css/components/drawer.css +79 -25
  82. package/src/css/components/fab.css +0 -6
  83. package/src/css/components/flyout.css +46 -63
  84. package/src/css/components/index.css +0 -1
  85. package/src/css/components/indicator.css +21 -0
  86. package/src/css/components/join.css +7 -0
  87. package/src/css/components/key.css +10 -0
  88. package/src/css/components/list.css +40 -4
  89. package/src/css/components/menu.css +18 -11
  90. package/src/css/components/modal.css +48 -33
  91. package/src/css/components/navbar.css +37 -4
  92. package/src/css/components/pagination.css +1 -0
  93. package/src/css/components/spinner.css +6 -6
  94. package/src/css/components/status-bar.css +0 -5
  95. package/src/css/components/steps.css +45 -59
  96. package/src/css/components/tab.css +29 -9
  97. package/src/css/components/table.css +5 -5
  98. package/src/css/components/tooltip.css +45 -13
  99. package/src/css/core/base.css +23 -1
  100. package/src/css/core/intents.css +17 -6
  101. package/src/css/core/theme.css +27 -0
  102. package/src/css/core/tokens.css +35 -15
  103. package/src/css/core/variants.css +64 -15
  104. package/src/css/forms/choice-card.css +4 -4
  105. package/src/css/forms/choice.css +6 -3
  106. package/src/css/forms/control.css +2 -1
  107. package/src/css/forms/floating-field.css +3 -3
  108. package/src/css/forms/form.css +4 -3
  109. package/src/css/forms/input-icon.css +20 -14
  110. package/src/css/forms/native.css +6 -1
  111. package/src/css/forms/otp.css +9 -1
  112. package/src/css/forms/range.css +2 -0
  113. package/src/css/forms/select.css +29 -9
  114. package/src/css/forms/switch.css +19 -5
  115. package/src/css/layout/app-layout.css +1 -1
  116. package/src/css/layout/grid.css +50 -76
  117. package/src/css/layout/scroll-snap.css +11 -7
  118. package/src/css/layout/scroller.css +3 -4
  119. package/src/css/layout/stack.css +2 -2
  120. package/src/css/typography/fluid.css +4 -3
  121. package/src/css/typography/prose.css +6 -5
  122. package/src/css/utilities/base.css +2 -1
  123. package/src/css/utilities/extra.css +20 -17
  124. package/src/js/context-menu.js +27 -26
  125. package/src/js/dialog.js +2 -1
  126. package/src/js/dismiss.js +5 -0
  127. package/src/js/enhance.js +34 -2
  128. package/src/js/enhancement-loader.js +36 -43
  129. package/src/js/events.js +0 -3
  130. package/src/js/filter.js +0 -1
  131. package/src/js/floating-space.js +42 -0
  132. package/src/js/flyout.js +9 -44
  133. package/src/js/focus-group.js +7 -0
  134. package/src/js/focus.js +6 -0
  135. package/src/js/menu.js +10 -1
  136. package/src/js/selectors.js +5 -9
  137. package/src/js/status.js +44 -7
  138. package/src/js/surface.js +106 -132
  139. package/src/js/tab.js +6 -1
  140. package/src/js/tooltip.js +152 -53
  141. package/src/js/validation.js +1 -1
  142. package/src/css/components/surface.css +0 -49
  143. package/src/js/floating.js +0 -432
@@ -17,14 +17,16 @@ unsupported — it is not tested, documented as supported, or preserved when the
17
17
  runtime evolves. The runtime assumes the modern platform primitives available
18
18
  across Minimal and ships no compatibility layer or polyfill.
19
19
 
20
- | Tier | Firefox | Safari | Chromium |
21
- | ------------ | ------- | --------- | -------- |
22
- | Degraded | 78+ | 14+ | 88+ |
23
- | **Minimal** | **98+** | **15.4+** | **99+** |
24
- | Intermediate | 121+ | 16+ | 106+ |
25
- | Recommended | 129+ | 17.5+ | 123+ |
20
+ | Tier | Firefox | Safari | Chromium |
21
+ | ----------- | -------- | ------- | -------- |
22
+ | Degraded | 78+ | 14+ | 88+ |
23
+ | **Minimal** | **125+** | **17+** | **116+** |
24
+ | Recommended | 129+ | 17.5+ | 123+ |
26
25
 
27
- The tier definitions live in the browser-support design note.
26
+ The floor is set by the native manual Popover transport (`popover="manual"`,
27
+ `showPopover()`, `hidePopover()`), which interactive surfaces use to promote a
28
+ panel to the top layer without moving it in the DOM. The tier definitions live
29
+ in the browser-support design note.
28
30
 
29
31
  ## Overview
30
32
 
@@ -109,7 +111,7 @@ The JS is designed to run on its own, without Actual's stylesheet.
109
111
  presentation class needed. Self-describing attributes (`data-mask`,
110
112
  `data-tooltip`, `data-context-menu`) stay framework-neutral by construction.
111
113
  - **`selectors.js`** lists the state classes the runtime *writes* (`is-open`,
112
- `was-validated`, `is-sheet`, …). Edit this file (or alias it in a bundler) to
114
+ `was-validated`, …). Edit this file (or alias it in a bundler) to
113
115
  match a different CSS framework's state vocabulary. It is a read-side-only
114
116
  adapter — it does not bridge CSS class names to JS initialization.
115
117
  - **One documented exception:** validation reads `.field-error` (or its alias
@@ -124,19 +126,18 @@ The JS is designed to run on its own, without Actual's stylesheet.
124
126
  ## The runtime as a primitive kit
125
127
 
126
128
  For building custom widgets (select, tags, date picker) on Actual's primitives,
127
- see the widget-primitives design note. Every primitive subpath is published in
128
- `package.json#exports`:
129
-
130
- | Subpath | Purpose |
131
- | ------------------------ | ---------------------------------------------------------------- |
132
- | `actual-css/js/enhance` | DOM lifecycle engine: `enhance()`, `registerEnhancement`, tokens |
133
- | `actual-css/js/escape` | Per-document LIFO Escape dismissal stack |
134
- | `actual-css/js/events` | The `actual:*` event name constants |
135
- | `actual-css/js/floating` | Positioning engine (`reposition`, `repositionAt`, `autoUpdate`) |
136
- | `actual-css/js/focus` | Focusable-item lookup and helpers |
137
- | `actual-css/js/keys` | Roving-focus item navigation helpers |
138
- | `actual-css/js/menu` | Menu keyboard/click wiring, item vocabulary |
139
- | `actual-css/js/surface` | Surface lifecycle (`openSurface`, `closeSurface`, …) |
129
+ see the widget-primitives design note. Every Actual runtime primitive subpath is
130
+ published in `package.json#exports`:
131
+
132
+ | Subpath | Purpose |
133
+ | ----------------------- | ---------------------------------------------------------------- |
134
+ | `actual-css/js/enhance` | DOM lifecycle engine: `enhance()`, `registerEnhancement`, tokens |
135
+ | `actual-css/js/escape` | Per-document LIFO Escape dismissal stack |
136
+ | `actual-css/js/events` | The `actual:*` event name constants |
137
+ | `actual-css/js/focus` | Focusable-item lookup and helpers |
138
+ | `actual-css/js/keys` | Roving-focus item navigation helpers |
139
+ | `actual-css/js/menu` | Menu keyboard/click wiring, item vocabulary |
140
+ | `actual-css/js/surface` | Surface lifecycle (`openSurface`, `closeSurface`, …) |
140
141
 
141
142
  The exports map is explicit — it *is* the statement of what is public.
142
143
 
@@ -149,33 +150,49 @@ computes and writes inline `left`/`top`, `data-placement`,
149
150
  caller owns the floating element's presentation entirely.
150
151
 
151
152
  ```js
152
- import { autoUpdate, reposition, repositionAt } from "actual-css/js/floating";
153
+ import { autoUpdate, reposition, repositionAt } from "@lekoala/floating";
153
154
 
154
- const stop = autoUpdate(float, () => {
155
+ const stop = autoUpdate(trigger, float, () => {
155
156
  reposition(trigger, float, { placement: "bottom-start", distance: 4 });
156
157
  });
157
158
  ```
158
159
 
159
160
  Options passed to `reposition()`:
160
161
 
161
- | Option | Default | Contract |
162
- | -------------- | ---------------- | ------------------------------------------- |
163
- | `placement` | `"bottom-start"` | Preferred side and alignment |
164
- | `distance` | `0` | Gap from the reference element |
165
- | `flip` | `true` | Flip when the preferred side is out of view |
166
- | `shift` | `true` | Shift along the main axis to stay in bounds |
167
- | `shiftPadding` | `4` | Minimum space kept from the boundary |
168
- | `scope` | viewport | Boundary element for overflow decisions |
162
+ | Option | Default | Contract |
163
+ | ----------------- | ---------------- | -------------------------------------------- |
164
+ | `placement` | `"bottom-start"` | Preferred side and alignment |
165
+ | `distance` | `0` | Gap from the reference element |
166
+ | `flip` | `true` | Flip when the preferred side is out of view |
167
+ | `shift` | `true` | Shift along the cross axis to stay in bounds |
168
+ | `shiftPadding` | `4` | Minimum space kept from the boundary |
169
+ | `scope` | viewport | Boundary element for overflow decisions |
170
+ | `coordinateSpace` | `"viewport"` | Space the written `left`/`top` use |
169
171
 
170
172
  `placement` takes a `-start` or `-end` alignment suffix.
171
173
 
172
- `autoUpdate(element, callback)` batches scroll, resize, and element-resize
173
- callbacks per document and returns a stop handle; it does not keep a closed or
174
+ Measurement is always in viewport coordinates; `coordinateSpace` only changes
175
+ what is written. `"viewport"` expects `position: fixed`; `"document"` adds the
176
+ page scroll and expects `position: absolute` against the initial containing
177
+ block, which the top layer supplies to an open popover. The browser then
178
+ scrolls the element with the page rather than `autoUpdate()` correcting it a
179
+ frame later — right for a reference that scrolls with the page, wrong for a
180
+ fixed or sticky one. `tooltip.js` and element-anchored surfaces choose this
181
+ coupled coordinate/position mode once per opening. A point-positioned surface
182
+ always uses fixed viewport coordinates because `x` and `y` are client
183
+ coordinates. Scrolling a nested container still needs the next `autoUpdate()`
184
+ frame; document coordinates remove correction only for page scrolling.
185
+
186
+ `@lekoala/floating` is the standalone positioning dependency used by the
187
+ runtime. `autoUpdate(reference, float, callback)` batches scroll, resize, and
188
+ element-resize callbacks per document and returns a stop handle; it observes
189
+ both the reference and floating element while the surface is open. Pass `null`
190
+ as the reference for point-positioned surfaces. It does not keep a closed or
174
191
  hidden element positioned. `repositionAt(x, y, float, opts)` positions relative
175
192
  to a fixed point — used by context menus.
176
193
 
177
- A third-party widget importing only `floating` with no Actual stylesheet must be
178
- able to position a listbox under an input — that is the test.
194
+ A third-party widget importing only `@lekoala/floating` with no Actual stylesheet
195
+ must be able to position a listbox under an input — that is the test.
179
196
 
180
197
  ## Surface contract
181
198
 
@@ -183,23 +200,32 @@ able to position a listbox under an input — that is the test.
183
200
  or account for:
184
201
 
185
202
  ```css
186
- .my-panel { position: absolute; z-index: 100; }
187
- .my-panel[hidden] { display: none; }
188
- /* surface.js sets position: fixed + inline left/top on open —
203
+ .my-panel { position: absolute; }
204
+ .my-panel[popover] { inset: auto; margin: 0; }
205
+ .my-panel[popover]:not(:popover-open) { display: none; }
206
+ /* surface.js sets a coupled fixed/viewport or absolute/document mode —
189
207
  do not override with !important */
190
- /* .is-open / .is-sheet / .surface-backdrop are written by surface.js
191
- — style or ignore them */
208
+ /* no z-index: an open panel is promoted to the top layer, where
209
+ numbers do not apply — see Layering in foundations/tokens */
210
+ /* .is-open is written by surface.js — style or ignore it */
192
211
  /* --surface-anchor-width is set while an open surface is positioned */
193
212
  /* data-actual-surface is written by surface.js for teardown
194
213
  — never select on it */
195
214
  ```
196
215
 
197
- Two non-obvious requirements:
216
+ Three non-obvious requirements:
198
217
 
199
- 1. `[hidden] { display: none }` must not be defeated by a higher-specificity
200
- `display` rule — `surface.js` toggles `hidden`, not `display`.
201
- 2. The open state switches to `position: fixed` with measured inline
202
- coordinates.
218
+ 1. `surface.js` writes `popover="manual"` and removes `hidden`: the panel is
219
+ promoted to the top layer where it stands, never reparented, and the closed
220
+ state belongs to the platform. `.is-open` is the state the runtime writes
221
+ and the only one to key styles on.
222
+ 2. A panel that declares its own `display` must restore `display: none` for the
223
+ closed popover. An author declaration outranks the UA origin whatever the
224
+ specificity, so a bare `display: grid` leaves a closed panel painted at its
225
+ static position.
226
+ 3. The open state switches to the coupled position and coordinate mode chosen
227
+ from its anchor, with measured inline coordinates. The UA's own `inset` /
228
+ `margin` must be cleared.
203
229
 
204
230
  Lifecycle: `prepareSurface` → `openSurface` → `closeSurface` →
205
231
  `disconnectSurface`. `retainSurface(panel)` reference-counts a surface shared by
@@ -207,6 +233,11 @@ several triggers and returns a release function; `isSurfaceOpen` and
207
233
  `getSurfaceAutoClose` read current state. The separate `escape` primitive adds
208
234
  visible dismissable UI to the shared per-document Escape stack.
209
235
 
236
+ Actual-managed surfaces are mutually exclusive within a document. Opening a
237
+ surface closes any other open Actual surface, including one containing its
238
+ trigger. Nested surfaces are not currently supported: a panel opened from
239
+ inside another panel replaces it rather than stacking on it.
240
+
210
241
  `openSurface(menu, opts)` dispatches a cancelable `actual:surface-open` event —
211
242
  widgets can veto or decorate opens, and context menus inject their own
212
243
  `source`/`restoreFocusTo` through it.
@@ -222,14 +253,11 @@ widgets can veto or decorate opens, and context menus inject their own
222
253
  | `shift` | `true` | Shift to stay within the boundary |
223
254
  | `shiftPadding` | `4` | Minimum space kept from the boundary |
224
255
  | `scope` | viewport | Boundary element |
225
- | `mobile` | `"auto"` | Sheet behavior |
226
- | `breakpoint` | `768` | Max viewport width for an automatic sheet |
227
256
  | `autoClose` | `"outside"` | Which clicks close the surface |
228
257
  | `dismissOnScroll` | `false` | Close on a scroll the user just started |
229
258
  | `restoreFocusTo` | `trigger` | Element focused on close |
230
259
 
231
260
  `x`, `y` replace the anchor entirely, which is what a context menu needs.
232
- `mobile` takes `"auto"`, `"sheet"`, `"none"` or `"anchored"`.
233
261
 
234
262
  `autoClose` takes `"true"` to close on inside and outside clicks, `"inside"` or
235
263
  `"outside"` to limit it to one side, and `"false"` to disable automatic click
@@ -389,6 +417,63 @@ on the same root throws, and `disconnect()` releases the name so it can be
389
417
  registered again. A custom root passed as the third argument must be disposed
390
418
  by its owner; the default `document.documentElement` root is not affected.
391
419
 
420
+ ### Applying behaviors from application selectors
421
+
422
+ Prefer declarative `data-enhance` in HTML. When server-rendered markup or
423
+ application conventions make JavaScript more convenient, use
424
+ `applyEnhancement(name, selector, root?)`:
425
+
426
+ ```js
427
+ import "actual-css/js/tab";
428
+ import "actual-css/js/flyout";
429
+ import "actual-css/js/validation";
430
+ import { applyEnhancement } from "actual-css/js/enhance";
431
+
432
+ const bindings = {
433
+ tabs: ".tabs",
434
+ flyout: ".menu-trigger",
435
+ validation: "form.needs-validation",
436
+ };
437
+
438
+ for (const [name, selector] of Object.entries(bindings)) {
439
+ applyEnhancement(name, selector);
440
+ }
441
+ ```
442
+
443
+ Run this after the target markup exists, for example in a module script.
444
+ Select the element that owns the behavior, not necessarily the element carrying
445
+ the component class. For example, `flyout` applies to **triggers**, not `.flyout`
446
+ panels. Those triggers need `aria-controls` pointing to their panels. Tooltips use
447
+ `data-tooltip` directly and do not need a named enhancement.
448
+
449
+ `selector` is a CSS selector. `root` defaults to `document.documentElement` and
450
+ uses `querySelectorAll()` semantics: only descendants match, never the scope
451
+ itself. Pass a container to limit the application:
452
+
453
+ ```js
454
+ applyEnhancement("tabs", ".tabs", container);
455
+ ```
456
+
457
+ Each registration on the scope or its ancestors refreshes the scope once.
458
+ Shadow roots need their own registrations; document registrations do not cross
459
+ into them.
460
+
461
+ The function returns an array of matched elements, preserves existing
462
+ tokens, and can be called repeatedly without initializing an active behavior
463
+ twice. Invalid names or selectors throw. Without a DOM it returns an
464
+ empty array. It does not import modules: applying a token before its module is
465
+ registered lets that registration's initial scan discover it later.
466
+
467
+ `applyEnhancement()` is a one-time convenience for adding an enhancement token
468
+ to elements selected by the application. It does not create a persistent
469
+ selector-to-enhancement binding. For injected markup, author `data-enhance`
470
+ directly or call the function with a scope containing the new elements.
471
+ Detached elements receive the token and initialize when inserted into a
472
+ registered root. Removing the token does not disconnect an active behavior.
473
+
474
+ `data-enhance` remains the portable behavior contract. Class mappings belong to
475
+ the application; Actual does not infer behavior from presentation classes.
476
+
392
477
  ### Enhancement manifests
393
478
 
394
479
  The enhancement-loader lets a server (or static HTML) declare document-wide
@@ -674,7 +759,9 @@ field, and dispatches a bubbling `actual:invalid` event with
674
759
  <input class="input" name="email" type="email" required
675
760
  aria-describedby="email-error" />
676
761
  <span class="field-error" id="email-error">Enter a valid email.</span>
677
- <button class="btn primary" type="submit">Submit</button>
762
+ <div class="form-actions">
763
+ <button class="btn primary" type="submit">Submit</button>
764
+ </div>
678
765
  </form>
679
766
 
680
767
  <div class="status-bar" data-status role="status" aria-live="polite" aria-atomic="true"></div>
@@ -163,7 +163,7 @@ Tailwind allows almost every utility to be changed independently at a breakpoint
163
163
  There is intentionally no equivalent Actual syntax such as:
164
164
 
165
165
  ```text
166
- md:cluster xl:gap-lg
166
+ md:cluster xl:spacious
167
167
  ```
168
168
 
169
169
  First ask what the layout is supposed to do.
@@ -213,7 +213,8 @@ Actual does not prohibit media queries. It simply does not encode every media qu
213
213
  | `max-w-*` for readable text | `.measure` where appropriate |
214
214
  | common visual component bundle | Actual component |
215
215
  | component colors | intents such as `.primary`, `.danger`, `.success` |
216
- | repeated layout spacing | `--gap` / layout hooks |
216
+ | `gap-*` on one container | `gap: var(--space-*)` |
217
+ | rebind rhythm for nested primitives | `--gap` on the layout context |
217
218
  | small common corrections | Actual utilities |
218
219
  | breakpoint-prefixed utilities | intrinsic layout, container query, or project CSS |
219
220
  | arbitrary values | custom property or project CSS |
@@ -25,4 +25,4 @@ Add `.items-center` to align the leading element with short trailing content. Fo
25
25
  </div>
26
26
  ```
27
27
 
28
- The media object is the underlying pattern of `.blog-author`, `.blog-comment`, and similar compositions in templates. Components can adopt it instead of repeating the grid rule.
28
+ The media object is the underlying pattern of `.blog-author`, `.blog-comment`, and similar compositions in templates. Components can adopt it instead of repeating the grid rule. The [Workspaces example](../examples/overview.md) shows the card pattern: `card > header` with `.media` plus `hgroup`, and row actions in the header — never a third `.media` child.
@@ -47,4 +47,14 @@ Most layout primitives read `--gap` and allow local overrides.
47
47
 
48
48
  Prefer a local custom property over adding many one-off utility classes. Use inline styles only for demos, prototypes, or truly dynamic values.
49
49
 
50
- There are no `.gap-sm` / `.gap-md` / `.gap-lg` density variants on the core layout primitives; set `--gap` locally when only spacing should change. The extra utilities (`src/css/utilities/extra.css`) ship those gap utilities for row/column containers.
50
+ Set `--gap` locally to retune the primitive and its nested layouts. To change only one element's rhythm without reaching nested primitives, set the `gap` property instead.
51
+
52
+ ## Spacing expresses relationships
53
+
54
+ **Components own their internal rhythm; layout owns the relationships between components.**
55
+
56
+ Prefer a parent `gap` for relationships between peers instead of encoding sibling margins into a component. Smaller gaps imply a stronger relationship; progressively larger gaps distinguish groups and sections.
57
+
58
+ Before adding a divider or other separating chrome, consider whether additional spacing already expresses the boundary clearly.
59
+
60
+ Density changes the amount of space available, not the relationships that space communicates. When a composition contains local, group, and section rhythms, preserve their relative hierarchy with nested or local `--gap` values rather than relying on one inherited gap at every level. See [Stack](./stack.md) for the nesting and density mechanics.
@@ -24,10 +24,10 @@ Use `.scroll-snap` for a niche row of items that should scroll horizontally, suc
24
24
 
25
25
  The baseline is flex layout plus `overflow-x: auto`, so older browsers still get a usable horizontal scroll area. Browsers with scroll snap support add `scroll-snap-type` and item alignment.
26
26
 
27
- Items snap to the inline start edge by default. Use `data-snap="center"` for visual rails where centered cards are the better fit.
27
+ Items snap to the inline start edge by default, with `proximity` snapping so a short rail never fights the pointer. Use `data-snap-align="center"` for visual rails where centered cards are the better fit, and `data-snap="mandatory"` only when every stop must land.
28
28
 
29
29
  ```html demo
30
- <div class="scroll-snap" data-snap="center" style="--scroll-snap-item-size: min(85%, 18rem)">
30
+ <div class="scroll-snap" data-snap-align="center" style="--scroll-snap-item-size: min(85%, 18rem)">
31
31
  <article class="card">One</article>
32
32
  <article class="card">Two</article>
33
33
  <article class="card">Three</article>
@@ -42,7 +42,7 @@ Items snap to the inline start edge by default. Use `data-snap="center"` for vis
42
42
  - `--scroll-snap-gap` — space between items.
43
43
  - `--scroll-snap-padding` — inline scroll padding, so a snapped item does not sit
44
44
  flush against the container edge. Not declared; falls back to `--scroll-snap-gap`.
45
- - `--scroll-snap-align` — item snap alignment. Prefer `data-snap="center"`
45
+ - `--scroll-snap-align` — item snap alignment. Prefer `data-snap-align="center"`
46
46
  over setting this directly.
47
47
 
48
48
  Keep scrollbars visible by default. If a product deliberately hides them, make that choice explicit with `data-scrollbar="hidden"`.
@@ -41,15 +41,17 @@ not as the wrapper that lays out independent full-width regions of a page.
41
41
 
42
42
  ## Choosing a density
43
43
 
44
- `--gap` is the primitive's channel, and the density variants set it at the point
45
- of use. Pick the one that matches how tightly the children belong together:
44
+ `--gap` is the primitive's channel, and the density context variants set it at
45
+ the point of use. Apply the density class on the stack itself to size one
46
+ instance, or on an ancestor to reach nested layouts. Pick the one that matches
47
+ how tightly the children belong together:
46
48
 
47
49
  | Markup | Gap | Use for |
48
50
  | ----------------- | ---- | ------------------------------------------ |
49
51
  | `.stack gap-none` | 0 | Lines forming one typographic unit |
50
- | `.stack sm` | 8px | A homogeneous series of controls |
52
+ | `.stack compact` | 8px | A homogeneous series of controls |
51
53
  | `.stack` | 12px | Normal flow between distinct elements |
52
- | `.stack lg` | 24px | Sections, or blocks that read as separated |
54
+ | `.stack spacious` | 24px | Sections, or blocks that read as separated |
53
55
 
54
56
  One typographic unit means a name and a job title, or a figure and its caption.
55
57
  A homogeneous series means a radio list, a checkbox list, or a compact vertical
@@ -57,8 +59,14 @@ menu.
57
59
 
58
60
  The distinction between the first two matters. `42` above `Open issues` is
59
61
  almost a single block of text, so it takes `gap-none`. Three `.choice` labels
60
- stay three separate controls that happen to form one group, so they take `sm` —
61
- tighter than the default, but still spaced.
62
+ stay three separate controls that happen to form one group, so they take
63
+ `compact` — tighter than the default, but still spaced.
64
+
65
+ `.compact` and `.spacious` rebind `--gap` to `--space-20` and `--space-50`.
66
+ Because the token is inherited, the same two classes also tighten the inner gap
67
+ of any nested stack, cluster, and grid. For a one-off gap that must *not* follow
68
+ density, set the `gap` property directly on the instance (the next section
69
+ explains why).
62
70
 
63
71
  A stack that mixes kinds keeps the default: five `.field` wrappers and one lone
64
72
  `.choice` is a form that contains a checkbox, not a list of options, and
@@ -84,7 +92,7 @@ depth:
84
92
  ```
85
93
 
86
94
  Both give the region the same spacing; only the second stops there. This is what
87
- makes a nested `.stack sm` a real density choice — 12px down to 8px — rather
95
+ makes a nested `.stack compact` a real density choice — 12px down to 8px — rather
88
96
  than a patch cancelling a rhythm inherited from three levels up.
89
97
 
90
98
  Setting `--gap` is still the right move on the element that *consumes* it: a
@@ -92,8 +100,8 @@ one-off `--gap` on a card body or an actions row tunes that primitive and
92
100
  nothing below it, because the setter is the consumer. The leak only appears when
93
101
  a container sets the token and its children read it.
94
102
 
95
- `.sm` and `.lg` are the deliberate exception: they set `--gap` precisely so a
96
- density context reaches nested layouts.
103
+ `.compact` and `.spacious` are the deliberate exception: they set `--gap`
104
+ precisely so a density context reaches nested layouts.
97
105
 
98
106
  ## Grouping items tightly
99
107
 
@@ -11,13 +11,15 @@ Use `.input-icon` for a search or filter input with a leading icon.
11
11
 
12
12
  It positions the wrapper's first child absolutely inside a relative container and grows the input's start padding to clear it. `pointer-events: none` on the icon keeps clicks passing through to the input underneath. It is icon-library agnostic — an `<i>` webfont icon, an inline `<svg>`, or a `<span>` all work as the wrapper's first child.
13
13
 
14
- The icon keeps a fixed `--input-icon-size` (1.25rem); density contexts shrink the control geometry around it, not the pictogram. Apply the density class to the wrapper and to the input so the control height follows:
14
+ The icon follows local typography through `--input-icon-size`. Apply a size to
15
+ the wrapper so icon, input type, and control geometry scale together. A density
16
+ context changes only the geometry.
15
17
 
16
18
  ```html demo
17
19
  <div class="stack">
18
20
  <label class="input-icon sm">
19
21
  <i class="ti ti-search" aria-hidden="true"></i>
20
- <input class="input sm" type="search" aria-label="Search">
22
+ <input class="input" type="search" aria-label="Search">
21
23
  </label>
22
24
  </div>
23
25
  ```
@@ -14,6 +14,8 @@ centers its glyph, and clips overflow:
14
14
  - `overflow: hidden`;
15
15
  - a child `svg` constrained to the box.
16
16
 
17
+ Icons inherit first: an icon that supports nearby content uses `currentColor`; it takes its own color only when it conveys an independent state or intent.
18
+
17
19
  ## The contract
18
20
 
19
21
  ```css
@@ -1,3 +1,3 @@
1
1
  # Intent Nesting
2
2
 
3
- Intent on an ancestor must not tint nested components, and a local intent inside an intent-carrying ancestor must win. Both directions are verified in `tests/css-audit.test.js` and the kitchen-sink demo template. Intent classes only expose `--intent` and `--intent-fg`; each consuming component opens a local intent boundary in a `:where()` block that resets exactly the tokens it consumes to initial. Parent intents do not leak into nested components, and a local intent class still wins because it has higher specificity than `:where()`.
3
+ Intent on an ancestor must not tint nested components, and a local intent inside an intent-carrying ancestor must win. Both directions are verified in `tests/css-audit.test.js`, `tests/browser/intent-boundary.test.js`, `tests/browser/soft-boundary.test.js` and the kitchen-sink demo template. Intent classes expose `--intent`, `--intent-fg`, and the soft-foreground relay `--intent-soft-fg`; each consuming component opens a local intent boundary in a `:where()` block that resets exactly the tokens it consumes to initial. Parent intents do not leak into nested components, and a local intent class still wins because it has higher specificity than `:where()`.
@@ -19,7 +19,7 @@ not about opting in. Importing `actual-css/css/utilities` — or
19
19
  | **Extra** | `actual-css/css/utilities/extra` | Explicit property/value names |
20
20
 
21
21
  Base is the curated set, and it is where a short name is allowed: `.px`, `.py`,
22
- `.mbs`, `.gap-none`. Extra spells out the property and the value — `.gap-sm`,
22
+ `.mbs`, `.gap-none`. Extra spells out the property and the value — `.row-gap-context`,
23
23
  `.overflow-x-auto`, `.justify-content-center` — and never gives a base utility
24
24
  a second spelling.
25
25
 
@@ -33,8 +33,29 @@ list the two layers as separate tables. Every other utility page is base only.
33
33
 
34
34
  > Utilities deliberately excluded to keep the surface small and intentional.
35
35
 
36
- - No display scale such as `.block`, `.flex`, `.grid` — use layout primitives.
36
+ - No display scale such as `.block`, `.flex`, `.grid` — use layout primitives. The extra layer does ship explicit `.display-flex` / `.display-grid` escape hatches; see Display escape hatches below.
37
37
  - No color scales such as `.text-primary` or `.bg-success` — intent colors belong to components and state.
38
38
  - No breakpoint utility variants — use container queries or layout primitives.
39
39
  - No utility variants for hover, focus, dark mode, or arbitrary selectors — use component states.
40
40
  - No full spacing scale with every step and direction — the 17 spacing helpers across the core and extra utilities cover the most common escape hatches.
41
+
42
+ ## Display escape hatches
43
+
44
+ A one-off display correction is the exception to the display Non-Goal. The extra
45
+ layer spells the property and the value, so these are opt-in corrections, not the
46
+ short atomic scale the Non-Goal bans.
47
+
48
+ | Class | Description |
49
+ | ----------------------- | ------------------------ |
50
+ | `.display-block` | `display: block`. |
51
+ | `.display-inline` | `display: inline`. |
52
+ | `.display-inline-block` | `display: inline-block`. |
53
+ | `.display-flex` | `display: flex`. |
54
+ | `.display-inline-flex` | `display: inline-flex`. |
55
+ | `.display-grid` | `display: grid`. |
56
+ | `.display-none` | `display: none`. |
57
+ | `.visibility-hidden` | `visibility: hidden`. |
58
+
59
+ Prefer `.stack`, `.cluster`, `.grid`, and the other layout primitives first.
60
+ Reach for a display helper when existing component or third-party markup needs a
61
+ one-line correction and no primitive expresses it.
@@ -27,7 +27,7 @@ layout primitive from wrapping — for example to force a `.cluster` onto a sing
27
27
  row — set `--cluster-wrap: nowrap`.
28
28
 
29
29
  ```html demo
30
- <div class="cluster sm" style="--cluster-wrap: nowrap;">
30
+ <div class="cluster compact" style="--cluster-wrap: nowrap;">
31
31
  <select class="select sm fit" aria-label="Segment">
32
32
  <option>All segments</option>
33
33
  </select>
@@ -18,9 +18,6 @@ Extra utilities:
18
18
 
19
19
  | Class | Description |
20
20
  | --------------------------- | --------------------------------------------- |
21
- | `.gap-sm` | `gap: var(--space-20)` — denser row rhythm. |
22
- | `.gap-md` | `gap: var(--space-40)` — default row rhythm. |
23
- | `.gap-lg` | `gap: var(--space-50)` — roomier row rhythm. |
24
21
  | `.padding-context` | All-side padding at the current density step. |
25
22
  | `.gap-context` | `gap: var(--gap)`. |
26
23
  | `.row-gap-context` | `row-gap: var(--gap)`. |
@@ -32,8 +29,8 @@ Extra utilities:
32
29
  | `.margin-block-end-auto` | Pushes content toward the block start. |
33
30
 
34
31
  Spacing helpers follow the density context. The default step is
35
- `--space-40` (1rem); the `.sm` context tightens it to `--space-20` (0.5rem)
36
- and the `.lg` context loosens it to `--space-50` (1.5rem). All helpers read
32
+ `--space-40` (1rem); the `.compact` context tightens it to `--space-20` (0.5rem)
33
+ and the `.spacious` context loosens it to `--space-50` (1.5rem). All helpers read
37
34
  `--density-space` and map to logical properties (`padding-block`,
38
35
  `margin-block-end`, etc.) for writing-direction safety.
39
36
 
@@ -51,8 +48,13 @@ Override the default gap from layout primitives (`.stack`, `.cluster`, `.grid`):
51
48
  - `.gap-none` → `gap: 0`
52
49
 
53
50
  `.gap-none` sets the element's own gap directly and does not change `--gap`
54
- for nested layouts. For a denser or roomier rhythm, use the `.sm` / `.lg`
55
- density contexts or override `--gap` on the layout instance.
51
+ for nested layouts. For a denser or roomier rhythm, use the `.compact` /
52
+ `.spacious` density contexts, or set the `gap` property on the instance when the
53
+ change must stop there.
54
+
55
+ `--gap` defines the contextual rhythm; `.gap-context` applies that rhythm to a
56
+ container that does not already consume `--gap`. The layout primitives
57
+ (`.stack`, `.cluster`, `.grid`, …) read it on their own.
56
58
 
57
59
  ## Padding
58
60
 
@@ -82,5 +84,5 @@ No `mbs-none` / `mbe-none` — use `margin: 0` via `.list-reset` for lists, or a
82
84
 
83
85
  ## CSS hooks
84
86
 
85
- - `--density-space` — the step every padding and margin helper reads; `.sm` sets it to `--space-20`, default to `--space-40`, `.lg` to `--space-50`.
87
+ - `--density-space` — the step every padding and margin helper reads; `.compact` sets it to `--space-20`, default to `--space-40`, `.spacious` to `--space-50`.
86
88
  - `--gap` — base gap of layout primitives; read by the optional `.gap-context`, `.row-gap-context`, and `.column-gap-context` helpers.
package/llms.txt CHANGED
@@ -14,9 +14,10 @@ Actual CSS uses a small public grammar: `.component [intent] [variant] [size] [m
14
14
 
15
15
  - `actual-css` — the minimal core: tokens, theme contract, reset, base elements, and universal variants. Components and layout primitives are NOT included.
16
16
  - `actual-css/full` — the complete framework: core plus every component, form, layout, typography, effect, and utility family.
17
- - `actual-css/js` — the enhancement loader only; it discovers `data-enhance` markers and loads enhancers on demand.
17
+ - `actual-css/js` — the enhancement-loader entry only: it watches `data-enhance-modules` manifest blocks and imports the declared enhancers on demand.
18
18
  - `actual-css/js/full` — the complete runtime with all built-in enhancers pre-registered.
19
19
  - `actual-css/reserved-classes.json` — machine-readable list of the framework-owned class namespace. Use it to check whether a class belongs to Actual CSS and to validate claimed framework classes; application-specific classes are expected to exist outside this manifest.
20
+ - `actual-css/components.json` — machine-readable component index, derived from the sources (CSS headers, selectors, package exports, `src/js`) by `scripts/build-catalog.js` and verified by `check:catalog`. Gives each component its import paths, owned classes, hooks, states, and registered enhancement tokens — enough to know what exists and where to read more, not a replacement for `docs/pages/components/*.md`.
20
21
  - Individual families and modules are importable separately (`actual-css/css/components`, `actual-css/css/layout/grid`, `actual-css/js/dialog`, …); see [README](README.md) and [Modular imports](docs/pages/guides/modular-import.md).
21
22
 
22
23
  Forms are the one family with two aggregate entrypoints:
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "actual-css",
3
- "version": "0.6.0",
3
+ "version": "0.8.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
- "packageManager": "bun@1.4.0",
6
+ "packageManager": "bun@1.4.2",
7
7
  "license": "MIT",
8
8
  "author": "Thomas <thomas@lekoala.be>",
9
9
  "repository": {
@@ -58,7 +58,6 @@
58
58
  "./js/parse-config": "./src/js/parse-config.js",
59
59
  "./js/events": "./src/js/events.js",
60
60
  "./js/filter": "./src/js/filter.js",
61
- "./js/floating": "./src/js/floating.js",
62
61
  "./js/flyout": "./src/js/flyout.js",
63
62
  "./js/focus": "./src/js/focus.js",
64
63
  "./js/focus-group": "./src/js/focus-group.js",
@@ -74,7 +73,8 @@
74
73
  "./js/validation": "./src/js/validation.js",
75
74
  "./js/status": "./src/js/status.js",
76
75
  "./js/surface": "./src/js/surface.js",
77
- "./reserved-classes.json": "./scripts/reserved-classes.json",
76
+ "./reserved-classes.json": "./reserved-classes.json",
77
+ "./components.json": "./components.json",
78
78
  "./package.json": "./package.json"
79
79
  },
80
80
  "bin": {
@@ -98,7 +98,8 @@
98
98
  "src/cli",
99
99
  "src/js",
100
100
  "src/tooling",
101
- "scripts/reserved-classes.json",
101
+ "reserved-classes.json",
102
+ "components.json",
102
103
  "docs",
103
104
  "CHANGELOG.md",
104
105
  "llms.txt",
@@ -120,6 +121,9 @@
120
121
  "check:compat": "bun scripts/check-compat.js",
121
122
  "check:css-api": "bun scripts/check-css-api.js",
122
123
  "check:color-space": "bun scripts/check-color-space.js",
124
+ "report:theme-contrast": "bun scripts/report-theme-contrast.js",
125
+ "report:neutral-ramp": "bun scripts/report-neutral-ramp.js",
126
+ "report:dead-css": "bun scripts/report-dead-css.js",
123
127
  "check:architecture": "bun scripts/check-css-architecture.js",
124
128
  "build:dist": "bun scripts/build-dist.js",
125
129
  "build:js": "bun scripts/build-js.js",
@@ -128,20 +132,27 @@
128
132
  "check:enhance": "bun run scripts/check-enhance.js",
129
133
  "generate:reserved": "bun scripts/generate-reserved-classes.js",
130
134
  "check:reserved": "bun scripts/check-reserved.js",
135
+ "generate:catalog": "bun scripts/build-catalog.js",
136
+ "check:catalog": "bun scripts/check-catalog.js",
131
137
  "check:links": "bun scripts/check-doc-links.js",
132
138
  "check:templates": "bun scripts/check-templates.js",
139
+ "check:markup": "bun scripts/check-markup.js",
133
140
  "shot:forced": "bun scripts/forced-colors-shot.js",
134
141
  "shot:multi": "bun scripts/multi-shot.js",
135
142
  "shot:page": "bun scripts/page-shot.js",
136
143
  "probe": "bun scripts/probe.js",
144
+ "serve": "bun scripts/serve.js",
137
145
  "check:sync": "bun scripts/check-sync.js",
138
146
  "test": "bun test",
139
147
  "test:changed": "bun test --changed=master",
140
- "build:all": "bun run lint && bun run build:dist && bun run build:js && bun run format:docs && bun run build:docs && bun run check:docs && bun run check:doc-classes && bun run check:compat && bun run check:css-api && bun run check:color-space && bun run check:architecture && bun run check:enhance && bun run generate:reserved && bun run check:reserved && bun run check:sync && bun run check:links && bun run check:templates && bun run test && bun run build:size",
148
+ "build:all": "bun run lint && bun run build:dist && bun run build:js && bun run format:docs && bun run build:docs && bun run check:docs && bun run check:doc-classes && bun run check:compat && bun run check:css-api && bun run check:color-space && bun run check:architecture && bun run check:enhance && bun run generate:reserved && bun run check:reserved && bun run generate:catalog && bun run check:catalog && bun run check:sync && bun run check:links && bun run check:templates && bun run check:markup && bun run test && bun run build:size",
141
149
  "prepublishOnly": "bun run build:all"
142
150
  },
143
151
  "devDependencies": {
144
- "@biomejs/biome": "^2.5",
145
- "happy-dom": "^20.11"
152
+ "@biomejs/biome": "^2.5.12",
153
+ "happy-dom": "^20.14.0"
154
+ },
155
+ "dependencies": {
156
+ "@lekoala/floating": "^0.2.0"
146
157
  }
147
158
  }