# 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 `<html>`, 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<string, string>;
    };
    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>): 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<ThemeConfig>) {
        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<string, string> };
    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<void>;
};

const ThemeTokenContext = createContext<ThemeTokenContextType>({ tokens: DEFAULT_TOKENS, refresh: async () => {} });

export const useThemeTokens = () => useContext(ThemeTokenContext);

const applyColorTokens = (brandColors: Record<string, string>) => {
    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<ThemeTokens>(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 (
        <ThemeTokenContext.Provider value={{ tokens, refresh: fetchTheme }}>
            {children}
        </ThemeTokenContext.Provider>
    );
};
```

- [ ] **Step 2: Wrap app in main.tsx**

In `main.tsx`, add import:
```typescript
import { ThemeTokenProvider } from '@/providers/theme-token-provider';
```

Wrap inside `ThemeProvider`:
```tsx
<ThemeProvider>
    <ThemeTokenProvider>
        <AuthProvider>
            ...
        </AuthProvider>
    </ThemeTokenProvider>
</ThemeProvider>
```

- [ ] **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
<Route path="/branding" element={<BrandingSettingsPage />} />
```

- [ ] **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 `<html>` 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
