Files
helix-engage/docs/superpowers/specs/2026-04-02-design-tokens-design.md
saridsa2 afd0829dc6 feat: design tokens — multi-hospital theming system
Backend (sidecar):
- ThemeService: read/write/backup/reset theme.json with versioning
- ThemeController: GET/PUT/POST /api/config/theme endpoints
- ConfigThemeModule registered in app

Frontend:
- ThemeTokenProvider: fetches theme, injects CSS variables on <html>
- Login page: logo, title, subtitle, Google/forgot toggles from tokens
- Sidebar: title, subtitle, active highlight from brand color scale
- AI chat: quick actions from tokens
- Branding settings page: 2-column layout, file upload for logo/favicon,
  single color picker with palette generation, font dropdowns, presets,
  pinned footer, versioning

Theme CSS:
- Sidebar active/hover text now references --color-brand-400

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:50:36 +05:30

7.5 KiB

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

{
  "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

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)