All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 49s
Cambios: - Agregar documentación completa del sistema de temas en /settings - Extender ThemeColors con 7 nuevas variables: * coffeeUva, coffeeOreado, coffeeMojado, coffeeVerde * statusPendiente, statusPagado, statusAnulado - Actualizar useTheme.ts para aplicar nuevas variables CSS - Actualizar main.css con definiciones de nuevas variables - Actualizar temas predefinidos (azul, verde, carbón) - Mantener colores de café y estados consistentes en todos los temas Las nuevas variables permiten personalizar: - Colores de identificación de tipos de café en gráficas - Colores de estados de pago en tablas y badges
278 lines
7.9 KiB
TypeScript
278 lines
7.9 KiB
TypeScript
/**
|
|
* 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
|
|
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',
|
|
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<ThemeColors>('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-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', '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
|
|
}
|
|
}
|