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>
This commit is contained in:
2026-04-02 15:50:36 +05:30
parent c5d5e9c4f9
commit afd0829dc6
10 changed files with 1358 additions and 28 deletions

View File

@@ -32,6 +32,7 @@ import type { NavItemType } from "@/components/application/app-navigation/config
import { Avatar } from "@/components/base/avatar/avatar";
import { useAuth } from "@/providers/auth-provider";
import { useAgentState } from "@/hooks/use-agent-state";
import { useThemeTokens } from "@/providers/theme-token-provider";
import { sidebarCollapsedAtom } from "@/state/sidebar-state";
import { cx } from "@/utils/cx";
@@ -80,6 +81,7 @@ const getNavSections = (role: string): NavSection[] => {
]},
{ label: 'Configuration', items: [
{ label: 'Rules Engine', href: '/rules', icon: IconSlidersUp },
{ label: 'Branding', href: '/branding', icon: IconGear },
]},
{ label: 'Admin', items: [
{ label: 'Settings', href: '/settings', icon: IconGear },
@@ -128,6 +130,7 @@ interface SidebarProps {
export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
const { logout, user } = useAuth();
const { tokens } = useThemeTokens();
const navigate = useNavigate();
const [collapsed, setCollapsed] = useAtom(sidebarCollapsedAtom);
const agentConfig = typeof window !== 'undefined' ? localStorage.getItem('helix_agent_config') : null;
@@ -161,11 +164,11 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
{/* Logo + collapse toggle */}
<div className={cx("flex items-center gap-2", collapsed ? "justify-center px-2" : "justify-between px-4 lg:px-5")}>
{collapsed ? (
<img src="/favicon-32.png" alt="Helix Engage" className="size-8 rounded-lg shrink-0" />
<img src={tokens.brand.logo} alt={tokens.brand.name} className="size-8 rounded-lg shrink-0" />
) : (
<div className="flex flex-col gap-1">
<span className="text-md font-bold text-white">Helix Engage</span>
<span className="text-xs text-white opacity-70">Global Hospital &middot; {getRoleSubtitle(user.role)}</span>
<span className="text-md font-bold text-white">{tokens.sidebar.title}</span>
<span className="text-xs text-white opacity-70">{tokens.sidebar.subtitle.replace('{role}', getRoleSubtitle(user.role))}</span>
</div>
)}
<button
@@ -204,7 +207,7 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
className={cx(
"flex size-10 items-center justify-center rounded-lg transition duration-100 ease-linear",
item.href === activeUrl
? "bg-active text-fg-brand-primary"
? "bg-sidebar-active text-sidebar-active"
: "text-fg-quaternary hover:bg-(--hover-bg) hover:text-(--hover-text)",
)}
>