rainbowindex 0.5.1 → 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.
- package/CHANGELOG.md +876 -0
- package/LICENSE +16 -17
- package/NOTICE.md +106 -0
- package/README.md +225 -69
- package/dist/browser.d.ts +4 -2
- package/dist/browser.mjs +12 -6
- package/dist/chunk-2T7V5XLK.mjs +912 -0
- package/dist/chunk-6OORICWF.mjs +16 -0
- package/dist/{chunk-PDORZSQX.mjs → chunk-FJOZJIKB.mjs} +7814 -5174
- package/dist/chunk-L56IRO7A.mjs +491 -0
- package/dist/chunk-PZDVDEZJ.mjs +196 -0
- package/dist/{chunk-4CTJLMYM.mjs → chunk-RC6DDE4L.mjs} +37 -23
- package/dist/chunk-TQJYVQPE.mjs +217 -0
- package/dist/chunk-W756NVYI.mjs +33 -0
- package/dist/chunk-WBESS2ZD.mjs +598 -0
- package/dist/{chunk-3HRMFZGE.mjs → chunk-X66Z2YHT.mjs} +2 -1
- package/dist/{chunk-4UKFK2GE.mjs → chunk-XQGSG2HK.mjs} +213 -756
- package/dist/cli.mjs +1101 -125
- package/dist/{context-B9yhJxd5.d.ts → context-DcBtnnan.d.ts} +55 -108
- package/dist/editor.d.ts +82 -421
- package/dist/editor.mjs +68 -363
- package/dist/eslint.d.ts +16 -0
- package/dist/eslint.mjs +32 -0
- package/dist/{index-Dx-NpFFx.d.ts → imports-C9esHd5Q.d.ts} +98 -81
- package/dist/index-CNqdL5U0.d.ts +56 -0
- package/dist/index-Czx-EUwh.d.ts +138 -0
- package/dist/index-DW8YSxTz.d.ts +104 -0
- package/dist/index.d.ts +49 -5
- package/dist/index.mjs +40 -13
- package/dist/oxlint.d.ts +21 -3
- package/dist/oxlint.mjs +19 -1
- package/dist/recipe.d.ts +111 -0
- package/dist/recipe.mjs +71 -0
- package/dist/safelist-CH3_PywB.d.ts +43 -0
- package/dist/session-CMaskdB7.d.ts +543 -0
- package/dist/tailwind.css +644 -0
- package/dist/theme-CIZiGlce.d.ts +115 -0
- package/dist/vite.d.ts +10 -1
- package/dist/vite.mjs +273 -113
- package/package.json +27 -5
- package/dist/chunk-F4VCBISU.mjs +0 -1866
- package/dist/chunk-RU4756NG.mjs +0 -243
- package/dist/safelist-DAkKuxCk.d.ts +0 -96
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,882 @@ 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
|
+
|
|
551
|
+
## [0.6.0] - 2026-09-01
|
|
552
|
+
|
|
553
|
+
### Added
|
|
554
|
+
|
|
555
|
+
- **Numeric font weights, checked against the fonts you load.** `font-<number>`
|
|
556
|
+
now sets `font-weight` directly, for any weight from 1 to 1000 —
|
|
557
|
+
`font-300`, `font-500`, `font-617`. A named `@weight` token of the same
|
|
558
|
+
spelling still wins, and `font-[850]` is unchanged.
|
|
559
|
+
|
|
560
|
+
The number is compared against the `weight` of every loaded `@font` face. A
|
|
561
|
+
range (`weight: 300 900`, the variable-font case) accepts every number
|
|
562
|
+
between its bounds; a list or a single value (`weight: 400,700`) accepts
|
|
563
|
+
only those numbers. A weight that no face provides warns with the new
|
|
564
|
+
`[RI-1504]`, and the message names the weights that are available.
|
|
565
|
+
|
|
566
|
+
A class on its own carries no family, and a page can load several fonts, so
|
|
567
|
+
that check is a union: one covering face is enough. It stays quiet with no
|
|
568
|
+
`@font` block, and with a `system` slot, whose OS font has every weight.
|
|
569
|
+
|
|
570
|
+
Where the family *is* named — a `font-<slot>` beside the weight in one
|
|
571
|
+
`@apply` / `@a` / `@utility` class list — that family alone decides:
|
|
572
|
+
|
|
573
|
+
```css
|
|
574
|
+
[data-slot="code"] {
|
|
575
|
+
@a font-mono font-550; /* Fira Code does not provide weight 550. It has 400, 700. */
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
The last family in the list wins, and the two classes must share a variant
|
|
580
|
+
prefix (`md:font-mono` does not set the family for a plain `font-550`). A
|
|
581
|
+
weight no font provides is still reported once, by the union check. A class
|
|
582
|
+
written in markup has no list — the scanner keeps only the set of names — so
|
|
583
|
+
the union check governs there.
|
|
584
|
+
|
|
585
|
+
Completions and the generated class types offer `font-100` … `font-900`
|
|
586
|
+
beside the named `@weight` tokens, and read `font-<number>` as an open
|
|
587
|
+
numeric template.
|
|
588
|
+
|
|
589
|
+
- **Three editor capabilities.** `rainbowindex/editor` gains `isSuppressible`
|
|
590
|
+
(`diagnostic-suppression`), so an editor offers a `ri-disable` comment only
|
|
591
|
+
where one would work; `weightIsLoaded` and `describeLoadedWeights`
|
|
592
|
+
(`font-weight-coverage`), the `[RI-1504]` check, so it can answer which
|
|
593
|
+
loaded fonts carry a weight; and `ThemeTokens.radii` / `ThemeTokens.fluidRanges`
|
|
594
|
+
(`named-radii-and-fluid-ranges`), which named radii and named `@fluid` ranges
|
|
595
|
+
had no token surface for. Gate on the capability strings, never on `version`.
|
|
596
|
+
|
|
597
|
+
- **`ri-disable` comments.** Two plain CSS comments silence a diagnostic, so a
|
|
598
|
+
stylesheet carrying them stays valid CSS for every other tool.
|
|
599
|
+
|
|
600
|
+
`/* ri-disable RI-1124 */` anywhere in the CSS entry silences that code
|
|
601
|
+
everywhere. It is the only form that reaches the scanner and compile codes
|
|
602
|
+
(14xx, 15xx), which carry no position in your CSS. One comment may name
|
|
603
|
+
several codes: `/* ri-disable RI-1124, RI-1122 */`.
|
|
604
|
+
|
|
605
|
+
`/* ri-disable-next-line RI-1124 */` silences one place. Inside a scale body
|
|
606
|
+
it guards the entry that follows; outside a body it guards the next
|
|
607
|
+
directive:
|
|
608
|
+
|
|
609
|
+
```css
|
|
610
|
+
@rounded {
|
|
611
|
+
roof: 24px;
|
|
612
|
+
/* ri-disable-next-line RI-1124 */
|
|
613
|
+
full: 30px;
|
|
614
|
+
hut: 8px; /* still checked */
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
Entry precision is available where the emitter knows the entry —
|
|
619
|
+
`[RI-1035]`, `[RI-1121]`, `[RI-1122]`, `[RI-1124]`. Other codes fall back to
|
|
620
|
+
the directive.
|
|
621
|
+
|
|
622
|
+
`RI-00xx` and `RI-20xx` cannot be silenced: they report a broken build or a
|
|
623
|
+
broken call, not a style choice. Naming one warns with the new `[RI-1040]`,
|
|
624
|
+
as does a comment that names no readable code.
|
|
625
|
+
|
|
626
|
+
- **`compileProject()` reports what it silenced.** The result gains
|
|
627
|
+
`suppressed`, the set of codes the entry's `ri-disable` comments named, so
|
|
628
|
+
a caller that pushes warnings of its own can drop the ones the author
|
|
629
|
+
hid. Every stage inside the compile already pushes through it.
|
|
630
|
+
|
|
631
|
+
- **Fluid endpoint pairs.** Fluid utilities take both ends of the ramp:
|
|
632
|
+
`p-fluid-4/8` grows from step 4 to step 8 across the `@fluid` range, and
|
|
633
|
+
`text-fluid-sm/3xl` ramps between two type steps. Arbitrary lengths and
|
|
634
|
+
`(--var)` endpoints mix (`p-fluid-[0.5rem]/(--x)`), zero is a legal
|
|
635
|
+
endpoint, and a descending pair (`p-fluid-8/4`) shrinks as the viewport
|
|
636
|
+
grows. Fluid type also takes the line-height modifier now:
|
|
637
|
+
`text-fluid-lg/7`, `text-fluid-sm/3xl/tight`.
|
|
638
|
+
|
|
639
|
+
- **Named `@fluid` ranges and scope classes.** `@fluid compact { min: 20rem;
|
|
640
|
+
max: 48rem; }` defines a range, the tokens `--fluid-compact-{min,max}`, and
|
|
641
|
+
the scope class `fluid-compact`, which points every fluid utility on the
|
|
642
|
+
element and its descendants at that range through the inherited
|
|
643
|
+
`--fluid-scope-{min,max}` pair. A unit on a named range warns with
|
|
644
|
+
`RI-1039`; an unknown range name on the class warns with `RI-1503`.
|
|
645
|
+
|
|
646
|
+
- **Container-query units in `@fluid`.** `unit` accepts `cqw`, `cqi`,
|
|
647
|
+
`cqmin`, and `cqmax`, so fluid ramps can track a container instead of the
|
|
648
|
+
viewport.
|
|
649
|
+
|
|
650
|
+
- **Named radii in `@rounded`.** A key without a `--` prefix now names a
|
|
651
|
+
radius: `@rounded { roof: 24px; }` gives the class `rounded-roof` and the
|
|
652
|
+
token `--rounded-roof`, and the name works with the side and corner
|
|
653
|
+
suffixes (`rounded-tl-roof`). `--corner-scale` is still the only option;
|
|
654
|
+
any other `--` key still warns with `[RI-1122]`. Radii without a name stay
|
|
655
|
+
spacing multiples, so `rounded-4` is unchanged.
|
|
656
|
+
|
|
657
|
+
- **Utility blocks in every named scale.** A `name { … }` or `name-* { … }`
|
|
658
|
+
block in a scale body defines a utility in that scale's class family, so the
|
|
659
|
+
math sits next to the tokens it reads:
|
|
660
|
+
|
|
661
|
+
```css
|
|
662
|
+
@rounded {
|
|
663
|
+
roof: 24px;
|
|
664
|
+
roof-minus-* {
|
|
665
|
+
border-radius: calc(var(--rounded-roof) - var(--value) * var(--spacing));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
`rounded-roof-minus-2` subtracts two spacing steps from `--rounded-roof`.
|
|
671
|
+
The block uses the `@utility` grammar and the name takes the family's
|
|
672
|
+
prefix. `@rounded`, `@shadow`, `@blur`, `@z`, `@leading`, `@tracking`,
|
|
673
|
+
`@opacity`, `@duration`, `@ease`, `@weight`, `@text` and `@animate` all
|
|
674
|
+
accept them; `@weight` blocks land under `font-`.
|
|
675
|
+
|
|
676
|
+
`@color` does not: a colour name feeds `bg-`, `text-`, `border-`, `ring-`
|
|
677
|
+
and more, so a block would have no one family to land in. `@breakpoint`
|
|
678
|
+
names variants rather than utilities, and `@spacing` holds a single base
|
|
679
|
+
value.
|
|
680
|
+
|
|
681
|
+
A colon is what separates a utility block from the two block grammars that
|
|
682
|
+
already existed. `key: value { … }` stays what it was — `@color` options,
|
|
683
|
+
`@animate` keyframes — and only a block with no key before it is a utility.
|
|
684
|
+
|
|
685
|
+
Radius tokens are written to `:root` whether used or not, because a block
|
|
686
|
+
body is raw CSS that no usage pass can read.
|
|
687
|
+
|
|
688
|
+
- **`@shadow` aliases.** A shadow value that is only another shadow's class
|
|
689
|
+
name now emits a reference to it: `@shadow { md: …; card: shadow-md; }`
|
|
690
|
+
gives `--shadow-card: var(--shadow-md)`. Using `shadow-card` pulls
|
|
691
|
+
`--shadow-md` into `:root` alongside it, so an alias never renders against
|
|
692
|
+
an undefined variable. The target may be defined in a later `@shadow`
|
|
693
|
+
block. An alias to a token that does not exist warns with the new
|
|
694
|
+
`[RI-1123]` and keeps its value verbatim.
|
|
695
|
+
|
|
696
|
+
### Removed
|
|
697
|
+
|
|
698
|
+
- **`@utility` no longer accepts a leading `.` on the name.** `@utility .card`
|
|
699
|
+
used to be silently treated as `@utility card`. No other directive did
|
|
700
|
+
this, and the dot was ignored without a word in `@source inline(".card")`,
|
|
701
|
+
`safelist(".card")`, and `class=".card"` markup, so the one exception
|
|
702
|
+
taught a rule that held nowhere else. A dotted name now warns with
|
|
703
|
+
`[RI-1035]` and is skipped, like every other invalid name. Write
|
|
704
|
+
`@utility card`.
|
|
705
|
+
|
|
706
|
+
- **The shipped shadow scales are gone.** No `shadow-*` tokens ship any more:
|
|
707
|
+
the layered `px`–`2xl` scale and its building blocks (`line`, `drop`,
|
|
708
|
+
`hi-1`–`hi-4`, `dark-line`, `ring`, `layer-1`–`layer-7`) are removed, along
|
|
709
|
+
with the hardcoded `inset-shadow-*`, `text-shadow-*`, and `drop-shadow-*`
|
|
710
|
+
size scales. A shadow is now a project decision, not a shipped opinion.
|
|
711
|
+
Every value form still works — `shadow-none`, `shadow-{color}`,
|
|
712
|
+
`shadow-[v]`, and the same forms for the other three families — and `ring`
|
|
713
|
+
and `inset-ring` are untouched. Named sizes come back with
|
|
714
|
+
`@shadow md: …;`, and bare `shadow` reads the `DEFAULT` token.
|
|
715
|
+
Because the scale is empty by default, bare `shadow` is no longer a
|
|
716
|
+
built-in static utility: it resolves only when the theme defines
|
|
717
|
+
`DEFAULT` (or `md`).
|
|
718
|
+
|
|
719
|
+
- **The last shipped scales are gone: breakpoints, weights, easing, blur,
|
|
720
|
+
animations, and the fluid range.** The package now ships exactly two
|
|
721
|
+
defaults — the colour palette and the `0.25rem` spacing base. Everything
|
|
722
|
+
else is a project decision:
|
|
723
|
+
|
|
724
|
+
| Gone | Was | Comes back with |
|
|
725
|
+
| --- | --- | --- |
|
|
726
|
+
| `sm:` `md:` `lg:` `xl:` and `@sm:` `@md:` … | 40/48/64/80rem | `@breakpoint { sm: 40rem; … }` |
|
|
727
|
+
| `font-thin` … `font-black` | 100–900 | `@weight { bold: 700; … }` |
|
|
728
|
+
| `ease-in`, `ease-out`, `ease-in-out` | cubic-beziers | `@ease { in: cubic-bezier(0.4, 0, 1, 1); … }` |
|
|
729
|
+
| `blur-xs` … `blur-3xl`, bare `blur` | 2px–64px, 8px | `@blur { md: 12px; … }` |
|
|
730
|
+
| `animate-spin`, `animate-pulse`, `animate-bounce`, `animate-ping` | four loops | `@animate { spin: spin 1s linear infinite { … } }` |
|
|
731
|
+
| `p-fluid-*`, `text-fluid-*`, `fluid-<name>` | 20rem–80rem ramp | `@fluid { min: 20rem; max: 80rem; }` |
|
|
732
|
+
|
|
733
|
+
The keyword and arbitrary forms are untouched: `blur-none`, `blur-[3px]`,
|
|
734
|
+
`ease-linear`, `ease-[cubic-bezier(…)]`, `animate-none`, `animate-[…]`,
|
|
735
|
+
`font-[850]`, and the whole enter/exit system (`animate-in`, `fade-in-50`,
|
|
736
|
+
`slide-in-from-top-4`, `blur-in-8`) need no tokens and keep working.
|
|
737
|
+
`max-w-sm` is a container width, not a breakpoint, and is unchanged.
|
|
738
|
+
|
|
739
|
+
Two consequences worth naming. A `@fluid` range no longer half-exists: with
|
|
740
|
+
no `min`/`max` the `--fluid-*` tokens are not written and every fluid
|
|
741
|
+
utility resolves to nothing, rather than ramping across a range nobody
|
|
742
|
+
chose. And `@fluid text`/`@fluid spacing`/named ranges no longer inherit a
|
|
743
|
+
shipped range to fill their gaps — a bound that is absent is simply not
|
|
744
|
+
configured, so `[RI-1022]`/`[RI-1023]` now fire only on a bound you
|
|
745
|
+
actually wrote.
|
|
746
|
+
|
|
747
|
+
- **The shipped type scale is gone.** No `--text-*` tokens ship any more: the
|
|
748
|
+
14-step `2xs`–`9xl` text scale, the `leading-*` scale (`3`–`10`, `none`,
|
|
749
|
+
`tight`, `snug`, `normal`, `relaxed`, `loose`), and the `tracking-*` scale
|
|
750
|
+
(`tighter`–`widest`) are removed. Type is now a project decision, not a
|
|
751
|
+
shipped opinion. Every value form still works — `text-[18px]`,
|
|
752
|
+
`text-lg/[1.5]`, `leading-px`, `leading-[1.5]`, `tracking-[0.1em]` — and
|
|
753
|
+
`text-{color}` is untouched. Named sizes come back with `@text lg: 1.25rem,
|
|
754
|
+
1.4;`, `@leading tight: 1.25;`, and `@tracking wide: 0.025em;`.
|
|
755
|
+
`text-fluid-{size}` needs at least two `@text` steps to interpolate
|
|
756
|
+
between, and the `text-lg/7` modifier reads `@leading` tokens.
|
|
757
|
+
|
|
758
|
+
- **`DEFAULT_TEXT_SIZES` is no longer exported.** `ri()` classified
|
|
759
|
+
`text-{name}` as a size against a hardcoded list that never matched the
|
|
760
|
+
shipped scale (it held `base`, which the scale lacked, and lacked `md`,
|
|
761
|
+
which the scale had). The merger now learns every size name from the
|
|
762
|
+
compiled theme, the way it already learns custom colors and font slots.
|
|
763
|
+
Before the first compile, `text-{name}` reads as a color.
|
|
764
|
+
|
|
765
|
+
### Fixed
|
|
766
|
+
|
|
767
|
+
- **A rejected utility block no longer corrupts the entries beside it.** A
|
|
768
|
+
block is cut from the directive body whether or not its name survived, but
|
|
769
|
+
the body was only swapped in when at least one block parsed. So a scale whose
|
|
770
|
+
blocks were *all* rejected kept its raw `{ … }` text, and the key/value
|
|
771
|
+
parsers — which do not read braces — took the block's own declarations for
|
|
772
|
+
scale entries: `@shadow { bad.name-* { box-shadow: … } }` defined a shadow
|
|
773
|
+
called `box-shadow`. Ten of the twelve block-taking scales were affected;
|
|
774
|
+
`@rounded` and `@animate` parse braces themselves and were not.
|
|
775
|
+
|
|
776
|
+
- **A circular `@shadow` alias chain is reported.** `a: shadow-b; b: shadow-a`
|
|
777
|
+
and the self-alias `a: shadow-a` were accepted and rewritten into `var()`
|
|
778
|
+
references that point at each other, which CSS treats as guaranteed-invalid —
|
|
779
|
+
the shadow resolved to nothing, with nothing said. Both now warn with the new
|
|
780
|
+
`[RI-1125]` and keep their value verbatim, the way `@color` has always
|
|
781
|
+
reported the same shape with `[RI-1107]`. A chain that is not a cycle still
|
|
782
|
+
resolves.
|
|
783
|
+
|
|
784
|
+
- **`FluidUnit` admits the container-query units.** `@fluid { unit: cqw; }`
|
|
785
|
+
validated and emitted correctly, but the exported type still listed only the
|
|
786
|
+
four viewport units, so a `FluidConfig` held a value its own type rejected.
|
|
787
|
+
|
|
788
|
+
- **An empty weight in a `@font` face is no longer read as weight zero.**
|
|
789
|
+
`Number("")` is `0` and finite, so the trailing comma in `weight: 400,700,`
|
|
790
|
+
produced a face covering weight 0 and listed it in the `[RI-1504]` inventory.
|
|
791
|
+
A range is also read either way round now, so `weight: 900 300` describes the
|
|
792
|
+
same span as `300 900`.
|
|
793
|
+
|
|
794
|
+
- **Named radii reach completions.** `@rounded { roof: 24px; }` compiled
|
|
795
|
+
`rounded-roof` and resolved it on hover, but class enumeration never read
|
|
796
|
+
`theme.radii`, so the name was offered by neither the completion list nor the
|
|
797
|
+
generated types — unlike every sibling scale, which reads its own record.
|
|
798
|
+
|
|
799
|
+
- **`ri-disable` now reaches the scanner codes on every surface.** The
|
|
800
|
+
file-wide comment is the only form that can silence a scan warning
|
|
801
|
+
(`RI-14xx`), because those codes carry no position in your CSS — but the
|
|
802
|
+
scanned build path pushed them without consulting it, so
|
|
803
|
+
`/* ri-disable RI-1408 */` worked headless and did nothing under the CLI,
|
|
804
|
+
PostCSS, and Vite. The scan warnings now enter through the same filter as
|
|
805
|
+
every other stage. Vite's `@apply` expansion runs before any compile, so it
|
|
806
|
+
reads the codes straight out of the entry it holds.
|
|
807
|
+
|
|
808
|
+
- **A `ri-disable-next-line` comment outside a directive body reports its own
|
|
809
|
+
mistakes.** Only directive bodies were read for pragma errors, so a typo or
|
|
810
|
+
an unsilenceable code in a top-level comment was ignored without the
|
|
811
|
+
`[RI-1040]` it promises. Such a comment is read once now, and an in-body
|
|
812
|
+
comment still warns exactly once.
|
|
813
|
+
|
|
814
|
+
- **A named scale entry no longer loses to the built-in it shadows.**
|
|
815
|
+
`@shadow { none: … }`, `@blur { none: … }` and `@duration { initial: … }`
|
|
816
|
+
parsed and resolved, then the generator's own keyword branch answered
|
|
817
|
+
first and the value was dropped without a word. Every named scale now
|
|
818
|
+
resolves theme-first, matching `@z`, `@leading`, `@opacity`, and `@ease`,
|
|
819
|
+
which already did.
|
|
820
|
+
|
|
821
|
+
- **`[RI-1408]` no longer fires on large files that hold no variant groups.**
|
|
822
|
+
The expansion budget counted plain pass-through text toward its 100,000
|
|
823
|
+
character limit, so any scanned file over that size warned as soon as it
|
|
824
|
+
held a single `{` — which every JavaScript and TypeScript file does. The
|
|
825
|
+
budget now counts only what expansion adds, so it measures the growth it
|
|
826
|
+
was written to bound. Input (500,000, `[RI-1407]`) and brace depth (10,
|
|
827
|
+
`[RI-1409]`) are unchanged.
|
|
828
|
+
- **A single oversized variant group is measured before it is built.** The
|
|
829
|
+
budget was only read between groups, so one group — its prefix copied onto
|
|
830
|
+
every member — could allocate far past the limit before anything stopped
|
|
831
|
+
it. Each group is now sized first and left verbatim when it does not fit.
|
|
832
|
+
|
|
833
|
+
### Changed
|
|
834
|
+
|
|
835
|
+
- **Clashing with a built-in class name now warns with the new `[RI-1124]`.**
|
|
836
|
+
`@rounded { full: 30px; }` takes over `rounded-full`; the warning names the
|
|
837
|
+
class so the takeover is a choice, not a surprise. It fires for every named
|
|
838
|
+
scale, and only for names the consumer actually wrote — replacing a default
|
|
839
|
+
token such as `@color { red: … }` or `@blur { sm: … }` stays quiet. The set
|
|
840
|
+
of built-in names is read from the generators themselves rather than from a
|
|
841
|
+
hand-kept list, so it cannot drift. Where the built-in belongs to another
|
|
842
|
+
family and keeps the class — `blur-in` is an enter-animation utility that
|
|
843
|
+
only shares the `blur-` prefix — the warning says so instead of claiming a
|
|
844
|
+
takeover that did not happen.
|
|
845
|
+
|
|
846
|
+
- **`ri()` resolves a named animation as an animation.** `animate-{name}`
|
|
847
|
+
merged through a hand-kept list of the shipped names, so a name from
|
|
848
|
+
`@animate` — and now every animation name, since none ships — was not
|
|
849
|
+
classified at all and could not replace another. The `animate-` prefix now
|
|
850
|
+
carries `animation`, the way `ease-` and `blur-` already carried theirs.
|
|
851
|
+
|
|
852
|
+
- **`[RI-1004]` no longer names `sm/md/lg/xl` as built-in variants.** No
|
|
853
|
+
breakpoint ships, so the suggestion pointed at variants that do not exist
|
|
854
|
+
until `@breakpoint` names them. It now says so.
|
|
855
|
+
|
|
856
|
+
- **Variant-group diagnostics name their source file.** `[RI-1407]`,
|
|
857
|
+
`[RI-1408]` and `[RI-1409]` now read `[RI-1408] src/App.tsx: …`, matching
|
|
858
|
+
`[RI-1411]`. Under PostCSS and Vite these warnings carried no location at
|
|
859
|
+
all, so a project-wide warning gave nothing to search for. Warnings
|
|
860
|
+
deduplicate on the full text, so a repeated breach now reports once per
|
|
861
|
+
file instead of once per project.
|
|
862
|
+
|
|
863
|
+
- **A rebuild reads only the files that changed.** `rainbowindex --watch`
|
|
864
|
+
and the Vite dev server now let the watcher own cache invalidation. The
|
|
865
|
+
source file list is cached until a file is added or removed — the CLI
|
|
866
|
+
watcher joins the Vite plugin, which already cached it — and a file's
|
|
867
|
+
scanned classes are trusted until the watcher reports that file changed,
|
|
868
|
+
so a rebuild no longer runs a `stat()` on every source file to learn that
|
|
869
|
+
one of them moved. The union of scanned classes is also kept between
|
|
870
|
+
rebuilds as a multiset, and only the files whose result changed are folded
|
|
871
|
+
again: re-unioning from scratch costs one set insert per class occurrence,
|
|
872
|
+
which on a 2000-file project is roughly 480,000 inserts to rediscover the
|
|
873
|
+
same few hundred names. A one-shot build arms neither cache — with no
|
|
874
|
+
watcher to evict entries, it would serve whatever it read last.
|
|
875
|
+
|
|
876
|
+
- **`compileProject()` reuses its analysis when the CSS is byte-identical.**
|
|
877
|
+
The memo that the CLI, PostCSS, and Vite builds already shared now sits
|
|
878
|
+
with the analysis itself, so the headless API gets it too. Repeat compiles
|
|
879
|
+
of one entry keep a single theme object, which is what every downstream
|
|
880
|
+
cache is keyed on — custom utilities, variants, and the per-class compile
|
|
881
|
+
memo. Warnings and diagnostics are still copied per call, so a caller that
|
|
882
|
+
mutates them cannot corrupt the memo.
|
|
883
|
+
|
|
8
884
|
## [0.5.1] - 2026-08-28
|
|
9
885
|
|
|
10
886
|
### Fixed
|