Files
design-tokens/docs/ARCHITECTURE.md
T
Kaloyan Danchev 8a295c4acf Initial commit: @ampeco/design-tokens v1.0.0
Design token system for AMPECO Embedded Web Apps with light/dark themes,
CSS custom properties, and runtime theme application via applyTheme().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 19:34:08 +02:00

229 lines
10 KiB
Markdown

# Architecture
## Overview
`@ampeco/design-tokens` is the single source of truth for visual theming in the EWA (Embedded Web App). It bridges the AMPECO mobile app's design system to the web via CSS custom properties.
The package has two consumers:
1. **CSS** -- imports `css/variables.css` for static defaults (colors, spacing, typography, radii, component sizes).
2. **JavaScript** -- calls `applyTheme()` at runtime to set color tokens based on the active theme and any overrides from the mobile app.
## Token Structure
Tokens are organized into two layers:
### Themeable color tokens (JS-managed)
Defined in `src/tokens.ts` as the `ThemeTokens` interface. These are the tokens that change between light and dark mode. Each token has a corresponding CSS custom property name in `TOKEN_CSS_MAP`.
```
ThemeTokens key CSS variable Category
------------------------------------------------------------
btnPrimaryBg --btn-primary-bg Buttons
btnPrimaryText --btn-primary-text Buttons
btnPrimaryPressed --btn-primary-pressed Buttons
btnSecondaryBg --btn-secondary-bg Buttons
btnSecondaryBorder --btn-secondary-border Buttons
btnSecondaryText --btn-secondary-text Buttons
surfaceBg --surface-bg Surfaces
surfaceElevation1 --surface-elevation-1 Surfaces
surfaceElevation2 --surface-elevation-2 Surfaces
textBase --text-base Text
textSupport --text-support Text
textPlaceholder --text-placeholder Text
feedbackSuccess --feedback-success Feedback
feedbackSuccessSubtle --feedback-success-subtle Feedback
feedbackError --feedback-error Feedback
feedbackErrorSubtle --feedback-error-subtle Feedback
feedbackWarning --feedback-warning Feedback
feedbackWarningSubtle --feedback-warning-subtle Feedback
feedbackInfo --feedback-info Feedback
feedbackInfoSubtle --feedback-info-subtle Feedback
inputFill --input-fill Input
inputBorder --input-border Input
inputBorderActive --input-border-active Input
inputText --input-text Input
inputPlaceholder --input-placeholder Input
separator --separator Separator
statusAccepted --status-accepted Booking status
statusReserved --status-reserved Booking status
statusCompleted --status-completed Booking status
statusCancelled --status-cancelled Booking status
statusNoShow --status-no-show Booking status
statusFailed --status-failed Booking status
```
### Static layout tokens (CSS-only)
Defined only in `css/variables.css`. These do not change between themes and are not managed by JS:
- **Spacing** -- 9-step scale from `--spacing-xxxs` (2px) to `--spacing-xxxl` (64px)
- **Border radius** -- 4-step scale: `--radius-s` (8px), `--radius-m` (12px), `--radius-l` (16px), `--radius-xl` (32px)
- **Typography** -- 12 type styles, each with `-size`, `-line-height`, and `-weight` variables. Font family: Inter with system fallbacks.
- **Component sizes** -- 4-step scale: `--size-xs` (14px) to `--size-l` (44px)
## Theme System
### Light and dark defaults
Two complete `ThemeTokens` objects are exported:
- `lightTheme` (`src/light.ts`) -- Green primary (#00A573), white surfaces, black text.
- `darkTheme` (`src/dark.ts`) -- Blue primary (#0066FF), black surfaces, white text.
Both share identical booking status colors (these are semantic and theme-independent).
Values are sourced from the AMPECO mobile app design system at:
`mobile-2/src/design_system/migration_file/colors/newColorsFormatExample.ts`
### CSS fallback
`css/variables.css` contains `:root` declarations matching the light theme. This ensures the page renders correctly even before JavaScript runs or if `applyTheme()` is never called.
## AMPECO Mobile Token Mapping
The AMPECO mobile app sends design tokens via `postMessage` using its own naming convention. The mapping layer (`src/tokenMapping.ts`) translates these to EWA token keys.
### Naming convention translation
```
AMPECO mobile name EWA token key
-----------------------------------------------------------
buttonsButtonPrimaryBackground --> btnPrimaryBg
buttonsButtonPrimaryText --> btnPrimaryText
buttonsButtonSecondaryBackground --> btnSecondaryBg
buttonsButtonSecondaryBorder --> btnSecondaryBorder
buttonsButtonSecondaryText --> btnSecondaryText
surfaceSurfaceBackground --> surfaceBg
surfaceSurfaceElevation1 --> surfaceElevation1
surfaceSurfaceElevation2 --> surfaceElevation2
textsTextBase --> textBase
textsTextSupport --> textSupport
textsTextPlaceholder --> textPlaceholder
feedbackColorFeedbackSuccess --> feedbackSuccess
feedbackColorFeedbackError --> feedbackError
feedbackColorFeedbackWarning --> feedbackWarning
feedbackColorFeedbackInfo --> feedbackInfo
inputFormsInputBorder --> inputBorder
inputFormsInputFill --> inputFill
inputFormsInputTextLabels --> inputText
```
The AMPECO mobile app sends a subset of tokens as `AmpecoDesignTokens` (all fields optional). The EWA has additional tokens (e.g. `btnPrimaryPressed`, `feedbackSuccessSubtle`, `inputBorderActive`, `separator`, all `status*` tokens) that are not mapped from mobile tokens and always come from the base theme.
### mapAmpecoTokens()
```ts
function mapAmpecoTokens(tokens: AmpecoDesignTokens): Partial<ThemeTokens>
```
Iterates `AMPECO_TO_EWA_MAP`, looks up each AMPECO key in the input, and includes only truthy values in the result. Returns a partial token set suitable for spreading over a base theme.
## How applyTheme() Works at Runtime
`applyTheme()` in `src/apply.ts` is the main entry point for theme application. Here is the exact sequence:
### Step 1: Select base theme
```ts
const base = theme === 'DARK' ? darkTheme : lightTheme;
```
The `theme` parameter is `'LIGHT'` or `'DARK'` (matches `AppTheme` type, which corresponds to what the mobile app sends).
### Step 2: Map and merge AMPECO overrides
```ts
const overrides = designTokens ? mapAmpecoTokens(designTokens) : {};
const merged: ThemeTokens = { ...base, ...overrides };
```
Mobile app tokens override the base theme. Tokens not provided by the mobile app retain their base theme defaults.
### Step 3: Set data-theme attribute
```ts
document.documentElement.setAttribute('data-theme', theme.toLowerCase());
```
Sets `data-theme="light"` or `data-theme="dark"` on `<html>`. This enables CSS selectors like `[data-theme="dark"] .card { ... }` for theme-conditional styling that cannot be expressed with CSS custom properties alone.
### Step 4: Write CSS custom properties
```ts
for (const [key, value] of Object.entries(merged)) {
const cssVar = TOKEN_CSS_MAP[key as keyof ThemeTokens];
if (cssVar && value) {
document.documentElement.style.setProperty(cssVar, value);
}
}
```
Every token in the merged set is written as an inline style on `<html>`, which overrides the `:root` defaults from `css/variables.css`.
### Sequence diagram
```
Mobile App EWA DOM
| | |
|-- postMessage ------>| |
| { theme, tokens } | |
| |-- applyTheme(theme, t) ->|
| | 1. select base |
| | 2. map + merge |
| | 3. set data-theme |
| | 4. set CSS vars |
| | |
| | [page re-renders
| | with new colors]
```
## CSS Variable Naming Conventions
All CSS custom properties follow a flat, kebab-case naming scheme with category prefixes:
| Prefix | Category | Examples |
|---|---|---|
| `--btn-` | Button colors | `--btn-primary-bg`, `--btn-secondary-text` |
| `--surface-` | Surface/background | `--surface-bg`, `--surface-elevation-1` |
| `--text-` | Text colors | `--text-base`, `--text-support` |
| `--feedback-` | Feedback/status colors | `--feedback-error`, `--feedback-info-subtle` |
| `--input-` | Form input | `--input-fill`, `--input-border-active` |
| `--separator` | Divider lines | `--separator` |
| `--status-` | Booking status | `--status-accepted`, `--status-no-show` |
| `--spacing-` | Spacing scale | `--spacing-xs`, `--spacing-xxl` |
| `--radius-` | Border radius | `--radius-s`, `--radius-xl` |
| `--font-` | Typography | `--font-h1-size`, `--font-body-medium-weight` |
| `--size-` | Component sizes | `--size-xs`, `--size-l` |
No BEM, no nesting, no component-scoped prefixes. Variables are global and intended for use across all components.
## Build System
The package uses [tsup](https://tsup.egoist.dev/) (configured in `tsup.config.ts`):
- **Entry**: `src/index.ts`
- **Formats**: ESM (`dist/index.js`) + CJS (`dist/index.cjs`)
- **Types**: `dist/index.d.ts` and `dist/index.d.cts`
- **Source maps**: enabled
The CSS file is not processed by tsup. It ships as-is from `css/variables.css` and is exposed via the `"./css"` export map entry in `package.json`.
## File Dependency Graph
```
src/index.ts (barrel)
|-- src/types.ts AppTheme, AmpecoDesignTokens
|-- src/tokens.ts ThemeTokens, TOKEN_CSS_MAP
|-- src/light.ts lightTheme (imports ThemeTokens from tokens.ts)
|-- src/dark.ts darkTheme (imports ThemeTokens from tokens.ts)
|-- src/tokenMapping.ts AMPECO_TO_EWA_MAP, mapAmpecoTokens()
| (imports ThemeTokens from tokens.ts,
| AmpecoDesignTokens from types.ts)
|-- src/apply.ts applyTheme()
(imports everything above)
css/variables.css standalone, no imports
```