@codenhub/styles 0.5.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 (119) hide show
  1. package/NOTICE +18 -16
  2. package/README.es.md +61 -0
  3. package/README.md +27 -22
  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 +120 -62
  18. package/dist/tw/aesthetics/cyber.css +102 -40
  19. package/dist/tw/aesthetics/glass.css +371 -19
  20. package/dist/tw/aesthetics/index.css +2 -1
  21. package/dist/tw/aesthetics/neobrutalism.css +121 -33
  22. package/dist/tw/aesthetics/pixel.css +111 -16
  23. package/dist/tw/aesthetics/sketch.css +100 -28
  24. package/dist/tw/box.css +69 -28
  25. package/dist/tw/components/button.css +35 -14
  26. package/dist/tw/components/feedback.css +61 -58
  27. package/dist/tw/components/form.css +255 -142
  28. package/dist/tw/components/index.css +4 -9
  29. package/dist/tw/components/surface.css +80 -68
  30. package/dist/tw/components/tooltip.css +75 -41
  31. package/dist/tw/elevation.css +14 -22
  32. package/dist/tw/index.css +15 -19
  33. package/dist/tw/intent.css +35 -27
  34. package/dist/tw/native.css +22 -12
  35. package/dist/tw/palette.css +14 -11
  36. package/dist/tw/presentation.css +16 -30
  37. package/dist/tw/reset.css +36 -43
  38. package/dist/tw/surface.css +11 -12
  39. package/dist/tw/theme.css +87 -103
  40. package/dist/tw/utilities/content.css +311 -200
  41. package/dist/tw/utilities/layout.css +41 -37
  42. package/docs/accessibility.md +104 -58
  43. package/docs/changelog/0.1.1.md +1 -0
  44. package/docs/changelog/0.2.0.md +1 -0
  45. package/docs/changelog/0.3.0.md +1 -0
  46. package/docs/changelog/0.4.0.md +1 -0
  47. package/docs/changelog/0.5.0.md +8 -3
  48. package/docs/changelog/0.6.0.md +53 -0
  49. package/docs/changelog/0.7.0.md +50 -0
  50. package/docs/changelog/index.md +2 -0
  51. package/docs/concepts.md +188 -46
  52. package/docs/es/accessibility.md +150 -0
  53. package/docs/es/concepts.md +212 -0
  54. package/docs/es/index.md +104 -0
  55. package/docs/es/integrating/astro.md +35 -0
  56. package/docs/es/integrating/index.md +47 -0
  57. package/docs/es/integrating/nextjs.md +67 -0
  58. package/docs/es/integrating/svelte.md +45 -0
  59. package/docs/es/integrating/tailwind.md +78 -0
  60. package/docs/es/integrating/vue.md +31 -0
  61. package/docs/es/integrating/without-a-bundler.md +115 -0
  62. package/docs/es/setup.md +180 -0
  63. package/docs/es/usage/aesthetics.md +276 -0
  64. package/docs/es/usage/buttons.md +131 -0
  65. package/docs/es/usage/composing.md +164 -0
  66. package/docs/es/usage/content-and-layout.md +215 -0
  67. package/docs/es/usage/customizing.md +298 -0
  68. package/docs/es/usage/feedback.md +153 -0
  69. package/docs/es/usage/forms.md +231 -0
  70. package/docs/es/usage/index.md +29 -0
  71. package/docs/es/usage/solo.md +84 -0
  72. package/docs/es/usage/theming.md +296 -0
  73. package/docs/es/usage/tooltips.md +127 -0
  74. package/docs/index.md +89 -21
  75. package/docs/integrating/astro.md +3 -4
  76. package/docs/integrating/index.md +18 -13
  77. package/docs/integrating/nextjs.md +4 -5
  78. package/docs/integrating/svelte.md +5 -4
  79. package/docs/integrating/tailwind.md +29 -8
  80. package/docs/integrating/vue.md +3 -4
  81. package/docs/integrating/without-a-bundler.md +114 -0
  82. package/docs/pt-BR/accessibility.md +150 -0
  83. package/docs/pt-BR/concepts.md +212 -0
  84. package/docs/pt-BR/index.md +104 -0
  85. package/docs/pt-BR/integrating/astro.md +35 -0
  86. package/docs/pt-BR/integrating/index.md +47 -0
  87. package/docs/pt-BR/integrating/nextjs.md +67 -0
  88. package/docs/pt-BR/integrating/svelte.md +45 -0
  89. package/docs/pt-BR/integrating/tailwind.md +78 -0
  90. package/docs/pt-BR/integrating/vue.md +31 -0
  91. package/docs/pt-BR/integrating/without-a-bundler.md +115 -0
  92. package/docs/pt-BR/setup.md +180 -0
  93. package/docs/pt-BR/usage/aesthetics.md +276 -0
  94. package/docs/pt-BR/usage/buttons.md +131 -0
  95. package/docs/pt-BR/usage/composing.md +164 -0
  96. package/docs/pt-BR/usage/content-and-layout.md +215 -0
  97. package/docs/pt-BR/usage/customizing.md +298 -0
  98. package/docs/pt-BR/usage/feedback.md +153 -0
  99. package/docs/pt-BR/usage/forms.md +231 -0
  100. package/docs/pt-BR/usage/index.md +29 -0
  101. package/docs/pt-BR/usage/solo.md +84 -0
  102. package/docs/pt-BR/usage/theming.md +296 -0
  103. package/docs/pt-BR/usage/tooltips.md +127 -0
  104. package/docs/setup.md +134 -91
  105. package/docs/usage/aesthetics.md +102 -65
  106. package/docs/usage/buttons.md +108 -40
  107. package/docs/usage/composing.md +105 -92
  108. package/docs/usage/content-and-layout.md +159 -127
  109. package/docs/usage/customizing.md +112 -43
  110. package/docs/usage/feedback.md +135 -70
  111. package/docs/usage/forms.md +184 -99
  112. package/docs/usage/index.md +18 -14
  113. package/docs/usage/solo.md +83 -0
  114. package/docs/usage/theming.md +260 -119
  115. package/docs/usage/tooltips.md +103 -42
  116. package/llms-full.txt +2465 -1272
  117. package/llms.txt +25 -17
  118. package/package.json +28 -35
  119. package/dist/tw/components/loader.css +0 -117
@@ -1,8 +1,7 @@
1
1
  /* Neobrutalism: thick ink outline, a hard unblurred offset shadow, and a press
2
2
  that moves the element into its own shadow. Material tokens only, no component
3
3
  named -- the ink reaches everything through the intent reset, the shadow is
4
- colourless geometry each component colours against its own intent. Load after
5
- the base stylesheet; source order decides against the zero-specificity reset. */
4
+ colourless geometry each component colours against its own intent. */
6
5
 
7
6
  /* The layer map, stated before anything uses a layer -- see `theme.css`. The
8
7
  aesthetic's token class sits in `components`, below every utility, so a
@@ -37,6 +36,10 @@
37
36
  is the whole look, so this aesthetic asks for the intent instead of the
38
37
  neutral depth colour every other shadow uses. */
39
38
  --ui-shadow-ink: 100%;
39
+ /* The ink replaces the depth colour whole, so this one is inert here; it is
40
+ restated so a region nested inside chunky tile or cyber does not inherit
41
+ their black or transparent depth. */
42
+ --elevation-color: light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55));
40
43
  --ui-shadow-x: var(--_neo-offset);
41
44
  --ui-shadow-y: var(--_neo-offset);
42
45
  --ui-shadow-blur: 0px;
@@ -47,19 +50,24 @@
47
50
  --ui-halo-spread: initial;
48
51
  --ui-halo-ink: initial;
49
52
 
50
- /* The press: travel the offset, shrink the shadow to nothing, so the element
51
- lands flat where its slab was. A translation `box` scales with the slab, so
52
- the two stay the same length at every size and elevation. Hover keeps the slab still under the pointer;
53
- `box-hover`'s derived fill tint is the whole hover response. One token pair,
54
- no `:hover` list, R3 intact. */
53
+ /* The press: travel the offset, shrink the shadow to nothing, so the
54
+ element lands flat where its slab was. A translation `box` scales with
55
+ the slab, so the two stay the same length at every size and elevation.
56
+ Hover keeps the slab still under the pointer; `box-hover`'s derived fill
57
+ tint is the whole hover response. One token pair, no `:hover` list, R3
58
+ intact. */
55
59
  --ui-active-shadow-x: 0px;
56
60
  --ui-active-shadow-y: 0px;
57
61
  --ui-active-transform: none;
58
62
  --ui-active-translate-x: var(--_neo-offset);
59
63
  --ui-active-translate-y: var(--_neo-offset);
64
+ --ui-hover-transform: none;
60
65
 
61
66
  /* Nothing here clips or blurs, and an aesthetic nested inside another has to
62
- say so rather than inherit it. */
67
+ say so rather than inherit it. Nothing clips, so the global focus outline
68
+ shows and no inset focus layer is wanted: cleared so a region nested
69
+ inside pixel does not inherit pixel's. */
70
+ --ui-focus-inset: initial;
63
71
  --ui-clip: none;
64
72
  --ui-clip-tight: none;
65
73
  /* Round corners, said explicitly, and the pill and chip corners cleared back
@@ -86,9 +94,13 @@
86
94
  --ui-bg-alpha: 1;
87
95
  --ui-surface-image: initial;
88
96
 
89
- /* The control ink glass and chunky tile name, cleared so a region nested
90
- inside either draws its controls in this aesthetic's own ink. */
91
- --ui-control-ink: initial;
97
+ /* A control's neutral line is the theme's control border, not this
98
+ aesthetic's ink: that ink is the primary colour in both themes, so every
99
+ unchecked toggle and resting field drew its boundary as loudly as a
100
+ primary one. Named rather than cleared, so a region nested inside glass
101
+ or chunky tile does not inherit theirs either. */
102
+ --ui-control-ink: var(--color-control-border);
103
+ --ui-rule-ink: initial;
92
104
 
93
105
  /* Chunky tile's heavier label, cleared so a region nested inside it keeps
94
106
  the button's and the badge's own label. */
@@ -108,48 +120,108 @@
108
120
 
109
121
  In `utilities`, not with the token class: it has to beat `@utility alert`'s
110
122
  own `--_d-elevation: 0`, which it does there at 0-2-0, and it writes only a
111
- 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. */
112
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
+
113
141
  .neobrutalism .alert {
114
- --_d-elevation: 1;
142
+ --_d-elevation: var(--_neo-alert-elevation, 0);
115
143
  }
116
144
  }
117
145
 
118
146
  /* Solo: the same look painted onto the one element carrying the class, for an
119
147
  element the package does not style. See docs/internal/model.md#solo-classes and
120
- the note on `.glass-solo`: own knobs and foundation tokens with literal
121
- fallbacks, never a shared `--ui-*`, `--intent-*`, or `--elevation-color`
122
- 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. */
123
153
  .neobrutalism-solo {
124
154
  --_neo-offset: var(--neo-offset, 4px);
125
- --_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));
126
181
 
127
- border: 2px solid var(--_neo-ink);
182
+ border: 2px solid var(--_solo-edge);
128
183
  border-radius: 0;
129
- 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);
130
187
  transition:
131
188
  box-shadow var(--motion-duration-normal, 200ms) var(--motion-ease, cubic-bezier(0.2, 0, 0, 1)),
132
189
  transform var(--motion-duration-normal, 200ms) var(--motion-ease, cubic-bezier(0.2, 0, 0, 1));
133
190
  }
134
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
+
135
221
  /* The press, only on something pressed: an action, not disabled. A solo
136
222
  container -- a toast clicked to dismiss -- stays put, the way only `.btn` and
137
223
  an opted-in card press under the aesthetic class. */
138
224
  .neobrutalism-solo:is(
139
- button,
140
- a[href],
141
- [role="button"],
142
- summary,
143
- input[type="button"],
144
- input[type="submit"],
145
- input[type="reset"]
146
- ):active:not(:disabled, [aria-disabled="true"]) {
147
- box-shadow: 0 0 0 0 var(--_neo-ink);
148
- transform: translate(var(--_neo-offset), var(--_neo-offset));
149
- }
150
-
151
- @media (prefers-reduced-motion: reduce) {
152
- .neobrutalism-solo:is(
153
225
  button,
154
226
  a[href],
155
227
  [role="button"],
@@ -158,6 +230,22 @@
158
230
  input[type="submit"],
159
231
  input[type="reset"]
160
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"]) {
161
249
  transform: none;
162
250
  }
163
251
  }
@@ -64,11 +64,10 @@
64
64
  has to be wider than the edge to show at all. */
65
65
  --ui-focus-inset: calc(var(--_pixel-unit) + var(--focus-ring-width));
66
66
 
67
- /* One square unit off each corner, and nothing else. The staircase this
68
- replaces stepped twice at half the unit to approximate a curve, which is the
69
- one thing a low-resolution grid cannot do: at any size it read as a bevel
70
- that had not quite committed. A single cut of the grid's own unit is the
71
- shape the look is actually made of.
67
+ /* One square unit off each corner, and nothing else. A staircase
68
+ approximates a curve, which is the one thing a low-resolution grid cannot
69
+ do: it reads as a bevel that has not quite committed. A single cut of the
70
+ grid's own unit is the shape the look is made of.
72
71
 
73
72
  The polygon resolves the unit here, on `.pixel`, and inherits down
74
73
  already-resolved -- so a component cannot rescale the corner by setting
@@ -108,6 +107,13 @@
108
107
  --ui-corner-br: initial;
109
108
  --ui-corner-bl: initial;
110
109
 
110
+ /* The ring is the edge and nothing here is depth, so the shadow ink, the
111
+ press's shadow, and the depth colour are cleared or restated rather than
112
+ inherited from an aesthetic around this one. */
113
+ --ui-shadow-ink: initial;
114
+ --ui-active-shadow-x: initial;
115
+ --ui-active-shadow-y: initial;
116
+ --elevation-color: light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55));
111
117
  --ui-hover-transform: none;
112
118
  --ui-active-transform: none;
113
119
  --ui-active-translate-x: initial;
@@ -122,9 +128,13 @@
122
128
  font to fall back to but a monospace stack. */
123
129
  font-family: var(--font-pixel, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
124
130
 
125
- /* The control ink glass and chunky tile name, cleared so a region nested
126
- inside either draws its controls in this aesthetic's own ink. */
127
- --ui-control-ink: initial;
131
+ /* A control's neutral line is the theme's control border, not this
132
+ aesthetic's ink: that ink is the primary colour in both themes, so every
133
+ unchecked toggle and resting field drew its boundary as loudly as a
134
+ primary one. Named rather than cleared, so a region nested inside glass
135
+ or chunky tile does not inherit theirs either. */
136
+ --ui-control-ink: var(--color-control-border);
137
+ --ui-rule-ink: initial;
128
138
 
129
139
  /* Chunky tile's heavier label, cleared so a region nested inside it keeps
130
140
  the button's and the badge's own label. */
@@ -137,18 +147,46 @@
137
147
 
138
148
  /* Solo: the same look painted onto the one element carrying the class, for an
139
149
  element the package does not style. See docs/internal/model.md#solo-classes and
140
- the note on `.glass-solo`: own knobs and foundation tokens with literal
141
- fallbacks, never a shared `--ui-*`, `--intent-*`, or `--elevation-color`
142
- 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. */
143
155
  .pixel-solo {
144
156
  --_pixel-unit: var(--pixel-unit, 4px);
145
- --_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);
146
182
 
147
183
  /* The clip removes a border at the straight sides, so the ring draws the
148
184
  edge and the element's own border goes. */
149
185
  border: 0;
150
186
  border-radius: 0;
151
- 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);
152
190
  clip-path: polygon(
153
191
  0 var(--_pixel-unit),
154
192
  var(--_pixel-unit) var(--_pixel-unit),
@@ -166,11 +204,68 @@
166
204
  font-family: var(--font-pixel, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
167
205
  }
168
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
+
169
236
  /* The clip takes the focus outline too, so focus is a second inset layer under
170
- 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. */
171
240
  .pixel-solo:focus-visible {
172
241
  box-shadow:
173
- inset 0 0 0 var(--_pixel-unit) var(--_pixel-ink),
242
+ inset 0 0 0 var(--_pixel-unit) var(--_solo-edge),
174
243
  inset 0 0 0 calc(var(--_pixel-unit) + var(--focus-ring-width, 3px))
175
- 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
+ }
176
271
  }
@@ -1,6 +1,6 @@
1
1
  /* Sketch: uneven elliptical corners, a solid ink edge, and a small hard shadow.
2
2
  The handwriting face is consumer-supplied, so this file has no font asset or
3
- network side effect. Load after the base stylesheet. */
3
+ network side effect. */
4
4
 
5
5
  /* The layer map, stated before anything uses a layer -- see `theme.css`. The
6
6
  aesthetic's token class sits in `components`, below every utility, so a
@@ -28,7 +28,10 @@
28
28
  --ui-corner-shape: round;
29
29
 
30
30
  --ui-ink: light-dark(var(--color-neutral-950), var(--color-neutral-50));
31
- --ui-control-ink: initial;
31
+ /* Controls take the theme's control border rather than the ink, which is the
32
+ primary colour in both themes; see `neobrutalism.css`. */
33
+ --ui-control-ink: var(--color-control-border);
34
+ --ui-rule-ink: initial;
32
35
  --ui-border-width: 1px;
33
36
  --ui-border-max: 100px;
34
37
  --ui-line-style: solid;
@@ -78,8 +81,14 @@
78
81
 
79
82
  /* Rounded variant: bounded elliptical corners, so the top and bottom edges of
80
83
  a wide element stay straight instead of bulging into an egg. On the region
81
- with `.sketch`, or on one component inside it. Knobs follow R8. */
82
- .sketch-rounded {
84
+ with `.sketch`, or on one component inside it. Knobs follow R8.
85
+
86
+ Never on a solo element, here or in the rotation below:
87
+ `.sketch-solo.sketch-rounded` paints its own outline, and a complete
88
+ radius declared on it would inherit into whatever it holds -- a `.btn`
89
+ inside a solo toast took a sketch outline on a page with no sketch in
90
+ scope. */
91
+ .sketch-rounded:not(.sketch-solo) {
83
92
  --ui-radius: var(--sketch-radius-rounded, 26px 10px 24px 11px / 12px 23px 11px 20px);
84
93
  --ui-radius-surface: var(--sketch-radius-surface-rounded, 38px 16px 36px 17px / 16px 34px 17px 32px);
85
94
  --ui-radius-pill: var(--sketch-radius-pill, 24px 16px 23px 17px / 17px 21px 16px 20px);
@@ -98,16 +107,16 @@
98
107
  given DOM order. A knob, when set, is one outline for every position. */
99
108
  :is(
100
109
  .sketch,
101
- .sketch-rounded,
102
- :is(.sketch, .sketch-rounded)
110
+ .sketch-rounded:not(.sketch-solo),
111
+ :is(.sketch, .sketch-rounded:not(.sketch-solo))
103
112
  :not(
104
- :is(.sketch, .sketch-rounded) :is(.neobrutalism, .glass, .pixel, .chunky-tile, .cyber),
105
- :is(.sketch, .sketch-rounded) :is(.neobrutalism, .glass, .pixel, .chunky-tile, .cyber) *
113
+ :is(.sketch, .sketch-rounded:not(.sketch-solo)) :is(.neobrutalism, .glass, .pixel, .chunky-tile, .cyber),
114
+ :is(.sketch, .sketch-rounded:not(.sketch-solo)) :is(.neobrutalism, .glass, .pixel, .chunky-tile, .cyber) *
106
115
  )
107
116
  ) {
108
117
  /* The pointed outlines: controls, fields, surfaces, and chips. */
109
- &:is(.sketch, .sketch *):not(.sketch-rounded, .sketch-rounded *) {
110
- &:is(.btn, .ipt, .textarea, .select, .input-group, .pre, .tooltip-bubble, .data-table) {
118
+ &:is(.sketch, .sketch *):not(.sketch-rounded:not(.sketch-solo), .sketch-rounded:not(.sketch-solo) *) {
119
+ &:is(.btn, .ipt, .textarea, .select, .input-group, .pre, .tooltip-bubble) {
111
120
  &:nth-child(4n + 2) {
112
121
  --ui-radius: var(--sketch-radius, 18px 240px 15px 255px / 240px 15px 255px 18px);
113
122
  }
@@ -139,7 +148,7 @@
139
148
  }
140
149
  }
141
150
 
142
- &:is(.card, .panel, .alert) {
151
+ &:is(.card, .panel, .alert, .data-table) {
143
152
  &:nth-child(4n + 2) {
144
153
  --ui-radius-surface: var(--sketch-radius-surface, 4px 75px 5px 80px / 75px 3px 80px 4px);
145
154
  }
@@ -169,8 +178,8 @@
169
178
  }
170
179
 
171
180
  /* The rounded outlines, in the same four places. */
172
- &:is(.sketch-rounded, .sketch-rounded *) {
173
- &:is(.btn, .ipt, .textarea, .select, .input-group, .pre, .tooltip-bubble, .data-table) {
181
+ &:is(.sketch-rounded:not(.sketch-solo), .sketch-rounded:not(.sketch-solo) *) {
182
+ &:is(.btn, .ipt, .textarea, .select, .input-group, .pre, .tooltip-bubble) {
174
183
  &:nth-child(4n + 2) {
175
184
  --ui-radius: var(--sketch-radius-rounded, 11px 25px 12px 24px / 23px 11px 22px 12px);
176
185
  }
@@ -202,7 +211,7 @@
202
211
  }
203
212
  }
204
213
 
205
- &:is(.card, .panel, .alert) {
214
+ &:is(.card, .panel, .alert, .data-table) {
206
215
  &:nth-child(4n + 2) {
207
216
  --ui-radius-surface: var(--sketch-radius-surface-rounded, 17px 36px 18px 34px / 33px 16px 32px 17px);
208
217
  }
@@ -274,19 +283,82 @@
274
283
  }
275
284
  }
276
285
 
277
- /* Solo: the look on one unstyled element. It uses standalone fallbacks rather
278
- than material tokens inherited from an aesthetic around it. */
286
+ /* Solo: the same look painted onto the one element carrying the class, for an
287
+ element the package does not style. See docs/internal/model.md#solo-classes and
288
+ the note on `.glass-solo`: it composes with the element's presentation,
289
+ intent, and elevation, and reads its material from its own knobs and
290
+ foundation tokens with literal fallbacks, never a shared material `--ui-*`
291
+ or `--elevation-color` token. Unlayered, and not for this package's own
292
+ components. */
279
293
  .sketch-solo {
280
- --_sketch-ink: light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0)));
281
-
282
- border: 1px solid var(--_sketch-ink);
294
+ /* The composition, the same in every solo class; `registry.test.ts` holds
295
+ the copies together. See the note on `.glass-solo`. */
296
+ --_solo-intent-color: initial;
297
+ --_solo-intent-contrast: initial;
298
+ --_solo-intent-strong: initial;
299
+ --_solo-intent-border: initial;
300
+ --_solo-intent-fill-max: initial;
301
+ --_solo-color: var(--_solo-intent-color, var(--color-text, light-dark(oklch(14.5% 0 0), oklch(98.5% 0 0))));
302
+ --_solo-contrast: var(
303
+ --_solo-intent-contrast,
304
+ var(--color-text-contrast, light-dark(oklch(98.5% 0 0), oklch(14.5% 0 0)))
305
+ );
306
+ --_solo-strong: var(--_solo-intent-strong, var(--color-text-strong, light-dark(oklch(20.5% 0 0), oklch(97% 0 0))));
307
+ --_solo-capped: min(var(--ui-fill, 0%), var(--_solo-intent-fill-max, 20%));
308
+ --_solo-fill: color-mix(in oklab, var(--_solo-color) var(--_solo-capped), transparent);
309
+ --_solo-on-fill: min(var(--ui-fg-on-fill, 0%), max(0%, calc(var(--_solo-capped) * 2 - 100%)));
310
+ --_solo-fg: color-mix(in oklab, var(--_solo-contrast) var(--_solo-on-fill), var(--_solo-strong));
311
+ /* The line with no intent on the element: this aesthetic's own ink. */
312
+ --_solo-ink: var(
313
+ --_solo-intent-border,
314
+ light-dark(var(--color-neutral-950, oklch(14.5% 0 0)), var(--color-neutral-50, oklch(98.5% 0 0)))
315
+ );
316
+ --_solo-edge: color-mix(in oklab, var(--_solo-ink) var(--ui-border, 100%), var(--_solo-fill));
317
+ --_solo-depth: calc(2px * var(--ui-elevation, 1));
318
+
319
+ border: 1px solid var(--_solo-edge);
283
320
  /* The surface shape: the element this exists for is usually a pane. An action
284
321
  takes the control shape below. */
285
322
  border-radius: var(--sketch-radius-surface, 80px 4px 75px 5px / 3px 70px 4px 80px);
286
- box-shadow: 2px 2px 0 0 var(--_sketch-ink);
323
+ /* The aesthetic's neutral depth cue, not the ink: the class casts its 2px
324
+ offset in the depth colour, and casting a solo pane in full ink would read
325
+ as a heavier, neobrutalist slab. The depth colour is a literal, for the reason
326
+ `.glass-solo` gives. */
327
+ box-shadow:
328
+ var(--_solo-depth) var(--_solo-depth) 0 0 light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55)),
329
+ inset 0 0 0 100vmax var(--_solo-fill);
287
330
  font-family: var(--font-sketch, cursive);
288
331
  }
289
332
 
333
+ /* An intent class on the element colours it, as on a component. Named rather
334
+ than read from `--intent-*` alone: those slots inherit, so a `.success`
335
+ container would colour a solo element where it leaves a component neutral. */
336
+ .sketch-solo:is(.primary, .secondary, .success, .warning, .destructive, .info) {
337
+ --_solo-intent-color: var(--intent-color);
338
+ --_solo-intent-contrast: var(--intent-contrast);
339
+ --_solo-intent-strong: var(--intent-strong);
340
+ --_solo-intent-border: var(--intent-border);
341
+ --_solo-intent-fill-max: var(--intent-fill-max);
342
+ }
343
+
344
+ /* The text colour is written only where a fill or an intent is asked for, on
345
+ the element or by a container's fill class. Anywhere else the element keeps
346
+ its own: a dark toast keeps its light text. */
347
+ .sketch-solo:is(
348
+ .solid,
349
+ .soft,
350
+ .ghost,
351
+ .primary,
352
+ .secondary,
353
+ .success,
354
+ .warning,
355
+ .destructive,
356
+ .info,
357
+ :is(.solid, .soft, .ghost) *
358
+ ) {
359
+ color: var(--_solo-fg);
360
+ }
361
+
290
362
  /* An action takes the expressive control shape. */
291
363
  .sketch-solo:is(
292
364
  button,
@@ -306,14 +378,14 @@
306
378
  }
307
379
 
308
380
  .sketch-solo.sketch-rounded:is(
309
- button,
310
- a[href],
311
- [role="button"],
312
- summary,
313
- input[type="button"],
314
- input[type="submit"],
315
- input[type="reset"]
316
- ) {
381
+ button,
382
+ a[href],
383
+ [role="button"],
384
+ summary,
385
+ input[type="button"],
386
+ input[type="submit"],
387
+ input[type="reset"]
388
+ ) {
317
389
  border-radius: var(--sketch-radius-rounded, 26px 10px 24px 11px / 12px 23px 11px 20px);
318
390
  }
319
391