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