сохранение при смене страниц

This commit is contained in:
2025-07-28 18:36:02 +03:00
parent 27b9b79c60
commit 4bc8d5753f
14 changed files with 617 additions and 473 deletions

View File

@@ -11,15 +11,10 @@ export const Cell = memo(
isSelected,
isInMultiSelection,
multiSelectionActive,
selectionTop = false,
selectionBottom = false,
selectionLeft = false,
selectionRight = false,
isEditing,
displayValue,
rawValue,
isModified,
nextContentCol,
availablePx,
onCellClick,
onCellDoubleClick,
@@ -32,157 +27,98 @@ export const Cell = memo(
stopEditing,
setActiveSheet,
activeCellRef,
style, // Добавляем style для react-window
style,
mergedCellInfo,
highlightColor, // Цвет подсветки для формул
isCopied, // Является ли ячейка скопированной
isCut, // Является ли ячейка вырезанной
highlightColor,
isCopied,
isCut,
}: CellProps) => {
// Если ячейка объединена, но не является основной, не рендерим её
if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) {
return null
}
// Определяем классы CSS для подсветки и обычного состояния
let className = 'border border-border'
// Базовый style для ячейки
let cellStyle: CSSProperties = {
...style,
overflow: 'visible',
padding: 0,
userSelect: 'none',
}
// Приоритет стилей: подсветка формул > множественное выделение > изменения > обычное состояние
if (highlightColor) {
className += ` ${highlightColor}`
} else if (isModified && !mergedCellInfo?.isMerged) {
className += ' bg-yellow-200'
}
// Настраиваем box-shadow, фон и z-index для выделенной области
if (isInMultiSelection) {
cellStyle = {
...cellStyle,
backgroundColor: 'rgba(59,130,246,0.08)', // голубоватый оттенок
}
}
// Стили для скопированных/вырезанных ячеек
if (isCopied || isCut) {
cellStyle = {
...cellStyle,
border: '2px dashed hsl(var(--primary))',
backgroundColor: isCut
? 'rgba(239, 68, 68, 0.1)'
: 'rgba(34, 197, 94, 0.1)', // красноватый для вырезанных, зеленоватый для скопированных
}
}
// Отладка стилей для измененных ячеек
if (isModified) {
// console.log(`🎨 Стили для измененной ячейки ${rowIndex},${colIndex}:`, //{
// className,
// isModified,
// isSelected,
// displayValue,
// highlightColor,
//})
}
// Для объединенных ячеек нужно рассчитать правильные размеры
if (
mergedCellInfo?.isMainCell &&
mergedCellInfo.rowSpan &&
mergedCellInfo.colSpan
) {
const { rowSpan, colSpan } = mergedCellInfo
// Рассчитываем общую ширину для объединенной ячейки
let totalWidth = 0
for (let i = 0; i < colSpan; i++) {
const currentColIndex = colIndex + i
if (currentColIndex < columnWidths.length) {
totalWidth += columnWidths[currentColIndex]
}
// Определяем стили ячейки
const getCellStyles = (): { className: string; style: CSSProperties } => {
let className = 'border border-border'
let cellStyle: CSSProperties = {
...style,
overflow: 'visible',
padding: 0,
userSelect: 'none',
}
// Рассчитываем общую высоту для объединенной ячейки
const totalHeight = ROW_HEIGHT * rowSpan
// Определяем цвет фона для объединенной ячейки
let backgroundColor = 'white'
// Подсветка формул имеет приоритет
if (highlightColor) {
// Если есть подсветка формулы, используем её
const bgMatch = highlightColor.match(/bg-(\w+)-(\d+)/)
if (bgMatch) {
backgroundColor = `var(--${bgMatch[1]}-${bgMatch[2]})`
}
} else if (isSelected) {
backgroundColor = 'hsl(var(--accent))'
} else if (isModified) {
backgroundColor =
'#fef08a' /* тот же жёлтый оттенок, что и bg-yellow-200 */
className += ` ${highlightColor}`
} else if (isModified && !mergedCellInfo?.isMerged) {
className += ' bg-yellow-200'
}
cellStyle = {
...cellStyle,
width: totalWidth,
height: totalHeight,
zIndex: 10, // Поднимаем выше обычных ячеек
backgroundColor,
// Множественное выделение
if (isInMultiSelection) {
cellStyle.backgroundColor = 'rgba(59,130,246,0.08)'
}
// Стили для скопированных/вырезанных ячеек
if (isCopied || isCut) {
cellStyle.border = '2px dashed hsl(var(--primary))'
cellStyle.backgroundColor = isCut
? 'rgba(239, 68, 68, 0.1)'
: 'rgba(34, 197, 94, 0.1)'
}
// Объединенные ячейки
if (
mergedCellInfo?.isMainCell &&
mergedCellInfo.rowSpan &&
mergedCellInfo.colSpan
) {
const { rowSpan, colSpan } = mergedCellInfo
// Рассчитываем размеры объединенной ячейки
const totalWidth = columnWidths
.slice(colIndex, colIndex + colSpan)
.reduce((sum, width) => sum + width, 0)
const totalHeight = ROW_HEIGHT * rowSpan
// Определяем фон
let backgroundColor = 'white'
if (highlightColor) {
const bgMatch = highlightColor.match(/bg-(\w+)-(\d+)/)
if (bgMatch) {
backgroundColor = `var(--${bgMatch[1]}-${bgMatch[2]})`
}
} else if (isSelected) {
backgroundColor = 'hsl(var(--accent))'
} else if (isModified) {
backgroundColor = '#fef08a'
}
cellStyle = {
...cellStyle,
width: totalWidth,
height: totalHeight,
zIndex: 10,
backgroundColor,
}
}
return { className, style: cellStyle }
}
// Используем обычное значение displayValue для всех ячеек, включая объединенные
const cellDisplayValue = displayValue
const { className, style: cellStyle } = getCellStyles()
const handleClick = useCallback(
(e: React.MouseEvent) => {
// console.log('🖱️ Cell клик:', {
// rowIndex,
// colIndex,
// sheetType,
// isInEditMode,
// isSelected,
//hasReferenceHandler: !!onCellReferenceClick,
//})
// Если мы в режиме редактирования, вставка была обработана на mousedown.
// Останавливаем всплытие, чтобы родительский Spreadsheet не сменил активный лист.
if (isInEditMode) {
e.preventDefault()
e.stopPropagation()
return
}
// Обычная логика клика (выделение ячейки)
// console.log('📍 Обычный клик - выбираем ячейку:', {
// rowIndex,
// colIndex,
// sheetType,
//})
// Подсказка для пользователя
if (!isInEditMode) {
// console.log(
// '💡 Чтобы вставить ссылку на ячейку: 1) Выберите ячейку 2) Нажмите F2 или Enter для редактирования 3) Кликните на нужную ячейку'
//)
}
onCellClick(rowIndex, colIndex, e)
},
[
setActiveSheet,
sheetType,
onCellClick,
onCellReferenceClick,
rowIndex,
colIndex,
isInEditMode,
isSelected,
]
[isInEditMode, onCellClick, rowIndex, colIndex]
)
const handleDoubleClick = useCallback(
@@ -193,24 +129,18 @@ export const Cell = memo(
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
if (!isInEditMode) {
// блокируем стандартное выделение текста браузером
e.preventDefault()
}
// Используем mousedown, чтобы сработать раньше blur FormulaBar
// Вставка ссылки в режиме редактирования
if (isInEditMode && onCellReferenceClick && !isSelected) {
e.preventDefault()
e.stopPropagation()
// console.log('🖱️ mousedown вставка ссылки до blur:', {
// rowIndex,
// colIndex,
// sheetType,
//})
onCellReferenceClick(sheetType, rowIndex, colIndex)
return
}
// Обработчик для начала drag selection
// Drag selection
if (onCellMouseDown) {
setActiveSheet(sheetType)
onCellMouseDown(rowIndex, colIndex, e)
@@ -229,17 +159,57 @@ export const Cell = memo(
)
const handleMouseEnter = useCallback(() => {
if (onCellMouseEnter) {
onCellMouseEnter(rowIndex, colIndex)
}
onCellMouseEnter?.(rowIndex, colIndex)
}, [onCellMouseEnter, rowIndex, colIndex])
const handleMouseUp = useCallback(() => {
if (onCellMouseUp) {
onCellMouseUp()
}
onCellMouseUp?.()
}, [onCellMouseUp])
const handleInputBlur = useCallback(
(e: React.FocusEvent) => {
const relatedTarget = e.relatedTarget as HTMLElement
if (relatedTarget?.closest('.formula-bar')) {
return
}
stopEditing(true)
},
[stopEditing]
)
const handleInputKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
stopEditing(true)
} else if (e.key === 'Escape') {
e.preventDefault()
stopEditing(false)
}
},
[stopEditing]
)
// Стили для отображения содержимого
const contentStyles: CSSProperties = {
overflow: availablePx > columnWidth ? 'visible' : 'hidden',
whiteSpace: 'nowrap',
...(isSelected && !multiSelectionActive
? { boxShadow: 'inset 0 0 0 2px hsl(var(--primary))' }
: highlightColor
? { boxShadow: 'inset 0 0 0 2px currentColor' }
: {}),
}
const textStyles: CSSProperties = {
width: availablePx > columnWidth ? availablePx - 8 : columnWidth - 16,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
pointerEvents: 'none',
zIndex: 1,
}
return (
<div
className={className}
@@ -254,57 +224,23 @@ export const Cell = memo(
<input
ref={activeCellRef as React.RefObject<HTMLInputElement>}
type="text"
value={rawValue} // Во время редактирования показываем текущее редактируемое значение
value={rawValue}
onChange={e => onCellValueChange(e.target.value)}
style={{ width: columnWidth - 4 }}
onBlur={e => {
// Проверяем, не переходит ли фокус в formula bar
const relatedTarget = e.relatedTarget as HTMLElement
if (relatedTarget && relatedTarget.closest('.formula-bar')) {
return // Не завершаем редактирование
}
stopEditing(true)
}}
onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault()
stopEditing(true)
} else if (e.key === 'Escape') {
e.preventDefault()
stopEditing(false)
}
}}
onBlur={handleInputBlur}
onKeyDown={handleInputKeyDown}
className="absolute left-0 top-0 z-20 box-border h-full border-2 border-primary px-2 py-1"
/>
) : (
// Контейнер, который допускает перепрыгивание текста, если впереди есть свободное место
<div
className="relative flex h-full w-full items-center px-2"
style={{
overflow: availablePx > columnWidth ? 'visible' : 'hidden',
whiteSpace: 'nowrap',
...(isSelected && !multiSelectionActive
? { boxShadow: 'inset 0 0 0 2px hsl(var(--primary))' }
: highlightColor
? { boxShadow: 'inset 0 0 0 2px currentColor' }
: {}),
}}
style={contentStyles}
>
<div
className="absolute left-1 top-0 flex h-full items-center"
style={{
width:
availablePx > columnWidth
? availablePx - 8
: columnWidth - 16,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
pointerEvents: 'none', // чтобы клики шли в "родную" ячейку
zIndex: 1,
}}
style={textStyles}
>
{cellDisplayValue}
{displayValue}
</div>
</div>
)}

View File

@@ -2,7 +2,6 @@ import { CellRendererProps } from '../types'
import { findMergedCellInfo, isInAnySelection } from '../utils'
import { Cell } from './Cell'
// Рендер-проп для ячеек грида
export const CellRenderer = ({
columnIndex,
rowIndex,
@@ -34,55 +33,25 @@ export const CellRenderer = ({
activeCellInputRef,
} = data
const isActiveSheet = activeSheet === sheetType
const isCellSelected =
selectedCell?.row === rowIndex &&
selectedCell?.col === columnIndex &&
activeSheet === sheetType
isActiveSheet
// Проверяем, является ли ячейка частью множественного выделения
const isInMultiSelection =
sheetType === activeSheet &&
isInAnySelection(rowIndex, columnIndex, multiSelection)
const multiSelectionActive =
sheetType === activeSheet && multiSelection.length > 0
// Вычисляем границы выделения, если ячейка внутри выделения
let selectionTop = false,
selectionBottom = false,
selectionLeft = false,
selectionRight = false
if (isInMultiSelection && multiSelection.length > 0) {
const bounds = {
top: Math.min(...multiSelection.map(s => Math.min(s.startRow, s.endRow))),
bottom: Math.max(
...multiSelection.map(s => Math.max(s.startRow, s.endRow))
),
left: Math.min(
...multiSelection.map(s => Math.min(s.startCol, s.endCol))
),
right: Math.max(
...multiSelection.map(s => Math.max(s.startCol, s.endCol))
),
}
selectionTop = rowIndex === bounds.top
selectionBottom = rowIndex === bounds.bottom
selectionLeft = columnIndex === bounds.left
selectionRight = columnIndex === bounds.right
}
isActiveSheet && isInAnySelection(rowIndex, columnIndex, multiSelection)
const multiSelectionActive = isActiveSheet && multiSelection.length > 0
const isCellEditing = isCellSelected && isEditing
const cellKey = `${rowIndex}-${columnIndex}`
const cellData = cachedData[cellKey]
const rawValue = isCellEditing ? editingValue : cellData?.rawValue || ''
const displayValue = isCellEditing
? '' // Не показывать вычисленное значение во время редактирования
: cellData?.displayValue || ''
const displayValue = isCellEditing ? '' : cellData?.displayValue || ''
// Проверяем, подсвечена ли эта ячейка
// Подсветка ячеек для формул
const currentSheetName = sheetType === 'report' ? 'L' : 'R'
const highlightedCell = highlightedCells.find(
highlight =>
@@ -90,23 +59,8 @@ export const CellRenderer = ({
highlight.row === rowIndex &&
highlight.col === columnIndex
)
const highlightColor = highlightedCell?.color
// Отладка для измененных ячеек
if (cellData?.isModified) {
// console.log(
// `🔍 CellRenderer: Измененная ячейка ${rowIndex},${columnIndex}:`,
// {
// cellKey,
// isModified: cellData.isModified,
// displayValue,
// rawValue,
// highlightColor,
// }
// )
}
// Получаем информацию об объединенных ячейках только для листа отчета
// Объединенные ячейки только для листа отчета
const mergedCellInfo =
sheetType === 'report' && mergedCells
? findMergedCellInfo(rowIndex, columnIndex, mergedCells)
@@ -118,14 +72,12 @@ export const CellRenderer = ({
isMainCell: mergedCellInfo.isMainCell,
rowSpan: mergedCellInfo.spans?.rowSpan,
colSpan: mergedCellInfo.spans?.colSpan,
// Убираем mergedValue - будем использовать актуальные данные из движка
}
: undefined
// Проверяем, является ли ячейка скопированной
// Скопированные/вырезанные ячейки
const isCopied = Boolean(
copiedCells &&
copiedCells.sourceSheet === sheetType &&
copiedCells?.sourceSheet === sheetType &&
copiedCells.data.some(
cell => cell.row === rowIndex && cell.col === columnIndex
)
@@ -143,15 +95,10 @@ export const CellRenderer = ({
isSelected={isCellSelected}
isInMultiSelection={isInMultiSelection}
multiSelectionActive={multiSelectionActive}
selectionTop={selectionTop}
selectionBottom={selectionBottom}
selectionLeft={selectionLeft}
selectionRight={selectionRight}
isEditing={isCellEditing}
displayValue={displayValue}
rawValue={rawValue}
isModified={cellData?.isModified || false}
nextContentCol={cellData?.nextContentCol || 0}
availablePx={cellData?.availablePx || widths[columnIndex]}
onCellClick={handleCellClick}
onCellDoubleClick={handleCellDoubleClick}
@@ -165,7 +112,7 @@ export const CellRenderer = ({
setActiveSheet={setActiveSheet}
activeCellRef={activeCellInputRef}
mergedCellInfo={mergedCellProps}
highlightColor={highlightColor}
highlightColor={highlightedCell?.color}
isCopied={isCopied}
isCut={isCut}
/>

View File

@@ -185,6 +185,16 @@ export const FormulaBar = memo(
</div>
)}
{/* ExcelUploadPanel */}
{onExcelFileChange && (
<ExcelUploadPanel
fileName={excelFileName}
isLoading={isExcelLoading}
onUploadClick={() => {}}
onFileChange={onExcelFileChange}
/>
)}
{/* Кнопка сохранения */}
{onManualSave && (
<button
@@ -215,16 +225,6 @@ export const FormulaBar = memo(
</span>
</button>
)}
{/* ExcelUploadPanel */}
{onExcelFileChange && (
<ExcelUploadPanel
fileName={excelFileName}
isLoading={isExcelLoading}
onUploadClick={() => {}}
onFileChange={onExcelFileChange}
/>
)}
</div>
</div>
</div>

View File

@@ -4,10 +4,8 @@ import { COL_COUNT, ROW_COUNT, SHEET_NAMES, SpreadsheetProps } from '../types'
import { getColumnLabel } from '../utils'
import { CellRenderer } from './CellRenderer'
// Создаем типизированный компонент для совместимости с React 18
const Grid = VariableSizeGrid as unknown as React.ComponentType<any>
// --- Spreadsheet Component ---
export const Spreadsheet = ({
sheetType,
columnWidths,
@@ -40,12 +38,12 @@ export const Spreadsheet = ({
mergedCells,
copiedCells,
}: SpreadsheetProps) => {
const widths = columnWidths
const cachedData = getCachedCellData(sheetType)
const isActiveSheet = activeSheet === sheetType
const itemData = {
sheetType,
widths,
widths: columnWidths,
selectedCell,
multiSelection,
activeSheet,
@@ -55,8 +53,6 @@ export const Spreadsheet = ({
mergedCells,
highlightedCells,
copiedCells,
multiSelectionActive:
activeSheet === sheetType && multiSelection.length > 0,
handleCellClick,
handleCellDoubleClick,
handleCellChange,
@@ -70,19 +66,8 @@ export const Spreadsheet = ({
activeCellInputRef,
}
// Отладочное логирование для проверки состояния
// console.log(`📊 Spreadsheet ${sheetType} состояние:`, {
// isEditing,
// isInEditMode,
// selectedCell,
// activeSheet,
// isActiveSheet: activeSheet === sheetType,
//})
const parentRef = useRef<HTMLDivElement>(null)
const [gridSize, setGridSize] = useState({ width: 0, height: 0 })
// Состояние текущего скролла грида
const [scrollPos, setScrollPos] = useState({ left: 0, top: 0 })
useEffect(() => {
@@ -100,17 +85,98 @@ export const Spreadsheet = ({
return () => resizeObserver.disconnect()
}, [])
const selectedRows = new Set<number>()
const selectedCols = new Set<number>()
if (sheetType === activeSheet && multiSelection.length > 0) {
multiSelection.forEach(sel => {
const top = Math.min(sel.startRow, sel.endRow)
const bottom = Math.max(sel.startRow, sel.endRow)
const left = Math.min(sel.startCol, sel.endCol)
const right = Math.max(sel.startCol, sel.endCol)
for (let r = top; r <= bottom; r++) selectedRows.add(r)
for (let c = left; c <= right; c++) selectedCols.add(c)
})
// Вычисляем выделенные строки и колонки для активного листа
const getSelectedRowsAndCols = () => {
const selectedRows = new Set<number>()
const selectedCols = new Set<number>()
if (isActiveSheet && multiSelection.length > 0) {
multiSelection.forEach(sel => {
const top = Math.min(sel.startRow, sel.endRow)
const bottom = Math.max(sel.startRow, sel.endRow)
const left = Math.min(sel.startCol, sel.endCol)
const right = Math.max(sel.startCol, sel.endCol)
for (let r = top; r <= bottom; r++) selectedRows.add(r)
for (let c = left; c <= right; c++) selectedCols.add(c)
})
}
return { selectedRows, selectedCols }
}
const { selectedRows, selectedCols } = getSelectedRowsAndCols()
const handleColumnResizeStart =
(colIndex: number) => (e: React.MouseEvent) => {
e.preventDefault()
const startX = e.clientX
const startWidth = columnWidths[colIndex]
const handleMouseMove = (me: MouseEvent) => {
const newWidth = startWidth + (me.clientX - startX)
handleColumnResize(sheetType, colIndex, newWidth)
}
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
// Вычисляем границы множественного выделения
const getSelectionBounds = () => {
if (!isActiveSheet || multiSelection.length === 0) return null
return {
topRow: Math.min(
...multiSelection.map(sel => Math.min(sel.startRow, sel.endRow))
),
bottomRow: Math.max(
...multiSelection.map(sel => Math.max(sel.startRow, sel.endRow))
),
leftCol: Math.min(
...multiSelection.map(sel => Math.min(sel.startCol, sel.endCol))
),
rightCol: Math.max(
...multiSelection.map(sel => Math.max(sel.startCol, sel.endCol))
),
}
}
const selectionBounds = getSelectionBounds()
const renderSelectionOverlay = () => {
if (!selectionBounds) return null
const topPx = selectionBounds.topRow * 28 - scrollPos.top
const heightPx =
(selectionBounds.bottomRow - selectionBounds.topRow + 1) * 28
const leftPx =
columnWidths
.slice(0, selectionBounds.leftCol)
.reduce((a, b) => a + b, 0) - scrollPos.left
const widthPx = columnWidths
.slice(selectionBounds.leftCol, selectionBounds.rightCol + 1)
.reduce((a, b) => a + b, 0)
return (
<div
style={{
position: 'absolute',
top: topPx,
left: leftPx,
width: widthPx,
height: heightPx,
border: '2px solid hsl(var(--primary))',
pointerEvents: 'none',
boxSizing: 'border-box',
zIndex: 5,
}}
/>
)
}
return (
@@ -131,12 +197,10 @@ export const Spreadsheet = ({
style={{ userSelect: isEditing ? 'text' : 'none' }}
tabIndex={0}
>
{/* Spreadsheet Body */}
<div className="flex flex-1">
{/* Row Headers */}
<div className="flex-shrink-0 bg-muted">
<div className="relative flex h-7 w-10 items-center justify-center border-b border-r border-border">
{/* Компактное обозначение листа */}
<div className="absolute inset-0 flex items-end justify-end p-0.5">
<span
className={`flex h-3 w-3 items-center justify-center rounded-sm text-[9px] font-medium text-white ${
@@ -159,13 +223,16 @@ export const Spreadsheet = ({
<div>
{Array.from({ length: ROW_COUNT }).map((_, rowIndex) => {
const isActiveRow =
(selectedCell?.row === rowIndex &&
activeSheet === sheetType) ||
(selectedCell?.row === rowIndex && isActiveSheet) ||
selectedRows.has(rowIndex)
return (
<div
key={rowIndex}
className={`flex h-7 w-10 items-center justify-center border-b border-r border-border text-xs font-medium ${isActiveRow ? 'bg-primary/15 font-medium text-foreground' : 'text-muted-foreground'}`}
className={`flex h-7 w-10 items-center justify-center border-b border-r border-border text-xs font-medium ${
isActiveRow
? 'bg-primary/15 font-medium text-foreground'
: 'text-muted-foreground'
}`}
>
{rowIndex + 1}
</div>
@@ -184,38 +251,25 @@ export const Spreadsheet = ({
className="column-headers overflow-x-hidden"
>
<div className="flex">
{Array.from({ length: COL_COUNT }).map((_, i) => {
{Array.from({ length: COL_COUNT }).map((_, colIndex) => {
const isActiveColumn =
(selectedCell?.col === i && activeSheet === sheetType) ||
selectedCols.has(i)
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault()
const startX = e.clientX
const startWidth = widths[i]
(selectedCell?.col === colIndex && isActiveSheet) ||
selectedCols.has(colIndex)
const handleMouseMove = (me: MouseEvent) => {
const newWidth = startWidth + (me.clientX - startX)
handleColumnResize(sheetType, i, newWidth)
}
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
return (
<div
key={i}
className={`relative flex h-7 flex-shrink-0 items-center justify-center border-b border-r border-border bg-muted text-center text-xs font-medium ${isActiveColumn ? 'bg-primary/15 font-medium text-foreground' : 'text-muted-foreground'}`}
style={{ width: widths[i] }}
key={colIndex}
className={`relative flex h-7 flex-shrink-0 items-center justify-center border-b border-r border-border bg-muted text-center text-xs font-medium ${
isActiveColumn
? 'bg-primary/15 font-medium text-foreground'
: 'text-muted-foreground'
}`}
style={{ width: columnWidths[colIndex] }}
>
{getColumnLabel(i)}
{getColumnLabel(colIndex)}
<div
className="resizer absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary hover:bg-opacity-50"
onMouseDown={handleMouseDown}
onMouseDown={handleColumnResizeStart(colIndex)}
/>
</div>
)
@@ -225,60 +279,7 @@ export const Spreadsheet = ({
{/* Grid */}
<div className="relative min-h-0 flex-1">
{/* selection overlay */}
{sheetType === activeSheet &&
multiSelection.length > 0 &&
(() => {
const bounds = {
topRow: Math.min(
...multiSelection.map(sel =>
Math.min(sel.startRow, sel.endRow)
)
),
bottomRow: Math.max(
...multiSelection.map(sel =>
Math.max(sel.startRow, sel.endRow)
)
),
leftCol: Math.min(
...multiSelection.map(sel =>
Math.min(sel.startCol, sel.endCol)
)
),
rightCol: Math.max(
...multiSelection.map(sel =>
Math.max(sel.startCol, sel.endCol)
)
),
}
const topPx =
bounds.topRow * 28 /* ROW_HEIGHT */ - scrollPos.top
const heightPx = (bounds.bottomRow - bounds.topRow + 1) * 28
const leftPx =
widths.slice(0, bounds.leftCol).reduce((a, b) => a + b, 0) -
scrollPos.left
const widthPx = widths
.slice(bounds.leftCol, bounds.rightCol + 1)
.reduce((a, b) => a + b, 0)
return (
<div
style={{
position: 'absolute',
top: topPx,
left: leftPx,
width: widthPx,
height: heightPx,
border: '2px solid hsl(var(--primary))',
pointerEvents: 'none',
boxSizing: 'border-box',
transition: 'none',
zIndex: 5,
}}
/>
)
})()}
{renderSelectionOverlay()}
<Grid
ref={(el: any) => {
if (gridRefs.current) {
@@ -287,7 +288,7 @@ export const Spreadsheet = ({
}}
columnCount={COL_COUNT}
rowCount={ROW_COUNT}
columnWidth={(index: number) => widths[index]}
columnWidth={(index: number) => columnWidths[index]}
rowHeight={() => 28}
height={gridSize.height}
width={gridSize.width}