/* * ============================================================================= * VibeQuant (vquant) — AI-Powered Financial Intelligence Platform * ----------------------------------------------------------------------------- * File: client/src/components/theme-provider.tsx * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * Website: https://www.spboucher.ai * Demo: https://www.vquant.ai * License: MIT (see LICENSE) * * Copyright © 2026 Simon-Pierre Boucher. All rights reserved. * ============================================================================= */ import { createContext, useContext, useEffect, useState } from "react"; type Theme = "dark" | "light"; export type ThemeColor = "blue" | "green" | "purple" | "red" | "orange" | "cyan"; type ThemeProviderProps = { children: React.ReactNode; defaultTheme?: Theme; storageKey?: string; }; type ThemeProviderState = { theme: Theme; setTheme: (theme: Theme) => void; toggleTheme: () => void; themeColor: ThemeColor; setThemeColor: (color: ThemeColor) => void; }; const initialState: ThemeProviderState = { theme: "light", setTheme: () => null, toggleTheme: () => null, themeColor: "orange", setThemeColor: () => null, }; const ThemeProviderContext = createContext(initialState); const themeColorMap: Record = { blue: { hue: 210, sat: 100 }, green: { hue: 142, sat: 76 }, purple: { hue: 255, sat: 85 }, red: { hue: 0, sat: 84 }, orange: { hue: 14, sat: 100 }, cyan: { hue: 199, sat: 89 }, }; export function ThemeProvider({ children, defaultTheme = "light", storageKey = "stock-theme", ...props }: ThemeProviderProps) { const [theme, setThemeState] = useState(() => { try { return (localStorage.getItem(storageKey) as Theme) || defaultTheme; } catch { return defaultTheme; } }); const [themeColor, setThemeColorState] = useState(() => { try { return (localStorage.getItem(`${storageKey}-color-v2`) as ThemeColor) || "orange"; } catch { return "purple"; } }); useEffect(() => { const root = window.document.documentElement; root.classList.remove("light", "dark"); root.classList.add(theme); }, [theme]); useEffect(() => { const root = window.document.documentElement; const colorConfig = themeColorMap[themeColor]; // Update primary color variables if (theme === "light") { root.style.setProperty("--primary", `${colorConfig.hue} ${colorConfig.sat}% 50%`); root.style.setProperty("--ring", `${colorConfig.hue} ${colorConfig.sat}% 50%`); root.style.setProperty("--sidebar-primary", `${colorConfig.hue} ${colorConfig.sat}% 50%`); root.style.setProperty("--sidebar-ring", `${colorConfig.hue} ${colorConfig.sat}% 50%`); } else { root.style.setProperty("--primary", `${colorConfig.hue} ${colorConfig.sat}% 50%`); root.style.setProperty("--ring", `${colorConfig.hue} ${colorConfig.sat}% 60%`); root.style.setProperty("--sidebar-primary", `${colorConfig.hue} ${colorConfig.sat}% 60%`); root.style.setProperty("--sidebar-ring", `${colorConfig.hue} ${colorConfig.sat}% 60%`); } }, [themeColor, theme]); const value = { theme, setTheme: (theme: Theme) => { try { localStorage.setItem(storageKey, theme); setThemeState(theme); } catch (error) { console.error("Failed to save theme:", error); setThemeState(theme); } }, toggleTheme: () => { try { const newTheme = theme === "dark" ? "light" : "dark"; localStorage.setItem(storageKey, newTheme); setThemeState(newTheme); } catch (error) { console.error("Failed to toggle theme:", error); setThemeState(theme === "dark" ? "light" : "dark"); } }, themeColor, setThemeColor: (color: ThemeColor) => { try { localStorage.setItem(`${storageKey}-color-v2`, color); setThemeColorState(color); } catch (error) { console.error("Failed to save theme color:", error); setThemeColorState(color); } }, }; return ( {children} ); } export const useTheme = () => { const context = useContext(ThemeProviderContext); if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider"); return context; };