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
+105 -67
View File
@@ -56,6 +56,12 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
articleName: 220, articleName: 220,
line: 100, line: 100,
classification: 130, classification: 130,
unitsOuter: 100,
outerW: 80,
outerL: 80,
outerH: 80,
check: 100,
actions: 60,
}); });
const [resizingColumn, setResizingColumn] = useState<string | null>(null); const [resizingColumn, setResizingColumn] = useState<string | null>(null);
const [resizeStartX, setResizeStartX] = useState<number | 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 handleMouseMove = (e: MouseEvent) => {
const delta = e.clientX - resizeStartX; 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 })); 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> <p className="text-sm mt-1">All products are correctly configured.</p>
</div> </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"> <thead className="sticky top-0 z-10 bg-slate-900">
<tr> <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"> <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 justify-between"> <div className="flex items-center gap-1">
<span>Art. No.</span> <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> </div>
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'articleNo', columnWidths.articleNo)} />
</th> </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"> <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>Article Name</span> <div className="flex items-center gap-1">
<span className="truncate">Article Name</span>
<button <button
onClick={() => setOpenFilter(openFilter === 'name' ? null : 'name')} onClick={() => setOpenFilter(openFilter === 'name' ? null : 'name')}
className={cn( 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' 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)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'articleName', columnWidths.articleName)} />
</th> </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"> <div className="flex items-center gap-1">
<span>Line</span> <span className="truncate">Line</span>
<button <button
onClick={() => setOpenFilter(openFilter === 'line' ? null : 'line')} onClick={() => setOpenFilter(openFilter === 'line' ? null : 'line')}
className={cn( 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' 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)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'line', columnWidths.line)} />
</th> </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"> <div className="flex items-center gap-1">
<span>Classification</span> <span className="truncate">Classification</span>
<button <button
onClick={() => setOpenFilter(openFilter === 'classification' ? null : 'classification')} onClick={() => setOpenFilter(openFilter === 'classification' ? null : 'classification')}
className={cn( 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' classificationFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)} )}
> >
@@ -740,19 +744,23 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'classification', columnWidths.classification)} />
</th> </th>
{pricingEditableCols.map(col => ( {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"> 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"> <div className="flex items-center gap-1">
<span>{col.name}</span> <span className="truncate">{col.name}</span>
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setOpenFilter(openFilter === `prc_${col.index}` ? null : `prc_${col.index}`); setOpenFilter(openFilter === `prc_${col.index}` ? null : `prc_${col.index}`);
}} }}
className={cn( className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors', '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' dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)} )}
> >
@@ -774,16 +782,18 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} />
</th> </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"> <div className="flex items-center gap-1">
<span>Units/Outer</span> <span className="truncate">Units/Outer</span>
<button <button
onClick={() => setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter')} onClick={() => setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter')}
className={cn( 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' 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)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'unitsOuter', columnWidths.unitsOuter)} />
</th> </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"> <div className="flex items-center gap-1">
<span>Outer W</span> <span className="truncate">Outer W</span>
<button <button
onClick={() => setOpenFilter(openFilter === 'outerW' ? null : 'outerW')} onClick={() => setOpenFilter(openFilter === 'outerW' ? null : 'outerW')}
className={cn( 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' 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)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerW', columnWidths.outerW)} />
</th> </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"> <div className="flex items-center gap-1">
<span>Outer L</span> <span className="truncate">Outer L</span>
<button <button
onClick={() => setOpenFilter(openFilter === 'outerL' ? null : 'outerL')} onClick={() => setOpenFilter(openFilter === 'outerL' ? null : 'outerL')}
className={cn( 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' 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)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerL', columnWidths.outerL)} />
</th> </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"> <div className="flex items-center gap-1">
<span>Outer H</span> <span className="truncate">Outer H</span>
<button <button
onClick={() => setOpenFilter(openFilter === 'outerH' ? null : 'outerH')} onClick={() => setOpenFilter(openFilter === 'outerH' ? null : 'outerH')}
className={cn( 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' outerHFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)} )}
> >
@@ -875,19 +888,23 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'outerH', columnWidths.outerH)} />
</th> </th>
{containerCols.map(col => ( {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"> 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"> <div className="flex items-center gap-1">
<span>{col.name}</span> <span className="truncate">{col.name}</span>
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setOpenFilter(openFilter === `con_${col.index}` ? null : `con_${col.index}`); setOpenFilter(openFilter === `con_${col.index}` ? null : `con_${col.index}`);
}} }}
className={cn( className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors', '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' dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)} )}
> >
@@ -909,23 +926,30 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
<ResizeHandle onMouseDown={e => handleResizeStart(e, colKey, width)} />
</th> </th>
))} );
})}
{/* Validation Check Column */} <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">
<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">
Check Check
<ResizeHandle onMouseDown={e => handleResizeStart(e, 'check', columnWidths.check)} />
</th> </th>
{/* All remaining Matrix columns */} {matrixCols.map(col => {
{matrixCols.map(col => ( const colKey = `mat_${col.index}`;
<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}> const width = columnWidths[colKey] ?? 120;
{col.name} 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> </th>
))} );
})}
{/* Actions */} <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)]">
<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)]" /> <ResizeHandle onMouseDown={e => handleResizeStart(e, 'actions', columnWidths.actions)} />
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -953,7 +977,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
)} )}
> >
{/* Art. No. */} {/* 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]} {row[COLUMNS.ARTICLE_NO]}
</td> </td>
@@ -965,14 +989,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
</td> </td>
{/* Line */} {/* 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] || '—'} {row[COLUMNS.LINE] || '—'}
</td> </td>
{/* Classification */} {/* Classification */}
<td className="px-3 py-2.5"> <td className="px-3 py-2.5 overflow-hidden">
<span className={cn( <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') String(row[COLUMNS.CLASSIFICATION] || '').toUpperCase().includes('CORE')
? 'bg-blue-500/10 text-blue-400 border-blue-500/20' ? 'bg-blue-500/10 text-blue-400 border-blue-500/20'
: String(row[COLUMNS.CLASSIFICATION] || '').toUpperCase().includes('OOC') : 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; const isEmpty = !row[col.index] || row[col.index] === '' || Number(row[col.index]) === 0;
return ( return (
<td key={col.index} className="px-3 py-2"> <td key={col.index} className="px-3 py-2 overflow-hidden">
{isEditing ? ( {isEditing ? (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<input <input
@@ -1001,17 +1025,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onChange={e => setEditingCell(prev => prev ? { ...prev, value: e.target.value } : null)} onChange={e => setEditingCell(prev => prev ? { ...prev, value: e.target.value } : null)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onBlur={commitEdit} 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> </div>
) : ( ) : (
<button <button
onClick={() => startEdit(dataIndex, col.index, val)} onClick={() => startEdit(dataIndex, col.index, val)}
className={cn( 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 isEmpty
? 'text-amber-400 border border-amber-500/40 bg-amber-500/5' ? 'text-amber-400 border border-amber-500/40 bg-amber-500/5'
: 'text-slate-200 border border-transparent hover:border-slate-600' : '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}`} title={isSaving ? 'Saving…' : `Click to edit ${col.name}`}
> >
{isSaving ? ( {isSaving ? (
<span className="text-slate-500 italic">saving</span> <span className="text-slate-500 italic truncate">saving</span>
) : isEmpty ? ( ) : 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> </button>
)} )}
@@ -1033,13 +1057,13 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{/* Units columns */} {/* Units columns */}
<td className="px-3 py-2.5">{unitOuterBadge(row[COLUMNS.UNITS_OUTER])}</td> <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 overflow-hidden truncate">{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 overflow-hidden truncate">{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_H] ?? '-'}</td>
{/* Container units columns */} {/* Container units columns */}
{containerCols.map(col => ( {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)} {unitBadge(row[col.index], col.name)}
</td> </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 ───────────────────────────────────────────────────────────────── // ── Stat card ─────────────────────────────────────────────────────────────────
function StatCard({ function StatCard({
label, label,