# Arquitectura de la Sidebar - Analítica Núcleo ## Resumen La sidebar utiliza un sistema de estado unificado centralizado que elimina problemas de sincronización, watchers en cascada y comportamientos inconsistentes. ## Componentes ### 1. `useSidebarState()` - Composable Unificado **Ubicación**: `app/composables/useSidebarState.ts` **Responsabilidades**: - Maneja TODO el estado relacionado con la sidebar en un solo lugar - Persiste el estado en cookies usando `useCookie` - Implementa lógica responsive (mobile vs desktop) - Maneja el cierre automático en navegación (solo mobile) **Estado Gestionado**: ```typescript interface SidebarState { open: boolean // Sidebar abierta/cerrada (mobile overlay) collapsed: boolean // Sidebar colapsada/expandida (desktop) size: number // Tamaño del panel (%) } ``` **API**: ```typescript const { // Estado reactivo open, // ComputedRef collapsed, // ComputedRef size, // ComputedRef isMobile, // ComputedRef // Acciones toggle, // () => void - Toggle open/closed toggleCollapse, // () => void - Toggle collapsed/expanded setOpen, // (value: boolean) => void setCollapsed // (value: boolean) => void } = useSidebarState() ``` **Características Clave**: 1. **Singleton Pattern**: El estado se inicializa una sola vez y se comparte en toda la aplicación 2. **Persistencia Automática**: Cada cambio se guarda automáticamente en cookies 3. **Responsive Behavior**: Detecta si está en mobile (`< 1024px`) y ajusta comportamiento 4. **Auto-close en Navegación**: En mobile, cierra automáticamente al cambiar de ruta ### 2. `AppSidebar.vue` - Componente de Sidebar **Ubicación**: `app/components/app/AppSidebar.vue` **Cambios**: - **Antes**: Usaba `defineModel` con refs locales que se desincronizaban - **Ahora**: Usa `useSidebarState()` como fuente única de verdad ```vue ``` ### 3. `dashboard.vue` - Layout **Ubicación**: `app/layouts/dashboard.vue` **Cambios**: - **Antes**: Tenía refs locales + workaround manual de focus/aria-hidden - **Ahora**: Solo usa el composable, sin lógica duplicada ```vue ``` ## Problemas Resueltos ### ❌ Antes: Múltiples Fuentes de Estado ``` Layout (ref) ──┐ ├──❌ CONFLICTOS ──> Comportamiento anómalo AppSidebar (defineModel) ──┤ │ DashboardGroup (ref) ──┘ │ Cookie Storage ──────┘ ``` ### ✅ Ahora: Fuente Única de Verdad ``` useSidebarState (singleton) │ ├──> Cookie Storage (auto-sync) │ ├──> AppSidebar (computed) │ └──> Layout (observador) ``` ## Beneficios ### 1. **Estado Consistente** - Una sola fuente de verdad - No hay desincronización entre componentes - No hay race conditions ### 2. **Simplicidad** - No más watchers en cascada - No más hooks indirectos - No más workarounds de DOM ### 3. **Mantenibilidad** - Lógica centralizada en un solo archivo - Fácil de testear - Fácil de extender ### 4. **Performance** - Menos re-renders innecesarios - Un solo watcher para navegación - Persistencia optimizada ### 5. **Responsive by Design** - Detecta mobile/desktop automáticamente - Comportamiento diferenciado según dispositivo - No requiere media queries en múltiples lugares ## Comportamiento Detallado ### Desktop (≥ 1024px) 1. **Toggle Collapse**: Botón en navbar colapsa/expande la sidebar 2. **Resizable**: Se puede arrastrar el borde para ajustar tamaño 3. **Persistente**: Permanece visible al navegar entre rutas 4. **Estado Guardado**: Tamaño y collapsed state se guardan en cookie ### Mobile (< 1024px) 1. **Toggle Open**: Botón en navbar abre sidebar como overlay (slideover) 2. **Auto-close**: Se cierra automáticamente al navegar a otra ruta 3. **No Resizable**: Ocupa ancho fijo optimizado para mobile 4. **Estado Guardado**: Solo el open state se guarda (collapsed no aplica) ## Uso en Otros Componentes Si necesitas acceder al estado de la sidebar desde cualquier otro componente: ```vue ``` ## Testing Para testear el comportamiento: ```typescript import { useSidebarState } from '~/composables/useSidebarState' describe('useSidebarState', () => { it('should initialize with default values', () => { const { open, collapsed } = useSidebarState() expect(open.value).toBe(true) expect(collapsed.value).toBe(false) }) it('should toggle open state', () => { const { open, toggle } = useSidebarState() const initial = open.value toggle() expect(open.value).toBe(!initial) }) it('should persist state in cookie', () => { const { setOpen } = useSidebarState() setOpen(false) // Verificar que la cookie se actualizó const cookie = useCookie('analytics-dashboard-sidebar') expect(cookie.value.open).toBe(false) }) }) ``` ## Migración de Código Existente Si tienes código que accedía directamente a refs del layout: ### ❌ Antes ```vue ``` ### ✅ Ahora ```vue ``` ## Extensibilidad Para agregar nueva funcionalidad (ej: animaciones, callbacks): ```typescript // En useSidebarState.ts export function useSidebarState() { // ... código existente ... // Nueva funcionalidad function onToggle(callback: () => void) { watch(open, callback) } return { // ... exports existentes ... onToggle // Nueva función } } ``` ## Notas Técnicas ### ¿Por qué Singleton? El patrón singleton asegura que todos los componentes lean y escriban del mismo objeto en memoria, eliminando cualquier posibilidad de desincronización. ### ¿Por qué Cookies en lugar de LocalStorage? Las cookies permiten SSR (Server-Side Rendering) - el servidor puede leer el estado inicial y hacer el render correcto en la primera carga, evitando flashes de contenido. ### ¿Cómo funciona la Persistencia? ```typescript const cookie = useCookie(STORAGE_KEY) // Cada set actualiza la cookie automáticamente set: (value: boolean) => { if (sidebarState.value) { sidebarState.value.open = value cookie.value = sidebarState.value // Auto-persist } } ``` ## Troubleshooting ### Problema: El estado no persiste entre reloads **Solución**: Verificar que las cookies no estén bloqueadas en el navegador ### Problema: Comportamiento diferente en mobile vs desktop **Respuesta**: Esto es intencional. El composable detecta el viewport y ajusta el comportamiento automáticamente. ### Problema: Quiero desactivar el auto-close en mobile **Solución**: Comentar el watcher de route en `useSidebarState.ts`: ```typescript // if (import.meta.client) { // watch(() => route.fullPath, () => { // if (isMobile.value) { // open.value = false // } // }) // } ``` --- **Autor**: Claude Code **Fecha**: 2025-10-30 **Versión**: 1.0.0