@codenhub/styles 0.0.4 → 0.1.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 (71) hide show
  1. package/LICENSE +201 -201
  2. package/NOTICE +27 -0
  3. package/README.md +60 -92
  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 +107 -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 +185 -0
  19. package/dist/tw/components/button.css +88 -0
  20. package/dist/tw/components/feedback.css +163 -0
  21. package/dist/tw/components/form.css +601 -0
  22. package/dist/tw/components/index.css +18 -0
  23. package/dist/tw/components/loader.css +122 -0
  24. package/dist/tw/components/surface.css +98 -0
  25. package/dist/tw/components/tooltip.css +132 -0
  26. package/dist/tw/elevation.css +39 -0
  27. package/dist/tw/index.css +12 -0
  28. package/dist/tw/intent.css +189 -0
  29. package/dist/tw/native.css +203 -0
  30. package/dist/tw/presentation.css +77 -0
  31. package/dist/tw/reset.css +142 -0
  32. package/dist/tw/theme.css +218 -0
  33. package/dist/tw/typography.css +34 -0
  34. package/dist/tw/utilities/content.css +349 -0
  35. package/dist/tw/utilities/index.css +6 -0
  36. package/dist/tw/utilities/layout.css +107 -0
  37. package/docs/accessibility.md +82 -0
  38. package/docs/concepts.md +94 -0
  39. package/docs/index.md +62 -0
  40. package/docs/integrating/astro.md +29 -0
  41. package/docs/integrating/index.md +32 -0
  42. package/docs/integrating/nextjs.md +44 -0
  43. package/docs/integrating/svelte.md +37 -0
  44. package/docs/integrating/tailwind.md +40 -0
  45. package/docs/integrating/vue.md +25 -0
  46. package/docs/setup.md +116 -0
  47. package/docs/usage/aesthetics.md +197 -0
  48. package/docs/usage/buttons.md +74 -0
  49. package/docs/usage/composing.md +218 -0
  50. package/docs/usage/content-and-layout.md +200 -0
  51. package/docs/usage/customizing.md +213 -0
  52. package/docs/usage/feedback.md +117 -0
  53. package/docs/usage/forms.md +177 -0
  54. package/docs/usage/index.md +32 -0
  55. package/docs/usage/theming.md +210 -0
  56. package/docs/usage/tooltips.md +45 -0
  57. package/llms-full.txt +2122 -0
  58. package/llms.txt +26 -0
  59. package/package.json +95 -29
  60. package/src/components/button.css +0 -155
  61. package/src/components/feedback.css +0 -214
  62. package/src/components/form.css +0 -303
  63. package/src/components/index.css +0 -9
  64. package/src/components/loader.css +0 -56
  65. package/src/components/surface.css +0 -6
  66. package/src/components/tooltip.css +0 -60
  67. package/src/index.css +0 -7
  68. package/src/overrides.css +0 -76
  69. package/src/theme.css +0 -234
  70. package/src/typography.css +0 -30
  71. package/src/utilities.css +0 -40
@@ -0,0 +1,349 @@
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
+ --quote-fill: calc(min(var(--ui-fill, var(--_d-fill)), var(--intent-fill-max, 100%)) * var(--ui-bg-alpha, 1));
310
+ --quote-bg: color-mix(in oklab, var(--intent-color) var(--quote-fill), transparent);
311
+ /* The same edge blend `box` draws, in the same order and for the same reason:
312
+ the blend is the line's colour, and how much line there is comes after it.
313
+ Written the other way round -- how much line, then blend the fill into it --
314
+ `.edgeless` lands on the fill colour rather than on nothing, and the border
315
+ box paints that over the fill it already drew, so a quotation that asked for
316
+ no bar got a bar of two coats of its own tint.
317
+
318
+ That this had to be fixed twice is the cost of a quotation drawing its own
319
+ edge instead of taking `box`'s. Recorded in `registry.json` beside the
320
+ decision to compose none of `box`. */
321
+ --quote-line: color-mix(in oklab, var(--quote-bg) var(--ui-fill, var(--_d-fill)), var(--intent-border));
322
+ --quote-edge: color-mix(in oklab, var(--quote-line) var(--ui-border, var(--_d-border)), transparent);
323
+ --quote-width: calc(var(--ui-border-width, var(--border-width)) * 2 + 1px);
324
+
325
+ @apply my-0 border-s py-2 ps-4 italic;
326
+
327
+ background-color: var(--quote-bg);
328
+ border-inline-start-color: var(--quote-edge);
329
+ border-inline-start-width: var(--quote-width);
330
+ color: color-mix(
331
+ in oklab,
332
+ var(--intent-contrast) min(var(--ui-fg-on-fill, var(--_d-fg-on-fill)), var(--intent-fill-max, 100%)),
333
+ var(--intent-strong)
334
+ );
335
+
336
+ /* The attribution takes no colour of its own, so it follows the quotation's.
337
+ Pinning it to a neutral secondary tone loses both the intent-specific strong
338
+ tone at zero fill and the contrast tone as the fill comes up.
339
+
340
+ Type and posture are what separate an attribution from the quotation: it is
341
+ upright where the quote is italic, and it sits on its own line. */
342
+ & cite {
343
+ @apply text-body mt-2 block not-italic;
344
+ }
345
+ }
346
+
347
+ @utility quote-inline {
348
+ @apply text-text-secondary italic;
349
+ }
@@ -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,82 @@
1
+ ---
2
+ title: Accessibility
3
+ ---
4
+
5
+ # Accessibility responsibilities
6
+
7
+ 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
+ ## 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
52
+
53
+ Use semantic HTML and behavior appropriate for the component.
54
+
55
+ | UI | Required outside CSS |
56
+ | ----------------- | --------------------------------------------------------------------------------------------------------------- |
57
+ | Buttons | Use `<button>` for actions or accessible links for navigation. Add accessible names for `.btn.icon`. |
58
+ | Forms | Use labels, `type`, validation logic, `aria-describedby`, and error message relationships. |
59
+ | Alerts | Add `role="status"` or `role="alert"` based on announcement urgency. |
60
+ | 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. |
62
+ | Progress | Use semantic progress elements or ARIA values when numeric progress must be announced. |
63
+ | Skeletons/loaders | Mark decorative loading visuals with `aria-hidden="true"` and expose loading state elsewhere when needed. |
64
+ | Popovers/modals | Provide focus trapping, escape handling, inert background behavior, labels, and roles outside this CSS package. |
65
+
66
+ ## State Attribute Guidance
67
+
68
+ Prefer native attributes first:
69
+
70
+ ```html
71
+ <button class="btn primary" disabled>Saving</button>
72
+ <input class="ipt" aria-invalid="true" aria-describedby="email-error" />
73
+ ```
74
+
75
+ Use ARIA or data attributes when native attributes are not available for the element or library:
76
+
77
+ ```html
78
+ <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>
80
+ ```
81
+
82
+ `aria-disabled="true"` communicates disabled state but does not prevent activation. JavaScript or element choice must prevent activation when required.
@@ -0,0 +1,94 @@
1
+ ---
2
+ title: Concepts
3
+ description: The three-axis model, the token system, and how theme selection works.
4
+ ---
5
+
6
+ # Concepts
7
+
8
+ Read this before the usage guides. It explains the mental model every
9
+ component, token, and class in this package shares.
10
+
11
+ ## The three axes
12
+
13
+ Every component separates three independent choices, so each can change
14
+ without a component-specific variant:
15
+
16
+ | Axis | Question | Classes | Inheritance |
17
+ | ------------ | --------------- | ----------------------------------------------------------------------------------------- | ------------ |
18
+ | Intent | Which color? | `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, `.info` | Element only |
19
+ | Presentation | How much of it? | `.solid`, `.soft`, `.ghost`, `.edged`, `.edgeless` | Cascades |
20
+ | Aesthetic | Made of what? | `.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile` from an optional aesthetic stylesheet | Cascades |
21
+
22
+ ```html
23
+ <section class="soft pixel">
24
+ <button class="btn primary">Soft primary pixel button</button>
25
+ <button class="btn destructive ghost edged">Outlined destructive override</button>
26
+ </section>
27
+ ```
28
+
29
+ Intent stays on the component so a semantic container cannot accidentally
30
+ recolor nested controls. Presentation and aesthetic cascade from a container;
31
+ putting either class directly on a component overrides the inherited choice.
32
+ Components apply documented clamps when an axis would remove an essential edge
33
+ or overwhelm compact geometry.
34
+
35
+ The maintained contract is the intersection of the combinations documented in
36
+ [Usage](./usage/index.md) and demonstrated by the playground. Undocumented and
37
+ undemonstrated combinations may produce CSS but are not supported behavior.
38
+
39
+ - [Usage → Composing](./usage/composing.md) shows how the three axes combine
40
+ on each component, with the full axis-support matrix.
41
+ - [Usage → Aesthetics](./usage/aesthetics.md) covers each aesthetic and its
42
+ documented exceptions in depth.
43
+
44
+ ## The token system
45
+
46
+ Tokens are CSS custom properties, grouped by what they control rather than
47
+ where they're used:
48
+
49
+ | Category | Controls | Reference |
50
+ | ------------ | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
51
+ | Color | The palette: primary, accent, success, warning, destructive, info, text, borders, surfaces. | [Usage → Theming](./usage/theming.md#color-tokens) |
52
+ | Intent | Which color slot a component reads once an intent class is applied. | [Usage → Theming](./usage/theming.md#intent-tokens) |
53
+ | Foundation | Layout, shape, motion, focus, depth, and layering defaults shared across components. | [Usage → Theming](./usage/theming.md#foundation-tokens) |
54
+ | Presentation | How much of an intent a component shows — fill and border percentages. | [Usage → Customizing](./usage/customizing.md#presentation-tokens) |
55
+ | Material | What a component is made of — radius, border width, shadow geometry, translucency. | [Usage → Customizing](./usage/customizing.md#material-tokens) |
56
+ | Aesthetic | Per-aesthetic knobs such as `--pixel-unit` or `--tile-lift`. | [Usage → Customizing](./usage/customizing.md#aesthetic-tokens) |
57
+
58
+ Color and foundation tokens are the ones most apps touch first, to match a
59
+ brand palette or spacing scale. Intent, presentation, and material tokens are
60
+ lower-level: components read them, and only presentation classes, aesthetic
61
+ classes, or a consumer override should set them.
62
+
63
+ ## Theme selection
64
+
65
+ The root follows the operating-system `color-scheme` preference until an
66
+ explicit selector overrides it.
67
+
68
+ | Theme | Equivalent selectors |
69
+ | ----- | ------------------------------------------------ |
70
+ | Light | `.light`, `.theme-light`, `[data-theme="light"]` |
71
+ | Dark | `.dark`, `.theme-dark`, `[data-theme="dark"]` |
72
+
73
+ - Theme selectors set `color-scheme` and nothing else. Because `color-scheme`
74
+ inherits, a selector themes its entire subtree.
75
+ - With no explicit selector, `:root` uses `color-scheme: light dark`, so the
76
+ system preference decides.
77
+ - A theme selector on `:root` overrides the system preference in either
78
+ direction.
79
+ - A nested explicit selector overrides an inherited theme, so the nearest
80
+ themed ancestor controls that subtree at any depth.
81
+ - If conflicting light and dark selectors are placed on the same element, the
82
+ dark declaration wins because it appears later with equal specificity. Do
83
+ not rely on this conflict behavior; apply one theme per element.
84
+ - Component colors resolve from tokens or `currentColor`, so they follow the
85
+ theme without separate dark-variant rules, with no exception: every glyph
86
+ the package draws is a `currentColor` mask. Icon artwork a consumer brings
87
+ is theirs to theme.
88
+
89
+ Setting `color-scheme` yourself on an element also re-themes the tokens below
90
+ it, because that is the only signal the palette reads. This also themes native
91
+ UI such as scrollbars and form-control internals to match.
92
+
93
+ This mechanism works from Chrome 123, Safari 17.5, and Firefox 120; the package
94
+ overall requires Firefox 121 for `:has()` (see [Setup](./setup.md#requirements)).