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>
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.tsbuildinfo
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# @ampeco/design-tokens
|
||||||
|
|
||||||
|
Design tokens for the AMPECO EWA (Embedded Web App). Provides theme definitions (light/dark), a token-to-CSS-variable mapping, and a runtime `applyTheme()` function that bridges the AMPECO mobile app design system to CSS custom properties.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @ampeco/design-tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
| Export path | What it provides |
|
||||||
|
|---|---|
|
||||||
|
| `@ampeco/design-tokens` | JS/TS: types, theme objects, `applyTheme()`, `mapAmpecoTokens()`, `TOKEN_CSS_MAP` |
|
||||||
|
| `@ampeco/design-tokens/css` | `css/variables.css` -- light-theme defaults plus spacing, radius, typography tokens |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### JavaScript / TypeScript
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { applyTheme, lightTheme, darkTheme, TOKEN_CSS_MAP } from '@ampeco/design-tokens';
|
||||||
|
|
||||||
|
// Apply light theme (sets CSS vars on <html>)
|
||||||
|
applyTheme('LIGHT');
|
||||||
|
|
||||||
|
// Apply dark theme with AMPECO mobile token overrides
|
||||||
|
applyTheme('DARK', {
|
||||||
|
buttonsButtonPrimaryBackground: '#FF0000',
|
||||||
|
surfaceSurfaceBackground: '#111111',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read a theme object directly
|
||||||
|
console.log(lightTheme.btnPrimaryBg); // '#00A573'
|
||||||
|
console.log(darkTheme.surfaceBg); // '#000000'
|
||||||
|
|
||||||
|
// Look up the CSS variable name for a token
|
||||||
|
console.log(TOKEN_CSS_MAP.btnPrimaryBg); // '--btn-primary-bg'
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS
|
||||||
|
|
||||||
|
Import the stylesheet to get light-theme defaults before JS initialises:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@import '@ampeco/design-tokens/css';
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use the custom properties in your styles:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.button-primary {
|
||||||
|
background: var(--btn-primary-bg);
|
||||||
|
color: var(--btn-primary-text);
|
||||||
|
font-size: var(--font-button-size);
|
||||||
|
font-weight: var(--font-button-weight);
|
||||||
|
line-height: var(--font-button-line-height);
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
padding: var(--spacing-s) var(--spacing-l);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mapping AMPECO mobile tokens
|
||||||
|
|
||||||
|
The mobile app sends tokens using its own naming convention (e.g. `buttonsButtonPrimaryBackground`). Use `mapAmpecoTokens()` to convert them to EWA token keys:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { mapAmpecoTokens } from '@ampeco/design-tokens';
|
||||||
|
|
||||||
|
const mobileTokens = {
|
||||||
|
buttonsButtonPrimaryBackground: '#FF0000',
|
||||||
|
textsTextBase: '#333333',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ewaTokens = mapAmpecoTokens(mobileTokens);
|
||||||
|
// { btnPrimaryBg: '#FF0000', textBase: '#333333' }
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
- **`AppTheme`** -- `'LIGHT' | 'DARK'`
|
||||||
|
- **`AmpecoDesignTokens`** -- Partial set of tokens as sent by the AMPECO mobile app via `postMessage`
|
||||||
|
- **`ThemeTokens`** -- Complete set of EWA theme tokens (buttons, surfaces, text, feedback, input, separator, booking status)
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
|
||||||
|
- **`TOKEN_CSS_MAP`** -- `Record<keyof ThemeTokens, string>` mapping each token key to its CSS custom property name (e.g. `btnPrimaryBg` -> `'--btn-primary-bg'`)
|
||||||
|
- **`lightTheme`** -- `ThemeTokens` with light mode defaults
|
||||||
|
- **`darkTheme`** -- `ThemeTokens` with dark mode defaults
|
||||||
|
- **`AMPECO_TO_EWA_MAP`** -- `Record<string, keyof ThemeTokens>` mapping AMPECO mobile token names to EWA token keys
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
- **`applyTheme(theme: AppTheme, designTokens?: AmpecoDesignTokens): void`** -- Selects the base theme, merges AMPECO overrides, sets `data-theme` attribute on `<html>`, and writes all tokens as CSS custom properties on `<html>`.
|
||||||
|
- **`mapAmpecoTokens(tokens: AmpecoDesignTokens): Partial<ThemeTokens>`** -- Converts AMPECO mobile token names to EWA token keys. Only truthy values are included.
|
||||||
|
|
||||||
|
## CSS Variable Categories
|
||||||
|
|
||||||
|
The `css/variables.css` file defines variables beyond the themeable color tokens:
|
||||||
|
|
||||||
|
| Category | Example variables | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Spacing | `--spacing-xxxs` (2px) through `--spacing-xxxl` (64px) | 9-step scale |
|
||||||
|
| Border radius | `--radius-s` (8px) through `--radius-xl` (32px) | 4-step scale |
|
||||||
|
| Typography | `--font-h1-size`, `--font-body-medium-weight`, etc. | 12 type styles |
|
||||||
|
| Component sizes | `--size-xs` (14px) through `--size-l` (44px) | 4-step scale |
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # one-shot build via tsup (ESM + CJS + .d.ts)
|
||||||
|
npm run dev # watch mode
|
||||||
|
```
|
||||||
|
|
||||||
|
Output lands in `dist/` (ESM as `index.js`, CJS as `index.cjs`, types as `index.d.ts` and `index.d.cts`).
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
index.ts Public API barrel export
|
||||||
|
types.ts AppTheme, AmpecoDesignTokens
|
||||||
|
tokens.ts ThemeTokens interface, TOKEN_CSS_MAP
|
||||||
|
light.ts lightTheme defaults
|
||||||
|
dark.ts darkTheme defaults
|
||||||
|
tokenMapping.ts AMPECO_TO_EWA_MAP, mapAmpecoTokens()
|
||||||
|
apply.ts applyTheme()
|
||||||
|
css/
|
||||||
|
variables.css Static CSS custom properties (light defaults + layout tokens)
|
||||||
|
```
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* @ampeco/design-tokens - CSS Custom Properties
|
||||||
|
*
|
||||||
|
* These variables are set programmatically by applyTheme() at runtime.
|
||||||
|
* The :root defaults below match the light theme so the page has
|
||||||
|
* sensible values even before JavaScript initialises.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* ─── Buttons ─────────────────────────────────────────────── */
|
||||||
|
--btn-primary-bg: #00A573;
|
||||||
|
--btn-primary-text: #FFFFFF;
|
||||||
|
--btn-primary-pressed: #D7E4DC;
|
||||||
|
--btn-secondary-bg: #FFFFFF;
|
||||||
|
--btn-secondary-border: #00A573;
|
||||||
|
--btn-secondary-text: #000000;
|
||||||
|
|
||||||
|
/* ─── Surfaces ────────────────────────────────────────────── */
|
||||||
|
--surface-bg: #FFFFFF;
|
||||||
|
--surface-elevation-1: #EEF2EF;
|
||||||
|
--surface-elevation-2: #D7E4DC;
|
||||||
|
|
||||||
|
/* ─── Text ────────────────────────────────────────────────── */
|
||||||
|
--text-base: #000000;
|
||||||
|
--text-support: #808080;
|
||||||
|
--text-placeholder: #808080;
|
||||||
|
|
||||||
|
/* ─── Feedback ────────────────────────────────────────────── */
|
||||||
|
--feedback-success: #24A629;
|
||||||
|
--feedback-success-subtle: #BDE4BF;
|
||||||
|
--feedback-error: #FF6868;
|
||||||
|
--feedback-error-subtle: #FFD2D2;
|
||||||
|
--feedback-warning: #FFB23E;
|
||||||
|
--feedback-warning-subtle: #FFE8C5;
|
||||||
|
--feedback-info: #2F83FF;
|
||||||
|
--feedback-info-subtle: #C1DAFF;
|
||||||
|
|
||||||
|
/* ─── Input ───────────────────────────────────────────────── */
|
||||||
|
--input-fill: #EEF2EF;
|
||||||
|
--input-border: #D7E4DC;
|
||||||
|
--input-border-active: #C8DAD7;
|
||||||
|
--input-text: #000000;
|
||||||
|
--input-placeholder: #808080;
|
||||||
|
|
||||||
|
/* ─── Separator ───────────────────────────────────────────── */
|
||||||
|
--separator: #C8DAD7;
|
||||||
|
|
||||||
|
/* ─── Booking Status ──────────────────────────────────────── */
|
||||||
|
--status-accepted: #2F83FF;
|
||||||
|
--status-reserved: #24A629;
|
||||||
|
--status-completed: #808080;
|
||||||
|
--status-cancelled: #FFB23E;
|
||||||
|
--status-no-show: #FF6868;
|
||||||
|
--status-failed: #B71C1C;
|
||||||
|
|
||||||
|
/* ─── Spacing Scale ───────────────────────────────────────── */
|
||||||
|
--spacing-xxxs: 2px;
|
||||||
|
--spacing-xxs: 4px;
|
||||||
|
--spacing-xs: 6px;
|
||||||
|
--spacing-s: 8px;
|
||||||
|
--spacing-m: 12px;
|
||||||
|
--spacing-l: 16px;
|
||||||
|
--spacing-xl: 24px;
|
||||||
|
--spacing-xxl: 32px;
|
||||||
|
--spacing-xxxl: 64px;
|
||||||
|
|
||||||
|
/* ─── Border Radius ───────────────────────────────────────── */
|
||||||
|
--radius-s: 8px;
|
||||||
|
--radius-m: 12px;
|
||||||
|
--radius-l: 16px;
|
||||||
|
--radius-xl: 32px;
|
||||||
|
|
||||||
|
/* ─── Typography ──────────────────────────────────────────── */
|
||||||
|
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
|
||||||
|
/* H1 - Semibold 22/26 */
|
||||||
|
--font-h1-size: 22px;
|
||||||
|
--font-h1-line-height: 26px;
|
||||||
|
--font-h1-weight: 600;
|
||||||
|
|
||||||
|
/* H2 - Semibold 18/22 */
|
||||||
|
--font-h2-size: 18px;
|
||||||
|
--font-h2-line-height: 22px;
|
||||||
|
--font-h2-weight: 600;
|
||||||
|
|
||||||
|
/* H3 - Medium 16/20 */
|
||||||
|
--font-h3-size: 16px;
|
||||||
|
--font-h3-line-height: 20px;
|
||||||
|
--font-h3-weight: 500;
|
||||||
|
|
||||||
|
/* H4 - Medium 14/17 */
|
||||||
|
--font-h4-size: 14px;
|
||||||
|
--font-h4-line-height: 17px;
|
||||||
|
--font-h4-weight: 500;
|
||||||
|
|
||||||
|
/* Body Large - Regular 16/20 */
|
||||||
|
--font-body-large-size: 16px;
|
||||||
|
--font-body-large-line-height: 20px;
|
||||||
|
--font-body-large-weight: 400;
|
||||||
|
|
||||||
|
/* Body Medium - Regular 14/17 */
|
||||||
|
--font-body-medium-size: 14px;
|
||||||
|
--font-body-medium-line-height: 17px;
|
||||||
|
--font-body-medium-weight: 400;
|
||||||
|
|
||||||
|
/* Body Large Heavy - Semibold 16/20 */
|
||||||
|
--font-body-large-heavy-size: 16px;
|
||||||
|
--font-body-large-heavy-line-height: 20px;
|
||||||
|
--font-body-large-heavy-weight: 600;
|
||||||
|
|
||||||
|
/* Body Medium Heavy - Semibold 14/17 */
|
||||||
|
--font-body-medium-heavy-size: 14px;
|
||||||
|
--font-body-medium-heavy-line-height: 17px;
|
||||||
|
--font-body-medium-heavy-weight: 600;
|
||||||
|
|
||||||
|
/* Buttons - Semibold 14/20 */
|
||||||
|
--font-button-size: 14px;
|
||||||
|
--font-button-line-height: 20px;
|
||||||
|
--font-button-weight: 600;
|
||||||
|
|
||||||
|
/* Buttons Small - Bold 12/17 */
|
||||||
|
--font-button-small-size: 12px;
|
||||||
|
--font-button-small-line-height: 17px;
|
||||||
|
--font-button-small-weight: 700;
|
||||||
|
|
||||||
|
/* Label - Regular 12/15 */
|
||||||
|
--font-label-size: 12px;
|
||||||
|
--font-label-line-height: 15px;
|
||||||
|
--font-label-weight: 400;
|
||||||
|
|
||||||
|
/* Caption - Medium 12/15 */
|
||||||
|
--font-caption-size: 12px;
|
||||||
|
--font-caption-line-height: 15px;
|
||||||
|
--font-caption-weight: 500;
|
||||||
|
|
||||||
|
/* ─── Component Sizes ─────────────────────────────────────── */
|
||||||
|
--size-xs: 14px;
|
||||||
|
--size-s: 20px;
|
||||||
|
--size-m: 32px;
|
||||||
|
--size-l: 44px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
Generated
+1475
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@ampeco/design-tokens",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
},
|
||||||
|
"./css": "./css/variables.css",
|
||||||
|
"./css/*": "./css/*"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"css"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tsup": "^8.0.0",
|
||||||
|
"typescript": "^5.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { ThemeTokens } from './tokens';
|
||||||
|
import { TOKEN_CSS_MAP } from './tokens';
|
||||||
|
import type { AppTheme, AmpecoDesignTokens } from './types';
|
||||||
|
import { lightTheme } from './light';
|
||||||
|
import { darkTheme } from './dark';
|
||||||
|
import { mapAmpecoTokens } from './tokenMapping';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the given theme to the document root element.
|
||||||
|
*
|
||||||
|
* 1. Selects the base theme (light or dark).
|
||||||
|
* 2. Merges in any AMPECO design token overrides received from the mobile app.
|
||||||
|
* 3. Sets the `data-theme` attribute on <html> for CSS selectors.
|
||||||
|
* 4. Sets each token as a CSS custom property on <html>.
|
||||||
|
*
|
||||||
|
* @param theme - The theme mode ('LIGHT' or 'DARK').
|
||||||
|
* @param designTokens - Optional partial token overrides from the AMPECO mobile app.
|
||||||
|
*/
|
||||||
|
export function applyTheme(theme: AppTheme, designTokens?: AmpecoDesignTokens): void {
|
||||||
|
const base = theme === 'DARK' ? darkTheme : lightTheme;
|
||||||
|
const overrides = designTokens ? mapAmpecoTokens(designTokens) : {};
|
||||||
|
const merged: ThemeTokens = { ...base, ...overrides };
|
||||||
|
|
||||||
|
document.documentElement.setAttribute('data-theme', theme.toLowerCase());
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
import type { ThemeTokens } from './tokens';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dark theme defaults.
|
||||||
|
* Values sourced from the AMPECO mobile app design system
|
||||||
|
* (mobile-2/src/design_system/migration_file/colors/newColorsFormatExample.ts).
|
||||||
|
*/
|
||||||
|
export const darkTheme: ThemeTokens = {
|
||||||
|
// Buttons
|
||||||
|
btnPrimaryBg: '#0066FF',
|
||||||
|
btnPrimaryText: '#FFFFFF',
|
||||||
|
btnPrimaryPressed: '#2C4965',
|
||||||
|
btnSecondaryBg: '#000000',
|
||||||
|
btnSecondaryBorder: '#0066FF',
|
||||||
|
btnSecondaryText: '#FFFFFF',
|
||||||
|
|
||||||
|
// Surfaces
|
||||||
|
surfaceBg: '#000000',
|
||||||
|
surfaceElevation1: '#1A2D3F',
|
||||||
|
surfaceElevation2: '#2C4965',
|
||||||
|
|
||||||
|
// Text
|
||||||
|
textBase: '#FFFFFF',
|
||||||
|
textSupport: '#999999',
|
||||||
|
textPlaceholder: '#999999',
|
||||||
|
|
||||||
|
// Feedback
|
||||||
|
feedbackSuccess: '#61E775',
|
||||||
|
feedbackSuccessSubtle: '#1D4523',
|
||||||
|
feedbackError: '#FF5454',
|
||||||
|
feedbackErrorSubtle: '#4C1919',
|
||||||
|
feedbackWarning: '#FFC062',
|
||||||
|
feedbackWarningSubtle: '#4C3A1D',
|
||||||
|
feedbackInfo: '#3C82EA',
|
||||||
|
feedbackInfoSubtle: '#122746',
|
||||||
|
|
||||||
|
// Input
|
||||||
|
inputFill: '#1A2D3F',
|
||||||
|
inputBorder: '#2C4965',
|
||||||
|
inputBorderActive: '#3B658C',
|
||||||
|
inputText: '#FFFFFF',
|
||||||
|
inputPlaceholder: '#999999',
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
separator: '#3B658C',
|
||||||
|
|
||||||
|
// Booking status (shared across themes)
|
||||||
|
statusAccepted: '#2F83FF',
|
||||||
|
statusReserved: '#24A629',
|
||||||
|
statusCompleted: '#808080',
|
||||||
|
statusCancelled: '#FFB23E',
|
||||||
|
statusNoShow: '#FF6868',
|
||||||
|
statusFailed: '#B71C1C',
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Types
|
||||||
|
export type { AppTheme, AmpecoDesignTokens } from './types';
|
||||||
|
export type { ThemeTokens } from './tokens';
|
||||||
|
|
||||||
|
// Token definitions and CSS variable map
|
||||||
|
export { TOKEN_CSS_MAP } from './tokens';
|
||||||
|
|
||||||
|
// Theme defaults
|
||||||
|
export { lightTheme } from './light';
|
||||||
|
export { darkTheme } from './dark';
|
||||||
|
|
||||||
|
// Token mapping (AMPECO mobile -> EWA)
|
||||||
|
export { AMPECO_TO_EWA_MAP, mapAmpecoTokens } from './tokenMapping';
|
||||||
|
|
||||||
|
// Theme application
|
||||||
|
export { applyTheme } from './apply';
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { ThemeTokens } from './tokens';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Light theme defaults.
|
||||||
|
* Values sourced from the AMPECO mobile app design system
|
||||||
|
* (mobile-2/src/design_system/migration_file/colors/newColorsFormatExample.ts).
|
||||||
|
*/
|
||||||
|
export const lightTheme: ThemeTokens = {
|
||||||
|
// Buttons
|
||||||
|
btnPrimaryBg: '#00A573',
|
||||||
|
btnPrimaryText: '#FFFFFF',
|
||||||
|
btnPrimaryPressed: '#D7E4DC',
|
||||||
|
btnSecondaryBg: '#FFFFFF',
|
||||||
|
btnSecondaryBorder: '#00A573',
|
||||||
|
btnSecondaryText: '#000000',
|
||||||
|
|
||||||
|
// Surfaces
|
||||||
|
surfaceBg: '#FFFFFF',
|
||||||
|
surfaceElevation1: '#EEF2EF',
|
||||||
|
surfaceElevation2: '#D7E4DC',
|
||||||
|
|
||||||
|
// Text
|
||||||
|
textBase: '#000000',
|
||||||
|
textSupport: '#808080',
|
||||||
|
textPlaceholder: '#808080',
|
||||||
|
|
||||||
|
// Feedback
|
||||||
|
feedbackSuccess: '#24A629',
|
||||||
|
feedbackSuccessSubtle: '#BDE4BF',
|
||||||
|
feedbackError: '#FF6868',
|
||||||
|
feedbackErrorSubtle: '#FFD2D2',
|
||||||
|
feedbackWarning: '#FFB23E',
|
||||||
|
feedbackWarningSubtle: '#FFE8C5',
|
||||||
|
feedbackInfo: '#2F83FF',
|
||||||
|
feedbackInfoSubtle: '#C1DAFF',
|
||||||
|
|
||||||
|
// Input
|
||||||
|
inputFill: '#EEF2EF',
|
||||||
|
inputBorder: '#D7E4DC',
|
||||||
|
inputBorderActive: '#C8DAD7',
|
||||||
|
inputText: '#000000',
|
||||||
|
inputPlaceholder: '#808080',
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
separator: '#C8DAD7',
|
||||||
|
|
||||||
|
// Booking status (shared across themes)
|
||||||
|
statusAccepted: '#2F83FF',
|
||||||
|
statusReserved: '#24A629',
|
||||||
|
statusCompleted: '#808080',
|
||||||
|
statusCancelled: '#FFB23E',
|
||||||
|
statusNoShow: '#FF6868',
|
||||||
|
statusFailed: '#B71C1C',
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { ThemeTokens } from './tokens';
|
||||||
|
import type { AmpecoDesignTokens } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps AMPECO mobile app token property names to EWA ThemeTokens keys.
|
||||||
|
* The mobile app sends tokens using its own naming convention; this map
|
||||||
|
* translates them to the EWA internal token names.
|
||||||
|
*/
|
||||||
|
export const AMPECO_TO_EWA_MAP: Record<string, keyof ThemeTokens> = {
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts AMPECO mobile design tokens into the EWA ThemeTokens format.
|
||||||
|
* Only tokens that are present and have a truthy value are included in the result.
|
||||||
|
*/
|
||||||
|
export function mapAmpecoTokens(tokens: AmpecoDesignTokens): Partial<ThemeTokens> {
|
||||||
|
const result: Partial<ThemeTokens> = {};
|
||||||
|
|
||||||
|
for (const [ampecoKey, ewaKey] of Object.entries(AMPECO_TO_EWA_MAP)) {
|
||||||
|
const value = tokens[ampecoKey as keyof AmpecoDesignTokens];
|
||||||
|
if (value) {
|
||||||
|
result[ewaKey] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Complete set of EWA theme tokens.
|
||||||
|
* Each key maps to a CSS custom property via TOKEN_CSS_MAP.
|
||||||
|
*/
|
||||||
|
export interface ThemeTokens {
|
||||||
|
// Buttons
|
||||||
|
btnPrimaryBg: string;
|
||||||
|
btnPrimaryText: string;
|
||||||
|
btnPrimaryPressed: string;
|
||||||
|
btnSecondaryBg: string;
|
||||||
|
btnSecondaryBorder: string;
|
||||||
|
btnSecondaryText: string;
|
||||||
|
|
||||||
|
// Surfaces
|
||||||
|
surfaceBg: string;
|
||||||
|
surfaceElevation1: string;
|
||||||
|
surfaceElevation2: string;
|
||||||
|
|
||||||
|
// Text
|
||||||
|
textBase: string;
|
||||||
|
textSupport: string;
|
||||||
|
textPlaceholder: string;
|
||||||
|
|
||||||
|
// Feedback
|
||||||
|
feedbackSuccess: string;
|
||||||
|
feedbackSuccessSubtle: string;
|
||||||
|
feedbackError: string;
|
||||||
|
feedbackErrorSubtle: string;
|
||||||
|
feedbackWarning: string;
|
||||||
|
feedbackWarningSubtle: string;
|
||||||
|
feedbackInfo: string;
|
||||||
|
feedbackInfoSubtle: string;
|
||||||
|
|
||||||
|
// Input
|
||||||
|
inputFill: string;
|
||||||
|
inputBorder: string;
|
||||||
|
inputBorderActive: string;
|
||||||
|
inputText: string;
|
||||||
|
inputPlaceholder: string;
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
separator: string;
|
||||||
|
|
||||||
|
// Booking status
|
||||||
|
statusAccepted: string;
|
||||||
|
statusReserved: string;
|
||||||
|
statusCompleted: string;
|
||||||
|
statusCancelled: string;
|
||||||
|
statusNoShow: string;
|
||||||
|
statusFailed: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps each ThemeTokens key to the corresponding CSS custom property name.
|
||||||
|
*/
|
||||||
|
export const TOKEN_CSS_MAP: Record<keyof ThemeTokens, string> = {
|
||||||
|
// Buttons
|
||||||
|
btnPrimaryBg: '--btn-primary-bg',
|
||||||
|
btnPrimaryText: '--btn-primary-text',
|
||||||
|
btnPrimaryPressed: '--btn-primary-pressed',
|
||||||
|
btnSecondaryBg: '--btn-secondary-bg',
|
||||||
|
btnSecondaryBorder: '--btn-secondary-border',
|
||||||
|
btnSecondaryText: '--btn-secondary-text',
|
||||||
|
|
||||||
|
// Surfaces
|
||||||
|
surfaceBg: '--surface-bg',
|
||||||
|
surfaceElevation1: '--surface-elevation-1',
|
||||||
|
surfaceElevation2: '--surface-elevation-2',
|
||||||
|
|
||||||
|
// Text
|
||||||
|
textBase: '--text-base',
|
||||||
|
textSupport: '--text-support',
|
||||||
|
textPlaceholder: '--text-placeholder',
|
||||||
|
|
||||||
|
// Feedback
|
||||||
|
feedbackSuccess: '--feedback-success',
|
||||||
|
feedbackSuccessSubtle: '--feedback-success-subtle',
|
||||||
|
feedbackError: '--feedback-error',
|
||||||
|
feedbackErrorSubtle: '--feedback-error-subtle',
|
||||||
|
feedbackWarning: '--feedback-warning',
|
||||||
|
feedbackWarningSubtle: '--feedback-warning-subtle',
|
||||||
|
feedbackInfo: '--feedback-info',
|
||||||
|
feedbackInfoSubtle: '--feedback-info-subtle',
|
||||||
|
|
||||||
|
// Input
|
||||||
|
inputFill: '--input-fill',
|
||||||
|
inputBorder: '--input-border',
|
||||||
|
inputBorderActive: '--input-border-active',
|
||||||
|
inputText: '--input-text',
|
||||||
|
inputPlaceholder: '--input-placeholder',
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
separator: '--separator',
|
||||||
|
|
||||||
|
// Booking status
|
||||||
|
statusAccepted: '--status-accepted',
|
||||||
|
statusReserved: '--status-reserved',
|
||||||
|
statusCompleted: '--status-completed',
|
||||||
|
statusCancelled: '--status-cancelled',
|
||||||
|
statusNoShow: '--status-no-show',
|
||||||
|
statusFailed: '--status-failed',
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Theme mode identifier. Matches the value sent by the AMPECO mobile app.
|
||||||
|
*/
|
||||||
|
export type AppTheme = 'LIGHT' | 'DARK';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design tokens as received from the AMPECO mobile app via postMessage.
|
||||||
|
* All fields are optional because the app may send a partial override set.
|
||||||
|
*/
|
||||||
|
export interface AmpecoDesignTokens {
|
||||||
|
buttonsButtonPrimaryBackground?: string;
|
||||||
|
buttonsButtonPrimaryText?: string;
|
||||||
|
buttonsButtonSecondaryBackground?: string;
|
||||||
|
buttonsButtonSecondaryBorder?: string;
|
||||||
|
buttonsButtonSecondaryText?: string;
|
||||||
|
surfaceSurfaceBackground?: string;
|
||||||
|
surfaceSurfaceElevation1?: string;
|
||||||
|
surfaceSurfaceElevation2?: string;
|
||||||
|
textsTextBase?: string;
|
||||||
|
textsTextSupport?: string;
|
||||||
|
textsTextPlaceholder?: string;
|
||||||
|
feedbackColorFeedbackSuccess?: string;
|
||||||
|
feedbackColorFeedbackError?: string;
|
||||||
|
feedbackColorFeedbackWarning?: string;
|
||||||
|
feedbackColorFeedbackInfo?: string;
|
||||||
|
inputFormsInputBorder?: string;
|
||||||
|
inputFormsInputFill?: string;
|
||||||
|
inputFormsInputTextLabels?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"lib": ["ES2020", "DOM"]
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
sourcemap: true,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user