Skip to content

ECHOES-1279 ECHOES revamp design token architecture for multi-brand and multi-mode support#669

Open
marciopmoreira6 wants to merge 26 commits intomainfrom
marcio/revamp-tokens
Open

ECHOES-1279 ECHOES revamp design token architecture for multi-brand and multi-mode support#669
marciopmoreira6 wants to merge 26 commits intomainfrom
marcio/revamp-tokens

Conversation

@marciopmoreira6
Copy link
Copy Markdown
Contributor

Summary

Refactored the design token system from a single-brand, flat structure into a scalable multi-brand, multi-mode architecture.

Architecture changes:

  • Renamed token layers from layer1/layer2/layer3 to brand/mode/component, establishing a clear semantic hierarchy from primitives to semantic usage to component-specific tokens
  • Fixed build.js to correctly resolve the Modes group from $themes.json (was filtering for Themes, producing no themed CSS output)
  • Fixed $themes.json light and dark mode entries to include brand primitive sets as source, restoring broken token resolution after the rename

Multi-brand foundation:

  • Structured both brandA and brandB color files with two distinct subgroups: palette (raw color scales) and roles (semantic role mappings)
  • Added 8 color roles to each brand — neutral, primary, secondary, info, success, warning, danger, and highlight — each with a 6-step tonal scale: lightest → light → medium → strong → bold → dark
  • primary and secondary roles point to brand-specific palette colors (indigo and tangerine), so switching brands automatically produces the correct identity colors while all sentiment roles remain consistent across brands
  • Gave brandB visually distinct palette values (teal for indigo, pink for tangerine) to make brand switching immediately visible during development

Semantic token layer:

  • Updated mode/light.json and mode/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 usage

Documentation:

  • Added design-tokens/README.md explaining the three-layer hierarchy, the palette/roles split, step naming, how to add a new brand, and the build output
  • Added design-tokens/decisions/001-multi-brand-token-architecture.md (ADR) capturing the motivation, decision rationale, and trade-offs

File structure after this PR

design-tokens/tokens/
  brand/
    brandA/
      base.json       # dimensions, typography
      colors.json     # palette + roles (primary = indigo)
    brandB/
      base.json
      colors.json     # palette + roles (primary = teal, secondary = pink)
  mode/
    light.json        # semantic tokens → role refs
    dark.json         # semantic tokens → role refs
  component/
    base.json
    light.json
    dark.json

Test plan

  • Run node build.js and confirm all CSS files are generated without errors
  • Verify src/generated/design-tokens-light.css and design-tokens-dark.css contain resolved color values
  • Visually test brand switching between brandA and brandB — primary and secondary colors should change, sentiment colors should stay the same
  • Verify light/dark mode switching still works correctly for both brands

🤖 Generated with Claude Code

marciopmoreira6 and others added 11 commits April 16, 2026 11:48
- 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>
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 16, 2026

Deploy Preview for echoes-react ready!

Name Link
🔨 Latest commit bc4554c
🔍 Latest deploy log https://app.netlify.com/projects/echoes-react/deploys/69e20b175f3dcd00085534dc
😎 Deploy Preview https://deploy-preview-669--echoes-react.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@hashicorp-vault-sonar-prod hashicorp-vault-sonar-prod Bot changed the title ECHOES revamp design token architecture for multi-brand and multi-mode support ECHOES-1279 ECHOES revamp design token architecture for multi-brand and multi-mode support Apr 16, 2026
@hashicorp-vault-sonar-prod
Copy link
Copy Markdown

hashicorp-vault-sonar-prod Bot commented Apr 16, 2026

ECHOES-1279

@sonar-review-alpha
Copy link
Copy Markdown

sonar-review-alpha Bot commented Apr 16, 2026

Summary

This 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 build.js filter from group === 'Themes' to group === 'Modes'. This was silently discarding all mode/theme build output, meaning the themed CSS files were never being generated.

Architecture: Renamed layer1/layer2/layer3 to brand/mode/component for semantic clarity. This establishes:

  • Brand layer (primitives): Raw color scales—brand-specific, never referenced directly downstream
  • Mode layer (semantic): Intent-based color tokens that reference role tokens, enabling brand swapping without editing mode files
  • Component layer (slots): Component-specific token overrides

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 mode/light.json and mode/dark.json. $themes.json completely reorganized to include brand primitives as source in each mode theme, enabling role token resolution at build time.

What reviewers should know

Start here: Read design-tokens/README.md (154 lines)—it explains the three-layer hierarchy, palette/roles split, step naming, and how brand switching works. The ADR (decisions/001-multi-brand-token-architecture.md) provides decision rationale and trade-offs.

Key files to review:

  1. build.js (line 52)—the critical 'Themes''Modes' fix that unblocks themed CSS generation
  2. $themes.json—structure completely reorganized. Brand themes now list their own token sets; Mode themes now include brand primitives as source
  3. brand/brandA/colors.json and brand/brandB/colors.json—the palette/roles split pattern. BrandB has intentionally different palette values (teal/pink vs indigo/tangerine) to make brand switching visually obvious during testing
  4. mode/light.json and mode/dark.json—sample a few sections to verify token references now point to {echoes.color.roles.*} instead of {echoes.color.palette.*}

Non-obvious details:

  • Precision loss: 10 palette shades (50, 100, 200, 300...) compressed into 6 role steps. Hover/active state distinctions that were adjacent palette shades now resolve to the same step—visual fidelity should be verified
  • Mode tokens still hardcode brandA in $themes.json. brandB + dark mode combos need follow-up theme config entries (trade-off noted in ADR)
  • All 8 color roles (neutral, primary, secondary, info, success, warning, danger, highlight) follow the same 6-step tonal scale—verify consistency across all role definitions

Test plan alignment: Build and verify themed CSS generation works. Check brand switching (brandA vs brandB) in light and dark modes.


  • Generate Walkthrough
  • Generate Diagram

🗣️ Give feedback

Copy link
Copy Markdown

@sonar-review-alpha sonar-review-alpha Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

🗣️ Give feedback

"default": {
"$type": "color",
"$value": "{echoes.color.green.200}"
"$value": "{echoes.color.palette.green.200}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

sonar-review-alpha[bot]

This comment was marked as outdated.

sonar-review-alpha[bot]

This comment was marked as outdated.

sonar-review-alpha[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@sonar-review-alpha sonar-review-alpha Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

🗣️ Give feedback

Comment thread design-tokens/tokens/mode/light.json Outdated
}
},
"primary": {
"interaction": {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: background.primary has different key names in light.json vs dark.json:

  • light.json:38 defines interaction (no default)
  • dark.json:32 defines default (no interaction)

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

Comment thread design-tokens/tokens/mode/light.json Outdated
"states": {
"disable": {
"$type": "color",
"$value": ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

@sonar-review-alpha sonar-review-alpha Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit partially addresses the empty background.states values, but three previously flagged issues remain open and a new one appears in this commit.

🗣️ Give feedback

Comment thread design-tokens/tokens/mode/dark.json Outdated
},
"select": {
"$type": "color",
"$value": "#5d6cd033"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

@sonar-review-alpha sonar-review-alpha Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

🗣️ Give feedback

Comment on lines +63 to +66
"onInteraction": {
"$type": "color",
"$value": "{echoes.color.roles.support.white}"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +79 to +82
"onInteractionWeak": {
"$type": "color",
"$value": "{echoes.color.roles.primary.strong}"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant