# 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 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 `` 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 ``, 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)