@codenhub/styles 0.4.0 → 0.5.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 (53) hide show
  1. package/README.md +1 -1
  2. package/dist/aesthetics/chunky-tile.css +1 -1
  3. package/dist/aesthetics/cyber.css +1 -1
  4. package/dist/aesthetics/glass.css +1 -1
  5. package/dist/aesthetics/index.css +1 -1
  6. package/dist/aesthetics/neobrutalism.css +1 -1
  7. package/dist/aesthetics/pixel.css +1 -1
  8. package/dist/aesthetics/sketch.css +1 -0
  9. package/dist/components.css +1 -1
  10. package/dist/index.css +1 -1
  11. package/dist/native.css +1 -1
  12. package/dist/palette.css +1 -1
  13. package/dist/theme.css +1 -1
  14. package/dist/tw/aesthetics/chunky-tile.css +142 -126
  15. package/dist/tw/aesthetics/cyber.css +132 -95
  16. package/dist/tw/aesthetics/glass.css +97 -56
  17. package/dist/tw/aesthetics/index.css +1 -0
  18. package/dist/tw/aesthetics/neobrutalism.css +99 -52
  19. package/dist/tw/aesthetics/pixel.css +127 -87
  20. package/dist/tw/aesthetics/sketch.css +326 -0
  21. package/dist/tw/box.css +100 -35
  22. package/dist/tw/components/button.css +40 -22
  23. package/dist/tw/components/feedback.css +29 -14
  24. package/dist/tw/components/form.css +151 -155
  25. package/dist/tw/components/index.css +15 -3
  26. package/dist/tw/components/loader.css +34 -25
  27. package/dist/tw/components/surface.css +28 -71
  28. package/dist/tw/components/tooltip.css +20 -10
  29. package/dist/tw/corner.css +26 -0
  30. package/dist/tw/elevation.css +8 -9
  31. package/dist/tw/intent.css +17 -9
  32. package/dist/tw/native.css +45 -39
  33. package/dist/tw/palette.css +452 -446
  34. package/dist/tw/presentation.css +9 -4
  35. package/dist/tw/surface.css +62 -0
  36. package/dist/tw/theme.css +106 -75
  37. package/dist/tw/utilities/content.css +25 -16
  38. package/dist/tw/utilities/layout.css +2 -2
  39. package/docs/accessibility.md +16 -16
  40. package/docs/changelog/0.5.0.md +38 -0
  41. package/docs/changelog/index.md +1 -0
  42. package/docs/index.md +3 -3
  43. package/docs/setup.md +23 -3
  44. package/docs/usage/aesthetics.md +56 -13
  45. package/docs/usage/buttons.md +13 -13
  46. package/docs/usage/composing.md +11 -11
  47. package/docs/usage/content-and-layout.md +13 -13
  48. package/docs/usage/customizing.md +90 -52
  49. package/docs/usage/feedback.md +1 -1
  50. package/docs/usage/forms.md +11 -11
  51. package/docs/usage/theming.md +1 -1
  52. package/llms-full.txt +274 -136
  53. package/package.json +12 -2
@@ -35,44 +35,12 @@
35
35
  derived from it and the two must not drift apart. Typed text is
36
36
  `--intent-color` over a wash of its own tone, past 10:1 in both themes at
37
37
  any value this cap is likely to take, so the bound still does what it was
38
- written for. */
38
+ written for.
39
+
40
+ The fill and the ink are `box`'s own, unrestated: its `--_capped` reads
41
+ this cap, so the plate and the ink ramp stay in step at every bound,
42
+ including the checked state below that lifts it. */
39
43
  --_fill-cap: 6%;
40
- /* The floor is zero everywhere except a checked toggle, which needs a ground
41
- under its mark. A container's `.ghost` would otherwise leave a checked
42
- checkbox as a tick floating on the page -- our own cascade producing the
43
- broken result, which is the test a bound has to pass. */
44
- --_fill-floor: 0%;
45
- --_fill: calc(
46
- clamp(
47
- var(--_fill-floor),
48
- min(var(--ui-fill, var(--_d-fill)), var(--_fill-cap), var(--intent-fill-max, 100%)),
49
- 100%
50
- ) *
51
- var(--ui-bg-alpha, 1)
52
- );
53
- /* The ink follows *this* control's fill, not `box`'s. `box` ramps against
54
- `--_capped`, which knows `--intent-fill-max` but not `--_fill-cap`, so on a
55
- control it would read a fill far higher than the one actually painted: a
56
- `.primary.solid` checkbox has `--ui-fill: 100%` and an unbounded intent, so
57
- `box` would call for the contrast tone while the plate sits at 20%. White
58
- tick on light grey.
59
-
60
- Ramping against `--_fill` keeps the two in step at every bound, including
61
- the checked state that lifts them.
62
-
63
- `--_fg-on-fill-floor` is the seam `:checked` pins instead of touching
64
- `--ui-fg-on-fill` directly. `.ghost`/`.soft` are unlayered CSS (P3, and see
65
- model.md's `--_fill-cap` history) and `text-control` compiles into
66
- Tailwind's `utilities` layer, so a layered `:checked` writing
67
- `--ui-fg-on-fill` the way it writes `--_fill-cap`/`--_fill-floor` would
68
- lose to `.ghost`'s unlayered `--ui-fg-on-fill: 0%` regardless of
69
- specificity -- the same trap `--_fill-cap` exists to avoid on the fill
70
- itself. A private floor `.ghost`/`.soft` never touch has nothing
71
- unlayered contesting it. */
72
- --_on-fill: max(
73
- var(--_fg-on-fill-floor, 0%),
74
- min(var(--ui-fg-on-fill, var(--_d-fg-on-fill)), max(0%, calc(var(--_fill) * 2 - 100%)))
75
- );
76
44
  /* The resting line is a fraction of the intent rather than all of it, and the
77
45
  fraction is what makes the pointer visible. Drawn at full strength a control
78
46
  already sits at its intent's darkest, and hover moves it to a neighbouring
@@ -87,9 +55,11 @@
87
55
  there is nothing else marking an unchecked box. */
88
56
  --_line-rest: 60%;
89
57
  /* Two seams rather than one composed result. `--_line-tone` is what colour the
90
- line is, and the states below move it; `--_line` folds in P3's blend toward
91
- the fill, exactly as `box` does it, so the two agree and a state never has to
92
- restate the blend to keep it.
58
+ line is, and the states below move it; `--_line` folds in the fill, walking
59
+ the line toward the intent colour by the fill amount, so a state never has
60
+ to restate that blend to keep it. It is a field's own form of P3 rather than
61
+ `box`'s: a field keeps its line whatever it is filled with, so the line walks
62
+ toward the fill's colour instead of fading out into it.
93
63
 
94
64
  Written as a single `--_line` that states overwrote, a hovered or focused
95
65
  control lost the blend along with the tone, because replacing a composed
@@ -133,14 +103,18 @@
133
103
  --_line-tone: var(--intent-color);
134
104
  }
135
105
 
136
- /* R8, P6: `:checked` pins the fill by making the floor and the cap the same
137
- value, so `clamp()` returns exactly that value whatever `--ui-fill` asks --
138
- still a bound on the input, still lifted rather than a rewrite of the
139
- result (the failure mode P6 exists for: an early version pinned `--_fill`
140
- and `--_fg` directly, which made a checked toggle ignore its own aesthetic
141
- and elevation along with presentation).
142
-
143
- The floor used to sit at `12%` rather than `100%`, which kept presentation
106
+ /* R8, P6: `:checked` is a state, so it re-declares the presentation inputs
107
+ -- a full fill and full contrast ink -- and lifts the cap that would bound
108
+ them, and `box` composes the rest. It does not write a result (the failure
109
+ mode P6 exists for: an early version pinned `--_fill` and `--_fg` directly,
110
+ which made a checked toggle ignore its own aesthetic and elevation along
111
+ with presentation). It sits in `utilities` at 0-2-0, above the fill
112
+ classes in `components`, so a checked toggle renders one plate whatever
113
+ presentation it or a container asks for, and a consumer's utility loses to
114
+ it because state wins over every choice (model.md#precedence). A toggle is
115
+ a void `<input>`, so the inputs it writes reach nothing but its own mark.
116
+
117
+ The fill used to be pinned at `12%` rather than `100%`, which kept presentation
144
118
  live on the checked mark too -- a checked `.soft` toggle rendered a pale
145
119
  plate with just the mark at full strength, the classic ring-and-dot radio.
146
120
  The stress-test pass found that cost more than it bought on a real screen:
@@ -154,20 +128,19 @@
154
128
  something. Lives here, not on the toggles, because a text input is never
155
129
  `:checked`.
156
130
 
157
- The ink is pinned alongside the fill, through `--_fg-on-fill-floor` rather
158
- than `--ui-fg-on-fill` directly (see that seam, above): the other half of
159
- what a fill class asks for is how much contrast ink it wants
160
- (`--ui-fg-on-fill: 0%` is `.ghost`/`.soft` declaring "none, my plate stays
161
- quiet"), and pinning the fill without pinning this left a checked
162
- `.ghost`/`.soft` toggle with an opaque plate and a mark that still read as
163
- though the plate were quiet -- both derived from the same intent tone,
164
- close enough to disappear into each other. `--_on-fill` ramps against the
165
- *composed* `--_fill`, so once that is pinned to 100% the ink has to reach
166
- full contrast too, or the ramp's own ceiling has nothing to reach. */
131
+ The ink is declared alongside the fill: the other half of what a fill
132
+ class asks for is how much contrast ink it wants (`--ui-fg-on-fill: 0%` is
133
+ `.ghost`/`.soft` declaring "none, my plate stays quiet"), and setting the
134
+ fill without this left a checked `.ghost`/`.soft` toggle with an opaque
135
+ plate and a mark that still read as though the plate were quiet -- both
136
+ derived from the same intent tone, close enough to disappear into each
137
+ other. `box` ramps the ink against the bounded fill before `--ui-bg-alpha`
138
+ thins it, so a checked toggle under `.glass` keeps full contrast ink on
139
+ its translucent plate. */
167
140
  &:checked {
141
+ --ui-fill: 100%;
142
+ --ui-fg-on-fill: 100%;
168
143
  --_fill-cap: 100%;
169
- --_fill-floor: 100%;
170
- --_fg-on-fill-floor: 100%;
171
144
  }
172
145
 
173
146
  /* Native decorations a field replaces with its own artwork. */
@@ -187,94 +160,104 @@
187
160
  }
188
161
  }
189
162
 
190
- /* A fill class on the element names its own cap; the 6% cascade cap in the
191
- utility is for a container reaching a field nobody classed. Both raise
192
- `--_fill-cap` rather than writing `--_fill` (P6), which is also what keeps
193
- `.soft` < `.solid` -- written as a rewrite the louder name rendered the
194
- quieter box, measured and inverted on `.ipt`, `.checkbox` and `.radio`.
195
- 20% clears `.soft`'s 12% and matches the plate a `.solid` badge draws.
196
-
197
- `.switch` is deliberately absent: its track caps at 40% in its own utility,
198
- and an unlayered rule here would overrule that. See model.md#the-bounds-that-survive-and-the-test-for-keeping-one. */
199
- :is(.text-control, .ipt, .textarea, .select, .input-group).soft {
200
- --_fill-cap: 12%;
201
- }
163
+ /* The component seams: rules that exist to beat their own component's
164
+ `@utility`, placed in `utilities` and winning there on specificity, each at
165
+ least 0-2-0 against the component's 0-1-0. They write only package privates
166
+ (`--_fill-cap`, `--_edge-floor`) or a state's intent slots, so nothing a
167
+ consumer writes competes with them. They used to be unlayered, which stopped
168
+ working once `/components` and `/native` put the components in `utilities`
169
+ too. See docs/internal/model.md#cascade-layers. */
170
+ @layer utilities {
171
+ /* A fill class on the element names its own cap; the 6% cascade cap in the
172
+ utility is for a container reaching a field nobody classed. Both raise
173
+ `--_fill-cap` rather than writing `--_fill` (P6), which is also what keeps
174
+ `.soft` < `.solid` -- written as a rewrite the louder name rendered the
175
+ quieter box, measured and inverted on `.ipt`, `.checkbox` and `.radio`.
176
+ 20% clears `.soft`'s 12% and matches the plate a `.solid` badge draws.
177
+
178
+ `.switch` is deliberately absent: its track caps at 40% in its own utility,
179
+ and a 0-2-0 rule here would overrule that. See model.md#the-bounds-that-survive-and-the-test-for-keeping-one. */
180
+ :is(.text-control, .ipt, .textarea, .select, .input-group).soft {
181
+ --_fill-cap: 12%;
182
+ }
202
183
 
203
- :is(.text-control, .ipt, .textarea, .select, .input-group).solid {
204
- --_fill-cap: 20%;
205
- }
184
+ :is(.text-control, .ipt, .textarea, .select, .input-group).solid {
185
+ --_fill-cap: 20%;
186
+ }
206
187
 
207
- /* A checked text-control fills with its own intent, so neutral has to drop its
208
- 20% cap here. No toggle needs this arm any more (see below), but a text
209
- input is never `:checked` in practice either -- kept for whatever custom
210
- `.text-control` composition does check itself. Declared where the intents
211
- are (0-2-0, unlayered): inside `text-control` `.neutral` would overrule it. */
212
- :is(.text-control, input):checked {
213
- --intent-fill-max: 100%;
214
- }
188
+ /* A checked text-control fills with its own intent, so neutral has to drop its
189
+ 20% cap here. No toggle needs this arm any more (see below), but a text
190
+ input is never `:checked` in practice either -- kept for whatever custom
191
+ `.text-control` composition does check itself. A plain rule at 0-2-0 in
192
+ `utilities`, above every intent class in `components`, so `.neutral` on the
193
+ element cannot put the cap back. */
194
+ :is(.text-control, input):checked {
195
+ --intent-fill-max: 100%;
196
+ }
215
197
 
216
- /* The three toggles read their own component cap (`--_fill-cap`, below) as the
217
- ceiling regardless of intent, checked or not -- not just at `:checked`, the
218
- way the arm above still works for a hypothetical checked text-control. A
219
- neutral fill stops at 20% everywhere else because it is the page's own ink,
220
- and a full one reads as a slab on a badge or a card. That reasoning does not
221
- hold for a toggle at rest: a neutral checkbox is the common case, most
222
- controls carry no intent class at all, and it has to look like a present,
223
- interactive box rather than a washed-out one just because nothing colored
224
- it. Declared where the intents are, for the reason given above. */
225
- :is(.checkbox, .radio, .switch) {
226
- --intent-fill-max: 100%;
227
- }
198
+ /* The three toggles read their own component cap (`--_fill-cap`, below) as the
199
+ ceiling regardless of intent, checked or not -- not just at `:checked`, the
200
+ way the arm above still works for a hypothetical checked text-control. A
201
+ neutral fill stops at 20% everywhere else because it is the page's own ink,
202
+ and a full one reads as a slab on a badge or a card. That reasoning does not
203
+ hold for a toggle at rest: a neutral checkbox is the common case, most
204
+ controls carry no intent class at all, and it has to look like a present,
205
+ interactive box rather than a washed-out one just because nothing colored
206
+ it. Declared where the intents are, for the reason given above. */
207
+ :is(.checkbox, .radio, .switch) {
208
+ --intent-fill-max: 100%;
209
+ }
228
210
 
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 absolute
233
- and `.edgeless` on them is published unsupported. `.input-group` is on it
234
- because the group is the field boundary now. */
235
- :is(.text-control, .ipt, .textarea, .select, .switch, .input-group).edgeless {
236
- --_edge-floor: 0%;
237
- }
211
+ /* The element's own `.edgeless` lowers the floor `text-control` raises -- a
212
+ class on the element is a consumer's request, a class on a container is a
213
+ cascade. `.checkbox` and `.radio` are absent: an unchecked one is a
214
+ transparent square/circle with nothing but its line, so the floor is absolute
215
+ and `.edgeless` on them is published unsupported. `.input-group` is on it
216
+ because the group is the field boundary now. */
217
+ :is(.text-control, .ipt, .textarea, .select, .switch, .input-group).edgeless {
218
+ --_edge-floor: 0%;
219
+ }
238
220
 
239
- /* `.switch`'s one exception to the opt-out above: `.ghost` already asks for
240
- zero fill, so a `.switch.ghost.edgeless` track has neither a line nor a
241
- plate and disappears entirely -- nothing marks its bounds but the floating
242
- knob. Every other fill on this component still has something to paint
243
- (`.soft`/`.solid`/default all rest above 0%), so only this one pairing
244
- needs the floor held up. `.switch.ghost.edgeless` renders identically to
245
- `.switch.ghost.edged` now, published unsupported as a distinct look --
246
- the same "produces a row indistinguishable from its neighbour" reason
247
- `.solid.edgeless`/`.solid.edged` already collapse to one row for. Higher
248
- specificity than the rule above (three classes, not an `:is()` of one)
249
- wins regardless of source order. */
250
- .switch.ghost.edgeless {
251
- --_edge-floor: 100%;
252
- }
221
+ /* `.switch`'s one exception to the opt-out above: `.ghost` already asks for
222
+ zero fill, so a `.switch.ghost.edgeless` track has neither a line nor a
223
+ plate and disappears entirely -- nothing marks its bounds but the floating
224
+ knob. Every other fill on this component still has something to paint
225
+ (`.soft`/`.solid`/default all rest above 0%), so only this one pairing
226
+ needs the floor held up. `.switch.ghost.edgeless` renders identically to
227
+ `.switch.ghost.edged` now, published unsupported as a distinct look --
228
+ the same "produces a row indistinguishable from its neighbour" reason
229
+ `.solid.edgeless`/`.solid.edged` already collapse to one row for. Higher
230
+ specificity than the rule above (three classes, not an `:is()` of one)
231
+ wins regardless of source order. */
232
+ .switch.ghost.edgeless {
233
+ --_edge-floor: 100%;
234
+ }
253
235
 
254
- /* An invalid control is destructive whatever else it was asked to be. A plain
255
- unlayered rule, not `&[aria-invalid]` inside `@utility`: the intent reset is
256
- unlayered too and would overrule a layered version (leaving a grey line and
257
- only the hint marking the error). `:is()` gives it 0-2-0 to outrank an intent
258
- class. `--intent-hover` is redirected with the others so the line does not go
259
- grey under the pointer. The `:has()` arm bridges an inner control's state to
260
- 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"],
274
- .input-group:has(> :is(.ipt, .textarea, .select, .text-control, input, textarea, select)[aria-invalid="true"]) {
275
- --intent-color: var(--color-destructive);
276
- --intent-hover: var(--color-destructive-hover);
277
- --intent-border: var(--color-destructive);
236
+ /* An invalid control is destructive whatever else it was asked to be. It sits
237
+ in `utilities`, above every intent class and both intent resets in
238
+ `components`, and `:is()` gives it 0-2-0 so it also outranks the control's
239
+ own utility; a consumer's utility setting an intent slot on an invalid field
240
+ loses to it too, because state wins over every choice (model.md#precedence). `--intent-hover` is redirected with the others so the line does not go
241
+ grey under the pointer. The `:has()` arm bridges an inner control's state to
242
+ an `.input-group`. */
243
+ :is(
244
+ .text-control,
245
+ .ipt,
246
+ .textarea,
247
+ .select,
248
+ .checkbox,
249
+ .radio,
250
+ .switch,
251
+ .input-group,
252
+ input,
253
+ textarea,
254
+ select
255
+ )[aria-invalid="true"],
256
+ .input-group:has(> :is(.ipt, .textarea, .select, .text-control, input, textarea, select)[aria-invalid="true"]) {
257
+ --intent-color: var(--color-destructive);
258
+ --intent-hover: var(--color-destructive-hover);
259
+ --intent-border: var(--color-destructive);
260
+ }
278
261
  }
279
262
 
280
263
  /* A ghost, edged, unelevated text control. */
@@ -316,7 +299,7 @@
316
299
  letter-spacing: 0.05em;
317
300
  line-height: 100%;
318
301
 
319
- &.error {
302
+ &.destructive {
320
303
  color: var(--intent-color);
321
304
  }
322
305
  }
@@ -363,6 +346,17 @@
363
346
  calc(100% - 0.6875rem) center;
364
347
  background-repeat: no-repeat;
365
348
  background-size: 0.3125rem 0.3125rem;
349
+
350
+ /* The open list is painted by the browser, over the select's own background
351
+ when it has one. A ghost select has none, so Chromium falls back to its
352
+ default popup -- white on Windows -- while the options inherit the select's
353
+ text, near-white in the dark theme: white on white. The options take the
354
+ page's ground and text instead, which follow the theme. */
355
+ & option,
356
+ & optgroup {
357
+ background-color: var(--color-background);
358
+ color: var(--color-text);
359
+ }
366
360
  }
367
361
 
368
362
  /* A wrapper that owns the field box so a control can carry adornments beside it
@@ -485,12 +479,12 @@
485
479
  --_d-border: 100%;
486
480
  --_d-elevation: 0;
487
481
 
488
- /* Capped at its own radius: a control radius is sized for a 40px control, and
489
- 14px of it on a 16px box renders as a circle, which is the one shape a
490
- checkbox must not be. A squaring aesthetic still reaches it, through the
491
- chip corner first -- `.cyber` squares chips outright, which is also what
492
- tells its checkbox from its diamond radio. */
493
- border-radius: min(var(--ui-radius-tight, var(--ui-radius, var(--radius-small))), var(--radius-small));
482
+ /* A chip keeps a small default even under a large control corner, but an
483
+ explicit tight radius is a complete shape, including elliptical corners. */
484
+ border-radius: var(
485
+ --ui-radius-tight,
486
+ min(var(--ui-radius, var(--ui-corner, var(--radius-small))), var(--radius-small))
487
+ );
494
488
 
495
489
  /* Chip geometry: the tight silhouette, and an inset edge no thicker than the
496
490
  line the aesthetic draws elsewhere. A 16px box cannot spend four pixels a
@@ -506,7 +500,7 @@
506
500
  rather than as an `outline`, so a clipping aesthetic clips it with
507
501
  everything else and its own inset focus layer still paints over the top. */
508
502
  &:focus-visible {
509
- --_shadow: var(--_shadow-parts), 0 0 0 var(--focus-ring-width) var(--focus-ring);
503
+ --_shadow: var(--_shadow-parts) var(--_halo-layer,), 0 0 0 var(--focus-ring-width) var(--focus-ring);
510
504
  }
511
505
 
512
506
  &::after {
@@ -531,7 +525,9 @@
531
525
  /* The same control with a round silhouette and a dot for its mark, and the
532
526
  same fill cap raise given on `checkbox`. */
533
527
  @utility radio {
534
- @apply text-control inline-grid size-4 cursor-pointer place-content-center rounded-full;
528
+ @apply text-control inline-grid size-4 cursor-pointer place-content-center;
529
+
530
+ border-radius: var(--ui-radius-pill, calc(infinity * 1px));
535
531
 
536
532
  --_d-fill: 12%;
537
533
  --_d-fg-on-fill: 0%;
@@ -550,7 +546,7 @@
550
546
 
551
547
  /* The ring, for the reason given on `checkbox`. */
552
548
  &:focus-visible {
553
- --_shadow: var(--_shadow-parts), 0 0 0 var(--focus-ring-width) var(--focus-ring);
549
+ --_shadow: var(--_shadow-parts) var(--_halo-layer,), 0 0 0 var(--focus-ring-width) var(--focus-ring);
554
550
  }
555
551
 
556
552
  &::after {
@@ -623,7 +619,7 @@
623
619
  /* Pill by default; a squaring aesthetic reaches the track and the knob
624
620
  together, and an aesthetic with a pill corner of its own reaches both
625
621
  through that first. */
626
- border-radius: var(--ui-radius-pill, var(--ui-radius, calc(infinity * 1px)));
622
+ border-radius: var(--ui-radius-pill, var(--ui-radius, var(--ui-corner, calc(infinity * 1px))));
627
623
 
628
624
  /* Chip geometry, for the reason given on `checkbox`: a 20px track has no more
629
625
  room for a thick inset edge than a 16px box does. */
@@ -633,7 +629,7 @@
633
629
 
634
630
  /* The ring, for the reason given on `checkbox`. */
635
631
  &:focus-visible {
636
- --_shadow: var(--_shadow-parts), 0 0 0 var(--focus-ring-width) var(--focus-ring);
632
+ --_shadow: var(--_shadow-parts) var(--_halo-layer,), 0 0 0 var(--focus-ring-width) var(--focus-ring);
637
633
  }
638
634
 
639
635
  &::after {
@@ -644,7 +640,7 @@
644
640
  tone as the label beside it. Checked, it takes the mark colour like the
645
641
  other two toggles. */
646
642
  background-color: var(--color-text-secondary);
647
- border-radius: var(--ui-radius-pill, var(--ui-radius, calc(infinity * 1px)));
643
+ border-radius: var(--ui-radius-pill, var(--ui-radius, var(--ui-corner, calc(infinity * 1px))));
648
644
  /* A pseudo-element is not a box: the knob takes the track's corner shape
649
645
  itself, so the two stay the same shape. */
650
646
  corner-shape: var(--ui-corner-shape, round);
@@ -1,8 +1,20 @@
1
+ /* The layer map, stated before any import uses a layer -- see `theme.css`.
2
+ Tailwind's utilities are imported into `utilities` the way
3
+ `tailwindcss/index.css` does it, so every component lands there on this
4
+ entrypoint and on `/native` too, exactly where `.` already puts them. Without
5
+ `layer(utilities)` the CLI emitted them unlayered here, and they beat every
6
+ consumer rule on these two entries alone. */
7
+ @layer theme, base, components, utilities;
8
+
9
+ /* The theme before `@reference "tailwindcss"`, as `native.css` has it:
10
+ Tailwind writes the theme's variables at the first `@theme` it meets, and
11
+ after the reference that is Tailwind's own reference-only one. See
12
+ docs/internal/model.md (Which files import the theme). */
13
+ @import "../theme.css";
1
14
  @reference "tailwindcss";
2
- @import "tailwindcss/utilities.css";
15
+ @import "tailwindcss/utilities.css" layer(utilities);
3
16
  /* Everything that is not `src/` -- see the note in `index.css`. Keeps a class
4
17
  name in a docs code fence from shipping as a real rule in `dist/components.css`. */
5
- @import "../theme.css";
6
18
  @import "../typography.css";
7
19
  @import "../box.css";
8
20
  @import "./button.css";
@@ -25,4 +37,4 @@
25
37
  makes the emission deliberate; the composition-body check in
26
38
  `exports.test.ts` fails the build if one is dropped. `text-control` and
27
39
  `surface` are also documented as consumer-usable on their own. */
28
- @source inline("box box-hover box-active loader-mask text-control surface input-group text-display text-title-lg text-title text-title-sm text-label-lg text-label text-body btn field label hint ipt email password url tel search date datetime month week time icon left right textarea select checkbox radio switch alert alert-icon badge skeleton progress tooltip tooltip-bubble tooltip-icon card panel interactive flush compact spacious neutral solid soft ghost edged edgeless flat raised floating loader");
40
+ @source inline("box box-hover box-active loader-mask text-control surface input-group text-display text-title-lg text-title text-title-sm text-label-lg text-label text-body btn field label hint ipt email password url tel search date datetime month week time icon left right textarea select checkbox radio switch alert alert-icon badge skeleton progress tooltip tooltip-bubble tooltip-icon card panel interactive flush neutral solid soft ghost edged edgeless flat raised floating loader");
@@ -1,10 +1,15 @@
1
- @reference "tailwindcss";
2
1
  @source inline("loader dots-wave dots-fade dots-queue dots-rotate dots-grow dots-grow-alternate dot-bounce bars-wave pulse-ring");
3
2
 
4
3
  /* The mask machinery, shared by the loader and by any component that shows a
5
4
  spinner of its own. Artwork is a mask rather than an image so it takes its
6
5
  colour from whatever paints behind it, which is the whole indicator contract:
7
- an intent class recolours the loader and nothing else has to know. */
6
+ an intent class recolours the loader and nothing else has to know.
7
+
8
+ Each artwork's closing `style` tag is percent-encoded. A stylesheet pasted
9
+ into an HTML `<style>` element ends at the first literal close tag, so one
10
+ left raw would silently drop every rule after it; the data URI decodes the
11
+ escape before the SVG is parsed, so the artwork is unchanged. The exports
12
+ test fails a build that ships the raw sequence. */
8
13
  @utility loader-mask {
9
14
  mask-image: var(--loader-art);
10
15
  -webkit-mask-image: var(--loader-art);
@@ -16,56 +21,60 @@
16
21
  -webkit-mask-position: center;
17
22
  }
18
23
 
19
- .loader:not(
20
- :where(
21
- .dots-wave,
22
- .dots-fade,
23
- .dots-queue,
24
- .dots-rotate,
25
- .dots-grow,
26
- .dots-grow-alternate,
27
- .dot-bounce,
28
- .bars-wave,
29
- .pulse-ring
30
- )
31
- ) {
32
- --loader-art: url("data:image/svg+xml;charset=utf-8,<svg stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.spinner_V8m1{transform-origin:center;animation:spinner_zKoa 2s linear infinite}.spinner_V8m1 circle{stroke-linecap:round;animation:spinner_YpZS 1.5s ease-in-out infinite}@keyframes spinner_zKoa{100%{transform:rotate(360deg)}}@keyframes spinner_YpZS{0%{stroke-dasharray:0 150;stroke-dashoffset:0}47.5%{stroke-dasharray:42 150;stroke-dashoffset:-16}95%,100%{stroke-dasharray:42 150;stroke-dashoffset:-59}}</style><g class='spinner_V8m1'><circle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'></circle></g></svg>");
24
+ /* In `utilities`, beside the loader and art utilities it defaults for -- see
25
+ the seams note in `form.css`. */
26
+ @layer utilities {
27
+ .loader:not(
28
+ :where(
29
+ .dots-wave,
30
+ .dots-fade,
31
+ .dots-queue,
32
+ .dots-rotate,
33
+ .dots-grow,
34
+ .dots-grow-alternate,
35
+ .dot-bounce,
36
+ .bars-wave,
37
+ .pulse-ring
38
+ )
39
+ ) {
40
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.spinner_V8m1{transform-origin:center;animation:spinner_zKoa 2s linear infinite}.spinner_V8m1 circle{stroke-linecap:round;animation:spinner_YpZS 1.5s ease-in-out infinite}@keyframes spinner_zKoa{100%{transform:rotate(360deg)}}@keyframes spinner_YpZS{0%{stroke-dasharray:0 150;stroke-dashoffset:0}47.5%{stroke-dasharray:42 150;stroke-dashoffset:-16}95%,100%{stroke-dasharray:42 150;stroke-dashoffset:-59}}%3C/style%3E<g class='spinner_V8m1'><circle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'></circle></g></svg>");
41
+ }
33
42
  }
34
43
 
35
44
  @utility dots-wave {
36
- --loader-art: url("data:image/svg+xml;charset=utf-8,<svg fill='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_8HQG{0%,57.14%{animation-timing-function:cubic-bezier(.33,.66,.66,1);transform:translate(0)}28.57%{animation-timing-function:cubic-bezier(.33,0,.66,.33);transform:translateY(-6px)}to{transform:translate(0)}}.spinner_qM83{animation:spinner_8HQG 1.05s infinite}</style><circle class='spinner_qM83' cx='4' cy='12' r='3'/><circle class='spinner_qM83' cx='12' cy='12' r='3' style='animation-delay:.1s'/><circle class='spinner_qM83' cx='20' cy='12' r='3' style='animation-delay:.2s'/></svg>");
45
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg fill='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_8HQG{0%,57.14%{animation-timing-function:cubic-bezier(.33,.66,.66,1);transform:translate(0)}28.57%{animation-timing-function:cubic-bezier(.33,0,.66,.33);transform:translateY(-6px)}to{transform:translate(0)}}.spinner_qM83{animation:spinner_8HQG 1.05s infinite}%3C/style%3E<circle class='spinner_qM83' cx='4' cy='12' r='3'/><circle class='spinner_qM83' cx='12' cy='12' r='3' style='animation-delay:.1s'/><circle class='spinner_qM83' cx='20' cy='12' r='3' style='animation-delay:.2s'/></svg>");
37
46
  }
38
47
 
39
48
  @utility dots-fade {
40
- --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.dot{animation:dot-anim 1s ease-in-out infinite;fill:currentColor}.d1{animation-delay:-0.32s}.d2{animation-delay:-0.16s}@keyframes dot-anim{0%,80%,100%{transform:scale(0.6);opacity:0.2}40%{transform:scale(1);opacity:1}}</style><circle class='dot d1' cx='4' cy='12' r='3' transform-origin='4px 12px'></circle><circle class='dot d2' cx='12' cy='12' r='3' transform-origin='12px 12px'></circle><circle class='dot d3' cx='20' cy='12' r='3' transform-origin='20px 12px'></circle></svg>");
49
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.dot{animation:dot-anim 1s ease-in-out infinite;fill:currentColor}.d1{animation-delay:-0.32s}.d2{animation-delay:-0.16s}@keyframes dot-anim{0%,80%,100%{transform:scale(0.6);opacity:0.2}40%{transform:scale(1);opacity:1}}%3C/style%3E<circle class='dot d1' cx='4' cy='12' r='3' transform-origin='4px 12px'></circle><circle class='dot d2' cx='12' cy='12' r='3' transform-origin='12px 12px'></circle><circle class='dot d3' cx='20' cy='12' r='3' transform-origin='20px 12px'></circle></svg>");
41
50
  }
42
51
 
43
52
  @utility dots-queue {
44
- --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_qtyZ{0%{r:0}25%{r:3px;cx:4px}50%{r:3px;cx:12px}75%{r:3px;cx:20px}to{r:0;cx:20px}}.spinner_nOfF{animation:spinner_qtyZ 2s cubic-bezier(.36,.6,.31,1) infinite}</style><circle class='spinner_nOfF' cx='4' cy='12' r='3'/><circle class='spinner_nOfF' cx='4' cy='12' r='3' style='animation-delay:-.5s'/><circle class='spinner_nOfF' cx='4' cy='12' r='3' style='animation-delay:-1s'/><circle class='spinner_nOfF' cx='4' cy='12' r='3' style='animation-delay:-1.5s'/></svg>");
53
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_qtyZ{0%{r:0}25%{r:3px;cx:4px}50%{r:3px;cx:12px}75%{r:3px;cx:20px}to{r:0;cx:20px}}.spinner_nOfF{animation:spinner_qtyZ 2s cubic-bezier(.36,.6,.31,1) infinite}%3C/style%3E<circle class='spinner_nOfF' cx='4' cy='12' r='3'/><circle class='spinner_nOfF' cx='4' cy='12' r='3' style='animation-delay:-.5s'/><circle class='spinner_nOfF' cx='4' cy='12' r='3' style='animation-delay:-1s'/><circle class='spinner_nOfF' cx='4' cy='12' r='3' style='animation-delay:-1.5s'/></svg>");
45
54
  }
46
55
 
47
56
  @utility dots-rotate {
48
- --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_XVY9{50%{transform:rotate(180deg)}to{transform:rotate(360deg)}}</style><circle cx='12' cy='12' r='3'/><g style='transform-origin:center;animation:spinner_XVY9 1s cubic-bezier(.36,.6,.31,1) infinite'><circle cx='4' cy='12' r='3'/><circle cx='20' cy='12' r='3'/></g></svg>");
57
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_XVY9{50%{transform:rotate(180deg)}to{transform:rotate(360deg)}}%3C/style%3E<circle cx='12' cy='12' r='3'/><g style='transform-origin:center;animation:spinner_XVY9 1s cubic-bezier(.36,.6,.31,1) infinite'><circle cx='4' cy='12' r='3'/><circle cx='20' cy='12' r='3'/></g></svg>");
49
58
  }
50
59
 
51
60
  @utility dots-grow {
52
- --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_xe7Q{93.75%,to{r:3px}46.875%{r:.2px}}.spinner_b2T7{animation:spinner_xe7Q .8s linear infinite}</style><circle class='spinner_b2T7' cx='4' cy='12' r='3'/><circle class='spinner_b2T7' cx='12' cy='12' r='3' style='animation-delay:-.65s'/><circle class='spinner_b2T7' cx='20' cy='12' r='3' style='animation-delay:-.5s'/></svg>");
61
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_xe7Q{93.75%,to{r:3px}46.875%{r:.2px}}.spinner_b2T7{animation:spinner_xe7Q .8s linear infinite}%3C/style%3E<circle class='spinner_b2T7' cx='4' cy='12' r='3'/><circle class='spinner_b2T7' cx='12' cy='12' r='3' style='animation-delay:-.65s'/><circle class='spinner_b2T7' cx='20' cy='12' r='3' style='animation-delay:-.5s'/></svg>");
53
62
  }
54
63
 
55
64
  @utility dots-grow-alternate {
56
- --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_qhi1{0%,to{r:1.5px}50%{r:3px}}.spinner_I8Q1{animation:spinner_qhi1 .75s linear infinite}</style><circle class='spinner_I8Q1' cx='4' cy='12' r='1.5'/><circle class='spinner_I8Q1' cx='12' cy='12' r='3' style='animation-delay:-.375s'/><circle class='spinner_I8Q1' cx='20' cy='12' r='1.5'/></svg>");
65
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_qhi1{0%,to{r:1.5px}50%{r:3px}}.spinner_I8Q1{animation:spinner_qhi1 .75s linear infinite}%3C/style%3E<circle class='spinner_I8Q1' cx='4' cy='12' r='1.5'/><circle class='spinner_I8Q1' cx='12' cy='12' r='3' style='animation-delay:-.375s'/><circle class='spinner_I8Q1' cx='20' cy='12' r='1.5'/></svg>");
57
66
  }
58
67
 
59
68
  @utility dot-bounce {
60
- --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_YeBj{0%{animation-timing-function:cubic-bezier(.33,0,.66,.33);cy:5px}46.875%{cy:20px;rx:4px;ry:4px}50%{animation-timing-function:cubic-bezier(.33,.66,.66,1);cy:20.5px;rx:4.8px;ry:3px}53.125%{rx:4px;ry:4px}to{cy:5px}}</style><circle cx='12' cy='5' style='animation:spinner_YeBj .8s infinite' r='4'/></svg>");
69
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_YeBj{0%{animation-timing-function:cubic-bezier(.33,0,.66,.33);cy:5px}46.875%{cy:20px;rx:4px;ry:4px}50%{animation-timing-function:cubic-bezier(.33,.66,.66,1);cy:20.5px;rx:4.8px;ry:3px}53.125%{rx:4px;ry:4px}to{cy:5px}}%3C/style%3E<circle cx='12' cy='5' style='animation:spinner_YeBj .8s infinite' r='4'/></svg>");
61
70
  }
62
71
 
63
72
  @utility bars-wave {
64
- --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.bar{animation:bar-anim 1s ease-in-out infinite;fill:currentColor}.b1{animation-delay:-0.4s}.b2{animation-delay:-0.2s}@keyframes bar-anim{0%,100%{transform:scaleY(0.3)}50%{transform:scaleY(1)}}</style><rect class='bar b1' x='3' y='2' width='4' height='20' rx='2' transform-origin='5px 12px'></rect><rect class='bar b2' x='10' y='2' width='4' height='20' rx='2' transform-origin='12px 12px'></rect><rect class='bar b3' x='17' y='2' width='4' height='20' rx='2' transform-origin='19px 12px'></rect></svg>");
73
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.bar{animation:bar-anim 1s ease-in-out infinite;fill:currentColor}.b1{animation-delay:-0.4s}.b2{animation-delay:-0.2s}@keyframes bar-anim{0%,100%{transform:scaleY(0.3)}50%{transform:scaleY(1)}}%3C/style%3E<rect class='bar b1' x='3' y='2' width='4' height='20' rx='2' transform-origin='5px 12px'></rect><rect class='bar b2' x='10' y='2' width='4' height='20' rx='2' transform-origin='12px 12px'></rect><rect class='bar b3' x='17' y='2' width='4' height='20' rx='2' transform-origin='19px 12px'></rect></svg>");
65
74
  }
66
75
 
67
76
  @utility pulse-ring {
68
- --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.pulse{animation:pulse-anim 1.2s cubic-bezier(0, 0.2, 0.8, 1) infinite;stroke:currentColor;transform-origin:center}.p2{animation-delay:-0.6s}@keyframes pulse-anim{0%{transform:scale(0);opacity:1}100%{transform:scale(1);opacity:0}}</style><circle class='pulse p1' cx='12' cy='12' r='10' fill='none' stroke-width='3'></circle><circle class='pulse p2' cx='12' cy='12' r='10' fill='none' stroke-width='3'></circle></svg>");
77
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.pulse{animation:pulse-anim 1.2s cubic-bezier(0, 0.2, 0.8, 1) infinite;stroke:currentColor;transform-origin:center}.p2{animation-delay:-0.6s}@keyframes pulse-anim{0%{transform:scale(0);opacity:1}100%{transform:scale(1);opacity:0}}%3C/style%3E<circle class='pulse p1' cx='12' cy='12' r='10' fill='none' stroke-width='3'></circle><circle class='pulse p2' cx='12' cy='12' r='10' fill='none' stroke-width='3'></circle></svg>");
69
78
  }
70
79
 
71
80
  /* A loader takes a colour and its own geometry, and nothing else. No fill, no