700 lines
22 KiB
TypeScript
700 lines
22 KiB
TypeScript
import { produce } from 'immer'
|
||
import {
|
||
FC,
|
||
memo,
|
||
useCallback,
|
||
useEffect,
|
||
useMemo,
|
||
useRef,
|
||
useState,
|
||
} from 'react'
|
||
import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine'
|
||
|
||
const ROW_COUNT = 90
|
||
const COL_COUNT = 20
|
||
const ROW_HEIGHT = 28
|
||
|
||
const SHEET_NAMES = {
|
||
report: 'L',
|
||
calculations: 'R',
|
||
} as const
|
||
|
||
type SheetType = keyof typeof SHEET_NAMES
|
||
|
||
// --- Helper Functions ---
|
||
const getColumnLabel = (index: number) => String.fromCharCode(65 + index)
|
||
|
||
// --- Interfaces ---
|
||
interface DualSpreadsheetProps {
|
||
templateData?: Record<string, Record<string, any>>
|
||
}
|
||
|
||
// --- Child Components ---
|
||
interface CellProps {
|
||
sheetType: SheetType
|
||
rowIndex: number
|
||
colIndex: number
|
||
columnWidth: number
|
||
isSelected: boolean
|
||
isEditing: boolean
|
||
displayValue: string // Отображаемое значение (вычисленное)
|
||
rawValue: string // "Сырое" значение (формула)
|
||
isModified: boolean
|
||
onCellClick: (row: number, col: number) => void
|
||
onCellDoubleClick: (row: number, col: number) => void
|
||
onCellValueChange: (newValue: string) => void // Изменение значения во время редактирования
|
||
stopEditing: (save: boolean) => void
|
||
setActiveSheet: (type: SheetType) => void
|
||
activeCellRef: React.RefObject<HTMLInputElement>
|
||
}
|
||
|
||
const Cell = memo(
|
||
({
|
||
sheetType,
|
||
rowIndex,
|
||
colIndex,
|
||
columnWidth,
|
||
isSelected,
|
||
isEditing,
|
||
displayValue,
|
||
rawValue,
|
||
isModified,
|
||
onCellClick,
|
||
onCellDoubleClick,
|
||
onCellValueChange,
|
||
stopEditing,
|
||
setActiveSheet,
|
||
activeCellRef,
|
||
}: CellProps) => {
|
||
const className = `border border-border ${isSelected ? 'bg-accent' : ''} ${
|
||
isModified ? 'bg-yellow-50' : ''
|
||
}`
|
||
|
||
const style = {
|
||
minWidth: `${columnWidth}px`,
|
||
width: `${columnWidth}px`,
|
||
height: `${ROW_HEIGHT}px`,
|
||
overflow: 'hidden',
|
||
padding: 0,
|
||
position: 'relative' as const,
|
||
}
|
||
|
||
const handleClick = useCallback(() => {
|
||
setActiveSheet(sheetType)
|
||
onCellClick(rowIndex, colIndex)
|
||
}, [setActiveSheet, sheetType, onCellClick, rowIndex, colIndex])
|
||
|
||
const handleDoubleClick = useCallback(
|
||
() => onCellDoubleClick(rowIndex, colIndex),
|
||
[onCellDoubleClick, rowIndex, colIndex],
|
||
)
|
||
|
||
return (
|
||
<td
|
||
className={className}
|
||
style={style}
|
||
onClick={handleClick}
|
||
onDoubleClick={handleDoubleClick}
|
||
>
|
||
{isEditing && isSelected ? (
|
||
<input
|
||
ref={activeCellRef}
|
||
type="text"
|
||
value={rawValue} // Во время редактирования показываем "сырое" значение
|
||
onChange={(e) => onCellValueChange(e.target.value)}
|
||
onBlur={() => stopEditing(true)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault()
|
||
stopEditing(true)
|
||
} else if (e.key === 'Escape') {
|
||
e.preventDefault()
|
||
stopEditing(false)
|
||
}
|
||
}}
|
||
className="absolute left-0 top-0 z-20 box-border h-full w-full border-2 border-primary px-2 py-1"
|
||
/>
|
||
) : (
|
||
<div
|
||
className="relative h-full w-full truncate px-2 py-1"
|
||
style={
|
||
isSelected
|
||
? { boxShadow: 'inset 0 0 0 2px hsl(var(--primary))' }
|
||
: {}
|
||
}
|
||
>
|
||
{displayValue}
|
||
</div>
|
||
)}
|
||
</td>
|
||
)
|
||
},
|
||
)
|
||
|
||
interface FormulaBarProps {
|
||
sheetType: SheetType
|
||
activeSheet: SheetType
|
||
selectedCell: { row: number; col: number } | null
|
||
formulaBarValue: string
|
||
isCalculating: boolean
|
||
isEditing: boolean
|
||
onValueChange: (value: string) => void
|
||
onFocus: () => void
|
||
onBlur: () => void
|
||
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void
|
||
formulaBarRef: (el: HTMLInputElement | null) => void
|
||
}
|
||
|
||
const FormulaBar = memo(
|
||
({
|
||
sheetType,
|
||
activeSheet,
|
||
selectedCell,
|
||
formulaBarValue,
|
||
isCalculating,
|
||
isEditing,
|
||
onValueChange,
|
||
onFocus,
|
||
onBlur,
|
||
onKeyDown,
|
||
formulaBarRef,
|
||
}: FormulaBarProps) => {
|
||
if (activeSheet !== sheetType) {
|
||
return (
|
||
<div className="flex-shrink-0 border-b bg-background px-3 py-1">
|
||
<div className="flex h-[28px] items-center" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="flex-shrink-0 border-b bg-background px-3 py-1">
|
||
<div className="flex items-center space-x-2">
|
||
<div className="flex min-w-[50px] items-center justify-center rounded-md border bg-muted px-2 py-1">
|
||
<span className="text-xs font-medium text-muted-foreground">
|
||
{selectedCell
|
||
? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}`
|
||
: 'A1'}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-1 items-center space-x-2">
|
||
<div className="flex items-center text-muted-foreground">
|
||
<svg
|
||
className="h-3.5 w-3.5"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<input
|
||
ref={formulaBarRef}
|
||
type="text"
|
||
className={`flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm ring-offset-background transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${
|
||
isEditing ? 'ring-2 ring-ring ring-offset-2' : ''
|
||
} ${isCalculating ? 'animate-pulse' : ''}`}
|
||
placeholder="Введите формулу или значение..."
|
||
value={formulaBarValue}
|
||
onChange={(e) => onValueChange(e.target.value)}
|
||
onFocus={onFocus}
|
||
onBlur={onBlur}
|
||
onKeyDown={onKeyDown}
|
||
/>
|
||
{isCalculating && (
|
||
<div className="flex items-center text-muted-foreground">
|
||
<svg
|
||
className="h-3.5 w-3.5 animate-spin"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<circle
|
||
className="opacity-25"
|
||
cx="12"
|
||
cy="12"
|
||
r="10"
|
||
stroke="currentColor"
|
||
strokeWidth="4"
|
||
></circle>
|
||
<path
|
||
className="opacity-75"
|
||
fill="currentColor"
|
||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||
></path>
|
||
</svg>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
},
|
||
)
|
||
|
||
// --- Main Component ---
|
||
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||
const { revision, ...multiSheetEngine } = useMultiSheetEngine({
|
||
sheets: [
|
||
{ name: SHEET_NAMES.report, rows: ROW_COUNT, cols: COL_COUNT },
|
||
{ name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT },
|
||
],
|
||
initialData: templateData || {},
|
||
})
|
||
|
||
const [activeSheet, setActiveSheet] = useState<SheetType>('report')
|
||
const [selectedCell, setSelectedCell] = useState<{
|
||
row: number
|
||
col: number
|
||
} | null>(null)
|
||
const [isEditing, setIsEditing] = useState(false)
|
||
const [editingValue, setEditingValue] = useState<string>('') // Локальное значение во время редактирования
|
||
|
||
const [columnWidths, setColumnWidths] = useState<Record<SheetType, number[]>>(
|
||
() => ({
|
||
report: Array(COL_COUNT).fill(96),
|
||
calculations: Array(COL_COUNT).fill(96),
|
||
}),
|
||
)
|
||
|
||
const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||
report: null,
|
||
calculations: null,
|
||
})
|
||
const activeCellInputRef = useRef<HTMLInputElement>(null)
|
||
const formulaBarRefs = useRef<Record<SheetType, HTMLInputElement | null>>({
|
||
report: null,
|
||
calculations: null,
|
||
})
|
||
const initialTemplateValues = useRef<Record<string, string>>({})
|
||
|
||
// Загрузка данных шаблона один раз при инициализации
|
||
useEffect(() => {
|
||
const reportTemplate = templateData?.[SHEET_NAMES.report]
|
||
if (reportTemplate) {
|
||
Object.entries(reportTemplate).forEach(([cellName, value]) => {
|
||
initialTemplateValues.current[cellName] = String(value)
|
||
})
|
||
}
|
||
}, [templateData])
|
||
|
||
// --- Синхронизация UI ---
|
||
|
||
// Синхронизация строки формул
|
||
const formulaBarValue = useMemo(() => {
|
||
if (isEditing) {
|
||
return editingValue
|
||
}
|
||
if (selectedCell) {
|
||
const sheetName = SHEET_NAMES[activeSheet]
|
||
return multiSheetEngine.getCellRawValue(
|
||
sheetName,
|
||
selectedCell.row,
|
||
selectedCell.col,
|
||
)
|
||
}
|
||
return ''
|
||
}, [
|
||
isEditing,
|
||
editingValue,
|
||
selectedCell,
|
||
activeSheet,
|
||
multiSheetEngine,
|
||
revision,
|
||
])
|
||
|
||
// Фокус на инпуте при начале редактирования
|
||
useEffect(() => {
|
||
if (isEditing) {
|
||
if (activeCellInputRef.current) {
|
||
activeCellInputRef.current.focus()
|
||
activeCellInputRef.current.select()
|
||
}
|
||
}
|
||
}, [isEditing])
|
||
|
||
// --- Функции-обработчики ---
|
||
|
||
const startEditing = useCallback(() => {
|
||
if (!selectedCell) return
|
||
const sheetName = SHEET_NAMES[activeSheet]
|
||
const rawValue = multiSheetEngine.getCellRawValue(
|
||
sheetName,
|
||
selectedCell.row,
|
||
selectedCell.col,
|
||
)
|
||
setIsEditing(true)
|
||
setEditingValue(rawValue)
|
||
}, [activeSheet, selectedCell, multiSheetEngine])
|
||
|
||
const stopEditing = useCallback(
|
||
(save: boolean) => {
|
||
if (save && selectedCell) {
|
||
const sheetName = SHEET_NAMES[activeSheet]
|
||
multiSheetEngine.setCellValueWithoutRecalc(
|
||
sheetName,
|
||
selectedCell.row,
|
||
selectedCell.col,
|
||
editingValue,
|
||
)
|
||
multiSheetEngine.debouncedRecalc()
|
||
}
|
||
setIsEditing(false)
|
||
setEditingValue('') // Очищаем временное значение
|
||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||
},
|
||
[activeSheet, selectedCell, editingValue, multiSheetEngine],
|
||
)
|
||
|
||
const handleCellChange = useCallback(
|
||
(newValue: string) => {
|
||
if (isEditing) {
|
||
setEditingValue(newValue)
|
||
}
|
||
},
|
||
[isEditing],
|
||
)
|
||
|
||
const handleClearCell = useCallback(() => {
|
||
if (!selectedCell) return
|
||
const sheetName = SHEET_NAMES[activeSheet]
|
||
multiSheetEngine.setCellValue(
|
||
sheetName,
|
||
selectedCell.row,
|
||
selectedCell.col,
|
||
'',
|
||
)
|
||
}, [activeSheet, selectedCell, multiSheetEngine])
|
||
|
||
const moveSelection = useCallback(
|
||
(deltaRow: number, deltaCol: number) => {
|
||
if (!selectedCell) {
|
||
// Если ничего не выбрано, выбираем A1
|
||
setSelectedCell({ row: 0, col: 0 })
|
||
return
|
||
}
|
||
|
||
if (isEditing) {
|
||
stopEditing(true)
|
||
}
|
||
|
||
const newRow = Math.max(
|
||
0,
|
||
Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow),
|
||
)
|
||
const newCol = Math.max(
|
||
0,
|
||
Math.min(COL_COUNT - 1, selectedCell.col + deltaCol),
|
||
)
|
||
|
||
setSelectedCell({ row: newRow, col: newCol })
|
||
},
|
||
[selectedCell, isEditing, stopEditing],
|
||
)
|
||
|
||
const handleKeyDown = useCallback(
|
||
(e: React.KeyboardEvent) => {
|
||
if (isEditing) {
|
||
// Логика внутри Cell компонента
|
||
return
|
||
}
|
||
|
||
if (!selectedCell && e.key.match(/^(Arrow|Enter|F2|Delete|Backspace)$/)) {
|
||
moveSelection(0, 0)
|
||
e.preventDefault()
|
||
return
|
||
}
|
||
if (!selectedCell) return
|
||
|
||
const keyHandlers: Record<string, () => void> = {
|
||
ArrowUp: () => moveSelection(-1, 0),
|
||
ArrowDown: () => moveSelection(1, 0),
|
||
ArrowLeft: () => moveSelection(0, -1),
|
||
ArrowRight: () => moveSelection(0, 1),
|
||
Enter: () => startEditing(),
|
||
F2: () => startEditing(),
|
||
Delete: handleClearCell,
|
||
Backspace: handleClearCell,
|
||
}
|
||
|
||
if (keyHandlers[e.key]) {
|
||
e.preventDefault()
|
||
keyHandlers[e.key]()
|
||
} else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
||
e.preventDefault()
|
||
setEditingValue(e.key) // Начинаем редактирование с введенного символа
|
||
setIsEditing(true)
|
||
}
|
||
},
|
||
[selectedCell, isEditing, moveSelection, startEditing, handleClearCell],
|
||
)
|
||
|
||
const handleCellClick = useCallback(
|
||
(row: number, col: number) => {
|
||
if (isEditing) {
|
||
stopEditing(true)
|
||
}
|
||
setSelectedCell({ row, col })
|
||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||
},
|
||
[isEditing, stopEditing, activeSheet],
|
||
)
|
||
|
||
const handleCellDoubleClick = useCallback(
|
||
(row: number, col: number) => {
|
||
setSelectedCell({ row, col })
|
||
startEditing()
|
||
},
|
||
[startEditing],
|
||
)
|
||
|
||
const handleColumnResize = useCallback(
|
||
(sheetType: SheetType, colIndex: number, newWidth: number) => {
|
||
setColumnWidths(
|
||
produce((draft) => {
|
||
draft[sheetType][colIndex] = Math.max(60, newWidth)
|
||
}),
|
||
)
|
||
},
|
||
[],
|
||
)
|
||
|
||
// --- Formula Bar Specific Handlers ---
|
||
const handleFormulaBarChange = useCallback(
|
||
(newValue: string) => {
|
||
if (selectedCell) {
|
||
if (!isEditing) {
|
||
setIsEditing(true)
|
||
}
|
||
setEditingValue(newValue)
|
||
}
|
||
},
|
||
[selectedCell, isEditing],
|
||
)
|
||
|
||
const handleFormulaBarFocus = useCallback(() => {
|
||
if (selectedCell && !isEditing) {
|
||
startEditing()
|
||
}
|
||
}, [selectedCell, isEditing, startEditing])
|
||
|
||
const handleFormulaBarBlur = useCallback(() => {
|
||
if (isEditing) {
|
||
stopEditing(true)
|
||
}
|
||
}, [isEditing, stopEditing])
|
||
|
||
const handleFormulaBarKeyDown = useCallback(
|
||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault()
|
||
stopEditing(true)
|
||
moveSelection(1, 0)
|
||
// Предотвращаем прокрутку страницы
|
||
setTimeout(() => {
|
||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||
}, 0)
|
||
} else if (e.key === 'Escape') {
|
||
e.preventDefault()
|
||
stopEditing(false)
|
||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||
}
|
||
},
|
||
[stopEditing, moveSelection, activeSheet],
|
||
)
|
||
|
||
// --- Render Functions ---
|
||
const renderSpreadsheet = (sheetType: SheetType) => {
|
||
const widths = columnWidths[sheetType]
|
||
const sheetName = SHEET_NAMES[sheetType]
|
||
|
||
return (
|
||
<div
|
||
ref={(el) => (containerRefs.current[sheetType] = el)}
|
||
className="flex h-full flex-col overflow-hidden rounded-lg border bg-card shadow-sm"
|
||
onKeyDown={handleKeyDown}
|
||
tabIndex={0}
|
||
>
|
||
<div className="flex-shrink-0 border-b bg-muted/50 px-3 py-1">
|
||
<div className="flex items-center space-x-2">
|
||
<div
|
||
className={`h-2 w-2 rounded-full ${
|
||
sheetType === 'report' ? 'bg-primary' : 'bg-emerald-500'
|
||
}`}
|
||
></div>
|
||
<h3 className="text-xs font-medium text-muted-foreground">
|
||
{sheetType === 'report' ? 'Отчет' : 'Расчеты'}
|
||
</h3>
|
||
</div>
|
||
</div>
|
||
<div className="flex-1 overflow-auto">
|
||
<table
|
||
className="w-full border-collapse"
|
||
style={{ tableLayout: 'fixed' }}
|
||
>
|
||
<thead>
|
||
<tr>
|
||
<th className="h-7 w-10 border border-border bg-muted text-xs font-medium text-muted-foreground"></th>
|
||
{Array.from({ length: COL_COUNT }, (_, i) => {
|
||
const handleMouseDown = (e: React.MouseEvent) => {
|
||
e.preventDefault()
|
||
const startX = e.clientX
|
||
const startWidth = widths[i]
|
||
|
||
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 (
|
||
<th
|
||
key={i}
|
||
className="relative h-7 border border-border bg-muted text-center text-xs font-medium text-muted-foreground"
|
||
style={{ width: widths[i] }}
|
||
>
|
||
{getColumnLabel(i)}
|
||
<div
|
||
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary hover:bg-opacity-50"
|
||
onMouseDown={handleMouseDown}
|
||
/>
|
||
</th>
|
||
)
|
||
})}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{Array.from({ length: ROW_COUNT }).map((_, rowIndex) => (
|
||
<tr key={rowIndex}>
|
||
<td className="h-7 w-10 border border-border bg-muted text-center text-xs font-medium text-muted-foreground">
|
||
{rowIndex + 1}
|
||
</td>
|
||
{Array.from({ length: COL_COUNT }).map((_, colIndex) => {
|
||
const isCellSelected =
|
||
selectedCell?.row === rowIndex &&
|
||
selectedCell?.col === colIndex &&
|
||
activeSheet === sheetType
|
||
const isCellEditing = isCellSelected && isEditing
|
||
|
||
const rawValue = isCellEditing
|
||
? editingValue
|
||
: multiSheetEngine.getCellRawValue(
|
||
sheetName,
|
||
rowIndex,
|
||
colIndex,
|
||
)
|
||
const displayValue = isCellEditing
|
||
? '' // Не показывать вычисленное значение во время редактирования
|
||
: String(
|
||
multiSheetEngine.getCellValue(
|
||
sheetName,
|
||
rowIndex,
|
||
colIndex,
|
||
) || '',
|
||
)
|
||
|
||
const cellName = getColumnLabel(colIndex) + (rowIndex + 1)
|
||
const templateValue =
|
||
initialTemplateValues.current[cellName] ?? ''
|
||
const isModified =
|
||
sheetType === 'report' &&
|
||
templateValue !== '' &&
|
||
rawValue !== templateValue
|
||
|
||
return (
|
||
<Cell
|
||
key={colIndex}
|
||
sheetType={sheetType}
|
||
rowIndex={rowIndex}
|
||
colIndex={colIndex}
|
||
columnWidth={widths[colIndex]}
|
||
isSelected={isCellSelected}
|
||
isEditing={isCellEditing}
|
||
displayValue={displayValue}
|
||
rawValue={rawValue}
|
||
isModified={isModified}
|
||
onCellClick={handleCellClick}
|
||
onCellDoubleClick={handleCellDoubleClick}
|
||
onCellValueChange={handleCellChange}
|
||
stopEditing={stopEditing}
|
||
setActiveSheet={setActiveSheet}
|
||
activeCellRef={activeCellInputRef}
|
||
/>
|
||
)
|
||
})}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Используем revision в качестве зависимости, чтобы гарантировать перерисовку
|
||
// при изменении данных в движке.
|
||
const memoizedReportSheet = useMemo(
|
||
() => renderSpreadsheet('report'),
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
[
|
||
columnWidths.report,
|
||
activeSheet,
|
||
selectedCell,
|
||
isEditing,
|
||
editingValue,
|
||
revision,
|
||
],
|
||
)
|
||
|
||
const memoizedCalculationsSheet = useMemo(
|
||
() => renderSpreadsheet('calculations'),
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
[
|
||
columnWidths.calculations,
|
||
activeSheet,
|
||
selectedCell,
|
||
isEditing,
|
||
editingValue,
|
||
revision,
|
||
],
|
||
)
|
||
|
||
return (
|
||
<div className="flex h-screen flex-col bg-background">
|
||
<div className="flex-shrink-0">
|
||
<FormulaBar
|
||
sheetType={activeSheet}
|
||
activeSheet={activeSheet}
|
||
selectedCell={selectedCell}
|
||
formulaBarValue={formulaBarValue}
|
||
isCalculating={multiSheetEngine.isCalculating}
|
||
isEditing={isEditing}
|
||
onValueChange={handleFormulaBarChange}
|
||
onFocus={handleFormulaBarFocus}
|
||
onBlur={handleFormulaBarBlur}
|
||
onKeyDown={handleFormulaBarKeyDown}
|
||
formulaBarRef={(el) => (formulaBarRefs.current[activeSheet] = el)}
|
||
/>
|
||
</div>
|
||
{/* Двойная панель */}
|
||
<div className="flex flex-1 gap-1 overflow-hidden p-1">
|
||
<div className="min-w-0 flex-1">{memoizedReportSheet}</div>
|
||
<div className="min-w-0 flex-1">{memoizedCalculationsSheet}</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default DualSpreadsheet
|