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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user