import { ref, onMounted } from 'vue'; export type UsePrimaryColorReturn = { primaryColor: ReturnType>; updatePrimaryColor: (value: string) => void; }; const STORAGE_KEY = 'primaryColor'; function setCookie(name: string, value: string, days = 365) { if (typeof document === 'undefined') return; const maxAge = days * 24 * 60 * 60; document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`; } function getStoredPrimaryColor(): string | null { if (typeof window === 'undefined') return null; return localStorage.getItem(STORAGE_KEY); } function parseColorToRGB(color: string): { r: number; g: number; b: number } | null { // Supports #rgb, #rrggbb, rgb(), hsl() color = color.trim().toLowerCase(); // Hex if (color.startsWith('#')) { let hex = color.slice(1); if (hex.length === 3) { hex = hex.split('').map((c) => c + c).join(''); } if (hex.length === 6) { const num = parseInt(hex, 16); return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 }; } } // rgb/rgba if (color.startsWith('rgb')) { const m = color.match(/rgba?\(([^)]+)\)/); if (m) { const [r, g, b] = m[1] .split(',') .slice(0, 3) .map((v) => parseFloat(v.trim())); return { r, g, b }; } } // hsl/hsla if (color.startsWith('hsl')) { const m = color.match(/hsla?\(([^)]+)\)/); if (m) { const [hStr, sStr, lStr] = m[1].split(',').map((v) => v.trim()); const h = parseFloat(hStr); const s = parseFloat(sStr) / 100; const l = parseFloat(lStr) / 100; // convert HSL to RGB const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); const m2 = l - c / 2; let r1 = 0, g1 = 0, b1 = 0; if (0 <= h && h < 60) [r1, g1, b1] = [c, x, 0]; else if (60 <= h && h < 120) [r1, g1, b1] = [x, c, 0]; else if (120 <= h && h < 180) [r1, g1, b1] = [0, c, x]; else if (180 <= h && h < 240) [r1, g1, b1] = [0, x, c]; else if (240 <= h && h < 300) [r1, g1, b1] = [x, 0, c]; else [r1, g1, b1] = [c, 0, x]; return { r: Math.round((r1 + m2) * 255), g: Math.round((g1 + m2) * 255), b: Math.round((b1 + m2) * 255), }; } } return null; } function getContrastColor(bg: string): '#000000' | '#FFFFFF' { const rgb = parseColorToRGB(bg) || { r: 0, g: 0, b: 0 }; // Relative luminance const srgb = [rgb.r, rgb.g, rgb.b].map((v) => { const c = v / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }); const L = 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2]; return L > 0.179 ? '#000000' : '#FFFFFF'; } export function applyPrimaryColor(value: string): void { if (typeof document === 'undefined') return; const root = document.documentElement; // Apply to global primary variables used by Tailwind theme tokens root.style.setProperty('--primary', value); root.style.setProperty('--primary-foreground', getContrastColor(value)); // Align commonly used accent variables in user layout to the same main color root.style.setProperty('--magenta', value); root.style.setProperty('--sidebar-primary', value); root.style.setProperty('--sidebar-primary-foreground', getContrastColor(value)); } export function initializePrimaryColor(): void { const saved = getStoredPrimaryColor(); if (saved) applyPrimaryColor(saved); } const primaryColorRef = ref(getStoredPrimaryColor() || '#ff007a'); export function usePrimaryColor(): UsePrimaryColorReturn { onMounted(() => { const saved = getStoredPrimaryColor(); if (saved) { primaryColorRef.value = saved; } }); function updatePrimaryColor(value: string) { primaryColorRef.value = value; if (typeof window !== 'undefined') { localStorage.setItem(STORAGE_KEY, value); } setCookie(STORAGE_KEY, value); applyPrimaryColor(value); } return { primaryColor: primaryColorRef, updatePrimaryColor }; }