actual-css 0.12.0 → 0.14.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 (108) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +7 -8
  3. package/components.json +124 -12
  4. package/dist/actual.full.css +1152 -309
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/architecture.md +67 -0
  8. package/docs/design-notes/steps.md +45 -28
  9. package/docs/navigation.json +2 -0
  10. package/docs/pages/components/alert.md +12 -2
  11. package/docs/pages/components/avatar.md +17 -11
  12. package/docs/pages/components/breadcrumb.md +16 -2
  13. package/docs/pages/components/button.md +3 -2
  14. package/docs/pages/components/list.md +15 -16
  15. package/docs/pages/components/navbar.md +42 -22
  16. package/docs/pages/components/pagination.md +19 -0
  17. package/docs/pages/components/segmented.md +83 -0
  18. package/docs/pages/components/steps.md +28 -22
  19. package/docs/pages/enhancements/flyout.md +5 -0
  20. package/docs/pages/enhancements/scrollspy.md +6 -6
  21. package/docs/pages/enhancements/tabs.md +79 -3
  22. package/docs/pages/examples/overview.md +3 -1
  23. package/docs/pages/forms/color-swatches.md +131 -0
  24. package/docs/pages/forms/combobox.md +2 -2
  25. package/docs/pages/forms/controls.md +6 -3
  26. package/docs/pages/forms/floating-field.md +21 -7
  27. package/docs/pages/forms/form-actions.md +3 -6
  28. package/docs/pages/forms/joined-controls.md +6 -2
  29. package/docs/pages/forms/overview.md +1 -0
  30. package/docs/pages/forms/states-and-sizes.md +15 -15
  31. package/docs/pages/forms/switch.md +4 -2
  32. package/docs/pages/foundations/philosophy.md +97 -1
  33. package/docs/pages/foundations/theming.md +1 -1
  34. package/docs/pages/foundations/tokens.md +94 -27
  35. package/docs/pages/guides/design-tools.md +14 -8
  36. package/docs/pages/guides/integrating-actual-css.md +74 -8
  37. package/docs/pages/guides/introduction.md +3 -2
  38. package/docs/pages/guides/modular-import.md +1 -1
  39. package/docs/pages/guides/progressive-enhancement.md +25 -7
  40. package/docs/pages/guides/upgrading.md +86 -9
  41. package/docs/pages/layout/center.md +2 -1
  42. package/docs/pages/layout/cluster.md +11 -4
  43. package/docs/pages/layout/grid.md +2 -0
  44. package/docs/pages/layout/scroll-snap.md +4 -2
  45. package/docs/pages/layout/sidebar-layout.md +3 -0
  46. package/docs/pages/layout/switcher.md +2 -0
  47. package/docs/pages/layout/topbar.md +6 -4
  48. package/docs/pages/patterns/icon-slot.md +17 -1
  49. package/docs/pages/utilities/flex.md +1 -1
  50. package/docs/pages/utilities/sizing-wrapping.md +51 -6
  51. package/package.json +6 -6
  52. package/reserved-classes.json +3 -0
  53. package/src/cli/actual-css.js +4 -4
  54. package/src/css/components/alert.css +99 -48
  55. package/src/css/components/app-nav.css +17 -9
  56. package/src/css/components/avatar.css +2 -18
  57. package/src/css/components/badge.css +23 -10
  58. package/src/css/components/breadcrumb.css +74 -8
  59. package/src/css/components/button.css +17 -2
  60. package/src/css/components/close.css +6 -3
  61. package/src/css/components/drawer.css +23 -42
  62. package/src/css/components/flyout.css +0 -1
  63. package/src/css/components/index.css +1 -0
  64. package/src/css/components/join.css +2 -2
  65. package/src/css/components/list.css +28 -13
  66. package/src/css/components/modal.css +17 -10
  67. package/src/css/components/navbar.css +15 -10
  68. package/src/css/components/pagination.css +5 -0
  69. package/src/css/components/scrollspy.css +1 -1
  70. package/src/css/components/segmented.css +186 -0
  71. package/src/css/components/steps.css +72 -15
  72. package/src/css/components/tab.css +135 -5
  73. package/src/css/core/focus.css +11 -6
  74. package/src/css/core/theme.css +6 -1
  75. package/src/css/core/tokens.css +24 -6
  76. package/src/css/core/variants.css +16 -5
  77. package/src/css/forms/choice-card.css +1 -1
  78. package/src/css/forms/choice.css +1 -1
  79. package/src/css/forms/color-swatch.css +159 -0
  80. package/src/css/forms/control.css +1 -1
  81. package/src/css/forms/floating-field.css +20 -18
  82. package/src/css/forms/form-actions.css +14 -8
  83. package/src/css/forms/index.css +1 -0
  84. package/src/css/forms/native.css +51 -21
  85. package/src/css/forms/otp.css +4 -5
  86. package/src/css/forms/switch.css +6 -3
  87. package/src/css/forms/validation.css +23 -30
  88. package/src/css/layout/center.css +2 -0
  89. package/src/css/layout/cluster.css +14 -0
  90. package/src/css/layout/grid.css +7 -0
  91. package/src/css/layout/scroll-snap.css +30 -0
  92. package/src/css/layout/scroller.css +19 -3
  93. package/src/css/layout/sidebar-layout.css +3 -0
  94. package/src/css/layout/switcher.css +3 -0
  95. package/src/css/layout/topbar.css +10 -0
  96. package/src/css/utilities/extra.css +3 -3
  97. package/src/js/focus.js +16 -1
  98. package/src/js/full.js +4 -0
  99. package/src/js/inline-visible.js +54 -0
  100. package/src/js/parsed.js +21 -0
  101. package/src/js/surface.js +10 -0
  102. package/src/js/tab.js +33 -7
  103. package/src/tooling/color.js +14 -1
  104. package/src/tooling/css-bundle.js +0 -5
  105. package/src/tooling/design-components.js +9 -5
  106. package/src/tooling/design-export.js +9 -1
  107. package/src/tooling/design-tokens.js +207 -76
  108. package/src/tooling/theme-contrast.js +182 -45
@@ -80,9 +80,11 @@ accent, override the tokens on the component:
80
80
  }
81
81
  ```
82
82
 
83
- `--state-selected` drives the completed fill, connector and the current ring;
83
+ `--state-selected` drives the completed fill and connector;
84
84
  `--state-selected-fg` corrects the number inside a completed marker when the
85
- accent needs a lighter ink.
85
+ accent needs a lighter ink. The current marker sits on the surface, so its
86
+ number and ring read `--state-selected-text` first
87
+ ([tokens](../foundations/tokens.md)).
86
88
 
87
89
  ## Horizontal steps
88
90
 
@@ -145,23 +147,28 @@ names, or when a step should be navigable:
145
147
  ```
146
148
 
147
149
  **Actual CSS never hides an authored step label.** Labels stay visible at every
148
- width. If a labelled row does not fit its space, it scrolls — and the honest
149
- answers to that are more room, fewer stages, or `.steps-vertical`.
150
+ width. Stacked steps keep their markers evenly spaced, and long names wrap
151
+ inside their share, hyphenated when the page declares a `lang`. The first and
152
+ last steps align with the row's edges, labels included, so the track lines up
153
+ with the heading above it. Short labels fit a phone with no width budget.
150
154
 
151
- Since scrolling is the fallback, a labelled row is worth pairing with
152
- `.scroller`, which gives the native scrollbar the theme's density and colour
153
- instead of the OS default:
155
+ Set `--step-min` when names need a wider minimum share. If the markers or that
156
+ minimum do not fit, the row scrolls; use `.steps-vertical` when every name must
157
+ remain visible without horizontal scrolling.
158
+
159
+ In supporting browsers, a row that scrolls starts on its `aria-current="step"`
160
+ instead of step 1. A row
161
+ that raises `--step-min` can scroll, so it is worth pairing with `.scroller`,
162
+ which gives the native scrollbar the theme's density and colour instead of the
163
+ OS default:
154
164
 
155
165
  ```html
156
166
  <ol class="steps steps-horizontal scroller">
157
167
  ```
158
168
 
159
- Steps does not apply it for you, because scrollbar treatment is an app-wide
160
- decision rather than a per-component one. Every other scroll container —
161
- `.table`, a `.flyout`, a scrolling `dialog` — leaves the scrollbar alone too,
162
- so a page that thinned only its stepper would show two kinds of scrollbar. Put
163
- `.scroller` on whatever scrolls, once. See the sizing and wrapping page for its
164
- hooks.
169
+ Steps does not apply it for you; see
170
+ [one-row strips](../utilities/sizing-wrapping.md#one-row-strips) for the
171
+ page-wide rule and the hooks.
165
172
 
166
173
  Do not add `.stable-gutter` here. It reserves gutter on the inline axis, for a
167
174
  vertical scrollbar this row never draws, so it narrows the row instead of
@@ -361,8 +368,9 @@ On `.steps`, so both orientations read them:
361
368
 
362
369
  `.steps-horizontal` only:
363
370
 
364
- - `--step-min` — minimum item width in the stacked horizontal layout
365
- - `--step-gap` — marker-to-label space in the stacked horizontal layout
371
+ - `--step-min` — minimum width budget for stacked steps; the first and last
372
+ cells use half that budget plus half the marker width
373
+ - `--step-gap` — marker-to-label and between-step space in the stacked layout
366
374
  - `--step-connector` — default horizontal connector background
367
375
  - `--step-inline-connector` — optional connector background for wide horizontal steps
368
376
 
@@ -372,12 +380,11 @@ On `.steps`, so both orientations read them:
372
380
 
373
381
  ## Notes
374
382
 
375
- Do not mix labelled and unlabelled steps in one sequence. Each step is sized on
376
- its own content, so a mixed sequence renders a narrow marker between reading-
377
- width steps — it lays out, but it reads as a gap rather than as progress.
383
+ Do not mix labelled and unlabelled steps in one sequence. Empty steps align
384
+ their markers to the start; labelled steps centre them over their names.
378
385
 
379
386
  A step name belongs in a `.step-label`. Bare text in an `<li>` is outside the
380
- contract: it is content, so it gets a reading width like a label, but no rule
387
+ contract: it takes the same item floor as a label, but no rule
381
388
  places or aligns it.
382
389
 
383
390
  The **2 to 5** range applies to the horizontal orientation, and the inline
@@ -399,9 +406,8 @@ to stay distinguishable there. Steps deliberately ships no forced-colors
399
406
  override of its own: painting the marker `Highlight` makes the number or glyph
400
407
  inside it unreadable, which is worse than the ambiguity it removes.
401
408
 
402
- The inline threshold is calibrated to the default `--step-min`. Container
403
- queries cannot read custom properties, so changing `--step-min` moves the
404
- scroll budget but not the threshold.
409
+ The inline threshold reserves connector space for five named stages. Changing
410
+ `--step-min` moves the stacked scroll budget without moving that threshold.
405
411
 
406
412
  See [Steps representations and thresholds](https://github.com/lekoala/actual-css/blob/master/docs/design-notes/steps.md)
407
413
  for why the contract is shaped this way, and which alternatives were measured
@@ -180,6 +180,11 @@ flyout when that specific control must close it regardless of the automatic
180
180
  policy. This is useful for an Apply or Done action in an `outside` or `false`
181
181
  panel. The trigger and Escape continue to close the flyout in every mode.
182
182
 
183
+ Arrow keys and Space in a radio group never close the flyout: they move
184
+ through the options, and a pointer pick closes it. Opening from the keyboard
185
+ focuses a group at its checked radio. The [palette picker](../forms/color-swatches.md#palette-in-a-flyout)
186
+ relies on both.
187
+
183
188
  ```html
184
189
  <div class="flyout"
185
190
  id="filters"
@@ -4,15 +4,15 @@
4
4
 
5
5
  Scrollspy is a behavior hook, not a visual component. It marks a navigation
6
6
  region for the JavaScript enhancement. Pair it with `.nav-list` and `.nav-link`,
7
- or style `[aria-current]` yourself.
7
+ or style `[aria-current="location"]` yourself.
8
8
 
9
9
  ## Class reference
10
10
 
11
- | Class | Kind | Description |
12
- | ------------ | ----------- | --------------------------------------------------------------- |
13
- | `.scrollspy` | Component | Marks the nav region for the enhancer; adds no visual styles. |
14
- | `.nav-list` | Composition | Vertical list of navigation links. |
15
- | `.nav-link` | Component | Navigation link whose active state comes from `[aria-current]`. |
11
+ | Class | Kind | Description |
12
+ | ------------ | ----------- | ------------------------------------------------------------- |
13
+ | `.scrollspy` | Component | Marks the nav region for the enhancer; adds no visual styles. |
14
+ | `.nav-list` | Composition | Vertical list of navigation links. |
15
+ | `.nav-link` | Component | Navigation link, current on `aria-current="location"`. |
16
16
 
17
17
  ## Usage
18
18
 
@@ -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,79 @@ 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 (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
+ For a few tabs on a track with a raised selected tab (segmented control, pill
99
+ tabs), put the same tablist on [`.segmented`](../components/segmented.md):
100
+ the runtime is unchanged. Its labels wrap instead of scrolling, so keep it to
101
+ what fits on one row.
102
+
103
+ ```html demo
104
+ <nav aria-label="Pricing sections">
105
+ <ul class="tabs" style="--tab-flex: 1 1 0">
106
+ <li><a class="tab primary" href="#reasons" aria-current="page">Reasons</a></li>
107
+ <li><a class="tab" href="#rates">Rates</a></li>
108
+ </ul>
109
+ </nav>
110
+ ```
111
+
112
+ ## Too many tabs
113
+
114
+ A horizontal strip is one row and never wraps, nor does a label: a second line
115
+ would not match the Left/Right keys and reads as a nested level. When the tabs
116
+ do not fit (narrow screen, mobile, long translations), the strip scrolls
117
+ horizontally without a scrollbar; in supporting browsers, the edges it can
118
+ still scroll toward fade out. Selecting or focusing the tab clipped at the edge
119
+ scrolls it into view with the next one peeking, so a click, the arrow keys or a
120
+ swipe move along the strip. A tab selected in the markup starts in view too,
121
+ when the strip is visible at connect; the placement moves the strip only, never
122
+ the page. In supporting browsers, so does the current link tab (or flyout
123
+ trigger), without the runtime; elsewhere the strip starts at its first tab. A
124
+ vertical strip (`aria-orientation="vertical"`) keeps wrapping its labels.
125
+
126
+ Scrolling is the fallback, not a responsive design. If a strip regularly
127
+ overflows, shorten the labels, group sections, or switch to a vertical rail.
128
+
129
+ ## Tab with a flyout
130
+
131
+ In navigation tabs, a `.tab` can be a [flyout](flyout.md) trigger for a group of
132
+ pages. The link to the current page inside its panel carries
133
+ `aria-current="page"`. The trigger is not a page, so it takes
134
+ `aria-current="true"` while its panel holds the current page: the section is
135
+ the current item of the strip. This is the only place a `.tab` takes `true`.
136
+
137
+ ```html demo
138
+ <nav aria-label="Account">
139
+ <ul class="tabs">
140
+ <li><a class="tab" href="#profile">Profile</a></li>
141
+ <li><a class="tab" href="#security">Security</a></li>
142
+ <li class="flyout-trigger">
143
+ <button class="tab"
144
+ type="button"
145
+ data-enhance="flyout"
146
+ aria-current="true"
147
+ aria-expanded="false"
148
+ aria-controls="billing-menu">
149
+ Billing <i class="ti ti-chevron-down" aria-hidden="true"></i>
150
+ </button>
151
+ <ul class="flyout menu" id="billing-menu" hidden>
152
+ <li><a class="menu-item" href="#invoices" aria-current="page">Invoices</a></li>
153
+ <li><a class="menu-item" href="#payment">Payment methods</a></li>
154
+ </ul>
155
+ </li>
156
+ </ul>
157
+ </nav>
158
+ ```
159
+
160
+ Do not put a flyout trigger in a `role="tablist"`: a tablist holds only
161
+ `role="tab"` items, and the arrow keys of the two widgets collide.
162
+
89
163
  ## CSS hooks
90
164
 
165
+ - `--tab-flex` — flex of each item; set `1 1 0` on the strip for equal-width tabs
166
+ (fill the width). A strip that cannot fit its labels still scrolls.
91
167
  - `--tab-gap` — space between an icon and the label text (default `0.375em`).
@@ -23,13 +23,15 @@ rest show it composed, isolated, or under stress.
23
23
  Whole pages, composed the way a real product would.
24
24
 
25
25
  - [Dashboard](../../demo/templates/dashboard.html) - data-heavy overview page
26
+ - [Order management](../../demo/templates/order-management.html) - one state at three levels (a badge in a selectable list, a message in context, a notification with its own action) composed on the `admin-neutral` business-interface theme
26
27
  - [App](../../demo/templates/app.html) - application shell with sidebar
27
28
  - [Workspaces](../../demo/templates/workspaces.html) - sectioned sidebar, master/detail list with native current state, and workspace cards
28
29
  - [Marketing](../../demo/templates/marketing.html) - landing page with typography and fluid type
29
30
  - [Blog](../../demo/templates/blog.html) - article page with prose
30
31
  - [Settings modal](../../demo/templates/settings-modal.html) - two-pane preferences dialog, with the local CSS it needs audited rule by rule
31
32
  - [Modal patterns](../../demo/templates/modal-patterns.html) - eight application modals, each the modal surface plus the smallest application CSS it needs
32
- - [Workspace settings](../../demo/templates/settings-pricing.html) - plans with visible native choices, preferences and a local segmented tab treatment
33
+ - [Workspace settings](../../demo/templates/settings-pricing.html) - plans with visible native choices, preferences and segmented tabs
34
+ - [iOS-style settings](../../demo/templates/settings-ios.html) - iOS-like controls and grouped settings from unchanged framework markup, a local theme, and its findings listed rule by rule
33
35
  - [Card compositions](../../demo/templates/card-compositions.html) - editorial, team, sign-in and horizontal cards composed from existing primitives
34
36
  - [Register form](../../demo/templates/register-form.html) - registration page with choice cards, masks, the password toggle, `tel-prefix` validation, an OTP verification dialog, and status feedback
35
37
 
@@ -0,0 +1,131 @@
1
+ # Color swatches
2
+
3
+ > Pick a color from a palette the application defines, inline or from a flyout.
4
+
5
+ **Related terms:** color picker, palette, swatch, color chip, accent color, label color.
6
+
7
+ `.color-swatch` is a `<label>` wrapping a native radio, with the color in
8
+ `--color-swatch-value`. The radio stays the control: it submits, takes the
9
+ arrow keys and carries the name in `aria-label`. The checked swatch shows a
10
+ double ring that reads on any color. Use checkboxes for several colors at once.
11
+
12
+ For a free color, use the native [`.color`](controls.md) picker; a custom
13
+ spectrum or RGB sliders are outside Actual.
14
+
15
+ ## Palette
16
+
17
+ Lay the swatches out with `.cluster` or `.grid`, and space them with `--gap`
18
+ (`.tight`, `.loose` or an inline value), not `gap`: the hit area reaches
19
+ half of `--gap` past the swatch, up to `--control-size`, so neighbours split
20
+ the gap between them. A tighter palette trades target size for density. Set
21
+ the column count with `--grid-columns: repeat(6, max-content)` on `.grid`.
22
+
23
+ ```html demo
24
+ <fieldset class="field-group cluster">
25
+ <legend>Category color</legend>
26
+ <label class="color-swatch" style="--color-swatch-value: #2563eb"><input type="radio" name="category" value="#2563eb" aria-label="Blue" checked></label>
27
+ <label class="color-swatch" style="--color-swatch-value: #10b981"><input type="radio" name="category" value="#10b981" aria-label="Green"></label>
28
+ <label class="color-swatch" style="--color-swatch-value: #f59e0b"><input type="radio" name="category" value="#f59e0b" aria-label="Amber"></label>
29
+ <label class="color-swatch" style="--color-swatch-value: #e11d48"><input type="radio" name="category" value="#e11d48" aria-label="Rose"></label>
30
+ <label class="color-swatch" style="--color-swatch-value: #ffffff"><input type="radio" name="category" value="#ffffff" aria-label="White"></label>
31
+ </fieldset>
32
+ ```
33
+
34
+ ## Palette in a flyout
35
+
36
+ A `<button class="color-swatch">` shows the current color and opens the
37
+ palette as a [flyout](../enhancements/flyout.md). A pointer pick closes it;
38
+ the arrow keys move through the colors with the panel open, and Escape returns
39
+ to the button. Give the panel `--flyout-pad` so the focus ring fits inside it.
40
+ Application code shows the pick on the button.
41
+
42
+ ```html demo
43
+ <span class="field-label" id="accent-label">Accent color</span>
44
+ <div class="flyout-trigger">
45
+ <button class="color-swatch" type="button" data-enhance="flyout"
46
+ aria-controls="accent-palette" aria-expanded="false"
47
+ aria-labelledby="accent-label accent-name"
48
+ style="--color-swatch-value: #2563eb">
49
+ <span class="sr-only" id="accent-name">Blue</span>
50
+ </button>
51
+ <div class="flyout" id="accent-palette" style="--flyout-pad: var(--space-30)" hidden>
52
+ <div class="cluster" role="radiogroup" aria-labelledby="accent-label">
53
+ <label class="color-swatch" style="--color-swatch-value: #2563eb"><input type="radio" name="accent" value="#2563eb" aria-label="Blue" checked></label>
54
+ <label class="color-swatch" style="--color-swatch-value: #10b981"><input type="radio" name="accent" value="#10b981" aria-label="Green"></label>
55
+ <label class="color-swatch" style="--color-swatch-value: #f59e0b"><input type="radio" name="accent" value="#f59e0b" aria-label="Amber"></label>
56
+ <label class="color-swatch" style="--color-swatch-value: #e11d48"><input type="radio" name="accent" value="#e11d48" aria-label="Rose"></label>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ <script>
61
+ (() => {
62
+ const preview = document.currentScript.closest(".docs-preview");
63
+ const trigger = preview.querySelector("button.color-swatch");
64
+ preview.querySelector(".flyout").addEventListener("change", (event) => {
65
+ trigger.style.setProperty("--color-swatch-value", event.target.value);
66
+ trigger.querySelector(".sr-only").textContent = event.target.getAttribute("aria-label");
67
+ });
68
+ })();
69
+ </script>
70
+ ```
71
+
72
+ ## Palette and custom color
73
+
74
+ Add the native [`.color`](controls.md) picker to the panel for a color
75
+ outside the palette. Two changes to the flyout above:
76
+
77
+ - `data-flyout-auto-close="outside"` on the panel, so opening the color dialog
78
+ does not close it, and `data-flyout-close` on each swatch, so a pointer pick
79
+ still does. The arrow keys still move through the palette with the panel
80
+ open.
81
+ - The custom input sits in a `<label class="cluster">`, which centers it on
82
+ its text; `.choice` aligns checkboxes, radios and switches only.
83
+ - The custom input takes its own name. Application code unchecks the palette
84
+ when a custom color is chosen, so the form sends `brand`, or `brand-custom`
85
+ when no palette color is checked.
86
+
87
+ ```html demo
88
+ <span class="field-label" id="brand-label">Brand color</span>
89
+ <div class="flyout-trigger">
90
+ <button class="color-swatch" type="button" data-enhance="flyout"
91
+ aria-controls="brand-palette" aria-expanded="false"
92
+ aria-labelledby="brand-label brand-name"
93
+ style="--color-swatch-value: #2563eb">
94
+ <span class="sr-only" id="brand-name">Blue</span>
95
+ </button>
96
+ <div class="flyout stack" id="brand-palette" data-flyout-auto-close="outside"
97
+ style="--flyout-pad: var(--space-30)" hidden>
98
+ <div class="cluster" role="radiogroup" aria-labelledby="brand-label">
99
+ <label class="color-swatch" data-flyout-close style="--color-swatch-value: #2563eb"><input type="radio" name="brand" value="#2563eb" aria-label="Blue" checked></label>
100
+ <label class="color-swatch" data-flyout-close style="--color-swatch-value: #10b981"><input type="radio" name="brand" value="#10b981" aria-label="Green"></label>
101
+ <label class="color-swatch" data-flyout-close style="--color-swatch-value: #f59e0b"><input type="radio" name="brand" value="#f59e0b" aria-label="Amber"></label>
102
+ <label class="color-swatch" data-flyout-close style="--color-swatch-value: #e11d48"><input type="radio" name="brand" value="#e11d48" aria-label="Rose"></label>
103
+ </div>
104
+ <label class="cluster"><input class="color" type="color" name="brand-custom" value="#7048e8"> Custom…</label>
105
+ </div>
106
+ </div>
107
+ <script>
108
+ (() => {
109
+ const preview = document.currentScript.closest(".docs-preview");
110
+ const trigger = preview.querySelector("button.color-swatch");
111
+ const palette = preview.querySelector(".flyout");
112
+ palette.addEventListener("change", (event) => {
113
+ const custom = event.target.type === "color";
114
+ if (custom) for (const radio of palette.querySelectorAll('[type="radio"]')) radio.checked = false;
115
+ trigger.style.setProperty("--color-swatch-value", event.target.value);
116
+ trigger.querySelector(".sr-only").textContent = custom
117
+ ? `Custom ${event.target.value}`
118
+ : event.target.getAttribute("aria-label");
119
+ });
120
+ })();
121
+ </script>
122
+ ```
123
+
124
+ The color dialog is the browser's own and looks different on each platform.
125
+
126
+ ## Hooks
127
+
128
+ - `--color-swatch-value` — the color, set on each swatch.
129
+ - `--color-swatch-size` — visible swatch size, the height of a `.switch` track
130
+ by default. `.sm` and `.lg` scale it and the hit area.
131
+ - `--color-swatch-radius` — corner radius; round by default.
@@ -93,9 +93,9 @@ contrast contracts apply to the combobox for free:
93
93
 
94
94
  .actual-combobox .cb-chip {
95
95
  border: var(--border-width) solid
96
- color-mix(in oklab, var(--surface) var(--soft-border-mix), var(--primary));
96
+ color-mix(in oklab, var(--primary) calc(100% - var(--soft-border-mix, 100%)), transparent);
97
97
  border-radius: var(--radius-full);
98
- background: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--primary));
98
+ background: color-mix(in oklab, var(--primary) calc(100% - var(--soft-bg-mix)), transparent);
99
99
  color: color-mix(in oklab, var(--primary) var(--soft-fg-mix), var(--text));
100
100
  font-size: var(--font-size-xs);
101
101
  font-weight: var(--font-weight-semibold);
@@ -6,9 +6,12 @@ Native-only control chrome: the file input and color picker mostly defeat custom
6
6
 
7
7
  > Small, theme-aware chrome for the native color picker.
8
8
 
9
- Use `.color` on an `input[type="color"]` to give the native picker a control-sized
10
- box with the framework's border, radius, surface, disabled, and focus styles.
11
- The color dialog and selected-color swatch remain browser-native.
9
+ Use `.color` on an `input[type="color"]` to show the native picker as a round
10
+ swatch the height of a [switch](switch.md), on a control-sized hit area, with
11
+ the framework's edge, disabled, and focus styles. `--color-size` sets the
12
+ swatch. The color dialog remains browser-native. To choose from a fixed
13
+ palette instead, use [color swatches](color-swatches.md), which share this
14
+ geometry.
12
15
 
13
16
  ```html demo
14
17
  <label class="field">
@@ -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.
@@ -8,10 +8,6 @@
8
8
  policy for form actions and the sticky behaviour. The space before the row
9
9
  belongs to the parent layout: put `.stack` on the form.
10
10
 
11
- `.form-actions` refines `.cluster` with alignment rules of equal specificity,
12
- so it wins on import order. In a modular build, import `layout/cluster` before
13
- `forms/form-actions`; the family entries and the full bundle already do.
14
-
15
11
  ```html demo
16
12
  <form class="stack" novalidate>
17
13
  <label class="field">
@@ -65,8 +61,9 @@ In application CSS, the same policy is one declaration on the region:
65
61
  }
66
62
  ```
67
63
 
68
- On a form action row, `--cluster-justify` and `--cluster-align` have no effect;
69
- use the hooks above, or a `.justify-content-*` / `.items-*` utility on one row.
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.
70
67
  A split pair keeps the dismissive action at the leading edge and the committing
71
68
  one at the trailing edge:
72
69
 
@@ -2,7 +2,11 @@
2
2
 
3
3
  > Use `.join` when controls need to be visually joined into a single unit.
4
4
 
5
- **Related terms:** segmented control, button group, input group.
5
+ **Related terms:** button group, input group, toolbar.
6
+
7
+ `.join` groups controls that keep their own identity and function. For one
8
+ choice among a few on a shared track, use a
9
+ [segmented control](../components/segmented.md).
6
10
 
7
11
  ## Class reference
8
12
 
@@ -13,7 +17,7 @@
13
17
 
14
18
  Add `.join` to the wrapper that groups the controls. Use `.join-addon` for static prefix/suffix content such as currency symbols, units, or protocol text. Action buttons inside the group should use `.btn`. The wrapper should still carry `role="group"` with a label for accessibility.
15
19
 
16
- `.join` reshapes corners and shared edges; it does not unify the segments' treatments, and the children keep whatever classes they carry. Give them one variant family so the group reads as a single unit, and change a segment's variant only to mark state — a solid button among outlines for the active segment of a segmented control. A group mixing `.btn`, `.btn outline` and `.btn soft` is valid markup that renders as three unrelated controls stuck together.
20
+ `.join` reshapes corners and shared edges; it does not unify the segments' treatments, and the children keep whatever classes they carry. Give them one variant family so the group reads as a single unit, and change a segment's variant only to mark state — a solid button among outlines for a pressed toggle. A group mixing `.btn`, `.btn outline` and `.btn soft` is valid markup that renders as three unrelated controls stuck together.
17
21
 
18
22
  When composing source files manually, import `components/join.css` after the controls it groups. `.join` writes child border-radius longhands directly so joined corners win at equal specificity.
19
23
 
@@ -28,6 +28,7 @@
28
28
  | `.range` | Component | Native range slider, theme-aware accent. |
29
29
  | `.file` | Component | Native file input, themed picker button. |
30
30
  | `.color` | Component | Native color picker in a control-sized box. |
31
+ | `.color-swatch` | Component | Palette color on a radio, or its trigger. |
31
32
  | `.field` | Composition | Field wrapper, on `<label>` or `<div>`. |
32
33
  | `.field-label` | Composition | Label text, element-agnostic. |
33
34
  | `.field-help` | Composition | Supporting helper text under a control. |
@@ -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>