All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 49s
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
333 lines
12 KiB
Vue
333 lines
12 KiB
Vue
<template>
|
|
<UDashboardSidebar
|
|
v-model:open="open"
|
|
v-model:collapsed="collapsed"
|
|
collapsible
|
|
resizable
|
|
:default-size="28"
|
|
:min-size="20"
|
|
:max-size="38"
|
|
:toggle="{ color: 'primary', variant: 'subtle', class: 'rounded-full' }"
|
|
>
|
|
<template #header="{ collapsed: isCollapsed }">
|
|
<div class="flex items-center gap-2">
|
|
<img
|
|
v-if="!isCollapsed"
|
|
src="/logo.png"
|
|
alt="Analítica Núcleo"
|
|
class="h-8 w-8 rounded-full border border-[#ffe0a0]/40"
|
|
/>
|
|
<UIcon v-else name="i-lucide-activity" class="size-5 text-[#ffe0a0]" />
|
|
<span v-if="!isCollapsed" class="text-sm font-semibold text-[var(--brand-text)]">Analítica Núcleo</span>
|
|
</div>
|
|
</template>
|
|
|
|
<template #default="{ collapsed: isCollapsed }">
|
|
<UButton
|
|
:label="isCollapsed ? undefined : 'Buscar...'"
|
|
icon="i-lucide-search"
|
|
color="neutral"
|
|
variant="outline"
|
|
block
|
|
:square="isCollapsed"
|
|
class="mb-4"
|
|
>
|
|
<template v-if="!isCollapsed" #trailing>
|
|
<div class="flex items-center gap-0.5 ms-auto text-[var(--brand-text-muted)]">
|
|
<UKbd value="⌘" variant="subtle" />
|
|
<UKbd value="K" variant="subtle" />
|
|
</div>
|
|
</template>
|
|
</UButton>
|
|
|
|
<UNavigationMenu
|
|
:collapsed="isCollapsed"
|
|
:items="navigationPrimary"
|
|
orientation="vertical"
|
|
class="gap-1"
|
|
/>
|
|
|
|
</template>
|
|
|
|
<template #footer="{ collapsed: isCollapsed }">
|
|
<div v-if="isAuthenticated" class="space-y-3">
|
|
<!-- User Profile Section -->
|
|
<div
|
|
v-if="!isCollapsed"
|
|
class="p-0 space-y-2"
|
|
>
|
|
<!-- User Info Card -->
|
|
<div class="px-3 py-2.5 bg-gradient-to-br from-primary-50/50 to-transparent dark:from-primary-950/20 dark:to-transparent rounded-lg border border-primary-100/60 dark:border-primary-900/40">
|
|
<div class="flex items-center gap-3">
|
|
<div class="relative flex-shrink-0">
|
|
<UAvatar
|
|
v-bind="userAvatar"
|
|
size="md"
|
|
:ui="{
|
|
wrapper: 'ring-2 ring-primary-400/50 dark:ring-primary-500/40 shadow-sm'
|
|
}"
|
|
/>
|
|
<span
|
|
class="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-500 border-2 border-white dark:border-gray-950 rounded-full shadow-sm"
|
|
title="En línea"
|
|
/>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="font-semibold text-sm text-gray-900 dark:text-white truncate leading-tight">
|
|
{{ user.name || user.username }}
|
|
</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 truncate mt-0.5">
|
|
{{ user.email }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="space-y-1">
|
|
<UButton
|
|
to="https://inicio.nucleoriofrio.com"
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="sm"
|
|
block
|
|
class="justify-start gap-2.5 hover:bg-gradient-to-r hover:from-primary-50/90 hover:to-green-50/60 dark:hover:from-primary-950/40 dark:hover:to-green-950/30 transition-all duration-300 hover:shadow-sm"
|
|
:ui="{
|
|
rounded: 'rounded-lg',
|
|
padding: { sm: 'px-2.5 py-2' }
|
|
}"
|
|
>
|
|
<template #leading>
|
|
<div class="w-7 h-7 rounded-md bg-gradient-to-br from-primary-50 to-green-50 dark:from-primary-950/40 dark:to-green-950/40 flex items-center justify-center flex-shrink-0 relative overflow-hidden shadow-sm border border-primary-100/40 dark:border-primary-900/30">
|
|
<img
|
|
src="/perfil-icon.png"
|
|
alt="Perfil"
|
|
class="w-4 h-4 absolute inset-0 m-auto object-cover rounded-sm opacity-90"
|
|
@error="handlePerfilIconError"
|
|
/>
|
|
<UIcon name="i-lucide-home" class="size-3.5 text-green-600 dark:text-green-400 opacity-0" />
|
|
</div>
|
|
</template>
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Inicio</span>
|
|
</UButton>
|
|
|
|
<UButton
|
|
to="/settings"
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="sm"
|
|
block
|
|
class="justify-start gap-2.5 hover:bg-primary-50/80 dark:hover:bg-primary-950/30 transition-all duration-200"
|
|
:ui="{
|
|
rounded: 'rounded-lg',
|
|
padding: { sm: 'px-2.5 py-2' }
|
|
}"
|
|
>
|
|
<template #leading>
|
|
<div class="w-7 h-7 rounded-md bg-gray-50 dark:bg-gray-800/50 flex items-center justify-center flex-shrink-0">
|
|
<UIcon name="i-lucide-settings" class="size-3.5 text-gray-600 dark:text-gray-400" />
|
|
</div>
|
|
</template>
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Configuración</span>
|
|
</UButton>
|
|
|
|
<UButton
|
|
to="/notifications"
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="sm"
|
|
block
|
|
class="justify-start gap-2.5 hover:bg-primary-50/80 dark:hover:bg-primary-950/30 transition-all duration-200"
|
|
:ui="{
|
|
rounded: 'rounded-lg',
|
|
padding: { sm: 'px-2.5 py-2' }
|
|
}"
|
|
>
|
|
<template #leading>
|
|
<div class="w-7 h-7 rounded-md bg-amber-50 dark:bg-amber-950/30 flex items-center justify-center relative flex-shrink-0">
|
|
<UIcon name="i-lucide-bell" class="size-3.5 text-amber-600 dark:text-amber-400" />
|
|
<span class="absolute -top-0.5 -right-0.5 w-2 h-2 bg-red-500 rounded-full ring-1 ring-white dark:ring-gray-900" />
|
|
</div>
|
|
</template>
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Notificaciones</span>
|
|
<template #trailing>
|
|
<UBadge color="red" variant="solid" size="xs" class="ml-auto">3</UBadge>
|
|
</template>
|
|
</UButton>
|
|
|
|
<div class="pt-2 mt-1.5 border-t border-gray-200/60 dark:border-gray-800/60">
|
|
<UButton
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="sm"
|
|
block
|
|
class="justify-start gap-2.5 hover:bg-red-50/80 dark:hover:bg-red-950/30 transition-all duration-200 group"
|
|
:ui="{
|
|
rounded: 'rounded-lg',
|
|
padding: { sm: 'px-2.5 py-2' }
|
|
}"
|
|
@click="logout"
|
|
>
|
|
<template #leading>
|
|
<div class="w-7 h-7 rounded-md bg-red-50 dark:bg-red-950/30 flex items-center justify-center flex-shrink-0 group-hover:bg-red-100 dark:group-hover:bg-red-950/50 transition-colors">
|
|
<UIcon name="i-lucide-log-out" class="size-3.5 text-red-600 dark:text-red-400" />
|
|
</div>
|
|
</template>
|
|
<span class="text-sm font-medium text-red-600 dark:text-red-400">Cerrar sesión</span>
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Collapsed View -->
|
|
<div v-else class="flex flex-col items-center gap-3">
|
|
<div class="relative group">
|
|
<UButton
|
|
color="neutral"
|
|
variant="ghost"
|
|
square
|
|
class="relative hover:bg-primary-50/50 dark:hover:bg-primary-950/20 transition-all duration-200"
|
|
:ui="{ rounded: 'rounded-lg' }"
|
|
>
|
|
<div class="relative">
|
|
<UAvatar
|
|
v-bind="userAvatar"
|
|
size="sm"
|
|
:ui="{
|
|
wrapper: 'ring-2 ring-primary-400/40 dark:ring-primary-500/30 group-hover:ring-primary-500/60 transition-all duration-200 shadow-sm'
|
|
}"
|
|
/>
|
|
<span
|
|
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 bg-green-500 border-2 border-white dark:border-gray-950 rounded-full shadow-sm"
|
|
/>
|
|
</div>
|
|
</UButton>
|
|
</div>
|
|
|
|
<div class="w-full h-px bg-gray-200/50 dark:bg-gray-800/50" />
|
|
|
|
<UButton
|
|
@click="logout"
|
|
color="neutral"
|
|
variant="ghost"
|
|
square
|
|
class="hover:bg-red-50 dark:hover:bg-red-950/20 transition-all duration-200"
|
|
:ui="{ rounded: 'rounded-lg' }"
|
|
>
|
|
<UIcon name="i-lucide-log-out" class="size-4 text-red-600 dark:text-red-400" />
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</UDashboardSidebar>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import type { NavigationMenuItem } from '@nuxt/ui'
|
|
|
|
const route = useRoute()
|
|
|
|
// 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)
|
|
const perfilIconSources = [
|
|
'/perfil-icon.png',
|
|
'https://inicio.nucleoriofrio.com/apple-touch-icon.png',
|
|
'https://inicio.nucleoriofrio.com/favicon.ico'
|
|
]
|
|
|
|
const handlePerfilIconError = (event: Event) => {
|
|
const img = event.target as HTMLImageElement
|
|
perfilIconFallbackIndex.value++
|
|
|
|
if (perfilIconFallbackIndex.value < perfilIconSources.length) {
|
|
img.src = perfilIconSources[perfilIconFallbackIndex.value]
|
|
} else {
|
|
// Si todos los iconos fallan, mostrar el icono de casa
|
|
img.style.display = 'none'
|
|
const icon = img.nextElementSibling as HTMLElement
|
|
if (icon) {
|
|
icon.style.opacity = '1'
|
|
}
|
|
}
|
|
}
|
|
|
|
const navigationPrimary = computed<NavigationMenuItem[]>(() => [
|
|
{
|
|
label: 'Inicio',
|
|
icon: 'i-lucide-home',
|
|
to: '/',
|
|
active: route.path === '/'
|
|
},
|
|
{
|
|
label: 'Panorama Facturador',
|
|
icon: 'i-lucide-bar-chart-3',
|
|
to: '/panorama',
|
|
active: route.path === '/panorama'
|
|
},
|
|
{
|
|
label: 'Informe Ingresos',
|
|
icon: 'i-lucide-file-bar-chart',
|
|
to: '/informe-ingresos',
|
|
active: route.path === '/informe-ingresos',
|
|
badge: { label: 'Mantenimiento', color: 'amber' }
|
|
},
|
|
{
|
|
label: 'Comparativa Cosechas',
|
|
icon: 'i-lucide-calendar-range',
|
|
to: '/comparativa-cosechas',
|
|
active: route.path === '/comparativa-cosechas',
|
|
badge: { label: 'Mantenimiento', color: 'amber' }
|
|
},
|
|
{
|
|
label: 'Explorador de datos',
|
|
icon: 'i-lucide-table',
|
|
to: '/explorer',
|
|
active: route.path === '/explorer'
|
|
},
|
|
{
|
|
label: 'Metadatos',
|
|
icon: 'i-lucide-database',
|
|
to: '/metadatos',
|
|
active: route.path === '/metadatos',
|
|
badge: { label: 'Mantenimiento', color: 'amber' }
|
|
},
|
|
{
|
|
label: 'Metabase Debug',
|
|
icon: 'i-lucide-bug',
|
|
to: '/metabase-debug',
|
|
active: route.path === '/metabase-debug'
|
|
},
|
|
{
|
|
label: 'Explorador de datos raw',
|
|
icon: 'i-lucide-table',
|
|
to: '/rawExplorer',
|
|
active: route.path === '/rawExplorer',
|
|
badge: { label: 'Mantenimiento', color: 'amber' }
|
|
}
|
|
])
|
|
|
|
const { user, isAuthenticated, logout } = useAuthentik()
|
|
|
|
// Computed para el avatar del usuario
|
|
const userAvatar = computed(() => ({
|
|
src: user.value?.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(user.value?.name || user.value?.username || 'User')}&background=3b82f6&color=fff&bold=true&format=svg`,
|
|
alt: user.value?.name || user.value?.username || 'User'
|
|
}))
|
|
</script>
|