Новые стили, починил фокус formulabar
This commit is contained in:
@@ -10,20 +10,25 @@
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96%;
|
||||
--secondary-foreground: 222.2 84% 4.9%;
|
||||
--muted: 210 40% 96%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96%;
|
||||
--accent-foreground: 222.2 84% 4.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -34,7 +39,7 @@
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 84% 4.9%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
@@ -46,6 +51,11 @@
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Input } from '@/component/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
@@ -104,30 +105,34 @@ export const selectDefinition: ElementDefinition<SelectConfig, string> = {
|
||||
)
|
||||
},
|
||||
Preview: ({ config }) => (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<Select disabled>
|
||||
<SelectTrigger className="w-full">
|
||||
<span className="text-gray-500">
|
||||
{config.placeholder || 'Выберите значение'}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
Render: ({ config, value, onChange }) => (
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<SelectTrigger>
|
||||
<Select>
|
||||
<SelectTrigger className="w-full min-w-32">
|
||||
<SelectValue placeholder={config.placeholder || 'Выберите значение'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{config.options.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectGroup>
|
||||
{config.options.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
),
|
||||
Render: ({ config, value, onChange }) => (
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<SelectTrigger className="w-full min-w-32">
|
||||
<SelectValue placeholder={config.placeholder || 'Выберите значение'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{config.options.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
),
|
||||
|
||||
@@ -41,6 +41,9 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
templateId,
|
||||
onEngineReady,
|
||||
enableAutoSave = true,
|
||||
excelFileName,
|
||||
isExcelLoading = false,
|
||||
onExcelFileChange,
|
||||
}) => {
|
||||
// Мемоизируем конфигурацию листов
|
||||
const sheetsConfig = useMemo(
|
||||
@@ -345,11 +348,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
}, [isEditing])
|
||||
|
||||
// Синхронизация между formula bar и ячейкой при редактировании
|
||||
useEffect(() => {
|
||||
if (isEditing && formulaBarRef.current) {
|
||||
formulaBarRef.current.value = editingValue
|
||||
}
|
||||
}, [editingValue, isEditing])
|
||||
// Теперь полагаемся на controlled компонент через formulaBarValue
|
||||
|
||||
// Обновление подсветки ячеек при вводе формулы
|
||||
useEffect(() => {
|
||||
@@ -774,7 +773,10 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
// Обычный клик - очищаем множественное выделение
|
||||
setMultiSelection([])
|
||||
setSelectedCell({ row, col })
|
||||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||||
// Не возвращаем фокус на контейнер, если он сейчас на formula bar
|
||||
if (document.activeElement !== formulaBarRef.current) {
|
||||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||||
}
|
||||
},
|
||||
[isEditing, stopEditing, activeSheet, selectedCell, copiedCells]
|
||||
)
|
||||
@@ -919,8 +921,30 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
|
||||
const handleFormulaBarFocus = useCallback(() => {
|
||||
if (selectedCell && !isEditing) {
|
||||
// Получаем позицию клика, если она была сохранена
|
||||
const clickPosition = formulaBarRef.current?.dataset.clickPosition
|
||||
const cursorPosition = clickPosition
|
||||
? parseInt(clickPosition)
|
||||
: formulaBarRef.current?.selectionStart || 0
|
||||
|
||||
setEditingSource('formula')
|
||||
startEditing()
|
||||
|
||||
// Восстанавливаем позицию курсора после того как React обновит value
|
||||
setTimeout(() => {
|
||||
if (formulaBarRef.current) {
|
||||
if (document.activeElement !== formulaBarRef.current) {
|
||||
formulaBarRef.current.focus()
|
||||
}
|
||||
// Восстанавливаем вычисленную позицию курсора
|
||||
formulaBarRef.current.setSelectionRange(
|
||||
cursorPosition,
|
||||
cursorPosition
|
||||
)
|
||||
// Очищаем сохраненную позицию
|
||||
delete formulaBarRef.current.dataset.clickPosition
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}, [selectedCell, isEditing, startEditing])
|
||||
|
||||
@@ -1155,6 +1179,9 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
lastSaveError={lastSaveError}
|
||||
onManualSave={manualSave}
|
||||
onClearError={clearError}
|
||||
excelFileName={excelFileName}
|
||||
isExcelLoading={isExcelLoading}
|
||||
onExcelFileChange={onExcelFileChange}
|
||||
/>
|
||||
<div
|
||||
className="flex min-w-0 flex-1 gap-1 overflow-hidden"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Redo2, Save, Undo2 } from 'lucide-react'
|
||||
import { Save } from 'lucide-react'
|
||||
import { memo } from 'react'
|
||||
import { ExcelUploadPanel } from '../../TemplateManager/ExcelUploadPanel'
|
||||
import { FormulaBarProps } from '../types'
|
||||
import { getColumnLabel } from '../utils'
|
||||
|
||||
@@ -20,17 +21,23 @@ export const FormulaBar = memo(
|
||||
lastSaveError = null,
|
||||
onManualSave,
|
||||
onClearError,
|
||||
excelFileName,
|
||||
isExcelLoading = false,
|
||||
onExcelFileChange,
|
||||
}: Omit<FormulaBarProps, 'sheetType' | 'activeSheet'>) => {
|
||||
return (
|
||||
<div className="formula-bar 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">
|
||||
<div className="formula-bar flex-shrink-0 border-b bg-background px-3 py-1.5">
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Адрес ячейки */}
|
||||
<div className="flex min-w-[45px] items-center justify-center rounded border bg-muted px-1.5 py-0.5">
|
||||
<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
|
||||
@@ -43,37 +50,81 @@ export const FormulaBar = memo(
|
||||
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"
|
||||
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 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' : ''}`}
|
||||
className={`text-m flex-1 rounded border border-input bg-background px-2 py-0.5 outline-none ring-offset-background transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${isCalculating ? 'animate-pulse' : ''}`}
|
||||
placeholder="Введите формулу или значение..."
|
||||
value={formulaBarValue}
|
||||
onChange={e => onValueChange(e.target.value)}
|
||||
onFocus={onFocus}
|
||||
onBlur={e => {
|
||||
// Проверяем, не переходит ли фокус в ячейку редактирования
|
||||
const relatedTarget = e.relatedTarget as HTMLElement
|
||||
if (
|
||||
relatedTarget &&
|
||||
relatedTarget.closest('input[type="text"]')
|
||||
) {
|
||||
return // Не завершаем редактирование
|
||||
return
|
||||
}
|
||||
onBlur(e)
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
onMouseDown={e => {
|
||||
// Если поле не в режиме редактирования, нужно запустить редактирование
|
||||
// но сохранить позицию курсора
|
||||
if (!isEditing) {
|
||||
// Вычисляем позицию клика вручную
|
||||
const input = e.currentTarget
|
||||
const rect = input.getBoundingClientRect()
|
||||
const clickX =
|
||||
e.clientX -
|
||||
rect.left -
|
||||
parseInt(getComputedStyle(input).paddingLeft)
|
||||
|
||||
// Создаем временный range для определения позиции
|
||||
const tempSpan = document.createElement('span')
|
||||
tempSpan.textContent = input.value
|
||||
tempSpan.style.cssText = getComputedStyle(input).cssText
|
||||
tempSpan.style.position = 'absolute'
|
||||
tempSpan.style.visibility = 'hidden'
|
||||
tempSpan.style.whiteSpace = 'pre'
|
||||
document.body.appendChild(tempSpan)
|
||||
|
||||
let position = 0
|
||||
let bestDistance = Infinity
|
||||
|
||||
for (let i = 0; i <= input.value.length; i++) {
|
||||
tempSpan.textContent = input.value.substring(0, i)
|
||||
const distance = Math.abs(tempSpan.offsetWidth - clickX)
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance
|
||||
position = i
|
||||
}
|
||||
}
|
||||
|
||||
document.body.removeChild(tempSpan)
|
||||
|
||||
// Сохраняем вычисленную позицию в data-атрибуте
|
||||
input.dataset.clickPosition = position.toString()
|
||||
|
||||
// Запускаем редактирование
|
||||
onFocus()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Правая группа: индикаторы и действия */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Индикатор процесса */}
|
||||
{(isCalculating || isSaving || isLoading) && (
|
||||
<div className="flex items-center text-muted-foreground">
|
||||
<svg
|
||||
className="h-3.5 w-3.5 animate-spin"
|
||||
className="h-2.5 w-2.5 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
@@ -100,6 +151,7 @@ export const FormulaBar = memo(
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ошибка сохранения */}
|
||||
{lastSaveError && (
|
||||
<div className="flex items-center gap-1 rounded-md bg-destructive/10 px-2 py-1">
|
||||
@@ -120,15 +172,15 @@ export const FormulaBar = memo(
|
||||
className="text-xs text-destructive"
|
||||
title={lastSaveError}
|
||||
>
|
||||
Ошибка сохранения
|
||||
Ошибка
|
||||
</span>
|
||||
{onClearError && (
|
||||
<button
|
||||
onClick={onClearError}
|
||||
className="ml-1 text-destructive/60 hover:text-destructive"
|
||||
className="ml-0.5 text-destructive/60 hover:text-destructive"
|
||||
>
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
className="h-2 w-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -144,31 +196,13 @@ export const FormulaBar = memo(
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Кнопки отмены и повтора */}
|
||||
{
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
disabled={true}
|
||||
className="flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
title="Отменить (Ctrl+Z)"
|
||||
>
|
||||
<Undo2 className="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
disabled={true}
|
||||
className="flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
title="Повторить (Ctrl+Y)"
|
||||
>
|
||||
<Redo2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
{/* Кнопка ручного сохранения */}
|
||||
|
||||
{/* Кнопка сохранения */}
|
||||
{onManualSave && (
|
||||
<button
|
||||
onClick={onManualSave}
|
||||
disabled={isSaving}
|
||||
className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors ${
|
||||
className={`flex items-center gap-1 rounded px-1.5 py-0.5 text-xs transition-colors ${
|
||||
hasUnsavedChanges
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||
: lastSaveError
|
||||
@@ -184,13 +218,25 @@ export const FormulaBar = memo(
|
||||
}
|
||||
>
|
||||
<Save className="h-3 w-3" />
|
||||
{lastSaveError
|
||||
? 'Повторить'
|
||||
: hasUnsavedChanges
|
||||
? 'Сохранить'
|
||||
: 'Сохранено'}
|
||||
<span className="hidden sm:inline">
|
||||
{lastSaveError
|
||||
? 'Повторить'
|
||||
: hasUnsavedChanges
|
||||
? 'Сохранить'
|
||||
: 'Сохранено'}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* ExcelUploadPanel */}
|
||||
{onExcelFileChange && (
|
||||
<ExcelUploadPanel
|
||||
fileName={excelFileName}
|
||||
isLoading={isExcelLoading}
|
||||
onUploadClick={() => {}}
|
||||
onFileChange={onExcelFileChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,10 @@ export interface DualSpreadsheetProps {
|
||||
templateId?: string // Добавляем ID шаблона для сохранения
|
||||
onEngineReady?: (engine: any) => void // Коллбэк, который отдаёт родителю методы движка
|
||||
enableAutoSave?: boolean // Возможность отключить автосохранение
|
||||
// Пропсы для ExcelUploadPanel
|
||||
excelFileName?: string
|
||||
isExcelLoading?: boolean
|
||||
onExcelFileChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export interface CellProps {
|
||||
@@ -122,6 +126,10 @@ export interface FormulaBarProps {
|
||||
lastSaveError?: string | null
|
||||
onManualSave?: () => void
|
||||
onClearError?: () => void
|
||||
// Пропсы для ExcelUploadPanel
|
||||
excelFileName?: string
|
||||
isExcelLoading?: boolean
|
||||
onExcelFileChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export interface CellRendererData {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/component/ui/popover'
|
||||
import { FileText, Upload } from 'lucide-react'
|
||||
import React, { useRef } from 'react'
|
||||
|
||||
@@ -18,47 +19,82 @@ export const ExcelUploadPanel: React.FC<ExcelUploadPanelProps> = ({
|
||||
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={`text-xs font-medium ${
|
||||
fileName ? 'text-emerald-700' : 'text-gray-500'
|
||||
}`}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 gap-1 px-2 text-xs"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{fileName || 'Файл не выбран'}
|
||||
</span>
|
||||
</div>
|
||||
<FileText
|
||||
className={`h-3 w-3 ${fileName ? 'text-emerald-600' : 'text-gray-400'}`}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<div className="h-2 w-2 animate-spin rounded-full border border-current border-t-transparent" />
|
||||
) : (
|
||||
<span className={fileName ? 'text-emerald-700' : 'text-gray-500'}>
|
||||
Шаблон
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-3" align="end">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm font-medium">Excel файл</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Загрузите или обновите Excel файл для шаблона
|
||||
</p>
|
||||
</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>
|
||||
{fileName && (
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Текущий файл:
|
||||
</label>
|
||||
<div className="flex items-center gap-2 rounded border bg-emerald-50 p-2">
|
||||
<FileText className="h-3 w-3 text-emerald-600" />
|
||||
<span
|
||||
className="flex-1 text-sm text-emerald-700"
|
||||
title={fileName}
|
||||
>
|
||||
{fileName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
onChange={onFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isLoading}
|
||||
className="w-full gap-2"
|
||||
size="sm"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
Загрузка...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="h-3 w-3" />
|
||||
{fileName ? 'Заменить файл' : 'Загрузить файл'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export const FormattingToolbar: React.FC = () => (
|
||||
<div className="flex items-center">
|
||||
{[Bold, Italic, Underline].map((Icon, i) => (
|
||||
<Button
|
||||
disabled
|
||||
key={i}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -33,6 +34,7 @@ export const FormattingToolbar: React.FC = () => (
|
||||
<div className="flex items-center">
|
||||
{[AlignLeft, AlignCenter, AlignRight].map((Icon, i) => (
|
||||
<Button
|
||||
disabled
|
||||
key={i}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -48,6 +50,7 @@ export const FormattingToolbar: React.FC = () => (
|
||||
<div className="flex items-center">
|
||||
{[Palette, Type, Grid].map((Icon, i) => (
|
||||
<Button
|
||||
disabled
|
||||
key={i}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -7,6 +7,10 @@ interface SpreadsheetViewerProps {
|
||||
mergedCells: ExcelParseResult['mergedCells']
|
||||
dataVersion: number
|
||||
templateId: string | undefined
|
||||
// Пропсы для ExcelUploadPanel
|
||||
excelFileName?: string
|
||||
isExcelLoading?: boolean
|
||||
onExcelFileChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export const SpreadsheetViewer: React.FC<SpreadsheetViewerProps> = ({
|
||||
@@ -14,6 +18,9 @@ export const SpreadsheetViewer: React.FC<SpreadsheetViewerProps> = ({
|
||||
mergedCells,
|
||||
dataVersion,
|
||||
templateId,
|
||||
excelFileName,
|
||||
isExcelLoading,
|
||||
onExcelFileChange,
|
||||
}) => (
|
||||
<div className="min-h-0 flex-1">
|
||||
<DualSpreadsheet
|
||||
@@ -21,6 +28,9 @@ export const SpreadsheetViewer: React.FC<SpreadsheetViewerProps> = ({
|
||||
templateData={{ L: data }}
|
||||
mergedCells={mergedCells}
|
||||
templateId={templateId}
|
||||
excelFileName={excelFileName}
|
||||
isExcelLoading={isExcelLoading}
|
||||
onExcelFileChange={onExcelFileChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
} from '@/service/fileApiService'
|
||||
import { Template } from '@/type/template'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { ExcelUploadPanel } from './ExcelUploadPanel'
|
||||
import { FormattingToolbar } from './FormattingToolbar'
|
||||
import { HeaderBar } from './HeaderBar'
|
||||
import { SpreadsheetViewer } from './SpreadsheetViewer'
|
||||
|
||||
@@ -113,22 +111,14 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
||||
return (
|
||||
<div className="flex h-screen flex-col bg-background">
|
||||
<HeaderBar template={editedTemplate} onBack={onBack} />
|
||||
<div className="flex-shrink-0 border-b bg-muted/30 px-4 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormattingToolbar />
|
||||
<ExcelUploadPanel
|
||||
fileName={serverFileName || editedTemplate.excelFile?.name}
|
||||
isLoading={isLoading}
|
||||
onUploadClick={() => {}}
|
||||
onFileChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SpreadsheetViewer
|
||||
data={excelData}
|
||||
mergedCells={editedTemplate.mergedCells || []}
|
||||
dataVersion={dataVersion}
|
||||
templateId={editedTemplate.id}
|
||||
excelFileName={serverFileName || editedTemplate.excelFile?.name}
|
||||
isExcelLoading={isLoading}
|
||||
onExcelFileChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,45 +1,51 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?:
|
||||
| 'default'
|
||||
| 'destructive'
|
||||
| 'outline'
|
||||
| 'secondary'
|
||||
| 'ghost'
|
||||
| 'link'
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon'
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = 'default', size = 'default', ...props }, ref) => {
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90':
|
||||
variant === 'default',
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90':
|
||||
variant === 'destructive',
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground':
|
||||
variant === 'outline',
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80':
|
||||
variant === 'secondary',
|
||||
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
|
||||
'text-primary underline-offset-4 hover:underline':
|
||||
variant === 'link',
|
||||
},
|
||||
{
|
||||
'h-10 px-4 py-2': size === 'default',
|
||||
'h-9 rounded-md px-3': size === 'sm',
|
||||
'h-11 rounded-md px-8': size === 'lg',
|
||||
'h-10 w-10': size === 'icon',
|
||||
},
|
||||
className
|
||||
)}
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
@@ -48,4 +54,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button }
|
||||
export { Button, buttonVariants }
|
||||
|
||||
@@ -1,147 +1,175 @@
|
||||
'use client'
|
||||
|
||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = 'default',
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: 'sm' | 'default'
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs flex w-fit items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 data-[placeholder]:text-muted-foreground *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = 'popper',
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
'p-1',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-select-content-available-height) origin-(--radix-select-content-transform-origin) relative z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn('px-2 py-1.5 text-xs text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn('pointer-events-none -mx-1 my-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -280,8 +280,8 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
const canSave = true
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col bg-gray-50">
|
||||
<div className="flex-shrink-0 border-b bg-card px-4 py-2">
|
||||
<div className="flex h-screen flex-col bg-background">
|
||||
<div className="flex-shrink-0 bg-card px-4 py-2">
|
||||
<div className="relative flex items-center">
|
||||
{/* Левая часть: кнопка назад и заголовок */}
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -297,6 +297,23 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Центрированные кнопки */}
|
||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 gap-2">
|
||||
<Button
|
||||
variant={showSpreadsheet ? 'default' : 'outline'}
|
||||
size="icon"
|
||||
onClick={() => setShowSpreadsheet(!showSpreadsheet)}
|
||||
aria-label={
|
||||
showSpreadsheet ? 'Скрыть таблицы' : 'Показать таблицы'
|
||||
}
|
||||
>
|
||||
<Grid className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleSave} disabled={!canSave}>
|
||||
<Save className="h-4 w-4" />
|
||||
Сохранить протокол
|
||||
</Button>
|
||||
</div>
|
||||
{/* Кнопки справа */}
|
||||
<div className="absolute right-0 flex gap-2">
|
||||
<Button
|
||||
@@ -318,23 +335,6 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
<span className="text-sm">Редактор интерфейса</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/* Центрированные кнопки */}
|
||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 gap-2">
|
||||
<Button
|
||||
variant={showSpreadsheet ? 'default' : 'outline'}
|
||||
size="icon"
|
||||
onClick={() => setShowSpreadsheet(!showSpreadsheet)}
|
||||
aria-label={
|
||||
showSpreadsheet ? 'Скрыть таблицы' : 'Показать таблицы'
|
||||
}
|
||||
>
|
||||
<Grid className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleSave} disabled={!canSave}>
|
||||
<Save className="h-4 w-4" />
|
||||
Сохранить протокол
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user