@codenhub/styles 0.0.3 → 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 -0
- package/NOTICE +52 -0
- package/README.md +60 -90
- 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 +43 -14
- package/docs/concepts.md +94 -0
- package/docs/index.md +62 -73
- 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 +96 -32
- package/docs/classes.md +0 -198
- package/docs/roadmap.md +0 -34
- package/docs/tests.md +0 -128
- package/docs/tokens.md +0 -101
- package/src/components/button.css +0 -155
- package/src/components/feedback.css +0 -214
- package/src/components/form.css +0 -296
- 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 -136
- package/src/typography.css +0 -30
- package/src/utilities.css +0 -40
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
@import "./theme.css";
|
|
2
|
+
@reference "tailwindcss";
|
|
3
|
+
@source not "../dist";
|
|
4
|
+
@source not "../tests";
|
|
5
|
+
@source not "../dev";
|
|
6
|
+
@source not "../debug";
|
|
7
|
+
@source not "../playground";
|
|
8
|
+
@import "./typography.css";
|
|
9
|
+
@import "./utilities/index.css";
|
|
10
|
+
@import "./components/index.css";
|
|
11
|
+
@import "./reset.css";
|
|
12
|
+
|
|
13
|
+
/* `intent.css`'s reset keys on class names; these bare elements need the same,
|
|
14
|
+
or a `color-mix()` reading an undefined `--intent-*` drops silently. Zero
|
|
15
|
+
specificity like the class reset, values matching it slot for slot -- a bare
|
|
16
|
+
`<code>` and a `.code` chip are one component reached two ways. */
|
|
17
|
+
:where(table, kbd, code, pre, blockquote, hr, input, textarea, select, button) {
|
|
18
|
+
--intent-color: var(--color-text);
|
|
19
|
+
--intent-contrast: var(--color-text-contrast);
|
|
20
|
+
--intent-hover: var(--color-text-hover);
|
|
21
|
+
--intent-strong: var(--color-text-strong);
|
|
22
|
+
--intent-subtle: var(--color-surface);
|
|
23
|
+
--intent-border: var(--ui-ink, var(--color-border));
|
|
24
|
+
--intent-fill-max: 20%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Native text controls need the same stronger neutral boundary as their classed
|
|
28
|
+
counterparts, without changing buttons, checkboxes, radios, or other inputs. */
|
|
29
|
+
:where(
|
|
30
|
+
input:not([type]),
|
|
31
|
+
input[type="text"],
|
|
32
|
+
input[type="number"],
|
|
33
|
+
input[type="email"],
|
|
34
|
+
input[type="password"],
|
|
35
|
+
input[type="url"],
|
|
36
|
+
input[type="tel"],
|
|
37
|
+
input[type="search"],
|
|
38
|
+
input[type="month"],
|
|
39
|
+
input[type="week"],
|
|
40
|
+
input[type="time"],
|
|
41
|
+
input[type="date"],
|
|
42
|
+
input[type="datetime-local"],
|
|
43
|
+
textarea,
|
|
44
|
+
select
|
|
45
|
+
) {
|
|
46
|
+
--intent-border: var(--ui-ink, var(--color-control-border));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:where(hr) {
|
|
50
|
+
--intent-color: currentColor;
|
|
51
|
+
--intent-border: currentColor;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@layer base {
|
|
55
|
+
h1,
|
|
56
|
+
h2,
|
|
57
|
+
h3,
|
|
58
|
+
h4,
|
|
59
|
+
h5,
|
|
60
|
+
h6,
|
|
61
|
+
p,
|
|
62
|
+
figure,
|
|
63
|
+
blockquote,
|
|
64
|
+
dl,
|
|
65
|
+
dd,
|
|
66
|
+
ul,
|
|
67
|
+
ol {
|
|
68
|
+
margin: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Headings */
|
|
72
|
+
h1 {
|
|
73
|
+
@apply text-title-lg;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
h2 {
|
|
77
|
+
@apply text-title;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
h3 {
|
|
81
|
+
@apply text-title-sm;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
h4 {
|
|
85
|
+
@apply text-label-lg;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
h5,
|
|
89
|
+
h6 {
|
|
90
|
+
@apply text-label;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
p,
|
|
94
|
+
li,
|
|
95
|
+
a {
|
|
96
|
+
@apply text-body;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Content */
|
|
100
|
+
table {
|
|
101
|
+
@apply data-table;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
kbd {
|
|
105
|
+
@apply kbd;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
blockquote {
|
|
109
|
+
@apply quote;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
q {
|
|
113
|
+
@apply quote-inline;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
code {
|
|
117
|
+
@apply code;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
pre {
|
|
121
|
+
@apply pre;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pre code {
|
|
125
|
+
@apply bg-transparent p-0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
hr {
|
|
129
|
+
@apply divider;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Form controls. Every text-like type maps onto `.ipt` for the shared chrome
|
|
133
|
+
and nothing else -- an icon is a wrapper composition (`.input-group`), not a
|
|
134
|
+
property of the type, so no type paints one. */
|
|
135
|
+
input:not([type]),
|
|
136
|
+
input[type="text"],
|
|
137
|
+
input[type="number"],
|
|
138
|
+
input[type="email"],
|
|
139
|
+
input[type="password"],
|
|
140
|
+
input[type="url"],
|
|
141
|
+
input[type="tel"],
|
|
142
|
+
input[type="search"],
|
|
143
|
+
input[type="month"],
|
|
144
|
+
input[type="week"],
|
|
145
|
+
input[type="time"],
|
|
146
|
+
input[type="date"],
|
|
147
|
+
input[type="datetime-local"] {
|
|
148
|
+
@apply ipt;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
textarea {
|
|
152
|
+
@apply textarea;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
select {
|
|
156
|
+
@apply select;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
input[type="checkbox"] {
|
|
160
|
+
@apply checkbox;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
input[type="radio"] {
|
|
164
|
+
@apply radio;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Buttons */
|
|
168
|
+
button,
|
|
169
|
+
input[type="button"],
|
|
170
|
+
input[type="submit"],
|
|
171
|
+
input[type="reset"] {
|
|
172
|
+
@apply btn;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@media (forced-colors: active) {
|
|
177
|
+
input[type="checkbox"],
|
|
178
|
+
input[type="radio"] {
|
|
179
|
+
/* Keep these explicit system colors from being forced back to transparent. */
|
|
180
|
+
forced-color-adjust: none;
|
|
181
|
+
background-color: Canvas;
|
|
182
|
+
border-color: CanvasText;
|
|
183
|
+
transition: none;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
input[type="checkbox"]:checked {
|
|
187
|
+
background-color: Highlight;
|
|
188
|
+
border-color: Highlight;
|
|
189
|
+
|
|
190
|
+
&::after {
|
|
191
|
+
background-color: HighlightText;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
input[type="radio"]:checked {
|
|
196
|
+
background-color: Canvas;
|
|
197
|
+
border-color: Highlight;
|
|
198
|
+
|
|
199
|
+
&::after {
|
|
200
|
+
background-color: Highlight;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* Presentation answers two independent questions, and each is its own closed
|
|
2
|
+
set: how much of the intent color fills the box, and whether the box draws a
|
|
3
|
+
line at its boundary. Exactly one value from each set applies, so the six
|
|
4
|
+
combinations are the whole vocabulary.
|
|
5
|
+
|
|
6
|
+
A fill class never decides an edge and an edge class never decides a fill.
|
|
7
|
+
`.solid` briefly broke that rule, writing `--ui-border: 0%` alongside its fill
|
|
8
|
+
so that a filled box could not be ringed in another colour. It bought a real
|
|
9
|
+
fix -- the edge blend runs the line toward the fill, which lands *on* the fill
|
|
10
|
+
when the fill is opaque and on a second coat of the same tint when it is not,
|
|
11
|
+
so every capped intent that drew a line drew a visible ring over its own plate
|
|
12
|
+
-- at the cost of the one property that makes the six combinations readable:
|
|
13
|
+
that each half means only what it says. Two axes that are independent in two
|
|
14
|
+
cases out of three are not axes.
|
|
15
|
+
|
|
16
|
+
The ring is a fault in the blend rather than in the class, and it is recorded
|
|
17
|
+
as such: `.solid.edged` on a capped intent measures 1.53:1 against its own
|
|
18
|
+
plate in light and 1.82:1 in dark. `.solid.edgeless` is how a consumer asks
|
|
19
|
+
for a filled box with no line.
|
|
20
|
+
|
|
21
|
+
Presentation declares only unitless numbers and percentages. A custom
|
|
22
|
+
property's value is resolved on the element that declares it, so anything
|
|
23
|
+
referencing an intent token here would resolve against the container's intent
|
|
24
|
+
and inherit down already-resolved. Numbers carry no intent, so they inherit
|
|
25
|
+
safely and each component mixes them against its own palette.
|
|
26
|
+
|
|
27
|
+
A declaration on the element beats an inherited one, which is what lets a
|
|
28
|
+
container set the look for its subtree while any element still opts out.
|
|
29
|
+
|
|
30
|
+
Hover is derived from these values rather than declared beside them:
|
|
31
|
+
`min(100%, resting fill + --hover-step)`, always in `--intent-hover`. There is
|
|
32
|
+
no hover token and no per-presentation branch. */
|
|
33
|
+
|
|
34
|
+
/* Filled with the intent color. Text is the contrast tone. */
|
|
35
|
+
.solid {
|
|
36
|
+
--ui-fill: 100%;
|
|
37
|
+
--ui-fg-on-fill: 100%;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Tinted with the intent color. Text is the intent color. */
|
|
41
|
+
.soft {
|
|
42
|
+
--ui-fill: 12%;
|
|
43
|
+
--ui-fg-on-fill: 0%;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* No fill at rest; the derived hover gives it one. Text is the intent color.
|
|
47
|
+
|
|
48
|
+
Named `.ghost` rather than `.bare` because `.bare` and `.edgeless` both read
|
|
49
|
+
as "less of something", so an author wanting no border reached for `.bare`
|
|
50
|
+
first and got no background instead. `.ghost` says fill and only fill.
|
|
51
|
+
|
|
52
|
+
A component that rests on a ground rather than on nothing keeps that ground
|
|
53
|
+
here: `.ghost` is zero fill, not zero paint. `.data-table` is the exception
|
|
54
|
+
that proves it -- its head plate *is* the tint, so a ghost table drops it. */
|
|
55
|
+
.ghost {
|
|
56
|
+
--ui-fill: 0%;
|
|
57
|
+
--ui-fg-on-fill: 0%;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* A line in the intent color, at the aesthetic's material width. The width comes
|
|
61
|
+
from the aesthetic and nothing else: there is no presentation scale on it, so
|
|
62
|
+
small components need no ceilings to survive a thick one.
|
|
63
|
+
|
|
64
|
+
The edge is the silhouette and nothing else. Rules *inside* a component that
|
|
65
|
+
has an inside are a separate decision with its own class -- `.data-table.ruled`
|
|
66
|
+
-- because tying them here made them arrive by implication: a table drew rules
|
|
67
|
+
only because its published edge default happened to be `edged`, so whether a
|
|
68
|
+
consumer got them depended on a default they had not read and could not name.
|
|
69
|
+
Two questions, two switches. */
|
|
70
|
+
.edged {
|
|
71
|
+
--ui-border: 100%;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* No line. */
|
|
75
|
+
.edgeless {
|
|
76
|
+
--ui-border: 0%;
|
|
77
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
@reference "tailwindcss";
|
|
2
|
+
@import "./theme.css";
|
|
3
|
+
@import "./typography.css";
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
/* Every sizing utility this package ships — `min-height`/`min-width:
|
|
7
|
+
var(--control-height)` on `.btn`, `.ipt`, `.select`, `.textarea` chief
|
|
8
|
+
among them — means that as the control's total footprint, padding and
|
|
9
|
+
border included. Without this, `apps/docs` reads correctly only because
|
|
10
|
+
its own Tailwind pipeline supplies Preflight's border-box reset
|
|
11
|
+
separately; this package only pulls in Tailwind's utility engine via
|
|
12
|
+
`@reference`, never its Preflight, so a consumer with no Tailwind
|
|
13
|
+
pipeline of its own — `apps/demo` imports this compiled stylesheet
|
|
14
|
+
directly — got the content-box default instead, inflating every sized
|
|
15
|
+
control by its own padding and border. */
|
|
16
|
+
*,
|
|
17
|
+
*::before,
|
|
18
|
+
*::after {
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
* {
|
|
23
|
+
scrollbar-width: thin;
|
|
24
|
+
scrollbar-color: var(--color-primary) var(--color-background);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
::-webkit-scrollbar {
|
|
28
|
+
width: calc(var(--spacing) * 4);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
::-webkit-scrollbar-thumb {
|
|
32
|
+
background: var(--color-primary);
|
|
33
|
+
border: var(--spacing) solid var(--color-background);
|
|
34
|
+
border-radius: calc(var(--spacing) * 2);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
html,
|
|
38
|
+
body {
|
|
39
|
+
@apply bg-background text-text font-default min-h-dvh;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
h1,
|
|
43
|
+
h2,
|
|
44
|
+
h3,
|
|
45
|
+
h4,
|
|
46
|
+
h5,
|
|
47
|
+
h6,
|
|
48
|
+
p,
|
|
49
|
+
figure,
|
|
50
|
+
blockquote,
|
|
51
|
+
dl,
|
|
52
|
+
dd,
|
|
53
|
+
ul,
|
|
54
|
+
ol {
|
|
55
|
+
margin: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
body {
|
|
59
|
+
@apply flex flex-col;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
button,
|
|
63
|
+
input,
|
|
64
|
+
optgroup,
|
|
65
|
+
select,
|
|
66
|
+
textarea {
|
|
67
|
+
font-family: inherit;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
button,
|
|
71
|
+
input[type="button"],
|
|
72
|
+
input[type="submit"],
|
|
73
|
+
input[type="reset"] {
|
|
74
|
+
@apply enabled:cursor-pointer;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
img {
|
|
78
|
+
@apply select-none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
::selection {
|
|
82
|
+
background: var(--color-primary);
|
|
83
|
+
color: var(--color-primary-contrast);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
:focus-visible {
|
|
87
|
+
outline: var(--focus-ring-width) solid var(--focus-ring);
|
|
88
|
+
outline-offset: var(--focus-ring-offset);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media (forced-colors: active) {
|
|
92
|
+
input:is(
|
|
93
|
+
[type="text"],
|
|
94
|
+
[type="email"],
|
|
95
|
+
[type="password"],
|
|
96
|
+
[type="url"],
|
|
97
|
+
[type="tel"],
|
|
98
|
+
[type="number"],
|
|
99
|
+
[type="search"],
|
|
100
|
+
[type="date"],
|
|
101
|
+
[type="datetime-local"],
|
|
102
|
+
[type="month"],
|
|
103
|
+
[type="week"],
|
|
104
|
+
[type="time"],
|
|
105
|
+
[type="checkbox"],
|
|
106
|
+
[type="radio"]
|
|
107
|
+
):focus-visible,
|
|
108
|
+
textarea:focus-visible,
|
|
109
|
+
select:focus-visible {
|
|
110
|
+
outline: 2px solid Highlight !important;
|
|
111
|
+
outline-offset: 2px;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@media (prefers-reduced-motion: reduce) {
|
|
116
|
+
*,
|
|
117
|
+
*::before,
|
|
118
|
+
*::after {
|
|
119
|
+
animation-duration: 0.01ms !important;
|
|
120
|
+
animation-iteration-count: 1 !important;
|
|
121
|
+
scroll-behavior: auto !important;
|
|
122
|
+
transition-duration: 0.01ms !important;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@layer components {
|
|
128
|
+
[aria-current="page"] {
|
|
129
|
+
@apply text-primary font-bold;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@media (forced-colors: active) {
|
|
133
|
+
.btn,
|
|
134
|
+
.ipt,
|
|
135
|
+
.textarea,
|
|
136
|
+
.select,
|
|
137
|
+
.alert,
|
|
138
|
+
.badge {
|
|
139
|
+
border-color: CanvasText;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
@import "tailwindcss/theme.css";
|
|
2
|
+
@import "./intent.css";
|
|
3
|
+
@import "./presentation.css";
|
|
4
|
+
@import "./elevation.css";
|
|
5
|
+
@source not "../dist";
|
|
6
|
+
@source not "../tests";
|
|
7
|
+
@source not "../dev";
|
|
8
|
+
@source not "../debug";
|
|
9
|
+
@source not "../playground";
|
|
10
|
+
|
|
11
|
+
/* Color tokens are declared once with `light-dark()`, which resolves at the
|
|
12
|
+
point of use against the consuming element's computed `color-scheme`. Theme
|
|
13
|
+
selectors below therefore set `color-scheme` and nothing else. Verified
|
|
14
|
+
across Chromium, Firefox, and WebKit, including resolution through
|
|
15
|
+
`color-mix()` and through Tailwind's `@theme`, which emits theme values as
|
|
16
|
+
unregistered custom properties rather than `@property` registrations. */
|
|
17
|
+
@theme {
|
|
18
|
+
--font-default: "Segoe UI", system-ui, sans-serif;
|
|
19
|
+
|
|
20
|
+
--color-primary: light-dark(var(--color-neutral-950), var(--color-neutral-50));
|
|
21
|
+
--color-primary-contrast: light-dark(var(--color-neutral-50), var(--color-neutral-950));
|
|
22
|
+
--color-primary-hover: light-dark(var(--color-neutral-700), var(--color-neutral-200));
|
|
23
|
+
--color-primary-subtle: light-dark(var(--color-neutral-200), var(--color-neutral-900));
|
|
24
|
+
--color-primary-strong: light-dark(var(--color-neutral-900), var(--color-neutral-100));
|
|
25
|
+
--color-accent: light-dark(var(--color-neutral-600), var(--color-neutral-400));
|
|
26
|
+
--color-accent-contrast: light-dark(var(--color-neutral-50), var(--color-neutral-950));
|
|
27
|
+
--color-accent-hover: light-dark(var(--color-neutral-700), var(--color-neutral-300));
|
|
28
|
+
--color-accent-subtle: light-dark(var(--color-neutral-100), var(--color-neutral-800));
|
|
29
|
+
--color-accent-strong: light-dark(var(--color-neutral-800), var(--color-neutral-200));
|
|
30
|
+
--color-border: light-dark(var(--color-neutral-300), var(--color-neutral-700));
|
|
31
|
+
--color-border-hover: light-dark(var(--color-neutral-600), var(--color-neutral-500));
|
|
32
|
+
|
|
33
|
+
/* A text control's line is the only thing identifying it: the box is
|
|
34
|
+
transparent at rest, so nothing else marks where typing goes.
|
|
35
|
+
`--color-border` is neutral-300/700 and measures 1.42:1 on the light page,
|
|
36
|
+
which is a line you can barely see.
|
|
37
|
+
|
|
38
|
+
One value rather than a `light-dark()` pair, and that is the point: the page
|
|
39
|
+
is neutral-50 in light and neutral-950 in dark, so a tone halfway between
|
|
40
|
+
them reads on both grounds at once. A text control draws it at
|
|
41
|
+
`--_line-rest` rather than whole, which is what keeps the field quiet while
|
|
42
|
+
leaving the pointer somewhere to go -- so the line as painted lands under
|
|
43
|
+
the 3:1 WCAG 1.4.11 asks of a control boundary. Recorded in
|
|
44
|
+
docs/accessibility.md. */
|
|
45
|
+
--color-control-border: var(--color-neutral-500);
|
|
46
|
+
--color-background: light-dark(var(--color-neutral-50), var(--color-neutral-950));
|
|
47
|
+
--color-foreground: light-dark(var(--color-neutral-100), var(--color-neutral-900));
|
|
48
|
+
--color-surface: light-dark(var(--color-neutral-200), var(--color-neutral-800));
|
|
49
|
+
--color-text: light-dark(var(--color-neutral-950), var(--color-neutral-50));
|
|
50
|
+
--color-text-secondary: light-dark(var(--color-neutral-700), var(--color-neutral-400));
|
|
51
|
+
--color-text-contrast: light-dark(var(--color-neutral-50), var(--color-neutral-950));
|
|
52
|
+
--color-text-hover: light-dark(var(--color-neutral-700), var(--color-neutral-200));
|
|
53
|
+
--color-text-subtle: light-dark(var(--color-neutral-200), var(--color-neutral-900));
|
|
54
|
+
--color-text-strong: light-dark(var(--color-neutral-900), var(--color-neutral-100));
|
|
55
|
+
|
|
56
|
+
/* The tooltip bubble is the one component whose plate is chosen rather than
|
|
57
|
+
derived. Everything else composes its ground from the intent, and for the
|
|
58
|
+
bubble that produced the same 20% of the page's ink in both themes -- which
|
|
59
|
+
steps *lighter* than the surface on a dark page and *darker* on a light one.
|
|
60
|
+
Dark got the conventional reading, a floating thing lifted off its page;
|
|
61
|
+
light got a mid-grey slab two steps darker than every card on the screen,
|
|
62
|
+
carrying dark ink. Measured, the light bubble was neutral-400 at 2.01:1
|
|
63
|
+
against the page while the dark one was 2.27:1 the other way.
|
|
64
|
+
|
|
65
|
+
One derivation cannot serve both directions, so the pair is stated. Both
|
|
66
|
+
ends read as lifted: near-white over a light page, a mid grey over a dark
|
|
67
|
+
one. Near-white is only 1.04:1 against the page, so the bubble draws a
|
|
68
|
+
hairline as well -- see `components/tooltip.css`. */
|
|
69
|
+
--color-tooltip: light-dark(var(--color-white), var(--color-neutral-700));
|
|
70
|
+
--color-tooltip-contrast: light-dark(var(--color-neutral-900), var(--color-neutral-100));
|
|
71
|
+
|
|
72
|
+
/* Emerald-700 rather than 600. A filled button's label is normal-size text, so
|
|
73
|
+
WCAG 1.4.3 asks 4.5:1, and white on emerald-600 measures 3.50:1 -- it passed
|
|
74
|
+
only as large text, which `.btn` is not. One step darker carries the white
|
|
75
|
+
label and leaves the hue where it was. */
|
|
76
|
+
--color-success: var(--color-emerald-700);
|
|
77
|
+
--color-success-contrast: var(--color-neutral-50);
|
|
78
|
+
/* Baked rather than mixed live. `box-hover` nests this token two `color-mix()`
|
|
79
|
+
levels deeper to compose the fill and the edge, and three levels of
|
|
80
|
+
`color-mix()` over a `light-dark()` crashes the WebKit renderer outright on
|
|
81
|
+
hover. Confirmed on all four semantic intents, not only this one. Each
|
|
82
|
+
pinned pair below is
|
|
83
|
+
`color-mix(in srgb, <base> 82%, <strong>)`, resolved once per theme and
|
|
84
|
+
screenshotted back so the blend stays exact; re-bake it if the base or
|
|
85
|
+
strong tone it was mixed from changes. */
|
|
86
|
+
--color-success-hover: light-dark(#007552, #008e66);
|
|
87
|
+
/* The dark ground is `-900` to make room for the `-300` ink above it. `-subtle`
|
|
88
|
+
is what `kbd`, `code`, `pre` and the tooltip bubble rest on, and those are the
|
|
89
|
+
only places a partial fill prints text onto a tinted ground rather than onto
|
|
90
|
+
the page. At `-800` a destructive key cap measured 4.1:1 -- under the 4.5:1
|
|
91
|
+
WCAG 1.4.3 asks of normal text -- and the other three sat at 4.9 to 5.0 with
|
|
92
|
+
no headroom. One step darker puts all four past 9:1. */
|
|
93
|
+
--color-success-subtle: light-dark(var(--color-emerald-100), var(--color-emerald-900));
|
|
94
|
+
/* The dark tone is `-300`, not `-100`. `-strong` is what a partial fill prints
|
|
95
|
+
-- the label of a soft badge, the dot of a soft radio -- and on a dark page
|
|
96
|
+
the `-100` shade of every hue is a pale tint that reads white at small
|
|
97
|
+
sizes. The intent's colour survived only in the fill and the border, so a
|
|
98
|
+
soft success badge and a soft destructive one had the same white label and a
|
|
99
|
+
radio's dot stopped matching its own ring. `-300` keeps the hue and stays
|
|
100
|
+
far above the contrast a mark or a label needs. Light keeps `-800`, which
|
|
101
|
+
was never the problem: it is saturated and dark against a light page. */
|
|
102
|
+
--color-success-strong: light-dark(var(--color-emerald-800), var(--color-emerald-300));
|
|
103
|
+
/* Amber is the one family that cannot solve this by darkening. White on
|
|
104
|
+
amber-600 measures 3.07:1, and the amber dark enough to carry white has
|
|
105
|
+
stopped being amber -- it reads brown, which is not the colour a warning is
|
|
106
|
+
supposed to be. So the ink moves instead of the hue: dark ink on amber is
|
|
107
|
+
what a warning looks like nearly everywhere, and it is the only intent whose
|
|
108
|
+
contrast tone is not the page colour. */
|
|
109
|
+
--color-warning: var(--color-amber-600);
|
|
110
|
+
--color-warning-contrast: var(--color-neutral-950);
|
|
111
|
+
/* Same crash, same fix as `--color-success-hover` above. */
|
|
112
|
+
--color-warning-hover: light-dark(#d46700, #e78300);
|
|
113
|
+
--color-warning-subtle: light-dark(var(--color-amber-100), var(--color-amber-900));
|
|
114
|
+
--color-warning-strong: light-dark(var(--color-amber-800), var(--color-amber-300));
|
|
115
|
+
--color-destructive: var(--color-rose-700);
|
|
116
|
+
--color-destructive-contrast: var(--color-neutral-50);
|
|
117
|
+
/* Same crash, same fix as `--color-success-hover` above. */
|
|
118
|
+
--color-destructive-hover: light-dark(#c00036, #d1054b);
|
|
119
|
+
--color-destructive-subtle: light-dark(var(--color-rose-100), var(--color-rose-900));
|
|
120
|
+
--color-destructive-strong: light-dark(var(--color-rose-800), var(--color-rose-300));
|
|
121
|
+
--color-info: var(--color-indigo-600);
|
|
122
|
+
--color-info-contrast: var(--color-neutral-50);
|
|
123
|
+
/* Same crash, same fix as `--color-success-hover` above. */
|
|
124
|
+
--color-info-hover: light-dark(#4b36e9, #5e4ff9);
|
|
125
|
+
--color-info-subtle: light-dark(var(--color-indigo-100), var(--color-indigo-900));
|
|
126
|
+
--color-info-strong: light-dark(var(--color-indigo-800), var(--color-indigo-300));
|
|
127
|
+
|
|
128
|
+
--breakpoint-xs: 30rem;
|
|
129
|
+
--breakpoint-2xl: 90rem;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
:root {
|
|
133
|
+
/* Both keywords: the system preference decides, and the explicit theme
|
|
134
|
+
selectors below override it at any depth because `color-scheme` inherits. */
|
|
135
|
+
color-scheme: light dark;
|
|
136
|
+
|
|
137
|
+
--container-narrow: 48rem;
|
|
138
|
+
--container-max: 80rem;
|
|
139
|
+
--container-wide: 90rem;
|
|
140
|
+
--layout-gutter: clamp(1rem, 4vw, 2rem);
|
|
141
|
+
--layout-section-block: clamp(4rem, 9vw, 7rem);
|
|
142
|
+
--layout-gap: 1rem;
|
|
143
|
+
--layout-grid-min: 16rem;
|
|
144
|
+
|
|
145
|
+
--radius-small: 0.25rem;
|
|
146
|
+
--radius-control: 0.5rem;
|
|
147
|
+
--radius-surface: 0.875rem;
|
|
148
|
+
--control-height: 2.5rem;
|
|
149
|
+
--border-width: 1px;
|
|
150
|
+
|
|
151
|
+
/* `light-dark()` takes colors only, so the shadow color is the token and the
|
|
152
|
+
three elevations compose from it. Dark themes need a much heavier color to
|
|
153
|
+
read at all against a dark background. */
|
|
154
|
+
--elevation-color: light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.55));
|
|
155
|
+
/* Four rungs: no shadow, then a Tailwind-style sm/md/lg progression, so a
|
|
156
|
+
consumer unsure which step to reach for has a named "no depth" value
|
|
157
|
+
instead of inferring it by omitting a shadow declaration. The elevation
|
|
158
|
+
modifier classes (`.elevation-none`/`.elevation-sm`/`.elevation-md` in
|
|
159
|
+
elevation.css) share this naming on purpose -- same weight, same word --
|
|
160
|
+
though they multiply an aesthetic's shadow parts rather than declaring a
|
|
161
|
+
fixed value, so the two are not read from the same place. */
|
|
162
|
+
--elevation-none: none;
|
|
163
|
+
--elevation-sm: 0 1px 2px var(--elevation-color), 0 1px 3px var(--elevation-color);
|
|
164
|
+
--elevation-md: 0 4px 6px -1px var(--elevation-color), 0 2px 4px -2px var(--elevation-color);
|
|
165
|
+
--elevation-lg: 0 20px 25px -5px var(--elevation-color), 0 8px 10px -6px var(--elevation-color);
|
|
166
|
+
|
|
167
|
+
--focus-ring: color-mix(in srgb, var(--color-primary) 64%, transparent);
|
|
168
|
+
/* Negative, so the ring is drawn over the edge rather than beside it: at a
|
|
169
|
+
positive offset the outline floats a gap away and reads as a second, detached
|
|
170
|
+
frame around the control. Pulled in by exactly the material line width, the
|
|
171
|
+
ring lands on top of the line and a focused control reads as its own edge
|
|
172
|
+
thickening -- which is what a focus indicator is meant to look like.
|
|
173
|
+
|
|
174
|
+
A component drawing no line at rest loses nothing: the outline moves one
|
|
175
|
+
pixel inward over its own fill, which at a hairline is not a visible change. */
|
|
176
|
+
--focus-ring-offset: calc(-1 * var(--border-width));
|
|
177
|
+
--focus-ring-width: 3px;
|
|
178
|
+
|
|
179
|
+
--motion-duration-fast: 120ms;
|
|
180
|
+
--motion-duration-normal: 200ms;
|
|
181
|
+
--motion-duration-slow: 400ms;
|
|
182
|
+
--motion-ease: cubic-bezier(0.2, 0, 0, 1);
|
|
183
|
+
|
|
184
|
+
--z-popover: 40;
|
|
185
|
+
|
|
186
|
+
/* The one number the derived-hover formula spends: how much fill a hover adds
|
|
187
|
+
on top of whatever the element rests at. */
|
|
188
|
+
--hover-step: 14%;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* The base look answers a press with a small scale-in. `box-active` applies it on
|
|
192
|
+
`.btn` and `.card.interactive`; every aesthetic sets its own
|
|
193
|
+
`--ui-active-transform` on its class, loaded after this file, so those win.
|
|
194
|
+
Reduced motion opts out. Kept outside `@theme` because a media query cannot go
|
|
195
|
+
inside it. */
|
|
196
|
+
:root {
|
|
197
|
+
--ui-active-transform: scale(0.97);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@media (prefers-reduced-motion: reduce) {
|
|
201
|
+
:root {
|
|
202
|
+
--ui-active-transform: none;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.light,
|
|
207
|
+
.theme-light,
|
|
208
|
+
[data-theme="light"] {
|
|
209
|
+
color-scheme: light;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.dark,
|
|
213
|
+
.theme-dark,
|
|
214
|
+
[data-theme="dark"] {
|
|
215
|
+
color-scheme: dark;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@custom-variant dark (&:where(.dark, .dark *, [data-theme="dark"], [data-theme="dark"] *));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@reference "tailwindcss";
|
|
2
|
+
@import "./theme.css";
|
|
3
|
+
|
|
4
|
+
/* `text-6xl` and up bundle `line-height: 1`, which sets a heading's line box to
|
|
5
|
+
the font size exactly: descenders reach the box edge and a wrapped heading's
|
|
6
|
+
lines collide. The two largest steps set an explicit leading that still
|
|
7
|
+
tightens as the type grows -- 1.1 under 1.15 under `text-3xl`'s 1.2. */
|
|
8
|
+
@utility text-display {
|
|
9
|
+
@apply text-6xl leading-[1.1] font-bold tracking-tight md:text-8xl;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@utility text-title-lg {
|
|
13
|
+
@apply text-5xl leading-[1.15] font-bold tracking-tight md:text-6xl;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@utility text-title {
|
|
17
|
+
@apply text-3xl font-bold tracking-tight;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@utility text-title-sm {
|
|
21
|
+
@apply text-xl font-semibold tracking-tight;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@utility text-label-lg {
|
|
25
|
+
@apply text-lg font-medium tracking-wide;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@utility text-label {
|
|
29
|
+
@apply text-base font-medium tracking-wide;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@utility text-body {
|
|
33
|
+
@apply text-sm font-normal tracking-wider;
|
|
34
|
+
}
|