# Design Tokens — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** JSON-driven multi-hospital theming — sidecar serves theme config, frontend provider injects CSS variables + content tokens, supervisor edits branding from Settings. **Architecture:** Sidecar stores `data/theme.json`, serves via REST. Frontend `ThemeTokenProvider` fetches on mount, overrides CSS custom properties on ``, exposes content tokens via React context. Settings page has a Branding tab for admins. **Tech Stack:** NestJS (sidecar controller/service), React context + CSS custom properties (frontend), Untitled UI components (settings form) **Spec:** `docs/superpowers/specs/2026-04-02-design-tokens-design.md` --- ## File Map | File | Action | Responsibility | |---|---|---| | `helix-engage-server/src/config/theme.controller.ts` | Create | GET/PUT/POST endpoints for theme | | `helix-engage-server/src/config/theme.service.ts` | Create | Read/write/validate/backup theme JSON | | `helix-engage-server/src/config/theme.defaults.ts` | Create | Default Global Hospital theme constant | | `helix-engage-server/src/config/config.module.ts` | Create | NestJS module for theme | | `helix-engage-server/src/app.module.ts` | Modify | Import ConfigThemeModule | | `helix-engage-server/data/theme.json` | Create | Default theme file | | `helix-engage/src/providers/theme-token-provider.tsx` | Create | Fetch theme, inject CSS vars, expose context | | `helix-engage/src/main.tsx` | Modify | Wrap app with ThemeTokenProvider | | `helix-engage/src/pages/login.tsx` | Modify | Consume tokens instead of hardcoded strings | | `helix-engage/src/components/layout/sidebar.tsx` | Modify | Consume tokens for title/subtitle | | `helix-engage/src/components/call-desk/ai-chat-panel.tsx` | Modify | Consume tokens for quick actions | | `helix-engage/src/pages/branding-settings.tsx` | Create | Branding tab in settings for admins | | `helix-engage/src/main.tsx` | Modify | Add branding settings route | --- ### Task 1: Default Theme Constant + Theme Service (Sidecar) **Files:** - Create: `helix-engage-server/src/config/theme.defaults.ts` - Create: `helix-engage-server/src/config/theme.service.ts` - [ ] **Step 1: Create theme.defaults.ts** ```typescript // src/config/theme.defaults.ts export type ThemeConfig = { brand: { name: string; hospitalName: string; logo: string; favicon: string; }; colors: { brand: Record; }; typography: { body: string; display: string; }; login: { title: string; subtitle: string; showGoogleSignIn: boolean; showForgotPassword: boolean; poweredBy: { label: string; url: string }; }; sidebar: { title: string; subtitle: string; }; ai: { quickActions: Array<{ label: string; prompt: string }>; }; }; export const DEFAULT_THEME: ThemeConfig = { brand: { name: 'Helix Engage', hospitalName: 'Global Hospital', logo: '/helix-logo.png', favicon: '/favicon.ico', }, colors: { brand: { '25': 'rgb(239 246 255)', '50': 'rgb(219 234 254)', '100': 'rgb(191 219 254)', '200': 'rgb(147 197 253)', '300': 'rgb(96 165 250)', '400': 'rgb(59 130 246)', '500': 'rgb(37 99 235)', '600': 'rgb(29 78 216)', '700': 'rgb(30 64 175)', '800': 'rgb(30 58 138)', '900': 'rgb(23 37 84)', '950': 'rgb(15 23 42)', }, }, typography: { body: "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", display: "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", }, login: { title: 'Sign in to Helix Engage', subtitle: 'Global Hospital', showGoogleSignIn: true, showForgotPassword: true, poweredBy: { label: 'Powered by F0rty2.ai', url: 'https://f0rty2.ai' }, }, sidebar: { title: 'Helix Engage', subtitle: 'Global Hospital · {role}', }, ai: { quickActions: [ { label: 'Doctor availability', prompt: 'What doctors are available and what are their visiting hours?' }, { label: 'Clinic timings', prompt: 'What are the clinic locations and timings?' }, { label: 'Patient history', prompt: "Can you summarize this patient's history?" }, { label: 'Treatment packages', prompt: 'What treatment packages are available?' }, ], }, }; ``` - [ ] **Step 2: Create theme.service.ts** ```typescript // src/config/theme.service.ts import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'fs'; import { join, dirname } from 'path'; import { DEFAULT_THEME, type ThemeConfig } from './theme.defaults'; const THEME_PATH = join(process.cwd(), 'data', 'theme.json'); const BACKUP_DIR = join(process.cwd(), 'data', 'theme-backups'); @Injectable() export class ThemeService implements OnModuleInit { private readonly logger = new Logger(ThemeService.name); private cached: ThemeConfig | null = null; onModuleInit() { this.load(); } getTheme(): ThemeConfig { if (this.cached) return this.cached; return this.load(); } updateTheme(updates: Partial): ThemeConfig { const current = this.getTheme(); // Deep merge const merged: ThemeConfig = { brand: { ...current.brand, ...updates.brand }, colors: { brand: { ...current.colors.brand, ...updates.colors?.brand }, }, typography: { ...current.typography, ...updates.typography }, login: { ...current.login, ...updates.login, poweredBy: { ...current.login.poweredBy, ...updates.login?.poweredBy } }, sidebar: { ...current.sidebar, ...updates.sidebar }, ai: { quickActions: updates.ai?.quickActions ?? current.ai.quickActions, }, }; // Backup current this.backup(); // Save const dir = dirname(THEME_PATH); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); writeFileSync(THEME_PATH, JSON.stringify(merged, null, 2), 'utf8'); this.cached = merged; this.logger.log('Theme updated and saved'); return merged; } resetTheme(): ThemeConfig { this.backup(); const dir = dirname(THEME_PATH); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); writeFileSync(THEME_PATH, JSON.stringify(DEFAULT_THEME, null, 2), 'utf8'); this.cached = DEFAULT_THEME; this.logger.log('Theme reset to defaults'); return DEFAULT_THEME; } private load(): ThemeConfig { try { if (existsSync(THEME_PATH)) { const raw = readFileSync(THEME_PATH, 'utf8'); const parsed = JSON.parse(raw); // Merge with defaults to fill missing fields this.cached = { brand: { ...DEFAULT_THEME.brand, ...parsed.brand }, colors: { brand: { ...DEFAULT_THEME.colors.brand, ...parsed.colors?.brand } }, typography: { ...DEFAULT_THEME.typography, ...parsed.typography }, login: { ...DEFAULT_THEME.login, ...parsed.login, poweredBy: { ...DEFAULT_THEME.login.poweredBy, ...parsed.login?.poweredBy } }, sidebar: { ...DEFAULT_THEME.sidebar, ...parsed.sidebar }, ai: { quickActions: parsed.ai?.quickActions ?? DEFAULT_THEME.ai.quickActions }, }; this.logger.log('Theme loaded from file'); return this.cached; } } catch (err) { this.logger.warn(`Failed to load theme: ${err}`); } this.cached = DEFAULT_THEME; this.logger.log('Using default theme'); return DEFAULT_THEME; } private backup() { try { if (!existsSync(THEME_PATH)) return; if (!existsSync(BACKUP_DIR)) mkdirSync(BACKUP_DIR, { recursive: true }); const ts = new Date().toISOString().replace(/[:.]/g, '-'); copyFileSync(THEME_PATH, join(BACKUP_DIR, `theme-${ts}.json`)); } catch (err) { this.logger.warn(`Backup failed: ${err}`); } } } ``` - [ ] **Step 3: Commit** ```bash cd helix-engage-server git add src/config/theme.defaults.ts src/config/theme.service.ts git commit -m "feat: theme service — read/write/backup theme JSON" ``` --- ### Task 2: Theme Controller + Module (Sidecar) **Files:** - Create: `helix-engage-server/src/config/theme.controller.ts` - Create: `helix-engage-server/src/config/config.module.ts` - Modify: `helix-engage-server/src/app.module.ts` - [ ] **Step 1: Create theme.controller.ts** ```typescript // src/config/theme.controller.ts import { Controller, Get, Put, Post, Body, Logger } from '@nestjs/common'; import { ThemeService } from './theme.service'; import type { ThemeConfig } from './theme.defaults'; @Controller('api/config') export class ThemeController { private readonly logger = new Logger(ThemeController.name); constructor(private readonly theme: ThemeService) {} @Get('theme') getTheme() { return this.theme.getTheme(); } @Put('theme') updateTheme(@Body() body: Partial) { this.logger.log('Theme update request'); return this.theme.updateTheme(body); } @Post('theme/reset') resetTheme() { this.logger.log('Theme reset request'); return this.theme.resetTheme(); } } ``` - [ ] **Step 2: Create config.module.ts** ```typescript // src/config/config.module.ts // Named ConfigThemeModule to avoid conflict with NestJS ConfigModule import { Module } from '@nestjs/common'; import { ThemeController } from './theme.controller'; import { ThemeService } from './theme.service'; @Module({ controllers: [ThemeController], providers: [ThemeService], exports: [ThemeService], }) export class ConfigThemeModule {} ``` - [ ] **Step 3: Register in app.module.ts** Add import at top: ```typescript import { ConfigThemeModule } from './config/config.module'; ``` Add to imports array: ```typescript ConfigThemeModule, ``` - [ ] **Step 4: Build and verify** ```bash cd helix-engage-server && npm run build ``` - [ ] **Step 5: Commit** ```bash git add src/config/ src/app.module.ts git commit -m "feat: theme REST API — GET/PUT/POST endpoints" ``` --- ### Task 3: ThemeTokenProvider (Frontend) **Files:** - Create: `helix-engage/src/providers/theme-token-provider.tsx` - Modify: `helix-engage/src/main.tsx` - [ ] **Step 1: Create theme-token-provider.tsx** ```typescript // src/providers/theme-token-provider.tsx import type { ReactNode } from 'react'; import { createContext, useContext, useEffect, useState, useCallback } from 'react'; const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100'; export type ThemeTokens = { brand: { name: string; hospitalName: string; logo: string; favicon: string }; colors: { brand: Record }; typography: { body: string; display: string }; login: { title: string; subtitle: string; showGoogleSignIn: boolean; showForgotPassword: boolean; poweredBy: { label: string; url: string } }; sidebar: { title: string; subtitle: string }; ai: { quickActions: Array<{ label: string; prompt: string }> }; }; const DEFAULT_TOKENS: ThemeTokens = { brand: { name: 'Helix Engage', hospitalName: 'Global Hospital', logo: '/helix-logo.png', favicon: '/favicon.ico' }, colors: { brand: {} }, typography: { body: '', display: '' }, login: { title: 'Sign in to Helix Engage', subtitle: 'Global Hospital', showGoogleSignIn: true, showForgotPassword: true, poweredBy: { label: 'Powered by F0rty2.ai', url: 'https://f0rty2.ai' } }, sidebar: { title: 'Helix Engage', subtitle: 'Global Hospital · {role}' }, ai: { quickActions: [ { label: 'Doctor availability', prompt: 'What doctors are available and what are their visiting hours?' }, { label: 'Clinic timings', prompt: 'What are the clinic locations and timings?' }, { label: 'Patient history', prompt: "Can you summarize this patient's history?" }, { label: 'Treatment packages', prompt: 'What treatment packages are available?' }, ] }, }; type ThemeTokenContextType = { tokens: ThemeTokens; refresh: () => Promise; }; const ThemeTokenContext = createContext({ tokens: DEFAULT_TOKENS, refresh: async () => {} }); export const useThemeTokens = () => useContext(ThemeTokenContext); const applyColorTokens = (brandColors: Record) => { const root = document.documentElement; for (const [stop, value] of Object.entries(brandColors)) { root.style.setProperty(`--color-brand-${stop}`, value); } }; const applyTypographyTokens = (typography: { body: string; display: string }) => { const root = document.documentElement; if (typography.body) root.style.setProperty('--font-body', typography.body); if (typography.display) root.style.setProperty('--font-display', typography.display); }; export const ThemeTokenProvider = ({ children }: { children: ReactNode }) => { const [tokens, setTokens] = useState(DEFAULT_TOKENS); const fetchTheme = useCallback(async () => { try { const res = await fetch(`${API_URL}/api/config/theme`); if (res.ok) { const data: ThemeTokens = await res.json(); setTokens(data); if (data.colors?.brand && Object.keys(data.colors.brand).length > 0) { applyColorTokens(data.colors.brand); } if (data.typography) { applyTypographyTokens(data.typography); } } } catch { // Use defaults silently } }, []); useEffect(() => { fetchTheme(); }, [fetchTheme]); return ( {children} ); }; ``` - [ ] **Step 2: Wrap app in main.tsx** In `main.tsx`, add import: ```typescript import { ThemeTokenProvider } from '@/providers/theme-token-provider'; ``` Wrap inside `ThemeProvider`: ```tsx ... ``` - [ ] **Step 3: Build and verify** ```bash npx tsc --noEmit ``` - [ ] **Step 4: Commit** ```bash git add src/providers/theme-token-provider.tsx src/main.tsx git commit -m "feat: ThemeTokenProvider — fetch theme, inject CSS variables" ``` --- ### Task 4: Consume Tokens in Login Page **Files:** - Modify: `helix-engage/src/pages/login.tsx` - [ ] **Step 1: Replace hardcoded values** Import `useThemeTokens`: ```typescript import { useThemeTokens } from '@/providers/theme-token-provider'; ``` Inside the component: ```typescript const { tokens } = useThemeTokens(); ``` Replace hardcoded strings: - `src="/helix-logo.png"` → `src={tokens.brand.logo}` - `"Sign in to Helix Engage"` → `{tokens.login.title}` - `"Global Hospital"` → `{tokens.login.subtitle}` - Google sign-in section: wrap with `{tokens.login.showGoogleSignIn && (...)}` - Forgot password: wrap with `{tokens.login.showForgotPassword && (...)}` - Powered by: `tokens.login.poweredBy.label` and `tokens.login.poweredBy.url` - [ ] **Step 2: Commit** ```bash git add src/pages/login.tsx git commit -m "feat: login page consumes theme tokens" ``` --- ### Task 5: Consume Tokens in Sidebar + AI Chat **Files:** - Modify: `helix-engage/src/components/layout/sidebar.tsx` - Modify: `helix-engage/src/components/call-desk/ai-chat-panel.tsx` - [ ] **Step 1: Update sidebar.tsx** Import `useThemeTokens` and replace: - Line 167: `"Helix Engage"` → `{tokens.sidebar.title}` - Line 168: `"Global Hospital · {getRoleSubtitle(user.role)}"` → `{tokens.sidebar.subtitle.replace('{role}', getRoleSubtitle(user.role))}` - Line 164: favicon src → `tokens.brand.logo` - [ ] **Step 2: Update ai-chat-panel.tsx** Import `useThemeTokens` and replace: - Lines 21-25: hardcoded `QUICK_ACTIONS` array → `tokens.ai.quickActions` Move `QUICK_ACTIONS` usage inside the component: ```typescript const { tokens } = useThemeTokens(); const quickActions = tokens.ai.quickActions; ``` - [ ] **Step 3: Commit** ```bash git add src/components/layout/sidebar.tsx src/components/call-desk/ai-chat-panel.tsx git commit -m "feat: sidebar + AI chat consume theme tokens" ``` --- ### Task 6: Branding Settings Page (Frontend) **Files:** - Create: `helix-engage/src/pages/branding-settings.tsx` - Modify: `helix-engage/src/main.tsx` (add route) - Modify: `helix-engage/src/components/layout/sidebar.tsx` (add nav item) - [ ] **Step 1: Create branding-settings.tsx** The page has 6 collapsible sections matching the spec. Uses Untitled UI `Input`, `TextArea`, `Checkbox`, `Button` components. On save, PUTs to `/api/config/theme` and calls `refresh()` from `useThemeTokens()`. Key patterns: - Fetch current theme on mount via `GET /api/config/theme` - Local state mirrors the theme JSON structure - Each section is a collapsible card - Color section: 12 text inputs for hex/rgb values with colored preview dots - Save button calls `PUT /api/config/theme` with the full state - Reset button calls `POST /api/config/theme/reset` - After save/reset, call `refresh()` to re-apply CSS variables immediately - [ ] **Step 2: Add route in main.tsx** ```typescript import { BrandingSettingsPage } from '@/pages/branding-settings'; ``` Add route: ```tsx } /> ``` - [ ] **Step 3: Add nav item in sidebar.tsx** Under the Configuration section (near Rules Engine), add "Branding" link for admin role only. - [ ] **Step 4: Build and verify** ```bash npx tsc --noEmit ``` - [ ] **Step 5: Commit** ```bash git add src/pages/branding-settings.tsx src/main.tsx src/components/layout/sidebar.tsx git commit -m "feat: branding settings page — theme editor for supervisors" ``` --- ### Task 7: Default Theme File + Build Verification **Files:** - Create: `helix-engage-server/data/theme.json` - [ ] **Step 1: Create default theme.json** Copy the `DEFAULT_THEME` object as JSON to `data/theme.json`. - [ ] **Step 2: Build both projects** ```bash cd helix-engage-server && npm run build cd ../helix-engage && npm run build ``` - [ ] **Step 3: Commit all** ```bash git add data/theme.json git commit -m "chore: default theme.json file" ``` --- ## Execution Notes - ThemeTokenProvider fetches before login — the endpoint is public (no auth) - CSS variable override on `` has higher specificity than the `@theme` block in `theme.css` - `tokens.sidebar.subtitle` supports `{role}` placeholder — replaced at render time by the sidebar component - The branding settings page is admin-only but the theme endpoint itself is unauthenticated (GET) — PUT requires auth - If the sidecar is unreachable, the frontend silently falls back to hardcoded defaults