Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7eb4ead
Update architecture
marciopmoreira6 Apr 16, 2026
d1a3e81
Update architecture with theming
marciopmoreira6 Apr 16, 2026
227bf98
Update theming
marciopmoreira6 Apr 16, 2026
9d70f86
Update colors.json
marciopmoreira6 Apr 16, 2026
079f12b
added roles json
marciopmoreira6 Apr 16, 2026
34d9b61
role changed from set to sub group
marciopmoreira6 Apr 16, 2026
e827b8e
brand A structure update
marciopmoreira6 Apr 16, 2026
25b8298
added color roles
marciopmoreira6 Apr 16, 2026
869543f
Update token names
marciopmoreira6 Apr 16, 2026
34a2bdb
update design tokens
marciopmoreira6 Apr 16, 2026
1428b1a
Add design token architecture docs and ADR
marciopmoreira6 Apr 16, 2026
2d17e01
new colors
marciopmoreira6 Apr 16, 2026
86e7c20
Update colors
marciopmoreira6 Apr 16, 2026
ccc8be0
Removed hover active and focus
marciopmoreira6 Apr 17, 2026
27715fa
remove neutral group
marciopmoreira6 Apr 17, 2026
1c9a436
Update group
marciopmoreira6 Apr 17, 2026
fb645db
Update the tokens
marciopmoreira6 Apr 17, 2026
3ed81fb
matching layers
marciopmoreira6 Apr 17, 2026
2acd27f
update indigo500
marciopmoreira6 Apr 17, 2026
6b3cfc0
Update neutral grouping
marciopmoreira6 Apr 17, 2026
39036d3
Merge branch 'marcio/revamp-tokens' of https://github.com/SonarSource…
marciopmoreira6 Apr 17, 2026
fe18b5f
neutral grouping
marciopmoreira6 Apr 17, 2026
13f8e83
Update grouping order
marciopmoreira6 Apr 17, 2026
9c20f0a
added states
marciopmoreira6 Apr 17, 2026
4dee7a0
updates to states
marciopmoreira6 Apr 17, 2026
bc4554c
update backgroiund.
marciopmoreira6 Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions design-tokens/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Design Tokens

This package defines and builds the design tokens for Echoes. It supports multiple brands and multiple color modes (light/dark) through a three-layer token architecture.

## Architecture

Tokens are organized in three layers, each building on the one below it:

```
brand/ ← Layer 1: Primitives (raw values, brand-specific)
mode/ ← Layer 2: Semantic (maps primitives to roles and intent)
component/ ← Layer 3: Component (maps semantic tokens to component slots)
```

### Layer 1 — Brand (`tokens/brand/`)

Contains raw, brand-specific values. Each brand lives in its own folder:

```
brand/
brandA/
base.json # Dimensions, spacing, typography, border radius
colors.json # Color palette + color roles
brandB/
base.json
colors.json
```

Each `colors.json` has two subgroups:

**`palette`** — the raw color scales for this brand. These are private to the brand layer and should not be referenced directly from mode or component tokens.

```jsonc
"palette": {
"grey": { "50": "#...", "100": "#...", ... "900": "#..." },
"indigo": { ... }, // brand primary color family
"tangerine": { ... }, // brand secondary color family
"green": { ... }, // shared sentiment colors
"yellow": { ... },
"orange": { ... },
"red": { ... },
"blue": { ... },
"purple": { ... }
}
```

**`roles`** — semantic color roles that map palette shades to named steps. These are the tokens that higher layers consume.

```jsonc
"roles": {
"support": { "black", "white", "transparent" },
"neutral": { "lightest", "light", "medium", "strong", "bold", "dark" },
"primary": { "lightest", "light", "medium", "strong", "bold", "dark" },
"secondary": { "lightest", "light", "medium", "strong", "bold", "dark" },
"info": { ... },
"success": { ... },
"warning": { ... },
"danger": { ... },
"highlight": { ... }
}
```

The step names go from lightest to darkest and map to the underlying palette like this:

| Step | Approx. palette shade |
|------------|----------------------|
| `lightest` | 50 |
| `light` | 100 |
| `medium` | 300 |
| `strong` | 500 |
| `bold` | 700 |
| `dark` | 900 |

`primary` and `secondary` reference the brand-specific palette families (`indigo` and `tangerine`). All other roles reference shared palette families and are identical across brands.

### Layer 2 — Mode (`tokens/mode/`)

Contains semantic color tokens that describe intent, not appearance. These tokens reference **role tokens** from the brand layer, never palette tokens directly.

```
mode/
light.json # Semantic tokens for light mode
dark.json # Semantic tokens for dark mode
```

Example:
```jsonc
"background": {
"accent": {
"default": { "$value": "{echoes.color.roles.primary.strong}" }
}
}
```

Because mode tokens reference roles (not palette shades), swapping a brand automatically changes the resolved color — no edits to mode files required.

### Layer 3 — Component (`tokens/component/`)

Maps semantic mode tokens to component-specific slots. These tokens are the ones consumed by component CSS.

```
component/
base.json # Non-color component tokens (shared across modes)
light.json # Light mode component token overrides
dark.json # Dark mode component token overrides
```

---

## Theme Configuration (`$themes.json`)

Tokens Studio themes define which token sets are active for each combination of brand and mode. There are three groups:

| Group | Themes | Purpose |
|---------|-----------------|---------------------------------------------|
| `Brand` | brandA, BrandB | Selects the active brand primitives |
| `Modes` | light, dark | Selects the active mode, using brandA colors |
| `Sonar` | base | Non-color base tokens, used at all times |

Each `Modes` theme includes the brand primitive sets as `"source"` so that role references resolve correctly at build time.

---

## Adding a New Brand

1. Copy `tokens/brand/brandA/` to `tokens/brand/brandC/`
2. Update `colors.json`:
- Replace `palette.indigo.*` values with the new brand's primary color scale
- Replace `palette.tangerine.*` values with the new brand's secondary color scale
- The `roles` section requires no changes — it references palette by name
3. Add `brand/brandC/base` and `brand/brandC/colors` entries to `$metadata.json`
4. Add a new `Brand` group entry to `$themes.json` for the new brand
5. No changes to `mode/` or `component/` files are needed

---

## Build

```sh
node build.js
```

Output is written to `src/generated/`:

| File | Contents |
|---------------------------------|-----------------------------------------------|
| `design-tokens-base.css` | Non-color CSS custom properties (`:root`) |
| `design-tokens-light.css` | Light mode color CSS custom properties |
| `design-tokens-dark.css` | Dark mode color CSS custom properties |
| `design-tokens.css` | Root file importing all of the above |
| `design-tokens-base.json` | Non-color tokens as flat JSON |
| `design-tokens-themed.json` | Light mode color tokens as flat JSON |
| `themes.ts` | `Theme` enum with all available theme names |
| `tailwindConfig.js` | Spacing, width and height tokens as a Tailwind preset |
2 changes: 1 addition & 1 deletion design-tokens/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const baseDesignTokenGroup = designTokenGroups.find(
({ group, name }) => group === 'Sonar' && name === 'base',
);

const themedDesignTokenGroups = designTokenGroups.filter(({ group }) => group === 'Themes');
const themedDesignTokenGroups = designTokenGroups.filter(({ group }) => group === 'Modes');

const sd = initStyleDictionary(licenseHeader);
await buildBaseTokens(baseDesignTokenGroup, sd);
Expand Down
82 changes: 82 additions & 0 deletions design-tokens/decisions/001-multi-brand-token-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# ADR 001 — Multi-Brand Design Token Architecture

**Date:** 2026-04-16
**Status:** Accepted
**Branch:** `marcio/revamp-tokens`

---

## Context

The original token architecture was designed for a single brand and a flat structure. Token sets were named `layer1`, `layer2`, `layer3` and were organized around the build pipeline rather than semantic meaning. As the need to support multiple brands (brandA, brandB) emerged, this structure created several problems:

- No clear separation between brand-specific values and shared semantic values
- Color palette tokens were referenced directly from semantic and component tokens, making brand swapping impossible without editing every downstream file
- The flat naming gave no guidance to contributors about where to add new tokens or what each layer was responsible for
- The `$themes.json` group name (`Themes`) was mismatched with the `build.js` filter (`group === 'Themes'`), which silently produced no themed output

---

## Decision

Rename and restructure the token layers around semantic responsibility rather than build order:

```
layer1/ → brand/ (primitives, brand-owned)
layer2/ → mode/ (semantic, shared)
layer3/ → component/ (component slots, shared)
```

Introduce a **palette / roles split** inside each brand's `colors.json`:

- **`palette`** holds raw color scales (50–900). It is private to the brand layer.
- **`roles`** holds named semantic steps (`lightest → light → medium → strong → bold → dark`) that map palette shades to intent. These are the values all upstream layers consume.

Mode files (`mode/light.json`, `mode/dark.json`) reference `roles.*` tokens, never `palette.*` tokens directly.

---

## Color Role Inventory

Each brand defines the following roles:

| Role | Brand-specific? | Source palette |
|-------------|-----------------|-------------------|
| `support` | No | Hardcoded black/white/transparent |
| `neutral` | No | `grey` |
| `primary` | **Yes** | `indigo` |
| `secondary` | **Yes** | `tangerine` |
| `info` | No | `blue` |
| `success` | No | `green` |
| `warning` | No | `yellow` |
| `danger` | No | `red` |
| `highlight` | No | `purple` |

Only `primary` and `secondary` differ between brands. All other roles resolve to the same hex values regardless of which brand is active.

---

## Consequences

**Positive:**

- Adding a new brand requires changes only in `brand/brandX/colors.json`. No mode or component tokens need to be updated.
- The step names (`lightest → dark`) communicate relative luminance without tying tokens to a specific palette shade, making them stable as palettes evolve.
- The `build.js` group mismatch (`Themes` vs `Modes`) was caught and fixed as part of this work, restoring correct themed CSS output.
- The architecture is self-documenting: a contributor looking at a mode token immediately knows it references a role, and a contributor looking at a role immediately knows which palette family backs it.

**Negative / Trade-offs:**

- Adding an additional indirection layer (palette → roles → semantic) means resolving a final CSS value requires following two hops instead of one.
- The palette shades used in the mode files before this change were granular (50, 100, 200, 300 ...). The roles layer collapses these to 6 steps. Some precision is intentionally lost — hover/active state distinctions that were previously encoded as adjacent palette shades now both resolve to the same role step. These interactions may need to be revisited if visual fidelity requires more granularity.
- The `mode/` files still hardcode `brandA` as the source in `$themes.json`. Multi-brand mode switching (e.g., brandB + dark mode) requires a follow-up theme configuration entry.

---

## Alternatives Considered

**Keep palette references in mode files, swap palettes per brand**
Rejected. Requires maintaining a full copy of every mode file per brand, or complex conditional logic in the build script.

**Single shared palette with CSS overrides at the brand level**
Rejected. Conflates the brand color system with CSS specificity concerns and doesn't model cleanly in Tokens Studio.
19 changes: 10 additions & 9 deletions design-tokens/tokens/$metadata.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"tokenSetOrder": [
"layer1/base",
"layer1/colors",
"layer2/base",
"layer2/light",
"layer2/dark",
"layer3/base",
"layer3/light",
"layer3/dark"
"brand/brandA/base",
"brand/brandA/colors",
"brand/brandB/base",
"brand/brandB/colors",
"mode/light",
"mode/dark",
"component/base",
"component/light",
"component/dark"
]
}
}
Loading
Loading