mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-24 07:28:10 +02:00
fix(grille): défilement horizontal visible et barre du bas dans le flux
Les deux symptômes n'en faisaient qu'un. L'ascenseur horizontal EXISTAIT déjà (conteneur en overflow-auto, table en min-width ~2600 px), mais il était caché sous la carte de synthèse. Celle-ci était en `position: fixed` et ne réservait donc aucune hauteur : seul un `pb-12` posé sur la page compensait, trop court d'une quarantaine de pixels — exactement la bande où se trouvent la barre de défilement et la dernière ligne. Son `left-[264px]` était en prime calé sur une barre latérale dépliée, alors qu'elle est repliée par défaut : 200 px de vide à gauche, qui écrasaient ses statistiques en hauteur. Elle est remise dans le flux : la grille lui cède exactement la place nécessaire, et il n'y a plus de valeur magique à tenir en phase avec la sidebar. L'ascenseur devient visible : la règle générale (8 px, rail transparent, pouce à 12 % d'opacité) convient aux panneaux où le défilement est accessoire, pas à la Grille où il est l'outil de navigation principal. Classe `.grid-scroll` dédiée, avec rail dessiné, pouce saisissable, et prise en charge de Firefox que les sélecteurs -webkit- ignorent. Corrigé aussi : - totalWidth lisait `columnDef.size` au lieu de `getSize()`. Le redimensionnement de colonnes étant actif et persisté, la largeur plancher restait figée sur l'ancienne somme : dernières colonnes rognées, course de défilement trop courte. - Conteneur non focusable : flèches, Origine et Fin ne défilaient pas la grille. - Barre de filtres qui passait sur deux lignes sous ~1200 px, volant 48 px au tableau : largeurs réduites en dessous de xl. - Barre d'actions groupées sans flex-wrap, qui s'écrasait sur petit écran. - Double croix dans la recherche : `type="search"` ajoutait la croix native du navigateur par-dessus le bouton maison. Masquage local — la recherche de l'en-tête n'a pas de bouton propre et garde donc le sien. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
This commit is contained in:
6 files changed
+83
-11
No files matched your search
@@ -238,6 +238,47 @@
|
|||||||
background: rgba(0, 0, 0, 0.18);
|
background: rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Défilement de la Grille ──
|
||||||
|
La règle ci-dessus (8 px, rail transparent, pouce très pâle) convient aux
|
||||||
|
panneaux où l'ascenseur est accessoire. Dans la Grille, le tableau fait
|
||||||
|
~2600 px de large : l'ascenseur HORIZONTAL est l'outil de navigation
|
||||||
|
principal, et un rail invisible ne dit pas qu'il reste des colonnes à
|
||||||
|
droite. On lui donne donc un rail dessiné et un pouce saisissable.
|
||||||
|
`scrollbar-*` couvre Firefox, que les sélecteurs -webkit- ignorent. */
|
||||||
|
.grid-scroll {
|
||||||
|
scrollbar-width: auto;
|
||||||
|
scrollbar-color: var(--text-muted) var(--bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-scroll::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-scroll::-webkit-scrollbar-track:horizontal {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--text-muted);
|
||||||
|
border-radius: 99px;
|
||||||
|
/* Bordure transparente + background-clip : le pouce garde de l'air dans son
|
||||||
|
rail sans qu'on ait à jouer sur des marges, impossibles ici. */
|
||||||
|
border: 3px solid transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-scroll::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-secondary);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Le coin entre les deux ascenseurs, sinon laissé en blanc par le navigateur. */
|
||||||
|
.grid-scroll::-webkit-scrollbar-corner {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Heatmap cells ── */
|
/* ── Heatmap cells ── */
|
||||||
.heatmap-zero {
|
.heatmap-zero {
|
||||||
background: var(--bg-elevated);
|
background: var(--bg-elevated);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function BulkActionToolbar({ selectedCodeins, onClearSelection }: BulkAct
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 px-4 py-2 bg-slate-900 border border-slate-700/50 rounded-xl backdrop-blur-md shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200">
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 px-4 py-2 bg-slate-900 border border-slate-700/50 rounded-xl backdrop-blur-md shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200">
|
||||||
<span className="text-[11px] font-black uppercase tracking-wider text-slate-200">
|
<span className="text-[11px] font-black uppercase tracking-wider text-slate-200">
|
||||||
{selectedCodeins.length} sélectionné{selectedCodeins.length > 1 ? "s" : ""}
|
{selectedCodeins.length} sélectionné{selectedCodeins.length > 1 ? "s" : ""}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -19,9 +19,12 @@ function Stat({ label, value, sub }: { label: string; value: string; sub?: strin
|
|||||||
// Determine color based on label to match the prototype
|
// Determine color based on label to match the prototype
|
||||||
const valColor = label.includes("CA") || label.includes("Marge") ? "text-emerald-600 dark:text-emerald-400" : "text-slate-900 dark:text-white";
|
const valColor = label.includes("CA") || label.includes("Marge") ? "text-emerald-600 dark:text-emerald-400" : "text-slate-900 dark:text-white";
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
// Barre maintenant dans le flux : chaque pixel de hauteur est pris au
|
||||||
<span className="text-slate-500 text-[10px] uppercase font-black mb-1 tracking-tighter">{label}</span>
|
// tableau. Ces chiffres sont déjà repris en gros dans l'en-tête de page,
|
||||||
<span className={`font-mono-nums font-black text-xl ${valColor}`}>{value}</span>
|
// ils n'ont pas besoin d'y être une seconde fois en 20 px.
|
||||||
|
<div className="flex flex-col items-center leading-tight">
|
||||||
|
<span className="text-slate-500 text-[10px] uppercase font-black tracking-tighter">{label}</span>
|
||||||
|
<span className={`font-mono-nums font-black text-base ${valColor}`}>{value}</span>
|
||||||
{sub && <span className="text-emerald-600 dark:text-emerald-500 text-[11px] font-bold">{sub}</span>}
|
{sub && <span className="text-emerald-600 dark:text-emerald-500 text-[11px] font-bold">{sub}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -153,7 +156,20 @@ export function FloatingSummaryBar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed bottom-6 left-[264px] right-6 p-4 flex justify-between items-center shadow-lg border rounded-xl z-50 transition-colors bg-slate-100 dark:bg-slate-800"
|
/*
|
||||||
|
* Dans le flux, plus en `fixed`.
|
||||||
|
*
|
||||||
|
* En `fixed` elle ne réservait aucune hauteur : seul un `pb-12` posé sur
|
||||||
|
* la page compensait, et il était trop court d'une quarantaine de pixels.
|
||||||
|
* Elle recouvrait donc la dernière ligne du tableau ET la barre de
|
||||||
|
* défilement horizontale — d'où l'impression qu'aucun ascenseur n'existait.
|
||||||
|
*
|
||||||
|
* Le `left-[264px]` était en prime calé sur une barre latérale dépliée,
|
||||||
|
* alors qu'elle est repliée par défaut (64 px) : 200 px de vide à gauche.
|
||||||
|
* En flux, la grille cède exactement la hauteur nécessaire et il n'y a
|
||||||
|
* plus aucune valeur magique à tenir en phase.
|
||||||
|
*/
|
||||||
|
className="shrink-0 px-4 py-2.5 flex flex-wrap justify-between items-center gap-x-4 gap-y-2 shadow-sm border rounded-xl transition-colors bg-slate-100 dark:bg-slate-800"
|
||||||
style={{
|
style={{
|
||||||
borderColor: hasDrafts ? "rgba(234, 179, 8, 0.5)" : "var(--border-strong)",
|
borderColor: hasDrafts ? "rgba(234, 179, 8, 0.5)" : "var(--border-strong)",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export function GridClient({ codeFournisseur, nomFournisseur, fournisseurs, maga
|
|||||||
const activeStoreNom = magasins.find(m => m.code === magasin)?.nom || "National (Total)";
|
const activeStoreNom = magasins.find(m => m.code === magasin)?.nom || "National (Total)";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full space-y-3 min-h-0 pb-12">
|
<div className="flex flex-col h-full space-y-3 min-h-0">
|
||||||
{/* Header — chrome raffiné (eyebrow teal + fournisseur héro + pastilles métriques) */}
|
{/* Header — chrome raffiné (eyebrow teal + fournisseur héro + pastilles métriques) */}
|
||||||
<header className="shrink-0 flex items-center justify-between gap-4 pb-2.5" style={{ borderBottom: "1px solid var(--border)" }}>
|
<header className="shrink-0 flex items-center justify-between gap-4 pb-2.5" style={{ borderBottom: "1px solid var(--border)" }}>
|
||||||
<div className="flex items-center gap-3.5 min-w-0">
|
<div className="flex items-center gap-3.5 min-w-0">
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export function GridFilterBar({ fournisseurs, magasins }: GridFilterBarProps) {
|
|||||||
fournisseurs={fournisseurs}
|
fournisseurs={fournisseurs}
|
||||||
selectedCode={searchParams.get("fournisseur")}
|
selectedCode={searchParams.get("fournisseur")}
|
||||||
onSelect={handleSupplierSelect}
|
onSelect={handleSupplierSelect}
|
||||||
className="w-[280px]"
|
className="w-[200px] xl:w-[280px]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Store Selector */}
|
{/* Store Selector */}
|
||||||
@@ -142,7 +142,7 @@ export function GridFilterBar({ fournisseurs, magasins }: GridFilterBarProps) {
|
|||||||
magasins={magasins}
|
magasins={magasins}
|
||||||
selectedCode={activeMagasin === "TOTAL" ? null : activeMagasin}
|
selectedCode={activeMagasin === "TOTAL" ? null : activeMagasin}
|
||||||
onSelect={handleStoreSelect}
|
onSelect={handleStoreSelect}
|
||||||
className="w-[220px]"
|
className="w-[170px] xl:w-[220px]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
@@ -156,12 +156,19 @@ export function GridFilterBar({ fournisseurs, magasins }: GridFilterBarProps) {
|
|||||||
placeholder="Rechercher (code, désignation...)"
|
placeholder="Rechercher (code, désignation...)"
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
className="apple-input w-72 pr-8"
|
// La croix native de `type="search"` (WebKit/Blink) est masquée :
|
||||||
|
// ce champ a déjà son propre bouton d'effacement juste à droite,
|
||||||
|
// et les deux apparaissaient côte à côte. On garde `type="search"`
|
||||||
|
// pour la sémantique ; le masquage est local, la recherche de
|
||||||
|
// l'en-tête n'ayant pas de bouton maison, elle garde le sien.
|
||||||
|
className="apple-input w-52 xl:w-72 pr-8 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-cancel-button]:hidden"
|
||||||
style={{ paddingLeft: "32px" }}
|
style={{ paddingLeft: "32px" }}
|
||||||
/>
|
/>
|
||||||
{searchValue && (
|
{searchValue && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setSearchValue("")}
|
onClick={() => setSearchValue("")}
|
||||||
|
aria-label="Effacer la recherche"
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2"
|
className="absolute right-2 top-1/2 -translate-y-1/2"
|
||||||
>
|
>
|
||||||
<X className="h-3.5 w-3.5 hover:opacity-100 opacity-60 transition-opacity" style={{ color: "var(--text-secondary)" }} />
|
<X className="h-3.5 w-3.5 hover:opacity-100 opacity-60 transition-opacity" style={{ color: "var(--text-secondary)" }} />
|
||||||
|
|||||||
@@ -1061,7 +1061,11 @@ export function HeatmapGrid({ onSelectionChange, isAdmin, nomFournisseur }: Heat
|
|||||||
});
|
});
|
||||||
|
|
||||||
const visibleColumns = table.getVisibleLeafColumns();
|
const visibleColumns = table.getVisibleLeafColumns();
|
||||||
const totalWidth = visibleColumns.reduce((s, c) => s + ((c.columnDef as { size?: number }).size ?? 150), 0);
|
// `getSize()` et non `columnDef.size` : le redimensionnement de colonnes est
|
||||||
|
// actif et persisté. En lisant la taille déclarée, la largeur plancher de la
|
||||||
|
// table restait figée sur l'ancienne somme dès qu'on élargissait une colonne,
|
||||||
|
// ce qui rognait les dernières et raccourcissait la course de défilement.
|
||||||
|
const totalWidth = visibleColumns.reduce((s, c) => s + c.getSize(), 0);
|
||||||
// Signature du jeu de colonnes affiché : elle sert de signal de re-rendu aux
|
// Signature du jeu de colonnes affiché : elle sert de signal de re-rendu aux
|
||||||
// lignes virtuelles (cf. `GridRow`). Calculée une fois par rendu de grille,
|
// lignes virtuelles (cf. `GridRow`). Calculée une fois par rendu de grille,
|
||||||
// pas une fois par ligne.
|
// pas une fois par ligne.
|
||||||
@@ -1214,7 +1218,11 @@ export function HeatmapGrid({ onSelectionChange, isAdmin, nomFournisseur }: Heat
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
ref={tableContainerRef}
|
ref={tableContainerRef}
|
||||||
className="h-full w-full overflow-auto rounded-[14px] relative"
|
// `tabIndex` : sans lui, les flèches, Origine et Fin ne défilent pas
|
||||||
|
// la grille. Il ne restait que Maj+molette, que personne ne devine.
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Tableau des articles — défilement horizontal sur 12 mois"
|
||||||
|
className="h-full w-full overflow-auto rounded-[14px] relative grid-scroll"
|
||||||
style={{
|
style={{
|
||||||
background: "var(--bg-surface)",
|
background: "var(--bg-surface)",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border)",
|
||||||
|
|||||||
Reference in new issue
Block a user