WORK-190
Setting up your dashboard 0 entities found · 9/32 branches scanned
ID:WORK-190Status:done

Preset loading and merge order

Implement the theme.presets: string[] field that accepts module identifiers (e.g. "@refrakt-md/lumina/presets/tideline"), loads each as a ThemeTokensConfig, and merges them in declared order — last write wins per token. Establishes the mechanism that everything downstream (tideline, niwaki, future community presets) plugs into.

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

Criteria completion

Criteria completion: 8 of 8 (100%) 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.presets: string[] accepted in refrakt.config.json (type-level via SiteThemeConfig from Chunk 1; config-load wiring lands with adapter integration in Chunk 3)
  • Each preset string is resolved as a module identifier; resolution failures surface clear errors ("preset 'X' not found — check the package is installed and the export path is correct") — loadPreset exported from @refrakt-md/transform/node
  • Each loaded preset is validated against ThemeTokensConfig shape; invalid presets rejected with clear errors — loadPreset checks the export is a plain object; validateThemeTokensConfig is available for adapters to call after load
  • Presets merge in declared order with last-write-wins per token leaf (not per top-level field — deep-merge across nested namespaces like color.surface.base) — mergeTokenContracts walks the tree explicitly
  • Mode overlays inside presets merge independently from base — a preset can contribute to base and to any mode — mergeThemeTokensConfigs separates base from per-mode layers and merges each independently
  • Final merge order: theme base → presets in order → site theme.tokens → site theme.modes — adapters call mergeThemeTokensConfigs(themeBase, ...presets, { tokens: site.tokens, modes: site.modes }) in this exact order
  • Composition demonstrably works: a test config with presets: ["A", "B"] where A sets color.primary = red and B sets color.primary = blue produces blue — verified in token-merge.test.ts
  • Unit tests cover: single preset, multiple presets in order, preset + site override, validation failures — 15 merge tests + 7 loader tests + 16 validation tests

Approach

The merge logic extends mergeThemeConfig (packages/transform/src/merge.ts) — currently it shallow-merges tints and runes; this work adds deep-merging for the token contract specifically.

Resolution: theme.presets strings get fed into Node's import() (or require() for legacy adapters). Each resolved module is expected to export a ThemeTokensConfig as default or as a named config export — document the contract.

Deep merge: hand-rolled, not lodash.merge (which has surprising behaviour around arrays and null). Walk the contract tree explicitly, last-write-wins per leaf. null at any leaf is interpreted as "reset to inherit-from-previous-layer" (per SPEC-052's expectation that null is meaningful).

Out of scope: the actual tideline and niwaki preset modules themselves (those are WORK-204 and WORK-205). This work item builds the loading and merging infrastructure that those modules will plug into.

Dependencies

  • WORK-185ThemeTokensConfig shape.
  • WORK-187 — base stylesheet generation; preset-merged config feeds into the same pipeline.
  • WORK-188 — mode overlay shape; presets can contribute to modes.

References

  • SPEC-048 — "Presets are plain data" design principle
  • SPEC-051 — flagship presets (tideline, niwaki) that consume this mechanism
  • SPEC-053extends field at the tint level; analogous to but separate from preset extension

Resolution

Completed: 2026-05-19

All 8 acceptance criteria checked. loadPreset/loadPresets exported from @refrakt-md/transform (preset-loader.ts) with clear error messages on resolution failure; mergeTokenContracts/mergeThemeTokensConfigs (token-merge.ts) walk the contract tree explicitly for deterministic deep-merge, separating base from per-mode layers. The final merge order (theme base → presets in declared order → site theme.tokens → site theme.modes) is implemented by composeSiteTokensCss in the SvelteKit plugin. Composition verified by 15 merge tests + 7 loader tests + the niwaki-on-tideline composition path used by the docs site.