@codenhub/styles 0.6.0 → 0.7.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 (113) hide show
  1. package/NOTICE +18 -16
  2. package/README.es.md +61 -0
  3. package/README.md +21 -16
  4. package/README.pt-BR.md +61 -0
  5. package/dist/aesthetics/chunky-tile.css +1 -1
  6. package/dist/aesthetics/cyber.css +1 -1
  7. package/dist/aesthetics/glass.css +1 -1
  8. package/dist/aesthetics/index.css +2 -2
  9. package/dist/aesthetics/neobrutalism.css +1 -1
  10. package/dist/aesthetics/pixel.css +1 -1
  11. package/dist/aesthetics/sketch.css +1 -1
  12. package/dist/components.css +2 -2
  13. package/dist/index.css +2 -2
  14. package/dist/native.css +2 -2
  15. package/dist/palette.css +1 -1
  16. package/dist/theme.css +2 -2
  17. package/dist/tw/aesthetics/chunky-tile.css +81 -23
  18. package/dist/tw/aesthetics/cyber.css +86 -29
  19. package/dist/tw/aesthetics/glass.css +177 -50
  20. package/dist/tw/aesthetics/neobrutalism.css +98 -22
  21. package/dist/tw/aesthetics/pixel.css +93 -8
  22. package/dist/tw/aesthetics/sketch.css +87 -22
  23. package/dist/tw/box.css +21 -2
  24. package/dist/tw/components/feedback.css +2 -2
  25. package/dist/tw/components/form.css +58 -19
  26. package/dist/tw/components/index.css +3 -4
  27. package/dist/tw/components/surface.css +15 -6
  28. package/dist/tw/index.css +1 -1
  29. package/dist/tw/intent.css +6 -5
  30. package/dist/tw/native.css +8 -8
  31. package/dist/tw/palette.css +6 -6
  32. package/dist/tw/reset.css +15 -15
  33. package/dist/tw/theme.css +29 -17
  34. package/dist/tw/utilities/content.css +43 -1
  35. package/dist/tw/utilities/layout.css +26 -12
  36. package/docs/accessibility.md +89 -60
  37. package/docs/changelog/0.1.1.md +1 -0
  38. package/docs/changelog/0.2.0.md +1 -0
  39. package/docs/changelog/0.3.0.md +1 -0
  40. package/docs/changelog/0.4.0.md +1 -0
  41. package/docs/changelog/0.5.0.md +8 -3
  42. package/docs/changelog/0.6.0.md +2 -1
  43. package/docs/changelog/0.7.0.md +50 -0
  44. package/docs/changelog/index.md +1 -0
  45. package/docs/concepts.md +188 -46
  46. package/docs/es/accessibility.md +150 -0
  47. package/docs/es/concepts.md +212 -0
  48. package/docs/es/index.md +104 -0
  49. package/docs/es/integrating/astro.md +35 -0
  50. package/docs/es/integrating/index.md +47 -0
  51. package/docs/es/integrating/nextjs.md +67 -0
  52. package/docs/es/integrating/svelte.md +45 -0
  53. package/docs/es/integrating/tailwind.md +78 -0
  54. package/docs/es/integrating/vue.md +31 -0
  55. package/docs/es/integrating/without-a-bundler.md +115 -0
  56. package/docs/es/setup.md +180 -0
  57. package/docs/es/usage/aesthetics.md +276 -0
  58. package/docs/es/usage/buttons.md +131 -0
  59. package/docs/es/usage/composing.md +164 -0
  60. package/docs/es/usage/content-and-layout.md +215 -0
  61. package/docs/es/usage/customizing.md +298 -0
  62. package/docs/es/usage/feedback.md +153 -0
  63. package/docs/es/usage/forms.md +231 -0
  64. package/docs/es/usage/index.md +29 -0
  65. package/docs/es/usage/solo.md +84 -0
  66. package/docs/es/usage/theming.md +296 -0
  67. package/docs/es/usage/tooltips.md +127 -0
  68. package/docs/index.md +85 -26
  69. package/docs/integrating/astro.md +3 -4
  70. package/docs/integrating/index.md +18 -13
  71. package/docs/integrating/nextjs.md +3 -4
  72. package/docs/integrating/svelte.md +5 -4
  73. package/docs/integrating/tailwind.md +16 -3
  74. package/docs/integrating/vue.md +3 -4
  75. package/docs/integrating/without-a-bundler.md +114 -0
  76. package/docs/pt-BR/accessibility.md +150 -0
  77. package/docs/pt-BR/concepts.md +212 -0
  78. package/docs/pt-BR/index.md +104 -0
  79. package/docs/pt-BR/integrating/astro.md +35 -0
  80. package/docs/pt-BR/integrating/index.md +47 -0
  81. package/docs/pt-BR/integrating/nextjs.md +67 -0
  82. package/docs/pt-BR/integrating/svelte.md +45 -0
  83. package/docs/pt-BR/integrating/tailwind.md +78 -0
  84. package/docs/pt-BR/integrating/vue.md +31 -0
  85. package/docs/pt-BR/integrating/without-a-bundler.md +115 -0
  86. package/docs/pt-BR/setup.md +180 -0
  87. package/docs/pt-BR/usage/aesthetics.md +276 -0
  88. package/docs/pt-BR/usage/buttons.md +131 -0
  89. package/docs/pt-BR/usage/composing.md +164 -0
  90. package/docs/pt-BR/usage/content-and-layout.md +215 -0
  91. package/docs/pt-BR/usage/customizing.md +298 -0
  92. package/docs/pt-BR/usage/feedback.md +153 -0
  93. package/docs/pt-BR/usage/forms.md +231 -0
  94. package/docs/pt-BR/usage/index.md +29 -0
  95. package/docs/pt-BR/usage/solo.md +84 -0
  96. package/docs/pt-BR/usage/theming.md +296 -0
  97. package/docs/pt-BR/usage/tooltips.md +127 -0
  98. package/docs/setup.md +134 -91
  99. package/docs/usage/aesthetics.md +76 -66
  100. package/docs/usage/buttons.md +108 -40
  101. package/docs/usage/composing.md +105 -92
  102. package/docs/usage/content-and-layout.md +157 -126
  103. package/docs/usage/customizing.md +107 -37
  104. package/docs/usage/feedback.md +134 -71
  105. package/docs/usage/forms.md +176 -107
  106. package/docs/usage/index.md +18 -14
  107. package/docs/usage/solo.md +83 -0
  108. package/docs/usage/theming.md +260 -118
  109. package/docs/usage/tooltips.md +90 -49
  110. package/llms-full.txt +2358 -1313
  111. package/llms.txt +24 -22
  112. package/package.json +25 -32
  113. package/dist/tw/components/loader.css +0 -116
@@ -154,12 +154,20 @@
154
154
 
155
155
  The corner knobs are glass's own, with larger defaults: a continuous corner
156
156
  reads smaller than a round one of the same radius. Where `corner-shape` is
157
- not drawn the corners stay round. */
158
- .glass-liquid {
157
+ not drawn the corners stay round.
158
+
159
+ Not on a solo element: `.glass-solo.glass-liquid` paints its own corner
160
+ below, and a token declared on it would inherit into whatever it holds --
161
+ a `.btn` inside a solo toast took liquid's corners on a page with no glass
162
+ in scope. Every scope below skips a solo element for the same reason. */
163
+ .glass-liquid:not(.glass-solo) {
159
164
  --ui-corner: var(--glass-radius, 1rem);
160
165
  --ui-corner-surface: var(--glass-radius-surface, 1.5rem);
161
166
  --ui-corner-shape: squircle;
167
+ }
162
168
 
169
+ /* The layers' values, on a solo element too: it draws the same two. */
170
+ .glass-liquid {
163
171
  --_glass-liquid-blur: blur(2px) saturate(1.8);
164
172
 
165
173
  /* The lens is its own layer rather than a second function in the blur's
@@ -177,6 +185,12 @@
177
185
  property, so the band and the strength are fixed, not knobs. */
178
186
  --_glass-liquid-lens: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='lens' x='0' y='0' width='100%' height='100%' color-interpolation-filters='sRGB'><feFlood flood-color='white'/><feMorphology operator='erode' radius='8'/><feGaussianBlur stdDeviation='10' result='ramp'/><feOffset in='ramp' dx='-3' result='l'/><feOffset in='ramp' dx='3' result='r'/><feComposite in='l' in2='r' operator='arithmetic' k2='2' k3='-2' k4='0.5' result='dx'/><feOffset in='ramp' dy='-3' result='u'/><feOffset in='ramp' dy='3' result='d'/><feComposite in='u' in2='d' operator='arithmetic' k2='2' k3='-2' k4='0.5' result='dy'/><feColorMatrix in='dx' values='0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1' result='x'/><feColorMatrix in='dy' values='0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1'/><feComposite in='x' operator='arithmetic' k2='1' k3='1'/><feDisplacementMap in='SourceGraphic' scale='110' xChannelSelector='R' yChannelSelector='G'/></filter></svg>#lens");
179
187
 
188
+ /* The same lens at a control's size. The pane's band is 8px of erosion
189
+ under a 10px blur, which is most of a 40px button's height: the two
190
+ sides' ramps meet in the middle and the label sits in a loop. A third of
191
+ each length keeps a clear centre for the label. */
192
+ --_glass-liquid-lens-control: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='lens' x='0' y='0' width='100%' height='100%' color-interpolation-filters='sRGB'><feFlood flood-color='white'/><feMorphology operator='erode' radius='3'/><feGaussianBlur stdDeviation='3.5' result='ramp'/><feOffset in='ramp' dx='-1' result='l'/><feOffset in='ramp' dx='1' result='r'/><feComposite in='l' in2='r' operator='arithmetic' k2='2' k3='-2' k4='0.5' result='dx'/><feOffset in='ramp' dy='-1' result='u'/><feOffset in='ramp' dy='1' result='d'/><feComposite in='u' in2='d' operator='arithmetic' k2='2' k3='-2' k4='0.5' result='dy'/><feColorMatrix in='dx' values='0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1' result='x'/><feColorMatrix in='dy' values='0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1'/><feComposite in='x' operator='arithmetic' k2='1' k3='1'/><feDisplacementMap in='SourceGraphic' scale='40' xChannelSelector='R' yChannelSelector='G'/></filter></svg>#lens");
193
+
180
194
  /* The rim: light caught on the top-left edge and, fainter, the bottom-right,
181
195
  over a soft inner glow. Drawn on the lens layer, the topmost, because a
182
196
  rim drawn on the surface itself would sit under the layers and blur. */
@@ -192,57 +206,77 @@
192
206
  }
193
207
 
194
208
  /* The glass material the modifier changes, where glass is in scope: on the
195
- same element, or on a surface inside a glass region. The blur moves off the
196
- surface onto a layer behind its content (below): a surface with a backdrop
197
- filter of its own is a backdrop root, and a layer inside one sees only the
198
- surface, never the page behind it. */
199
- .glass.glass-liquid,
200
- .glass .glass-liquid {
209
+ same element, or on a surface inside a glass region.
210
+
211
+ The backdrop is liquid's lighter blur, for whatever frosts under glass and
212
+ takes no layers below: a bare `<table>` under `/native`, a consumer's own
213
+ `@apply surface`. The layers' selector list cannot name those, and with
214
+ the backdrop cleared here they kept the clearer ground with nothing
215
+ blurred behind it. A surface that does take the layers clears its own. */
216
+ .glass.glass-liquid:not(.glass-solo),
217
+ .glass .glass-liquid:not(.glass-solo) {
201
218
  --ui-ink: light-dark(rgb(255 255 255 / 0.45), rgb(255 255 255 / 0.16));
202
219
  --ui-surface-ground: color-mix(in srgb, var(--color-background) 30%, transparent);
203
220
  --ui-surface-image: initial;
204
- --ui-backdrop: none;
221
+ --ui-backdrop: var(--_glass-liquid-blur);
205
222
  }
206
223
 
207
224
  @media (prefers-reduced-transparency: reduce) {
208
- .glass.glass-liquid,
209
- .glass .glass-liquid {
225
+ .glass.glass-liquid:not(.glass-solo),
226
+ .glass .glass-liquid:not(.glass-solo) {
210
227
  --ui-surface-ground: var(--color-background);
211
228
  }
212
229
 
213
230
  .glass-liquid {
214
231
  --_glass-liquid-blur: none;
215
232
  --_glass-liquid-lens: none;
233
+ --_glass-liquid-lens-control: none;
216
234
  }
217
235
  }
218
236
 
219
237
  /* The layers: a treatment no token reaches, so a selector list glass owns and
220
- the registry records. Two pseudo-elements behind a surface's content, the
221
- blur beneath and the lens with the rim and sheen above it. `isolation`
222
- keeps them above the surface's own plate, which they then blur with the
223
- page; `position` anchors them, and a component that positions itself --
224
- the tooltip bubble -- keeps its own. A default or `.soft` table frosts
225
- under glass, so it takes the layers too. Both slots are spent, so a surface
226
- inside a liquid region carries no `::before` or `::after` of the
227
- consumer's; the fields cannot host one, and controls stay solid as they do
228
- under glass.
229
-
230
- A region of another aesthetic nested inside, or of plain glass, keeps its
231
- own material and takes no layers. This list names the other aesthetics, and
232
- `registry.test.ts` fails when one is missing. */
233
- :is(.glass-liquid, .glass-liquid *):is(.glass, .glass *):is(
234
- .card,
235
- .panel,
236
- .alert,
237
- .tooltip-bubble,
238
- .surface,
239
- .data-table:not(.solid, .ghost)
240
- ):not(
241
- .glass-liquid :is(.neobrutalism, .pixel, .chunky-tile, .cyber, .sketch),
242
- .glass-liquid :is(.neobrutalism, .pixel, .chunky-tile, .cyber, .sketch) *,
243
- .glass-liquid .glass:not(.glass-liquid),
244
- .glass-liquid .glass:not(.glass-liquid) *
245
- ) {
238
+ the registry records. Two pseudo-elements behind the content, the blur
239
+ beneath and the lens with the rim and sheen above it. `isolation` keeps
240
+ them above the element's own plate, which they then blur with the page;
241
+ `position` anchors them, and a component that positions itself -- the
242
+ tooltip bubble -- keeps its own. A default or `.soft` table frosts under
243
+ glass, so it takes the layers too. Both slots are spent, so a host carries
244
+ no `::before` or `::after` of the consumer's; the fields cannot host one.
245
+
246
+ The blur is on the layer and not on the host: an element with a backdrop
247
+ filter of its own is a backdrop root, and a layer inside one sees only that
248
+ element, never the page behind it. So a host clears the region's backdrop.
249
+
250
+ Surfaces take the layers from a liquid scope. A region of another
251
+ aesthetic nested inside, or of plain glass, keeps its own material and
252
+ takes none; this list names the other aesthetics, and `registry.test.ts`
253
+ fails when one is missing.
254
+
255
+ A button takes them only when it carries `.glass-liquid` itself, in a
256
+ glass scope. Controls stay solid under a liquid region, for the reason
257
+ glass gives -- two composited layers and a filter under every control of a
258
+ dense cluster -- and a class on the one button that is meant to be a pane
259
+ is a request rather than a cascade, the idiom `text-control` uses for
260
+ `.edgeless`. Its fill is still presentation's: `.solid` stays an opaque
261
+ plate under the rim, and `.soft` and `.ghost` are the clear panes. A
262
+ disabled one is dimmed by `opacity`, which makes it a backdrop root, so it
263
+ keeps the ground and the rim and bends nothing. */
264
+ :is(.glass-liquid:not(.glass-solo), .glass-liquid:not(.glass-solo) *):is(.glass, .glass *):is(
265
+ .card,
266
+ .panel,
267
+ .alert,
268
+ .tooltip-bubble,
269
+ .surface,
270
+ .data-table:not(.solid, .ghost)
271
+ ):not(
272
+ .glass-liquid :is(.neobrutalism, .pixel, .chunky-tile, .cyber, .sketch),
273
+ .glass-liquid :is(.neobrutalism, .pixel, .chunky-tile, .cyber, .sketch) *,
274
+ .glass-liquid .glass:not(.glass-liquid),
275
+ .glass-liquid .glass:not(.glass-liquid) *
276
+ ),
277
+ .btn.glass-liquid:is(.glass, .glass *) {
278
+ --ui-backdrop: none;
279
+
246
280
  position: relative;
247
281
  isolation: isolate;
248
282
 
@@ -269,6 +303,28 @@
269
303
  box-shadow: var(--_glass-liquid-rim);
270
304
  }
271
305
  }
306
+
307
+ /* A button's lens is the control one. Swapped as the value the layer reads
308
+ rather than on the layer: the list above outranks any selector that names
309
+ the button's `::after` alone. */
310
+ .btn.glass-liquid:is(.glass, .glass *) {
311
+ --_glass-liquid-lens: var(--_glass-liquid-lens-control);
312
+ }
313
+ }
314
+
315
+ /* A liquid button rests on liquid's ground rather than on nothing, so a
316
+ `.ghost` or `.soft` one is a pane and not a hole in the page. It reads the
317
+ surface ground the modifier set on it, so reduced transparency and a
318
+ consumer's own `--ui-surface-ground` reach it as they reach a card.
319
+
320
+ In `utilities`, not with the layers: it has to beat `box`'s own
321
+ `--_d-ground: transparent`, which it does there on specificity, and it
322
+ writes only a private nothing a consumer sets. See
323
+ docs/internal/model.md#cascade-layers. */
324
+ @layer utilities {
325
+ .btn.glass-liquid:is(.glass, .glass *) {
326
+ --_d-ground: var(--ui-surface-ground, transparent);
327
+ }
272
328
  }
273
329
 
274
330
  /* Forced colours paint the plain surface: no layer blurs, bends, or lights it. */
@@ -276,6 +332,7 @@
276
332
  .glass-liquid {
277
333
  --_glass-liquid-blur: none;
278
334
  --_glass-liquid-lens: none;
335
+ --_glass-liquid-lens-control: none;
279
336
  --_glass-liquid-rim: none;
280
337
  --_glass-liquid-sheen: none;
281
338
  }
@@ -286,24 +343,63 @@
286
343
  `<div>` on a page that never loaded the base stylesheet. See
287
344
  docs/internal/model.md#solo-classes.
288
345
 
289
- It reads the aesthetic's own knobs and the theme's foundation tokens, each
290
- with its shipped literal as the fallback, and never a `--ui-*`, `--intent-*`,
291
- or `--elevation-color` token: those belong to whichever aesthetic an ancestor
292
- carries, and a solo element has to look the same inside any of them.
346
+ It composes with what the element is asked to be, the way a component does:
347
+ the fill and edge a presentation class asks for, on the element or cascaded
348
+ from a container; the intent class on the element; the element's own
349
+ elevation class. Each is read with the unclassed look as its fallback, so
350
+ with none of them, or with no theme loaded, it paints what it always did.
351
+
352
+ Material is the exception. It reads the aesthetic's own knobs and the
353
+ theme's foundation tokens, each with its shipped literal as the fallback,
354
+ and never a material `--ui-*` or `--elevation-color` token: those belong to
355
+ whichever aesthetic an ancestor carries, and a solo element has to be this
356
+ aesthetic inside any of them.
357
+
293
358
  Unlayered and one class deep, so it beats a foreign component's
294
359
  zero-specificity rules wherever they load. Not for this package's own
295
- components, which take the aesthetic class instead. */
360
+ components, which take the aesthetic class instead.
361
+
362
+ The other five paint the fill as an inset layer over whatever the element
363
+ already is, and write no background, so an unclassed solo element keeps its
364
+ own. Glass's translucent ground is its material, so it owns the background
365
+ and mixes the fill over that ground. */
296
366
  .glass-solo {
297
- background-color: color-mix(
367
+ /* The composition, the same in every solo class; `registry.test.ts` holds
368
+ the copies together. See the note on `.glass-solo`. */
369
+ --_solo-intent-color: initial;
370
+ --_solo-intent-contrast: initial;
371
+ --_solo-intent-strong: initial;
372
+ --_solo-intent-border: initial;
373
+ --_solo-intent-fill-max: initial;
374
+ --_solo-color: var(--_solo-intent-color, var(--color-text, light-dark(oklch(14.5% 0 0), oklch(98.5% 0 0))));
375
+ --_solo-contrast: var(
376
+ --_solo-intent-contrast,
377
+ var(--color-text-contrast, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)))
378
+ );
379
+ --_solo-strong: var(--_solo-intent-strong, var(--color-text-strong, light-dark(oklch(20.5% 0 0), oklch(97% 0 0))));
380
+ --_solo-capped: min(var(--ui-fill, 0%), var(--_solo-intent-fill-max, 20%));
381
+ --_solo-fill: color-mix(in oklab, var(--_solo-color) var(--_solo-capped), transparent);
382
+ --_solo-on-fill: min(var(--ui-fg-on-fill, 0%), max(0%, calc(var(--_solo-capped) * 2 - 100%)));
383
+ --_solo-fg: color-mix(in oklab, var(--_solo-contrast) var(--_solo-on-fill), var(--_solo-strong));
384
+ /* The line with no intent on the element: this aesthetic's own ink. */
385
+ --_solo-ink: var(--_solo-intent-border, light-dark(rgb(255 255 255 / 0.65), rgb(255 255 255 / 0.22)));
386
+ --_solo-ground: color-mix(
298
387
  in srgb,
299
388
  var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0))) 45%,
300
389
  transparent
301
390
  );
302
- border: 1px solid light-dark(rgb(255 255 255 / 0.65), rgb(255 255 255 / 0.22));
391
+ /* The plate runs under the border, so the line fades to nothing, as `box`'s
392
+ does, rather than to the fill. */
393
+ --_solo-edge: color-mix(in oklab, var(--_solo-ink) var(--ui-border, 100%), transparent);
394
+
395
+ background-color: color-mix(in oklab, var(--_solo-color) var(--_solo-capped), var(--_solo-ground));
396
+ border: 1px solid var(--_solo-edge);
303
397
  /* The surface corner: the element this exists for is a pane. */
304
398
  border-radius: var(--glass-radius-surface, 1rem);
305
399
  /* The theme's depth colour as a literal, for the reason above: inside a
306
- `.chunky-tile` region the token is black. */
400
+ `.chunky-tile` region the token is black. A complete shadow, so the
401
+ elevation classes do not reach it, as they do not under the aesthetic
402
+ class. */
307
403
  box-shadow:
308
404
  0 1px 2px -1px light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55)),
309
405
  0 18px 40px -16px light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55));
@@ -313,9 +409,39 @@
313
409
  backdrop-filter: blur(14px) saturate(1.6);
314
410
  }
315
411
 
412
+ /* An intent class on the element colours it, as on a component. Named rather
413
+ than read from `--intent-*` alone: those slots inherit, so a `.success`
414
+ container would colour a solo element where it leaves a component neutral. */
415
+ .glass-solo:is(.primary, .secondary, .success, .warning, .destructive, .info) {
416
+ --_solo-intent-color: var(--intent-color);
417
+ --_solo-intent-contrast: var(--intent-contrast);
418
+ --_solo-intent-strong: var(--intent-strong);
419
+ --_solo-intent-border: var(--intent-border);
420
+ --_solo-intent-fill-max: var(--intent-fill-max);
421
+ }
422
+
423
+ /* The text colour is written only where a fill or an intent is asked for, on
424
+ the element or by a container's fill class. Anywhere else the element keeps
425
+ its own: a dark toast keeps its light text. */
426
+ .glass-solo:is(
427
+ .solid,
428
+ .soft,
429
+ .ghost,
430
+ .primary,
431
+ .secondary,
432
+ .success,
433
+ .warning,
434
+ .destructive,
435
+ .info,
436
+ :is(.solid, .soft, .ghost) *
437
+ ) {
438
+ color: var(--_solo-fg);
439
+ }
440
+
316
441
  @media (prefers-reduced-transparency: reduce) {
317
442
  .glass-solo {
318
- background-color: var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)));
443
+ --_solo-ground: var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)));
444
+
319
445
  -webkit-backdrop-filter: none;
320
446
  backdrop-filter: none;
321
447
  }
@@ -336,13 +462,14 @@
336
462
  }
337
463
 
338
464
  .glass-solo.glass-liquid {
339
- isolation: isolate;
340
- background-color: color-mix(
465
+ --_solo-ground: color-mix(
341
466
  in srgb,
342
467
  var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0))) 30%,
343
468
  transparent
344
469
  );
345
- border-color: light-dark(rgb(255 255 255 / 0.45), rgb(255 255 255 / 0.16));
470
+ --_solo-ink: var(--_solo-intent-border, light-dark(rgb(255 255 255 / 0.45), rgb(255 255 255 / 0.16)));
471
+
472
+ isolation: isolate;
346
473
  border-radius: var(--glass-radius-surface, 1.5rem);
347
474
  corner-shape: squircle;
348
475
  -webkit-backdrop-filter: none;
@@ -373,6 +500,6 @@
373
500
 
374
501
  @media (prefers-reduced-transparency: reduce) {
375
502
  .glass-solo.glass-liquid {
376
- background-color: var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)));
503
+ --_solo-ground: var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)));
377
504
  }
378
505
  }
@@ -120,48 +120,108 @@
120
120
 
121
121
  In `utilities`, not with the token class: it has to beat `@utility alert`'s
122
122
  own `--_d-elevation: 0`, which it does there at 0-2-0, and it writes only a
123
- private nothing a consumer sets. */
123
+ private nothing a consumer sets.
124
+
125
+ Bounded by the nearest aesthetic region: an alert inside another aesthetic
126
+ nested in a neobrutalist region rests as that aesthetic rests it, and one in
127
+ a neobrutalist region nested again inside that takes the slab back. A
128
+ `:not(.neobrutalism :is(...) *)` exclusion cannot tell those two apart, since
129
+ the re-entered alert also sits below the other aesthetic, so each region
130
+ writes a private flag instead and inheritance hands the alert the nearest
131
+ one's. The slab is the alert's own resting default when the flag is unset. */
124
132
  @layer utilities {
133
+ .neobrutalism {
134
+ --_neo-alert-elevation: 1;
135
+ }
136
+
137
+ :is(.glass, .pixel, .chunky-tile, .cyber, .sketch) {
138
+ --_neo-alert-elevation: initial;
139
+ }
140
+
125
141
  .neobrutalism .alert {
126
- --_d-elevation: 1;
142
+ --_d-elevation: var(--_neo-alert-elevation, 0);
127
143
  }
128
144
  }
129
145
 
130
146
  /* Solo: the same look painted onto the one element carrying the class, for an
131
147
  element the package does not style. See docs/internal/model.md#solo-classes and
132
- the note on `.glass-solo`: own knobs and foundation tokens with literal
133
- fallbacks, never a shared `--ui-*`, `--intent-*`, or `--elevation-color`
134
- token, unlayered, and not for this package's own components. */
148
+ the note on `.glass-solo`: it composes with the element's presentation,
149
+ intent, and elevation, and reads its material from its own knobs and
150
+ foundation tokens with literal fallbacks, never a shared material `--ui-*`
151
+ or `--elevation-color` token. Unlayered, and not for this package's own
152
+ components. */
135
153
  .neobrutalism-solo {
136
154
  --_neo-offset: var(--neo-offset, 4px);
137
- --_neo-ink: light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0)));
155
+ /* The composition, the same in every solo class; `registry.test.ts` holds
156
+ the copies together. See the note on `.glass-solo`. */
157
+ --_solo-intent-color: initial;
158
+ --_solo-intent-contrast: initial;
159
+ --_solo-intent-strong: initial;
160
+ --_solo-intent-border: initial;
161
+ --_solo-intent-fill-max: initial;
162
+ --_solo-color: var(--_solo-intent-color, var(--color-text, light-dark(oklch(14.5% 0 0), oklch(98.5% 0 0))));
163
+ --_solo-contrast: var(
164
+ --_solo-intent-contrast,
165
+ var(--color-text-contrast, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)))
166
+ );
167
+ --_solo-strong: var(--_solo-intent-strong, var(--color-text-strong, light-dark(oklch(20.5% 0 0), oklch(97% 0 0))));
168
+ --_solo-capped: min(var(--ui-fill, 0%), var(--_solo-intent-fill-max, 20%));
169
+ --_solo-fill: color-mix(in oklab, var(--_solo-color) var(--_solo-capped), transparent);
170
+ --_solo-on-fill: min(var(--ui-fg-on-fill, 0%), max(0%, calc(var(--_solo-capped) * 2 - 100%)));
171
+ --_solo-fg: color-mix(in oklab, var(--_solo-contrast) var(--_solo-on-fill), var(--_solo-strong));
172
+ /* The line with no intent on the element: this aesthetic's own ink. */
173
+ --_solo-ink: var(
174
+ --_solo-intent-border,
175
+ light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0)))
176
+ );
177
+ --_solo-edge: color-mix(in oklab, var(--_solo-ink) var(--ui-border, 100%), var(--_solo-fill));
178
+ /* The slab, scaled by the element's own elevation class: `.flat` takes it off
179
+ and `.floating` doubles it, and the press travels the same distance. */
180
+ --_solo-depth: calc(var(--_neo-offset) * var(--ui-elevation, 1));
138
181
 
139
- border: 2px solid var(--_neo-ink);
182
+ border: 2px solid var(--_solo-edge);
140
183
  border-radius: 0;
141
- box-shadow: var(--_neo-offset) var(--_neo-offset) 0 0 var(--_neo-ink);
184
+ box-shadow:
185
+ var(--_solo-depth) var(--_solo-depth) 0 0 var(--_solo-ink),
186
+ inset 0 0 0 100vmax var(--_solo-fill);
142
187
  transition:
143
188
  box-shadow var(--motion-duration-normal, 200ms) var(--motion-ease, cubic-bezier(0.2, 0, 0, 1)),
144
189
  transform var(--motion-duration-normal, 200ms) var(--motion-ease, cubic-bezier(0.2, 0, 0, 1));
145
190
  }
146
191
 
192
+ /* An intent class on the element colours it, as on a component. Named rather
193
+ than read from `--intent-*` alone: those slots inherit, so a `.success`
194
+ container would colour a solo element where it leaves a component neutral. */
195
+ .neobrutalism-solo:is(.primary, .secondary, .success, .warning, .destructive, .info) {
196
+ --_solo-intent-color: var(--intent-color);
197
+ --_solo-intent-contrast: var(--intent-contrast);
198
+ --_solo-intent-strong: var(--intent-strong);
199
+ --_solo-intent-border: var(--intent-border);
200
+ --_solo-intent-fill-max: var(--intent-fill-max);
201
+ }
202
+
203
+ /* The text colour is written only where a fill or an intent is asked for, on
204
+ the element or by a container's fill class. Anywhere else the element keeps
205
+ its own: a dark toast keeps its light text. */
206
+ .neobrutalism-solo:is(
207
+ .solid,
208
+ .soft,
209
+ .ghost,
210
+ .primary,
211
+ .secondary,
212
+ .success,
213
+ .warning,
214
+ .destructive,
215
+ .info,
216
+ :is(.solid, .soft, .ghost) *
217
+ ) {
218
+ color: var(--_solo-fg);
219
+ }
220
+
147
221
  /* The press, only on something pressed: an action, not disabled. A solo
148
222
  container -- a toast clicked to dismiss -- stays put, the way only `.btn` and
149
223
  an opted-in card press under the aesthetic class. */
150
224
  .neobrutalism-solo:is(
151
- button,
152
- a[href],
153
- [role="button"],
154
- summary,
155
- input[type="button"],
156
- input[type="submit"],
157
- input[type="reset"]
158
- ):active:not(:disabled, [aria-disabled="true"]) {
159
- box-shadow: 0 0 0 0 var(--_neo-ink);
160
- transform: translate(var(--_neo-offset), var(--_neo-offset));
161
- }
162
-
163
- @media (prefers-reduced-motion: reduce) {
164
- .neobrutalism-solo:is(
165
225
  button,
166
226
  a[href],
167
227
  [role="button"],
@@ -170,6 +230,22 @@
170
230
  input[type="submit"],
171
231
  input[type="reset"]
172
232
  ):active:not(:disabled, [aria-disabled="true"]) {
233
+ box-shadow:
234
+ 0 0 0 0 var(--_solo-ink),
235
+ inset 0 0 0 100vmax var(--_solo-fill);
236
+ transform: translate(var(--_solo-depth), var(--_solo-depth));
237
+ }
238
+
239
+ @media (prefers-reduced-motion: reduce) {
240
+ .neobrutalism-solo:is(
241
+ button,
242
+ a[href],
243
+ [role="button"],
244
+ summary,
245
+ input[type="button"],
246
+ input[type="submit"],
247
+ input[type="reset"]
248
+ ):active:not(:disabled, [aria-disabled="true"]) {
173
249
  transform: none;
174
250
  }
175
251
  }
@@ -147,18 +147,46 @@
147
147
 
148
148
  /* Solo: the same look painted onto the one element carrying the class, for an
149
149
  element the package does not style. See docs/internal/model.md#solo-classes and
150
- the note on `.glass-solo`: own knobs and foundation tokens with literal
151
- fallbacks, never a shared `--ui-*`, `--intent-*`, or `--elevation-color`
152
- token, unlayered, and not for this package's own components. */
150
+ the note on `.glass-solo`: it composes with the element's presentation,
151
+ intent, and elevation, and reads its material from its own knobs and
152
+ foundation tokens with literal fallbacks, never a shared material `--ui-*`
153
+ or `--elevation-color` token. Unlayered, and not for this package's own
154
+ components. */
153
155
  .pixel-solo {
154
156
  --_pixel-unit: var(--pixel-unit, 4px);
155
- --_pixel-ink: light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0)));
157
+ /* The composition, the same in every solo class; `registry.test.ts` holds
158
+ the copies together. See the note on `.glass-solo`. */
159
+ --_solo-intent-color: initial;
160
+ --_solo-intent-contrast: initial;
161
+ --_solo-intent-strong: initial;
162
+ --_solo-intent-border: initial;
163
+ --_solo-intent-fill-max: initial;
164
+ --_solo-color: var(--_solo-intent-color, var(--color-text, light-dark(oklch(14.5% 0 0), oklch(98.5% 0 0))));
165
+ --_solo-contrast: var(
166
+ --_solo-intent-contrast,
167
+ var(--color-text-contrast, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)))
168
+ );
169
+ --_solo-strong: var(--_solo-intent-strong, var(--color-text-strong, light-dark(oklch(20.5% 0 0), oklch(97% 0 0))));
170
+ --_solo-capped: min(var(--ui-fill, 0%), var(--_solo-intent-fill-max, 20%));
171
+ --_solo-fill: color-mix(in oklab, var(--_solo-color) var(--_solo-capped), transparent);
172
+ --_solo-on-fill: min(var(--ui-fg-on-fill, 0%), max(0%, calc(var(--_solo-capped) * 2 - 100%)));
173
+ --_solo-fg: color-mix(in oklab, var(--_solo-contrast) var(--_solo-on-fill), var(--_solo-strong));
174
+ /* The line with no intent on the element: this aesthetic's own ink. */
175
+ --_solo-ink: var(
176
+ --_solo-intent-border,
177
+ light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0)))
178
+ );
179
+ /* The ring is the edge, and the fill is a layer under it rather than a plate
180
+ beneath a border, so the line fades to nothing instead of to the fill. */
181
+ --_solo-edge: color-mix(in oklab, var(--_solo-ink) var(--ui-border, 100%), transparent);
156
182
 
157
183
  /* The clip removes a border at the straight sides, so the ring draws the
158
184
  edge and the element's own border goes. */
159
185
  border: 0;
160
186
  border-radius: 0;
161
- box-shadow: inset 0 0 0 var(--_pixel-unit) var(--_pixel-ink);
187
+ box-shadow:
188
+ inset 0 0 0 var(--_pixel-unit) var(--_solo-edge),
189
+ inset 0 0 0 100vmax var(--_solo-fill);
162
190
  clip-path: polygon(
163
191
  0 var(--_pixel-unit),
164
192
  var(--_pixel-unit) var(--_pixel-unit),
@@ -176,11 +204,68 @@
176
204
  font-family: var(--font-pixel, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
177
205
  }
178
206
 
207
+ /* An intent class on the element colours it, as on a component. Named rather
208
+ than read from `--intent-*` alone: those slots inherit, so a `.success`
209
+ container would colour a solo element where it leaves a component neutral. */
210
+ .pixel-solo:is(.primary, .secondary, .success, .warning, .destructive, .info) {
211
+ --_solo-intent-color: var(--intent-color);
212
+ --_solo-intent-contrast: var(--intent-contrast);
213
+ --_solo-intent-strong: var(--intent-strong);
214
+ --_solo-intent-border: var(--intent-border);
215
+ --_solo-intent-fill-max: var(--intent-fill-max);
216
+ }
217
+
218
+ /* The text colour is written only where a fill or an intent is asked for, on
219
+ the element or by a container's fill class. Anywhere else the element keeps
220
+ its own: a dark toast keeps its light text. */
221
+ .pixel-solo:is(
222
+ .solid,
223
+ .soft,
224
+ .ghost,
225
+ .primary,
226
+ .secondary,
227
+ .success,
228
+ .warning,
229
+ .destructive,
230
+ .info,
231
+ :is(.solid, .soft, .ghost) *
232
+ ) {
233
+ color: var(--_solo-fg);
234
+ }
235
+
179
236
  /* The clip takes the focus outline too, so focus is a second inset layer under
180
- the ring, wider than it so it shows -- shadows paint first-on-top. */
237
+ the ring, wider than it so it shows -- shadows paint first-on-top. A line of
238
+ the page colour sits inside that one, as in `box`: the ring is the ink, and
239
+ on an element filled with the ink it would not show. */
181
240
  .pixel-solo:focus-visible {
182
241
  box-shadow:
183
- inset 0 0 0 var(--_pixel-unit) var(--_pixel-ink),
242
+ inset 0 0 0 var(--_pixel-unit) var(--_solo-edge),
184
243
  inset 0 0 0 calc(var(--_pixel-unit) + var(--focus-ring-width, 3px))
185
- var(--focus-ring, color-mix(in srgb, var(--_pixel-ink) 64%, transparent));
244
+ var(
245
+ --focus-ring,
246
+ color-mix(
247
+ in srgb,
248
+ light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0))) 64%,
249
+ transparent
250
+ )
251
+ ),
252
+ inset 0 0 0 calc(var(--_pixel-unit) + var(--focus-ring-width, 3px) + 2px)
253
+ var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0))),
254
+ inset 0 0 0 100vmax var(--_solo-fill);
255
+ }
256
+
257
+ /* Forced colours remove every box shadow, and the ring is one: the element
258
+ would have no edge at all. An outline stands in for it, drawn inward so
259
+ nothing moves, and the clip goes so the focus outline can show. This
260
+ entrypoint may be the only package stylesheet loaded, so it says so itself. */
261
+ @media (forced-colors: active) {
262
+ .pixel-solo {
263
+ outline: var(--_pixel-unit) solid CanvasText;
264
+ outline-offset: calc(-1 * var(--_pixel-unit));
265
+ clip-path: none;
266
+ }
267
+
268
+ .pixel-solo:focus-visible {
269
+ outline-color: Highlight;
270
+ }
186
271
  }