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