@codenhub/styles 0.1.0 → 0.2.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 (51) hide show
  1. package/README.md +8 -69
  2. package/dist/aesthetics/chunky-tile.css +1 -1
  3. package/dist/aesthetics/index.css +1 -1
  4. package/dist/components.css +1 -1
  5. package/dist/index.css +1 -1
  6. package/dist/native.css +1 -1
  7. package/dist/theme.css +1 -1
  8. package/dist/tw/aesthetics/chunky-tile.css +20 -2
  9. package/dist/tw/box.css +45 -2
  10. package/dist/tw/components/button.css +38 -29
  11. package/dist/tw/components/feedback.css +58 -41
  12. package/dist/tw/components/form.css +139 -47
  13. package/dist/tw/components/index.css +16 -6
  14. package/dist/tw/components/loader.css +2 -16
  15. package/dist/tw/components/surface.css +122 -5
  16. package/dist/tw/components/tooltip.css +102 -91
  17. package/dist/tw/elevation.css +57 -14
  18. package/dist/tw/index.css +23 -5
  19. package/dist/tw/intent.css +36 -12
  20. package/dist/tw/native.css +4 -20
  21. package/dist/tw/reset.css +40 -32
  22. package/dist/tw/theme.css +46 -8
  23. package/dist/tw/utilities/content.css +104 -18
  24. package/dist/tw/utilities/index.css +1 -1
  25. package/dist/tw/utilities/layout.css +33 -0
  26. package/docs/accessibility.md +68 -48
  27. package/docs/changelog/0.1.0.md +37 -0
  28. package/docs/changelog/0.1.1.md +10 -0
  29. package/docs/changelog/0.2.0.md +41 -0
  30. package/docs/changelog/index.md +11 -0
  31. package/docs/concepts.md +21 -46
  32. package/docs/index.md +18 -45
  33. package/docs/integrating/astro.md +13 -7
  34. package/docs/integrating/index.md +25 -16
  35. package/docs/integrating/nextjs.md +31 -8
  36. package/docs/integrating/svelte.md +14 -8
  37. package/docs/integrating/tailwind.md +31 -15
  38. package/docs/integrating/vue.md +13 -7
  39. package/docs/setup.md +31 -32
  40. package/docs/usage/aesthetics.md +31 -107
  41. package/docs/usage/buttons.md +24 -36
  42. package/docs/usage/composing.md +31 -102
  43. package/docs/usage/content-and-layout.md +65 -83
  44. package/docs/usage/customizing.md +25 -90
  45. package/docs/usage/feedback.md +25 -56
  46. package/docs/usage/forms.md +48 -80
  47. package/docs/usage/index.md +9 -17
  48. package/docs/usage/theming.md +25 -81
  49. package/docs/usage/tooltips.md +55 -35
  50. package/llms-full.txt +1193 -1510
  51. package/package.json +121 -44
package/dist/tw/theme.css CHANGED
@@ -2,11 +2,8 @@
2
2
  @import "./intent.css";
3
3
  @import "./presentation.css";
4
4
  @import "./elevation.css";
5
- @source not "../dist";
6
- @source not "../tests";
7
- @source not "../dev";
8
- @source not "../debug";
9
- @source not "../playground";
5
+ /* Everything that is not `src/` -- see the note in `index.css`. Keeps a
6
+ `--color-*` reference in a docs example from shipping as a real theme entry. */
10
7
 
11
8
  /* Color tokens are declared once with `light-dark()`, which resolves at the
12
9
  point of use against the consuming element's computed `color-scheme`. Theme
@@ -27,7 +24,17 @@
27
24
  --color-accent-hover: light-dark(var(--color-neutral-700), var(--color-neutral-300));
28
25
  --color-accent-subtle: light-dark(var(--color-neutral-100), var(--color-neutral-800));
29
26
  --color-accent-strong: light-dark(var(--color-neutral-800), var(--color-neutral-200));
30
- --color-border: light-dark(var(--color-neutral-300), var(--color-neutral-700));
27
+ /* Light was neutral-300 (1.42:1 against the page, already under 3:1 by the
28
+ text-control comment below) until a `.card.soft.edged` measured its own
29
+ border against its own fill rather than against the page: 12% of
30
+ `--intent-color` mixed into the ground and a line resolving to nearly the
31
+ same tone landed the two 4 RGB units apart, which reads as no boundary at
32
+ all. Dark's neutral-700 pairing already separated -- 27 units, still under
33
+ 3:1 but at least legible -- so only light moved. Neutral-400 is not a new
34
+ number: `.chunky-tile` already measured it at 2.48:1 against the light
35
+ page and picked it for the same reason, "the heaviest grey that still
36
+ leaves a field legible." Dark is unchanged. */
37
+ --color-border: light-dark(var(--color-neutral-400), var(--color-neutral-700));
31
38
  --color-border-hover: light-dark(var(--color-neutral-600), var(--color-neutral-500));
32
39
 
33
40
  /* A text control's line is the only thing identifying it: the box is
@@ -50,7 +57,9 @@
50
57
  --color-text-secondary: light-dark(var(--color-neutral-700), var(--color-neutral-400));
51
58
  --color-text-contrast: light-dark(var(--color-neutral-50), var(--color-neutral-950));
52
59
  --color-text-hover: light-dark(var(--color-neutral-700), var(--color-neutral-200));
53
- --color-text-subtle: light-dark(var(--color-neutral-200), var(--color-neutral-900));
60
+ /* No `--color-text-subtle`: the neutral intent points `--intent-subtle` at
61
+ `--color-surface`, not at a text-family tint, so the slot the other families
62
+ fill had no reader here. */
54
63
  --color-text-strong: light-dark(var(--color-neutral-900), var(--color-neutral-100));
55
64
 
56
65
  /* The tooltip bubble is the one component whose plate is chosen rather than
@@ -215,4 +224,33 @@
215
224
  color-scheme: dark;
216
225
  }
217
226
 
218
- @custom-variant dark (&:where(.dark, .dark *, [data-theme="dark"], [data-theme="dark"] *));
227
+ /* The first arm mirrors the `color-scheme: dark` block above, all three
228
+ spellings included -- a consumer writing `.theme-dark` for the token theme
229
+ was otherwise left without the matching `dark:` variant.
230
+
231
+ The second arm closes the gap that left open: `--color-*` tokens resolve
232
+ through `light-dark()` against the computed `color-scheme`, so with no
233
+ explicit selector at all `:root`'s `color-scheme: light dark` already
234
+ follows the OS preference -- see `concepts.md#theme-selection`. A selector
235
+ list cannot query computed style the way `light-dark()` does, but it can
236
+ still match "OS prefers dark, and not under a forced-light subtree" with a
237
+ plain descendant exclusion: `:not(:where(.light, .light *, ...))` matches
238
+ only when no ancestor (or the element itself) carries one of the three
239
+ light spellings. That is the one case the exclusion cannot see -- a
240
+ subtree forced light through some means other than these three selectors,
241
+ where `light-dark()` would still honor it but this variant could not
242
+ detect it. Out of contract: those three spellings are the only supported
243
+ way to force a theme, so nothing else is expected to set `color-scheme`
244
+ directly. Each arm is independent, so an explicit selector still wins at
245
+ any depth even inside a subtree the other arm would otherwise match. */
246
+ @custom-variant dark {
247
+ &:where(.dark, .dark *, .theme-dark, .theme-dark *, [data-theme="dark"], [data-theme="dark"] *) {
248
+ @slot;
249
+ }
250
+
251
+ @media (prefers-color-scheme: dark) {
252
+ &:not(:where(.light, .light *, .theme-light, .theme-light *, [data-theme="light"], [data-theme="light"] *)) {
253
+ @slot;
254
+ }
255
+ }
256
+ }
@@ -37,14 +37,17 @@
37
37
  ghost table is boundaries and type and nothing else -- which is what `.ghost`
38
38
  is for on this component, since the published resting fill is `soft`.
39
39
 
40
- Any fill at all brings the ground back whole rather than in proportion. A
41
- soft head mixed onto the page instead of onto `--intent-subtle` is a
42
- different colour from the one this component has always drawn, and the fill
43
- is meant to read as a step past the tint rather than as a replacement for
44
- it. A saturating ramp says that, because the signal is a percentage and
45
- there is no branch to hang off one: the smallest fill any presentation asks
46
- for is 12%, so a multiplier of 100 puts every non-zero fill over the top and
47
- leaves zero where it is.
40
+ Zero stays zero, and nothing past it saturates the same way any more. A
41
+ `* 100` multiplier used to put every non-zero fill over the `min(100%, ...)`
42
+ ceiling in one step -- `.soft`'s 12% and `.solid`'s 100% both landed on a
43
+ fully opaque plate, so the only thing left to tell them apart was how much
44
+ `--intent-color` got mixed onto it after (`--table-fill`, 12% vs 20% capped),
45
+ a difference too small to read as the volume-level step presentation
46
+ promises everywhere else in the model. `* 5` keeps zero at zero and a
47
+ `.solid`'s 100% raw fill still clears the ceiling -- the same fully opaque
48
+ head it always drew -- but stops `.soft`'s 12% short, at 60%: a visibly
49
+ lighter, partly-page-showing-through plate instead of a second opaque one,
50
+ which is what makes the two presentations separate again.
48
51
 
49
52
  It reads `--ui-fill` rather than `--table-fill` because that is the question
50
53
  being asked: what the presentation wanted, not what the intent cap and an
@@ -53,10 +56,48 @@
53
56
  the raw class value also keeps the expression a `calc()` over one `var()`
54
57
  rather than over `--table-fill`'s own `calc()`/`min()` chain, which matters
55
58
  for the reason `--table-rule-plated` below records. */
56
- --table-ground: min(100%, calc(var(--ui-fill, var(--_d-fill)) * 100));
59
+ --table-ground: min(100%, calc(var(--ui-fill, var(--_d-fill)) * 5));
57
60
  --table-head-plate: color-mix(in oklab, var(--intent-subtle) var(--table-ground), transparent);
58
61
  --table-head-bg: color-mix(in oklab, var(--intent-color) var(--table-fill), var(--table-head-plate));
59
62
  --table-head-fg: color-mix(in oklab, var(--intent-contrast) var(--_on-fill), var(--intent-strong));
63
+
64
+ /* Untinted rather than mixed, the same call `.card.soft` makes: 12% ink onto
65
+ `--intent-subtle` still reads as `.solid` on neutral, since neutral's ink
66
+ and ground are both on the same greyscale axis and no ramp over that ground
67
+ changes it much. A head has no hover or interaction state to reveal a tint
68
+ into the way a card does on `.interactive`/`.hoverable`, so there is no
69
+ second state to move a mechanism to -- `.soft` is `--color-foreground`,
70
+ flat, always, for every intent. That mutes a colored `.success`/`.destructive`
71
+ head at rest the way it mutes a non-interactive `.card.soft`; accepted for
72
+ the same reason, not solved a different way.
73
+
74
+ The table's own base plate rests the same way, for a reason the head fix
75
+ alone did not cover: a `.soft` head landing at `--color-foreground` (five
76
+ units off the page) sitting directly above a body still carrying the
77
+ ordinary 12% tint (twenty-four units past the head) read backwards -- the
78
+ seam that should read as one table read as the head belonging to the page
79
+ and the body being the odd one out. `--_fill-cap` is `box.css`'s override
80
+ slot (the same one `.card.soft` uses, for the same cascade-layer reason
81
+ the comment there gives), and `--_d-ground` is box's own private ground
82
+ slot, unclaimed here until now -- both land on the table's own element,
83
+ same as `--table-fill`/`--table-head-plate` do, so nothing here fights
84
+ `.soft` for `--ui-fill` either. `.ghost` is left at box's default
85
+ (`--_d-ground: transparent`), so it stays the fully bare boundaries-only
86
+ table it already was -- `.soft` now differs from it by keeping an opaque,
87
+ untinted plate and a head label strip, not by how much ink is in either.
88
+
89
+ `:not(.solid, .ghost)` rather than `.soft`: the published resting fill is
90
+ `soft`, so a bare `<table class="data-table">` -- the members table in
91
+ `playground/app-shell/`, with no fill class at all -- is the common case
92
+ this has to reach, not the edge case. `.soft` alone would miss every table
93
+ that gets soft by doing nothing. */
94
+ &:not(.solid, .ghost) {
95
+ --table-fill: 0%;
96
+ --table-head-plate: var(--color-foreground);
97
+ --_fill-cap: 0%;
98
+ --_d-ground: var(--color-foreground);
99
+ }
100
+
60
101
  /* How much rule there is between body rows, and none unless asked for. Ruling
61
102
  every row is a style choice rather than part of what a table is, and tying it
62
103
  to the edge axis made it arrive by implication instead: a table drew rules
@@ -148,14 +189,21 @@
148
189
  color: var(--table-head-fg);
149
190
  }
150
191
 
151
- /* Every cell carries the hairline; what varies is whether it is painted. A rule
152
- is structure rather than the silhouette, so it stays hairline under an
153
- aesthetic that draws a thick edge, and it survives a fill that would
154
- otherwise paint over it. */
192
+ /* Every cell carries the hairline; what varies is whether it is painted. The
193
+ width tracks the aesthetic's own line weight (`--_width`, the same token
194
+ `box` uses for the table's outer frame) rather than a fixed `1px`, so a
195
+ row rule reads as the same material as the boundary around it instead of
196
+ as a leftover default under an aesthetic whose whole point is thickness --
197
+ `.chunky-tile`'s 2px frame used to sit over a 1px row rule, which read as
198
+ unstyled rather than as a deliberate, quieter line. It still survives a
199
+ fill that would otherwise paint over it, because color and width are
200
+ independent here: `.ruleless` zeroes the color, not this. */
155
201
  & th,
156
202
  & td {
157
- @apply border-b px-4 py-3 align-top;
203
+ @apply px-4 py-3 align-top;
158
204
 
205
+ border-bottom-style: solid;
206
+ border-bottom-width: var(--_width);
159
207
  border-color: var(--table-rule);
160
208
  }
161
209
 
@@ -173,8 +221,8 @@
173
221
  }
174
222
 
175
223
  & tfoot tr:first-child > * {
176
- @apply border-t;
177
-
224
+ border-top-style: solid;
225
+ border-top-width: var(--_width);
178
226
  border-top-color: var(--table-boundary);
179
227
  }
180
228
 
@@ -247,6 +295,20 @@
247
295
  itself, and amber-600 on amber-100 is a smudge. `box` now mixes toward
248
296
  `--intent-strong`, which is what these three were reaching for, so the three
249
297
  copies are gone and the ground is the only thing left to declare. */
298
+ /* Neutral, for the same reason `.panel`'s own carve-out is: 12% of
299
+ `--color-text` mixed onto `--intent-subtle` (`--color-surface` for
300
+ neutral) reads as an assertive slab rather than a quiet chip -- measured,
301
+ `199, 199, 199` on a light page whose own `--color-surface` step is
302
+ `229, 229, 229`, further off than the ground alone already carries a key
303
+ cap. A named intent keeps its wash unchanged: a colored chip needs it to
304
+ separate from the others the way `.ghost` being unsupported here already
305
+ protects (dropping *all* fill collapsed every intent's ghost chip onto the
306
+ same near-page `--intent-subtle` tone) -- the neutral case has nothing to
307
+ separate from, so it alone drops the extra ink. The ground itself is not
308
+ moved the way `.panel`'s is: `--intent-subtle` already resolves to
309
+ `--color-surface` for neutral, a real, visible step past the page on its
310
+ own, so only the wash on top of it goes. `:not(.solid)` so an explicit
311
+ `.solid` chip is never clamped to a cap it did not ask for. */
250
312
  @utility kbd {
251
313
  @apply box inline-flex items-center px-1.5 py-0.5 font-mono text-xs font-medium;
252
314
 
@@ -260,6 +322,10 @@
260
322
  cap this small would read as a lozenge. A squaring aesthetic still reaches it. */
261
323
  border-radius: min(var(--ui-radius, var(--radius-small)), var(--radius-small));
262
324
  clip-path: var(--ui-clip-tight, var(--ui-clip, none));
325
+
326
+ &:not(.primary, .secondary, .success, .warning, .destructive, .danger, .error, .info, .solid) {
327
+ --_fill-cap: 0%;
328
+ }
263
329
  }
264
330
 
265
331
  /* The same plate with no line around it. */
@@ -274,6 +340,10 @@
274
340
 
275
341
  border-radius: min(var(--ui-radius, var(--radius-small)), var(--radius-small));
276
342
  clip-path: var(--ui-clip-tight, var(--ui-clip, none));
343
+
344
+ &:not(.primary, .secondary, .success, .warning, .destructive, .danger, .error, .info, .solid) {
345
+ --_fill-cap: 0%;
346
+ }
277
347
  }
278
348
 
279
349
  /* And at block scale, where the radius is a control's rather than a chip's. */
@@ -285,6 +355,10 @@
285
355
  --_d-border: 0%;
286
356
  --_d-elevation: 0;
287
357
  --_d-ground: var(--intent-subtle);
358
+
359
+ &:not(.primary, .secondary, .success, .warning, .destructive, .danger, .error, .info, .solid) {
360
+ --_fill-cap: 0%;
361
+ }
288
362
  }
289
363
 
290
364
  /* Typography wearing a component's name, so it composes none of `box`: `box`
@@ -306,7 +380,11 @@
306
380
  --_d-fg-on-fill: 0%;
307
381
  --_d-border: 100%;
308
382
  --_d-elevation: 0;
309
- --quote-fill: calc(min(var(--ui-fill, var(--_d-fill)), var(--intent-fill-max, 100%)) * var(--ui-bg-alpha, 1));
383
+ /* Presentation capped by the intent, the same `--_capped` `box` composes, kept
384
+ separate from `--quote-fill` because the ink ramp below reads it before
385
+ `--ui-bg-alpha` thins it. */
386
+ --quote-capped: min(var(--ui-fill, var(--_d-fill)), var(--intent-fill-max, 100%));
387
+ --quote-fill: calc(var(--quote-capped) * var(--ui-bg-alpha, 1));
310
388
  --quote-bg: color-mix(in oklab, var(--intent-color) var(--quote-fill), transparent);
311
389
  /* The same edge blend `box` draws, in the same order and for the same reason:
312
390
  the blend is the line's colour, and how much line there is comes after it.
@@ -327,9 +405,17 @@
327
405
  background-color: var(--quote-bg);
328
406
  border-inline-start-color: var(--quote-edge);
329
407
  border-inline-start-width: var(--quote-width);
408
+ /* The ink is gated by the plate, not tied to the fill: `--ui-fg-on-fill` is the
409
+ ceiling, and the contrast tone only ramps in past a half fill, reaching full
410
+ at a full fill. Below that the ink stays `--intent-strong`, the tone chosen
411
+ to read on the page. This is `box`'s `--_on-fill` exactly -- `min(asked,
412
+ --intent-fill-max)` read plausibly and was wrong at the bottom of the range,
413
+ printing a capped-neutral `.solid` quote at ~6.6:1 where the ramp gives
414
+ ~11:1, and this renderer has to agree with `box`. */
330
415
  color: color-mix(
331
416
  in oklab,
332
- var(--intent-contrast) min(var(--ui-fg-on-fill, var(--_d-fg-on-fill)), var(--intent-fill-max, 100%)),
417
+ var(--intent-contrast)
418
+ min(var(--ui-fg-on-fill, var(--_d-fg-on-fill)), max(0%, calc(var(--quote-capped) * 2 - 100%))),
333
419
  var(--intent-strong)
334
420
  );
335
421
 
@@ -3,4 +3,4 @@
3
3
  @import "./layout.css";
4
4
  @import "./content.css";
5
5
 
6
- @source inline("view vertical horizontal stack tight loose cluster between auto-grid section section-content narrow wide divider selection-contrast table-wrap data-table kbd quote quote-inline code pre");
6
+ @source inline("view vertical horizontal stack tight loose cluster between auto-grid section section-content narrow wide divider selection-contrast visually-hidden table-wrap data-table kbd quote quote-inline code pre");
@@ -105,3 +105,36 @@
105
105
  color: var(--color-primary);
106
106
  }
107
107
  }
108
+
109
+ /* The gap this closes: `sr-only` is Tailwind's own name and the one a consumer
110
+ reaches for out of habit, but on the vanilla, ready-to-import entries (`.`,
111
+ `/native`) there is no Tailwind utility layer to reach -- those compile real
112
+ CSS from a scan of `src/` alone, which used the class nowhere, so `sr-only`
113
+ silently did nothing there. It worked fine on a `/tw` entry, where the
114
+ consumer's own Tailwind build supplies it, and that split made the gap easy
115
+ to miss: it read as a package decision rather than a hole in one build and
116
+ not the other.
117
+
118
+ Not published as `.sr-only` itself. `.table` already did that once --
119
+ shadowing Tailwind's own `display: table` utility until a consumer needed
120
+ the one this package ate -- and `registry.test.ts`'s collision check exists
121
+ because of it (see `data-table`'s `renamedFrom` in `registry.json`). Naming
122
+ this `sr-only` would trip the same trap from the other direction: on a `/tw`
123
+ entry a real Tailwind build runs too, and two same-named utilities in the
124
+ same cascade is exactly the silent collision that test polices.
125
+ `.visually-hidden` says the same thing under a name nothing else claims,
126
+ using the standard clip-based recipe. `@source inline("visually-hidden")` in
127
+ `utilities/index.css` force-emits it on the entries with no scannable markup
128
+ of their own, the same mechanism that file's inline list already uses for
129
+ every layout and content utility name in it. */
130
+ @utility visually-hidden {
131
+ position: absolute;
132
+ width: 1px;
133
+ height: 1px;
134
+ padding: 0;
135
+ margin: -1px;
136
+ overflow: hidden;
137
+ clip: rect(0, 0, 0, 0);
138
+ white-space: nowrap;
139
+ border-width: 0;
140
+ }
@@ -1,54 +1,72 @@
1
1
  ---
2
2
  title: Accessibility
3
+ order: 5
3
4
  ---
4
5
 
5
6
  # Accessibility responsibilities
6
7
 
7
8
  This package provides CSS hooks for accessible states. It does not provide semantic HTML, ARIA attributes, keyboard behavior, focus management, validation, announcement timing, or JavaScript behavior.
8
9
 
9
- ## Provided By CSS
10
-
11
- | Feature | Behavior |
12
- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- |
13
- | `:focus-visible` | Global focus-visible outline using `--focus-ring`, `--focus-ring-width`, and `--focus-ring-offset`. |
14
- | Form control focus | `.ipt`, `.textarea`, and `.select` draw the global focus ring and move their line to the intent color. |
15
- | Control boundaries | Text controls rest at `--_line-rest` of their intent, under 3:1 by choice; hover and focus take the tone whole. |
16
- | Toggle boundaries | `.checkbox`, `.radio`, and `.switch` draw their line whole, because it is the only thing marking an unchecked box. |
17
- | Cascaded `.edgeless` | Floored on every text control: a container cannot erase the line of a field or toggle nobody classed. |
18
- | Own `.edgeless` | On the element, `.ipt`, `.textarea`, `.select`, and `.switch` do drop their line and do not meet 1.4.11 at rest. |
19
- | `.checkbox`/`.radio` | Never drop their line, on the element or from a container. `.edgeless` on them is unsupported, not merely discouraged. |
20
- | Invalid controls | `[aria-invalid="true"]` applies destructive border/focus color on form controls. |
21
- | Disabled controls | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]`, and `.disabled` apply disabled cursor/opacity where supported. |
22
- | Current page | `[aria-current="page"]` applies primary color and stronger font weight. |
23
- | Open state | `[data-state="open"]` styles supported surfaces and tooltips. |
24
- | Reduced-motion loaders | Loader variants use animated embedded SVGs normally and static masks when `prefers-reduced-motion: reduce` matches. |
25
- | Reduced-motion document | The reset additionally shortens document animations and transitions when `prefers-reduced-motion: reduce` matches. |
26
- | Reduced transparency | `.glass` drops its blur and becomes an opaque surface when `prefers-reduced-transparency: reduce` matches. |
27
- | Filled labels | Every intent's `.solid` label meets 1.4.3 (4.5:1) as normal text, in both themes. Lowest is success at 5.14:1. |
28
- | Warning ink | `--color-warning-contrast` is the near-black tone, not the page tone. Amber dark enough to carry white is not amber. |
29
- | Partial fills | Contrast ink appears only past a half fill, so a capped plate keeps `--intent-strong` rather than walking toward the page. |
30
- | Intent hue in dark | `--intent-strong` is the `-300` shade in dark, so a soft or ghost label keeps its own hue instead of reading white. |
31
- | Toggle marks | A checked mark meets 3:1 as a state indicator on every plate its fill class can paint, in both themes. |
32
- | Forced colors | `forced-colors: active` preserves visible borders and checked states using system colors. |
33
- | Text direction | Quote bar, alert icon, progress fill, vertical divider, and switch knob mirror under `dir="rtl"`. No class needed. |
34
-
35
- The loader fallback is embedded in loader CSS, so focused imports that include
36
- loaders honor reduced motion without the reset. This includes
37
- `@codenhub/styles/components`, `@codenhub/styles/tw/components`,
38
- `@codenhub/styles/tw/button`, and `@codenhub/styles/tw/loader`. The complete and
39
- native entrypoints include both that fallback and the reset's broader animation
40
- and transition shortening.
41
-
42
- In forced colors, class-based form controls including `.text-control` receive a
43
- 2px `Highlight` system-color focus outline. The native entrypoint provides the
44
- same visible system outline for unclassed text inputs, selects, textareas,
45
- checkboxes, and radios.
46
-
47
- Custom checkboxes and radios use the system `Canvas`, `CanvasText`, `Highlight`,
48
- and `HighlightText` colors in forced-colors mode so checked and unchecked states
49
- remain distinct. Shape, size, and spacing remain unchanged.
50
-
51
- ## Required Outside CSS
10
+ ## Provided by CSS
11
+
12
+ ### Focus and state
13
+
14
+ | Feature | Behavior |
15
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | `:focus-visible` | Global focus-visible outline using `--focus-ring`, `--focus-ring-width`, and `--focus-ring-offset`. |
17
+ | Form control focus | `.ipt`, `.textarea`, and `.select` draw the global focus ring and move their line to the intent color. |
18
+ | Invalid controls | `[aria-invalid="true"]` applies destructive border/focus color on form controls. |
19
+ | Disabled controls | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]`, and `.disabled` apply disabled cursor/opacity where supported. |
20
+ | Open state | `[data-state="open"]` styles supported surfaces and tooltips. |
21
+ | Native `<dialog>` | `dialog:not([open])` and a centered `dialog[open]` are restored in author origin, so `.card` composed onto a `<dialog>` (the closest primitive the package ships) does not render open on load or lose its centering to another rule. Focus trapping, escape handling, and inert-background behavior stay outside this package. |
22
+
23
+ ### Visually hidden content
24
+
25
+ `.visually-hidden` hides content from sighted users while keeping it in the accessibility tree, using the standard clip-based recipe. It ships wherever the layout utilities do -- `.`, `./native`, `./tw`, `./tw/native`, and `./tw/utilities` -- including the vanilla entries with no Tailwind utility layer of their own; it is not part of `./components` or the narrower `./tw/components`/`./tw/*` component slices. See [Content and layout](./usage/content-and-layout.md).
26
+
27
+ ### Control boundaries
28
+
29
+ | Feature | Behavior |
30
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
31
+ | Control boundaries | Text controls rest at `--_line-rest` of their intent, under 3:1 by choice; hover and focus take the tone whole. |
32
+ | Toggle boundaries | `.checkbox`, `.radio`, and `.switch` draw their line whole, because it is the only thing marking an unchecked box. |
33
+ | Cascaded `.edgeless` | Floored on every text control: a container cannot erase the line of a field or toggle nobody classed. |
34
+ | Own `.edgeless` | On the element, `.ipt`, `.textarea`, `.select`, and `.switch` do drop their line and do not meet 1.4.11 at rest, except `.switch.ghost`: `.ghost` already asks for zero fill, so dropping the line too would leave no boundary at all, and `.switch.ghost.edgeless` keeps it up instead. |
35
+ | `.checkbox`/`.radio` | Never drop their line, on the element or from a container. `.edgeless` on them is unsupported, not merely discouraged. |
36
+
37
+ ### Motion and transparency
38
+
39
+ | Feature | Behavior |
40
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
41
+ | Reduced-motion loaders | Loader variants use animated embedded SVGs normally and static masks when `prefers-reduced-motion: reduce` matches. |
42
+ | Reduced-motion document | The reset additionally shortens document animations and transitions when `prefers-reduced-motion: reduce` matches. |
43
+ | Reduced transparency | `.glass` drops its blur and becomes an opaque surface when `prefers-reduced-transparency: reduce` matches. |
44
+
45
+ The loader fallback is embedded in loader CSS, so focused imports that include loaders honor reduced motion without the reset. This includes `@codenhub/styles/components`, `@codenhub/styles/tw/components`, and `@codenhub/styles/tw/loader`. The complete and native entrypoints include both that fallback and the reset's broader animation and transition shortening.
46
+
47
+ ### Color and contrast
48
+
49
+ | Feature | Behavior |
50
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | Filled labels | Every intent's `.solid` label meets 1.4.3 (4.5:1) as normal text, in both themes. Lowest is success at 5.14:1. |
52
+ | Warning ink | `--color-warning-contrast` is the near-black tone, not the page tone. Amber dark enough to carry white is not amber. |
53
+ | Partial fills | Contrast ink appears only past a half fill, so a capped plate keeps `--intent-strong` rather than walking toward the page. |
54
+ | Intent hue in dark | `--intent-strong` is the `-300` shade in dark, so a soft or ghost label keeps its own hue instead of reading white. |
55
+ | Toggle marks | A checked mark meets 3:1 as a state indicator on every plate its fill class can paint, in both themes. |
56
+ | Soft-edged border | `.alert`, `.panel`, `.badge`, `.kbd`, and `.code`/`.pre`'s `.edged` border blends toward the component's own fill; at `.soft`'s 12% fill this misses 3:1 against the fill for some intents, and which ones misses depends on the theme. Known, not fixed -- `--color-border`/`--intent-border` is a shared blend seam every one of them reads, so a targeted fix would need to touch it for one component without moving it for the rest. |
57
+
58
+ ### Forced colors and direction
59
+
60
+ | Feature | Behavior |
61
+ | -------------- | ------------------------------------------------------------------------------------------------------------------ |
62
+ | Forced colors | `forced-colors: active` preserves visible borders and checked states using system colors. |
63
+ | Text direction | Quote bar, alert icon, progress fill, vertical divider, and switch knob mirror under `dir="rtl"`. No class needed. |
64
+
65
+ In forced colors, class-based form controls including `.text-control` receive a 2px `Highlight` system-color focus outline. The native entrypoint provides the same visible system outline for unclassed text inputs, selects, textareas, checkboxes, and radios.
66
+
67
+ Custom checkboxes and radios use the system `Canvas`, `CanvasText`, `Highlight`, and `HighlightText` colors in forced-colors mode so checked and unchecked states remain distinct. Shape, size, and spacing remain unchanged.
68
+
69
+ ## Required outside CSS
52
70
 
53
71
  Use semantic HTML and behavior appropriate for the component.
54
72
 
@@ -58,25 +76,27 @@ Use semantic HTML and behavior appropriate for the component.
58
76
  | Forms | Use labels, `type`, validation logic, `aria-describedby`, and error message relationships. |
59
77
  | Alerts | Add `role="status"` or `role="alert"` based on announcement urgency. |
60
78
  | Toasts | Add live-region behavior, dismissal behavior, focus rules, and pause/timeout logic when needed. |
61
- | Tooltips | Provide accessible names/descriptions. CSS pseudo-element content is not enough for all assistive tech. |
79
+ | Tooltips | Give the bubble a real `id` and `role="tooltip"`, and reference it from the trigger's `aria-describedby`. |
62
80
  | Progress | Use semantic progress elements or ARIA values when numeric progress must be announced. |
63
81
  | Skeletons/loaders | Mark decorative loading visuals with `aria-hidden="true"` and expose loading state elsewhere when needed. |
64
82
  | Popovers/modals | Provide focus trapping, escape handling, inert background behavior, labels, and roles outside this CSS package. |
65
83
 
66
- ## State Attribute Guidance
84
+ ## State attribute guidance
67
85
 
68
86
  Prefer native attributes first:
69
87
 
70
88
  ```html
71
- <button class="btn primary" disabled>Saving</button>
72
- <input class="ipt" aria-invalid="true" aria-describedby="email-error" />
89
+ <button class="btn primary" disabled>Saving</button> <input class="ipt" aria-invalid="true" aria-describedby="email-error" />
73
90
  ```
74
91
 
75
92
  Use ARIA or data attributes when native attributes are not available for the element or library:
76
93
 
77
94
  ```html
78
95
  <a class="btn secondary" aria-disabled="true">Unavailable</a>
79
- <button class="tooltip tooltip-icon" data-state="open" data-tooltip="More details" aria-label="More details">?</button>
96
+ <span class="tooltip" data-state="open">
97
+ <span class="tooltip-icon" tabindex="0" aria-label="More details" aria-describedby="more-details-bubble">?</span>
98
+ <span class="tooltip-bubble" role="tooltip" id="more-details-bubble">More details</span>
99
+ </span>
80
100
  ```
81
101
 
82
102
  `aria-disabled="true"` communicates disabled state but does not prevent activation. JavaScript or element choice must prevent activation when required.
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: 0.1.0
3
+ date: 2026-08-31
4
+ description: The three-axis styling model, split presentation, opt-in aesthetics, and Tailwind v4 source entrypoints.
5
+ ---
6
+
7
+ # 0.1.0
8
+
9
+ `0.1.0` replaces the model published in `0.0.4` wholesale. The old model had three orthogonal axes with per-component clamps and no chosen default: a plain element got whatever `var()` fallback each component happened to declare. This release makes each decision explicitly and is the baseline the `0.x` line builds on. Nearly everything below is breaking against `0.0.4`; there are no compatibility aliases.
10
+
11
+ ## Changed
12
+
13
+ - **Every element now composes from three named axes.** _Intent_ (`.primary`, `.success`, `.destructive`, …) sets hue only. _Presentation_ sets how much of that hue fills the element and whether it draws an edge. _Aesthetic_ sets the material — radius, edge width, shadow, silhouette, type. Intent is redeclared per component; presentation and aesthetic cascade, so a container can set the look of a subtree while any element opts out. [Concepts](../concepts.md) and [Composing](../usage/composing.md) describe the full model.
14
+ - **Presentation is two independent questions.** Fill is `.solid` / `.soft` / `.ghost`; edge is `.edged` / `.edgeless`. Exactly one of each applies, and they never decide each other. The common web button — a subtle fill with a border — is `.soft.edged`, which had no spelling before.
15
+ - **`.bare` is renamed `.ghost`.** `.bare` and `.edgeless` both read as "less of something", so the name is now a fill and only a fill: no fill at rest, a tint on hover.
16
+ - **Hover is derived, not declared.** A hovered element takes its resting fill plus one published step and its `--intent-hover` colour. There are no hover tokens to set. An outline button therefore tints on hover rather than filling completely.
17
+ - **A sunk text field is `.soft.edgeless`.** Two classes saying two things, where `.soft` alone used to silently suppress the field's border.
18
+ - **Table row rules are opt-in via `.ruled`.** Rules no longer arrive by implication from a component's edge default. The head and foot separators stay at every presentation.
19
+ - **Depth is a modifier, not part of a component.** `surface` no longer carries a structural shadow, so cards, buttons, and chips on a plain page rest flat. Use `.raised` / `.floating` (or `.elevation-sm` / `.elevation-md`) to add depth, and `.flat` to remove an aesthetic's.
20
+ - **The Tailwind v4 entrypoint moved from `/overrides` to `/native`.**
21
+
22
+ ## Added
23
+
24
+ - **Opt-in aesthetics.** `.neobrutalism`, `.glass`, `.pixel`, and `.chunky-tile`, each shipped as its own entrypoint (`@codenhub/styles/aesthetics/pixel`, …) and as an aggregate (`@codenhub/styles/aesthetics`). An aesthetic sets material tokens only; it never repaints hue. See [Aesthetics](../usage/aesthetics.md).
25
+ - **An elevation modifier.** `.flat` / `.raised` / `.floating` (and the `.elevation-none` / `-sm` / `-md` names) multiply an aesthetic's shadow geometry. Unitless, so a container can flatten a whole toolbar and any element can opt back in.
26
+ - **Granular Tailwind v4 source entrypoints.** `@codenhub/styles/tw` and per-family subpaths (`/tw/button`, `/tw/form`, `/tw/feedback`, `/tw/tooltip`, `/tw/utilities`, `/tw/aesthetics/*`, …) for consumers compiling their own build. `package.json` is also exported.
27
+ - **`.input-group`.** A wrapper that draws one box and lets the control inside give up its own paint, so an icon can sit alongside a field. The package ships no icons — any element works in the slot.
28
+ - **`registry.json`.** A machine-readable record of every supported class, each component's default fill/edge/elevation, the axes it reads, and every bound and exception with its reason.
29
+ - **`--intent-strong`**, the intent tone chosen to be read on a page, used wherever a fill is partial.
30
+
31
+ ## Removed
32
+
33
+ - **`.out` and `.fill`.** Outline is now `.ghost.edged`; there is no class that makes an outline button fill completely on hover.
34
+ - **`--ui-border-scale` / `.out`'s doubled border.** The edge width comes from the aesthetic and nothing else, which removed the per-component border ceilings small components needed.
35
+ - **`--color-text-subtle`**, which nothing read.
36
+ - **`--layout-stack-gap` and `--layout-cluster-gap`**, replaced by a single `--layout-gap`.
37
+ - **The bundled input-field icons.** Icons were a `background-image` data URI with a light and a dark copy per glyph; they are gone, and `.input-group` carries a consumer-supplied icon instead.
@@ -0,0 +1,10 @@
1
+ ---
2
+ title: 0.1.1
3
+ date: 2026-09-10
4
+ ---
5
+
6
+ # 0.1.1
7
+
8
+ ## Fixed
9
+
10
+ - `.p-xs` and its alias `.dense` now clear a button's `--control-height` minimum, so a chip-sized button fits inside a table cell or a dense toolbar. Before this, every button kept the full control-height floor even at the tightest padding, so a button that small could not be built. Every other size and padding class still keeps the floor, so a plain button is unchanged. An icon-only `.p-xs`/`.dense` button also gives up its `min-width`; size the glyph yourself, as the package sizes no icons.
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: 0.2.0
3
+ date: 2026-09-14
4
+ ---
5
+
6
+ # 0.2.0
7
+
8
+ Fixes from a stress-test pass composing real screens (`app-shell`, `form`, `settings`) against the published `0.1.x` model. Several change default token values rather than adding new surface, so this lands as one minor rather than a run of patches -- see [Changed](#changed) for what an upgrader needs to check.
9
+
10
+ ## Added
11
+
12
+ - `.visually-hidden`, a utility that hides content from sighted users while keeping it in the accessibility tree. Ships wherever the layout utilities do (`.`, `/native`, `/tw`, `/tw/native`, `/tw/utilities`), including the vanilla entries that have no Tailwind utility layer of their own; not part of `/components` or the narrower `/tw/*` component slices.
13
+ - `.tooltip-bubble`, a real element for a tooltip's message that can carry its own `id` and be referenced by `aria-describedby`.
14
+ - `.alert-icon`, sizing and alignment for a consumer-supplied icon element dropped into `.alert` -- any inline SVG, `<img>`, or icon-set class works, not just a built-in glyph.
15
+ - `dialog:not([open])`/`dialog[open]` restated in author origin, so `.card` composed onto a `<dialog>` hides until opened and centers when shown, without a dialog primitive of its own.
16
+ - `.icon` added to `registry.json` under `modifiers.shape`. The button shape modifier was already shipped and documented; it was missing only from the machine-readable contract.
17
+ - `.switch.ghost.edgeless` now renders a track instead of disappearing.
18
+
19
+ ## Changed
20
+
21
+ - **Tooltips are composition, not a single class.** `.tooltip` now wraps a trigger and a `.tooltip-bubble`, one more DOM node than before. Intent, presentation, aesthetic, and elevation classes move to `.tooltip-bubble`, the element that actually paints.
22
+ - **`.checkbox`, `.radio`, and `.switch` default to `.soft` instead of `.solid`, and their checked state now composes one plate regardless of presentation.** Unchecked, `.ghost` (0% fill), `.soft` (12%), and `.solid` now read as three distinct plates instead of three shades of one wash; `.ghost` is a supported look on every toggle for the first time.
23
+ - **A neutral (no named intent) `.card.soft`, `data-table`, `.panel`, or `.alert` rests untinted instead of mixing a 12% ink wash.** A named intent (`.card.soft.destructive`, `.alert.success`, and so on) is unaffected and keeps its ordinary tint; `.badge` is unaffected either way, since it has no border to fall back on if untinted.
24
+ - **Elevation no longer cascades onto descendants.** `--ui-elevation`, `--elevation-y`, and `--elevation-blur` are registered non-inheriting, so an elevation class lifts only the element it is written on. A container that used to lift a whole unclassed subtree in one class now needs an elevation class on each element that wants depth.
25
+ - `--input-group-gap` replaces a hardcoded gap in `.input-group`; unchanged by default for icon compositions, and settable to `0` on a text affix for a flush look.
26
+ - `.loader`'s sizes step down from 24/32/40px to 20/28/36px for `.sm`/default/`.lg`.
27
+ - A `/tw` consumer's own `dark:` utilities now also fire under `prefers-color-scheme: dark` when no explicit theme selector is present, matching how the token palette already follows the OS default.
28
+
29
+ ## Removed
30
+
31
+ - **`.btn.loading`.** A loading button is now a disabled `.btn` composed with a real `.loader` child instead of a hardcoded `::after` spinner -- any loader variant works, not just one circular spinner.
32
+ - **`.alert.icon`'s hardcoded per-intent SVG mask.** Replaced by `.alert-icon` (above), sized for a consumer-supplied icon element.
33
+
34
+ ## Fixed
35
+
36
+ - `.icon` buttons kept the wrong `min-width` floor under `.compact`/`.spacious`; all three padding tiers now size `.icon` to its own padding.
37
+ - `.chunky-tile`'s offset shadow read as a solid fill behind a small `.icon.ghost` button; halved for that combination.
38
+ - Table row and boundary rules stayed a 1px hairline under `.chunky-tile`'s 2px frame; both now track the same width token the frame uses.
39
+ - `.progress`'s track failed WCAG 1.4.11 in both themes; `--progress-surface` now mixes enough ink to clear 3:1.
40
+ - `.card.soft.edged`'s border was nearly invisible against its own fill in light theme; `--color-border`'s light value moved one step darker.
41
+ - A badge nested inside an untinted `data-table` lost its own fill; box now restates its fill cap on every element that uses it, so a nested badge, button, or chip no longer inherits its container's cap.
@@ -0,0 +1,11 @@
1
+ ---
2
+ title: Changelog
3
+ curated: true
4
+ group: Changelog
5
+ ---
6
+
7
+ # Changelog
8
+
9
+ - [0.2.0](0.2.0.md)
10
+ - [0.1.1](0.1.1.md)
11
+ - [0.1.0](0.1.0.md)