rainbowindex 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +197 -0
- package/README.md +66 -37
- package/dist/browser.d.ts +2 -2
- package/dist/browser.mjs +1 -1
- package/dist/{chunk-CMB6BHVE.mjs → chunk-4CTJLMYM.mjs} +47 -29
- package/dist/{chunk-KRZL4IDK.mjs → chunk-4UKFK2GE.mjs} +62 -47
- package/dist/{chunk-DT5HYIM3.mjs → chunk-F4VCBISU.mjs} +454 -432
- package/dist/{chunk-ZI5ZYNSU.mjs → chunk-PDORZSQX.mjs} +205 -138
- package/dist/{chunk-NXJZX6KI.mjs → chunk-RU4756NG.mjs} +5 -6
- package/dist/cli.mjs +8 -7
- package/dist/{context-ruu2x_jR.d.ts → context-B9yhJxd5.d.ts} +26 -8
- package/dist/editor.d.ts +9 -6
- package/dist/editor.mjs +9 -5
- package/dist/{index-Dp6i5TSv.d.ts → index-Dx-NpFFx.d.ts} +5 -5
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +6 -4
- package/dist/oxlint.d.ts +55 -0
- package/dist/oxlint.mjs +38 -0
- package/dist/{safelist-D9-Plqta.d.ts → safelist-DAkKuxCk.d.ts} +2 -15
- package/dist/vite.mjs +53 -12
- package/package.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,203 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.1] - 2026-08-28
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Inline SVG path data is no longer scanned for classes.** Raising the
|
|
13
|
+
line-length guard to 10,000 characters in 0.5.0 let a multi-KB `d="…"`
|
|
14
|
+
attribute reach the whole-file token scan, where path data tokenizes
|
|
15
|
+
cleanly against the class grammar: one 10 KB icon component went from 33
|
|
16
|
+
candidates to 845, the extra 812 being fragments like `9.17-57.2` and
|
|
17
|
+
`40c-.35-1.1-1.04`. They matched no utility and carried the `plain` origin
|
|
18
|
+
editors skip, so nothing rendered wrong and nothing was reported — each
|
|
19
|
+
one just cost a compile lookup and a cache entry on every build. `d` and
|
|
20
|
+
`points` values are now blanked before extraction, in every file type.
|
|
21
|
+
Only quoted values are matched, so `d={expr}` bindings still yield their
|
|
22
|
+
classes.
|
|
23
|
+
- **A JavaScript term is no longer reported as a class.** Candidate origins
|
|
24
|
+
were assigned purely by span containment, so any token the whole-file scan
|
|
25
|
+
matched inside a helper call or class attribute inherited that context's
|
|
26
|
+
origin. The token scan's grammar also matches bare identifiers, so
|
|
27
|
+
`ri(mode === "default" ? "fill-white" : "fill-black")` reported `mode` with
|
|
28
|
+
`origin: "helper"` — and editors, which read a certain origin as "this is a
|
|
29
|
+
class", flagged it as an unknown class. A candidate now inherits a context's
|
|
30
|
+
origin only when a context-aware collector tokenized it; containment still
|
|
31
|
+
decides which context wins. Extracted values are unchanged, so no generated
|
|
32
|
+
CSS moves.
|
|
33
|
+
- **Bare unquoted class attributes keep their `attribute` origin.**
|
|
34
|
+
`collectAssignedValues` now tokenizes an undelimited value (`class=flex` in
|
|
35
|
+
HTML, Vue and Svelte) the same way it already tokenized a quoted one. Without
|
|
36
|
+
this they would have lost provenance and demoted to `plain`.
|
|
37
|
+
- **`@apply` no longer emits a rule with no selector.** A rule carrying both
|
|
38
|
+
the `group` marker and a `group-*` variant resolved its group root to
|
|
39
|
+
itself, so stripping the root prefix off the resolved selector left nothing:
|
|
40
|
+
`.self { @apply group group-hover:underline; }` emitted a bare
|
|
41
|
+
` { text-decoration-line: underline; }`, which a browser discards as a parse
|
|
42
|
+
error, taking the declarations with it. The group root is the element, so
|
|
43
|
+
the variant now targets the element — `.self:hover`. The same shape reached
|
|
44
|
+
by climbing rather than matching in place, a nested `&` block inside the
|
|
45
|
+
group root, is fixed with it.
|
|
46
|
+
- **RI-1002 no longer fires for a bracket token the scanner merely read.** An
|
|
47
|
+
unresolved arbitrary value is a typo worth reporting when the author wrote
|
|
48
|
+
the class, but the scanner reads whole files, comments and prose included,
|
|
49
|
+
where `min-[437px]` is just text — and every such token warned. Classes
|
|
50
|
+
written by hand still warn: `@source inline(...)`, `@apply`, and a
|
|
51
|
+
caller-supplied `classNames` list. Provenance is not baked into the compile
|
|
52
|
+
memo, so a class that is both scanned and authored warns once rather than
|
|
53
|
+
never.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- **RI-1412 — whitespace in an arbitrary value now warns.** A class name
|
|
58
|
+
cannot contain whitespace: `class`, `@a`/`@apply`, and `safelist()` all
|
|
59
|
+
split on it, so `bg-[url('a b')]` reaches the browser as the two tokens
|
|
60
|
+
`bg-[url('a` and `b')]` and matches nothing. The scanner has always dropped
|
|
61
|
+
these, silently, leaving no CSS and no reason why. The warning names the
|
|
62
|
+
class and points at the `_` escape (`bg-[url('a_b')]` emits `url('a b')`).
|
|
63
|
+
It fires only where a collector treats its input as a class list — an
|
|
64
|
+
attribute value, a helper argument, a `safelist()` argument — so ordinary
|
|
65
|
+
JS and prose stay quiet, `styles["my class"]` in a `className` expression
|
|
66
|
+
included. Extracted values are unchanged.
|
|
67
|
+
- **`"expression"` candidate origin.** A string literal that is an operand of
|
|
68
|
+
`==`/`!=` (so also `===`/`!==`) cannot be a class list: in
|
|
69
|
+
`mode === "default"`, `"default"` is a value being compared. Such literals
|
|
70
|
+
are still extracted — dropping them would change generated CSS — but they
|
|
71
|
+
now report `origin: "expression"` instead of `"helper"`/`"attribute"`.
|
|
72
|
+
Editors should treat it like `"plain"` and never report it as a bad class.
|
|
73
|
+
Assignment is deliberately not matched: `const base = "px-2"` is an ordinary
|
|
74
|
+
class list.
|
|
75
|
+
- **`candidate-origin-provenance` capability.** Feature-detect both behaviours
|
|
76
|
+
above through `editorCapabilities`, never a version compare.
|
|
77
|
+
- **Functional custom utilities — `@utility name-* { … }`.** The body reads
|
|
78
|
+
`var(--value)` and the class suffix replaces it, nested blocks included:
|
|
79
|
+
`@utility glow-* { box-shadow: 0 0 var(--value) gold; }` answers `glow-4`
|
|
80
|
+
and `glow-[3px]`, with `[2px_4px]` decoding to `2px 4px`. An exact static
|
|
81
|
+
name beats a functional match, so `@utility card` and `@utility card-*`
|
|
82
|
+
coexist; the longest root wins between functional entries (`a-b-*` over
|
|
83
|
+
`a-*` for `a-b-4`); and neither a bare root (`glow`) nor a negated class
|
|
84
|
+
(`-glow-4`) matches. A suffix carrying `;`, `{`, or `}` is rejected rather
|
|
85
|
+
than allowed to break out of the declaration. `ri()` treats two suffixes of
|
|
86
|
+
one root as conflicting, so `ri("glow-4 glow-8")` keeps `glow-8`.
|
|
87
|
+
- **Consumer documentation in [`docs/`](docs/README.md).** One page per
|
|
88
|
+
subject — getting started, class syntax, utilities, theming, fonts, source
|
|
89
|
+
scanning, class merging, diagnostics, environment variables — and one per
|
|
90
|
+
integration surface — CLI, PostCSS plugin, Vite plugin, Vite+, Node API,
|
|
91
|
+
editor API. The README keeps the overview and links out.
|
|
92
|
+
- **Vite+ support.** The Vite plugin now adds every stylesheet that activates
|
|
93
|
+
Rainbow Index to `fmt.ignorePatterns`, so `vp check` no longer stops at
|
|
94
|
+
`Syntax error: component value is expected` before it can lint or type
|
|
95
|
+
check. Directive bodies are not valid CSS — a `@font` entry carries a block
|
|
96
|
+
after a declaration, a scale removes a token with `!name;`, `@fluid` takes
|
|
97
|
+
bare keywords, and `@apply` takes variant groups — and Oxfmt parses CSS
|
|
98
|
+
strictly. Vite+ reads its `fmt` block off the resolved Vite config, so the
|
|
99
|
+
plugin contributes the patterns from its `config` hook; plain Vite ignores
|
|
100
|
+
the extra key. Projects without the Vite plugin add the patterns by hand.
|
|
101
|
+
See [docs/vite-plus.md](docs/vite-plus.md).
|
|
102
|
+
- **`rainbowindex/oxlint`.** A new entry point with one Oxlint rule,
|
|
103
|
+
`prefer-ri`, which reports an import of `clsx`, `classnames`, or
|
|
104
|
+
`tailwind-merge`. Each merges classes against a Tailwind utility table, so
|
|
105
|
+
it resolves conflicts against the wrong utility set and never sees the
|
|
106
|
+
theme. The rule is off until a project enables it, and the plugin has no
|
|
107
|
+
dependencies.
|
|
108
|
+
|
|
109
|
+
### Changed
|
|
110
|
+
|
|
111
|
+
- **`ri()` caches results in two generations instead of an LRU.** The old
|
|
112
|
+
cache moved every hit to the end of a `Map` so insertion order tracked
|
|
113
|
+
recency, which made the steady state — the same class lists on every render
|
|
114
|
+
— pay a delete and a re-insert per call. The new cache keeps a current and a
|
|
115
|
+
previous generation: a hit in the current one is a single `Map.get`, and
|
|
116
|
+
when the current fills it becomes the previous and a fresh one starts. A hit
|
|
117
|
+
on a previous entry promotes it, so hot keys survive the swap while cold
|
|
118
|
+
ones age out with the dropped generation. The cache now holds at most twice
|
|
119
|
+
`RI_CACHE_MAX` (500) entries instead of evicting the oldest quarter at the
|
|
120
|
+
cap, and `evictLRU` is gone with the design that needed it.
|
|
121
|
+
- **The per-class compile memo is bounded.** Keyed on theme identity, it grew
|
|
122
|
+
for the life of the process, so a long dev session could hold every class
|
|
123
|
+
ever compiled against a still-live theme. It now clears wholesale at 50,000
|
|
124
|
+
entries. Steady state is the project's whole scanned vocabulary, which sits
|
|
125
|
+
far below the cap — a lower one would clear mid-compile on every rebuild and
|
|
126
|
+
defeat the memo.
|
|
127
|
+
- **`@apply` resolution is cached per theme, not per invocation.** Every
|
|
128
|
+
rebuild re-resolved every `@apply` class from scratch, although the scan
|
|
129
|
+
analysis and pipeline memos keep the theme object stable across rebuilds, so
|
|
130
|
+
the old results were still valid. The cache is a `WeakMap` keyed on the
|
|
131
|
+
theme and dies with it. The `[RI-1005]` walk of a custom utility body is
|
|
132
|
+
cached alongside: its warnings are now replayed into every class that hits
|
|
133
|
+
the utility rather than emitted once per compile, so they survive a rebuild
|
|
134
|
+
in which the class that first triggered the walk is gone. The plugin still
|
|
135
|
+
dedupes repeats downstream.
|
|
136
|
+
- **The resolved source-file list is cached while a watcher runs.** Each
|
|
137
|
+
rebuild re-globbed the project to find the same files. A cached list is only
|
|
138
|
+
correct while something reacts to file adds and deletes, so caching is
|
|
139
|
+
opt-in: the Vite plugin arms it and clears it from the dev-server watcher
|
|
140
|
+
(`add`, `unlink`, `unlinkDir`). One-shot builds and `postcss-cli --watch`,
|
|
141
|
+
which has no watcher hook, keep the always-fresh glob. A glob already in
|
|
142
|
+
flight when an invalidation arrives is not cached.
|
|
143
|
+
- **A source edit that changes no class no longer re-transforms the CSS.** The
|
|
144
|
+
Vite plugin now keeps a sorted candidate list per source file and compares it
|
|
145
|
+
on every hot update. An edit to logic, comments, or copy leaves that list
|
|
146
|
+
identical, and identical candidates produce byte-identical CSS — so the
|
|
147
|
+
Rainbow Index stylesheets stay out of the update and only the edited module
|
|
148
|
+
reloads. A first sighting or an unreadable file invalidates conservatively.
|
|
149
|
+
- **`preload-fonts --help` names the faces that produce a tag.** Only local
|
|
150
|
+
file and raw-URL faces marked `preload` do. Google serves CSS rather than the
|
|
151
|
+
font binary, so a Google or system slot never emits one — but the help text
|
|
152
|
+
claimed `@font-face` was covered too. Text only; the command is unchanged.
|
|
153
|
+
|
|
154
|
+
### Removed
|
|
155
|
+
|
|
156
|
+
- **The named radius scale.** `rounded-2xs` through `rounded-xl`, the bare
|
|
157
|
+
`rounded` / `rounded-t` / `rounded-tl` shorthands, the `--rounded-*`
|
|
158
|
+
variables, the `--rounded-roof` anchor, and the token body of `@rounded` are
|
|
159
|
+
all gone. A radius is now a spacing multiple: `rounded-4` is
|
|
160
|
+
`calc(var(--spacing) * 4 * var(--ri-rounded-scale, 1))`. `rounded-none`,
|
|
161
|
+
`rounded-full`, `rounded-scale-*`, arbitrary values, and every logical side
|
|
162
|
+
and corner (`rounded-t-4`, `rounded-ss-2`) are unchanged.
|
|
163
|
+
|
|
164
|
+
Two scales set the same shape, so they drifted apart: the named tokens hung
|
|
165
|
+
off `--rounded-roof` while the numeric forms hung off `--spacing`, and
|
|
166
|
+
`rounded-lg` next to `rounded-4` mixed two rhythms in one component. One
|
|
167
|
+
scale, anchored to spacing, keeps the whole system in step.
|
|
168
|
+
|
|
169
|
+
Migration: replace each named token with the spacing step you want. A
|
|
170
|
+
`@rounded` body that sets radius tokens now warns with `[RI-1122]`; keep
|
|
171
|
+
`--corner-scale` and drop the rest. `--roof` is gone — set the radius you
|
|
172
|
+
want on the element.
|
|
173
|
+
|
|
174
|
+
### Notes for integrators
|
|
175
|
+
|
|
176
|
+
`ResolvedTheme` lost `rounded` and `roundedRoof`, `Theme` lost `rounded`,
|
|
177
|
+
`listThemeTokens()` no longer returns a `rounded` record, and
|
|
178
|
+
`CompilationResult` lost `usedRounded`. Nothing prunes `--rounded-*` any more,
|
|
179
|
+
because the token layer no longer emits it. `roundedShape` and
|
|
180
|
+
`roundedShapeScale` stay — `@rounded <shape>` is unchanged.
|
|
181
|
+
|
|
182
|
+
`CandidateOrigin` gained a member. Code that switches on it exhaustively must
|
|
183
|
+
handle `"expression"`; code that tests `origin !== "plain"` to mean "this is a
|
|
184
|
+
class" was already wrong and is now wrong in a new way — test for the origins
|
|
185
|
+
you trust instead.
|
|
186
|
+
|
|
187
|
+
`CompilationSnapshot` gained `customFunctionalProps`, the roots of functional
|
|
188
|
+
`@utility` entries and the properties each one claims. `createThemeSnapshot()`
|
|
189
|
+
fills it in; a snapshot built by hand needs the field.
|
|
190
|
+
|
|
191
|
+
`createCompiler().compile()` takes an optional third argument, the set of
|
|
192
|
+
classes the caller wrote by hand. Omit it and every class counts as authored,
|
|
193
|
+
which is what a caller assembling its own list wants. `compileProject()`
|
|
194
|
+
applies the same split on its own: `classNames` is authored, `sources` content
|
|
195
|
+
is scanned.
|
|
196
|
+
|
|
197
|
+
`getCustomUtility(theme, name)` is replaced by
|
|
198
|
+
`matchCustomUtility(utility, value, negative, theme)`, which returns the
|
|
199
|
+
matching entry and the text a functional body substitutes for `var(--value)`.
|
|
200
|
+
The `@apply` walk and the declaration expansion both go through it, so they
|
|
201
|
+
can never disagree about which utility a class hit. The old function resolved
|
|
202
|
+
static entries only. Neither name is exported from the package entry, so only
|
|
203
|
+
code importing the internal module path is affected.
|
|
204
|
+
|
|
8
205
|
## [0.5.0] - 2026-08-27
|
|
9
206
|
|
|
10
207
|
### Added
|
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@ The product is not a visual design tool, a component library, or a framework abs
|
|
|
20
20
|
|
|
21
21
|
When tradeoffs arise, we consistently prioritize composability, user control, predictable performance, explicit behavior, and correctness over convenience or familiarity.
|
|
22
22
|
|
|
23
|
+
Full consumer documentation lives in [docs/](docs/README.md).
|
|
24
|
+
|
|
23
25
|
## Install
|
|
24
26
|
|
|
25
27
|
```sh
|
|
@@ -67,15 +69,21 @@ export default defineConfig({
|
|
|
67
69
|
```css
|
|
68
70
|
/* src/styles.css */
|
|
69
71
|
@import "rainbowindex";
|
|
72
|
+
|
|
73
|
+
@color {
|
|
74
|
+
brand: 0.18 330;
|
|
75
|
+
}
|
|
70
76
|
```
|
|
71
77
|
|
|
72
78
|
```tsx
|
|
73
79
|
// src/App.tsx
|
|
74
80
|
export default function App() {
|
|
75
|
-
return <div className="flex gap-4 px-6 py-3 bg-
|
|
81
|
+
return <div className="flex gap-4 px-6 py-3 bg-brand-500 text-white">Hello</div>;
|
|
76
82
|
}
|
|
77
83
|
```
|
|
78
84
|
|
|
85
|
+
The default palette ships only the neutral `theme` color (plus `black`, `white`, `paper`, `ink`, `transparent`, `current`, `inherit`) — declare your palette with `@color`.
|
|
86
|
+
|
|
79
87
|
To scaffold a fresh app instead, use the CLI:
|
|
80
88
|
|
|
81
89
|
```sh
|
|
@@ -86,14 +94,15 @@ pnpm dlx rainbowindex init
|
|
|
86
94
|
|
|
87
95
|
## CLI
|
|
88
96
|
|
|
89
|
-
The `rainbowindex` binary exposes
|
|
97
|
+
The `rainbowindex` binary exposes six subcommands. The default is `build`.
|
|
90
98
|
|
|
91
99
|
```
|
|
92
100
|
rainbowindex <glob> [options] Generate CSS from source files
|
|
93
101
|
rainbowindex init Wire Rainbow Index into the current Vite app
|
|
94
102
|
rainbowindex create <dir> Scaffold a Vite app with Rainbow Index ready
|
|
95
103
|
rainbowindex generate-types Generate TypeScript types for ri() autocomplete
|
|
96
|
-
rainbowindex preload-fonts
|
|
104
|
+
rainbowindex preload-fonts Print <link rel="preload"> tags for local faces marked preload
|
|
105
|
+
rainbowindex scan <glob> Print the class names the scanner extracts from files
|
|
97
106
|
```
|
|
98
107
|
|
|
99
108
|
Common flags:
|
|
@@ -103,7 +112,7 @@ Common flags:
|
|
|
103
112
|
| `-o`, `--output <file>` | Output CSS file path. Required with `--watch`. |
|
|
104
113
|
| `--watch` | Re-run on source-file changes (chokidar). |
|
|
105
114
|
| `--minify` | Minification + browser-fallback passes via LightningCSS. `--optimize` is an accepted alias. |
|
|
106
|
-
| `--css <file>` | CSS input with directives. Auto-detected if omitted. |
|
|
115
|
+
| `--css <file>` | CSS input with directives. Auto-detected if omitted. For `init`/`create`: the stylesheet to create or patch (default `src/index.css`). |
|
|
107
116
|
| `--strict` | Drop the string escape hatch in generated types. |
|
|
108
117
|
| `--template <name>` | Vite template to scaffold (default: `react-ts`). |
|
|
109
118
|
|
|
@@ -130,7 +139,7 @@ Options:
|
|
|
130
139
|
|
|
131
140
|
| Option | Type | Description |
|
|
132
141
|
| --- | --- | --- |
|
|
133
|
-
| `sources` | `string[]` | Glob patterns for files to scan. Can also be declared via `@source` in CSS. |
|
|
142
|
+
| `sources` | `string[]` | Glob patterns for files to scan. Can also be declared via `@source` in CSS. Any positive glob (here or via `@source`) replaces the default scan patterns. |
|
|
134
143
|
| `cwd` | `string` | Working directory. Defaults to `process.cwd()`. |
|
|
135
144
|
|
|
136
145
|
## Vite plugin
|
|
@@ -139,7 +148,25 @@ Options:
|
|
|
139
148
|
import rainbowindex from "rainbowindex/vite";
|
|
140
149
|
```
|
|
141
150
|
|
|
142
|
-
Auto-detects your CSS entry, injects a PostCSS config if none exists, supports HMR with file versioning.
|
|
151
|
+
Auto-detects your CSS entry, injects a PostCSS config if none exists, supports HMR with file versioning. The plugin takes no options — to pass PostCSS options, create a `postcss.config.js` and register the PostCSS plugin there yourself.
|
|
152
|
+
|
|
153
|
+
## Vite+
|
|
154
|
+
|
|
155
|
+
[Vite+](https://viteplus.dev) works out of the box. Directive syntax is not valid CSS, so Oxfmt — the formatter behind `vp fmt` and `vp check` — cannot parse a stylesheet that holds directives, and the whole check fails before it lints or type checks. The Vite plugin prevents that: it adds every activated stylesheet to `fmt.ignorePatterns`, and leaves the rest of the project formatted. Without the Vite plugin, add the pattern yourself.
|
|
156
|
+
|
|
157
|
+
One more integration is opt-in:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// vite.config.ts — lint rule: merge classes with ri(), not clsx/tailwind-merge
|
|
161
|
+
export default defineConfig({
|
|
162
|
+
lint: {
|
|
163
|
+
jsPlugins: [{ name: "rainbowindex", specifier: "rainbowindex/oxlint" }],
|
|
164
|
+
rules: { "rainbowindex/prefer-ri": "error" },
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
See [docs/vite-plus.md](docs/vite-plus.md).
|
|
143
170
|
|
|
144
171
|
## Class syntax
|
|
145
172
|
|
|
@@ -152,7 +179,7 @@ Roughly the same surface area as Tailwind: spacing, sizing, typography, color, l
|
|
|
152
179
|
Prefix any utility with one or more variants, separated by `:`.
|
|
153
180
|
|
|
154
181
|
```html
|
|
155
|
-
<button class="bg-
|
|
182
|
+
<button class="bg-brand-500 hover:bg-brand-600 dark:bg-brand-400 sm:px-6">…</button>
|
|
156
183
|
```
|
|
157
184
|
|
|
158
185
|
Supported variants:
|
|
@@ -165,6 +192,8 @@ Supported variants:
|
|
|
165
192
|
- **Attribute selectors** — `data-[state=open]`, `aria-[pressed=true]`
|
|
166
193
|
- **Arbitrary** — `[selector]`, `[@media(...)]`
|
|
167
194
|
|
|
195
|
+
This list is a subset — the full variant tables are in [docs/class-syntax.md](docs/class-syntax.md).
|
|
196
|
+
|
|
168
197
|
### Arbitrary values
|
|
169
198
|
|
|
170
199
|
```html
|
|
@@ -183,24 +212,21 @@ When multiple utilities share the same variant prefix, group them with `{…}` i
|
|
|
183
212
|
|
|
184
213
|
```html
|
|
185
214
|
<!-- These two lines are equivalent -->
|
|
186
|
-
<div class="hover:text-
|
|
187
|
-
<div class="hover:{text-
|
|
215
|
+
<div class="hover:text-brand-500 hover:bg-brand-100 hover:underline">…</div>
|
|
216
|
+
<div class="hover:{text-brand-500 bg-brand-100 underline}">…</div>
|
|
188
217
|
```
|
|
189
218
|
|
|
190
|
-
|
|
219
|
+
Prefixes chain:
|
|
191
220
|
|
|
192
221
|
```html
|
|
193
222
|
<!-- Chained variants -->
|
|
194
|
-
<div class="sm:hover:{bg-
|
|
223
|
+
<div class="sm:hover:{bg-theme-700 text-white}">…</div>
|
|
195
224
|
|
|
196
225
|
<!-- Multiple groups in one class string -->
|
|
197
|
-
<div class="focus:{outline-2 outline-
|
|
198
|
-
|
|
199
|
-
<!-- The data-attribute case -->
|
|
200
|
-
<div class="data-[active]:{relative px-2} data-[slot=sidebar]:{fixed leading-none}">…</div>
|
|
226
|
+
<div class="focus:{outline-2 outline-brand-500} disabled:{opacity-50 cursor-not-allowed}">…</div>
|
|
201
227
|
```
|
|
202
228
|
|
|
203
|
-
Expansion happens at scan-time, so the runtime never sees the grouped form.
|
|
229
|
+
Expansion happens at scan-time, so the runtime never sees the grouped form. Only plain prefixes work — bracketed variants like `data-[state=open]:` cannot prefix a group, and braces do not nest (chain the prefixes instead). Expansion input is capped at 500,000 characters, output at 100,000.
|
|
204
230
|
|
|
205
231
|
## Theming with CSS directives
|
|
206
232
|
|
|
@@ -211,8 +237,8 @@ Customization happens in your CSS input, not a JS config. The engine recognizes:
|
|
|
211
237
|
| `@color` | Define color tokens. Supports generative (`chroma hue`), explicit (`oklch(...)`, `#rrggbb`), light/dark pairs, and aliases. |
|
|
212
238
|
| `@spacing` | Set the spacing base unit. |
|
|
213
239
|
| `@text` | Define text size tokens (`size, line-height`). |
|
|
214
|
-
| `@font
|
|
215
|
-
| `@rounded` |
|
|
240
|
+
| `@font` | Register font families inside a single `@font { … }` block. Local files are repeatable `face:` entries (e.g. upright + italic). Known families get an automatic zero-CLS metrics fallback (`metrics: none` opts out). Plain `@font-face` rules are standard CSS and pass through untouched. |
|
|
241
|
+
| `@rounded` | Corner shape (`round`, `squircle`, `superellipse(N)`, etc). Radii are spacing multiples, so there are no radius tokens. |
|
|
216
242
|
| `@fluid` | Configure fluid type/spacing range. |
|
|
217
243
|
| `@animate` | Register named animations with inline `@keyframes`. |
|
|
218
244
|
| `@utility` | Define a custom utility (static or functional `name-*`). |
|
|
@@ -221,7 +247,10 @@ Customization happens in your CSS input, not a JS config. The engine recognizes:
|
|
|
221
247
|
| `@slot` | Slot marker inside `@custom` block form. |
|
|
222
248
|
| `@source` | Declare additional source globs from CSS. Supports `not "..."` and `inline("...")`. |
|
|
223
249
|
| `@preflight` | Toggle preflight base styles. |
|
|
224
|
-
| `@
|
|
250
|
+
| `@breakpoint`, `@shadow`, `@weight`, `@ease`, `@blur`, `@z`, `@leading`, `@tracking`, `@opacity`, `@duration` | Key-value token scales; `!key;` removes a token. |
|
|
251
|
+
| `@register` | Emit CSS `@property` registrations. |
|
|
252
|
+
| `@layer` | Place the generated output in cascade layers (intercepted, own grammar). |
|
|
253
|
+
| `@media`, `@import`, other standard at-rules | Standard CSS — passed through untouched. |
|
|
225
254
|
|
|
226
255
|
Example:
|
|
227
256
|
|
|
@@ -250,11 +279,11 @@ Example:
|
|
|
250
279
|
}
|
|
251
280
|
}
|
|
252
281
|
|
|
253
|
-
@source "
|
|
254
|
-
@source not "
|
|
282
|
+
@source "emails/**/*.html";
|
|
283
|
+
@source not "src/**/legacy/*";
|
|
255
284
|
```
|
|
256
285
|
|
|
257
|
-
Default theme
|
|
286
|
+
Default theme scales: `colors`, `spacing`, `text`, `breakpoints`, `shadows`, `weights`, `easing`, `fluid`, `animations`, `blur`, `leading`, `tracking`. The `z`, `opacity`, and `duration` scales ship empty — numeric class forms are computed, and their directives add named tokens.
|
|
258
287
|
|
|
259
288
|
## `ri()` — runtime class merger
|
|
260
289
|
|
|
@@ -263,8 +292,8 @@ Default theme keys: `colors`, `spacing`, `text`, `breakpoints`, `rounded`, `shad
|
|
|
263
292
|
```ts
|
|
264
293
|
import { ri } from "rainbowindex";
|
|
265
294
|
|
|
266
|
-
ri("px-2 py-1", isActive && "bg-
|
|
267
|
-
// → "py-1 bg-
|
|
295
|
+
ri("px-2 py-1", isActive && "bg-brand-500", "px-4");
|
|
296
|
+
// → "py-1 bg-brand-500 px-4" (px-2 is dropped — px-4 wins)
|
|
268
297
|
```
|
|
269
298
|
|
|
270
299
|
Accepted inputs:
|
|
@@ -297,17 +326,17 @@ themes in the same Node process, that shared state will leak — use
|
|
|
297
326
|
// Anywhere ri() is single-compile-safe (browser, Vite, PostCSS):
|
|
298
327
|
import { ri } from "rainbowindex";
|
|
299
328
|
|
|
300
|
-
const className = ri("px-2 py-1", isActive && "bg-
|
|
329
|
+
const className = ri("px-2 py-1", isActive && "bg-brand-500", "px-4");
|
|
301
330
|
```
|
|
302
331
|
|
|
303
332
|
```ts
|
|
304
|
-
// SSR / multi-tenant —
|
|
305
|
-
import { compileProject, createRi
|
|
333
|
+
// SSR / multi-tenant — bind a merger to one compile, isolated per theme:
|
|
334
|
+
import { compileProject, createRi } from "rainbowindex";
|
|
335
|
+
import { createThemeSnapshot } from "rainbowindex/editor";
|
|
306
336
|
|
|
307
337
|
// At server startup (once per theme):
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
const ri = createRi(snapshot);
|
|
338
|
+
const result = await compileProject({ css });
|
|
339
|
+
const ri = createRi(createThemeSnapshot(result.theme));
|
|
311
340
|
|
|
312
341
|
// In your request handler:
|
|
313
342
|
function render(req, res) {
|
|
@@ -316,9 +345,9 @@ function render(req, res) {
|
|
|
316
345
|
}
|
|
317
346
|
```
|
|
318
347
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
348
|
+
In any Node/SSR process, the default `ri()` emits a throttled `[RI-2004]`
|
|
349
|
+
warning (at most once per 60 s) to flag its shared module state. Switching to
|
|
350
|
+
`createRi(snapshot)` silences it.
|
|
322
351
|
|
|
323
352
|
## Editor tooling API
|
|
324
353
|
|
|
@@ -370,16 +399,16 @@ Warnings carry `RI-NNNN` codes. Ranges:
|
|
|
370
399
|
|
|
371
400
|
| Range | Subsystem |
|
|
372
401
|
| --- | --- |
|
|
402
|
+
| 00xx | PostCSS plugin bootstrap (thrown) |
|
|
373
403
|
| 10xx | Compilation & directives |
|
|
374
|
-
| 11xx | Color directives |
|
|
404
|
+
| 11xx | Color directives & resolver catch-alls |
|
|
375
405
|
| 12xx | Font system |
|
|
376
406
|
| 13xx | Merge / compilation context |
|
|
377
407
|
| 14xx | Source scanner |
|
|
378
408
|
| 15xx | Typography utilities |
|
|
379
409
|
| 16xx | Integration plugins (Vite, PostCSS, CLI wiring) |
|
|
380
|
-
| 20xx | CSS
|
|
381
|
-
| 21xx | `ri()` runtime |
|
|
410
|
+
| 20xx | CSS functions, `ri()` runtime & `compile()` validation |
|
|
382
411
|
|
|
383
|
-
See
|
|
412
|
+
See [docs/diagnostics.md](docs/diagnostics.md) for the full code → cause → fix table.
|
|
384
413
|
|
|
385
414
|
Warnings are deduplicated and capped at 200 per compile, with 20 slots reserved for high-severity errors.
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { C as ColorDefinition, a as CompilationContext, b as CompilationSnapshot, F as FluidConfig, T as TextSize, c as Theme, d as createCompilationContext, e as defaultTheme, f as finalizeCompilationContext, r as registerColorNames, g as registerCustomFontFamilies, h as registerCustomTextSizes, i as registerCustomUtility } from './context-
|
|
2
|
-
export {
|
|
1
|
+
export { C as ColorDefinition, a as CompilationContext, b as CompilationSnapshot, D as DEFAULT_TEXT_SIZES, F as FluidConfig, T as TextSize, c as Theme, d as createCompilationContext, e as defaultTheme, f as finalizeCompilationContext, r as registerColorNames, g as registerCustomFontFamilies, h as registerCustomTextSizes, i as registerCustomUtility } from './context-B9yhJxd5.js';
|
|
2
|
+
export { c as createRi, r as ri, s as safelist } from './safelist-DAkKuxCk.js';
|
|
3
3
|
|
|
4
4
|
declare function browserEntryUnavailable(): never;
|
|
5
5
|
|
package/dist/browser.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
compileScannedProject
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-F4VCBISU.mjs";
|
|
4
4
|
import {
|
|
5
5
|
APPLY_ALIASES,
|
|
6
6
|
DIRECTIVE_NAMES_SET,
|
|
@@ -11,16 +11,16 @@ import {
|
|
|
11
11
|
computeSortKey,
|
|
12
12
|
expandVariantGroups,
|
|
13
13
|
forEachApplyClass,
|
|
14
|
-
getCustomUtility,
|
|
15
14
|
hasApplyLikeDirective,
|
|
16
15
|
hasCSSFunctions,
|
|
17
16
|
hasRIActivation,
|
|
17
|
+
matchCustomUtility,
|
|
18
18
|
parseUtility,
|
|
19
19
|
pushWarningsDeduped,
|
|
20
20
|
resolveUtilityDeclarations,
|
|
21
21
|
resolveVariant,
|
|
22
22
|
splitSelectorList
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-PDORZSQX.mjs";
|
|
24
24
|
|
|
25
25
|
// src/integrations/postcss/index.ts
|
|
26
26
|
import postcss2 from "postcss";
|
|
@@ -28,6 +28,7 @@ import { isAbsolute } from "path";
|
|
|
28
28
|
|
|
29
29
|
// src/integrations/postcss/apply.ts
|
|
30
30
|
import postcss from "postcss";
|
|
31
|
+
var applyStateByTheme = /* @__PURE__ */ new WeakMap();
|
|
31
32
|
function makeDeclGroup(r) {
|
|
32
33
|
const sortKey = computeSortKey(r.declarations[0]?.property ?? "");
|
|
33
34
|
return {
|
|
@@ -100,9 +101,16 @@ function processApply(root, theme, warnings) {
|
|
|
100
101
|
atRule.name = "apply";
|
|
101
102
|
});
|
|
102
103
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
let state = applyStateByTheme.get(theme);
|
|
105
|
+
if (!state) {
|
|
106
|
+
state = {
|
|
107
|
+
customVariantMap: new Map(theme.customVariants.map((cv) => [cv.name, cv])),
|
|
108
|
+
applyRootWarnings: /* @__PURE__ */ new Map(),
|
|
109
|
+
resolveCache: /* @__PURE__ */ new Map()
|
|
110
|
+
};
|
|
111
|
+
applyStateByTheme.set(theme, state);
|
|
112
|
+
}
|
|
113
|
+
const { customVariantMap, applyRootWarnings, resolveCache } = state;
|
|
106
114
|
for (let depth = 0; depth < MAX_APPLY_DEPTH; depth++) {
|
|
107
115
|
const applyNodes = [];
|
|
108
116
|
const classListByNode = /* @__PURE__ */ new Map();
|
|
@@ -128,7 +136,7 @@ function processApply(root, theme, warnings) {
|
|
|
128
136
|
warnings,
|
|
129
137
|
customVariantMap,
|
|
130
138
|
groupRoots,
|
|
131
|
-
|
|
139
|
+
applyRootWarnings,
|
|
132
140
|
resolveCache
|
|
133
141
|
);
|
|
134
142
|
}
|
|
@@ -146,7 +154,7 @@ function processApply(root, theme, warnings) {
|
|
|
146
154
|
}
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
|
-
function expandApply(atRule, classNames, theme, warnings, customVariantMap, groupRoots,
|
|
157
|
+
function expandApply(atRule, classNames, theme, warnings, customVariantMap, groupRoots, applyRootWarnings, resolveCache) {
|
|
150
158
|
const parentRule = atRule.parent;
|
|
151
159
|
if (parentRule?.type !== "rule") {
|
|
152
160
|
warnings.push("[RI-1006] @apply must be used inside a CSS rule, not at the top level.");
|
|
@@ -177,7 +185,7 @@ function expandApply(atRule, classNames, theme, warnings, customVariantMap, grou
|
|
|
177
185
|
theme,
|
|
178
186
|
entryWarnings,
|
|
179
187
|
customVariantMap,
|
|
180
|
-
|
|
188
|
+
applyRootWarnings
|
|
181
189
|
),
|
|
182
190
|
warnings: entryWarnings
|
|
183
191
|
};
|
|
@@ -252,19 +260,24 @@ function expandApply(atRule, classNames, theme, warnings, customVariantMap, grou
|
|
|
252
260
|
parentContainer.insertAfter(insertAfter, rule);
|
|
253
261
|
insertAfter = rule;
|
|
254
262
|
}
|
|
255
|
-
const groupInfoByKey = /* @__PURE__ */ new Map();
|
|
256
|
-
let hasGroupVariants = false;
|
|
257
|
-
for (const [key, bucket] of variantBuckets) {
|
|
258
|
-
const info = resolveGroupVariantInfo(bucket.wrappers, parentRule, groupRoots);
|
|
259
|
-
groupInfoByKey.set(key, info);
|
|
260
|
-
if (info) hasGroupVariants = true;
|
|
261
|
-
}
|
|
262
263
|
let fullSelectorMemo = null;
|
|
263
264
|
const fullNestingSelector = () => {
|
|
264
265
|
if (fullSelectorMemo === null)
|
|
265
266
|
fullSelectorMemo = resolveFullNestingSelector(parentRule);
|
|
266
267
|
return fullSelectorMemo;
|
|
267
268
|
};
|
|
269
|
+
const groupInfoByKey = /* @__PURE__ */ new Map();
|
|
270
|
+
let hasGroupVariants = false;
|
|
271
|
+
for (const [key, bucket] of variantBuckets) {
|
|
272
|
+
const info = resolveGroupVariantInfo(
|
|
273
|
+
bucket.wrappers,
|
|
274
|
+
parentRule,
|
|
275
|
+
groupRoots,
|
|
276
|
+
fullNestingSelector
|
|
277
|
+
);
|
|
278
|
+
groupInfoByKey.set(key, info);
|
|
279
|
+
if (info) hasGroupVariants = true;
|
|
280
|
+
}
|
|
268
281
|
let docRootMemo = null;
|
|
269
282
|
const docRoot = () => {
|
|
270
283
|
if (docRootMemo === null) docRootMemo = parentContainer.root();
|
|
@@ -276,7 +289,7 @@ function expandApply(atRule, classNames, theme, warnings, customVariantMap, grou
|
|
|
276
289
|
if (groupVariantInfo) {
|
|
277
290
|
const fullSelector = fullNestingSelector();
|
|
278
291
|
const { groupRootSelector } = groupVariantInfo;
|
|
279
|
-
const rootBranches = splitSelectorList(groupRootSelector);
|
|
292
|
+
const rootBranches = groupVariantInfo.isSelf ? [] : splitSelectorList(groupRootSelector);
|
|
280
293
|
const fullBranches = splitSelectorList(fullSelector);
|
|
281
294
|
const baseBranches = [];
|
|
282
295
|
for (const fb of fullBranches) {
|
|
@@ -332,7 +345,7 @@ function expandApply(atRule, classNames, theme, warnings, customVariantMap, grou
|
|
|
332
345
|
}
|
|
333
346
|
atRule.remove();
|
|
334
347
|
}
|
|
335
|
-
function resolveGroupVariantInfo(wrappers, parentRule, groupRoots) {
|
|
348
|
+
function resolveGroupVariantInfo(wrappers, parentRule, groupRoots, fullNestingSelector) {
|
|
336
349
|
if (groupRoots.size === 0) return null;
|
|
337
350
|
const hasGroupVariant = wrappers.some(
|
|
338
351
|
(w) => w.selectorSuffix && w.replaceAmpersand && GROUP_VARIANT_RE.test(w.selectorSuffix)
|
|
@@ -340,23 +353,23 @@ function resolveGroupVariantInfo(wrappers, parentRule, groupRoots) {
|
|
|
340
353
|
if (!hasGroupVariant) return null;
|
|
341
354
|
const groupInfo = findGroupAncestor(parentRule, groupRoots);
|
|
342
355
|
if (!groupInfo) return null;
|
|
356
|
+
const isSelf = fullNestingSelector() === groupInfo.groupRootSelector;
|
|
343
357
|
const rewrittenWrappers = wrappers.map((w) => {
|
|
344
358
|
if (!w.selectorSuffix || !w.replaceAmpersand) return w;
|
|
345
359
|
const match = w.selectorSuffix.match(GROUP_VARIANT_RE);
|
|
346
360
|
if (!match) return w;
|
|
347
361
|
const pseudo = match[1];
|
|
362
|
+
if (isSelf) return { ...w, selectorSuffix: `&${pseudo}` };
|
|
348
363
|
const rootBranches = splitSelectorList(groupInfo.groupRootSelector);
|
|
349
364
|
const newSuffix = rootBranches.map((b) => `${b}${pseudo} &`).join(", ");
|
|
350
365
|
return { ...w, selectorSuffix: newSuffix };
|
|
351
366
|
});
|
|
352
|
-
return { rewrittenWrappers, groupRootSelector: groupInfo.groupRootSelector };
|
|
367
|
+
return { rewrittenWrappers, groupRootSelector: groupInfo.groupRootSelector, isSelf };
|
|
353
368
|
}
|
|
354
369
|
var MARKER_CLASSES = /* @__PURE__ */ new Set(["group"]);
|
|
355
370
|
var MAX_CUSTOM_APPLY_DEPTH = 5;
|
|
356
|
-
function findCustomUtility(utility, value, theme) {
|
|
357
|
-
|
|
358
|
-
const cu = getCustomUtility(theme, target);
|
|
359
|
-
return cu && !cu.functional ? cu : void 0;
|
|
371
|
+
function findCustomUtility(utility, value, negative, theme) {
|
|
372
|
+
return matchCustomUtility(utility, value, negative, theme)?.cu;
|
|
360
373
|
}
|
|
361
374
|
function checkCustomApplyWarnings(cu, theme, warnings, visiting = /* @__PURE__ */ new Set()) {
|
|
362
375
|
if (!hasApplyLikeDirective(cu.body)) return;
|
|
@@ -378,12 +391,12 @@ function checkCustomApplyWarnings(cu, theme, warnings, visiting = /* @__PURE__ *
|
|
|
378
391
|
visiting.add(cu.name);
|
|
379
392
|
for (const innerClass of innerClasses) {
|
|
380
393
|
const parsed = parseUtility(innerClass);
|
|
381
|
-
const innerCu = findCustomUtility(parsed.utility, parsed.value, theme);
|
|
394
|
+
const innerCu = findCustomUtility(parsed.utility, parsed.value, parsed.negative, theme);
|
|
382
395
|
if (innerCu) checkCustomApplyWarnings(innerCu, theme, warnings, visiting);
|
|
383
396
|
}
|
|
384
397
|
visiting.delete(cu.name);
|
|
385
398
|
}
|
|
386
|
-
function resolveClassName(className, theme, warnings, customVariantMap,
|
|
399
|
+
function resolveClassName(className, theme, warnings, customVariantMap, applyRootWarnings) {
|
|
387
400
|
const parsed = parseUtility(className);
|
|
388
401
|
if (parsed.variants.length === 0 && MARKER_CLASSES.has(parsed.utility) && parsed.value === null) {
|
|
389
402
|
return null;
|
|
@@ -394,10 +407,15 @@ function resolveClassName(className, theme, warnings, customVariantMap, checkedA
|
|
|
394
407
|
return null;
|
|
395
408
|
}
|
|
396
409
|
const declarations = [...utilResult.declarations];
|
|
397
|
-
const cu = findCustomUtility(parsed.utility, parsed.value, theme);
|
|
398
|
-
if (cu
|
|
399
|
-
|
|
400
|
-
|
|
410
|
+
const cu = findCustomUtility(parsed.utility, parsed.value, parsed.negative, theme);
|
|
411
|
+
if (cu) {
|
|
412
|
+
let cuWarnings = applyRootWarnings.get(cu.name);
|
|
413
|
+
if (!cuWarnings) {
|
|
414
|
+
cuWarnings = [];
|
|
415
|
+
checkCustomApplyWarnings(cu, theme, cuWarnings);
|
|
416
|
+
applyRootWarnings.set(cu.name, cuWarnings);
|
|
417
|
+
}
|
|
418
|
+
warnings.push(...cuWarnings);
|
|
401
419
|
}
|
|
402
420
|
const variantWrappers = [];
|
|
403
421
|
for (const variant of parsed.variants) {
|