From 83d76b95d489f3e7b6f230bc2dff3609e65f194c Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 16 Apr 2026 11:26:41 -0300 Subject: [PATCH] feat(web): tooltips Radix + toggle sidebar al lado del brand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cambios: - Instalado @radix-ui/react-tooltip 1.2.8 (componente faltante de shadcn/ui que faltaba en el set inicial). - Nuevo src/web/src/components/ui/tooltip.tsx (shadcn pattern): TooltipProvider, Tooltip, TooltipTrigger, TooltipContent con animaciones data-state (fade + zoom + slide direccional). - App.tsx: TooltipProvider envuelve toda la app con delayDuration 150ms. - AppSidebar refactorizado: - Toggle button MOVIDO al header (top), al lado izquierdo del nombre 'SIG-CM 2.0'. Eliminado boton bottom (era redundante). - Cuando collapsed: solo el toggle visible centrado (68px width). - Cuando expanded: [Toggle] [SIG-CM 2.0] aligned left. - Quitado overflow-hidden del aside (era lo que impedia que los tooltips fueran visibles — los clipping containers padres tampoco importan ahora porque Radix portalea el tooltip a body). - Tooltips en TODOS los items collapsed (incluido el toggle) y en items disabled muestra 'Label · Próximamente'. - Eliminado el componente CSS-only SidebarTooltip (reemplazado por Radix que se renderiza fuera del DOM tree con Portal). El bug original era que tanto el aside con overflow-hidden como el ProtectedLayout con overflow-hidden clipean cualquier elemento que intente escapar via absolute positioning. Radix Portal soluciona eso renderizando el tooltip en document.body. Tests 136/136 verde. --- src/web/package-lock.json | 91 ++++++++++++++ src/web/package.json | 1 + src/web/src/App.tsx | 11 +- src/web/src/components/layout/AppSidebar.tsx | 125 +++++++++---------- src/web/src/components/ui/tooltip.tsx | 33 +++++ 5 files changed, 191 insertions(+), 70 deletions(-) create mode 100644 src/web/src/components/ui/tooltip.tsx diff --git a/src/web/package-lock.json b/src/web/package-lock.json index db4dd82..54939a6 100644 --- a/src/web/package-lock.json +++ b/src/web/package-lock.json @@ -19,6 +19,7 @@ "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.99.0", "axios": "1.7", "class-variance-authority": "^0.7.1", @@ -2723,6 +2724,96 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", diff --git a/src/web/package.json b/src/web/package.json index 2f0bde3..7366720 100644 --- a/src/web/package.json +++ b/src/web/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.99.0", "axios": "1.7", "class-variance-authority": "^0.7.1", diff --git a/src/web/src/App.tsx b/src/web/src/App.tsx index 8286e98..d669adf 100644 --- a/src/web/src/App.tsx +++ b/src/web/src/App.tsx @@ -1,6 +1,7 @@ import { BrowserRouter } from 'react-router-dom' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { Toaster } from 'sonner' +import { TooltipProvider } from '@/components/ui/tooltip' import { AppRoutes } from './router' const queryClient = new QueryClient({ @@ -13,10 +14,12 @@ const queryClient = new QueryClient({ function App() { return ( - - - - + + + + + + ) } diff --git a/src/web/src/components/layout/AppSidebar.tsx b/src/web/src/components/layout/AppSidebar.tsx index a92e9d3..0ca5055 100644 --- a/src/web/src/components/layout/AppSidebar.tsx +++ b/src/web/src/components/layout/AppSidebar.tsx @@ -11,10 +11,10 @@ import { KeyRound, PanelLeftClose, PanelLeftOpen, - Newspaper, } from 'lucide-react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useAuthStore } from '@/stores/authStore' import { useSidebar } from '@/hooks/useSidebar' @@ -65,18 +65,42 @@ export function SidebarNav({ forceExpanded = false }: SidebarNavProps) { return ( ) } @@ -151,12 +149,13 @@ function NavRow({ item, collapsed, active }: NavRowProps) { const Icon = item.icon const baseClasses = cn( - 'group/item relative flex items-center rounded-md text-sm transition-colors', - collapsed ? 'justify-center h-10 px-0' : 'gap-3 px-3 py-2', + 'relative flex items-center rounded-md text-sm transition-colors', + collapsed ? 'justify-center h-10 w-10 mx-auto' : 'gap-3 px-3 py-2', ) + // Disabled item if (item.disabled) { - return ( + const content = (
)} - {collapsed && {item.label} · Próximamente}
) + + if (!collapsed) return content + return ( + + {content} + + {item.label}{' '} + · Próximamente + + + ) } - return ( + // Active link + const link = ( - {/* Active indicator bar (left edge) — sutil cuando expanded, dot cuando collapsed */} + {/* Active indicator bar (left edge) when expanded */} {active && !collapsed && ( )} {!collapsed && {item.label}} - {collapsed && {item.label}} ) + + if (!collapsed) return link + return ( + + {link} + {item.label} + + ) } function SectionLabel({ @@ -208,9 +225,7 @@ function SectionLabel({ children: React.ReactNode }) { if (collapsed) { - return ( -