cssints 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -53
- package/dist/check.mjs +2 -2
- package/dist/{contrast-BUKVao74.mjs → contrast-B49XCnVX.mjs} +2 -2
- package/dist/devtools-link-D2YxsJKZ.mjs +94 -0
- package/dist/devtools-link.d.mts +55 -0
- package/dist/devtools-link.mjs +2 -0
- package/dist/group-common-9zpBxfUF.d.mts +1 -0
- package/dist/group-types-9ttphdku.d.mts +55 -0
- package/dist/groups/baseline-2015/with-downstream.d.mts +81 -0
- package/dist/groups/baseline-2015.d.mts +291 -0
- package/dist/groups/baseline-2016/with-downstream.d.mts +86 -0
- package/dist/groups/baseline-2016.d.mts +310 -0
- package/dist/groups/baseline-2017/with-downstream.d.mts +94 -0
- package/dist/groups/baseline-2017.d.mts +365 -0
- package/dist/groups/baseline-2018/with-downstream.d.mts +94 -0
- package/dist/groups/baseline-2018.d.mts +368 -0
- package/dist/groups/baseline-2019/with-downstream.d.mts +97 -0
- package/dist/groups/baseline-2019.d.mts +375 -0
- package/dist/groups/baseline-2020/with-downstream.d.mts +112 -0
- package/dist/groups/baseline-2020.d.mts +480 -0
- package/dist/groups/baseline-2021/with-downstream.d.mts +125 -0
- package/dist/groups/baseline-2021.d.mts +536 -0
- package/dist/groups/baseline-2022/with-downstream.d.mts +126 -0
- package/dist/groups/baseline-2022.d.mts +562 -0
- package/dist/groups/baseline-2023/with-downstream.d.mts +120 -0
- package/dist/groups/baseline-2023.d.mts +606 -0
- package/dist/groups/baseline-2024/with-downstream.d.mts +627 -0
- package/dist/groups/baseline-2024.d.mts +632 -0
- package/dist/groups/baseline-2025/with-downstream.d.mts +628 -0
- package/dist/groups/baseline-2025.d.mts +640 -0
- package/dist/groups/baseline-2026/with-downstream.d.mts +3 -0
- package/dist/groups/baseline-2026.d.mts +3 -0
- package/dist/groups/compositing-2.d.mts +20 -0
- package/dist/groups/css-align-3.d.mts +29 -0
- package/dist/groups/css-anchor-position-1.d.mts +49 -0
- package/dist/groups/css-anchor-position-2.d.mts +50 -0
- package/dist/groups/css-animations-1.d.mts +28 -0
- package/dist/groups/css-animations-2.d.mts +29 -0
- package/dist/groups/css-backgrounds-3.d.mts +27 -0
- package/dist/groups/css-backgrounds-4.d.mts +29 -0
- package/dist/groups/css-borders-4.d.mts +95 -0
- package/dist/groups/css-box-4.d.mts +38 -0
- package/dist/groups/css-break-4.d.mts +23 -0
- package/dist/groups/css-cascade-5.d.mts +18 -0
- package/dist/groups/css-cascade-6.d.mts +18 -0
- package/dist/groups/css-color-4.d.mts +29 -0
- package/dist/groups/css-color-5.d.mts +32 -0
- package/dist/groups/css-color-6.d.mts +32 -0
- package/dist/groups/css-color-adjust-1.d.mts +20 -0
- package/dist/groups/css-color-hdr-1.d.mts +19 -0
- package/dist/groups/css-conditional-3.d.mts +17 -0
- package/dist/groups/css-conditional-4.d.mts +17 -0
- package/dist/groups/css-conditional-5.d.mts +22 -0
- package/dist/groups/css-contain-2.d.mts +19 -0
- package/dist/groups/css-content-3.d.mts +23 -0
- package/dist/groups/css-counter-styles-3.d.mts +16 -0
- package/dist/groups/css-display-4.d.mts +22 -0
- package/dist/groups/css-easing-2.d.mts +18 -0
- package/dist/groups/css-env-1.d.mts +16 -0
- package/dist/groups/css-flexbox-1.d.mts +24 -0
- package/dist/groups/css-fonts-4.d.mts +46 -0
- package/dist/groups/css-fonts-5.d.mts +44 -0
- package/dist/groups/css-forms-1.d.mts +19 -0
- package/dist/groups/css-gaps-1.d.mts +21 -0
- package/dist/groups/css-gcpm-3.d.mts +20 -0
- package/dist/groups/css-gcpm-4.d.mts +20 -0
- package/dist/groups/css-grid-2.d.mts +34 -0
- package/dist/groups/css-grid-3.d.mts +34 -0
- package/dist/groups/css-images-3.d.mts +25 -0
- package/dist/groups/css-images-4.d.mts +32 -0
- package/dist/groups/css-images-5.d.mts +33 -0
- package/dist/groups/css-inline-3.d.mts +28 -0
- package/dist/groups/css-lists-3.d.mts +26 -0
- package/dist/groups/css-logical-1.d.mts +35 -0
- package/dist/groups/css-masking-1.d.mts +37 -0
- package/dist/groups/css-multicol-2.d.mts +24 -0
- package/dist/groups/css-overflow-3.d.mts +28 -0
- package/dist/groups/css-overflow-4.d.mts +29 -0
- package/dist/groups/css-overflow-5.d.mts +31 -0
- package/dist/groups/css-overscroll-1.d.mts +22 -0
- package/dist/groups/css-page-3.d.mts +18 -0
- package/dist/groups/css-page-floats-3.d.mts +19 -0
- package/dist/groups/css-position-3.d.mts +29 -0
- package/dist/groups/css-position-4.d.mts +30 -0
- package/dist/groups/css-rhythm-1.d.mts +18 -0
- package/dist/groups/css-round-display-1.d.mts +16 -0
- package/dist/groups/css-ruby-1.d.mts +21 -0
- package/dist/groups/css-scroll-anchoring-1.d.mts +18 -0
- package/dist/groups/css-scroll-snap-1.d.mts +42 -0
- package/dist/groups/css-scroll-snap-2.d.mts +43 -0
- package/dist/groups/css-scrollbars-1.d.mts +19 -0
- package/dist/groups/css-shapes-1.d.mts +27 -0
- package/dist/groups/css-shapes-2.d.mts +27 -0
- package/dist/groups/css-size-adjust-1.d.mts +18 -0
- package/dist/groups/css-sizing-3.d.mts +34 -0
- package/dist/groups/css-sizing-4.d.mts +33 -0
- package/dist/groups/css-speech-1.d.mts +18 -0
- package/dist/groups/css-tables-3.d.mts +22 -0
- package/dist/groups/css-text-3.d.mts +36 -0
- package/dist/groups/css-text-4.d.mts +40 -0
- package/dist/groups/css-text-decor-3.d.mts +30 -0
- package/dist/groups/css-text-decor-4.d.mts +31 -0
- package/dist/groups/css-transforms-1.d.mts +31 -0
- package/dist/groups/css-transforms-2.d.mts +48 -0
- package/dist/groups/css-transitions-1.d.mts +22 -0
- package/dist/groups/css-transitions-2.d.mts +23 -0
- package/dist/groups/css-ui-4.d.mts +35 -0
- package/dist/groups/css-values-4.d.mts +38 -0
- package/dist/groups/css-values-5.d.mts +43 -0
- package/dist/groups/css-variables-2.d.mts +6 -0
- package/dist/groups/css-view-transitions-2.d.mts +20 -0
- package/dist/groups/css-viewport.d.mts +18 -0
- package/dist/groups/css-will-change-1.d.mts +18 -0
- package/dist/groups/css-writing-modes-4.d.mts +22 -0
- package/dist/groups/css2.d.mts +19 -0
- package/dist/groups/fill-stroke-3.d.mts +27 -0
- package/dist/groups/filter-effects-1.d.mts +32 -0
- package/dist/groups/filter-effects-2.d.mts +33 -0
- package/dist/groups/limited-availability.d.mts +2 -0
- package/dist/groups/mediaqueries-4.d.mts +16 -0
- package/dist/groups/mediaqueries-5.d.mts +16 -0
- package/dist/groups/motion-1.d.mts +24 -0
- package/dist/groups/newly-available/with-downstream.d.mts +632 -0
- package/dist/groups/newly-available.d.mts +631 -0
- package/dist/groups/not-discouraged.d.mts +727 -0
- package/dist/groups/scroll-animations-1.d.mts +30 -0
- package/dist/groups/selectors-4.d.mts +54 -0
- package/dist/groups/selectors-5.d.mts +54 -0
- package/dist/groups/widely-available/with-downstream.d.mts +121 -0
- package/dist/groups/widely-available.d.mts +616 -0
- package/dist/index.d.mts +19 -5
- package/dist/index.mjs +166 -14
- package/dist/instrument-BqIZpbIU.mjs +3956 -0
- package/dist/{lowered-CeoXbm0P.mjs → lowered-aJVapQT6.mjs} +1 -1
- package/dist/{node-CdhAGmF3.d.mts → node-OJ_w_Usj.d.mts} +4 -0
- package/dist/plugin-types.d.mts +8 -1
- package/dist/plugin.d.mts +2 -2
- package/dist/plugin.mjs +8 -1
- package/dist/{registry-n9JCX4pl.mjs → registry-BUSEO32-.mjs} +8 -1
- package/dist/runtime-Dt4Tm0Q7.d.mts +19 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.mjs +6 -2
- package/dist/{scope-oWEaWE9n.d.mts → scope-INuJD4RT.d.mts} +18 -5
- package/dist/{sheet-CO7a-mdA.mjs → sheet-BeHdq4xt.mjs} +2 -1
- package/dist/surface.d.mts +1 -1
- package/dist/{theme-BGEbFAYh.mjs → theme-SlQ73rFL.mjs} +31 -8
- package/dist/{theme-types-DHdtTZhz.d.mts → theme-types-Bqv0Ggz_.d.mts} +11 -6
- package/dist/tui-types.d.mts +1 -1
- package/dist/tui.d.mts +1 -8
- package/dist/tui.mjs +35 -7
- package/dist/types.d.mts +1 -1
- package/dist/vite.d.mts +20 -2
- package/dist/vite.mjs +90 -41
- package/docs/API.md +519 -251
- package/package.json +10 -2
- package/skills/cssints/SKILL.md +12 -9
- package/skills/cssints/references/errors.md +8 -7
- package/skills/cssints-plugin/SKILL.md +3 -1
- package/dist/instrument-Bdtk2INz.mjs +0 -172
- package/dist/lib-2OEIfPAE.d.mts +0 -9
- package/dist/runtime-CTvuM-Hm.d.mts +0 -5
package/docs/API.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# CSS in TS
|
|
2
2
|
|
|
3
|
-
The 0.
|
|
3
|
+
The 0.x API, **alpha**: it can still change between minor versions. Setup (the Vite plugin, the stylesheet import) is in the [README](../README.md). Decisions and findings are in [wayfinder.md](wayfinder.md).
|
|
4
4
|
|
|
5
5
|
Every `ts` block here is type-checked against the workspace `cssints` types by `packages/cssints/test/docs.mjs` (`bun run test`). A line that ends in `// error` must fail to type-check. Blocks fenced `ts skip` (output) are not checked. A few blocks show a build-time error that the types cannot see; their comments give the message.
|
|
6
6
|
|
|
7
7
|
### Authoring surface (decided 2026-09-29, cssints-5dbi)
|
|
8
8
|
|
|
9
|
-
- **Import.** `with { type: "cssints" }` marks code that runs at build time: cssints itself, plugins such as icons, and your own helper modules. The plugin finds call sites by this attribute.
|
|
9
|
+
- **Import.** `with { type: "cssints" }` marks code that runs at build time: cssints itself, plugins such as icons, and your own helper modules. The plugin finds call sites by this attribute. Nothing is imported without it: the few runtime lines a module needs (`__cx` for a `cn()` with runtime arguments, `__cv` for a `cv()`) are imported by the plugin. Using a build-time member without the attribute is a build error.
|
|
10
10
|
- **Names.** Every mdn property as camelCase (`paddingLeft`, `alignItems`, `justifyContent`), plus a short set: `p pt pr pb pl`, `m mt mr mb ml`, `bg`. Everything is available both as named exports and through the namespace (`css.p`). There is no `items` or `justify`: they were ambiguous (`align-items` or `align-content`, `justify-content` or `justify-items`).
|
|
11
11
|
- **Numbers.** A bare number is a spacing step of 0.25rem in `padding*`, `margin*`, `gap*` and `inset*` (`p(4)` is `1rem`). In any other property it is a CSS `<number>`: `opacity(0.5)`, `zIndex(3)`.
|
|
12
12
|
- **Values.** A property takes one or more parts joined by a space. Each part is a string or a Typed OM value: `border("1px solid", accent)`, `padding(CSS.rem(1), CSS.rem(2))`, `padding("1rem 2rem")`. Parts are checked against the mdn grammar in types and again at build time. `CSS.px`, `rem`, `em`, `percent`, `number`, `CSSUnitValue` and `CSSColorValue` come from `cssints`, imported with the attribute.
|
|
13
13
|
- **Editor.** A bad part is marked on that part: `padding("2px", "red")` puts the error on `"red"`, with the message of the whole value. While a string is not a value yet, the editor offers the keywords of the property that the word under the cursor begins (`display("fl…")` gives `flex`, `flow`, `flow-root`; `border("1px", "so…")` gives `solid`), and all of them in an empty part (`border("1px", "")`; an empty part is an error). The offer is the keywords of the whole grammar, not only those that fit at that point, it stops once the string is valid, and functions such as `rgb(` are not offered.
|
|
14
14
|
- **Math.** `calc()`, `min()`, `max()`, `clamp()` and the other CSS Values 4 math functions are accepted wherever their type is, as strings (`width("calc(100% - 2rem)")`, checked by the build: `calc(1px + 2s)` is an error) and as calls that take tokens (`calc(t.space.md, "*", 2)`, see "Functions").
|
|
15
|
-
- **Layout contexts.** `flex()` and `grid()` without arguments set `display` and chain only their container properties. Names drop only the context prefix: `flex().direction("column").wrap("wrap").alignItems("center").justifyContent("space-between")`, `grid().templateColumns("1fr 2fr").gap(4)`. Plain styles do not chain; merge them with `cn(...)
|
|
15
|
+
- **Layout contexts.** `flex()` and `grid()` without arguments set `display` and chain only their container properties. Names drop only the context prefix: `flex().direction("column").wrap("wrap").alignItems("center").justifyContent("space-between")`, `grid().templateColumns("1fr 2fr").gap(4)`. Plain styles do not chain; merge them with `cn(...)`. Item properties (`flexGrow`, `alignSelf`, `gridColumn`, `order`) are plain functions.
|
|
16
16
|
- **`flex(...parts)` and `grid(...parts)` are the shorthand properties.** With at least one argument the call is the `flex` (or `grid`) property, checked by its grammar like any member: `flex("1")`, `flex(1)` and `flex("1 1 0")` are `flex: 1`, `flex: 1` and `flex: 1 1 0`, `grid("auto / 1fr 1fr")` is `grid: auto / 1fr 1fr`. A bare number is a `<number>` here, not a spacing step. The result is a plain style: it sets no `display` and does not chain. The types tell the two forms apart by overload.
|
|
17
17
|
- **`container` is the condition.** `container("sidebar (width < 30rem)")` is a query; the `container` shorthand property is written with its longhands `containerName` and `containerType`.
|
|
18
|
-
- **Arguments are build-time values.** The plugin runs your module at build time and replaces each call with a class string. A literal, a `const`, a token or a value from another attributed module works. A value that exists only at runtime (a prop, state) does not: `p(n)` with `n` a parameter is a build error (`css arguments must be known at build time: n is not defined where the call is evaluated`) that points at the call, also when the local is named like a global (`open`, `name`) or like a module binding. The types do not catch it: a helper module may take parameters (below), so a `string` or `number` is a fine argument there. Choose between styles with `
|
|
18
|
+
- **Arguments are build-time values.** The plugin runs your module at build time and replaces each call with a class string. A literal, a `const`, a token or a value from another attributed module works. A value that exists only at runtime (a prop, state) does not: `p(n)` with `n` a parameter is a build error (`css arguments must be known at build time: n is not defined where the call is evaluated`) that points at the call, also when the local is named like a global (`open`, `name`) or like a module binding. The types do not catch it: a helper module may take parameters (below), so a `string` or `number` is a fine argument there. Choose between styles with `cn(base, on && p(4))` (see "Runtime: `cn`, `cv`"), a `cv` variant, or a token set in a `style` prop. A helper module imported with the attribute may take parameters, because each call of it is a site with its own arguments: `stack(4)` below is a build-time call in the file that makes it.
|
|
19
19
|
- **Top-level code runs at build time.** A call the run does not reach (`false && p(4)`, a branch not taken, a ternary arm) still gets its class string: it is evaluated after the module has loaded, like a call inside a function, so its arguments may use the bindings of the module but not the locals of the branch. A `cv(...)` at the top of a module works too: during the build it is the same function the browser gets, so `button({ size: "lg" })` at the top of a module gives its classes.
|
|
20
20
|
|
|
21
21
|
```ts
|
|
@@ -30,49 +30,75 @@ export const stack = (gap: number) => css.flex().direction("column").gap(gap);
|
|
|
30
30
|
### Basic usage
|
|
31
31
|
|
|
32
32
|
```tsx
|
|
33
|
-
import {
|
|
34
|
-
import { alignItems, justifyContent, p } from "cssints" with { type: "cssints" };
|
|
33
|
+
import { alignItems, cn, justifyContent, p } from "cssints" with { type: "cssints" };
|
|
35
34
|
import * as css from "cssints" with { type: "cssints" };
|
|
36
35
|
|
|
37
36
|
export const A = () => (
|
|
38
|
-
|
|
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>
|
|
39
47
|
);
|
|
40
48
|
// or with a layout context
|
|
41
49
|
export const B = () => (
|
|
42
|
-
|
|
50
|
+
<div className={cn(css.flex().alignItems("center").justifyContent("center"), prop && css.p(4))}>
|
|
51
|
+
Hello, world!
|
|
52
|
+
</div>
|
|
43
53
|
);
|
|
44
54
|
```
|
|
45
55
|
|
|
46
56
|
Output for `A`:
|
|
47
57
|
|
|
48
58
|
```tsx skip
|
|
49
|
-
import {
|
|
59
|
+
import { _cx as __cx } from "cssints";
|
|
50
60
|
|
|
51
|
-
export const A = () =>
|
|
61
|
+
export const A = () => (
|
|
62
|
+
<div className={__cx([], "_1s47490 _1ob5z5m _cn8dee", prop && "_lwugp7")}>Hello, world!</div>
|
|
63
|
+
);
|
|
52
64
|
```
|
|
53
65
|
|
|
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.
|
|
67
|
+
|
|
54
68
|
```css
|
|
55
69
|
@layer _.k, _.a, _.b, _.c, _.d, _.e;
|
|
56
70
|
@layer _.a {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
}
|
|
61
83
|
}
|
|
62
84
|
```
|
|
63
85
|
|
|
64
86
|
- **The class names** are a hash of the declaration (and its conditions), so the same declaration gives the same class in every file.
|
|
65
87
|
- **One sheet.** All rules are in `virtual:cssints.css`, each once, sorted by layer, then condition, then shorthand before longhand (`border` before `border-top`, counted from Chrome's own expansion of each shorthand), then class name. So a longhand wins over a shorthand that contains it, whatever the order of the arguments: `cn(pl(2), p(4))` keeps `padding-left: 0.5rem`.
|
|
66
|
-
- **`cn()` keeps the later of the same property.** `cn(p(4), p(2))` is `p(2)`: the earlier declaration is dropped at build time (also for an alias: `wordWrap` is `overflowWrap`). Under different conditions both stay, because they are in different layers: `cn(md(p(6)), p(2))`. Two properties that overlap without one containing the other are a build error, because the sheet cannot order them by the arguments: `cn(borderTop("1px solid red"), borderColor("blue"))` both set `border-top-color`; so do a physical and a logical side (`marginLeft` and `marginInline`, `width` and `inlineSize`), since which physical side a logical one is depends on the writing mode. A logical property inside a physical shorthand that sets every side is contained, not overlapping: `cn(border(...), borderInline(...))` is fine, and `borderInline` wins.
|
|
88
|
+
- **`cn()` keeps the later of the same property.** `cn(p(4), p(2))` is `p(2)`: the earlier declaration is dropped at build time (also for an alias: `wordWrap` is `overflowWrap`). Under different conditions both stay, because they are in different layers: `cn(md(p(6)), p(2))`. Two properties that overlap without one containing the other are a build error, because the sheet cannot order them by the arguments: `cn(borderTop("1px solid red"), borderColor("blue"))` both set `border-top-color`; so do a physical and a logical side (`marginLeft` and `marginInline`, `width` and `inlineSize`), since which physical side a logical one is depends on the writing mode. A logical property inside a physical shorthand that sets every side is contained, not overlapping: `cn(border(...), borderInline(...))` is fine, and `borderInline` wins. A style in a runtime argument is compared with the static arguments at build time, and replaces the classes it overrides while it is active (see "Runtime: `cn`, `cv`").
|
|
67
89
|
- **A layout context** gives one string: `flex().alignItems("center")` is `"_1s47490 _1ob5z5m"`.
|
|
68
|
-
- **Also exported:** `css` (the namespace as a value) and `cn`, `cv`, `firstThatWorks`, `media`, `container`, `supports`, `attr`, `within`, `group`, `global`, `token`, `style`, `createTokens`, `createGlobalTheme`, `check`.
|
|
90
|
+
- **Also exported:** `css` (the namespace as a value) and `cn`, `cv`, `firstThatWorks`, `media`, `container`, `supports`, `attr`, `within`, `group`, `global`, `token`, `style`, `createTokens`, `createGlobalTheme`, `check`. Nothing is exported for the browser: the runtime is the plugin's.
|
|
69
91
|
|
|
70
92
|
### Without runtime
|
|
71
93
|
|
|
72
94
|
```tsx
|
|
73
95
|
import * as css from "cssints" with { type: "cssints" };
|
|
74
96
|
|
|
75
|
-
export const C = () =>
|
|
97
|
+
export const C = () => (
|
|
98
|
+
<div className={css.cn(css.flex().alignItems("center").justifyContent("center"), css.p(4))}>
|
|
99
|
+
Hello, world!
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
76
102
|
```
|
|
77
103
|
|
|
78
104
|
Output:
|
|
@@ -81,14 +107,13 @@ Output:
|
|
|
81
107
|
export const C = () => <div className="_1s47490 _1ob5z5m _cn8dee _lwugp7">Hello, world!</div>;
|
|
82
108
|
```
|
|
83
109
|
|
|
84
|
-
- **`cn(...)`** merges styles at build time into one class string.
|
|
110
|
+
- **`cn(...)`** merges styles at build time into one class string. With arguments that are all known at build time nothing is left at runtime and the runtime is not imported.
|
|
85
111
|
|
|
86
112
|
### Conditions and layers (decided 2026-09-29, cssints-kpq1)
|
|
87
113
|
|
|
88
114
|
Conditions are wrapper functions. A wrapper takes styles and returns them under its condition. Wrappers nest.
|
|
89
115
|
|
|
90
116
|
```ts
|
|
91
|
-
import { cx } from "cssints";
|
|
92
117
|
import * as css from "cssints" with { type: "cssints" };
|
|
93
118
|
|
|
94
119
|
const md = css.media("(width >= 40rem)");
|
|
@@ -98,14 +123,21 @@ const dark = css.media("(prefers-color-scheme: dark)");
|
|
|
98
123
|
const narrow = css.container("sidebar (width < 30rem)");
|
|
99
124
|
const grid = css.supports("(display: grid)");
|
|
100
125
|
|
|
101
|
-
export const styles =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
export const styles = css.cn(
|
|
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 = cx(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,29 +153,40 @@ Output of `styles`; each form in `forms` gives one more rule in `_.b` (`dark`, `
|
|
|
121
153
|
|
|
122
154
|
```css
|
|
123
155
|
@layer _.a {
|
|
124
|
-
|
|
156
|
+
._1t8gqy5 {
|
|
157
|
+
padding: 0.5rem;
|
|
158
|
+
}
|
|
125
159
|
}
|
|
126
160
|
@layer _.b {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
161
|
+
._16f8jrr:hover {
|
|
162
|
+
background-color: #eee;
|
|
163
|
+
}
|
|
164
|
+
@media (width >= 40rem) {
|
|
165
|
+
._1q848uz {
|
|
166
|
+
padding-top: 1rem;
|
|
167
|
+
}
|
|
168
|
+
._t70zhd {
|
|
169
|
+
padding-left: 8ch;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
@media (width >= 60rem) {
|
|
173
|
+
._1khcbr2 {
|
|
174
|
+
padding: 2rem;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
135
177
|
}
|
|
136
178
|
@layer _.c {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
179
|
+
@media (width >= 40rem) {
|
|
180
|
+
._ronnd2:hover {
|
|
181
|
+
background-color: #ddd;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
140
184
|
}
|
|
141
185
|
```
|
|
142
186
|
|
|
143
187
|
### Global rules, attributes and parent states (decided 2026-10-03, cssints-635k, cssints-eaob)
|
|
144
188
|
|
|
145
189
|
```ts
|
|
146
|
-
import { cx } from "cssints";
|
|
147
190
|
import * as css from "cssints" with { type: "cssints" };
|
|
148
191
|
|
|
149
192
|
const dark = css.media("(prefers-color-scheme: dark)");
|
|
@@ -159,13 +202,16 @@ css.global("a", css.hover(css.color("red"))); // global() takes media(), contain
|
|
|
159
202
|
|
|
160
203
|
// an attribute state, and the state of a marked ancestor
|
|
161
204
|
export const row = css.cn(css.group(), css.p(2));
|
|
162
|
-
export const link =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
205
|
+
export const link = css.cn(
|
|
206
|
+
css.color("gray"),
|
|
207
|
+
css.attr("aria-current", "page")(css.color("blue")),
|
|
208
|
+
css.within(css.hover)(css.textDecoration("underline")),
|
|
166
209
|
);
|
|
167
210
|
export const icon = css.attr("href", ".pdf", "$=")(css.opacity(0.8));
|
|
168
|
-
export const nested = css.cn(
|
|
211
|
+
export const nested = css.cn(
|
|
212
|
+
css.group("card"),
|
|
213
|
+
css.within(css.focusVisible, "card")(css.outline("2px solid")),
|
|
214
|
+
);
|
|
169
215
|
css.attr("a b"); // error: a name has no space
|
|
170
216
|
css.attr("a", "x", "!="); // error: not an operator
|
|
171
217
|
css.within(css.media("(width >= 40rem)")); // error: a media query is not a state
|
|
@@ -182,15 +228,36 @@ css.within(css.media("(width >= 40rem)")); // error: a media query is not a stat
|
|
|
182
228
|
|
|
183
229
|
```css
|
|
184
230
|
@layer _.k {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
}
|
|
189
250
|
}
|
|
190
251
|
@layer _.b {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
+
}
|
|
194
261
|
}
|
|
195
262
|
```
|
|
196
263
|
|
|
@@ -216,9 +283,9 @@ css.token.set(muted, "red"); // error: private tokens cannot be set
|
|
|
216
283
|
|
|
217
284
|
```css
|
|
218
285
|
@property --color-primary {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
286
|
+
syntax: "<color>";
|
|
287
|
+
inherits: true;
|
|
288
|
+
initial-value: blue;
|
|
222
289
|
}
|
|
223
290
|
```
|
|
224
291
|
|
|
@@ -235,14 +302,14 @@ Tokens come from an inline DTCG-shaped `const` object. Every leaf is a token (se
|
|
|
235
302
|
import * as css from "cssints" with { type: "cssints" };
|
|
236
303
|
|
|
237
304
|
const t = css.createTokens({
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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" } },
|
|
246
313
|
});
|
|
247
314
|
|
|
248
315
|
const dark = css.media("(prefers-color-scheme: dark)");
|
|
@@ -252,7 +319,9 @@ css.createGlobalTheme(t.color, css.container("(width < 30rem)"), { bg: "#eee" })
|
|
|
252
319
|
|
|
253
320
|
css.check.contrast(t.color.fg, t.color.bg, { min: 4.5 });
|
|
254
321
|
|
|
255
|
-
export const A = () =>
|
|
322
|
+
export const A = () => (
|
|
323
|
+
<div className={css.cn(css.bg(t.color.bg), css.color(t.color.fg), css.p(t.space.md))} />
|
|
324
|
+
);
|
|
256
325
|
css.createGlobalTheme(t, dark, { color: { nope: "red" } }); // error: no token at this path
|
|
257
326
|
css.createGlobalTheme(t, css.hover, { color: { bg: "red" } }); // error: a pseudo-class is not a theme condition
|
|
258
327
|
```
|
|
@@ -264,12 +333,12 @@ css.createGlobalTheme(t, css.hover, { color: { bg: "red" } }); // error: a pseud
|
|
|
264
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).
|
|
265
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.
|
|
266
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.
|
|
267
|
-
- Checked in Chromium 109 (`apps/
|
|
268
|
-
- **Derived tokens in older browsers: a runtime** (cssints-i9xo). Where a target cannot parse a derived colour (`oklch(0.637 0.203 var(--hue-accent))`, `lab(from color(from var(--color-accent) srgb-linear …) …)`), the slider would do nothing. The engine then ships a small browser module, `cssints/derived`, that computes the live values in JavaScript. Tokens are not members, so this is not a plugin you import: the engine registers the runtime itself when a derived colour needs it, and delivers it the way it delivers a plugin's runtime (`virtual:cssints/runtime/derived`, empty, `export {};`, unless a browserslist target lacks a feature the literals need and has `@property`; the same `needed()` decision). The client modules that use such a token import it. With targets that parse every value nothing is written or shipped:
|
|
336
|
+
- Checked in Chromium 109 (`apps/csssky/test/old-browser.mjs`: Mu pink in real colours, light and dark, before the runtime of the next bullet moves anything) and Chrome 154 (the colours stay `oklch()` and follow the slider). The cost on csssky is one more `:root` declaration per token and one `@supports` rule per derived value: 27.97 kB to 29.58 kB of CSS with the default targets.
|
|
337
|
+
- **Derived tokens in older browsers: a runtime** (cssints-i9xo). Where a target cannot parse a derived colour (`oklch(0.637 0.203 var(--hue-accent))`, `lab(from color(from var(--color-accent) srgb-linear …) …)`), the slider would do nothing. The engine then ships a small browser module, `cssints/derived`, that computes the live values in JavaScript. Tokens are not members, so this is not a plugin you import: the engine registers the runtime itself when a derived colour needs it, and delivers it the way it delivers a plugin's runtime (`virtual:cssints/runtime/derived`, empty, `export {};`, unless a browserslist target lacks a feature the literals need and has `@property`; the same `needed()` decision). The client modules that use such a token import it. With targets that parse every value nothing is written or shipped: csssky built for `chrome 154` is byte-identical to a build without the runtime.
|
|
269
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.
|
|
270
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.
|
|
271
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).
|
|
272
|
-
- **Cost.** `cssints/derived` is 7.2 kB minified, 3.1 kB gzipped;
|
|
341
|
+
- **Cost.** `cssints/derived` is 7.2 kB minified, 3.1 kB gzipped; csssky's CSS grows by the data (29.6 kB to 31.4 kB with the default targets, about 0.6 kB gzipped). The colours of the default theme take 10 µs of JavaScript per input; in Chromium 109 a slider input costs 1.2 ms against 0.6 ms for the style recalculation alone, which the browser pays on any change of the hue. Checked in `apps/csssky/test/old-browser.mjs`: in Chromium 109 the four accent steps, the label's onAccent and the page background at hues 0, ±90 and ±180 are within 1/255 of the pixels Chrome 154 paints, in light, dim, dark and system under a light and a dark scheme, and the system scheme changing recomputes them; `packages/cssints/test/derived-runtime.mjs` holds the evaluator to culori (the maths of `check.contrast`) on the csssky templates and random inputs.
|
|
273
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.
|
|
274
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 `-`.
|
|
275
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 })`.
|
|
@@ -280,7 +349,12 @@ import * as css from "cssints" with { type: "cssints" };
|
|
|
280
349
|
css.layers("app", "author", "viewer");
|
|
281
350
|
const t = css.createTokens({ color: { $type: "color", bg: { $value: "#fff" } } });
|
|
282
351
|
css.createGlobalTheme(t, '[data-mode="dark"]', { color: { bg: "#000" } }, { layer: "viewer" });
|
|
283
|
-
css.createGlobalTheme(
|
|
352
|
+
css.createGlobalTheme(
|
|
353
|
+
t,
|
|
354
|
+
'[data-mode="hero"]',
|
|
355
|
+
{ color: { bg: "#123" } },
|
|
356
|
+
{ layer: "author", scope: "article.post" },
|
|
357
|
+
);
|
|
284
358
|
css.createGlobalTheme(t, ".x", { color: { bg: "#000" } }, { scope: "a[href" }); // error: unbalanced brackets in the scope
|
|
285
359
|
css.layers(); // error: a declaration has a name
|
|
286
360
|
css.layers("reset", { before: true }); // @layer reset; before @layer _.k, _.a, …;
|
|
@@ -295,11 +369,11 @@ css.layers("reset", { before: true }); // @layer reset; before @layer _.k, _.a,
|
|
|
295
369
|
import * as css from "cssints" with { type: "cssints" };
|
|
296
370
|
|
|
297
371
|
export const t = css.createTokens({
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
372
|
+
color: {
|
|
373
|
+
$type: "color",
|
|
374
|
+
accent: { $value: "#2563eb" },
|
|
375
|
+
hover: { $value: "color-mix(in oklab, {color.accent}, black 15%)" },
|
|
376
|
+
},
|
|
303
377
|
});
|
|
304
378
|
css.createGlobalTheme(t, ".brand", { color: { accent: "#db2777" } }); // hover follows
|
|
305
379
|
```
|
|
@@ -310,12 +384,15 @@ css.createGlobalTheme(t, ".brand", { color: { accent: "#db2777" } }); // hover f
|
|
|
310
384
|
import * as css from "cssints" with { type: "cssints" };
|
|
311
385
|
|
|
312
386
|
export const t = css.createTokens({
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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})" } },
|
|
316
390
|
});
|
|
317
391
|
css.createGlobalTheme(t, ".square", { radius: { md: "{radius.sm}" } });
|
|
318
|
-
css.createGlobalTheme(t, ".dim", {
|
|
392
|
+
css.createGlobalTheme(t, ".dim", {
|
|
393
|
+
hue: { accent: 12 },
|
|
394
|
+
color: { accent: "oklch(0.7 0.1 {hue.accent})" },
|
|
395
|
+
});
|
|
319
396
|
css.createGlobalTheme(t, ".x", { radius: { md: "{hue.accent}" } }); // error: a number, not a length
|
|
320
397
|
```
|
|
321
398
|
|
|
@@ -332,45 +409,71 @@ import { register } from "@tokens-studio/sd-transforms";
|
|
|
332
409
|
register(StyleDictionary);
|
|
333
410
|
const DTCG = { fontSize: "dimension", lineHeight: "number", opacity: "number" }; // the types sd-transforms leaves
|
|
334
411
|
StyleDictionary.registerFormat({
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
+
},
|
|
346
423
|
});
|
|
347
424
|
await new StyleDictionary({
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
+
},
|
|
351
433
|
}).buildAllPlatforms();
|
|
352
434
|
```
|
|
353
435
|
|
|
354
|
-
|
|
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`.
|
|
355
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.
|
|
356
|
-
- **`css.check.contrast(fg, bg, { min })`** computes the WCAG 2.x contrast ratio (`min` from 1 to 21) for the default values and for every theme that sets either token or one they are aliases of. It reads themes one at a time, except those of one selector: themes with the same selector (and scope) meet on one element, so they are read together, laid over each other in the order of the cascade (no layer first, then the declared layers in order, then source order). A viewer look that sets only the text colour is checked on the background of the app look under the same selector. Themes of different selectors, or a selector and a condition, are still read one at a time, not in their combinations. A value that is set at run time (a slider that sets `--hue-accent` on `<html>`) belongs to no theme and is checked at its default only; a theme per value would put each rule in the sheet and hold it to the thresholds of the other themes, since a check applies to every theme that touches its tokens, so measure a range outside the build (apps/
|
|
438
|
+
- **`css.check.contrast(fg, bg, { min })`** computes the WCAG 2.x contrast ratio (`min` from 1 to 21) for the default values and for every theme that sets either token or one they are aliases of. It reads themes one at a time, except those of one selector: themes with the same selector (and scope) meet on one element, so they are read together, laid over each other in the order of the cascade (no layer first, then the declared layers in order, then source order). A viewer look that sets only the text colour is checked on the background of the app look under the same selector. Themes of different selectors, or a selector and a condition, are still read one at a time, not in their combinations. A value that is set at run time (a slider that sets `--hue-accent` on `<html>`) belongs to no theme and is checked at its default only; a theme per value would put each rule in the sheet and hold it to the thresholds of the other themes, since a check applies to every theme that touches its tokens, so measure a range outside the build (apps/csssky `test/contrast.mjs`). A ratio below `min` is a build error that names the theme and both resolved values (the ratio is cut, not rounded: 4.499 is 4.49). A value the check cannot evaluate (`color-mix()`, a relative colour, `light-dark()`, `currentColor`, a system colour) or a translucent one is a warning that names the token; a build says it once, and a dev server says it again after each edit. A derived token (or a theme value with `{path}` references) is read in each theme by putting that theme's values, or the defaults, in place of its references, through a chain of derived tokens: a ratio when the result is a colour (`oklch(0.6 0.2 {hue.accent})`), a warning that names the theme when it is not (`color-mix()` over another token), and a theme that sets any token a checked colour depends on, directly, through an alias or through references, is checked. The colour library (culori, `culori/fn`: 27 kB minified, 9 kB gzipped) is build-only and never ships. The check runs when its module has run, and again at the end of a build over the themes of every module.
|
|
357
439
|
|
|
358
440
|
Output (the `@property` rules of `space-md` and `color-fg` are not shown):
|
|
359
441
|
|
|
360
442
|
```css
|
|
361
443
|
@layer _.k, _.a, _.b, _.c, _.d, _.e;
|
|
362
|
-
@property --color-accent {
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
}
|
|
365
459
|
@layer _.k {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
+
}
|
|
374
477
|
}
|
|
375
478
|
```
|
|
376
479
|
|
|
@@ -404,10 +507,25 @@ export const d = minHeight(firstThatWorks("100dvh", "100vhh")); // error: the fa
|
|
|
404
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.
|
|
405
508
|
|
|
406
509
|
```ts
|
|
407
|
-
import {
|
|
510
|
+
import {
|
|
511
|
+
animation,
|
|
512
|
+
cn,
|
|
513
|
+
hover,
|
|
514
|
+
keyframes,
|
|
515
|
+
margin,
|
|
516
|
+
opacity,
|
|
517
|
+
positionTry,
|
|
518
|
+
positionTryFallbacks,
|
|
519
|
+
top,
|
|
520
|
+
transform,
|
|
521
|
+
} from "cssints" with { type: "cssints" };
|
|
408
522
|
|
|
409
523
|
export const fade = keyframes({ from: opacity(0), to: opacity(1) });
|
|
410
|
-
export const wiggle = keyframes({
|
|
524
|
+
export const wiggle = keyframes({
|
|
525
|
+
from: transform("rotate(0deg)"),
|
|
526
|
+
"50%": cn(transform("rotate(8deg)"), opacity(0.5)),
|
|
527
|
+
to: transform("rotate(0deg)"),
|
|
528
|
+
});
|
|
411
529
|
export const above = positionTry(cn(top("anchor(bottom)"), margin(2)));
|
|
412
530
|
|
|
413
531
|
export const a = animation(fade, "200ms", "ease-out");
|
|
@@ -428,32 +546,71 @@ export const f = positionTryFallbacks(fade); // error: a keyframes name is not a
|
|
|
428
546
|
|
|
429
547
|
```css
|
|
430
548
|
@layer _.k {
|
|
431
|
-
|
|
432
|
-
|
|
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
|
+
}
|
|
433
561
|
}
|
|
434
562
|
@layer _.a {
|
|
435
|
-
|
|
563
|
+
._44ecsn {
|
|
564
|
+
animation: kf-0e6af964 200ms ease-out;
|
|
565
|
+
}
|
|
436
566
|
}
|
|
437
567
|
```
|
|
438
568
|
|
|
439
|
-
### Runtime: `
|
|
569
|
+
### Runtime: `cn`, `cv` (decided 2026-09-29, cssints-lo9x; `cv` without `_cv` 2026-10-07, cssints-ep2l; `cx` folded into `cn` 2026-10-11, cssints-4e9f)
|
|
570
|
+
|
|
571
|
+
- **One runtime package: `cssints` itself.** You import nothing for the browser. A module with a `cn()` that has runtime arguments, or a `cv()`, gets `import { _cx as __cx, _cv as __cv } from "cssints"` from the plugin, and those few lines are the only code that ships. `@cssints/variants` is gone.
|
|
572
|
+
- **`cn(...)` is the one function that merges.** Arguments known at build time merge into one class string, as above. An argument that depends on a runtime value is joined when the code runs, and the `css.*` calls inside it are still class strings made at build time: only the choice is left.
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
import { bg, cn, hover, p } from "cssints" with { type: "cssints" };
|
|
576
|
+
|
|
577
|
+
export const box = (props: { on: boolean; class?: string }) =>
|
|
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
|
+
);
|
|
585
|
+
export const bad = (n: number) => cn(p(2), p(n)); // the build refuses it: css arguments must be known at build time
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
```tsx skip
|
|
589
|
+
import { _cx as __cx } from "cssints";
|
|
440
590
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
591
|
+
export const box = (props) =>
|
|
592
|
+
__cx([["_lwugp7", "_1t8gqy5"]], "_1t8gqy5 _1ebepn0 _16f8jrr", props.on && "_lwugp7", props.class);
|
|
593
|
+
```
|
|
594
|
+
|
|
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.
|
|
596
|
+
- **What the build settles.** Of two parts, static or runtime, the later one replaces the earlier one for a property it sets again under the same conditions (the same property, an alias, or a shorthand over it, as in `cv`): the plugin lists, per class, the classes it drops, and the runtime leaves them out while that class is active. So `cn(p(2), on && p(4))` gives `p-4` when `on` and `p-2` otherwise, and `cn(on && p(4), p(2))` always gives `p-2`. The sheet order never decides. Under another condition (`hover`, a media query) a class replaces nothing: `cn(p(2), on && hover(p(4)))` keeps both, as in a build-time `cn`. A longhand after a shorthand needs no drop. Two properties that overlap without one containing the other, in parts that can be active together, are a build error as above (`cssints: cn() padding-inline and padding-left overlap ...`); both arms of `cond ? a : b` count as active together.
|
|
597
|
+
- **Opaque strings are only appended.** `props.class`, `"legacy"` or a style held in a variable inside a runtime argument (`on && blurred`) has no declarations in the argument, so nothing is dropped for it or by it; write the call there (`on && p(4)`) to have it settled.
|
|
598
|
+
- **A style whose own argument is a runtime value is a build error.** `cn(p(2), p(n))` with `n` a parameter stays `css arguments must be known at build time: n is not defined where the call is evaluated`; choose between styles (`n > 1 ? p(2) : p(4)`), or use a `cv` variant or a token set in `style`.
|
|
599
|
+
- **The call.** `__cx(drops, ...parts)`: `drops` is a list of `[class, "classes it replaces"]`, empty when nothing conflicts; the parts are class strings, runtime values and arrays. With no static argument there is no static string: `cn(props.class)` is `__cx([], props.class)`. Nothing is imported when every argument is static.
|
|
600
|
+
- **`cx` is gone.** `import { cx } from "cssints"` is a build error in a module the plugin handles, with or without the attribute: `cssints: <file> imports cx from "cssints", which is gone: cn(...) takes runtime values now. Write cn(base, on && p(4)) with cn from "cssints" with { type: "cssints" }`, and a type error (the name is now the SVG property `cx`, which you write `css.cx(...)`). The fix is mechanical: add `cn` to the attributed import and write `cn(` for `cx(`. Arguments that were only joined are now merged and checked at build time, so `cx(p(4), p(2))` (both classes, the sheet decides) is `cn(p(4), p(2))` (`p(2)`).
|
|
444
601
|
- **`cv(config)`** takes `css.*` values, so types check them, and is the function you call: one call, no wrapper. The plugin replaces the site by a call of the small runtime (and imports it for you; `_cv` is not exported any more) with a config of class strings. The props are typed from `variants`: a name or a value that is not there is a type error, in the props and in `defaultVariants` and `compoundVariants` alike.
|
|
445
602
|
|
|
446
603
|
```ts
|
|
447
604
|
import { cn, cv, fontSize, opacity, p } from "cssints" with { type: "cssints" };
|
|
448
605
|
|
|
449
606
|
const button = cv({
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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) }],
|
|
457
614
|
});
|
|
458
615
|
|
|
459
616
|
export const classes = () => button({ size: "lg" }); // "_lwugp7 _z6yj9k _1t0bw2x"
|
|
@@ -465,7 +622,7 @@ export const bad2 = cv({ base: "padding: 1rem" }); // error: a base is a css val
|
|
|
465
622
|
- **What ships** for `button` is `__cv({ base: "_1t8gqy5 _6tpir7", variants: { size: { sm: "_lqk467", lg: "_lwugp7 _z6yj9k" }, ... }, defaultVariants: { size: "sm", disabled: false }, ... })` with `import { _cv as __cv } from "cssints"` added to the module: class strings only. `cv()` works in a function, at the top of a module and in a library.
|
|
466
623
|
- **Old code.** `import { _cv } from "cssints"` is a type error (no such export) and, in a module the plugin handles, a build error at the import: `cssints: <file> imports _cv from "cssints", which is gone: cv({ ... }) is the function now. Write const button = cv({ base, variants }) ...`. It does not keep working for a release: `_cv(cv(...))` would hand a function to the runtime, and a shim would keep two spellings of one thing. The fix is mechanical: drop `_cv(` and its closing bracket. A library built the old way fails with the same error, naming its file.
|
|
467
624
|
- **Boolean variants.** A variant whose keys are `true` and `false` (or only one of them) takes a boolean: `button({ disabled: true })`, and a default is a boolean too. `false` is a value, not a missing one: `button({ disabled: false })` selects the `false` key even when the default is `true`. `undefined` is the default.
|
|
468
|
-
- **`compoundVariants`** as in cva: each entry names values (any of the variants, a boolean for a boolean one) and the style that is added when all of them are active. The key is `class`, as in cva, since what the entry adds is a class (a `
|
|
625
|
+
- **`compoundVariants`** as in cva: each entry names values (any of the variants, a boolean for a boolean one) and the style that is added when all of them are active. The key is `class`, as in cva, since what the entry adds is a class (a `cn` or `style()` key would suggest an inline style, and cssints has `style()` for that; `css` is the namespace). Its value is a `css.*` style like the others, not a string. The values of an entry are one value each, not a list of values.
|
|
469
626
|
- **Values** of `base` and of each variant are one property, `cn(...)`, or a `flex()`/`grid()` chain. A CSS string is a build error that shows the new form: `cv() base must be a css.* value (cn(...), a flex() chain or one property), got "padding: 1rem". CSS strings are gone: write cn(p(4), fontSize("1rem"))`.
|
|
470
627
|
|
|
471
628
|
- **The variant wins.** The function joins the base and the variants that are active at runtime, so the build compares them. Under one condition, a variant that sets what the base sets (the same property, an alias, or a shorthand over a base longhand: `pl(2)` in the base, `p(4)` in the variant) replaces it: the base class is dropped from the string while that value is active, which the runtime does from a `drops` entry in the config (`drops: { size: { lg: "_77ujlx" } }`, only where there is one). Which rule comes later in the sheet does not decide. A variant longhand inside a base shorthand needs no drop, since the sheet puts a longhand after its shorthand: base `border(...)` with `active: { yes: borderBottomColor(...) }` is fine. Under different conditions both stay, as in `cn()`.
|
|
@@ -507,13 +664,19 @@ css.supports("(display: grdi)"); // @supports: unexpected "grdi" in "(display: g
|
|
|
507
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.
|
|
508
665
|
|
|
509
666
|
```ts
|
|
510
|
-
import {
|
|
667
|
+
import {
|
|
668
|
+
cn,
|
|
669
|
+
display,
|
|
670
|
+
overflow,
|
|
671
|
+
WebkitBoxOrient,
|
|
672
|
+
WebkitLineClamp,
|
|
673
|
+
} from "cssints" with { type: "cssints" };
|
|
511
674
|
|
|
512
675
|
export const clamp2 = cn(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
676
|
+
display("-webkit-box"),
|
|
677
|
+
WebkitBoxOrient("vertical"),
|
|
678
|
+
WebkitLineClamp(2),
|
|
679
|
+
overflow("hidden"),
|
|
517
680
|
);
|
|
518
681
|
```
|
|
519
682
|
|
|
@@ -522,39 +685,47 @@ export const clamp2 = cn(
|
|
|
522
685
|
|
|
523
686
|
### Plugins (decided 2026-09-30, cssints-zho2; `scope()` 2026-10-05, cssints-8ogz)
|
|
524
687
|
|
|
525
|
-
A plugin is a module that exports the members of a `scope()` from `cssints/plugin`: `scope("name", table).build(impl).export()`. The table says what the plugin has (syntax names, kernels, a runtime, conflicts, token sets, other scopes, and its members); the members are typed from the table, with no second file of types. User code imports the plugin with the attribute, like `cssints` itself: `import { ring } from "./ring.ts" with { type: "cssints" }`. There is no registration in `vite.config` and no type augmentation; the engine finds the sites by the attribute, and learns kernels, runtimes and conflicts from the style each member returns. In user code a member returns a class string, so it goes into `cn()
|
|
688
|
+
A plugin is a module that exports the members of a `scope()` from `cssints/plugin`: `scope("name", table).build(impl).export()`. The table says what the plugin has (syntax names, kernels, a runtime, conflicts, token sets, other scopes, and its members); the members are typed from the table, with no second file of types. User code imports the plugin with the attribute, like `cssints` itself: `import { ring } from "./ring.ts" with { type: "cssints" }`. There is no registration in `vite.config` and no type augmentation; the engine finds the sites by the attribute, and learns kernels, runtimes and conflicts from the style each member returns. In user code a member returns a class string, so it goes into `cn()` and the conditions like any `css.*` value.
|
|
526
689
|
|
|
527
690
|
```ts
|
|
528
691
|
import { cn, p } from "cssints" with { type: "cssints" };
|
|
529
692
|
import { scope } from "cssints/plugin";
|
|
530
693
|
|
|
531
694
|
export const { ring, dot, webkitFontSmoothing } = scope("ring", {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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" },
|
|
553
719
|
})
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
export const card = cn(
|
|
720
|
+
.build({ dot: (size) => ({ decls: [["--_size", size === "small" ? "4px" : "8px"]] }) })
|
|
721
|
+
.export();
|
|
722
|
+
|
|
723
|
+
export const card = cn(
|
|
724
|
+
p(2),
|
|
725
|
+
ring().width("thin").color("red"),
|
|
726
|
+
webkitFontSmoothing("antialiased"),
|
|
727
|
+
dot("small"),
|
|
728
|
+
);
|
|
558
729
|
export const twice = ring().width("1px").width("2px"); // error: a method is used once
|
|
559
730
|
export const wrong = ring().width("red"); // error: not a width
|
|
560
731
|
export const bad = webkitFontSmoothing("bold"); // error: not in the grammar
|
|
@@ -578,15 +749,15 @@ import { scope } from "cssints/plugin";
|
|
|
578
749
|
|
|
579
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
|
|
580
751
|
export const { mode } = scope("mode", {
|
|
581
|
-
|
|
582
|
-
|
|
752
|
+
layers: { layers: ["app", "viewer"] },
|
|
753
|
+
mode: { args: ["$tokens", "<custom-ident>", "$overrides"], uses: ["layers"] },
|
|
583
754
|
})
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
755
|
+
.build({
|
|
756
|
+
mode: (tokens, name, overrides) => ({
|
|
757
|
+
themes: [{ tokens, overrides, selector: `[data-mode="${name}"]`, layer: "viewer" }],
|
|
758
|
+
}),
|
|
759
|
+
})
|
|
760
|
+
.export();
|
|
590
761
|
```
|
|
591
762
|
|
|
592
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.
|
|
@@ -596,15 +767,15 @@ import { cn, paddingLeft } from "cssints" with { type: "cssints" };
|
|
|
596
767
|
import { scope } from "cssints/plugin";
|
|
597
768
|
|
|
598
769
|
export const { chip } = scope("chip", {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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]] } } },
|
|
608
779
|
}).export();
|
|
609
780
|
|
|
610
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}}
|
|
@@ -622,15 +793,15 @@ import { scope } from "cssints/plugin";
|
|
|
622
793
|
const fade = keyframes({ from: opacity(0) });
|
|
623
794
|
|
|
624
795
|
export const { row } = scope("row", {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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]] } } },
|
|
634
805
|
}).export();
|
|
635
806
|
|
|
636
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)}
|
|
@@ -652,10 +823,10 @@ import { lacking } from "cssints/plugin";
|
|
|
652
823
|
|
|
653
824
|
const old = lacking("css.at-rules.container.scroll-state_queries").length > 0; // ["firefox 140"] or []
|
|
654
825
|
export const stuck = (...styles: Style[]) =>
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
826
|
+
cn(
|
|
827
|
+
container("scroll-state(stuck: top)")(...styles),
|
|
828
|
+
...(old ? [within(attr("data-stuck", "top", "~="), "stuck")(...styles)] : []),
|
|
829
|
+
);
|
|
659
830
|
export const title = stuck(color("red"));
|
|
660
831
|
```
|
|
661
832
|
|
|
@@ -668,13 +839,26 @@ import { color } from "cssints" with { type: "cssints" };
|
|
|
668
839
|
import { dependsOn } from "cssints/plugin";
|
|
669
840
|
|
|
670
841
|
export const swatch = (file: string) => {
|
|
671
|
-
|
|
672
|
-
|
|
842
|
+
dependsOn(file);
|
|
843
|
+
return color(readFileSync(file, "utf8").trim());
|
|
673
844
|
};
|
|
674
845
|
```
|
|
675
846
|
|
|
676
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.
|
|
677
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
|
+
|
|
678
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.
|
|
679
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`.
|
|
680
864
|
|
|
@@ -686,9 +870,9 @@ Every CSS function that mdn-data has a grammar for is a member of `css` and a na
|
|
|
686
870
|
import * as css from "cssints" with { type: "cssints" };
|
|
687
871
|
|
|
688
872
|
const t = css.createTokens({
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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" } },
|
|
692
876
|
});
|
|
693
877
|
|
|
694
878
|
export const a = css.width(css.calc(t.space.md, "*", 2)); // width: calc(var(--space-md) * 2)
|
|
@@ -698,7 +882,9 @@ export const d = css.maxWidth(css.min("48rem", "100%"));
|
|
|
698
882
|
export const e = css.color(css.lightDark("white", t.color.bg)); // color: light-dark(white, var(--color-bg))
|
|
699
883
|
export const f = css.color(css.colorMix("in oklab", t.color.accent, "black 15%"));
|
|
700
884
|
export const g = css.color(css.oklch(0.7, 0.1, 200, "/", 0.5)); // color: oklch(0.7 0.1 200 / 0.5)
|
|
701
|
-
export const h = css.backgroundImage(
|
|
885
|
+
export const h = css.backgroundImage(
|
|
886
|
+
css.linearGradient("to right", [t.color.accent, "25%"], "blue"),
|
|
887
|
+
);
|
|
702
888
|
export const i = css.transitionTimingFunction(css.cubicBezier(0.1, 0.7, 1, 0.1));
|
|
703
889
|
export const img = css.backgroundImage(css.url('"a.png"')); // url("a.png"): the quotes are yours
|
|
704
890
|
export const j = css.transform(css.translateFn(t.space.md, 0), css.rotateFn("10deg"));
|
|
@@ -723,8 +909,8 @@ A token is its `var()` already, so `var(--name)` is not written. `css.var(token,
|
|
|
723
909
|
import * as css from "cssints" with { type: "cssints" };
|
|
724
910
|
|
|
725
911
|
const t = css.createTokens({
|
|
726
|
-
|
|
727
|
-
|
|
912
|
+
color: { $type: "color", accent: { $value: "red" } },
|
|
913
|
+
space: { $type: "dimension", sm: { $value: "8px" } },
|
|
728
914
|
});
|
|
729
915
|
|
|
730
916
|
export const a = css.padding(css.var(t.space.sm, "4px")); // padding: var(--space-sm, 4px)
|
|
@@ -743,11 +929,11 @@ import { scope } from "cssints/plugin";
|
|
|
743
929
|
|
|
744
930
|
// `args` is the grammar of the parts, joined by a space: `value` gets each one as text, a token as its var(--name).
|
|
745
931
|
export const { tint, step } = scope("units", {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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` },
|
|
751
937
|
}).export();
|
|
752
938
|
|
|
753
939
|
tint("red", "20%");
|
|
@@ -762,11 +948,12 @@ step(2);
|
|
|
762
948
|
import { arg, fn, scope } from "cssints/plugin";
|
|
763
949
|
|
|
764
950
|
export const { shade } = scope("shades", {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
+
}),
|
|
770
957
|
}).export();
|
|
771
958
|
|
|
772
959
|
shade("red");
|
|
@@ -791,10 +978,17 @@ import { frame } from "cssints/frame" with { type: "cssints" };
|
|
|
791
978
|
const radius = css.token.length("12px", "radius-lg");
|
|
792
979
|
|
|
793
980
|
// radius 24px, padding 16px, border 1px: the field inside gets 24 - 16 - 1 = 7px
|
|
794
|
-
export const card = css.cn(
|
|
981
|
+
export const card = css.cn(
|
|
982
|
+
frame().radius("1.5rem").padding(4).border("1px"),
|
|
983
|
+
css.bg("white"),
|
|
984
|
+
css.borderColor("#9ca3af"),
|
|
985
|
+
);
|
|
795
986
|
export const field = css.cn(frame().padding(2).border("1px"), css.borderColor("#9ca3af"));
|
|
796
987
|
// a token is a length; a condition just sets the custom property, and the whole subtree follows
|
|
797
|
-
export const panel = css.cn(
|
|
988
|
+
export const panel = css.cn(
|
|
989
|
+
frame().radius(radius).padding(2),
|
|
990
|
+
css.media("(width >= 40rem)")(frame().padding(6)),
|
|
991
|
+
);
|
|
798
992
|
|
|
799
993
|
export const noNumber = frame().radius(4); // error: a bare number is a spacing step in padding and margin only
|
|
800
994
|
export const noPercent = frame().padding("5%"); // error: a length, not a percentage
|
|
@@ -803,7 +997,7 @@ export const own = frame().color("red"); // error: a frame chains its own method
|
|
|
803
997
|
```
|
|
804
998
|
|
|
805
999
|
- **Methods**, each once and in any order: `radius(<length>)`, `padding(<length> | step)`, `border(<length>)` (a width; the style is `solid`), `margin(<length> | step)`. `radius` is the radius of the outermost frame; inside a frame it is ignored (the radius is computed). A lone number in `padding` and `margin` is a step of the spacing scale (0.25rem). Tokens go anywhere a length does.
|
|
806
|
-
- **Build errors** (at the call site): a negative radius, padding or border, a percentage, and a `cn()` that holds `frame()` and a property it sets: `padding*`, `margin*`, `border-radius` and its corners, `border`, `border-width`, `border-style` and the border sides (`cssints: frame: frame() sets padding, margin, border-radius and the border: use its methods (this style also sets padding)`). Border colours (`borderColor`, `borderTopColor`) are plain styles and do not conflict. Conflicts
|
|
1000
|
+
- **Build errors** (at the call site): a negative radius, padding or border, a percentage, and a `cn()` that holds `frame()` and a property it sets: `padding*`, `margin*`, `border-radius` and its corners, `border`, `border-width`, `border-style` and the border sides (`cssints: frame: frame() sets padding, margin, border-radius and the border: use its methods (this style also sets padding)`). Border colours (`borderColor`, `borderTopColor`) are plain styles and do not conflict. Conflicts with an opaque runtime string in `cn()` are not seen, as for any plugin.
|
|
807
1001
|
- **What it emits:** the class `_frame-core` and one class per method (`._x{--frame-p:0.5rem}`, in `_.a`, or `_.b` inside a condition). The kernel `frame.core` (about 1.2 kB minified with its registrations and fallback, once, in `_.k`) is two `@container style(--frame-c: ...)` rules that alternate two names, because a property cannot read itself; the `@property` rules of its custom properties (`--frame-*`) are at the top of the sheet. The names are the scope's own (`--_x` in the source is `--frame-x`, `&` is `_frame-core`), which is why they are longer than the `--fr-*` and `_f` of the helpers it was first written on. The inputs are registered lengths, so `em`, `calc()` and tokens reach the child as exact lengths (an `em` padding is the parent's `em`, not the child's). The 24-line kernel for px and rem only was not chosen: `@property` is the floor of the runtime anyway, and the registered one costs about 0.2 kB more.
|
|
808
1002
|
- **Browsers.** The kernel needs `@container style()` on custom properties (Chrome 111, Safari 18, Firefox 151) and `@property` (Chrome 85, Safari 16.4, Firefox 128). For a target that has `@property` and lacks style queries the build ships the fallback instead: `polyfills: ["css.at-rules.container.style_queries_for_custom_properties"]` and `cssints/parent-vars` (about 1 kB gzip, shared with other plugins), which copies each frame's inner radius to its frame children, and the fallback CSS. The script probes the browser and stays off where style queries work. Below `@property` (Firefox 127, Safari 16.3) the kernel is a build warning and nothing is shipped: a frame keeps no concentric radius there.
|
|
809
1003
|
- **Limits of the runtime.** It costs about two or three times the style-query kernel on a first pass (190 ms for 5 500 nested frames in Firefox 128); a `transition` of your own on a frame replaces the sentinel that lets `:hover`, media and container conditions reach the script (the script still follows class and style changes); shadow roots are not crossed; a first paint before the script runs shows declared radii.
|
|
@@ -830,8 +1024,8 @@ export const noValue = surface().ink("black"); // error: ink() takes no value
|
|
|
830
1024
|
export const own = surface().radius("4px"); // error: a surface chains its own methods only
|
|
831
1025
|
```
|
|
832
1026
|
|
|
833
|
-
- **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
|
|
834
|
-
- **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
|
|
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)`).
|
|
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.
|
|
835
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.
|
|
836
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.
|
|
837
1031
|
- **Browsers.** The kernel needs `@container style()` on custom properties (Chrome 111, Safari 18, Firefox 151), `@property` (Chrome 85, Safari 16.4, Firefox 128) and relative colour (Chrome 122, Safari 18, Firefox 128). Chrome 119 to 121 and Safari 16.4 to 17.6 have an early relative colour with other channel scales: the steps come out wrong (in Chrome `l` is a number from 0 to 100), so the kernel's `requires` asks for the whole feature (`exact`, see Plugins) and a target set that holds one of them gets the warning. Safari 17 has no style queries, so it gets the runtime as well, which does not change the steps; Chrome 120 has style queries and gets no runtime, only the warning. For a target that has `@property` and relative colour but lacks style queries (Firefox 128 to 150) the build ships the same fallback as `frame()`: the shared `cssints/parent-vars` script (one observer however many plugins use it) copies each surface's colour to its surface children. A surface that is also a frame needs both transition sentinels, so the fallback has a rule for `._surface-core._frame-core` (the surface scope embeds the frame scope for that class: `&(frame.core)`). Below `@property` or relative colour the kernel is a build warning: a surface keeps no tonal step there.
|
|
@@ -847,8 +1041,8 @@ import { icons } from "@cssints/icons" with { type: "cssints" };
|
|
|
847
1041
|
|
|
848
1042
|
// icons.ts: a build-time module (it runs while the build runs, and never reaches the browser)
|
|
849
1043
|
export const mdi = icons<typeof import("@iconify-json/mdi/icons.json")>({
|
|
850
|
-
|
|
851
|
-
|
|
1044
|
+
provider: "iconify-json",
|
|
1045
|
+
collection: "mdi",
|
|
852
1046
|
});
|
|
853
1047
|
export const brand = icons({ provider: "fs", dir: "src/icons" });
|
|
854
1048
|
|
|
@@ -860,12 +1054,20 @@ export const typo = mdi("clos"); // error: not an icon of mdi
|
|
|
860
1054
|
|
|
861
1055
|
```css
|
|
862
1056
|
/* kernel icons.mask, layer _.k; LightningCSS adds -webkit-mask for your targets */
|
|
863
|
-
._icons-mask {
|
|
1057
|
+
._icons-mask {
|
|
1058
|
+
display: inline-block;
|
|
1059
|
+
width: 1em;
|
|
1060
|
+
height: 1em;
|
|
1061
|
+
background-color: currentColor;
|
|
1062
|
+
mask: var(--icons-svg) no-repeat center / 100% 100%;
|
|
1063
|
+
}
|
|
864
1064
|
/* the only per-icon rule */
|
|
865
|
-
._x1 {
|
|
1065
|
+
._x1 {
|
|
1066
|
+
--icons-svg: url("data:image/svg+xml,%3Csvg …");
|
|
1067
|
+
}
|
|
866
1068
|
```
|
|
867
1069
|
|
|
868
|
-
- **`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()
|
|
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.
|
|
869
1071
|
- **Names.** TypeScript cannot read a module type from a string, so the names of a collection come from the type argument: `icons<typeof import("@iconify-json/mdi/icons.json")>(…)` types `mdi(name)` as the icons and aliases of that collection, and a typo is an error in the editor. Without the type argument, and for `fs`, a name is any string. In every case a name that does not exist is a build error at the call, with the nearest name when it is close: `cssints: icons: no icon "hom" in iconify-json mdi, did you mean home?`.
|
|
870
1072
|
- **Mask or image.** An icon that paints with `currentColor` (every one-colour icon of a collection) gets the marker `_icons-mask` and takes the text `color`. Any other (a logo with its own colours) gets `_icons-image`, a background image. This is iconify's own rule. A non-square icon is centred on a square view box, so `width` and `height` set its size (`1em` by default).
|
|
871
1073
|
- **Cost.** The kernel is printed once. Each icon adds one class, and the same icon is the same class everywhere. The kernel has no `requires`: LightningCSS adds `-webkit-mask` for targets that have only the prefixed form.
|
|
@@ -876,24 +1078,24 @@ export const typo = mdi("clos"); // error: not an icon of mdi
|
|
|
876
1078
|
|
|
877
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`.
|
|
878
1080
|
|
|
879
|
-
| Package
|
|
880
|
-
|
|
881
|
-
| [`@cssints/fluid`](../packages/fluid/README.md)
|
|
882
|
-
| [`@cssints/layout`](../packages/layout/README.md)
|
|
883
|
-
| [`@cssints/fonts`](../packages/fonts/README.md)
|
|
884
|
-
| [`@cssints/reset`](../packages/reset/README.md)
|
|
885
|
-
| [`@cssints/tokens`](../packages/tokens/README.md)
|
|
886
|
-
| [`@cssints/gradient`](../packages/gradient/README.md)
|
|
887
|
-
| [`@cssints/motion`](../packages/motion/README.md)
|
|
888
|
-
| [`@cssints/squircle`](../packages/squircle/README.md)
|
|
889
|
-
| [`@cssints/reveal`](../packages/reveal/README.md)
|
|
890
|
-
| [`@cssints/stuck`](../packages/stuck/README.md)
|
|
891
|
-
| [`@cssints/balance`](../packages/balance/README.md)
|
|
892
|
-
| [`@cssints/masonry`](../packages/masonry/README.md)
|
|
893
|
-
| [`@cssints/placeholder`](../packages/placeholder/README.md) | `placeholder(path, { mode? })`
|
|
894
|
-
| [`@cssints/ramp`](../packages/ramp/README.md)
|
|
895
|
-
| [`@cssints/rybitten`](../packages/rybitten/README.md)
|
|
896
|
-
| [`@cssints/skeleton`](../packages/skeleton/README.md)
|
|
1081
|
+
| Package | Members | What it does |
|
|
1082
|
+
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1083
|
+
| [`@cssints/fluid`](../packages/fluid/README.md) | `fluid(min, max, { from?, to?, zoom? })`, `fluid.container(...)` | A `clamp()` that grows between two widths, computed at build time; a range that fails WCAG 1.4.4 is a build error |
|
|
1084
|
+
| [`@cssints/layout`](../packages/layout/README.md) | `stack()`, `cluster()`, `sidebar()`, `switcher()`, `cover()`, `autoGrid()` | Every Layout primitives as chain members |
|
|
1085
|
+
| [`@cssints/fonts`](../packages/fonts/README.md) | `font(family, { provider, ... })` → `.family()`, `.trim()` | `@font-face` and a metric-matched fallback face; cap-height trimming by `text-box`, else capsize's margins |
|
|
1086
|
+
| [`@cssints/reset`](../packages/reset/README.md) | `reset({ media?, prose? })` | A modern reset in a top-level `@layer reset` declared before the engine's, below every style; `@cssints/reset/<baseline group>` keeps only the declarations of that group |
|
|
1087
|
+
| [`@cssints/tokens`](../packages/tokens/README.md) | `/open-props`, `/radix`, `/flexoki`, `/reasonable-colors` | DTCG data for `createTokens` |
|
|
1088
|
+
| [`@cssints/gradient`](../packages/gradient/README.md) | `gradient(kind, direction, from, to, easing?, stops?)` | Eased colour stops mixed in oklab at build time |
|
|
1089
|
+
| [`@cssints/motion`](../packages/motion/README.md) | `motion().preset().duration().delay().easing()` | Animation presets over `keyframes()`, with reduced motion |
|
|
1090
|
+
| [`@cssints/squircle`](../packages/squircle/README.md) | `squircle().radius()` | `corner-shape: squircle`, else a CSS `mask` of the same shape |
|
|
1091
|
+
| [`@cssints/reveal`](../packages/reveal/README.md) | `reveal().preset().range().duration().easing()` | motion's entrance presets on `animation-timeline: view()`; for other targets a script plays it once on entry (delivered by the runtime) |
|
|
1092
|
+
| [`@cssints/stuck`](../packages/stuck/README.md) | `sticky().top()`, `stuck(styles)`, `stuck("bottom")(styles)` | Styles inside a sticky element while it is stuck, by `@container scroll-state()`; for other targets an `IntersectionObserver` (delivered by the runtime) |
|
|
1093
|
+
| [`@cssints/balance`](../packages/balance/README.md) | `balance()`, `pretty()` | `text-wrap: balance` / `pretty`; for targets without balance a script balances the lines (delivered by the runtime) |
|
|
1094
|
+
| [`@cssints/masonry`](../packages/masonry/README.md) | `masonry().columns().gap()` | `display: grid-lanes`, else CSS `columns` (top to bottom per column) |
|
|
1095
|
+
| [`@cssints/placeholder`](../packages/placeholder/README.md) | `placeholder(path, { mode? })` | An `<image>` from a JPEG or PNG read at build time: a gradient of its colours laid out as the image, or a tiny thumbhash PNG |
|
|
1096
|
+
| [`@cssints/ramp`](../packages/ramp/README.md) | `ramp(base, { mode, steps \| ratios, background })`, `overrides(ramp)` | An OKLCH palette as DTCG data for `createTokens`; the contrast mode meets each ratio against its background |
|
|
1097
|
+
| [`@cssints/rybitten`](../packages/rybitten/README.md) | `color(hue, { cube })`, `wheel(steps, { cube })`, `shades(hue, lightness)`, `overrides(group)` | RYBitten's red-yellow-blue colours (36 historical cubes) as DTCG data for `createTokens`: a hue, a wheel of hues, a hue at several lightnesses |
|
|
1098
|
+
| [`@cssints/skeleton`](../packages/skeleton/README.md) | `skeleton().radius().color().highlight()` | Loading placeholders whose shimmer is in phase across the page (one clock on `:root`), static under reduced motion |
|
|
897
1099
|
|
|
898
1100
|
### Publishing a library (decided 2026-10-02, cssints-0l3f)
|
|
899
1101
|
|
|
@@ -919,7 +1121,7 @@ import { t } from "my-ui" with { type: "cssints" };
|
|
|
919
1121
|
createGlobalTheme(t, ".dark", { color: { brand: "#ef4444" } });
|
|
920
1122
|
```
|
|
921
1123
|
|
|
922
|
-
- **Build with tsdown** (`vp pack`), with `cssints` external and `dts: true`. tsdown keeps the import attributes, so there is no option to set.
|
|
1124
|
+
- **Build with tsdown** (`vp pack`), with `cssints` external and `dts: true`. tsdown keeps the import attributes, so there is no option to set. The built `.d.ts` types `button` as `(props?: …) => string`, with the props of its variants. Do not minify, bundle or transform the JS further with a tool that drops import attributes, and do not bundle `cssints` into it.
|
|
923
1125
|
- **Keep in the package:** `cssints` in `peerDependencies` (the app needs it to build; a library that imports it must list it), the JS with the attributes, the `.d.ts` (it types `cssints` members as class strings and tokens as tokens, so the app is checked across the boundary), and no `.css` file: the library has no sheet of its own.
|
|
924
1126
|
- **The app needs cssints and its plugin.** The package has no classes until the app's `cssints()` plugin has evaluated it, so an app without cssints cannot use it. Pre-compiled output (class strings plus a sheet fragment for apps without cssints) is not supported yet; it waits for a first user.
|
|
925
1127
|
- **The package must be a direct dependency of the app.** `cssints/vite` reads the `package.json` of the app and of each of its dependencies, and for a dependency that lists `cssints` it adds the package to `optimizeDeps.exclude` (the dev optimizer would pre-bundle it with esbuild and drop the attributes) and to `ssr.noExternal` (the engine's runner would otherwise load it as native Node, which does not know the attribute). This is a few file reads at startup, one per dependency, and nothing else. A package that is only a dependency of another package is not found; add it to the app's own dependencies (or to those two options).
|
|
@@ -946,7 +1148,7 @@ In dev the sheet has a source map: each rule with a class points at the first `c
|
|
|
946
1148
|
export default defineConfig({ plugins: [cssints()], css: { devSourcemap: true } });
|
|
947
1149
|
```
|
|
948
1150
|
|
|
949
|
-
A class is a hash of its declaration, so a class that many calls share points at its first call in the module that comes first by path. The place is the whole site (`cn(...)`,
|
|
1151
|
+
A class is a hash of its declaration, so a class that many calls share points at its first call in the module that comes first by path. The place is the whole site (`cn(...)`, a member call), not the argument inside it. Token, theme and `@property` rules carry no class and map nowhere.
|
|
950
1152
|
|
|
951
1153
|
A build maps the sheet too, when the app asks for maps (`build: { sourcemap: true }`, `"hidden"` or `"inline"`, as for its JS): the CSS asset that holds the sheet gets `<file>.css.map` and a `sourceMappingURL` comment (`"hidden"`: the file only; `"inline"`: a data URL). Vite writes no CSS map in a build, so cssints maps the asset as written, after LightningCSS and the minify: each rule with a class points at its call, the rest maps nowhere. Bundle analysers read it to split the sheet's bytes by module (docs/research/devtools.md: Sonda). Without `build.sourcemap` nothing is recorded and the output is unchanged. Devtools that read the CSSOM (atomic-css-devtools) see the sheet as any other; what they miss is in docs/research/devtools.md.
|
|
952
1154
|
|
|
@@ -980,7 +1182,8 @@ TOKENS
|
|
|
980
1182
|
the initial value of its @property
|
|
981
1183
|
```
|
|
982
1184
|
|
|
983
|
-
- **Classes by call.** The element's classes, grouped by the `css.*` call that made them (`cn(...)`,
|
|
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.
|
|
984
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.
|
|
985
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).
|
|
986
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).
|
|
@@ -994,7 +1197,7 @@ TOKENS
|
|
|
994
1197
|
// or: opencode: { agent: "build", skills: ["cssints"] }
|
|
995
1198
|
```
|
|
996
1199
|
|
|
997
|
-
The dev server, never the page, talks to the OpenCode V2 service that is running on the machine (the one `opencode serve --service` and the desktop app register; found with `Service.discover` of `@opencode/client`, so the page holds no address and no credentials). It creates a session in the workspace root (the nearest folder up from the Vite root with a `.git`)
|
|
1200
|
+
The dev server, never the page, talks to the OpenCode V2 service that is running on the machine (the one `opencode serve --service` and the desktop app register; found with `Service.discover` of `@opencode/client`, so the page holds no address and no credentials). It sends the prompt to the session that the OpenCode command `/cssints-devtools` attached to this dev server (the plugin is `@cssints/devtools/opencode`, loaded from `.opencode/plugins/`; the files they share are in `<repository>/node_modules/.cache/cssints-devtools/`), or, with none attached, creates a session in the workspace root (the nearest folder up from the Vite root with a `.git`) and sends it there, waits for the session to end, and tells the overlay the status over the same HMR channel (`cssints:devtools:ask` from the page, `cssints:devtools:asked` back: no route that another origin could post to). The box shows `sending`, then `running · ses_…`, then `completed`, `failed` or `interrupted`; with no service running it says so and keeps your text. `@opencode/client` is an optional peer dependency: install it in the app (the message names it when it is missing). The prompt is made of cssints' own data, not source markers added to the markup, and is bounded (8 call sites, 24 properties, 3 beaten declarations each, 12 tokens, a call cut at 400 characters; the server cuts the whole at 16000):
|
|
998
1201
|
|
|
999
1202
|
```text skip
|
|
1000
1203
|
Change the UI of the running app as the instruction says.
|
|
@@ -1020,6 +1223,7 @@ TOKENS
|
|
|
1020
1223
|
```
|
|
1021
1224
|
|
|
1022
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
|
+
|
|
1023
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.
|
|
1024
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.
|
|
1025
1229
|
|
|
@@ -1042,7 +1246,7 @@ Jaeger takes traces only, which is all there is: a warning or an error is an eve
|
|
|
1042
1246
|
|
|
1043
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.
|
|
1044
1248
|
|
|
1045
|
-
`packages/cssints/test/trace.mjs` (in `bun run test`) checks the spans, the file attribute and the error's trace id against an in-process collector. `node packages/cssints/test/trace-live.mjs` builds apps/
|
|
1249
|
+
`packages/cssints/test/trace.mjs` (in `bun run test`) checks the spans, the file attribute and the error's trace id against an in-process collector. `node packages/cssints/test/trace-live.mjs` builds apps/csssky and runs its HMR test traced, then reads both traces back through Jaeger's API (`GET /api/traces?service=cssints`) and prints span counts and the slowest hooks; it skips when no backend answers. Effect's DevTools (the VS Code extension, `effect/devtools`) is not wired: it needs the editor's extension listening, so neither a script nor an agent can read it.
|
|
1046
1250
|
|
|
1047
1251
|
### Checks for tools: `cssints/check` (2026-10-09, cssints-p3d2)
|
|
1048
1252
|
|
|
@@ -1066,17 +1270,19 @@ A terminal app writes its styles with `cssints/tui`, and the same Vite plugin co
|
|
|
1066
1270
|
```ts
|
|
1067
1271
|
import * as css from "cssints/tui" with { type: "cssints" };
|
|
1068
1272
|
|
|
1069
|
-
const t = css.createTokens({
|
|
1273
|
+
const t = css.createTokens({
|
|
1274
|
+
color: { $type: "color", bg: { $value: "#1e1e2e" }, accent: { $value: "#89b4fa" } },
|
|
1275
|
+
});
|
|
1070
1276
|
css.createGlobalTheme(t, "light", { color: { bg: "#eff1f5", accent: "#1e66f5" } });
|
|
1071
1277
|
|
|
1072
1278
|
export const button = css.cn(
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1279
|
+
css.border("rounded", "#585b70"),
|
|
1280
|
+
css.p(0, 1),
|
|
1281
|
+
css.width(12),
|
|
1282
|
+
css.bg(t.color.bg),
|
|
1283
|
+
css.cursor("pointer"),
|
|
1284
|
+
css.hover(css.borderColor(t.color.accent)),
|
|
1285
|
+
css.active(css.borderStyle("double")),
|
|
1080
1286
|
);
|
|
1081
1287
|
css.width("10rem"); // error: a terminal counts cells
|
|
1082
1288
|
css.boxShadow("0 0 1px red"); // error: no such member in a terminal
|
|
@@ -1084,13 +1290,30 @@ css.color("blu"); // error: not a colour
|
|
|
1084
1290
|
```
|
|
1085
1291
|
|
|
1086
1292
|
```ts skip
|
|
1087
|
-
export const button = {
|
|
1293
|
+
export const button = {
|
|
1294
|
+
border: true,
|
|
1295
|
+
borderStyle: "rounded",
|
|
1296
|
+
borderColor: "#585b70",
|
|
1297
|
+
paddingTop: 0,
|
|
1298
|
+
paddingRight: 1,
|
|
1299
|
+
paddingBottom: 0,
|
|
1300
|
+
paddingLeft: 1,
|
|
1301
|
+
width: 12,
|
|
1302
|
+
backgroundColor: "#1e1e2e",
|
|
1303
|
+
cursor: "pointer",
|
|
1304
|
+
":hover": { borderColor: "#89b4fa" },
|
|
1305
|
+
":active": { border: true, borderStyle: "double" },
|
|
1306
|
+
themes: { light: { backgroundColor: "#eff1f5", ":hover": { borderColor: "#1e66f5" } } },
|
|
1307
|
+
vars: { backgroundColor: "color-bg", ":hover": { borderColor: "color-accent" } },
|
|
1308
|
+
};
|
|
1088
1309
|
```
|
|
1089
1310
|
|
|
1090
1311
|
```tsx skip
|
|
1091
1312
|
import { attach, setTheme, use } from "@cssints/tui/opentui";
|
|
1092
1313
|
|
|
1093
|
-
<box {...use(button)}
|
|
1314
|
+
<box {...use(button)}>
|
|
1315
|
+
<text>Save</text>
|
|
1316
|
+
</box>; // Solid or React: the spread is a ref that calls attach
|
|
1094
1317
|
<text {...use(() => (active() ? tabActive : tab))}>Home</text>; // a style that changes: an accessor
|
|
1095
1318
|
attach(new BoxRenderable(renderer, {}), button); // core
|
|
1096
1319
|
setTheme("light");
|
|
@@ -1102,6 +1325,7 @@ setTheme("light");
|
|
|
1102
1325
|
- **Colours** are checked by the web's `color` grammar in the types, read at build time and written as `#rrggbb` (`#rrggbbaa` when translucent). `currentColor`, system colours and `light-dark()` are build errors.
|
|
1103
1326
|
- **`cn()`** merges in argument order (a later prop wins). States wrap styles and do not nest; the runtime lays `:hover`, then `:focus`, then `:active` over the props.
|
|
1104
1327
|
- **Themes.** A theme is named by its selector argument (`"light"`). Every colour token is resolved at build time, per theme, aliases and derived tokens included; the site holds the default values and, under `themes`, what each theme changes. `setTheme(name)` swaps them on every attached renderable; `""` is the default. A theme under `media()` is not read.
|
|
1328
|
+
- **Terminal tokens are not the sheet's** (cssints-0s1z, 2026-10-11). The `createTokens` and `createGlobalTheme` of `cssints/tui` keep their tokens and themes in tables of their own: no `@property` rule, no `:root` rule and no theme reaches a stylesheet, and a web token of the same name in the same build (`--color-bg` of the site and of a terminal app) is another token, with no clash. A terminal style reads terminal tokens and themes only; a web style reads web ones only. Two terminal modules that give one token name two values are an error, as on the web (`--color-bg is already declared with another @property`). A set of one `createTokens` given to the other's `createGlobalTheme` is a build error at the call that names the right import (`these are web tokens of "cssints": theme them with the createGlobalTheme of "cssints"`, and the other way round; 2026-10-11, cssints-v872). A token set still ships as `var(--name)` strings, which the system theme reads.
|
|
1105
1329
|
- **The adapter.** `attach(renderable, style)` sets the props (`color` is OpenTUI's `fg`, the text attributes are the bits of `attributes`, the caret is an editable's `cursorStyle` and `cursorColor`), follows `over`/`out`, `down`/`up` and `focused`/`blurred`, and sets the mouse pointer of `cursor` on over and `default` on out. A prop the renderable lacks is not set. A focused renderable draws colours of its own (a box `focusedBorderColor`, also while a descendant has the focus; an editable or a select `focusedBackgroundColor` and `focusedTextColor`): each follows the style's `borderColor`, `backgroundColor` and `color` in the current state, theme included, so a focused box draws its `:focus` border, not OpenTUI's `#00AAFF` (2026-10-10, cssints-pkpg). On an editable or a select, `color` is `textColor`. A text node (`<span>`, `<a>`, `<b>` of `@opentui/solid`: a `TextNodeRenderable`) takes the props (`fg`, `bg`, `attributes`) and the theme, not the states: it has no mouse events and no focus, so `:hover` belongs on the `<text>` around it; its theme listener stops when it is destroyed, as `@opentui/solid` does with what it removes (2026-10-10, cssints-5vnf). To change the style of a renderable, attach it again: `attach(r, next)` on a renderable it has attached swaps the style and keeps the states, the theme and the hooks; a prop only the old style set goes back to the renderable's own value (OpenTUI's default, or the element's own prop), the `focused*` colours follow, and under the mouse the pointer turns to the new `cursor` (or `default`). `use()` takes a style or an accessor, `use(() => (active() ? a : b))`: Solid calls a ref in a render effect, so the signals the accessor reads run it again and each run swaps the style; React calls the new ref of each render, which swaps it too and no longer stacks the hooks. The adapter imports no framework (2026-10-10, cssints-332g). `resolve(style, state, theme)` of `@cssints/tui` is the renderer-neutral part, for other adapters.
|
|
1106
1330
|
- **The bomb.sh adapter** (cssints-kotl, 2026-10-10). `@cssints/tui/bomb` is for [@bomb.sh/tty](https://bomb.sh/docs/tty/api/), where a frame is a list of ops: `createTracker()` gives `ui.open(id, style, extra?)` and `ui.text(content, style, id?, extra?)`, the ops with the options of the style in the state of that element id, and `ui.render(term, build, { pointer })`, which reads the frame's `pointerenter`/`pointerleave` events and a press of `pointer.down`, draws the frame again when a state changed, and adds the OSC 22 pointer of `cursor` and the caret (DECSCUSR, OSC 12) of the element of `ui.focus(id)`; `ui.reset()` gives them back on exit. Sizes are `fixed`, `percent`, `fit`, and `grow` for `flexGrow`; `rounded` is a corner radius; `double` and `heavy` are drawn single; margins, `flexShrink`, `flexBasis`, `flexWrap`, `alignSelf`, `opacity` and `space-*` are not drawn (the table is in research/tui.md). `openProps(props)` and `textProps(props)` are the plain mapping of `resolve()`.
|
|
1107
1331
|
|
|
@@ -1122,14 +1346,18 @@ process.stdout.write(ui.render(term, build, { pointer: { x, y, down } }).output)
|
|
|
1122
1346
|
import { createSystemTheme } from "@cssints/tui/system";
|
|
1123
1347
|
import { t } from "./styles.ts";
|
|
1124
1348
|
|
|
1125
|
-
export const system = createSystemTheme(
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1349
|
+
export const system = createSystemTheme(
|
|
1350
|
+
t,
|
|
1351
|
+
{
|
|
1352
|
+
color: {
|
|
1353
|
+
bg: { surface: true },
|
|
1354
|
+
card: { surface: true, on: [t.color.bg], contrast: 1.1 },
|
|
1355
|
+
text: { hue: "fg", on: [t.color.bg, t.color.card], contrast: 7 },
|
|
1356
|
+
accent: { hue: "blue", on: [t.color.bg, t.color.card], contrast: 3 },
|
|
1357
|
+
},
|
|
1358
|
+
},
|
|
1359
|
+
{ themes: { light: "light" } },
|
|
1360
|
+
);
|
|
1133
1361
|
|
|
1134
1362
|
// OpenTUI: renderer.getPalette() and its theme_mode event
|
|
1135
1363
|
await followSystemTheme(await createCliRenderer(), system);
|
|
@@ -1139,7 +1367,47 @@ process.stdin.on("data", (bytes) => dispatch(input.scan(follow.feed(bytes)).even
|
|
|
1139
1367
|
```
|
|
1140
1368
|
|
|
1141
1369
|
- **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.
|
|
1142
|
-
- **
|
|
1370
|
+
- **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.
|
|
1371
|
+
- **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).
|
|
1372
|
+
|
|
1373
|
+
### Export groups: `cssints/widely-available`, `cssints/css-grid-2`, … (decided 2026-10-11, cssints-n7z3)
|
|
1374
|
+
|
|
1375
|
+
A group is a subpath of `cssints` that exports the members of `cssints` which a set of CSS features has, and refuses a value that holds a feature outside the set. It is the same import (`with { type: "cssints" }`), the same members, the same classes:
|
|
1376
|
+
|
|
1377
|
+
```ts
|
|
1378
|
+
import { color, display, media } from "cssints/widely-available" with { type: "cssints" };
|
|
1379
|
+
|
|
1380
|
+
export const card = [
|
|
1381
|
+
display("grid"),
|
|
1382
|
+
color("rebeccapurple"),
|
|
1383
|
+
media("(width >= 40rem)")(color("black")),
|
|
1384
|
+
];
|
|
1385
|
+
|
|
1386
|
+
display("grid-lanes"); // error: display: "grid-lanes" is not in cssints/widely-available
|
|
1387
|
+
color("light-dark(white, black)"); // error: color: "light-dark()" is not in cssints/widely-available
|
|
1388
|
+
media("(prefers-reduced-data: reduce)"); // error: @media: "prefers-reduced-data" is not in cssints/widely-available
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
```ts
|
|
1392
|
+
import { inlineSize } from "cssints/widely-available/with-downstream" with { type: "cssints" };
|
|
1393
|
+
import { color } from "cssints/baseline-2022" with { type: "cssints" };
|
|
1394
|
+
|
|
1395
|
+
inlineSize("10rem");
|
|
1396
|
+
inlineSize("fit-content"); // error: inline-size: "fit-content" is not in cssints/widely-available/with-downstream
|
|
1397
|
+
color("rebeccapurple");
|
|
1398
|
+
color("color-mix(in oklab, red, blue)"); // error: color: "color-mix()" is not in cssints/baseline-2022
|
|
1399
|
+
```
|
|
1400
|
+
|
|
1401
|
+
- **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.
|
|
1402
|
+
- **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://www.npmjs.com/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).
|
|
1403
|
+
- **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.
|
|
1404
|
+
- **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.
|
|
1405
|
+
- **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"`.
|
|
1406
|
+
- **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.
|
|
1407
|
+
- **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).
|
|
1408
|
+
- **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).
|
|
1409
|
+
|
|
1410
|
+
`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).
|
|
1143
1411
|
|
|
1144
1412
|
### Browser support and what 0.1 does not have
|
|
1145
1413
|
|