cssints 0.2.0 → 0.3.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/README.md +88 -44
- package/dist/check.mjs +2 -2
- package/dist/{contrast-DhK69Xak.mjs → contrast-B49XCnVX.mjs} +2 -2
- package/dist/devtools-link-D2YxsJKZ.mjs +94 -0
- package/dist/devtools-link.d.mts +55 -0
- package/dist/devtools-link.mjs +2 -0
- package/dist/groups/baseline-2015/with-downstream.d.mts +81 -0
- package/dist/groups/baseline-2015.d.mts +291 -0
- package/dist/groups/baseline-2016/with-downstream.d.mts +86 -0
- package/dist/groups/baseline-2016.d.mts +310 -0
- package/dist/groups/baseline-2017/with-downstream.d.mts +94 -0
- package/dist/groups/baseline-2017.d.mts +365 -0
- package/dist/groups/baseline-2018/with-downstream.d.mts +94 -0
- package/dist/groups/baseline-2018.d.mts +368 -0
- package/dist/groups/baseline-2019/with-downstream.d.mts +97 -0
- package/dist/groups/baseline-2019.d.mts +375 -0
- package/dist/groups/baseline-2020/with-downstream.d.mts +112 -0
- package/dist/groups/baseline-2020.d.mts +480 -0
- package/dist/groups/baseline-2021/with-downstream.d.mts +125 -0
- package/dist/groups/baseline-2021.d.mts +536 -0
- package/dist/groups/baseline-2022/with-downstream.d.mts +126 -0
- package/dist/groups/baseline-2022.d.mts +562 -0
- package/dist/groups/baseline-2023/with-downstream.d.mts +120 -0
- package/dist/groups/baseline-2023.d.mts +606 -0
- package/dist/groups/baseline-2024/with-downstream.d.mts +627 -0
- package/dist/groups/baseline-2024.d.mts +632 -0
- package/dist/groups/baseline-2025/with-downstream.d.mts +628 -0
- package/dist/groups/baseline-2025.d.mts +640 -0
- package/dist/groups/baseline-2026/with-downstream.d.mts +3 -0
- package/dist/groups/baseline-2026.d.mts +3 -0
- package/dist/groups/newly-available/with-downstream.d.mts +632 -0
- package/dist/groups/widely-available/with-downstream.d.mts +121 -0
- package/dist/index.mjs +22 -7
- package/dist/instrument-BqIZpbIU.mjs +3956 -0
- package/dist/{lowered-D2KusPLp.mjs → lowered-aJVapQT6.mjs} +1 -1
- package/dist/plugin-types.d.mts +7 -0
- package/dist/plugin.d.mts +2 -2
- package/dist/plugin.mjs +8 -1
- package/dist/{registry-CTumHB8p.mjs → registry-BUSEO32-.mjs} +7 -1
- package/dist/{scope-ZVhUxQH1.d.mts → scope-INuJD4RT.d.mts} +8 -1
- package/dist/{sheet-BKsMTa0c.mjs → sheet-BeHdq4xt.mjs} +1 -1
- package/dist/surface.d.mts +1 -1
- package/dist/{theme-BsP2E4LB.mjs → theme-SlQ73rFL.mjs} +3 -3
- package/dist/tui.mjs +4 -4
- package/dist/vite.d.mts +3 -1
- package/dist/vite.mjs +92 -39
- package/docs/API.md +458 -231
- package/package.json +6 -2
- package/skills/cssints-plugin/SKILL.md +2 -0
- package/dist/instrument-gCazujmb.mjs +0 -1624
package/docs/API.md
CHANGED
|
@@ -34,11 +34,22 @@ import { alignItems, cn, justifyContent, p } from "cssints" with { type: "cssint
|
|
|
34
34
|
import * as css from "cssints" with { type: "cssints" };
|
|
35
35
|
|
|
36
36
|
export const A = () => (
|
|
37
|
-
|
|
37
|
+
<div
|
|
38
|
+
className={cn(
|
|
39
|
+
css.display("flex"),
|
|
40
|
+
alignItems("center"),
|
|
41
|
+
justifyContent("center"),
|
|
42
|
+
prop && p(4),
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
Hello, world!
|
|
46
|
+
</div>
|
|
38
47
|
);
|
|
39
48
|
// or with a layout context
|
|
40
49
|
export const B = () => (
|
|
41
|
-
|
|
50
|
+
<div className={cn(css.flex().alignItems("center").justifyContent("center"), prop && css.p(4))}>
|
|
51
|
+
Hello, world!
|
|
52
|
+
</div>
|
|
42
53
|
);
|
|
43
54
|
```
|
|
44
55
|
|
|
@@ -47,7 +58,9 @@ Output for `A`:
|
|
|
47
58
|
```tsx skip
|
|
48
59
|
import { _cx as __cx } from "cssints";
|
|
49
60
|
|
|
50
|
-
export const A = () =>
|
|
61
|
+
export const A = () => (
|
|
62
|
+
<div className={__cx([], "_1s47490 _1ob5z5m _cn8dee", prop && "_lwugp7")}>Hello, world!</div>
|
|
63
|
+
);
|
|
51
64
|
```
|
|
52
65
|
|
|
53
66
|
The static arguments of `cn` are one class string; `prop && p(4)` depends on a runtime value, so it stays an argument of the runtime call, and the `p(4)` in it is already its class.
|
|
@@ -55,10 +68,18 @@ The static arguments of `cn` are one class string; `prop && p(4)` depends on a r
|
|
|
55
68
|
```css
|
|
56
69
|
@layer _.k, _.a, _.b, _.c, _.d, _.e;
|
|
57
70
|
@layer _.a {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
._lwugp7 {
|
|
72
|
+
padding: 1rem;
|
|
73
|
+
}
|
|
74
|
+
._1ob5z5m {
|
|
75
|
+
align-items: center;
|
|
76
|
+
}
|
|
77
|
+
._1s47490 {
|
|
78
|
+
display: flex;
|
|
79
|
+
}
|
|
80
|
+
._cn8dee {
|
|
81
|
+
justify-content: center;
|
|
82
|
+
}
|
|
62
83
|
}
|
|
63
84
|
```
|
|
64
85
|
|
|
@@ -73,7 +94,11 @@ The static arguments of `cn` are one class string; `prop && p(4)` depends on a r
|
|
|
73
94
|
```tsx
|
|
74
95
|
import * as css from "cssints" with { type: "cssints" };
|
|
75
96
|
|
|
76
|
-
export const C = () =>
|
|
97
|
+
export const C = () => (
|
|
98
|
+
<div className={css.cn(css.flex().alignItems("center").justifyContent("center"), css.p(4))}>
|
|
99
|
+
Hello, world!
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
77
102
|
```
|
|
78
103
|
|
|
79
104
|
Output:
|
|
@@ -99,13 +124,20 @@ const narrow = css.container("sidebar (width < 30rem)");
|
|
|
99
124
|
const grid = css.supports("(display: grid)");
|
|
100
125
|
|
|
101
126
|
export const styles = css.cn(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
css.p(2),
|
|
128
|
+
md(css.pl("8ch"), css.pt(4)),
|
|
129
|
+
css.hover(css.bg("#eee")),
|
|
130
|
+
md(css.hover(css.bg("#ddd"))),
|
|
131
|
+
prop && lg(css.p(8)),
|
|
132
|
+
);
|
|
133
|
+
export const forms = css.cn(
|
|
134
|
+
dark(css.bg("#111")),
|
|
135
|
+
narrow(css.m(0)),
|
|
136
|
+
grid(css.display("grid")),
|
|
137
|
+
tablet(css.m(1)),
|
|
138
|
+
wide(css.m(2)),
|
|
139
|
+
sm(css.m(3)),
|
|
107
140
|
);
|
|
108
|
-
export const forms = css.cn(dark(css.bg("#111")), narrow(css.m(0)), grid(css.display("grid")), tablet(css.m(1)), wide(css.m(2)), sm(css.m(3)));
|
|
109
141
|
```
|
|
110
142
|
|
|
111
143
|
- **Pseudo-classes** are ready-made wrappers named after the selector: `hover`, `focus`, `focusVisible`, `active`, `disabled`, and so on: 49 of them, from mdn-data `selectors.json` (standard pseudo-classes without arguments). Functional ones (`:nth-child()`) are not wrappers.
|
|
@@ -121,22 +153,34 @@ Output of `styles`; each form in `forms` gives one more rule in `_.b` (`dark`, `
|
|
|
121
153
|
|
|
122
154
|
```css
|
|
123
155
|
@layer _.a {
|
|
124
|
-
|
|
156
|
+
._1t8gqy5 {
|
|
157
|
+
padding: 0.5rem;
|
|
158
|
+
}
|
|
125
159
|
}
|
|
126
160
|
@layer _.b {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
161
|
+
._16f8jrr:hover {
|
|
162
|
+
background-color: #eee;
|
|
163
|
+
}
|
|
164
|
+
@media (width >= 40rem) {
|
|
165
|
+
._1q848uz {
|
|
166
|
+
padding-top: 1rem;
|
|
167
|
+
}
|
|
168
|
+
._t70zhd {
|
|
169
|
+
padding-left: 8ch;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
@media (width >= 60rem) {
|
|
173
|
+
._1khcbr2 {
|
|
174
|
+
padding: 2rem;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
135
177
|
}
|
|
136
178
|
@layer _.c {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
179
|
+
@media (width >= 40rem) {
|
|
180
|
+
._ronnd2:hover {
|
|
181
|
+
background-color: #ddd;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
140
184
|
}
|
|
141
185
|
```
|
|
142
186
|
|
|
@@ -159,12 +203,15 @@ css.global("a", css.hover(css.color("red"))); // global() takes media(), contain
|
|
|
159
203
|
// an attribute state, and the state of a marked ancestor
|
|
160
204
|
export const row = css.cn(css.group(), css.p(2));
|
|
161
205
|
export const link = css.cn(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
206
|
+
css.color("gray"),
|
|
207
|
+
css.attr("aria-current", "page")(css.color("blue")),
|
|
208
|
+
css.within(css.hover)(css.textDecoration("underline")),
|
|
165
209
|
);
|
|
166
210
|
export const icon = css.attr("href", ".pdf", "$=")(css.opacity(0.8));
|
|
167
|
-
export const nested = css.cn(
|
|
211
|
+
export const nested = css.cn(
|
|
212
|
+
css.group("card"),
|
|
213
|
+
css.within(css.focusVisible, "card")(css.outline("2px solid")),
|
|
214
|
+
);
|
|
168
215
|
css.attr("a b"); // error: a name has no space
|
|
169
216
|
css.attr("a", "x", "!="); // error: not an operator
|
|
170
217
|
css.within(css.media("(width >= 40rem)")); // error: a media query is not a state
|
|
@@ -181,15 +228,36 @@ css.within(css.media("(width >= 40rem)")); // error: a media query is not a stat
|
|
|
181
228
|
|
|
182
229
|
```css
|
|
183
230
|
@layer _.k {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
231
|
+
*,
|
|
232
|
+
*::before,
|
|
233
|
+
*::after {
|
|
234
|
+
box-sizing: border-box;
|
|
235
|
+
}
|
|
236
|
+
body {
|
|
237
|
+
margin: 8px;
|
|
238
|
+
}
|
|
239
|
+
:root {
|
|
240
|
+
color-scheme: light;
|
|
241
|
+
}
|
|
242
|
+
@media (prefers-color-scheme: dark) {
|
|
243
|
+
:root {
|
|
244
|
+
color-scheme: dark;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
a[href^="http"] > b {
|
|
248
|
+
color: red;
|
|
249
|
+
}
|
|
188
250
|
}
|
|
189
251
|
@layer _.b {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
252
|
+
:where(._g:hover) ._72037 {
|
|
253
|
+
text-decoration: underline;
|
|
254
|
+
}
|
|
255
|
+
._fc1pxh[aria-current="page"] {
|
|
256
|
+
color: blue;
|
|
257
|
+
}
|
|
258
|
+
._pl3vkk[href$=".pdf"] {
|
|
259
|
+
opacity: 0.8;
|
|
260
|
+
}
|
|
193
261
|
}
|
|
194
262
|
```
|
|
195
263
|
|
|
@@ -215,9 +283,9 @@ css.token.set(muted, "red"); // error: private tokens cannot be set
|
|
|
215
283
|
|
|
216
284
|
```css
|
|
217
285
|
@property --color-primary {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
286
|
+
syntax: "<color>";
|
|
287
|
+
inherits: true;
|
|
288
|
+
initial-value: blue;
|
|
221
289
|
}
|
|
222
290
|
```
|
|
223
291
|
|
|
@@ -234,14 +302,14 @@ Tokens come from an inline DTCG-shaped `const` object. Every leaf is a token (se
|
|
|
234
302
|
import * as css from "cssints" with { type: "cssints" };
|
|
235
303
|
|
|
236
304
|
const t = css.createTokens({
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
305
|
+
color: {
|
|
306
|
+
$type: "color",
|
|
307
|
+
bg: { $value: "#ffffff" },
|
|
308
|
+
fg: { $value: "#111827" },
|
|
309
|
+
accent: { $value: "oklch(62% 0.19 259)" },
|
|
310
|
+
link: { $value: "{color.accent}" },
|
|
311
|
+
},
|
|
312
|
+
space: { $type: "dimension", md: { $value: "16px" } },
|
|
245
313
|
});
|
|
246
314
|
|
|
247
315
|
const dark = css.media("(prefers-color-scheme: dark)");
|
|
@@ -251,7 +319,9 @@ css.createGlobalTheme(t.color, css.container("(width < 30rem)"), { bg: "#eee" })
|
|
|
251
319
|
|
|
252
320
|
css.check.contrast(t.color.fg, t.color.bg, { min: 4.5 });
|
|
253
321
|
|
|
254
|
-
export const A = () =>
|
|
322
|
+
export const A = () => (
|
|
323
|
+
<div className={css.cn(css.bg(t.color.bg), css.color(t.color.fg), css.p(t.space.md))} />
|
|
324
|
+
);
|
|
255
325
|
css.createGlobalTheme(t, dark, { color: { nope: "red" } }); // error: no token at this path
|
|
256
326
|
css.createGlobalTheme(t, css.hover, { color: { bg: "red" } }); // error: a pseudo-class is not a theme condition
|
|
257
327
|
```
|
|
@@ -263,12 +333,12 @@ css.createGlobalTheme(t, css.hover, { color: { bg: "red" } }); // error: a pseud
|
|
|
263
333
|
- **A plain token** (`token.color("oklch(0.637 0.203 345)", "accent")`, a `createTokens()` leaf): `@property --accent{…initial-value:#db4aa7}` and `:root{--accent:oklch(0.637 0.203 345)}`, which LightningCSS lowers like any custom property (`--accent:#db4aa7`, then the wide-gamut `lab()` under `@supports (color: lab(0% 0 0))`). A browser that parses the value gets the wide one, so a modern browser reads the same colour as before, written as `lab()` (`:where(*)` stands for `:root` when the token does not inherit).
|
|
264
334
|
- **A derived token or a theme value that holds `var()`** (`oklch(0.637 0.203 {hue.accent})`, `color-mix()` over a token, `{path}` references): LightningCSS cannot lower a value with `var()`. Its default is the lowered literal (the references replaced by the values the tokens start with) and the live value comes after it under `@supports` of the literal itself, the feature the value needs and no table of features: `:root{--accent:#db4aa7}` then `@supports (color:oklch(0.637 0.203 345)){:root{--accent:oklch(0.637 0.203 var(--hue-accent))}}`. A browser that parses the literal reads the live value, so the hue slider works; one that does not keeps the default, unless the runtime of the next bullet computes the value. A theme does the same inside its own rule, `@supports (…){@media (…){:root{…}}}` or `@supports (…){.dark{…}}`, after the default of that theme. An alias (`{color.accent}` as the whole value) stays live, since it needs no feature of its own, and reads the default of its target. A theme that sets a token a derived token depends on declares that derived token again (live, as above), and an older browser keeps the default of the derived token there.
|
|
265
335
|
- The support warning for those values is gone: the fallback handles it. A token value that LightningCSS leaves alone keeps its warning and its single declaration, as today.
|
|
266
|
-
- Checked in Chromium 109 (`apps/
|
|
267
|
-
- **Derived tokens in older browsers: a runtime** (cssints-i9xo). Where a target cannot parse a derived colour (`oklch(0.637 0.203 var(--hue-accent))`, `lab(from color(from var(--color-accent) srgb-linear …) …)`), the slider would do nothing. The engine then ships a small browser module, `cssints/derived`, that computes the live values in JavaScript. Tokens are not members, so this is not a plugin you import: the engine registers the runtime itself when a derived colour needs it, and delivers it the way it delivers a plugin's runtime (`virtual:cssints/runtime/derived`, empty, `export {};`, unless a browserslist target lacks a feature the literals need and has `@property`; the same `needed()` decision). The client modules that use such a token import it. With targets that parse every value nothing is written or shipped:
|
|
336
|
+
- Checked in Chromium 109 (`apps/csssky/test/old-browser.mjs`: Mu pink in real colours, light and dark, before the runtime of the next bullet moves anything) and Chrome 154 (the colours stay `oklch()` and follow the slider). The cost on csssky is one more `:root` declaration per token and one `@supports` rule per derived value: 27.97 kB to 29.58 kB of CSS with the default targets.
|
|
337
|
+
- **Derived tokens in older browsers: a runtime** (cssints-i9xo). Where a target cannot parse a derived colour (`oklch(0.637 0.203 var(--hue-accent))`, `lab(from color(from var(--color-accent) srgb-linear …) …)`), the slider would do nothing. The engine then ships a small browser module, `cssints/derived`, that computes the live values in JavaScript. Tokens are not members, so this is not a plugin you import: the engine registers the runtime itself when a derived colour needs it, and delivers it the way it delivers a plugin's runtime (`virtual:cssints/runtime/derived`, empty, `export {};`, unless a browserslist target lacks a feature the literals need and has `@property`; the same `needed()` decision). The client modules that use such a token import it. With targets that parse every value nothing is written or shipped: csssky built for `chrome 154` is byte-identical to a build without the runtime.
|
|
268
338
|
- **The data** is one custom property on `:root` in layer `_.k` (`--_derived`, JSON in a string) with only what the sheet uses: `p`, the literals a browser must parse for the runtime to stay idle (the same literals as the `@supports` tests); `b`, the template of each derived colour of the default theme (`oklch(0.637 0.203 var(--hue-accent))`); `t`, per theme in sheet order, its condition (a selector, or `@media (…)`) and the templates it sets, `0` for a token it sets to a literal. A token that a theme overrides with a plain `{path}` alias stays live and is released by the runtime.
|
|
269
339
|
- **The runtime** does nothing without a `document`, and nothing when the browser parses every `p` (Chrome 154 gets the same CSS as an old one and writes nothing on `<html>`). Otherwise it picks the themes that apply to `<html>` (`matches()` of the selector, `matchMedia()` of the condition), evaluates the templates over the computed values of the inputs (`--hue-accent` and any other token a template reads; the runtime's own results are used as they are, not read back), and writes the results as inline custom properties on `<html>`, as `rgb()`. It runs again on any attribute change of `<html>` (class, style: the slider, the theme classes) and when a theme's media query changes (`prefers-color-scheme`), and once the stylesheet has loaded if it was not there at the start. It reads every input before the first write, so one input costs one style recalculation. A token it cannot compute at the moment (an input is missing) keeps its static default.
|
|
270
340
|
- **The subset** is hex, `rgb()`/`rgba()`, `oklch()`, `oklab()`, `lab()`, `color(srgb | srgb-linear …)`, `color-mix()` in `srgb`, `srgb-linear`, `oklab` or `lab` (shares that add up to 100%), the names `black`, `white` and `transparent`; relative colour (`from`) with the channel keywords, numbers, percentages, `deg`, `+ - * /`, parentheses, `calc()`, `clamp()`, `min()`, `max()` and `var()`. A colour outside sRGB is clipped to it (an old browser has no wider gamut). Anything else (`light-dark()`, `hsl()`, a polar `color-mix()`, a wide-gamut `color()`, another unit), a derived token that is not a colour, one that does not inherit, and a value in a `container()` theme stays at its static default, and the build warns, naming the token and the target: `tf.color.mixed: "color-mix(in oklch, …)" stays at its static default in chrome 109: the runtime for derived tokens does not evaluate "oklch"`. The themes it reads are those of `<html>` (`:root`, a selector that matches it, a media query), not of an element inside the page. A template should read plain tokens and derived tokens, not an alias of a derived token (it would be read one update late).
|
|
271
|
-
- **Cost.** `cssints/derived` is 7.2 kB minified, 3.1 kB gzipped;
|
|
341
|
+
- **Cost.** `cssints/derived` is 7.2 kB minified, 3.1 kB gzipped; csssky's CSS grows by the data (29.6 kB to 31.4 kB with the default targets, about 0.6 kB gzipped). The colours of the default theme take 10 µs of JavaScript per input; in Chromium 109 a slider input costs 1.2 ms against 0.6 ms for the style recalculation alone, which the browser pays on any change of the hue. Checked in `apps/csssky/test/old-browser.mjs`: in Chromium 109 the four accent steps, the label's onAccent and the page background at hues 0, ±90 and ±180 are within 1/255 of the pixels Chrome 154 paints, in light, dim, dark and system under a light and a dark scheme, and the system scheme changing recomputes them; `packages/cssints/test/derived-runtime.mjs` holds the evaluator to culori (the maths of `check.contrast`) on the csssky templates and random inputs.
|
|
272
342
|
- **Themes:** `createGlobalTheme(tokens, condition | selector, overrides, { layer?, scope? }?)` re-declares the overridden tokens under a `media()` condition on `:root`, or under a selector. Under `container()` it declares them on `:where(*)`, since `:root` has no container above it and would never match. `layer` names a top-level layer for the rule (`{ layer: "app" }` puts an app look in `@layer app`, above the engine's `:root` fallbacks) that `layers()` has declared, in this module or one it imports: an undeclared layer is a build error at the call. Without `layer` the rule is in `_.k`. `scope` (with a selector) confines the rule to a subtree, `@scope (article.post) { [data-mode="hero"] { ... } }`, so a foreign look cannot repaint the rest of the page; it is a selector, checked by the build like the one of `global()` and balanced in the types, and it is an error with a condition (`:root` is not inside a scope). Overrides are checked against the token types, and an unknown path is a type error; they take the same values as tokens, and `tokens` may be a group (`t.color`). `supports()` and pseudo-classes are not themes: `createGlobalTheme(t, hover, …)` is a type error, and a build error when the types are bypassed. A module that uses a token gets the theme rules that set it, with the `@property` rules of the tokens it uses and of those their values refer to, not of every token the theme sets.
|
|
273
343
|
- **`layers(...names)`** declares top-level layers in order. The sheet prints `@layer app, author, viewer;` once, right after the engine's `@layer _.k, _.a, …;`, however many modules declare it (a module's sheet has the statements of its own `layers()` calls and of the layers it uses). The same names again are fine; names in another order (`layers("viewer", "app")` after the one above), or sharing layers without one statement holding the other (`layers("viewer", "base")`), are a build error at the second call, since the sheet could not say which comes first. A statement that holds another is printed first; statements that share nothing are printed longest first. A name is letters, digits, `_` and `-`.
|
|
274
344
|
- **`layers(...names, { before: true })`** declares them before the engine's: the sheet prints `@layer reset;` above `@layer _.k, _.a, …;`, so `_.k`, the classes and every layer declared after rank above what is in them (a reset that any style beats). The rules above hold among the statements before, in the same order; a layer declared both before and after (`layers("reset")` once `layers("reset", { before: true })` has run, or the other way) is a build error at the second call. A stylesheet's own layer goes between a reset and cssints with `layers("reset", "base", { before: true })`.
|
|
@@ -279,7 +349,12 @@ import * as css from "cssints" with { type: "cssints" };
|
|
|
279
349
|
css.layers("app", "author", "viewer");
|
|
280
350
|
const t = css.createTokens({ color: { $type: "color", bg: { $value: "#fff" } } });
|
|
281
351
|
css.createGlobalTheme(t, '[data-mode="dark"]', { color: { bg: "#000" } }, { layer: "viewer" });
|
|
282
|
-
css.createGlobalTheme(
|
|
352
|
+
css.createGlobalTheme(
|
|
353
|
+
t,
|
|
354
|
+
'[data-mode="hero"]',
|
|
355
|
+
{ color: { bg: "#123" } },
|
|
356
|
+
{ layer: "author", scope: "article.post" },
|
|
357
|
+
);
|
|
283
358
|
css.createGlobalTheme(t, ".x", { color: { bg: "#000" } }, { scope: "a[href" }); // error: unbalanced brackets in the scope
|
|
284
359
|
css.layers(); // error: a declaration has a name
|
|
285
360
|
css.layers("reset", { before: true }); // @layer reset; before @layer _.k, _.a, …;
|
|
@@ -294,11 +369,11 @@ css.layers("reset", { before: true }); // @layer reset; before @layer _.k, _.a,
|
|
|
294
369
|
import * as css from "cssints" with { type: "cssints" };
|
|
295
370
|
|
|
296
371
|
export const t = css.createTokens({
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
372
|
+
color: {
|
|
373
|
+
$type: "color",
|
|
374
|
+
accent: { $value: "#2563eb" },
|
|
375
|
+
hover: { $value: "color-mix(in oklab, {color.accent}, black 15%)" },
|
|
376
|
+
},
|
|
302
377
|
});
|
|
303
378
|
css.createGlobalTheme(t, ".brand", { color: { accent: "#db2777" } }); // hover follows
|
|
304
379
|
```
|
|
@@ -309,12 +384,15 @@ css.createGlobalTheme(t, ".brand", { color: { accent: "#db2777" } }); // hover f
|
|
|
309
384
|
import * as css from "cssints" with { type: "cssints" };
|
|
310
385
|
|
|
311
386
|
export const t = css.createTokens({
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
387
|
+
hue: { $type: "number", accent: { $value: 345 } },
|
|
388
|
+
radius: { $type: "dimension", sm: { $value: "4px" }, md: { $value: "8px" } },
|
|
389
|
+
color: { $type: "color", accent: { $value: "oklch(0.6 0.2 {hue.accent})" } },
|
|
315
390
|
});
|
|
316
391
|
css.createGlobalTheme(t, ".square", { radius: { md: "{radius.sm}" } });
|
|
317
|
-
css.createGlobalTheme(t, ".dim", {
|
|
392
|
+
css.createGlobalTheme(t, ".dim", {
|
|
393
|
+
hue: { accent: 12 },
|
|
394
|
+
color: { accent: "oklch(0.7 0.1 {hue.accent})" },
|
|
395
|
+
});
|
|
318
396
|
css.createGlobalTheme(t, ".x", { radius: { md: "{hue.accent}" } }); // error: a number, not a length
|
|
319
397
|
```
|
|
320
398
|
|
|
@@ -331,45 +409,71 @@ import { register } from "@tokens-studio/sd-transforms";
|
|
|
331
409
|
register(StyleDictionary);
|
|
332
410
|
const DTCG = { fontSize: "dimension", lineHeight: "number", opacity: "number" }; // the types sd-transforms leaves
|
|
333
411
|
StyleDictionary.registerFormat({
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
412
|
+
name: "cssints/dtcg",
|
|
413
|
+
format: ({ dictionary }) => {
|
|
414
|
+
const out = {};
|
|
415
|
+
for (const t of dictionary.allTokens) {
|
|
416
|
+
let group = out;
|
|
417
|
+
for (const key of t.path.slice(0, -1)) group = group[key] ??= {};
|
|
418
|
+
const type = t.$type ?? t.type;
|
|
419
|
+
group[t.path.at(-1)] = { $type: DTCG[type] ?? type, $value: t.$value ?? t.value };
|
|
420
|
+
}
|
|
421
|
+
return JSON.stringify(out, null, 2);
|
|
422
|
+
},
|
|
345
423
|
});
|
|
346
424
|
await new StyleDictionary({
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
425
|
+
source: ["tokens/**/*.json"],
|
|
426
|
+
preprocessors: ["tokens-studio"],
|
|
427
|
+
platforms: {
|
|
428
|
+
cssints: {
|
|
429
|
+
transformGroup: "tokens-studio",
|
|
430
|
+
files: [{ destination: "tokens.json", format: "cssints/dtcg" }],
|
|
431
|
+
},
|
|
432
|
+
},
|
|
350
433
|
}).buildAllPlatforms();
|
|
351
434
|
```
|
|
352
435
|
|
|
353
|
-
|
|
436
|
+
- **Build errors** for what would be misread: `value`/`type` without `$` (`global.Main.0 has value and type without $, the legacy format of Tokens Studio …`), `$themes` or `$metadata` in the tree, a Tokens Studio type (the error names the DTCG type: `"spacing" is a Tokens Studio type: its DTCG type is "dimension"`), math outside `calc()` (`write "calc({spacing.base} * 2)"`), a Tokens Studio colour modifier (`$extensions["studio.tokens"].modify`), a Figma boolean (`com.figma.type: "boolean"` on a number), and more than one mode in `$extensions` (`mode`, or `modes` of `com.figma`). `string`, `boolean`, `fontFamily` and the composite types are errors as before (no token type), so leave them out of the file (Figma: a collection without them; TokensBrücke: no styles). Samples of each tool are in `packages/cssints/test/fixtures/importers`.
|
|
354
437
|
- **The sheet:** token sets and themes are rules of layer `_.k`, after the global rules of `css.global()` and before the class layers. They set custom properties only, and no class does, so the layer order never decides between them; `_.k` is the lowest layer, so your own unlayered `--color-bg` rule wins over a theme. The `:root` rules of aliases come first, then themes under a condition, then themes under a selector, each in source order, so a selector theme beats a condition theme on the same element. A module has the rules of the tokens it uses, from wherever they were made, and of the tokens those are made of (the references of a derived token or of a theme value, through chains: a `hue.accent` that no class uses still gets its `@property` rule), and its own; import a theme module (`import "./theme.ts"`) to have all of it.
|
|
355
|
-
- **`css.check.contrast(fg, bg, { min })`** computes the WCAG 2.x contrast ratio (`min` from 1 to 21) for the default values and for every theme that sets either token or one they are aliases of. It reads themes one at a time, except those of one selector: themes with the same selector (and scope) meet on one element, so they are read together, laid over each other in the order of the cascade (no layer first, then the declared layers in order, then source order). A viewer look that sets only the text colour is checked on the background of the app look under the same selector. Themes of different selectors, or a selector and a condition, are still read one at a time, not in their combinations. A value that is set at run time (a slider that sets `--hue-accent` on `<html>`) belongs to no theme and is checked at its default only; a theme per value would put each rule in the sheet and hold it to the thresholds of the other themes, since a check applies to every theme that touches its tokens, so measure a range outside the build (apps/
|
|
438
|
+
- **`css.check.contrast(fg, bg, { min })`** computes the WCAG 2.x contrast ratio (`min` from 1 to 21) for the default values and for every theme that sets either token or one they are aliases of. It reads themes one at a time, except those of one selector: themes with the same selector (and scope) meet on one element, so they are read together, laid over each other in the order of the cascade (no layer first, then the declared layers in order, then source order). A viewer look that sets only the text colour is checked on the background of the app look under the same selector. Themes of different selectors, or a selector and a condition, are still read one at a time, not in their combinations. A value that is set at run time (a slider that sets `--hue-accent` on `<html>`) belongs to no theme and is checked at its default only; a theme per value would put each rule in the sheet and hold it to the thresholds of the other themes, since a check applies to every theme that touches its tokens, so measure a range outside the build (apps/csssky `test/contrast.mjs`). A ratio below `min` is a build error that names the theme and both resolved values (the ratio is cut, not rounded: 4.499 is 4.49). A value the check cannot evaluate (`color-mix()`, a relative colour, `light-dark()`, `currentColor`, a system colour) or a translucent one is a warning that names the token; a build says it once, and a dev server says it again after each edit. A derived token (or a theme value with `{path}` references) is read in each theme by putting that theme's values, or the defaults, in place of its references, through a chain of derived tokens: a ratio when the result is a colour (`oklch(0.6 0.2 {hue.accent})`), a warning that names the theme when it is not (`color-mix()` over another token), and a theme that sets any token a checked colour depends on, directly, through an alias or through references, is checked. The colour library (culori, `culori/fn`: 27 kB minified, 9 kB gzipped) is build-only and never ships. The check runs when its module has run, and again at the end of a build over the themes of every module.
|
|
356
439
|
|
|
357
440
|
Output (the `@property` rules of `space-md` and `color-fg` are not shown):
|
|
358
441
|
|
|
359
442
|
```css
|
|
360
443
|
@layer _.k, _.a, _.b, _.c, _.d, _.e;
|
|
361
|
-
@property --color-accent {
|
|
362
|
-
|
|
363
|
-
|
|
444
|
+
@property --color-accent {
|
|
445
|
+
syntax: "<color>";
|
|
446
|
+
inherits: true;
|
|
447
|
+
initial-value: oklch(62% 0.19 259);
|
|
448
|
+
}
|
|
449
|
+
@property --color-bg {
|
|
450
|
+
syntax: "<color>";
|
|
451
|
+
inherits: true;
|
|
452
|
+
initial-value: #ffffff;
|
|
453
|
+
}
|
|
454
|
+
@property --color-link {
|
|
455
|
+
syntax: "<color>";
|
|
456
|
+
inherits: true;
|
|
457
|
+
initial-value: oklch(62% 0.19 259);
|
|
458
|
+
}
|
|
364
459
|
@layer _.k {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
460
|
+
:root {
|
|
461
|
+
--color-link: var(--color-accent);
|
|
462
|
+
}
|
|
463
|
+
@media (prefers-color-scheme: dark) {
|
|
464
|
+
:root {
|
|
465
|
+
--color-bg: #0b0b0f;
|
|
466
|
+
--color-fg: #f3f4f6;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
@container (width < 30rem) {
|
|
470
|
+
:where(*) {
|
|
471
|
+
--color-bg: #eee;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
.light {
|
|
475
|
+
--color-bg: #fff;
|
|
476
|
+
}
|
|
373
477
|
}
|
|
374
478
|
```
|
|
375
479
|
|
|
@@ -403,10 +507,25 @@ export const d = minHeight(firstThatWorks("100dvh", "100vhh")); // error: the fa
|
|
|
403
507
|
`keyframes()` and `positionTry()` define an at-rule next to the styles that use it and return its name, a typed value that fits where the grammar wants a name.
|
|
404
508
|
|
|
405
509
|
```ts
|
|
406
|
-
import {
|
|
510
|
+
import {
|
|
511
|
+
animation,
|
|
512
|
+
cn,
|
|
513
|
+
hover,
|
|
514
|
+
keyframes,
|
|
515
|
+
margin,
|
|
516
|
+
opacity,
|
|
517
|
+
positionTry,
|
|
518
|
+
positionTryFallbacks,
|
|
519
|
+
top,
|
|
520
|
+
transform,
|
|
521
|
+
} from "cssints" with { type: "cssints" };
|
|
407
522
|
|
|
408
523
|
export const fade = keyframes({ from: opacity(0), to: opacity(1) });
|
|
409
|
-
export const wiggle = keyframes({
|
|
524
|
+
export const wiggle = keyframes({
|
|
525
|
+
from: transform("rotate(0deg)"),
|
|
526
|
+
"50%": cn(transform("rotate(8deg)"), opacity(0.5)),
|
|
527
|
+
to: transform("rotate(0deg)"),
|
|
528
|
+
});
|
|
410
529
|
export const above = positionTry(cn(top("anchor(bottom)"), margin(2)));
|
|
411
530
|
|
|
412
531
|
export const a = animation(fade, "200ms", "ease-out");
|
|
@@ -427,11 +546,23 @@ export const f = positionTryFallbacks(fade); // error: a keyframes name is not a
|
|
|
427
546
|
|
|
428
547
|
```css
|
|
429
548
|
@layer _.k {
|
|
430
|
-
|
|
431
|
-
|
|
549
|
+
@keyframes kf-0e6af964 {
|
|
550
|
+
from {
|
|
551
|
+
opacity: 0;
|
|
552
|
+
}
|
|
553
|
+
to {
|
|
554
|
+
opacity: 1;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
@position-try --pt-23965b44 {
|
|
558
|
+
top: anchor(bottom);
|
|
559
|
+
margin: 0.5rem;
|
|
560
|
+
}
|
|
432
561
|
}
|
|
433
562
|
@layer _.a {
|
|
434
|
-
|
|
563
|
+
._44ecsn {
|
|
564
|
+
animation: kf-0e6af964 200ms ease-out;
|
|
565
|
+
}
|
|
435
566
|
}
|
|
436
567
|
```
|
|
437
568
|
|
|
@@ -444,15 +575,21 @@ export const f = positionTryFallbacks(fade); // error: a keyframes name is not a
|
|
|
444
575
|
import { bg, cn, hover, p } from "cssints" with { type: "cssints" };
|
|
445
576
|
|
|
446
577
|
export const box = (props: { on: boolean; class?: string }) =>
|
|
447
|
-
|
|
448
|
-
export const row = (kind: "a" | "b", items: boolean[]) =>
|
|
578
|
+
cn(p(2), bg("white"), props.on && p(4), hover(bg("#eee")), props.class);
|
|
579
|
+
export const row = (kind: "a" | "b", items: boolean[]) =>
|
|
580
|
+
cn(
|
|
581
|
+
p(1),
|
|
582
|
+
kind === "a" ? p(2) : bg("red"),
|
|
583
|
+
items.map((x) => x && p(3)),
|
|
584
|
+
);
|
|
449
585
|
export const bad = (n: number) => cn(p(2), p(n)); // the build refuses it: css arguments must be known at build time
|
|
450
586
|
```
|
|
451
587
|
|
|
452
588
|
```tsx skip
|
|
453
589
|
import { _cx as __cx } from "cssints";
|
|
454
590
|
|
|
455
|
-
export const box = (props) =>
|
|
591
|
+
export const box = (props) =>
|
|
592
|
+
__cx([["_lwugp7", "_1t8gqy5"]], "_1t8gqy5 _1ebepn0 _16f8jrr", props.on && "_lwugp7", props.class);
|
|
456
593
|
```
|
|
457
594
|
|
|
458
595
|
- **What is a runtime argument.** One that needs a binding the build does not have: it names a parameter, a prop, a signal or any other local of the function or block around the call (`on && p(4)`, `cond ? a : b`, `state() && p(4)`, `props.class`, `items.map(...)`), or a string or template literal, or a call of something that is not a `css.*` member (`button({ size })`, a `cv` result: a class string at build time too). Everything else is known at build time, a module `const` too, and merges. A spread (`...list`) is judged by what it spreads. Strings, `false`, `null`, `undefined` and `0` are skipped, arrays are flattened; there are no clsx-style objects (`cond && style`). A `false`, `null` or `undefined` that is known at build time (`dev && p(4)` with a module `const dev = false`) is no style and is skipped there.
|
|
@@ -467,13 +604,13 @@ export const box = (props) => __cx([["_lwugp7", "_1t8gqy5"]], "_1t8gqy5 _1ebepn0
|
|
|
467
604
|
import { cn, cv, fontSize, opacity, p } from "cssints" with { type: "cssints" };
|
|
468
605
|
|
|
469
606
|
const button = cv({
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
607
|
+
base: cn(p(2), fontSize("1rem")),
|
|
608
|
+
variants: {
|
|
609
|
+
size: { sm: p(1), lg: cn(p(4), fontSize("1.25rem")) },
|
|
610
|
+
disabled: { true: opacity(0.5), false: opacity(1) },
|
|
611
|
+
},
|
|
612
|
+
defaultVariants: { size: "sm", disabled: false },
|
|
613
|
+
compoundVariants: [{ size: "lg", disabled: true, class: opacity(0.3) }],
|
|
477
614
|
});
|
|
478
615
|
|
|
479
616
|
export const classes = () => button({ size: "lg" }); // "_lwugp7 _z6yj9k _1t0bw2x"
|
|
@@ -527,13 +664,19 @@ css.supports("(display: grdi)"); // @supports: unexpected "grdi" in "(display: g
|
|
|
527
664
|
- Clamping text to N lines is the WebKit box, in every engine today: `display("-webkit-box")` (the `display` grammar takes `-webkit-box` and `-webkit-inline-box`), `WebkitBoxOrient("vertical")` and `WebkitLineClamp(N)`, with `overflow("hidden")`. The three are quiet: BCD files them under prefixed standard names, which the support lookup does not follow. LightningCSS passes them through and adds nothing. The standard `lineClamp(N)` (css-overflow-4) is a member too, and warns for the default targets: no engine ships it unprefixed (Chrome 154 rejects it) and LightningCSS does not prefix it.
|
|
528
665
|
|
|
529
666
|
```ts
|
|
530
|
-
import {
|
|
667
|
+
import {
|
|
668
|
+
cn,
|
|
669
|
+
display,
|
|
670
|
+
overflow,
|
|
671
|
+
WebkitBoxOrient,
|
|
672
|
+
WebkitLineClamp,
|
|
673
|
+
} from "cssints" with { type: "cssints" };
|
|
531
674
|
|
|
532
675
|
export const clamp2 = cn(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
676
|
+
display("-webkit-box"),
|
|
677
|
+
WebkitBoxOrient("vertical"),
|
|
678
|
+
WebkitLineClamp(2),
|
|
679
|
+
overflow("hidden"),
|
|
537
680
|
);
|
|
538
681
|
```
|
|
539
682
|
|
|
@@ -549,32 +692,40 @@ import { cn, p } from "cssints" with { type: "cssints" };
|
|
|
549
692
|
import { scope } from "cssints/plugin";
|
|
550
693
|
|
|
551
694
|
export const { ring, dot, webkitFontSmoothing } = scope("ring", {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
695
|
+
// a local syntax name: `<width>` in a grammar of this scope (it does not leak into mdn's names)
|
|
696
|
+
width: "<length [0,∞]> | thin | thick",
|
|
697
|
+
// a kernel: global CSS, once. `&` is the class the engine mints for it, `--_w` is the custom property `--ring-w`
|
|
698
|
+
"#core": {
|
|
699
|
+
css: "& { outline: var(--_w) solid var(--_c); outline-offset: 2px }",
|
|
700
|
+
atProperty: '@property --_w { syntax: "<length>"; inherits: false; initial-value: 2px; }',
|
|
701
|
+
requires: ["css.at-rules.property"],
|
|
702
|
+
},
|
|
703
|
+
"#noOutline": {
|
|
704
|
+
props: ["outline", /^outline-/],
|
|
705
|
+
message: "ring() sets the outline: use its methods",
|
|
706
|
+
},
|
|
707
|
+
// a chain member, like flex(): a call gives the marker and the kernel, each method emits custom properties
|
|
708
|
+
ring: {
|
|
709
|
+
uses: ["core", "noOutline"],
|
|
710
|
+
chain: {
|
|
711
|
+
width: { args: "<width>", spacing: true, emit: (v) => [["--_w", v]] },
|
|
712
|
+
color: { args: "<color>", emit: (v) => [["--_c", v]] },
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
// a property of your own, typed by its grammar (it is not in mdn-data); the key starts with `-`
|
|
716
|
+
"-webkit-font-smoothing": "auto | none | antialiased | subpixel-antialiased",
|
|
717
|
+
// a call member: its function is the entry of the same name in build()
|
|
718
|
+
dot: { args: "small | large" },
|
|
573
719
|
})
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
export const card = cn(
|
|
720
|
+
.build({ dot: (size) => ({ decls: [["--_size", size === "small" ? "4px" : "8px"]] }) })
|
|
721
|
+
.export();
|
|
722
|
+
|
|
723
|
+
export const card = cn(
|
|
724
|
+
p(2),
|
|
725
|
+
ring().width("thin").color("red"),
|
|
726
|
+
webkitFontSmoothing("antialiased"),
|
|
727
|
+
dot("small"),
|
|
728
|
+
);
|
|
578
729
|
export const twice = ring().width("1px").width("2px"); // error: a method is used once
|
|
579
730
|
export const wrong = ring().width("red"); // error: not a width
|
|
580
731
|
export const bad = webkitFontSmoothing("bold"); // error: not in the grammar
|
|
@@ -598,15 +749,15 @@ import { scope } from "cssints/plugin";
|
|
|
598
749
|
|
|
599
750
|
// A member that sets tokens in a look: `mode(t, "dark", { color: { bg: "#000" } })` is `[data-mode="dark"] { --color-bg: #000 }` in @layer viewer
|
|
600
751
|
export const { mode } = scope("mode", {
|
|
601
|
-
|
|
602
|
-
|
|
752
|
+
layers: { layers: ["app", "viewer"] },
|
|
753
|
+
mode: { args: ["$tokens", "<custom-ident>", "$overrides"], uses: ["layers"] },
|
|
603
754
|
})
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
755
|
+
.build({
|
|
756
|
+
mode: (tokens, name, overrides) => ({
|
|
757
|
+
themes: [{ tokens, overrides, selector: `[data-mode="${name}"]`, layer: "viewer" }],
|
|
758
|
+
}),
|
|
759
|
+
})
|
|
760
|
+
.export();
|
|
610
761
|
```
|
|
611
762
|
|
|
612
763
|
- **Conditions in a kernel written as styles.** `hover` and the other pseudo-classes, `attr()`, `within()`, `media()`, `container()` and `supports()` wrap the kernel's styles as they wrap a user's: `css.hover(css.opacity(0.8))`, `css.media("(width >= 40rem)")(css.padding("1rem"))`. The rules are printed under the kernel's class, in layer `_.k`, in the order of the sheet (fewer conditions first, then queries by width and pseudo-classes by place, a shorthand before its longhands), so the order the author writes them in does not matter; the declarations of one selector share a block. At most four conditions, as everywhere.
|
|
@@ -616,15 +767,15 @@ import { cn, paddingLeft } from "cssints" with { type: "cssints" };
|
|
|
616
767
|
import { scope } from "cssints/plugin";
|
|
617
768
|
|
|
618
769
|
export const { chip } = scope("chip", {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
770
|
+
core: {
|
|
771
|
+
css: (css) => [
|
|
772
|
+
css.padding("var(--_p)"),
|
|
773
|
+
css.border("1px solid"),
|
|
774
|
+
css.hover(css.opacity(0.8)),
|
|
775
|
+
css.media("(width >= 40rem)")(css.padding("1rem")),
|
|
776
|
+
],
|
|
777
|
+
},
|
|
778
|
+
chip: { uses: ["core"], chain: { padding: { args: "<length>", emit: (v) => [["--_p", v]] } } },
|
|
628
779
|
}).export();
|
|
629
780
|
|
|
630
781
|
// layer _.k: ._chip-core{border:1px solid;padding:var(--chip-p)} ._chip-core:hover{opacity:0.8} @media (width >= 40rem){._chip-core{padding:1rem}}
|
|
@@ -642,15 +793,15 @@ import { scope } from "cssints/plugin";
|
|
|
642
793
|
const fade = keyframes({ from: opacity(0) });
|
|
643
794
|
|
|
644
795
|
export const { row } = scope("row", {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
796
|
+
core: {
|
|
797
|
+
css: (css) => [
|
|
798
|
+
css.vars({ "--_gap": "1rem", "--_fade": fade }),
|
|
799
|
+
css.display("flex"),
|
|
800
|
+
css.animation("var(--_fade) 200ms"),
|
|
801
|
+
css.nest("& > * + *")(css.marginInlineStart("var(--_gap)")),
|
|
802
|
+
],
|
|
803
|
+
},
|
|
804
|
+
row: { uses: ["core"], chain: { gap: { args: "<length>", emit: (v) => [["--_gap", v]] } } },
|
|
654
805
|
}).export();
|
|
655
806
|
|
|
656
807
|
// layer _.k: @keyframes kf-…{from{opacity:0}} ._row-core{animation:var(--row-fade) 200ms;--row-gap:1rem;--row-fade:kf-…;display:flex} ._row-core > * + *{margin-inline-start:var(--row-gap)}
|
|
@@ -672,10 +823,10 @@ import { lacking } from "cssints/plugin";
|
|
|
672
823
|
|
|
673
824
|
const old = lacking("css.at-rules.container.scroll-state_queries").length > 0; // ["firefox 140"] or []
|
|
674
825
|
export const stuck = (...styles: Style[]) =>
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
826
|
+
cn(
|
|
827
|
+
container("scroll-state(stuck: top)")(...styles),
|
|
828
|
+
...(old ? [within(attr("data-stuck", "top", "~="), "stuck")(...styles)] : []),
|
|
829
|
+
);
|
|
679
830
|
export const title = stuck(color("red"));
|
|
680
831
|
```
|
|
681
832
|
|
|
@@ -688,13 +839,26 @@ import { color } from "cssints" with { type: "cssints" };
|
|
|
688
839
|
import { dependsOn } from "cssints/plugin";
|
|
689
840
|
|
|
690
841
|
export const swatch = (file: string) => {
|
|
691
|
-
|
|
692
|
-
|
|
842
|
+
dependsOn(file);
|
|
843
|
+
return color(readFileSync(file, "utf8").trim());
|
|
693
844
|
};
|
|
694
845
|
```
|
|
695
846
|
|
|
696
847
|
A dependency is a file, not a directory: a file added to a directory is not seen. Vite's watcher skips `node_modules`, so a file there (a collection of `@iconify-json`, `@fontsource`) is not watched. An evaluation that fails records nothing: after a build error, edit the module or reload.
|
|
697
848
|
|
|
849
|
+
- **`inGroup(group, property, value)`** says whether a declaration is in an export group (`widely-available`, `baseline-2022/with-downstream`, `css-grid-2`, ...; see "Export groups"): the group has the property, and the value holds no word it bans (a keyword of that property, a function, a unit). It is the scan that `cssints/<group>` runs on a call, from the same data (2026-10-11, cssints-l5z5). A property cssints has no member for (a custom property, `-webkit-text-size-adjust`) names no feature of the data and is in every group; `limited-availability` is everything; an unknown group is an error. A plugin that has a variant per group emits only what passes (`@cssints/reset/widely-available`). The data covers properties, keywords, functions, units and the pseudo-classes without arguments, not a selector as a whole: `:where()`, `::backdrop`, attribute selectors and combinators are not checked. Build time, like `scope()`:
|
|
850
|
+
|
|
851
|
+
```ts
|
|
852
|
+
import { inGroup } from "cssints/plugin";
|
|
853
|
+
|
|
854
|
+
const keep = (group: string, decls: [string, string][]) =>
|
|
855
|
+
decls.filter(([p, v]) => inGroup(group, p, v));
|
|
856
|
+
export const heading = keep("widely-available", [
|
|
857
|
+
["text-wrap", "balance"],
|
|
858
|
+
["overflow-wrap", "break-word"],
|
|
859
|
+
]); // overflow-wrap only
|
|
860
|
+
```
|
|
861
|
+
|
|
698
862
|
- **Types.** `cssints/plugin` points at `author.ts` and `scope.ts`: the signatures and the type-level code (`Members`, `Chain`, `Expand`, the call checks) and no code that runs, because a program that imports a plugin must not need Node's types or a newer `lib`. The engine is `scope-engine.ts`; `macro.ts` registers it when the library loads.
|
|
699
863
|
- **Not in 0.2:** `keyword` members, completions of keywords inside plugin grammar strings (the message still names the grammar), and a plugin that is a package of members you install other than `@cssints/icons`.
|
|
700
864
|
|
|
@@ -706,9 +870,9 @@ Every CSS function that mdn-data has a grammar for is a member of `css` and a na
|
|
|
706
870
|
import * as css from "cssints" with { type: "cssints" };
|
|
707
871
|
|
|
708
872
|
const t = css.createTokens({
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
873
|
+
color: { $type: "color", accent: { $value: "oklch(62% 0.19 259)" }, bg: { $value: "white" } },
|
|
874
|
+
space: { $type: "dimension", sm: { $value: "8px" }, md: { $value: "16px" } },
|
|
875
|
+
time: { $type: "duration", fast: { $value: "100ms" } },
|
|
712
876
|
});
|
|
713
877
|
|
|
714
878
|
export const a = css.width(css.calc(t.space.md, "*", 2)); // width: calc(var(--space-md) * 2)
|
|
@@ -718,7 +882,9 @@ export const d = css.maxWidth(css.min("48rem", "100%"));
|
|
|
718
882
|
export const e = css.color(css.lightDark("white", t.color.bg)); // color: light-dark(white, var(--color-bg))
|
|
719
883
|
export const f = css.color(css.colorMix("in oklab", t.color.accent, "black 15%"));
|
|
720
884
|
export const g = css.color(css.oklch(0.7, 0.1, 200, "/", 0.5)); // color: oklch(0.7 0.1 200 / 0.5)
|
|
721
|
-
export const h = css.backgroundImage(
|
|
885
|
+
export const h = css.backgroundImage(
|
|
886
|
+
css.linearGradient("to right", [t.color.accent, "25%"], "blue"),
|
|
887
|
+
);
|
|
722
888
|
export const i = css.transitionTimingFunction(css.cubicBezier(0.1, 0.7, 1, 0.1));
|
|
723
889
|
export const img = css.backgroundImage(css.url('"a.png"')); // url("a.png"): the quotes are yours
|
|
724
890
|
export const j = css.transform(css.translateFn(t.space.md, 0), css.rotateFn("10deg"));
|
|
@@ -743,8 +909,8 @@ A token is its `var()` already, so `var(--name)` is not written. `css.var(token,
|
|
|
743
909
|
import * as css from "cssints" with { type: "cssints" };
|
|
744
910
|
|
|
745
911
|
const t = css.createTokens({
|
|
746
|
-
|
|
747
|
-
|
|
912
|
+
color: { $type: "color", accent: { $value: "red" } },
|
|
913
|
+
space: { $type: "dimension", sm: { $value: "8px" } },
|
|
748
914
|
});
|
|
749
915
|
|
|
750
916
|
export const a = css.padding(css.var(t.space.sm, "4px")); // padding: var(--space-sm, 4px)
|
|
@@ -763,11 +929,11 @@ import { scope } from "cssints/plugin";
|
|
|
763
929
|
|
|
764
930
|
// `args` is the grammar of the parts, joined by a space: `value` gets each one as text, a token as its var(--name).
|
|
765
931
|
export const { tint, step } = scope("units", {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
932
|
+
tint: {
|
|
933
|
+
args: "<color> <percentage>",
|
|
934
|
+
value: ([color, share]) => new CSSColorValue(`color-mix(in oklab, ${color}, white ${share})`),
|
|
935
|
+
},
|
|
936
|
+
step: { args: "<number>", value: ([n]) => `${Number(n) * 4}px` },
|
|
771
937
|
}).export();
|
|
772
938
|
|
|
773
939
|
tint("red", "20%");
|
|
@@ -782,11 +948,12 @@ step(2);
|
|
|
782
948
|
import { arg, fn, scope } from "cssints/plugin";
|
|
783
949
|
|
|
784
950
|
export const { shade } = scope("shades", {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
951
|
+
shade: fn({
|
|
952
|
+
args: ["<color>", { "by?": "<percentage>", "dark?": arg<boolean>() }],
|
|
953
|
+
label: "tint.shade",
|
|
954
|
+
value: ([color, { by = "10%", dark }]) =>
|
|
955
|
+
`color-mix(in oklab, ${color}, ${dark ? "black" : "white"} ${by})`,
|
|
956
|
+
}),
|
|
790
957
|
}).export();
|
|
791
958
|
|
|
792
959
|
shade("red");
|
|
@@ -811,10 +978,17 @@ import { frame } from "cssints/frame" with { type: "cssints" };
|
|
|
811
978
|
const radius = css.token.length("12px", "radius-lg");
|
|
812
979
|
|
|
813
980
|
// radius 24px, padding 16px, border 1px: the field inside gets 24 - 16 - 1 = 7px
|
|
814
|
-
export const card = css.cn(
|
|
981
|
+
export const card = css.cn(
|
|
982
|
+
frame().radius("1.5rem").padding(4).border("1px"),
|
|
983
|
+
css.bg("white"),
|
|
984
|
+
css.borderColor("#9ca3af"),
|
|
985
|
+
);
|
|
815
986
|
export const field = css.cn(frame().padding(2).border("1px"), css.borderColor("#9ca3af"));
|
|
816
987
|
// a token is a length; a condition just sets the custom property, and the whole subtree follows
|
|
817
|
-
export const panel = css.cn(
|
|
988
|
+
export const panel = css.cn(
|
|
989
|
+
frame().radius(radius).padding(2),
|
|
990
|
+
css.media("(width >= 40rem)")(frame().padding(6)),
|
|
991
|
+
);
|
|
818
992
|
|
|
819
993
|
export const noNumber = frame().radius(4); // error: a bare number is a spacing step in padding and margin only
|
|
820
994
|
export const noPercent = frame().padding("5%"); // error: a length, not a percentage
|
|
@@ -850,7 +1024,7 @@ export const noValue = surface().ink("black"); // error: ink() takes no value
|
|
|
850
1024
|
export const own = surface().radius("4px"); // error: a surface chains its own methods only
|
|
851
1025
|
```
|
|
852
1026
|
|
|
853
|
-
- **Methods**, each once and in any order: `color(<color> | token)` is an absolute base (without it the base is the nearest surface around, or `Canvas` for the outermost); `lighten(step)`, `darken(step)` and `contrast(step)` move the lightness of the base by a `<number [0,1]>`, and only one of the three goes in a chain; `ink()` sets the text colour to black or white by the lightness of the surface (above 0.5636 black, else white). `contrast` moves
|
|
1027
|
+
- **Methods**, each once and in any order: `color(<color> | token)` is an absolute base (without it the base is the nearest surface around, or `Canvas` for the outermost); `lighten(step)`, `darken(step)` and `contrast(step)` move the lightness of the base by a `<number [0,1]>`, and only one of the three goes in a chain; `ink()` sets the text colour to black or white by the lightness of the surface (above 0.5636 black, else white). `contrast` moves _away_ from the parent: a light parent is darkened, a dark one lightened. The base may be a relative colour (`oklch(from var(--brand) l c h)`).
|
|
854
1028
|
- **Build errors** (at the call site): a step outside 0 to 1, a bad colour, a method used twice or with another step, and a `cn()` that holds `surface()` and `background` or `background-color`, `bg()` included (`cssints: surface: surface() sets the background colour: use its methods (this style also sets background-color)`). `backgroundImage`, `color` and the border colours do not conflict. Conflicts with an opaque runtime string in `cn()` are not seen, as for any plugin.
|
|
855
1029
|
- **What it emits:** the class `_surface-core` and one class per method (`._x{--surface-d:0.06}`, in `_.a`, or `_.b` inside a condition: a `hover()` or a media query moves every surface below). The kernel `surface.core` (about 1 kB minified with its registrations and fallback, once, in `_.k`) is two `@container style(--surface-p: ...)` rules that alternate two registered `<color>` properties, `--surface-e` and `--surface-o`, because a property cannot read itself; the step `--surface-d` is not registered, since it may contain `l`, which only means something inside `oklch(from …)`. Its names are `--surface-*`; frame's are `--frame-*`, so a frame and a surface can be one element, or nest in each other, with no collision.
|
|
856
1030
|
- **Not a contrast guarantee.** A lightness threshold of 0.5636 gives 4.6:1 at the worst on greys, but 3.7:1 on 3 000 random chromatic colours (research, Chrome). `ink()` on a saturated surface can fall short of WCAG AA. For a ratio, declare the colours as tokens and use `css.check.contrast` (see Themes): it checks the declared pair at build time, which the kernel cannot do because it never sees the parent.
|
|
@@ -867,8 +1041,8 @@ import { icons } from "@cssints/icons" with { type: "cssints" };
|
|
|
867
1041
|
|
|
868
1042
|
// icons.ts: a build-time module (it runs while the build runs, and never reaches the browser)
|
|
869
1043
|
export const mdi = icons<typeof import("@iconify-json/mdi/icons.json")>({
|
|
870
|
-
|
|
871
|
-
|
|
1044
|
+
provider: "iconify-json",
|
|
1045
|
+
collection: "mdi",
|
|
872
1046
|
});
|
|
873
1047
|
export const brand = icons({ provider: "fs", dir: "src/icons" });
|
|
874
1048
|
|
|
@@ -880,9 +1054,17 @@ export const typo = mdi("clos"); // error: not an icon of mdi
|
|
|
880
1054
|
|
|
881
1055
|
```css
|
|
882
1056
|
/* kernel icons.mask, layer _.k; LightningCSS adds -webkit-mask for your targets */
|
|
883
|
-
._icons-mask {
|
|
1057
|
+
._icons-mask {
|
|
1058
|
+
display: inline-block;
|
|
1059
|
+
width: 1em;
|
|
1060
|
+
height: 1em;
|
|
1061
|
+
background-color: currentColor;
|
|
1062
|
+
mask: var(--icons-svg) no-repeat center / 100% 100%;
|
|
1063
|
+
}
|
|
884
1064
|
/* the only per-icon rule */
|
|
885
|
-
._x1 {
|
|
1065
|
+
._x1 {
|
|
1066
|
+
--icons-svg: url("data:image/svg+xml,%3Csvg …");
|
|
1067
|
+
}
|
|
886
1068
|
```
|
|
887
1069
|
|
|
888
1070
|
- **`icons({ provider: "iconify-json", collection })`** reads `@iconify-json/<collection>/icons.json` from the `node_modules` of the working directory, while the module runs. **`icons({ provider: "fs", dir })`** reads `<dir>/<name>.svg` (`dir` is relative to the working directory; the SVG needs a `viewBox`). The result is a function of the icon name: `mdi("home")` is a style that goes into `cn()` and the conditions like any member. Call `icons()` in a module of its own, imported with the attribute, as above.
|
|
@@ -896,24 +1078,24 @@ export const typo = mdi("clos"); // error: not an icon of mdi
|
|
|
896
1078
|
|
|
897
1079
|
More plugins on `scope()`, each a package of its own with its rules and limits in its README. Import them with the attribute, like `cssints`.
|
|
898
1080
|
|
|
899
|
-
| Package
|
|
900
|
-
|
|
901
|
-
| [`@cssints/fluid`](../packages/fluid/README.md)
|
|
902
|
-
| [`@cssints/layout`](../packages/layout/README.md)
|
|
903
|
-
| [`@cssints/fonts`](../packages/fonts/README.md)
|
|
904
|
-
| [`@cssints/reset`](../packages/reset/README.md)
|
|
905
|
-
| [`@cssints/tokens`](../packages/tokens/README.md)
|
|
906
|
-
| [`@cssints/gradient`](../packages/gradient/README.md)
|
|
907
|
-
| [`@cssints/motion`](../packages/motion/README.md)
|
|
908
|
-
| [`@cssints/squircle`](../packages/squircle/README.md)
|
|
909
|
-
| [`@cssints/reveal`](../packages/reveal/README.md)
|
|
910
|
-
| [`@cssints/stuck`](../packages/stuck/README.md)
|
|
911
|
-
| [`@cssints/balance`](../packages/balance/README.md)
|
|
912
|
-
| [`@cssints/masonry`](../packages/masonry/README.md)
|
|
913
|
-
| [`@cssints/placeholder`](../packages/placeholder/README.md) | `placeholder(path, { mode? })`
|
|
914
|
-
| [`@cssints/ramp`](../packages/ramp/README.md)
|
|
915
|
-
| [`@cssints/rybitten`](../packages/rybitten/README.md)
|
|
916
|
-
| [`@cssints/skeleton`](../packages/skeleton/README.md)
|
|
1081
|
+
| Package | Members | What it does |
|
|
1082
|
+
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1083
|
+
| [`@cssints/fluid`](../packages/fluid/README.md) | `fluid(min, max, { from?, to?, zoom? })`, `fluid.container(...)` | A `clamp()` that grows between two widths, computed at build time; a range that fails WCAG 1.4.4 is a build error |
|
|
1084
|
+
| [`@cssints/layout`](../packages/layout/README.md) | `stack()`, `cluster()`, `sidebar()`, `switcher()`, `cover()`, `autoGrid()` | Every Layout primitives as chain members |
|
|
1085
|
+
| [`@cssints/fonts`](../packages/fonts/README.md) | `font(family, { provider, ... })` → `.family()`, `.trim()` | `@font-face` and a metric-matched fallback face; cap-height trimming by `text-box`, else capsize's margins |
|
|
1086
|
+
| [`@cssints/reset`](../packages/reset/README.md) | `reset({ media?, prose? })` | A modern reset in a top-level `@layer reset` declared before the engine's, below every style; `@cssints/reset/<baseline group>` keeps only the declarations of that group |
|
|
1087
|
+
| [`@cssints/tokens`](../packages/tokens/README.md) | `/open-props`, `/radix`, `/flexoki`, `/reasonable-colors` | DTCG data for `createTokens` |
|
|
1088
|
+
| [`@cssints/gradient`](../packages/gradient/README.md) | `gradient(kind, direction, from, to, easing?, stops?)` | Eased colour stops mixed in oklab at build time |
|
|
1089
|
+
| [`@cssints/motion`](../packages/motion/README.md) | `motion().preset().duration().delay().easing()` | Animation presets over `keyframes()`, with reduced motion |
|
|
1090
|
+
| [`@cssints/squircle`](../packages/squircle/README.md) | `squircle().radius()` | `corner-shape: squircle`, else a CSS `mask` of the same shape |
|
|
1091
|
+
| [`@cssints/reveal`](../packages/reveal/README.md) | `reveal().preset().range().duration().easing()` | motion's entrance presets on `animation-timeline: view()`; for other targets a script plays it once on entry (delivered by the runtime) |
|
|
1092
|
+
| [`@cssints/stuck`](../packages/stuck/README.md) | `sticky().top()`, `stuck(styles)`, `stuck("bottom")(styles)` | Styles inside a sticky element while it is stuck, by `@container scroll-state()`; for other targets an `IntersectionObserver` (delivered by the runtime) |
|
|
1093
|
+
| [`@cssints/balance`](../packages/balance/README.md) | `balance()`, `pretty()` | `text-wrap: balance` / `pretty`; for targets without balance a script balances the lines (delivered by the runtime) |
|
|
1094
|
+
| [`@cssints/masonry`](../packages/masonry/README.md) | `masonry().columns().gap()` | `display: grid-lanes`, else CSS `columns` (top to bottom per column) |
|
|
1095
|
+
| [`@cssints/placeholder`](../packages/placeholder/README.md) | `placeholder(path, { mode? })` | An `<image>` from a JPEG or PNG read at build time: a gradient of its colours laid out as the image, or a tiny thumbhash PNG |
|
|
1096
|
+
| [`@cssints/ramp`](../packages/ramp/README.md) | `ramp(base, { mode, steps \| ratios, background })`, `overrides(ramp)` | An OKLCH palette as DTCG data for `createTokens`; the contrast mode meets each ratio against its background |
|
|
1097
|
+
| [`@cssints/rybitten`](../packages/rybitten/README.md) | `color(hue, { cube })`, `wheel(steps, { cube })`, `shades(hue, lightness)`, `overrides(group)` | RYBitten's red-yellow-blue colours (36 historical cubes) as DTCG data for `createTokens`: a hue, a wheel of hues, a hue at several lightnesses |
|
|
1098
|
+
| [`@cssints/skeleton`](../packages/skeleton/README.md) | `skeleton().radius().color().highlight()` | Loading placeholders whose shimmer is in phase across the page (one clock on `:root`), static under reduced motion |
|
|
917
1099
|
|
|
918
1100
|
### Publishing a library (decided 2026-10-02, cssints-0l3f)
|
|
919
1101
|
|
|
@@ -1001,6 +1183,7 @@ TOKENS
|
|
|
1001
1183
|
```
|
|
1002
1184
|
|
|
1003
1185
|
- **Classes by call.** The element's classes, grouped by the `css.*` call that made them (`cn(...)`, a member), the call that covers most of them first; the link opens the file at that line through Vite's `/__open-in-editor`, and the call's source is shown under it. Each class shows its declarations, its conditions (green: holds now) and its layer; a marker shows its kernel (`marker of kernel frame.core`), a member's class its scope (`(frame)`); a class that is not cssints' is listed as such.
|
|
1186
|
+
- **Readable classes (dev only, decided 2026-10-12, cssints-12tp).** With `devtools` on in the dev server, the plugin adds one class to the string of each `cn(...)` and `cv({...})` site, to find an element in the DOM inspector and in the panel: `cs-<owner>[-<label>]`. The owner is the nearest enclosing function or class with a PascalCase name (`Landing`, `Button`), else the file's name (`page` for `landing/page.ts`; `index.ts` takes its folder's). The label is what the call is assigned to: a variable, an object property, a class field or method, a helper function that returns it, or a JSX attribute other than `class` and `className` (`<Tone tone={cn(...)}>` gives `cs-Button-tone`); no label if there is none. So `const logo = cn(...)` at the top of `landing/page.ts` gives `cs-page-logo`, inside `function Landing()` `cs-Landing-logo`, and `<div class={cn(...)}>` in `Button` `cs-Button`. Only `[A-Za-z0-9_-]`, an owner and a label cut at 24 and 20 characters. The name comes from where the call stands, so editing code elsewhere does not change it; two calls in one place can share a name. A call whose shape has no name to read still gets its file's name; a `css.p(4)` or other member on its own, and a `cn` inside the runtime arguments of another `cn`, get none. The `cs-` prefix keeps it off the app's own classes. It is text in the module the browser gets and nothing else: not in the hashes, the dedupe, the `drops` of a `cn` with runtime arguments or of a `cv` (it stays a static class, never dropped), the sheet or the table. The panel shows it first in its bar and in the hover tag, and the prompt's `Classes:` line lists it with the others. A build, a dev server without `devtools`, and the `ssr` environment (where the plugin evaluates; its modules hold calls, not class strings) are byte-identical to before.
|
|
1004
1187
|
- **Properties.** Per property, the winning declaration, what it beat (struck through) and what does not apply now, with the condition that fails. The winner is the highest by **layer, then specificity, then the sheet's order**, among the rules that apply: a rule applies when its selector matches the element now and each of its at-rules holds for it, which the browser decides (a probe sheet around the element, so `@container`, `@container style()`, `@supports` and `@scope` are evaluated where the element is). A shorthand counts for each of its longhands: `padding` beside a `padding-left` of a higher layer says `except padding-left`. Kernels and global rules in `_.k`, themes and looks in their layers are rules like the others.
|
|
1005
1188
|
- **Checked against the browser.** Each winner is set inline on a childless clone of the element, in its place, and the clone's computed value must equal the element's: ✓. When it does not (✗), the panel says why: the element's `style` attribute, a rule of another sheet (a rule outside every layer beats every layer, cssints' too: `.mine { color: … } in src/inspect.css beats it`), or a running animation. A value that depends on the element's content (`width: auto`, grid tracks) is `?`, not checked. A custom property is checked with its declaration as the page's sheet has it, since LightningCSS reprints it (`0.04` as `.04`) and an unregistered one computes to its tokens as written (cssints-g3u1).
|
|
1006
1189
|
- **Tokens.** Each custom property the element's rules read: its value on the element, its path (`color.surface`), and the rule that sets it on the element or the nearest ancestor (a theme, a look in `@scope`, a member's class).
|
|
@@ -1014,7 +1197,7 @@ TOKENS
|
|
|
1014
1197
|
// or: opencode: { agent: "build", skills: ["cssints"] }
|
|
1015
1198
|
```
|
|
1016
1199
|
|
|
1017
|
-
The dev server, never the page, talks to the OpenCode V2 service that is running on the machine (the one `opencode serve --service` and the desktop app register; found with `Service.discover` of `@opencode/client`, so the page holds no address and no credentials). It creates a session in the workspace root (the nearest folder up from the Vite root with a `.git`)
|
|
1200
|
+
The dev server, never the page, talks to the OpenCode V2 service that is running on the machine (the one `opencode serve --service` and the desktop app register; found with `Service.discover` of `@opencode/client`, so the page holds no address and no credentials). It sends the prompt to the session that the OpenCode command `/cssints-devtools` attached to this dev server (the plugin is `@cssints/devtools/opencode`, loaded from `.opencode/plugins/`; the files they share are in `<repository>/node_modules/.cache/cssints-devtools/`), or, with none attached, creates a session in the workspace root (the nearest folder up from the Vite root with a `.git`) and sends it there, waits for the session to end, and tells the overlay the status over the same HMR channel (`cssints:devtools:ask` from the page, `cssints:devtools:asked` back: no route that another origin could post to). The box shows `sending`, then `running · ses_…`, then `completed`, `failed` or `interrupted`; with no service running it says so and keeps your text. `@opencode/client` is an optional peer dependency: install it in the app (the message names it when it is missing). The prompt is made of cssints' own data, not source markers added to the markup, and is bounded (8 call sites, 24 properties, 3 beaten declarations each, 12 tokens, a call cut at 400 characters; the server cuts the whole at 16000):
|
|
1018
1201
|
|
|
1019
1202
|
```text skip
|
|
1020
1203
|
Change the UI of the running app as the instruction says.
|
|
@@ -1040,6 +1223,7 @@ TOKENS
|
|
|
1040
1223
|
```
|
|
1041
1224
|
|
|
1042
1225
|
A build has none of it: the server side is in the `apply: "serve"` plugin, and `devtools: "build"` with `opencode` on still gives a loader without a way to ask (the dormant box is part of the overlay chunk, about 1 kB). The idea and the V2 client calls follow brendonovich/vite-plugin-opencode (MIT).
|
|
1226
|
+
|
|
1043
1227
|
- **Its own styles do not touch yours.** The overlay is a custom element with a shadow root and its own cssints sheet, built with the package: the page's rules do not reach it, and its classes are not in your sheet or in the table.
|
|
1044
1228
|
- **Limits.** The proximity of `@scope` is not ranked (no two looks set one property on one element), and a logical and a physical property (`margin-inline-start`, `margin-left`) are two properties, as `cn()` treats them; an element inside another shadow root is picked as its host; a page that Vite does not serve as HTML (server rendering) gets no entry.
|
|
1045
1229
|
|
|
@@ -1062,7 +1246,11 @@ Jaeger takes traces only, which is all there is: a warning or an error is an eve
|
|
|
1062
1246
|
|
|
1063
1247
|
The process must end the exporter or Node stays alive: `vite build` and `server.close()` do it (the `closeBundle` hook flushes and stops it); a script that exits by itself calls `closeTrace()` from `cssints/src/trace.ts` first. A backend that is not there costs a retry of up to three seconds at the end and loses the trace, nothing else.
|
|
1064
1248
|
|
|
1065
|
-
`packages/cssints/test/trace.mjs` (in `bun run test`) checks the spans, the file attribute and the error's trace id against an in-process collector. `node packages/cssints/test/trace-live.mjs` builds apps/
|
|
1249
|
+
`packages/cssints/test/trace.mjs` (in `bun run test`) checks the spans, the file attribute and the error's trace id against an in-process collector. `node packages/cssints/test/trace-live.mjs` builds apps/csssky and runs its HMR test traced, then reads both traces back through Jaeger's API (`GET /api/traces?service=cssints`) and prints span counts and the slowest hooks; it skips when no backend answers. Effect's DevTools (the VS Code extension, `effect/devtools`) is not wired: it needs the editor's extension listening, so neither a script nor an agent can read it.
|
|
1250
|
+
|
|
1251
|
+
### Debugging: the limit of an evaluation (`CSSINTS_EVAL_TIMEOUT`, cssints-joex)
|
|
1252
|
+
|
|
1253
|
+
A module is evaluated with all it imports in at most 30 seconds, else the build fails with `cssints: evaluating <file> took more than 30 seconds`. The engine cannot stop the code, so it closes the Module Runner: the evaluation ends at the next module it would load, and the next evaluation starts on a new runner. `CSSINTS_EVAL_TIMEOUT=<milliseconds>` changes the limit (read once, when the plugin loads); it is for tests that run on a busy machine, and the default is the one for your code.
|
|
1066
1254
|
|
|
1067
1255
|
### Checks for tools: `cssints/check` (2026-10-09, cssints-p3d2)
|
|
1068
1256
|
|
|
@@ -1086,17 +1274,19 @@ A terminal app writes its styles with `cssints/tui`, and the same Vite plugin co
|
|
|
1086
1274
|
```ts
|
|
1087
1275
|
import * as css from "cssints/tui" with { type: "cssints" };
|
|
1088
1276
|
|
|
1089
|
-
const t = css.createTokens({
|
|
1277
|
+
const t = css.createTokens({
|
|
1278
|
+
color: { $type: "color", bg: { $value: "#1e1e2e" }, accent: { $value: "#89b4fa" } },
|
|
1279
|
+
});
|
|
1090
1280
|
css.createGlobalTheme(t, "light", { color: { bg: "#eff1f5", accent: "#1e66f5" } });
|
|
1091
1281
|
|
|
1092
1282
|
export const button = css.cn(
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1283
|
+
css.border("rounded", "#585b70"),
|
|
1284
|
+
css.p(0, 1),
|
|
1285
|
+
css.width(12),
|
|
1286
|
+
css.bg(t.color.bg),
|
|
1287
|
+
css.cursor("pointer"),
|
|
1288
|
+
css.hover(css.borderColor(t.color.accent)),
|
|
1289
|
+
css.active(css.borderStyle("double")),
|
|
1100
1290
|
);
|
|
1101
1291
|
css.width("10rem"); // error: a terminal counts cells
|
|
1102
1292
|
css.boxShadow("0 0 1px red"); // error: no such member in a terminal
|
|
@@ -1104,13 +1294,30 @@ css.color("blu"); // error: not a colour
|
|
|
1104
1294
|
```
|
|
1105
1295
|
|
|
1106
1296
|
```ts skip
|
|
1107
|
-
export const button = {
|
|
1297
|
+
export const button = {
|
|
1298
|
+
border: true,
|
|
1299
|
+
borderStyle: "rounded",
|
|
1300
|
+
borderColor: "#585b70",
|
|
1301
|
+
paddingTop: 0,
|
|
1302
|
+
paddingRight: 1,
|
|
1303
|
+
paddingBottom: 0,
|
|
1304
|
+
paddingLeft: 1,
|
|
1305
|
+
width: 12,
|
|
1306
|
+
backgroundColor: "#1e1e2e",
|
|
1307
|
+
cursor: "pointer",
|
|
1308
|
+
":hover": { borderColor: "#89b4fa" },
|
|
1309
|
+
":active": { border: true, borderStyle: "double" },
|
|
1310
|
+
themes: { light: { backgroundColor: "#eff1f5", ":hover": { borderColor: "#1e66f5" } } },
|
|
1311
|
+
vars: { backgroundColor: "color-bg", ":hover": { borderColor: "color-accent" } },
|
|
1312
|
+
};
|
|
1108
1313
|
```
|
|
1109
1314
|
|
|
1110
1315
|
```tsx skip
|
|
1111
1316
|
import { attach, setTheme, use } from "@cssints/tui/opentui";
|
|
1112
1317
|
|
|
1113
|
-
<box {...use(button)}
|
|
1318
|
+
<box {...use(button)}>
|
|
1319
|
+
<text>Save</text>
|
|
1320
|
+
</box>; // Solid or React: the spread is a ref that calls attach
|
|
1114
1321
|
<text {...use(() => (active() ? tabActive : tab))}>Home</text>; // a style that changes: an accessor
|
|
1115
1322
|
attach(new BoxRenderable(renderer, {}), button); // core
|
|
1116
1323
|
setTheme("light");
|
|
@@ -1143,14 +1350,18 @@ process.stdout.write(ui.render(term, build, { pointer: { x, y, down } }).output)
|
|
|
1143
1350
|
import { createSystemTheme } from "@cssints/tui/system";
|
|
1144
1351
|
import { t } from "./styles.ts";
|
|
1145
1352
|
|
|
1146
|
-
export const system = createSystemTheme(
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1353
|
+
export const system = createSystemTheme(
|
|
1354
|
+
t,
|
|
1355
|
+
{
|
|
1356
|
+
color: {
|
|
1357
|
+
bg: { surface: true },
|
|
1358
|
+
card: { surface: true, on: [t.color.bg], contrast: 1.1 },
|
|
1359
|
+
text: { hue: "fg", on: [t.color.bg, t.color.card], contrast: 7 },
|
|
1360
|
+
accent: { hue: "blue", on: [t.color.bg, t.color.card], contrast: 3 },
|
|
1361
|
+
},
|
|
1362
|
+
},
|
|
1363
|
+
{ themes: { light: "light" } },
|
|
1364
|
+
);
|
|
1154
1365
|
|
|
1155
1366
|
// OpenTUI: renderer.getPalette() and its theme_mode event
|
|
1156
1367
|
await followSystemTheme(await createCliRenderer(), system);
|
|
@@ -1160,8 +1371,8 @@ process.stdin.on("data", (bytes) => dispatch(input.scan(follow.feed(bytes)).even
|
|
|
1160
1371
|
```
|
|
1161
1372
|
|
|
1162
1373
|
- **The theme table** (cssints-q1j3). A site holds, besides its values and static `themes`, `vars`: the token of each colour prop set to a lone token (`vars: { backgroundColor: "color-bg" }`, the name without `--`). `defineTheme(name, values, base)` of `@cssints/tui` registers a run-time theme: a colour per token name, laid over the static theme `base` for every prop with a token; `setTheme(name)` selects it, and `defineTheme` of the theme in use resolves every attached renderable again. The system theme is one; static themes stay inline, since they need no registration and no import order. A colour that is not a lone token (`color-mix()` over one) keeps its static value.
|
|
1163
|
-
- **Live in the browser** (cssints-ey13, 2026-10-11). [The TUI page](https://cssints.dev/tui) of this site runs apps/
|
|
1164
|
-
- **Cost.** A colour member costs what the same web member costs in the types (the `color` grammar); `bench/tui.ts` is 184k instantiations (tsc) for 16 calls and a token set. No terminal HMR in this version. The system theme brings culori's rgb, oklab, oklch and okhsl modes to the app (45 kB unminified in
|
|
1374
|
+
- **Live in the browser** (cssints-ey13, 2026-10-11). [The TUI page](https://cssints.dev/tui) of this site runs apps/csssky-tui's styles on `@cssints/tui/bomb` in a real terminal: Ghostty's emulator compiled to WebAssembly (ghostty-web) draws csssky's feed and thread from its recorded fixtures, with keys and the mouse. The page is the terminal: it answers the system theme's queries (OSC 10, 11, 4, `CSI ? 996 n`) from the site's display settings and reports a change of them (mode 2031), so the theme is solved again as the palette or the mode changes. OpenTUI is not shown: its renderer is native Zig over `bun:ffi`. On a change notification, `followSystemTheme` of `@cssints/tui/bomb` asks the colours again (`COLOURS` of `@cssints/tui/system`), not the scheme report: its reply is the same sequence as a notification, and asking it looped with a terminal that answers it.
|
|
1375
|
+
- **Cost.** A colour member costs what the same web member costs in the types (the `color` grammar); `bench/tui.ts` is 184k instantiations (tsc) for 16 calls and a token set. No terminal HMR in this version. The system theme brings culori's rgb, oklab, oklch and okhsl modes to the app (45 kB unminified in csssky-tui's bundle).
|
|
1165
1376
|
|
|
1166
1377
|
### Export groups: `cssints/widely-available`, `cssints/css-grid-2`, … (decided 2026-10-11, cssints-n7z3)
|
|
1167
1378
|
|
|
@@ -1170,19 +1381,35 @@ A group is a subpath of `cssints` that exports the members of `cssints` which a
|
|
|
1170
1381
|
```ts
|
|
1171
1382
|
import { color, display, media } from "cssints/widely-available" with { type: "cssints" };
|
|
1172
1383
|
|
|
1173
|
-
export const card = [
|
|
1384
|
+
export const card = [
|
|
1385
|
+
display("grid"),
|
|
1386
|
+
color("rebeccapurple"),
|
|
1387
|
+
media("(width >= 40rem)")(color("black")),
|
|
1388
|
+
];
|
|
1174
1389
|
|
|
1175
1390
|
display("grid-lanes"); // error: display: "grid-lanes" is not in cssints/widely-available
|
|
1176
1391
|
color("light-dark(white, black)"); // error: color: "light-dark()" is not in cssints/widely-available
|
|
1177
1392
|
media("(prefers-reduced-data: reduce)"); // error: @media: "prefers-reduced-data" is not in cssints/widely-available
|
|
1178
1393
|
```
|
|
1179
1394
|
|
|
1395
|
+
```ts
|
|
1396
|
+
import { inlineSize } from "cssints/widely-available/with-downstream" with { type: "cssints" };
|
|
1397
|
+
import { color } from "cssints/baseline-2022" with { type: "cssints" };
|
|
1398
|
+
|
|
1399
|
+
inlineSize("10rem");
|
|
1400
|
+
inlineSize("fit-content"); // error: inline-size: "fit-content" is not in cssints/widely-available/with-downstream
|
|
1401
|
+
color("rebeccapurple");
|
|
1402
|
+
color("color-mix(in oklab, red, blue)"); // error: color: "color-mix()" is not in cssints/baseline-2022
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1180
1405
|
- **Baseline groups** (web-features 3.42, the status of each BCD key): `widely-available` is Baseline high, `newly-available` is high and low, `limited-availability` is everything (it is `cssints` itself, under its Baseline name) and `not-discouraged` is everything web-features does not mark `discouraged` (`clip`, the `page-break-*` aliases, `ime-mode`, `word-break: break-word`, `text-decoration-line: blink`, device media features). A feature web-features does not know is limited availability.
|
|
1406
|
+
- **Downstream groups** (cssints-n12x, 2026-10-11): `widely-available/with-downstream` and `newly-available/with-downstream` are the Baseline group also checked against Opera, Opera Mobile, Samsung Internet, Android WebView, UC and QQ, as `browserslist-config-baseline/with-downstream` is. A feature is in one when the Baseline group has it and every one of those browsers has it, by BCD, at the minimum version that [baseline-browser-mapping](https://npmx.dev/package/baseline-browser-mapping) 2.11.28 gives for the target (the rule of the support warnings: no prefix, no flag; UC and QQ are Blink, checked as Chrome at the Blink version of that build). `widely-available/with-downstream` has 520 of the 521 members (not `maskImage`: Opera has `-webkit-mask-image` only) and refuses `inline-size: fit-content` (Samsung Internet), `shape-outside: xywh()` and `prefers-contrast`; `newly-available/with-downstream` has 533 of 567 (UC and QQ are on Blink 123 and 121).
|
|
1407
|
+
- **Baseline years:** `baseline-2015` to `baseline-2026` (the first year web-features has to the current one) and `baseline-YYYY/with-downstream`, as `browserslist-config-baseline/YYYY` has them. `baseline-YYYY` is **cumulative**: what was Baseline (newly available, `baseline_low_date`) by the end of YYYY, so `baseline-2022` refuses `color-mix()` (2023) and `baseline-2023` refuses `light-dark()` (2024). 220 members in `baseline-2015`, 387 in `baseline-2020`, 537 in `baseline-2024`. `baseline-2026` is `newly-available` today.
|
|
1181
1408
|
- **Spec groups** are named by the W3C shortname that @webref/css uses: `cssints/css-grid-2`, `cssints/css-color-4`, `cssints/mediaqueries-4`, `cssints/selectors-4`, … (92 of them). A group holds what the spec defines, and **a level includes the levels of its series before it**: `css-color-5` has what Color 3, 4 and 5 define, `css-color-4` has Color 3 and 4. A level is a delta (Color 5 over Color 4) or a rewrite (Media Queries 4 over 3), and "I write for Color 5" means both. What only a later level defines is refused: `css-color-4` has no `colorMix` and refuses `color-mix()`, `mediaqueries-4` refuses `prefers-color-scheme`. A group has the properties, functions, pseudo-class wrappers and conditions its spec defines, so `display` is in `css-display-4` and `gridTemplateColumns` in `css-grid-2`: import from several groups in one file.
|
|
1182
1409
|
- **What is a member:** the properties (and their short names), the functions, the pseudo-class wrappers, `media`, `container`, `supports`, `keyframes` and `positionTry`. Everything else (`cn`, `cv`, `firstThatWorks`, `token`, `createTokens`, `createGlobalTheme`, `check`, `style`, `global`, `layers`, `attr`, `within`, `group`, the Typed OM values) is in every group. A group has no `css` object: write `import * as css from "cssints/widely-available"`.
|
|
1183
1410
|
- **What is refused**, in the types (an error on the argument) and in the build (an error at the call), with the same words: a keyword of the property, a function (as a call of a member, or as text, in a `firstThatWorks` too), a unit (`10rch`), a media feature, a `scroll-state()` or `style()` container query and a `selector()` support query, and a property, function or condition the group does not have (`flex().gap()` is checked as `gap`). A value that is not valid CSS is still the usual error.
|
|
1184
1411
|
- **What is not:** the types a spec only references (`<color>` in `accent-color` is any colour of any group), a feature of a module that is not in the series of the group (`css-ui-4`'s `accentColor` takes `color-mix()` as text), and what nothing attributes to a feature: the grammar of a value that no BCD key names is not narrowed. A function with no BCD key counts as limited availability. A keyword of `cursor` has the status of `cursor` (web-features counts it limited for iOS Safari, which has no pointer).
|
|
1185
|
-
- **Cost:** a program that does not import a group pays nothing. In one that does, a call costs about 250 instantiations more (a 105-call bench: +2.9%)
|
|
1412
|
+
- **Cost:** a program that does not import a group pays nothing. In one that does, a call costs about 250 instantiations more (a 105-call bench: +2.9%). The 122 subpaths add 1 MB of declarations to `dist/` (the tarball: 303 kB for the first 96 groups, 384 kB with the Baseline years and the downstream groups; a downstream group re-exports the members it shares with its Baseline group, and a group the same as another is that group, so the editor's message may name the other).
|
|
1186
1413
|
|
|
1187
1414
|
`bun run groups` (in `packages/cssints`) regenerates `src/groups/<id>.ts` and `src/groups-data.ts` from web-features, @webref/css 6 (one file per level; 8 keeps the latest level of a series only) and BCD. Research and numbers: [research/groups.md](research/groups.md).
|
|
1188
1415
|
|