Checkpoint: Amélioration du menu accordéon avec comportement exclusif (un seul ouvert à la fois) et design moderne coloré avec dégradés, icônes agrandies, et animations fluides

This commit is contained in:
Manus
2026-02-13 04:43:05 -05:00
parent d0ae99042c
commit 8b74f8e0ea
2 changed files with 63 additions and 39 deletions

View File

@@ -37,6 +37,7 @@ type MenuItem = {
path?: string; path?: string;
adminOnly?: boolean; adminOnly?: boolean;
children?: MenuItem[]; children?: MenuItem[];
color?: string; // Couleur de la section
}; };
const menuStructure: MenuItem[] = [ const menuStructure: MenuItem[] = [
@@ -44,10 +45,12 @@ const menuStructure: MenuItem[] = [
icon: LayoutDashboard, icon: LayoutDashboard,
label: "Tableau de bord", label: "Tableau de bord",
path: "/dashboard", path: "/dashboard",
color: "from-blue-500 to-cyan-500",
}, },
{ {
icon: Receipt, icon: Receipt,
label: "Facturation", label: "Facturation",
color: "from-purple-500 to-pink-500",
children: [ children: [
{ icon: Upload, label: "Import", path: "/upload" }, { icon: Upload, label: "Import", path: "/upload" },
{ icon: FileText, label: "Factures", path: "/invoices" }, { icon: FileText, label: "Factures", path: "/invoices" },
@@ -57,6 +60,7 @@ const menuStructure: MenuItem[] = [
{ {
icon: Cog, icon: Cog,
label: "Configuration", label: "Configuration",
color: "from-orange-500 to-amber-500",
children: [ children: [
{ icon: Settings, label: "Paramètres", path: "/settings" }, { icon: Settings, label: "Paramètres", path: "/settings" },
{ icon: Download, label: "Paramètres d'import", path: "/import-settings" }, { icon: Download, label: "Paramètres d'import", path: "/import-settings" },
@@ -68,6 +72,7 @@ const menuStructure: MenuItem[] = [
{ {
icon: ClipboardList, icon: ClipboardList,
label: "Traçabilité", label: "Traçabilité",
color: "from-green-500 to-emerald-500",
children: [ children: [
{ icon: History, label: "Historiques", path: "/history" }, { icon: History, label: "Historiques", path: "/history" },
], ],
@@ -156,8 +161,8 @@ function DashboardLayoutContent({
const sidebarRef = useRef<HTMLDivElement>(null); const sidebarRef = useRef<HTMLDivElement>(null);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
// État pour les sections ouvertes (Tableau de bord ouvert par défaut) // État pour la section ouverte (une seule à la fois, Tableau de bord par défaut)
const [openSections, setOpenSections] = useState<Set<string>>(new Set(["Tableau de bord"])); const [openSection, setOpenSection] = useState<string | null>("Tableau de bord");
// Trouver le label actif pour le header mobile // Trouver le label actif pour le header mobile
const findActiveLabel = (): string => { const findActiveLabel = (): string => {
@@ -172,15 +177,9 @@ function DashboardLayoutContent({
}; };
const toggleSection = (label: string) => { const toggleSection = (label: string) => {
setOpenSections(prev => { // Si on clique sur la section déjà ouverte, on la ferme
const newSet = new Set(prev); // Sinon, on ouvre la nouvelle section (et ferme automatiquement l'ancienne)
if (newSet.has(label)) { setOpenSection(prev => prev === label ? null : label);
newSet.delete(label);
} else {
newSet.add(label);
}
return newSet;
});
}; };
useEffect(() => { useEffect(() => {
@@ -227,22 +226,28 @@ function DashboardLayoutContent({
if (!item.children) { if (!item.children) {
const isActive = location === item.path; const isActive = location === item.path;
return ( return (
<SidebarMenuItem key={item.path}> <SidebarMenuItem key={item.path} className="mb-2">
<SidebarMenuButton <SidebarMenuButton
isActive={isActive} isActive={isActive}
onClick={() => item.path && setLocation(item.path)} onClick={() => item.path && setLocation(item.path)}
tooltip={item.label} tooltip={item.label}
className="h-10 transition-all font-normal" className={`h-12 transition-all font-medium rounded-xl ${
isActive
? `bg-gradient-to-r ${item.color} text-white shadow-lg`
: "hover:bg-accent/50"
}`}
> >
<item.icon className={`h-4 w-4 ${isActive ? "text-primary" : ""}`} /> <div className={`p-2 rounded-lg ${isActive ? "bg-white/20" : "bg-accent"}`}>
<span>{item.label}</span> <item.icon className="h-5 w-5" />
</div>
<span className="text-base">{item.label}</span>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
); );
} }
// Item avec enfants (accordéon) // Item avec enfants (accordéon)
const isOpen = openSections.has(item.label); const isOpen = openSection === item.label;
const hasActiveChild = item.children.some(child => child.path === location); const hasActiveChild = item.children.some(child => child.path === location);
return ( return (
@@ -250,24 +255,29 @@ function DashboardLayoutContent({
key={item.label} key={item.label}
open={isOpen} open={isOpen}
onOpenChange={() => toggleSection(item.label)} onOpenChange={() => toggleSection(item.label)}
className="group/collapsible" className="group/collapsible mb-2"
> >
<SidebarMenuItem> <SidebarMenuItem>
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<SidebarMenuButton <SidebarMenuButton
tooltip={item.label} tooltip={item.label}
className="h-10 transition-all font-normal" className={`h-12 transition-all font-medium rounded-xl ${
isActive={hasActiveChild} hasActiveChild || isOpen
? `bg-gradient-to-r ${item.color} text-white shadow-lg`
: "hover:bg-accent/50"
}`}
> >
<item.icon className={`h-4 w-4 ${hasActiveChild ? "text-primary" : ""}`} /> <div className={`p-2 rounded-lg ${hasActiveChild || isOpen ? "bg-white/20" : "bg-accent"}`}>
<span>{item.label}</span> <item.icon className="h-5 w-5" />
</div>
<span className="text-base">{item.label}</span>
<ChevronDown <ChevronDown
className={`ml-auto h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`} className={`ml-auto h-5 w-5 transition-transform duration-300 ${isOpen ? "rotate-180" : ""}`}
/> />
</SidebarMenuButton> </SidebarMenuButton>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent className="transition-all duration-300 ease-in-out">
<SidebarMenuSub> <SidebarMenuSub className="ml-2 mt-2 space-y-1 border-l-2 border-border pl-4">
{item.children {item.children
.filter(child => !child.adminOnly || user?.role === "admin") .filter(child => !child.adminOnly || user?.role === "admin")
.map(child => { .map(child => {
@@ -277,9 +287,13 @@ function DashboardLayoutContent({
<SidebarMenuSubButton <SidebarMenuSubButton
isActive={isActive} isActive={isActive}
onClick={() => child.path && setLocation(child.path)} onClick={() => child.path && setLocation(child.path)}
className="h-9" className={`h-10 rounded-lg transition-all ${
isActive
? "bg-accent text-accent-foreground font-medium shadow-sm"
: "hover:bg-accent/50"
}`}
> >
<child.icon className={`h-3.5 w-3.5 ${isActive ? "text-primary" : ""}`} /> <child.icon className={`h-4 w-4 ${isActive ? "text-primary" : ""}`} />
<span>{child.label}</span> <span>{child.label}</span>
</SidebarMenuSubButton> </SidebarMenuSubButton>
</SidebarMenuSubItem> </SidebarMenuSubItem>
@@ -300,18 +314,18 @@ function DashboardLayoutContent({
className="border-r-0" className="border-r-0"
disableTransition={isResizing} disableTransition={isResizing}
> >
<SidebarHeader className="h-16 justify-center"> <SidebarHeader className="h-16 justify-center border-b">
<div className="flex items-center gap-3 px-2 transition-all w-full"> <div className="flex items-center gap-3 px-2 transition-all w-full">
<button <button
onClick={toggleSidebar} onClick={toggleSidebar}
className="h-8 w-8 flex items-center justify-center hover:bg-accent rounded-lg transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0" className="h-9 w-9 flex items-center justify-center hover:bg-accent rounded-xl transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0"
aria-label="Toggle navigation" aria-label="Toggle navigation"
> >
<PanelLeft className="h-4 w-4 text-muted-foreground" /> <PanelLeft className="h-5 w-5 text-muted-foreground" />
</button> </button>
{!isCollapsed ? ( {!isCollapsed ? (
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<span className="font-semibold tracking-tight truncate"> <span className="font-bold text-lg tracking-tight truncate bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
Navigation Navigation
</span> </span>
</div> </div>
@@ -319,23 +333,23 @@ function DashboardLayoutContent({
</div> </div>
</SidebarHeader> </SidebarHeader>
<SidebarContent className="gap-0"> <SidebarContent className="gap-0 p-3">
<SidebarMenu className="px-2 py-1"> <SidebarMenu>
{menuStructure.map(renderMenuItem)} {menuStructure.map(renderMenuItem)}
</SidebarMenu> </SidebarMenu>
</SidebarContent> </SidebarContent>
<SidebarFooter className="p-3"> <SidebarFooter className="p-3 border-t">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className="flex items-center gap-3 rounded-lg px-1 py-1 hover:bg-accent/50 transition-colors w-full text-left group-data-[collapsible=icon]:justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"> <button className="flex items-center gap-3 rounded-xl px-2 py-2 hover:bg-accent/50 transition-all w-full text-left group-data-[collapsible=icon]:justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<Avatar className="h-9 w-9 border shrink-0"> <Avatar className="h-10 w-10 border-2 shrink-0 shadow-sm">
<AvatarFallback className="text-xs font-medium"> <AvatarFallback className="text-sm font-semibold bg-gradient-to-br from-primary to-primary/60">
{user?.name?.charAt(0).toUpperCase()} {user?.name?.charAt(0).toUpperCase()}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex-1 min-w-0 group-data-[collapsible=icon]:hidden"> <div className="flex-1 min-w-0 group-data-[collapsible=icon]:hidden">
<p className="text-sm font-medium truncate leading-none"> <p className="text-sm font-semibold truncate leading-none">
{user?.name || "-"} {user?.name || "-"}
</p> </p>
<p className="text-xs text-muted-foreground truncate mt-1.5"> <p className="text-xs text-muted-foreground truncate mt-1.5">
@@ -373,7 +387,7 @@ function DashboardLayoutContent({
<SidebarTrigger className="h-9 w-9 rounded-lg bg-background" /> <SidebarTrigger className="h-9 w-9 rounded-lg bg-background" />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="tracking-tight text-foreground"> <span className="tracking-tight text-foreground font-semibold">
{findActiveLabel()} {findActiveLabel()}
</span> </span>
</div> </div>

12
todo.md
View File

@@ -415,5 +415,15 @@
- [x] Configurer "Tableau de bord" comme section ouverte par défaut - [x] Configurer "Tableau de bord" comme section ouverte par défaut
- [x] Créer les pages manquantes (Factures BAP, Automatismes, Utilisateurs, Historiques) - [x] Créer les pages manquantes (Factures BAP, Automatismes, Utilisateurs, Historiques)
- [x] Mettre à jour App.tsx avec les nouvelles routes - [x] Mettre à jour App.tsx avec les nouvelles routes
- [ ] Tester la navigation et le comportement de l'accordéon - [x] Tester la navigation et le comportement de l'accordéon
- [x] Déployer sur le VPS
## Amélioration du menu accordéon
- [x] Modifier le comportement pour qu'un seul accordéon soit ouvert à la fois (exclusif)
- [x] Ajouter des dégradés de couleurs distinctifs pour chaque section
- [x] Agrandir les icônes et améliorer leur visibilité
- [x] Ajouter des animations fluides pour l'ouverture/fermeture
- [x] Améliorer les effets hover et les transitions
- [x] Optimiser les espacements et la typographie
- [ ] Tester le comportement exclusif
- [ ] Déployer sur le VPS - [ ] Déployer sur le VPS