@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.
- package/LICENSE +201 -201
- package/NOTICE +27 -0
- package/README.md +22 -116
- package/dist/aesthetics/chunky-tile.css +1 -0
- package/dist/aesthetics/glass.css +1 -0
- package/dist/aesthetics/index.css +2 -0
- package/dist/aesthetics/neobrutalism.css +1 -0
- package/dist/aesthetics/pixel.css +1 -0
- package/dist/components.css +2 -2
- package/dist/index.css +2 -2
- package/dist/native.css +2 -0
- package/dist/theme.css +2 -2
- package/dist/tw/aesthetics/chunky-tile.css +109 -0
- package/dist/tw/aesthetics/glass.css +66 -0
- package/dist/tw/aesthetics/index.css +9 -0
- package/dist/tw/aesthetics/neobrutalism.css +63 -0
- package/dist/tw/aesthetics/pixel.css +88 -0
- package/dist/tw/box.css +206 -0
- package/dist/tw/components/button.css +108 -0
- package/dist/tw/components/feedback.css +163 -0
- package/dist/tw/components/form.css +599 -0
- package/dist/tw/components/index.css +28 -0
- package/dist/tw/components/loader.css +122 -0
- package/dist/tw/components/surface.css +122 -0
- package/dist/tw/components/tooltip.css +132 -0
- package/dist/tw/elevation.css +46 -0
- package/dist/tw/index.css +27 -0
- package/dist/tw/intent.css +209 -0
- package/dist/tw/native.css +187 -0
- package/dist/tw/presentation.css +77 -0
- package/dist/tw/reset.css +129 -0
- package/dist/tw/theme.css +223 -0
- package/dist/tw/typography.css +34 -0
- package/dist/tw/utilities/content.css +361 -0
- package/dist/tw/utilities/index.css +6 -0
- package/dist/tw/utilities/layout.css +107 -0
- package/docs/accessibility.md +92 -0
- package/docs/changelog/0.1.0.md +37 -0
- package/docs/changelog/0.1.1.md +10 -0
- package/docs/changelog/index.md +10 -0
- package/docs/concepts.md +68 -0
- package/docs/index.md +35 -0
- package/docs/integrating/astro.md +35 -0
- package/docs/integrating/index.md +41 -0
- package/docs/integrating/nextjs.md +67 -0
- package/docs/integrating/svelte.md +43 -0
- package/docs/integrating/tailwind.md +56 -0
- package/docs/integrating/vue.md +31 -0
- package/docs/setup.md +96 -0
- package/docs/usage/aesthetics.md +120 -0
- package/docs/usage/buttons.md +58 -0
- package/docs/usage/composing.md +143 -0
- package/docs/usage/content-and-layout.md +175 -0
- package/docs/usage/customizing.md +148 -0
- package/docs/usage/feedback.md +79 -0
- package/docs/usage/forms.md +135 -0
- package/docs/usage/index.md +24 -0
- package/docs/usage/theming.md +154 -0
- package/docs/usage/tooltips.md +39 -0
- package/llms-full.txt +1676 -0
- package/llms.txt +26 -0
- package/package.json +176 -33
- package/src/components/button.css +0 -155
- package/src/components/feedback.css +0 -214
- package/src/components/form.css +0 -303
- package/src/components/index.css +0 -9
- package/src/components/loader.css +0 -56
- package/src/components/surface.css +0 -6
- package/src/components/tooltip.css +0 -60
- package/src/index.css +0 -7
- package/src/overrides.css +0 -76
- package/src/theme.css +0 -234
- package/src/typography.css +0 -30
- 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.
|