8a295c4acf
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>
229 lines
10 KiB
Markdown
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
|
|
```
|