@codenhub/styles 0.0.4 → 0.1.1

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 (74) hide show
  1. package/LICENSE +201 -201
  2. package/NOTICE +27 -0
  3. package/README.md +22 -116
  4. package/dist/aesthetics/chunky-tile.css +1 -0
  5. package/dist/aesthetics/glass.css +1 -0
  6. package/dist/aesthetics/index.css +2 -0
  7. package/dist/aesthetics/neobrutalism.css +1 -0
  8. package/dist/aesthetics/pixel.css +1 -0
  9. package/dist/components.css +2 -2
  10. package/dist/index.css +2 -2
  11. package/dist/native.css +2 -0
  12. package/dist/theme.css +2 -2
  13. package/dist/tw/aesthetics/chunky-tile.css +109 -0
  14. package/dist/tw/aesthetics/glass.css +66 -0
  15. package/dist/tw/aesthetics/index.css +9 -0
  16. package/dist/tw/aesthetics/neobrutalism.css +63 -0
  17. package/dist/tw/aesthetics/pixel.css +88 -0
  18. package/dist/tw/box.css +206 -0
  19. package/dist/tw/components/button.css +108 -0
  20. package/dist/tw/components/feedback.css +163 -0
  21. package/dist/tw/components/form.css +599 -0
  22. package/dist/tw/components/index.css +28 -0
  23. package/dist/tw/components/loader.css +122 -0
  24. package/dist/tw/components/surface.css +122 -0
  25. package/dist/tw/components/tooltip.css +132 -0
  26. package/dist/tw/elevation.css +46 -0
  27. package/dist/tw/index.css +27 -0
  28. package/dist/tw/intent.css +209 -0
  29. package/dist/tw/native.css +187 -0
  30. package/dist/tw/presentation.css +77 -0
  31. package/dist/tw/reset.css +129 -0
  32. package/dist/tw/theme.css +223 -0
  33. package/dist/tw/typography.css +34 -0
  34. package/dist/tw/utilities/content.css +361 -0
  35. package/dist/tw/utilities/index.css +6 -0
  36. package/dist/tw/utilities/layout.css +107 -0
  37. package/docs/accessibility.md +92 -0
  38. package/docs/changelog/0.1.0.md +37 -0
  39. package/docs/changelog/0.1.1.md +10 -0
  40. package/docs/changelog/index.md +10 -0
  41. package/docs/concepts.md +68 -0
  42. package/docs/index.md +35 -0
  43. package/docs/integrating/astro.md +35 -0
  44. package/docs/integrating/index.md +41 -0
  45. package/docs/integrating/nextjs.md +67 -0
  46. package/docs/integrating/svelte.md +43 -0
  47. package/docs/integrating/tailwind.md +56 -0
  48. package/docs/integrating/vue.md +31 -0
  49. package/docs/setup.md +96 -0
  50. package/docs/usage/aesthetics.md +120 -0
  51. package/docs/usage/buttons.md +58 -0
  52. package/docs/usage/composing.md +143 -0
  53. package/docs/usage/content-and-layout.md +175 -0
  54. package/docs/usage/customizing.md +148 -0
  55. package/docs/usage/feedback.md +79 -0
  56. package/docs/usage/forms.md +135 -0
  57. package/docs/usage/index.md +24 -0
  58. package/docs/usage/theming.md +154 -0
  59. package/docs/usage/tooltips.md +39 -0
  60. package/llms-full.txt +1676 -0
  61. package/llms.txt +26 -0
  62. package/package.json +176 -33
  63. package/src/components/button.css +0 -155
  64. package/src/components/feedback.css +0 -214
  65. package/src/components/form.css +0 -303
  66. package/src/components/index.css +0 -9
  67. package/src/components/loader.css +0 -56
  68. package/src/components/surface.css +0 -6
  69. package/src/components/tooltip.css +0 -60
  70. package/src/index.css +0 -7
  71. package/src/overrides.css +0 -76
  72. package/src/theme.css +0 -234
  73. package/src/typography.css +0 -30
  74. package/src/utilities.css +0 -40
@@ -0,0 +1,361 @@
1
+ @reference "tailwindcss";
2
+ @reference "../box.css";
3
+
4
+ @utility table-wrap {
5
+ @apply w-full overflow-x-auto;
6
+ }
7
+
8
+ /* The one component that takes the frame of `box` and not the whole of it. The
9
+ border, the radius and the clip come from the box; the head tone, the cell
10
+ rules and the row hover are private tokens, because none of the three axes has
11
+ a vocabulary for the inside of a table.
12
+
13
+ Its `overflow: hidden` is what keeps `border-separate` corners inside the
14
+ radius, and it does not compose with an aesthetic's `clip-path`: where both
15
+ apply, the clip wins and the corners square off. Recorded in `registry.json`
16
+ beside the decision. */
17
+ @utility data-table {
18
+ @apply box text-body w-full border-separate text-left;
19
+
20
+ --_d-fill: 12%;
21
+ --_d-fg-on-fill: 0%;
22
+ /* Edgeless, so the frame is asked for rather than inherited from a default
23
+ nobody can see in their own markup. Published `edged`, `.edged` on a table
24
+ computed exactly what no class computed -- a switch that could not move
25
+ anything -- and the frame arrived because of a line in `registry.json`.
26
+ `.edged` draws it now, and the two boundaries a table keeps whatever else it
27
+ draws mean a frameless one still reads as a table. */
28
+ --_d-border: 0%;
29
+ --_d-elevation: 0;
30
+
31
+ /* Capped the way `box` caps, because the head is a fill of the intent like any
32
+ other and a neutral table head is otherwise a slab of the page's ink with
33
+ ink printed on it. */
34
+ --table-fill: calc(min(var(--ui-fill, var(--_d-fill)), var(--intent-fill-max, 100%)) * var(--ui-bg-alpha, 1));
35
+ /* The head's plate is the tint, so a table carrying no fill has no head plate.
36
+ `.ghost` means no tint here the way it means no fill everywhere else, and a
37
+ ghost table is boundaries and type and nothing else -- which is what `.ghost`
38
+ is for on this component, since the published resting fill is `soft`.
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.
48
+
49
+ It reads `--ui-fill` rather than `--table-fill` because that is the question
50
+ being asked: what the presentation wanted, not what the intent cap and an
51
+ aesthetic's alpha left of it. A glass `.soft` table still wants its head
52
+ plate, and neutral's 20% cap has no bearing on whether there is one. Reading
53
+ the raw class value also keeps the expression a `calc()` over one `var()`
54
+ rather than over `--table-fill`'s own `calc()`/`min()` chain, which matters
55
+ for the reason `--table-rule-plated` below records. */
56
+ --table-ground: min(100%, calc(var(--ui-fill, var(--_d-fill)) * 100));
57
+ --table-head-plate: color-mix(in oklab, var(--intent-subtle) var(--table-ground), transparent);
58
+ --table-head-bg: color-mix(in oklab, var(--intent-color) var(--table-fill), var(--table-head-plate));
59
+ --table-head-fg: color-mix(in oklab, var(--intent-contrast) var(--_on-fill), var(--intent-strong));
60
+ /* How much rule there is between body rows, and none unless asked for. Ruling
61
+ every row is a style choice rather than part of what a table is, and tying it
62
+ to the edge axis made it arrive by implication instead: a table drew rules
63
+ because its published edge default happened to be `edged`, which is a fact
64
+ about the registry rather than anything the markup says. A consumer reading
65
+ their own HTML could not tell whether they had asked for rules.
66
+
67
+ `.ruled` is that ask and `.ruleless` is its opposite, with `--ui-rule` behind
68
+ both -- so a consumer who wants a whole region ruled, or a whole region bare,
69
+ sets the token on a container instead of classing every table.
70
+
71
+ One token covers three positions because the two places that read it take
72
+ different fallbacks. Unset, each site answers for itself: no rules between
73
+ rows, but the boundaries below still drawn. Set, the answer is the same at
74
+ both. So `.ruled` writes 100% and gets rules everywhere, `.ruleless` writes 0%
75
+ and gets a table with no lines in it at all, and a table nobody classed sits
76
+ between them. A second token would have said the same thing with more parts,
77
+ and would have let the two drift. */
78
+ --table-rule-amount: var(--ui-rule, 0%);
79
+ --table-boundary-amount: var(--ui-rule, 100%);
80
+ /* What the rule is drawn in, which is not the same tone at every fill.
81
+ `.ghost` and `.soft` keep `--intent-border` -- the slot an aesthetic restates
82
+ through `--ui-ink` -- because at those fills the plate has not swallowed it.
83
+ `.solid` is where it has: at a full fill `--intent-border` *is* the plate for
84
+ every intent, and at neutral's capped fill it is a near shade of the same
85
+ grey, so the rule the rows are separated by disappears exactly where the
86
+ plate is busiest.
87
+
88
+ The filled case draws in `currentColor` instead, at a fifth. That is the ink
89
+ the cell is already printing -- `--table-head-fg` on a head, `box`'s own
90
+ `--_fg` in the body -- so it reads on a light plate and a dark one without a
91
+ per-intent branch, and it lands *over* the plate because a translucent border
92
+ composites onto the background the element already painted.
93
+
94
+ Naming the ink rather than composing it is also what keeps this expression
95
+ cheap, and cheap is a requirement here rather than a preference. Written as
96
+ `color-mix(--table-head-fg 20%, --_bg)` the rule expanded, through four levels
97
+ of substitution, into a `color-mix()` over two more `color-mix()`es over a
98
+ `min()`/`max()`/`calc()` chain -- and every `th` and `td` on a page resolves it.
99
+ WebKit's renderer crashed outright on the two routes that draw tables, taking
100
+ 22 specs across four files with it, while Chromium and Firefox rendered them
101
+ correctly. Neither the depth nor the table alone did it: bisected, the new
102
+ tokens were fine until the cells consumed them.
103
+
104
+ `currentColor` and `transparent` are leaves the engine has resolved already,
105
+ which is what takes the expression back down to something every engine can
106
+ carry. `--table-row-hover` below is written the same way, for the same
107
+ reason. */
108
+ --table-rule-plated: color-mix(in oklab, currentColor 20%, transparent);
109
+ --table-rule-tone: color-mix(in oklab, var(--table-rule-plated) var(--ui-fill, var(--_d-fill)), var(--intent-border));
110
+ --table-rule: color-mix(in oklab, var(--table-rule-tone) var(--table-rule-amount), transparent);
111
+ --table-boundary: color-mix(in oklab, var(--table-rule-tone) var(--table-boundary-amount), transparent);
112
+ /* A hovered row is a step from the table it is in, not from a tone the page
113
+ happens to own. Mixed into `--color-foreground` it was neither: a solid
114
+ table paints its body with the intent, so a primary table in dark theme is
115
+ white and its hovered row came out near-black -- a dark band across a light
116
+ table. Painting a translucent step instead makes it relative at every fill,
117
+ the way `box-hover` is: the row composites onto whatever plate the table
118
+ drew, so a filled table shifts within its own colour, a soft one deepens its
119
+ tint, and a ghost one picks up a faint wash of the page.
120
+
121
+ Which tone it steps toward has to move with the fill as well. On a bare or
122
+ tinted plate that is `--intent-hover`, the deepening every other component
123
+ gets. On a filled one it cannot be: `--intent-hover` is
124
+ `mix(--intent-color 82%, --intent-strong)`, which is very nearly the colour
125
+ the plate is already painted, so a step toward it is no step. Measured, a
126
+ solid success table moved 1.00:1 in light and a solid destructive one
127
+ 1.008:1 -- the feedback was declared and invisible. A filled plate steps
128
+ toward `currentColor` instead, which is the ink that plate already carries
129
+ and therefore always contrasts with it.
130
+
131
+ Both ends are leaves, and the step lands on `transparent` rather than on
132
+ `--_bg`, for the reason `--table-rule-plated` above gives: compositing is
133
+ what the browser does with a translucent background anyway, so naming `--_bg`
134
+ buys the same colour for two more levels of substitution. */
135
+ --table-hover-tone: color-mix(in oklab, currentColor var(--ui-fill, var(--_d-fill)), var(--intent-hover));
136
+ --table-row-hover: color-mix(in oklab, var(--table-hover-tone) 12%, transparent);
137
+
138
+ border-spacing: 0;
139
+ overflow: hidden;
140
+
141
+ & caption {
142
+ @apply text-label text-text-secondary pb-2 text-left;
143
+ }
144
+
145
+ & thead,
146
+ & tfoot {
147
+ background-color: var(--table-head-bg);
148
+ color: var(--table-head-fg);
149
+ }
150
+
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. */
155
+ & th,
156
+ & td {
157
+ @apply border-b px-4 py-3 align-top;
158
+
159
+ border-color: var(--table-rule);
160
+ }
161
+
162
+ /* The two lines that separate the parts of a table from each other: under the
163
+ head and above the foot. They are boundaries rather than decoration on the
164
+ rows, so they are drawn by default where the row rules are not, and they do
165
+ not answer to the edge axis -- `.edgeless` takes the silhouette off the box,
166
+ and a head that runs into its own body is not a silhouette question.
167
+
168
+ The foot draws upward rather than relying on the last body row drawing down,
169
+ because with rules off that row paints nothing. Stated on the foot, the
170
+ boundary holds at every setting. */
171
+ & thead tr:last-child > * {
172
+ border-bottom-color: var(--table-boundary);
173
+ }
174
+
175
+ & tfoot tr:first-child > * {
176
+ @apply border-t;
177
+
178
+ border-top-color: var(--table-boundary);
179
+ }
180
+
181
+ /* Rules between body rows, on request -- and every line off, on the opposite
182
+ request. `.ruleless` is declared second so it wins the nonsense combination
183
+ rather than leaving it to whichever class the author typed first. */
184
+ &.ruled {
185
+ --ui-rule: 100%;
186
+ }
187
+
188
+ &.ruleless {
189
+ --ui-rule: 0%;
190
+ }
191
+
192
+ & th {
193
+ @apply text-label font-semibold;
194
+ }
195
+
196
+ /* Rows deliberately inherit the table's intent instead of resetting it, so a
197
+ `.data-table.success` tints its own rows. A `<tr class="warning">` still wins,
198
+ because an intent class on the element beats an inherited value. */
199
+ & tbody tr {
200
+ transition: background-color var(--motion-duration-fast) var(--motion-ease);
201
+ }
202
+
203
+ & tbody tr:hover {
204
+ background-color: var(--table-row-hover);
205
+ }
206
+
207
+ /* The rule comes off the row that is visually last, which is not always the
208
+ last row of the body. Written as `tbody tr:last-child` it came off the body's
209
+ last row unconditionally, so a table with a footer lost the one rule that
210
+ separates the two -- the summary ran straight on from the rows it summarises --
211
+ while the footer kept a bottom rule of its own, drawing a hairline immediately
212
+ inside the frame. A missing line where one belongs and a doubled one where it
213
+ does not, from the same declaration.
214
+
215
+ Both selectors are `:last-child` on the section, so whichever section the
216
+ markup puts last is the one that loses its rule. That covers a footer written
217
+ before the body, which HTML still allows, and multiple bodies, where only the
218
+ last one ends the table.
219
+
220
+ It went unseen because it was invisible until this release: `thead` and
221
+ `tfoot` shared an unconditional `--intent-subtle` plate, so the footer was a
222
+ band and the band did the separating. `.ghost` dropping that plate is what
223
+ left the rule doing the job alone. */
224
+ & tbody:last-child tr:last-child > *,
225
+ & tfoot:last-child tr:last-child > * {
226
+ @apply border-b-0;
227
+ }
228
+ }
229
+
230
+ /* Three components rest on a ground rather than on nothing: a key cap, an inline
231
+ code chip and a code block are each a quiet tinted plate at rest, and that tone
232
+ is `--intent-subtle` rather than a fill of the component's own intent.
233
+
234
+ All three rest at `soft`, and `.ghost` is published as unsupported on them.
235
+ The ground is what makes a chip a chip, and `.ghost` asks for no fill while
236
+ the ground stays -- so on these three it drew the plate anyway and meant
237
+ nothing, which is the opposite of what it means everywhere else. Worse, at
238
+ zero fill the plate is `--intent-subtle` alone, and that slot is a near-page
239
+ tint: measured on the light page, a ghost `code` chip was `#e5e5e5` for both
240
+ neutral and primary, and `#f5f5f5` for secondary at 1.04:1 against a `#fafafa`
241
+ page. A class that renders four intents as the same invisible chip is not a
242
+ variation. Resting at `soft` puts a real 12% of the intent over the ground, so
243
+ the plate is a step past the page and the intents separate.
244
+
245
+ Each used to restate its foreground too, because `box` mixed toward
246
+ `--intent-color` -- the tone meant to sit *on* a page rather than on a tint of
247
+ itself, and amber-600 on amber-100 is a smudge. `box` now mixes toward
248
+ `--intent-strong`, which is what these three were reaching for, so the three
249
+ copies are gone and the ground is the only thing left to declare. */
250
+ @utility kbd {
251
+ @apply box inline-flex items-center px-1.5 py-0.5 font-mono text-xs font-medium;
252
+
253
+ --_d-fill: 12%;
254
+ --_d-fg-on-fill: 0%;
255
+ --_d-border: 100%;
256
+ --_d-elevation: 0;
257
+ --_d-ground: var(--intent-subtle);
258
+
259
+ /* Capped at its own radius: a control radius is sized for a control, and a key
260
+ cap this small would read as a lozenge. A squaring aesthetic still reaches it. */
261
+ border-radius: min(var(--ui-radius, var(--radius-small)), var(--radius-small));
262
+ clip-path: var(--ui-clip-tight, var(--ui-clip, none));
263
+ }
264
+
265
+ /* The same plate with no line around it. */
266
+ @utility code {
267
+ @apply box px-1.25 py-0.75 font-mono;
268
+
269
+ --_d-fill: 12%;
270
+ --_d-fg-on-fill: 0%;
271
+ --_d-border: 0%;
272
+ --_d-elevation: 0;
273
+ --_d-ground: var(--intent-subtle);
274
+
275
+ border-radius: min(var(--ui-radius, var(--radius-small)), var(--radius-small));
276
+ clip-path: var(--ui-clip-tight, var(--ui-clip, none));
277
+ }
278
+
279
+ /* And at block scale, where the radius is a control's rather than a chip's. */
280
+ @utility pre {
281
+ @apply box block overflow-x-auto px-2.5 py-1.5 font-mono;
282
+
283
+ --_d-fill: 12%;
284
+ --_d-fg-on-fill: 0%;
285
+ --_d-border: 0%;
286
+ --_d-elevation: 0;
287
+ --_d-ground: var(--intent-subtle);
288
+ }
289
+
290
+ /* Typography wearing a component's name, so it composes none of `box`: `box`
291
+ draws a border on four sides and a quotation wants one, and a radius, a clip
292
+ and a shadow are inert on a left bar and an indent. What is left of `box`
293
+ after removing the border is not worth composing.
294
+
295
+ The cost is the fill and edge blends below, which repeat `box` and have to
296
+ agree with it. Recorded in `registry.json` beside the
297
+ decision.
298
+
299
+ The bar is twice the line width the aesthetic asks for, plus a pixel, which is
300
+ this component's own choice about what a quotation looks like rather than a
301
+ scale applied to an edge -- there is no such scale any more. The constant is
302
+ what makes the bar read as a bar at the default hairline: doubling 1px gives
303
+ 2px, and a quotation mark needs more presence than a table rule. */
304
+ @utility quote {
305
+ --_d-fill: 0%;
306
+ --_d-fg-on-fill: 0%;
307
+ --_d-border: 100%;
308
+ --_d-elevation: 0;
309
+ /* Presentation capped by the intent, the same `--_capped` `box` composes, kept
310
+ separate from `--quote-fill` because the ink ramp below reads it before
311
+ `--ui-bg-alpha` thins it. */
312
+ --quote-capped: min(var(--ui-fill, var(--_d-fill)), var(--intent-fill-max, 100%));
313
+ --quote-fill: calc(var(--quote-capped) * var(--ui-bg-alpha, 1));
314
+ --quote-bg: color-mix(in oklab, var(--intent-color) var(--quote-fill), transparent);
315
+ /* The same edge blend `box` draws, in the same order and for the same reason:
316
+ the blend is the line's colour, and how much line there is comes after it.
317
+ Written the other way round -- how much line, then blend the fill into it --
318
+ `.edgeless` lands on the fill colour rather than on nothing, and the border
319
+ box paints that over the fill it already drew, so a quotation that asked for
320
+ no bar got a bar of two coats of its own tint.
321
+
322
+ That this had to be fixed twice is the cost of a quotation drawing its own
323
+ edge instead of taking `box`'s. Recorded in `registry.json` beside the
324
+ decision to compose none of `box`. */
325
+ --quote-line: color-mix(in oklab, var(--quote-bg) var(--ui-fill, var(--_d-fill)), var(--intent-border));
326
+ --quote-edge: color-mix(in oklab, var(--quote-line) var(--ui-border, var(--_d-border)), transparent);
327
+ --quote-width: calc(var(--ui-border-width, var(--border-width)) * 2 + 1px);
328
+
329
+ @apply my-0 border-s py-2 ps-4 italic;
330
+
331
+ background-color: var(--quote-bg);
332
+ border-inline-start-color: var(--quote-edge);
333
+ border-inline-start-width: var(--quote-width);
334
+ /* The ink is gated by the plate, not tied to the fill: `--ui-fg-on-fill` is the
335
+ ceiling, and the contrast tone only ramps in past a half fill, reaching full
336
+ at a full fill. Below that the ink stays `--intent-strong`, the tone chosen
337
+ to read on the page. This is `box`'s `--_on-fill` exactly -- `min(asked,
338
+ --intent-fill-max)` read plausibly and was wrong at the bottom of the range,
339
+ printing a capped-neutral `.solid` quote at ~6.6:1 where the ramp gives
340
+ ~11:1, and this renderer has to agree with `box`. */
341
+ color: color-mix(
342
+ in oklab,
343
+ var(--intent-contrast)
344
+ min(var(--ui-fg-on-fill, var(--_d-fg-on-fill)), max(0%, calc(var(--quote-capped) * 2 - 100%))),
345
+ var(--intent-strong)
346
+ );
347
+
348
+ /* The attribution takes no colour of its own, so it follows the quotation's.
349
+ Pinning it to a neutral secondary tone loses both the intent-specific strong
350
+ tone at zero fill and the contrast tone as the fill comes up.
351
+
352
+ Type and posture are what separate an attribution from the quotation: it is
353
+ upright where the quote is italic, and it sits on its own line. */
354
+ & cite {
355
+ @apply text-body mt-2 block not-italic;
356
+ }
357
+ }
358
+
359
+ @utility quote-inline {
360
+ @apply text-text-secondary italic;
361
+ }
@@ -0,0 +1,6 @@
1
+ @import "../theme.css";
2
+ @import "../typography.css";
3
+ @import "./layout.css";
4
+ @import "./content.css";
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");
@@ -0,0 +1,107 @@
1
+ @reference "tailwindcss";
2
+
3
+ @utility view {
4
+ @apply flex gap-(--layout-gap);
5
+
6
+ &.vertical {
7
+ @apply flex-col;
8
+ }
9
+
10
+ &.horizontal {
11
+ @apply flex-row flex-wrap items-center;
12
+ }
13
+
14
+ &.tight {
15
+ --layout-gap: 0.5rem;
16
+ }
17
+
18
+ &.loose {
19
+ --layout-gap: 1.5rem;
20
+ }
21
+ }
22
+
23
+ @utility stack {
24
+ @apply flex flex-col gap-(--layout-gap);
25
+
26
+ &.tight {
27
+ --layout-gap: 0.5rem;
28
+ }
29
+
30
+ &.loose {
31
+ --layout-gap: 1.5rem;
32
+ }
33
+ }
34
+
35
+ @utility cluster {
36
+ @apply flex flex-wrap items-center gap-(--layout-gap);
37
+
38
+ &.between {
39
+ @apply justify-between;
40
+ }
41
+
42
+ &.tight {
43
+ --layout-gap: 0.5rem;
44
+ }
45
+
46
+ &.loose {
47
+ --layout-gap: 1.5rem;
48
+ }
49
+ }
50
+
51
+ @utility auto-grid {
52
+ @apply grid gap-(--layout-gap);
53
+
54
+ grid-template-columns: repeat(auto-fit, minmax(min(var(--layout-grid-min), 100%), 1fr));
55
+
56
+ &.tight {
57
+ --layout-gap: 0.5rem;
58
+ }
59
+
60
+ &.loose {
61
+ --layout-gap: 1.5rem;
62
+ }
63
+ }
64
+
65
+ @utility section {
66
+ @apply px-(--layout-gutter) py-(--layout-section-block);
67
+ }
68
+
69
+ @utility section-content {
70
+ @apply mx-auto w-full max-w-(--container-max);
71
+
72
+ &.narrow {
73
+ max-width: var(--container-narrow);
74
+ }
75
+
76
+ &.wide {
77
+ max-width: var(--container-wide);
78
+ }
79
+ }
80
+
81
+ /* An indicator: intent only, and no presentation token at all. A rule is a line,
82
+ and neither a fill class nor an edge class has anything to say about one -- while
83
+ reading both used to mean a container's `.ghost.edgeless` made every `<hr>`
84
+ inside it invisible. The width is the aesthetic's line width, unscaled. */
85
+ @utility divider {
86
+ --divider-color: var(--intent-color);
87
+ --divider-width: var(--ui-border-width, var(--border-width));
88
+
89
+ @apply m-0 w-full border-0 border-t;
90
+
91
+ border-color: var(--divider-color);
92
+ border-top-width: var(--divider-width);
93
+
94
+ &.vertical {
95
+ @apply h-auto w-0 self-stretch border-s border-t-0;
96
+
97
+ border-inline-start-width: var(--divider-width);
98
+ }
99
+ }
100
+
101
+ @utility selection-contrast {
102
+ &::selection,
103
+ & *::selection {
104
+ background: var(--color-primary-contrast);
105
+ color: var(--color-primary);
106
+ }
107
+ }
@@ -0,0 +1,92 @@
1
+ ---
2
+ title: Accessibility
3
+ order: 5
4
+ ---
5
+
6
+ # Accessibility responsibilities
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.
9
+
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
+
22
+ ### Control boundaries
23
+
24
+ | Feature | Behavior |
25
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
26
+ | Control boundaries | Text controls rest at `--_line-rest` of their intent, under 3:1 by choice; hover and focus take the tone whole. |
27
+ | Toggle boundaries | `.checkbox`, `.radio`, and `.switch` draw their line whole, because it is the only thing marking an unchecked box. |
28
+ | Cascaded `.edgeless` | Floored on every text control: a container cannot erase the line of a field or toggle nobody classed. |
29
+ | Own `.edgeless` | On the element, `.ipt`, `.textarea`, `.select`, and `.switch` do drop their line and do not meet 1.4.11 at rest. |
30
+ | `.checkbox`/`.radio` | Never drop their line, on the element or from a container. `.edgeless` on them is unsupported, not merely discouraged. |
31
+
32
+ ### Motion and transparency
33
+
34
+ | Feature | Behavior |
35
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
36
+ | Reduced-motion loaders | Loader variants use animated embedded SVGs normally and static masks when `prefers-reduced-motion: reduce` matches. |
37
+ | Reduced-motion document | The reset additionally shortens document animations and transitions when `prefers-reduced-motion: reduce` matches. |
38
+ | Reduced transparency | `.glass` drops its blur and becomes an opaque surface when `prefers-reduced-transparency: reduce` matches. |
39
+
40
+ 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.
41
+
42
+ ### Color and contrast
43
+
44
+ | Feature | Behavior |
45
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------- |
46
+ | 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. |
47
+ | Warning ink | `--color-warning-contrast` is the near-black tone, not the page tone. Amber dark enough to carry white is not amber. |
48
+ | Partial fills | Contrast ink appears only past a half fill, so a capped plate keeps `--intent-strong` rather than walking toward the page. |
49
+ | 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. |
50
+ | Toggle marks | A checked mark meets 3:1 as a state indicator on every plate its fill class can paint, in both themes. |
51
+
52
+ ### Forced colors and direction
53
+
54
+ | Feature | Behavior |
55
+ | -------------- | ------------------------------------------------------------------------------------------------------------------ |
56
+ | Forced colors | `forced-colors: active` preserves visible borders and checked states using system colors. |
57
+ | Text direction | Quote bar, alert icon, progress fill, vertical divider, and switch knob mirror under `dir="rtl"`. No class needed. |
58
+
59
+ 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.
60
+
61
+ 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.
62
+
63
+ ## Required outside CSS
64
+
65
+ Use semantic HTML and behavior appropriate for the component.
66
+
67
+ | UI | Required outside CSS |
68
+ | ----------------- | --------------------------------------------------------------------------------------------------------------- |
69
+ | Buttons | Use `<button>` for actions or accessible links for navigation. Add accessible names for `.btn.icon`. |
70
+ | Forms | Use labels, `type`, validation logic, `aria-describedby`, and error message relationships. |
71
+ | Alerts | Add `role="status"` or `role="alert"` based on announcement urgency. |
72
+ | Toasts | Add live-region behavior, dismissal behavior, focus rules, and pause/timeout logic when needed. |
73
+ | Tooltips | Provide accessible names/descriptions. CSS pseudo-element content is not enough for all assistive tech. |
74
+ | Progress | Use semantic progress elements or ARIA values when numeric progress must be announced. |
75
+ | Skeletons/loaders | Mark decorative loading visuals with `aria-hidden="true"` and expose loading state elsewhere when needed. |
76
+ | Popovers/modals | Provide focus trapping, escape handling, inert background behavior, labels, and roles outside this CSS package. |
77
+
78
+ ## State attribute guidance
79
+
80
+ Prefer native attributes first:
81
+
82
+ ```html
83
+ <button class="btn primary" disabled>Saving</button> <input class="ipt" aria-invalid="true" aria-describedby="email-error" />
84
+ ```
85
+
86
+ Use ARIA or data attributes when native attributes are not available for the element or library:
87
+
88
+ ```html
89
+ <a class="btn secondary" aria-disabled="true">Unavailable</a> <button class="tooltip tooltip-icon" data-state="open" data-tooltip="More details" aria-label="More details">?</button>
90
+ ```
91
+
92
+ `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,10 @@
1
+ ---
2
+ title: Changelog
3
+ curated: true
4
+ group: Changelog
5
+ ---
6
+
7
+ # Changelog
8
+
9
+ - [0.1.1](0.1.1.md)
10
+ - [0.1.0](0.1.0.md)