feat: implement resizable columns in Pricing Units tab

This commit is contained in:
Christian Vidal Wolf
2026-04-21 09:03:01 +02:00
parent 9b861f1c19
commit 2b994e3c86
+164 -126
View File
@@ -56,6 +56,12 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
articleName: 220,
line: 100,
classification: 130,
unitsOuter: 100,
outerW: 80,
outerL: 80,
outerH: 80,
check: 100,
actions: 60,
});
const [resizingColumn, setResizingColumn] = useState<string | null>(null);
const [resizeStartX, setResizeStartX] = useState<number | null>(null);
@@ -135,7 +141,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const handleMouseMove = (e: MouseEvent) => {
const delta = e.clientX - resizeStartX;
const newWidth = Math.max(50, Math.min(400, resizeStartWidth + delta));
const newWidth = Math.max(40, Math.min(800, resizeStartWidth + delta));
setColumnWidths(prev => ({ ...prev, [resizingColumn]: newWidth }));
};
@@ -659,26 +665,22 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<p className="text-sm mt-1">All products are correctly configured.</p>
</div>
) : (
<table ref={tableRef} className="w-full text-sm border-collapse">
<table ref={tableRef} className="w-full text-sm border-collapse table-fixed">
<thead className="sticky top-0 z-10 bg-slate-900">
<tr>
<th data-col-key="articleNo" style={{ width: columnWidths.articleNo || 100, minWidth: 60 }} className="text-left px-2 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap relative">
<div className="flex items-center justify-between">
<th style={{ width: columnWidths.articleNo }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap relative">
<div className="flex items-center gap-1">
<span>Art. No.</span>
<div
onMouseDown={(e) => handleResizeStart(e, 'articleNo', columnWidths.articleNo || 100)}
onClick={(e) => e.stopPropagation()}
className="w-1.5 h-4 bg-slate-600 hover:bg-blue-500 cursor-col-resize rounded-sm ml-1 shrink-0 pointer-events-auto"
title="Drag to resize"
/>
</div>
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'articleNo', columnWidths.articleNo)} />
</th>
<th data-col-key="articleName" style={{ width: columnWidths.articleName || 220 }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 group relative">
<div className="flex items-center gap-1"><span>Article Name</span>
<th style={{ width: columnWidths.articleName }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 group relative">
<div className="flex items-center gap-1">
<span className="truncate">Article Name</span>
<button
onClick={() => setOpenFilter(openFilter === 'name' ? null : 'name')}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors',
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
nameColFilter ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
@@ -692,14 +694,15 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)}
/>
)}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'articleName', columnWidths.articleName)} />
</th>
<th className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<th style={{ width: columnWidths.line }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<div className="flex items-center gap-1">
<span>Line</span>
<span className="truncate">Line</span>
<button
onClick={() => setOpenFilter(openFilter === 'line' ? null : 'line')}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors',
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
lineMultiFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
@@ -716,14 +719,15 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)}
/>
)}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'line', columnWidths.line)} />
</th>
<th className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<th style={{ width: columnWidths.classification }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<div className="flex items-center gap-1">
<span>Classification</span>
<span className="truncate">Classification</span>
<button
onClick={() => setOpenFilter(openFilter === 'classification' ? null : 'classification')}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors',
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
classificationFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
@@ -740,50 +744,56 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)}
/>
)}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'classification', columnWidths.classification)} />
</th>
{pricingEditableCols.map(col => (
<th key={col.index} className="text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<div className="flex items-center gap-1">
<span>{col.name}</span>
<button
onClick={(e) => {
e.stopPropagation();
setOpenFilter(openFilter === `prc_${col.index}` ? null : `prc_${col.index}`);
}}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors',
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
<Filter className="w-3 h-3" />
</button>
</div>
{openFilter === `prc_${col.index}` && (
<ColumnFilterPopover
uniqueValues={dynamicColUniqueValues[col.index] || []}
selectedValues={dynamicColFilters[col.index] || []}
onToggle={val => setDynamicColFilters(prev => {
const current = prev[col.index] || [];
return current.includes(val)
? { ...prev, [col.index]: current.filter(v => v !== val) }
: { ...prev, [col.index]: [...current, val] };
})}
onSelectAll={vals => setDynamicColFilters(prev => ({ ...prev, [col.index]: vals }))}
onClear={() => { setDynamicColFilters(prev => { const next = { ...prev }; delete next[col.index]; return next; }); setOpenFilter(null); }}
onClose={() => setOpenFilter(null)}
/>
)}
</th>
))}
{pricingEditableCols.map(col => {
const colKey = `prc_${col.index}`;
const width = columnWidths[colKey] ?? 100;
return (
<th key={col.index} style={{ width }} className="text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<div className="flex items-center gap-1">
<span className="truncate">{col.name}</span>
<button
onClick={(e) => {
e.stopPropagation();
setOpenFilter(openFilter === `prc_${col.index}` ? null : `prc_${col.index}`);
}}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
<Filter className="w-3 h-3" />
</button>
</div>
{openFilter === `prc_${col.index}` && (
<ColumnFilterPopover
uniqueValues={dynamicColUniqueValues[col.index] || []}
selectedValues={dynamicColFilters[col.index] || []}
onToggle={val => setDynamicColFilters(prev => {
const current = prev[col.index] || [];
return current.includes(val)
? { ...prev, [col.index]: current.filter(v => v !== val) }
: { ...prev, [col.index]: [...current, val] };
})}
onSelectAll={vals => setDynamicColFilters(prev => ({ ...prev, [col.index]: vals }))}
onClear={() => { setDynamicColFilters(prev => { const next = { ...prev }; delete next[col.index]; return next; }); setOpenFilter(null); }}
onClose={() => setOpenFilter(null)}
/>
)}
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} />
</th>
);
})}
<th className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<th style={{ width: columnWidths.unitsOuter }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<div className="flex items-center gap-1">
<span>Units/Outer</span>
<span className="truncate">Units/Outer</span>
<button
onClick={() => setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter')}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors',
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
unitsOuterFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
@@ -800,15 +810,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)}
/>
)}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter)} />
</th>
<th className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<th style={{ width: columnWidths.outerW }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<div className="flex items-center gap-1">
<span>Outer W</span>
<span className="truncate">Outer W</span>
<button
onClick={() => setOpenFilter(openFilter === 'outerW' ? null : 'outerW')}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors',
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
outerWFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
@@ -825,15 +836,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)}
/>
)}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerW', columnWidths.outerW)} />
</th>
<th className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<th style={{ width: columnWidths.outerL }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<div className="flex items-center gap-1">
<span>Outer L</span>
<span className="truncate">Outer L</span>
<button
onClick={() => setOpenFilter(openFilter === 'outerL' ? null : 'outerL')}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors',
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
outerLFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
@@ -850,15 +862,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)}
/>
)}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerL', columnWidths.outerL)} />
</th>
<th className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<th style={{ width: columnWidths.outerH }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<div className="flex items-center gap-1">
<span>Outer H</span>
<span className="truncate">Outer H</span>
<button
onClick={() => setOpenFilter(openFilter === 'outerH' ? null : 'outerH')}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors',
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
outerHFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
@@ -875,57 +888,68 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)}
/>
)}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerH', columnWidths.outerH)} />
</th>
{containerCols.map(col => (
<th key={col.index} className="text-left px-3 py-3 text-xs font-semibold text-orange-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<div className="flex items-center gap-1">
<span>{col.name}</span>
<button
onClick={(e) => {
e.stopPropagation();
setOpenFilter(openFilter === `con_${col.index}` ? null : `con_${col.index}`);
}}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors',
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
<Filter className="w-3 h-3" />
</button>
</div>
{openFilter === `con_${col.index}` && (
<ColumnFilterPopover
uniqueValues={dynamicColUniqueValues[col.index] || []}
selectedValues={dynamicColFilters[col.index] || []}
onToggle={val => setDynamicColFilters(prev => {
const current = prev[col.index] || [];
return current.includes(val)
? { ...prev, [col.index]: current.filter(v => v !== val) }
: { ...prev, [col.index]: [...current, val] };
})}
onSelectAll={vals => setDynamicColFilters(prev => ({ ...prev, [col.index]: vals }))}
onClear={() => { setDynamicColFilters(prev => { const next = { ...prev }; delete next[col.index]; return next; }); setOpenFilter(null); }}
onClose={() => setOpenFilter(null)}
/>
)}
</th>
))}
{containerCols.map(col => {
const colKey = `con_${col.index}`;
const width = columnWidths[colKey] ?? 110;
return (
<th key={col.index} style={{ width }} className="text-left px-3 py-3 text-xs font-semibold text-orange-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative">
<div className="flex items-center gap-1">
<span className="truncate">{col.name}</span>
<button
onClick={(e) => {
e.stopPropagation();
setOpenFilter(openFilter === `con_${col.index}` ? null : `con_${col.index}`);
}}
className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)}
>
<Filter className="w-3 h-3" />
</button>
</div>
{openFilter === `con_${col.index}` && (
<ColumnFilterPopover
uniqueValues={dynamicColUniqueValues[col.index] || []}
selectedValues={dynamicColFilters[col.index] || []}
onToggle={val => setDynamicColFilters(prev => {
const current = prev[col.index] || [];
return current.includes(val)
? { ...prev, [col.index]: current.filter(v => v !== val) }
: { ...prev, [col.index]: [...current, val] };
})}
onSelectAll={vals => setDynamicColFilters(prev => ({ ...prev, [col.index]: vals }))}
onClear={() => { setDynamicColFilters(prev => { const next = { ...prev }; delete next[col.index]; return next; }); setOpenFilter(null); }}
onClose={() => setOpenFilter(null)}
/>
)}
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} />
</th>
);
})}
{/* Validation Check Column */}
<th className="text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap">
<th style={{ width: columnWidths.check }} className="text-left px-3 py-3 text-xs font-semibold text-blue-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap relative">
Check
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'check', columnWidths.check)} />
</th>
{/* All remaining Matrix columns */}
{matrixCols.map(col => (
<th key={col.index} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap overflow-hidden max-w-[150px] truncate" title={col.name}>
{col.name}
</th>
))}
{matrixCols.map(col => {
const colKey = `mat_${col.index}`;
const width = columnWidths[colKey] ?? 120;
return (
<th key={col.index} style={{ width }} className="text-left px-3 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-700 whitespace-nowrap group relative" title={col.name}>
<span className="truncate block">{col.name}</span>
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} />
</th>
);
})}
{/* Actions */}
<th className="px-3 py-3 border-b border-slate-700 w-10 sticky right-0 bg-slate-900 shadow-[-4px_0_8px_rgba(0,0,0,0.2)]" />
<th style={{ width: columnWidths.actions }} className="px-3 py-3 border-b border-slate-700 sticky right-0 bg-slate-900 shadow-[-4px_0_8px_rgba(0,0,0,0.2)]">
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'actions', columnWidths.actions)} />
</th>
</tr>
</thead>
<tbody>
@@ -953,7 +977,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)}
>
{/* Art. No. */}
<td className="px-3 py-2.5 font-mono text-xs text-slate-300 whitespace-nowrap">
<td className="px-3 py-2.5 font-mono text-xs text-slate-300 whitespace-nowrap overflow-hidden truncate">
{row[COLUMNS.ARTICLE_NO]}
</td>
@@ -965,14 +989,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</td>
{/* Line */}
<td className="px-3 py-2.5 text-slate-400 whitespace-nowrap text-xs">
<td className="px-3 py-2.5 text-slate-400 whitespace-nowrap text-xs overflow-hidden truncate">
{row[COLUMNS.LINE] || '—'}
</td>
{/* Classification */}
<td className="px-3 py-2.5">
<td className="px-3 py-2.5 overflow-hidden">
<span className={cn(
'px-2 py-0.5 rounded text-[10px] font-bold border whitespace-nowrap',
'px-2 py-0.5 rounded text-[10px] font-bold border whitespace-nowrap inline-block max-w-full truncate',
String(row[COLUMNS.CLASSIFICATION] || '').toUpperCase().includes('CORE')
? 'bg-blue-500/10 text-blue-400 border-blue-500/20'
: String(row[COLUMNS.CLASSIFICATION] || '').toUpperCase().includes('OOC')
@@ -991,7 +1015,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const isEmpty = !row[col.index] || row[col.index] === '' || Number(row[col.index]) === 0;
return (
<td key={col.index} className="px-3 py-2">
<td key={col.index} className="px-3 py-2 overflow-hidden">
{isEditing ? (
<div className="flex items-center gap-1">
<input
@@ -1001,17 +1025,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onChange={e => setEditingCell(prev => prev ? { ...prev, value: e.target.value } : null)}
onKeyDown={handleKeyDown}
onBlur={commitEdit}
className="w-24 bg-slate-900 border border-blue-500 rounded px-2 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full bg-slate-900 border border-blue-500 rounded px-2 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<button onClick={commitEdit} className="text-emerald-400 hover:text-emerald-300 p-0.5">
<Save className="w-3.5 h-3.5" />
</button>
</div>
) : (
<button
onClick={() => startEdit(dataIndex, col.index, val)}
className={cn(
'group flex items-center gap-1.5 px-2 py-1 rounded text-xs font-mono transition-colors hover:bg-slate-700/60 min-w-[72px]',
'group flex items-center gap-1.5 px-2 py-1 rounded text-xs font-mono transition-colors hover:bg-slate-700/60 w-full overflow-hidden',
isEmpty
? 'text-amber-400 border border-amber-500/40 bg-amber-500/5'
: 'text-slate-200 border border-transparent hover:border-slate-600'
@@ -1019,11 +1040,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
title={isSaving ? 'Saving…' : `Click to edit ${col.name}`}
>
{isSaving ? (
<span className="text-slate-500 italic">saving</span>
<span className="text-slate-500 italic truncate">saving</span>
) : isEmpty ? (
<><AlertTriangle className="w-3 h-3" /><span>Missing</span></>
<div className="flex items-center gap-1 truncate"><AlertTriangle className="w-3 h-3 shrink-0" /><span>Missing</span></div>
) : (
<>{val}<Edit2 className="w-2.5 h-2.5 opacity-0 group-hover:opacity-50" /></>
<div className="flex items-center justify-between w-full truncate">
<span>{val}</span>
<Edit2 className="w-2.5 h-2.5 opacity-0 group-hover:opacity-50 shrink-0" />
</div>
)}
</button>
)}
@@ -1033,13 +1057,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{/* Units columns */}
<td className="px-3 py-2.5">{unitOuterBadge(row[COLUMNS.UNITS_OUTER])}</td>
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs">{row[COLUMNS.OUTER_W] ?? '-'}</td>
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs">{row[COLUMNS.OUTER_L] ?? '-'}</td>
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs">{row[COLUMNS.OUTER_H] ?? '-'}</td>
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate">{row[COLUMNS.OUTER_W] ?? '-'}</td>
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate">{row[COLUMNS.OUTER_L] ?? '-'}</td>
<td className="px-3 py-2.5 text-slate-400 font-mono text-xs overflow-hidden truncate">{row[COLUMNS.OUTER_H] ?? '-'}</td>
{/* Container units columns */}
{containerCols.map(col => (
<td key={col.index} className="px-3 py-2.5">
<td key={col.index} className="px-3 py-2.5 overflow-hidden truncate">
{unitBadge(row[col.index], col.name)}
</td>
))}
@@ -1139,6 +1163,20 @@ function TextFilterPopover({ value, onChange, onClose }: {
);
}
// ── Resize Handle Component ─────────────────────────────────────────────
function ResizeHandle({ onMouseDown }: { onMouseDown: (e: React.MouseEvent) => void }) {
return (
<div
onMouseDown={onMouseDown}
onClick={e => e.stopPropagation()}
className="absolute right-0 top-0 bottom-0 w-1.5 hover:bg-blue-500/50 cursor-col-resize group/resizer flex items-center justify-center z-20"
title="Drag to resize"
>
<div className="w-[1px] h-4 bg-slate-700 group-hover/resizer:bg-blue-400 transition-colors" />
</div>
);
}
// ── Stat card ─────────────────────────────────────────────────────────────────
function StatCard({
label,