# Design Tokens — Multi-Hospital Theming

**Date**: 2026-04-02
**Status**: Draft

---

## Overview

A JSON-driven design token system that allows each hospital customer to rebrand Helix Engage by providing a single JSON configuration file. The JSON is served by the sidecar, consumed by the frontend at runtime via a React provider that injects CSS custom properties.

---

## Architecture

```
Sidecar (helix-engage-server)
  └─ GET /api/config/theme → returns hospital theme JSON
  └─ theme stored as JSON file at data/theme.json (editable, hot-reloadable)

Frontend (helix-engage)
  └─ ThemeTokenProvider (wraps app) → fetches theme JSON on mount
  └─ Injects CSS custom properties on <html> element
  └─ Exposes useThemeTokens() hook for content tokens (logo, name, text)
  └─ Components read colors via existing Tailwind classes (no changes needed)
```

---

## Theme JSON Schema

```json
{
  "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, Inter, -apple-system, sans-serif",
    "display": "General Sans, Inter, -apple-system, 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 · Call Center Agent"
  },
  "ai": {
    "quickActions": [
      { "label": "Doctor availability", "prompt": "What doctors are available?" },
      { "label": "Clinic timings", "prompt": "What are the clinic timings?" },
      { "label": "Patient history", "prompt": "Summarize this patient's history" },
      { "label": "Treatment packages", "prompt": "What packages are available?" }
    ]
  }
}
```

---

## Sidecar Implementation

### Endpoints

```
GET  /api/config/theme        — Returns theme JSON (no auth, public — needed before login)
PUT  /api/config/theme        — Updates theme JSON (auth required, admin only)
POST /api/config/theme/reset  — Resets to default theme (auth required, admin only)
```

- Stored in `data/theme.json` on the sidecar filesystem
- Cached in memory, invalidated on PUT
- If file doesn't exist, returns a hardcoded default (Global Hospital theme)
- PUT validates the JSON schema before saving
- PUT also writes a timestamped backup to `data/theme-backups/`

### Files

- `helix-engage-server/src/config/theme.controller.ts` — REST endpoints
- `helix-engage-server/src/config/theme.service.ts` — read/write/validate/backup logic

---

## Frontend Implementation

### ThemeTokenProvider

New provider wrapping the app in `main.tsx`. Responsibilities:

1. **Fetch** `GET /api/config/theme` on mount (before rendering anything)
2. **Inject CSS variables** on `document.documentElement.style`:
   - `--color-brand-25` through `--color-brand-950` (overrides the Untitled UI brand scale)
   - `--font-body`, `--font-display` (overrides typography)
3. **Store content tokens** in React context (brand name, logo, login text, sidebar text, quick actions)
4. **Expose** `useThemeTokens()` hook for components to read content tokens

### File: `src/providers/theme-token-provider.tsx`

```tsx
type ThemeTokens = {
  brand: { name: string; hospitalName: string; logo: string; favicon: 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 }> };
};
```

### CSS Variable Injection

The provider maps `colors.brand.*` to CSS custom properties that Untitled UI already reads:

```
theme.colors.brand["500"] → document.documentElement.style.setProperty('--color-brand-500', value)
```

Since `theme.css` defines `--color-brand-500: var(--color-blue-500)`, setting `--color-brand-500` directly on `<html>` overrides the alias with higher specificity.

Typography:
```
theme.typography.body → --font-body
theme.typography.display → --font-display
```

### Consumers

Components that currently hardcode hospital-specific content:

| Component | Current hardcoded value | Token path |
|---|---|---|
| `login.tsx` line 93 | "Sign in to Helix Engage" | `login.title` |
| `login.tsx` line 94 | "Global Hospital" | `login.subtitle` |
| `login.tsx` line 92 | `/helix-logo.png` | `brand.logo` |
| `login.tsx` line 181 | "Powered by F0rty2.ai" | `login.poweredBy.label` |
| `sidebar.tsx` | "Helix Engage" | `sidebar.title` |
| `sidebar.tsx` | "Global Hospital · Call Center Agent" | `sidebar.subtitle` |
| `ai-chat-panel.tsx` lines 21-25 | Quick action prompts | `ai.quickActions` |
| `app-shell.tsx` | favicon | `brand.favicon` |

---

## Default Theme

If the sidecar returns no theme (endpoint down, file missing), the frontend uses a hardcoded default matching the current Global Hospital branding. This ensures the app works without a sidecar theme endpoint.

---

## Settings UI (Supervisor)

New tab in the Settings page: **Branding**. Visible only to admin role.

### Sections

**1. Brand Identity**
- Hospital name (text input)
- App name (text input)
- Logo upload (file input → stores URL)
- Favicon upload

**2. Brand Colors**
- 12 color swatches (25 through 950) with hex/rgb input per swatch
- Live preview strip showing the full scale
- "Reset to default" button per section

**3. Typography**
- Body font family (text input with common font suggestions)
- Display font family (text input)

**4. Login Page**
- Title text
- Subtitle text
- Show Google sign-in (toggle)
- Show forgot password (toggle)
- Powered-by label + URL

**5. Sidebar**
- Title text
- Subtitle template (supports `{role}` placeholder — "Global Hospital · {role}")

**6. AI Quick Actions**
- Editable list of label + prompt pairs
- Add / remove / reorder

### Save Flow
- Supervisor edits fields → clicks Save → `PUT /api/config/theme` → sidecar validates + saves + backs up
- Frontend re-fetches theme on save → CSS variables update → page reflects changes immediately (no reload needed)

### File
`src/pages/settings.tsx` — new "Branding" tab (or `src/pages/branding-settings.tsx` if settings page is already complex)

---

## What This Does NOT Change

- **Tailwind classes** — no changes. Components continue using `text-brand-secondary`, `bg-brand-solid`, etc. The CSS variables they reference are overridden at runtime.
- **Component structure** — no layout changes. Only content strings and colors change.
- **Untitled UI theme.css** — not modified. The provider overrides are applied inline on `<html>`, higher specificity.

---

## Scope

**In scope:**
- Sidecar theme endpoint + JSON file
- ThemeTokenProvider + useThemeTokens hook
- Login page consuming tokens
- Sidebar consuming tokens
- AI quick actions consuming tokens
- Brand color override via CSS variables
- Typography override via CSS variables

**Out of scope:**
- Dark mode customization (inherits from Untitled UI)
- Per-role theming
- Logo upload to cloud storage (uses URL for now — can be a data URI or hosted path)
