ручной рефакторинг

This commit is contained in:
2025-07-20 09:11:46 +03:00
parent 1d92316db7
commit da7024cd08
8 changed files with 489 additions and 462 deletions

View File

@@ -1,3 +1,6 @@
import { useFormulaAutoSave } from '@/hooks/useFormulaAutoSave'
import { useMultiSheetEngine } from '@/hooks/useMultiSheetEngine'
import { MergedCell } from '@/types/template'
import { produce } from 'immer' import { produce } from 'immer'
import { import {
CSSProperties, CSSProperties,
@@ -10,9 +13,6 @@ import {
useState, useState,
} from 'react' } from 'react'
import { VariableSizeGrid } from 'react-window' import { VariableSizeGrid } from 'react-window'
import { useFormulaAutoSave } from '../../hooks/useFormulaAutoSave'
import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine'
import { MergedCell } from '../../types/template'
const ROW_COUNT = 90 const ROW_COUNT = 90
const COL_COUNT = 20 const COL_COUNT = 20
@@ -25,6 +25,9 @@ const SHEET_NAMES = {
type SheetType = keyof typeof SHEET_NAMES type SheetType = keyof typeof SHEET_NAMES
// Добавляем массив типов листов для итерации
const SHEET_TYPES: SheetType[] = ['report', 'calculations']
// --- Helper Functions --- // --- Helper Functions ---
const getColumnLabel = (index: number) => String.fromCharCode(65 + index) const getColumnLabel = (index: number) => String.fromCharCode(65 + index)
@@ -43,7 +46,7 @@ const cellAddressToCoords = (address: string): { row: number; col: number } => {
const findMergedCellInfo = ( const findMergedCellInfo = (
row: number, row: number,
col: number, col: number,
mergedCells: MergedCell[], mergedCells: MergedCell[]
): { ): {
isMerged: boolean isMerged: boolean
isMainCell: boolean isMainCell: boolean
@@ -77,6 +80,57 @@ const findMergedCellInfo = (
return { isMerged: false, isMainCell: false } return { isMerged: false, isMainCell: false }
} }
// --- Хук для перетаскивания разделителя ---
const useDraggableDivider = (onDrag: (delta: number) => void) => {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const el = ref.current
if (!el) return
const handleMouseDown = (e: MouseEvent) => {
e.preventDefault()
document.body.style.cursor = 'col-resize'
const container = el.parentElement
if (!container) return
const bounds = container.getBoundingClientRect()
const handleMouseMove = (me: MouseEvent) => {
// Получаем актуальные границы контейнера
const currentBounds = container.getBoundingClientRect()
const newLeftWidth = me.clientX - currentBounds.left
const totalWidth = currentBounds.width
// Рассчитываем процент напрямую от позиции мыши
let newLeftPercentage = newLeftWidth / totalWidth
newLeftPercentage = Math.max(0.2, Math.min(0.8, newLeftPercentage))
// Вызываем callback с новыми процентами
onDrag(newLeftPercentage)
}
const handleMouseUp = () => {
document.body.style.cursor = 'auto'
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
el.addEventListener('mousedown', handleMouseDown)
return () => {
el.removeEventListener('mousedown', handleMouseDown)
}
}, [onDrag])
return ref
}
// --- Interfaces --- // --- Interfaces ---
interface DualSpreadsheetProps { interface DualSpreadsheetProps {
templateData?: Record<string, Record<string, any>> templateData?: Record<string, Record<string, any>>
@@ -197,8 +251,8 @@ const Cell = memo(
backgroundColor: isSelected backgroundColor: isSelected
? 'hsl(var(--accent))' ? 'hsl(var(--accent))'
: isModified : isModified
? '#fef3c7' ? '#fef3c7'
: 'white', : 'white',
} }
} }
@@ -212,7 +266,7 @@ const Cell = memo(
const handleDoubleClick = useCallback( const handleDoubleClick = useCallback(
() => onCellDoubleClick(rowIndex, colIndex), () => onCellDoubleClick(rowIndex, colIndex),
[onCellDoubleClick, rowIndex, colIndex], [onCellDoubleClick, rowIndex, colIndex]
) )
return ( return (
@@ -227,8 +281,8 @@ const Cell = memo(
ref={activeCellRef} ref={activeCellRef}
type="text" type="text"
value={rawValue} // Во время редактирования показываем текущее редактируемое значение value={rawValue} // Во время редактирования показываем текущее редактируемое значение
onChange={(e) => onCellValueChange(e.target.value)} onChange={e => onCellValueChange(e.target.value)}
onBlur={(e) => { onBlur={e => {
// Проверяем, не переходит ли фокус в formula bar // Проверяем, не переходит ли фокус в formula bar
const relatedTarget = e.relatedTarget as HTMLElement const relatedTarget = e.relatedTarget as HTMLElement
if (relatedTarget && relatedTarget.closest('.formula-bar')) { if (relatedTarget && relatedTarget.closest('.formula-bar')) {
@@ -236,7 +290,7 @@ const Cell = memo(
} }
stopEditing(true) stopEditing(true)
}} }}
onKeyDown={(e) => { onKeyDown={e => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault() e.preventDefault()
stopEditing(true) stopEditing(true)
@@ -273,7 +327,7 @@ const Cell = memo(
)} )}
</div> </div>
) )
}, }
) )
interface FormulaBarProps { interface FormulaBarProps {
@@ -347,9 +401,9 @@ const FormulaBar = memo(
} ${isCalculating ? 'animate-pulse' : ''}`} } ${isCalculating ? 'animate-pulse' : ''}`}
placeholder="Введите формулу или значение..." placeholder="Введите формулу или значение..."
value={formulaBarValue} value={formulaBarValue}
onChange={(e) => onValueChange(e.target.value)} onChange={e => onValueChange(e.target.value)}
onFocus={onFocus} onFocus={onFocus}
onBlur={(e) => { onBlur={e => {
// Проверяем, не переходит ли фокус в ячейку редактирования // Проверяем, не переходит ли фокус в ячейку редактирования
const relatedTarget = e.relatedTarget as HTMLElement const relatedTarget = e.relatedTarget as HTMLElement
if ( if (
@@ -441,15 +495,15 @@ const FormulaBar = memo(
hasUnsavedChanges hasUnsavedChanges
? 'bg-primary text-primary-foreground hover:bg-primary/90' ? 'bg-primary text-primary-foreground hover:bg-primary/90'
: lastSaveError : lastSaveError
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
: 'bg-muted text-muted-foreground' : 'bg-muted text-muted-foreground'
} disabled:opacity-50`} } disabled:opacity-50`}
title={ title={
lastSaveError lastSaveError
? 'Повторить сохранение' ? 'Повторить сохранение'
: hasUnsavedChanges : hasUnsavedChanges
? 'Есть несохраненные изменения' ? 'Есть несохраненные изменения'
: 'Все изменения сохранены' : 'Все изменения сохранены'
} }
> >
<svg <svg
@@ -468,15 +522,15 @@ const FormulaBar = memo(
{lastSaveError {lastSaveError
? 'Повторить' ? 'Повторить'
: hasUnsavedChanges : hasUnsavedChanges
? 'Сохранить' ? 'Сохранить'
: 'Сохранено'} : 'Сохранено'}
</button> </button>
)} )}
</div> </div>
</div> </div>
</div> </div>
) )
}, }
) )
// Рендер-проп для ячеек грида // Рендер-проп для ячеек грида
@@ -531,7 +585,7 @@ const CellRenderer = ({
isModified: cellData.isModified, isModified: cellData.isModified,
displayValue, displayValue,
rawValue, rawValue,
}, }
) )
} }
@@ -599,15 +653,15 @@ interface SpreadsheetProps {
rowHeaderRefs: React.MutableRefObject< rowHeaderRefs: React.MutableRefObject<
Record<SheetType, HTMLDivElement | null> Record<SheetType, HTMLDivElement | null>
> >
gridRefs: React.MutableRefObject<Record<SheetType, VariableSizeGrid | null>> gridRefs: React.MutableRefObject<Record<SheetType, any>>
onGridScroll: ( onGridScroll: (
sheetType: SheetType, sheetType: SheetType,
params: { scrollLeft: number; scrollTop: number }, params: { scrollLeft: number; scrollTop: number }
) => void ) => void
handleColumnResize: ( handleColumnResize: (
sheetType: SheetType, sheetType: SheetType,
colIndex: number, colIndex: number,
newWidth: number, newWidth: number
) => void ) => void
mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек
} }
@@ -681,11 +735,12 @@ const Spreadsheet = ({
className="flex min-w-0 flex-col overflow-hidden" className="flex min-w-0 flex-col overflow-hidden"
style={{ style={{
flexBasis: `${spreadsheetWidths[SHEET_NAMES[sheetType]] * 100}%`, flexBasis: `${spreadsheetWidths[SHEET_NAMES[sheetType]] * 100}%`,
transition: 'none',
}} }}
onClick={() => setActiveSheet(sheetType)} onClick={() => setActiveSheet(sheetType)}
> >
<div <div
ref={(el) => (containerRefs.current[sheetType] = el)} ref={el => (containerRefs.current[sheetType] = el)}
className="flex h-full flex-col overflow-hidden border bg-card shadow-sm" className="flex h-full flex-col overflow-hidden border bg-card shadow-sm"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
tabIndex={0} tabIndex={0}
@@ -709,7 +764,7 @@ const Spreadsheet = ({
</div> </div>
</div> </div>
<div <div
ref={(el) => (rowHeaderRefs.current[sheetType] = el)} ref={el => (rowHeaderRefs.current[sheetType] = el)}
className="overflow-y-hidden" className="overflow-y-hidden"
style={{ height: gridSize.height }} style={{ height: gridSize.height }}
> >
@@ -729,7 +784,7 @@ const Spreadsheet = ({
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
{/* Column Headers */} {/* Column Headers */}
<div <div
ref={(el) => (headerRefs.current[sheetType] = el)} ref={el => (headerRefs.current[sheetType] = el)}
className="column-headers overflow-x-hidden" className="column-headers overflow-x-hidden"
> >
<div className="flex"> <div className="flex">
@@ -772,7 +827,7 @@ const Spreadsheet = ({
{/* Grid */} {/* Grid */}
<div className="min-h-0 flex-1"> <div className="min-h-0 flex-1">
<VariableSizeGrid <VariableSizeGrid
ref={(el) => (gridRefs.current[sheetType] = el)} ref={el => (gridRefs.current[sheetType] = el)}
columnCount={COL_COUNT} columnCount={COL_COUNT}
rowCount={ROW_COUNT} rowCount={ROW_COUNT}
columnWidth={(index: number) => widths[index]} columnWidth={(index: number) => widths[index]}
@@ -780,7 +835,7 @@ const Spreadsheet = ({
height={gridSize.height} height={gridSize.height}
width={gridSize.width} width={gridSize.width}
itemData={itemData} itemData={itemData}
onScroll={(e) => onGridScroll(sheetType, e)} onScroll={e => onGridScroll(sheetType, e)}
> >
{CellRenderer} {CellRenderer}
</VariableSizeGrid> </VariableSizeGrid>
@@ -804,7 +859,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
{ name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT }, { name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT },
], ],
initialData: templateData || {}, initialData: templateData || {},
}, }
) )
const [activeSheet, setActiveSheet] = useState<SheetType>('report') const [activeSheet, setActiveSheet] = useState<SheetType>('report')
@@ -820,9 +875,9 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
() => ({ () => ({
report: Array(COL_COUNT).fill(96), report: Array(COL_COUNT).fill(96),
calculations: Array(COL_COUNT).fill(96), calculations: Array(COL_COUNT).fill(96),
}), })
) )
const gridRefs = useRef<Record<SheetType, VariableSizeGrid | null>>({ const gridRefs = useRef<Record<SheetType, any>>({
report: null, report: null,
calculations: null, calculations: null,
}) })
@@ -844,7 +899,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
const initialTemplateValues = useRef<Record<string, string>>({}) const initialTemplateValues = useRef<Record<string, string>>({})
const cellDataCache = useRef<Record<string, Record<string, CachedCellData>>>( const cellDataCache = useRef<Record<string, Record<string, CachedCellData>>>(
{}, {}
) )
const lastRevision = useRef<number>(-1) const lastRevision = useRef<number>(-1)
@@ -866,7 +921,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
console.log( console.log(
'✅ Загружено значений шаблона:', '✅ Загружено значений шаблона:',
Object.keys(initialTemplateValues.current).length, Object.keys(initialTemplateValues.current).length
) )
} else { } else {
console.log('⚠️ DualSpreadsheet: Данные шаблона отчета не найдены') console.log('⚠️ DualSpreadsheet: Данные шаблона отчета не найдены')
@@ -900,7 +955,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
for (let col = 0; col < COL_COUNT; col++) { for (let col = 0; col < COL_COUNT; col++) {
const cellKey = `${row}-${col}` const cellKey = `${row}-${col}`
const displayValue = String( const displayValue = String(
multiSheetEngine.getCellValue(sheetName, row, col) || '', multiSheetEngine.getCellValue(sheetName, row, col) || ''
) )
allCellValues[cellKey] = displayValue.trim() allCellValues[cellKey] = displayValue.trim()
} }
@@ -976,7 +1031,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
} }
if (lastRevision.current !== revision) { if (lastRevision.current !== revision) {
Object.keys(cellDataCache.current).forEach((key) => { Object.keys(cellDataCache.current).forEach(key => {
if (!key.endsWith(`-${revision}`)) { if (!key.endsWith(`-${revision}`)) {
delete cellDataCache.current[key] delete cellDataCache.current[key]
} }
@@ -987,7 +1042,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
cellDataCache.current[cacheKey] = cachedData cellDataCache.current[cacheKey] = cachedData
return cachedData return cachedData
}, },
[revision, multiSheetEngine, initialTemplateValues], [revision, multiSheetEngine, initialTemplateValues]
) )
const formulaBarValue = useMemo(() => { const formulaBarValue = useMemo(() => {
@@ -999,7 +1054,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
return multiSheetEngine.getCellRawValue( return multiSheetEngine.getCellRawValue(
sheetName, sheetName,
selectedCell.row, selectedCell.row,
selectedCell.col, selectedCell.col
) )
} }
return '' return ''
@@ -1034,7 +1089,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
const rawValue = multiSheetEngine.getCellRawValue( const rawValue = multiSheetEngine.getCellRawValue(
sheetName, sheetName,
selectedCell.row, selectedCell.row,
selectedCell.col, selectedCell.col
) )
setIsEditing(true) setIsEditing(true)
setEditingValue(rawValue) setEditingValue(rawValue)
@@ -1049,7 +1104,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
const oldValue = multiSheetEngine.getCellRawValue( const oldValue = multiSheetEngine.getCellRawValue(
sheetName, sheetName,
selectedCell.row, selectedCell.row,
selectedCell.col, selectedCell.col
) )
const templateValue = initialTemplateValues.current[cellName] ?? '' const templateValue = initialTemplateValues.current[cellName] ?? ''
@@ -1066,7 +1121,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
sheetName, sheetName,
selectedCell.row, selectedCell.row,
selectedCell.col, selectedCell.col,
editingValue, editingValue
) )
updateRevision() updateRevision()
multiSheetEngine.debouncedRecalc() multiSheetEngine.debouncedRecalc()
@@ -1075,7 +1130,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
setEditingValue('') setEditingValue('')
containerRefs.current[activeSheet]?.focus({ preventScroll: true }) containerRefs.current[activeSheet]?.focus({ preventScroll: true })
}, },
[activeSheet, selectedCell, editingValue, multiSheetEngine, updateRevision], [activeSheet, selectedCell, editingValue, multiSheetEngine, updateRevision]
) )
const handleCellChange = useCallback( const handleCellChange = useCallback(
@@ -1084,7 +1139,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
setEditingValue(newValue) setEditingValue(newValue)
} }
}, },
[isEditing], [isEditing]
) )
const handleClearCell = useCallback(() => { const handleClearCell = useCallback(() => {
@@ -1094,7 +1149,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
sheetName, sheetName,
selectedCell.row, selectedCell.row,
selectedCell.col, selectedCell.col,
'', ''
) )
updateRevision() updateRevision()
multiSheetEngine.debouncedRecalc() multiSheetEngine.debouncedRecalc()
@@ -1113,16 +1168,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
const newRow = Math.max( const newRow = Math.max(
0, 0,
Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow), Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow)
) )
const newCol = Math.max( const newCol = Math.max(
0, 0,
Math.min(COL_COUNT - 1, selectedCell.col + deltaCol), Math.min(COL_COUNT - 1, selectedCell.col + deltaCol)
) )
setSelectedCell({ row: newRow, col: newCol }) setSelectedCell({ row: newRow, col: newCol })
}, },
[selectedCell, isEditing, stopEditing], [selectedCell, isEditing, stopEditing]
) )
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
@@ -1158,7 +1213,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
setIsEditing(true) setIsEditing(true)
} }
}, },
[selectedCell, isEditing, moveSelection, startEditing, handleClearCell], [selectedCell, isEditing, moveSelection, startEditing, handleClearCell]
) )
const handleCellClick = useCallback( const handleCellClick = useCallback(
@@ -1169,7 +1224,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
setSelectedCell({ row, col }) setSelectedCell({ row, col })
containerRefs.current[activeSheet]?.focus({ preventScroll: true }) containerRefs.current[activeSheet]?.focus({ preventScroll: true })
}, },
[isEditing, stopEditing, activeSheet], [isEditing, stopEditing, activeSheet]
) )
const handleCellDoubleClick = useCallback( const handleCellDoubleClick = useCallback(
@@ -1177,22 +1232,22 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
setSelectedCell({ row, col }) setSelectedCell({ row, col })
startEditing() startEditing()
}, },
[startEditing], [startEditing]
) )
const handleColumnResize = useCallback( const handleColumnResize = useCallback(
(sheetType: SheetType, colIndex: number, newWidth: number) => { (sheetType: SheetType, colIndex: number, newWidth: number) => {
setColumnWidths( setColumnWidths(
produce((draft) => { produce(draft => {
draft[sheetType][colIndex] = Math.max(60, newWidth) draft[sheetType][colIndex] = Math.max(60, newWidth)
const grid = gridRefs.current[sheetType] const grid = gridRefs.current[sheetType]
if (grid) { if (grid) {
grid.resetAfterColumnIndex(colIndex) grid.resetAfterColumnIndex(colIndex)
} }
}), })
) )
}, },
[], []
) )
const handleFormulaBarChange = useCallback( const handleFormulaBarChange = useCallback(
@@ -1204,7 +1259,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
setEditingValue(newValue) setEditingValue(newValue)
} }
}, },
[selectedCell, isEditing], [selectedCell, isEditing]
) )
const handleFormulaBarFocus = useCallback(() => { const handleFormulaBarFocus = useCallback(() => {
@@ -1225,7 +1280,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
stopEditing(true) stopEditing(true)
} }
}, },
[isEditing, stopEditing], [isEditing, stopEditing]
) )
const handleFormulaBarKeyDown = useCallback( const handleFormulaBarKeyDown = useCallback(
@@ -1243,13 +1298,13 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
containerRefs.current[activeSheet]?.focus({ preventScroll: true }) containerRefs.current[activeSheet]?.focus({ preventScroll: true })
} }
}, },
[stopEditing, moveSelection, activeSheet], [stopEditing, moveSelection, activeSheet]
) )
const onGridScroll = useCallback( const onGridScroll = useCallback(
( (
sheetType: SheetType, sheetType: SheetType,
{ scrollLeft, scrollTop }: { scrollLeft: number; scrollTop: number }, { scrollLeft, scrollTop }: { scrollLeft: number; scrollTop: number }
) => { ) => {
if (headerRefs.current[sheetType]) { if (headerRefs.current[sheetType]) {
headerRefs.current[sheetType]!.scrollLeft = scrollLeft headerRefs.current[sheetType]!.scrollLeft = scrollLeft
@@ -1258,10 +1313,9 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
rowHeaderRefs.current[sheetType]!.scrollTop = scrollTop rowHeaderRefs.current[sheetType]!.scrollTop = scrollTop
} }
}, },
[], []
) )
const [separatorRef, setSeparatorRef] = useState<HTMLDivElement | null>(null)
const [spreadsheetWidths, setSpreadsheetWidths] = useState({ const [spreadsheetWidths, setSpreadsheetWidths] = useState({
L: 0.5, L: 0.5,
R: 0.5, R: 0.5,
@@ -1283,9 +1337,22 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
templateId, templateId,
autoSaveDelay: 2000, autoSaveDelay: 2000,
enableAutoSave: true, enableAutoSave: true,
}, }
) )
// Используем хук для перетаскивания разделителя
const handleDividerDrag = useCallback((newLeftPercentage: number) => {
// Используем requestAnimationFrame для более плавного обновления
requestAnimationFrame(() => {
setSpreadsheetWidths({
L: newLeftPercentage,
R: 1 - newLeftPercentage,
})
})
}, [])
const separatorRef = useDraggableDivider(handleDividerDrag)
// Загружаем сохраненные формулы при инициализации // Загружаем сохраненные формулы при инициализации
useEffect(() => { useEffect(() => {
if (!templateId || isInitialized) return if (!templateId || isInitialized) return
@@ -1293,7 +1360,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
// Проверяем, есть ли начальные данные // Проверяем, есть ли начальные данные
const hasInitialData = const hasInitialData =
Object.keys(templateData).length > 0 && Object.keys(templateData).length > 0 &&
Object.values(templateData).some((sheet) => Object.keys(sheet).length > 0) Object.values(templateData).some(sheet => Object.keys(sheet).length > 0)
// Всегда загружаем сохраненные данные, если они есть // Всегда загружаем сохраненные данные, если они есть
const savedFormulas = loadSavedFormulas() const savedFormulas = loadSavedFormulas()
@@ -1348,47 +1415,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
// --- Spreadsheet Component --- // --- Spreadsheet Component ---
useEffect(() => {
if (!separatorRef) return
const handleMouseDown = (e: MouseEvent) => {
e.preventDefault()
document.body.style.cursor = 'col-resize'
const handleMouseMove = (me: MouseEvent) => {
const container = separatorRef.parentElement
if (!container) return
const bounds = container.getBoundingClientRect()
const newLeftWidth = me.clientX - bounds.left
const totalWidth = bounds.width
let newLeftPercentage = newLeftWidth / totalWidth
// Ограничиваем минимальную ширину (например 20%)
newLeftPercentage = Math.max(0.2, Math.min(0.8, newLeftPercentage))
setSpreadsheetWidths({
L: newLeftPercentage,
R: 1 - newLeftPercentage,
})
}
const handleMouseUp = () => {
document.body.style.cursor = 'auto'
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
separatorRef.addEventListener('mousedown', handleMouseDown)
return () => {
separatorRef.removeEventListener('mousedown', handleMouseDown)
}
}, [separatorRef])
return ( return (
<div className="flex h-full flex-col bg-background"> <div className="flex h-full flex-col bg-background">
<FormulaBar <FormulaBar
@@ -1400,7 +1426,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
onFocus={handleFormulaBarFocus} onFocus={handleFormulaBarFocus}
onBlur={handleFormulaBarBlur} onBlur={handleFormulaBarBlur}
onKeyDown={handleFormulaBarKeyDown} onKeyDown={handleFormulaBarKeyDown}
formulaBarRef={(el) => { formulaBarRef={el => {
formulaBarRef.current = el formulaBarRef.current = el
}} }}
isSaving={isSaving} isSaving={isSaving}
@@ -1409,61 +1435,44 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
onManualSave={manualSave} onManualSave={manualSave}
onClearError={clearError} onClearError={clearError}
/> />
<div className="flex min-w-0 flex-1 gap-1 overflow-hidden"> <div
<Spreadsheet className="flex min-w-0 flex-1 gap-1 overflow-hidden"
sheetType="report" style={{ transition: 'none' }}
columnWidths={columnWidths.report} >
spreadsheetWidths={spreadsheetWidths} {SHEET_TYPES.map((sheetType, index) => (
getCachedCellData={getCachedCellData} <Spreadsheet
selectedCell={selectedCell} key={sheetType}
activeSheet={activeSheet} sheetType={sheetType}
isEditing={isEditing} columnWidths={columnWidths[sheetType]}
editingValue={editingValue} spreadsheetWidths={spreadsheetWidths}
handleCellClick={handleCellClick} getCachedCellData={getCachedCellData}
handleCellDoubleClick={handleCellDoubleClick} selectedCell={selectedCell}
handleCellChange={handleCellChange} activeSheet={activeSheet}
stopEditing={stopEditing} isEditing={isEditing}
setActiveSheet={setActiveSheet} editingValue={editingValue}
activeCellInputRef={activeCellInputRef} handleCellClick={handleCellClick}
containerRefs={containerRefs} handleCellDoubleClick={handleCellDoubleClick}
handleKeyDown={handleKeyDown} handleCellChange={handleCellChange}
headerRefs={headerRefs} stopEditing={stopEditing}
rowHeaderRefs={rowHeaderRefs} setActiveSheet={setActiveSheet}
gridRefs={gridRefs} activeCellInputRef={activeCellInputRef}
onGridScroll={onGridScroll} containerRefs={containerRefs}
handleColumnResize={handleColumnResize} handleKeyDown={handleKeyDown}
mergedCells={mergedCells} headerRefs={headerRefs}
/> rowHeaderRefs={rowHeaderRefs}
gridRefs={gridRefs}
onGridScroll={onGridScroll}
handleColumnResize={handleColumnResize}
mergedCells={mergedCells}
/>
))}
<div <div
ref={setSeparatorRef} ref={separatorRef}
className="flex w-2 cursor-col-resize items-center justify-center bg-border transition-colors hover:bg-primary/20" className="flex w-2 cursor-col-resize items-center justify-center bg-border transition-colors hover:bg-primary/20"
style={{ userSelect: 'none' }}
> >
<div className="h-4 w-0.5 rounded-full bg-muted-foreground/50" /> <div className="h-4 w-0.5 rounded-full bg-muted-foreground/50" />
</div> </div>
<Spreadsheet
sheetType="calculations"
columnWidths={columnWidths.calculations}
spreadsheetWidths={spreadsheetWidths}
getCachedCellData={getCachedCellData}
selectedCell={selectedCell}
activeSheet={activeSheet}
isEditing={isEditing}
editingValue={editingValue}
handleCellClick={handleCellClick}
handleCellDoubleClick={handleCellDoubleClick}
handleCellChange={handleCellChange}
stopEditing={stopEditing}
setActiveSheet={setActiveSheet}
activeCellInputRef={activeCellInputRef}
containerRefs={containerRefs}
handleKeyDown={handleKeyDown}
headerRefs={headerRefs}
rowHeaderRefs={rowHeaderRefs}
gridRefs={gridRefs}
onGridScroll={onGridScroll}
handleColumnResize={handleColumnResize}
mergedCells={mergedCells}
/>
</div> </div>
</div> </div>
) )

View File

@@ -0,0 +1,64 @@
import { Button } from '@/components/ui/button'
import { FileText, Upload } from 'lucide-react'
import React, { useRef } from 'react'
interface ExcelUploadPanelProps {
fileName?: string
isLoading: boolean
onUploadClick: () => void
onFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
export const ExcelUploadPanel: React.FC<ExcelUploadPanelProps> = ({
fileName,
isLoading,
onUploadClick,
onFileChange,
}) => {
const fileInputRef = useRef<HTMLInputElement>(null)
return (
<div className="flex items-center gap-2">
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.xls"
onChange={onFileChange}
className="hidden"
/>
<div
className={`flex items-center gap-1.5 rounded-md border px-2 py-1 ${
fileName
? 'border-emerald-200 bg-emerald-50'
: 'border-gray-200 bg-gray-50'
}`}
>
<FileText
className={`h-3 w-3 ${fileName ? 'text-emerald-600' : 'text-gray-400'}`}
/>
<span
className={`max-w-32 truncate text-xs font-medium ${
fileName ? 'text-emerald-700' : 'text-gray-500'
}`}
>
{fileName || 'Файл не выбран'}
</span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
className="h-7 rounded-md px-2"
disabled={isLoading}
>
{isLoading ? (
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : (
<Upload className="h-3 w-3" />
)}
</Button>
</div>
)
}

View File

@@ -0,0 +1,61 @@
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import {
AlignCenter,
AlignLeft,
AlignRight,
Bold,
Grid,
Italic,
Palette,
Type,
Underline,
} from 'lucide-react'
import React from 'react'
export const FormattingToolbar: React.FC = () => (
<div className="flex items-center gap-0.5 rounded-lg border bg-background p-0.5">
<div className="flex items-center">
{[Bold, Italic, Underline].map((Icon, i) => (
<Button
key={i}
variant="ghost"
size="sm"
className="h-6 w-6 rounded p-0"
>
<Icon className="h-3 w-3" />
</Button>
))}
</div>
<Separator orientation="vertical" className="mx-0.5 h-4" />
<div className="flex items-center">
{[AlignLeft, AlignCenter, AlignRight].map((Icon, i) => (
<Button
key={i}
variant="ghost"
size="sm"
className="h-6 w-6 rounded p-0"
>
<Icon className="h-3 w-3" />
</Button>
))}
</div>
<Separator orientation="vertical" className="mx-0.5 h-4" />
<div className="flex items-center">
{[Palette, Type, Grid].map((Icon, i) => (
<Button
key={i}
variant="ghost"
size="sm"
className="h-6 w-6 rounded p-0"
>
<Icon className="h-3 w-3" />
</Button>
))}
</div>
</div>
)

View File

@@ -0,0 +1,52 @@
import { Button } from '@/components/ui/button'
import { Template } from '@/types/template'
import { ArrowLeft, FileText, Wrench } from 'lucide-react'
import React from 'react'
import { useNavigate } from 'react-router-dom'
interface HeaderBarProps {
template: Template
onBack: () => void
}
export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
const navigate = useNavigate()
return (
<div className="flex-shrink-0 border-b bg-card px-4 py-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={onBack}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-lg font-semibold">{template.name}</h1>
<p className="mt-0.5 text-xs text-muted-foreground">
Настройте Excel шаблон и значения отчета
</p>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/templates/${template.id}/elements`)}
className="flex items-center gap-2"
>
<Wrench className="h-3.5 w-3.5" />
Элементы интерфейса
</Button>
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/templates/${template.id}/protocols`)}
className="flex items-center gap-2"
>
<FileText className="h-3.5 w-3.5" />
Создать протокол
</Button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,26 @@
import DualSpreadsheet from '@/components/DualSpreadsheet/DualSpreadsheet'
import { ExcelParseResult } from '@/services/excelSevice'
import React from 'react'
interface SpreadsheetViewerProps {
data: Record<string, any>
mergedCells: ExcelParseResult['mergedCells']
dataVersion: number
templateId: number | string
}
export const SpreadsheetViewer: React.FC<SpreadsheetViewerProps> = ({
data,
mergedCells,
dataVersion,
templateId,
}) => (
<div className="min-h-0 flex-1">
<DualSpreadsheet
key={dataVersion}
templateData={{ L: data }}
mergedCells={mergedCells}
templateId={`template-${templateId}`}
/>
</div>
)

View File

@@ -1,25 +1,10 @@
import { import { parseExcelFile } from '@/services/excelSevice'
AlignCenter, import { Template } from '@/types/template'
AlignLeft, import React, { useState } from 'react'
AlignRight, import { ExcelUploadPanel } from './ExcelUploadPanel'
ArrowLeft, import { FormattingToolbar } from './FormattingToolbar'
Bold, import { HeaderBar } from './HeaderBar'
FileText, import { SpreadsheetViewer } from './SpreadsheetViewer'
Grid,
Italic,
Palette,
Type,
Underline,
Upload,
Wrench,
} from 'lucide-react'
import React, { useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import * as XLSX from 'xlsx'
import { Template } from '../../types/template'
import DualSpreadsheet from '../DualSpreadsheet/DualSpreadsheet'
import { Button } from '../ui/button'
import { Separator } from '../ui/separator'
interface TemplateEditorProps { interface TemplateEditorProps {
template: Template template: Template
@@ -30,284 +15,56 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
template, template,
onBack, onBack,
}) => { }) => {
const [editedTemplate, setEditedTemplate] = useState<Template>(template) const [editedTemplate, setEditedTemplate] = useState(template)
const [excelData, setExcelData] = useState<Record<string, any>>( const [excelData, setExcelData] = useState<Record<string, any>>(
template.excelData || {}, template.excelData || {}
) )
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [dataVersion, setDataVersion] = useState(0) // Для принудительного const [dataVersion, setDataVersion] = useState(0)
const fileInputRef = useRef<HTMLInputElement>(null)
const navigate = useNavigate()
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) {
console.log('No file selected')
return
}
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setIsLoading(true) setIsLoading(true)
const reader = new FileReader() try {
const { data, mergedCells } = await parseExcelFile(file)
reader.onload = (e) => { setExcelData(data)
try { setEditedTemplate(prev => ({
const data = new Uint8Array(e.target?.result as ArrayBuffer) ...prev,
const workbook = XLSX.read(data, { type: 'array' }) excelFile: file,
excelData: data,
// Читаем первый лист mergedCells,
const firstSheetName = workbook.SheetNames[0] updatedAt: new Date(),
const worksheet = workbook.Sheets[firstSheetName] }))
setDataVersion(v => v + 1)
// Получаем диапазон ячеек } catch (err) {
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1:A1') console.error(err)
alert('Ошибка при чтении Excel файла')
// Преобразуем в формат для нашей системы } finally {
const formattedData: Record<string, any> = {}
const mergedCells: Array<{ from: string; to: string; value: any }> = []
// Обрабатываем объединенные ячейки
const merges = worksheet['!merges'] || []
// Сначала читаем обычные ячейки
for (let R = range.s.r; R <= range.e.r; ++R) {
for (let C = range.s.c; C <= range.e.c; ++C) {
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C })
const cell = worksheet[cellAddress]
if (
cell &&
cell.v !== undefined &&
cell.v !== null &&
cell.v !== ''
) {
formattedData[cellAddress] = cell.v
}
}
}
// Обрабатываем объединенные ячейки
merges.forEach((merge) => {
const startCell = XLSX.utils.encode_cell({
r: merge.s.r,
c: merge.s.c,
})
const endCell = XLSX.utils.encode_cell({ r: merge.e.r, c: merge.e.c })
const startCellData = worksheet[startCell]
if (startCellData && startCellData.v !== undefined) {
// Записываем значение в первую ячейку объединенного диапазона
formattedData[startCell] = startCellData.v
// Сохраняем информацию об объединении для UI
mergedCells.push({
from: startCell,
to: endCell,
value: startCellData.v,
})
// Заполняем остальные ячейки в объединенном диапазоне пустыми значениями
// но помечаем их как часть объединения
for (let R = merge.s.r; R <= merge.e.r; ++R) {
for (let C = merge.s.c; C <= merge.e.c; ++C) {
const cellAddr = XLSX.utils.encode_cell({ r: R, c: C })
if (cellAddr !== startCell) {
formattedData[cellAddr] = '' // Пустое значение для объединенных ячеек
}
}
}
}
})
// Обновляем состояние
const newData = { ...formattedData }
setExcelData(newData)
setEditedTemplate((prev) => ({
...prev,
excelFile: file,
excelData: newData,
mergedCells: mergedCells, // Сохраняем информацию об объединенных ячейках
updatedAt: new Date(),
}))
// Принудительно обновляем версию данных для перерендера
setDataVersion((prev) => prev + 1)
setIsLoading(false)
} catch (error) {
console.error('Ошибка при чтении Excel файла:', error)
alert('Ошибка при чтении Excel файла')
setIsLoading(false)
}
}
reader.onerror = () => {
alert('Ошибка при загрузке файла')
setIsLoading(false) setIsLoading(false)
} }
reader.readAsArrayBuffer(file)
}
const handleCreateElements = () => {
navigate(`/templates/${editedTemplate.id}/elements`)
} }
return ( return (
<div className="flex h-screen flex-col bg-background"> <div className="flex h-screen flex-col bg-background">
{/* Заголовок */} <HeaderBar template={editedTemplate} onBack={onBack} />
<div className="flex-shrink-0 border-b bg-card px-4 py-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={onBack}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-lg font-semibold">{editedTemplate.name}</h1>
<p className="mt-0.5 text-xs text-muted-foreground">
Настройте Excel шаблон и значения отчета
</p>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleCreateElements}
className="flex items-center gap-2"
>
<Wrench className="h-3.5 w-3.5" />
Элементы интерфейса
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
navigate(`/templates/${editedTemplate.id}/protocols`)
}
className="flex items-center gap-2"
>
<FileText className="h-3.5 w-3.5" />
Создать протокол
</Button>
</div>
</div>
</div>
{/* Компактная панель загрузки Excel и инструментов */}
<div className="flex-shrink-0 border-b bg-muted/30 px-4 py-2"> <div className="flex-shrink-0 border-b bg-muted/30 px-4 py-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{/* Левая часть - панель инструментов стилизации */} <FormattingToolbar />
<div className="flex items-center gap-0.5 rounded-lg border bg-background p-0.5"> <ExcelUploadPanel
{/* Группа текстового форматирования */} fileName={editedTemplate.excelFile?.name}
<div className="flex items-center"> isLoading={isLoading}
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0"> onUploadClick={() => {}}
<Bold className="h-3 w-3" /> onFileChange={handleFileChange}
</Button> />
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<Italic className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<Underline className="h-3 w-3" />
</Button>
</div>
<Separator orientation="vertical" className="mx-0.5 h-4" />
{/* Группа выравнивания */}
<div className="flex items-center">
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<AlignLeft className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<AlignCenter className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<AlignRight className="h-3 w-3" />
</Button>
</div>
<Separator orientation="vertical" className="mx-0.5 h-4" />
{/* Группа цвета и стилей */}
<div className="flex items-center">
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<Palette className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<Type className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<Grid className="h-3 w-3" />
</Button>
</div>
</div>
{/* Правая часть - загрузка Excel */}
<div className="flex items-center gap-2">
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.xls"
onChange={handleFileUpload}
className="hidden"
/>
{/* Статус файла - всегда отображается */}
<div
className={`flex items-center gap-1.5 rounded-md border px-2 py-1 ${
editedTemplate.excelFile
? 'border-emerald-200 bg-emerald-50'
: 'border-gray-200 bg-gray-50'
}`}
>
<FileText
className={`h-3 w-3 ${
editedTemplate.excelFile
? 'text-emerald-600'
: 'text-gray-400'
}`}
/>
<span
className={`max-w-32 truncate text-xs font-medium ${
editedTemplate.excelFile
? 'text-emerald-700'
: 'text-gray-500'
}`}
>
{editedTemplate.excelFile
? editedTemplate.excelFile.name
: 'Файл не выбран'}
</span>
</div>
{/* Кнопка загрузки - всегда отображается */}
<Button
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
className="h-7 rounded-md px-2"
disabled={isLoading}
>
{isLoading ? (
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : (
<Upload className="h-3 w-3" />
)}
</Button>
</div>
</div> </div>
</div> </div>
<SpreadsheetViewer
{/* Таблицы - теперь занимают всё оставшееся пространство */} data={excelData}
<div className="min-h-0 flex-1"> mergedCells={editedTemplate.mergedCells || []}
<DualSpreadsheet dataVersion={dataVersion}
key={dataVersion} templateId={editedTemplate.id}
templateData={{ L: excelData }} />
mergedCells={editedTemplate.mergedCells}
templateId={`template-${editedTemplate.id}`}
/>
</div>
</div> </div>
) )
} }

View File

@@ -0,0 +1,58 @@
import * as XLSX from 'xlsx'
export interface ExcelParseResult {
data: Record<string, any>
mergedCells: Array<{ from: string; to: string; value: any }>
}
// fetch(/api/upload)
export async function parseExcelFile(file: File): Promise<ExcelParseResult> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = e => {
try {
const data = new Uint8Array(e.target?.result as ArrayBuffer)
const workbook = XLSX.read(data, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1:A1')
const formattedData: Record<string, any> = {}
const mergedCells: ExcelParseResult['mergedCells'] = []
// Основные ячейки
for (let R = range.s.r; R <= range.e.r; ++R) {
for (let C = range.s.c; C <= range.e.c; ++C) {
const addr = XLSX.utils.encode_cell({ r: R, c: C })
const cell = worksheet[addr]
if (cell && cell.v != null && cell.v !== '') {
formattedData[addr] = cell.v
}
}
}
// Обработать объединения
;(worksheet['!merges'] || []).forEach(m => {
const from = XLSX.utils.encode_cell({ r: m.s.r, c: m.s.c })
const to = XLSX.utils.encode_cell({ r: m.e.r, c: m.e.c })
const val = worksheet[from]?.v
if (val != null) {
mergedCells.push({ from, to, value: val })
// остальные ячейки затираем пустым, чтобы UI знал об объединении
for (let R = m.s.r; R <= m.e.r; ++R) {
for (let C = m.s.c; C <= m.e.c; ++C) {
const addr = XLSX.utils.encode_cell({ r: R, c: C })
if (addr !== from) formattedData[addr] = ''
}
}
}
})
resolve({ data: formattedData, mergedCells })
} catch (err) {
reject(err)
}
}
reader.onerror = () => reject(new Error('Ошибка чтения файла'))
reader.readAsArrayBuffer(file)
})
}

View File

@@ -1,49 +1,49 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: { theme: {
extend: { extend: {
colors: { colors: {
border: "hsl(var(--border))", border: 'hsl(var(--border))',
input: "hsl(var(--input))", input: 'hsl(var(--input))',
ring: "hsl(var(--ring))", ring: 'hsl(var(--ring))',
background: "hsl(var(--background))", background: 'hsl(var(--background))',
foreground: "hsl(var(--foreground))", foreground: 'hsl(var(--foreground))',
primary: { primary: {
DEFAULT: "hsl(var(--primary))", DEFAULT: 'hsl(var(--primary))',
foreground: "hsl(var(--primary-foreground))", foreground: 'hsl(var(--primary-foreground))',
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: 'hsl(var(--secondary))',
foreground: "hsl(var(--secondary-foreground))", foreground: 'hsl(var(--secondary-foreground))',
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive))", DEFAULT: 'hsl(var(--destructive))',
foreground: "hsl(var(--destructive-foreground))", foreground: 'hsl(var(--destructive-foreground))',
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: 'hsl(var(--muted))',
foreground: "hsl(var(--muted-foreground))", foreground: 'hsl(var(--muted-foreground))',
}, },
accent: { accent: {
DEFAULT: "hsl(var(--accent))", DEFAULT: 'hsl(var(--accent))',
foreground: "hsl(var(--accent-foreground))", foreground: 'hsl(var(--accent-foreground))',
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover))", DEFAULT: 'hsl(var(--popover))',
foreground: "hsl(var(--popover-foreground))", foreground: 'hsl(var(--popover-foreground))',
}, },
card: { card: {
DEFAULT: "hsl(var(--card))", DEFAULT: 'hsl(var(--card))',
foreground: "hsl(var(--card-foreground))", foreground: 'hsl(var(--card-foreground))',
}, },
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: 'var(--radius)',
md: "calc(var(--radius) - 2px)", md: 'calc(var(--radius) - 2px)',
sm: "calc(var(--radius) - 4px)", sm: 'calc(var(--radius) - 4px)',
}, },
}, },
}, },
plugins: [], plugins: [],
}; }