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:
@@ -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
12
todo.md
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user