actual-css 0.3.1 → 0.4.1

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 (223) hide show
  1. package/CHANGELOG.md +109 -3
  2. package/README.md +35 -30
  3. package/dist/actual.css +66 -4958
  4. package/dist/actual.full.css +7548 -0
  5. package/dist/actual.full.js +1 -0
  6. package/dist/actual.full.min.css +1 -0
  7. package/dist/actual.js +1 -1
  8. package/dist/actual.min.css +1 -1
  9. package/docs/design-notes/app-ui.md +33 -0
  10. package/docs/design-notes/cascade-layer.md +34 -13
  11. package/docs/design-notes/column-layout.md +677 -0
  12. package/docs/design-notes/enhancement-contract.md +25 -0
  13. package/docs/design-notes/forced-colors.md +3 -0
  14. package/docs/design-notes/grid.md +785 -0
  15. package/docs/design-notes/naming.md +15 -0
  16. package/docs/design-notes/references.md +1 -1
  17. package/docs/design-notes/widget-primitives.md +1 -0
  18. package/docs/navigation.json +12 -2
  19. package/docs/pages/components/alert.md +21 -14
  20. package/docs/pages/components/app-navigation.md +34 -0
  21. package/docs/pages/components/aura.md +1 -1
  22. package/docs/pages/components/avatar.md +16 -6
  23. package/docs/pages/components/badge.md +9 -7
  24. package/docs/pages/components/button.md +46 -16
  25. package/docs/pages/components/card.md +17 -9
  26. package/docs/pages/components/chat.md +8 -8
  27. package/docs/pages/components/dialog.md +7 -7
  28. package/docs/pages/components/drawer.md +3 -3
  29. package/docs/pages/components/fab.md +9 -7
  30. package/docs/pages/components/indicator.md +52 -0
  31. package/docs/pages/components/list.md +62 -0
  32. package/docs/pages/components/navbar.md +15 -8
  33. package/docs/pages/components/pagination.md +4 -3
  34. package/docs/pages/components/rating.md +68 -0
  35. package/docs/pages/components/skeleton.md +4 -4
  36. package/docs/pages/components/spinner.md +5 -5
  37. package/docs/pages/components/steps.md +57 -0
  38. package/docs/pages/components/table.md +30 -5
  39. package/docs/pages/enhancements/flyout.md +15 -15
  40. package/docs/pages/enhancements/overview.md +1 -1
  41. package/docs/pages/enhancements/scrollspy.md +6 -6
  42. package/docs/pages/enhancements/status-bar.md +5 -1
  43. package/docs/pages/enhancements/tabs.md +7 -8
  44. package/docs/pages/enhancements/tooltip.md +1 -1
  45. package/docs/pages/examples/overview.md +47 -22
  46. package/docs/pages/forms/choice-cards.md +11 -6
  47. package/docs/pages/forms/controls.md +1 -1
  48. package/docs/pages/forms/detached-actions.md +1 -1
  49. package/docs/pages/forms/floating-field.md +10 -5
  50. package/docs/pages/forms/form-actions.md +1 -1
  51. package/docs/pages/forms/input-policy.md +1 -1
  52. package/docs/pages/forms/joined-controls.md +4 -4
  53. package/docs/pages/forms/labels.md +6 -6
  54. package/docs/pages/forms/otp.md +33 -1
  55. package/docs/pages/forms/overview.md +19 -19
  56. package/docs/pages/forms/password.md +1 -1
  57. package/docs/pages/forms/range.md +1 -1
  58. package/docs/pages/forms/states-and-sizes.md +44 -48
  59. package/docs/pages/forms/sticky-actions.md +3 -3
  60. package/docs/pages/forms/switch.md +110 -2
  61. package/docs/pages/forms/validation.md +9 -9
  62. package/docs/pages/foundations/philosophy.md +1 -1
  63. package/docs/pages/foundations/theming.md +17 -2
  64. package/docs/pages/foundations/tokens.md +22 -10
  65. package/docs/pages/foundations/typography.md +50 -20
  66. package/docs/pages/guides/bootstrap.md +11 -11
  67. package/docs/pages/guides/integrating-actual-css.md +192 -0
  68. package/docs/pages/guides/introduction.md +23 -26
  69. package/docs/pages/guides/modular-import.md +173 -0
  70. package/docs/pages/guides/progressive-enhancement.md +81 -78
  71. package/docs/pages/guides/tailwind.md +6 -8
  72. package/docs/pages/layout/app-layout.md +57 -0
  73. package/docs/pages/layout/app-shell.md +28 -7
  74. package/docs/pages/layout/center.md +12 -1
  75. package/docs/pages/layout/choosing.md +54 -0
  76. package/docs/pages/layout/column-layout.md +206 -0
  77. package/docs/pages/layout/frame.md +6 -1
  78. package/docs/pages/layout/grid.md +133 -102
  79. package/docs/pages/layout/media.md +1 -1
  80. package/docs/pages/layout/overview.md +15 -2
  81. package/docs/pages/layout/scroll-snap.md +2 -2
  82. package/docs/pages/layout/scroll-target.md +45 -0
  83. package/docs/pages/layout/sidebar-layout.md +38 -18
  84. package/docs/pages/layout/stack.md +70 -1
  85. package/docs/pages/layout/switcher.md +10 -26
  86. package/docs/pages/layout/topbar.md +2 -2
  87. package/docs/pages/patterns/actions.md +1 -1
  88. package/docs/pages/patterns/code-block.md +1 -1
  89. package/docs/pages/patterns/exclusive-accordion.md +1 -1
  90. package/docs/pages/patterns/hierarchical-nav.md +1 -1
  91. package/docs/pages/patterns/icon-input.md +1 -1
  92. package/docs/pages/patterns/icon-slot.md +61 -0
  93. package/docs/pages/patterns/intent-nesting.md +1 -1
  94. package/docs/pages/patterns/navigation-list.md +1 -1
  95. package/docs/pages/patterns/overview.md +1 -1
  96. package/docs/pages/patterns/structured-lists.md +38 -40
  97. package/docs/pages/utilities/accessibility.md +3 -3
  98. package/docs/pages/utilities/flex.md +15 -15
  99. package/docs/pages/utilities/links.md +4 -4
  100. package/docs/pages/utilities/list.md +8 -8
  101. package/docs/pages/utilities/overflow.md +8 -8
  102. package/docs/pages/utilities/overview.md +1 -1
  103. package/docs/pages/utilities/shape.md +3 -3
  104. package/docs/pages/utilities/sizing-wrapping.md +23 -16
  105. package/docs/pages/utilities/spacing.md +20 -20
  106. package/docs/pages/utilities/text.md +10 -10
  107. package/llms.txt +46 -4
  108. package/package.json +43 -13
  109. package/scripts/reserved-classes.json +278 -0
  110. package/src/cli/actual-css.js +133 -0
  111. package/src/css/actual.css +1 -31
  112. package/src/css/actual.full.css +11 -2
  113. package/src/css/components/alert.css +17 -4
  114. package/src/css/components/app-nav.css +76 -0
  115. package/src/css/components/avatar.css +4 -0
  116. package/src/css/components/badge.css +19 -19
  117. package/src/css/components/breadcrumb.css +1 -1
  118. package/src/css/components/button.css +20 -32
  119. package/src/css/components/card.css +34 -9
  120. package/src/css/{optional → components}/chat.css +0 -6
  121. package/src/css/components/drawer.css +24 -5
  122. package/src/css/{optional → components}/fab.css +1 -7
  123. package/src/css/components/flyout.css +5 -0
  124. package/src/css/components/index.css +37 -27
  125. package/src/css/components/indicator.css +47 -0
  126. package/src/css/components/join.css +8 -1
  127. package/src/css/components/key.css +2 -0
  128. package/src/css/components/list.css +83 -0
  129. package/src/css/components/menu.css +11 -8
  130. package/src/css/components/meter.css +4 -1
  131. package/src/css/components/modal.css +5 -17
  132. package/src/css/components/navbar.css +6 -15
  133. package/src/css/components/pagination.css +11 -0
  134. package/src/css/components/rating.css +101 -0
  135. package/src/css/components/spinner.css +5 -0
  136. package/src/css/components/status-bar.css +5 -0
  137. package/src/css/components/steps.css +122 -0
  138. package/src/css/components/surface.css +5 -0
  139. package/src/css/components/tab.css +20 -22
  140. package/src/css/components/table.css +6 -0
  141. package/src/css/components/tooltip.css +3 -7
  142. package/src/css/{focus.css → core/focus.css} +0 -8
  143. package/src/css/core/index.css +9 -0
  144. package/src/css/core/print.css +18 -0
  145. package/src/css/{theme.css → core/theme.css} +19 -19
  146. package/src/css/{tokens.css → core/tokens.css} +16 -4
  147. package/src/css/{variants.css → core/variants.css} +3 -2
  148. package/src/css/{optional → effects}/aura.css +1 -10
  149. package/src/css/effects/index.css +2 -0
  150. package/src/css/forms/base.css +8 -0
  151. package/src/css/forms/choice-card.css +7 -15
  152. package/src/css/forms/choice.css +49 -37
  153. package/src/css/forms/control.css +4 -1
  154. package/src/css/forms/custom-select.css +2 -0
  155. package/src/css/{optional → forms}/floating-field.css +25 -7
  156. package/src/css/forms/index.css +10 -13
  157. package/src/css/forms/native.css +15 -2
  158. package/src/css/{optional → forms}/otp.css +25 -16
  159. package/src/css/forms/range.css +14 -4
  160. package/src/css/forms/switch.css +31 -29
  161. package/src/css/layout/app-layout.css +102 -0
  162. package/src/css/layout/app-shell.css +6 -0
  163. package/src/css/layout/center.css +14 -0
  164. package/src/css/layout/cluster.css +21 -0
  165. package/src/css/layout/column-layout.css +144 -0
  166. package/src/css/layout/container-query.css +15 -0
  167. package/src/css/layout/frame.css +19 -0
  168. package/src/css/layout/grid.css +155 -0
  169. package/src/css/layout/index.css +44 -0
  170. package/src/css/layout/measure.css +9 -0
  171. package/src/css/layout/media.css +10 -0
  172. package/src/css/layout/scroll-target.css +15 -0
  173. package/src/css/layout/scroller.css +41 -0
  174. package/src/css/layout/sidebar-layout.css +52 -0
  175. package/src/css/layout/stack.css +21 -0
  176. package/src/css/layout/switcher.css +23 -0
  177. package/src/css/{optional/layout-extra.css → layout/topbar.css} +0 -20
  178. package/src/css/typography/index.css +5 -0
  179. package/src/css/typography/lead.css +7 -0
  180. package/src/css/{components → typography}/overline.css +2 -2
  181. package/src/css/{prose.css → typography/prose.css} +57 -38
  182. package/src/css/{utilities.css → utilities/base.css} +2 -0
  183. package/src/css/utilities/index.css +3 -0
  184. package/src/js/escape.js +36 -0
  185. package/src/js/flyout.js +22 -12
  186. package/src/js/full.js +26 -0
  187. package/src/js/index.js +5 -21
  188. package/src/js/surface.js +1 -31
  189. package/src/js/tooltip.js +48 -19
  190. package/src/js/validation.js +1 -1
  191. package/src/tooling/css-bundle.js +308 -0
  192. package/dist/actual-themes.min.css +0 -1
  193. package/dist/optional.css +0 -1048
  194. package/dist/optional.min.css +0 -1
  195. package/docs/pages/guides/optional-components.md +0 -33
  196. package/docs/pages/layout/list.md +0 -41
  197. package/src/css/grid.css +0 -109
  198. package/src/css/layout.css +0 -123
  199. package/src/css/optional/index.css +0 -10
  200. package/src/css/optional/scroller.css +0 -63
  201. package/src/css/print.css +0 -47
  202. package/src/css/themes/corporate.css +0 -60
  203. package/src/css/themes/cyberpunk.css +0 -60
  204. package/src/css/themes/dim.css +0 -33
  205. package/src/css/themes/edge.css +0 -117
  206. package/src/css/themes/forest.css +0 -65
  207. package/src/css/themes/gradient.css +0 -97
  208. package/src/css/themes/index.css +0 -47
  209. package/src/css/themes/indigo.css +0 -33
  210. package/src/css/themes/lavender.css +0 -66
  211. package/src/css/themes/mono.css +0 -60
  212. package/src/css/themes/neon.css +0 -66
  213. package/src/css/themes/ocean.css +0 -60
  214. package/src/css/themes/petrol.css +0 -64
  215. package/src/css/themes/spruce.css +0 -64
  216. package/src/css/themes/square.css +0 -65
  217. package/src/css/themes/sunset.css +0 -66
  218. /package/src/css/{base.css → core/base.css} +0 -0
  219. /package/src/css/{intents.css → core/intents.css} +0 -0
  220. /package/src/css/{reset.css → core/reset.css} +0 -0
  221. /package/src/css/{optional → layout}/scroll-snap.css +0 -0
  222. /package/src/css/{optional/typography-fluid.css → typography/fluid.css} +0 -0
  223. /package/src/css/{optional/utilities-extra.css → utilities/extra.css} +0 -0
@@ -101,6 +101,16 @@
101
101
  --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
102
102
  }
103
103
 
104
+ /* Native platform controls (checkbox, radio, range, progress) that are not
105
+ restyled by a component class still follow the theme accent. Declared on
106
+ every island: accent-color inherits as a computed value, so islands must
107
+ recompute it from their own --primary (same resolution rule as above).
108
+ Unsupported browsers ignore it; forced-colors keeps browser control. */
109
+ :root,
110
+ [data-theme] {
111
+ accent-color: var(--primary);
112
+ }
113
+
104
114
  /* Tone subsystem — fixed-backdrop foreground for components placed over media
105
115
  or other surfaces outside the theme token stack. Use data-theme islands for
106
116
  full dark/light subtree inversion. */
@@ -115,13 +125,13 @@
115
125
  /* Focus ring derivation — color-mix tier. Runs whether or not light-dark()
116
126
  is also supported, so it covers the color-mix-without-light-dark browser
117
127
  tier on its own; the light-dark block below refines it further with the
118
- light/dark alpha split once both features are available. Deriving from
119
- --focus (not a literal) means overriding --primary/--focus, in the
120
- default theme or any custom one, keeps the ring in sync automatically. */
128
+ light/dark alpha split once both features are available. Declared on every
129
+ [data-theme] island (not just the built-in light/dark pair) so custom
130
+ islands recolor the ring for free; a preset that re-declares --focus-ring
131
+ later in source order intentionally pins its own ring instead. */
121
132
  @supports (color: color-mix(in oklch, red, white)) {
122
133
  :root,
123
- [data-theme="light"],
124
- [data-theme="dark"] {
134
+ [data-theme] {
125
135
  --focus-ring: color-mix(in oklch, var(--focus) 30%, transparent);
126
136
  }
127
137
  }
@@ -170,7 +180,7 @@
170
180
 
171
181
  @supports (color: color-mix(in oklch, red, white)) and (color: light-dark(red, blue)) {
172
182
  :root,
173
- [data-theme="light"] {
183
+ [data-theme] {
174
184
  --focus-ring: light-dark(
175
185
  color-mix(in oklch, var(--focus) 30%, transparent),
176
186
  color-mix(in oklch, var(--focus) 28%, transparent)
@@ -178,21 +188,11 @@
178
188
  }
179
189
  }
180
190
 
181
- @media (forced-colors: active) {
182
- :root,
183
- [data-theme="light"],
184
- [data-theme="dark"] {
185
- --focus-outline-color: Highlight;
186
- --focus-ring-shadow: none;
187
-
188
- --backdrop-fill: Canvas;
189
- }
190
- }
191
-
192
191
  @media (prefers-contrast: more) {
193
192
  :root,
194
- [data-theme="light"],
195
- [data-theme="dark"] {
193
+ /* The repeated attribute intentionally outranks single-attribute preset
194
+ palettes, including a preset stylesheet loaded after the core. */
195
+ [data-theme][data-theme] {
196
196
  --border-width: 2px;
197
197
  --focus-outline-offset: 3px;
198
198
  --focus-ring-width: 4px;
@@ -83,13 +83,21 @@
83
83
 
84
84
  /* Controls — inputs and buttons share --control-size so heights always match.
85
85
  .sm/.lg (variants.css) switch --control-size; --control-font-size stays
86
- at its baseline because density does not change typography. */
86
+ at its baseline because density does not change typography.
87
+
88
+ Inline choices (checkbox, radio, switch) are typographic, not field-sized:
89
+ they sit on a line of text, so they follow --control-font-size through
90
+ --choice-control-size instead of --control-size. Being an em value in
91
+ :root is deliberate — an unregistered custom property resolves its units
92
+ at the element that consumes it, so one declaration tracks whatever font
93
+ size the choice actually renders at. */
87
94
  --control-size-sm: 2rem;
88
95
  --control-size-md: 2.375rem;
89
96
  --control-size-lg: 2.75rem;
90
97
  --control-size: var(--control-size-md);
91
98
  --control-pad-x: 1em;
92
99
  --control-font-size: var(--font-size-md);
100
+ --choice-control-size: 1.125em;
93
101
  --disabled-opacity: 0.65;
94
102
  }
95
103
 
@@ -118,6 +126,10 @@
118
126
  --soft-bg-mix: 88%;
119
127
  --soft-border-mix: 65%;
120
128
  --soft-hover-alpha: 12%;
129
+ /* Soft foreground: the share of raw intent in soft text. 100% keeps the
130
+ pure intent color; lower values rebate it toward --text, which vivid or
131
+ light intent palettes need to stay legible on their own soft surface. */
132
+ --soft-fg-mix: 100%;
121
133
 
122
134
  /* Shared icon assets. Override these URLs when a strict CSP disallows data:. */
123
135
  --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
@@ -142,10 +154,10 @@
142
154
  }
143
155
 
144
156
  @media (forced-colors: active) {
145
- :root {
146
- --state-selected: Highlight;
147
- --state-selected-fg: HighlightText;
157
+ :root,
158
+ [data-theme] {
148
159
  --state-disabled: GrayText;
160
+ --disabled-opacity: 1;
149
161
  }
150
162
  }
151
163
 
@@ -85,12 +85,13 @@
85
85
  @supports (color: color-mix(in oklch, red, white)) {
86
86
  .soft {
87
87
  /* @sync soft-recipe */
88
- --ui-bg: color-mix(in oklch, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)));
88
+ --ui-bg: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)));
89
89
  --ui-border: color-mix(
90
- in oklch,
90
+ in oklab,
91
91
  var(--surface) var(--soft-border-mix),
92
92
  var(--intent, var(--neutral))
93
93
  );
94
+ --ui-fg: color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix));
94
95
  /* @sync soft-recipe */
95
96
  }
96
97
 
@@ -32,6 +32,7 @@
32
32
  display: inline-grid;
33
33
  padding: var(--aura-width);
34
34
  border-radius: calc(var(--aura-radius) + var(--aura-width));
35
+ border: var(--border-width) solid transparent;
35
36
  background: var(--aura-color);
36
37
  background: conic-gradient(from var(--aura-angle), transparent 0 65%, var(--aura-color));
37
38
  }
@@ -81,13 +82,3 @@
81
82
  --aura-radius: var(--radius-full);
82
83
  }
83
84
  }
84
-
85
- @media (forced-colors: active) {
86
- .aura {
87
- background: CanvasText;
88
- }
89
-
90
- .aura::before {
91
- display: none;
92
- }
93
- }
@@ -0,0 +1,2 @@
1
+ /* Effects — purely visual enrichments. */
2
+ @import "./aura.css";
@@ -0,0 +1,8 @@
1
+ /* Forms base — the ordinary native controls bundle. */
2
+ @import "./form.css";
3
+ @import "./control.css";
4
+ @import "./choice.css";
5
+ @import "./select.css";
6
+ @import "./native.css";
7
+ @import "./validation.css";
8
+ @import "./form-actions.css";
@@ -85,7 +85,7 @@
85
85
  /* Subtle background tint on checked — modern browsers only */
86
86
  @supports (color: color-mix(in oklch, red, white)) {
87
87
  .choice-card:has(:checked) {
88
- --choice-card-bg: color-mix(in oklch, var(--surface) 96%, var(--intent, var(--primary)));
88
+ --choice-card-bg: color-mix(in oklab, var(--surface) 96%, var(--intent, var(--primary)));
89
89
  }
90
90
  }
91
91
 
@@ -131,23 +131,15 @@
131
131
  transform: scale(1);
132
132
  }
133
133
 
134
+ /* The native input is hidden and the checked marker is custom-drawn: without
135
+ this repair, forced colors pushes disc and check to the same system color
136
+ and the state disappears. Highlight/Canvas/HighlightText are preserved as-is,
137
+ so no forced-color-adjust is needed. The pseudo is not focusable — the
138
+ outline is a separator against the card, not a focus channel. */
134
139
  @media (forced-colors: active) {
135
- .choice-card {
136
- --choice-card-border: ButtonText;
137
- --choice-card-bg: Canvas;
138
- }
139
-
140
- .choice-card:has(:checked) {
141
- --choice-card-border: Highlight;
142
- /* Pin the background: with forced-color-adjust: none the color-mix tint
143
- above would actually render, uncontrolled in high-contrast themes. */
144
- --choice-card-bg: Canvas;
145
- forced-color-adjust: none;
146
- }
147
-
148
140
  .choice-card:has(:checked)::before {
149
141
  background: Highlight;
150
- box-shadow: 0 0 0 2px Canvas;
142
+ outline: 2px solid Canvas;
151
143
  }
152
144
 
153
145
  .choice-card:has(:checked)::after {
@@ -14,6 +14,10 @@
14
14
  * --choice-border
15
15
  * --choice-bg
16
16
  * --choice-mark
17
+ *
18
+ * --choice-control-size (tokens.css) is the shared typographic size for every
19
+ * inline choice; switch.css derives its track from it so a checkbox and a
20
+ * switch on the same line stay proportionate.
17
21
  */
18
22
  /* Intent boundary — inherited intents do not tint this component.
19
23
  Local intent classes still win because :where() has zero specificity. */
@@ -30,8 +34,6 @@
30
34
  }
31
35
 
32
36
  .choice {
33
- --choice-control-offset: 0.25em;
34
-
35
37
  display: inline-flex;
36
38
  align-items: flex-start;
37
39
  gap: var(--space-20);
@@ -40,17 +42,47 @@
40
42
  cursor: pointer;
41
43
  }
42
44
 
43
- /* Optical alignment: nudge the control down so the checkbox/radio sits
44
- visually centered with the first line of label text, not flush with the
45
- top of the line-box. Default 0.25em (~4px at 16px) places the control just
46
- below the cap-height, matching what Bootstrap, Material, and most polished
47
- form libraries ship. Multi-line labels still keep the control at the top
48
- thanks to align-items: flex-start. Override per-instance via
49
- --choice-control-offset for themes with a different line-height or control
50
- size. */
45
+ /* Optical alignment: nudge the control down so it sits visually centered with
46
+ the first line of label text, not flush with the top of the line-box. Every
47
+ inline choice runs the same formula — center the control's own block size on
48
+ the first line box, then drop it 0.0625em so it reads level with the
49
+ cap-height rather than the em-box. A checkbox and a switch therefore stay
50
+ level with each other under any line-height or --choice-control-size, which
51
+ a per-control constant cannot do (a taller switch and a shorter checkbox
52
+ need different offsets to land on the same optical line).
53
+
54
+ The constants are what the formula yields at the default 1.5 line-height:
55
+ 0.25em (4px) for a checkbox, 0.1875em (3px) for the taller switch track.
56
+ They serve browsers without lh units. Multi-line labels still keep the
57
+ control at the top thanks to align-items: flex-start.
58
+
59
+ --choice-control-offset stays fallback-only rather than declared here, so an
60
+ override placed anywhere up the tree — :root included — reaches the control
61
+ instead of losing to a component-level declaration. */
51
62
  .choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
52
63
  flex: 0 0 auto;
53
- margin-block-start: var(--choice-control-offset);
64
+ margin-block-start: var(--choice-control-offset, 0.25em);
65
+ }
66
+
67
+ .choice > .switch {
68
+ flex: 0 0 auto;
69
+ margin-block-start: var(--choice-control-offset, 0.1875em);
70
+ }
71
+
72
+ @supports (margin-block-start: 1lh) {
73
+ .choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
74
+ margin-block-start: var(
75
+ --choice-control-offset,
76
+ calc((1lh - var(--choice-control-size)) / 2 + 0.0625em)
77
+ );
78
+ }
79
+
80
+ .choice > .switch {
81
+ margin-block-start: var(
82
+ --choice-control-offset,
83
+ calc((1lh - var(--switch-block-size)) / 2 + 0.0625em)
84
+ );
85
+ }
54
86
  }
55
87
 
56
88
  .choice > span > .field-help {
@@ -76,8 +108,8 @@
76
108
  vertical-align: middle;
77
109
  display: inline-grid;
78
110
  place-content: center;
79
- inline-size: 1.125em;
80
- block-size: 1.125em;
111
+ inline-size: var(--choice-control-size);
112
+ block-size: var(--choice-control-size);
81
113
  margin: 0;
82
114
  border: var(--border-width) solid var(--form-invalid-border, var(--choice-border));
83
115
  background: var(--choice-bg);
@@ -163,27 +195,7 @@
163
195
  opacity: 1;
164
196
  }
165
197
 
166
- @media (forced-colors: active) {
167
- .check,
168
- .radio {
169
- --choice-border: ButtonText;
170
- --choice-bg: Canvas;
171
- --choice-mark: ButtonText;
172
- }
173
-
174
- .check:checked,
175
- .radio:checked,
176
- .check:indeterminate {
177
- --choice-border: Highlight;
178
- --choice-bg: Highlight;
179
- --choice-mark: HighlightText;
180
- forced-color-adjust: none;
181
- }
182
-
183
- .check:disabled,
184
- .radio:disabled {
185
- --choice-border: GrayText;
186
- --choice-bg: Canvas;
187
- --choice-mark: GrayText;
188
- }
189
- }
198
+ /* Forced colors intentionally stay under UA control: the visible check/dot
199
+ carries state while native remapping preserves platform contrast and focus.
200
+ If real HCM testing ever loses a mark, opt out on that pseudo only—never on
201
+ the interactive input host. */
@@ -46,7 +46,10 @@
46
46
  color: var(--control-fg);
47
47
  font-size: var(--control-font-size);
48
48
  cursor: var(--control-cursor);
49
- outline: none;
49
+ /* No outline: none here — the core :focus fallback must survive for browsers
50
+ without :focus-visible. Pointer focus is hidden by core's
51
+ :focus:not(:focus-visible); keyboard focus swaps in the transparent
52
+ outline + ring below. */
50
53
  transition:
51
54
  border-color var(--duration),
52
55
  box-shadow var(--duration),
@@ -12,6 +12,8 @@
12
12
  * --control-border
13
13
  * --control-bg
14
14
  */
15
+ /* This enhancement is excluded from forced-colors, where the native/shared
16
+ outline remains the focus fallback. */
15
17
  @media (forced-colors: none) and (prefers-contrast: no-preference) and (hover: hover) and (
16
18
  pointer: fine
17
19
  ) {
@@ -16,9 +16,13 @@
16
16
  * and the reserved headroom with it, leaving typography untouched.
17
17
  *
18
18
  * Public hooks:
19
+ * --floating-label-top
20
+ * --floating-label-opacity
19
21
  * --floating-pad-block-start
20
22
  */
21
23
  .floating-field {
24
+ --floating-label-top: var(--space-10);
25
+ --floating-label-opacity: 0.8;
22
26
  --floating-pad-block-start: calc(var(--control-size) / 2 + var(--space-20));
23
27
 
24
28
  /* Grid blockifies the control, removing the inline descender gap a textarea
@@ -37,24 +41,30 @@
37
41
 
38
42
  .floating-field > .field-label {
39
43
  position: absolute;
40
- inset-block-start: 50%;
41
44
  inset-inline-start: calc(var(--space-30) + var(--border-width));
42
- transform: translateY(-50%);
43
45
  color: var(--text-muted);
44
46
  font-size: var(--control-font-size);
45
47
  font-weight: var(--font-weight);
48
+ opacity: var(--floating-label-opacity);
46
49
  pointer-events: none;
47
50
  transition:
48
51
  inset-block-start var(--duration),
49
52
  transform var(--duration),
50
53
  font-size var(--duration),
54
+ opacity var(--duration),
51
55
  color var(--duration);
52
56
  }
53
57
 
54
- /* A textarea's resting label aligns with the first text line instead of
55
- centering in the (tall) control. */
58
+ /* Single-line controls rest in the middle until they receive a value. */
59
+ .floating-field > :is(.input, .select) + .field-label {
60
+ inset-block-start: 50%;
61
+ transform: translateY(-50%);
62
+ }
63
+
64
+ /* An empty textarea label follows the control's normal first-line position;
65
+ the control itself keeps the extra headroom reserved for its floated state. */
56
66
  .floating-field > .textarea + .field-label {
57
- inset-block-start: var(--floating-pad-block-start);
67
+ inset-block-start: calc(1em + var(--border-width));
58
68
  transform: none;
59
69
  }
60
70
 
@@ -63,9 +73,10 @@
63
73
  .floating-field
64
74
  > :is(.input, .textarea):is(:focus, :autofill, :not(:placeholder-shown))
65
75
  + .field-label {
66
- inset-block-start: var(--space-10);
76
+ inset-block-start: var(--floating-label-top);
67
77
  transform: none;
68
78
  font-size: var(--font-size-xs);
79
+ opacity: 1;
69
80
  }
70
81
 
71
82
  /* Controls without a dependable :placeholder-shown empty state stay floated:
@@ -81,7 +92,14 @@
81
92
  .input[type="week"]
82
93
  )
83
94
  + .field-label {
84
- inset-block-start: var(--space-10);
95
+ inset-block-start: var(--floating-label-top);
85
96
  transform: none;
86
97
  font-size: var(--font-size-xs);
98
+ opacity: 1;
99
+ }
100
+
101
+ @media (forced-colors: active) {
102
+ .floating-field > .field-label {
103
+ opacity: 1;
104
+ }
87
105
  }
@@ -1,16 +1,13 @@
1
- /* Forms — import order matters: switch.css follows choice.css so its rules
2
- land after the shared choice recipe. validation.css is part of the default
3
- forms bundle, but custom builds may omit it when validation styling is
4
- app-owned. */
5
- @import "./form.css";
6
- @import "./control.css";
1
+ /* Forms — the complete Forms family. base covers the ordinary native
2
+ controls; the leaves below add opt-in control skins. switch.css follows
3
+ choice.css (via base) so its rules land after the shared choice recipe.
4
+ validation.css stays part of base, but custom builds may omit it when
5
+ validation styling is app-owned. */
6
+ @import "./base.css";
7
7
  @import "./input-icon.css";
8
- @import "./choice.css";
8
+ @import "./switch.css";
9
+ @import "./range.css";
9
10
  @import "./choice-card.css";
10
- @import "./select.css";
11
11
  @import "./custom-select.css";
12
- @import "./native.css";
13
- @import "./range.css";
14
- @import "./switch.css";
15
- @import "./validation.css";
16
- @import "./form-actions.css";
12
+ @import "./floating-field.css";
13
+ @import "./otp.css";
@@ -41,11 +41,13 @@
41
41
  box-shadow: var(--focus-ring-shadow);
42
42
  }
43
43
 
44
- /* Native color picker — keep the browser's dialog and selected-color UI. */
44
+ /* Native color picker — keep the browser's dialog and selected-color UI. The
45
+ swatch fills almost the whole control so it reads as a color, not as a tiny
46
+ square inside a button; the 1px padding keeps a hair of --surface around it. */
45
47
  .color {
46
48
  inline-size: var(--control-size);
47
49
  block-size: var(--control-size);
48
- padding: var(--space-10);
50
+ padding: var(--border-width);
49
51
  border: var(--border-width) solid var(--border);
50
52
  border-radius: var(--radius);
51
53
  background: var(--surface);
@@ -58,10 +60,12 @@
58
60
 
59
61
  .color::-webkit-color-swatch {
60
62
  border: 0;
63
+ border-radius: calc(var(--radius) - var(--border-width));
61
64
  }
62
65
 
63
66
  .color::-moz-color-swatch {
64
67
  border: 0;
68
+ border-radius: calc(var(--radius) - var(--border-width));
65
69
  }
66
70
 
67
71
  .color:disabled {
@@ -78,6 +82,15 @@
78
82
  .color {
79
83
  border-color: ButtonText;
80
84
  background: ButtonFace;
85
+ }
86
+
87
+ /* The swatch color is content, so preserve it without disabling the host's
88
+ native forced-color focus treatment. Vendor pseudos cannot share a rule. */
89
+ .color::-webkit-color-swatch {
90
+ forced-color-adjust: none;
91
+ }
92
+
93
+ .color::-moz-color-swatch {
81
94
  forced-color-adjust: none;
82
95
  }
83
96
  }
@@ -11,7 +11,9 @@
11
11
  *
12
12
  * Internal:
13
13
  * --otp-cell-border
14
+ * --otp-cell-border-width
14
15
  * --otp-cell-bg
16
+ * --otp-caret-space
15
17
  */
16
18
  .otp {
17
19
  --otp-cell-size: var(--control-size);
@@ -19,6 +21,7 @@
19
21
  --otp-font-size: 1.5rem;
20
22
  --otp-cell-border: var(--border);
21
23
  --otp-cell-bg: var(--surface);
24
+ --otp-caret-space: calc((var(--otp-cell-size) - 1ch) / 2 + var(--otp-gap) + 1px);
22
25
 
23
26
  position: relative;
24
27
  display: inline-grid;
@@ -28,31 +31,35 @@
28
31
  max-inline-size: 100%;
29
32
  gap: var(--otp-gap);
30
33
  border-radius: var(--radius);
34
+ /* Unlike hidden, clip cannot become a scroll container when the caret moves
35
+ into the input extension. */
36
+ overflow: clip;
31
37
  direction: ltr;
32
38
  }
33
39
 
34
40
  .otp > span {
35
41
  inline-size: var(--otp-cell-size);
36
42
  block-size: var(--otp-cell-size);
37
- border: var(--border-width) solid var(--otp-cell-border);
43
+ border: var(--otp-cell-border-width, var(--border-width)) solid var(--otp-cell-border);
38
44
  border-radius: var(--radius);
39
45
  background: var(--otp-cell-bg);
40
46
  transition:
41
47
  border-color var(--duration),
42
- background-color var(--duration),
43
- box-shadow var(--duration);
48
+ background-color var(--duration);
44
49
  }
45
50
 
46
51
  /* The native input owns focus, selection, autofill, paste, and one-time-code
47
- integration. Its spacing aligns monospace glyphs with the visual cells. */
52
+ integration. Its clipped extension accommodates the trailing letter-space
53
+ and final caret so the browser never scrolls the glyphs out of alignment. */
48
54
  .otp > input {
49
55
  position: absolute;
50
56
  z-index: 1;
51
57
  inset: 0;
52
- inline-size: 100%;
58
+ inline-size: calc(100% + var(--otp-caret-space));
53
59
  block-size: 100%;
54
60
  margin: 0;
55
- padding: 0 calc((var(--otp-cell-size) - 1ch) / 2);
61
+ padding-block: 0;
62
+ padding-inline: calc((var(--otp-cell-size) - 1ch) / 2) 0;
56
63
  border: 0;
57
64
  outline: 2px solid transparent;
58
65
  box-shadow: none;
@@ -67,12 +74,20 @@
67
74
  appearance: none;
68
75
  }
69
76
 
70
- .otp:focus-within {
71
- box-shadow: var(--focus-ring-shadow);
72
- }
77
+ /* Deliberately no group ring: a box around all the cells reads as a second
78
+ component. The caret already carries focus while the code is being typed,
79
+ but it sits in the clipped input extension once every cell is filled, so
80
+ the focused edge must be unambiguous on its own — hence the doubled width,
81
+ not only the color swap. Width is safe to change: box-sizing is border-box
82
+ and the cells have a fixed inline/block size, so nothing reflows.
73
83
 
84
+ The <input> must be the first direct child, with one cell <span> per
85
+ character after it: the state rules select the cells as following siblings
86
+ (input:focus ~ span, input:disabled ~ span, …), so that order is part of the
87
+ contract. */
74
88
  .otp > input:focus ~ span {
75
89
  --otp-cell-border: var(--focus);
90
+ --otp-cell-border-width: calc(var(--border-width) * 2);
76
91
  }
77
92
 
78
93
  .otp > input[aria-invalid="true"] ~ span,
@@ -93,17 +108,11 @@
93
108
 
94
109
  .otp > input:disabled ~ span {
95
110
  --otp-cell-bg: var(--surface-subtle);
96
-
111
+ --otp-cell-border: var(--state-disabled);
97
112
  opacity: var(--disabled-opacity);
98
113
  }
99
114
 
100
115
  @media (forced-colors: active) {
101
- .otp:focus-within {
102
- outline: var(--focus-outline);
103
- outline-offset: var(--focus-outline-offset);
104
- box-shadow: none;
105
- }
106
-
107
116
  .otp > input:focus ~ span {
108
117
  --otp-cell-border: Highlight;
109
118
  }
@@ -93,11 +93,21 @@
93
93
  opacity: var(--disabled-opacity);
94
94
  }
95
95
 
96
- /* Forced colors: map the custom thumb/track to system colors. The focus
97
- indicator stays visible through the transparent outline on :focus-visible. */
96
+ /* Forced colors: the track must stay a visible line against the page (Canvas
97
+ would merge with it) and the thumb needs a real boundary to remain
98
+ distinguishable from the track. Disabled keeps GrayText via :disabled.
99
+ Focus stays on the transparent outline of :focus-visible. */
98
100
  @media (forced-colors: active) {
99
101
  .range {
100
- --range-thumb-bg: ButtonText;
101
- --range-track-bg: Canvas;
102
+ --range-track-bg: CanvasText;
103
+ --range-thumb-bg: ButtonFace;
104
+ }
105
+
106
+ .range::-webkit-slider-thumb {
107
+ border: var(--border-width) solid ButtonText;
108
+ }
109
+
110
+ .range::-moz-range-thumb {
111
+ border: var(--border-width) solid ButtonText;
102
112
  }
103
113
  }