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».
| Nom complet | Actif | Actions | |||
|---|---|---|---|---|---|
Alice Martin | Oui | ||||
Bernard Dupont | Oui | ||||
Camille Petit | Non | ||||
David Leroy | Oui | ||||
Émilie Moreau | Oui | ||||
| Alice Martin | alice.martin@email.com | Administrateur | Oui | 20/07/2026 14:30:00 | |
| Bernard Dupont | bernard.dupont@email.com | Utilisateur | Oui | 19/07/2026 09:15:00 | |
| Camille Petit | camille.petit@email.com | Modérateur | Non | 28/06/2026 11:00:00 | |
| David Leroy | david.leroy@email.com | Utilisateur | Oui | 21/07/2026 16:45:00 | |
| Émilie Moreau | emilie.moreau@email.com | Administrateur | Oui | - | |
| François Girard | francois.girard@email.com | Utilisateur | Oui | 18/07/2026 11:20:00 | |
| Gaëlle Lefèvre | gaelle.lefevre@email.com | Modérateur | Oui | 17/07/2026 08:00:00 | |
| Hugo Roussel | hugo.roussel@email.com | Utilisateur | Non | 15/06/2026 16:30:00 | |
| Isabelle Morel | isabelle.morel@email.com | Administrateur | Oui | 22/07/2026 10:00:00 | |
| Jérôme Blanc | jerome.blanc@email.com | Utilisateur | Oui | 20/07/2026 09:45:00 | |
| Karine Fournier | karine.fournier@email.com | Modérateur | Oui | 19/07/2026 14:00:00 | |
| Laurent Gauthier | laurent.gauthier@email.com | Utilisateur | Non | 30/05/2026 12:10:00 | |
| Manon Perrot | manon.perrot@email.com | Administrateur | Oui | 21/07/2026 08:30:00 | |
| Nicolas Chevalier | nicolas.chevalier@email.com | Utilisateur | Oui | 18/07/2026 15:50:00 | |
| Ophélie Lambert | ophelie.lambert@email.com | Modérateur | Oui | 17/07/2026 10:25:00 | |
| Patrick Fontaine | patrick.fontaine@email.com | Utilisateur | Oui | 22/07/2026 07:15:00 | |
| Quentin Garcia | quentin.garcia@email.com | Utilisateur | Non | 01/06/2026 13:40:00 | |
| Rachel Vincent | rachel.vincent@email.com | Administrateur | Oui | 20/07/2026 16:00:00 | |
| Sébastien Henry | sebastien.henry@email.com | Utilisateur | Oui | 19/07/2026 11:35:00 | |
| Tiffany Mercier | tiffany.mercier@email.com | Modérateur | Oui | - |
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
}
]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>
);
}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(hightoujours 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 unModalqui 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.
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'objetrender— fonction qui retourne du JSX (badge, date formatée, ActionsMenu, etc.)
Props du Table
data— le tableau d'objetscolumns— la config des colonneslabel— titre + compteurprintable— bouton d'impression
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} />;
}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"
/>
);
}| Prop | Type | Défaut | Description |
|---|---|---|---|
| data | T[] | requis | Données à afficher |
| columns | Column<T>[] | requis | Configuration des colonnes |
| loading | boolean | false | Affiche un état de chargement |
| error | ApiError | null | null | Affiche une bannière d'erreur |
| label | string | — | Titre du tableau (avec compteur) |
| printable | boolean | false | Ajoute un bouton Imprimer et une vue print |
| pageSize | number | 5 | Nombre de lignes par page |
| pageSizeOptions | number[] | [5, 10, 20] | Options du sélecteur de taille de page |
| showPagination | boolean | true | Afficher/masquer la pagination |
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> = {
label: string;
icon: React.ReactNode;
callback: (row: T) => void;
color?: string;
};- Si
loadingesttrue→ 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:highpartout,mediumdè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)