feat: introduce heatmap grid component for displaying and managing product data with virtualization and performance optimizations.

This commit is contained in:
Michael committed 2026-03-06 11:18:35 +01:00
1 parent eb5bceb5bf
commit 57449ec603
1 file changed
+77 -75
+77 -75
View File
@@ -490,27 +490,19 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
} }
return ( return (
<div <div className="flex flex-col h-full w-full gap-3 relative">
ref={tableContainerRef} <div className="flex justify-end items-center w-full px-1">
className="h-full w-full overflow-auto rounded-[12px] scroll-smooth relative"
style={{
background: "var(--bg-surface)",
border: "1px solid var(--border)",
boxShadow: "var(--shadow-sm)"
}}
>
<div className="absolute top-2 right-4 z-50">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button <button
className="flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-semibold bg-[var(--bg-elevated)] border border-[var(--border-strong)] hover:border-emerald-500 hover:text-emerald-500 transition-colors shadow-sm" className="flex items-center justify-center gap-2 px-3 py-1.5 rounded-md text-sm font-semibold bg-[var(--action-secondary-bg)] text-[var(--action-secondary-text)] border border-[var(--border-strong)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-all shadow-sm apple-btn-secondary"
aria-label="Afficher/masquer les colonnes" aria-label="Afficher/masquer les colonnes"
> >
<SlidersHorizontal className="w-3.5 h-3.5" /> <SlidersHorizontal className="w-4 h-4" />
Colonnes Colonnes
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px] max-h-[400px] overflow-y-auto z-50"> <DropdownMenuContent align="end" className="w-[220px] max-h-[50vh] overflow-y-auto z-50">
{table {table
.getAllLeafColumns() .getAllLeafColumns()
.filter((column: any) => column.getCanHide()) .filter((column: any) => column.getCanHide())
@@ -518,7 +510,7 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
return ( return (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
key={column.id} key={column.id}
className="capitalize text-xs cursor-pointer" className="capitalize text-xs cursor-pointer font-medium"
checked={column.getIsVisible()} checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)} onCheckedChange={(value) => column.toggleVisibility(!!value)}
onSelect={(e: Event) => e.preventDefault()} onSelect={(e: Event) => e.preventDefault()}
@@ -535,68 +527,78 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
</DropdownMenu> </DropdownMenu>
</div> </div>
<table className="text-sm block" style={{ width: "100%", minWidth: totalWidth }}> <div
<thead className="sticky top-0 z-10 block" style={{ ref={tableContainerRef}
background: "linear-gradient(to bottom, var(--bg-elevated), var(--bg-surface))", className="flex-1 min-h-0 w-full overflow-auto rounded-[12px] scroll-smooth relative"
borderBottom: "1px solid var(--border-strong)", style={{
backdropFilter: "blur(10px)", background: "var(--bg-surface)",
WebkitBackdropFilter: "blur(10px)" border: "1px solid var(--border)",
}}> boxShadow: "var(--shadow-sm)"
{table.getHeaderGroups().map((headerGroup) => ( }}
<tr key={headerGroup.id} className="flex w-full"> >
{headerGroup.headers.map((header) => { <table className="text-sm block" style={{ width: "100%", minWidth: totalWidth }}>
const isFlexible = header.column.id === "libelle1" || header.column.id === "ai" || header.column.id === "libelle3"; <thead className="sticky top-0 z-10 block" style={{
const isCenter = header.column.id === "totalQuantite" || header.column.id === "totalCa" || header.column.id === "totalMarge" || header.column.id.startsWith("month_") || header.column.id === "gammeInitial" || header.column.id === "score" || header.column.id === "gamme"; background: "linear-gradient(to bottom, var(--bg-elevated), var(--bg-surface))",
const size = header.getSize(); borderBottom: "1px solid var(--border-strong)",
return ( backdropFilter: "blur(10px)",
<th WebkitBackdropFilter: "blur(10px)"
key={header.id} }}>
className="px-2 py-3 text-[10px] font-black uppercase tracking-[0.05em] whitespace-nowrap select-none flex items-center transition-colors hover:bg-white/5" {table.getHeaderGroups().map((headerGroup) => (
style={{ <tr key={headerGroup.id} className="flex w-full">
width: isFlexible ? "100%" : size, {headerGroup.headers.map((header) => {
flex: isFlexible ? `1 1 ${size}px` : `0 0 ${size}px`, const isFlexible = header.column.id === "libelle1" || header.column.id === "ai" || header.column.id === "libelle3";
minWidth: size, const isCenter = header.column.id === "totalQuantite" || header.column.id === "totalCa" || header.column.id === "totalMarge" || header.column.id.startsWith("month_") || header.column.id === "gammeInitial" || header.column.id === "score" || header.column.id === "gamme";
maxWidth: isFlexible ? "none" : size, const size = header.getSize();
color: "var(--text-muted)", return (
justifyContent: isCenter ? "center" : "flex-start" <th
}} key={header.id}
onClick={header.column.getToggleSortingHandler()} className="px-2 py-3 text-[10px] font-black uppercase tracking-[0.05em] whitespace-nowrap select-none flex items-center transition-colors hover:bg-white/5"
> style={{
<div className={`flex items-center gap-1.5 cursor-pointer ${isCenter ? "justify-center w-full" : ""}`}> width: isFlexible ? "100%" : size,
{flexRender(header.column.columnDef.header, header.getContext())} flex: isFlexible ? `1 1 ${size}px` : `0 0 ${size}px`,
{header.column.getCanSort() && ( minWidth: size,
<div className="shrink-0 opacity-40"> maxWidth: isFlexible ? "none" : size,
{header.column.getIsSorted() === "asc" ? <ChevronUp className="w-3 h-3 text-emerald-500" /> color: "var(--text-muted)",
: header.column.getIsSorted() === "desc" ? <ChevronDown className="w-3 h-3 text-emerald-500" /> justifyContent: isCenter ? "center" : "flex-start"
: <ChevronsUpDown className="w-3 h-3" /> }}
} onClick={header.column.getToggleSortingHandler()}
</div> >
)} <div className={`flex items-center gap-1.5 cursor-pointer ${isCenter ? "justify-center w-full" : ""}`}>
</div> {flexRender(header.column.columnDef.header, header.getContext())}
</th> {header.column.getCanSort() && (
); <div className="shrink-0 opacity-40">
})} {header.column.getIsSorted() === "asc" ? <ChevronUp className="w-3 h-3 text-emerald-500" />
</tr> : header.column.getIsSorted() === "desc" ? <ChevronDown className="w-3 h-3 text-emerald-500" />
))} : <ChevronsUpDown className="w-3 h-3" />
</thead> }
<tbody key={displayDensity} style={{ height: rowVirtualizer.getTotalSize(), position: "relative", display: "block" }}> </div>
{rowVirtualizer.getVirtualItems().map((virtualRow) => { )}
const row = tableRows[virtualRow.index]; </div>
const isSelected = row.getIsSelected(); </th>
);
})}
</tr>
))}
</thead>
<tbody key={displayDensity} style={{ height: rowVirtualizer.getTotalSize(), position: "relative", display: "block" }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const row = tableRows[virtualRow.index];
const isSelected = row.getIsSelected();
return ( return (
<div key={row.id} ref={rowVirtualizer.measureElement} style={{ position: 'absolute', top: 0, left: 0, width: '100%' }}> <div key={row.id} ref={rowVirtualizer.measureElement} style={{ position: 'absolute', top: 0, left: 0, width: '100%' }}>
<GridRow <GridRow
virtualRow={virtualRow} virtualRow={virtualRow}
row={row} row={row}
rowHeight={rowHeight} rowHeight={rowHeight}
isSelected={isSelected} isSelected={isSelected}
/> />
</div> </div>
); );
})} })}
</tbody> </tbody>
</table> </table>
</div>
</div> </div>
); );
} }