@codenhub/styles 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.es.md +9 -8
  2. package/README.md +8 -7
  3. package/README.pt-BR.md +9 -8
  4. package/dist/aesthetics/chunky-tile.css +1 -1
  5. package/dist/aesthetics/cyber.css +1 -1
  6. package/dist/aesthetics/glass.css +1 -1
  7. package/dist/aesthetics/index.css +2 -2
  8. package/dist/aesthetics/neobrutalism.css +1 -1
  9. package/dist/aesthetics/pixel.css +1 -1
  10. package/dist/aesthetics/sketch.css +1 -1
  11. package/dist/cli.js +358 -0
  12. package/dist/components.css +50 -2
  13. package/dist/index.css +50 -2
  14. package/dist/index.d.ts +1 -0
  15. package/dist/native.css +50 -2
  16. package/dist/palette.css +1 -1
  17. package/dist/purge/classes.json +1 -0
  18. package/dist/purge/index.css +50 -0
  19. package/dist/theme.css +2 -2
  20. package/docs/accessibility.md +46 -27
  21. package/docs/changelog/0.8.0.md +70 -0
  22. package/docs/changelog/0.9.0.md +40 -0
  23. package/docs/changelog/index.md +2 -0
  24. package/docs/es/accessibility.md +47 -28
  25. package/docs/es/index.md +4 -3
  26. package/docs/es/integrating/index.md +38 -10
  27. package/docs/es/integrating/tailwind.md +79 -8
  28. package/docs/es/integrating/without-a-bundler.md +157 -5
  29. package/docs/es/setup.md +43 -34
  30. package/docs/es/usage/aesthetics.md +20 -14
  31. package/docs/es/usage/buttons.md +6 -2
  32. package/docs/es/usage/composing.md +6 -2
  33. package/docs/es/usage/content-and-layout.md +10 -4
  34. package/docs/es/usage/customizing.md +46 -26
  35. package/docs/es/usage/feedback.md +5 -1
  36. package/docs/es/usage/forms.md +30 -5
  37. package/docs/es/usage/index.md +7 -3
  38. package/docs/es/usage/presets.md +92 -0
  39. package/docs/es/usage/solo.md +4 -2
  40. package/docs/es/usage/theming.md +29 -16
  41. package/docs/es/usage/tooltips.md +2 -2
  42. package/docs/index.md +3 -2
  43. package/docs/integrating/index.md +38 -10
  44. package/docs/integrating/tailwind.md +78 -7
  45. package/docs/integrating/without-a-bundler.md +156 -4
  46. package/docs/pt-BR/accessibility.md +47 -28
  47. package/docs/pt-BR/index.md +4 -3
  48. package/docs/pt-BR/integrating/index.md +38 -10
  49. package/docs/pt-BR/integrating/tailwind.md +79 -8
  50. package/docs/pt-BR/integrating/without-a-bundler.md +157 -5
  51. package/docs/pt-BR/setup.md +43 -34
  52. package/docs/pt-BR/usage/aesthetics.md +20 -14
  53. package/docs/pt-BR/usage/buttons.md +6 -2
  54. package/docs/pt-BR/usage/composing.md +6 -2
  55. package/docs/pt-BR/usage/content-and-layout.md +10 -4
  56. package/docs/pt-BR/usage/customizing.md +46 -26
  57. package/docs/pt-BR/usage/feedback.md +5 -1
  58. package/docs/pt-BR/usage/forms.md +30 -5
  59. package/docs/pt-BR/usage/index.md +7 -3
  60. package/docs/pt-BR/usage/presets.md +92 -0
  61. package/docs/pt-BR/usage/solo.md +4 -2
  62. package/docs/pt-BR/usage/theming.md +22 -9
  63. package/docs/pt-BR/usage/tooltips.md +2 -2
  64. package/docs/setup.md +31 -22
  65. package/docs/usage/aesthetics.md +19 -13
  66. package/docs/usage/buttons.md +5 -1
  67. package/docs/usage/composing.md +5 -1
  68. package/docs/usage/content-and-layout.md +9 -3
  69. package/docs/usage/customizing.md +45 -25
  70. package/docs/usage/feedback.md +4 -0
  71. package/docs/usage/forms.md +29 -4
  72. package/docs/usage/index.md +6 -2
  73. package/docs/usage/presets.md +91 -0
  74. package/docs/usage/solo.md +3 -1
  75. package/docs/usage/theming.md +21 -8
  76. package/docs/usage/tooltips.md +1 -1
  77. package/llms-full.txt +695 -134
  78. package/llms.txt +3 -2
  79. package/package.json +117 -65
  80. package/{dist/tw → tw}/aesthetics/chunky-tile.css +43 -20
  81. package/{dist/tw → tw}/aesthetics/cyber.css +19 -25
  82. package/{dist/tw → tw}/aesthetics/glass.css +110 -36
  83. package/{dist/tw → tw}/aesthetics/neobrutalism.css +41 -21
  84. package/{dist/tw → tw}/aesthetics/pixel.css +3 -0
  85. package/tw/aesthetics/sketch.css +435 -0
  86. package/{dist/tw → tw}/box.css +109 -26
  87. package/tw/components/button.css +11 -0
  88. package/tw/components/button.rules.css +187 -0
  89. package/tw/components/feedback.css +11 -0
  90. package/{dist/tw/components/feedback.css → tw/components/feedback.rules.css} +156 -33
  91. package/tw/components/form.css +11 -0
  92. package/{dist/tw/components/form.css → tw/components/form.rules.css} +584 -141
  93. package/tw/components/index.css +16 -0
  94. package/tw/components/rules.css +41 -0
  95. package/tw/components/surface.css +11 -0
  96. package/tw/components/surface.rules.css +225 -0
  97. package/tw/components/tooltip.css +11 -0
  98. package/{dist/tw/components/tooltip.css → tw/components/tooltip.rules.css} +82 -61
  99. package/{dist/tw → tw}/corner.css +18 -12
  100. package/{dist/tw → tw}/elevation.css +27 -21
  101. package/{dist/tw → tw}/index.css +11 -9
  102. package/tw/index.d.ts +1 -0
  103. package/tw/intent.css +312 -0
  104. package/tw/modifiers.css +127 -0
  105. package/tw/native.css +323 -0
  106. package/tw/presentation.css +139 -0
  107. package/tw/reset.css +10 -0
  108. package/{dist/tw/reset.css → tw/reset.rules.css} +7 -8
  109. package/tw/size.css +34 -0
  110. package/tw/surface.css +9 -0
  111. package/tw/surface.rules.css +212 -0
  112. package/{dist/tw → tw}/theme.css +93 -29
  113. package/tw/typography.css +8 -0
  114. package/{dist/tw/typography.css → tw/typography.rules.css} +0 -3
  115. package/{dist/tw → tw}/utilities/content.css +248 -108
  116. package/tw/utilities/index.css +11 -0
  117. package/tw/utilities/layout.css +210 -0
  118. package/tw/utilities/rules.css +8 -0
  119. package/dist/tw/aesthetics/sketch.css +0 -398
  120. package/dist/tw/components/button.css +0 -139
  121. package/dist/tw/components/index.css +0 -35
  122. package/dist/tw/components/surface.css +0 -184
  123. package/dist/tw/intent.css +0 -229
  124. package/dist/tw/native.css +0 -203
  125. package/dist/tw/palette.css +0 -460
  126. package/dist/tw/presentation.css +0 -68
  127. package/dist/tw/surface.css +0 -61
  128. package/dist/tw/utilities/index.css +0 -6
  129. package/dist/tw/utilities/layout.css +0 -144
  130. /package/{dist/tw → tw}/aesthetics/index.css +0 -0
@@ -1,7 +1,3 @@
1
- @reference "tailwindcss";
2
- @import "../theme.css";
3
- @reference "../box.css";
4
-
5
1
  /* What all six text controls share: the box, the chrome, and two bounds that
6
2
  exist because presentation cascades -- a cascaded-fill cap so a container's
7
3
  `.solid` cannot fill a field with its own text colour, and an edge floor so a
@@ -24,6 +20,13 @@
24
20
  --_d-fg-on-fill: 0%;
25
21
  --_d-border: 100%;
26
22
 
23
+ /* A field's fill is at most a wash, never a plate, so it never holds an ink
24
+ of its own, even under a `.solid` it or a container names: it prints in
25
+ the ink it was handed. */
26
+ --_held-ink: inherit;
27
+ --_held-on: inherit;
28
+ --_own-hold: 0%;
29
+
27
30
  /* 6% is a quiet wash, and it is the cap rather than a presentation: `.soft`
28
31
  names the tint it wants and takes it whole below, so what this governs is
29
32
  `.solid` and the container that cascades one onto a field nobody classed. A
@@ -37,8 +40,16 @@
37
40
 
38
41
  The fill and the ink are `box`'s own, unrestated: its `--_capped` reads
39
42
  this cap, so the plate and the ink ramp stay in step at every bound,
40
- including the checked state below that lifts it. */
41
- --_fill-cap: 6%;
43
+ including the checked state below that lifts it.
44
+
45
+ The element's own fill class names its own cap: `.soft` its 12% whole,
46
+ `.solid` 20%, which clears `.soft` and matches the plate a `.solid`
47
+ badge draws. Read from `--_own-fill`, which the class writes and which
48
+ does not inherit, so the 6% still answers a container's class. Raised
49
+ here rather than written as a fill (P6), which is what keeps `.soft` <
50
+ `.solid`. See
51
+ model.md#the-bounds-that-survive-and-the-test-for-keeping-one. */
52
+ --_fill-cap: max(6%, min(var(--_own-fill), 20%));
42
53
  /* The resting line is a fraction of the intent rather than all of it, and the
43
54
  fraction is what makes the pointer visible. Drawn at full strength a control
44
55
  already sits at its intent's darkest, and hover moves it to a neighbouring
@@ -52,6 +63,13 @@
52
63
  back to 100% -- its line is a silhouette rather than a field's edge, and
53
64
  there is nothing else marking an unchecked box. */
54
65
  --_line-rest: 60%;
66
+ /* Someone who asks for more contrast gets the line whole: above 4:1
67
+ against the page in both themes, where the quiet line is under 3:1. Hover
68
+ and focus then change nothing in the tone, which is the trade the
69
+ preference asks for. See docs/accessibility.md. */
70
+ @media (prefers-contrast: more) {
71
+ --_line-rest: 100%;
72
+ }
55
73
  /* Two seams rather than one composed result. `--_line-tone` is what colour
56
74
  the line is, and the states below move it; `--_line` folds in the fill,
57
75
  walking the line toward the intent colour by the fill amount, so a state
@@ -70,20 +88,26 @@
70
88
  typing goes and a toggle with no silhouette at all (WCAG 1.4.11). That is
71
89
  our own cascade producing the broken result, so our own floor answers it.
72
90
 
73
- An element's own `.edgeless` lowers the floor -- see the rule below the
74
- utility -- which is a consumer naming what they want rather than a cascade
75
- reaching something nobody classed. `.checkbox` and `.radio` are left off
76
- that rule and keep the floor absolute: they have nothing outside their
77
- line, so removing it removes the control. */
78
- --_edge-floor: 100%;
91
+ An element's own `.edgeless` lowers the floor -- `--_own-edge`, which
92
+ the class writes and which does not inherit -- because that is a consumer
93
+ naming what they want rather than a cascade reaching something nobody
94
+ classed. `.checkbox` and `.radio` restate the floor absolute: they have
95
+ nothing outside their line, so removing it removes the control, and
96
+ `.edgeless` on them is published unsupported. `.input-group` keeps the
97
+ lowering because the group is the field boundary. */
98
+ --_edge-floor: var(--_own-edge);
79
99
 
80
100
  @apply w-full appearance-none;
81
101
 
82
102
  /* The surrounding font rather than the user agent's smaller field one, on
83
- every entry: `/components` carries no reset to say so. */
103
+ every entry: `/components` carries no reset to say so. Its tracking is
104
+ typed text's own, `normal`, rather than the page's: body copy's wider
105
+ tracking is set on `body`, and Preflight's `letter-spacing: inherit` on
106
+ `/tw` handed it, or a container's, to every field. */
84
107
  font-family: inherit;
85
108
  font-size: inherit;
86
109
  line-height: inherit;
110
+ letter-spacing: normal;
87
111
 
88
112
  &::placeholder {
89
113
  color: var(--color-text-secondary);
@@ -92,16 +116,20 @@
92
116
  /* The last exclusion is `.input-group`'s: a group whose control is disabled
93
117
  dims as a disabled field does (see `input-group`), so it does not answer
94
118
  the pointer either. The list is the group's own, so a `<select>` with a
95
- disabled `<option>` is not caught by it. */
96
- &:hover:not(
97
- :disabled,
98
- .disabled,
99
- [aria-disabled="true"],
100
- [data-disabled],
101
- :focus-visible,
102
- :has(> :is(.ipt, .textarea, .select, .text-control, input, textarea, select):disabled)
103
- ) {
104
- --_line-tone: var(--intent-hover);
119
+ disabled `<option>` is not caught by it. Under `(hover: hover)` as the
120
+ other hover responses are: a tapped checkbox, select, or group stays in
121
+ `:hover` without matching `:focus-visible`, and kept the hover line. */
122
+ @media (hover: hover) {
123
+ &:hover:not(
124
+ :disabled,
125
+ .disabled,
126
+ [aria-disabled="true"],
127
+ [data-disabled],
128
+ :focus-visible,
129
+ :has(> :is(.ipt, .textarea, .select, .text-control, input, textarea, select):disabled)
130
+ ) {
131
+ --_line-tone: var(--intent-hover);
132
+ }
105
133
  }
106
134
 
107
135
  /* The line moves to the intent on top of the `reset.css` `:focus-visible`
@@ -112,12 +140,115 @@
112
140
  --_line-tone: var(--intent-color);
113
141
  }
114
142
 
143
+ /* The forced-colours ring, carried so `@apply text-control` draws it: the
144
+ unlayered block at the end of this file names the package's own classes,
145
+ and `reset.css` names only typed native fields, so an applied control --
146
+ an `<input>` with no `type`, a `contenteditable` -- kept the 3px ring in a
147
+ system colour instead of the 2px `Highlight` one beside a `.text-control`.
148
+
149
+ `!important`, as the reset's ring is: the copy lands in whichever layer the
150
+ consumer applied it in, and an accessibility override has to win over any
151
+ layered rule there. So it cannot be suppressed and is kept off a control
152
+ inside an `.input-group`, which draws the field's ring and has the control
153
+ inside draw none; the offset and the cleared shadow are not important and
154
+ reach that control too, as the unlayered block's do. */
155
+ @media (forced-colors: active) {
156
+ &:focus-visible {
157
+ outline-offset: 2px;
158
+ box-shadow: none;
159
+ }
160
+
161
+ &:focus-visible:where(:not(.input-group > *)) {
162
+ outline: 2px solid Highlight !important;
163
+ }
164
+ }
165
+
166
+ /* Everything below until `:checked` is keyed on the class, and lives inside
167
+ the utility so `@apply text-control` carries it: `@apply` copies this body
168
+ with the consumer's own selector, and a rule naming `.text-control` from
169
+ outside it never reaches their element. See model.md#the-composition-api.
170
+
171
+ The neutral reset, for an element that carries none of the package's own
172
+ text-control classes -- a consumer's `.my-control { @apply text-control }`.
173
+ Those classes keep the reset in `intent.css`, in `components`; this one
174
+ lands in `utilities` and would beat a consumer's own intent class declared
175
+ in `components` there. It writes the values that reset and the control
176
+ line do, so a field inside a `.card.success` stays neutral and draws the
177
+ 3:1 control line rather than `--color-border`.
178
+
179
+ Zero specificity, and never on an element with an intent class, so it
180
+ cannot overrule one: the intent class is excluded, and every rule that
181
+ writes an intent slot in `utilities` -- `aria-invalid` below, the
182
+ `:checked` arm, `.input-group`'s inherited `--intent-strong` -- carries at
183
+ least 0-1-0. `.neutral` is not excluded, so `.neutral` takes the control
184
+ line here as it does on `.ipt.neutral`. */
185
+ /* In `families`, beside the intent classes: applied with an intent --
186
+ `@apply text-control info` -- the reset and the intent land in one layer, where the
187
+ intent's 0-1-0 beats this. Left in the rule's own layer it would beat
188
+ every intent applied with it. */
189
+ @layer families {
190
+ :where(
191
+ &:not(
192
+ .text-control,
193
+ .ipt,
194
+ .textarea,
195
+ .select,
196
+ .input-group,
197
+ .checkbox,
198
+ .radio,
199
+ .switch,
200
+ .primary,
201
+ .secondary,
202
+ .success,
203
+ .warning,
204
+ .destructive,
205
+ .info
206
+ )
207
+ ) {
208
+ --intent-color: var(--color-text);
209
+ --intent-contrast: var(--color-text-contrast);
210
+ --intent-hover: var(--color-text-hover);
211
+ --intent-strong: var(--color-text-strong);
212
+ --intent-subtle: var(--color-surface);
213
+ --intent-border: var(--ui-control-ink, var(--ui-ink, var(--color-control-border)));
214
+ --intent-fill-max: 20%;
215
+ }
216
+ }
217
+
218
+ /* The component seams: rules that beat their own component's body on
219
+ specificity, 0-2-0 against 0-1-0, in `utilities`. They write only package
220
+ privates or a state's intent slots, so nothing a consumer writes competes
221
+ with them. */
222
+
223
+ /* An invalid control is destructive whatever else it was asked to be. In
224
+ `utilities` above every intent class in `utilities.families` and both
225
+ resets in `components`, and
226
+ 0-2-0 so it also outranks the control's own body and the reset above; a
227
+ consumer's utility setting an intent slot on an invalid field loses to it
228
+ too, because state wins over every choice (model.md#precedence).
229
+ `--intent-hover` is redirected with the others so the line does not go grey
230
+ under the pointer, and `--intent-strong` so typed text is destructive too.
231
+ `--intent-contrast` and `--intent-subtle` go with them, so an invalid
232
+ toggle is a destructive one in every part: left neutral, a checked invalid
233
+ checkbox in the dark theme drew the near-black neutral mark on a red plate
234
+ (3.28:1). The bare elements and the `.input-group` bridge are below the
235
+ utility, since they name no class `@apply` could carry. */
236
+ &[aria-invalid="true"] {
237
+ --intent-color: var(--color-destructive);
238
+ --intent-contrast: var(--color-destructive-contrast);
239
+ --intent-hover: var(--color-destructive-hover);
240
+ --intent-border: var(--color-destructive);
241
+ --intent-strong: var(--color-destructive-strong);
242
+ --intent-subtle: var(--color-destructive-subtle);
243
+ }
244
+
115
245
  /* S1, P6: `:checked` is a state, so it re-declares the presentation inputs --
116
246
  a full fill and full contrast ink -- and lifts the cap that would bound
117
247
  them, and `box` composes the rest. It does not write a result: pinning
118
248
  `--_fill` and `--_fg` directly would make a checked toggle ignore its own
119
249
  aesthetic and elevation along with presentation. It sits in `utilities` at
120
- 0-2-0, above the fill classes in `components`, so a checked toggle renders
250
+ 0-2-0, above the fill classes in `utilities.families`, so a checked toggle
251
+ renders
121
252
  one plate whatever presentation it or a container asks for, and a
122
253
  consumer's utility loses to it because state wins over every choice
123
254
  (model.md#precedence). A toggle is a void `<input>`, so the inputs it
@@ -178,41 +309,41 @@
178
309
  }
179
310
  }
180
311
 
181
- /* The component seams: rules that exist to beat their own component's
182
- `@utility`, placed in `utilities` and winning there on specificity, each at
183
- least 0-2-0 against the component's 0-1-0. They write only package privates
184
- (`--_fill-cap`, `--_edge-floor`) or a state's intent slots, so nothing a
185
- consumer writes competes with them. See
312
+ /* The seams that name no class `@apply text-control` could carry: bare
313
+ elements, the toggles' shared lift, and one component's own combination.
314
+ Placed in `utilities` and winning there on specificity, like the seams inside
315
+ the utility. They write only package privates or a state's intent slots, so
316
+ nothing a consumer writes competes with them. See
186
317
  docs/internal/model.md#cascade-layers. */
187
318
  @layer utilities {
188
- /* A fill class on the element names its own cap; the 6% cascade cap in the
189
- utility is for a container reaching a field nobody classed. Both raise
190
- `--_fill-cap` rather than writing `--_fill` (P6), which is also what keeps
191
- `.soft` < `.solid`: written as a rewrite, the louder name renders the
192
- quieter box. 20% clears `.soft`'s 12% and matches the plate a `.solid`
193
- badge draws.
194
-
195
- `.switch` is deliberately absent: its track caps at 40% in its own utility,
196
- and a 0-2-0 rule here would overrule that. See
197
- model.md#the-bounds-that-survive-and-the-test-for-keeping-one. */
198
- :is(.text-control, .ipt, .textarea, .select, .input-group).soft {
199
- --_fill-cap: 12%;
200
- }
201
-
202
- :is(.text-control, .ipt, .textarea, .select, .input-group).solid {
203
- --_fill-cap: 20%;
204
- }
205
-
206
319
  /* A checked text-control fills with its own intent, so neutral has to drop its
207
320
  20% cap here. No toggle needs this arm any more (see below), but a text
208
321
  input is never `:checked` in practice either -- kept for whatever custom
209
- `.text-control` composition does check itself. A plain rule at 0-2-0 in
210
- `utilities`, above every intent class in `components`, so `.neutral` on the
211
- element cannot put the cap back. */
322
+ `.text-control` composition does check itself; only an `<input>` can be,
323
+ so the `input` arm reaches an `@apply text-control` one too. A plain rule
324
+ at 0-2-0 in `utilities`, above every intent class in
325
+ `utilities.families`, so
326
+ `.neutral` on the element cannot put the cap back. */
212
327
  :is(.text-control, input):checked {
213
328
  --intent-fill-max: 100%;
214
329
  }
215
330
 
331
+ /* A bare toggle's "on" plate. `/native` reaches the toggles through `@apply`
332
+ in `base`, where their `:checked` and `:indeterminate` pins lose to
333
+ `.ghost` and `.soft` in `utilities`, so a checked quiet checkbox kept its
334
+ resting plate. The class route already wins on specificity in this layer;
335
+ this repeats its values for the bare element, with the neutral cap
336
+ `native.css`'s reset puts back, which the arm above lifts only when
337
+ checked. Not `radio:indeterminate`, which every radio of a group with
338
+ nothing chosen matches. */
339
+ input:is([type="checkbox"], [type="radio"]):checked,
340
+ input[type="checkbox"]:indeterminate {
341
+ --intent-fill-max: 100%;
342
+ --ui-fill: 100%;
343
+ --ui-fg-on-fill: 100%;
344
+ --_fill-cap: 100%;
345
+ }
346
+
216
347
  /* The three toggles read their own component cap (`--_fill-cap`, below) as the
217
348
  ceiling regardless of intent, checked or not -- not just at `:checked`, the
218
349
  way the arm above still works for a hypothetical checked text-control. A
@@ -222,60 +353,28 @@
222
353
  controls carry no intent class at all, and it has to look like a present,
223
354
  interactive box rather than a washed-out one just because nothing colored
224
355
  it. Declared where the intents are, for the reason given above. */
225
- :is(.checkbox, .radio, .switch) {
226
- --intent-fill-max: 100%;
227
- }
228
-
229
- /* The element's own `.edgeless` lowers the floor `text-control` raises -- a
230
- class on the element is a consumer's request, a class on a container is a
231
- cascade. `.checkbox` and `.radio` are absent: an unchecked one is a
232
- transparent square/circle with nothing but its line, so the floor is
233
- absolute and `.edgeless` on them is published unsupported. `.input-group`
234
- is on it because the group is the field boundary. */
235
- :is(.text-control, .ipt, .textarea, .select, .switch, .input-group).edgeless {
236
- --_edge-floor: 0%;
237
- }
238
-
239
- /* `.switch`'s one exception to the opt-out above: `.ghost` already asks for
240
- zero fill, so a `.switch.ghost.edgeless` track would have neither a line
241
- nor a plate and disappear -- nothing would mark its bounds but the floating
242
- knob. Every other fill on this component has something to paint
243
- (`.soft`/`.solid`/default all rest above 0%), so only this pairing needs
244
- the floor held up. `.switch.ghost.edgeless` therefore renders identically
245
- to `.switch.ghost.edged`, and is published unsupported as a distinct look.
246
- Higher specificity than the rule above (three classes, not an `:is()` of
247
- one) wins regardless of source order. */
248
- .switch.ghost.edgeless {
249
- --_edge-floor: 100%;
250
- }
251
356
 
252
- /* An invalid control is destructive whatever else it was asked to be. It sits
253
- in `utilities`, above every intent class and both intent resets in
254
- `components`, and `:is()` gives it 0-2-0 so it also outranks the control's
255
- own utility; a consumer's utility setting an intent slot on an invalid
256
- field loses to it too, because state wins over every choice
257
- (model.md#precedence). `--intent-hover` is redirected with the others so
258
- the line does not go grey under the pointer, and `--intent-strong` so typed
259
- text is destructive too. The `:has()` arm bridges an
260
- inner control's state to an `.input-group`. */
261
- :is(
262
- .text-control,
263
- .ipt,
264
- .textarea,
265
- .select,
266
- .checkbox,
267
- .radio,
268
- .switch,
269
- .input-group,
270
- input,
271
- textarea,
272
- select
273
- )[aria-invalid="true"],
357
+ /* `.switch`'s one exception to the `.edgeless` opt-out in `text-control`:
358
+ `.ghost` already asks for zero fill, so a `.switch.ghost.edgeless` track
359
+ would have neither a line nor a plate and disappear -- nothing would mark
360
+ its bounds but the floating knob. Every other fill on this component has
361
+ something to paint (`.soft`/`.solid`/default all rest above 0%), so only
362
+ this pairing needs the floor held up. `.switch.ghost.edgeless` therefore
363
+ renders identically to `.switch.ghost.edged`, and is published unsupported
364
+ as a distinct look. Three classes against the opt-out's two, so it wins
365
+ regardless of source order. */
366
+
367
+ /* `text-control`'s invalid state, for a bare field no class reaches and for
368
+ the `.input-group` whose control is invalid. The values are the
369
+ utility's; see the state there. */
370
+ :is(input, textarea, select)[aria-invalid="true"],
274
371
  .input-group:has(> :is(.ipt, .textarea, .select, .text-control, input, textarea, select)[aria-invalid="true"]) {
275
372
  --intent-color: var(--color-destructive);
373
+ --intent-contrast: var(--color-destructive-contrast);
276
374
  --intent-hover: var(--color-destructive-hover);
277
375
  --intent-border: var(--color-destructive);
278
376
  --intent-strong: var(--color-destructive-strong);
377
+ --intent-subtle: var(--color-destructive-subtle);
279
378
  }
280
379
  }
281
380
 
@@ -296,21 +395,48 @@
296
395
  4.5:1 there for several intents. On the three text inputs, not
297
396
  `text-control`, because a toggle's mark takes `box`'s composed `--_fg`
298
397
  instead. See docs/internal/model.md#intent. */
299
- --_fg: var(--intent-strong);
300
-
301
- min-height: var(--control-height);
398
+ --_fg-own: var(--intent-strong);
399
+
400
+ /* The button's size steps, so an `.ipt.sm` and a `.btn.sm` line up in a row:
401
+ the same 2rem and 3rem heights, the same text sizes, and the same
402
+ `--_scale-size` step `box` scales the corner by. Flags, read here, for the
403
+ reason `btn` gives. */
404
+ --_sm-h: var(--_size-sm) 2rem;
405
+ --_lg-h: var(--_size-lg) 3rem;
406
+ --_sm-px: var(--_size-sm) 0.625rem;
407
+ --_lg-px: var(--_size-lg) 1rem;
408
+ --_sm-py: var(--_size-sm) 0.25rem;
409
+ --_lg-py: var(--_size-lg) 0.625rem;
410
+ --_sm-text: var(--_size-sm) var(--text-sm);
411
+ --_lg-text: var(--_size-lg) 1.0625rem;
412
+ --_sm-leading: var(--_size-sm) var(--text-sm--line-height);
413
+ --_sm-scale: var(--_size-sm) 0.75;
414
+ --_lg-scale: var(--_size-lg) 1.25;
415
+ --_scale-size: var(--_sm-scale, var(--_lg-scale));
416
+
417
+ height: var(--_sm-h, var(--_lg-h, var(--control-height, 2.5rem)));
418
+ min-height: var(--_sm-h, var(--_lg-h, var(--control-height)));
419
+ padding-inline: var(--_sm-px, var(--_lg-px, 0.75rem));
420
+ padding-block: var(--_sm-py, var(--_lg-py, 0.5rem));
421
+ font-size: var(--_sm-text, var(--_lg-text, inherit));
422
+ line-height: var(--_sm-leading, inherit);
302
423
  }
303
424
 
304
425
  @utility field {
305
- --field-gap: 0.35rem;
426
+ @apply flex w-full flex-col;
306
427
 
307
- @apply flex w-full flex-col gap-(--field-gap);
428
+ /* Public, read with its default, so a value on a form reaches every field. */
429
+ gap: var(--field-gap, 0.35rem);
308
430
  }
309
431
 
310
432
  @utility label {
311
- font-size: 1rem;
312
- font-weight: 500;
313
- letter-spacing: 0.025em;
433
+ /* In `:where()`: a label's type is a default, and Tailwind emits this rule
434
+ after its own type utilities, so at 0-1-0 `.label.text-sm` stayed 16px. */
435
+ :where(&) {
436
+ font-size: 1rem;
437
+ font-weight: 500;
438
+ letter-spacing: 0.025em;
439
+ }
314
440
  }
315
441
 
316
442
  /* A hint wraps, so it takes `text-sm`'s leading like the package's other 14px
@@ -322,8 +448,15 @@
322
448
  font-weight: 400;
323
449
  letter-spacing: 0.05em;
324
450
 
325
- &.destructive {
326
- color: var(--intent-color);
451
+ /* A named intent prints its readable tone, `--intent-strong`, the colour
452
+ typed text takes: a hint is text on the page, and `--intent-color` is a
453
+ fill colour that reads below 4.5:1 there (destructive measured 3.28:1 on
454
+ the dark page). No intent, or `.neutral`, keeps the secondary grey that
455
+ marks a hint as helper text. A hint is not a component, so the intent
456
+ reset does not reach it and an intent on a container does not recolour
457
+ it; only a class on the hint itself does. */
458
+ &:is(.primary, .secondary, .success, .warning, .destructive, .info) {
459
+ color: var(--intent-strong);
327
460
  }
328
461
  }
329
462
 
@@ -341,9 +474,27 @@
341
474
  4.5:1 there for several intents. On the three text inputs, not
342
475
  `text-control`, because a toggle's mark takes `box`'s composed `--_fg`
343
476
  instead. See docs/internal/model.md#intent. */
344
- --_fg: var(--intent-strong);
477
+ --_fg-own: var(--intent-strong);
345
478
 
346
479
  @apply min-h-28 resize-y;
480
+
481
+ /* The size steps `ipt` takes, for its text, padding, and corner. Its
482
+ height is its rows, so it keeps its own floor. */
483
+ --_sm-px: var(--_size-sm) 0.625rem;
484
+ --_lg-px: var(--_size-lg) 1rem;
485
+ --_sm-py: var(--_size-sm) 0.25rem;
486
+ --_lg-py: var(--_size-lg) 0.625rem;
487
+ --_sm-text: var(--_size-sm) var(--text-sm);
488
+ --_lg-text: var(--_size-lg) 1.0625rem;
489
+ --_sm-leading: var(--_size-sm) var(--text-sm--line-height);
490
+ --_sm-scale: var(--_size-sm) 0.75;
491
+ --_lg-scale: var(--_size-lg) 1.25;
492
+ --_scale-size: var(--_sm-scale, var(--_lg-scale));
493
+
494
+ padding-inline: var(--_sm-px, var(--_lg-px, 0.75rem));
495
+ padding-block: var(--_sm-py, var(--_lg-py, 0.5rem));
496
+ font-size: var(--_sm-text, var(--_lg-text, inherit));
497
+ line-height: var(--_sm-leading, inherit);
347
498
  }
348
499
 
349
500
  /* The same control with a native list behind it. The chevron is drawn from two
@@ -362,7 +513,7 @@
362
513
  4.5:1 there for several intents. On the three text inputs, not
363
514
  `text-control`, because a toggle's mark takes `box`'s composed `--_fg`
364
515
  instead. See docs/internal/model.md#intent. */
365
- --_fg: var(--intent-strong);
516
+ --_fg-own: var(--intent-strong);
366
517
 
367
518
  min-height: var(--control-height);
368
519
  background-image:
@@ -374,6 +525,27 @@
374
525
  background-repeat: no-repeat;
375
526
  background-size: 0.3125rem 0.3125rem;
376
527
 
528
+ /* The size steps `ipt` takes. The inline end keeps the chevron's room. */
529
+ --_sm-h: var(--_size-sm) 2rem;
530
+ --_lg-h: var(--_size-lg) 3rem;
531
+ --_sm-px: var(--_size-sm) 0.625rem;
532
+ --_lg-px: var(--_size-lg) 1rem;
533
+ --_sm-py: var(--_size-sm) 0.25rem;
534
+ --_lg-py: var(--_size-lg) 0.625rem;
535
+ --_sm-text: var(--_size-sm) var(--text-sm);
536
+ --_lg-text: var(--_size-lg) 1.0625rem;
537
+ --_sm-leading: var(--_size-sm) var(--text-sm--line-height);
538
+ --_sm-scale: var(--_size-sm) 0.75;
539
+ --_lg-scale: var(--_size-lg) 1.25;
540
+ --_scale-size: var(--_sm-scale, var(--_lg-scale));
541
+
542
+ height: var(--_sm-h, var(--_lg-h, var(--control-height, 2.5rem)));
543
+ min-height: var(--_sm-h, var(--_lg-h, var(--control-height)));
544
+ padding-inline-start: var(--_sm-px, var(--_lg-px, 0.75rem));
545
+ padding-block: var(--_sm-py, var(--_lg-py, 0.5rem));
546
+ font-size: var(--_sm-text, var(--_lg-text, inherit));
547
+ line-height: var(--_sm-leading, inherit);
548
+
377
549
  /* The chevron sits at the inline end, like the padding kept for it, so a
378
550
  right-to-left select draws it at the left. `background-position` has no
379
551
  logical form, so the mirror is named here. */
@@ -402,6 +574,14 @@
402
574
  padding-inline: 0.75rem;
403
575
  background-image: none;
404
576
  }
577
+
578
+ /* Forced colours, carried for an applied drop-down: the platform's arrow, for
579
+ the reason the unlayered copy at the foot of this file gives. */
580
+ @media (forced-colors: active) {
581
+ &:not([multiple], [size]:not([size="1"])) {
582
+ appearance: auto;
583
+ }
584
+ }
405
585
  }
406
586
 
407
587
  /* A wrapper that owns the field box so a control can carry adornments beside it
@@ -422,7 +602,7 @@
422
602
  breathing room a distinct glyph needs, but a text affix meant to glue onto the
423
603
  value -- a URL host, a currency symbol, a unit -- reads the same gap as a
424
604
  stray space breaking the two apart. A consumer sets `--input-group-gap: 0` on
425
- their own `.input-group` for a flush affix; the default stays the icon-tuned
605
+ the group or an ancestor for a flush affix; the default stays the icon-tuned
426
606
  0.5rem so every existing icon composition is unchanged. */
427
607
  @utility input-group {
428
608
  @apply text-control flex w-full items-center;
@@ -436,12 +616,28 @@
436
616
  --_d-border: 100%;
437
617
  --_d-elevation: 0;
438
618
 
439
- --input-group-gap: 0.5rem;
440
- --input-group-pad: 0.75rem;
441
-
442
- min-height: var(--control-height);
443
- gap: var(--input-group-gap);
444
- padding-inline: var(--input-group-pad);
619
+ /* The size steps `ipt` takes, on the group rather than the control inside:
620
+ the group is the field, and the control and its adornments inherit the
621
+ text size. The control's height is the group's (below), so the group's
622
+ floor is its height. */
623
+ --_sm-h: var(--_size-sm) 2rem;
624
+ --_lg-h: var(--_size-lg) 3rem;
625
+ --_sm-px: var(--_size-sm) 0.625rem;
626
+ --_lg-px: var(--_size-lg) 1rem;
627
+ --_sm-text: var(--_size-sm) var(--text-sm);
628
+ --_lg-text: var(--_size-lg) 1.0625rem;
629
+ --_sm-leading: var(--_size-sm) var(--text-sm--line-height);
630
+ --_sm-scale: var(--_size-sm) 0.75;
631
+ --_lg-scale: var(--_size-lg) 1.25;
632
+ --_scale-size: var(--_sm-scale, var(--_lg-scale));
633
+
634
+ min-height: var(--_sm-h, var(--_lg-h, var(--control-height)));
635
+ /* Public, read with their defaults, so a value set on any ancestor reaches
636
+ the group. */
637
+ gap: var(--input-group-gap, 0.5rem);
638
+ padding-inline: var(--input-group-padding, var(--_sm-px, var(--_lg-px, 0.75rem)));
639
+ font-size: var(--_sm-text, var(--_lg-text, inherit));
640
+ line-height: var(--_sm-leading, inherit);
445
641
 
446
642
  /* The focus ring the wrapper cannot get from `:focus-visible` -- a `<div>` is
447
643
  not focusable, and `:has(:focus-visible)` is unreliable because the inner
@@ -454,27 +650,92 @@
454
650
  &:focus-within {
455
651
  --_line-tone: var(--intent-color);
456
652
 
457
- outline: var(--focus-ring-width) solid var(--focus-ring);
653
+ outline: var(--focus-ring-width) solid var(--focus-ring, color-mix(in srgb, var(--color-primary) 64%, transparent));
458
654
  outline-offset: var(--focus-ring-offset, calc(-1 * var(--ui-border-width, var(--border-width))));
459
- box-shadow: var(--_focus-stack, var(--_shadow));
655
+ box-shadow: var(--_focus-stack, var(--_shadow), inset 0 0 0 2px var(--_focus-gap));
656
+ }
657
+
658
+ /* Focus on anything but the field -- the clear button the docs show, a
659
+ toggle -- is focus on that control, so it keeps its own ring and the group
660
+ drops the field's. Enter presses a focused button rather than typing into
661
+ the field, and two rings around one target say neither. The line keeps its
662
+ focus tone: something in the field still has focus. A separate rule rather
663
+ than a `:not(:has())` on the ring above, so an engine without `:has()`
664
+ drops only this rule and keeps the group's ring. */
665
+ &:has(> :not(.ipt, .textarea, .select, .text-control, input, textarea, select):focus) {
666
+ outline: none;
667
+ box-shadow: var(--_shadow);
460
668
  }
461
669
 
462
670
  /* A disabled control inside dims the whole group. `.input-group.disabled` and
463
671
  the aria/data spellings are already handled by `box`'s own rule, carried in
464
672
  by `text-control`; this is the child-driven case a wrapper cannot see
465
673
  without `:has()`. */
466
- &:has(> :is(.ipt, .textarea, .select, .text-control, input, textarea, select):disabled) {
674
+ &:has(
675
+ > :is(.ipt, .textarea, .select, .text-control, input, textarea, select):where(
676
+ :not(
677
+ .checkbox,
678
+ .radio,
679
+ .switch,
680
+ .btn,
681
+ [type="checkbox"],
682
+ [type="radio"],
683
+ [type="button"],
684
+ [type="submit"],
685
+ [type="reset"],
686
+ [type="image"],
687
+ [type="range"],
688
+ [type="color"],
689
+ [type="file"],
690
+ [type="hidden"]
691
+ )
692
+ ):disabled
693
+ ) {
467
694
  @apply cursor-not-allowed;
468
695
 
469
696
  opacity: 0.58;
470
697
  }
471
698
 
472
- /* The control goes flush: the group drew the box, so the control paints no
699
+ /* "The control inside" is a text field. Every selector below that names it
700
+ ends in the same `:where(:not(...))`, which takes out the toggles, the
701
+ button-like inputs, and the inputs that draw their own widget: matched,
702
+ a checkbox in a group lost its box and its line, and stretched like a
703
+ field. In `:where()`, so the exclusion adds no specificity.
704
+
705
+ The control goes flush: the group drew the box, so the control paints no
473
706
  fill, no line and no ring, and gives its horizontal padding to the group.
474
707
  `border: none` rather than a transparent 1px, so the control does not add
475
- its own width inside the group. */
476
- & > :is(.ipt, .textarea, .select, .text-control, input, textarea, select) {
708
+ its own width inside the group.
709
+
710
+ The typed text is the one paint the control keeps, and it is the group's
711
+ intent: the control resets its intent to neutral at its own root, like
712
+ every component, so an `.input-group.success` drew a success line around
713
+ neutral text. `--intent-strong` is the only slot the text reads, so it
714
+ alone inherits, and an invalid group -- by its own `aria-invalid` or the
715
+ control's, through the `:has()` arm -- prints destructive text the way a
716
+ lone invalid field does. The group owns the field, so an intent class on
717
+ the control inside does not colour its text. */
718
+ &
719
+ > :is(.ipt, .textarea, .select, .text-control, input, textarea, select):where(
720
+ :not(
721
+ .checkbox,
722
+ .radio,
723
+ .switch,
724
+ .btn,
725
+ [type="checkbox"],
726
+ [type="radio"],
727
+ [type="button"],
728
+ [type="submit"],
729
+ [type="reset"],
730
+ [type="image"],
731
+ [type="range"],
732
+ [type="color"],
733
+ [type="file"],
734
+ [type="hidden"]
735
+ )
736
+ ) {
477
737
  --_fill-cap: 0%;
738
+ --intent-strong: inherit;
478
739
 
479
740
  flex: 1 1 auto;
480
741
  min-width: 0;
@@ -487,6 +748,46 @@
487
748
  outline: none;
488
749
  box-shadow: none;
489
750
  }
751
+
752
+ /* The group dims for a disabled control, so the control does not dim
753
+ again inside it: opacity multiplies, and the two 0.58s left the value
754
+ at about 0.34. Only `:disabled`, the state the group's `:has()` above
755
+ bridges; a control disabled by class or aria dims itself as a lone one
756
+ does, because the group does not dim for it. */
757
+ &:disabled {
758
+ opacity: 1;
759
+ }
760
+ }
761
+
762
+ /* A one-row control gives its height to the group too. Keeping its own
763
+ `--control-height` inside a box that draws its own line made the group
764
+ 42px next to 40px fields and buttons. It stretches to the group's inside
765
+ instead, with no block padding, so the group is the control height and the
766
+ whole of its inside still takes a click. A textarea and a list box keep
767
+ their own height: their rows are the height. */
768
+ &
769
+ > :is(.ipt, .select, input, select):not([multiple], [size]:not([size="1"])):where(
770
+ :not(
771
+ .checkbox,
772
+ .radio,
773
+ .switch,
774
+ .btn,
775
+ [type="checkbox"],
776
+ [type="radio"],
777
+ [type="button"],
778
+ [type="submit"],
779
+ [type="reset"],
780
+ [type="image"],
781
+ [type="range"],
782
+ [type="color"],
783
+ [type="file"],
784
+ [type="hidden"]
785
+ )
786
+ ) {
787
+ align-self: stretch;
788
+ height: auto;
789
+ min-height: 0;
790
+ padding-block: 0;
490
791
  }
491
792
 
492
793
  /* Except a drop-down's end: its chevron is the select's own background,
@@ -498,12 +799,45 @@
498
799
  }
499
800
 
500
801
  /* Anything else in the group -- an icon, affix text -- does not stretch and
501
- reads as secondary. A nested `<button>` keeps its own styling. */
502
- & > :not(.ipt, .textarea, .select, .text-control, input, textarea, select, button) {
802
+ reads as secondary. A button, a link button, and any `<input>` keep their
803
+ own styling. Zero specificity: it is a default for unstyled content, and at
804
+ the 0-2-0 it had, a badge or a text utility on the adornment lost its
805
+ colour to this grey. */
806
+ :where(& > :not(.ipt, .textarea, .select, .text-control, input, textarea, select, button, .btn)) {
503
807
  @apply inline-flex flex-none items-center;
504
808
 
505
809
  color: var(--color-text-secondary);
506
810
  }
811
+
812
+ /* The group is destructive when its control is invalid. Carried here for an
813
+ applied group; the package's own keeps the plain rule above the utility. */
814
+ &:has(
815
+ > :is(.ipt, .textarea, .select, .text-control, input, textarea, select):where(
816
+ :not(
817
+ .checkbox,
818
+ .radio,
819
+ .switch,
820
+ .btn,
821
+ [type="checkbox"],
822
+ [type="radio"],
823
+ [type="button"],
824
+ [type="submit"],
825
+ [type="reset"],
826
+ [type="image"],
827
+ [type="range"],
828
+ [type="color"],
829
+ [type="file"],
830
+ [type="hidden"]
831
+ )
832
+ )[aria-invalid="true"]
833
+ ) {
834
+ --intent-color: var(--color-destructive);
835
+ --intent-contrast: var(--color-destructive-contrast);
836
+ --intent-hover: var(--color-destructive-hover);
837
+ --intent-border: var(--color-destructive);
838
+ --intent-strong: var(--color-destructive-strong);
839
+ --intent-subtle: var(--color-destructive-subtle);
840
+ }
507
841
  }
508
842
 
509
843
  /* The three toggles compose `text-control` for its bounds, not its chrome: the
@@ -523,6 +857,8 @@
523
857
  --_d-fill: 12%;
524
858
  --_d-fg-on-fill: 0%;
525
859
  --_fill-cap: 40%;
860
+ /* Absolute, whatever the element's own edge class: see `text-control`. */
861
+ --_edge-floor: 100%;
526
862
  --_line-rest: 100%;
527
863
  --_d-border: 100%;
528
864
  --_d-elevation: 0;
@@ -548,7 +884,9 @@
548
884
  rather than as an `outline`, so a clipping aesthetic clips it with
549
885
  everything else and its own inset focus layer still paints over the top. */
550
886
  &:focus-visible {
551
- --_shadow: var(--_shadow-parts) var(--_halo-layer,), 0 0 0 var(--focus-ring-width) var(--focus-ring);
887
+ --_shadow:
888
+ var(--_shadow-parts) var(--_halo-layer,),
889
+ 0 0 0 var(--focus-ring-width) var(--focus-ring, color-mix(in srgb, var(--color-primary) 64%, transparent));
552
890
  }
553
891
 
554
892
  &::after {
@@ -593,6 +931,33 @@
593
931
  transition-duration: 0.01ms;
594
932
  }
595
933
  }
934
+
935
+ /* The toggle's cap is its own (`--_fill-cap`), whatever the intent: a
936
+ neutral fill stops at 20% elsewhere because it is the page's ink, and a
937
+ neutral checkbox is the common case and has to look like a present,
938
+ interactive box. In the body so `@apply` carries it; it beats the intent
939
+ classes from here as it did as a plain rule in `utilities`. */
940
+ --intent-fill-max: 100%;
941
+
942
+ /* Forced colours, carried for an applied toggle. The package's own classes
943
+ also keep the unlayered copy at the foot of this file, which beats every
944
+ layered rule. */
945
+ @media (forced-colors: active) {
946
+ forced-color-adjust: none;
947
+ background-color: Canvas;
948
+ border-color: CanvasText;
949
+ transition: none;
950
+
951
+ &:checked,
952
+ &:indeterminate {
953
+ background-color: Highlight;
954
+ border-color: Highlight;
955
+
956
+ &::after {
957
+ background-color: HighlightText;
958
+ }
959
+ }
960
+ }
596
961
  }
597
962
 
598
963
  /* The same control with a round silhouette and a dot for its mark, and the
@@ -605,6 +970,8 @@
605
970
  --_d-fill: 12%;
606
971
  --_d-fg-on-fill: 0%;
607
972
  --_fill-cap: 40%;
973
+ /* Absolute, whatever the element's own edge class: see `text-control`. */
974
+ --_edge-floor: 100%;
608
975
  --_line-rest: 100%;
609
976
  --_d-border: 100%;
610
977
  --_d-elevation: 0;
@@ -619,7 +986,9 @@
619
986
 
620
987
  /* The ring, for the reason given on `checkbox`. */
621
988
  &:focus-visible {
622
- --_shadow: var(--_shadow-parts) var(--_halo-layer,), 0 0 0 var(--focus-ring-width) var(--focus-ring);
989
+ --_shadow:
990
+ var(--_shadow-parts) var(--_halo-layer,),
991
+ 0 0 0 var(--focus-ring-width) var(--focus-ring, color-mix(in srgb, var(--color-primary) 64%, transparent));
623
992
  }
624
993
 
625
994
  &::after {
@@ -660,6 +1029,32 @@
660
1029
  transition-duration: 0.01ms;
661
1030
  }
662
1031
  }
1032
+
1033
+ /* The toggle's cap is its own (`--_fill-cap`), whatever the intent: a
1034
+ neutral fill stops at 20% elsewhere because it is the page's ink, and a
1035
+ neutral checkbox is the common case and has to look like a present,
1036
+ interactive box. In the body so `@apply` carries it; it beats the intent
1037
+ classes from here as it did as a plain rule in `utilities`. */
1038
+ --intent-fill-max: 100%;
1039
+
1040
+ /* Forced colours, carried for an applied toggle. The package's own classes
1041
+ also keep the unlayered copy at the foot of this file, which beats every
1042
+ layered rule. */
1043
+ @media (forced-colors: active) {
1044
+ forced-color-adjust: none;
1045
+ background-color: Canvas;
1046
+ border-color: CanvasText;
1047
+ transition: none;
1048
+
1049
+ &:checked {
1050
+ background-color: Highlight;
1051
+ border-color: Highlight;
1052
+
1053
+ &::after {
1054
+ background-color: HighlightText;
1055
+ }
1056
+ }
1057
+ }
663
1058
  }
664
1059
 
665
1060
  /* A2: every dimension derives from the border width `box` resolved, so the knob
@@ -686,22 +1081,24 @@
686
1081
  a badge or a card stays under. */
687
1082
  --_fill-cap: 40%;
688
1083
 
689
- --switch-height: 1.25rem;
690
- --switch-width: 2.5rem;
691
- --switch-gap: 1px;
1084
+ /* The track's size is public, read with its default, so a value set on any
1085
+ ancestor reaches the switch; the knob is derived from it. */
1086
+ --_switch-height: var(--switch-height, 1.25rem);
1087
+ --_switch-width: var(--switch-width, 2.5rem);
1088
+ --_switch-gap: 1px;
692
1089
  /* The edge an aesthetic draws as an inset ring rather than as a border
693
1090
  (`.pixel`), present only while `--ui-shadow-edge` is declared -- the idiom
694
1091
  `box` uses. That ring paints under the knob, so a knob sized from the
695
1092
  border alone covered it: under pixel the border is zero, the knob met the
696
1093
  edge, and an unchecked and a checked switch both read as two halves. */
697
1094
  --_switch-ring: var(--ui-shadow-edge) var(--_ss);
698
- --_switch-offset: calc(var(--_switch-ring, 0px) + var(--switch-gap));
699
- --switch-inset: calc(var(--_width) + var(--_switch-offset));
700
- --switch-knob: calc(var(--switch-height) - var(--switch-inset) * 2);
701
- --switch-travel: calc(var(--switch-width) - var(--switch-inset) * 2 - var(--switch-knob));
1095
+ --_switch-offset: calc(var(--_switch-ring, 0px) + var(--_switch-gap));
1096
+ --_switch-inset: calc(var(--_width) + var(--_switch-offset));
1097
+ --_switch-knob: calc(var(--_switch-height) - var(--_switch-inset) * 2);
1098
+ --_switch-travel: calc(var(--_switch-width) - var(--_switch-inset) * 2 - var(--_switch-knob));
702
1099
 
703
- height: var(--switch-height);
704
- width: var(--switch-width);
1100
+ height: var(--_switch-height);
1101
+ width: var(--_switch-width);
705
1102
  /* Pill by default; a squaring aesthetic reaches the track and the knob
706
1103
  together, and an aesthetic with a pill corner of its own reaches both
707
1104
  through that first. */
@@ -715,7 +1112,9 @@
715
1112
 
716
1113
  /* The ring, for the reason given on `checkbox`. */
717
1114
  &:focus-visible {
718
- --_shadow: var(--_shadow-parts) var(--_halo-layer,), 0 0 0 var(--focus-ring-width) var(--focus-ring);
1115
+ --_shadow:
1116
+ var(--_shadow-parts) var(--_halo-layer,),
1117
+ 0 0 0 var(--focus-ring-width) var(--focus-ring, color-mix(in srgb, var(--color-primary) 64%, transparent));
719
1118
  }
720
1119
 
721
1120
  &::after {
@@ -731,8 +1130,8 @@
731
1130
  itself, so the two stay the same shape. */
732
1131
  corner-shape: var(--ui-corner-shape, round);
733
1132
  content: "";
734
- height: var(--switch-knob);
735
- width: var(--switch-knob);
1133
+ height: var(--_switch-knob);
1134
+ width: var(--_switch-knob);
736
1135
  top: var(--_switch-offset);
737
1136
  inset-inline-start: var(--_switch-offset);
738
1137
  }
@@ -742,14 +1141,14 @@
742
1141
  /* translate has no logical form, so the checked travel is named as a
743
1142
  physical x offset and mirrored under :dir(rtl) to travel toward the
744
1143
  opposite edge, matching the inset-inline-start resting position. */
745
- translate: var(--switch-travel);
1144
+ translate: var(--_switch-travel);
746
1145
  }
747
1146
 
748
1147
  /* Direction sits on the control, ahead of the pseudo-element: nested as
749
1148
  `:dir(rtl) &` inside `&:checked::after`, `&` carries `::after`, which
750
1149
  cannot appear inside `:is()`, and the emitted rule matched nothing. */
751
1150
  &:checked:dir(rtl)::after {
752
- translate: calc(-1 * var(--switch-travel));
1151
+ translate: calc(-1 * var(--_switch-travel));
753
1152
  }
754
1153
 
755
1154
  /* The mark appears without moving under reduced motion, for the entries
@@ -759,6 +1158,43 @@
759
1158
  transition-duration: 0.01ms;
760
1159
  }
761
1160
  }
1161
+
1162
+ /* `.ghost.edgeless` would leave the track neither a line nor a plate, so the
1163
+ floor is held up: three classes against the opt-out's two in
1164
+ `text-control`. Published unsupported as a distinct look. */
1165
+ &.ghost.edgeless {
1166
+ --_edge-floor: 100%;
1167
+ }
1168
+
1169
+ /* The toggle's cap is its own (`--_fill-cap`), whatever the intent: a
1170
+ neutral fill stops at 20% elsewhere because it is the page's ink, and a
1171
+ neutral checkbox is the common case and has to look like a present,
1172
+ interactive box. In the body so `@apply` carries it; it beats the intent
1173
+ classes from here as it did as a plain rule in `utilities`. */
1174
+ --intent-fill-max: 100%;
1175
+
1176
+ /* Forced colours, carried for an applied toggle. The package's own classes
1177
+ also keep the unlayered copy at the foot of this file, which beats every
1178
+ layered rule. */
1179
+ @media (forced-colors: active) {
1180
+ forced-color-adjust: none;
1181
+ background-color: Canvas;
1182
+ border-color: CanvasText;
1183
+ transition: none;
1184
+
1185
+ &::after {
1186
+ background-color: CanvasText;
1187
+ }
1188
+
1189
+ &:checked {
1190
+ background-color: Highlight;
1191
+ border-color: Highlight;
1192
+
1193
+ &::after {
1194
+ background-color: HighlightText;
1195
+ }
1196
+ }
1197
+ }
762
1198
  }
763
1199
 
764
1200
  @media (forced-colors: active) {
@@ -810,4 +1246,11 @@
810
1246
  outline-offset: 2px;
811
1247
  box-shadow: none;
812
1248
  }
1249
+
1250
+ /* The group draws the field's ring, so the control inside draws none here
1251
+ either: this block is unlayered, and the ring above beat the group's
1252
+ layered suppression and drew a second ring inside the group's. */
1253
+ .input-group > :is(.ipt, .textarea, .select, .text-control, input, textarea, select):focus-visible {
1254
+ outline: none;
1255
+ }
813
1256
  }