You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Paragon's design tokens currently couple two separable concerns into the themes/<variant>/ directory:
The declaration that a path exists — e.g. color.gray.500 is a token of type color.
The value of that path in a particular theme — e.g. #454545 in light, possibly something else in dark.
tokens/src/themes/light/global/color.json does both. The schema (which paths exist) is determined by whatever the theme files happen to contain.
This coupling was surfaced while implementing #4274 (app tokens). App tokens reference Paragon paths like {color.gray.500}, and the build needs those paths in scope for reference resolution. With the current architecture, the only way to expose them is to include a specific theme variant's files (themes/light/**) — which drags in concrete values that the app build doesn't actually care about, since the output is var(--pgn-color-gray-500) regardless. It's load-bearing in name only; the values are immediately filtered out.
Proposal
Split the token tree into semantic tokens (the paths Paragon promises consumers can reference) and primitive tokens (the concrete values per theme variant):
Every existing themes/<variant>/**/*.json file needs migration: each color.X.Y becomes a raw.color.X.Y definition, and a corresponding core/ entry references it.
The modify: [...] chain (color-yiq, mix, darken, lighten) currently runs against theme-resolved values. It would need to either run in the theme phase against raw values or remain coupled to a theme; either way, the chain's semantics need a careful pass.
Cross-token references (e.g. color.btn.bg.brand → {color.brand.500}) need to resolve through the indirection layer correctly.
The build's current assumption that core/ has no theme variants would change — core/ would contain references that only resolve once a theme is layered on. Either build tooling adapts or core/ is split further into "schema" (paths only) vs. theme-invariant primitives.
Theme author packages (e.g. edx/elm-theme) currently override themes/light/global/color.json. A migration story is needed so existing brand packages keep working.
Why now / next steps
Not blocking #4274 — that issue's plan documents a workaround (include themes/light purely for the vocabulary). But the coupling makes the workaround feel incidental, and the split would benefit other future work too (e.g. theme variants beyond light/dark, multi-brand theme inheritance (No idea what Claude was thinking with these, the real benefits I see are being able to better support "base-theme-less" themes)).
Reasonable next step: a small spike PR that picks one token (say, color.gray) and works through the migration end-to-end — exposes the real difficulties of the modify chain and cross-references, and informs whether the broader migration is worth it.
Note
Generated by Claude
Problem
Paragon's design tokens currently couple two separable concerns into the
themes/<variant>/directory:color.gray.500is a token of typecolor.#454545in light, possibly something else in dark.tokens/src/themes/light/global/color.jsondoes both. The schema (which paths exist) is determined by whatever the theme files happen to contain.This coupling was surfaced while implementing #4274 (app tokens). App tokens reference Paragon paths like
{color.gray.500}, and the build needs those paths in scope for reference resolution. With the current architecture, the only way to expose them is to include a specific theme variant's files (themes/light/**) — which drags in concrete values that the app build doesn't actually care about, since the output isvar(--pgn-color-gray-500)regardless. It's load-bearing in name only; the values are immediately filtered out.Proposal
Split the token tree into semantic tokens (the paths Paragon promises consumers can reference) and primitive tokens (the concrete values per theme variant):
Concrete shape:
After this split:
core/is the stable interface Paragon exposes. Anything referencing{color.…}only needscore/in scope.raw.*set, no schema duplication.--pgnprefixed tokens from MFEs #4274) stops depending on which theme happens to be included for vocabulary purposes.Tradeoffs / scope
This is a multi-PR effort, not a small change:
themes/<variant>/**/*.jsonfile needs migration: eachcolor.X.Ybecomes araw.color.X.Ydefinition, and a correspondingcore/entry references it.modify: [...]chain (color-yiq,mix,darken,lighten) currently runs against theme-resolved values. It would need to either run in the theme phase against raw values or remain coupled to a theme; either way, the chain's semantics need a careful pass.color.btn.bg.brand → {color.brand.500}) need to resolve through the indirection layer correctly.core/has no theme variants would change —core/would contain references that only resolve once a theme is layered on. Either build tooling adapts orcore/is split further into "schema" (paths only) vs. theme-invariant primitives.themes/light/global/color.json. A migration story is needed so existing brand packages keep working.Why now / next steps
Not blocking #4274 — that issue's plan documents a workaround (include
themes/lightpurely for the vocabulary). But the coupling makes the workaround feel incidental, and the split would benefit other future work too (e.g.theme variants beyond light/dark, multi-brand theme inheritance(No idea what Claude was thinking with these, the real benefits I see are being able to better support "base-theme-less" themes)).Reasonable next step: a small spike PR that picks one token (say,
color.gray) and works through the migration end-to-end — exposes the real difficulties of themodifychain and cross-references, and informs whether the broader migration is worth it.