WORK-190
Setting up your dashboard 0 entities found · 8/33 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

Criteria completion

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

Tracking started May 18 — check back for trends.

Branches 3
History 5
  1. 116c321
    statusin-progressdone
    by bjornolofandersson
  2. 908d19f
    statusreadyin-progress
    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.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.