@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.
- package/LICENSE +201 -201
- package/NOTICE +27 -0
- package/README.md +60 -92
- 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 +107 -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 +185 -0
- package/dist/tw/components/button.css +88 -0
- package/dist/tw/components/feedback.css +163 -0
- package/dist/tw/components/form.css +601 -0
- package/dist/tw/components/index.css +18 -0
- package/dist/tw/components/loader.css +122 -0
- package/dist/tw/components/surface.css +98 -0
- package/dist/tw/components/tooltip.css +132 -0
- package/dist/tw/elevation.css +39 -0
- package/dist/tw/index.css +12 -0
- package/dist/tw/intent.css +189 -0
- package/dist/tw/native.css +203 -0
- package/dist/tw/presentation.css +77 -0
- package/dist/tw/reset.css +142 -0
- package/dist/tw/theme.css +218 -0
- package/dist/tw/typography.css +34 -0
- package/dist/tw/utilities/content.css +349 -0
- package/dist/tw/utilities/index.css +6 -0
- package/dist/tw/utilities/layout.css +107 -0
- package/docs/accessibility.md +82 -0
- package/docs/concepts.md +94 -0
- package/docs/index.md +62 -0
- package/docs/integrating/astro.md +29 -0
- package/docs/integrating/index.md +32 -0
- package/docs/integrating/nextjs.md +44 -0
- package/docs/integrating/svelte.md +37 -0
- package/docs/integrating/tailwind.md +40 -0
- package/docs/integrating/vue.md +25 -0
- package/docs/setup.md +116 -0
- package/docs/usage/aesthetics.md +197 -0
- package/docs/usage/buttons.md +74 -0
- package/docs/usage/composing.md +218 -0
- package/docs/usage/content-and-layout.md +200 -0
- package/docs/usage/customizing.md +213 -0
- package/docs/usage/feedback.md +117 -0
- package/docs/usage/forms.md +177 -0
- package/docs/usage/index.md +32 -0
- package/docs/usage/theming.md +210 -0
- package/docs/usage/tooltips.md +45 -0
- package/llms-full.txt +2122 -0
- package/llms.txt +26 -0
- package/package.json +95 -29
- 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,601 @@
|
|
|
1
|
+
@reference "tailwindcss";
|
|
2
|
+
@import "../theme.css";
|
|
3
|
+
@reference "../box.css";
|
|
4
|
+
|
|
5
|
+
/* What all six text controls share: the box, the chrome, and two bounds that
|
|
6
|
+
exist because presentation cascades -- a cascaded-fill cap so a container's
|
|
7
|
+
`.solid` cannot fill a field with its own text colour, and an edge floor so a
|
|
8
|
+
container's `.edgeless` cannot leave a field or an unchecked toggle with no
|
|
9
|
+
boundary (WCAG 1.4.11). `switch` opts out of the floor on two of its own
|
|
10
|
+
fills, the only component that can, because its knob still carries the state;
|
|
11
|
+
`.checkbox` and `.radio` have nothing outside their line and cannot.
|
|
12
|
+
|
|
13
|
+
No derived hover: on something you type into, only the line responds to a
|
|
14
|
+
pointer. See model.md#the-bounds-that-survive-and-the-test-for-keeping-one. */
|
|
15
|
+
@utility text-control {
|
|
16
|
+
@apply box;
|
|
17
|
+
|
|
18
|
+
/* The resting geometry, stated whole for the same reason every component
|
|
19
|
+
states it: `box` reads these three with no fallback, so a slot left out is
|
|
20
|
+
an undefined `var()` inside a `color-mix()` and the declaration reading it
|
|
21
|
+
collapses to its initial value rather than reporting anything. `text-control`
|
|
22
|
+
is typeable on its own, so it owes them exactly as a component does. */
|
|
23
|
+
--_d-fill: 0%;
|
|
24
|
+
--_d-fg-on-fill: 0%;
|
|
25
|
+
--_d-border: 100%;
|
|
26
|
+
|
|
27
|
+
/* The cap used to be 12%, which is exactly what `.soft` asks for, so `min()`
|
|
28
|
+
handed `.solid` and `.soft` the same tint and three fill names rendered two
|
|
29
|
+
boxes. 6% is a quiet wash, and it is the cap rather than a presentation:
|
|
30
|
+
`.soft` names the tint it wants and takes it whole below, so what this
|
|
31
|
+
governs is `.solid` and the container that cascades one onto a field nobody
|
|
32
|
+
classed.
|
|
33
|
+
|
|
34
|
+
Named rather than written into the `min()`, because the soft hover below is
|
|
35
|
+
derived from it and the two must not drift apart. Typed text is
|
|
36
|
+
`--intent-color` over a wash of its own tone, past 10:1 in both themes at
|
|
37
|
+
any value this cap is likely to take, so the bound still does what it was
|
|
38
|
+
written for. */
|
|
39
|
+
--_fill-cap: 6%;
|
|
40
|
+
/* The floor is zero everywhere except a checked toggle, which needs a ground
|
|
41
|
+
under its mark. A container's `.ghost` would otherwise leave a checked
|
|
42
|
+
checkbox as a tick floating on the page -- our own cascade producing the
|
|
43
|
+
broken result, which is the test a bound has to pass. */
|
|
44
|
+
--_fill-floor: 0%;
|
|
45
|
+
--_fill: calc(
|
|
46
|
+
clamp(
|
|
47
|
+
var(--_fill-floor),
|
|
48
|
+
min(var(--ui-fill, var(--_d-fill)), var(--_fill-cap), var(--intent-fill-max, 100%)),
|
|
49
|
+
100%
|
|
50
|
+
) *
|
|
51
|
+
var(--ui-bg-alpha, 1)
|
|
52
|
+
);
|
|
53
|
+
/* The ink follows *this* control's fill, not `box`'s. `box` ramps against
|
|
54
|
+
`--_capped`, which knows `--intent-fill-max` but not `--_fill-cap`, so on a
|
|
55
|
+
control it would read a fill far higher than the one actually painted: a
|
|
56
|
+
`.primary.solid` checkbox has `--ui-fill: 100%` and an unbounded intent, so
|
|
57
|
+
`box` would call for the contrast tone while the plate sits at 20%. White
|
|
58
|
+
tick on light grey.
|
|
59
|
+
|
|
60
|
+
Ramping against `--_fill` keeps the two in step at every bound, including
|
|
61
|
+
the checked state that lifts them. */
|
|
62
|
+
--_on-fill: min(var(--ui-fg-on-fill, var(--_d-fg-on-fill)), max(0%, calc(var(--_fill) * 2 - 100%)));
|
|
63
|
+
/* The resting line is a fraction of the intent rather than all of it, and the
|
|
64
|
+
fraction is what makes the pointer visible. Drawn at full strength a control
|
|
65
|
+
already sits at its intent's darkest, and hover moves it to a neighbouring
|
|
66
|
+
tone -- `--color-primary` to `--color-primary-hover` is one step in the
|
|
67
|
+
palette, which reads as nothing. Only the neutral family looked right,
|
|
68
|
+
because its resting tone is a border token and its hover is the text one,
|
|
69
|
+
two places apart.
|
|
70
|
+
|
|
71
|
+
Resting at 60% gives every intent that distance: hover and focus both take
|
|
72
|
+
the tone whole, so the line arrives rather than shifts. A toggle sets this
|
|
73
|
+
back to 100% -- its line is a silhouette rather than a field's edge, and
|
|
74
|
+
there is nothing else marking an unchecked box. */
|
|
75
|
+
--_line-rest: 60%;
|
|
76
|
+
/* Two seams rather than one composed result. `--_line-tone` is what colour the
|
|
77
|
+
line is, and the states below move it; `--_line` folds in P3's blend toward
|
|
78
|
+
the fill, exactly as `box` does it, so the two agree and a state never has to
|
|
79
|
+
restate the blend to keep it.
|
|
80
|
+
|
|
81
|
+
Written as a single `--_line` that states overwrote, a hovered or focused
|
|
82
|
+
control lost the blend along with the tone, because replacing a composed
|
|
83
|
+
result drops every term it was composed from. Same failure as the edge
|
|
84
|
+
below, one property over. */
|
|
85
|
+
--_line-tone: color-mix(in oklab, var(--intent-border) var(--_line-rest), transparent);
|
|
86
|
+
--_line: color-mix(in oklab, var(--intent-color) var(--_fill), var(--_line-tone));
|
|
87
|
+
/* The edge floor, as an input to `box` rather than a rewrite of its result.
|
|
88
|
+
The line is drawn whatever a *container's* edge class says, because a
|
|
89
|
+
`.edgeless` toolbar would otherwise leave a field with no mark of where
|
|
90
|
+
typing goes and a toggle with no silhouette at all (WCAG 1.4.11). That is
|
|
91
|
+
our own cascade producing the broken result, so our own floor answers it.
|
|
92
|
+
|
|
93
|
+
An element's own `.edgeless` lowers the floor -- see the rule below the
|
|
94
|
+
utility -- which is a consumer naming what they want rather than a cascade
|
|
95
|
+
reaching something nobody classed. `.checkbox` and `.radio` are left off
|
|
96
|
+
that rule and keep the floor absolute: they have nothing outside their line,
|
|
97
|
+
so removing it removes the control.
|
|
98
|
+
|
|
99
|
+
This used to be `--_edge: color-mix(...)` written after `@apply box`, which
|
|
100
|
+
dropped `--ui-border` out of the composition entirely. `.edged` and
|
|
101
|
+
`.edgeless` were both inert on all six text controls, and `--_d-border` was
|
|
102
|
+
declared and never read on each of them. */
|
|
103
|
+
--_edge-floor: 100%;
|
|
104
|
+
|
|
105
|
+
@apply w-full appearance-none;
|
|
106
|
+
|
|
107
|
+
box-sizing: border-box;
|
|
108
|
+
|
|
109
|
+
&::placeholder {
|
|
110
|
+
color: var(--color-text-secondary);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
&:hover:not(:disabled, [aria-disabled="true"], [data-disabled], :focus-visible) {
|
|
114
|
+
--_line-tone: var(--intent-hover);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* The line moves to the intent on top of the `reset.css` `:focus-visible`
|
|
118
|
+
outline -- a second signal, not a replacement. This must NOT suppress the
|
|
119
|
+
outline: `&:focus { outline-none }` nested here lands in the utilities layer
|
|
120
|
+
and beats `reset.css`'s `@layer base` ring, killing the keyboard ring too. */
|
|
121
|
+
&:focus-visible {
|
|
122
|
+
--_line-tone: var(--intent-color);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* R8, P6: `:checked` lifts the bounds and lets the fill class compose, rather
|
|
126
|
+
than pinning `--_fill`/`--_fg` (which made every checked toggle render
|
|
127
|
+
identically). The floor keeps a mark on a ground when a container's `.ghost`
|
|
128
|
+
would take it away. Lives here, not on the toggles, because a text input is
|
|
129
|
+
never `:checked`. */
|
|
130
|
+
&:checked {
|
|
131
|
+
--_fill-cap: 100%;
|
|
132
|
+
--_fill-floor: 12%;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Native decorations a field replaces with its own artwork. */
|
|
136
|
+
&::-webkit-search-decoration,
|
|
137
|
+
&::-webkit-search-cancel-button,
|
|
138
|
+
&::-webkit-search-results-button,
|
|
139
|
+
&::-webkit-search-results-decoration,
|
|
140
|
+
&::-webkit-calendar-picker-indicator,
|
|
141
|
+
&::-webkit-inner-spin-button,
|
|
142
|
+
&::-webkit-clear-button {
|
|
143
|
+
-webkit-appearance: none;
|
|
144
|
+
display: none !important;
|
|
145
|
+
opacity: 0;
|
|
146
|
+
pointer-events: none;
|
|
147
|
+
width: 0;
|
|
148
|
+
height: 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* A fill class on the element names its own cap; the 6% cascade cap in the
|
|
153
|
+
utility is for a container reaching a field nobody classed. Both raise
|
|
154
|
+
`--_fill-cap` rather than writing `--_fill` (P6), which is also what keeps
|
|
155
|
+
`.soft` < `.solid` -- written as a rewrite the louder name rendered the
|
|
156
|
+
quieter box, measured and inverted on `.ipt`, `.checkbox` and `.radio`.
|
|
157
|
+
20% clears `.soft`'s 12% and matches the plate a `.solid` badge draws.
|
|
158
|
+
|
|
159
|
+
`.switch` is deliberately absent: its track caps at 40% in its own utility,
|
|
160
|
+
and an unlayered rule here would overrule that. See model.md#the-bounds-that-survive-and-the-test-for-keeping-one. */
|
|
161
|
+
:is(.text-control, .ipt, .textarea, .select, .input-group).soft {
|
|
162
|
+
--_fill-cap: 12%;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
:is(.text-control, .ipt, .textarea, .select, .input-group).solid {
|
|
166
|
+
--_fill-cap: 20%;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* A checked toggle fills with its own intent, so neutral has to drop its 20%
|
|
170
|
+
cap here. Declared where the intents are (0-2-0, unlayered): inside
|
|
171
|
+
`text-control` `.neutral` would overrule it and a checked toggle would fill to
|
|
172
|
+
a fifth of its colour. */
|
|
173
|
+
:is(.text-control, .checkbox, .radio, .switch, input):checked {
|
|
174
|
+
--intent-fill-max: 100%;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* The element's own `.edgeless` lowers the floor `text-control` raises -- a
|
|
178
|
+
class on the element is a consumer's request, a class on a container is a
|
|
179
|
+
cascade. `.checkbox` and `.radio` are absent: an unchecked one is a
|
|
180
|
+
transparent square/circle with nothing but its line, so the floor is absolute
|
|
181
|
+
and `.edgeless` on them is published unsupported. `.input-group` is on it
|
|
182
|
+
because the group is the field boundary now. */
|
|
183
|
+
:is(.text-control, .ipt, .textarea, .select, .switch, .input-group).edgeless {
|
|
184
|
+
--_edge-floor: 0%;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* An invalid control is destructive whatever else it was asked to be. A plain
|
|
188
|
+
unlayered rule, not `&[aria-invalid]` inside `@utility`: the intent reset is
|
|
189
|
+
unlayered too and would overrule a layered version (leaving a grey line and
|
|
190
|
+
only the hint marking the error). `:is()` gives it 0-2-0 to outrank an intent
|
|
191
|
+
class. `--intent-hover` is redirected with the others so the line does not go
|
|
192
|
+
grey under the pointer. The `:has()` arm bridges an inner control's state to
|
|
193
|
+
an `.input-group`. */
|
|
194
|
+
:is(
|
|
195
|
+
.text-control,
|
|
196
|
+
.ipt,
|
|
197
|
+
.textarea,
|
|
198
|
+
.select,
|
|
199
|
+
.checkbox,
|
|
200
|
+
.radio,
|
|
201
|
+
.switch,
|
|
202
|
+
.input-group,
|
|
203
|
+
input,
|
|
204
|
+
textarea,
|
|
205
|
+
select
|
|
206
|
+
)[aria-invalid="true"],
|
|
207
|
+
.input-group:has(> :is(.ipt, .textarea, .select, .text-control, input, textarea, select)[aria-invalid="true"]) {
|
|
208
|
+
--intent-color: var(--color-destructive);
|
|
209
|
+
--intent-hover: var(--color-destructive-hover);
|
|
210
|
+
--intent-border: var(--color-destructive);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* A ghost, edged, unelevated text control. */
|
|
214
|
+
@utility ipt {
|
|
215
|
+
@apply text-control h-(--control-height,2.5rem) px-3 py-2;
|
|
216
|
+
|
|
217
|
+
/* After the `@apply`, not before: it inlines `text-control`'s own resting
|
|
218
|
+
geometry at that point, so a component's defaults written above it lose to
|
|
219
|
+
the utility they are meant to specialise. */
|
|
220
|
+
--_d-fill: 0%;
|
|
221
|
+
--_d-fg-on-fill: 0%;
|
|
222
|
+
--_d-border: 100%;
|
|
223
|
+
--_d-elevation: 0;
|
|
224
|
+
|
|
225
|
+
/* Typed text is the intent colour at full strength -- on the three text
|
|
226
|
+
inputs, not `text-control`, because a toggle's mark takes `box`'s composed
|
|
227
|
+
`--_fg` instead. */
|
|
228
|
+
--_fg: var(--intent-color);
|
|
229
|
+
|
|
230
|
+
min-height: var(--control-height);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@utility field {
|
|
234
|
+
--field-gap: 0.35rem;
|
|
235
|
+
|
|
236
|
+
@apply flex w-full flex-col gap-(--field-gap);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@utility label {
|
|
240
|
+
font-size: 1rem;
|
|
241
|
+
font-weight: 500;
|
|
242
|
+
letter-spacing: 0.025em;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@utility hint {
|
|
246
|
+
color: var(--color-text-secondary);
|
|
247
|
+
font-size: 0.875rem;
|
|
248
|
+
font-weight: 400;
|
|
249
|
+
letter-spacing: 0.05em;
|
|
250
|
+
line-height: 100%;
|
|
251
|
+
|
|
252
|
+
&.error {
|
|
253
|
+
color: var(--intent-color);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* A ghost, edged, unelevated multi-line text control. */
|
|
258
|
+
@utility textarea {
|
|
259
|
+
@apply text-control px-3 py-2;
|
|
260
|
+
|
|
261
|
+
--_d-fill: 0%;
|
|
262
|
+
--_d-fg-on-fill: 0%;
|
|
263
|
+
--_d-border: 100%;
|
|
264
|
+
--_d-elevation: 0;
|
|
265
|
+
|
|
266
|
+
/* Typed text is the intent colour at full strength -- on the three text
|
|
267
|
+
inputs, not `text-control`, because a toggle's mark takes `box`'s composed
|
|
268
|
+
`--_fg` instead. */
|
|
269
|
+
--_fg: var(--intent-color);
|
|
270
|
+
|
|
271
|
+
@apply min-h-28 resize-y;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* The same control with a native list behind it. The chevron is drawn from two
|
|
275
|
+
gradients rather than masked, because `appearance: none` removes the platform
|
|
276
|
+
arrow and a background image is the only thing a replaced element can paint. */
|
|
277
|
+
@utility select {
|
|
278
|
+
@apply text-control h-(--control-height,2.5rem) py-2 pr-10 pl-3;
|
|
279
|
+
|
|
280
|
+
--_d-fill: 0%;
|
|
281
|
+
--_d-fg-on-fill: 0%;
|
|
282
|
+
--_d-border: 100%;
|
|
283
|
+
--_d-elevation: 0;
|
|
284
|
+
|
|
285
|
+
/* Typed text is the intent colour at full strength -- on the three text
|
|
286
|
+
inputs, not `text-control`, because a toggle's mark takes `box`'s composed
|
|
287
|
+
`--_fg` instead. */
|
|
288
|
+
--_fg: var(--intent-color);
|
|
289
|
+
|
|
290
|
+
min-height: var(--control-height);
|
|
291
|
+
background-image:
|
|
292
|
+
linear-gradient(45deg, transparent 50%, currentColor 50%),
|
|
293
|
+
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
|
294
|
+
background-position:
|
|
295
|
+
calc(100% - 1rem) center,
|
|
296
|
+
calc(100% - 0.6875rem) center;
|
|
297
|
+
background-repeat: no-repeat;
|
|
298
|
+
background-size: 0.3125rem 0.3125rem;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* A wrapper that owns the field box so a control can carry adornments beside it
|
|
302
|
+
-- a leading or trailing icon today, an affix or an in-field button later. It
|
|
303
|
+
composes `text-control` for the border, radius, fill cap, aesthetic shape,
|
|
304
|
+
and the invalid and disabled handling a lone field carries; `:focus-within`
|
|
305
|
+
stands in for the `:focus-visible` a wrapper cannot have. The control inside
|
|
306
|
+
goes flush: its own box paint is switched off below, so the boundary is drawn
|
|
307
|
+
once, by the group.
|
|
308
|
+
|
|
309
|
+
No icons ship. Any element works in a slot -- an inline `<svg>`, an `<img>`,
|
|
310
|
+
or a class from an icon set such as `@codenhub/icons` -- placed before the
|
|
311
|
+
control for a leading adornment and after it for a trailing one. See
|
|
312
|
+
docs/usage/forms.md#icons. */
|
|
313
|
+
@utility input-group {
|
|
314
|
+
@apply text-control flex w-full items-center gap-2;
|
|
315
|
+
|
|
316
|
+
/* After the `@apply`, for the reason `ipt` states it: `text-control` inlines
|
|
317
|
+
its own resting geometry there, so a default written above it loses to the
|
|
318
|
+
utility meant to specialise it. Ghost, edged, flat -- a field boundary and
|
|
319
|
+
nothing more. */
|
|
320
|
+
--_d-fill: 0%;
|
|
321
|
+
--_d-fg-on-fill: 0%;
|
|
322
|
+
--_d-border: 100%;
|
|
323
|
+
--_d-elevation: 0;
|
|
324
|
+
|
|
325
|
+
--input-group-pad: 0.75rem;
|
|
326
|
+
|
|
327
|
+
min-height: var(--control-height);
|
|
328
|
+
padding-inline: var(--input-group-pad);
|
|
329
|
+
|
|
330
|
+
/* The focus ring the wrapper cannot get from `:focus-visible` -- a `<div>` is
|
|
331
|
+
not focusable, and `:has(:focus-visible)` is unreliable because the inner
|
|
332
|
+
control only matches `:focus-visible` while the document itself has focus.
|
|
333
|
+
`:focus-within` is the honest stand-in: the `reset.css` outline every
|
|
334
|
+
control draws, plus `box`'s inset layer when a clipping aesthetic asks for
|
|
335
|
+
one, and the focus line tone a field states for itself. The control inside
|
|
336
|
+
drops its own ring below so the group shows one. The cost is that the ring
|
|
337
|
+
also shows on a mouse click, which a lone field's does not. */
|
|
338
|
+
&:focus-within {
|
|
339
|
+
--_line-tone: var(--intent-color);
|
|
340
|
+
|
|
341
|
+
outline: var(--focus-ring-width) solid var(--focus-ring);
|
|
342
|
+
outline-offset: var(--focus-ring-offset);
|
|
343
|
+
box-shadow: var(--_focus-stack, var(--_shadow));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* A disabled control inside dims the whole group. `.input-group.disabled` and
|
|
347
|
+
the aria/data spellings are already handled by `box`'s own rule, carried in
|
|
348
|
+
by `text-control`; this is the child-driven case a wrapper cannot see
|
|
349
|
+
without `:has()`. */
|
|
350
|
+
&:has(> :is(.ipt, .textarea, .select, .text-control, input, textarea, select):disabled) {
|
|
351
|
+
@apply cursor-not-allowed;
|
|
352
|
+
|
|
353
|
+
opacity: 0.58;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* The control goes flush: the group drew the box, so the control paints no
|
|
357
|
+
fill, no line and no ring, and gives its horizontal padding to the group.
|
|
358
|
+
`border: none` rather than a transparent 1px, so the control does not add
|
|
359
|
+
its own width inside the group. */
|
|
360
|
+
& > :is(.ipt, .textarea, .select, .text-control, input, textarea, select) {
|
|
361
|
+
--_fill-cap: 0%;
|
|
362
|
+
|
|
363
|
+
flex: 1 1 auto;
|
|
364
|
+
min-width: 0;
|
|
365
|
+
padding-inline: 0;
|
|
366
|
+
border: none;
|
|
367
|
+
background-color: transparent;
|
|
368
|
+
box-shadow: none;
|
|
369
|
+
|
|
370
|
+
&:focus-visible {
|
|
371
|
+
outline: none;
|
|
372
|
+
box-shadow: none;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* Anything else in the group -- an icon, affix text -- does not stretch and
|
|
377
|
+
reads as secondary. A nested `<button>` keeps its own styling. */
|
|
378
|
+
& > :not(.ipt, .textarea, .select, .text-control, input, textarea, select, button) {
|
|
379
|
+
@apply inline-flex flex-none items-center;
|
|
380
|
+
|
|
381
|
+
color: var(--color-text-secondary);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* The three toggles compose `text-control` for its bounds, not its chrome: the
|
|
386
|
+
edge floor keeps a container's `.edgeless` from erasing the closed silhouette
|
|
387
|
+
(WCAG 1.4.11), and the checked state that lifts the fill bounds lives there.
|
|
388
|
+
They raise the fill cap to 20% -- the 6% one keeps typed text legible and a
|
|
389
|
+
toggle has none; 20% is a `.solid` badge's plate and separates the three
|
|
390
|
+
fills. `.ghost` is unsupported (an unchecked ghost toggle is the silhouette
|
|
391
|
+
it already has, a checked one a mark on nothing); the registry records it and
|
|
392
|
+
the checked fill floor clamps a cascaded one. See model.md#unsupported-values. */
|
|
393
|
+
@utility checkbox {
|
|
394
|
+
@apply text-control inline-grid size-4 cursor-pointer place-content-center;
|
|
395
|
+
|
|
396
|
+
--_d-fill: 100%;
|
|
397
|
+
--_d-fg-on-fill: 100%;
|
|
398
|
+
--_fill-cap: 20%;
|
|
399
|
+
--_line-rest: 100%;
|
|
400
|
+
--_d-border: 100%;
|
|
401
|
+
--_d-elevation: 0;
|
|
402
|
+
|
|
403
|
+
/* Capped at its own radius: a control radius is sized for a 40px control, and
|
|
404
|
+
14px of it on a 16px box renders as a circle, which is the one shape a
|
|
405
|
+
checkbox must not be. A squaring aesthetic still reaches it. */
|
|
406
|
+
border-radius: min(var(--ui-radius, var(--radius-small)), var(--radius-small));
|
|
407
|
+
|
|
408
|
+
/* Chip geometry: the tight silhouette, and an inset edge no thicker than the
|
|
409
|
+
line the aesthetic draws elsewhere. A 16px box cannot spend four pixels a
|
|
410
|
+
side on an edge -- under `.pixel` that leaves an eight-pixel hole and an
|
|
411
|
+
unchecked box reads as a filled one. Our own aesthetic on our own component
|
|
412
|
+
produces that, so our own `min()` answers it. */
|
|
413
|
+
--_ss: min(var(--ui-shadow-spread, 0px), var(--ui-border-width, var(--border-width)));
|
|
414
|
+
|
|
415
|
+
clip-path: var(--ui-clip-tight, var(--ui-clip, none));
|
|
416
|
+
|
|
417
|
+
/* `text-control` trades the outline for its own line colour, which reads on a
|
|
418
|
+
40px field and not on a 16px box. The ring goes back as a shadow layer
|
|
419
|
+
rather than as an `outline`, so a clipping aesthetic clips it with
|
|
420
|
+
everything else and its own inset focus layer still paints over the top. */
|
|
421
|
+
&:focus-visible {
|
|
422
|
+
--_shadow: var(--_shadow-parts), 0 0 0 var(--focus-ring-width) var(--focus-ring);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
&::after {
|
|
426
|
+
@apply block size-3 opacity-0 transition-opacity duration-(--motion-duration-normal) ease-(--motion-ease);
|
|
427
|
+
|
|
428
|
+
/* The mark takes the box's own foreground, which `box` already mixed against
|
|
429
|
+
this component's intent and fill, so a checked box in any intent has a
|
|
430
|
+
mark that reads against it. */
|
|
431
|
+
background-color: currentcolor;
|
|
432
|
+
content: "";
|
|
433
|
+
mask-image: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
|
|
434
|
+
-webkit-mask-image: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
|
|
435
|
+
mask-size: contain;
|
|
436
|
+
-webkit-mask-size: contain;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
&:checked::after {
|
|
440
|
+
@apply opacity-100;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* The same control with a round silhouette and a dot for its mark. */
|
|
445
|
+
@utility radio {
|
|
446
|
+
@apply text-control inline-grid size-4 cursor-pointer place-content-center rounded-full;
|
|
447
|
+
|
|
448
|
+
--_d-fill: 100%;
|
|
449
|
+
--_d-fg-on-fill: 100%;
|
|
450
|
+
--_fill-cap: 20%;
|
|
451
|
+
--_line-rest: 100%;
|
|
452
|
+
--_d-border: 100%;
|
|
453
|
+
--_d-elevation: 0;
|
|
454
|
+
|
|
455
|
+
/* The inset edge is capped for the reason given on `checkbox`, but the
|
|
456
|
+
silhouette is not taken: the circle is the only thing telling a radio from a
|
|
457
|
+
checkbox at a glance, and a stepped polygon would square it. An inset shadow
|
|
458
|
+
follows `border-radius`, so the edge stays round without one. */
|
|
459
|
+
--_ss: min(var(--ui-shadow-spread, 0px), var(--ui-border-width, var(--border-width)));
|
|
460
|
+
|
|
461
|
+
clip-path: none;
|
|
462
|
+
|
|
463
|
+
/* The ring, for the reason given on `checkbox`. */
|
|
464
|
+
&:focus-visible {
|
|
465
|
+
--_shadow: var(--_shadow-parts), 0 0 0 var(--focus-ring-width) var(--focus-ring);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
&::after {
|
|
469
|
+
@apply block size-2 scale-0 rounded-full opacity-0 transition-[transform,opacity] duration-(--motion-duration-normal) ease-(--motion-ease);
|
|
470
|
+
|
|
471
|
+
background-color: currentcolor;
|
|
472
|
+
content: "";
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* A checked radio thickens its ring and takes the intent whole on the line;
|
|
476
|
+
the fill is left to the presentation. The dot takes `box`'s composed
|
|
477
|
+
foreground, which walks to the contrast tone as the plate fills, so a
|
|
478
|
+
filled circle still carries a readable dot. `--_line-tone` rather than
|
|
479
|
+
`--_edge`, so `text-control`'s edge floor still draws the ring under a
|
|
480
|
+
container's `.edgeless`. */
|
|
481
|
+
&:checked {
|
|
482
|
+
--_line-tone: var(--intent-color);
|
|
483
|
+
|
|
484
|
+
/* Twice the line the aesthetic draws, which is the 2px the original pinned
|
|
485
|
+
at the default hairline, and capped so a thick aesthetic cannot close the
|
|
486
|
+
circle over its own dot: 3px leaves 10px of interior for an 8px mark. */
|
|
487
|
+
border-width: min(calc(var(--_width) * 2), 3px);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
&:checked::after {
|
|
491
|
+
@apply scale-100 opacity-100;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* A2: every dimension derives from the border width `box` resolved, so the knob
|
|
496
|
+
cannot drift out of a track a thick aesthetic widened. A switch caps its fill
|
|
497
|
+
higher than the other toggles because its track is the whole component. */
|
|
498
|
+
@utility switch {
|
|
499
|
+
@apply text-control relative cursor-pointer;
|
|
500
|
+
|
|
501
|
+
--_d-fill: 100%;
|
|
502
|
+
--_d-fg-on-fill: 100%;
|
|
503
|
+
--_line-rest: 100%;
|
|
504
|
+
--_d-border: 100%;
|
|
505
|
+
--_d-elevation: 0;
|
|
506
|
+
|
|
507
|
+
/* 40%, measured: at `text-control`'s 6% the three fills collapsed to two looks
|
|
508
|
+
and the switch had to vary its *line* per fill class -- the one place a fill
|
|
509
|
+
class decided an edge. 40% separates all three (29/67/144 sRGB steps on the
|
|
510
|
+
light page, against ~20 for a visible one) without reaching a checked
|
|
511
|
+
track's 100%, so the knob's position still says which. `axes.spec.ts`
|
|
512
|
+
asserts all three. `--intent-fill-max` still caps neutral at 20%. A cap
|
|
513
|
+
rather than a resting value, because it answers the cascade case. */
|
|
514
|
+
--_fill-cap: 40%;
|
|
515
|
+
|
|
516
|
+
--switch-height: 1.25rem;
|
|
517
|
+
--switch-width: 2.5rem;
|
|
518
|
+
--switch-gap: 1px;
|
|
519
|
+
--switch-inset: calc(var(--_width) + var(--switch-gap));
|
|
520
|
+
--switch-knob: calc(var(--switch-height) - var(--switch-inset) * 2);
|
|
521
|
+
--switch-travel: calc(var(--switch-width) - var(--switch-inset) * 2 - var(--switch-knob));
|
|
522
|
+
|
|
523
|
+
height: var(--switch-height);
|
|
524
|
+
width: var(--switch-width);
|
|
525
|
+
/* Pill by default; a squaring aesthetic reaches the track and the knob together. */
|
|
526
|
+
border-radius: var(--ui-radius, calc(infinity * 1px));
|
|
527
|
+
|
|
528
|
+
/* Chip geometry, for the reason given on `checkbox`: a 20px track has no more
|
|
529
|
+
room for a thick inset edge than a 16px box does. */
|
|
530
|
+
--_ss: min(var(--ui-shadow-spread, 0px), var(--ui-border-width, var(--border-width)));
|
|
531
|
+
|
|
532
|
+
clip-path: var(--ui-clip-tight, var(--ui-clip, none));
|
|
533
|
+
|
|
534
|
+
/* The ring, for the reason given on `checkbox`. */
|
|
535
|
+
&:focus-visible {
|
|
536
|
+
--_shadow: var(--_shadow-parts), 0 0 0 var(--focus-ring-width) var(--focus-ring);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
&::after {
|
|
540
|
+
@apply absolute transition-[translate,background-color] duration-(--motion-duration-normal) ease-(--motion-ease);
|
|
541
|
+
|
|
542
|
+
/* The knob is the one part that does not take the box's foreground: an
|
|
543
|
+
unchecked track is unfilled, so a knob in that colour would be the same
|
|
544
|
+
tone as the label beside it. Checked, it takes the mark colour like the
|
|
545
|
+
other two toggles. */
|
|
546
|
+
background-color: var(--color-text-secondary);
|
|
547
|
+
border-radius: var(--ui-radius, calc(infinity * 1px));
|
|
548
|
+
content: "";
|
|
549
|
+
height: var(--switch-knob);
|
|
550
|
+
width: var(--switch-knob);
|
|
551
|
+
top: var(--switch-gap);
|
|
552
|
+
inset-inline-start: var(--switch-gap);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
&:checked::after {
|
|
556
|
+
background-color: currentcolor;
|
|
557
|
+
/* translate has no logical form, so the checked travel is named as a
|
|
558
|
+
physical x offset and mirrored under :dir(rtl) to travel toward the
|
|
559
|
+
opposite edge, matching the inset-inline-start resting position. */
|
|
560
|
+
translate: var(--switch-travel);
|
|
561
|
+
|
|
562
|
+
:dir(rtl) & {
|
|
563
|
+
translate: calc(-1 * var(--switch-travel));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
@media (forced-colors: active) {
|
|
569
|
+
.checkbox,
|
|
570
|
+
.radio,
|
|
571
|
+
.switch {
|
|
572
|
+
/* Keep these explicit system colors from being forced back to transparent. */
|
|
573
|
+
forced-color-adjust: none;
|
|
574
|
+
background-color: Canvas;
|
|
575
|
+
border-color: CanvasText;
|
|
576
|
+
transition: none;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.checkbox:checked,
|
|
580
|
+
.radio:checked,
|
|
581
|
+
.switch:checked {
|
|
582
|
+
background-color: Highlight;
|
|
583
|
+
border-color: Highlight;
|
|
584
|
+
|
|
585
|
+
&::after {
|
|
586
|
+
background-color: HighlightText;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.text-control:focus-visible,
|
|
591
|
+
.ipt:focus-visible,
|
|
592
|
+
.textarea:focus-visible,
|
|
593
|
+
.select:focus-visible,
|
|
594
|
+
.checkbox:focus-visible,
|
|
595
|
+
.radio:focus-visible,
|
|
596
|
+
.switch:focus-visible {
|
|
597
|
+
outline: 2px solid Highlight;
|
|
598
|
+
outline-offset: 2px;
|
|
599
|
+
box-shadow: none;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
@reference "tailwindcss";
|
|
2
|
+
@import "tailwindcss/utilities.css";
|
|
3
|
+
@source not "../../dist";
|
|
4
|
+
@source not "../../tests";
|
|
5
|
+
@source not "../../dev";
|
|
6
|
+
@source not "../../debug";
|
|
7
|
+
@source not "../../playground";
|
|
8
|
+
@import "../theme.css";
|
|
9
|
+
@import "../typography.css";
|
|
10
|
+
@import "../box.css";
|
|
11
|
+
@import "./button.css";
|
|
12
|
+
@import "./feedback.css";
|
|
13
|
+
@import "./form.css";
|
|
14
|
+
@import "./loader.css";
|
|
15
|
+
@import "./surface.css";
|
|
16
|
+
@import "./tooltip.css";
|
|
17
|
+
|
|
18
|
+
@source inline("text-display text-title-lg text-title text-title-sm text-label-lg text-label text-body btn field label hint ipt email password url tel search date datetime month week time icon left right textarea select checkbox radio switch alert badge skeleton progress tooltip card panel interactive flush compact spacious neutral solid soft ghost edged edgeless flat raised floating loader");
|