/** * Composable para personalización del color principal del tema * Guarda la preferencia en localStorage y aplica los gradientes dinámicamente */ interface ColorHSL { h: number s: number l: number } /** * Convierte un color hex a HSL */ function hexToHSL(hex: string): ColorHSL { // Remover el # si existe hex = hex.replace('#', '') // Convertir a RGB const r = parseInt(hex.substring(0, 2), 16) / 255 const g = parseInt(hex.substring(2, 4), 16) / 255 const b = parseInt(hex.substring(4, 6), 16) / 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)) / 6 break case g: h = ((b - r) / d + 2) / 6 break case b: h = ((r - g) / d + 4) / 6 break } } return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100), } } /** * Genera una paleta de colores basada en el color base */ function generateColorPalette(baseColor: string): Record { const hsl = hexToHSL(baseColor) // Generar escala de luminosidad const palette: Record = { 50: `${hsl.h} ${Math.min(100, hsl.s + 20)}% 95%`, 100: `${hsl.h} ${Math.min(100, hsl.s + 15)}% 90%`, 200: `${hsl.h} ${Math.min(100, hsl.s + 10)}% 80%`, 300: `${hsl.h} ${hsl.s}% 70%`, 400: `${hsl.h} ${hsl.s}% 60%`, 500: `${hsl.h} ${hsl.s}% ${hsl.l}%`, // Base 600: `${hsl.h} ${Math.max(0, hsl.s - 5)}% ${Math.max(0, hsl.l - 10)}%`, 700: `${hsl.h} ${Math.max(0, hsl.s - 10)}% ${Math.max(0, hsl.l - 20)}%`, 800: `${hsl.h} ${Math.max(0, hsl.s - 15)}% ${Math.max(0, hsl.l - 30)}%`, 900: `${hsl.h} ${Math.max(0, hsl.s - 20)}% ${Math.max(0, hsl.l - 40)}%`, 950: `${hsl.h} ${Math.max(0, hsl.s - 25)}% ${Math.max(0, hsl.l - 45)}%`, } return palette } /** * Convierte HSL a HEX */ function hslToHex(h: number, s: number, l: number): string { const sDecimal = s / 100 const lDecimal = l / 100 const c = (1 - Math.abs(2 * lDecimal - 1)) * sDecimal const x = c * (1 - Math.abs((h / 60) % 2 - 1)) const m = lDecimal - c / 2 let r = 0 let g = 0 let b = 0 if (h >= 0 && h < 60) { r = c; g = x; b = 0 } else if (h >= 60 && h < 120) { r = x; g = c; b = 0 } else if (h >= 120 && h < 180) { r = 0; g = c; b = x } else if (h >= 180 && h < 240) { r = 0; g = x; b = c } else if (h >= 240 && h < 300) { r = x; g = 0; b = c } else if (h >= 300 && h < 360) { r = c; g = 0; b = x } const toHex = (value: number) => { const hex = Math.round((value + m) * 255).toString(16) return hex.length === 1 ? '0' + hex : hex } return `#${toHex(r)}${toHex(g)}${toHex(b)}` } /** * Aplica los colores personalizados a las variables CSS */ function applyCustomColors(primary?: string, foreground?: string, background?: string) { if (import.meta.server) return // Aplicar color principal si se proporciona if (primary) { const hsl = hexToHSL(primary) document.documentElement.style.setProperty('--cata-primary', primary) // Generar variantes light y dark const lightHsl = { ...hsl, l: Math.min(hsl.l + 15, 95) } const darkHsl = { ...hsl, l: Math.max(hsl.l - 15, 10) } const lightColor = hslToHex(lightHsl.h, lightHsl.s, lightHsl.l) const darkColor = hslToHex(darkHsl.h, darkHsl.s, darkHsl.l) document.documentElement.style.setProperty('--cata-primary-light', lightColor) document.documentElement.style.setProperty('--cata-primary-dark', darkColor) // Aplicar paleta completa para Nuxt UI (en formato HSL) const palette = generateColorPalette(primary) Object.entries(palette).forEach(([key, value]) => { document.documentElement.style.setProperty(`--color-primary-${key}`, value) }) } // Aplicar color de fuente si se proporciona if (foreground) { document.documentElement.style.setProperty('--cata-fg', foreground) } // Aplicar color de background si se proporciona if (background) { document.documentElement.style.setProperty('--cata-bg', background) } } interface ThemeColors { primary: string | null foreground: string | null background: string | null } /** * Colores por defecto según el tema */ const DEFAULT_COLORS = { light: { primary: '#4682B4', foreground: '#000000', background: '#ffffff', }, dark: { primary: '#00FF00', foreground: '#00FF00', background: '#000000', }, } const STORAGE_KEY = 'riocata-custom-colors' export const useColorCustomization = () => { const colorMode = useColorMode() const isDark = computed(() => colorMode.value === 'dark') // Estado reactivo de los colores personalizados const customColors = useState<{ light: ThemeColors; dark: ThemeColors }>( 'custom-colors', () => ({ light: { primary: null, foreground: null, background: null }, dark: { primary: null, foreground: null, background: null }, }) ) /** * Carga los colores personalizados desde localStorage */ const loadCustomColors = () => { if (import.meta.server) return try { const stored = localStorage.getItem(STORAGE_KEY) if (stored) { const colors = JSON.parse(stored) // Validar estructura de datos if ( colors && typeof colors === 'object' && colors.light && typeof colors.light === 'object' && colors.dark && typeof colors.dark === 'object' ) { customColors.value = colors } else { // Datos corruptos o formato antiguo, resetear console.warn('Formato de colores incompatible, reseteando...') localStorage.removeItem(STORAGE_KEY) } } } catch (error) { console.error('Error al cargar colores personalizados:', error) // En caso de error, limpiar localStorage localStorage.removeItem(STORAGE_KEY) } } /** * Guarda los colores personalizados en localStorage */ const saveCustomColors = () => { if (import.meta.server) return try { localStorage.setItem(STORAGE_KEY, JSON.stringify(customColors.value)) } catch (error) { console.error('Error al guardar colores personalizados:', error) } } /** * Establece colores personalizados para el tema actual */ const setCustomColors = (primary?: string, foreground?: string, background?: string) => { if (import.meta.server) return const theme = isDark.value ? 'dark' : 'light' // Asegurar que la estructura existe if (!customColors.value[theme] || typeof customColors.value[theme] !== 'object') { customColors.value[theme] = { primary: null, foreground: null, background: null } } if (primary !== undefined) customColors.value[theme].primary = primary if (foreground !== undefined) customColors.value[theme].foreground = foreground if (background !== undefined) customColors.value[theme].background = background saveCustomColors() applyCustomColors(primary, foreground, background) } /** * Resetea los colores al valor por defecto del tema actual */ const resetColors = () => { if (import.meta.server) return const theme = isDark.value ? 'dark' : 'light' customColors.value[theme] = { primary: null, foreground: null, background: null } saveCustomColors() // Limpiar las variables CSS personalizadas para que vuelvan a los valores del CSS document.documentElement.style.removeProperty('--cata-primary') document.documentElement.style.removeProperty('--cata-primary-light') document.documentElement.style.removeProperty('--cata-primary-dark') document.documentElement.style.removeProperty('--cata-fg') document.documentElement.style.removeProperty('--cata-bg') // Limpiar paleta de Nuxt UI for (let i = 50; i <= 950; i += (i < 100 ? 50 : 100)) { document.documentElement.style.removeProperty(`--color-primary-${i}`) } } /** * Obtiene los colores actuales (personalizados o por defecto) */ const getCurrentColors = computed(() => { const theme = isDark.value ? 'dark' : 'light' const colors = customColors.value[theme] // Validar que la estructura existe if (!colors || typeof colors !== 'object') { return DEFAULT_COLORS[theme] } return { primary: colors.primary || DEFAULT_COLORS[theme].primary, foreground: colors.foreground || DEFAULT_COLORS[theme].foreground, background: colors.background || DEFAULT_COLORS[theme].background, } }) /** * Verifica si hay colores personalizados para el tema actual */ const hasCustomColors = computed(() => { const theme = isDark.value ? 'dark' : 'light' const colors = customColors.value[theme] // Validar que la estructura existe if (!colors || typeof colors !== 'object') { return false } return colors.primary !== null || colors.foreground !== null || colors.background !== null }) /** * Inicializa el composable */ const inicializar = () => { if (import.meta.server) return loadCustomColors() // Solo aplicar colores si hay colores personalizados guardados const theme = isDark.value ? 'dark' : 'light' const colors = customColors.value[theme] if (colors && typeof colors === 'object' && (colors.primary || colors.foreground || colors.background)) { applyCustomColors( colors.primary || undefined, colors.foreground || undefined, colors.background || undefined ) } // Observar cambios en el modo de color watch(isDark, (newIsDark) => { const theme = newIsDark ? 'dark' : 'light' const colors = customColors.value[theme] // Solo aplicar si hay colores personalizados para este tema if (colors && typeof colors === 'object' && (colors.primary || colors.foreground || colors.background)) { applyCustomColors( colors.primary || undefined, colors.foreground || undefined, colors.background || undefined ) } }) } return { customColors: readonly(customColors), getCurrentColors, hasCustomColors, setCustomColors, resetColors, inicializar, } }