actual-css 0.3.0 → 0.4.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 (205) hide show
  1. package/CHANGELOG.md +78 -3
  2. package/README.md +35 -30
  3. package/dist/actual.css +98 -4882
  4. package/dist/actual.full.css +7090 -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/cascade-layer.md +35 -13
  10. package/docs/design-notes/column-layout.md +677 -0
  11. package/docs/design-notes/forced-colors.md +3 -0
  12. package/docs/design-notes/grid.md +612 -0
  13. package/docs/design-notes/naming.md +15 -0
  14. package/docs/design-notes/widget-primitives.md +1 -0
  15. package/docs/navigation.json +5 -1
  16. package/docs/pages/components/alert.md +16 -14
  17. package/docs/pages/components/aura.md +1 -1
  18. package/docs/pages/components/avatar.md +6 -6
  19. package/docs/pages/components/badge.md +7 -7
  20. package/docs/pages/components/button.md +22 -17
  21. package/docs/pages/components/card.md +29 -13
  22. package/docs/pages/components/chat.md +8 -8
  23. package/docs/pages/components/dialog.md +7 -7
  24. package/docs/pages/components/drawer.md +3 -3
  25. package/docs/pages/components/fab.md +7 -7
  26. package/docs/pages/components/navbar.md +9 -8
  27. package/docs/pages/components/pagination.md +4 -3
  28. package/docs/pages/components/skeleton.md +4 -4
  29. package/docs/pages/components/spinner.md +5 -5
  30. package/docs/pages/components/table.md +30 -5
  31. package/docs/pages/enhancements/flyout.md +13 -13
  32. package/docs/pages/enhancements/overview.md +1 -1
  33. package/docs/pages/enhancements/scrollspy.md +6 -6
  34. package/docs/pages/enhancements/status-bar.md +5 -1
  35. package/docs/pages/enhancements/tabs.md +6 -6
  36. package/docs/pages/enhancements/tooltip.md +1 -1
  37. package/docs/pages/examples/overview.md +41 -22
  38. package/docs/pages/forms/choice-cards.md +11 -6
  39. package/docs/pages/forms/controls.md +1 -1
  40. package/docs/pages/forms/detached-actions.md +1 -1
  41. package/docs/pages/forms/floating-field.md +10 -5
  42. package/docs/pages/forms/form-actions.md +1 -1
  43. package/docs/pages/forms/input-policy.md +1 -1
  44. package/docs/pages/forms/joined-controls.md +4 -4
  45. package/docs/pages/forms/labels.md +6 -6
  46. package/docs/pages/forms/otp.md +1 -1
  47. package/docs/pages/forms/overview.md +19 -19
  48. package/docs/pages/forms/password.md +1 -1
  49. package/docs/pages/forms/range.md +1 -1
  50. package/docs/pages/forms/states-and-sizes.md +41 -45
  51. package/docs/pages/forms/sticky-actions.md +3 -3
  52. package/docs/pages/forms/switch.md +110 -2
  53. package/docs/pages/forms/validation.md +9 -9
  54. package/docs/pages/foundations/philosophy.md +1 -1
  55. package/docs/pages/foundations/theming.md +29 -7
  56. package/docs/pages/foundations/tokens.md +22 -10
  57. package/docs/pages/foundations/typography.md +50 -20
  58. package/docs/pages/guides/bootstrap.md +11 -11
  59. package/docs/pages/guides/integrating-actual-css.md +186 -0
  60. package/docs/pages/guides/introduction.md +23 -26
  61. package/docs/pages/guides/modular-import.md +173 -0
  62. package/docs/pages/guides/progressive-enhancement.md +81 -78
  63. package/docs/pages/guides/tailwind.md +6 -8
  64. package/docs/pages/layout/app-shell.md +1 -1
  65. package/docs/pages/layout/center.md +1 -1
  66. package/docs/pages/layout/choosing.md +39 -0
  67. package/docs/pages/layout/cluster.md +4 -2
  68. package/docs/pages/layout/column-layout.md +206 -0
  69. package/docs/pages/layout/frame.md +6 -1
  70. package/docs/pages/layout/grid.md +100 -102
  71. package/docs/pages/layout/list.md +1 -1
  72. package/docs/pages/layout/media.md +10 -3
  73. package/docs/pages/layout/overview.md +15 -2
  74. package/docs/pages/layout/scroll-snap.md +2 -2
  75. package/docs/pages/layout/scroll-target.md +45 -0
  76. package/docs/pages/layout/sidebar-layout.md +20 -19
  77. package/docs/pages/layout/stack.md +90 -1
  78. package/docs/pages/layout/switcher.md +10 -26
  79. package/docs/pages/layout/topbar.md +2 -2
  80. package/docs/pages/patterns/actions.md +1 -1
  81. package/docs/pages/patterns/code-block.md +1 -1
  82. package/docs/pages/patterns/exclusive-accordion.md +1 -1
  83. package/docs/pages/patterns/hierarchical-nav.md +1 -1
  84. package/docs/pages/patterns/icon-input.md +1 -1
  85. package/docs/pages/patterns/intent-nesting.md +1 -1
  86. package/docs/pages/patterns/navigation-list.md +1 -1
  87. package/docs/pages/patterns/overview.md +1 -1
  88. package/docs/pages/patterns/structured-lists.md +1 -1
  89. package/docs/pages/utilities/accessibility.md +3 -3
  90. package/docs/pages/utilities/flex.md +15 -15
  91. package/docs/pages/utilities/links.md +4 -4
  92. package/docs/pages/utilities/list.md +8 -7
  93. package/docs/pages/utilities/overflow.md +8 -8
  94. package/docs/pages/utilities/overview.md +1 -1
  95. package/docs/pages/utilities/shape.md +3 -3
  96. package/docs/pages/utilities/sizing-wrapping.md +23 -16
  97. package/docs/pages/utilities/spacing.md +20 -20
  98. package/docs/pages/utilities/text.md +10 -10
  99. package/llms.txt +46 -4
  100. package/package.json +43 -13
  101. package/scripts/reserved-classes.json +261 -0
  102. package/src/cli/actual-css.js +133 -0
  103. package/src/css/actual.css +1 -28
  104. package/src/css/actual.full.css +11 -2
  105. package/src/css/components/alert.css +33 -10
  106. package/src/css/components/badge.css +38 -19
  107. package/src/css/components/breadcrumb.css +1 -1
  108. package/src/css/components/button.css +35 -13
  109. package/src/css/components/card.css +37 -4
  110. package/src/css/components/drawer.css +27 -2
  111. package/src/css/components/flyout.css +5 -0
  112. package/src/css/components/index.css +32 -27
  113. package/src/css/components/join.css +8 -1
  114. package/src/css/components/key.css +2 -0
  115. package/src/css/components/menu.css +11 -1
  116. package/src/css/components/meter.css +4 -1
  117. package/src/css/components/modal.css +5 -0
  118. package/src/css/components/navbar.css +13 -5
  119. package/src/css/components/pagination.css +11 -0
  120. package/src/css/components/spinner.css +5 -0
  121. package/src/css/components/status-bar.css +5 -0
  122. package/src/css/components/surface.css +5 -0
  123. package/src/css/components/table.css +6 -0
  124. package/src/css/components/tooltip.css +5 -0
  125. package/src/css/core/index.css +9 -0
  126. package/src/css/core/print.css +18 -0
  127. package/src/css/{theme.css → core/theme.css} +20 -10
  128. package/src/css/{tokens.css → core/tokens.css} +15 -2
  129. package/src/css/{variants.css → core/variants.css} +27 -13
  130. package/src/css/effects/index.css +2 -0
  131. package/src/css/forms/base.css +8 -0
  132. package/src/css/forms/choice-card.css +5 -4
  133. package/src/css/forms/choice.css +49 -37
  134. package/src/css/forms/control.css +6 -1
  135. package/src/css/forms/custom-select.css +2 -0
  136. package/src/css/{optional → forms}/floating-field.css +25 -7
  137. package/src/css/forms/index.css +10 -13
  138. package/src/css/forms/native.css +15 -2
  139. package/src/css/{optional → forms}/otp.css +20 -16
  140. package/src/css/forms/switch.css +31 -29
  141. package/src/css/layout/app-shell.css +6 -0
  142. package/src/css/layout/center.css +13 -0
  143. package/src/css/layout/cluster.css +21 -0
  144. package/src/css/layout/column-layout.css +144 -0
  145. package/src/css/layout/container-query.css +13 -0
  146. package/src/css/layout/frame.css +19 -0
  147. package/src/css/layout/grid.css +141 -0
  148. package/src/css/layout/index.css +42 -0
  149. package/src/css/layout/measure.css +9 -0
  150. package/src/css/layout/media.css +10 -0
  151. package/src/css/layout/scroll-target.css +15 -0
  152. package/src/css/layout/scroller.css +41 -0
  153. package/src/css/layout/sidebar-layout.css +31 -0
  154. package/src/css/layout/stack.css +21 -0
  155. package/src/css/layout/switcher.css +23 -0
  156. package/src/css/{optional/layout-extra.css → layout/topbar.css} +0 -20
  157. package/src/css/typography/index.css +5 -0
  158. package/src/css/typography/lead.css +7 -0
  159. package/src/css/{components → typography}/overline.css +10 -4
  160. package/src/css/{prose.css → typography/prose.css} +53 -38
  161. package/src/css/{utilities.css → utilities/base.css} +2 -0
  162. package/src/css/utilities/index.css +3 -0
  163. package/src/js/escape.js +36 -0
  164. package/src/js/flyout.js +22 -12
  165. package/src/js/full.js +26 -0
  166. package/src/js/index.js +5 -21
  167. package/src/js/surface.js +4 -32
  168. package/src/js/tooltip.js +65 -24
  169. package/src/js/validation.js +1 -1
  170. package/src/tooling/css-bundle.js +308 -0
  171. package/dist/actual-themes.min.css +0 -1
  172. package/dist/optional.css +0 -1048
  173. package/dist/optional.min.css +0 -1
  174. package/docs/pages/guides/optional-components.md +0 -33
  175. package/src/css/grid.css +0 -96
  176. package/src/css/layout.css +0 -108
  177. package/src/css/optional/index.css +0 -10
  178. package/src/css/optional/scroller.css +0 -63
  179. package/src/css/print.css +0 -47
  180. package/src/css/themes/corporate.css +0 -60
  181. package/src/css/themes/cyberpunk.css +0 -60
  182. package/src/css/themes/dim.css +0 -33
  183. package/src/css/themes/edge.css +0 -117
  184. package/src/css/themes/forest.css +0 -65
  185. package/src/css/themes/gradient.css +0 -97
  186. package/src/css/themes/index.css +0 -43
  187. package/src/css/themes/indigo.css +0 -33
  188. package/src/css/themes/lavender.css +0 -66
  189. package/src/css/themes/mono.css +0 -60
  190. package/src/css/themes/neon.css +0 -66
  191. package/src/css/themes/ocean.css +0 -60
  192. package/src/css/themes/petrol.css +0 -64
  193. package/src/css/themes/spruce.css +0 -64
  194. package/src/css/themes/square.css +0 -65
  195. package/src/css/themes/sunset.css +0 -66
  196. /package/src/css/{optional → components}/chat.css +0 -0
  197. /package/src/css/{optional → components}/fab.css +0 -0
  198. /package/src/css/{base.css → core/base.css} +0 -0
  199. /package/src/css/{focus.css → core/focus.css} +0 -0
  200. /package/src/css/{intents.css → core/intents.css} +0 -0
  201. /package/src/css/{reset.css → core/reset.css} +0 -0
  202. /package/src/css/{optional → effects}/aura.css +0 -0
  203. /package/src/css/{optional → layout}/scroll-snap.css +0 -0
  204. /package/src/css/{optional/typography-fluid.css → typography/fluid.css} +0 -0
  205. /package/src/css/{optional/utilities-extra.css → utilities/extra.css} +0 -0
@@ -0,0 +1,9 @@
1
+ /* Core — the minimal Actual language. Every module depends on this order. */
2
+ @import "./reset.css";
3
+ @import "./tokens.css";
4
+ @import "./theme.css";
5
+ @import "./base.css";
6
+ @import "./intents.css";
7
+ @import "./variants.css";
8
+ @import "./focus.css";
9
+ @import "./print.css";
@@ -0,0 +1,18 @@
1
+ @media print {
2
+ :root,
3
+ body {
4
+ /* Save some ink in dark mode */
5
+ color-scheme: light;
6
+ background: #fff;
7
+ color: #000;
8
+ }
9
+
10
+ h1,
11
+ h2,
12
+ h3,
13
+ h4,
14
+ h5,
15
+ h6 {
16
+ break-after: avoid;
17
+ }
18
+ }
@@ -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)
@@ -180,8 +190,7 @@
180
190
 
181
191
  @media (forced-colors: active) {
182
192
  :root,
183
- [data-theme="light"],
184
- [data-theme="dark"] {
193
+ [data-theme] {
185
194
  --focus-outline-color: Highlight;
186
195
  --focus-ring-shadow: none;
187
196
 
@@ -191,8 +200,9 @@
191
200
 
192
201
  @media (prefers-contrast: more) {
193
202
  :root,
194
- [data-theme="light"],
195
- [data-theme="dark"] {
203
+ /* The repeated attribute intentionally outranks single-attribute preset
204
+ palettes, including a preset stylesheet loaded after the core. */
205
+ [data-theme][data-theme] {
196
206
  --border-width: 2px;
197
207
  --focus-outline-offset: 3px;
198
208
  --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,7 +154,8 @@
142
154
  }
143
155
 
144
156
  @media (forced-colors: active) {
145
- :root {
157
+ :root,
158
+ [data-theme] {
146
159
  --state-selected: Highlight;
147
160
  --state-selected-fg: HighlightText;
148
161
  --state-disabled: GrayText;
@@ -2,14 +2,18 @@
2
2
  * Cross-cutting variant token mapping.
3
3
  *
4
4
  * Variant scope: .solid/.soft/.outline set --ui-* for components that opt into
5
- * shared variants. .inverted is a surface modifier, not a variant: it also
6
- * paints the element directly (background/color/border-color) so it works on
7
- * any block, competing with component surface treatments (.card.subtle).
5
+ * shared variants. .inverted is a surface modifier, not a variant: it paints
6
+ * the element directly (background/color/border-color) so it works on any bare
7
+ * block, and remaps the shared --ui-* tokens so participating components
8
+ * (.card, .navbar, .alert, .badge) invert through their own rules. Loaded
9
+ * before components, so a component with its own explicit surface (e.g.
10
+ * .accordion) naturally keeps that surface — .inverted never forces a
11
+ * non-participant.
8
12
  * Component-specific defaults (.btn solid-by-default, .badge
9
13
  * soft-by-default, .alert soft-by-default) live with their components:
10
14
  * components/button.css — .btn default solid + .btn.ghost + .btn.link
11
- * components/badge.css — .badge default soft
12
- * components/alert.css — .alert default soft (consumes --ui-* directly)
15
+ * components/badge.css — .badge default soft behind explicit --ui-*
16
+ * components/alert.css — .alert default soft behind explicit --ui-*
13
17
  *
14
18
  * .sm/.lg set shared density tokens: spacing rhythm and geometry only.
15
19
  * Density never changes typography — controls keep their font-size and
@@ -37,18 +41,23 @@
37
41
 
38
42
  .outline {
39
43
  --ui-bg: transparent;
40
- --ui-fg: var(--intent, var(--text));
41
- --ui-border: var(--intent, var(--border));
44
+ --ui-fg: var(--intent, currentColor);
45
+ --ui-border: var(--intent, currentColor);
42
46
  }
43
47
 
44
- /* Inverse surface treatment. Components (e.g. .card) compose through --ui-*;
45
- the direct background/color/border-color also make .inverted useful on a
46
- generic element. --heading is set so global headings (var(--heading, inherit))
47
- do not inherit an outside heading color onto the dark surface. */
48
+ /* Inverse surface treatment. Paints the element directly (so it works on any
49
+ bare block) and remaps the shared --ui-* tokens so opt-in components
50
+ (.card, .navbar) invert through their own rules. Because this loads before
51
+ component base surfaces, components with their own explicit surface
52
+ (e.g. .accordion) keep it — participation is opt-in, never forced. --heading
53
+ is set so global headings (var(--heading, inherit)) do not inherit an outside
54
+ heading color onto the dark surface; surface-owning descendants derive
55
+ their contextual aliases from their own local surface. */
48
56
  .inverted {
49
57
  --ui-bg: var(--surface-solid);
50
58
  --ui-fg: var(--surface);
51
59
  --ui-border: transparent;
60
+ --ui-hover-bg: var(--surface-solid);
52
61
 
53
62
  --heading: var(--surface);
54
63
  --busy-overlay-bg: var(--surface-solid);
@@ -76,14 +85,19 @@
76
85
  @supports (color: color-mix(in oklch, red, white)) {
77
86
  .soft {
78
87
  /* @sync soft-recipe */
79
- --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)));
80
89
  --ui-border: color-mix(
81
- in oklch,
90
+ in oklab,
82
91
  var(--surface) var(--soft-border-mix),
83
92
  var(--intent, var(--neutral))
84
93
  );
94
+ --ui-fg: color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix));
85
95
  /* @sync soft-recipe */
86
96
  }
97
+
98
+ .inverted {
99
+ --ui-hover-bg: color-mix(in oklch, var(--surface-solid) 88%, black);
100
+ }
87
101
  }
88
102
 
89
103
  /* Interactive hover recipes live in the components that consume them:
@@ -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
 
@@ -139,18 +139,19 @@
139
139
 
140
140
  .choice-card:has(:checked) {
141
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. */
142
+ /* Pin the selected surface to the system canvas: only the essential
143
+ marker pseudos opt out of remapping, never the interactive card host. */
144
144
  --choice-card-bg: Canvas;
145
- forced-color-adjust: none;
146
145
  }
147
146
 
148
147
  .choice-card:has(:checked)::before {
149
148
  background: Highlight;
150
149
  box-shadow: 0 0 0 2px Canvas;
150
+ forced-color-adjust: none;
151
151
  }
152
152
 
153
153
  .choice-card:has(:checked)::after {
154
154
  background: HighlightText;
155
+ forced-color-adjust: none;
155
156
  }
156
157
  }
@@ -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. */
@@ -20,6 +20,8 @@
20
20
  * --control-bg
21
21
  * --control-fg
22
22
  * --control-cursor
23
+ * --control-border-block-end-color
24
+ * --control-border-block-end-width
23
25
  */
24
26
  .input,
25
27
  .textarea,
@@ -44,7 +46,10 @@
44
46
  color: var(--control-fg);
45
47
  font-size: var(--control-font-size);
46
48
  cursor: var(--control-cursor);
47
- 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. */
48
53
  transition:
49
54
  border-color var(--duration),
50
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,15 @@
67
74
  appearance: none;
68
75
  }
69
76
 
70
- .otp:focus-within {
71
- box-shadow: var(--focus-ring-shadow);
72
- }
73
-
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. */
74
83
  .otp > input:focus ~ span {
75
84
  --otp-cell-border: var(--focus);
85
+ --otp-cell-border-width: calc(var(--border-width) * 2);
76
86
  }
77
87
 
78
88
  .otp > input[aria-invalid="true"] ~ span,
@@ -98,12 +108,6 @@
98
108
  }
99
109
 
100
110
  @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
111
  .otp > input:focus ~ span {
108
112
  --otp-cell-border: Highlight;
109
113
  }