Table

Documentation de cbk-toolkit

Table

Tableau de données typé avec pagination, chargement, gestion d'erreur, contenu imprimable et colonnes personnalisables (render, accessor). Optimisé pour le responsive : les colonnes ont une prioritéet se masquent sur mobile, où le détail passe par une action «Voir plus».

Démo en direct
Utilisateurs: 20
Nom completActifActions
Alice Martin
Oui
Bernard Dupont
Oui
Camille Petit
Non
David Leroy
Oui
Émilie Moreau
Oui
Lignes par page :
Page 1 sur 4
Table avec 20 utilisateurs, pagination (5 par page), colonnes avec accessor, render et priorité responsive, menu d'actions Modifier/Supprimer/Voir plus (le «Voir plus» ouvre un modal avec toutes les infos de la ligne).
1 — Fichier de données (users.json)

Les données sont stockées dans un fichier JSON statique. Dans un projet réel, elles viendraient d'une API. Copiez ce fichier dans votre projet.

[
  {
    "id": 1,
    "fullname": "Alice Martin",
    "email": "alice.martin@email.com",
    "role": {
      "name": "Administrateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-20T14:30:00Z"
  },
  {
    "id": 2,
    "fullname": "Bernard Dupont",
    "email": "bernard.dupont@email.com",
    "role": {
      "name": "Utilisateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-19T09:15:00Z"
  },
  {
    "id": 3,
    "fullname": "Camille Petit",
    "email": "camille.petit@email.com",
    "role": {
      "name": "Modérateur"
    },
    "isActive": false,
    "lastAccess": "2026-06-28T11:00:00Z"
  },
  {
    "id": 4,
    "fullname": "David Leroy",
    "email": "david.leroy@email.com",
    "role": {
      "name": "Utilisateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-21T16:45:00Z"
  },
  {
    "id": 5,
    "fullname": "Émilie Moreau",
    "email": "emilie.moreau@email.com",
    "role": {
      "name": "Administrateur"
    },
    "isActive": true,
    "lastAccess": null
  },
  {
    "id": 6,
    "fullname": "François Girard",
    "email": "francois.girard@email.com",
    "role": {
      "name": "Utilisateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-18T11:20:00Z"
  },
  {
    "id": 7,
    "fullname": "Gaëlle Lefèvre",
    "email": "gaelle.lefevre@email.com",
    "role": {
      "name": "Modérateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-17T08:00:00Z"
  },
  {
    "id": 8,
    "fullname": "Hugo Roussel",
    "email": "hugo.roussel@email.com",
    "role": {
      "name": "Utilisateur"
    },
    "isActive": false,
    "lastAccess": "2026-06-15T16:30:00Z"
  },
  {
    "id": 9,
    "fullname": "Isabelle Morel",
    "email": "isabelle.morel@email.com",
    "role": {
      "name": "Administrateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-22T10:00:00Z"
  },
  {
    "id": 10,
    "fullname": "Jérôme Blanc",
    "email": "jerome.blanc@email.com",
    "role": {
      "name": "Utilisateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-20T09:45:00Z"
  },
  {
    "id": 11,
    "fullname": "Karine Fournier",
    "email": "karine.fournier@email.com",
    "role": {
      "name": "Modérateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-19T14:00:00Z"
  },
  {
    "id": 12,
    "fullname": "Laurent Gauthier",
    "email": "laurent.gauthier@email.com",
    "role": {
      "name": "Utilisateur"
    },
    "isActive": false,
    "lastAccess": "2026-05-30T12:10:00Z"
  },
  {
    "id": 13,
    "fullname": "Manon Perrot",
    "email": "manon.perrot@email.com",
    "role": {
      "name": "Administrateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-21T08:30:00Z"
  },
  {
    "id": 14,
    "fullname": "Nicolas Chevalier",
    "email": "nicolas.chevalier@email.com",
    "role": {
      "name": "Utilisateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-18T15:50:00Z"
  },
  {
    "id": 15,
    "fullname": "Ophélie Lambert",
    "email": "ophelie.lambert@email.com",
    "role": {
      "name": "Modérateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-17T10:25:00Z"
  },
  {
    "id": 16,
    "fullname": "Patrick Fontaine",
    "email": "patrick.fontaine@email.com",
    "role": {
      "name": "Utilisateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-22T07:15:00Z"
  },
  {
    "id": 17,
    "fullname": "Quentin Garcia",
    "email": "quentin.garcia@email.com",
    "role": {
      "name": "Utilisateur"
    },
    "isActive": false,
    "lastAccess": "2026-06-01T13:40:00Z"
  },
  {
    "id": 18,
    "fullname": "Rachel Vincent",
    "email": "rachel.vincent@email.com",
    "role": {
      "name": "Administrateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-20T16:00:00Z"
  },
  {
    "id": 19,
    "fullname": "Sébastien Henry",
    "email": "sebastien.henry@email.com",
    "role": {
      "name": "Utilisateur"
    },
    "isActive": true,
    "lastAccess": "2026-07-19T11:35:00Z"
  },
  {
    "id": 20,
    "fullname": "Tiffany Mercier",
    "email": "tiffany.mercier@email.com",
    "role": {
      "name": "Modérateur"
    },
    "isActive": true,
    "lastAccess": null
  }
]
2 — Code source complet de la page

Tout le code nécessaire pour reproduire cette démo. Copiez-collez dans votre projet et adaptez les données.

"use client";

import { useState } from "react";
import { Table, ActionsMenu, Modal } from "@ramses1er/cbk-toolkit/client";
import type { Column, TableAction } from "@ramses1er/cbk-toolkit/client";
import { Pencil, Trash2, Eye } from "lucide-react";

// --- Type des données ---
type User = {
  id: number;
  fullname: string;
  email: string;
  role: { name: string };
  isActive: boolean;
  lastAccess: string | null;
};

// --- Données (viennent d'une API dans la vraie vie) ---
const users: User[] = [
  { "id": 1, "fullname": "Alice Martin", "email": "alice.martin@email.com", "role": { "name": "Administrateur" }, "isActive": true, "lastAccess": "2026-07-20T14:30:00Z" },
  { "id": 2, "fullname": "Bernard Dupont", "email": "bernard.dupont@email.com", "role": { "name": "Utilisateur" }, "isActive": true, "lastAccess": "2026-07-19T09:15:00Z" },
  { "id": 3, "fullname": "Camille Petit", "email": "camille.petit@email.com", "role": { "name": "Modérateur" }, "isActive": false, "lastAccess": "2026-06-28T11:00:00Z" },
  { "id": 4, "fullname": "David Leroy", "email": "david.leroy@email.com", "role": { "name": "Utilisateur" }, "isActive": true, "lastAccess": "2026-07-21T16:45:00Z" },
  { "id": 5, "fullname": "Émilie Moreau", "email": "emilie.moreau@email.com", "role": { "name": "Administrateur" }, "isActive": true, "lastAccess": null },
  { "id": 6, "fullname": "François Girard", "email": "francois.girard@email.com", "role": { "name": "Utilisateur" }, "isActive": true, "lastAccess": "2026-07-18T11:20:00Z" },
  { "id": 7, "fullname": "Gaëlle Lefèvre", "email": "gaelle.lefevre@email.com", "role": { "name": "Modérateur" }, "isActive": true, "lastAccess": "2026-07-17T08:00:00Z" },
  { "id": 8, "fullname": "Hugo Roussel", "email": "hugo.roussel@email.com", "role": { "name": "Utilisateur" }, "isActive": false, "lastAccess": "2026-06-15T16:30:00Z" },
  { "id": 9, "fullname": "Isabelle Morel", "email": "isabelle.morel@email.com", "role": { "name": "Administrateur" }, "isActive": true, "lastAccess": "2026-07-22T10:00:00Z" },
  { "id": 10, "fullname": "Jérôme Blanc", "email": "jerome.blanc@email.com", "role": { "name": "Utilisateur" }, "isActive": true, "lastAccess": "2026-07-20T09:45:00Z" },
  { "id": 11, "fullname": "Karine Fournier", "email": "karine.fournier@email.com", "role": { "name": "Modérateur" }, "isActive": true, "lastAccess": "2026-07-19T14:00:00Z" },
  { "id": 12, "fullname": "Laurent Gauthier", "email": "laurent.gauthier@email.com", "role": { "name": "Utilisateur" }, "isActive": false, "lastAccess": "2026-05-30T12:10:00Z" },
  { "id": 13, "fullname": "Manon Perrot", "email": "manon.perrot@email.com", "role": { "name": "Administrateur" }, "isActive": true, "lastAccess": "2026-07-21T08:30:00Z" },
  { "id": 14, "fullname": "Nicolas Chevalier", "email": "nicolas.chevalier@email.com", "role": { "name": "Utilisateur" }, "isActive": true, "lastAccess": "2026-07-18T15:50:00Z" },
  { "id": 15, "fullname": "Ophélie Lambert", "email": "ophelie.lambert@email.com", "role": { "name": "Modérateur" }, "isActive": true, "lastAccess": "2026-07-17T10:25:00Z" },
  { "id": 16, "fullname": "Patrick Fontaine", "email": "patrick.fontaine@email.com", "role": { "name": "Utilisateur" }, "isActive": true, "lastAccess": "2026-07-22T07:15:00Z" },
  { "id": 17, "fullname": "Quentin Garcia", "email": "quentin.garcia@email.com", "role": { "name": "Utilisateur" }, "isActive": false, "lastAccess": "2026-06-01T13:40:00Z" },
  { "id": 18, "fullname": "Rachel Vincent", "email": "rachel.vincent@email.com", "role": { "name": "Administrateur" }, "isActive": true, "lastAccess": "2026-07-20T16:00:00Z" },
  { "id": 19, "fullname": "Sébastien Henry", "email": "sebastien.henry@email.com", "role": { "name": "Utilisateur" }, "isActive": true, "lastAccess": "2026-07-19T11:35:00Z" },
  { "id": 20, "fullname": "Tiffany Mercier", "email": "tiffany.mercier@email.com", "role": { "name": "Modérateur" }, "isActive": true, "lastAccess": null }
];

export default function UsersPage() {
  const [selectedUser, setSelectedUser] = useState<User | null>(null);

  // --- Actions du menu contextuel ---
  const actions: TableAction<User>[] = [
    { label: "Modifier", icon: <Pencil className="w-4 h-4" />, callback: (row) => alert("Modifier l'utilisateur : " + row.fullname), color: "text-green-600" },
    { label: "Supprimer", icon: <Trash2 className="w-4 h-4" />, callback: (row) => alert("Supprimer l'utilisateur : " + row.fullname), color: "text-red-500" },
    { label: "Voir plus", icon: <Eye className="w-4 h-4" />, callback: (row) => setSelectedUser(row), color: "text-gray-700" },
  ];

  // --- Configuration des colonnes (priorité responsive) ---
  const columns: Column<User>[] = [
    { header: "Nom complet", accessor: "fullname", priority: "high" },
    { header: "Email", accessor: "email", priority: "medium" },
    { header: "Rôle", render: (row) => row.role.name, priority: "low" },
    {
      header: "Actif",
      render: (row) =>
        row.isActive
          ? <span className="text-green-600 font-medium">Oui</span>
          : <span className="text-red-500 font-medium">Non</span>,
      priority: "high",
    },
    {
      header: "Dernier accès",
      render: (row) =>
        row.lastAccess
          ? new Date(row.lastAccess).toLocaleString("fr-FR")
          : "-",
      priority: "low",
    },
    {
      header: "Actions",
      render: (row) => <ActionsMenu row={row} actions={actions} />,
      excludeFromPrint: true,
      priority: "high",
    },
  ];

  return (
    <div>
      <Table data={users} columns={columns} label="Utilisateurs" printable />

      {selectedUser && (
        <Modal title={selectedUser.fullname} isOpen onClose={() => setSelectedUser(null)}>
          <div className="space-y-3">
            <div className="flex items-start justify-between gap-3">
              <span className="text-xs font-semibold text-gray-500 uppercase tracking-wide shrink-0">Email</span>
              <span className="text-sm text-gray-700 text-right break-words min-w-0">{selectedUser.email}</span>
            </div>
            <div className="flex items-start justify-between gap-3">
              <span className="text-xs font-semibold text-gray-500 uppercase tracking-wide shrink-0">Rôle</span>
              <span className="text-sm text-gray-700 text-right break-words min-w-0">{selectedUser.role.name}</span>
            </div>
            <div className="flex items-start justify-between gap-3">
              <span className="text-xs font-semibold text-gray-500 uppercase tracking-wide shrink-0">Actif</span>
              <span className="text-sm text-gray-700 text-right break-words min-w-0">{selectedUser.isActive ? "Oui" : "Non"}</span>
            </div>
            <div className="flex items-start justify-between gap-3">
              <span className="text-xs font-semibold text-gray-500 uppercase tracking-wide shrink-0">Dernier accès</span>
              <span className="text-sm text-gray-700 text-right break-words min-w-0">
                {selectedUser.lastAccess ? new Date(selectedUser.lastAccess).toLocaleString("fr-FR") : "-"}
              </span>
            </div>
          </div>
        </Modal>
      )}
    </div>
  );
}
3 — Explication du code

Optimisations responsive (cette version)

Deux nouveautés rendent le tableau lisible sur mobile sans compresser les contenus :

  • Priorité des colonnes— chaque colonne porte une prop priority (high toujours visible, mediumvisible dès la tablette, lowvisible seulement sur desktop). Sur mobile, on n'affiche que les colonnes importantes (2 à 3), donc rien n'est coupé.
  • Action «Voir plus»— pour ne rien perdre, on ajoute une action Voir plusdans le menu d'actions. Son callback ouvre un Modalqui affiche toutes les infos de la ligne, y compris les colonnes masquées sur mobile.

Le composant Tablene génère aucundétail : c'est vous qui fournissez l'action et le modal, en fonction de vos besoins.

Type User

Définit la structure des données. Permet au TypeScript de vérifier les accessor et le typage des render.

Note impression : La colonne Actions a la prop excludeFromPrint: true. Quand l'utilisateur clique sur Imprimer, seules les colonnes Nom complet, Email, Rôle, Actif et Dernier accès apparaissent. La colonne Actions est masquée car elle n'a pas de valeur utile sur papier.

TableAction<T>[]

Chaque action a un label, une icon JSX, un callbackqui reçoit la ligne, et une color optionnelle. Le callback reçoit l'objet complet de la ligne.

Column<T>[]

Deux façons de remplir une colonne :

  • accessor — lit directement une propriété de l'objet
  • render— fonction qui retourne du JSX (badge, date formatée, ActionsMenu, etc.)

Props du Table

  • data— le tableau d'objets
  • columns — la config des colonnes
  • label— titre + compteur
  • printable — bouton d'impression
Utilisation — basique (accessor)

Le cas le plus simple : chaque colonne lit une propriété de l'objet via accessor.

import { Table } from "@ramses1er/cbk-toolkit/client";
import type { Column } from "@ramses1er/cbk-toolkit/client";

type User = { fullname: string; email: string };

const columns: Column<User>[] = [
  { header: "Nom complet", accessor: "fullname" },
  { header: "Email", accessor: "email" },
];

function MonComposant() {
  return <Table data={users} columns={columns} />;
}
Utilisation — avec chargement et erreur

Le tableau gère nativement les états loading et error. Passez-les depuis votre hook de données.

function UsersPage() {
  const listUsers = useGetUsers();

  return (
    <Table
      data={listUsers.data?.users || []}
      columns={columns}
      loading={listUsers.isPending}
      error={listUsers.error}
      label="Utilisateurs"
    />
  );
}
Props — Table
PropTypeDéfautDescription
dataT[]requisDonnées à afficher
columnsColumn<T>[]requisConfiguration des colonnes
loadingbooleanfalseAffiche un état de chargement
errorApiError | nullnullAffiche une bannière d'erreur
labelstringTitre du tableau (avec compteur)
printablebooleanfalseAjoute un bouton Imprimer et une vue print
pageSizenumber5Nombre de lignes par page
pageSizeOptionsnumber[][5, 10, 20]Options du sélecteur de taille de page
showPaginationbooleantrueAfficher/masquer la pagination
Type — Column<T>
type Column<T> = {
  header: string;
  accessor?: keyof T;
  render?: (row: T) => React.ReactNode;
  excludeFromPrint?: boolean;
  priority?: "high" | "medium" | "low";
};

header— Texte d'en-tête de la colonne.

accessor— Propriété de l'objet à afficher (optionnel si render est fourni).

render— Fonction de rendu personnalisé (optionnel si accessor est fourni).

excludeFromPrint— Masque la colonne à l'impression.

priority — Responsive : high toujours visible, medium visible dès la tablette, lowvisible seulement sur desktop. Défaut : high. Les colonnes masquées sur mobile restent accessibles via une action «Voir plus» que vous ajoutez au menu d'actions (elle ouvre un modal avec toutes les infos de la ligne).

Type — TableAction<T>
type TableAction<T> = {
  label: string;
  icon: React.ReactNode;
  callback: (row: T) => void;
  color?: string;
};
Comportement
  • Si loading est true→ affiche un message «Chargement...»
  • Si dataest vide → affiche «Aucun élément»
  • Si errorest fourni → bannière d'erreur au-dessus du tableau
  • Pagination adaptable : les pages s'ajustent automatiquement avec «...» pour les grands jeux de données
  • Tooltip au survol : si le contenu d'une cellule est tronqué ( truncate), le texte complet apparaît au survol
  • Colonnes responsive via priority : high partout, medium dès la tablette, lowseulement sur desktop. Défaut : high
  • «Voir plus» dans le menu d'actions : ouvre un modal avec toutes les infos de la ligne (utile pour les colonnes masquées sur mobile)