Soluciona todos los problemas identificados en la arquitectura anterior: Cambios principales: - Nuevo composable useSidebarState() que centraliza todo el estado - Elimina múltiples fuentes de verdad que causaban desincronización - Remueve watchers en cascada y hooks indirectos - Elimina workarounds manuales de DOM y aria-hidden - Implementa persistencia consistente en cookies - Manejo responsive automático (mobile vs desktop) Archivos modificados: - app/composables/useSidebarState.ts (nuevo): Composable singleton - app/components/app/AppSidebar.vue: Usa el nuevo composable - app/layouts/dashboard.vue: Simplificado, sin refs locales ni workarounds - docs/SIDEBAR_ARCHITECTURE.md (nuevo): Documentación completa Beneficios: ✓ Estado consistente en toda la aplicación ✓ No más flickering o comportamientos anómalos ✓ Código más simple y mantenible ✓ Mejor performance (menos re-renders) ✓ Auto-close en mobile al navegar Referencias: - app/composables/useSidebarState.ts:1 - app/components/app/AppSidebar.vue:232 - app/layouts/dashboard.vue:40
7.9 KiB
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:
interface SidebarState {
open: boolean // Sidebar abierta/cerrada (mobile overlay)
collapsed: boolean // Sidebar colapsada/expandida (desktop)
size: number // Tamaño del panel (%)
}
API:
const {
// Estado reactivo
open, // ComputedRef<boolean>
collapsed, // ComputedRef<boolean>
size, // ComputedRef<number>
isMobile, // ComputedRef<boolean>
// Acciones
toggle, // () => void - Toggle open/closed
toggleCollapse, // () => void - Toggle collapsed/expanded
setOpen, // (value: boolean) => void
setCollapsed // (value: boolean) => void
} = useSidebarState()
Características Clave:
- Singleton Pattern: El estado se inicializa una sola vez y se comparte en toda la aplicación
- Persistencia Automática: Cada cambio se guarda automáticamente en cookies
- Responsive Behavior: Detecta si está en mobile (
< 1024px) y ajusta comportamiento - 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
defineModelcon refs locales que se desincronizaban - Ahora: Usa
useSidebarState()como fuente única de verdad
<script setup lang="ts">
const sidebarState = useSidebarState()
// Computed para compatibilidad con v-model de UDashboardSidebar
const open = computed({
get: () => sidebarState.open.value,
set: (value: boolean) => sidebarState.setOpen(value)
})
const collapsed = computed({
get: () => sidebarState.collapsed.value,
set: (value: boolean) => sidebarState.setCollapsed(value)
})
</script>
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
<script setup lang="ts">
const sidebarState = useSidebarState()
</script>
<template>
<UDashboardGroup storage-key="analytics-dashboard">
<AppSidebar />
<!-- ... -->
</UDashboardGroup>
</template>
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)
- Toggle Collapse: Botón en navbar colapsa/expande la sidebar
- Resizable: Se puede arrastrar el borde para ajustar tamaño
- Persistente: Permanece visible al navegar entre rutas
- Estado Guardado: Tamaño y collapsed state se guardan en cookie
Mobile (< 1024px)
- Toggle Open: Botón en navbar abre sidebar como overlay (slideover)
- Auto-close: Se cierra automáticamente al navegar a otra ruta
- No Resizable: Ocupa ancho fijo optimizado para mobile
- 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:
<script setup lang="ts">
const { open, collapsed, toggle, isMobile } = useSidebarState()
// Leer estado
console.log('Sidebar abierta?', open.value)
console.log('Sidebar colapsada?', collapsed.value)
console.log('Es mobile?', isMobile.value)
// Cambiar estado
function handleAction() {
toggle() // Abre/cierra la sidebar
}
</script>
Testing
Para testear el comportamiento:
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
<script setup>
const sidebarOpen = ref(true)
const sidebarCollapsed = ref(false)
</script>
<template>
<AppSidebar v-model:open="sidebarOpen" v-model:collapsed="sidebarCollapsed" />
</template>
✅ Ahora
<script setup>
// No se necesita ninguna ref local
</script>
<template>
<AppSidebar />
</template>
Extensibilidad
Para agregar nueva funcionalidad (ej: animaciones, callbacks):
// 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?
const cookie = useCookie<SidebarState>(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:
// 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