"use client"; import React, { createContext, useContext, useMemo } from "react"; import { hexToOklch, getContrastText, generatePrimaryShades } from "./colors"; export interface ThemeProviderProps { children: React.ReactNode; /** Tenant primary brand color (hex). Default: WealthX green */ primary?: string; /** Tenant secondary brand color (hex). Default: WealthX dark navy */ secondary?: string; /** Tenant font family override. Default: Figtree */ fontFamily?: string; /** Disable CSS variable injection (e.g. for SSR with static CSS) */ injectCssVariables?: boolean; } /** * React context that stores the computed theme CSS variables. * * Portal-rendered components (Dialog, AlertDialog, Popover, Tooltip, etc.) * escape the DOM subtree of the ThemeProvider wrapper \, so they lose * the scoped CSS custom properties set via inline styles. Components that * render inside a portal consume this context and re-apply the variables * on their outermost portal element, restoring the theme cascade. */ const ThemeVarsContext = createContext>({}); /** * Returns the theme CSS variables from the nearest ThemeProvider. * Portal-based components use this to re-apply scoped theme vars * on content rendered outside the ThemeProvider's DOM subtree. */ export function useThemeVars(): Record { return useContext(ThemeVarsContext); } const DEFAULT_PRIMARY = "#33FF99"; const DEFAULT_SECONDARY = "#162029"; const DEFAULT_FONT = '"Figtree", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; /** * WealthX ThemeProvider — injects tenant brand colors as scoped CSS variables. * * How it works: * 1. Takes primary/secondary hex colors from tenant config * 2. Converts to oklch() (matching globals.css format) * 3. Auto-computes WCAG contrast text for each via WCAG luminance * 4. Sets CSS custom properties as inline styles on a wrapper \, * scoped to this subtree (inherited by children via CSS cascade). * This allows multiple ThemeProviders on the same page with different colors. * * Usage: * \ * \ * \ */ export function ThemeProvider({ children, primary = DEFAULT_PRIMARY, secondary = DEFAULT_SECONDARY, fontFamily = DEFAULT_FONT, injectCssVariables = true, }: ThemeProviderProps): React.ReactElement { const vars = useMemo(() => { if (!injectCssVariables) return {} as Record; const primaryOklch = hexToOklch(primary); const primaryFgOklch = hexToOklch(getContrastText(primary)); const secondaryOklch = hexToOklch(secondary); const secondaryFgOklch = hexToOklch(getContrastText(secondary)); return { // Brand primary — used by buttons, links, focus rings, active states "--primary": primaryOklch, "--primary-foreground": primaryFgOklch, "--ring": primaryOklch, // Sidebar uses primary for active navigation "--sidebar-primary": primaryOklch, "--sidebar-primary-foreground": primaryFgOklch, // Brand secondary — tenant navy used by Secondary button variant // NOTE: --secondary is reserved for paper/surface (#F5F8FA), so we use --brand-secondary "--brand-secondary": secondaryOklch, "--brand-secondary-foreground": secondaryFgOklch, // Font family — applies to all components + typography utility classes "--font-sans": fontFamily, "--font-family-sans": fontFamily, // Legacy compat (used by existing WealthX apps) "--theme-primary": primary, "--theme-secondary": secondary, // Primary shade palette (50–950) — Tailwind-style, tenant-adaptive. // Defined here so they react to tenant theme switches. // Referenced as var(--primary-50) … var(--primary-950) in components. ...generatePrimaryShades(primary), // Pipeline stage palette — maps stage slots to primary shades. // stage-1 = darkest visible, stage-5 = lightest visible on white bg. "--color-stage-1": `var(--primary-600)`, "--color-stage-2": `var(--primary-400)`, "--color-stage-3": `var(--primary-300)`, "--color-stage-4": `var(--primary-200)`, "--color-stage-5": `var(--primary-100)`, // WCAG-computed contrast text for any primary-shaded background "--color-stage-fg": primaryFgOklch, }; }, [primary, secondary, fontFamily, injectCssVariables]); return (
{children}
); } /** * Build CSS variables object for SSR or static contexts. * Returns a flat Record\ that can be serialized to a \ tag. */ export function buildCssVariables(options: { primary?: string; secondary?: string; fontFamily?: string; }): Record { const primary = options.primary ?? DEFAULT_PRIMARY; const secondary = options.secondary ?? DEFAULT_SECONDARY; const fontFamily = options.fontFamily ?? DEFAULT_FONT; return { "--primary": hexToOklch(primary), "--primary-foreground": hexToOklch(getContrastText(primary)), "--ring": hexToOklch(primary), "--sidebar-primary": hexToOklch(primary), "--sidebar-primary-foreground": hexToOklch(getContrastText(primary)), "--brand-secondary": hexToOklch(secondary), "--brand-secondary-foreground": hexToOklch(getContrastText(secondary)), "--font-sans": fontFamily, "--font-family-sans": fontFamily, "--theme-primary": primary, "--theme-secondary": secondary, }; }