feat: add theme-based layouts
Each theme now has a unique layout: - Light: Standard fixed sidebar - Vibrant: Horizontal top navigation - Corporate: Compact sidebar (expands on hover) - Dark: Floating sidebar with glass effect Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,13 +5,37 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { useThemeStore } from '@/stores/theme-store';
|
import { useThemeStore } from '@/stores/theme-store';
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
import { themes, type ThemeName } from '@/themes';
|
import { themes, type ThemeName } from '@/themes';
|
||||||
import { Check, Palette, User, Building } from 'lucide-react';
|
import { Check, Palette, User, Building, Sidebar, PanelTop, Minimize2, Sparkles } from 'lucide-react';
|
||||||
|
|
||||||
const themeOptions: { name: ThemeName; label: string; description: string }[] = [
|
const themeOptions: { name: ThemeName; label: string; description: string; layoutDesc: string; layoutIcon: typeof Sidebar }[] = [
|
||||||
{ name: 'light', label: 'Light', description: 'Tema claro profesional' },
|
{
|
||||||
{ name: 'vibrant', label: 'Vibrant', description: 'Colores vivos y modernos' },
|
name: 'light',
|
||||||
{ name: 'corporate', label: 'Corporate', description: 'Diseño empresarial denso' },
|
label: 'Light',
|
||||||
{ name: 'dark', label: 'Dark', description: 'Modo oscuro con acentos neón' },
|
description: 'Tema claro profesional',
|
||||||
|
layoutDesc: 'Sidebar estándar fijo',
|
||||||
|
layoutIcon: Sidebar,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vibrant',
|
||||||
|
label: 'Vibrant',
|
||||||
|
description: 'Colores vivos y modernos',
|
||||||
|
layoutDesc: 'Navegación horizontal superior',
|
||||||
|
layoutIcon: PanelTop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'corporate',
|
||||||
|
label: 'Corporate',
|
||||||
|
description: 'Diseño empresarial denso',
|
||||||
|
layoutDesc: 'Sidebar compacto (expande al hover)',
|
||||||
|
layoutIcon: Minimize2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dark',
|
||||||
|
label: 'Dark',
|
||||||
|
description: 'Modo oscuro con acentos neón',
|
||||||
|
layoutDesc: 'Sidebar flotante con efecto glass',
|
||||||
|
layoutIcon: Sparkles,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function ConfiguracionPage() {
|
export default function ConfiguracionPage() {
|
||||||
@@ -71,41 +95,72 @@ export default function ConfiguracionPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
<Palette className="h-4 w-4" />
|
<Palette className="h-4 w-4" />
|
||||||
Tema Visual
|
Tema Visual y Layout
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Elige el tema que mejor se adapte a tu preferencia
|
Cada tema incluye colores y distribución de elementos únicos
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{themeOptions.map((option) => (
|
{themeOptions.map((option) => {
|
||||||
<button
|
const LayoutIcon = option.layoutIcon;
|
||||||
key={option.name}
|
return (
|
||||||
onClick={() => setTheme(option.name)}
|
<button
|
||||||
className={`relative p-4 rounded-lg border-2 text-left transition-all ${
|
key={option.name}
|
||||||
theme === option.name
|
onClick={() => setTheme(option.name)}
|
||||||
? 'border-primary bg-primary/5'
|
className={`relative p-4 rounded-lg border-2 text-left transition-all ${
|
||||||
: 'border-border hover:border-primary/50'
|
theme === option.name
|
||||||
}`}
|
? 'border-primary bg-primary/5 shadow-md'
|
||||||
>
|
: 'border-border hover:border-primary/50'
|
||||||
{theme === option.name && (
|
}`}
|
||||||
<div className="absolute top-2 right-2">
|
>
|
||||||
<Check className="h-4 w-4 text-primary" />
|
{theme === option.name && (
|
||||||
|
<div className="absolute top-3 right-3">
|
||||||
|
<div className="h-6 w-6 rounded-full bg-primary flex items-center justify-center">
|
||||||
|
<Check className="h-4 w-4 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Color Preview */}
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<div
|
||||||
|
className="h-12 w-12 rounded-lg shadow-inner"
|
||||||
|
style={{
|
||||||
|
background: `hsl(${themes[option.name].cssVars['--primary']})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="h-12 w-8 rounded-lg shadow-inner"
|
||||||
|
style={{
|
||||||
|
background: `hsl(${themes[option.name].cssVars['--secondary']})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="h-12 w-8 rounded-lg shadow-inner"
|
||||||
|
style={{
|
||||||
|
background: `hsl(${themes[option.name].cssVars['--accent']})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div
|
{/* Theme Info */}
|
||||||
className="h-20 rounded-md mb-3"
|
<p className="font-semibold text-lg mb-1">{option.label}</p>
|
||||||
style={{
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
background: `hsl(${themes[option.name].cssVars['--primary']})`,
|
{option.description}
|
||||||
}}
|
</p>
|
||||||
/>
|
|
||||||
<p className="font-medium">{option.label}</p>
|
{/* Layout Info */}
|
||||||
<p className="text-xs text-muted-foreground">
|
<div className="flex items-center gap-2 pt-3 border-t">
|
||||||
{option.description}
|
<LayoutIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
</p>
|
<span className="text-xs text-muted-foreground">
|
||||||
</button>
|
{option.layoutDesc}
|
||||||
))}
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
|
import { useThemeStore } from '@/stores/theme-store';
|
||||||
|
import { themes } from '@/themes';
|
||||||
import { Sidebar } from '@/components/layouts/sidebar';
|
import { Sidebar } from '@/components/layouts/sidebar';
|
||||||
|
import { TopNav } from '@/components/layouts/topnav';
|
||||||
|
import { SidebarCompact } from '@/components/layouts/sidebar-compact';
|
||||||
|
import { SidebarFloating } from '@/components/layouts/sidebar-floating';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export default function DashboardLayout({
|
export default function DashboardLayout({
|
||||||
children,
|
children,
|
||||||
@@ -12,6 +18,10 @@ export default function DashboardLayout({
|
|||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isAuthenticated } = useAuthStore();
|
const { isAuthenticated } = useAuthStore();
|
||||||
|
const { theme } = useThemeStore();
|
||||||
|
|
||||||
|
const currentTheme = themes[theme];
|
||||||
|
const layout = currentTheme.layout;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
@@ -23,10 +33,42 @@ export default function DashboardLayout({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render layout based on theme
|
||||||
|
const renderNavigation = () => {
|
||||||
|
switch (layout) {
|
||||||
|
case 'topnav':
|
||||||
|
return <TopNav />;
|
||||||
|
case 'sidebar-compact':
|
||||||
|
return <SidebarCompact />;
|
||||||
|
case 'sidebar-floating':
|
||||||
|
return <SidebarFloating />;
|
||||||
|
case 'sidebar-standard':
|
||||||
|
default:
|
||||||
|
return <Sidebar />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContentClasses = () => {
|
||||||
|
switch (layout) {
|
||||||
|
case 'topnav':
|
||||||
|
return 'pt-16'; // Top padding for fixed top nav
|
||||||
|
case 'sidebar-compact':
|
||||||
|
return 'pl-16'; // Small left padding for compact sidebar
|
||||||
|
case 'sidebar-floating':
|
||||||
|
return 'pl-72 pr-4 py-4'; // Padding for floating sidebar
|
||||||
|
case 'sidebar-standard':
|
||||||
|
default:
|
||||||
|
return 'pl-64'; // Standard sidebar width
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className={cn(
|
||||||
<Sidebar />
|
'min-h-screen bg-background',
|
||||||
<div className="pl-64">
|
layout === 'sidebar-floating' && 'bg-gradient-to-br from-background via-background to-muted/20'
|
||||||
|
)}>
|
||||||
|
{renderNavigation()}
|
||||||
|
<div className={getContentClasses()}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
130
apps/web/components/layouts/sidebar-compact.tsx
Normal file
130
apps/web/components/layouts/sidebar-compact.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
FileText,
|
||||||
|
Calculator,
|
||||||
|
Settings,
|
||||||
|
LogOut,
|
||||||
|
BarChart3,
|
||||||
|
Calendar,
|
||||||
|
Bell,
|
||||||
|
Users,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
|
import { logout } from '@/lib/api/auth';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||||
|
{ name: 'CFDI', href: '/cfdi', icon: FileText },
|
||||||
|
{ name: 'Impuestos', href: '/impuestos', icon: Calculator },
|
||||||
|
{ name: 'Reportes', href: '/reportes', icon: BarChart3 },
|
||||||
|
{ name: 'Calendario', href: '/calendario', icon: Calendar },
|
||||||
|
{ name: 'Alertas', href: '/alertas', icon: Bell },
|
||||||
|
{ name: 'Usuarios', href: '/usuarios', icon: Users },
|
||||||
|
{ name: 'Configuración', href: '/configuracion', icon: Settings },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SidebarCompact() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const { user, logout: clearAuth } = useAuthStore();
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await logout();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
} finally {
|
||||||
|
clearAuth();
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className={cn(
|
||||||
|
'fixed left-0 top-0 z-40 h-screen border-r bg-card transition-all duration-300',
|
||||||
|
expanded ? 'w-64' : 'w-16'
|
||||||
|
)}
|
||||||
|
onMouseEnter={() => setExpanded(true)}
|
||||||
|
onMouseLeave={() => setExpanded(false)}
|
||||||
|
>
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex h-14 items-center border-b px-4">
|
||||||
|
<Link href="/dashboard" className="flex items-center gap-2">
|
||||||
|
<div className="h-8 w-8 rounded bg-primary flex items-center justify-center flex-shrink-0">
|
||||||
|
<span className="text-primary-foreground font-bold text-lg">H</span>
|
||||||
|
</div>
|
||||||
|
<span className={cn(
|
||||||
|
'font-bold text-lg whitespace-nowrap transition-opacity duration-300',
|
||||||
|
expanded ? 'opacity-100' : 'opacity-0 w-0 overflow-hidden'
|
||||||
|
)}>
|
||||||
|
Horux360
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="flex-1 space-y-1 px-2 py-3">
|
||||||
|
{navigation.map((item) => {
|
||||||
|
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 rounded px-2 py-2 text-sm font-medium transition-colors',
|
||||||
|
isActive
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||||
|
)}
|
||||||
|
title={!expanded ? item.name : undefined}
|
||||||
|
>
|
||||||
|
<item.icon className="h-5 w-5 flex-shrink-0" />
|
||||||
|
<span className={cn(
|
||||||
|
'whitespace-nowrap transition-opacity duration-300',
|
||||||
|
expanded ? 'opacity-100' : 'opacity-0 w-0 overflow-hidden'
|
||||||
|
)}>
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* User & Logout */}
|
||||||
|
<div className="border-t p-2">
|
||||||
|
{expanded && (
|
||||||
|
<div className="mb-2 px-2 py-1">
|
||||||
|
<p className="text-xs font-medium truncate">{user?.nombre}</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">{user?.email}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className={cn(
|
||||||
|
'flex w-full items-center gap-3 rounded px-2 py-2 text-sm font-medium text-muted-foreground hover:bg-destructive hover:text-destructive-foreground transition-colors'
|
||||||
|
)}
|
||||||
|
title={!expanded ? 'Cerrar sesión' : undefined}
|
||||||
|
>
|
||||||
|
<LogOut className="h-5 w-5 flex-shrink-0" />
|
||||||
|
<span className={cn(
|
||||||
|
'whitespace-nowrap transition-opacity duration-300',
|
||||||
|
expanded ? 'opacity-100' : 'opacity-0 w-0 overflow-hidden'
|
||||||
|
)}>
|
||||||
|
Cerrar sesión
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
111
apps/web/components/layouts/sidebar-floating.tsx
Normal file
111
apps/web/components/layouts/sidebar-floating.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
FileText,
|
||||||
|
Calculator,
|
||||||
|
Settings,
|
||||||
|
LogOut,
|
||||||
|
BarChart3,
|
||||||
|
Calendar,
|
||||||
|
Bell,
|
||||||
|
Users,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
|
import { logout } from '@/lib/api/auth';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||||
|
{ name: 'CFDI', href: '/cfdi', icon: FileText },
|
||||||
|
{ name: 'Impuestos', href: '/impuestos', icon: Calculator },
|
||||||
|
{ name: 'Reportes', href: '/reportes', icon: BarChart3 },
|
||||||
|
{ name: 'Calendario', href: '/calendario', icon: Calendar },
|
||||||
|
{ name: 'Alertas', href: '/alertas', icon: Bell },
|
||||||
|
{ name: 'Usuarios', href: '/usuarios', icon: Users },
|
||||||
|
{ name: 'Config', href: '/configuracion', icon: Settings },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SidebarFloating() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const { user, logout: clearAuth } = useAuthStore();
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await logout();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
} finally {
|
||||||
|
clearAuth();
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="fixed left-4 top-4 bottom-4 z-40 w-64 rounded-2xl border border-border/50 bg-card/80 backdrop-blur-xl shadow-2xl shadow-primary/5">
|
||||||
|
<div className="flex h-full flex-col p-4">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex items-center gap-3 mb-6 px-2">
|
||||||
|
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg shadow-primary/25">
|
||||||
|
<span className="text-primary-foreground font-bold text-xl">H</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-bold text-lg block">Horux360</span>
|
||||||
|
<span className="text-xs text-muted-foreground">Análisis Fiscal</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="flex-1 space-y-1">
|
||||||
|
{navigation.map((item) => {
|
||||||
|
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200',
|
||||||
|
isActive
|
||||||
|
? 'bg-primary/20 text-primary shadow-sm shadow-primary/20 border border-primary/30'
|
||||||
|
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon className={cn(
|
||||||
|
'h-5 w-5 transition-transform',
|
||||||
|
isActive && 'scale-110'
|
||||||
|
)} />
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* User & Logout */}
|
||||||
|
<div className="mt-4 pt-4 border-t border-border/50">
|
||||||
|
<div className="flex items-center gap-3 px-2 mb-3">
|
||||||
|
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-secondary to-muted flex items-center justify-center">
|
||||||
|
<span className="text-foreground font-medium">
|
||||||
|
{user?.nombre?.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate">{user?.nombre}</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">{user?.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-muted-foreground hover:bg-destructive/20 hover:text-destructive transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut className="h-5 w-5" />
|
||||||
|
Cerrar sesión
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
118
apps/web/components/layouts/topnav.tsx
Normal file
118
apps/web/components/layouts/topnav.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
FileText,
|
||||||
|
Calculator,
|
||||||
|
Settings,
|
||||||
|
LogOut,
|
||||||
|
BarChart3,
|
||||||
|
Calendar,
|
||||||
|
Bell,
|
||||||
|
Users,
|
||||||
|
ChevronDown,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
|
import { logout } from '@/lib/api/auth';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||||
|
{ name: 'CFDI', href: '/cfdi', icon: FileText },
|
||||||
|
{ name: 'Impuestos', href: '/impuestos', icon: Calculator },
|
||||||
|
{ name: 'Reportes', href: '/reportes', icon: BarChart3 },
|
||||||
|
{ name: 'Calendario', href: '/calendario', icon: Calendar },
|
||||||
|
{ name: 'Alertas', href: '/alertas', icon: Bell },
|
||||||
|
{ name: 'Usuarios', href: '/usuarios', icon: Users },
|
||||||
|
{ name: 'Config', href: '/configuracion', icon: Settings },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function TopNav() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const { user, logout: clearAuth } = useAuthStore();
|
||||||
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await logout();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
} finally {
|
||||||
|
clearAuth();
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="fixed top-0 left-0 right-0 z-40 h-16 border-b bg-card">
|
||||||
|
<div className="flex h-full items-center px-6">
|
||||||
|
{/* Logo */}
|
||||||
|
<Link href="/dashboard" className="flex items-center gap-2 mr-8">
|
||||||
|
<div className="h-8 w-8 rounded-lg bg-primary flex items-center justify-center">
|
||||||
|
<span className="text-primary-foreground font-bold text-lg">H</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-bold text-xl">Horux360</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="flex-1 flex items-center gap-1">
|
||||||
|
{navigation.map((item) => {
|
||||||
|
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
||||||
|
isActive
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon className="h-4 w-4" />
|
||||||
|
<span className="hidden lg:inline">{item.name}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* User Menu */}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
||||||
|
className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium hover:bg-accent transition-colors"
|
||||||
|
>
|
||||||
|
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||||
|
<span className="text-primary font-medium text-sm">
|
||||||
|
{user?.nombre?.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="hidden md:inline">{user?.nombre}</span>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{userMenuOpen && (
|
||||||
|
<div className="absolute right-0 top-full mt-2 w-48 rounded-lg border bg-card shadow-lg">
|
||||||
|
<div className="p-3 border-b">
|
||||||
|
<p className="text-sm font-medium">{user?.nombre}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{user?.email}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-destructive hover:bg-destructive/10 transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4" />
|
||||||
|
Cerrar sesión
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export const corporateTheme = {
|
export const corporateTheme = {
|
||||||
name: 'corporate' as const,
|
name: 'corporate' as const,
|
||||||
label: 'Corporate',
|
label: 'Corporate',
|
||||||
layout: 'multi-panel',
|
layout: 'sidebar-compact' as const,
|
||||||
cssVars: {
|
cssVars: {
|
||||||
'--background': '210 20% 96%',
|
'--background': '210 20% 96%',
|
||||||
'--foreground': '210 50% 10%',
|
'--foreground': '210 50% 10%',
|
||||||
@@ -24,9 +24,4 @@ export const corporateTheme = {
|
|||||||
'--ring': '210 100% 25%',
|
'--ring': '210 100% 25%',
|
||||||
'--radius': '0.25rem',
|
'--radius': '0.25rem',
|
||||||
},
|
},
|
||||||
sidebar: {
|
|
||||||
width: '200px',
|
|
||||||
collapsible: false,
|
|
||||||
},
|
|
||||||
density: 'compact',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const darkTheme = {
|
export const darkTheme = {
|
||||||
name: 'dark' as const,
|
name: 'dark' as const,
|
||||||
label: 'Dark',
|
label: 'Dark',
|
||||||
layout: 'minimal-floating',
|
layout: 'sidebar-floating' as const,
|
||||||
cssVars: {
|
cssVars: {
|
||||||
'--background': '0 0% 3.9%',
|
'--background': '0 0% 3.9%',
|
||||||
'--foreground': '0 0% 98%',
|
'--foreground': '0 0% 98%',
|
||||||
@@ -24,13 +24,4 @@ export const darkTheme = {
|
|||||||
'--ring': '187.2 85.7% 53.3%',
|
'--ring': '187.2 85.7% 53.3%',
|
||||||
'--radius': '0.75rem',
|
'--radius': '0.75rem',
|
||||||
},
|
},
|
||||||
sidebar: {
|
|
||||||
width: '64px',
|
|
||||||
collapsible: false,
|
|
||||||
iconsOnly: true,
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
blur: '10px',
|
|
||||||
glow: '0 0 20px rgba(34,211,238,0.3)',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ export const themes = {
|
|||||||
|
|
||||||
export type ThemeName = keyof typeof themes;
|
export type ThemeName = keyof typeof themes;
|
||||||
export type Theme = (typeof themes)[ThemeName];
|
export type Theme = (typeof themes)[ThemeName];
|
||||||
|
export type LayoutType = 'sidebar-standard' | 'topnav' | 'sidebar-compact' | 'sidebar-floating';
|
||||||
|
|
||||||
export { lightTheme, vibrantTheme, corporateTheme, darkTheme };
|
export { lightTheme, vibrantTheme, corporateTheme, darkTheme };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const lightTheme = {
|
export const lightTheme = {
|
||||||
name: 'light' as const,
|
name: 'light' as const,
|
||||||
label: 'Light',
|
label: 'Light',
|
||||||
layout: 'sidebar-fixed',
|
layout: 'sidebar-standard' as const,
|
||||||
cssVars: {
|
cssVars: {
|
||||||
'--background': '0 0% 100%',
|
'--background': '0 0% 100%',
|
||||||
'--foreground': '222.2 84% 4.9%',
|
'--foreground': '222.2 84% 4.9%',
|
||||||
@@ -24,8 +24,4 @@ export const lightTheme = {
|
|||||||
'--ring': '221.2 83.2% 53.3%',
|
'--ring': '221.2 83.2% 53.3%',
|
||||||
'--radius': '0.5rem',
|
'--radius': '0.5rem',
|
||||||
},
|
},
|
||||||
sidebar: {
|
|
||||||
width: '240px',
|
|
||||||
collapsible: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const vibrantTheme = {
|
export const vibrantTheme = {
|
||||||
name: 'vibrant' as const,
|
name: 'vibrant' as const,
|
||||||
label: 'Vibrant',
|
label: 'Vibrant',
|
||||||
layout: 'sidebar-collapsible',
|
layout: 'topnav' as const,
|
||||||
cssVars: {
|
cssVars: {
|
||||||
'--background': '270 50% 98%',
|
'--background': '270 50% 98%',
|
||||||
'--foreground': '263.4 84% 6.7%',
|
'--foreground': '263.4 84% 6.7%',
|
||||||
@@ -24,8 +24,4 @@ export const vibrantTheme = {
|
|||||||
'--ring': '262.1 83.3% 57.8%',
|
'--ring': '262.1 83.3% 57.8%',
|
||||||
'--radius': '1rem',
|
'--radius': '1rem',
|
||||||
},
|
},
|
||||||
sidebar: {
|
|
||||||
width: '280px',
|
|
||||||
collapsible: true,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user