2025-08-28 10:41:04 +02:00
|
|
|
import React, { useState, useCallback, forwardRef } from 'react';
|
|
|
|
|
import { clsx } from 'clsx';
|
2025-08-28 23:40:44 +02:00
|
|
|
import { useNavigate } from 'react-router-dom';
|
2025-08-28 10:41:04 +02:00
|
|
|
import { useAuthUser, useIsAuthenticated, useAuthActions } from '../../../stores';
|
|
|
|
|
import { useTheme } from '../../../contexts/ThemeContext';
|
2025-08-31 22:14:05 +02:00
|
|
|
import { useBakery } from '../../../contexts/BakeryContext';
|
2025-08-28 10:41:04 +02:00
|
|
|
import { Button } from '../../ui';
|
|
|
|
|
import { Avatar } from '../../ui';
|
|
|
|
|
import { Badge } from '../../ui';
|
|
|
|
|
import { Modal } from '../../ui';
|
2025-08-31 22:14:05 +02:00
|
|
|
import { BakerySelector } from '../../ui/BakerySelector/BakerySelector';
|
2025-08-28 10:41:04 +02:00
|
|
|
import {
|
|
|
|
|
Menu,
|
|
|
|
|
Search,
|
|
|
|
|
Bell,
|
|
|
|
|
Sun,
|
|
|
|
|
Moon,
|
|
|
|
|
Computer,
|
|
|
|
|
Settings,
|
|
|
|
|
User,
|
|
|
|
|
LogOut,
|
|
|
|
|
X
|
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
export interface HeaderProps {
|
|
|
|
|
className?: string;
|
|
|
|
|
/**
|
|
|
|
|
* Callback when menu button is clicked (for mobile sidebar toggle)
|
|
|
|
|
*/
|
|
|
|
|
onMenuClick?: () => void;
|
|
|
|
|
/**
|
|
|
|
|
* Whether the sidebar is currently collapsed (affects logo display)
|
|
|
|
|
*/
|
|
|
|
|
sidebarCollapsed?: boolean;
|
|
|
|
|
/**
|
|
|
|
|
* Show/hide search functionality
|
|
|
|
|
*/
|
|
|
|
|
showSearch?: boolean;
|
|
|
|
|
/**
|
|
|
|
|
* Show/hide notifications
|
|
|
|
|
*/
|
|
|
|
|
showNotifications?: boolean;
|
|
|
|
|
/**
|
|
|
|
|
* Show/hide theme toggle
|
|
|
|
|
*/
|
|
|
|
|
showThemeToggle?: boolean;
|
|
|
|
|
/**
|
|
|
|
|
* Show/hide user menu
|
|
|
|
|
*/
|
|
|
|
|
showUserMenu?: boolean;
|
|
|
|
|
/**
|
|
|
|
|
* Custom logo component
|
|
|
|
|
*/
|
|
|
|
|
logo?: React.ReactNode;
|
|
|
|
|
/**
|
|
|
|
|
* Custom search placeholder
|
|
|
|
|
*/
|
|
|
|
|
searchPlaceholder?: string;
|
|
|
|
|
/**
|
|
|
|
|
* Notification count
|
|
|
|
|
*/
|
|
|
|
|
notificationCount?: number;
|
|
|
|
|
/**
|
|
|
|
|
* Custom notification handler
|
|
|
|
|
*/
|
|
|
|
|
onNotificationClick?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface HeaderRef {
|
|
|
|
|
focusSearch: () => void;
|
|
|
|
|
toggleUserMenu: () => void;
|
|
|
|
|
closeUserMenu: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Header - Top navigation header with logo, user menu, notifications, theme toggle
|
|
|
|
|
*
|
|
|
|
|
* Features:
|
|
|
|
|
* - Logo/brand area with responsive sizing
|
|
|
|
|
* - Global search functionality with keyboard shortcuts
|
|
|
|
|
* - User avatar with dropdown menu
|
|
|
|
|
* - Notifications bell with badge count
|
|
|
|
|
* - Theme toggle button (light/dark/system)
|
|
|
|
|
* - Mobile hamburger menu integration
|
|
|
|
|
* - Keyboard navigation support
|
|
|
|
|
*/
|
|
|
|
|
export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
|
|
|
|
className,
|
|
|
|
|
onMenuClick,
|
|
|
|
|
sidebarCollapsed = false,
|
|
|
|
|
showSearch = true,
|
|
|
|
|
showNotifications = true,
|
|
|
|
|
showThemeToggle = true,
|
|
|
|
|
showUserMenu = true,
|
|
|
|
|
logo,
|
|
|
|
|
searchPlaceholder = 'Buscar...',
|
|
|
|
|
notificationCount = 0,
|
|
|
|
|
onNotificationClick,
|
|
|
|
|
}, ref) => {
|
2025-08-28 23:40:44 +02:00
|
|
|
const navigate = useNavigate();
|
2025-08-28 10:41:04 +02:00
|
|
|
const user = useAuthUser();
|
|
|
|
|
const isAuthenticated = useIsAuthenticated();
|
|
|
|
|
const { logout } = useAuthActions();
|
|
|
|
|
const { theme, resolvedTheme, setTheme } = useTheme();
|
2025-08-31 22:14:05 +02:00
|
|
|
const { bakeries, currentBakery, selectBakery } = useBakery();
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
|
|
|
|
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
|
|
|
|
const [searchValue, setSearchValue] = useState('');
|
|
|
|
|
const [isThemeMenuOpen, setIsThemeMenuOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
// Focus search input
|
|
|
|
|
const focusSearch = useCallback(() => {
|
|
|
|
|
searchInputRef.current?.focus();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Toggle user menu
|
|
|
|
|
const toggleUserMenu = useCallback(() => {
|
|
|
|
|
setIsUserMenuOpen(prev => !prev);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Close user menu
|
|
|
|
|
const closeUserMenu = useCallback(() => {
|
|
|
|
|
setIsUserMenuOpen(false);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Expose ref methods
|
|
|
|
|
React.useImperativeHandle(ref, () => ({
|
|
|
|
|
focusSearch,
|
|
|
|
|
toggleUserMenu,
|
|
|
|
|
closeUserMenu,
|
|
|
|
|
}), [focusSearch, toggleUserMenu, closeUserMenu]);
|
|
|
|
|
|
|
|
|
|
// Handle search
|
|
|
|
|
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
setSearchValue(e.target.value);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleSearchSubmit = useCallback((e: React.FormEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (searchValue.trim()) {
|
|
|
|
|
// TODO: Implement search functionality
|
|
|
|
|
console.log('Search:', searchValue);
|
|
|
|
|
}
|
|
|
|
|
}, [searchValue]);
|
|
|
|
|
|
|
|
|
|
const clearSearch = useCallback(() => {
|
|
|
|
|
setSearchValue('');
|
|
|
|
|
searchInputRef.current?.focus();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Handle logout
|
|
|
|
|
const handleLogout = useCallback(async () => {
|
|
|
|
|
await logout();
|
|
|
|
|
setIsUserMenuOpen(false);
|
|
|
|
|
}, [logout]);
|
|
|
|
|
|
|
|
|
|
// Keyboard shortcuts
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
// Cmd/Ctrl + K for search
|
|
|
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
focusSearch();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Escape to close menus
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
setIsUserMenuOpen(false);
|
|
|
|
|
setIsThemeMenuOpen(false);
|
|
|
|
|
if (isSearchFocused) {
|
|
|
|
|
searchInputRef.current?.blur();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
}, [focusSearch, isSearchFocused]);
|
|
|
|
|
|
|
|
|
|
// Close menus when clicking outside
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
|
|
|
const target = event.target as Element;
|
|
|
|
|
if (!target.closest('[data-user-menu]')) {
|
|
|
|
|
setIsUserMenuOpen(false);
|
|
|
|
|
}
|
|
|
|
|
if (!target.closest('[data-theme-menu]')) {
|
|
|
|
|
setIsThemeMenuOpen(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click', handleClickOutside);
|
|
|
|
|
return () => document.removeEventListener('click', handleClickOutside);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const themeIcons = {
|
|
|
|
|
light: Sun,
|
|
|
|
|
dark: Moon,
|
|
|
|
|
auto: Computer,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ThemeIcon = themeIcons[theme] || Sun;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<header
|
|
|
|
|
className={clsx(
|
|
|
|
|
'fixed top-0 left-0 right-0 h-[var(--header-height)]',
|
|
|
|
|
'bg-[var(--bg-primary)] border-b border-[var(--border-primary)]',
|
|
|
|
|
'flex items-center justify-between px-4 lg:px-6',
|
|
|
|
|
'transition-all duration-300 ease-in-out',
|
|
|
|
|
'backdrop-blur-sm bg-[var(--bg-primary)]/95',
|
2025-08-28 17:15:29 +02:00
|
|
|
'z-[var(--z-fixed)]',
|
2025-08-28 10:41:04 +02:00
|
|
|
className
|
|
|
|
|
)}
|
|
|
|
|
role="banner"
|
|
|
|
|
aria-label="Navegación principal"
|
|
|
|
|
>
|
|
|
|
|
{/* Left section */}
|
2025-08-31 22:14:05 +02:00
|
|
|
<div className="flex items-center gap-2 sm:gap-4 flex-1 min-w-0 h-full">
|
2025-08-28 10:41:04 +02:00
|
|
|
{/* Mobile menu button */}
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={onMenuClick}
|
2025-08-28 23:40:44 +02:00
|
|
|
className="lg:hidden w-10 h-10 p-0 flex items-center justify-center hover:bg-[var(--bg-secondary)] active:scale-95 transition-all duration-150"
|
2025-08-28 10:41:04 +02:00
|
|
|
aria-label="Abrir menú de navegación"
|
|
|
|
|
>
|
2025-08-28 23:40:44 +02:00
|
|
|
<Menu className="h-5 w-5 text-[var(--text-primary)]" />
|
2025-08-28 10:41:04 +02:00
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* Logo */}
|
2025-08-31 22:14:05 +02:00
|
|
|
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-shrink-0">
|
2025-08-28 10:41:04 +02:00
|
|
|
{logo || (
|
|
|
|
|
<>
|
2025-08-28 17:15:29 +02:00
|
|
|
<div className="w-8 h-8 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
|
2025-08-28 10:41:04 +02:00
|
|
|
PI
|
|
|
|
|
</div>
|
|
|
|
|
<h1 className={clsx(
|
|
|
|
|
'font-semibold text-[var(--text-primary)] transition-opacity duration-300',
|
2025-08-31 22:14:05 +02:00
|
|
|
'hidden md:block text-lg leading-tight whitespace-nowrap',
|
2025-08-28 17:15:29 +02:00
|
|
|
'self-center',
|
2025-08-28 10:41:04 +02:00
|
|
|
sidebarCollapsed ? 'lg:block' : 'lg:hidden xl:block'
|
|
|
|
|
)}>
|
|
|
|
|
Panadería IA
|
|
|
|
|
</h1>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-08-31 22:14:05 +02:00
|
|
|
{/* Bakery Selector - Desktop */}
|
|
|
|
|
{isAuthenticated && currentBakery && bakeries.length > 0 && (
|
|
|
|
|
<div className="hidden md:block mx-2 lg:mx-4 flex-shrink-0">
|
|
|
|
|
<BakerySelector
|
|
|
|
|
bakeries={bakeries}
|
|
|
|
|
selectedBakery={currentBakery}
|
|
|
|
|
onSelectBakery={selectBakery}
|
|
|
|
|
onAddBakery={() => {
|
|
|
|
|
// TODO: Navigate to add bakery page or open modal
|
|
|
|
|
console.log('Add new bakery');
|
|
|
|
|
}}
|
|
|
|
|
size="md"
|
|
|
|
|
className="min-w-[160px] max-w-[220px] lg:min-w-[200px] lg:max-w-[280px]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Bakery Selector - Mobile (in title area) */}
|
|
|
|
|
{isAuthenticated && currentBakery && bakeries.length > 0 && (
|
|
|
|
|
<div className="md:hidden flex-1 min-w-0 ml-3">
|
|
|
|
|
<BakerySelector
|
|
|
|
|
bakeries={bakeries}
|
|
|
|
|
selectedBakery={currentBakery}
|
|
|
|
|
onSelectBakery={selectBakery}
|
|
|
|
|
onAddBakery={() => {
|
|
|
|
|
// TODO: Navigate to add bakery page or open modal
|
|
|
|
|
console.log('Add new bakery');
|
|
|
|
|
}}
|
|
|
|
|
size="sm"
|
|
|
|
|
className="w-full max-w-none"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-08-28 10:41:04 +02:00
|
|
|
{/* Search */}
|
|
|
|
|
{showSearch && isAuthenticated && (
|
|
|
|
|
<form
|
|
|
|
|
onSubmit={handleSearchSubmit}
|
|
|
|
|
className="hidden md:flex items-center flex-1 max-w-md mx-4"
|
|
|
|
|
>
|
|
|
|
|
<div className="relative w-full">
|
2025-08-28 17:15:29 +02:00
|
|
|
<div className="absolute left-3 top-0 bottom-0 flex items-center pointer-events-none">
|
|
|
|
|
<Search className="h-4 w-4 text-[var(--text-tertiary)]" />
|
|
|
|
|
</div>
|
2025-08-28 10:41:04 +02:00
|
|
|
<input
|
|
|
|
|
ref={searchInputRef}
|
|
|
|
|
type="text"
|
|
|
|
|
value={searchValue}
|
|
|
|
|
onChange={handleSearchChange}
|
|
|
|
|
onFocus={() => setIsSearchFocused(true)}
|
|
|
|
|
onBlur={() => setIsSearchFocused(false)}
|
|
|
|
|
placeholder={searchPlaceholder}
|
|
|
|
|
className={clsx(
|
2025-08-28 17:15:29 +02:00
|
|
|
'w-full pl-10 pr-12 py-2.5 text-sm',
|
2025-08-28 10:41:04 +02:00
|
|
|
'bg-[var(--bg-secondary)] border border-[var(--border-primary)]',
|
|
|
|
|
'rounded-lg transition-colors duration-200',
|
|
|
|
|
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20',
|
|
|
|
|
'focus:border-[var(--color-primary)]',
|
2025-08-28 17:15:29 +02:00
|
|
|
'placeholder:text-[var(--text-tertiary)]',
|
|
|
|
|
'h-9'
|
2025-08-28 10:41:04 +02:00
|
|
|
)}
|
|
|
|
|
aria-label="Buscar en la aplicación"
|
|
|
|
|
/>
|
2025-08-28 17:15:29 +02:00
|
|
|
{searchValue ? (
|
2025-08-28 10:41:04 +02:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={clearSearch}
|
2025-08-28 17:15:29 +02:00
|
|
|
className="absolute right-3 top-0 bottom-0 flex items-center p-1 hover:bg-[var(--bg-tertiary)] rounded-full transition-colors"
|
2025-08-28 10:41:04 +02:00
|
|
|
aria-label="Limpiar búsqueda"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3 w-3 text-[var(--text-tertiary)]" />
|
|
|
|
|
</button>
|
2025-08-28 17:15:29 +02:00
|
|
|
) : (
|
|
|
|
|
<div className="absolute right-3 top-0 bottom-0 flex items-center pointer-events-none">
|
|
|
|
|
<kbd className="hidden lg:inline-flex items-center justify-center h-5 px-1.5 text-xs text-[var(--text-tertiary)] font-mono bg-[var(--bg-tertiary)] rounded border border-[var(--border-primary)]">
|
|
|
|
|
⌘K
|
|
|
|
|
</kbd>
|
|
|
|
|
</div>
|
2025-08-28 10:41:04 +02:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Right section */}
|
|
|
|
|
{isAuthenticated && (
|
2025-08-30 19:21:15 +02:00
|
|
|
<div className="flex items-center gap-1">
|
2025-08-28 10:41:04 +02:00
|
|
|
{/* Mobile search */}
|
|
|
|
|
{showSearch && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={focusSearch}
|
2025-08-28 17:15:29 +02:00
|
|
|
className="md:hidden w-10 h-10 p-0 flex items-center justify-center"
|
2025-08-28 10:41:04 +02:00
|
|
|
aria-label="Buscar"
|
|
|
|
|
>
|
|
|
|
|
<Search className="h-5 w-5" />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Theme toggle */}
|
|
|
|
|
{showThemeToggle && (
|
|
|
|
|
<div className="relative" data-theme-menu>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setIsThemeMenuOpen(!isThemeMenuOpen)}
|
2025-08-28 17:15:29 +02:00
|
|
|
className="w-10 h-10 p-0 flex items-center justify-center"
|
2025-08-28 10:41:04 +02:00
|
|
|
aria-label={`Tema actual: ${theme}`}
|
|
|
|
|
aria-expanded={isThemeMenuOpen}
|
|
|
|
|
aria-haspopup="true"
|
|
|
|
|
>
|
|
|
|
|
<ThemeIcon className="h-5 w-5" />
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{isThemeMenuOpen && (
|
|
|
|
|
<div className="absolute right-0 top-full mt-2 w-48 bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-lg shadow-lg py-1 z-[var(--z-dropdown)]">
|
|
|
|
|
{[
|
|
|
|
|
{ key: 'light' as const, label: 'Claro', icon: Sun },
|
|
|
|
|
{ key: 'dark' as const, label: 'Oscuro', icon: Moon },
|
|
|
|
|
{ key: 'auto' as const, label: 'Sistema', icon: Computer },
|
|
|
|
|
].map(({ key, label, icon: Icon }) => (
|
|
|
|
|
<button
|
|
|
|
|
key={key}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setTheme(key);
|
|
|
|
|
setIsThemeMenuOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
className={clsx(
|
|
|
|
|
'w-full px-4 py-2 text-left text-sm flex items-center gap-3',
|
|
|
|
|
'hover:bg-[var(--bg-secondary)] transition-colors',
|
|
|
|
|
theme === key && 'bg-[var(--bg-secondary)] text-[var(--color-primary)]'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Icon className="h-4 w-4" />
|
|
|
|
|
{label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Notifications */}
|
|
|
|
|
{showNotifications && (
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={onNotificationClick}
|
2025-08-28 17:15:29 +02:00
|
|
|
className="w-10 h-10 p-0 flex items-center justify-center relative"
|
2025-08-28 10:41:04 +02:00
|
|
|
aria-label={`Notificaciones${notificationCount > 0 ? ` (${notificationCount})` : ''}`}
|
|
|
|
|
>
|
|
|
|
|
<Bell className="h-5 w-5" />
|
|
|
|
|
{notificationCount > 0 && (
|
|
|
|
|
<Badge
|
|
|
|
|
variant="error"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="absolute -top-1 -right-1 min-w-[18px] h-[18px] text-xs flex items-center justify-center"
|
|
|
|
|
>
|
|
|
|
|
{notificationCount > 99 ? '99+' : notificationCount}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* User menu */}
|
|
|
|
|
{showUserMenu && user && (
|
|
|
|
|
<div className="relative" data-user-menu>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={toggleUserMenu}
|
2025-08-30 19:21:15 +02:00
|
|
|
className={clsx(
|
|
|
|
|
"flex items-center gap-2 pl-2 pr-3 py-1.5 h-9 min-w-0 rounded-lg",
|
|
|
|
|
"hover:bg-[var(--bg-secondary)] hover:ring-2 hover:ring-[var(--color-primary)]/20",
|
|
|
|
|
"transition-all duration-200 ease-in-out",
|
|
|
|
|
"active:scale-95",
|
|
|
|
|
isUserMenuOpen && "bg-[var(--bg-secondary)] ring-2 ring-[var(--color-primary)]/20"
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
aria-label="Menú de usuario"
|
|
|
|
|
aria-expanded={isUserMenuOpen}
|
|
|
|
|
aria-haspopup="true"
|
|
|
|
|
>
|
|
|
|
|
<Avatar
|
2025-08-30 19:32:53 +02:00
|
|
|
src={user.avatar}
|
|
|
|
|
alt={user.name}
|
2025-08-31 15:16:40 +02:00
|
|
|
name={user.name}
|
2025-08-30 19:21:15 +02:00
|
|
|
size="xs"
|
|
|
|
|
className={clsx(
|
|
|
|
|
"flex-shrink-0 transition-all duration-200",
|
|
|
|
|
"hover:ring-2 hover:ring-[var(--color-primary)]/30",
|
|
|
|
|
isUserMenuOpen && "ring-2 ring-[var(--color-primary)]/30"
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
/>
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{isUserMenuOpen && (
|
|
|
|
|
<div className="absolute right-0 top-full mt-2 w-56 bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-lg shadow-lg py-2 z-[var(--z-dropdown)]">
|
|
|
|
|
{/* User info */}
|
2025-08-31 15:16:40 +02:00
|
|
|
<div className="px-4 py-3 border-b border-[var(--border-primary)] flex items-center gap-3">
|
|
|
|
|
<Avatar
|
|
|
|
|
src={user.avatar || undefined}
|
|
|
|
|
alt={user.name}
|
|
|
|
|
name={user.name}
|
|
|
|
|
size="sm"
|
|
|
|
|
className="flex-shrink-0"
|
|
|
|
|
/>
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<div className="text-xs text-[var(--text-tertiary)] truncate">
|
|
|
|
|
{user.email}
|
|
|
|
|
</div>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Menu items */}
|
|
|
|
|
<div className="py-1">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
2025-08-28 23:40:44 +02:00
|
|
|
navigate('/app/settings/profile');
|
2025-08-28 10:41:04 +02:00
|
|
|
setIsUserMenuOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-secondary)] transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<User className="h-4 w-4" />
|
|
|
|
|
Perfil
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
2025-08-28 23:40:44 +02:00
|
|
|
navigate('/app/settings');
|
2025-08-28 10:41:04 +02:00
|
|
|
setIsUserMenuOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-secondary)] transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Settings className="h-4 w-4" />
|
|
|
|
|
Configuración
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Logout */}
|
|
|
|
|
<div className="border-t border-[var(--border-primary)] pt-1">
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleLogout}
|
|
|
|
|
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-[var(--bg-secondary)] transition-colors text-[var(--color-error)]"
|
|
|
|
|
>
|
|
|
|
|
<LogOut className="h-4 w-4" />
|
|
|
|
|
Cerrar Sesión
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</header>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Header.displayName = 'Header';
|