rainbowindex 0.5.0 → 0.6.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 CHANGED
@@ -5,6 +5,536 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2026-09-01
9
+
10
+ ### Added
11
+
12
+ - **Numeric font weights, checked against the fonts you load.** `font-<number>`
13
+ now sets `font-weight` directly, for any weight from 1 to 1000 —
14
+ `font-300`, `font-500`, `font-617`. A named `@weight` token of the same
15
+ spelling still wins, and `font-[850]` is unchanged.
16
+
17
+ The number is compared against the `weight` of every loaded `@font` face. A
18
+ range (`weight: 300 900`, the variable-font case) accepts every number
19
+ between its bounds; a list or a single value (`weight: 400,700`) accepts
20
+ only those numbers. A weight that no face provides warns with the new
21
+ `[RI-1504]`, and the message names the weights that are available.
22
+
23
+ A class on its own carries no family, and a page can load several fonts, so
24
+ that check is a union: one covering face is enough. It stays quiet with no
25
+ `@font` block, and with a `system` slot, whose OS font has every weight.
26
+
27
+ Where the family *is* named — a `font-<slot>` beside the weight in one
28
+ `@apply` / `@a` / `@utility` class list — that family alone decides:
29
+
30
+ ```css
31
+ [data-slot="code"] {
32
+ @a font-mono font-550; /* Fira Code does not provide weight 550. It has 400, 700. */
33
+ }
34
+ ```
35
+
36
+ The last family in the list wins, and the two classes must share a variant
37
+ prefix (`md:font-mono` does not set the family for a plain `font-550`). A
38
+ weight no font provides is still reported once, by the union check. A class
39
+ written in markup has no list — the scanner keeps only the set of names — so
40
+ the union check governs there.
41
+
42
+ Completions and the generated class types offer `font-100` … `font-900`
43
+ beside the named `@weight` tokens, and read `font-<number>` as an open
44
+ numeric template.
45
+
46
+ - **Three editor capabilities.** `rainbowindex/editor` gains `isSuppressible`
47
+ (`diagnostic-suppression`), so an editor offers a `ri-disable` comment only
48
+ where one would work; `weightIsLoaded` and `describeLoadedWeights`
49
+ (`font-weight-coverage`), the `[RI-1504]` check, so it can answer which
50
+ loaded fonts carry a weight; and `ThemeTokens.radii` / `ThemeTokens.fluidRanges`
51
+ (`named-radii-and-fluid-ranges`), which named radii and named `@fluid` ranges
52
+ had no token surface for. Gate on the capability strings, never on `version`.
53
+
54
+ - **`ri-disable` comments.** Two plain CSS comments silence a diagnostic, so a
55
+ stylesheet carrying them stays valid CSS for every other tool.
56
+
57
+ `/* ri-disable RI-1124 */` anywhere in the CSS entry silences that code
58
+ everywhere. It is the only form that reaches the scanner and compile codes
59
+ (14xx, 15xx), which carry no position in your CSS. One comment may name
60
+ several codes: `/* ri-disable RI-1124, RI-1122 */`.
61
+
62
+ `/* ri-disable-next-line RI-1124 */` silences one place. Inside a scale body
63
+ it guards the entry that follows; outside a body it guards the next
64
+ directive:
65
+
66
+ ```css
67
+ @rounded {
68
+ roof: 24px;
69
+ /* ri-disable-next-line RI-1124 */
70
+ full: 30px;
71
+ hut: 8px; /* still checked */
72
+ }
73
+ ```
74
+
75
+ Entry precision is available where the emitter knows the entry —
76
+ `[RI-1035]`, `[RI-1121]`, `[RI-1122]`, `[RI-1124]`. Other codes fall back to
77
+ the directive.
78
+
79
+ `RI-00xx` and `RI-20xx` cannot be silenced: they report a broken build or a
80
+ broken call, not a style choice. Naming one warns with the new `[RI-1040]`,
81
+ as does a comment that names no readable code.
82
+
83
+ - **`compileProject()` reports what it silenced.** The result gains
84
+ `suppressed`, the set of codes the entry's `ri-disable` comments named, so
85
+ a caller that pushes warnings of its own can drop the ones the author
86
+ hid. Every stage inside the compile already pushes through it.
87
+
88
+ - **Fluid endpoint pairs.** Fluid utilities take both ends of the ramp:
89
+ `p-fluid-4/8` grows from step 4 to step 8 across the `@fluid` range, and
90
+ `text-fluid-sm/3xl` ramps between two type steps. Arbitrary lengths and
91
+ `(--var)` endpoints mix (`p-fluid-[0.5rem]/(--x)`), zero is a legal
92
+ endpoint, and a descending pair (`p-fluid-8/4`) shrinks as the viewport
93
+ grows. Fluid type also takes the line-height modifier now:
94
+ `text-fluid-lg/7`, `text-fluid-sm/3xl/tight`.
95
+
96
+ - **Named `@fluid` ranges and scope classes.** `@fluid compact { min: 20rem;
97
+ max: 48rem; }` defines a range, the tokens `--fluid-compact-{min,max}`, and
98
+ the scope class `fluid-compact`, which points every fluid utility on the
99
+ element and its descendants at that range through the inherited
100
+ `--fluid-scope-{min,max}` pair. A unit on a named range warns with
101
+ `RI-1039`; an unknown range name on the class warns with `RI-1503`.
102
+
103
+ - **Container-query units in `@fluid`.** `unit` accepts `cqw`, `cqi`,
104
+ `cqmin`, and `cqmax`, so fluid ramps can track a container instead of the
105
+ viewport.
106
+
107
+ - **Named radii in `@rounded`.** A key without a `--` prefix now names a
108
+ radius: `@rounded { roof: 24px; }` gives the class `rounded-roof` and the
109
+ token `--rounded-roof`, and the name works with the side and corner
110
+ suffixes (`rounded-tl-roof`). `--corner-scale` is still the only option;
111
+ any other `--` key still warns with `[RI-1122]`. Radii without a name stay
112
+ spacing multiples, so `rounded-4` is unchanged.
113
+
114
+ - **Utility blocks in every named scale.** A `name { … }` or `name-* { … }`
115
+ block in a scale body defines a utility in that scale's class family, so the
116
+ math sits next to the tokens it reads:
117
+
118
+ ```css
119
+ @rounded {
120
+ roof: 24px;
121
+ roof-minus-* {
122
+ border-radius: calc(var(--rounded-roof) - var(--value) * var(--spacing));
123
+ }
124
+ }
125
+ ```
126
+
127
+ `rounded-roof-minus-2` subtracts two spacing steps from `--rounded-roof`.
128
+ The block uses the `@utility` grammar and the name takes the family's
129
+ prefix. `@rounded`, `@shadow`, `@blur`, `@z`, `@leading`, `@tracking`,
130
+ `@opacity`, `@duration`, `@ease`, `@weight`, `@text` and `@animate` all
131
+ accept them; `@weight` blocks land under `font-`.
132
+
133
+ `@color` does not: a colour name feeds `bg-`, `text-`, `border-`, `ring-`
134
+ and more, so a block would have no one family to land in. `@breakpoint`
135
+ names variants rather than utilities, and `@spacing` holds a single base
136
+ value.
137
+
138
+ A colon is what separates a utility block from the two block grammars that
139
+ already existed. `key: value { … }` stays what it was — `@color` options,
140
+ `@animate` keyframes — and only a block with no key before it is a utility.
141
+
142
+ Radius tokens are written to `:root` whether used or not, because a block
143
+ body is raw CSS that no usage pass can read.
144
+
145
+ - **`@shadow` aliases.** A shadow value that is only another shadow's class
146
+ name now emits a reference to it: `@shadow { md: …; card: shadow-md; }`
147
+ gives `--shadow-card: var(--shadow-md)`. Using `shadow-card` pulls
148
+ `--shadow-md` into `:root` alongside it, so an alias never renders against
149
+ an undefined variable. The target may be defined in a later `@shadow`
150
+ block. An alias to a token that does not exist warns with the new
151
+ `[RI-1123]` and keeps its value verbatim.
152
+
153
+ ### Removed
154
+
155
+ - **`@utility` no longer accepts a leading `.` on the name.** `@utility .card`
156
+ used to be silently treated as `@utility card`. No other directive did
157
+ this, and the dot was ignored without a word in `@source inline(".card")`,
158
+ `safelist(".card")`, and `class=".card"` markup, so the one exception
159
+ taught a rule that held nowhere else. A dotted name now warns with
160
+ `[RI-1035]` and is skipped, like every other invalid name. Write
161
+ `@utility card`.
162
+
163
+ - **The shipped shadow scales are gone.** No `shadow-*` tokens ship any more:
164
+ the layered `px`–`2xl` scale and its building blocks (`line`, `drop`,
165
+ `hi-1`–`hi-4`, `dark-line`, `ring`, `layer-1`–`layer-7`) are removed, along
166
+ with the hardcoded `inset-shadow-*`, `text-shadow-*`, and `drop-shadow-*`
167
+ size scales. A shadow is now a project decision, not a shipped opinion.
168
+ Every value form still works — `shadow-none`, `shadow-{color}`,
169
+ `shadow-[v]`, and the same forms for the other three families — and `ring`
170
+ and `inset-ring` are untouched. Named sizes come back with
171
+ `@shadow md: …;`, and bare `shadow` reads the `DEFAULT` token.
172
+ Because the scale is empty by default, bare `shadow` is no longer a
173
+ built-in static utility: it resolves only when the theme defines
174
+ `DEFAULT` (or `md`).
175
+
176
+ - **The last shipped scales are gone: breakpoints, weights, easing, blur,
177
+ animations, and the fluid range.** The package now ships exactly two
178
+ defaults — the colour palette and the `0.25rem` spacing base. Everything
179
+ else is a project decision:
180
+
181
+ | Gone | Was | Comes back with |
182
+ | --- | --- | --- |
183
+ | `sm:` `md:` `lg:` `xl:` and `@sm:` `@md:` … | 40/48/64/80rem | `@breakpoint { sm: 40rem; … }` |
184
+ | `font-thin` … `font-black` | 100–900 | `@weight { bold: 700; … }` |
185
+ | `ease-in`, `ease-out`, `ease-in-out` | cubic-beziers | `@ease { in: cubic-bezier(0.4, 0, 1, 1); … }` |
186
+ | `blur-xs` … `blur-3xl`, bare `blur` | 2px–64px, 8px | `@blur { md: 12px; … }` |
187
+ | `animate-spin`, `animate-pulse`, `animate-bounce`, `animate-ping` | four loops | `@animate { spin: spin 1s linear infinite { … } }` |
188
+ | `p-fluid-*`, `text-fluid-*`, `fluid-<name>` | 20rem–80rem ramp | `@fluid { min: 20rem; max: 80rem; }` |
189
+
190
+ The keyword and arbitrary forms are untouched: `blur-none`, `blur-[3px]`,
191
+ `ease-linear`, `ease-[cubic-bezier(…)]`, `animate-none`, `animate-[…]`,
192
+ `font-[850]`, and the whole enter/exit system (`animate-in`, `fade-in-50`,
193
+ `slide-in-from-top-4`, `blur-in-8`) need no tokens and keep working.
194
+ `max-w-sm` is a container width, not a breakpoint, and is unchanged.
195
+
196
+ Two consequences worth naming. A `@fluid` range no longer half-exists: with
197
+ no `min`/`max` the `--fluid-*` tokens are not written and every fluid
198
+ utility resolves to nothing, rather than ramping across a range nobody
199
+ chose. And `@fluid text`/`@fluid spacing`/named ranges no longer inherit a
200
+ shipped range to fill their gaps — a bound that is absent is simply not
201
+ configured, so `[RI-1022]`/`[RI-1023]` now fire only on a bound you
202
+ actually wrote.
203
+
204
+ - **The shipped type scale is gone.** No `--text-*` tokens ship any more: the
205
+ 14-step `2xs`–`9xl` text scale, the `leading-*` scale (`3`–`10`, `none`,
206
+ `tight`, `snug`, `normal`, `relaxed`, `loose`), and the `tracking-*` scale
207
+ (`tighter`–`widest`) are removed. Type is now a project decision, not a
208
+ shipped opinion. Every value form still works — `text-[18px]`,
209
+ `text-lg/[1.5]`, `leading-px`, `leading-[1.5]`, `tracking-[0.1em]` — and
210
+ `text-{color}` is untouched. Named sizes come back with `@text lg: 1.25rem,
211
+ 1.4;`, `@leading tight: 1.25;`, and `@tracking wide: 0.025em;`.
212
+ `text-fluid-{size}` needs at least two `@text` steps to interpolate
213
+ between, and the `text-lg/7` modifier reads `@leading` tokens.
214
+
215
+ - **`DEFAULT_TEXT_SIZES` is no longer exported.** `ri()` classified
216
+ `text-{name}` as a size against a hardcoded list that never matched the
217
+ shipped scale (it held `base`, which the scale lacked, and lacked `md`,
218
+ which the scale had). The merger now learns every size name from the
219
+ compiled theme, the way it already learns custom colors and font slots.
220
+ Before the first compile, `text-{name}` reads as a color.
221
+
222
+ ### Fixed
223
+
224
+ - **A rejected utility block no longer corrupts the entries beside it.** A
225
+ block is cut from the directive body whether or not its name survived, but
226
+ the body was only swapped in when at least one block parsed. So a scale whose
227
+ blocks were *all* rejected kept its raw `{ … }` text, and the key/value
228
+ parsers — which do not read braces — took the block's own declarations for
229
+ scale entries: `@shadow { bad.name-* { box-shadow: … } }` defined a shadow
230
+ called `box-shadow`. Ten of the twelve block-taking scales were affected;
231
+ `@rounded` and `@animate` parse braces themselves and were not.
232
+
233
+ - **A circular `@shadow` alias chain is reported.** `a: shadow-b; b: shadow-a`
234
+ and the self-alias `a: shadow-a` were accepted and rewritten into `var()`
235
+ references that point at each other, which CSS treats as guaranteed-invalid —
236
+ the shadow resolved to nothing, with nothing said. Both now warn with the new
237
+ `[RI-1125]` and keep their value verbatim, the way `@color` has always
238
+ reported the same shape with `[RI-1107]`. A chain that is not a cycle still
239
+ resolves.
240
+
241
+ - **`FluidUnit` admits the container-query units.** `@fluid { unit: cqw; }`
242
+ validated and emitted correctly, but the exported type still listed only the
243
+ four viewport units, so a `FluidConfig` held a value its own type rejected.
244
+
245
+ - **An empty weight in a `@font` face is no longer read as weight zero.**
246
+ `Number("")` is `0` and finite, so the trailing comma in `weight: 400,700,`
247
+ produced a face covering weight 0 and listed it in the `[RI-1504]` inventory.
248
+ A range is also read either way round now, so `weight: 900 300` describes the
249
+ same span as `300 900`.
250
+
251
+ - **Named radii reach completions.** `@rounded { roof: 24px; }` compiled
252
+ `rounded-roof` and resolved it on hover, but class enumeration never read
253
+ `theme.radii`, so the name was offered by neither the completion list nor the
254
+ generated types — unlike every sibling scale, which reads its own record.
255
+
256
+ - **`ri-disable` now reaches the scanner codes on every surface.** The
257
+ file-wide comment is the only form that can silence a scan warning
258
+ (`RI-14xx`), because those codes carry no position in your CSS — but the
259
+ scanned build path pushed them without consulting it, so
260
+ `/* ri-disable RI-1408 */` worked headless and did nothing under the CLI,
261
+ PostCSS, and Vite. The scan warnings now enter through the same filter as
262
+ every other stage. Vite's `@apply` expansion runs before any compile, so it
263
+ reads the codes straight out of the entry it holds.
264
+
265
+ - **A `ri-disable-next-line` comment outside a directive body reports its own
266
+ mistakes.** Only directive bodies were read for pragma errors, so a typo or
267
+ an unsilenceable code in a top-level comment was ignored without the
268
+ `[RI-1040]` it promises. Such a comment is read once now, and an in-body
269
+ comment still warns exactly once.
270
+
271
+ - **A named scale entry no longer loses to the built-in it shadows.**
272
+ `@shadow { none: … }`, `@blur { none: … }` and `@duration { initial: … }`
273
+ parsed and resolved, then the generator's own keyword branch answered
274
+ first and the value was dropped without a word. Every named scale now
275
+ resolves theme-first, matching `@z`, `@leading`, `@opacity`, and `@ease`,
276
+ which already did.
277
+
278
+ - **`[RI-1408]` no longer fires on large files that hold no variant groups.**
279
+ The expansion budget counted plain pass-through text toward its 100,000
280
+ character limit, so any scanned file over that size warned as soon as it
281
+ held a single `{` — which every JavaScript and TypeScript file does. The
282
+ budget now counts only what expansion adds, so it measures the growth it
283
+ was written to bound. Input (500,000, `[RI-1407]`) and brace depth (10,
284
+ `[RI-1409]`) are unchanged.
285
+ - **A single oversized variant group is measured before it is built.** The
286
+ budget was only read between groups, so one group — its prefix copied onto
287
+ every member — could allocate far past the limit before anything stopped
288
+ it. Each group is now sized first and left verbatim when it does not fit.
289
+
290
+ ### Changed
291
+
292
+ - **Clashing with a built-in class name now warns with the new `[RI-1124]`.**
293
+ `@rounded { full: 30px; }` takes over `rounded-full`; the warning names the
294
+ class so the takeover is a choice, not a surprise. It fires for every named
295
+ scale, and only for names the consumer actually wrote — replacing a default
296
+ token such as `@color { red: … }` or `@blur { sm: … }` stays quiet. The set
297
+ of built-in names is read from the generators themselves rather than from a
298
+ hand-kept list, so it cannot drift. Where the built-in belongs to another
299
+ family and keeps the class — `blur-in` is an enter-animation utility that
300
+ only shares the `blur-` prefix — the warning says so instead of claiming a
301
+ takeover that did not happen.
302
+
303
+ - **`ri()` resolves a named animation as an animation.** `animate-{name}`
304
+ merged through a hand-kept list of the shipped names, so a name from
305
+ `@animate` — and now every animation name, since none ships — was not
306
+ classified at all and could not replace another. The `animate-` prefix now
307
+ carries `animation`, the way `ease-` and `blur-` already carried theirs.
308
+
309
+ - **`[RI-1004]` no longer names `sm/md/lg/xl` as built-in variants.** No
310
+ breakpoint ships, so the suggestion pointed at variants that do not exist
311
+ until `@breakpoint` names them. It now says so.
312
+
313
+ - **Variant-group diagnostics name their source file.** `[RI-1407]`,
314
+ `[RI-1408]` and `[RI-1409]` now read `[RI-1408] src/App.tsx: …`, matching
315
+ `[RI-1411]`. Under PostCSS and Vite these warnings carried no location at
316
+ all, so a project-wide warning gave nothing to search for. Warnings
317
+ deduplicate on the full text, so a repeated breach now reports once per
318
+ file instead of once per project.
319
+
320
+ - **A rebuild reads only the files that changed.** `rainbowindex --watch`
321
+ and the Vite dev server now let the watcher own cache invalidation. The
322
+ source file list is cached until a file is added or removed — the CLI
323
+ watcher joins the Vite plugin, which already cached it — and a file's
324
+ scanned classes are trusted until the watcher reports that file changed,
325
+ so a rebuild no longer runs a `stat()` on every source file to learn that
326
+ one of them moved. The union of scanned classes is also kept between
327
+ rebuilds as a multiset, and only the files whose result changed are folded
328
+ again: re-unioning from scratch costs one set insert per class occurrence,
329
+ which on a 2000-file project is roughly 480,000 inserts to rediscover the
330
+ same few hundred names. A one-shot build arms neither cache — with no
331
+ watcher to evict entries, it would serve whatever it read last.
332
+
333
+ - **`compileProject()` reuses its analysis when the CSS is byte-identical.**
334
+ The memo that the CLI, PostCSS, and Vite builds already shared now sits
335
+ with the analysis itself, so the headless API gets it too. Repeat compiles
336
+ of one entry keep a single theme object, which is what every downstream
337
+ cache is keyed on — custom utilities, variants, and the per-class compile
338
+ memo. Warnings and diagnostics are still copied per call, so a caller that
339
+ mutates them cannot corrupt the memo.
340
+
341
+ ## [0.5.1] - 2026-08-28
342
+
343
+ ### Fixed
344
+
345
+ - **Inline SVG path data is no longer scanned for classes.** Raising the
346
+ line-length guard to 10,000 characters in 0.5.0 let a multi-KB `d="…"`
347
+ attribute reach the whole-file token scan, where path data tokenizes
348
+ cleanly against the class grammar: one 10 KB icon component went from 33
349
+ candidates to 845, the extra 812 being fragments like `9.17-57.2` and
350
+ `40c-.35-1.1-1.04`. They matched no utility and carried the `plain` origin
351
+ editors skip, so nothing rendered wrong and nothing was reported — each
352
+ one just cost a compile lookup and a cache entry on every build. `d` and
353
+ `points` values are now blanked before extraction, in every file type.
354
+ Only quoted values are matched, so `d={expr}` bindings still yield their
355
+ classes.
356
+ - **A JavaScript term is no longer reported as a class.** Candidate origins
357
+ were assigned purely by span containment, so any token the whole-file scan
358
+ matched inside a helper call or class attribute inherited that context's
359
+ origin. The token scan's grammar also matches bare identifiers, so
360
+ `ri(mode === "default" ? "fill-white" : "fill-black")` reported `mode` with
361
+ `origin: "helper"` — and editors, which read a certain origin as "this is a
362
+ class", flagged it as an unknown class. A candidate now inherits a context's
363
+ origin only when a context-aware collector tokenized it; containment still
364
+ decides which context wins. Extracted values are unchanged, so no generated
365
+ CSS moves.
366
+ - **Bare unquoted class attributes keep their `attribute` origin.**
367
+ `collectAssignedValues` now tokenizes an undelimited value (`class=flex` in
368
+ HTML, Vue and Svelte) the same way it already tokenized a quoted one. Without
369
+ this they would have lost provenance and demoted to `plain`.
370
+ - **`@apply` no longer emits a rule with no selector.** A rule carrying both
371
+ the `group` marker and a `group-*` variant resolved its group root to
372
+ itself, so stripping the root prefix off the resolved selector left nothing:
373
+ `.self { @apply group group-hover:underline; }` emitted a bare
374
+ ` { text-decoration-line: underline; }`, which a browser discards as a parse
375
+ error, taking the declarations with it. The group root is the element, so
376
+ the variant now targets the element — `.self:hover`. The same shape reached
377
+ by climbing rather than matching in place, a nested `&` block inside the
378
+ group root, is fixed with it.
379
+ - **RI-1002 no longer fires for a bracket token the scanner merely read.** An
380
+ unresolved arbitrary value is a typo worth reporting when the author wrote
381
+ the class, but the scanner reads whole files, comments and prose included,
382
+ where `min-[437px]` is just text — and every such token warned. Classes
383
+ written by hand still warn: `@source inline(...)`, `@apply`, and a
384
+ caller-supplied `classNames` list. Provenance is not baked into the compile
385
+ memo, so a class that is both scanned and authored warns once rather than
386
+ never.
387
+
388
+ ### Added
389
+
390
+ - **RI-1412 — whitespace in an arbitrary value now warns.** A class name
391
+ cannot contain whitespace: `class`, `@a`/`@apply`, and `safelist()` all
392
+ split on it, so `bg-[url('a b')]` reaches the browser as the two tokens
393
+ `bg-[url('a` and `b')]` and matches nothing. The scanner has always dropped
394
+ these, silently, leaving no CSS and no reason why. The warning names the
395
+ class and points at the `_` escape (`bg-[url('a_b')]` emits `url('a b')`).
396
+ It fires only where a collector treats its input as a class list — an
397
+ attribute value, a helper argument, a `safelist()` argument — so ordinary
398
+ JS and prose stay quiet, `styles["my class"]` in a `className` expression
399
+ included. Extracted values are unchanged.
400
+ - **`"expression"` candidate origin.** A string literal that is an operand of
401
+ `==`/`!=` (so also `===`/`!==`) cannot be a class list: in
402
+ `mode === "default"`, `"default"` is a value being compared. Such literals
403
+ are still extracted — dropping them would change generated CSS — but they
404
+ now report `origin: "expression"` instead of `"helper"`/`"attribute"`.
405
+ Editors should treat it like `"plain"` and never report it as a bad class.
406
+ Assignment is deliberately not matched: `const base = "px-2"` is an ordinary
407
+ class list.
408
+ - **`candidate-origin-provenance` capability.** Feature-detect both behaviours
409
+ above through `editorCapabilities`, never a version compare.
410
+ - **Functional custom utilities — `@utility name-* { … }`.** The body reads
411
+ `var(--value)` and the class suffix replaces it, nested blocks included:
412
+ `@utility glow-* { box-shadow: 0 0 var(--value) gold; }` answers `glow-4`
413
+ and `glow-[3px]`, with `[2px_4px]` decoding to `2px 4px`. An exact static
414
+ name beats a functional match, so `@utility card` and `@utility card-*`
415
+ coexist; the longest root wins between functional entries (`a-b-*` over
416
+ `a-*` for `a-b-4`); and neither a bare root (`glow`) nor a negated class
417
+ (`-glow-4`) matches. A suffix carrying `;`, `{`, or `}` is rejected rather
418
+ than allowed to break out of the declaration. `ri()` treats two suffixes of
419
+ one root as conflicting, so `ri("glow-4 glow-8")` keeps `glow-8`.
420
+ - **Consumer documentation in [`docs/`](docs/README.md).** One page per
421
+ subject — getting started, class syntax, utilities, theming, fonts, source
422
+ scanning, class merging, diagnostics, environment variables — and one per
423
+ integration surface — CLI, PostCSS plugin, Vite plugin, Vite+, Node API,
424
+ editor API. The README keeps the overview and links out.
425
+ - **Vite+ support.** The Vite plugin now adds every stylesheet that activates
426
+ Rainbow Index to `fmt.ignorePatterns`, so `vp check` no longer stops at
427
+ `Syntax error: component value is expected` before it can lint or type
428
+ check. Directive bodies are not valid CSS — a `@font` entry carries a block
429
+ after a declaration, a scale removes a token with `!name;`, `@fluid` takes
430
+ bare keywords, and `@apply` takes variant groups — and Oxfmt parses CSS
431
+ strictly. Vite+ reads its `fmt` block off the resolved Vite config, so the
432
+ plugin contributes the patterns from its `config` hook; plain Vite ignores
433
+ the extra key. Projects without the Vite plugin add the patterns by hand.
434
+ See [docs/vite-plus.md](docs/vite-plus.md).
435
+ - **`rainbowindex/oxlint`.** A new entry point with one Oxlint rule,
436
+ `prefer-ri`, which reports an import of `clsx`, `classnames`, or
437
+ `tailwind-merge`. Each merges classes against a Tailwind utility table, so
438
+ it resolves conflicts against the wrong utility set and never sees the
439
+ theme. The rule is off until a project enables it, and the plugin has no
440
+ dependencies.
441
+
442
+ ### Changed
443
+
444
+ - **`ri()` caches results in two generations instead of an LRU.** The old
445
+ cache moved every hit to the end of a `Map` so insertion order tracked
446
+ recency, which made the steady state — the same class lists on every render
447
+ — pay a delete and a re-insert per call. The new cache keeps a current and a
448
+ previous generation: a hit in the current one is a single `Map.get`, and
449
+ when the current fills it becomes the previous and a fresh one starts. A hit
450
+ on a previous entry promotes it, so hot keys survive the swap while cold
451
+ ones age out with the dropped generation. The cache now holds at most twice
452
+ `RI_CACHE_MAX` (500) entries instead of evicting the oldest quarter at the
453
+ cap, and `evictLRU` is gone with the design that needed it.
454
+ - **The per-class compile memo is bounded.** Keyed on theme identity, it grew
455
+ for the life of the process, so a long dev session could hold every class
456
+ ever compiled against a still-live theme. It now clears wholesale at 50,000
457
+ entries. Steady state is the project's whole scanned vocabulary, which sits
458
+ far below the cap — a lower one would clear mid-compile on every rebuild and
459
+ defeat the memo.
460
+ - **`@apply` resolution is cached per theme, not per invocation.** Every
461
+ rebuild re-resolved every `@apply` class from scratch, although the scan
462
+ analysis and pipeline memos keep the theme object stable across rebuilds, so
463
+ the old results were still valid. The cache is a `WeakMap` keyed on the
464
+ theme and dies with it. The `[RI-1005]` walk of a custom utility body is
465
+ cached alongside: its warnings are now replayed into every class that hits
466
+ the utility rather than emitted once per compile, so they survive a rebuild
467
+ in which the class that first triggered the walk is gone. The plugin still
468
+ dedupes repeats downstream.
469
+ - **The resolved source-file list is cached while a watcher runs.** Each
470
+ rebuild re-globbed the project to find the same files. A cached list is only
471
+ correct while something reacts to file adds and deletes, so caching is
472
+ opt-in: the Vite plugin arms it and clears it from the dev-server watcher
473
+ (`add`, `unlink`, `unlinkDir`). One-shot builds and `postcss-cli --watch`,
474
+ which has no watcher hook, keep the always-fresh glob. A glob already in
475
+ flight when an invalidation arrives is not cached.
476
+ - **A source edit that changes no class no longer re-transforms the CSS.** The
477
+ Vite plugin now keeps a sorted candidate list per source file and compares it
478
+ on every hot update. An edit to logic, comments, or copy leaves that list
479
+ identical, and identical candidates produce byte-identical CSS — so the
480
+ Rainbow Index stylesheets stay out of the update and only the edited module
481
+ reloads. A first sighting or an unreadable file invalidates conservatively.
482
+ - **`preload-fonts --help` names the faces that produce a tag.** Only local
483
+ file and raw-URL faces marked `preload` do. Google serves CSS rather than the
484
+ font binary, so a Google or system slot never emits one — but the help text
485
+ claimed `@font-face` was covered too. Text only; the command is unchanged.
486
+
487
+ ### Removed
488
+
489
+ - **The named radius scale.** `rounded-2xs` through `rounded-xl`, the bare
490
+ `rounded` / `rounded-t` / `rounded-tl` shorthands, the `--rounded-*`
491
+ variables, the `--rounded-roof` anchor, and the token body of `@rounded` are
492
+ all gone. A radius is now a spacing multiple: `rounded-4` is
493
+ `calc(var(--spacing) * 4 * var(--ri-rounded-scale, 1))`. `rounded-none`,
494
+ `rounded-full`, `rounded-scale-*`, arbitrary values, and every logical side
495
+ and corner (`rounded-t-4`, `rounded-ss-2`) are unchanged.
496
+
497
+ Two scales set the same shape, so they drifted apart: the named tokens hung
498
+ off `--rounded-roof` while the numeric forms hung off `--spacing`, and
499
+ `rounded-lg` next to `rounded-4` mixed two rhythms in one component. One
500
+ scale, anchored to spacing, keeps the whole system in step.
501
+
502
+ Migration: replace each named token with the spacing step you want. A
503
+ `@rounded` body that sets radius tokens now warns with `[RI-1122]`; keep
504
+ `--corner-scale` and drop the rest. `--roof` is gone — set the radius you
505
+ want on the element.
506
+
507
+ ### Notes for integrators
508
+
509
+ `ResolvedTheme` lost `rounded` and `roundedRoof`, `Theme` lost `rounded`,
510
+ `listThemeTokens()` no longer returns a `rounded` record, and
511
+ `CompilationResult` lost `usedRounded`. Nothing prunes `--rounded-*` any more,
512
+ because the token layer no longer emits it. `roundedShape` and
513
+ `roundedShapeScale` stay — `@rounded <shape>` is unchanged.
514
+
515
+ `CandidateOrigin` gained a member. Code that switches on it exhaustively must
516
+ handle `"expression"`; code that tests `origin !== "plain"` to mean "this is a
517
+ class" was already wrong and is now wrong in a new way — test for the origins
518
+ you trust instead.
519
+
520
+ `CompilationSnapshot` gained `customFunctionalProps`, the roots of functional
521
+ `@utility` entries and the properties each one claims. `createThemeSnapshot()`
522
+ fills it in; a snapshot built by hand needs the field.
523
+
524
+ `createCompiler().compile()` takes an optional third argument, the set of
525
+ classes the caller wrote by hand. Omit it and every class counts as authored,
526
+ which is what a caller assembling its own list wants. `compileProject()`
527
+ applies the same split on its own: `classNames` is authored, `sources` content
528
+ is scanned.
529
+
530
+ `getCustomUtility(theme, name)` is replaced by
531
+ `matchCustomUtility(utility, value, negative, theme)`, which returns the
532
+ matching entry and the text a functional body substitutes for `var(--value)`.
533
+ The `@apply` walk and the declaration expansion both go through it, so they
534
+ can never disagree about which utility a class hit. The old function resolved
535
+ static entries only. Neither name is exported from the package entry, so only
536
+ code importing the internal module path is affected.
537
+
8
538
  ## [0.5.0] - 2026-08-27
9
539
 
10
540
  ### Added