подстветка ячеек

This commit is contained in:
2025-07-22 05:35:04 +03:00
parent 555505e09e
commit faad68cdc0
8 changed files with 394 additions and 46 deletions

View File

@@ -11,12 +11,17 @@ import {
CachedCellData, CachedCellData,
COL_COUNT, COL_COUNT,
DualSpreadsheetProps, DualSpreadsheetProps,
HighlightedCell,
ROW_COUNT, ROW_COUNT,
SHEET_NAMES, SHEET_NAMES,
SHEET_TYPES, SHEET_TYPES,
SheetType, SheetType,
} from './types' } from './types'
import { getColumnLabel } from './utils' import {
convertReferencesToHighlights,
extractCellReferences,
getColumnLabel,
} from './utils'
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
templateData = {}, templateData = {},
@@ -66,6 +71,9 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
const [baseFileData, setBaseFileData] = useState< const [baseFileData, setBaseFileData] = useState<
Record<string, Record<string, any>> Record<string, Record<string, any>>
>({}) // Базовые данные файла >({}) // Базовые данные файла
const [highlightedCells, setHighlightedCells] = useState<HighlightedCell[]>(
[]
) // Подсвеченные ячейки для формул
// Мемоизируем начальное состояние ширин колонок // Мемоизируем начальное состояние ширин колонок
const initialColumnWidths = useMemo( const initialColumnWidths = useMemo(
@@ -120,7 +128,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
const rowHeaderRefs = const rowHeaderRefs =
useRef<Record<SheetType, HTMLDivElement | null>>(initialRowHeaderRefs) useRef<Record<SheetType, HTMLDivElement | null>>(initialRowHeaderRefs)
const activeCellInputRef = useRef<HTMLInputElement>(null) const activeCellInputRef = useRef<HTMLInputElement | null>(null)
const formulaBarRef = useRef<HTMLInputElement | null>(null) const formulaBarRef = useRef<HTMLInputElement | null>(null)
const initialTemplateValues = useRef<Record<string, string>>({}) const initialTemplateValues = useRef<Record<string, string>>({})
@@ -294,6 +302,38 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
[revision, multiSheetEngine, initialTemplateValues, columnWidths] [revision, multiSheetEngine, initialTemplateValues, columnWidths]
) )
// Функция для обновления подсветки ячеек при вводе формулы
const updateCellHighlights = useCallback(
(formula: string) => {
if (!formula.startsWith('=')) {
setHighlightedCells([])
return
}
try {
// Извлекаем ссылки на ячейки из формулы
const references = extractCellReferences(formula)
// Конвертируем ссылки в подсвеченные ячейки
const highlights = convertReferencesToHighlights(
references,
activeSheet
)
setHighlightedCells(highlights)
console.log('🎨 Подсветка ячеек для формулы:', formula, {
references,
highlights,
})
} catch (error) {
console.error('Ошибка при обновлении подсветки ячеек:', error)
setHighlightedCells([])
}
},
[activeSheet]
)
const formulaBarValue = useMemo(() => { const formulaBarValue = useMemo(() => {
if (isEditing) { if (isEditing) {
return editingValue return editingValue
@@ -332,6 +372,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
} }
}, [editingValue, isEditing]) }, [editingValue, isEditing])
// Обновление подсветки ячеек при вводе формулы
useEffect(() => {
if (isEditing) {
updateCellHighlights(editingValue)
} else {
// Очищаем подсветку когда не редактируем
setHighlightedCells([])
}
}, [isEditing, editingValue, updateCellHighlights])
const startEditing = useCallback(() => { const startEditing = useCallback(() => {
if (!selectedCell) return if (!selectedCell) return
const sheetName = SHEET_NAMES[activeSheet] const sheetName = SHEET_NAMES[activeSheet]
@@ -642,10 +692,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
try { try {
const latestFile = await getLatestFileForTemplate(templateId) const latestFile = await getLatestFileForTemplate(templateId)
if (latestFile) { if (latestFile) {
console.log( console.log('📁 Найден последний файл для шаблона:', latestFile.name)
'📁 Найден последний файл для шаблона:',
latestFile.filename
)
const fileData = await getFileData(latestFile.id) const fileData = await getFileData(latestFile.id)
// TODO: Пока getFileData возвращает пустые данные, используем templateData // TODO: Пока getFileData возвращает пустые данные, используем templateData
@@ -813,6 +860,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
activeSheet={activeSheet} activeSheet={activeSheet}
isEditing={isEditing} isEditing={isEditing}
editingValue={editingValue} editingValue={editingValue}
highlightedCells={highlightedCells}
handleCellClick={handleCellClick} handleCellClick={handleCellClick}
handleCellDoubleClick={handleCellDoubleClick} handleCellDoubleClick={handleCellDoubleClick}
handleCellChange={handleCellChange} handleCellChange={handleCellChange}

View File

@@ -23,13 +23,22 @@ export const Cell = memo(
activeCellRef, activeCellRef,
style, // Добавляем style для react-window style, // Добавляем style для react-window
mergedCellInfo, mergedCellInfo,
highlightColor, // Цвет подсветки для формул
}: CellProps) => { }: CellProps) => {
// Если ячейка объединена, но не является основной, не рендерим её // Если ячейка объединена, но не является основной, не рендерим её
if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) { if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) {
return null return null
} }
const className = `border border-border ${isModified && !mergedCellInfo?.isMerged ? 'bg-yellow-200' : ''}` // Определяем классы CSS для подсветки и обычного состояния
let className = 'border border-border'
// Приоритет стилей: подсветка формул > изменения > обычное состояние
if (highlightColor) {
className += ` ${highlightColor}`
} else if (isModified && !mergedCellInfo?.isMerged) {
className += ' bg-yellow-200'
}
// Отладка стилей для измененных ячеек // Отладка стилей для измененных ячеек
if (isModified) { if (isModified) {
@@ -38,6 +47,7 @@ export const Cell = memo(
isModified, isModified,
isSelected, isSelected,
displayValue, displayValue,
highlightColor,
}) })
} }
@@ -68,16 +78,27 @@ export const Cell = memo(
// Рассчитываем общую высоту для объединенной ячейки // Рассчитываем общую высоту для объединенной ячейки
const totalHeight = ROW_HEIGHT * rowSpan 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 */
}
cellStyle = { cellStyle = {
...cellStyle, ...cellStyle,
width: totalWidth, width: totalWidth,
height: totalHeight, height: totalHeight,
zIndex: 10, // Поднимаем выше обычных ячеек zIndex: 10, // Поднимаем выше обычных ячеек
backgroundColor: isSelected backgroundColor,
? 'hsl(var(--accent))'
: isModified
? '#fef08a' /* тот же жёлтый оттенок, что и bg-yellow-200 */
: 'white',
} }
} }

View File

@@ -18,6 +18,7 @@ export const CellRenderer = ({
editingValue, editingValue,
cachedData, cachedData,
mergedCells, mergedCells,
highlightedCells,
handleCellClick, handleCellClick,
handleCellDoubleClick, handleCellDoubleClick,
handleCellChange, handleCellChange,
@@ -40,6 +41,16 @@ export const CellRenderer = ({
? '' // Не показывать вычисленное значение во время редактирования ? '' // Не показывать вычисленное значение во время редактирования
: cellData?.displayValue || '' : cellData?.displayValue || ''
// Проверяем, подсвечена ли эта ячейка
const currentSheetName = sheetType === 'report' ? 'L' : 'R'
const highlightedCell = highlightedCells.find(
highlight =>
highlight.sheet === currentSheetName &&
highlight.row === rowIndex &&
highlight.col === columnIndex
)
const highlightColor = highlightedCell?.color
// Отладка для измененных ячеек // Отладка для измененных ячеек
if (cellData?.isModified) { if (cellData?.isModified) {
console.log( console.log(
@@ -49,6 +60,7 @@ export const CellRenderer = ({
isModified: cellData.isModified, isModified: cellData.isModified,
displayValue, displayValue,
rawValue, rawValue,
highlightColor,
} }
) )
} }
@@ -91,6 +103,7 @@ export const CellRenderer = ({
setActiveSheet={setActiveSheet} setActiveSheet={setActiveSheet}
activeCellRef={activeCellInputRef} activeCellRef={activeCellInputRef}
mergedCellInfo={mergedCellProps} mergedCellInfo={mergedCellProps}
highlightColor={highlightColor}
/> />
) )
} }

View File

@@ -14,6 +14,7 @@ export const Spreadsheet = ({
activeSheet, activeSheet,
isEditing, isEditing,
editingValue, editingValue,
highlightedCells,
handleCellClick, handleCellClick,
handleCellDoubleClick, handleCellDoubleClick,
handleCellChange, handleCellChange,
@@ -41,6 +42,7 @@ export const Spreadsheet = ({
editingValue, editingValue,
cachedData, cachedData,
mergedCells, mergedCells,
highlightedCells,
handleCellClick, handleCellClick,
handleCellDoubleClick, handleCellDoubleClick,
handleCellChange, handleCellChange,
@@ -78,7 +80,9 @@ export const Spreadsheet = ({
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}
@@ -102,7 +106,9 @@ export 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 }}
> >
@@ -126,7 +132,9 @@ export 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">
@@ -171,9 +179,9 @@ export const Spreadsheet = ({
{/* Grid */} {/* Grid */}
<div className="min-h-0 flex-1"> <div className="min-h-0 flex-1">
<VariableSizeGrid <VariableSizeGrid
ref={(el: VariableSizeGrid | null) => ref={(el: VariableSizeGrid | null) => {
(gridRefs.current[sheetType] = 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]}

View File

@@ -16,6 +16,14 @@ export type SheetType = keyof typeof SHEET_NAMES
// Добавляем массив типов листов для итерации // Добавляем массив типов листов для итерации
export const SHEET_TYPES: SheetType[] = ['report', 'calculations'] export const SHEET_TYPES: SheetType[] = ['report', 'calculations']
// Интерфейс для подсвеченной ячейки
export interface HighlightedCell {
sheet: string // нормализованное имя листа ('L' или 'R')
row: number
col: number
color: string // класс CSS для цвета подсветки
}
// Кэшированные данные ячеек для оптимизации // Кэшированные данные ячеек для оптимизации
export interface CachedCellData { export interface CachedCellData {
displayValue: string displayValue: string
@@ -59,6 +67,7 @@ export interface CellProps {
rowSpan?: number rowSpan?: number
colSpan?: number colSpan?: number
} }
highlightColor?: string // Цвет подсветки для формул
} }
export interface FormulaBarProps { export interface FormulaBarProps {
@@ -91,6 +100,7 @@ export interface CellRendererData {
editingValue: string editingValue: string
cachedData: Record<string, CachedCellData> cachedData: Record<string, CachedCellData>
mergedCells?: MergedCell[] mergedCells?: MergedCell[]
highlightedCells: HighlightedCell[] // Добавляем подсвеченные ячейки
handleCellClick: (row: number, col: number) => void handleCellClick: (row: number, col: number) => void
handleCellDoubleClick: (row: number, col: number) => void handleCellDoubleClick: (row: number, col: number) => void
handleCellChange: (newValue: string) => void handleCellChange: (newValue: string) => void
@@ -115,6 +125,7 @@ export interface SpreadsheetProps {
activeSheet: SheetType activeSheet: SheetType
isEditing: boolean isEditing: boolean
editingValue: string editingValue: string
highlightedCells: HighlightedCell[] // Добавляем подсвеченные ячейки
handleCellClick: (row: number, col: number) => void handleCellClick: (row: number, col: number) => void
handleCellDoubleClick: (row: number, col: number) => void handleCellDoubleClick: (row: number, col: number) => void
handleCellChange: (newValue: string) => void handleCellChange: (newValue: string) => void

View File

@@ -1,42 +1,289 @@
import { cellAddressToCoordinates, getColumnLabel } from '@/lib/cell-utils'
import { MergedCell } from '@/type/template' import { MergedCell } from '@/type/template'
import { MergedCellInfo } from './types' import { MergedCellInfo } from './types'
// --- Helper Functions --- export function getColumnLabel(index: number): string {
export { getColumnLabel } let label = ''
let col = index
while (col >= 0) {
label = String.fromCharCode(65 + (col % 26)) + label
col = Math.floor(col / 26) - 1
}
return label
}
// Вспомогательная функция для совместимости с существующим кодом export function findMergedCellInfo(
export const cellAddressToCoords = cellAddressToCoordinates
// Функция для проверки, является ли ячейка частью объединенной группы
export const findMergedCellInfo = (
row: number, row: number,
col: number, col: number,
mergedCells: MergedCell[] mergedCells: MergedCell[]
): MergedCellInfo => { ): MergedCellInfo {
for (const merged of mergedCells) { for (const mergedCell of mergedCells) {
const fromCoords = cellAddressToCoordinates(merged.from) const { startRow, startCol, endRow, endCol } = mergedCell
const toCoords = cellAddressToCoordinates(merged.to)
// Проверяем, попадает ли текущая ячейка в диапазон объединения // Проверяем, попадает ли ячейка в диапазон объединенных ячеек
if ( if (row >= startRow && row <= endRow && col >= startCol && col <= endCol) {
row >= fromCoords.row && // Это главная ячейка (левая верхняя)
row <= toCoords.row && const isMainCell = row === startRow && col === startCol
col >= fromCoords.col &&
col <= toCoords.col
) {
const isMainCell = row === fromCoords.row && col === fromCoords.col
const rowSpan = toCoords.row - fromCoords.row + 1
const colSpan = toCoords.col - fromCoords.col + 1
return { return {
isMerged: true, isMerged: true,
isMainCell, isMainCell,
mergedCell: merged, mergedCell,
spans: { rowSpan, colSpan }, spans: {
rowSpan: endRow - startRow + 1,
colSpan: endCol - startCol + 1,
},
} }
} }
} }
return { isMerged: false, isMainCell: false } return { isMerged: false, isMainCell: false }
} }
// Регулярные выражения для разных типов ссылок на ячейки
const QUALIFIED_REF_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+\b/g // SheetName!A1
const LOCAL_REF_RE = /\b[A-Z]+[0-9]+\b/g // A1, B2, etc.
const RANGE_RE = /\b[A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g // A1:B2, D5:D7, etc.
const QUALIFIED_RANGE_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g // SheetName!A1:B2
// Интерфейс для ссылки на ячейку
export interface CellReference {
sheet: string | null // null для локальных ссылок
address: string // например "A1" или "B2:C3"
isRange: boolean
startRow: number
startCol: number
endRow: number
endCol: number
}
// Парсит адрес ячейки типа "A1" в координаты
export function parseCellAddress(
address: string
): { row: number; col: number } | null {
const match = address.match(/^([A-Z]+)(\d+)$/)
if (!match) return null
const column = match[1]
const row = parseInt(match[2], 10)
// Конвертируем колонку в индекс
let colIndex = 0
for (let i = 0; i < column.length; i++) {
colIndex = colIndex * 26 + (column.charCodeAt(i) - 65 + 1)
}
colIndex -= 1 // Конвертируем в 0-based индекс
return { row: row - 1, col: colIndex } // Конвертируем в 0-based индексы
}
// Парсит диапазон ячеек типа "A1:B2"
export function parseCellRange(range: string): {
startRow: number
startCol: number
endRow: number
endCol: number
} | null {
const parts = range.split(':')
if (parts.length !== 2) return null
const start = parseCellAddress(parts[0])
const end = parseCellAddress(parts[1])
if (!start || !end) return null
return {
startRow: Math.min(start.row, end.row),
startCol: Math.min(start.col, end.col),
endRow: Math.max(start.row, end.row),
endCol: Math.max(start.col, end.col),
}
}
// Извлекает все ссылки на ячейки из формулы
export function extractCellReferences(formula: string): CellReference[] {
if (!formula.startsWith('=')) return []
const expr = formula.substring(1) // Убираем знак =
const references: CellReference[] = []
// Функция для проверки, не является ли ссылка частью имени функции
const isFunction = (ref: string): boolean => {
const commonFunctions = [
'SUM',
'AVERAGE',
'COUNT',
'MAX',
'MIN',
'IF',
'AND',
'OR',
'NOT',
'СУММ',
'СРЕДНЕЕ',
'СЧЁТ',
'МАКС',
'МИН',
'ЕСЛИ',
]
return commonFunctions.includes(ref.toUpperCase())
}
// 1. Обрабатываем квалифицированные диапазоны (SheetName!A1:B2)
const qualifiedRangeMatches = expr.match(QUALIFIED_RANGE_RE)
if (qualifiedRangeMatches) {
for (const match of qualifiedRangeMatches) {
const [sheetPart, rangePart] = match.split('!')
const rangeCoords = parseCellRange(rangePart)
if (rangeCoords) {
references.push({
sheet: sheetPart,
address: rangePart,
isRange: true,
...rangeCoords,
})
}
}
}
// 2. Обрабатываем локальные диапазоны (A1:B2)
let cleanExpr = expr
// Удаляем уже обработанные квалифицированные диапазоны
cleanExpr = cleanExpr.replace(QUALIFIED_RANGE_RE, '')
const rangeMatches = cleanExpr.match(RANGE_RE)
if (rangeMatches) {
for (const match of rangeMatches) {
const rangeCoords = parseCellRange(match)
if (rangeCoords) {
references.push({
sheet: null,
address: match,
isRange: true,
...rangeCoords,
})
}
}
}
// 3. Обрабатываем квалифицированные одиночные ссылки (SheetName!A1)
const qualifiedMatches = expr.match(QUALIFIED_REF_RE)
if (qualifiedMatches) {
for (const match of qualifiedMatches) {
const [sheetPart, cellPart] = match.split('!')
const cellCoords = parseCellAddress(cellPart)
if (cellCoords) {
references.push({
sheet: sheetPart,
address: cellPart,
isRange: false,
startRow: cellCoords.row,
startCol: cellCoords.col,
endRow: cellCoords.row,
endCol: cellCoords.col,
})
}
}
}
// 4. Обрабатываем локальные одиночные ссылки (A1, B2)
// Удаляем уже обработанные квалифицированные ссылки и диапазоны
cleanExpr = expr
.replace(QUALIFIED_RANGE_RE, '')
.replace(QUALIFIED_REF_RE, '')
.replace(RANGE_RE, '')
const localMatches = cleanExpr.match(LOCAL_REF_RE)
if (localMatches) {
for (const match of localMatches) {
// Пропускаем функции
if (isFunction(match)) continue
const cellCoords = parseCellAddress(match)
if (cellCoords) {
references.push({
sheet: null,
address: match,
isRange: false,
startRow: cellCoords.row,
startCol: cellCoords.col,
endRow: cellCoords.row,
endCol: cellCoords.col,
})
}
}
}
return references
}
// Интерфейс для подсвеченной ячейки
export interface HighlightedCell {
sheet: string // нормализованное имя листа ('L' или 'R')
row: number
col: number
color: string // класс CSS для цвета подсветки
}
// Конвертирует ссылки на ячейки в подсвеченные ячейки
export function convertReferencesToHighlights(
references: CellReference[],
currentSheetType: 'report' | 'calculations'
): HighlightedCell[] {
const highlights: HighlightedCell[] = []
const colors = [
'bg-blue-200 border-blue-400',
'bg-green-200 border-green-400',
'bg-purple-200 border-purple-400',
'bg-red-200 border-red-400',
'bg-yellow-200 border-yellow-400',
'bg-indigo-200 border-indigo-400',
'bg-pink-200 border-pink-400',
'bg-orange-200 border-orange-400',
]
let colorIndex = 0
for (const ref of references) {
let sheetName: string
// Определяем имя листа
if (ref.sheet) {
// Квалифицированная ссылка
if (ref.sheet === 'L' || ref.sheet.toLowerCase() === 'report') {
sheetName = 'L'
} else if (
ref.sheet === 'R' ||
ref.sheet.toLowerCase() === 'calculations'
) {
sheetName = 'R'
} else {
// Неизвестный лист, пропускаем
continue
}
} else {
// Локальная ссылка - используем текущий лист
sheetName = currentSheetType === 'report' ? 'L' : 'R'
}
const color = colors[colorIndex % colors.length]
colorIndex++
// Добавляем все ячейки в диапазоне
for (let row = ref.startRow; row <= ref.endRow; row++) {
for (let col = ref.startCol; col <= ref.endCol; col++) {
highlights.push({
sheet: sheetName,
row,
col,
color,
})
}
}
}
return highlights
}

View File

@@ -36,7 +36,7 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
className="flex items-center gap-1 hover:bg-muted" className="flex items-center gap-1 hover:bg-muted"
> >
<Wrench className="h-4 w-4" /> <Wrench className="h-4 w-4" />
<span className="text-sm">Элементы интерфейса</span> <span className="text-sm">Редактор интерфейса</span>
</Button> </Button>
<Button <Button
variant="secondary" variant="secondary"

View File

@@ -2,7 +2,7 @@ import { ElementConstructor } from '@/component/TemplateManager/ElementConstruct
import { Button } from '@/component/ui/button' import { Button } from '@/component/ui/button'
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext' import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { Template, TemplateElement } from '@/type/template' import { Template, TemplateElement } from '@/type/template'
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react' import { ArrowLeft, FileText, Wrench } from 'lucide-react'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams } from 'react-router-dom'
@@ -145,8 +145,8 @@ export const ElementsCreation: FC = () => {
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
</Button> </Button>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Settings className="h-5 w-5" /> <Wrench className="h-5 w-5" />
<h1 className="text-lg font-semibold">Редактор шаблонов</h1> <h1 className="text-lg font-semibold">Редактор интерфейса</h1>
<span className="text-sm text-muted-foreground"></span> <span className="text-sm text-muted-foreground"></span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{selectedTemplate.name} {selectedTemplate.name}