Sidebar (SideNav)
Barre de navigation latérale avec header, sous-menus imbriqués, footer et branding configurable. Compatible Next.js, Vite et React Router.
createSideNav. Elle utilise la configuration dans playground/src/config/side-nav.config.ts. Vous utilisez déjà le composant en ce moment même.Créer un fichier dédié dans src (ex: config/side-nav.config.ts) avec createSideNav. Cette approche centralise la config et évite de répéter les props.
// config/side-nav.ts
import { createSideNav } from "@ramses1er/cbk-toolkit/client";
export const MySideNav = createSideNav({
brand: {
name: "Mon App",
tagline: "Slogan",
logo: "/logo.png",
},
mainItems: [
{
id: "dashboard",
label: "Dashboard",
icon: "🏠",
href: "/dashboard",
},
{
id: "students",
label: "Élèves",
icon: "🎓",
href: "/students",
subItems: [
{ id: "list", label: "Liste", href: "/students" },
{ id: "classes", label: "Classes", href: "/students/classes" },
],
},
],
footer: [
{ icon: "⚙️", label: "Paramètres", href: "/settings" },
],
});- Chaîne (emoji) :
icon: "🏠" - JSX (
.tsx) :icon: <Plug size={20} /> - Composant (
.ts) :icon: Plug— devient automatiquement<Plug size={20} />au rendu
{ size?: number }.Idéal pour un projet simple qui n'a pas besoin d'exporter metadata. Un seul fichier, layout.tsx en "use client".
"use client";
import { usePathname } from "next/navigation";
import { MySideNav } from "@/config/side-nav.config";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
return (
<html lang="fr">
<body>
<MySideNav currentPath={pathname}>{children}</MySideNav>
</body>
</html>
);
}Obligatoire dès que vous exportez metadata(un composant marqué "use client" ne peut pas le faire). On déporte le SideNav dans un deuxième fichier ClientLayout.tsx.
"use client")import type { Metadata } from "next";
import ClientLayout from "./ClientLayout";
export const metadata: Metadata = {
title: "Mon App",
description: "...",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr">
<body>
<ClientLayout>{children}</ClientLayout>
</body>
</html>
);
}"use client")"use client";
import { usePathname } from "next/navigation";
import { MySideNav } from "@/config/side-nav.config";
export default function ClientLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
return <MySideNav currentPath={pathname}>{children}</MySideNav>;
}Pour que Tailwind génère les classes CSS du toolkit, ajoutez cette directive dans votre fichier CSS principal (généralement app/globals.css) :
@import "tailwindcss";
@source "../../node_modules/@ramses1er/cbk-toolkit/dist";Import depuis le package (déjà vu étape 1) :
import { createSideNav } from "@ramses1er/cbk-toolkit/client";Pour une utilisation directe (sans factory) ou pour typer :
import { SideNav, createSideNav } from "@ramses1er/cbk-toolkit/client";
import type { NavItem } from "@ramses1er/cbk-toolkit/client";Le titre affiché en haut de la page est déterminé automatiquement selon cet ordre :
titleexplicite (prop deSideNavoucreateSideNav)titleoptionnel de l'item actif le plus spécifiquelabelde l'item actif (par défaut)
L'item actif est recherché dans toute la hiérarchie : les mainItems, leurs subItems, puis les items du footer (liens avec href). Quand plusieurs routes correspondent, le hrefle plus long (le plus spécifique, ex. un sous-item) gagne.
mainItems: [
{
id: "users",
label: "Utilisateurs", // titre par défaut = "Utilisateurs"
title: "Gestion", // si défini → titre = "Gestion"
href: "/users",
subItems: [
{
id: "profils",
label: "Profils", // sur /users/profils → titre = "Profils"
title: "Profils clients",// sur /users/profils → titre = "Profils clients"
href: "/users/profils",
},
],
},
];- Un sous-item affiche son propre
title(sinon sonlabel) plutôt que celui du parent. - Un lien de footer (ex. une page
/profil) fait également apparaître le header avec le titre de l'item. - Les items de footer de type
confirm(sanshref) n'affectent pas le titre du header.
Utilisation directe du composant SideNavsans passer par createSideNav. Utile pour des configurations jetables ou prototypes.
<SideNav
currentPath={pathname}
brand={{ name: "Mon App", logo: "/logo.svg", tagline: "Slogan" }}
mainItems={[
{ id: "dashboard", label: "Dashboard", icon: "🏠", href: "/dashboard" },
]}
title="Dashboard"
>
<YourContent />
</SideNav>Un item de footer peut être un lien (href) ou une action avec confirmation (confirm). Au clic, le SideNav ouvre automatiquement un Modal contenant un Confirmation (les deux composants du toolkit). Cas typique : le bouton de déconnexion.
footer: [
{ icon: UserRound, label: "Profil", href: "/profil" },
{
icon: LogOut,
label: "Déconnexion",
confirm: {
title: "Déconnexion",
question: "Voulez-vous vraiment vous déconnecter ?",
messageYes: "Oui, me déconnecter",
messageNo: "Non",
onAction: async () => {
await apiLogout(); // l'appel API
router.push("/login"); // la redirection après succès
},
},
},
]onActionest la fonction exécutée à la confirmation. Retournez unePromise: si elle résout, le Modal se ferme ; si elle rejette, le message d'erreur s'affiche dans la Confirmation (avec le countdown automatique).titleest optionnel (défaut : lelabelde l'item).- L'état de chargement et la gestion d'erreur sont gérés en interne par le SideNav : rien à câbler dans le layout.
onActionaccepte aussi une mutation TanStack Query : appelezmutateAsync()dedans (ou une fonction standalone qui nettoie le cache avecqueryClient.clear()).- Un item de footer avec
href(ex. une page/profil) bénéficie aussi du titre dynamique du header: le titre de l'item s'affiche en haut de la page, comme pour un item du menu principal.
@tanstack/react-query.Le logout est un effet de bord, pas une donnée à cacher : une simple fonction standalone suffit. Exportez votre QueryClient depuis un module partagé pour vider le cache sans hook.
// lib/query-client.ts
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient();// config/side-nav.ts
import { createSideNav } from "@ramses1er/cbk-toolkit/client";
import { LogOut } from "lucide-react";
import { queryClient } from "@/lib/query-client";
import { apiLogout } from "@/api/auth";
export const MySideNav = createSideNav({
// ... mainItems ...
footer: [
{
icon: LogOut,
label: "Déconnexion",
confirm: {
question: "Voulez-vous vraiment vous déconnecter ?",
messageYes: "Oui",
messageNo: "Non",
onAction: async () => {
await apiLogout();
queryClient.clear(); // purge le cache TanStack Query
window.location.href = "/login"; // redirection
},
},
},
],
});Pour utiliser un useMutation, déclarez la config dans un composant client et mémorisez-la avec useMemo (sinon le composant SideNav serait recréé à chaque rendu). mutateAsync() rejette en cas d'erreur : le toolkit l'affiche dans la Confirmation.
"use client";
import { useMemo } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { createSideNav } from "@ramses1er/cbk-toolkit/client";
import { LogOut } from "lucide-react";
import { apiLogout } from "@/api/auth";
export default function ClientLayout({
children,
currentPath,
}: {
children: React.ReactNode;
currentPath: string;
}) {
const router = useRouter();
const queryClient = useQueryClient();
const logout = useMutation({
mutationFn: apiLogout,
onSuccess: () => {
queryClient.clear();
router.push("/login");
},
});
const MySideNav = useMemo(
() =>
createSideNav({
// ... mainItems ...
footer: [
{
icon: LogOut,
label: "Déconnexion",
confirm: {
question: "Voulez-vous vraiment vous déconnecter ?",
messageYes: "Oui",
messageNo: "Non",
onAction: () => logout.mutateAsync(),
},
},
],
}),
[logout],
);
return <MySideNav currentPath={currentPath}>{children}</MySideNav>;
}isProcessing). Vous n'avez pas besoin de isPending de la mutation.| Prop | Type | Défaut | Description |
|---|---|---|---|
| mainItems | NavItem[] | requis | Entrées de navigation |
| brand | { logo?, name?, tagline? } | — | Branding en haut de la sidebar |
| footer | FooterItem[] | — | Items en pied de sidebar : lien (href) ou action avec confirmation (confirm) |
| title | string | — | Override le titre du header |
| toggleIcon | ReactNode | "▼"/"▶" | Icône de toggle des sous-menus |
| children | ReactNode | requis | Contenu principal |
| currentPath | string | requis | Chemin actif pour le surlignage |
type IconType = ReactNode | React.ComponentType<{ size?: number }>;
type NavItem = {
id: string;
label: string;
title?: string; // Optionnel : surcharge le titre du header
icon?: IconType;
href: string;
subItems?: NavItem[];
};Type du footer (union discriminée) :
type FooterItem =
| { icon?: IconType; label: string; href: string }
| {
icon?: IconType;
label: string;
confirm: {
title?: string; // Titre du Modal (défaut : label)
question: string;
messageYes: string;
messageNo: string;
onAction: () => Promise<void> | void; // Exécutée après confirmation
};
};