Files
analiticaNucleo/nuxt4-app/docs/SIDEBAR_ARCHITECTURE.md
josedario87 a10d39a179
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 49s
Refactor: Implementación impecable de la sidebar con estado unificado
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
2025-10-30 11:16:15 -06:00

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:

  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
<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)

  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:

<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