feat: update TODO.md and refactor Header component

- Removed redundant theme handling code from Header component, improving readability and maintainability.
- Integrated HeaderMobile and HeaderDesktop components for better responsive design.
- Marked the task for repositioning the theme icon in the header as complete in TODO.md.
This commit is contained in:
Julien Froidefond
2025-10-04 11:06:49 +02:00
parent 89af1fc597
commit ad0b723e00
7 changed files with 413 additions and 376 deletions

View File

@@ -1,15 +1,7 @@
'use client';
import { useTheme } from '@/contexts/ThemeContext';
import { useJiraConfig } from '@/contexts/JiraConfigContext';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { useState } from 'react';
import { Theme, THEME_CONFIG, getThemeIcon } from '@/lib/ui-config';
import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext';
import { AuthButton } from '@/components/AuthButton';
import { useSession, signOut } from 'next-auth/react';
import { Check, X, Menu } from 'lucide-react';
import { HeaderMobile } from './header/HeaderMobile';
import { HeaderDesktop } from './header/HeaderDesktop';
interface HeaderProps {
title?: string;
@@ -18,373 +10,12 @@ interface HeaderProps {
}
export function Header({ title = "TowerControl", subtitle = "Task Management", syncing = false }: HeaderProps) {
const { theme, setTheme } = useTheme();
const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig();
const pathname = usePathname();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [tabletMenuOpen, setTabletMenuOpen] = useState(false);
const [themeDropdownOpen, setThemeDropdownOpen] = useState(false);
const { openModal: openShortcutsModal } = useKeyboardShortcutsModal();
const { data: session } = useSession();
// Liste des thèmes disponibles avec leurs labels et icônes
const themes: { value: Theme; label: string; icon: string }[] = THEME_CONFIG.allThemes.map(themeValue => {
return {
value: themeValue,
label: themeValue.charAt(0).toUpperCase() + themeValue.slice(1).replace('_', ' '),
icon: getThemeIcon(themeValue)
};
});
// Fonction pour déterminer si un lien est actif
const isActiveLink = (href: string) => {
if (href === '/') {
return pathname === '/';
}
return pathname.startsWith(href);
};
// Fonction pour obtenir les classes CSS d'un lien (desktop)
const getLinkClasses = (href: string) => {
const baseClasses = "font-mono text-sm uppercase tracking-wider transition-colors px-3 py-1.5 rounded-md";
if (isActiveLink(href)) {
return `${baseClasses} text-[var(--primary)] bg-[var(--primary)]/10 border border-[var(--primary)]/30`;
}
return `${baseClasses} text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]`;
};
// Fonction pour obtenir les classes CSS d'un lien (mobile)
const getMobileLinkClasses = (href: string) => {
const baseClasses = "font-mono text-sm uppercase tracking-wider transition-colors px-4 py-3 rounded-md block w-full text-left";
if (isActiveLink(href)) {
return `${baseClasses} text-[var(--primary)] bg-[var(--primary)]/10 border border-[var(--primary)]/30`;
}
return `${baseClasses} text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]`;
};
// Liste des liens de navigation
const navLinks = [
{ href: '/', label: 'Dashboard' },
{ href: '/kanban', label: 'Kanban' },
{ href: '/daily', label: 'Daily' },
{ href: '/weekly-manager', label: 'Weekly' },
...(isJiraConfigured ? [{ href: '/jira-dashboard', label: `Jira${jiraConfig?.projectKey ? ` (${jiraConfig.projectKey})` : ''}` }] : []),
{ href: '/settings', label: 'Settings' }
];
return (
<header className="relative z-50 bg-[var(--card)]/80 border-b border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20">
<div className="container mx-auto px-4 sm:px-6 py-4">
{/* Layout mobile/tablette */}
<div className="lg:hidden">
<div className="flex items-center justify-between">
{/* Titre et status */}
<div className="flex items-center gap-3 sm:gap-4 min-w-0 flex-1">
<div className={`w-3 h-3 rounded-full shadow-lg flex-shrink-0 ${
syncing
? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
}`}></div>
<div className="min-w-0">
<h1 className="text-xl sm:text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider truncate">
{title}
</h1>
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-xs sm:text-sm truncate">
{subtitle}
</p>
</div>
</div>
{/* Controls mobile/tablette */}
<div className="flex items-center gap-2 flex-shrink-0">
{/* Keyboard Shortcuts */}
<button
onClick={openShortcutsModal}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
title="Raccourcis clavier (Cmd+?)"
>
</button>
{/* Theme Dropdown */}
<div className="relative">
<button
onClick={() => setThemeDropdownOpen(!themeDropdownOpen)}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
title="Select theme"
>
{themes.find(t => t.value === theme)?.icon || '🎨'}
</button>
{themeDropdownOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[200]"
onClick={() => setThemeDropdownOpen(false)}
/>
{/* Dropdown */}
<div className="absolute right-0 top-full mt-2 w-48 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] overflow-hidden">
<div className="py-2">
{themes.map((themeOption) => (
<button
key={themeOption.value}
onClick={() => {
setTheme(themeOption.value);
setThemeDropdownOpen(false);
}}
className={`w-full text-left px-4 py-2 text-sm transition-colors flex items-center gap-3 ${
theme === themeOption.value
? 'text-[var(--primary)] bg-[var(--primary)]/10'
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--card-hover)]'
}`}
>
<span className="text-base">{themeOption.icon}</span>
<span className="font-mono">{themeOption.label}</span>
{theme === themeOption.value && (
<Check className="w-4 h-4 ml-auto" />
)}
</button>
))}
</div>
</div>
</>
)}
</div>
{/* Menu burger */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
title="Toggle menu"
>
{mobileMenuOpen ? (
<X className="w-5 h-5" />
) : (
<Menu className="w-5 h-5" />
)}
</button>
</div>
</div>
{/* Auth controls à droite mobile - dans la ligne principale */}
<div className="hidden sm:block">
<AuthButton />
</div>
</div>
{/* Ligne Auth séparée sur très petits écrans */}
<div className="lg:hidden sm:hidden flex justify-end pt-2">
<AuthButton />
</div>
{/* Layout desktop - une seule ligne comme avant */}
<div className="hidden lg:flex items-center justify-between gap-6">
{/* Titre et status */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-4 min-w-0">
<div className={`w-3 h-3 rounded-full shadow-lg ${
syncing
? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
}`}></div>
<div className="min-w-0">
<h1 className="text-xl xl:text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider truncate">
<span className="sm:hidden">{title}</span>
<span className="hidden sm:inline">{title}</span>
</h1>
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-xs sm:text-sm truncate">
{subtitle}
</p>
</div>
</div>
{/* Navigation desktop */}
<nav className="flex items-center gap-1 xl:gap-2 flex-wrap">
{navLinks.slice(0, 4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={`${getLinkClasses(href)} text-xs xl:text-sm`}
>
{label}
</Link>
))}
{/* Plus d'éléments sur très grands écrans */}
<div className="hidden 2xl:flex items-center gap-1">
{navLinks.slice(4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={getLinkClasses(href)}
>
{label}
</Link>
))}
</div>
{/* Menu dropdown pour écrans moyens */}
<div className="xl:hidden relative">
<button
onClick={() => setTabletMenuOpen(!tabletMenuOpen)}
className="font-mono text-xs uppercase tracking-wider transition-colors px-2 py-1.5 rounded-md text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]"
title="Plus de liens"
>
</button>
{tabletMenuOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[200]"
onClick={() => setTabletMenuOpen(false)}
/>
{/* Menu items */}
<div className="absolute right-0 top-full mt-2 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] py-2 min-w-[119px]">
{navLinks.slice(4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={`block px-4 py-2 text-sm transition-colors ${getMobileLinkClasses(href)}`}
onClick={() => setTabletMenuOpen(false)}
>
{label}
</Link>
))}
</div>
</>
)}
</div>
{/* Keyboard Shortcuts desktop */}
<button
onClick={openShortcutsModal}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-1 rounded-md hover:bg-[var(--card-hover)]"
title="Raccourcis clavier (Cmd+?)"
>
</button>
{/* Theme Dropdown desktop */}
<div className="relative">
<button
onClick={() => setThemeDropdownOpen(!themeDropdownOpen)}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-1 rounded-md hover:bg-[var(--card-hover)]"
title="Select theme"
>
{themes.find(t => t.value === theme)?.icon || '🎨'}
</button>
{themeDropdownOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[200]"
onClick={() => setThemeDropdownOpen(false)}
/>
{/* Dropdown */}
<div className="absolute right-0 top-full mt-2 w-48 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] overflow-hidden">
<div className="py-2">
{themes.map((themeOption) => (
<button
key={themeOption.value}
onClick={() => {
setTheme(themeOption.value);
setThemeDropdownOpen(false);
}}
className={`w-full text-left px-4 py-2 text-sm transition-colors flex items-center gap-3 ${
theme === themeOption.value
? 'text-[var(--primary)] bg-[var(--primary)]/10'
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--card-hover)]'
}`}
>
<span className="text-base">{themeOption.icon}</span>
<span className="font-mono">{themeOption.label}</span>
{theme === themeOption.value && (
<Check className="w-4 h-4 ml-auto" />
)}
</button>
))}
</div>
</div>
</>
)}
</div>
</nav>
</div>
{/* Contrôles à droite */}
<div className="flex items-center gap-2">
<AuthButton />
</div>
</div>
<HeaderMobile title={title} subtitle={subtitle} syncing={syncing} />
<HeaderDesktop title={title} subtitle={subtitle} syncing={syncing} />
</div>
{/* Menu mobile/tablette en modal */}
{mobileMenuOpen && (
<div className="lg:hidden fixed inset-0 z-[9999] flex items-start justify-center pt-20">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50"
onClick={() => setMobileMenuOpen(false)}
/>
{/* Modal content */}
<div className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl mx-4 w-full max-w-sm">
<nav className="p-6">
<div className="space-y-3">
{navLinks.map(({ href, label }) => (
<Link
key={href}
href={href}
className={getMobileLinkClasses(href)}
onClick={() => setMobileMenuOpen(false)}
>
{label}
</Link>
))}
{/* Séparateur */}
{session && (
<>
<div className="border-t border-[var(--border)]/30 my-4"></div>
{/* Lien profil */}
<Link
href="/profile"
className={getMobileLinkClasses('/profile')}
onClick={() => setMobileMenuOpen(false)}
>
Profil
</Link>
{/* Bouton déconnexion */}
<button
onClick={() => {
signOut({ callbackUrl: '/login' });
setMobileMenuOpen(false);
}}
className="font-mono text-sm uppercase tracking-wider transition-colors px-4 py-3 rounded-md block w-full text-left text-[var(--destructive)] hover:text-[var(--destructive)] hover:bg-[var(--destructive)]/10"
>
Déconnexion
</button>
</>
)}
</div>
</nav>
</div>
</div>
)}
</header>
);
}

View File

@@ -0,0 +1,59 @@
'use client';
import { useState } from 'react';
import { ThemeDropdown } from './ThemeDropdown';
import { HeaderNavigation } from './HeaderNavigation';
interface HeaderControlsProps {
variant: 'desktop' | 'mobile';
className?: string;
}
export function HeaderControls({ variant, className = '' }: HeaderControlsProps) {
const [tabletMenuOpen, setTabletMenuOpen] = useState(false);
if (variant === 'mobile') {
return (
<div className={`flex items-center gap-2 flex-shrink-0 ${className}`}>
{/* Theme Dropdown */}
<ThemeDropdown variant="mobile" />
</div>
);
}
// Desktop version
return (
<div className={`flex items-center gap-2 ${className}`}>
{/* Navigation */}
<HeaderNavigation variant="desktop" />
{/* Plus de navigation pour écrans moyens */}
<div className="xl:hidden relative">
<button
onClick={() => setTabletMenuOpen(!tabletMenuOpen)}
className="font-mono text-xs uppercase tracking-wider transition-colors px-2 py-1.5 rounded-md text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]"
title="Plus de liens"
>
</button>
{tabletMenuOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[200]"
onClick={() => setTabletMenuOpen(false)}
/>
{/* Menu items */}
<div className="absolute right-0 top-full mt-2 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] py-2 min-w-[119px]">
<HeaderNavigation
variant="mobile"
onLinkClick={() => setTabletMenuOpen(false)}
/>
</div>
</>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,57 @@
'use client';
import { AuthButton } from '@/components/AuthButton';
import { HeaderControls } from './HeaderControls';
import { ThemeDropdown } from './ThemeDropdown';
import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext';
interface HeaderDesktopProps {
title: string;
subtitle: string;
syncing: boolean;
}
export function HeaderDesktop({ title, subtitle, syncing }: HeaderDesktopProps) {
const { openModal: openShortcutsModal } = useKeyboardShortcutsModal();
return (
<div className="hidden lg:flex items-center justify-between gap-6">
{/* Titre et status */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-4 min-w-0">
<div className={`w-3 h-3 rounded-full shadow-lg ${
syncing
? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
}`}></div>
<div className="min-w-0">
<h1 className="text-xl xl:text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider truncate">
<span className="sm:hidden">{title}</span>
<span className="hidden sm:inline">{title}</span>
</h1>
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-xs sm:text-sm truncate">
{subtitle}
</p>
</div>
</div>
{/* Navigation desktop */}
<HeaderControls variant="desktop" />
</div>
{/* Contrôles à droite */}
<div className="flex items-center gap-2">
{/* Keyboard Shortcuts desktop */}
<button
onClick={openShortcutsModal}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-1 rounded-md hover:bg-[var(--card-hover)]"
title="Raccourcis clavier (Cmd+?)"
>
</button>
<ThemeDropdown variant="desktop" />
<AuthButton />
</div>
</div>
);
}

View File

@@ -0,0 +1,116 @@
'use client';
import { useState } from 'react';
import { X, Menu } from 'lucide-react';
import { signOut } from 'next-auth/react';
import Link from 'next/link';
import { HeaderControls } from './HeaderControls';
import { HeaderNavigation } from './HeaderNavigation';
import { AuthButton } from '@/components/AuthButton';
interface HeaderMobileProps {
title: string;
subtitle: string;
syncing: boolean;
}
export function HeaderMobile({ title, subtitle, syncing }: HeaderMobileProps) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
// Fonction pour obtenir les classes CSS d'un lien (mobile)
const getMobileLinkClasses = (href: string) => {
const baseClasses = "font-mono text-sm uppercase tracking-wider transition-colors px-4 py-3 rounded-md block w-full text-left";
// Simplifier pour éviter la duplication de logique
return `${baseClasses} text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]`;
};
return (
<>
{/* Layout mobile/tablette */}
<div className="lg:hidden">
<div className="flex items-center justify-between">
{/* Titre et status */}
<div className="flex items-center gap-3 sm:gap-4 min-w-0 flex-1">
<div className={`w-3 h-3 rounded-full shadow-lg flex-shrink-0 ${
syncing
? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
}`}></div>
<div className="min-w-0">
<h1 className="text-xl sm:text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider truncate">
{title}
</h1>
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-xs sm:text-sm truncate">
{subtitle}
</p>
</div>
</div>
{/* Controls mobile/tablette */}
<HeaderControls variant="mobile" />
{/* Menu burger */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
title="Toggle menu"
>
{mobileMenuOpen ? (
<X className="w-5 h-5" />
) : (
<Menu className="w-5 h-5" />
)}
</button>
</div>
{/* Auth controls à droite mobile - dans la ligne principale */}
<div className="hidden sm:block">
<AuthButton />
</div>
</div>
{/* Ligne Auth séparée sur très petits écrans */}
<div className="lg:hidden sm:hidden flex justify-end pt-2">
<AuthButton />
</div>
{/* Menu mobile/tablette en modal */}
{mobileMenuOpen && (
<div className="lg:hidden fixed inset-0 z-[9999] flex items-start justify-center pt-20">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50"
onClick={() => setMobileMenuOpen(false)}
/>
{/* Modal content */}
<div className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl mx-4 w-full max-w-sm">
<nav className="p-6">
<div className="space-y-3">
<HeaderNavigation
variant="mobile"
onLinkClick={() => setMobileMenuOpen(false)}
/>
{/* Séparateur */}
<div className="border-t border-[var(--border)]/30 my-4"></div>
{/* Bouton déconnexion */}
<button
onClick={() => {
signOut({ callbackUrl: '/login' });
setMobileMenuOpen(false);
}}
className="font-mono text-sm uppercase tracking-wider transition-colors px-4 py-3 rounded-md block w-full text-left text-[var(--destructive)] hover:text-[var(--destructive)] hover:bg-[var(--destructive)]/10"
>
Déconnexion
</button>
</div>
</nav>
</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,101 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useJiraConfig } from '@/contexts/JiraConfigContext';
interface HeaderNavigationProps {
variant: 'desktop' | 'mobile';
className?: string;
onLinkClick?: () => void;
}
export function HeaderNavigation({ variant, className = '', onLinkClick }: HeaderNavigationProps) {
const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig();
const pathname = usePathname();
// Liste des liens de navigation
const navLinks = [
{ href: '/', label: 'Dashboard' },
{ href: '/kanban', label: 'Kanban' },
{ href: '/daily', label: 'Daily' },
{ href: '/weekly-manager', label: 'Weekly' },
...(isJiraConfigured ? [{ href: '/jira-dashboard', label: `Jira${jiraConfig?.projectKey ? ` (${jiraConfig.projectKey})` : ''}` }] : []),
{ href: '/settings', label: 'Settings' }
];
// Fonction pour déterminer si un lien est actif
const isActiveLink = (href: string) => {
if (href === '/') {
return pathname === '/';
}
return pathname.startsWith(href);
};
// Fonction pour obtenir les classes CSS d'un lien (desktop)
const getLinkClasses = (href: string) => {
const baseClasses = "font-mono text-sm uppercase tracking-wider transition-colors px-3 py-1.5 rounded-md";
if (isActiveLink(href)) {
return `${baseClasses} text-[var(--primary)] bg-[var(--primary)]/10 border border-[var(--primary)]/30`;
}
return `${baseClasses} text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]`;
};
// Fonction pour obtenir les classes CSS d'un lien (mobile)
const getMobileLinkClasses = (href: string) => {
const baseClasses = "font-mono text-sm uppercase tracking-wider transition-colors px-4 py-3 rounded-md block w-full text-left";
if (isActiveLink(href)) {
return `${baseClasses} text-[var(--primary)] bg-[var(--primary)]/10 border border-[var(--primary)]/30`;
}
return `${baseClasses} text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover"]`;
};
if (variant === 'mobile') {
return (
<nav className={`space-y-3 ${className}`}>
{navLinks.map(({ href, label }) => (
<Link
key={href}
href={href}
className={getMobileLinkClasses(href)}
onClick={onLinkClick}
>
{label}
</Link>
))}
</nav>
);
}
// Desktop version
return (
<nav className={`flex items-center gap-1 xl:gap-2 flex-wrap ${className}`}>
{navLinks.slice(0, 4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={`${getLinkClasses(href)} text-xs xl:text-sm`}
>
{label}
</Link>
))}
{/* Plus d'éléments sur très grands écrans */}
<div className="hidden 2xl:flex items-center gap-1">
{navLinks.slice(4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={getLinkClasses(href)}
>
{label}
</Link>
))}
</div>
</nav>
);
}

View File

@@ -0,0 +1,74 @@
'use client';
import { useState } from 'react';
import { Check } from 'lucide-react';
import { useTheme } from '@/contexts/ThemeContext';
import { Theme, THEME_CONFIG, getThemeIcon } from '@/lib/ui-config';
interface ThemeDropdownProps {
variant: 'desktop' | 'mobile';
className?: string;
}
export function ThemeDropdown({ variant, className = '' }: ThemeDropdownProps) {
const { theme, setTheme } = useTheme();
const [themeDropdownOpen, setThemeDropdownOpen] = useState(false);
// Liste des thèmes disponibles avec leurs labels et icônes
const themes: { value: Theme; label: string; icon: string }[] = THEME_CONFIG.allThemes.map(themeValue => {
return {
value: themeValue,
label: themeValue.charAt(0).toUpperCase() + themeValue.slice(1).replace('_', ' '),
icon: getThemeIcon(themeValue)
};
});
const buttonSize = variant === 'mobile' ? 'p-2' : 'p-1';
return (
<div className={`relative ${className}`}>
<button
onClick={() => setThemeDropdownOpen(!themeDropdownOpen)}
className={`text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors ${buttonSize} rounded-md hover:bg-[var(--card-hover)]`}
title="Select theme"
>
{themes.find(t => t.value === theme)?.icon || '🎨'}
</button>
{themeDropdownOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[200]"
onClick={() => setThemeDropdownOpen(false)}
/>
{/* Dropdown */}
<div className="absolute right-0 top-full mt-2 w-48 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] overflow-hidden">
<div className="py-2">
{themes.map((themeOption) => (
<button
key={themeOption.value}
onClick={() => {
setTheme(themeOption.value);
setThemeDropdownOpen(false);
}}
className={`w-full text-left px-4 py-2 text-sm transition-colors flex items-center gap-3 ${
theme === themeOption.value
? 'text-[var(--primary)] bg-[var(--primary)]/10'
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--card-hover)]'
}`}
>
<span className="text-base">{themeOption.icon}</span>
<span className="font-mono">{themeOption.label}</span>
{theme === themeOption.value && (
<Check className="w-4 h-4 ml-auto" />
)}
</button>
))}
</div>
</div>
</>
)}
</div>
);
}