actual-css 0.11.1 → 0.13.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 (123) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +25 -6
  3. package/components.json +12 -18
  4. package/dist/actual.full.css +780 -479
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/brand/favicon.svg +5 -0
  8. package/docs/brand/lockup.svg +12 -0
  9. package/docs/brand/logo-dark.svg +9 -0
  10. package/docs/brand/logo-light.svg +9 -0
  11. package/docs/brand/logo.svg +11 -0
  12. package/docs/brand/mark.svg +11 -0
  13. package/docs/design-notes/architecture.md +67 -0
  14. package/docs/design-notes/steps.md +45 -28
  15. package/docs/navigation.json +3 -2
  16. package/docs/pages/components/alert.md +2 -10
  17. package/docs/pages/components/avatar.md +17 -11
  18. package/docs/pages/components/badge.md +3 -9
  19. package/docs/pages/components/breadcrumb.md +16 -2
  20. package/docs/pages/components/card.md +31 -2
  21. package/docs/pages/components/dialog.md +121 -33
  22. package/docs/pages/components/drawer.md +2 -2
  23. package/docs/pages/components/list.md +9 -13
  24. package/docs/pages/components/navbar.md +49 -30
  25. package/docs/pages/components/pagination.md +19 -0
  26. package/docs/pages/components/steps.md +24 -20
  27. package/docs/pages/components/table.md +3 -3
  28. package/docs/pages/enhancements/flyout.md +2 -2
  29. package/docs/pages/enhancements/scrollspy.md +7 -7
  30. package/docs/pages/enhancements/status-bar.md +2 -2
  31. package/docs/pages/enhancements/tabs.md +74 -3
  32. package/docs/pages/examples/overview.md +2 -1
  33. package/docs/pages/forms/date-picker.md +90 -0
  34. package/docs/pages/forms/detached-actions.md +37 -30
  35. package/docs/pages/forms/floating-field.md +21 -7
  36. package/docs/pages/forms/form-actions.md +58 -28
  37. package/docs/pages/forms/input-policy.md +1 -1
  38. package/docs/pages/forms/overview.md +6 -5
  39. package/docs/pages/forms/states-and-sizes.md +15 -15
  40. package/docs/pages/forms/sticky-actions.md +85 -83
  41. package/docs/pages/forms/validation.md +5 -5
  42. package/docs/pages/foundations/philosophy.md +102 -4
  43. package/docs/pages/foundations/theming.md +15 -4
  44. package/docs/pages/foundations/tokens.md +89 -36
  45. package/docs/pages/foundations/typography.md +4 -7
  46. package/docs/pages/guides/bootstrap.md +2 -2
  47. package/docs/pages/guides/design-tools.md +2 -2
  48. package/docs/pages/guides/integrating-actual-css.md +79 -9
  49. package/docs/pages/guides/introduction.md +5 -5
  50. package/docs/pages/guides/modular-import.md +3 -0
  51. package/docs/pages/guides/progressive-enhancement.md +27 -9
  52. package/docs/pages/guides/tailwind.md +70 -0
  53. package/docs/pages/guides/upgrading.md +151 -0
  54. package/docs/pages/layout/center.md +2 -1
  55. package/docs/pages/layout/cluster.md +37 -4
  56. package/docs/pages/layout/grid.md +2 -0
  57. package/docs/pages/layout/overview.md +1 -1
  58. package/docs/pages/layout/sidebar-layout.md +3 -0
  59. package/docs/pages/layout/stack.md +21 -16
  60. package/docs/pages/layout/switcher.md +2 -0
  61. package/docs/pages/layout/topbar.md +7 -5
  62. package/docs/pages/patterns/icon-slot.md +17 -1
  63. package/docs/pages/patterns/navigation-list.md +16 -46
  64. package/docs/pages/patterns/overview.md +3 -4
  65. package/docs/pages/utilities/flex.md +30 -20
  66. package/docs/pages/utilities/sizing-wrapping.md +52 -7
  67. package/docs/pages/utilities/spacing.md +4 -3
  68. package/package.json +6 -6
  69. package/reserved-classes.json +3 -3
  70. package/skills/actual-css/references/enhancements.md +10 -7
  71. package/src/cli/actual-css.js +39 -13
  72. package/src/css/components/alert.css +34 -34
  73. package/src/css/components/avatar.css +8 -17
  74. package/src/css/components/badge.css +1 -6
  75. package/src/css/components/breadcrumb.css +74 -8
  76. package/src/css/components/button.css +3 -9
  77. package/src/css/components/card.css +18 -10
  78. package/src/css/components/chat.css +4 -5
  79. package/src/css/components/drawer.css +42 -45
  80. package/src/css/components/flyout.css +3 -1
  81. package/src/css/components/index.css +0 -1
  82. package/src/css/components/join.css +2 -2
  83. package/src/css/components/list.css +10 -10
  84. package/src/css/components/modal.css +113 -134
  85. package/src/css/components/navbar.css +22 -31
  86. package/src/css/components/pagination.css +5 -0
  87. package/src/css/components/scrollspy.css +1 -1
  88. package/src/css/components/steps.css +66 -12
  89. package/src/css/components/tab.css +134 -4
  90. package/src/css/core/base.css +1 -1
  91. package/src/css/core/focus.css +8 -6
  92. package/src/css/core/reset.css +2 -1
  93. package/src/css/core/theme.css +10 -1
  94. package/src/css/core/tokens.css +32 -12
  95. package/src/css/core/variants.css +41 -14
  96. package/src/css/forms/choice-card.css +1 -1
  97. package/src/css/forms/choice.css +1 -1
  98. package/src/css/forms/control.css +1 -1
  99. package/src/css/forms/floating-field.css +20 -18
  100. package/src/css/forms/form-actions.css +14 -12
  101. package/src/css/forms/native.css +2 -2
  102. package/src/css/forms/otp.css +4 -5
  103. package/src/css/forms/switch.css +1 -1
  104. package/src/css/forms/validation.css +23 -30
  105. package/src/css/layout/center.css +2 -0
  106. package/src/css/layout/cluster.css +25 -0
  107. package/src/css/layout/grid.css +7 -0
  108. package/src/css/layout/scroller.css +19 -3
  109. package/src/css/layout/sidebar-layout.css +3 -0
  110. package/src/css/layout/switcher.css +3 -0
  111. package/src/css/layout/topbar.css +12 -6
  112. package/src/css/utilities/base.css +5 -0
  113. package/src/css/utilities/extra.css +3 -7
  114. package/src/js/full.js +4 -0
  115. package/src/js/inline-visible.js +54 -0
  116. package/src/js/parsed.js +21 -0
  117. package/src/js/tab.js +33 -7
  118. package/src/tooling/color.js +65 -0
  119. package/src/tooling/design-export.js +1 -1
  120. package/src/tooling/design-tokens.js +53 -34
  121. package/src/tooling/theme-contrast.js +217 -0
  122. package/docs/pages/patterns/actions.md +0 -52
  123. package/src/css/components/actions.css +0 -27
@@ -31,14 +31,14 @@ fails to submit shows its `data-validation-message` in the status bar with the
31
31
  stays optional.
32
32
 
33
33
  ```html demo
34
- <form class="needs-validation" data-enhance="validation" data-validation-message="Please check the highlighted fields.">
34
+ <form class="stack needs-validation" data-enhance="validation" data-validation-message="Please check the highlighted fields.">
35
35
  <label class="field">
36
36
  <span class="field-label">Email</span>
37
37
  <input class="input" type="email" name="email" required
38
38
  aria-describedby="sb-email-error" />
39
39
  <span class="field-error" id="sb-email-error" role="alert">Enter a valid email.</span>
40
40
  </label>
41
- <div class="form-actions">
41
+ <div class="cluster form-actions">
42
42
  <button class="btn primary" type="submit">Submit</button>
43
43
  </div>
44
44
  </form>
@@ -14,14 +14,17 @@
14
14
 
15
15
  - Use real tab semantics when panels switch in place.
16
16
  - Use normal links and `aria-current="page"` for page navigation that only looks like tabs.
17
- - On that link variant, `aria-current="page"` on the current link gets the same
18
- color and underline treatment as `aria-selected="true"` on a widget tab;
19
- `primary` tints it.
17
+ - On that link variant, `aria-current="page"` gets the same color and
18
+ underline treatment as `aria-selected="true"` on a widget tab; `primary`
19
+ tints it. A flyout trigger takes `"true"` (see below); any other value is
20
+ inert.
20
21
  - Tab selection never changes text metrics: the active tab keeps the shared
21
22
  weight and is marked by color and the indicator line.
22
23
  - JavaScript owns roving `tabindex`, `aria-selected`, `hidden`, and keyboard behavior.
23
24
  - Left/Right select tabs and wrap at the ends. Home/End jump to first/last. Down moves focus into the selected panel.
24
25
  - A tab list needs both `.tabs` and `role="tablist"`; `.tab` styles each trigger.
26
+ A tab belongs to its closest `role="tablist"`. `data-enhance="tabs"` on an
27
+ element without that role wires nothing and warns in the console.
25
28
  - Naming follows the container/item convention (like `menu` / `menu-item`):
26
29
  `.tabs` is the strip, `.tab` is one trigger. The JS module
27
30
  (`actual-css/js/tab`) is named for the file; the behavior token
@@ -86,6 +89,74 @@ JavaScript involved:
86
89
  </nav>
87
90
  ```
88
91
 
92
+ ## Equal-width tabs
93
+
94
+ To make a few tabs share the strip's width (segmented, full-width, fill), set
95
+ `--tab-flex: 1 1 0` on the strip. `.switcher` does not apply here: a tab strip
96
+ never wraps, so it could never stack.
97
+
98
+ ```html demo
99
+ <nav aria-label="Pricing sections">
100
+ <ul class="tabs" style="--tab-flex: 1 1 0">
101
+ <li><a class="tab primary" href="#reasons" aria-current="page">Reasons</a></li>
102
+ <li><a class="tab" href="#rates">Rates</a></li>
103
+ </ul>
104
+ </nav>
105
+ ```
106
+
107
+ ## Too many tabs
108
+
109
+ A horizontal strip is one row and never wraps, nor does a label: a second line
110
+ would not match the Left/Right keys and reads as a nested level. When the tabs
111
+ do not fit (narrow screen, mobile, long translations), the strip scrolls
112
+ horizontally without a scrollbar; in supporting browsers, the edges it can
113
+ still scroll toward fade out. Selecting or focusing the tab clipped at the edge
114
+ scrolls it into view with the next one peeking, so a click, the arrow keys or a
115
+ swipe move along the strip. A tab selected in the markup starts in view too,
116
+ when the strip is visible at connect; the placement moves the strip only, never
117
+ the page. In supporting browsers, so does the current link tab (or flyout
118
+ trigger), without the runtime; elsewhere the strip starts at its first tab. A
119
+ vertical strip (`aria-orientation="vertical"`) keeps wrapping its labels.
120
+
121
+ Scrolling is the fallback, not a responsive design. If a strip regularly
122
+ overflows, shorten the labels, group sections, or switch to a vertical rail.
123
+
124
+ ## Tab with a flyout
125
+
126
+ In navigation tabs, a `.tab` can be a [flyout](flyout.md) trigger for a group of
127
+ pages. The link to the current page inside its panel carries
128
+ `aria-current="page"`. The trigger is not a page, so it takes
129
+ `aria-current="true"` while its panel holds the current page: the section is
130
+ the current item of the strip. This is the only place a `.tab` takes `true`.
131
+
132
+ ```html demo
133
+ <nav aria-label="Account">
134
+ <ul class="tabs">
135
+ <li><a class="tab" href="#profile">Profile</a></li>
136
+ <li><a class="tab" href="#security">Security</a></li>
137
+ <li class="flyout-trigger">
138
+ <button class="tab"
139
+ type="button"
140
+ data-enhance="flyout"
141
+ aria-current="true"
142
+ aria-expanded="false"
143
+ aria-controls="billing-menu">
144
+ Billing <i class="ti ti-chevron-down" aria-hidden="true"></i>
145
+ </button>
146
+ <ul class="flyout menu" id="billing-menu" hidden>
147
+ <li><a class="menu-item" href="#invoices" aria-current="page">Invoices</a></li>
148
+ <li><a class="menu-item" href="#payment">Payment methods</a></li>
149
+ </ul>
150
+ </li>
151
+ </ul>
152
+ </nav>
153
+ ```
154
+
155
+ Do not put a flyout trigger in a `role="tablist"`: a tablist holds only
156
+ `role="tab"` items, and the arrow keys of the two widgets collide.
157
+
89
158
  ## CSS hooks
90
159
 
160
+ - `--tab-flex` — flex of each item; set `1 1 0` on the strip for equal-width tabs
161
+ (segmented, fill the width). A strip that cannot fit its labels still scrolls.
91
162
  - `--tab-gap` — space between an icon and the label text (default `0.375em`).
@@ -14,7 +14,7 @@ rest show it composed, isolated, or under stress.
14
14
 
15
15
  ### Start here
16
16
 
17
- - [Visual guide](../../demo/templates/visual-guide.html) - seventeen illustrated figures on the spacing scale, the layout primitives, and density versus local size
17
+ - [Visual guide](../../demo/templates/visual-guide.html) - twenty illustrated figures on the spacing scale, the layout primitives, and rhythm versus density versus local size
18
18
  - [Color guide](../../demo/templates/color-guide.html) - theming principles measured live on `ocean` and `sunset`, in light and dark, as `data-theme` islands
19
19
  - [Kitchen sink](../../demo/templates/kitchen-sink.html) - one-screen test bed for themes, scopes, variants and interactions
20
20
 
@@ -60,6 +60,7 @@ extension points.
60
60
 
61
61
  - [Data grid](../../demo/templates/data-grid.html) - an external data grid component themed with Actual tokens (sort, filter, search, selection)
62
62
  - [Combobox](../../demo/templates/combobox.html) - an external combobox themed with Actual tokens (search, tags, autocomplete)
63
+ - [Date fields](../../demo/templates/date-fields.html) - a native date input, a masked text field and an external date picker on Actual tokens, side by side
63
64
 
64
65
  ## Sites
65
66
 
@@ -0,0 +1,90 @@
1
+ # Date picker
2
+
3
+ > A JavaScript date picker is not shipped by the framework — the native `<input type="date">` stays the baseline. Use `data-mask` for a fixed text shape, or theme an external picker with Actual tokens.
4
+
5
+ The native control is the default: a real calendar, no JavaScript, and an ISO
6
+ `yyyy-mm-dd` value whatever the platform displays. Reach for an external picker
7
+ only when the product needs a shape the native control cannot express — a month
8
+ grid for a range, a typed constraint, a styled calendar.
9
+
10
+ **Related terms:** date field, datepicker, calendar, native date, date input, masked date, input mask, date range.
11
+
12
+ ## Native baseline
13
+
14
+ ```html demo
15
+ <label class="field">
16
+ <span class="field-label">Start date</span>
17
+ <input class="input" type="date" />
18
+ </label>
19
+ ```
20
+
21
+ ## Masked text field
22
+
23
+ `data-mask` structures a plain text field to a fixed shape and the `date` rule
24
+ validates meaning. The mask enforces shape, not validity, so `2026-13-40` needs
25
+ the rule (or server validation).
26
+
27
+ ```html demo
28
+ <label class="field">
29
+ <span class="field-label">Date</span>
30
+ <input class="input" data-mask="9999-99-99" inputmode="numeric" autocomplete="off" placeholder="yyyy-mm-dd" />
31
+ </label>
32
+ ```
33
+
34
+ ## Theme bridge
35
+
36
+ The [date fields template](../../demo/templates/date-fields.html) puts the native
37
+ control, the masked field and
38
+ [`@lekoala/date-picker`](https://github.com/lekoala/date-picker) side by side, the
39
+ picker skinned entirely with Actual tokens.
40
+
41
+ ```html
42
+ <label class="field">
43
+ <span class="field-label">Booking date</span>
44
+ <date-picker value="2026-09-25" open-on-focus="false">
45
+ <input class="input" name="date">
46
+ </date-picker>
47
+ </label>
48
+ ```
49
+
50
+ ```css
51
+ date-picker,
52
+ date-calendar {
53
+ --dp-bg: var(--surface-raised);
54
+ --dp-fg: var(--text);
55
+ --dp-muted: var(--text-muted);
56
+ --dp-border: var(--border);
57
+ --dp-hover: var(--surface-subtle);
58
+ --dp-accent: var(--primary);
59
+ --dp-accent-fg: var(--primary-fg);
60
+ --dp-radius: var(--radius);
61
+ /* Trigger glyph on the select chevron axis. */
62
+ --dp-picker-button-size: calc(var(--control-pad-x) * 2 + 1em + var(--border-width) * 2);
63
+ }
64
+
65
+ .dp-calendar-header :where(button, select, input):focus-visible {
66
+ border-color: var(--dp-border);
67
+ outline: var(--focus-ring-width) solid var(--focus);
68
+ outline-offset: calc(var(--focus-ring-width) * -1);
69
+ box-shadow: none;
70
+ }
71
+
72
+ .dp-picker-button:focus-visible,
73
+ .dp-day:focus-visible {
74
+ outline-color: var(--focus);
75
+ }
76
+ ```
77
+
78
+ The field is a plain `.input`, so the core inset focus outline already outranks
79
+ the library's halo; the calendar's own controls paint a `--dp-accent` halo, so
80
+ the two focus rules map them onto the core outline. The calendar popup is its own
81
+ element, so the `--dp-*` map is declared on `date-picker`/`date-calendar` rather
82
+ than inherited from a wrapper.
83
+ As with the combobox, the bridge stays in docs/demo — it is a recipe, not package
84
+ API, and pins no widget version.
85
+
86
+ ## Which one?
87
+
88
+ - A plain date the platform can collect: native `<input type="date">`.
89
+ - A fixed text format that must survive as typed text: `data-mask` plus the `date` rule.
90
+ - A calendar, range, or constraint the native control cannot express: an external picker on the token bridge.
@@ -5,12 +5,12 @@
5
5
  The simplest case stays inside the form.
6
6
 
7
7
  ```html
8
- <form novalidate>
8
+ <form class="stack" novalidate>
9
9
  <div class="stack">
10
10
  <!-- fields -->
11
11
  </div>
12
12
 
13
- <div class="form-actions">
13
+ <div class="cluster form-actions">
14
14
  <button type="button" class="btn outline">Cancel</button>
15
15
  <button type="submit" class="btn primary">Save changes</button>
16
16
  </div>
@@ -20,40 +20,47 @@ The simplest case stays inside the form.
20
20
  For sticky page footers, dialog footers, card footers, and split layouts, detach the actions and connect the submit button with `form="<id>"`.
21
21
 
22
22
  ```html
23
- <form id="profile-form" novalidate>
24
- <div class="stack">
25
- <!-- long form fields -->
26
- </div>
27
- </form>
23
+ <div class="stack">
24
+ <form id="profile-form" novalidate>
25
+ <div class="stack">
26
+ <!-- long form fields -->
27
+ </div>
28
+ </form>
28
29
 
29
- <footer class="form-actions">
30
- <button type="button" class="btn outline">Cancel</button>
31
- <button type="submit" class="btn primary" form="profile-form">
32
- Save changes
33
- </button>
34
- </footer>
30
+ <footer class="cluster form-actions">
31
+ <button type="button" class="btn outline">Cancel</button>
32
+ <button type="submit" class="btn primary" form="profile-form">
33
+ Save changes
34
+ </button>
35
+ </footer>
36
+ </div>
35
37
  ```
36
38
 
39
+ The wrapper that holds the form and its detached actions owns the distance
40
+ between them: give it `.stack`.
41
+
37
42
  For multiple submit intents, pair the `form` attribute with `formaction`. The form has one action; each submit button can override it.
38
43
 
39
44
  ```html
40
- <form id="article-form"
41
- action="/articles/publish"
42
- method="post">
43
- <!-- fields -->
44
- </form>
45
+ <div class="stack">
46
+ <form id="article-form"
47
+ action="/articles/publish"
48
+ method="post">
49
+ <!-- fields -->
50
+ </form>
45
51
 
46
- <div class="form-actions">
47
- <button type="submit"
48
- class="btn outline"
49
- form="article-form"
50
- formaction="/articles/draft">
51
- Save draft
52
- </button>
53
- <button type="submit"
54
- class="btn primary"
55
- form="article-form">
56
- Publish
57
- </button>
52
+ <div class="cluster form-actions">
53
+ <button type="submit"
54
+ class="btn outline"
55
+ form="article-form"
56
+ formaction="/articles/draft">
57
+ Save draft
58
+ </button>
59
+ <button type="submit"
60
+ class="btn primary"
61
+ form="article-form">
62
+ Publish
63
+ </button>
64
+ </div>
58
65
  </div>
59
66
  ```
@@ -7,11 +7,12 @@ the label rests inside the control and floats to the top when the control is
7
7
  focused or filled. It composes the existing `.field` layout and the text
8
8
  controls (`.input`, `.textarea`, `.select`) with zero JavaScript.
9
9
 
10
- The cell holds exactly one control and its label, with the control before the
11
- label. Label placement is linked through the general sibling combinator, so an
12
- injected node between the two (a password manager, an extension wrapper) is
13
- tolerated. Help and error messages stay in the surrounding `.field`, otherwise
14
- the resting label centers on the whole field instead of the control.
10
+ The cell holds exactly one control and its label, in either order: a form
11
+ theme that renders the label before the widget needs no reordering. An
12
+ injected sibling (a password manager, an extension wrapper) is tolerated as
13
+ long as the control stays a direct child of the cell. Help and error messages
14
+ stay in the surrounding `.field`, otherwise the resting label centers on the
15
+ whole field instead of the control.
15
16
 
16
17
  ```html demo
17
18
  <div class="field">
@@ -28,6 +29,17 @@ can detect emptiness — the space keeps the placeholder invisible while making
28
29
  the state detectable. `:autofill` is covered too, so a password manager that
29
30
  prefills a field still lifts the label.
30
31
 
32
+ The same cell with the label first, as a label-then-widget form theme renders it:
33
+
34
+ ```html demo
35
+ <div class="field">
36
+ <label class="floating-field">
37
+ <span class="field-label">Full name</span>
38
+ <input class="input" type="text" autocomplete="name" placeholder=" " />
39
+ </label>
40
+ </div>
41
+ ```
42
+
31
43
  ## Textarea
32
44
 
33
45
  A textarea's empty label starts at the control's normal first-line position,
@@ -89,8 +101,10 @@ line on top of the control's minimum size.
89
101
  Validation and focus come from the existing form contracts: the control's
90
102
  border and focus ring flip through `--form-invalid-border` and the shared
91
103
  control focus rules. Coloring the label itself on focus or invalid state is
92
- left to the surrounding recipe — the joined floating form in the
93
- [blocks demo](../../demo/templates/blocks.html) shows one such recipe.
104
+ left to the surrounding recipe; select the label through the cell
105
+ (`.floating-field:has(> :focus) > .field-label`) so it holds in both orders.
106
+ The joined floating form in the [blocks demo](../../demo/templates/blocks.html)
107
+ shows one such recipe.
94
108
 
95
109
  > Not currently compatible with `.input-icon`; use standard labels when an
96
110
  > embedded leading icon is required.
@@ -1,20 +1,21 @@
1
1
  # Form Actions
2
2
 
3
- > `.form-actions` is a class-only flex row that closes a form. It may live
4
- > inside or outside the `<form>` (see Detached Actions) and has an opt-in
3
+ > `.form-actions` is the role of the `.cluster` row that closes a form. It may
4
+ > live inside or outside the `<form>` (see Detached Actions) and has an opt-in
5
5
  > sticky variant (see Sticky Actions).
6
6
 
7
- The default layout is a wrapping flex row: items are centered on the cross
8
- axis and start-aligned on the main axis, with a default top margin that
9
- separates the actions from the fields above.
7
+ `.cluster` owns the row: flex, wrap and gap. `.form-actions` adds the alignment
8
+ policy for form actions and the sticky behaviour. The space before the row
9
+ belongs to the parent layout: put `.stack` on the form.
10
10
 
11
- ```html
12
- <form novalidate>
13
- <div class="stack">
14
- <!-- fields -->
15
- </div>
11
+ ```html demo
12
+ <form class="stack" novalidate>
13
+ <label class="field">
14
+ <span class="field-label">Project name</span>
15
+ <input class="input" type="text" value="Atlas" />
16
+ </label>
16
17
 
17
- <div class="form-actions">
18
+ <div class="cluster form-actions">
18
19
  <button type="button" class="btn outline">Cancel</button>
19
20
  <button type="submit" class="btn primary">Save changes</button>
20
21
  </div>
@@ -23,34 +24,63 @@ separates the actions from the fields above.
23
24
 
24
25
  ## CSS hooks
25
26
 
26
- The three layout hooks are unset by default and fall back to the values below.
27
+ - `--form-actions-justify` — main-axis distribution (`justify-content`).
28
+ - `--form-actions-align` — cross-axis alignment (`align-items`).
27
29
 
28
- - `--form-actions-align` — `center` — cross-axis alignment (`align-items`).
29
- - `--form-actions-justify` — `flex-start` — main-axis distribution (`justify-content`).
30
- - `--form-actions-margin-block-start` — `var(--space-50)` — top margin separating actions from the form.
30
+ Both are inherited role hooks: set them on a page or container to align every
31
+ form action row inside it, without touching other clusters. Below, the
32
+ container right-aligns the form actions while the tag row, also a `.cluster`,
33
+ keeps its start alignment.
31
34
 
32
- ### Common arrangements
35
+ ```html demo
36
+ <section class="stack" style="--form-actions-justify: flex-end">
37
+ <div class="cluster">
38
+ <span class="badge">Draft</span>
39
+ <span class="badge">Internal</span>
40
+ </div>
33
41
 
34
- Right-align the submit on wide rows:
42
+ <form class="stack" novalidate>
43
+ <label class="field">
44
+ <span class="field-label">Release title</span>
45
+ <input class="input" type="text" value="Autumn update" />
46
+ </label>
35
47
 
36
- ```css
37
- .form-actions {
38
- --form-actions-justify: flex-end;
39
- }
48
+ <div class="cluster form-actions">
49
+ <button type="button" class="btn outline">Cancel</button>
50
+ <button type="submit" class="btn primary">Publish</button>
51
+ </div>
52
+ </form>
53
+ </section>
40
54
  ```
41
55
 
42
- Stretch each action to fill the full row width:
56
+ In application CSS, the same policy is one declaration on the region:
43
57
 
44
58
  ```css
45
- .form-actions {
46
- --form-actions-align: stretch;
59
+ .checkout {
60
+ --form-actions-justify: flex-end;
47
61
  }
48
62
  ```
49
63
 
50
- Because the hooks are inherited custom properties, set them on a page or
51
- container to apply one arrangement to every `.form-actions` inside it.
64
+ On a form action row, use the hooks above rather than `--cluster-justify` /
65
+ `--cluster-align`: the role writes those from its own hooks. For one row, a
66
+ `.justify-content-*` / `.items-*` utility also works.
67
+ A split pair keeps the dismissive action at the leading edge and the committing
68
+ one at the trailing edge:
69
+
70
+ ```html demo
71
+ <form class="stack" novalidate>
72
+ <label class="field">
73
+ <span class="field-label">Display name</span>
74
+ <input class="input" type="text" value="Jane Doe" />
75
+ </label>
76
+
77
+ <div class="cluster form-actions justify-content-space-between">
78
+ <button type="button" class="btn outline">Cancel</button>
79
+ <button type="submit" class="btn primary">Save</button>
80
+ </div>
81
+ </form>
82
+ ```
52
83
 
53
84
  > The sticky variant adds `--form-actions-sticky-block-offset`,
54
85
  > `--form-actions-sticky-padding`, and `--form-actions-sticky-inline-offset` —
55
- > see Sticky Actions. `.form-actions` is class-only and works inside or
56
- > outside the form — see Detached Actions.
86
+ > see Sticky Actions.
@@ -124,7 +124,7 @@ of the value and never rewrites it.
124
124
  </span>
125
125
  </label>
126
126
 
127
- <div class="form-actions">
127
+ <div class="cluster form-actions">
128
128
  <button class="btn primary" type="submit">Submit</button>
129
129
  <button class="btn neutral outline" type="reset">Reset</button>
130
130
  </div>
@@ -10,6 +10,7 @@
10
10
  - `.field-group` and `.choice` provide layout without depending on parent scope.
11
11
  - Customizable select is progressive enhancement only — the native select remains the baseline.
12
12
  - Searchable selects, tags and autocomplete are a JavaScript widget concern; theme an external combobox with Actual tokens — see the [combobox template](../../demo/templates/combobox.html).
13
+ - A date is native `<input type="date">` first; use `data-mask` for a fixed text shape, or theme an external picker — see [date picker](date-picker.md) and the [date fields template](../../demo/templates/date-fields.html).
13
14
  - Formatted text (bold, lists, links) is a hosted-editor concern; the native textarea stays the baseline and an external engine is themed the same way — see the [rich-text template](../../demo/templates/rich-text.html) and the [composer template](../../demo/templates/composer.html) for the plain-text case.
14
15
 
15
16
  **Related terms:** form controls, select, custom select, dropdown, input, textarea, checkbox, radio.
@@ -33,7 +34,7 @@
33
34
  | `.field-error` | Composition | Inline error message, shown while invalid. |
34
35
  | `.field-group` | Composition | `<fieldset>` with a tunable legend gutter. |
35
36
  | `.choice` | Composition | Choice-label layout around an inline control. |
36
- | `.form-actions` | Composition | Class-only flex row that closes a form. |
37
+ | `.form-actions` | Composition | Role of the `.cluster` row closing a form. |
37
38
 
38
39
  `.input` shares the text-control recipe with `.textarea` and `.select`, and
39
40
  `.select` also has an opt-in enhanced picker. `.field-label` works on a
@@ -46,7 +47,7 @@
46
47
  A full form using the control classes, field wrappers, and form actions:
47
48
 
48
49
  ```html demo
49
- <form novalidate>
50
+ <form class="stack" novalidate>
50
51
  <div class="stack">
51
52
 
52
53
  <label class="field">
@@ -101,7 +102,7 @@ A full form using the control classes, field wrappers, and form actions:
101
102
 
102
103
  <fieldset class="field-group">
103
104
  <legend class="field-label">Availability</legend>
104
- <div class="stack compact">
105
+ <div class="stack tight">
105
106
  <label class="choice">
106
107
  <input class="radio" type="radio" name="availability" value="available" checked />
107
108
  <span>Available for work</span>
@@ -161,7 +162,7 @@ A full form using the control classes, field wrappers, and form actions:
161
162
 
162
163
  </div>
163
164
 
164
- <div class="form-actions">
165
+ <div class="cluster form-actions">
165
166
  <button type="button" class="btn outline">Cancel</button>
166
167
  <button type="submit" class="btn primary">Save changes</button>
167
168
  </div>
@@ -185,7 +186,7 @@ it a stronger fill.
185
186
  - `.choice` is the choice-label API. Use `.check` or `.radio` on the nested control. The control goes first, the label group second, so multi-line labels align the control with the first line.
186
187
  - For a switch, use `class="switch"` and `role="switch"`.
187
188
  - `.range` skins the native range control (`appearance: none` track, thumb, and focus ring). Add `data-enhance="range"` for the fill share, named `datalist` values, and output sync.
188
- - `.form-actions` carries a default top margin. Override it with `--form-actions-margin-block-start`, `--form-actions-align`, or `--form-actions-justify`. It is class-only and may live inside or outside `<form>`. See Detached Actions below.
189
+ - `.form-actions` is the role of a `.cluster` row (`class="cluster form-actions"`) and carries no outer margin: `.stack` on the form spaces it from the fields. It is class-only and may live inside or outside `<form>`. See Detached Actions below.
189
190
  - `.join` visually joins adjacent controls into a single unit. Use `.join-addon` for static prefix/suffix content. The container still carries `role="group"` for accessibility.
190
191
 
191
192
  ## Optional automatic required marks
@@ -26,8 +26,8 @@ together. Density contexts leave both unchanged.
26
26
  <form class="stack" novalidate>
27
27
  <fieldset class="field-group">
28
28
  <legend class="field-label">Text control states</legend>
29
- <div class="stack" style="--grid-min: 14rem;">
30
- <div class="grid items-start">
29
+ <div class="stack">
30
+ <div class="grid items-start" style="--grid-min: 14rem;">
31
31
  <label class="field">
32
32
  <span class="field-label">Editable</span>
33
33
  <input class="input" type="text" value="Ocean Beach Clinic" />
@@ -46,7 +46,7 @@ together. Density contexts leave both unchanged.
46
46
  </label>
47
47
  </div>
48
48
 
49
- <div class="grid items-start">
49
+ <div class="grid items-start" style="--grid-min: 14rem;">
50
50
  <label class="field">
51
51
  <span class="field-label">Editable notes</span>
52
52
  <textarea class="textarea">Patient asked for an invoice copy.</textarea>
@@ -67,8 +67,8 @@ together. Density contexts leave both unchanged.
67
67
 
68
68
  <fieldset class="field-group">
69
69
  <legend class="field-label">Choice and select states</legend>
70
- <div class="stack" style="--grid-min: 14rem;">
71
- <div class="grid items-start">
70
+ <div class="stack">
71
+ <div class="grid items-start" style="--grid-min: 14rem;">
72
72
  <label class="field">
73
73
  <span class="field-label">Editable select</span>
74
74
  <select class="select">
@@ -98,7 +98,7 @@ together. Density contexts leave both unchanged.
98
98
  </label>
99
99
  </div>
100
100
 
101
- <div class="grid items-start">
101
+ <div class="grid items-start" style="--grid-min: 14rem;">
102
102
  <label class="choice">
103
103
  <input class="check" type="checkbox" />
104
104
  <span>
@@ -140,7 +140,7 @@ together. Density contexts leave both unchanged.
140
140
  </label>
141
141
  </div>
142
142
 
143
- <div class="grid items-start">
143
+ <div class="grid items-start" style="--grid-min: 14rem;">
144
144
  <label class="choice">
145
145
  <input class="radio" type="radio" name="demo-radio-state" />
146
146
  <span>
@@ -174,7 +174,7 @@ together. Density contexts leave both unchanged.
174
174
  </label>
175
175
  </div>
176
176
 
177
- <div class="grid items-start">
177
+ <div class="grid items-start" style="--grid-min: 14rem;">
178
178
  <label class="choice">
179
179
  <input class="switch" type="checkbox" role="switch" />
180
180
  <span>
@@ -212,8 +212,8 @@ together. Density contexts leave both unchanged.
212
212
 
213
213
  <fieldset class="field-group">
214
214
  <legend class="field-label">Control sizes</legend>
215
- <div class="stack" style="--grid-min: 14rem;">
216
- <div class="grid items-start">
215
+ <div class="stack">
216
+ <div class="grid items-start" style="--grid-min: 14rem;">
217
217
  <label class="field sm">
218
218
  <span class="field-label">Small field</span>
219
219
  <input class="input" type="text" value="sm" />
@@ -230,7 +230,7 @@ together. Density contexts leave both unchanged.
230
230
  </label>
231
231
  </div>
232
232
 
233
- <div class="grid items-start">
233
+ <div class="grid items-start" style="--grid-min: 14rem;">
234
234
  <label class="field sm">
235
235
  <span class="field-label">Small select</span>
236
236
  <select class="select">
@@ -253,7 +253,7 @@ together. Density contexts leave both unchanged.
253
253
  </label>
254
254
  </div>
255
255
 
256
- <div class="grid items-start">
256
+ <div class="grid items-start" style="--grid-min: 14rem;">
257
257
  <label class="field sm">
258
258
  <span class="field-label">Small textarea</span>
259
259
  <textarea class="textarea" rows="2">Short note</textarea>
@@ -270,14 +270,14 @@ together. Density contexts leave both unchanged.
270
270
  </label>
271
271
  </div>
272
272
 
273
- <div class="grid items-start">
273
+ <div class="grid items-start" style="--grid-min: 14rem;">
274
274
  <button class="btn outline sm" type="button">Small</button>
275
275
  <button class="btn outline" type="button">Default</button>
276
276
  <button class="btn outline lg" type="button">Large</button>
277
277
  </div>
278
278
 
279
279
  <!-- Size belongs on the choice wrapper so label and control move together. -->
280
- <div class="grid items-start">
280
+ <div class="grid items-start" style="--grid-min: 14rem;">
281
281
  <label class="choice sm">
282
282
  <input class="check" type="checkbox" checked />
283
283
  <span>Small checkbox</span>
@@ -297,7 +297,7 @@ together. Density contexts leave both unchanged.
297
297
  </div>
298
298
 
299
299
  <!-- The underlying typography hook remains available for custom scales. -->
300
- <div class="grid items-start" style="--control-font-size: var(--font-size-sm);">
300
+ <div class="grid items-start" style="--grid-min: 14rem; --control-font-size: var(--font-size-sm);">
301
301
  <label class="choice">
302
302
  <input class="check" type="checkbox" checked />
303
303
  <span>Smaller type</span>