@codenhub/styles 0.6.0 → 0.8.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 (151) hide show
  1. package/NOTICE +18 -16
  2. package/README.es.md +62 -0
  3. package/README.md +28 -22
  4. package/README.pt-BR.md +62 -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/cli.js +189 -0
  13. package/dist/components.css +2 -2
  14. package/dist/index.css +2 -2
  15. package/dist/index.d.ts +1 -0
  16. package/dist/native.css +2 -2
  17. package/dist/palette.css +1 -1
  18. package/dist/theme.css +2 -2
  19. package/docs/accessibility.md +108 -60
  20. package/docs/changelog/0.1.1.md +1 -0
  21. package/docs/changelog/0.2.0.md +1 -0
  22. package/docs/changelog/0.3.0.md +1 -0
  23. package/docs/changelog/0.4.0.md +1 -0
  24. package/docs/changelog/0.5.0.md +8 -3
  25. package/docs/changelog/0.6.0.md +2 -1
  26. package/docs/changelog/0.7.0.md +50 -0
  27. package/docs/changelog/0.8.0.md +70 -0
  28. package/docs/changelog/index.md +2 -0
  29. package/docs/concepts.md +188 -46
  30. package/docs/es/accessibility.md +169 -0
  31. package/docs/es/concepts.md +212 -0
  32. package/docs/es/index.md +105 -0
  33. package/docs/es/integrating/astro.md +35 -0
  34. package/docs/es/integrating/index.md +75 -0
  35. package/docs/es/integrating/nextjs.md +67 -0
  36. package/docs/es/integrating/svelte.md +45 -0
  37. package/docs/es/integrating/tailwind.md +148 -0
  38. package/docs/es/integrating/vue.md +31 -0
  39. package/docs/es/integrating/without-a-bundler.md +238 -0
  40. package/docs/es/setup.md +188 -0
  41. package/docs/es/usage/aesthetics.md +282 -0
  42. package/docs/es/usage/buttons.md +135 -0
  43. package/docs/es/usage/composing.md +168 -0
  44. package/docs/es/usage/content-and-layout.md +219 -0
  45. package/docs/es/usage/customizing.md +304 -0
  46. package/docs/es/usage/feedback.md +157 -0
  47. package/docs/es/usage/forms.md +256 -0
  48. package/docs/es/usage/index.md +33 -0
  49. package/docs/es/usage/presets.md +92 -0
  50. package/docs/es/usage/solo.md +86 -0
  51. package/docs/es/usage/theming.md +309 -0
  52. package/docs/es/usage/tooltips.md +127 -0
  53. package/docs/index.md +86 -26
  54. package/docs/integrating/astro.md +3 -4
  55. package/docs/integrating/index.md +46 -13
  56. package/docs/integrating/nextjs.md +3 -4
  57. package/docs/integrating/svelte.md +5 -4
  58. package/docs/integrating/tailwind.md +89 -6
  59. package/docs/integrating/vue.md +3 -4
  60. package/docs/integrating/without-a-bundler.md +237 -0
  61. package/docs/pt-BR/accessibility.md +169 -0
  62. package/docs/pt-BR/concepts.md +212 -0
  63. package/docs/pt-BR/index.md +105 -0
  64. package/docs/pt-BR/integrating/astro.md +35 -0
  65. package/docs/pt-BR/integrating/index.md +75 -0
  66. package/docs/pt-BR/integrating/nextjs.md +67 -0
  67. package/docs/pt-BR/integrating/svelte.md +45 -0
  68. package/docs/pt-BR/integrating/tailwind.md +148 -0
  69. package/docs/pt-BR/integrating/vue.md +31 -0
  70. package/docs/pt-BR/integrating/without-a-bundler.md +238 -0
  71. package/docs/pt-BR/setup.md +188 -0
  72. package/docs/pt-BR/usage/aesthetics.md +282 -0
  73. package/docs/pt-BR/usage/buttons.md +135 -0
  74. package/docs/pt-BR/usage/composing.md +168 -0
  75. package/docs/pt-BR/usage/content-and-layout.md +219 -0
  76. package/docs/pt-BR/usage/customizing.md +304 -0
  77. package/docs/pt-BR/usage/feedback.md +157 -0
  78. package/docs/pt-BR/usage/forms.md +256 -0
  79. package/docs/pt-BR/usage/index.md +33 -0
  80. package/docs/pt-BR/usage/presets.md +92 -0
  81. package/docs/pt-BR/usage/solo.md +86 -0
  82. package/docs/pt-BR/usage/theming.md +309 -0
  83. package/docs/pt-BR/usage/tooltips.md +127 -0
  84. package/docs/setup.md +142 -91
  85. package/docs/usage/aesthetics.md +89 -73
  86. package/docs/usage/buttons.md +112 -40
  87. package/docs/usage/composing.md +109 -92
  88. package/docs/usage/content-and-layout.md +160 -125
  89. package/docs/usage/customizing.md +117 -41
  90. package/docs/usage/feedback.md +138 -71
  91. package/docs/usage/forms.md +201 -107
  92. package/docs/usage/index.md +22 -14
  93. package/docs/usage/presets.md +91 -0
  94. package/docs/usage/solo.md +85 -0
  95. package/docs/usage/theming.md +273 -118
  96. package/docs/usage/tooltips.md +90 -49
  97. package/llms-full.txt +2820 -1298
  98. package/llms.txt +24 -22
  99. package/package.json +134 -91
  100. package/{dist/tw → tw}/aesthetics/chunky-tile.css +107 -29
  101. package/{dist/tw → tw}/aesthetics/cyber.css +88 -40
  102. package/{dist/tw → tw}/aesthetics/glass.css +257 -61
  103. package/{dist/tw → tw}/aesthetics/neobrutalism.css +113 -20
  104. package/{dist/tw → tw}/aesthetics/pixel.css +93 -8
  105. package/tw/aesthetics/sketch.css +417 -0
  106. package/{dist/tw → tw}/box.css +105 -9
  107. package/tw/components/button.css +11 -0
  108. package/tw/components/button.rules.css +187 -0
  109. package/tw/components/feedback.css +11 -0
  110. package/{dist/tw/components/feedback.css → tw/components/feedback.rules.css} +136 -32
  111. package/tw/components/form.css +11 -0
  112. package/{dist/tw/components/form.css → tw/components/form.rules.css} +577 -125
  113. package/tw/components/index.css +16 -0
  114. package/tw/components/rules.css +41 -0
  115. package/tw/components/surface.css +11 -0
  116. package/tw/components/surface.rules.css +222 -0
  117. package/tw/components/tooltip.css +11 -0
  118. package/{dist/tw/components/tooltip.css → tw/components/tooltip.rules.css} +43 -27
  119. package/{dist/tw → tw}/corner.css +18 -12
  120. package/{dist/tw → tw}/elevation.css +23 -17
  121. package/{dist/tw → tw}/index.css +12 -10
  122. package/tw/index.d.ts +1 -0
  123. package/tw/intent.css +312 -0
  124. package/tw/modifiers.css +127 -0
  125. package/tw/native.css +320 -0
  126. package/tw/presentation.css +139 -0
  127. package/tw/reset.css +10 -0
  128. package/{dist/tw/reset.css → tw/reset.rules.css} +21 -22
  129. package/tw/size.css +34 -0
  130. package/tw/surface.css +9 -0
  131. package/tw/surface.rules.css +212 -0
  132. package/{dist/tw → tw}/theme.css +121 -45
  133. package/tw/typography.css +8 -0
  134. package/{dist/tw/typography.css → tw/typography.rules.css} +0 -3
  135. package/{dist/tw → tw}/utilities/content.css +212 -50
  136. package/tw/utilities/index.css +11 -0
  137. package/tw/utilities/layout.css +208 -0
  138. package/tw/utilities/rules.css +8 -0
  139. package/dist/tw/aesthetics/sketch.css +0 -333
  140. package/dist/tw/components/button.css +0 -139
  141. package/dist/tw/components/index.css +0 -36
  142. package/dist/tw/components/loader.css +0 -116
  143. package/dist/tw/components/surface.css +0 -175
  144. package/dist/tw/intent.css +0 -228
  145. package/dist/tw/native.css +0 -203
  146. package/dist/tw/palette.css +0 -460
  147. package/dist/tw/presentation.css +0 -68
  148. package/dist/tw/surface.css +0 -61
  149. package/dist/tw/utilities/index.css +0 -6
  150. package/dist/tw/utilities/layout.css +0 -130
  151. /package/{dist/tw → tw}/aesthetics/index.css +0 -0
@@ -67,10 +67,15 @@
67
67
  --ui-rule-style: initial;
68
68
 
69
69
  /* The glow is light, not depth, so it is the halo: the component's own intent
70
- colour at 40%, blurred, with no offset. Elevation does not scale it, so
71
- every component glows -- fields and chips included -- and `.flat` does
72
- not put it out. There is no depth, and the transparent depth colour keeps
73
- `.raised` and `.floating` from drawing any. */
70
+ colour at 60%, blurred, with no offset. The 1px spread and the 60% put
71
+ the brightest point on the outline: at 40% with no spread the halo
72
+ peaked at about a fifth of the ink just outside the box, brighter than a
73
+ soft or capped neutral fill, so an edgeless element read as something in
74
+ front of a light -- a shadow -- rather than the light. An edged element
75
+ has its line for a source and read as lit either way. Elevation does
76
+ not scale it, so every component glows -- fields and chips included --
77
+ and `.flat` does not put it out. There is no depth, and the transparent
78
+ depth colour keeps `.raised` and `.floating` from drawing any. */
74
79
  --elevation-color: transparent;
75
80
  --ui-shadow-ink: 0%;
76
81
  --ui-shadow-x: 0px;
@@ -78,12 +83,12 @@
78
83
  --ui-shadow-blur: 0px;
79
84
  --ui-shadow-spread: 0px;
80
85
  --ui-halo-blur: var(--_cyber-glow);
81
- --ui-halo-spread: 0px;
82
- --ui-halo-ink: 40%;
86
+ --ui-halo-spread: 1px;
87
+ --ui-halo-ink: 60%;
83
88
 
84
89
  /* The glow holds still under the pointer and under a press; the press is the
85
90
  base scale, restated because a region nested inside another aesthetic must
86
- not inherit that one's travel. Reduced motion drops it below. */
91
+ not inherit that one's travel. */
87
92
  --ui-hover-transform: none;
88
93
  --ui-active-shadow-x: 0px;
89
94
  --ui-active-shadow-y: 0px;
@@ -140,26 +145,41 @@
140
145
  --ui-radius-surface: 0;
141
146
  }
142
147
  }
143
-
144
- @media (prefers-reduced-motion: reduce) {
145
- .cyber {
146
- --ui-active-transform: none;
147
- }
148
- }
149
148
  }
150
149
 
151
150
  /* Solo: the same look painted onto the one element carrying the class, for an
152
151
  element the package does not style. See docs/internal/model.md#solo-classes and
153
- the note on `.glass-solo`: own knobs and foundation tokens with literal
154
- fallbacks, never a shared `--ui-*`, `--intent-*`, or `--elevation-color`
155
- token, unlayered, and not for this package's own components. */
152
+ the note on `.glass-solo`: it composes with the element's presentation,
153
+ intent, and elevation, and reads its material from its own knobs and
154
+ foundation tokens with literal fallbacks, never a shared material `--ui-*`
155
+ or `--elevation-color` token. Unlayered, and not for this package's own
156
+ components. */
156
157
  .cyber-solo {
157
- --_cyber-ink: var(
158
- --cyber-ink,
159
- light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0)))
158
+ /* The composition, the same in every solo class; `registry.test.ts` holds
159
+ the copies together. See the note on `.glass-solo`. */
160
+ --_solo-intent-color: initial;
161
+ --_solo-intent-contrast: initial;
162
+ --_solo-intent-strong: initial;
163
+ --_solo-intent-border: initial;
164
+ --_solo-intent-fill-max: initial;
165
+ --_solo-color: var(--_solo-intent-color, var(--color-text, light-dark(oklch(14.5% 0 0), oklch(98.5% 0 0))));
166
+ --_solo-contrast: var(
167
+ --_solo-intent-contrast,
168
+ var(--color-text-contrast, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)))
169
+ );
170
+ --_solo-strong: var(--_solo-intent-strong, var(--color-text-strong, light-dark(oklch(20.5% 0 0), oklch(97% 0 0))));
171
+ --_solo-capped: min(var(--ui-fill, 0%), var(--_solo-intent-fill-max, 20%));
172
+ --_solo-fill: color-mix(in oklab, var(--_solo-color) var(--_solo-capped), transparent);
173
+ --_solo-on-fill: min(var(--ui-fg-on-fill, 0%), max(0%, calc(var(--_solo-capped) * 2 - 100%)));
174
+ --_solo-fg: color-mix(in oklab, var(--_solo-contrast) var(--_solo-on-fill), var(--_solo-strong));
175
+ /* The line with no intent on the element: this aesthetic's own ink. */
176
+ --_solo-ink: var(
177
+ --_solo-intent-border,
178
+ var(--cyber-ink, light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0))))
160
179
  );
180
+ --_solo-edge: color-mix(in oklab, var(--_solo-ink) var(--ui-border, 100%), var(--_solo-fill));
161
181
 
162
- border: 1px solid var(--_cyber-ink);
182
+ border: 1px solid var(--_solo-edge);
163
183
  /* The surface shape: the element this exists for is usually a pane. An action
164
184
  takes the control shape below. */
165
185
  border-radius: var(
@@ -167,14 +187,49 @@
167
187
  0 min(var(--cyber-cut, 0.625rem), 25%) / 0 min(var(--cyber-cut, 0.625rem), 25%)
168
188
  );
169
189
  corner-shape: bevel;
170
- /* Always lit: the elevation gate the aesthetic class answers to is `box`'s,
171
- and a solo element is one the consumer chose to light up. */
172
- box-shadow: 0 0 var(--cyber-glow, 8px) 0 color-mix(in oklab, var(--_cyber-ink) 40%, transparent);
190
+ /* Always lit, in the element's own colour: the glow is a halo and not depth,
191
+ so `.flat` leaves it on, as under the aesthetic class. */
192
+ box-shadow:
193
+ 0 0 var(--cyber-glow, 8px) 1px color-mix(in oklab, var(--_solo-ink) 60%, transparent),
194
+ inset 0 0 0 100vmax var(--_solo-fill);
173
195
  font-family: var(--font-cyber, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
174
- transition: transform var(--motion-duration-normal, 200ms) var(--motion-ease, cubic-bezier(0.2, 0, 0, 1));
175
196
  }
176
197
 
177
- /* An action takes the control shape, on the opposite diagonal to a pane's. */
198
+ /* An intent class on the element colours it, as on a component. Named rather
199
+ than read from `--intent-*` alone: those slots inherit, so a `.success`
200
+ container would colour a solo element where it leaves a component neutral. */
201
+ .cyber-solo:is(.primary, .secondary, .success, .warning, .destructive, .info) {
202
+ --_solo-intent-color: var(--intent-color);
203
+ --_solo-intent-contrast: var(--intent-contrast);
204
+ --_solo-intent-strong: var(--intent-strong);
205
+ --_solo-intent-border: var(--intent-border);
206
+ --_solo-intent-fill-max: var(--intent-fill-max);
207
+ }
208
+
209
+ /* The text colour is written only where a fill or an intent is asked for, on
210
+ the element or by a container's fill class. Anywhere else the element keeps
211
+ its own: a dark toast keeps its light text. */
212
+ .cyber-solo:is(
213
+ .solid,
214
+ .soft,
215
+ .ghost,
216
+ .primary,
217
+ .secondary,
218
+ .success,
219
+ .warning,
220
+ .destructive,
221
+ .info,
222
+ :is(.solid, .soft, .ghost) *
223
+ ) {
224
+ color: var(--_solo-fg);
225
+ }
226
+
227
+ /* An action takes the control shape, on the opposite diagonal to a pane's.
228
+
229
+ And the press's transition, on an action only: `transition` is one list, so
230
+ writing it on every solo element replaced a container's own -- a toast's
231
+ fade and height stopped animating. The press is the independent `scale`, so
232
+ the element's own `transform` stays its own. */
178
233
  .cyber-solo:is(
179
234
  button,
180
235
  a[href],
@@ -185,6 +240,13 @@
185
240
  input[type="reset"]
186
241
  ) {
187
242
  border-radius: var(--cyber-shape, min(var(--cyber-cut, 0.625rem), 25%) 0 / min(var(--cyber-cut, 0.625rem), 25%) 0);
243
+ transition: scale var(--motion-duration-normal, 200ms) var(--motion-ease, cubic-bezier(0.2, 0, 0, 1));
244
+
245
+ /* Reduced motion keeps the press and drops its motion: the element gives at
246
+ once. The near-zero duration `box` uses, so a `transitionend` still fires. */
247
+ @media (prefers-reduced-motion: reduce) {
248
+ transition-duration: 0.01ms;
249
+ }
188
250
  }
189
251
 
190
252
  @supports not (corner-shape: bevel) {
@@ -206,19 +268,6 @@
206
268
  container stays put, the way only `.btn` and an opted-in card press under
207
269
  the aesthetic class. */
208
270
  .cyber-solo:is(
209
- button,
210
- a[href],
211
- [role="button"],
212
- summary,
213
- input[type="button"],
214
- input[type="submit"],
215
- input[type="reset"]
216
- ):active:not(:disabled, [aria-disabled="true"]) {
217
- transform: scale(0.97);
218
- }
219
-
220
- @media (prefers-reduced-motion: reduce) {
221
- .cyber-solo:is(
222
271
  button,
223
272
  a[href],
224
273
  [role="button"],
@@ -227,6 +276,5 @@
227
276
  input[type="submit"],
228
277
  input[type="reset"]
229
278
  ):active:not(:disabled, [aria-disabled="true"]) {
230
- transform: none;
231
- }
279
+ scale: 0.97;
232
280
  }
@@ -39,6 +39,13 @@
39
39
  on a field's 60% resting line, in light. See
40
40
  docs/internal/model.md#presentation. */
41
41
  --ui-control-ink: light-dark(rgb(0 0 0 / 0.55), rgb(255 255 255 / 0.55));
42
+ /* Buttons and badges keep the hairline, which needs a plate to be seen
43
+ on. Unfilled they have none -- white at 65% on a light page is 1.01:1,
44
+ an edged ghost button with no edge -- so there the neutral line is the
45
+ control one. `box` reads it only for a neutral box resolving to no
46
+ fill; the other aesthetics clear it below, so the nearest region
47
+ decides. */
48
+ --_unfilled-ink: light-dark(rgb(0 0 0 / 0.55), rgb(255 255 255 / 0.55));
42
49
  /* Nor is a line drawn on a plate: the hairline vanishes on a table's light
43
50
  plate, and its default, `.ruled`, and `.ruleless` forms would render as
44
51
  one. A table's frame and rules take the theme's ink at a fifth,
@@ -124,20 +131,41 @@
124
131
  --ui-label-tracking: initial;
125
132
  --ui-label-case: initial;
126
133
  --ui-label-shadow: initial;
134
+
135
+ /* No face of its own, so the theme's, named rather than inherited: a glass
136
+ region nested inside pixel, cyber, sketch, or chunky tile otherwise took
137
+ their face. `--font-default` is the token the reset applies to the page,
138
+ as chunky tile reads it. */
139
+ font-family: var(--font-default, ui-sans-serif, system-ui, sans-serif);
140
+ }
141
+
142
+ /* A region of another aesthetic nested in glass draws its own line. */
143
+ :is(.neobrutalism, .pixel, .chunky-tile, .cyber, .sketch) {
144
+ --_unfilled-ink: initial;
127
145
  }
128
146
 
129
147
  /* Transparency is the whole aesthetic, so the honest degradation is an opaque
130
- surface rather than a softer blur. */
131
- @media (prefers-reduced-transparency: reduce) {
148
+ surface rather than a softer blur.
149
+
150
+ Forced colours take the same branch. They repaint a background in `Canvas`
151
+ but keep its alpha -- measured in Chromium -- so the translucent ground
152
+ stayed translucent over whatever was behind it, and a liquid card's text
153
+ sat at about 2.5:1 over a busy page. An opaque ground is `Canvas` whole. */
154
+ @media (prefers-reduced-transparency: reduce), (forced-colors: active), (prefers-contrast: more) {
132
155
  .glass {
133
156
  --ui-backdrop: none;
134
157
  --ui-surface-ground: var(--color-background);
135
158
  }
136
159
  }
137
160
 
138
- @media (prefers-reduced-motion: reduce) {
161
+ /* More contrast also takes the ground whole, above, and then the white
162
+ hairline has nothing to be seen against: 1.03:1 on an opaque light page.
163
+ The line becomes the theme's control grey, above 4:1 on both grounds.
164
+ Only under this preference: reduced transparency asks for no stronger
165
+ line. See docs/accessibility.md. */
166
+ @media (prefers-contrast: more) {
139
167
  .glass {
140
- --ui-active-transform: none;
168
+ --ui-ink: var(--color-control-border);
141
169
  }
142
170
  }
143
171
 
@@ -154,12 +182,20 @@
154
182
 
155
183
  The corner knobs are glass's own, with larger defaults: a continuous corner
156
184
  reads smaller than a round one of the same radius. Where `corner-shape` is
157
- not drawn the corners stay round. */
158
- .glass-liquid {
185
+ not drawn the corners stay round.
186
+
187
+ Not on a solo element: `.glass-solo.glass-liquid` paints its own corner
188
+ below, and a token declared on it would inherit into whatever it holds --
189
+ a `.btn` inside a solo toast took liquid's corners on a page with no glass
190
+ in scope. Every scope below skips a solo element for the same reason. */
191
+ .glass-liquid:not(.glass-solo) {
159
192
  --ui-corner: var(--glass-radius, 1rem);
160
193
  --ui-corner-surface: var(--glass-radius-surface, 1.5rem);
161
194
  --ui-corner-shape: squircle;
195
+ }
162
196
 
197
+ /* The layers' values, on a solo element too: it draws the same two. */
198
+ .glass-liquid {
163
199
  --_glass-liquid-blur: blur(2px) saturate(1.8);
164
200
 
165
201
  /* The lens is its own layer rather than a second function in the blur's
@@ -177,6 +213,12 @@
177
213
  property, so the band and the strength are fixed, not knobs. */
178
214
  --_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
215
 
216
+ /* The same lens at a control's size. The pane's band is 8px of erosion
217
+ under a 10px blur, which is most of a 40px button's height: the two
218
+ sides' ramps meet in the middle and the label sits in a loop. A third of
219
+ each length keeps a clear centre for the label. */
220
+ --_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");
221
+
180
222
  /* The rim: light caught on the top-left edge and, fainter, the bottom-right,
181
223
  over a soft inner glow. Drawn on the lens layer, the topmost, because a
182
224
  rim drawn on the surface itself would sit under the layers and blur. */
@@ -192,59 +234,104 @@
192
234
  }
193
235
 
194
236
  /* 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 {
237
+ same element, or on a surface inside a glass region.
238
+
239
+ The backdrop is liquid's lighter blur, for whatever frosts under glass and
240
+ takes no layers below: a bare `<table>` under `/native`. With the backdrop
241
+ cleared here it kept the clearer ground with nothing blurred behind it. A
242
+ host of the layers clears its own. */
243
+ .glass.glass-liquid:not(.glass-solo),
244
+ .glass .glass-liquid:not(.glass-solo) {
201
245
  --ui-ink: light-dark(rgb(255 255 255 / 0.45), rgb(255 255 255 / 0.16));
202
246
  --ui-surface-ground: color-mix(in srgb, var(--color-background) 30%, transparent);
203
247
  --ui-surface-image: initial;
204
- --ui-backdrop: none;
248
+ --ui-backdrop: var(--_glass-liquid-blur);
205
249
  }
206
250
 
207
- @media (prefers-reduced-transparency: reduce) {
208
- .glass.glass-liquid,
209
- .glass .glass-liquid {
251
+ @media (prefers-reduced-transparency: reduce), (forced-colors: active), (prefers-contrast: more) {
252
+ .glass.glass-liquid:not(.glass-solo),
253
+ .glass .glass-liquid:not(.glass-solo) {
210
254
  --ui-surface-ground: var(--color-background);
211
255
  }
212
256
 
213
257
  .glass-liquid {
214
258
  --_glass-liquid-blur: none;
215
259
  --_glass-liquid-lens: none;
260
+ --_glass-liquid-lens-control: none;
216
261
  }
217
262
  }
218
263
 
219
- /* 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
- ) {
246
- position: relative;
247
- isolation: isolate;
264
+ @media (prefers-contrast: more) {
265
+ .glass.glass-liquid:not(.glass-solo),
266
+ .glass .glass-liquid:not(.glass-solo) {
267
+ --ui-ink: var(--color-control-border);
268
+ }
269
+ }
270
+
271
+ /* The layers: a treatment no token reaches. Two pseudo-elements behind the
272
+ content, the blur beneath and the lens with the rim and sheen above it.
273
+ `isolation` keeps them above the element's own plate, which they then blur
274
+ with the page; `position` anchors them, and a component that positions
275
+ itself -- the tooltip bubble -- keeps its own. Both slots are spent, so a
276
+ host carries no `::before` or `::after` of the consumer's; the fields
277
+ cannot host one.
278
+
279
+ Surfaces draw them in `surface` itself, from the flag and the values
280
+ below, so every element that applies it takes them: the package's
281
+ surfaces and a consumer's `@apply surface` alike. A selector list here
282
+ reached only the classes it named. The list below is for the two hosts
283
+ that are not surfaces, and the registry records it: a default or `.soft`
284
+ table, which frosts under glass, and a liquid button.
285
+
286
+ The blur is on the layer and not on the host: an element with a backdrop
287
+ filter of its own is a backdrop root, and a layer inside one sees only that
288
+ element, never the page behind it. So a host clears the region's backdrop.
289
+
290
+ Surfaces take the layers from a liquid scope. A region of another
291
+ aesthetic nested inside, or of plain glass, keeps its own material and
292
+ takes none, and a liquid region nested again inside that takes them back:
293
+ the nearest region decides. A `:not(.glass-liquid :is(...) *)` exclusion
294
+ cannot tell those two apart, since the re-entered surface also sits below
295
+ the other aesthetic, so each region writes a private flag instead and
296
+ inheritance hands a surface the nearest one's -- the way neobrutalism
297
+ rests its alert. The flag is on only for a liquid region in a glass
298
+ scope, so it alone says whether a surface takes the layers. Off, it
299
+ withholds the layers, the anchor, and the isolation, and a table or a
300
+ button gets back the backdrop the nearest region set. The list below
301
+ names the other aesthetics, and `registry.test.ts` fails when one is
302
+ missing. A region's own element is not a host: it declares the flag's
303
+ backdrop from its own, which a host would then declare from the flag.
304
+
305
+ A button takes them only when it carries `.glass-liquid` itself, in a
306
+ glass scope. Controls stay solid under a liquid region, for the reason
307
+ glass gives -- two composited layers and a filter under every control of a
308
+ dense cluster -- and a class on the one button that is meant to be a pane
309
+ is a request rather than a cascade, the idiom `text-control` uses for
310
+ `.edgeless`. Its fill is still presentation's: `.solid` stays an opaque
311
+ plate under the rim, and `.soft` and `.ghost` are the clear panes. A
312
+ disabled one is dimmed by `opacity`, which makes it a backdrop root, so it
313
+ keeps the ground and the rim and bends nothing. */
314
+ .glass-liquid:not(.glass-solo):is(.glass, .glass *) {
315
+ /* A space toggle: empty is on, and `initial` makes every declaration
316
+ reading it invalid at computed-value time, which is off. */
317
+ --_glass-liquid-on: ;
318
+ --_glass-liquid-backdrop: initial;
319
+ }
320
+
321
+ .glass:not(.glass-liquid),
322
+ :is(.neobrutalism, .pixel, .chunky-tile, .cyber, .sketch) {
323
+ --_glass-liquid-on: initial;
324
+ --_glass-liquid-backdrop: var(--ui-backdrop);
325
+ }
326
+
327
+ :is(.glass-liquid:not(.glass-solo), .glass-liquid:not(.glass-solo) *):is(.glass, .glass *):is(
328
+ .data-table:not(.solid, .ghost)
329
+ ):not(.glass:not(.glass-liquid), :is(.neobrutalism, .pixel, .chunky-tile, .cyber, .sketch)),
330
+ .btn.glass-liquid:is(.glass, .glass *) {
331
+ --ui-backdrop: var(--_glass-liquid-backdrop, none);
332
+
333
+ position: var(--_glass-liquid-on) relative;
334
+ isolation: var(--_glass-liquid-on) isolate;
248
335
 
249
336
  &::before,
250
337
  &::after {
@@ -253,7 +340,7 @@
253
340
  z-index: -1;
254
341
  border-radius: inherit;
255
342
  corner-shape: inherit;
256
- content: "";
343
+ content: var(--_glass-liquid-on) "";
257
344
  pointer-events: none;
258
345
  }
259
346
 
@@ -269,6 +356,28 @@
269
356
  box-shadow: var(--_glass-liquid-rim);
270
357
  }
271
358
  }
359
+
360
+ /* A button's lens is the control one. Swapped as the value the layer reads
361
+ rather than on the layer: the list above outranks any selector that names
362
+ the button's `::after` alone. */
363
+ .btn.glass-liquid:is(.glass, .glass *) {
364
+ --_glass-liquid-lens: var(--_glass-liquid-lens-control);
365
+ }
366
+ }
367
+
368
+ /* A liquid button rests on liquid's ground rather than on nothing, so a
369
+ `.ghost` or `.soft` one is a pane and not a hole in the page. It reads the
370
+ surface ground the modifier set on it, so reduced transparency and a
371
+ consumer's own `--ui-surface-ground` reach it as they reach a card.
372
+
373
+ In `utilities`, not with the layers: it has to beat `box`'s own
374
+ `--_d-ground: transparent`, which it does there on specificity, and it
375
+ writes only a private nothing a consumer sets. See
376
+ docs/internal/model.md#cascade-layers. */
377
+ @layer utilities {
378
+ .btn.glass-liquid:is(.glass, .glass *) {
379
+ --_d-ground: var(--ui-surface-ground, transparent);
380
+ }
272
381
  }
273
382
 
274
383
  /* Forced colours paint the plain surface: no layer blurs, bends, or lights it. */
@@ -276,6 +385,7 @@
276
385
  .glass-liquid {
277
386
  --_glass-liquid-blur: none;
278
387
  --_glass-liquid-lens: none;
388
+ --_glass-liquid-lens-control: none;
279
389
  --_glass-liquid-rim: none;
280
390
  --_glass-liquid-sheen: none;
281
391
  }
@@ -286,24 +396,63 @@
286
396
  `<div>` on a page that never loaded the base stylesheet. See
287
397
  docs/internal/model.md#solo-classes.
288
398
 
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.
399
+ It composes with what the element is asked to be, the way a component does:
400
+ the fill and edge a presentation class asks for, on the element or cascaded
401
+ from a container; the intent class on the element; the element's own
402
+ elevation class. Each is read with the unclassed look as its fallback, so
403
+ with none of them, or with no theme loaded, it paints what it always did.
404
+
405
+ Material is the exception. It reads the aesthetic's own knobs and the
406
+ theme's foundation tokens, each with its shipped literal as the fallback,
407
+ and never a material `--ui-*` or `--elevation-color` token: those belong to
408
+ whichever aesthetic an ancestor carries, and a solo element has to be this
409
+ aesthetic inside any of them.
410
+
293
411
  Unlayered and one class deep, so it beats a foreign component's
294
412
  zero-specificity rules wherever they load. Not for this package's own
295
- components, which take the aesthetic class instead. */
413
+ components, which take the aesthetic class instead.
414
+
415
+ The other five paint the fill as an inset layer over whatever the element
416
+ already is, and write no background, so an unclassed solo element keeps its
417
+ own. Glass's translucent ground is its material, so it owns the background
418
+ and mixes the fill over that ground. */
296
419
  .glass-solo {
297
- background-color: color-mix(
420
+ /* The composition, the same in every solo class; `registry.test.ts` holds
421
+ the copies together. See the note on `.glass-solo`. */
422
+ --_solo-intent-color: initial;
423
+ --_solo-intent-contrast: initial;
424
+ --_solo-intent-strong: initial;
425
+ --_solo-intent-border: initial;
426
+ --_solo-intent-fill-max: initial;
427
+ --_solo-color: var(--_solo-intent-color, var(--color-text, light-dark(oklch(14.5% 0 0), oklch(98.5% 0 0))));
428
+ --_solo-contrast: var(
429
+ --_solo-intent-contrast,
430
+ var(--color-text-contrast, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)))
431
+ );
432
+ --_solo-strong: var(--_solo-intent-strong, var(--color-text-strong, light-dark(oklch(20.5% 0 0), oklch(97% 0 0))));
433
+ --_solo-capped: min(var(--ui-fill, 0%), var(--_solo-intent-fill-max, 20%));
434
+ --_solo-fill: color-mix(in oklab, var(--_solo-color) var(--_solo-capped), transparent);
435
+ --_solo-on-fill: min(var(--ui-fg-on-fill, 0%), max(0%, calc(var(--_solo-capped) * 2 - 100%)));
436
+ --_solo-fg: color-mix(in oklab, var(--_solo-contrast) var(--_solo-on-fill), var(--_solo-strong));
437
+ /* The line with no intent on the element: this aesthetic's own ink. */
438
+ --_solo-ink: var(--_solo-intent-border, light-dark(rgb(255 255 255 / 0.65), rgb(255 255 255 / 0.22)));
439
+ --_solo-ground: color-mix(
298
440
  in srgb,
299
441
  var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0))) 45%,
300
442
  transparent
301
443
  );
302
- border: 1px solid light-dark(rgb(255 255 255 / 0.65), rgb(255 255 255 / 0.22));
444
+ /* The plate runs under the border, so the line fades to nothing, as `box`'s
445
+ does, rather than to the fill. */
446
+ --_solo-edge: color-mix(in oklab, var(--_solo-ink) var(--ui-border, 100%), transparent);
447
+
448
+ background-color: color-mix(in oklab, var(--_solo-color) var(--_solo-capped), var(--_solo-ground));
449
+ border: 1px solid var(--_solo-edge);
303
450
  /* The surface corner: the element this exists for is a pane. */
304
451
  border-radius: var(--glass-radius-surface, 1rem);
305
452
  /* The theme's depth colour as a literal, for the reason above: inside a
306
- `.chunky-tile` region the token is black. */
453
+ `.chunky-tile` region the token is black. A complete shadow, so the
454
+ elevation classes do not reach it, as they do not under the aesthetic
455
+ class. */
307
456
  box-shadow:
308
457
  0 1px 2px -1px light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55)),
309
458
  0 18px 40px -16px light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55));
@@ -313,14 +462,52 @@
313
462
  backdrop-filter: blur(14px) saturate(1.6);
314
463
  }
315
464
 
316
- @media (prefers-reduced-transparency: reduce) {
465
+ /* An intent class on the element colours it, as on a component. Named rather
466
+ than read from `--intent-*` alone: those slots inherit, so a `.success`
467
+ container would colour a solo element where it leaves a component neutral. */
468
+ .glass-solo:is(.primary, .secondary, .success, .warning, .destructive, .info) {
469
+ --_solo-intent-color: var(--intent-color);
470
+ --_solo-intent-contrast: var(--intent-contrast);
471
+ --_solo-intent-strong: var(--intent-strong);
472
+ --_solo-intent-border: var(--intent-border);
473
+ --_solo-intent-fill-max: var(--intent-fill-max);
474
+ }
475
+
476
+ /* The text colour is written only where a fill or an intent is asked for, on
477
+ the element or by a container's fill class. Anywhere else the element keeps
478
+ its own: a dark toast keeps its light text. */
479
+ .glass-solo:is(
480
+ .solid,
481
+ .soft,
482
+ .ghost,
483
+ .primary,
484
+ .secondary,
485
+ .success,
486
+ .warning,
487
+ .destructive,
488
+ .info,
489
+ :is(.solid, .soft, .ghost) *
490
+ ) {
491
+ color: var(--_solo-fg);
492
+ }
493
+
494
+ /* Forced colours with it, for the reason the aesthetic class gives. */
495
+ @media (prefers-reduced-transparency: reduce), (forced-colors: active), (prefers-contrast: more) {
317
496
  .glass-solo {
318
- background-color: var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)));
497
+ --_solo-ground: var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)));
498
+
319
499
  -webkit-backdrop-filter: none;
320
500
  backdrop-filter: none;
321
501
  }
322
502
  }
323
503
 
504
+ /* The line on an opaque ground, as the aesthetic class draws it. */
505
+ @media (prefers-contrast: more) {
506
+ .glass-solo {
507
+ --_solo-ink: var(--_solo-intent-border, var(--color-control-border, oklch(55.6% 0 0)));
508
+ }
509
+ }
510
+
324
511
  /* With `.glass-liquid`, the liquid pane: its corner and ground, and the same two
325
512
  layers behind the element's content. The element's own blur goes, for the
326
513
  backdrop-root reason the modifier gives. Both pseudo-elements are spent, so a
@@ -336,13 +523,14 @@
336
523
  }
337
524
 
338
525
  .glass-solo.glass-liquid {
339
- isolation: isolate;
340
- background-color: color-mix(
526
+ --_solo-ground: color-mix(
341
527
  in srgb,
342
528
  var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0))) 30%,
343
529
  transparent
344
530
  );
345
- border-color: light-dark(rgb(255 255 255 / 0.45), rgb(255 255 255 / 0.16));
531
+ --_solo-ink: var(--_solo-intent-border, light-dark(rgb(255 255 255 / 0.45), rgb(255 255 255 / 0.16)));
532
+
533
+ isolation: isolate;
346
534
  border-radius: var(--glass-radius-surface, 1.5rem);
347
535
  corner-shape: squircle;
348
536
  -webkit-backdrop-filter: none;
@@ -371,8 +559,16 @@
371
559
  }
372
560
  }
373
561
 
374
- @media (prefers-reduced-transparency: reduce) {
562
+ @media (prefers-reduced-transparency: reduce), (forced-colors: active), (prefers-contrast: more) {
563
+ .glass-solo.glass-liquid {
564
+ --_solo-ground: var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)));
565
+ }
566
+ }
567
+
568
+ /* After the liquid pane's own ink, which it would otherwise override by source
569
+ order at the same specificity. */
570
+ @media (prefers-contrast: more) {
375
571
  .glass-solo.glass-liquid {
376
- background-color: var(--color-background, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)));
572
+ --_solo-ink: var(--_solo-intent-border, var(--color-control-border, oklch(55.6% 0 0)));
377
573
  }
378
574
  }