mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 10:23:27 +00:00
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>
88 lines
3.8 KiB
TypeScript
88 lines
3.8 KiB
TypeScript
import type { ReactNode } from 'react';
|
|
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
|
|
const THEME_API_URL = import.meta.env.VITE_THEME_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: {
|
|
'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 \u00b7 {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(`${THEME_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>
|
|
);
|
|
};
|