"use client" import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react" import { useTheme } from "next-themes" import { deriveSecondaryTextColor } from "@/utils/color-utils" export type ColorPaletteType = { name: string value: string textColor: string cssVar: string description: string } type SystemColorValues = { [key: string]: string } type ColorContextType = { primaryColor: string setPrimaryColor: (color: string) => void systemColors: SystemColorValues setSystemColor: (key: string, value: string) => void currentTheme: string | undefined updatePaletteImmediately: (palette: { primary: string backgroundPrimary: { light: string; dark: string } backgroundSecondary: { light: string; dark: string } }) => { primaryColor: string; bgPrimary: string; bgSecondary: string } } /** * A simplified color utility class that handles all color conversions and calculations */ class ColorUtils { /** * Converts a hex color string to RGB values */ static hexToRgb(hex: string): { r: number; g: number; b: number } { // Remove the # if present hex = hex.replace("#", "") // Handle both 3-digit and 6-digit hex formats const r = hex.length === 3 ? Number.parseInt(hex[0] + hex[0], 16) : Number.parseInt(hex.substring(0, 2), 16) const g = hex.length === 3 ? Number.parseInt(hex[1] + hex[1], 16) : Number.parseInt(hex.substring(2, 4), 16) const b = hex.length === 3 ? Number.parseInt(hex[2] + hex[2], 16) : Number.parseInt(hex.substring(4, 6), 16) return { r, g, b } } /** * Converts RGB values to a hex color string */ static rgbToHex(r: number, g: number, b: number): string { return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) } /** * Applies an overlay color to a base color with the specified opacity */ static applyOverlay(baseColor: string, overlayColor: string, opacity: number): string { const base = this.hexToRgb(baseColor) const overlay = this.hexToRgb(overlayColor) // Blend the colors based on opacity const r = Math.round(base.r * (1 - opacity) + overlay.r * opacity) const g = Math.round(base.g * (1 - opacity) + overlay.g * opacity) const b = Math.round(base.b * (1 - opacity) + overlay.b * opacity) return this.rgbToHex(r, g, b) } /** * Calculates the luminance of a color (used for determining text color) */ static calculateLuminance(r: number, g: number, b: number): number { // Using the relative luminance formula from WCAG const normalizedR = r / 255 const normalizedG = g / 255 const normalizedB = b / 255 return 0.2126 * normalizedR + 0.7152 * normalizedG + 0.0722 * normalizedB } /** * Converts RGB values to HSL format */ static rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } { r /= 255 g /= 255 b /= 255 const max = Math.max(r, g, b) const min = Math.min(r, g, b) let h = 0 let s = 0 const l = (max + min) / 2 if (max !== min) { const d = max - min s = l > 0.5 ? d / (2 - max - min) : d / (max + min) switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0) break case g: h = (b - r) / d + 2 break case b: h = (r - g) / d + 4 break } h /= 6 } return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100), } } /** * Determines the appropriate text color (light or dark) based on background color */ static getTextColor(backgroundColor: string, luminanceThreshold = 0.8): string { const { r, g, b } = this.hexToRgb(backgroundColor) const luminance = this.calculateLuminance(r, g, b) return luminance > luminanceThreshold ? "#1a1a1a" : "#ffffff" } /** * Converts a hex color to HSL format */ static hexToHsl(hex: string): { h: number; s: string; l: number } { const { r, g, b } = this.hexToRgb(hex) return this.rgbToHsl(r, g, b) } /** * Converts HSL to RGB */ static hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } { h = h / 360 s = s / 100 l = l / 100 const hue2rgb = (p: number, q: number, t: number) => { if (t < 0) t += 1 if (t > 1) t -= 1 if (t < 1 / 6) return p + (q - p) * 6 * t if (t < 1 / 2) return q if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 return p } let r, g, b if (s === 0) { r = g = b = l // achromatic } else { const q = l < 0.5 ? l * (1 + s) : l + s - l * s const p = 2 * l - q r = hue2rgb(p, q, h + 1 / 3) g = hue2rgb(p, q, h) b = hue2rgb(p, q, h - 1 / 3) } return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255), } } static calculateBorderColor(backgroundPrimary: string, theme: string | undefined): string { const bgColorRgb = ColorUtils.hexToRgb(backgroundPrimary) const bgColorLuminance = ColorUtils.calculateLuminance(bgColorRgb.r, bgColorRgb.g, bgColorRgb.b) let borderColor: string if (theme === "dark") { // Dark theme: lighten the background color slightly const luminanceIncrease = Math.min(0.08, 1 - bgColorLuminance) // Ensure it doesn't exceed 1 const newLuminance = bgColorLuminance + luminanceIncrease borderColor = ColorUtils.hslToHex(0, 0, newLuminance * 100) } else { // Light theme: darken the background color slightly const luminanceDecrease = Math.min(0.08, bgColorLuminance) // Ensure it doesn't go below 0 const newLuminance = bgColorLuminance - luminanceDecrease borderColor = ColorUtils.hslToHex(0, 0, newLuminance * 100) } return borderColor } static hslToHex(h: number, s: number, l: number): string { s /= 100 l /= 100 let c = (1 - Math.abs(2 * l - 1)) * s, x = c * (1 - Math.abs(((h / 60) % 2) - 1)), m = l - c / 2, r = 0, g = 0, b = 0 if (0 <= h && h < 60) { r = c g = x b = 0 } else if (60 <= h && h < 120) { r = x g = c b = 0 } else if (120 <= h && h < 180) { r = 0 g = c b = x } else if (180 <= h && h < 240) { r = 0 g = x b = c } else if (240 <= h && h < 300) { r = x g = 0 b = c } else if (300 <= h && h < 360) { r = c g = 0 b = x } // Normalize to 0-255 r = Math.round((r + m) * 255) g = Math.round((g + m) * 255) b = Math.round((b + m) * 255) return "#" + r.toString(16).padStart(2, "0") + g.toString(16).padStart(2, "0") + b.toString(16).padStart(2, "0") } } const ColorContext = createContext(undefined) // Default system colors - updated to match Stripe's aesthetic const defaultSystemColors = { backgroundPrimary: { light: "#ffffff", dark: "#0a0a0a" }, // Changed from pure black backgroundSecondary: { light: "#f6f9fc", dark: "#1a3a5f" }, } /** * Provider component for color theming */ export function ColorProvider({ children }: { children: ReactNode }) { const { theme } = useTheme() const [primaryColor, setPrimaryColorState] = useState("#3b82f6") // Base blue const [systemColors, setSystemColors] = useState({}) const [mounted, setMounted] = useState(false) // Set mounted state after hydration useEffect(() => { setMounted(true) }, []) // Initialize system colors based on theme useEffect(() => { if (mounted && theme) { setSystemColors({ backgroundPrimary: theme === "dark" ? defaultSystemColors.backgroundPrimary.dark : defaultSystemColors.backgroundPrimary.light, backgroundSecondary: theme === "dark" ? defaultSystemColors.backgroundSecondary.dark : defaultSystemColors.backgroundSecondary.light, }) } }, [mounted, theme]) /** * Calculates the overlay background color based on the background-primary color and theme */ const calculateOverlayBackground = useCallback( (backgroundPrimary: string): string => { // Apply 2% white overlay in both light and dark mode const overlayColor = "#ffffff" const overlayOpacity = 0.02 // 2% opacity return ColorUtils.applyOverlay(backgroundPrimary, overlayColor, overlayOpacity) }, [theme], ) /** * Updates CSS variables based on the selected color and theme */ const updateCssVariables = useCallback( (color: string, sysColors: SystemColorValues) => { if (typeof window === "undefined") return const root = document.documentElement // Set primary color as HSL const colorHsl = ColorUtils.hexToHsl(color) root.style.setProperty("--primary", `${colorHsl.h} ${colorHsl.s}% ${colorHsl.l}%`) root.style.setProperty("--ring", `${colorHsl.h} ${colorHsl.s}% ${colorHsl.l}%`) // Generate primary color shades const primary50 = `${colorHsl.h} ${Math.min(colorHsl.s + 10, 100)}% ${Math.min(colorHsl.l + 45, 95)}%` const primary100 = `${colorHsl.h} ${Math.min(colorHsl.s + 5, 100)}% ${Math.min(colorHsl.l + 35, 90)}%` const primary200 = `${colorHsl.h} ${colorHsl.s}% ${Math.min(colorHsl.l + 25, 85)}%` const primary300 = `${colorHsl.h} ${colorHsl.s}% ${Math.min(colorHsl.l + 15, 80)}%` const primary400 = `${colorHsl.h} ${colorHsl.s}% ${Math.min(colorHsl.l + 5, 75)}%` const primary500 = `${colorHsl.h} ${colorHsl.s}% ${colorHsl.l}%` // Base color const primary600 = `${colorHsl.h} ${colorHsl.s}% ${Math.max(colorHsl.l - 10, 25)}%` const primary700 = `${colorHsl.h} ${colorHsl.s}% ${Math.max(colorHsl.l - 20, 20)}%` const primary800 = `${colorHsl.h} ${colorHsl.s}% ${Math.max(colorHsl.l - 30, 15)}%` const primary900 = `${colorHsl.h} ${colorHsl.s}% ${Math.max(colorHsl.l - 40, 10)}%` // Set primary color shade variables root.style.setProperty("--primary-50", primary50) root.style.setProperty("--primary-100", primary100) root.style.setProperty("--primary-200", primary200) root.style.setProperty("--primary-300", primary300) root.style.setProperty("--primary-400", primary400) root.style.setProperty("--primary-500", primary500) root.style.setProperty("--primary-600", primary600) root.style.setProperty("--primary-700", primary700) root.style.setProperty("--primary-800", primary800) root.style.setProperty("--primary-900", primary900) // Set text color based on luminance const textColor = ColorUtils.getTextColor(color, 0.8) root.style.setProperty("--primary-text", textColor) // Set theme-specific colors const themeColors = { backgroundPrimary: sysColors.backgroundPrimary || (theme === "dark" ? defaultSystemColors.backgroundPrimary.dark : defaultSystemColors.backgroundPrimary.light), backgroundSecondary: sysColors.backgroundSecondary || (theme === "dark" ? defaultSystemColors.backgroundSecondary.dark : defaultSystemColors.backgroundSecondary.light), textPrimary: theme === "dark" ? "#ffffff" : "#1b1b1b", foregroundColor: theme === "dark" ? "#707173" : "#cdcdce", } // Calculate secondary text color using luminance-based approach const secondaryTextColor = deriveSecondaryTextColor(themeColors.textPrimary, themeColors.backgroundPrimary) // Apply theme colors Object.entries(themeColors).forEach(([key, value]) => { const cssVar = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}` root.style.setProperty(cssVar, value) }) // Set the calculated secondary text color root.style.setProperty("--text-secondary", secondaryTextColor) // Calculate and set overlay background colors const overlayBackground = calculateOverlayBackground(themeColors.backgroundPrimary) root.style.setProperty("--input-background", overlayBackground) root.style.setProperty("--dropdown-background", overlayBackground) // Set border colors based on theme only (not palette-dependent) if (theme === "dark") { // Dark theme: white with 12% opacity root.style.setProperty("--border", "0 0% 100% / 0.12") root.style.setProperty("--input", "0 0% 100% / 0.12") root.style.setProperty("--separator", "0 0% 100% / 0.06") // Slightly more subtle } else { // Light theme: black with 12% opacity root.style.setProperty("--border", "0 0% 0% / 0.12") root.style.setProperty("--input", "0 0% 0% / 0.12") root.style.setProperty("--separator", "0 0% 0% / 0.06") // Slightly more subtle } }, [theme, calculateOverlayBackground], ) // Add a new direct CSS variable update function that bypasses React state for immediate visual updates: const updatePaletteImmediately = useCallback( (palette: { primary: string backgroundPrimary: { light: string; dark: string } backgroundSecondary: { light: string; dark: string } }) => { if (typeof window === "undefined") return const root = document.documentElement // Get the primary color const primaryColor = palette.primary // Get theme-specific background colors const bgPrimary = theme === "dark" ? palette.backgroundPrimary.dark : palette.backgroundPrimary.light const bgSecondary = theme === "dark" ? palette.backgroundSecondary.dark : palette.backgroundSecondary.light // Update primary color const primaryHsl = ColorUtils.hexToHsl(primaryColor) root.style.setProperty("--primary", `${primaryHsl.h} ${primaryHsl.s}% ${primaryHsl.l}%`) root.style.setProperty("--ring", `${primaryHsl.h} ${primaryHsl.s}% ${primaryHsl.l}%`) // Set text color based on luminance const textColor = ColorUtils.getTextColor(primaryColor, 0.8) root.style.setProperty("--primary-text", textColor) // Update background colors root.style.setProperty("--background-primary", bgPrimary) root.style.setProperty("--background-secondary", bgSecondary) // Calculate primary text color based on theme const primaryTextColor = theme === "dark" ? "#ffffff" : "#1b1b1b" // Calculate secondary text color using luminance-based approach const secondaryTextColor = deriveSecondaryTextColor(primaryTextColor, bgPrimary) root.style.setProperty("--text-secondary", secondaryTextColor) // Calculate and set overlay background colors const overlayBackground = calculateOverlayBackground(bgPrimary) root.style.setProperty("--input-background", overlayBackground) root.style.setProperty("--dropdown-background", overlayBackground) // Set border colors based on theme only (not palette-dependent) if (theme === "dark") { // Dark theme: white with 12% opacity root.style.setProperty("--border", "0 0% 100% / 0.12") root.style.setProperty("--input", "0 0% 100% / 0.12") } else { // Light theme: black with 12% opacity root.style.setProperty("--border", "0 0% 0% / 0.12") root.style.setProperty("--input", "0 0% 0% / 0.12") } return { primaryColor, bgPrimary, bgSecondary } }, [theme, calculateOverlayBackground], ) // Custom setPrimaryColor function that also updates CSS variables const setPrimaryColor = useCallback( (color: string) => { setPrimaryColorState(color) updateCssVariables(color, systemColors) }, [updateCssVariables, systemColors], ) // Function to set a system color const setSystemColor = useCallback( (key: string, value: string) => { setSystemColors((prev) => { const newColors = { ...prev, [key]: value } updateCssVariables(primaryColor, newColors) return newColors }) }, [primaryColor, updateCssVariables], ) // Add a new useEffect to handle theme changes and update colors accordingly useEffect(() => { if (mounted && theme) { // Find the current palette by name const currentPaletteName = localStorage.getItem("selectedPalette") || "Sage" // Distinct color palettes with unique background colors that reflect each palette's character const colorPalettes = [ { name: "Sage", primary: "#87a96b", backgroundPrimary: { light: "#fbfcfb", // Subtle green tint dark: "#0a0b0a", // Subtle green tint in dark }, }, { name: "Slate", primary: "#64748b", backgroundPrimary: { light: "#fafbfc", // Cool blue-gray tint dark: "#0a0a0c", // Cool blue tint in dark }, }, { name: "Terracotta", primary: "#c17b5a", backgroundPrimary: { light: "#fdfbfa", // Warm terracotta tint dark: "#0c0a09", // Warm tint in dark }, }, { name: "Navy", primary: "#2c3e50", backgroundPrimary: { light: "#fafbfc", // Cool navy tint dark: "#090a0c", // Deep blue tint in dark }, }, { name: "Charcoal", primary: "#4a5568", backgroundPrimary: { light: "#fbfbfb", // Neutral charcoal dark: "#0a0a0a", // Pure neutral }, }, { name: "Burgundy", primary: "#8b4a6b", backgroundPrimary: { light: "#fcfafc", // Subtle burgundy tint dark: "#0c0a0b", // Warm burgundy tint in dark }, }, { name: "Forest", primary: "#4a6741", backgroundPrimary: { light: "#fafcfa", // Deep forest green tint dark: "#0a0c0a", // Forest green tint in dark }, }, { name: "Stone", primary: "#8b7d6b", backgroundPrimary: { light: "#fcfcfb", // Warm stone beige tint dark: "#0c0b0a", // Warm stone tint in dark }, }, { name: "Midnight", primary: "#2d3748", backgroundPrimary: { light: "#fafafc", // Deep midnight blue tint dark: "#09090c", // Deep midnight tint in dark }, }, { name: "Copper", primary: "#b7704f", backgroundPrimary: { light: "#fdfcfb", // Warm copper tint dark: "#0c0b09", // Copper tint in dark }, }, { name: "Pewter", primary: "#6b7280", backgroundPrimary: { light: "#fbfbfc", // Cool pewter gray tint dark: "#0a0a0b", // Cool pewter tint in dark }, }, { name: "Olive", primary: "#6b7c32", backgroundPrimary: { light: "#fcfcfa", // Yellow-green olive tint dark: "#0b0c0a", // Olive tint in dark }, }, ] // Helper function to calculate secondary background with 12% lightness difference const calculateSecondaryBackground = (primaryBg: string, isDark: boolean): string => { const primaryHsl = ColorUtils.hexToHsl(primaryBg) const lightnessAdjustment = 12 // Increased from 9% to 12% difference // Always make secondary darker than primary (parallel relationship) const newLightness = Math.max(primaryHsl.l - lightnessAdjustment, 0) const { r, g, b } = ColorUtils.hslToRgb(primaryHsl.h, primaryHsl.s, newLightness) return ColorUtils.rgbToHex(r, g, b) } // Find the selected palette const selectedPalette = colorPalettes.find((p) => p.name === currentPaletteName) if (selectedPalette && currentPaletteName !== "Custom") { const primaryBg = theme === "dark" ? selectedPalette.backgroundPrimary.dark : selectedPalette.backgroundPrimary.light const secondaryBg = calculateSecondaryBackground(primaryBg, theme === "dark") // Apply the theme-specific background colors from the selected palette setSystemColors((prev) => ({ ...prev, backgroundPrimary: primaryBg, backgroundSecondary: secondaryBg, })) } else { // Fallback to default colors if no palette is found or using custom palette const primaryBg = theme === "dark" ? defaultSystemColors.backgroundPrimary.dark : defaultSystemColors.backgroundPrimary.light const secondaryBg = calculateSecondaryBackground(primaryBg, theme === "dark") setSystemColors((prev) => ({ ...prev, backgroundPrimary: primaryBg, backgroundSecondary: secondaryBg, })) } } }, [theme, mounted]) // Update CSS variables when theme or primary color changes useEffect(() => { if (mounted) { updateCssVariables(primaryColor, systemColors) } }, [primaryColor, theme, updateCssVariables, mounted, systemColors]) // Update the contextValue to include the new function const contextValue = mounted ? { primaryColor, setPrimaryColor, systemColors, setSystemColor, currentTheme: theme, updatePaletteImmediately, } : { primaryColor: "#3b82f6", setPrimaryColor: () => {}, systemColors: {}, setSystemColor: () => {}, currentTheme: undefined, updatePaletteImmediately: () => ({ primaryColor: "#3b82f6", bgPrimary: "", bgSecondary: "" }), } return {children} } /** * Hook to access the color context */ export function useColorPalette() { const context = useContext(ColorContext) if (context === undefined) { throw new Error("useColorPalette must be used within a ColorProvider") } return context } export const useColor = useColorPalette