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>
10 KiB
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:
- CSS -- imports
css/variables.cssfor static defaults (colors, spacing, typography, radii, component sizes). - 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-weightvariables. 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()
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
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
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
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
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 (configured in tsup.config.ts):
- Entry:
src/index.ts - Formats: ESM (
dist/index.js) + CJS (dist/index.cjs) - Types:
dist/index.d.tsanddist/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