actual-css 0.7.0 → 0.9.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 (144) hide show
  1. package/CHANGELOG.md +217 -0
  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 +1087 -643
  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 +62 -30
  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 +53 -7
  18. package/docs/design-notes/popover-manual-poc.md +105 -25
  19. package/docs/design-notes/surface-reparenting.md +56 -47
  20. package/docs/design-notes/tooltip-coordinate-space.md +132 -0
  21. package/docs/design-notes/widget-primitives.md +7 -4
  22. package/docs/pages/components/accordion.md +41 -2
  23. package/docs/pages/components/alert.md +85 -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 +21 -6
  30. package/docs/pages/components/drawer.md +13 -0
  31. package/docs/pages/components/fab.md +4 -3
  32. package/docs/pages/components/list.md +42 -4
  33. package/docs/pages/components/navbar.md +17 -1
  34. package/docs/pages/components/pagination.md +2 -2
  35. package/docs/pages/components/skeleton.md +3 -1
  36. package/docs/pages/components/steps.md +19 -0
  37. package/docs/pages/components/table.md +3 -2
  38. package/docs/pages/enhancements/flyout.md +82 -38
  39. package/docs/pages/enhancements/overview.md +25 -0
  40. package/docs/pages/enhancements/status-bar.md +1 -1
  41. package/docs/pages/enhancements/tabs.md +45 -37
  42. package/docs/pages/enhancements/tooltip.md +36 -10
  43. package/docs/pages/examples/overview.md +38 -5
  44. package/docs/pages/forms/choice-cards.md +11 -6
  45. package/docs/pages/forms/combobox.md +1 -1
  46. package/docs/pages/forms/floating-field.md +4 -3
  47. package/docs/pages/forms/joined-controls.md +22 -0
  48. package/docs/pages/forms/otp.md +2 -1
  49. package/docs/pages/forms/overview.md +1 -1
  50. package/docs/pages/forms/states-and-sizes.md +16 -18
  51. package/docs/pages/forms/sticky-actions.md +2 -2
  52. package/docs/pages/forms/switch.md +1 -1
  53. package/docs/pages/foundations/philosophy.md +57 -26
  54. package/docs/pages/foundations/theming.md +2 -2
  55. package/docs/pages/foundations/tokens.md +118 -21
  56. package/docs/pages/foundations/typography.md +35 -14
  57. package/docs/pages/guides/introduction.md +11 -9
  58. package/docs/pages/guides/progressive-enhancement.md +115 -30
  59. package/docs/pages/guides/tailwind.md +3 -2
  60. package/docs/pages/layout/media.md +1 -1
  61. package/docs/pages/layout/overview.md +11 -1
  62. package/docs/pages/layout/scroll-snap.md +3 -3
  63. package/docs/pages/layout/stack.md +17 -9
  64. package/docs/pages/patterns/icon-input.md +4 -2
  65. package/docs/pages/patterns/icon-slot.md +2 -0
  66. package/docs/pages/patterns/intent-nesting.md +1 -1
  67. package/docs/pages/utilities/overview.md +23 -2
  68. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  69. package/docs/pages/utilities/spacing.md +10 -8
  70. package/llms.txt +2 -1
  71. package/package.json +159 -149
  72. package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
  73. package/src/css/components/accordion.css +42 -3
  74. package/src/css/components/alert.css +25 -13
  75. package/src/css/components/app-nav.css +18 -8
  76. package/src/css/components/avatar.css +13 -1
  77. package/src/css/components/badge.css +78 -12
  78. package/src/css/components/breadcrumb.css +2 -3
  79. package/src/css/components/button.css +9 -16
  80. package/src/css/components/card.css +9 -2
  81. package/src/css/components/chat.css +1 -0
  82. package/src/css/components/data-list.css +1 -1
  83. package/src/css/components/drawer.css +101 -32
  84. package/src/css/components/fab.css +0 -6
  85. package/src/css/components/flyout.css +35 -98
  86. package/src/css/components/index.css +0 -1
  87. package/src/css/components/indicator.css +21 -0
  88. package/src/css/components/key.css +10 -0
  89. package/src/css/components/list.css +40 -4
  90. package/src/css/components/menu.css +2 -1
  91. package/src/css/components/modal.css +48 -33
  92. package/src/css/components/navbar.css +37 -4
  93. package/src/css/components/pagination.css +1 -0
  94. package/src/css/components/skeleton.css +15 -4
  95. package/src/css/components/spinner.css +6 -6
  96. package/src/css/components/status-bar.css +0 -5
  97. package/src/css/components/steps.css +45 -59
  98. package/src/css/components/tab.css +29 -9
  99. package/src/css/components/table.css +13 -5
  100. package/src/css/components/tooltip.css +36 -40
  101. package/src/css/core/base.css +23 -1
  102. package/src/css/core/intents.css +17 -6
  103. package/src/css/core/theme.css +27 -0
  104. package/src/css/core/tokens.css +35 -15
  105. package/src/css/core/variants.css +64 -15
  106. package/src/css/forms/choice-card.css +4 -4
  107. package/src/css/forms/choice.css +76 -29
  108. package/src/css/forms/control.css +2 -1
  109. package/src/css/forms/floating-field.css +3 -3
  110. package/src/css/forms/form.css +4 -3
  111. package/src/css/forms/input-icon.css +20 -14
  112. package/src/css/forms/native.css +6 -1
  113. package/src/css/forms/otp.css +9 -1
  114. package/src/css/forms/range.css +2 -0
  115. package/src/css/forms/select.css +29 -9
  116. package/src/css/forms/switch.css +28 -8
  117. package/src/css/layout/app-layout.css +1 -1
  118. package/src/css/layout/grid.css +50 -76
  119. package/src/css/layout/scroll-snap.css +11 -7
  120. package/src/css/layout/scroller.css +3 -4
  121. package/src/css/layout/stack.css +2 -2
  122. package/src/css/typography/fluid.css +4 -3
  123. package/src/css/typography/prose.css +6 -5
  124. package/src/css/utilities/base.css +2 -1
  125. package/src/css/utilities/extra.css +20 -17
  126. package/src/js/context-menu.js +27 -26
  127. package/src/js/dialog.js +2 -1
  128. package/src/js/dismiss.js +5 -0
  129. package/src/js/enhance.js +34 -2
  130. package/src/js/enhancement-loader.js +36 -43
  131. package/src/js/events.js +0 -3
  132. package/src/js/filter.js +0 -1
  133. package/src/js/floating-space.js +42 -0
  134. package/src/js/flyout.js +9 -44
  135. package/src/js/focus-group.js +7 -0
  136. package/src/js/focus.js +6 -0
  137. package/src/js/menu.js +10 -1
  138. package/src/js/selectors.js +5 -9
  139. package/src/js/status.js +6 -5
  140. package/src/js/surface.js +97 -139
  141. package/src/js/tab.js +6 -1
  142. package/src/js/tooltip.js +150 -51
  143. package/src/js/validation.js +1 -1
  144. package/src/css/components/surface.css +0 -49
@@ -38,7 +38,7 @@ It applies everywhere — cards, dialogs, heroes, app shells — not only in pro
38
38
 
39
39
  It does not own visual hierarchy. No global `font-size` on headings, no global `font-weight` on headings, no global margins on headings, no global `text-decoration` on links. These choices would shape app UI in ways the framework cannot predict.
40
40
 
41
- The mildly opinionated defaults at the baseline are few: line-height on headings (tight, so multi-line headings stay readable), `text-wrap: balance` on headings where supported, the `hr` rule, the `::selection` rule, the `code` / `kbd` / `samp` / `pre` font family, `small` font size, and a global link underline offset that prose and components reuse. Everything else stays neutral so components and `.prose` can take over without fighting the baseline.
41
+ The mildly opinionated defaults at the baseline are few: line-height on headings (tight, so multi-line headings stay readable), `text-wrap: balance` on headings where supported, the `hr` rule, the `::selection` rule, the `code` / `kbd` / `samp` / `pre` font family, `pre` overflow containment, `small` font size, and a global link underline offset that prose and components reuse. Everything else stays neutral so components and `.prose` can take over without fighting the baseline.
42
42
 
43
43
  Global links inherit color. The visual affordance of a link is the responsibility of `.prose` (for authored content) or of a component (for nav links, button-like links, tabs, breadcrumbs, etc.). Underlining every `a` globally would collide with all of those.
44
44
 
@@ -54,10 +54,11 @@ The reason for that boundary: a dialog title, a card heading, or a sidebar secti
54
54
 
55
55
  ### Fluid module
56
56
 
57
- `src/css/typography/fluid.css` is a small module that is not imported by
58
- `actual.css`. Projects that need display, title, and lead sizes that scale
59
- with the viewport (landing pages, marketing pages, hero sections,
60
- documentation homepages) import it manually:
57
+ `src/css/typography/fluid.css` ships with the typography family
58
+ (`actual-css/css/typography`) and `actual-css/full`, but not with the minimal
59
+ core. A core-only entry point that needs display, title, and lead sizes that
60
+ scale with the viewport (landing pages, marketing pages, hero sections,
61
+ documentation homepages) imports it explicitly:
61
62
 
62
63
  ```css
63
64
  @import "actual-css/css/typography/fluid";
@@ -116,14 +117,14 @@ baseline stays sans so app UI inherits predictably.
116
117
 
117
118
  The framework exposes a small set of weights:
118
119
 
119
- - `--font-weight` (400) — body default
120
- - `--font-weight-medium` (500) — slightly heavier than body, for nav links
121
- - `--font-weight-strong` (650) — emphasis, prose headings, badges, tabs, table headers
122
- - `--font-weight-bold` (700) — the strongest weight exposed, used by the navbar brand and the accordion summary
120
+ - `--font-weight-normal` (400) — body default
121
+ - `--font-weight-medium` (500) — slightly heavier than body, for nav links and accordion summaries
122
+ - `--font-weight-semibold` (600) — emphasis, prose headings, badges, tabs, table headers
123
+ - `--font-weight-bold` (700) — the strongest weight exposed, used by the navbar brand
123
124
 
124
125
  No `--font-weight-light`. Light weights depend on the typeface, are rarely a good default, and add surface area without a recurring need.
125
126
 
126
- These weights are also the values used by components. Components do not use literal `font-weight: 600` or `font-weight: 750`; they reference the tokens. Theme authors can re-map them in one place.
127
+ These weights are also the values used by components. Components reference the tokens instead of literal weights, so theme authors can re-map the scale in one place.
127
128
 
128
129
  ## Line heights
129
130
 
@@ -159,7 +160,7 @@ Headings are split between the global baseline and `.prose`.
159
160
 
160
161
  Globally, headings get a tight line-height, a color from `--heading` (or inherited), and progressive `text-wrap: balance`. They do not get a `font-size`, a `font-weight`, or margins. The reason is that a card heading, a dialog title, or a sidebar section should not be auto-magically enlarged by selector.
161
162
 
162
- Inside `.prose`, headings get a `font-weight` from `--font-weight-strong`, a tighter line-height from `--prose-heading-line-height`, margin rhythm, and keep the global `text-wrap: balance`. Their color follows the contextual `--heading` alias, so prose also composes correctly with a contextual surface. The `h1` to `h4` font sizes are scoped to `.prose`, remain low-specificity, and are not exposed as a public scale.
163
+ Inside `.prose`, headings get a `font-weight` from `--font-weight-semibold`, a tighter line-height from `--prose-heading-line-height`, margin rhythm, and keep the global `text-wrap: balance`. Their color follows the contextual `--heading` alias, so prose also composes correctly with a contextual surface. The `h1` to `h4` font sizes are scoped to `.prose`, remain low-specificity, and are not exposed as a public scale.
163
164
 
164
165
  The `hgroup` element gets only structural styling: a grid layout, a gap, and a reset of internal margins. No font-size, no color, no weight. Its visual identity is the responsibility of the headings and paragraphs it contains.
165
166
 
@@ -175,7 +176,27 @@ Three logical alignment utilities exist (`.text-start`, `.text-center`, `.text-e
175
176
 
176
177
  `small` gets a smaller font size globally (`0.875em`) because it is a semantic element, not a utility. There is no `.small` class competing with the element.
177
178
 
178
- `code`, `kbd`, `samp`, and `pre` get the mono font family globally. Visual treatment (background, padding, radius) is left to `.prose` or to code-block components.
179
+ `code`, `kbd`, `samp`, and `pre` get the mono font family globally. `pre` also gets `overflow-x: auto`, because one unbreakable line widens the whole page and nothing in the markup warns you. That is containment, not decoration: background, padding, and radius are still left to `.prose` or to a code-block component.
180
+
181
+ For a standalone snippet outside authored content, `.prose` is the ready-made box — opt out of the reading measure so the code can use the full width:
182
+
183
+ ```html
184
+ <div class="prose" style="--prose-measure: none">
185
+ <pre><code>bun add actual-css</code></pre>
186
+ </div>
187
+ ```
188
+
189
+ `hr` resets the UA's `margin-inline: auto`. In document flow that changes nothing — auto inline margins on an auto-width block resolve to zero — but as a flex or grid item they replace the cross-axis stretch, which made an `<hr>` inside a `.stack` compute to zero width and disappear. `.stack` deliberately leaves the inline axis free (self-centering children keep working), so the reset belongs to the element.
190
+
191
+ Its block margin is tunable with `--hr-space`. The `--space-60` default suits a rule closing a page section; a rule separating two rows inside a panel wants far less.
192
+
193
+ ```css
194
+ .settings-panel hr {
195
+ --hr-space: var(--space-10);
196
+ }
197
+ ```
198
+
199
+ The hook applies to an `hr` in document flow. Inside `.stack` or `.prose` the parent owns the distance between siblings and overrides the element's own margin — express the rhythm there instead, with `--gap` or `--prose-flow`.
179
200
 
180
201
  ## Tables
181
202
 
@@ -198,14 +219,14 @@ To enable fluid display type on a marketing or documentation surface, import the
198
219
 
199
220
  The module does not change anything in the core: it adds three size tokens and three classes. Body text, app UI, and the default prose scale are not affected.
200
221
 
201
- Projects that do not need fluid display type do nothing — the module is not part of the default bundle.
222
+ Projects on `actual-css/css/typography` or `actual-css/full` already have the module. A minimal-core project that does not need fluid display type does nothing — the module is not in the core.
202
223
 
203
224
  ## What not to reintroduce
204
225
 
205
226
  These are decisions that the framework has already made. Re-introducing them silently would regress the system.
206
227
 
207
228
  - Do not add `font-size` on global `h1` to `h6`. Headings inherit size from context (component or `.prose`).
208
- - Do not add `font-weight` on global `h1` to `h6`. Strong weight is scoped to `.prose` and to components that need it.
229
+ - Do not add `font-weight` on global `h1` to `h6`. Heavier weights are scoped to `.prose` and to components that need them.
209
230
  - Do not add global `text-decoration: underline` on `a`. Links get their visual affordance from `.prose` or from their component.
210
231
  - Do not add `transition: all`. Transitions target specific properties.
211
232
  - Do not add a `--font-weight-light`. It is rarely a good default and depends on the typeface.
@@ -78,7 +78,9 @@ Intents are `.primary`, `.secondary`, `.success`, `.warning`, and `.danger`, and
78
78
  work across components. Variants such as `.solid`, `.soft`, `.outline`, and `.surface` are
79
79
  shared by buttons, badges, alerts, and cards; `.inverted` is a shared surface
80
80
  modifier that paints any block with the inverse surface. `.ghost` and `.link`
81
- are button-only. Size variants `.sm` and `.lg` scale controls consistently.
81
+ are button-only. Size variants `.sm` and `.lg` scale typography and
82
+ participating component geometry consistently. Density contexts `.compact` and
83
+ `.spacious` change geometry and spacing without changing typography or icons.
82
84
  Undocumented `is-*` classes are runtime internals.
83
85
 
84
86
  ```html
@@ -139,16 +141,16 @@ internal design documents for contributors.
139
141
 
140
142
  Actual CSS is built around progressive enhancement.
141
143
 
142
- | Tier | Firefox | Safari | Chromium |
143
- | ------------ | ------- | --------- | -------- |
144
- | Degraded | 78+ | 14+ | 88+ |
145
- | **Minimal** | **98+** | **15.4+** | **99+** |
146
- | Intermediate | 121+ | 16+ | 106+ |
147
- | Recommended | 129+ | 17.5+ | 123+ |
144
+ | Tier | Firefox | Safari | Chromium |
145
+ | ----------- | -------- | ------- | -------- |
146
+ | Degraded | 78+ | 14+ | 88+ |
147
+ | **Minimal** | **125+** | **17+** | **116+** |
148
+ | Recommended | 129+ | 17.5+ | 123+ |
148
149
 
149
150
  **Degraded** — semantic HTML and core CSS remain usable. JavaScript
150
151
  enhancements are outside the supported contract.
151
152
 
152
153
  **Minimal** — full Actual support, including the JavaScript runtime. The
153
- runtime assumes modern browser APIs and ships no legacy compatibility layers or
154
- polyfills.
154
+ runtime assumes modern browser APIs — including the native manual Popover
155
+ transport used by interactive surfaces — and ships no legacy compatibility
156
+ layers or polyfills.
@@ -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
@@ -157,17 +159,30 @@ const stop = autoUpdate(trigger, float, () => {
157
159
 
158
160
  Options passed to `reposition()`:
159
161
 
160
- | Option | Default | Contract |
161
- | -------------- | ---------------- | -------------------------------------------- |
162
- | `placement` | `"bottom-start"` | Preferred side and alignment |
163
- | `distance` | `0` | Gap from the reference element |
164
- | `flip` | `true` | Flip when the preferred side is out of view |
165
- | `shift` | `true` | Shift along the cross axis to stay in bounds |
166
- | `shiftPadding` | `4` | Minimum space kept from the boundary |
167
- | `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 |
168
171
 
169
172
  `placement` takes a `-start` or `-end` alignment suffix.
170
173
 
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
+
171
186
  `@lekoala/floating` is the standalone positioning dependency used by the
172
187
  runtime. `autoUpdate(reference, float, callback)` batches scroll, resize, and
173
188
  element-resize callbacks per document and returns a stop handle; it observes
@@ -185,23 +200,32 @@ must be able to position a listbox under an input — that is the test.
185
200
  or account for:
186
201
 
187
202
  ```css
188
- .my-panel { position: absolute; z-index: 100; }
189
- .my-panel[hidden] { display: none; }
190
- /* 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 —
191
207
  do not override with !important */
192
- /* .is-open / .is-sheet / .surface-backdrop are written by surface.js
193
- — 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 */
194
211
  /* --surface-anchor-width is set while an open surface is positioned */
195
212
  /* data-actual-surface is written by surface.js for teardown
196
213
  — never select on it */
197
214
  ```
198
215
 
199
- Two non-obvious requirements:
216
+ Three non-obvious requirements:
200
217
 
201
- 1. `[hidden] { display: none }` must not be defeated by a higher-specificity
202
- `display` rule — `surface.js` toggles `hidden`, not `display`.
203
- 2. The open state switches to `position: fixed` with measured inline
204
- 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.
205
229
 
206
230
  Lifecycle: `prepareSurface` → `openSurface` → `closeSurface` →
207
231
  `disconnectSurface`. `retainSurface(panel)` reference-counts a surface shared by
@@ -209,6 +233,11 @@ several triggers and returns a release function; `isSurfaceOpen` and
209
233
  `getSurfaceAutoClose` read current state. The separate `escape` primitive adds
210
234
  visible dismissable UI to the shared per-document Escape stack.
211
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
+
212
241
  `openSurface(menu, opts)` dispatches a cancelable `actual:surface-open` event —
213
242
  widgets can veto or decorate opens, and context menus inject their own
214
243
  `source`/`restoreFocusTo` through it.
@@ -224,14 +253,11 @@ widgets can veto or decorate opens, and context menus inject their own
224
253
  | `shift` | `true` | Shift to stay within the boundary |
225
254
  | `shiftPadding` | `4` | Minimum space kept from the boundary |
226
255
  | `scope` | viewport | Boundary element |
227
- | `mobile` | `"auto"` | Sheet behavior |
228
- | `breakpoint` | `768` | Max viewport width for an automatic sheet |
229
256
  | `autoClose` | `"outside"` | Which clicks close the surface |
230
257
  | `dismissOnScroll` | `false` | Close on a scroll the user just started |
231
258
  | `restoreFocusTo` | `trigger` | Element focused on close |
232
259
 
233
260
  `x`, `y` replace the anchor entirely, which is what a context menu needs.
234
- `mobile` takes `"auto"`, `"sheet"`, `"none"` or `"anchored"`.
235
261
 
236
262
  `autoClose` takes `"true"` to close on inside and outside clicks, `"inside"` or
237
263
  `"outside"` to limit it to one side, and `"false"` to disable automatic click
@@ -391,6 +417,63 @@ on the same root throws, and `disconnect()` releases the name so it can be
391
417
  registered again. A custom root passed as the third argument must be disposed
392
418
  by its owner; the default `document.documentElement` root is not affected.
393
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
+
394
477
  ### Enhancement manifests
395
478
 
396
479
  The enhancement-loader lets a server (or static HTML) declare document-wide
@@ -676,7 +759,9 @@ field, and dispatches a bubbling `actual:invalid` event with
676
759
  <input class="input" name="email" type="email" required
677
760
  aria-describedby="email-error" />
678
761
  <span class="field-error" id="email-error">Enter a valid email.</span>
679
- <button class="btn primary" type="submit">Submit</button>
762
+ <div class="form-actions">
763
+ <button class="btn primary" type="submit">Submit</button>
764
+ </div>
680
765
  </form>
681
766
 
682
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: