feat(ux-ui): update layout, providers and config for UX remediation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-02-03 19:25:56 -06:00
parent a3b61b8ae4
commit 6568b9bfed
4 changed files with 662 additions and 104 deletions

View File

@ -16,36 +16,73 @@ import {
ChevronDown, ChevronDown,
LogOut, LogOut,
Users2, Users2,
Search,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@utils/cn'; import { cn } from '@utils/cn';
import { useUIStore } from '@stores/useUIStore'; import { useUIStore } from '@stores/useUIStore';
import { useAuthStore } from '@stores/useAuthStore'; import { useAuthStore } from '@stores/useAuthStore';
import { useIsMobile } from '@hooks/useMediaQuery'; import { useIsMobile } from '@hooks/useMediaQuery';
import { useFilteredNavigation, type NavigationItem } from '@hooks/useFilteredNavigation';
import { ThemeToggle } from '@components/atoms/ThemeToggle';
import { CommandPaletteWithRouter, useCommandPalette } from '@components/organisms/CommandPalette';
interface DashboardLayoutProps { interface DashboardLayoutProps {
children: ReactNode; children: ReactNode;
} }
const navigation = [ /**
* Search button component that opens the command palette
*/
function SearchButton() {
const { open } = useCommandPalette();
const isMac = typeof navigator !== 'undefined' && navigator.platform.toUpperCase().indexOf('MAC') >= 0;
return (
<button
onClick={open}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-1.5',
'border border-gray-200 bg-gray-50 text-gray-500',
'hover:bg-gray-100 hover:text-gray-700',
'dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400',
'dark:hover:bg-gray-600 dark:hover:text-gray-300',
'transition-colors duration-150'
)}
aria-label="Buscar"
>
<Search className="h-4 w-4" />
<span className="hidden text-sm sm:inline">Buscar...</span>
<kbd className="hidden rounded border border-gray-300 bg-white px-1.5 py-0.5 text-[10px] font-medium text-gray-500 sm:inline dark:border-gray-500 dark:bg-gray-600 dark:text-gray-400">
{isMac ? '⌘' : 'Ctrl'}+K
</kbd>
</button>
);
}
const navigation: NavigationItem[] = [
{ name: 'Dashboard', href: '/dashboard', icon: Home }, { name: 'Dashboard', href: '/dashboard', icon: Home },
{ name: 'Usuarios', href: '/users', icon: Users }, { name: 'Usuarios', href: '/users', icon: Users, module: 'users' },
{ name: 'Empresas', href: '/companies', icon: Building2 }, { name: 'Empresas', href: '/companies', icon: Building2, module: 'companies' },
{ name: 'Partners', href: '/partners', icon: Users2 }, { name: 'Partners', href: '/partners', icon: Users2, module: 'partners' },
{ name: 'Inventario', href: '/inventory', icon: Package }, { name: 'Inventario', href: '/inventory', icon: Package, module: 'inventory' },
{ name: 'Ventas', href: '/sales', icon: ShoppingCart }, { name: 'Ventas', href: '/sales', icon: ShoppingCart, module: 'sales' },
{ name: 'Compras', href: '/purchases', icon: ShoppingCart }, { name: 'Compras', href: '/purchases', icon: ShoppingCart, module: 'purchases' },
{ name: 'Finanzas', href: '/financial', icon: Receipt }, { name: 'Finanzas', href: '/financial', icon: Receipt, module: 'finance' },
{ name: 'Proyectos', href: '/projects', icon: FolderKanban }, { name: 'Proyectos', href: '/projects', icon: FolderKanban, module: 'projects' },
{ name: 'CRM', href: '/crm', icon: UserCircle }, { name: 'CRM', href: '/crm', icon: UserCircle, module: 'crm' },
{ name: 'RRHH', href: '/hr', icon: Users }, { name: 'RRHH', href: '/hr', icon: Users, module: 'hr' },
{ name: 'Configuración', href: '/settings', icon: Settings }, { name: 'Configuración', href: '/settings', icon: Settings, roles: ['admin', 'super_admin'] },
]; ];
export function DashboardLayout({ children }: DashboardLayoutProps) { /**
* Internal layout component (used inside CommandPaletteWithRouter)
*/
function DashboardLayoutInner({ children }: DashboardLayoutProps) {
const location = useLocation(); const location = useLocation();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { sidebarOpen, sidebarCollapsed, toggleSidebar, setSidebarOpen, setIsMobile } = useUIStore(); const { sidebarOpen, sidebarCollapsed, toggleSidebar, setSidebarOpen, setIsMobile } = useUIStore();
const { user, logout } = useAuthStore(); const { user, logout } = useAuthStore();
const { items: filteredNavigation, isLoading: isNavigationLoading } = useFilteredNavigation(navigation);
useEffect(() => { useEffect(() => {
setIsMobile(isMobile); setIsMobile(isMobile);
@ -59,11 +96,11 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
}, [location.pathname, isMobile, setSidebarOpen]); }, [location.pathname, isMobile, setSidebarOpen]);
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Mobile sidebar backdrop */} {/* Mobile sidebar backdrop */}
{isMobile && sidebarOpen && ( {isMobile && sidebarOpen && (
<div <div
className="fixed inset-0 z-40 bg-gray-600/75" className="fixed inset-0 z-40 bg-gray-600/75 dark:bg-gray-900/80"
onClick={() => setSidebarOpen(false)} onClick={() => setSidebarOpen(false)}
/> />
)} )}
@ -71,7 +108,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
{/* Sidebar */} {/* Sidebar */}
<aside <aside
className={cn( className={cn(
'fixed inset-y-0 left-0 z-50 flex flex-col bg-white shadow-lg transition-all duration-300', 'fixed inset-y-0 left-0 z-50 flex flex-col bg-white shadow-lg transition-all duration-300 dark:bg-gray-800 dark:shadow-gray-900/50',
isMobile isMobile
? sidebarOpen ? sidebarOpen
? 'translate-x-0' ? 'translate-x-0'
@ -82,17 +119,17 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
)} )}
> >
{/* Logo */} {/* Logo */}
<div className="flex h-16 items-center justify-between border-b px-4"> <div className="flex h-16 items-center justify-between border-b px-4 dark:border-gray-700">
{(!sidebarCollapsed || isMobile) && ( {(!sidebarCollapsed || isMobile) && (
<Link to="/dashboard" className="flex items-center"> <Link to="/dashboard" className="flex items-center">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-600"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-600">
<span className="text-lg font-bold text-white">E</span> <span className="text-lg font-bold text-white">E</span>
</div> </div>
<span className="ml-2 text-lg font-bold text-gray-900">ERP</span> <span className="ml-2 text-lg font-bold text-gray-900 dark:text-white">ERP</span>
</Link> </Link>
)} )}
{isMobile && ( {isMobile && (
<button onClick={() => setSidebarOpen(false)} className="p-2"> <button onClick={() => setSidebarOpen(false)} className="p-2 dark:text-gray-400" aria-label="Cerrar menú">
<X className="h-5 w-5" /> <X className="h-5 w-5" />
</button> </button>
)} )}
@ -100,8 +137,14 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 space-y-1 overflow-y-auto p-2"> <nav className="flex-1 space-y-1 overflow-y-auto p-2">
{navigation.map((item) => { {isNavigationLoading ? (
<div className="flex items-center justify-center py-4">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary-600 border-t-transparent" />
</div>
) : (
filteredNavigation.map((item) => {
const isActive = location.pathname.startsWith(item.href); const isActive = location.pathname.startsWith(item.href);
const Icon = item.icon;
return ( return (
<Link <Link
key={item.name} key={item.name}
@ -109,36 +152,38 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
className={cn( className={cn(
'flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors', 'flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive isActive
? 'bg-primary-50 text-primary-700' ? 'bg-primary-50 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400'
: 'text-gray-700 hover:bg-gray-100' : 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'
)} )}
> >
<item.icon className={cn('h-5 w-5 flex-shrink-0', isActive ? 'text-primary-600' : 'text-gray-400')} /> {Icon && <Icon className={cn('h-5 w-5 flex-shrink-0', isActive ? 'text-primary-600 dark:text-primary-400' : 'text-gray-400 dark:text-gray-500')} />}
{(!sidebarCollapsed || isMobile) && ( {(!sidebarCollapsed || isMobile) && (
<span className="ml-3">{item.name}</span> <span className="ml-3">{item.name}</span>
)} )}
</Link> </Link>
); );
})} })
)}
</nav> </nav>
{/* User menu */} {/* User menu */}
<div className="border-t p-4"> <div className="border-t p-4 dark:border-gray-700">
{(!sidebarCollapsed || isMobile) ? ( {(!sidebarCollapsed || isMobile) ? (
<div className="flex items-center"> <div className="flex items-center">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary-100 text-primary-700"> <div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400">
{user?.firstName?.[0]}{user?.lastName?.[0]} {user?.firstName?.[0]}{user?.lastName?.[0]}
</div> </div>
<div className="ml-3 flex-1 overflow-hidden"> <div className="ml-3 flex-1 overflow-hidden">
<p className="truncate text-sm font-medium text-gray-900"> <p className="truncate text-sm font-medium text-gray-900 dark:text-white">
{user?.firstName} {user?.lastName} {user?.firstName} {user?.lastName}
</p> </p>
<p className="truncate text-xs text-gray-500">{user?.email}</p> <p className="truncate text-xs text-gray-500 dark:text-gray-400">{user?.email}</p>
</div> </div>
<button <button
onClick={logout} onClick={logout}
className="p-2 text-gray-400 hover:text-gray-600" className="p-2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
title="Cerrar sesión" title="Cerrar sesión"
aria-label="Cerrar sesión"
> >
<LogOut className="h-4 w-4" /> <LogOut className="h-4 w-4" />
</button> </button>
@ -146,8 +191,9 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
) : ( ) : (
<button <button
onClick={logout} onClick={logout}
className="flex w-full items-center justify-center p-2 text-gray-400 hover:text-gray-600" className="flex w-full items-center justify-center p-2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
title="Cerrar sesión" title="Cerrar sesión"
aria-label="Cerrar sesión"
> >
<LogOut className="h-5 w-5" /> <LogOut className="h-5 w-5" />
</button> </button>
@ -163,23 +209,28 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
)} )}
> >
{/* Top bar */} {/* Top bar */}
<header className="sticky top-0 z-30 flex h-16 items-center justify-between border-b bg-white px-4 shadow-sm"> <header className="sticky top-0 z-30 flex h-16 items-center justify-between border-b bg-white px-4 shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div className="flex items-center gap-4">
<button <button
onClick={toggleSidebar} onClick={toggleSidebar}
className="rounded-lg p-2 hover:bg-gray-100" className="rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-700"
aria-label="Abrir menú"
> >
<Menu className="h-5 w-5" /> <Menu className="h-5 w-5 dark:text-gray-400" />
</button> </button>
<SearchButton />
</div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<button className="relative rounded-lg p-2 hover:bg-gray-100"> <ThemeToggle />
<Bell className="h-5 w-5 text-gray-500" /> <button className="relative rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-700" aria-label="Notificaciones">
<Bell className="h-5 w-5 text-gray-500 dark:text-gray-400" />
<span className="absolute right-1 top-1 flex h-4 w-4 items-center justify-center rounded-full bg-danger-500 text-xs text-white"> <span className="absolute right-1 top-1 flex h-4 w-4 items-center justify-center rounded-full bg-danger-500 text-xs text-white">
3 3
</span> </span>
</button> </button>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary-100 text-sm font-medium text-primary-700"> <div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary-100 text-sm font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-400">
{user?.firstName?.[0]}{user?.lastName?.[0]} {user?.firstName?.[0]}{user?.lastName?.[0]}
</div> </div>
<ChevronDown className="h-4 w-4 text-gray-400" /> <ChevronDown className="h-4 w-4 text-gray-400" />
@ -193,3 +244,15 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
</div> </div>
); );
} }
/**
* Dashboard Layout with Command Palette support
* Wraps the layout with CommandPaletteWithRouter for Cmd+K / Ctrl+K navigation
*/
export function DashboardLayout({ children }: DashboardLayoutProps) {
return (
<CommandPaletteWithRouter>
<DashboardLayoutInner>{children}</DashboardLayoutInner>
</CommandPaletteWithRouter>
);
}

View File

@ -1,5 +1,6 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { ToastContainer } from '@components/organisms/Toast'; import { ToastContainer } from '@components/organisms/Toast';
import { ThemeProvider } from '@shared/providers/ThemeProvider';
interface AppProvidersProps { interface AppProvidersProps {
children: ReactNode; children: ReactNode;
@ -7,9 +8,9 @@ interface AppProvidersProps {
export function AppProviders({ children }: AppProvidersProps) { export function AppProviders({ children }: AppProvidersProps) {
return ( return (
<> <ThemeProvider>
{children} {children}
<ToastContainer /> <ToastContainer />
</> </ThemeProvider>
); );
} }

View File

@ -2,39 +2,240 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* =============================================================================
CSS VARIABLES - Design Tokens para Runtime Theming
============================================================================= */
@layer base { @layer base {
:root {
/* =========================================================================
RGB VALUES FOR TAILWIND ALPHA SUPPORT
Format: R G B (space-separated for rgb(var(--color) / alpha) syntax)
========================================================================= */
/* Primary Palette - ISEM Blue (RGB values) */
--color-primary-50: 230 243 255;
--color-primary-100: 204 231 255;
--color-primary-200: 153 207 255;
--color-primary-300: 102 183 255;
--color-primary-400: 51 159 255;
--color-primary-500: 0 97 168;
--color-primary-600: 0 77 134;
--color-primary-700: 0 58 101;
--color-primary-800: 0 38 67;
--color-primary-900: 0 19 34;
/* Secondary Palette - ISEM Green (RGB values) */
--color-secondary-50: 230 255 245;
--color-secondary-100: 204 255 235;
--color-secondary-200: 153 255 214;
--color-secondary-300: 102 255 194;
--color-secondary-400: 51 255 173;
--color-secondary-500: 0 168 104;
--color-secondary-600: 0 134 83;
--color-secondary-700: 0 101 63;
--color-secondary-800: 0 67 42;
--color-secondary-900: 0 34 21;
/* Success Palette (RGB values) */
--color-success-50: 209 231 221;
--color-success-100: 209 231 221;
--color-success-500: 25 135 84;
--color-success-600: 21 115 71;
--color-success-700: 15 81 50;
/* Warning Palette (RGB values) */
--color-warning-50: 255 243 205;
--color-warning-100: 255 243 205;
--color-warning-500: 255 193 7;
--color-warning-600: 224 168 0;
--color-warning-700: 102 77 3;
/* Danger Palette (RGB values) */
--color-danger-50: 248 215 218;
--color-danger-100: 248 215 218;
--color-danger-500: 220 53 69;
--color-danger-600: 187 45 59;
--color-danger-700: 132 32 41;
/* Info Palette (RGB values) */
--color-info-50: 207 244 252;
--color-info-100: 207 244 252;
--color-info-500: 13 202 240;
--color-info-600: 10 162 192;
--color-info-700: 5 81 96;
/* Background Colors (RGB values) - Light Theme */
--color-background: 255 255 255;
--color-background-subtle: 248 249 251;
--color-background-muted: 241 243 245;
--color-background-emphasis: 233 236 239;
/* Foreground Colors (RGB values) - Light Theme */
--color-foreground: 33 37 41;
--color-foreground-muted: 108 117 125;
--color-foreground-subtle: 173 181 189;
/* Border Colors (RGB values) - Light Theme */
--color-border: 222 226 230;
--color-border-subtle: 233 236 239;
--color-border-emphasis: 206 212 218;
/* Surface Colors (RGB values) - Light Theme */
--color-surface: 249 250 251;
--color-surface-hover: 243 244 246;
--color-surface-card: 255 255 255;
--color-surface-popover: 255 255 255;
--color-surface-modal: 255 255 255;
--color-surface-dropdown: 255 255 255;
/* =========================================================================
LEGACY HEX VALUES (Backward Compatibility)
Keep these for direct CSS usage and gradual migration
========================================================================= */
/* Colores de Marca ISEM (HEX) */
--color-brand-primary: #0061A8;
--color-brand-secondary: #00A868;
/* Colores Semanticos (HEX) */
--color-success-hex: #198754;
--color-success-light-hex: #D1E7DD;
--color-success-dark-hex: #0F5132;
--color-warning-hex: #FFC107;
--color-warning-light-hex: #FFF3CD;
--color-warning-dark-hex: #664D03;
--color-danger-hex: #DC3545;
--color-danger-light-hex: #F8D7DA;
--color-danger-dark-hex: #842029;
--color-info-hex: #0DCAF0;
--color-info-light-hex: #CFF4FC;
--color-info-dark-hex: #055160;
/* Semantic Aliases (HEX for legacy components) */
--color-success: #198754;
--color-success-light: #D1E7DD;
--color-success-dark: #0F5132;
--color-warning: #FFC107;
--color-warning-light: #FFF3CD;
--color-warning-dark: #664D03;
--color-danger: #DC3545;
--color-danger-light: #F8D7DA;
--color-danger-dark: #842029;
--color-info: #0DCAF0;
--color-info-light: #CFF4FC;
--color-info-dark: #055160;
/* Primary/Secondary HEX aliases */
--color-primary-hex: var(--color-brand-primary);
--color-secondary-hex: var(--color-brand-secondary);
/* Sombras Tema Claro */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-default: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
/* Tipografia */
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', Consolas, Monaco, monospace;
/* Animaciones */
--duration-fast: 150ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
--easing-default: cubic-bezier(0.4, 0, 0.2, 1);
/* Bordes */
--radius-sm: 0.125rem;
--radius-default: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-full: 9999px;
}
/* Tema Oscuro */
.dark {
/* Background Colors (RGB values) - Dark Theme */
--color-background: 27 30 35;
--color-background-subtle: 33 37 41;
--color-background-muted: 45 49 57;
--color-background-emphasis: 52 58 64;
/* Foreground Colors (RGB values) - Dark Theme */
--color-foreground: 236 236 236;
--color-foreground-muted: 160 160 160;
--color-foreground-subtle: 108 117 125;
/* Border Colors (RGB values) - Dark Theme */
--color-border: 73 80 87;
--color-border-subtle: 52 58 64;
--color-border-emphasis: 108 117 125;
/* Surface Colors (RGB values) - Dark Theme */
--color-surface: 31 41 55;
--color-surface-hover: 55 65 81;
--color-surface-card: 45 49 57;
--color-surface-popover: 52 58 64;
--color-surface-modal: 45 49 57;
--color-surface-dropdown: 52 58 64;
/* Sombras Tema Oscuro */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-default: 0 1px 3px 0 rgb(0 0 0 / 0.4), 0 1px 2px -1px rgb(0 0 0 / 0.4);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.4);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.4);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.4);
}
html { html {
@apply antialiased; @apply antialiased;
font-family: var(--font-sans);
} }
body { body {
@apply bg-gray-50 text-gray-900; background-color: rgb(var(--color-background));
color: rgb(var(--color-foreground));
} }
* { * {
@apply border-gray-200; border-color: rgb(var(--color-border));
} }
} }
/* =============================================================================
COMPONENTES BASE
============================================================================= */
@layer components { @layer components {
/* Botones */
.btn { .btn {
@apply inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50; @apply inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
transition-duration: var(--duration-fast);
} }
.btn-primary { .btn-primary {
@apply bg-primary-600 text-white hover:bg-primary-700 focus-visible:ring-primary-500; @apply bg-primary-500 text-white hover:bg-primary-600 focus-visible:ring-primary-500;
} }
.btn-secondary { .btn-secondary {
@apply bg-secondary-100 text-secondary-900 hover:bg-secondary-200 focus-visible:ring-secondary-500; @apply bg-secondary-500 text-white hover:bg-secondary-600 focus-visible:ring-secondary-500;
} }
.btn-outline { .btn-outline {
@apply border border-gray-300 bg-white hover:bg-gray-50 focus-visible:ring-gray-500; @apply border border-border-emphasis bg-transparent hover:bg-background-muted focus-visible:ring-primary-500;
color: rgb(var(--color-foreground));
}
.btn-ghost {
@apply bg-transparent hover:bg-background-muted focus-visible:ring-primary-500;
color: rgb(var(--color-foreground));
} }
.btn-danger { .btn-danger {
@apply bg-danger-600 text-white hover:bg-danger-700 focus-visible:ring-danger-500; @apply bg-danger-500 text-white hover:bg-danger-600 focus-visible:ring-danger-500;
} }
.btn-sm { .btn-sm {
@ -49,31 +250,125 @@
@apply h-12 px-6 text-lg; @apply h-12 px-6 text-lg;
} }
/* Inputs */
.input { .input {
@apply block w-full rounded-md border border-gray-300 px-3 py-2 text-sm placeholder-gray-400 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500; @apply block w-full rounded-md border px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50;
background-color: rgb(var(--color-background));
border-color: rgb(var(--color-border));
color: rgb(var(--color-foreground));
transition-duration: var(--duration-fast);
}
.input::placeholder {
color: rgb(var(--color-foreground-subtle));
}
.input:focus {
border-color: rgb(var(--color-primary-500));
--tw-ring-color: rgb(var(--color-primary-500));
} }
.input-error { .input-error {
@apply border-danger-500 focus:border-danger-500 focus:ring-danger-500; @apply border-danger-500 focus:border-danger-500 focus:ring-danger-500;
} }
/* Labels */
.label { .label {
@apply block text-sm font-medium text-gray-700; @apply block text-sm font-medium;
color: rgb(var(--color-foreground));
} }
/* Cards */
.card { .card {
@apply rounded-lg border bg-white shadow-sm; @apply rounded-lg border;
background-color: rgb(var(--color-surface-card));
border-color: rgb(var(--color-border));
box-shadow: var(--shadow-default);
} }
/* Links */
.link { .link {
@apply text-primary-600 hover:text-primary-700 hover:underline; @apply hover:underline;
color: rgb(var(--color-primary-500));
transition-duration: var(--duration-fast);
}
.link:hover {
color: rgb(var(--color-primary-600));
}
/* Badges */
.badge {
@apply inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium;
}
.badge-primary {
@apply bg-primary-100 text-primary-700;
}
.badge-secondary {
@apply bg-secondary-100 text-secondary-700;
}
.badge-success {
background-color: var(--color-success-light);
color: var(--color-success-dark);
}
.badge-warning {
background-color: var(--color-warning-light);
color: var(--color-warning-dark);
}
.badge-danger {
background-color: var(--color-danger-light);
color: var(--color-danger-dark);
}
.badge-info {
background-color: var(--color-info-light);
color: var(--color-info-dark);
}
/* Alerts */
.alert {
@apply rounded-lg border p-4;
}
.alert-success {
background-color: var(--color-success-light);
border-color: var(--color-success);
color: var(--color-success-dark);
}
.alert-warning {
background-color: var(--color-warning-light);
border-color: var(--color-warning);
color: var(--color-warning-dark);
}
.alert-danger {
background-color: var(--color-danger-light);
border-color: var(--color-danger);
color: var(--color-danger-dark);
}
.alert-info {
background-color: var(--color-info-light);
border-color: var(--color-info);
color: var(--color-info-dark);
} }
} }
/* =============================================================================
UTILIDADES
============================================================================= */
@layer utilities { @layer utilities {
/* Scrollbar personalizado */
.scrollbar-thin { .scrollbar-thin {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: theme('colors.gray.300') transparent; scrollbar-color: rgb(var(--color-border)) transparent;
} }
.scrollbar-thin::-webkit-scrollbar { .scrollbar-thin::-webkit-scrollbar {
@ -86,7 +381,50 @@
} }
.scrollbar-thin::-webkit-scrollbar-thumb { .scrollbar-thin::-webkit-scrollbar-thumb {
background-color: theme('colors.gray.300'); background-color: rgb(var(--color-border));
border-radius: 3px; border-radius: 3px;
} }
/* Focus visible mejorado */
.focus-ring {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
--tw-ring-color: rgb(var(--color-primary-500));
} }
/* Texto truncado */
.truncate-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.truncate-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Transiciones suaves */
.transition-theme {
transition-property: background-color, border-color, color, fill, stroke;
transition-duration: var(--duration-normal);
transition-timing-function: var(--easing-default);
}
}
/* =============================================================================
MULTI-TENANT THEMING (Placeholder para override por tenant)
============================================================================= */
/*
* Los tenants pueden sobreescribir estas variables via JS:
* document.documentElement.style.setProperty('--color-brand-primary', '#FF0000');
*
* O cargar un CSS adicional con:
* [data-tenant="empresa-abc"] {
* --color-brand-primary: #123456;
* --color-brand-secondary: #654321;
* }
*/

View File

@ -1,78 +1,234 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
// Helper function to create CSS variable color with alpha support
const withAlpha = (variableName) => `rgb(var(${variableName}) / <alpha-value>)`
export default { export default {
content: [ content: [
"./index.html", "./index.html",
"./src/**/*.{js,ts,jsx,tsx}", "./src/**/*.{js,ts,jsx,tsx}",
], ],
darkMode: 'class',
theme: { theme: {
extend: { extend: {
colors: { colors: {
// =================================================================
// CSS VARIABLE COLORS (Dynamic Theming Support)
// These reference CSS variables defined in index.css
// Supports alpha via: bg-primary-500/50, text-foreground/80, etc.
// =================================================================
// Primary Palette - Uses CSS Variables
primary: { primary: {
50: '#f0f9ff', 50: withAlpha('--color-primary-50'),
100: '#e0f2fe', 100: withAlpha('--color-primary-100'),
200: '#bae6fd', 200: withAlpha('--color-primary-200'),
300: '#7dd3fc', 300: withAlpha('--color-primary-300'),
400: '#38bdf8', 400: withAlpha('--color-primary-400'),
500: '#0ea5e9', 500: withAlpha('--color-primary-500'),
600: '#0284c7', 600: withAlpha('--color-primary-600'),
700: '#0369a1', 700: withAlpha('--color-primary-700'),
800: '#075985', 800: withAlpha('--color-primary-800'),
900: '#0c4a6e', 900: withAlpha('--color-primary-900'),
950: '#082f49', DEFAULT: withAlpha('--color-primary-500'),
}, },
// Secondary Palette - Uses CSS Variables
secondary: { secondary: {
50: '#f8fafc', 50: withAlpha('--color-secondary-50'),
100: '#f1f5f9', 100: withAlpha('--color-secondary-100'),
200: '#e2e8f0', 200: withAlpha('--color-secondary-200'),
300: '#cbd5e1', 300: withAlpha('--color-secondary-300'),
400: '#94a3b8', 400: withAlpha('--color-secondary-400'),
500: '#64748b', 500: withAlpha('--color-secondary-500'),
600: '#475569', 600: withAlpha('--color-secondary-600'),
700: '#334155', 700: withAlpha('--color-secondary-700'),
800: '#1e293b', 800: withAlpha('--color-secondary-800'),
900: '#0f172a', 900: withAlpha('--color-secondary-900'),
950: '#020617', DEFAULT: withAlpha('--color-secondary-500'),
}, },
// Semantic Colors - Uses CSS Variables
success: { success: {
50: '#f0fdf4', 50: withAlpha('--color-success-50'),
500: '#22c55e', 100: withAlpha('--color-success-100'),
600: '#16a34a', 500: withAlpha('--color-success-500'),
700: '#15803d', 600: withAlpha('--color-success-600'),
700: withAlpha('--color-success-700'),
DEFAULT: withAlpha('--color-success-500'),
light: withAlpha('--color-success-50'),
dark: withAlpha('--color-success-700'),
}, },
warning: { warning: {
50: '#fffbeb', 50: withAlpha('--color-warning-50'),
500: '#f59e0b', 100: withAlpha('--color-warning-100'),
600: '#d97706', 500: withAlpha('--color-warning-500'),
700: '#b45309', 600: withAlpha('--color-warning-600'),
700: withAlpha('--color-warning-700'),
DEFAULT: withAlpha('--color-warning-500'),
light: withAlpha('--color-warning-50'),
dark: withAlpha('--color-warning-700'),
}, },
danger: { danger: {
50: '#fef2f2', 50: withAlpha('--color-danger-50'),
500: '#ef4444', 100: withAlpha('--color-danger-100'),
600: '#dc2626', 500: withAlpha('--color-danger-500'),
700: '#b91c1c', 600: withAlpha('--color-danger-600'),
700: withAlpha('--color-danger-700'),
DEFAULT: withAlpha('--color-danger-500'),
light: withAlpha('--color-danger-50'),
dark: withAlpha('--color-danger-700'),
},
info: {
50: withAlpha('--color-info-50'),
100: withAlpha('--color-info-100'),
500: withAlpha('--color-info-500'),
600: withAlpha('--color-info-600'),
700: withAlpha('--color-info-700'),
DEFAULT: withAlpha('--color-info-500'),
light: withAlpha('--color-info-50'),
dark: withAlpha('--color-info-700'),
},
// Neutral Colors - Uses CSS Variables (auto dark/light)
background: {
DEFAULT: withAlpha('--color-background'),
subtle: withAlpha('--color-background-subtle'),
muted: withAlpha('--color-background-muted'),
emphasis: withAlpha('--color-background-emphasis'),
},
foreground: {
DEFAULT: withAlpha('--color-foreground'),
muted: withAlpha('--color-foreground-muted'),
subtle: withAlpha('--color-foreground-subtle'),
},
border: {
DEFAULT: withAlpha('--color-border'),
subtle: withAlpha('--color-border-subtle'),
emphasis: withAlpha('--color-border-emphasis'),
},
surface: {
DEFAULT: withAlpha('--color-surface'),
hover: withAlpha('--color-surface-hover'),
card: withAlpha('--color-surface-card'),
popover: withAlpha('--color-surface-popover'),
modal: withAlpha('--color-surface-modal'),
dropdown: withAlpha('--color-surface-dropdown'),
},
// =================================================================
// STATIC HEX COLORS (Backward Compatibility)
// Keep these for legacy code - prefix with 'static-'
// =================================================================
'static-primary': {
50: '#E6F3FF',
100: '#CCE7FF',
200: '#99CFFF',
300: '#66B7FF',
400: '#339FFF',
500: '#0061A8',
600: '#004D86',
700: '#003A65',
800: '#002643',
900: '#001322',
},
'static-secondary': {
50: '#E6FFF5',
100: '#CCFFEB',
200: '#99FFD6',
300: '#66FFC2',
400: '#33FFAD',
500: '#00A868',
600: '#008653',
700: '#00653F',
800: '#00432A',
900: '#002215',
}, },
}, },
fontFamily: { fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'], sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'monospace'],
},
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.25rem' }],
base: ['1rem', { lineHeight: '1.5rem' }],
lg: ['1.125rem', { lineHeight: '1.75rem' }],
xl: ['1.25rem', { lineHeight: '1.75rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
}, },
spacing: { spacing: {
'18': '4.5rem', '18': '4.5rem',
'88': '22rem', '88': '22rem',
}, },
borderRadius: {
sm: '0.125rem',
DEFAULT: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
xl: '0.75rem',
'2xl': '1rem',
'3xl': '1.5rem',
},
boxShadow: {
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
'2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)',
inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)',
},
zIndex: {
dropdown: '1000',
sticky: '1020',
fixed: '1030',
modalBackdrop: '1040',
modal: '1050',
popover: '1060',
tooltip: '1070',
toast: '1080',
},
animation: { animation: {
'fade-in': 'fadeIn 0.2s ease-out', 'fade-in': 'fadeIn 0.2s ease-out',
'fade-out': 'fadeOut 0.2s ease-in',
'slide-in': 'slideIn 0.2s ease-out', 'slide-in': 'slideIn 0.2s ease-out',
'slide-out': 'slideOut 0.2s ease-in',
'spin-slow': 'spin 2s linear infinite', 'spin-slow': 'spin 2s linear infinite',
'pulse-slow': 'pulse 3s ease-in-out infinite',
}, },
keyframes: { keyframes: {
fadeIn: { fadeIn: {
'0%': { opacity: '0' }, '0%': { opacity: '0' },
'100%': { opacity: '1' }, '100%': { opacity: '1' },
}, },
fadeOut: {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
slideIn: { slideIn: {
'0%': { transform: 'translateY(-10px)', opacity: '0' }, '0%': { transform: 'translateY(-10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' }, '100%': { transform: 'translateY(0)', opacity: '1' },
}, },
slideOut: {
'0%': { transform: 'translateY(0)', opacity: '1' },
'100%': { transform: 'translateY(-10px)', opacity: '0' },
},
},
transitionDuration: {
'0': '0ms',
'75': '75ms',
'100': '100ms',
'150': '150ms',
'200': '200ms',
'300': '300ms',
'500': '500ms',
'700': '700ms',
'1000': '1000ms',
},
transitionTimingFunction: {
'ease-in': 'cubic-bezier(0.4, 0, 1, 1)',
'ease-out': 'cubic-bezier(0, 0, 0.2, 1)',
'ease-in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
}, },
}, },
}, },