WORK-187
ID:WORK-187Status:done

Config-driven token stylesheet generation

Make refrakt.config.jsontheme.tokens the canonical authoring surface for token overrides. The build pipeline validates the config against the typed contract, generates a :root { --rf-* } stylesheet, and injects it after the theme's base CSS but before any user CSS. Power users can still drop a stylesheet for things JSON can't express (color-mix(), scoped overrides), but the common case is config sugar.

Priority:highComplexity:mediumMilestone:v0.14.0Source:SPEC-048
chore/v0.14.0-milestone-closeout View source

Criteria completion

Criteria completion: 5 of 7 (71%) checked; tracking started on May 19, no incremental history yet0%25%50%75%100%May 19Jun 15

Tracking started May 19 — check back for trends.

Branches 3
History 1
  1. d617243
    Created (done)by bjornolofandersson

Acceptance Criteria

  • theme.tokens field in refrakt.config.json validated against ThemeTokensConfig at build time — validateThemeTokensConfig exported from @refrakt-md/transform walks the contract tree and rejects unknown keys, non-string leaves, and bad shapes
  • Validation errors surface clear messages (path + invalid value + valid options where applicable), not opaque schema errors — TokenValidationError carries dot-path + human-readable message; formatTokenValidationErrors produces multi-line output for adapters to throw or log
  • Build pipeline emits a generated :root { --rf-* } stylesheet matching the validated config — generateTokenStylesheet and generateThemeStylesheet exported from @refrakt-md/transform
  • Generated stylesheet injected into the rendered page after the theme package's CSS and before any user CSS files (deferred to Chunk 3 — adapter integration lands with the Lumina migration in WORK-191, where the pipeline can be verified against a real config-driven theme)
  • extra: Record<string, string> escape hatch passes through to the generated stylesheet as :root { --<key>: <value>; } declarations
  • A site with theme.tokens.color.text = "#ff0000" in config renders body text as red without any custom CSS (deferred to Chunk 3 with adapter integration)
  • Unit tests cover: validation passes for valid configs, validation fails with clear messages for invalid configs, generated stylesheet matches expected output — 35 new tests across token-merge.test.ts, token-stylesheet.test.ts, token-validate.test.ts

Approach

The generation pipeline lives in @refrakt-md/transform or @refrakt-md/content (decide during implementation — likely transform since it's where the merge logic lives).

Validation: use a runtime schema validator (Zod is already in use elsewhere in the project — check first; otherwise valibot or hand-rolled given the shape's stability). Validate at config-load time, fail fast.

Stylesheet generation: walk the ThemeTokensConfig tree, emit one CSS custom property per leaf, generate the dot-to-dash mapping (color.surface.base--rf-color-surface-base).

Injection: the SvelteKit Vite plugin and any other adapter integrates the generated stylesheet after the theme package's CSS in the document head. Order matters — theme provides defaults, generated CSS overrides them.

Out of scope here: the actual token values for the neutral default (that's WORK-200), preset merge (that's WORK-190), mode overlays (that's WORK-188).

Dependencies

  • WORK-185ThemeTokensConfig shape must exist.

References

  • SPEC-048 — "Config is sugar over CSS, not a replacement" design principle
  • Existing refrakt.config.json validation (if any) — extend rather than parallel-implement
  • packages/sveltekit — Vite plugin where CSS injection order is currently controlled

Resolution

Completed: 2026-05-19

Shipped across v0.14.0 Chunks 2 + 3 (commits ed12113a, f82bf685). validateThemeTokensConfig, generateTokenStylesheet, and generateThemeStylesheet live in @refrakt-md/transform and are wired through the SvelteKit plugin's composeSiteTokensCss (runs in buildStart), which serves the generated :root { --rf-* } declarations as the virtual:refrakt/site-tokens.css module loaded after Lumina's base CSS. Remaining unchecked criteria explicitly deferred to WORK-191 (adapter integration), which has also shipped.