Skip to content

Releases

v0.8.1

Choose a tag to compare

@mareszhar mareszhar released this 07 Oct 17:46

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.1

If upgrading Selenita from 0.3:

  • Replace its CodeFix type imports with CodeAction.
  • 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

Choose a tag to compare

@mareszhar mareszhar released this 06 Oct 06:09

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.layerOrder takes 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 constructor and __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 derive callbacks and native policy, 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 undefined remain 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 remain undefined, 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_LAYER identifies invalid system layer names, including empty entries in policies.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.layerOrder entries 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.
  • VanityOpenAxisConfig and VanityAxisConfig require control and derive when 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’s defineProject() or createProject(). 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.layerOrder entries now report VANITY_SYSTEM_INVALID_LAYER instead of VANITY_POLICY_INVALID.
  • Rename one axis if two names generate the same control attribute, such as colorScheme and color-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 derive or native field was ignored: preserving those fields can change CSS, compatibility, and runtime identities. Ordinary names and already-preserved configurations retain their identity bytes.
  • Supply control or derive when explicitly annotating known capabilities. Inference or satisfies preserves 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 constructor or toString can fail assignability even though it is valid at runtime.
  • Remove a Nuxt app.head layer statement added as a workaround and use compiler.layerOrder.
  • For editor-DX tests, update to Selenita 0.3 and Vitest 5. Replace defineVanityProject(options) with defineProject(createVanityProjectConfig(options)), importing defineProject from @mszr/selenita/vitest. Standalone tools use core createProject() and own disposal. The testing-kit guide shows setup and lifecycle.
  • Import cursor, mark, snippet and 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 plus atEach(). A negated toSuggest() list rejects every listed name. Selenita supplies its own TypeScript 6 editor backend, independently of Vanity’s compiler tooling.

v0.7.0

Choose a tag to compare

@mareszhar mareszhar released this 26 Sep 03:29

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_BYPASSED warns 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 no compiler.diagnostics sink is configured.
  • VANITY_AUTHORING_IN_APP_MODULE names the application module that imports @mszr/vanity and 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/vanity in 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.diagnostics sink 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

Choose a tag to compare

@mareszhar mareszhar released this 21 Sep 09:45

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.ts files 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

Choose a tag to compare

@mareszhar mareszhar released this 19 Sep 04:18

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_MODULE reports 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_INVALID replaces 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 — reset when 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.system entry:

    // 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: cls was silently dropped and importing cls from application code failed. Exports are now matched by identity against the evaluated system, so any name you choose resolves — including through autoImports: { 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.system at a symlinked path no longer blames your code. Configuring it through node_modules while imports resolved to the real file produced VANITY_SYSTEM_IN_STYLE_MODULE against 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.ts outside 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 under node_modules.

  • A barrel no longer overwrites the export namespace of the module it re-exports. export { ds as theme } from './system' now serves theme from the barrel and ds from 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 under base: '/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

Choose a tag to compare

@mareszhar mareszhar released this 11 Sep 23:57

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.adjustSpace now reaches every style emitter. A bare channel adjustment — lighten, darken, saturate, desaturate, or rotate, in both the free-function (ds.lighten(color, amount)) and method (color.lighten(amount)) forms — used directly as a value inside class(), rules(), recipe(), anatomy(), or atoms() threw channel adjustment needs a working polar color space even when the owning system declared policies.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 sets adjustSpace still receives the same cursor diagnostic naming both repairs (use a named namespace such as oklch.lighten(), or declare policies.color.adjustSpace).

v0.5.0

Choose a tag to compare

@mareszhar mareszhar released this 11 Sep 10:03

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 carrying mixSpace (the interpolation space supplied when colorMix() omits .in()) and adjustSpace (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. New VanityColorPolicies type.

  • 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/darken address l in OKLCH, LCH, and HSL; saturate/desaturate address c in OKLCH and LCH and s in HSL; rotate addresses h everywhere. HWB has no lightness or chroma channel, so its namespace exposes only rotate — the others are absent from the type. New VanityColorAdjustmentNamespace, VanityColorAdjustmentConstructor, VanityColorAdjustmentResult, VanityPendingColorAdjustment types.

  • 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 their var() expressions already recompute in the browser. Like tdec, 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 reports VANITY_TOKENS_INVALID_OVERRIDE rather than emitting an unconditional value that would win over the axis arm. New VanityTokenDeclarationProducer type.

    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 axisCoverage record (contributingAxes, requiredCases).

  • Two new locked-system audit categories, bringing the system-scope set from five to seven:

    • staleDerivations — error by 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 with reference: 'var'.

    Both respect consolidation-time off/warn/error policy 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 in explain(), in the manifest's new contrast.fallback field, and in the existing contrast audit category. A non-color target remains an error.

  • defaultMode is 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 axis default key; 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 conventional scheme mount.

  • Color methods are now typed on color token handles. VanityColorTokenHandle and inferred color handles carry alpha, lighten, darken, saturate, desaturate, rotate, and mix through the new shared VanityColorMethods interface, so ds.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_ROWS for 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 — srgb for parsed names and hex leaves, the authored space where a fold preserves it, oklch for canonical computed values — with full component precision, instead of normalizing everything to OKLCH. A live CSS expression such as a non-oklab color-mix() is not a standard DTCG value; strict export (the default) reports VANITY_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') emits blue; hsl(200, 50, 50) emits hsl(200 50% 50%); color('display-p3', …) stays Display-P3, including through a static alpha replacement. Only computed values with no preserved notation canonicalize to oklch(). 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's 0..100 units on each item, and CSS's required interpolation method must be supplied by .in(space) / .in(space, { hue }) or by policies.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 to 100. Other spaces, hue paths, live percentage inputs, and both-explicit percentages that require CSS normalization stay native color-mix() so the browser remains the semantic authority. Two percentages summing to 0 are an authoring diagnostic.

  • Breaking: bare channel adjustments require a working polar space. lighten, darken, saturate, desaturate, and rotate no longer assume OKLCH. Without policies.color.adjustSpace, the bare constructors return VanityPendingColorAdjustment and 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. Setting adjustSpace: 'hwb' removes lighten/darken/saturate/desaturate from the projected constructor surface entirely, since HWB has no channel for them to address. alpha is exempt: it is a color-wide operation that never needs adjustSpace.

  • Breaking: channel-adjustment folding is gamut-aware. A static origin folds when the operation's space can represent it; cross-space adjustments into unbounded oklch and lch always fold; adjustments into sRGB-bounded hsl and hwb fold 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 the relative-color feature 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's 0..100 saturation/lightness) and pass the canonical four-decimal formatter.

  • Breaking: the <prefix>.tokens.overrides phase 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 now tokens.base, tokens.axes.<axis>, and tokens.cases; overrides remains an ordinary top-level cascade layer.

  • Breaking: the built-in color-scheme axis derives its attribute and selectors from its mount name. A conventional scheme mount still uses data-scheme; the same colorSchemes() definition mounted as appearance now uses data-appearance across its explicit selectors, its runtime attribute control, its built-in dark/light conditions, and its color-scheme root synchronization. `native.kind: ...

Read more

v0.4.0

Choose a tag to compare

@mareszhar mareszhar released this 05 Sep 00:50

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 same vanity.config.ts, giving browser-extension projects the day-one setup Vite and Nuxt already had. Adds an optional wxt peer dependency.
  • autoImports.shared role and the $system source shorthand — one source can be injected into both the style (*.css.ts) and application/SSR module roles at once, and $system is the explicit shorthand for the configured compiler.system entry.
  • 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 a VanityAuditReport (findings + unevaluated); new VanityAuditFinding, VanityAuditReport, VanityUnevaluatedAudit types.
  • VanityRuntimeError — exported from both @mszr/vanity and @mszr/vanity/runtime, thrown on synchronous runtime misuse and runtime-controller failures, carrying a stable code and a diagnostic (semantic location, detail, repair guidance).
  • scale and check are now exported from the package top level for authoring scales and contrast checks.
  • Offline manifest validation — associate .vanity/manifest.json with 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.styleAutoImports and app.runtimeAutoImports are replaced by a single top-level autoImports that routes sources into style, app, and shared module roles. compiler.system and compiler.layerOrder stay under compiler. One vanity.config.ts still travels unchanged across defineVanityConfig, vanityPlugin, the Nuxt vanity key, and vanity prepare.
  • Breaking: explicit token policy configuration. The public policy shape uses tokens.reference and tokens.emit (represented by VanityTokenPolicies) as token-wide defaults for shorthand tokens and omitted tdef traits. Explicit per-token traits win; conditional tokens retain their required var reference 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, with portable.engine renamed to portable.capabilities.
    • Introspection map vanity.introspection/1 → vanity.introspection/2; ds.introspect()'s top-level version is now 2, 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).
  • 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 V1 suffixes and engine vocabulary: VanityPortableSystemV1 → VanityPortableSystem (now split into granular VanityPortableCapabilities, VanityPortableConstructor, VanityPortableExtension, VanityPortablePolicies, plus VanityCapabilityOrigin), 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, and introspect are now included in extension collision checks, alongside existing locked-system members and Object.prototype names. 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 styleAutoImports forms (true / false) and the app config group / compiler.styleAutoImports key — superseded by autoImports (see Migration).
  • Breaking: redundant color/token type aliases with no drop-in replacement — VanityColor, VanityColorMode, VanityColorToken, VanityContrastToken, VanityInterpolatedColor, VanityModeOf, VanityTokenMode, and VanityValueToken.
  • Breaking: build-time token overrides removed. ds.tokenOverride() and VanityTokenOverrides are removed. Authored CSS override declarations can use tdec() with class() where appropriate; runtime changes use mutable-token $set operations 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 wxt peer dependency, used only by @mszr/vanity/wxt.

Migration

  • Restructure the config (all hosts — defineVanityConfig, vanityPlugin, Nuxt vanity, 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' } (or shared: '$system' to reach app modules too).
    • styleAutoImports: false or omitted → omit the key.
    • styleAutoImports: './mod' → autoImports: { style: './mod' }; use shared: './mod' to inject the same source into application modules as well.
    • app.runtimeAutoImports: [...] → autoImports: { app: [...] }.
    • compiler.layerOrder is unchanged — it stays under compiler.
  • Move token policy defaults into tokens. A top-level reference policy becomes tokens: { reference: 'var' }. Set tokens.emit to choose the default CSS-emission behavior. Explicit per-token tdef traits still win, and conditional token definitions retain their required var reference 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 previous vanity.manifest/3, vanity.system/1, and vanity.introspection/1 formats instead of translating them. A normal build (or vanity prepare for declarations) regenerates them. Update any external tooling that reads format / version / $schema, or the portable engine field (now capabilities).

  • Update type imports per the renames: drop V1 suffixes (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 from VanityTokenHandle / the token types, or inline the shape.

v0.3.0

Choose a tag to compare

@mareszhar mareszhar released this 29 Aug 08:05

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 from vanity.config.ts (or --config), without compiling styles or evaluating the system, so any host can run it before tsc. --root and --config select the project root and config module.
  • @mszr/vanity/prepare entrypoint — planAutoImportDeclarations() returns a side-effect-free declaration plan for hosts with their own type-generation hooks (e.g. WXT's prepare:types); writeAutoImportDeclarations() reconciles the canonical files; loadVanityConfig() loads a vanity.config.ts.
  • @mszr/vanity/config entrypoint with defineVanityConfig() — a host-neutral vanity.config.ts passed unchanged to vanityPlugin, the Nuxt vanity key, and vanity prepare. One options object, no duplicated compiler/app configuration.
  • Documented IntelliSense at the cursor for every shared config key — compiler, app, system, styleAutoImports, runtimeAutoImports, and their nested options — in defineVanityConfig, vanityPlugin, and the Nuxt vanity config 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's dts), so build- and prepare-generated declarations are byte-identical from one plan. unplugin-auto-import remains 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-imports packages as small discovery bridges. Generated text uses relative local references and bare package specifiers, so it is portable and cacheable across checkouts. The @types package names are unchanged, so existing compilerOptions.types pins keep working.
  • app.runtimeAutoImports sources are modules or barrels, enumerated statically — no longer scanned as directory globs (v0.2.0 already documented them as "curated barrels").

Dependencies

  • Added jiti to load an optional TypeScript vanity.config.ts from 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 prepare step before your typecheck — e.g. "prepare": "vanity prepare", then run prepare → tsc → build. Nuxt projects need no change; nuxt prepare already registers both lanes.
  • Directory sources (rare): if an app.runtimeAutoImports source pointed at a directory expecting a scan, point it at that directory's barrel / index module instead.
  • Stale declarations: the previous node_modules/.vanity/style-auto-imports.d.ts location is obsolete and regenerates under .vanity/types/ automatically — no action unless you referenced the old path directly.

v0.2.0

Choose a tag to compare

@mareszhar mareszhar released this 28 Aug 09:40

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, compiler and app, instead of a flat options object. system, styleAutoImports, and layerOrder live under compiler; runtime imports live under app.runtimeAutoImports.
  • Breaking: Renamed the plugin's cascade option to compiler.layerOrder, matching the system's layerOrder policy and consolidate({ layerOrder }).
  • Breaking: Renamed the layers key to layerOrder in createSystem, consolidate, and addPolicies / overwritePolicies. The resolved manifest field remains layers.
  • Breaking: compiler.styleAutoImports is now false | true | string | { from?, include | exclude }. true reuses compiler.system; a string names an unfiltered alternate source; the object form narrows a source with include or exclude (never both). Replaces the previous names filter vocabulary.
  • Breaking: Renamed the generated style-lane declaration package from @types/vanity-auto-imports to @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 / exclude filter vocabulary, with a single diagnostic when both are supplied.

Added

  • app.runtimeAutoImports for application- and template-facing runtime imports: a string names one source, an array selects the built-in core and vue presets, and the object form combines presets with package or local sources. Sources expose all named value exports by default and accept include / exclude filters.
  • New @mszr/vanity/imports entrypoint exposing the dependency-free core / vue runtime presets (vanityCoreAutoImports, vanityVueAutoImports, vanityRuntimeAutoImportPresets) for custom host adapters.
  • Authoring-alias provenance: a local alias of ds.class (or another authoring member) declared in a compiler.styleAutoImports barrel — 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.runtimeAutoImports sources against the resolved Vite root instead of process.cwd(), so filtered and unfiltered local sources resolve identically in monorepos.
  • The core / vue runtime presets no longer pull @nuxt/kit into non-Nuxt hosts, fixing ERR_MODULE_NOT_FOUND under strict package isolation (pnpm) for wxt, plain-Vite, and Vue-on-Vite consumers.

Removed

  • Breaking: Removed vanityNuxtImports and the imports: { presets } ceremony from the Nuxt module. Use app.runtimeAutoImports: ['core', 'vue']; Nuxt registers them through its native import registry.

Dependencies

  • @mszr/vanity/vite now depends on unplugin-auto-import for the application auto-import lane.

Migration

  • Move options under lanes. Put system, styleAutoImports, and (renamed) layerOrder under compiler; put runtime imports under app:

    // 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 → layerOrder in createSystem, consolidate, and policy groups. Drop the key to accept the default order.

  • styleAutoImports: true reuses compiler.system; a string names an alternate source; use { from?, include } or { from?, exclude } to filter (never both) — the old names option is gone.

  • Nuxt: delete any vanityNuxtImports import and imports: { presets } block; replace with app: { runtimeAutoImports: ['core', 'vue'] }.

  • Explicit compilerOptions.types: an explicit types list disables TypeScript's automatic @types discovery, so add the packages for the lanes you use — vanity-style-auto-imports (style lane) and/or vanity-runtime-auto-imports (runtime lane). Projects that don't pin types need 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.ts is obsolete. These regenerate automatically; you only need to act if you pinned the old name in types.

  • Authoring shorthand (optional): ds.class remains canonical. For a bare shorthand in *.css.ts, expose a styleAutoImports barrel and alias the reserved member — export const { class: cls, t } = ds — then write cls(…); provenance is preserved (see Added).