From a10d39a17923bc70e305513e6d1f53f69937b04e Mon Sep 17 00:00:00 2001 From: josedario87 Date: Thu, 30 Oct 2025 11:16:15 -0600 Subject: [PATCH] =?UTF-8?q?Refactor:=20Implementaci=C3=B3n=20impecable=20d?= =?UTF-8?q?e=20la=20sidebar=20con=20estado=20unificado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- nuxt4-app/app/components/app/AppSidebar.vue | 15 +- nuxt4-app/app/composables/useSidebarState.ts | 129 ++++++++ nuxt4-app/app/layouts/dashboard.vue | 32 +- nuxt4-app/docs/SIDEBAR_ARCHITECTURE.md | 319 +++++++++++++++++++ 4 files changed, 465 insertions(+), 30 deletions(-) create mode 100644 nuxt4-app/app/composables/useSidebarState.ts create mode 100644 nuxt4-app/docs/SIDEBAR_ARCHITECTURE.md diff --git a/nuxt4-app/app/components/app/AppSidebar.vue b/nuxt4-app/app/components/app/AppSidebar.vue index 48e68a9..996010a 100644 --- a/nuxt4-app/app/components/app/AppSidebar.vue +++ b/nuxt4-app/app/components/app/AppSidebar.vue @@ -229,8 +229,19 @@ import type { NavigationMenuItem } from '@nuxt/ui' const route = useRoute() -const open = defineModel('open', { default: true }) -const collapsed = defineModel('collapsed', { default: false }) +// Usar el composable unificado para el estado de la sidebar +const sidebarState = useSidebarState() + +// Exponer como models para compatibilidad con 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) +}) // Manejo del fallback de iconos para el botón Inicio const perfilIconFallbackIndex = ref(0) diff --git a/nuxt4-app/app/composables/useSidebarState.ts b/nuxt4-app/app/composables/useSidebarState.ts new file mode 100644 index 0000000..d0a272f --- /dev/null +++ b/nuxt4-app/app/composables/useSidebarState.ts @@ -0,0 +1,129 @@ +/** + * Composable unificado para manejar el estado de la sidebar + * + * Centraliza todo el estado relacionado con la sidebar para evitar + * inconsistencias entre múltiples refs y watchers en cascada. + * + * Características: + * - Estado persistente en cookies + * - Sincronización automática entre open/collapsed + * - Manejo de responsive (mobile vs desktop) + * - Cierre automático en navegación (solo mobile) + */ + +import { ref, computed, watch } from 'vue' +import { useRoute } from 'vue-router' + +// Storage key para las cookies +const STORAGE_KEY = 'analytics-dashboard-sidebar' + +// Tipos +interface SidebarState { + open: boolean + collapsed: boolean + size: number +} + +// Estado global singleton +const sidebarState = ref(null) + +export function useSidebarState() { + const route = useRoute() + + // Inicializar estado solo una vez (singleton) + if (!sidebarState.value) { + // Leer de cookie si existe + const savedState = useCookie(STORAGE_KEY, { + default: () => ({ + open: true, + collapsed: false, + size: 28 // defaultSize + }) + }) + + sidebarState.value = savedState.value + } + + // Referencias reactivas al estado + const open = computed({ + get: () => sidebarState.value?.open ?? true, + set: (value: boolean) => { + if (sidebarState.value) { + sidebarState.value.open = value + // Persistir en cookie + const cookie = useCookie(STORAGE_KEY) + cookie.value = sidebarState.value + } + } + }) + + const collapsed = computed({ + get: () => sidebarState.value?.collapsed ?? false, + set: (value: boolean) => { + if (sidebarState.value) { + sidebarState.value.collapsed = value + // Persistir en cookie + const cookie = useCookie(STORAGE_KEY) + cookie.value = sidebarState.value + } + } + }) + + const size = computed({ + get: () => sidebarState.value?.size ?? 28, + set: (value: number) => { + if (sidebarState.value) { + sidebarState.value.size = value + // Persistir en cookie + const cookie = useCookie(STORAGE_KEY) + cookie.value = sidebarState.value + } + } + }) + + // Funciones de control + function toggle() { + open.value = !open.value + } + + function toggleCollapse() { + collapsed.value = !collapsed.value + } + + function setOpen(value: boolean) { + open.value = value + } + + function setCollapsed(value: boolean) { + collapsed.value = value + } + + // Detectar si estamos en mobile + const isMobile = computed(() => { + if (import.meta.server) return false + return window.innerWidth < 1024 // lg breakpoint + }) + + // Auto-cerrar en navegación solo en mobile + if (import.meta.client) { + watch(() => route.fullPath, () => { + if (isMobile.value) { + open.value = false + } + }) + } + + return { + // Estado + open, + collapsed, + size, + isMobile, + + // Acciones + toggle, + toggleCollapse, + setOpen, + setCollapsed + } +} diff --git a/nuxt4-app/app/layouts/dashboard.vue b/nuxt4-app/app/layouts/dashboard.vue index 6733bc5..b84ad65 100644 --- a/nuxt4-app/app/layouts/dashboard.vue +++ b/nuxt4-app/app/layouts/dashboard.vue @@ -1,7 +1,7 @@