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.
Files changed (50) hide show
  1. package/README.md +88 -44
  2. package/dist/check.mjs +2 -2
  3. package/dist/{contrast-DhK69Xak.mjs → contrast-B49XCnVX.mjs} +2 -2
  4. package/dist/devtools-link-D2YxsJKZ.mjs +94 -0
  5. package/dist/devtools-link.d.mts +55 -0
  6. package/dist/devtools-link.mjs +2 -0
  7. package/dist/groups/baseline-2015/with-downstream.d.mts +81 -0
  8. package/dist/groups/baseline-2015.d.mts +291 -0
  9. package/dist/groups/baseline-2016/with-downstream.d.mts +86 -0
  10. package/dist/groups/baseline-2016.d.mts +310 -0
  11. package/dist/groups/baseline-2017/with-downstream.d.mts +94 -0
  12. package/dist/groups/baseline-2017.d.mts +365 -0
  13. package/dist/groups/baseline-2018/with-downstream.d.mts +94 -0
  14. package/dist/groups/baseline-2018.d.mts +368 -0
  15. package/dist/groups/baseline-2019/with-downstream.d.mts +97 -0
  16. package/dist/groups/baseline-2019.d.mts +375 -0
  17. package/dist/groups/baseline-2020/with-downstream.d.mts +112 -0
  18. package/dist/groups/baseline-2020.d.mts +480 -0
  19. package/dist/groups/baseline-2021/with-downstream.d.mts +125 -0
  20. package/dist/groups/baseline-2021.d.mts +536 -0
  21. package/dist/groups/baseline-2022/with-downstream.d.mts +126 -0
  22. package/dist/groups/baseline-2022.d.mts +562 -0
  23. package/dist/groups/baseline-2023/with-downstream.d.mts +120 -0
  24. package/dist/groups/baseline-2023.d.mts +606 -0
  25. package/dist/groups/baseline-2024/with-downstream.d.mts +627 -0
  26. package/dist/groups/baseline-2024.d.mts +632 -0
  27. package/dist/groups/baseline-2025/with-downstream.d.mts +628 -0
  28. package/dist/groups/baseline-2025.d.mts +640 -0
  29. package/dist/groups/baseline-2026/with-downstream.d.mts +3 -0
  30. package/dist/groups/baseline-2026.d.mts +3 -0
  31. package/dist/groups/newly-available/with-downstream.d.mts +632 -0
  32. package/dist/groups/widely-available/with-downstream.d.mts +121 -0
  33. package/dist/index.mjs +22 -7
  34. package/dist/instrument-BqIZpbIU.mjs +3956 -0
  35. package/dist/{lowered-D2KusPLp.mjs → lowered-aJVapQT6.mjs} +1 -1
  36. package/dist/plugin-types.d.mts +7 -0
  37. package/dist/plugin.d.mts +2 -2
  38. package/dist/plugin.mjs +8 -1
  39. package/dist/{registry-CTumHB8p.mjs → registry-BUSEO32-.mjs} +7 -1
  40. package/dist/{scope-ZVhUxQH1.d.mts → scope-INuJD4RT.d.mts} +8 -1
  41. package/dist/{sheet-BKsMTa0c.mjs → sheet-BeHdq4xt.mjs} +1 -1
  42. package/dist/surface.d.mts +1 -1
  43. package/dist/{theme-BsP2E4LB.mjs → theme-SlQ73rFL.mjs} +3 -3
  44. package/dist/tui.mjs +4 -4
  45. package/dist/vite.d.mts +3 -1
  46. package/dist/vite.mjs +92 -39
  47. package/docs/API.md +458 -231
  48. package/package.json +6 -2
  49. package/skills/cssints-plugin/SKILL.md +2 -0
  50. 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
- <div className={cn(css.display("flex"), alignItems("center"), justifyContent("center"), prop && p(4))}>Hello, world!</div>
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
- <div className={cn(css.flex().alignItems("center").justifyContent("center"), prop && css.p(4))}>Hello, world!</div>
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 = () => <div className={__cx([], "_1s47490 _1ob5z5m _cn8dee", prop && "_lwugp7")}>Hello, world!</div>;
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
- ._lwugp7 { padding: 1rem; }
59
- ._1ob5z5m { align-items: center; }
60
- ._1s47490 { display: flex; }
61
- ._cn8dee { justify-content: center; }
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 = () => <div className={css.cn(css.flex().alignItems("center").justifyContent("center"), css.p(4))}>Hello, world!</div>;
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
- css.p(2),
103
- md(css.pl("8ch"), css.pt(4)),
104
- css.hover(css.bg("#eee")),
105
- md(css.hover(css.bg("#ddd"))),
106
- prop && lg(css.p(8)),
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
- ._1t8gqy5 { padding: 0.5rem; }
156
+ ._1t8gqy5 {
157
+ padding: 0.5rem;
158
+ }
125
159
  }
126
160
  @layer _.b {
127
- ._16f8jrr:hover { background-color: #eee; }
128
- @media (width >= 40rem) {
129
- ._1q848uz { padding-top: 1rem; }
130
- ._t70zhd { padding-left: 8ch; }
131
- }
132
- @media (width >= 60rem) {
133
- ._1khcbr2 { padding: 2rem; }
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
- @media (width >= 40rem) {
138
- ._ronnd2:hover { background-color: #ddd; }
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
- css.color("gray"),
163
- css.attr("aria-current", "page")(css.color("blue")),
164
- css.within(css.hover)(css.textDecoration("underline")),
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(css.group("card"), css.within(css.focusVisible, "card")(css.outline("2px solid")));
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
- *, *::before, *::after{box-sizing:border-box}
185
- body{margin:8px}
186
- :root{color-scheme:light}@media (prefers-color-scheme: dark){:root{color-scheme:dark}}
187
- a[href^='http'] > b{color:red}
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
- :where(._g:hover) ._72037{text-decoration:underline}
191
- ._fc1pxh[aria-current="page"]{color:blue}
192
- ._pl3vkk[href$=".pdf"]{opacity:0.8}
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
- syntax: "<color>";
219
- inherits: true;
220
- initial-value: blue;
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
- color: {
238
- $type: "color",
239
- bg: { $value: "#ffffff" },
240
- fg: { $value: "#111827" },
241
- accent: { $value: "oklch(62% 0.19 259)" },
242
- link: { $value: "{color.accent}" },
243
- },
244
- space: { $type: "dimension", md: { $value: "16px" } },
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 = () => <div className={css.cn(css.bg(t.color.bg), css.color(t.color.fg), css.p(t.space.md))} />;
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/sky/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 sky 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.
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: sky built for `chrome 154` is byte-identical to a build without the runtime.
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; sky'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/sky/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 sky templates and random inputs.
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(t, '[data-mode="hero"]', { color: { bg: "#123" } }, { layer: "author", scope: "article.post" });
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
- color: {
298
- $type: "color",
299
- accent: { $value: "#2563eb" },
300
- hover: { $value: "color-mix(in oklab, {color.accent}, black 15%)" },
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
- hue: { $type: "number", accent: { $value: 345 } },
313
- radius: { $type: "dimension", sm: { $value: "4px" }, md: { $value: "8px" } },
314
- color: { $type: "color", accent: { $value: "oklch(0.6 0.2 {hue.accent})" } },
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", { hue: { accent: 12 }, color: { accent: "oklch(0.7 0.1 {hue.accent})" } });
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
- name: "cssints/dtcg",
335
- format: ({ dictionary }) => {
336
- const out = {};
337
- for (const t of dictionary.allTokens) {
338
- let group = out;
339
- for (const key of t.path.slice(0, -1)) group = group[key] ??= {};
340
- const type = t.$type ?? t.type;
341
- group[t.path.at(-1)] = { $type: DTCG[type] ?? type, $value: t.$value ?? t.value };
342
- }
343
- return JSON.stringify(out, null, 2);
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
- source: ["tokens/**/*.json"],
348
- preprocessors: ["tokens-studio"],
349
- platforms: { cssints: { transformGroup: "tokens-studio", files: [{ destination: "tokens.json", format: "cssints/dtcg" }] } },
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
- - **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`.
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/sky `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.
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 { syntax: "<color>"; inherits: true; initial-value: oklch(62% 0.19 259); }
362
- @property --color-bg { syntax: "<color>"; inherits: true; initial-value: #ffffff; }
363
- @property --color-link { syntax: "<color>"; inherits: true; initial-value: oklch(62% 0.19 259); }
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
- :root { --color-link: var(--color-accent); }
366
- @media (prefers-color-scheme: dark) {
367
- :root { --color-bg: #0b0b0f; --color-fg: #f3f4f6; }
368
- }
369
- @container (width < 30rem) {
370
- :where(*) { --color-bg: #eee; }
371
- }
372
- .light { --color-bg: #fff; }
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 { animation, cn, hover, keyframes, margin, opacity, positionTry, positionTryFallbacks, top, transform } from "cssints" with { type: "cssints" };
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({ from: transform("rotate(0deg)"), "50%": cn(transform("rotate(8deg)"), opacity(0.5)), to: transform("rotate(0deg)") });
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
- @keyframes kf-0e6af964{from{opacity:0}to{opacity:1}}
431
- @position-try --pt-23965b44{top:anchor(bottom);margin:0.5rem}
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
- ._44ecsn{animation:kf-0e6af964 200ms ease-out}
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
- cn(p(2), bg("white"), props.on && p(4), hover(bg("#eee")), props.class);
448
- export const row = (kind: "a" | "b", items: boolean[]) => cn(p(1), kind === "a" ? p(2) : bg("red"), items.map((x) => x && p(3)));
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) => __cx([["_lwugp7", "_1t8gqy5"]], "_1t8gqy5 _1ebepn0 _16f8jrr", props.on && "_lwugp7", props.class);
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
- base: cn(p(2), fontSize("1rem")),
471
- variants: {
472
- size: { sm: p(1), lg: cn(p(4), fontSize("1.25rem")) },
473
- disabled: { true: opacity(0.5), false: opacity(1) },
474
- },
475
- defaultVariants: { size: "sm", disabled: false },
476
- compoundVariants: [{ size: "lg", disabled: true, class: opacity(0.3) }],
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 { cn, display, overflow, WebkitBoxOrient, WebkitLineClamp } from "cssints" with { type: "cssints" };
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
- display("-webkit-box"),
534
- WebkitBoxOrient("vertical"),
535
- WebkitLineClamp(2),
536
- overflow("hidden"),
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
- // a local syntax name: `<width>` in a grammar of this scope (it does not leak into mdn's names)
553
- width: "<length [0,∞]> | thin | thick",
554
- // a kernel: global CSS, once. `&` is the class the engine mints for it, `--_w` is the custom property `--ring-w`
555
- "#core": {
556
- css: "& { outline: var(--_w) solid var(--_c); outline-offset: 2px }",
557
- atProperty: '@property --_w { syntax: "<length>"; inherits: false; initial-value: 2px; }',
558
- requires: ["css.at-rules.property"],
559
- },
560
- "#noOutline": { props: ["outline", /^outline-/], message: "ring() sets the outline: use its methods" },
561
- // a chain member, like flex(): a call gives the marker and the kernel, each method emits custom properties
562
- ring: {
563
- uses: ["core", "noOutline"],
564
- chain: {
565
- width: { args: "<width>", spacing: true, emit: (v) => [["--_w", v]] },
566
- color: { args: "<color>", emit: (v) => [["--_c", v]] },
567
- },
568
- },
569
- // a property of your own, typed by its grammar (it is not in mdn-data); the key starts with `-`
570
- "-webkit-font-smoothing": "auto | none | antialiased | subpixel-antialiased",
571
- // a call member: its function is the entry of the same name in build()
572
- dot: { args: "small | large" },
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
- .build({ dot: (size) => ({ decls: [["--_size", size === "small" ? "4px" : "8px"]] }) })
575
- .export();
576
-
577
- export const card = cn(p(2), ring().width("thin").color("red"), webkitFontSmoothing("antialiased"), dot("small"));
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
- layers: { layers: ["app", "viewer"] },
602
- mode: { args: ["$tokens", "<custom-ident>", "$overrides"], uses: ["layers"] },
752
+ layers: { layers: ["app", "viewer"] },
753
+ mode: { args: ["$tokens", "<custom-ident>", "$overrides"], uses: ["layers"] },
603
754
  })
604
- .build({
605
- mode: (tokens, name, overrides) => ({
606
- themes: [{ tokens, overrides, selector: `[data-mode="${name}"]`, layer: "viewer" }],
607
- }),
608
- })
609
- .export();
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
- core: {
620
- css: (css) => [
621
- css.padding("var(--_p)"),
622
- css.border("1px solid"),
623
- css.hover(css.opacity(0.8)),
624
- css.media("(width >= 40rem)")(css.padding("1rem")),
625
- ],
626
- },
627
- chip: { uses: ["core"], chain: { padding: { args: "<length>", emit: (v) => [["--_p", v]] } } },
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
- core: {
646
- css: (css) => [
647
- css.vars({ "--_gap": "1rem", "--_fade": fade }),
648
- css.display("flex"),
649
- css.animation("var(--_fade) 200ms"),
650
- css.nest("& > * + *")(css.marginInlineStart("var(--_gap)")),
651
- ],
652
- },
653
- row: { uses: ["core"], chain: { gap: { args: "<length>", emit: (v) => [["--_gap", v]] } } },
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
- cn(
676
- container("scroll-state(stuck: top)")(...styles),
677
- ...(old ? [within(attr("data-stuck", "top", "~="), "stuck")(...styles)] : []),
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
- dependsOn(file);
692
- return color(readFileSync(file, "utf8").trim());
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
- color: { $type: "color", accent: { $value: "oklch(62% 0.19 259)" }, bg: { $value: "white" } },
710
- space: { $type: "dimension", sm: { $value: "8px" }, md: { $value: "16px" } },
711
- time: { $type: "duration", fast: { $value: "100ms" } },
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(css.linearGradient("to right", [t.color.accent, "25%"], "blue"));
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
- color: { $type: "color", accent: { $value: "red" } },
747
- space: { $type: "dimension", sm: { $value: "8px" } },
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
- tint: {
767
- args: "<color> <percentage>",
768
- value: ([color, share]) => new CSSColorValue(`color-mix(in oklab, ${color}, white ${share})`),
769
- },
770
- step: { args: "<number>", value: ([n]) => `${Number(n) * 4}px` },
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
- shade: fn({
786
- args: ["<color>", { "by?": "<percentage>", "dark?": arg<boolean>() }],
787
- label: "tint.shade",
788
- value: ([color, { by = "10%", dark }]) => `color-mix(in oklab, ${color}, ${dark ? "black" : "white"} ${by})`,
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(frame().radius("1.5rem").padding(4).border("1px"), css.bg("white"), css.borderColor("#9ca3af"));
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(frame().radius(radius).padding(2), css.media("(width >= 40rem)")(frame().padding(6)));
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 *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)`).
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
- provider: "iconify-json",
871
- collection: "mdi",
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 { display: inline-block; width: 1em; height: 1em; background-color: currentColor; mask: var(--icons-svg) no-repeat center / 100% 100%; }
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 { --icons-svg: url("data:image/svg+xml,%3Csvg …"); }
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 | Members | What it does |
900
- |---|---|---|
901
- | [`@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 |
902
- | [`@cssints/layout`](../packages/layout/README.md) | `stack()`, `cluster()`, `sidebar()`, `switcher()`, `cover()`, `autoGrid()` | Every Layout primitives as chain members |
903
- | [`@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 |
904
- | [`@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 |
905
- | [`@cssints/tokens`](../packages/tokens/README.md) | `/open-props`, `/radix`, `/flexoki`, `/reasonable-colors` | DTCG data for `createTokens` |
906
- | [`@cssints/gradient`](../packages/gradient/README.md) | `gradient(kind, direction, from, to, easing?, stops?)` | Eased colour stops mixed in oklab at build time |
907
- | [`@cssints/motion`](../packages/motion/README.md) | `motion().preset().duration().delay().easing()` | Animation presets over `keyframes()`, with reduced motion |
908
- | [`@cssints/squircle`](../packages/squircle/README.md) | `squircle().radius()` | `corner-shape: squircle`, else a CSS `mask` of the same shape |
909
- | [`@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) |
910
- | [`@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) |
911
- | [`@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) |
912
- | [`@cssints/masonry`](../packages/masonry/README.md) | `masonry().columns().gap()` | `display: grid-lanes`, else CSS `columns` (top to bottom per column) |
913
- | [`@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 |
914
- | [`@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 |
915
- | [`@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 |
916
- | [`@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 |
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`), sends the prompt, 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):
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/sky 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.
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({ color: { $type: "color", bg: { $value: "#1e1e2e" }, accent: { $value: "#89b4fa" } } });
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
- css.border("rounded", "#585b70"),
1094
- css.p(0, 1),
1095
- css.width(12),
1096
- css.bg(t.color.bg),
1097
- css.cursor("pointer"),
1098
- css.hover(css.borderColor(t.color.accent)),
1099
- css.active(css.borderStyle("double")),
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 = { border: true, borderStyle: "rounded", borderColor: "#585b70", paddingTop: 0, paddingRight: 1, paddingBottom: 0, paddingLeft: 1, width: 12, backgroundColor: "#1e1e2e", cursor: "pointer", ":hover": { borderColor: "#89b4fa" }, ":active": { border: true, borderStyle: "double" }, themes: { light: { backgroundColor: "#eff1f5", ":hover": { borderColor: "#1e66f5" } } }, vars: { backgroundColor: "color-bg", ":hover": { borderColor: "color-accent" } } };
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)}><text>Save</text></box>; // Solid or React: the spread is a ref that calls attach
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(t, {
1147
- color: {
1148
- bg: { surface: true },
1149
- card: { surface: true, on: [t.color.bg], contrast: 1.1 },
1150
- text: { hue: "fg", on: [t.color.bg, t.color.card], contrast: 7 },
1151
- accent: { hue: "blue", on: [t.color.bg, t.color.card], contrast: 3 },
1152
- },
1153
- }, { themes: { light: "light" } });
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/sky-tui's styles on `@cssints/tui/bomb` in a real terminal: Ghostty's emulator compiled to WebAssembly (ghostty-web) draws sky'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.
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 sky-tui's bundle).
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 = [display("grid"), color("rebeccapurple"), media("(width >= 40rem)")(color("black"))];
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%), and the 96 declaration files add 340 KB to `dist/` (about 240 KB of `src/groups`, the tarball gains about 100 KB gzip).
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