All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 50s
El icono del toggle mostraba "X" (cerrar) en lugar del icono hamburguesa al cargar la página, aunque se corregía tras la primera interacción. Causa raíz: - UDashboardGroup inicializa sidebarOpen en false - Nuestro composable inicializaba open en true por defecto - El toggle lee el estado del contexto de DashboardGroup - Desincronización causaba que el icono mostrara el estado incorrecto Solución: - Cambiar el default de open a false en el composable - En desktop, open=false es correcto (no hay overlay, sidebar siempre visible) - En mobile, open=false significa overlay cerrado (comportamiento esperado) - Alineación total con el comportamiento de Nuxt UI Archivos modificados: - app/composables/useSidebarState.ts:43-46 (default open: false) - app/composables/useSidebarState.ts:57 (fallback a false) Referencias: - app/composables/useSidebarState.ts:46 - app/composables/useSidebarState.ts:57
132 lines
3.2 KiB
TypeScript
132 lines
3.2 KiB
TypeScript
/**
|
|
* 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<SidebarState | null>(null)
|
|
|
|
export function useSidebarState() {
|
|
const route = useRoute()
|
|
|
|
// Detectar si estamos en mobile (debe hacerse antes de leer el estado)
|
|
const isMobile = computed(() => {
|
|
if (import.meta.server) return false
|
|
return window.innerWidth < 1024 // lg breakpoint
|
|
})
|
|
|
|
// Inicializar estado solo una vez (singleton)
|
|
if (!sidebarState.value) {
|
|
// Leer de cookie si existe
|
|
const savedState = useCookie<SidebarState>(STORAGE_KEY, {
|
|
default: () => ({
|
|
// En desktop, open=false porque no hay overlay (la sidebar siempre está visible)
|
|
// En mobile, open=false significa que el overlay está cerrado (comportamiento correcto)
|
|
open: false,
|
|
collapsed: false,
|
|
size: 28 // defaultSize
|
|
})
|
|
})
|
|
|
|
sidebarState.value = savedState.value
|
|
}
|
|
|
|
// Referencias reactivas al estado
|
|
const open = computed({
|
|
get: () => sidebarState.value?.open ?? false,
|
|
set: (value: boolean) => {
|
|
if (sidebarState.value) {
|
|
sidebarState.value.open = value
|
|
// Persistir en cookie
|
|
const cookie = useCookie<SidebarState>(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<SidebarState>(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<SidebarState>(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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|