From 7b7ef1c137249523f0b3d515963663b1e3338e92 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 16 Apr 2026 11:21:42 -0300 Subject: [PATCH] feat(web): sidebar colapsable con tooltips + fix scroll horizontal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cambios: - Nuevo hook useSidebar() con persistencia en localStorage ('sidebar-collapsed' = '1'/'0'). - SidebarNav refactorizado: - Width controlled internamente (w-60 expanded, w-[68px] collapsed) - Toggle button al pie con PanelLeftClose/Open icon - Brand mark con gradient brand+violet (consistencia con login) - Active indicator: barra vertical sutil a la izquierda cuando expanded, bg accent cuando collapsed - SectionLabel se reemplaza por divider sutil cuando collapsed - Custom SidebarTooltip puro CSS (sin radix dep nuevo) que aparece a la derecha del item con animacion fade + slide al hover. Funciona con group-hover/item y group-hover/toggle (Tailwind named groups). - Items disabled muestran badge 'Próx.' chico (era 'Próximamente' largo) y en tooltip cuando collapsed: 'Label · Próximamente'. - Fix scroll horizontal: overflow-x-hidden en nav, truncate en spans, shrink-0 en iconos y badges. Layout robusto a labels largos. - ProtectedLayout deja de hardcodear lg:w-60 — sidebar controla su width. - AppHeader Sheet (mobile) usa para que en mobile siempre se vea expanded sin importar el state desktop. Tests 136/136 verde. --- src/web/src/components/layout/AppHeader.tsx | 2 +- src/web/src/components/layout/AppSidebar.tsx | 301 ++++++++++++------- src/web/src/hooks/useSidebar.ts | 24 ++ src/web/src/layouts/ProtectedLayout.tsx | 4 +- 4 files changed, 224 insertions(+), 107 deletions(-) create mode 100644 src/web/src/hooks/useSidebar.ts diff --git a/src/web/src/components/layout/AppHeader.tsx b/src/web/src/components/layout/AppHeader.tsx index 77b27d3..895b285 100644 --- a/src/web/src/components/layout/AppHeader.tsx +++ b/src/web/src/components/layout/AppHeader.tsx @@ -57,7 +57,7 @@ export function AppHeader() { Navegación - + diff --git a/src/web/src/components/layout/AppSidebar.tsx b/src/web/src/components/layout/AppSidebar.tsx index a46ac66..a92e9d3 100644 --- a/src/web/src/components/layout/AppSidebar.tsx +++ b/src/web/src/components/layout/AppSidebar.tsx @@ -9,10 +9,14 @@ import { Users, ShieldCheck, KeyRound, + PanelLeftClose, + PanelLeftOpen, + Newspaper, } from 'lucide-react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { useAuthStore } from '@/stores/authStore' +import { useSidebar } from '@/hooks/useSidebar' interface NavItem { label: string @@ -25,127 +29,216 @@ const navItems: NavItem[] = [ { label: 'Dashboard', href: '/', icon: LayoutDashboard }, { label: 'Ventas', href: '/ventas', icon: ShoppingCart, disabled: true }, { label: 'Tasación', href: '/tasacion', icon: Calculator, disabled: true }, - { - label: 'Integraciones', - href: '/integraciones', - icon: Zap, - disabled: true, - }, - { - label: 'Administración', - href: '/administracion', - icon: Settings, - disabled: true, - }, + { label: 'Integraciones', href: '/integraciones', icon: Zap, disabled: true }, + { label: 'Administración', href: '/administracion', icon: Settings, disabled: true }, ] -export function SidebarNav() { +const adminItems: NavItem[] = [ + { label: 'Usuarios', href: '/usuarios', icon: Users }, + { label: 'Crear Usuario', href: '/usuarios/nuevo', icon: UserPlus }, + { label: 'Roles', href: '/admin/roles', icon: ShieldCheck }, + { label: 'Permisos', href: '/admin/permisos', icon: KeyRound }, +] + +interface SidebarNavProps { + /** When true forces expanded layout regardless of persisted state — used by mobile Sheet. */ + forceExpanded?: boolean +} + +export function SidebarNav({ forceExpanded = false }: SidebarNavProps) { const { pathname } = useLocation() const user = useAuthStore((s) => s.user) const isAdmin = user?.rol === 'admin' + const { collapsed: persisted, toggle } = useSidebar() + const collapsed = forceExpanded ? false : persisted + + function isItemActive(item: NavItem): boolean { + if (item.disabled) return false + if (item.href === '/') return pathname === '/' + if (item.href === '/usuarios') { + return pathname.startsWith('/usuarios') && pathname !== '/usuarios/nuevo' + } + if (item.href === '/usuarios/nuevo') return pathname === '/usuarios/nuevo' + return pathname.startsWith(item.href) + } return ( -