feat(backoffice): amélioration navigation mobile et tablette

- Ajout d'un menu hamburger mobile (MobileNav) avec drawer animé via React Portal (évite le piège du backdrop-filter du header)
- Popin JobsIndicator adaptée mobile : positionnement fixed plein-écran sur petit écran, backdrop semi-transparent
- Navigation tablette (md→lg) : icônes seules avec tooltip natif, labels visibles uniquement sur lg+

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 22:44:33 +01:00
parent 4c75e08056
commit e8bb014874
3 changed files with 127 additions and 19 deletions

View File

@@ -146,10 +146,22 @@ export function JobsIndicator() {
/> />
</button> </button>
{/* Backdrop mobile */}
{isOpen && (
<div
className="fixed inset-0 z-40 sm:hidden bg-background/60 backdrop-blur-sm"
onClick={() => setIsOpen(false)}
aria-hidden="true"
/>
)}
{/* Popin/Dropdown with glassmorphism */} {/* Popin/Dropdown with glassmorphism */}
{isOpen && ( {isOpen && (
<div className=" <div className="
absolute right-0 top-full mt-2 w-96 fixed sm:absolute
inset-x-3 sm:inset-x-auto
top-[4.5rem] sm:top-full sm:mt-2
sm:w-96
bg-popover/95 backdrop-blur-md bg-popover/95 backdrop-blur-md
rounded-xl rounded-xl
shadow-elevation-2 shadow-elevation-2

View File

@@ -0,0 +1,93 @@
"use client";
import { useState, useEffect } from "react";
import { createPortal } from "react-dom";
import Link from "next/link";
import { NavIcon } from "./ui";
type NavItem = {
href: "/" | "/books" | "/libraries" | "/jobs" | "/tokens" | "/settings";
label: string;
icon: "dashboard" | "books" | "libraries" | "jobs" | "tokens" | "settings";
};
const HamburgerIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className="w-5 h-5">
<path d="M3 6h18M3 12h18M3 18h18" strokeLinecap="round" />
</svg>
);
const XIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className="w-5 h-5">
<path d="M18 6L6 18M6 6l12 12" strokeLinecap="round" />
</svg>
);
export function MobileNav({ navItems }: { navItems: NavItem[] }) {
const [isOpen, setIsOpen] = useState(false);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const overlay = (
<>
{/* Backdrop */}
<div
className={`fixed inset-0 z-[60] bg-background/80 backdrop-blur-sm md:hidden transition-opacity duration-300 ${isOpen ? "opacity-100" : "opacity-0 pointer-events-none"}`}
onClick={() => setIsOpen(false)}
aria-hidden="true"
/>
{/* Drawer */}
<div
className={`
fixed inset-y-0 left-0 z-[70] w-64
bg-background/95 backdrop-blur-xl
border-r border-border/60
flex flex-col
transform transition-transform duration-300 ease-in-out
md:hidden
${isOpen ? "translate-x-0" : "-translate-x-full"}
`}
>
<div className="h-16 border-b border-border/40 flex items-center px-4">
<span className="text-sm font-semibold text-muted-foreground tracking-wide uppercase">Navigation</span>
</div>
<nav className="flex flex-col gap-1 p-3 flex-1">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center gap-3 px-3 py-3 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-colors duration-200 active:scale-[0.98]"
onClick={() => setIsOpen(false)}
>
<NavIcon name={item.icon} />
<span className="font-medium">{item.label}</span>
</Link>
))}
</nav>
</div>
</>
);
return (
<>
{/* Hamburger button — reste dans le header */}
<button
className="md:hidden p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
onClick={() => setIsOpen(!isOpen)}
aria-label={isOpen ? "Close menu" : "Open menu"}
aria-expanded={isOpen}
>
{isOpen ? <XIcon /> : <HamburgerIcon />}
</button>
{/* Backdrop + Drawer portés directement sur document.body,
hors du header et de son backdrop-filter */}
{mounted && createPortal(overlay, document.body)}
</>
);
}

View File

@@ -7,6 +7,7 @@ import { ThemeProvider } from "./theme-provider";
import { ThemeToggle } from "./theme-toggle"; import { ThemeToggle } from "./theme-toggle";
import { JobsIndicator } from "./components/JobsIndicator"; import { JobsIndicator } from "./components/JobsIndicator";
import { NavIcon, Icon } from "./components/ui"; import { NavIcon, Icon } from "./components/ui";
import { MobileNav } from "./components/MobileNav";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "StripStream Backoffice", title: "StripStream Backoffice",
@@ -61,9 +62,9 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="hidden md:flex items-center gap-1"> <div className="hidden md:flex items-center gap-1">
{navItems.map((item) => ( {navItems.map((item) => (
<NavLink key={item.href} href={item.href}> <NavLink key={item.href} href={item.href} title={item.label}>
<NavIcon name={item.icon} /> <NavIcon name={item.icon} />
<span className="ml-2">{item.label}</span> <span className="ml-2 hidden lg:inline">{item.label}</span>
</NavLink> </NavLink>
))} ))}
</div> </div>
@@ -79,6 +80,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<Icon name="settings" size="md" /> <Icon name="settings" size="md" />
</Link> </Link>
<ThemeToggle /> <ThemeToggle />
<MobileNav navItems={navItems} />
</div> </div>
</div> </div>
</nav> </nav>
@@ -95,13 +97,14 @@ export default function RootLayout({ children }: { children: ReactNode }) {
} }
// Navigation Link Component // Navigation Link Component
function NavLink({ href, children }: { href: NavItem["href"]; children: React.ReactNode }) { function NavLink({ href, title, children }: { href: NavItem["href"]; title?: string; children: React.ReactNode }) {
return ( return (
<Link <Link
href={href} href={href}
title={title}
className=" className="
flex items-center flex items-center
px-3 py-2 px-2 lg:px-3 py-2
rounded-lg rounded-lg
text-sm font-medium text-sm font-medium
text-muted-foreground text-muted-foreground