rainbowindex 0.6.0 → 0.7.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +543 -0
  2. package/LICENSE +16 -17
  3. package/NOTICE.md +106 -0
  4. package/README.md +219 -65
  5. package/dist/browser.d.ts +4 -2
  6. package/dist/browser.mjs +12 -4
  7. package/dist/chunk-2T7V5XLK.mjs +912 -0
  8. package/dist/chunk-6OORICWF.mjs +16 -0
  9. package/dist/{chunk-KSNYSR3C.mjs → chunk-FJOZJIKB.mjs} +2499 -329
  10. package/dist/chunk-L56IRO7A.mjs +491 -0
  11. package/dist/chunk-PZDVDEZJ.mjs +196 -0
  12. package/dist/{chunk-3LWJTLOJ.mjs → chunk-RC6DDE4L.mjs} +23 -15
  13. package/dist/chunk-TQJYVQPE.mjs +217 -0
  14. package/dist/chunk-W756NVYI.mjs +33 -0
  15. package/dist/chunk-WBESS2ZD.mjs +598 -0
  16. package/dist/{chunk-3HRMFZGE.mjs → chunk-X66Z2YHT.mjs} +2 -1
  17. package/dist/{chunk-6U4IOFOS.mjs → chunk-XQGSG2HK.mjs} +199 -555
  18. package/dist/cli.mjs +1077 -123
  19. package/dist/{index-DSgpB6bS.d.ts → context-DcBtnnan.d.ts} +47 -103
  20. package/dist/editor.d.ts +71 -433
  21. package/dist/editor.mjs +51 -368
  22. package/dist/eslint.d.ts +16 -0
  23. package/dist/eslint.mjs +32 -0
  24. package/dist/{index-4Kyaq3IZ.d.ts → imports-C9esHd5Q.d.ts} +78 -84
  25. package/dist/index-CNqdL5U0.d.ts +56 -0
  26. package/dist/index-Czx-EUwh.d.ts +138 -0
  27. package/dist/index-DW8YSxTz.d.ts +104 -0
  28. package/dist/index.d.ts +46 -5
  29. package/dist/index.mjs +33 -9
  30. package/dist/oxlint.d.ts +21 -3
  31. package/dist/oxlint.mjs +19 -1
  32. package/dist/recipe.d.ts +111 -0
  33. package/dist/recipe.mjs +71 -0
  34. package/dist/safelist-CH3_PywB.d.ts +43 -0
  35. package/dist/session-CMaskdB7.d.ts +543 -0
  36. package/dist/tailwind.css +644 -0
  37. package/dist/theme-CIZiGlce.d.ts +115 -0
  38. package/dist/vite.d.ts +10 -1
  39. package/dist/vite.mjs +266 -118
  40. package/package.json +27 -5
  41. package/dist/chunk-WK6S4HTC.mjs +0 -1921
  42. package/dist/chunk-ZR7XJMUN.mjs +0 -251
  43. package/dist/safelist-CGCtF-Fr.d.ts +0 -96
package/CHANGELOG.md CHANGED
@@ -5,6 +5,549 @@ 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
+ ## [Unreleased]
9
+
10
+ ## [0.7.0] - 2026-09-07
11
+
12
+ ### Added
13
+
14
+ - **`rainbowindex migrate tailwind`.** Translates a Tailwind v4 CSS entry into
15
+ directives — `--color-*` to `@color`, `--text-*` plus its `--line-height` to
16
+ one `@text` entry, `--animate-*` joined to its `@keyframes`, a bare
17
+ `--shadow`/`--radius`/`--blur` to the scale's `DEFAULT` — adds the Tailwind
18
+ preset so every familiar class keeps resolving, and carries a `dark` custom
19
+ variant across as `@color dark { variant: selector(.dark) }`. Then it checks
20
+ the classes *you wrote* against the migrated theme and reports what stops
21
+ resolving, grouped by why. Plugins, a v3 JS config, and namespaces with no
22
+ counterpart are listed in `migration-report.md` with what to do instead —
23
+ never dropped quietly. **Nothing is overwritten without `--write`.** See
24
+ [`docs/migrating.md`](docs/migrating.md).
25
+ - **Every directive spelling is valid CSS now.** All four forms that were
26
+ not have a spelling that is, and both are accepted:
27
+
28
+ | Write | Instead of |
29
+ | --- | --- |
30
+ | `sans { family: "Inter"; from: google; }` | `sans: "Inter" from google { … }` |
31
+ | `spin { animation: spin 1s linear; @keyframes spin { … } }` | `spin: spin 1s linear { … }` |
32
+ | `punchy { ramp: 0.18 330; }` | `punchy: 0.18 330 { … }` |
33
+ | `brand: initial;` | `!brand;` |
34
+ | `inline: true;` / `parabolic: false;` | `inline;` / `no-parabolic;` |
35
+ | `@apply hover:(px-2 py-1);` | `@apply hover:{px-2 py-1};` |
36
+
37
+ The old forms keep working and warn `RI-1046` naming the replacement; they go
38
+ at 1.0. A test asserts the two spellings produce byte-identical generated CSS.
39
+
40
+ **The payoff: your stylesheets are no longer hidden from your formatter.** The
41
+ Vite plugin adds a file to `fmt.ignorePatterns` only when it still uses one of
42
+ the deprecated forms, so a project written the canonical way has nothing
43
+ excluded from `vp fmt` / `vp check`. And `@apply hover:(…)` expands under
44
+ plain PostCSS, where the brace form is a parse error before any plugin runs.
45
+ - **Three new `@animate` diagnostics**, all for mistakes that used to be silent.
46
+ `RI-1047`: an entry with an `animation:` and no `@keyframes` is read as a
47
+ custom utility and animates nothing. `RI-1048`: keyframes named after
48
+ something other than the entry, which the emitter renames anyway. `RI-1049`:
49
+ a shorthand that never names its own entry, so the class sets no
50
+ `animation-name` — which the `@animate` documentation's own example did.
51
+ - **`rainbowindex/editor` grew the surface an editor integration needs**:
52
+ `import-inlining` (directives in an `@import`ed file are read, given a host
53
+ resolver), `class-sorting`, `stylesheet-rendering` and a serializable
54
+ snapshot. The [VS Code extension](https://github.com/miloag/extension) —
55
+ published separately — is the reference consumer; see
56
+ [`docs/editor.md`](docs/editor.md).
57
+ - **A documented preset protocol for design-system packages**, at
58
+ [`docs/preset-protocol.md`](docs/preset-protocol.md). A package exports a
59
+ stylesheet of directives (`exports["./rainbow.css"]`) and declares where its
60
+ own classes live (`rainbowindex.safelistSources`); a consumer writes one
61
+ `@import` and gets both. No plugin API, no registry, no `@source` for
62
+ `node_modules`. Two new example packages prove it, and the consumer fails its
63
+ own build when either half stops working.
64
+ - **`rainbowindex/recipe`.** `recipe({ base, variants, compoundVariants,
65
+ defaultVariants })` returns a function from typed variant props to a class
66
+ string — the `cva`/`tv` shape, merged through `ri()` so conflicts resolve
67
+ against your compiled theme rather than a Tailwind utility table:
68
+ `recipe({ base: "p-2", variants: { size: { lg: "p-8" } } })({ size: "lg" })`
69
+ is `"p-8"`, not `"p-2 p-8"`. An unknown option is a type error, a boolean
70
+ group takes a boolean, `null` opts out of a default, and the caller's
71
+ `class`/`className` always merges last. `recipe` joins `cva` and `tv` on the
72
+ scanner's variant-helper list, so its classes reach a build with no
73
+ configuration. See [`docs/recipe.md`](docs/recipe.md).
74
+ - **Two theme-aware lint rules, for Oxlint and ESLint.**
75
+ `no-unknown-class` reports a class in a class position that compiles to
76
+ nothing, with the reason and — when the name is a near miss — an editor
77
+ suggestion that rewrites only the failing fragment, so `hovr:flex` becomes
78
+ `hover:flex` rather than losing its variant. `no-conflicting-classes` reports
79
+ a class that another class **in the same string** erases; separate arguments
80
+ to `ri()` never conflict, because that is what the function is for. Both
81
+ locate your CSS entry on their own and follow it as it changes. Enable them
82
+ through `rainbowindex/oxlint` or the new `rainbowindex/eslint` flat-config
83
+ entry — see [`docs/lint.md`](docs/lint.md).
84
+ - **`outermostCandidates` on `rainbowindex/editor`.** One candidate per class
85
+ the author wrote. The scanner over-collects on purpose, and in JavaScript it
86
+ emits the fragments around each `:` as well as the joined token, so anything
87
+ that turns a candidate into an underline needs this filter first — without
88
+ it, `sm` inside `sm:flex` reads as an unknown class. Feature-detect with the
89
+ `candidate-spans-deduped` capability.
90
+ - **A stability contract**, at [`docs/stability.md`](docs/stability.md). What is
91
+ already treated as a contract before 1.0 and what is deliberately not, the
92
+ two-minor deprecation window, the supported Node versions, and a browser floor
93
+ that was measured against the minifier rather than assumed — `--minify` lowers
94
+ `light-dark()`, `oklch()` and nesting, but cannot lower a `color-mix()` over a
95
+ `var()`, which is what an alpha modifier on a theme colour emits.
96
+ - **`sortClasses` on `rainbowindex/editor`.** Orders a class list the way the
97
+ generated stylesheet orders its rules, so an editor's "sort classes" command,
98
+ a formatter plugin and a codemod cannot drift apart: they call the same
99
+ function. Classes the compiler emits nothing for come first in their original
100
+ order, duplicates are kept, and sorting twice changes nothing.
101
+ `EditorSession.sortClasses` is the same call bound to a session. It is **not**
102
+ for a list that reaches `ri()`, which is right-most-wins over its arguments.
103
+ Feature-detect with the `class-sorting` capability.
104
+ - **`renderStylesheet` on `rainbowindex/editor`.** The first export that
105
+ produces CSS rather than answering a question about it: `createCompiler()`
106
+ plus `assembleSections()`, the same pair the PostCSS plugin and the CLI run.
107
+ `EditorSession.render(classNames)` is the same call bound to a session's
108
+ theme. Two steps of a real build are deliberately absent — Google font weights
109
+ are not narrowed (a network call) and `@apply` is not expanded (PostCSS's
110
+ job); a test asserts byte-for-byte equality with `compileProject` for
111
+ everything else. Feature-detect with the `stylesheet-rendering` capability.
112
+ See [`docs/editor-api.md`](docs/editor-api.md#rendering-a-stylesheet).
113
+ - **`rainbowindex generate-tokens`.** Writes the theme as data for the places a
114
+ class cannot go — a chart's series colors, a canvas fill, a Figma import.
115
+ `rainbowindex-tokens.ts` gives `tokens.color.brand[500]` typed `as const`, as
116
+ `var()` references so they still follow the cascade; `tokens.json` gives the
117
+ same theme in the [W3C Design Tokens](https://tr.designtokens.org/format/)
118
+ format with values resolved, which Style Dictionary consumes directly. See
119
+ [`docs/cli.md`](docs/cli.md#generate-tokens).
120
+ - **Framework examples and guides.** Five buildable apps in
121
+ [`examples/`](examples) — Vite + React, React Router, SvelteKit, Astro and
122
+ Next.js — plus [`docs/frameworks.md`](docs/frameworks.md), which is the
123
+ copy-paste version of each. CI builds all five, because a framework
124
+ integration breaks in ways a unit suite cannot see.
125
+ - **A benchmark harness and a real Tailwind parity number.** `pnpm bench`
126
+ compares Rainbow Index, Tailwind v4 and UnoCSS on a generated codebase across
127
+ cold build, rebuild, scan, output size and peak memory, one process per
128
+ measurement. `pnpm bench:parity` asks Tailwind for its own complete class list
129
+ — 23,289 names — and reports which ones Rainbow Index does not implement:
130
+ currently **98.7%**, the remainder being the deliberately deferred
131
+ `ring-offset-*` family and `ring-inset`. The published numbers are in
132
+ [`README.md`](README.md#benchmarks) with the method in
133
+ [`bench/README.md`](bench/README.md); they say plainly that Rainbow Index has
134
+ the smallest output of the three and the slowest rebuild.
135
+ - **Bare `transform`, `filter` and `backdrop-filter`.** The chain-enabling
136
+ spellings a Tailwind v3 codebase is full of. Each emits the composed chain
137
+ with no contribution of its own; `transform` is the same declaration as
138
+ `transform-cpu`, as it is upstream.
139
+ - **Fractional `translate` values.** `translate-x-1/3`, `-translate-y-2/3` and
140
+ the rest of the 26-value fraction table, on `translate-x`, `translate-y` and
141
+ the shorthand — 150 classes where only `1/2` had worked. Plus `translate-3d`.
142
+ - **`min-w-auto`, `min-h-auto`, `min-inline-auto`, `min-block-auto`.** `auto`
143
+ stays off the `max-*` roots, where it is not valid CSS.
144
+ - **`basis-px`**, closing the one gap in `basis-*`'s spacing coverage.
145
+ - **`shadow-inner`**, and **`shadow-initial` / `inset-shadow-initial` /
146
+ `text-shadow-initial`**, which unset a family's colour variable so the shadow
147
+ value's own colour applies again — for `shadow-red-500 dark:shadow-initial`.
148
+
149
+ ### Changed
150
+
151
+ - **`[RI-2004]` warns about a real problem instead of a category of setup.** It
152
+ now fires once per process, only when `ri()` merges a class whose meaning
153
+ depends on the theme while no theme has been published, and it names the class
154
+ it had to guess about. Previously it fired on *every* `ri()` call in *any*
155
+ Node process, throttled to once a minute, whether or not a theme was
156
+ published — which warned loudest at the setup that was already correct. It
157
+ also now fires in the browser, where the wrong answer actually ships.
158
+ - `pnpm check` runs `pnpm format:check` first, so the local gate and CI match.
159
+ - `packageManager` is `pnpm@10.34.5`. The `minimumReleaseAge` and `trustPolicy`
160
+ settings in `pnpm-workspace.yaml` need pnpm >= 10.21 and were silently
161
+ ignored by the pinned 10.12.1; the 7-day hold on freshly published dependency
162
+ versions is now actually enforced.
163
+
164
+ ### Fixed
165
+
166
+ - **The `@animate` documentation showed an animation that cannot run.** Its
167
+ example wrote `shimmer: 2s linear infinite { … }` — a shorthand with no
168
+ animation-name, so the emitted class set none and the keyframes never played.
169
+ `RI-1049` now reports the shape, and the example names the animation.
170
+ - **Emitted `@keyframes` carried the source's own indentation**, so the same
171
+ animation written two ways produced two different stylesheets. The body is
172
+ normalised and indented by the emitter.
173
+ - **`rainbowindex/editor` could not be bundled for a browser.** The entry has
174
+ always documented itself as `node:`-free, and CSS assembly pulled the font
175
+ barrel, which re-exports the Google client and its on-disk metadata cache —
176
+ `node:crypto`, `node:fs/promises`, `node:path`. Nothing in a Node test suite
177
+ could see it. The pure `@font-face` emission is now its own module and the
178
+ barrel re-exports both halves, so Node consumers are unaffected, and a test
179
+ bundles the entry with esbuild's browser platform and fails on the first
180
+ builtin that reaches the graph.
181
+ - **`shadow-{color}` did nothing.** Every shadow family wrote a colour variable
182
+ that no emitted rule read, so `shadow-red-500`, `inset-shadow-red-500`,
183
+ `text-shadow-red-500` and `drop-shadow-red-500` had never worked, under any
184
+ theme — four documented families, silently inert. Each family's value now
185
+ carries the slot: `shadow-md` emits
186
+ `0 4px 6px -1px var(--ri-shadow-color, rgb(0 0 0 / 0.1)), …`, with the
187
+ original colour as the fallback, so an uncoloured shadow renders exactly as
188
+ before.
189
+
190
+ This required inlining the value instead of referencing `var(--shadow-md)`,
191
+ and that is not a stylistic choice: a `var()` written inside a `:root` custom
192
+ property is substituted when that property is computed **on `:root`**, where
193
+ no element has set a colour, so the slot would bake in its own fallback before
194
+ reaching the element. Tailwind inlines for the same reason. The consequence is
195
+ that a `--shadow-*` token now reaches `:root` only when your own CSS
196
+ references it — `.card { box-shadow: var(--shadow-md) }` still keeps it, and
197
+ so does a `@shadow` alias, but a stylesheet the compiler never sees will no
198
+ longer find the variable defined.
199
+ - **Fractions outside the enumerated set did not resolve.** `w-7/9`, `h-3/8`,
200
+ `max-w-13/17`, `translate-x-2/7` — anything beyond the 26 fractions the
201
+ completion table lists — compiled to nothing, while Tailwind compiles every
202
+ fraction. All four percentage-valued families (`w-`, `basis-`, `flex-`,
203
+ `translate-`) now share one grammar: the table for the common values, a
204
+ `calc()` for the rest. This also settled `basis-1/3`, which emitted
205
+ `33.3333%` where `w-1/3` emitted `33.333333%`.
206
+ - **`translate-none` and `translate-3d` were inert next to an axis utility.**
207
+ Both emit the `translate` shorthand and sorted at the same key as
208
+ `translate-x-*`, so the tie-break put them first and the axis utility won the
209
+ cascade — a reset that reset nothing, and a `translate-3d` that could not add
210
+ the Z axis it exists for. They now sort after the axes, as they do upstream.
211
+ - **The editor API could not complete classes it compiles.** The logical sizing
212
+ spellings (`min-inline-full`, `min-block-0`, `inline-full`, `block-full` and
213
+ the rest of the block axis) and the shadow resets (`text-shadow-none`,
214
+ `shadow-initial` and their siblings) resolved but were absent from
215
+ `enumerate()`, so completions, the strict `ri()` types and the suggestion
216
+ corpus did not know them. 3,700 → 3,883 classes on the default theme.
217
+ - **`ri()` deleted composable filters.** `ri("blur-sm grayscale")` returned
218
+ `"grayscale"`, and `ri("blur-sm invert sepia")` returned `"sepia"` — the whole
219
+ filter family annihilated itself, and the backdrop family with it. Every
220
+ composable filter emits the shared `filter` shorthand as its second
221
+ declaration, and that property was expanded to all nine slot variables, so any
222
+ filter claimed every other filter's slot. The reset spellings (`filter-none`,
223
+ `filter-[v]`, and the backdrop equivalents) now claim the slots directly,
224
+ which is what the shadow and ring families already did.
225
+ - **`ri()` deleted a working `ring-2` next to a `ring-offset-*`.**
226
+ `ri("ring-2 ring-offset-2 ring-offset-white")` returned just
227
+ `"ring-offset-white"`. `ring-offset-*` and `ring-inset` emit no CSS — that
228
+ omission is deliberate — but they fell into the `ring` prefix and claimed its
229
+ `box-shadow`. They now claim their own properties.
230
+ - **The scanner dropped arbitrary values that were a bare integer.**
231
+ `z-[60]`, `order-[3]`, `flex-[2]`, `col-span-[7]`, `line-clamp-[8]` compiled
232
+ correctly when passed by hand and vanished when scanned out of markup: the
233
+ filter that keeps JavaScript subscripts (`items[0]`, `string[]`) out of the
234
+ candidate set rejected anything ending in `[digits]`. It now keys on the dash
235
+ before the bracket, which a utility always has and a subscript never does.
236
+ - **`translate-z-*` accepted percentages.** `--ri-translate-z` is registered
237
+ `<length>`, so `translate-z-full` compiled to a declaration the browser
238
+ discards. Rejected now, as it is upstream.
239
+
240
+ - **`&` in a relational variant now means the group, not the utility.**
241
+ `group-[&.open]:flex` emitted `.group:is(<that very class>.open) <that very
242
+ class>` — an element required to be its own ancestor, so the rule was valid
243
+ CSS that could never match. It now emits `.group.open .CLASS`, which is what
244
+ Tailwind emits. `peer-[…]` and the named forms are fixed with it, and every
245
+ `&` in the bracket is substituted, not just the first.
246
+
247
+ `has-[&…]`, `not-[&…]` and `in-[&…]` are now rejected rather than shipping
248
+ `:has(&.foo)`: that is not valid in the flat CSS this emits, and an element
249
+ cannot contain itself. The standalone `[&_p]:` form, where `&` really is the
250
+ element being styled, is unchanged.
251
+ - **A variant's arbitrary value can contain a nested `]`.** `has-[[data-x]]:`,
252
+ `peer-[[type=checkbox]]:`, `group-[&[href^="/x"]]:` — the bracket was matched
253
+ with a pattern that stops at the first `]`, so the colon after it was never
254
+ found and the whole class was read as a utility name. That is why it reported
255
+ `unknown-utility` rather than `unknown-variant`.
256
+
257
+ The scanner had the same bug one level up, and there it was worse: a torn
258
+ token's tail is often a valid utility, so `group-[&[href]]:underline` in your
259
+ markup made the compiler emit a real `.underline` rule for a class nobody
260
+ wrote. A `]` inside quotes is now content rather than a delimiter, so
261
+ `data-[x="]"]:` works too.
262
+
263
+ While in there: whitespace inside a *variant's* own bracket escaped the
264
+ `[RI-1412]` check entirely, because the variant was stripped before the test
265
+ ran. `group-[&[a b]]:flex` now warns and is dropped, as `bg-[a b]` always was.
266
+ - **A bare number is a line height, and an unresolvable modifier is an error.**
267
+ `leading-6` and `text-sm/6` mean 6 × the spacing base, as they do in Tailwind.
268
+ They used to fail in two different ways: `leading-6` was rejected outright,
269
+ while `text-sm/6` quietly fell back to the size's own line height — so a
270
+ migrating codebase got different typography with nothing to grep for. Watch
271
+ out if you were relying on the old behaviour by accident: Tailwind's default
272
+ leadings are spacing multiples, so `text-lg/7` happened to be correct already.
273
+
274
+ A stated modifier that resolves to nothing now invalidates the whole class,
275
+ the rule the fluid path already followed. `text-lg/zzz` emits nothing instead
276
+ of a half-honoured `text-lg`, and `text-[20px]/7` carries its line height
277
+ instead of dropping the declaration and inheriting. `px` is still not a
278
+ multiple — `text-lg/px` is not a line height anyone means — and a
279
+ `@leading` entry still wins over the multiple.
280
+ - **`group`, `peer`, and their named forms are valid classes.** The variant half
281
+ landed first, so `group-hover/item:` worked while the `group/item` its parent
282
+ has to wear read as unknown — flagging correct markup in editors and lint
283
+ rules. They emit nothing, as upstream does. The check sits after the utility
284
+ resolvers, so `@utility group { … }` of your own still wins, and only the bare
285
+ spelling counts: `sm:group` and `group!` are still errors, because
286
+ `.sm\:group` in the DOM is not `.group` and cannot anchor anything.
287
+
288
+ `validate()` now accepts exactly one class the compiler emits no rule for, and
289
+ the parity suite states that exception rather than leaving it implicit.
290
+ - **`placeholder-{color}`.** Recorded as a deliberate difference on the grounds
291
+ that `placeholder:text-{color}` is the v4 spelling — which is true of
292
+ Tailwind's docs site but not of its implementation, where `placeholder` is an
293
+ ordinary colour utility. It colours `::placeholder` through a nested selector,
294
+ so it never recolours the value the user typed, and it is merge-scoped, so
295
+ `text-red-500 placeholder-gray-400` keeps both. The variant spelling still
296
+ works.
297
+ - **The container ladder reaches every family that should have it.** `--container-*`
298
+ is Tailwind's inline-axis scale, and here it fed `max-w-*` alone — while
299
+ `columns-*` kept a second copy of the same table that added `3xs` and `2xs`,
300
+ so `columns-3xs` resolved and `max-w-3xs` did not. One table now, read by
301
+ `w-`, `min-w-`, `max-w-`, `basis-`, `columns-`, and the inline-axis logical
302
+ forms. The block axis is deliberately left out, as upstream leaves it. Editor
303
+ completions offer the ladder too — those rows enumerated the theme's
304
+ breakpoint names, a set no sizing table has a key for, so `max-w-md` was
305
+ resolvable but never suggested.
306
+ - **Breakpoint names work in `min-` and `max-` variants.** `max-sm:hidden` and
307
+ `min-md:flex` are ordinary Tailwind and resolved to nothing here: the branch
308
+ took a bracketed length only, seven lines below the one that resolves a
309
+ breakpoint by name. Container queries gained the same forms — `@max-md:`,
310
+ `@min-md:`, `@sidebar/max-md:`. The bracket spellings are unchanged, plain
311
+ `sm:` still emits `min-width`, and a `max-`/`min-` name that is not a
312
+ breakpoint still falls through to your own `@custom` variant rather than being
313
+ swallowed. An arbitrary container size stays `[@container(min-width:600px)]`.
314
+ - **Variants compose, and groups can be named.** A variant nested inside
315
+ another was a second, much smaller grammar: `group-` took a pseudo-class or a
316
+ bracket selector and nothing else, so `group-hover:` worked and
317
+ `group-data-[state=open]:` did not — even though `data-[state=open]:` on its
318
+ own always had. The inner segment now resolves as a variant in its own right,
319
+ which makes all of these work:
320
+
321
+ ```
322
+ group-data-[state=open]:rotate-180 peer-has-[:checked]:block
323
+ group-aria-expanded:rotate-180 peer-data-[open]:block
324
+ has-checked:bg-blue-50 not-data-[open]:block
325
+ not-supports-[display:grid]:block in-focus:underline
326
+ nth-3:underline nth-of-type-2:underline
327
+ ```
328
+
329
+ An inner variant that describes the viewport rather than the anchor
330
+ (`group-sm:`) stays unknown, as does one that rewrites the selector's position
331
+ (`group-in-[.foo]:`) — neither is a state an ancestor can be in.
332
+
333
+ Named groups and peers work too: `group-hover/item:underline` anchors on
334
+ `.group\/item`, so nested markup can address the group it means. The slash
335
+ used to defeat the variant-prefix check, which left the whole class read as a
336
+ utility name. A malformed name is rejected rather than falling back to any
337
+ `.group`.
338
+ - **`.astro` files are scanned properly.** They were an unknown extension:
339
+ static classes came through the generic token scan by accident, `class:list`
340
+ on a long line did not, a helper call in the frontmatter did not, and the dev
341
+ server never recompiled on save because `.astro` was not a source file — so a
342
+ new class only appeared after editing the CSS itself. Both halves are read
343
+ now: the markup for `class="…"` and `class:list={…}`, the frontmatter for
344
+ `clsx`/`cn`/`ri` calls and `cva`/`tv` recipe configs. `.astro` joins the
345
+ default `src/**` pattern.
346
+ - **`ri()` knows your theme on the client.** This fixes a shipped correctness
347
+ bug. `ri()` decides whether `text-lg` is a font size or a color by asking the
348
+ published theme; a compile publishes one, and a browser bundle never runs a
349
+ compile. Since 0.6.0 made every text size, weight, font slot, and color name
350
+ project-defined, that meant a client-side `ri("text-lg text-white")` read
351
+ `text-lg` as a color, decided the two conflicted, and returned `"text-white"`
352
+ — silently, because the warning was a dev-only one.
353
+
354
+ **With Vite, nothing to do.** This holds for a theme behind an `@import`
355
+ too — including the Tailwind preset, whose whole theme arrives that way.
356
+ The plugin serves the theme as a virtual module
357
+ and prepends it to every module that imports `rainbowindex`. Imports evaluate
358
+ in order, so the theme is published before your code runs — dev, build, and
359
+ SSR alike — and editing your CSS entry republishes it over HMR.
360
+
361
+ **With any other bundler**, one command and one import:
362
+
363
+ ```sh
364
+ rainbowindex generate-snapshot # writes rainbowindex-snapshot.ts
365
+ ```
366
+
367
+ ```ts
368
+ import "./rainbowindex-snapshot"; // app entry, before anything calls ri()
369
+ ```
370
+
371
+ The generated module also exports a `ri` bound to that snapshot, for rendering
372
+ more than one theme in one process. Its output is deterministic, so re-running
373
+ it on an unchanged theme produces no diff.
374
+
375
+ New exports on all three entries: `serializeSnapshot`, `hydrateSnapshot`, and
376
+ `publishSnapshot`. A snapshot holds Sets, which `JSON.stringify` silently
377
+ turns into `{}`, so `serializeSnapshot` is the only safe way to send one
378
+ across a server/client boundary. `hydrateSnapshot` never throws on a malformed
379
+ payload — a stale generated module degrades to "this part of the theme is
380
+ unknown" rather than breaking the bundle at import time. Editor capability:
381
+ `serializable-snapshot`.
382
+ - **`@import` is read for its directives.** A `@color` block living in
383
+ `./theme/tokens.css` — or in a stylesheet a package ships — now reaches the
384
+ theme. Until this release only the Vite path ever saw imported text, because
385
+ Vite resolves CSS imports before PostCSS plugins run; the CLI, the PostCSS
386
+ plugin, `compileProject`, and the editor entry all passed `@import` through
387
+ and never opened the file.
388
+
389
+ ```css
390
+ @import "rainbowindex";
391
+ @import "./theme/tokens.css"; /* its directives apply */
392
+ @import "some-preset/theme.css"; /* through the package exports map */
393
+ ```
394
+
395
+ Reading is not emitting, so nothing is duplicated. The PostCSS plugin builds
396
+ its output from the AST exactly as before and leaves your `@import` at-rules
397
+ for the browser; the CLI, which runs no PostCSS, writes an inlined file's CSS
398
+ into the output once, in import order. You no longer need `postcss-import`
399
+ ahead of the plugin just to make a token file's directives count.
400
+
401
+ Left alone: `@import "rainbowindex"` (that is activation — and a second one
402
+ inside an imported file is dropped rather than activating twice), remote URLs,
403
+ and site-root paths. A conditional import — one carrying a media query,
404
+ `layer()`, or `supports()` — is left in place and warns `[RI-1045]`, since
405
+ directives have no conditional form. A file reached twice by different paths
406
+ is read once; the chain may nest 8 deep and total 5 MB.
407
+
408
+ New codes: `RI-1041` unresolved, `RI-1042` cycle, `RI-1043` too deep,
409
+ `RI-1044` too large, `RI-1045` conditional.
410
+
411
+ `compileProject` takes `cssPath` (which turns inlining on) and
412
+ `resolveImport`. `createEditorSession` takes `resolveImport` and `cssPath`,
413
+ and exposes `importedFiles` so a host knows what else to watch — the entry
414
+ still does no IO of its own, so the resolver is the host's. The new
415
+ `inlineDirectiveImports` and `createNodeImportResolver` are exported for
416
+ callers running the step themselves. Editor capability: `import-inlining`.
417
+ - **`rainbowindex/tailwind.css`, an optional Tailwind-default preset.** Since
418
+ 0.6.0 removed every shipped scale, a fresh install renders nothing for
419
+ `sm:flex`, `text-lg`, `font-bold`, `shadow-md`, `rounded-lg`, `ease-in`, or
420
+ `animate-spin`. Shipping no opinions is still the default; this makes the
421
+ opinions one import away.
422
+
423
+ ```css
424
+ @import "rainbowindex";
425
+ @import "rainbowindex/tailwind.css";
426
+ ```
427
+
428
+ It is Tailwind v4.3.3's default theme written entirely in directives —
429
+ 26 color families, the `sm`–`2xl` breakpoints, `text-xs` through `text-9xl`,
430
+ and the weight, leading, tracking, radius, shadow, blur, easing, and
431
+ animation scales — generated by `scripts/generate-tailwind-preset.mjs` from
432
+ Tailwind's own `theme.css`, so it is readable CSS you can open and copy from
433
+ rather than a compiled blob. Import it after the package; later blocks win,
434
+ so any token you declare below it overrides it. Two entries Tailwind
435
+ implements as utilities rather than tokens, `leading-none` and `container`,
436
+ are written in by hand. The values are Tailwind's — see `NOTICE.md`.
437
+
438
+ Every scale it names, colors included, is pruned to what the build actually
439
+ uses — see the color-token pruning entry below, which landed after this one
440
+ and removed the 14 KB of unconditional `:root` output the preset used to add.
441
+ `docs/utilities.md` lists what the preset cannot paper over, and
442
+ `__tests__/core/tailwind-preset.test.ts` holds the compatibility table plus
443
+ the known gaps, asserted red.
444
+ - **Bare `rounded` reads a `DEFAULT` radius.** `@rounded { DEFAULT: 0.25rem; }`
445
+ makes `rounded` resolve, as do the bare sides and corners `rounded-t`,
446
+ `rounded-tl`, `rounded-ss`. `@shadow` and `@blur` already used the same key;
447
+ `@rounded` was the odd one out.
448
+ - `NOTICE.md`, shipped in the package, carrying the full license texts of
449
+ Tailwind CSS, tailwind-merge, and tw-animate-css. `LICENSE` is now the
450
+ canonical MIT text, so SPDX detectors and dependency scanners read the
451
+ package as MIT instead of "Other".
452
+ - `CONTRIBUTING.md` and `SECURITY.md`, plus issue and pull-request templates.
453
+ - Continuous integration on every push and pull request (Node 20.19 and 24),
454
+ running the same `pnpm check` gate as a release. The release workflow now
455
+ waits on that gate, so a version bump whose tests fail publishes nothing.
456
+ - Golden output tests (`__tests__/golden/`): whole-stylesheet fixtures that
457
+ diff every emitted declaration. `pnpm test:golden:update` regenerates them.
458
+ - A docs drift guard (`__tests__/core/docs-sync.test.ts`): adding an editor
459
+ capability without documenting it now fails the test suite.
460
+ - A quick-start end-to-end test (`__tests__/cli/cli.test.ts`): the documented
461
+ path, walked as a new user walks it — `rainbowindex init`, one of the two
462
+ starts, a build — with the package resolved through `node_modules` and its
463
+ `exports` map rather than a hand-built theme. The unit suite constructs
464
+ themes in memory, which is how the Tailwind-familiar start reached a release
465
+ candidate loading no theme at all.
466
+ - **A directive named inside a CSS comment no longer leaks every directive
467
+ after it into the output.** The strip patterns run forward to the next `{` or
468
+ `;`, so `/* palette via @color */` matched all the way to the *real*
469
+ `@color {` opener; skipping that false match resumed the scan past the block
470
+ it had swallowed, and everything from there on was copied into the emitted
471
+ stylesheet verbatim, as invalid CSS. Any comment documenting the theme by
472
+ name was enough. Affected `compileProject` and the CLI; the PostCSS plugin
473
+ strips on the AST and was never affected.
474
+ - **`dark:` and the color tokens can finally agree.** They were two theme
475
+ switches that disagreed: tokens flip through `light-dark()` under
476
+ `html[data-appearance]`, while `dark:` only ever asked the OS — so setting
477
+ `data-appearance="dark"` recolored the page but left every `dark:` utility
478
+ inert. `docs/theming.md` carried a CAUTION saying so; it is gone.
479
+
480
+ ```css
481
+ @color dark {
482
+ variant: appearance; /* or: media (default), selector(.dark) */
483
+ }
484
+ ```
485
+
486
+ `media` is the old behaviour and stays the default, so nothing changes until
487
+ you ask. `appearance` matches exactly when `light-dark()` flips under the
488
+ shipped preflight — the explicit attribute, or the OS preference where no
489
+ attribute overrides it — which takes two rules, and the class emits both.
490
+ `selector(.dark)` is the strategy a Tailwind project arrives with. `light:`
491
+ mirrors whichever you choose, and every form is wrapped in `:where()`, so the
492
+ variant adds no specificity. New code `RI-1111` for an unrecognized value.
493
+ - **Color tokens are pruned to what the build uses.** Explicit and pair `@color`
494
+ entries were emitted into `:root` unconditionally, so importing
495
+ `rainbowindex/tailwind.css` added all 286 of its stops — about 14 KB — to a
496
+ page that used two colors. They are now emitted on demand, like every other
497
+ scale. On the preset's own test fixture `:root` drops from 14,713 bytes to
498
+ 1,297, and the whole stylesheet from 25,345 to 11,929.
499
+
500
+ Demand is followed transitively, because emitting one token can require
501
+ another: an explicit value may name a second color, an alias emits a `var()`
502
+ at its source, and both can chain. Four sources that were never scanned at all
503
+ now count — a `@shadow` token's value, a `@keyframes` body from `@animate`, an
504
+ `@register` initial value, and the `var(--color-x, fallback)` spelling — each
505
+ of which could already leave a dangling reference before pruning existed.
506
+ `paper` and `ink` are always emitted.
507
+ - **A theme stop named by a value now follows `data-theme`.** The `[data-theme]`
508
+ override blocks and the `:root` palette stops are two halves of one mechanism
509
+ and were computed from different inputs, so they agreed only by accident.
510
+ `hairline: theme-282/52` put `--color-theme-282` in `:root` but got no
511
+ override block, and switching `data-theme` left it behind — silently, since
512
+ the variable resolved perfectly well, just never changed. Both halves now
513
+ read the same demand.
514
+ - **A bare color name in a `@color` value is a reference, not literal text.**
515
+ `soft: surface/50` emitted `color-mix(in oklab, surface 50%, transparent)` —
516
+ `surface` verbatim, which is not a CSS color — and `duo: surface / raised` and
517
+ `light-dark(surface, raised)` did the same. All three were invalid at
518
+ computed-value time and warned about nothing. A bare name now expands to
519
+ `var(--color-<name>)` everywhere it cannot be the alias spelling: both halves
520
+ of a pair, inside `light-dark()`, and any side carrying an `/alpha` modifier.
521
+ `accent: brand` is still an alias, and `transparent`, `currentColor`, and
522
+ `inherit` are still themselves.
523
+
524
+ New code `RI-1109` covers the case a reference cannot fix: a generative
525
+ palette defines `--color-brand-<stop>` and no bare `--color-brand`, so
526
+ `duo: surface / brand` now says so instead of emitting something that cannot
527
+ resolve.
528
+ - **A `@color` alias no longer points at a variable nobody emits.** `@color
529
+ { brand: 0.18 330; accent: brand; }` emits
530
+ `--color-accent-500: var(--color-brand-500)`, but nothing marked
531
+ `--color-brand-500` as used, so it was pruned from `:root` and `text-accent-500`
532
+ rendered nothing — silently, and only when the same stop was not used directly
533
+ somewhere else, which is why it looked intermittent. Every stop an alias uses
534
+ is now emitted on its source. Alias chains (`accent: mid; mid: brand;`) work
535
+ too: each link emits its own `var()` hop, where before the chain collapsed to a
536
+ single stop-less `--color-accent: var(--color-mid)` that no utility reads. A
537
+ circular chain still warns `[RI-1107]` and now emits nothing rather than two
538
+ variables pointing at each other. An alias to a keyword now inlines the keyword's
539
+ value instead of referencing a `--color-*` variable that keywords never emit.
540
+ - **Bare `shadow` no longer renders nothing when the theme names a `DEFAULT`.**
541
+ It compiles to `var(--shadow-DEFAULT)`, but the usage scanner's token-reference
542
+ pattern was lowercase-only, so the token was pruned from `:root` and the class
543
+ fell through to the transparent fallback — with no warning, and with
544
+ `validate()` reporting the class as fine. Defining a `DEFAULT` was worse than
545
+ omitting one, since `shadow` otherwise falls back to `md`.
546
+ - `docs/editor-api.md` documented fewer editor capabilities than the entry
547
+ exports. Every capability the entry declares is now listed, and the stale
548
+ class and template counts are corrected — with a docs-sync test that reads
549
+ the prose and compares it to the code, so the next gap fails the suite.
550
+
8
551
  ## [0.6.0] - 2026-09-01
9
552
 
10
553
  ### Added
package/LICENSE CHANGED
@@ -1,22 +1,21 @@
1
1
  MIT License
2
2
 
3
- This project is a fork of Tailwind CSS v4. Portions of this software are derived from Tailwind CSS: Copyright (c) Tailwind Labs, Inc.
3
+ Copyright (c) 2026 Milo Tech Forecast
4
4
 
5
- The class merging algorithm is derived from tailwind-merge: Copyright (c) 2021 Dany Castillo
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
6
11
 
7
- The animation system is inspired by tw-animate-css: Copyright (c) 2025 Wombosvideo (Luca Bosin)
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
8
14
 
9
- Rainbow Index Copyright (c) 2026 Milo Tech Forecast
10
-
11
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
12
- documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
13
- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
14
- persons to whom the Software is furnished to do so, subject to the following conditions:
15
-
16
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
17
- Software.
18
-
19
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
20
- WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
22
- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.