Repository navigation
Releases
v0.8.1
Summary
Vanity's testing kit now accepts Selenita 0.4 while preserving 0.3 compatibility. This patch also clarifies how to restart the development server when updating dependencies.
Fixed
- Selenita compatibility. The testing kit's optional peer range now includes Selenita 0.4 (
>=0.3.0 <0.5.0). The recommended setup uses 0.4.1, with observations independent of assertion order and markers that work across separate installs of the same version.
Documentation
- Dependency updates. Stop the development server before updating Vanity or its build host, then start it after installation completes. Reloading the browser does not restart loaded compiler code. The guide also explains how to investigate compilation errors that persist after restarting.
- Editor-DX tests. The testing guide explains independent observations and marker compatibility. Vanity's auto-import tests now recheck Selenita's native completion actions directly.
Upgrading
Existing applications need no migration. Selenita remains optional, and TypeScript and Vitest requirements are unchanged.
For editor-DX tests, the recommended Selenita version is:
pnpm add -D @mszr/selenita@^0.4.1If upgrading Selenita from 0.3:
- Replace its
CodeFixtype imports withCodeAction. - Keep Selenita copies that exchange fixture markers on the same version.
- Review any changed editor assertions: observations no longer depend on which assertion ran first. Diagnostics-first warming cannot establish what a user sees while typing.
v0.8.0
Summary
Vanity's layer order now travels with its CSS. Component rules keep their intended precedence even when their stylesheet arrives before the system stylesheet. Configured layer roots also work in Nuxt, and development edits retire old Vite stylesheets after each page installs their replacements. Overlapping saves avoid errors from outdated system inputs, while genuine failures stay visible and repair reliably.
Fixed
- Repository tooling typechecks from a clean checkout before the SDK is built.
- Component rules retain their rank above lower system layers, such as a reset, regardless of stylesheet delivery order. This includes Nuxt development, where stylesheets for other routes can arrive first.
compiler.layerOrdertakes effect in Nuxt development and builds.- Development edits that reorder layers or remove rules take effect after replacement CSS is installed. Compatible edits keep the page and runtime state; application-imported runtime identity changes, root-order changes, and configured-system recovery retain the reload boundary.
- Repairing a failed configured system reloads the page once, after all replacement CSS is ready.
- Overlapping system and stylesheet saves use the latest system, avoiding spurious missing-token errors and stale exported values. Genuine failures remain visible and identify any retained CSS as excluding the failed edit.
- Repairing overlapping failures installs current CSS for every consumer, including stylesheets an earlier failed update had already compiled.
- System edits update stylesheets previously requested only by SSR without Vite's misleading “outdated pre-bundle” error.
- Complete layer headers survive repeated compilation through cached systems, including independently bundled authoring code.
- Invalid layer or axis names, repeated CSS identities, and axes sharing a generated control attribute fail locally with an authored diagnostic instead of silently losing rules or coupling controls.
- Escaped axis names generate matching CSS selectors and DOM/SSR attributes, including native color-scheme controls and quoted names. Authored keys stay intact.
- Axis and mode keys such as
constructorand__proto__survive composition, token branches, native JSON, and restored runtime selection as own data. Independent systems keep independent branch data. - Direct and detached axis configurations preserve
derivecallbacks andnativepolicy, with contextual callback parameters and inferred branches. Authored mode values still win over derivations. - Axis types expose only capabilities their configuration guarantees. Optional derivations and callbacks returning
undefinedremain usable without promising an absent branch. - Reused plugin instances keep the correct layer-order asset across successive Vite HTML builds. Builds without listed layers skip asset-owner lookup, including overlapping builds on Vite 6.
- Token rename stays within its originating graph, including builder keys whose literal spelling also appears in unrelated graphs or types.
- Accepting the editor’s type-only declaration import clears its ambient-source notice; unrelated imports leave the notice intact.
- Authoring tests run through the application’s mounted Vite configuration on Vitest 5.
$case()preserves the selected case's value type in build and runtime handles. Dynamic addresses retain their possible values, reservations remainundefined, and extra address keys are rejected.- Extracted case selectors can be exported by declaration-emitting libraries. Runtime projection types share token identities across the core and runtime entrypoints, preserving cases and setters.
- Runtime custom-property helpers accept typed values created through the core entrypoint.
bindPort()accepts narrow ports and preserves their declaration types. - Token hovers show resolved public traits without repeating the system's token-definition and axis configuration types.
- Constructor, explicit-unit, composition, recipe, anatomy, port and runtime-control documentation survives native member completion, including published declarations. Port guidance describes
dec()and the actual metadata behavior. - Incompatible calculation sums name the dimensions and suggest compatible operands at the offending value; signature help explains the active operand. Known unions with a possible incompatible dimension require narrowing before the sum.
- Agent-context field documentation explains semantic paths, emitted contexts, portability and defaults. The guide distinguishes focused explanation from complete token-vocabulary context.
Added
VANITY_SYSTEM_INVALID_LAYERidentifies invalid system layer names, including empty entries inpolicies.layerOrder.
Changed
- Repository spike experiments share the workspace dependency catalog and lockfile, including Selenita 0.3 and Vitest 5. Version-specific Vite 6 host evidence keeps an explicit host pin.
- Compiler dependencies are refreshed, including OXC’s TypeScript parser fixes. Ordinary styling peer ranges are unchanged; editor-DX testing now uses Selenita 0.3.
compiler.layerOrderentries must be complete CSS identifiers with unique decoded identities. Equivalent escape spellings name one layer; omitted configured roots follow in configuration order.- Without
compiler.layerOrder, plain Vite adds no document-first root-order stylesheet. Vanity's rank among other layered CSS follows stylesheet order. - The linked layer-order stylesheet is content-hashed under
build.assetsDir. - Every Vanity stylesheet opens with the layer statements it needs to preserve system and host-root order.
VanityOpenAxisConfigandVanityAxisConfigrequirecontrolandderivewhen their type arguments guarantee those capabilities. Optional fields and derivations that may omit results guarantee no activation or branch.- The testing kit returns reusable Selenita configuration through
createVanityProjectConfig(), composed with Selenita’sdefineProject()orcreateProject(). Fixture helpers come directly from Selenita.
Upgrade notes
- If your application mixes Vanity with other layered CSS and relies on their relative rank, list those roots in
compiler.layerOrder, for example['app', 'theme', 'base', 'components', 'utilities']. - Rename rejected layer or axis names and remove repeated CSS identities. Empty
policies.layerOrderentries now reportVANITY_SYSTEM_INVALID_LAYERinstead ofVANITY_POLICY_INVALID. - Rename one axis if two names generate the same control attribute, such as
colorSchemeandcolor-scheme. - Regenerate portable artifacts for escaped axis names: their generated attributes and identities now reflect the decoded name.
- Regenerate artifacts for axis/mode keys previously lost to inherited JavaScript members: restoring their own metadata can change compatibility and runtime identities.
- Regenerate artifacts for direct configurations whose
deriveornativefield was ignored: preserving those fields can change CSS, compatibility, and runtime identities. Ordinary names and already-preserved configurations retain their identity bytes. - Supply
controlorderivewhen explicitly annotating known capabilities. Inference orsatisfiespreserves supplied fields and callback results. Author a mode value or narrow its derivation before treating an optional branch as guaranteed. - TypeScript still counts inherited members as structure. A sparse literal that omits a declared
constructorortoStringcan fail assignability even though it is valid at runtime. - Remove a Nuxt
app.headlayer statement added as a workaround and usecompiler.layerOrder. - For editor-DX tests, update to Selenita 0.3 and Vitest 5. Replace
defineVanityProject(options)withdefineProject(createVanityProjectConfig(options)), importingdefineProjectfrom@mszr/selenita/vitest. Standalone tools use corecreateProject()and own disposal. The testing-kit guide shows setup and lifecycle. - Import
cursor,mark,snippetand the project constructor from the same Selenita entrypoint; the testing kit no longer re-exports fixture helpers. - Replace old completion matchers with
toSuggest()/toSuggestOnly(), and groups with scoped snippets plusatEach(). A negatedtoSuggest()list rejects every listed name. Selenita supplies its own TypeScript 6 editor backend, independently of Vanity’s compiler tooling.
v0.7.0
Summary
Vanity now costs only what your application uses. It takes ownership of your system module when Vite loads it, instead of inspecting every import. On a large generated graph, the Vanity build is four times faster than in v0.6.1. Design packages installed from a registry now work exactly like workspace-linked ones, in development and SSR. Two new diagnostics make formerly silent or obscure failures actionable: a configuration that bypasses a source package, and application code that imports Vanity's authoring entry.
Fixed
- A source package installed from a registry now works in development and SSR like a workspace-linked package. Its style modules load their CSS, the browser receives the application's system projection instead of a pre-bundled compiler copy, and SSR development runs the projection rather than the build-time system.
- Vanity no longer adds work for every module in an application. On a generated graph with 3,000 application modules and 128 installed dependency modules, the local Vanity build fell from 1.921 s in v0.6.1 to 0.478 s, against 0.167 s for plain Vite on the same graph. These timings are local signals, not cross-machine comparisons.
- Application auto-imports no longer parse installed dependencies.
- Imports of configured system files preserve the host's
?raw,?url, and other non-module query behavior.
Added
VANITY_VITE_SOURCE_PACKAGE_BYPASSEDwarns when Vite configuration would pre-bundle or externalize a package that ships or leads to Vanity source. The warning names the package and fix, and appears in Vite's log when nocompiler.diagnosticssink is configured.VANITY_AUTHORING_IN_APP_MODULEnames the application module that imports@mszr/vanityand how to fix it. Previously the browser failed inside a Node-only package, and SSR ran build-time code.
Changed
- Browser tools show your system module under its own file instead of a generated virtual module.
- Vanity declares packages whose dependency graph reaches
@mszr/vanityin Vite's optimizer and SSR configuration. Application auto-imports still reach workspace-linked packages. - On Vite 5, one
vanityPlugin()instance cannot serve two hosts at the same time. Configuring a second live host raises an error that explains how to create one instance per host; sequential builds from one instance continue to work. - Non-error adapter diagnostics appear in the Vite log when no
compiler.diagnosticssink is configured.
No upgrade action is required unless application code imports @mszr/vanity. VANITY_AUTHORING_IN_APP_MODULE now names that module. Move its authoring into compiler.system or a *.css.ts style module.
v0.6.1
Fixed
- A style module that lives outside the application's build root now loads its own generated CSS, in development and in production, under any public base. This is the ordinary shape for a source-shipping component library — a package whose
*.css.tsfiles are compiled by the application that consumes them — and in v0.6.0 its stylesheets failed to resolve at every base. Hot updates to such modules now reach the browser as well, so editing a shipped component swaps its styles without a reload.
Changed
- Compiler-owned stylesheets now share one internal address scheme under the artifact directory inside the build root. This is internal: no consumer-visible identity, asset name, or API moves, and production output is unchanged. The development-server URL for a stylesheet now sits under that directory, so a stylesheet's request path in browser tools reads
/.vanity/virtual/style/…while its filename stays the one it had.
No upgrade action is required.
v0.6.0
Summary
System CSS is now a semantic artifact that Vanity owns by identity, instead of something inherited from whichever style module happened to evaluate first. That fixes missing token declarations across package boundaries and multi-page builds, and brings matching corrections to how identities are derived, how artifacts are published, and how the dev server recovers from a failed evaluation.
A design system can also live in one file now, and module roles are enforced at the file boundary with a diagnostic instead of a silent rewrite.
Upgrading
If you ship or consume a design system as build JavaScript plus a portable JSON artifact, rebuild it. Each of the four identities now derives from exactly the facts that projection owns, so an artifact generated before 0.6.0 disagrees with a 0.6.0 build and the strict reader rejects the pair, naming the package and the identities that differ. A normal build regenerates everything. There is no migration format and no code change to make, and the vanity.system/2 discriminator is unchanged.
Rebuild published design-system packages before consuming them from a 0.6.0 app. Compatibility identity changed, so a package built with 0.5.x and an application built with 0.6.0 will not be recognized as compatible.
Stored runtime snapshots keep working. A snapshot serialized before 0.6.0 carries the older runtime schema identity; reconciliation matches it by semantic address and reports a VANITY_RUNTIME_SCHEMA_MISMATCH note rather than failing. Snapshots produced and consumed within one deploy are unaffected.
Everyone else upgrades with no action. If your system module exports something that is neither the system nor a value taken from it, you will get a new build error naming the export and the one-line fix — see Added below.
Added
-
VANITY_APP_EXPORT_IN_SYSTEM_MODULEreports an application export that cannot travel with a system module. Application code receives a generated projection of that module, so an unrelated export has nowhere to go. The diagnostic names the file, the offending exports, your own system export name, and the fix — move the declaration into any other module and import it normally. It needs no Vanity configuration. Type-only exports are unaffected. -
VANITY_ARTIFACT_TARGET_INVALIDreplaces a bare failure when something other than a regular file occupies an artifact path, and names the path to clear.
Changed
-
A named rule's description is documentation only. Editing a description updates the manifest and leaves CSS bytes, CSS identity, compatibility identity, runtime identity, file mtimes, and the HMR channel untouched. Rule names and order remain structural; selectors, declarations, layers, and registrations stay in the projections that own them.
-
An omitted named-rule layer resolves to the system's global rule layer —
resetwhen declared, otherwise the first declared layer — and that resolved layer and order now drive sorting, emission, and identity alike. An omitted layer therefore behaves exactly like writing the effective default, and rules with equal layer and order keep registration order. Previously an omitted layer could be sorted as one thing and emitted as another. -
CSS identity preserves declaration order, inside named rules as well as between them, along with nested selectors, at-rules, and fallback arrays.
{ margin, marginTop }and{ marginTop, margin }produce different top margins, so they now receive different identities instead of sharing a stylesheet. -
Runtime schema identity now covers the conditions and layers an application projection exposes. Two systems whose generated modules expose different static application semantics can no longer share one runtime backing. This matches the documented runtime-schema contract.
-
Module roles hold at the file boundary. Vanity projects a system module for application graphs; it never rewrites your source to make one file serve two roles. Ordinary modules and pure re-export barrels pass through your bundler untouched, so objects keep reference identity, functions stay callable, and live bindings stay live.
Fixed
-
The documented one-file system now works in application code. A module can export its consolidated system alongside values taken from it, and serve as your
compiler.systementry:// src/design/ds.ts export const ds = createSystem() .addTokens({ color: { brand: '#635bff' } }) .consolidate({ prefix: 'app' }) export const { class: cls, rules, t } = ds
Style modules always handled this. Application modules did not: the generated projection matched exports by name, so a renamed destructure like
class: clswas silently dropped and importingclsfrom application code failed. Exports are now matched by identity against the evaluated system, so any name you choose resolves — including throughautoImports: { shared: '$system' }. Splitting the system and its authoring barrel across files remains fully supported. -
Token declarations no longer go missing when the system lives outside the app root. A system reached through a workspace package, an export map, a symlink, or an authoring barrel emitted its custom properties into whichever stylesheet the first style module happened to produce. A second page could then link rules referencing variables that were never declared. System CSS is now projected on its own, before any style can consume it, and every style keeps an edge to each system it reaches. This also resolves the case where two independent entry points — two HTML documents, or two extension pages — disagreed about which one carried the token layer.
-
A style module that throws no longer takes the system stylesheet with it. The failed evaluation used to consume the system's one-time emission and discard it, leaving every later style — and the repaired module itself — with no token declarations for the life of the dev server.
-
A failed evaluation no longer leaks its scope. File scope, captures, reference counters, and diagnostic sources are restored on both success and failure, so a repaired style, a plain system import, and the no-plugin misuse diagnostic all behave correctly afterwards without restarting the server.
-
Pointing
compiler.systemat a symlinked path no longer blames your code. Configuring it throughnode_moduleswhile imports resolved to the real file producedVANITY_SYSTEM_IN_STYLE_MODULEagainst a correct system module. Configured entries now resolve through your bundler, and the configured spelling, resolved module, authored source, CSS identity, virtual module, and browser URL are tracked as separate facts. -
Source-shipping packages keep their names and diagnostics. A
*.css.tsoutside the app root fell back to a filename-derived class name and lost source-local diagnostics. Authoring modules are now instrumented by role, whether linked from a workspace or installed undernode_modules. -
A barrel no longer overwrites the export namespace of the module it re-exports.
export { ds as theme } from './system'now servesthemefrom the barrel anddsfrom the leaf, sharing one runtime backing, in build, browser, and SSR graphs alike. -
HMR works under a non-root
base. Module-graph lookups, HMR payload paths, and browser request URLs are distinct protocols; the public base is applied only at the browser boundary. A token edit underbase: '/app/'now reaches the page instead of silently leaving the old computed value. -
Artifact publication is failure-atomic. A later write failure or a superseded generation no longer leaves files from a rejected candidate at accepted paths; already-replaced files are restored and temporary files are cleaned up. This is atomic within the running process, not against power loss.
-
Shared system CSS survives losing one owner. Two systems with the same CSS identity share a stylesheet; removing or replacing one no longer deletes the other's CSS, and a stylesheet is retired only once no owner and no in-flight transition still needs it. Updating equivalent owners together no longer reports a false namespace collision.
-
A CSS-identity change now reaches every page that uses it. A value edit that changes the identity switches each importer to the new virtual module and retires the old one, so an obsolete stylesheet can no longer win the cascade from a stale tag.
v0.5.1
Summary
The policies.color.adjustSpace default is now honored inside style modules, not only in token definitions. No API changes, no migration.
Fixed
policies.color.adjustSpacenow reaches every style emitter. A bare channel adjustment —lighten,darken,saturate,desaturate, orrotate, in both the free-function (ds.lighten(color, amount)) and method (color.lighten(amount)) forms — used directly as a value insideclass(),rules(),recipe(),anatomy(), oratoms()threwchannel adjustment needs a working polar color spaceeven when the owning system declaredpolicies.color.adjustSpace. The policy was resolved during token-graph resolution but not at the style-emitter cursor. It is now resolved at both, so a system's configured working space applies wherever the adjustment is authored — bringing the implementation into line with the contract already documented in system authoring §8 and typed CSS values §8.- Unaffected because they were already correct: explicit polar namespaces (
ds.oklch.lighten(...)),colorMix()/policies.color.mixSpace(bound into the constructor at system creation), and constructor policies such as the default length unit (bound into the constructor). A system that never setsadjustSpacestill receives the same cursor diagnostic naming both repairs (use a named namespace such as oklch.lighten(), or declare policies.color.adjustSpace).
- Unaffected because they were already correct: explicit polar namespaces (
v0.5.0
Summary
Emitted color CSS now looks like the CSS that was authored, and build-time derivation stays trustworthy wherever its inputs vary. Static color tokens keep their authored notation instead of canonicalizing to oklch(); build-folded derivations are re-resolved per axis mode and emit the intersection declarations their mode product requires; and the same substitution primitive is exposed to authors as ds.tdec.propagated(). The color surface is realigned onto exact CSS grammar — colorMix() is the CSS two-item form and requires an interpolation space, channel adjustments name their working polar space, and both read defaults from the new policies.color group. The release makes breaking changes at four user-facing seams: the color constructor set, emitted CSS and its token layer structure, the portable/manifest contract, and the audit taxonomy. The migration is mechanical — see Migration below.
Added
-
policies.color— a new top-level system policy group carryingmixSpace(the interpolation space supplied whencolorMix()omits.in()) andadjustSpace(the working polar space for the bare channel-adjust conveniences). Both are optional, both are validated at the cursor, and both travel in the portable contract so a restored system binds the same constructor grammar. NewVanityColorPoliciestype. -
Polar color-space adjustment namespaces. Every polar constructor now exposes channel sugar beside
.from():ds.oklch.lighten(color, 0.1) ds.hsl.saturate(color, 0.1) ds.lch.rotate(color, 30) ds.hwb.rotate(color, 30)
The mapping is space-owned:
lighten/darkenaddresslin OKLCH, LCH, and HSL;saturate/desaturateaddresscin OKLCH and LCH andsin HSL;rotateaddressesheverywhere. HWB has no lightness or chroma channel, so its namespace exposes onlyrotate— the others are absent from the type. NewVanityColorAdjustmentNamespace,VanityColorAdjustmentConstructor,VanityColorAdjustmentResult,VanityPendingColorAdjustmenttypes. -
ds.tdec.propagated(tree)— a graph-aware declaration producer. It declares the named tokens and every build-folded token whose value changes under those substitutions, re-resolved from a fresh result map. Live derivations are omitted because theirvar()expressions already recompute in the browser. Liketdec, it returns inert style data with no placement or emission effect; the locked system and its base declarations do not change. A substitution whose folded dependents resolve differently across any mode of an axis reportsVANITY_TOKENS_INVALID_OVERRIDErather than emitting an unconditional value that would win over the axis arm. NewVanityTokenDeclarationProducertype.const midnightCard = ds.class({ ...ds.tdec.propagated({ surface: { base: '#18181b' } }), padding: ds.t.space.md, })
-
Automatic intersection declarations for multi-axis folded derivations. For each axis mode and explicit case, the compiler re-resolves the graph with that branch's definitions and writes only the changed custom properties into the existing axis or case arm. When a build-folded token's dependencies vary across more than one axis, it receives an intersection declaration for every required mode combination, so the folded value stays correct under any composed state. Introspection, the portable artifact, and the manifest gain an optional
axisCoveragerecord (contributingAxes,requiredCases). -
Two new locked-system audit categories, bringing the system-scope set from five to seven:
staleDerivations—errorby default, because a missing intersection declaration leaves an emitted build-folded value visibly wrong under an active mode combination.derivedCaseGrowth— advisory, with a documented budget of 32 required cases; the escape is keeping the derivation live withreference: 'var'.
Both respect consolidation-time
off/warn/errorpolicy and call-time config like every other category. -
legibleOn()fallback is now recorded rather than silent. When the target is live, or is a valid static color whose mix shape Vanity declines to fold,legibleOn()uses a documented representative approximation instead of failing consolidation. The pick and its approximation appear inexplain(), in the manifest's newcontrast.fallbackfield, and in the existingcontrastaudit category. A non-color target remains an error. -
defaultModeis now exported from the package top level, closing a gap against the documented axis-authoring vocabulary (axis,defaultMode,condition,schemeIs). It marks the default mode inline and is equivalent to the axisdefaultkey;defaultMode(trigger)marks the default mode while still giving it a condition. -
schemeIs(scheme, axisName?)accepts the mounted scheme axis name, so a standalone condition stays aligned with a non-conventional mount. The default remains the conventionalschememount. -
Color methods are now typed on color token handles.
VanityColorTokenHandleand inferred color handles carryalpha,lighten,darken,saturate,desaturate,rotate, andmixthrough the new sharedVanityColorMethodsinterface, sods.t.color.brand.lighten(0.06)types the same way as the authored-value form. -
VANITY_COINED_CONSTRUCTOR_NAMES— exported from@mszr/vanity/capabilities. Every builtin constructor is now machine-checked to appear in exactly one ownership map:VANITY_CSS_NAMED_API_ROWSfor CSS-owned spellings, this map for Vanity-coined spellings with their one-line rationale. -
Standard DTCG color export follows the emitted CSS notation. Resolved colors use the color space named by their CSS —
srgbfor parsed names and hex leaves, the authored space where a fold preserves it,oklchfor canonical computed values — with full component precision, instead of normalizing everything to OKLCH. A live CSS expression such as a non-oklabcolor-mix()is not a standard DTCG value; strict export (the default) reportsVANITY_DTCG_UNSUPPORTED, and{ strict: false }remains the explicit lossy snapshot.
Changed
-
Breaking: emitted CSS preserves authored color notation. Static color expressions no longer canonicalize to
oklch().color('blue')emitsblue;hsl(200, 50, 50)emitshsl(200 50% 50%);color('display-p3', …)stays Display-P3, including through a static alpha replacement. Only computed values with no preserved notation canonicalize tooklch(). Committed CSS snapshots, golden tests, and manifests will differ. -
Breaking:
colorMix()is the CSS-exact two-color constructor. It takes exactly two items, percentages are CSS's0..100units on each item, and CSS's required interpolation method must be supplied by.in(space)/.in(space, { hue })or bypolicies.color.mixSpace. An explicit.in()always wins over the policy; a missing space is a cursor diagnostic. The free constructor and the color-value/token-handle.mix()method now build the same expression node, so the two spellings cannot drift.colorMix(['red', ['blue', 35]]).in('oklab') color('red').mix('blue', 35).in('oklab')
-
Breaking: build folding of
color-mix()is deliberately conservative. An oklab mix with no hue path folds only when CSS determines both weights without an alpha multiplier — both percentages omitted, one given, or both given summing to100. Other spaces, hue paths, live percentage inputs, and both-explicit percentages that require CSS normalization stay nativecolor-mix()so the browser remains the semantic authority. Two percentages summing to0are an authoring diagnostic. -
Breaking: bare channel adjustments require a working polar space.
lighten,darken,saturate,desaturate, androtateno longer assume OKLCH. Withoutpolicies.color.adjustSpace, the bare constructors returnVanityPendingColorAdjustmentand fail in types, and the color-value/handle method forms fail at build with a diagnostic naming both repairs. An explicit namespace (oklch.lighten(…)) always wins. SettingadjustSpace: 'hwb'removeslighten/darken/saturate/desaturatefrom the projected constructor surface entirely, since HWB has no channel for them to address.alphais exempt: it is a color-wide operation that never needsadjustSpace. -
Breaking: channel-adjustment folding is gamut-aware. A static origin folds when the operation's space can represent it; cross-space adjustments into unbounded
oklchandlchalways fold; adjustments into sRGB-boundedhslandhwbfold only when the origin resolves inside sRGB. An out-of-gamut origin stays native relative-color CSS so its gamut and the browser's resolution remain intact; such a token then requires therelative-colorfeature in its support target, and a target without it fails at consolidation rather than silently narrowing the gamut. An adjustment authored in the origin's own space additionally keeps that origin's notation. Folds apply deltas in CSS relative-color channel units (including HSL's0..100saturation/lightness) and pass the canonical four-decimal formatter. -
Breaking: the
<prefix>.tokens.overridesphase layer is no longer declared or emitted. It was the last structural remnant of the build-time token overrides removed in v0.4.0. The token phase layers are nowtokens.base,tokens.axes.<axis>, andtokens.cases;overridesremains an ordinary top-level cascade layer. -
Breaking: the built-in color-scheme axis derives its attribute and selectors from its mount name. A conventional
schememount still usesdata-scheme; the samecolorSchemes()definition mounted asappearancenow usesdata-appearanceacross its explicit selectors, its runtime attribute control, its built-indark/lightconditions, and itscolor-schemeroot synchronization. `native.kind: ...
v0.4.0
Summary
An architectural cleanup aligns Vanity’s internal structure with its existing identity as a design-system engine and TypeScript harness for CSS. The nested legacy engine APIs are replaced by role-named layers — values, tokens, systems, CSS, compiler, and runtime — without changing that product model. The release makes breaking changes at five user-facing seams: integration/auto-import configuration, token-policy configuration, build-time token overrides, outward artifact formats (portable system, introspection map, manifest), and TypeScript type names. It also adds a WXT host adapter. The migration is mechanical — see Migration below.
Added
@mszr/vanity/wxt— a WXT host adapter (default-exported module) that registers Vanity's compiler and auto-import module roles in a WXT build from the samevanity.config.ts, giving browser-extension projects the day-one setup Vite and Nuxt already had. Adds an optionalwxtpeer dependency.autoImports.sharedrole and the$systemsource shorthand — one source can be injected into both the style (*.css.ts) and application/SSR module roles at once, and$systemis the explicit shorthand for the configuredcompiler.systementry.- Locked-system self-audit.
ds.audit(config?)now reports the five system-scope categories decidable from the semantic map alone (ambiguousAxes,mutableRootHazards,overwriteInventory,nonportableValues,specificityContexts) and, rather than signalling false success, names the twelve evidence-dependent categories it cannot decide without build evidence. Returns aVanityAuditReport(findings+unevaluated); newVanityAuditFinding,VanityAuditReport,VanityUnevaluatedAudittypes. VanityRuntimeError— exported from both@mszr/vanityand@mszr/vanity/runtime, thrown on synchronous runtime misuse and runtime-controller failures, carrying a stablecodeand adiagnostic(semantic location, detail, repair guidance).scaleandcheckare now exported from the package top level for authoring scales and contrast checks.- Offline manifest validation — associate
.vanity/manifest.jsonwith the shipped@mszr/vanity/manifest.schema.json; the schema is now closed to the recognized Manifest v4 records and backed by a strict manifest reader.
Changed
- Breaking: unified auto-import configuration.
compiler.styleAutoImportsandapp.runtimeAutoImportsare replaced by a single top-levelautoImportsthat routes sources intostyle,app, andsharedmodule roles.compiler.systemandcompiler.layerOrderstay undercompiler. Onevanity.config.tsstill travels unchanged acrossdefineVanityConfig,vanityPlugin, the Nuxtvanitykey, andvanity prepare. - Breaking: explicit token policy configuration. The public policy shape uses
tokens.referenceandtokens.emit(represented byVanityTokenPolicies) as token-wide defaults for shorthand tokens and omittedtdeftraits. Explicit per-token traits win; conditional tokens retain their requiredvarreference and CSS emission. - Breaking: outward artifact formats bumped and made strict. Old formats are now rejected rather than translated:
- Portable compiler artifact
vanity.system/1→vanity.system/2, withportable.enginerenamed toportable.capabilities. - Introspection map
vanity.introspection/1→vanity.introspection/2;ds.introspect()'s top-levelversionis now2, and its fields drop engine vocabulary ("engine extensions" → "value extensions", "engine policies" → "system policies"). - Manifest
vanity.manifest/3→vanity.manifest/4($schema→manifest-4.schema.json).
- Portable compiler artifact
- Breaking: the runtime "facade" is now the runtime "controller" throughout the API and docs:
VanityBoundRuntime→VanityRuntimeController,VanityRuntimeFactory→VanityRuntimeControllerFactory,VanityRuntimeSnapshotV1→VanityRuntimeSnapshot. - Breaking: portable/introspection type renames drop
V1suffixes and engine vocabulary:VanityPortableSystemV1→VanityPortableSystem(now split into granularVanityPortableCapabilities,VanityPortableConstructor,VanityPortableExtension,VanityPortablePolicies, plusVanityCapabilityOrigin),VanitySystemMapV1→VanitySystemMap,VanitySystemEnvironment→VanitySystemShape. - Breaking: token and rule type renames.
VanityTokenStage→VanityTokenDerivationTree,VanityContrast→VanityAuthoredContrast,VanityRuleGroup→VanitySystemRule(same shapes, new names). - Fixed: complete locked-system namespace reservations.
axes,consts,policies, andintrospectare now included in extension collision checks, alongside existing locked-system members andObject.prototypenames. Conflicts fail at registration with structured diagnostics. These accessors already existed on locked systems; v0.4.0 closes the reservation and enforcement gap.
Removed
- Breaking: the boolean
styleAutoImportsforms (true/false) and theappconfig group /compiler.styleAutoImportskey — superseded byautoImports(see Migration). - Breaking: redundant color/token type aliases with no drop-in replacement —
VanityColor,VanityColorMode,VanityColorToken,VanityContrastToken,VanityInterpolatedColor,VanityModeOf,VanityTokenMode, andVanityValueToken. - Breaking: build-time token overrides removed.
ds.tokenOverride()andVanityTokenOverridesare removed. Authored CSS override declarations can usetdec()withclass()where appropriate; runtime changes use mutable-token$setoperations and snapshots. There is no drop-in replacement for the old automatic re-folding behavior. - Internally, the nested engine object and its parallel token/styling dialects were dissolved into role-named layers — no public value export changed as a result of the refactor itself.
Dependencies
- Added optional
wxtpeer dependency, used only by@mszr/vanity/wxt.
Migration
-
Restructure the config (all hosts —
defineVanityConfig,vanityPlugin, Nuxtvanity,vanity prepare):// before vanityPlugin({ compiler: { system: './design/system.ts', styleAutoImports: './design/authoring.ts' }, app: { runtimeAutoImports: ['core', 'vue'] }, }) // after vanityPlugin({ compiler: { system: './design/system.ts' }, autoImports: { shared: './design/authoring.ts', // inject into style *and* app modules app: ['core', 'vue'], }, })
styleAutoImports: true→autoImports: { style: '$system' }(orshared: '$system'to reach app modules too).styleAutoImports: falseor omitted → omit the key.styleAutoImports: './mod'→autoImports: { style: './mod' }; useshared: './mod'to inject the same source into application modules as well.app.runtimeAutoImports: [...]→autoImports: { app: [...] }.compiler.layerOrderis unchanged — it stays undercompiler.
-
Move token policy defaults into
tokens. A top-levelreferencepolicy becomestokens: { reference: 'var' }. Settokens.emitto choose the default CSS-emission behavior. Explicit per-tokentdeftraits still win, and conditional token definitions retain their requiredvarreference and CSS emission. -
Regenerate outward artifacts. Any committed or cached
.vanity/manifest.json, portable-system JSON, or introspection JSON must be rebuilt — v0.4.0 rejects the previousvanity.manifest/3,vanity.system/1, andvanity.introspection/1formats instead of translating them. A normal build (orvanity preparefor declarations) regenerates them. Update any external tooling that readsformat/version/$schema, or the portableenginefield (nowcapabilities). -
Update type imports per the renames: drop
V1suffixes (VanityPortableSystemV1→VanityPortableSystem,VanitySystemMapV1→VanitySystemMap,VanityRuntimeSnapshotV1→VanityRuntimeSnapshot);VanityBoundRuntime→VanityRuntimeController;VanityRuntimeFactory→VanityRuntimeControllerFactory;VanitySystemEnvironment→VanitySystemShape;VanityTokenStage→VanityTokenDerivationTree;VanityRuleGroup→VanitySystemRule. The removed color aliases (VanityColor,VanityColorToken,VanityTokenMode, …) have no direct replacement — derive fromVanityTokenHandle/ the token types, or inline the shape.
v0.3.0
Summary
Prepare-time auto-import declarations. Non-Nuxt hosts — plain Vite, Vue-on-Vite, WXT — can now generate Vanity's ambient types before typechecking, so a clean checkout runs prepare → typecheck → build with the same day-one experience Nuxt already had. Adds a shared vanity.config.ts, a vanity prepare command, and documented IntelliSense across every config surface. Additive — no migration required.
Added
vanity prepare— statically plans and writes the enabled auto-import lanes' TypeScript declarations fromvanity.config.ts(or--config), without compiling styles or evaluating the system, so any host can run it beforetsc.--rootand--configselect the project root and config module.@mszr/vanity/prepareentrypoint —planAutoImportDeclarations()returns a side-effect-free declaration plan for hosts with their own type-generation hooks (e.g. WXT'sprepare:types);writeAutoImportDeclarations()reconciles the canonical files;loadVanityConfig()loads avanity.config.ts.@mszr/vanity/configentrypoint withdefineVanityConfig()— a host-neutralvanity.config.tspassed unchanged tovanityPlugin, the Nuxtvanitykey, andvanity prepare. One options object, no duplicatedcompiler/appconfiguration.- Documented IntelliSense at the cursor for every shared config key —
compiler,app,system,styleAutoImports,runtimeAutoImports, and their nested options — indefineVanityConfig,vanityPlugin, and the Nuxtvanityconfig key. - A single cross-lane collision diagnostic with a fix hint when the style and runtime lanes expose the same name, enforced consistently across Vite, Nuxt, and
vanity prepare.
Changed
- Vanity now owns runtime-lane declaration rendering (previously delegated to
unplugin-auto-import'sdts), so build- and prepare-generated declarations are byte-identical from one plan.unplugin-auto-importremains the runtime source transformer only. - Canonical declarations now live at
.vanity/types/{style,runtime}-auto-imports.d.ts, with the@types/vanity-{style,runtime}-auto-importspackages as small discovery bridges. Generated text uses relative local references and bare package specifiers, so it is portable and cacheable across checkouts. The@typespackage names are unchanged, so existingcompilerOptions.typespins keep working. app.runtimeAutoImportssources are modules or barrels, enumerated statically — no longer scanned as directory globs (v0.2.0 already documented them as "curated barrels").
Dependencies
- Added
jitito load an optional TypeScriptvanity.config.tsfrom the CLI.
Migration
Additive — existing vanityPlugin({ compiler, app }) and Nuxt vanity: { … } configurations work unchanged; vanity.config.ts and vanity prepare are opt-in.
- Non-Nuxt clean checkouts: add a
vanity preparestep before your typecheck — e.g."prepare": "vanity prepare", then runprepare → tsc → build. Nuxt projects need no change;nuxt preparealready registers both lanes. - Directory sources (rare): if an
app.runtimeAutoImportssource pointed at a directory expecting a scan, point it at that directory's barrel /indexmodule instead. - Stale declarations: the previous
node_modules/.vanity/style-auto-imports.d.tslocation is obsolete and regenerates under.vanity/types/automatically — no action unless you referenced the old path directly.
v0.2.0
Sumary
Two-lane integration API for Vite and Nuxt, a framework-agnostic runtime auto-import surface, and provenance that follows authoring aliases. Breaking, but small to migrate — see Migration below.
Changed
- Breaking: The Vite and Nuxt integrations now take two explicit lanes,
compilerandapp, instead of a flat options object.system,styleAutoImports, andlayerOrderlive undercompiler; runtime imports live underapp.runtimeAutoImports. - Breaking: Renamed the plugin's
cascadeoption tocompiler.layerOrder, matching the system'slayerOrderpolicy andconsolidate({ layerOrder }). - Breaking: Renamed the
layerskey tolayerOrderincreateSystem,consolidate, andaddPolicies/overwritePolicies. The resolved manifest field remainslayers. - Breaking:
compiler.styleAutoImportsis nowfalse | true | string | { from?, include | exclude }.truereusescompiler.system; a string names an unfiltered alternate source; the object form narrows a source withincludeorexclude(never both). Replaces the previousnamesfilter vocabulary. - Breaking: Renamed the generated style-lane declaration package from
@types/vanity-auto-importsto@types/vanity-style-auto-imports, so the two auto-import lanes have symmetric, self-documenting names alongside@types/vanity-runtime-auto-imports. - Both auto-import lanes now share one
include/excludefilter vocabulary, with a single diagnostic when both are supplied.
Added
app.runtimeAutoImportsfor application- and template-facing runtime imports: a string names one source, an array selects the built-incoreandvuepresets, and the object form combinespresetswith package or localsources. Sources expose all named value exports by default and acceptinclude/excludefilters.- New
@mszr/vanity/importsentrypoint exposing the dependency-freecore/vueruntime presets (vanityCoreAutoImports,vanityVueAutoImports,vanityRuntimeAutoImportPresets) for custom host adapters. - Authoring-alias provenance: a local alias of
ds.class(or another authoring member) declared in acompiler.styleAutoImportsbarrel — e.g.export const { class: cls } = ds— now keeps debug names and source-local diagnostics even when injected ambiently, with no in-file binding. Aliases that cannot be resolved syntactically fall back to file-level provenance.
Fixed
- The Vite adapter resolves relative
app.runtimeAutoImportssources against the resolved Vite root instead ofprocess.cwd(), so filtered and unfiltered local sources resolve identically in monorepos. - The
core/vueruntime presets no longer pull@nuxt/kitinto non-Nuxt hosts, fixingERR_MODULE_NOT_FOUNDunder strict package isolation (pnpm) for wxt, plain-Vite, and Vue-on-Vite consumers.
Removed
- Breaking: Removed
vanityNuxtImportsand theimports: { presets }ceremony from the Nuxt module. Useapp.runtimeAutoImports: ['core', 'vue']; Nuxt registers them through its native import registry.
Dependencies
@mszr/vanity/vitenow depends onunplugin-auto-importfor the application auto-import lane.
Migration
-
Move options under lanes. Put
system,styleAutoImports, and (renamed)layerOrderundercompiler; put runtime imports underapp:// before vanityPlugin({ system: './design/system.ts', cascade: ['vendor', 'app'] }) // after vanityPlugin({ compiler: { system: './design/system.ts', layerOrder: ['vendor', 'app'] }, app: { runtimeAutoImports: ['core', 'vue'] }, // opt-in })
-
layers→layerOrderincreateSystem,consolidate, and policy groups. Drop the key to accept the default order. -
styleAutoImports:truereusescompiler.system; a string names an alternate source; use{ from?, include }or{ from?, exclude }to filter (never both) — the oldnamesoption is gone. -
Nuxt: delete any
vanityNuxtImportsimport andimports: { presets }block; replace withapp: { runtimeAutoImports: ['core', 'vue'] }. -
Explicit
compilerOptions.types: an explicittypeslist disables TypeScript's automatic@typesdiscovery, so add the packages for the lanes you use —vanity-style-auto-imports(style lane) and/orvanity-runtime-auto-imports(runtime lane). Projects that don't pintypesneed no change. Nuxt projects rely on Nuxt's generated type references. -
Stale declarations: the style-lane rename means any pre-existing
@types/vanity-auto-imports/.vanity/auto-imports.d.tsis obsolete. These regenerate automatically; you only need to act if you pinned the old name intypes. -
Authoring shorthand (optional):
ds.classremains canonical. For a bare shorthand in*.css.ts, expose astyleAutoImportsbarrel and alias the reserved member —export const { class: cls, t } = ds— then writecls(…); provenance is preserved (see Added).