feat(ui): lot 2 — socle visuel, menu regroupé et composants communs

Thème
- Clair par défaut ; les classes dark: suivent le bouton de thème (et non
  plus le réglage du système) ; animations tw-animate-css enfin importées.
- Couleurs shadcn branchées sur les variables (survols des menus visibles),
  libellés gris au contraste AA dans les deux thèmes, fond plein pour le
  menu et l'en-tête.

Composants partagés (src/components/ui)
- PageHeader, Card/Field, StatCard, Badge/DeltaBadge/StoreBadge/GammeBadge,
  Tabs/Segmented/useUrlTab, Select/SearchInput, EmptyState/ErrorState/
  Skeleton, Tooltip/Terme (glossaire), toast et confirmer, Pagination,
  DataTable (recherche, filtres, tri, pagination, export, totaux).
- Sources uniques : magasins, gammes (avec leur sens), glossaire, seuils de
  marge, export Excel.

Navigation
- Menu groupé (Au quotidien / Analyses / Suivi / Administration), déplié
  par défaut, libellés en français.
- En-tête : nom de la page, thème, menu du compte avec le vrai nom, la
  hauteur des lignes de la Grille et la déconnexion. La recherche globale,
  qui ne filtrait que la Grille, est retirée.
- Page Historique (sessions + exports) ; /snapshots et /exports y
  redirigent.
- Squelettes de chargement au lieu de la fenêtre plein écran, pages
  d'erreur et « introuvable » en français, arrivée sur l'Accueil après
  connexion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
This commit is contained in:
Claude committed 2026-10-03 20:45:03 +00:00
1 parent 7da4c6d5fd
commit 0c890c84c5
47 files changed
+2004 -272

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
/**
* Les gammes d'assortiment : code, nom et couleurs — source unique.
*
* Les couleurs étaient définies trois fois (liste déroulante, filtre, actions
* groupées) et le sens des lettres n'apparaissait que dans la barre d'actions
* groupées : ailleurs, l'utilisateur ne voyait que « A », « B »…
*/
export type CodeGamme = "A" | "B" | "C" | "Y" | "Z";
export interface Gamme {
code: CodeGamme;
/** Nom court : « Cœur », « Sortie »… */
nom: string;
/** Phrase d'explication pour les infobulles. */
description: string;
/** Classes de pastille (fond, bordure, texte) lisibles en clair comme en sombre. */
classes: string;
}
export const GAMMES: readonly Gamme[] = [
{
code: "A",
nom: "Cœur",
description: "Produit permanent, au cœur de l'assortiment.",
classes: "border-emerald-500/40 bg-emerald-500/10 text-emerald-800 dark:text-emerald-300",
},
{
code: "B",
nom: "Complémentaire",
description: "Produit qui complète l'offre du rayon.",
classes: "border-blue-500/40 bg-blue-500/10 text-blue-800 dark:text-blue-300",
},
{
code: "C",
nom: "Saisonnier",
description: "Produit présent selon la saison.",
classes: "border-amber-500/40 bg-amber-500/10 text-amber-800 dark:text-amber-300",
},
{
code: "Y",
nom: "En veille",
description: "Produit mis en attente : il n'est plus commandé pour l'instant.",
classes: "border-violet-500/40 bg-violet-500/10 text-violet-800 dark:text-violet-300",
},
{
code: "Z",
nom: "Sortie",
description: "Produit à retirer de l'assortiment.",
classes: "border-rose-500/40 bg-rose-500/10 text-rose-800 dark:text-rose-300",
},
];
/** Pastille d'un produit sans gamme. */
export const CLASSES_SANS_GAMME = "border-slate-400/40 bg-slate-500/10 text-slate-700 dark:text-slate-300";
export function trouverGamme(code: string | null | undefined): Gamme | undefined {
if (!code) return undefined;
return GAMMES.find((g) => g.code === code.trim());
}
/** « A — Cœur », ou « Sans gamme ». */
export function libelleGamme(code: string | null | undefined): string {
const g = trouverGamme(code);
return g ? `${g.code} — ${g.nom}` : "Sans gamme";
}