ECHOES-1279 ECHOES revamp design token architecture for multi-brand and multi-mode support#669
ECHOES-1279 ECHOES revamp design token architecture for multi-brand and multi-mode support#669marciopmoreira6 wants to merge 26 commits intomainfrom
Conversation
- README explaining the three-layer hierarchy, palette/roles split, step naming convention, how to add a brand, and build output - ADR 001 capturing the motivation, decision, and trade-offs behind the multi-brand token architecture revamp Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
✅ Deploy Preview for echoes-react ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
SummaryThis PR refactors the token system from a single-brand flat structure into a three-layer multi-brand architecture while fixing a critical build bug. Build Fix (critical): Changed Architecture: Renamed
Multi-brand enabler: Introduced a palette/roles split within each brand's colors. Palette tokens (raw shades 50–900) are private. Role tokens (lightest/light/medium/strong/bold/dark) are the public contract. Mode and component tokens reference roles, not palette. Results: Primary and secondary roles are brand-specific (indigo vs. teal/pink in brandB), while all sentiment roles (neutral, info, success, warning, danger, highlight) remain consistent across brands. Switching brands now changes only resolved color values, not downstream token references. Scale of changes: 234 token references updated in What reviewers should knowStart here: Read Key files to review:
Non-obvious details:
Test plan alignment: Build and verify themed CSS generation works. Check brand switching (brandA vs brandB) in light and dark modes.
|
There was a problem hiding this comment.
The token architecture refactor is clean and well-documented. There are two issues worth fixing: a naming inconsistency in $themes.json that will surface in generated output when brand-switching is wired into the build, and a meaningful architecture violation in the component layer.
| "default": { | ||
| "$type": "color", | ||
| "$value": "{echoes.color.green.200}" | ||
| "$value": "{echoes.color.palette.green.200}" |
There was a problem hiding this comment.
The component layer contains 51 direct echoes.color.palette.* references (green, yellow, orange, red, blue, grey) — both in this file and symmetrically in component/dark.json. The README and ADR explicitly state that palette is private to the brand layer and must not be referenced from component tokens; the whole point of the roles layer is to provide the interface that component and mode tokens consume.
The practical risk today is low (these are shared sentiment colors, not brand-specific), but this creates two problems:
- It violates the declared contract, setting a precedent for future contributors to bypass roles.
- If a brand ever wants different sentiment colors, these component tokens won't respond to brand switching — they'll silently stay hardcoded to the shared palette values.
These references should be migrated to role tokens (e.g. {echoes.color.roles.success.light} instead of {echoes.color.palette.green.200}).
- Mark as noise
…/echoes-react into marcio/revamp-tokens
There was a problem hiding this comment.
This commit adds new semantic groupings (background.neutral.* consolidation, background.intent.*, background.states.*) and flattens previously nested .default nodes. The structural intent is clear, but the commit ships two new bugs alongside the unresolved issue from the previous round.
| } | ||
| }, | ||
| "primary": { | ||
| "interaction": { |
There was a problem hiding this comment.
Bug: background.primary has different key names in light.json vs dark.json:
light.json:38definesinteraction(nodefault)dark.json:32definesdefault(nointeraction)
Any component token that references {echoes.background.primary.interaction} will fail to resolve in dark mode, and any reference to {echoes.background.primary.default} will fail to resolve in light mode. The set of child keys under a token path must be the same across every mode file — only the $value should differ.
- Mark as noise
| "states": { | ||
| "disable": { | ||
| "$type": "color", | ||
| "$value": "" |
There was a problem hiding this comment.
Bug: All 8 background.states.* tokens ship with "$value": "" (identical in dark.json lines 140–168). Empty values are not valid in Style Dictionary — the build will either throw or emit empty CSS custom properties, breaking anything that consumes these tokens. If these are placeholders meant to be filled in a follow-up, they should not be committed as $type: color tokens; either use a sentinel reference or omit the token until it has a real value.
- Mark as noise
| }, | ||
| "select": { | ||
| "$type": "color", | ||
| "$value": "#5d6cd033" |
There was a problem hiding this comment.
Bug: states.select is hardcoded to #5d6cd033 — an indigo primary color at 20% alpha — directly in the mode file. This violates the mode layer contract (no brand-specific raw values) and will silently ignore brand switching: brandB uses teal as its primary color, but this token will stay indigo.
The existing background.primary.interactionWeak on line 36 of this same file shows the correct pattern: reference a role token and apply the alpha modifier via the Tokens Studio extension.
Correct approach:
"select": {
"$extensions": {
"studio.tokens": {
"modify": {
"type": "alpha",
"value": "0.2",
"space": "hsl"
}
}
},
"$type": "color",
"$value": "{echoes.color.roles.primary.strong}"
}- Mark as noise
There was a problem hiding this comment.
This commit restructures the token namespaces (lifting primary, intent, states, utility out of background) and fixes the ghost button transparent references. However, none of the four previously flagged bugs are resolved, and the commit introduces two new mode-asymmetry bugs.
| "onInteraction": { | ||
| "$type": "color", | ||
| "$value": "{echoes.color.roles.support.white}" | ||
| } |
There was a problem hiding this comment.
Bug: background.onInteraction is defined in light.json but has no counterpart in dark.json. Any component token that references {echoes.color.background.onInteraction} will resolve correctly in light mode and silently fail (unresolved variable) in dark mode. Every token path that exists in one mode file must exist in both.
- Mark as noise
| "onInteractionWeak": { | ||
| "$type": "color", | ||
| "$value": "{echoes.color.roles.primary.strong}" | ||
| } |
There was a problem hiding this comment.
Bug: primary.onInteractionWeak is defined in light.json but has no counterpart in dark.json. Same problem as background.onInteraction above — any reference to {echoes.color.primary.onInteractionWeak} will break in dark mode.
- Mark as noise
Summary
Refactored the design token system from a single-brand, flat structure into a scalable multi-brand, multi-mode architecture.
Architecture changes:
layer1/layer2/layer3tobrand/mode/component, establishing a clear semantic hierarchy from primitives to semantic usage to component-specific tokensbuild.jsto correctly resolve theModesgroup from$themes.json(was filtering forThemes, producing no themed CSS output)$themes.jsonlight and dark mode entries to include brand primitive sets assource, restoring broken token resolution after the renameMulti-brand foundation:
brandAandbrandBcolor files with two distinct subgroups:palette(raw color scales) androles(semantic role mappings)neutral,primary,secondary,info,success,warning,danger, andhighlight— each with a 6-step tonal scale:lightest → light → medium → strong → bold → darkprimaryandsecondaryroles point to brand-specific palette colors (indigoandtangerine), so switching brands automatically produces the correct identity colors while all sentiment roles remain consistent across brandsbrandBvisually distinct palette values (teal forindigo, pink fortangerine) to make brand switching immediately visible during developmentSemantic token layer:
mode/light.jsonandmode/dark.json(234 references total) to point to role tokens (echoes.color.roles.*) instead of raw palette tokens (echoes.color.palette.*), completing the indirection layer between primitives and usageDocumentation:
design-tokens/README.mdexplaining the three-layer hierarchy, the palette/roles split, step naming, how to add a new brand, and the build outputdesign-tokens/decisions/001-multi-brand-token-architecture.md(ADR) capturing the motivation, decision rationale, and trade-offsFile structure after this PR
Test plan
node build.jsand confirm all CSS files are generated without errorssrc/generated/design-tokens-light.cssanddesign-tokens-dark.csscontain resolved color values🤖 Generated with Claude Code