/** * Composable para gestionar el sistema de temas de Analítica Núcleo * * Proporciona funciones para cargar, aplicar, guardar y resetear temas personalizados. * El tema se guarda en localStorage y se sincroniza entre pestañas del navegador. * * @example * ```ts * const { theme, applyTheme, loadTheme, saveTheme, resetTheme } = useTheme() * * // Cargar tema guardado * onMounted(() => loadTheme()) * * // Modificar tema * theme.value.primary = '#ff5733' * applyTheme() * saveTheme() * ``` */ export interface ThemeColors { // Colores base bg: string surface: string border: string primary: string primaryStrong: string accent: string success: string // Verde de éxito/confirmación text: string textMuted: string // Colores para tipos de café coffeeUva: string // Purple - Café Uva coffeeOreado: string // Orange - Café Oreado coffeeMojado: string // Cyan - Café Mojado coffeeVerde: string // Green - Café Verde // Colores para estados statusPendiente: string // Amarillo/naranja - Pendiente de pago statusPagado: string // Verde - Pagado statusAnulado: string // Rojo/gris - Anulado } /** * Tema por defecto - Café */ export const defaultTheme: ThemeColors = { // Colores base bg: '#14100b', surface: '#1f180f', border: '#3a2a16', primary: '#e0c080', primaryStrong: '#c08040', accent: '#ffe0a0', success: '#00dc82', // Nuxt green text: '#fef9f0', textMuted: '#d8c7a6', // Colores para tipos de café coffeeUva: '#a855f7', // Purple coffeeOreado: '#f97316', // Orange coffeeMojado: '#06b6d4', // Cyan coffeeVerde: '#22c55e', // Green // Colores para estados statusPendiente: '#f59e0b', // Amber statusPagado: '#10b981', // Emerald statusAnulado: '#6b7280' // Gray } /** * Clave de localStorage donde se guarda el tema */ const STORAGE_KEY = 'custom-theme' /** * Hook principal del sistema de temas */ export const useTheme = () => { // Estado reactivo compartido entre todos los componentes const theme = useState('app-theme', () => ({ ...defaultTheme })) /** * Aplica las variables CSS del tema al DOM * @param themeColors - Objeto con los colores del tema (opcional, usa el estado actual si no se proporciona) */ const applyTheme = (themeColors?: ThemeColors) => { if (import.meta.client) { const colors = themeColors || theme.value const root = document.documentElement // Colores base root.style.setProperty('--brand-bg', colors.bg) root.style.setProperty('--brand-surface', colors.surface) root.style.setProperty('--brand-border', colors.border) root.style.setProperty('--brand-primary', colors.primary) root.style.setProperty('--brand-primary-strong', colors.primaryStrong) root.style.setProperty('--brand-accent', colors.accent) root.style.setProperty('--brand-success', colors.success) root.style.setProperty('--brand-text', colors.text) root.style.setProperty('--brand-text-muted', colors.textMuted) // Colores para tipos de café root.style.setProperty('--coffee-uva', colors.coffeeUva) root.style.setProperty('--coffee-oreado', colors.coffeeOreado) root.style.setProperty('--coffee-mojado', colors.coffeeMojado) root.style.setProperty('--coffee-verde', colors.coffeeVerde) // Colores para estados root.style.setProperty('--status-pendiente', colors.statusPendiente) root.style.setProperty('--status-pagado', colors.statusPagado) root.style.setProperty('--status-anulado', colors.statusAnulado) } } /** * Carga el tema guardado desde localStorage * Si no existe tema guardado, aplica el tema por defecto */ const loadTheme = () => { if (import.meta.client) { try { const savedTheme = localStorage.getItem(STORAGE_KEY) if (savedTheme) { const parsed = JSON.parse(savedTheme) as ThemeColors theme.value = parsed applyTheme(parsed) } else { // No hay tema guardado, aplicar el por defecto applyTheme(defaultTheme) } } catch (error) { console.error('Error al cargar el tema desde localStorage:', error) // En caso de error, aplicar tema por defecto applyTheme(defaultTheme) } } } /** * Guarda el tema actual en localStorage * @returns true si se guardó correctamente, false si hubo un error */ const saveTheme = (): boolean => { if (import.meta.client) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(theme.value)) return true } catch (error) { console.error('Error al guardar el tema en localStorage:', error) return false } } return false } /** * Resetea el tema a los valores por defecto y limpia localStorage */ const resetTheme = () => { theme.value = { ...defaultTheme } applyTheme(defaultTheme) if (import.meta.client) { try { localStorage.removeItem(STORAGE_KEY) } catch (error) { console.error('Error al limpiar el tema de localStorage:', error) } } } /** * Establece un nuevo tema completo * @param newTheme - Objeto con los colores del nuevo tema * @param save - Si true, guarda automáticamente en localStorage (default: false) */ const setTheme = (newTheme: ThemeColors, save = false) => { theme.value = { ...newTheme } applyTheme(newTheme) if (save) { saveTheme() } } /** * Exporta el tema actual como JSON string * @returns JSON string del tema actual */ const exportTheme = (): string => { return JSON.stringify(theme.value, null, 2) } /** * Importa un tema desde JSON string * @param jsonString - JSON string con la configuración del tema * @param save - Si true, guarda automáticamente en localStorage (default: false) * @returns true si se importó correctamente, false si hubo un error */ const importTheme = (jsonString: string, save = false): boolean => { try { const parsed = JSON.parse(jsonString) as ThemeColors // Validar que tenga todas las propiedades requeridas const requiredKeys: (keyof ThemeColors)[] = [ 'bg', 'surface', 'border', 'primary', 'primaryStrong', 'accent', 'success', 'text', 'textMuted', 'coffeeUva', 'coffeeOreado', 'coffeeMojado', 'coffeeVerde', 'statusPendiente', 'statusPagado', 'statusAnulado' ] for (const key of requiredKeys) { if (!parsed[key] || typeof parsed[key] !== 'string') { throw new Error(`Propiedad "${key}" faltante o inválida`) } } setTheme(parsed, save) return true } catch (error) { console.error('Error al importar el tema:', error) return false } } /** * Listener para sincronizar cambios de tema entre pestañas * Se ejecuta automáticamente cuando se detecta un cambio en localStorage */ const syncThemeAcrossTabs = (event: StorageEvent) => { if (event.key === STORAGE_KEY && event.newValue) { try { const newTheme = JSON.parse(event.newValue) as ThemeColors theme.value = newTheme applyTheme(newTheme) } catch (error) { console.error('Error al sincronizar tema entre pestañas:', error) } } else if (event.key === STORAGE_KEY && event.newValue === null) { // El tema fue eliminado en otra pestaña, resetear resetTheme() } } /** * Inicializa los listeners de sincronización entre pestañas * Solo debe llamarse una vez al montar la aplicación */ const initStorageListener = () => { if (import.meta.client) { window.addEventListener('storage', syncThemeAcrossTabs) } } /** * Limpia los listeners de sincronización * Debe llamarse al desmontar si es necesario */ const cleanupStorageListener = () => { if (import.meta.client) { window.removeEventListener('storage', syncThemeAcrossTabs) } } return { theme, defaultTheme, applyTheme, loadTheme, saveTheme, resetTheme, setTheme, exportTheme, importTheme, initStorageListener, cleanupStorageListener } }