SideNav

Documentation de cbk-toolkit

Sidebar (SideNav)

Barre de navigation latérale avec header, sous-menus imbriqués, footer et branding configurable. Compatible Next.js, Vite et React Router.

Dogfooding :La sidebar que vous voyez sur cette page est elle-même construite avec createSideNav. Elle utilise la configuration dans playground/src/config/side-nav.config.ts. Vous utilisez déjà le composant en ce moment même.
Étape 1 — Fichier de configuration

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" },
  ],
});
Icônes :Vous pouvez passer une icône de trois façons :
  • Chaîne (emoji) : icon: "🏠"
  • JSX (.tsx) : icon: <Plug size={20} />
  • Composant (.ts) : icon: Plug — devient automatiquement <Plug size={20} /> au rendu
Compatible avec les icônes lucide-react, react-icons et tout composant React acceptant { size?: number }.
Étape 2 — Intégration dans le layout
🟢Cas 1 — Layout 100% client

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>
  );
}
🔵Cas 2 — Layout serveur + ClientLayout séparé

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.

app/layout.tsx — Serveur (pas de "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>
  );
}
app/ClientLayout.tsx — Client ( "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>;
}
⚙️ Configuration Tailwind requise

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";
Important :Sans cette ligne, les classes Tailwind utilisées dans le toolkit ne seront pas présentes dans le bundle CSS final et le rendu sera cassé (pas de style, ou mise en page déstructurée).
3 — Imports de référence

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";
4 — Titre dynamique du header

Le titre affiché en haut de la page est déterminé automatiquement selon cet ordre :

  1. title explicite (prop de SideNav ou createSideNav)
  2. titleoptionnel de l'item actif le plus spécifique
  3. labelde 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",
      },
    ],
  },
];
Notes :
  • Un sous-item affiche son propre title (sinon son label) 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 (sans href) n'affectent pas le titre du header.
5 — API directe (sans factory)

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>
6 — Footer : action avec confirmation (ex. Logout)

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
      },
    },
  },
]
Fonctionnement :
  • onActionest la fonction exécutée à la confirmation. Retournez une Promise : 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 : le labelde 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.
  • onAction accepte aussi une mutation TanStack Query : appelez mutateAsync() dedans (ou une fonction standalone qui nettoie le cache avec queryClient.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.
Intégration TanStack Query— deux façons d'utiliser la fonctionnalité avec @tanstack/react-query.
Cas 1 — Config statique + queryClient (recommandé)

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
        },
      },
    },
  ],
});
Cas 2 — useMutation (config déclarée dans un composant)

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>;
}
Astuce :l'état de chargement du bouton est géré par le toolkit (prop interne isProcessing). Vous n'avez pas besoin de isPending de la mutation.
7 — Props
PropTypeDéfautDescription
mainItemsNavItem[]requisEntrées de navigation
brand{ logo?, name?, tagline? }Branding en haut de la sidebar
footerFooterItem[]Items en pied de sidebar : lien (href) ou action avec confirmation (confirm)
titlestringOverride le titre du header
toggleIconReactNode"▼"/"▶"Icône de toggle des sous-menus
childrenReactNoderequisContenu principal
currentPathstringrequisChemin actif pour le surlignage
8 — Type NavItem
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
      };
    };