WORK-187
Setting up your dashboard 0 entities found · 8/31 branches scanned
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

Criteria completion

Criteria completion: 1 of 7 (14%) checked; history from May 18 to May 180%25%50%75%100%May 18May 18
Branches 3
History 5
  1. 116c321
    statusin-progressdone
    by bjornolofandersson
  2. 908d19f
    • ☑ `extra: Record<string, string>` escape hatch passes through to the generated stylesheet as `:root { --<key>: <value>; }` declarations
    by bjornolofandersson
  3. ed12113
    Content editedby Claude
    v0.14.0 Chunk 2: SPEC-048 pipeline (WORK-187, 188, 190 — machinery)
  4. 3b92415
    Created (ready)by bjornolofandersson
  5. f73346a
    Content editedby Claude
    plan: add v0.14.0 milestone and SPEC-048 work items (WORK-185 to 191)

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.