новые стили редактирования элементов

This commit is contained in:
2025-07-24 13:55:43 +03:00
parent 80e97f8334
commit a13bbc7a68
3 changed files with 424 additions and 388 deletions

View File

@@ -44,35 +44,28 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({
}
return (
<div className="space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium">
<Settings className="h-4 w-4" />
Настройки эталонов
</label>
<div className="flex items-center gap-2">
<label className="text-sm font-medium">Максимум эталонов</label>
<Select
value={safeConfig.maxItems.toString()}
onValueChange={value => updateConfig({ maxItems: parseInt(value) })}
>
<SelectTrigger className="h-8 w-16">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="3">3</SelectItem>
<SelectItem value="7">7</SelectItem>
<SelectItem value="9">9</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectContent>
</Select>
</div>
<p className="text-xs text-gray-500">
Настройте параметры для выбора измерительных эталонов
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Максимум эталонов</label>
<Select
value={safeConfig.maxItems.toString()}
onValueChange={value => updateConfig({ maxItems: parseInt(value) })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="3">3 эталона</SelectItem>
<SelectItem value="5">5 эталонов</SelectItem>
<SelectItem value="7">7 эталонов</SelectItem>
<SelectItem value="10">10 эталонов</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-gray-500">
Максимальное количество эталонов для выбора
Максимальное количество эталонов для выбора (0-12)
</p>
</div>
@@ -105,16 +98,6 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({
<p className="text-xs text-gray-500">
Эталоны, которые будут выбраны по умолчанию при создании протокола
</p>
<StandardsSelector
isOpen={isStandardsDialogOpen}
onClose={() => setIsStandardsDialogOpen(false)}
value={safeConfig.selectedStandards}
onChange={standardIds => {
updateConfig({ selectedStandards: standardIds })
}}
maxItems={safeConfig.maxItems}
/>
</div>
<div className="rounded-lg bg-blue-50 p-3">
@@ -130,6 +113,16 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({
</div>
</div>
</div>
<StandardsSelector
isOpen={isStandardsDialogOpen}
onClose={() => setIsStandardsDialogOpen(false)}
value={safeConfig.selectedStandards}
onChange={standardIds => {
updateConfig({ selectedStandards: standardIds })
}}
maxItems={safeConfig.maxItems}
/>
</div>
)
}

View File

@@ -11,9 +11,20 @@ import {
Template,
TemplateElement,
} from '@/type/template'
import { Plus, Trash2 } from 'lucide-react'
import {
AlertTriangle,
Eye,
HelpCircle,
Plus,
Settings2,
Tag,
Target,
Trash2,
Type,
} from 'lucide-react'
import React, { useState } from 'react'
import { Button } from '../ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
import { Checkbox } from '../ui/checkbox'
import {
Dialog,
@@ -23,6 +34,7 @@ import {
DialogTitle,
} from '../ui/dialog'
import { Input } from '../ui/input'
import { Label } from '../ui/label'
import {
Select,
SelectContent,
@@ -30,6 +42,7 @@ import {
SelectTrigger,
SelectValue,
} from '../ui/select'
import { Separator } from '../ui/separator'
import { ElementFormulaBar } from './ElementFormulaBar'
import { VisualLayoutEditor } from './VisualLayoutEditor'
@@ -79,72 +92,92 @@ const CellTargetEditor: React.FC<{
}
return (
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Целевые ячейки</label>
<Button
variant="outline"
size="sm"
onClick={handleAddTarget}
className="h-7 px-2 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
Добавить
</Button>
</div>
<div className="max-h-40 space-y-2 overflow-y-auto">
{targets.map((target, index) => (
<div key={index} className="rounded border bg-gray-50/80 p-2">
<div className="mb-1.5 grid grid-cols-2 gap-2">
<div>
<label className="text-xs font-medium text-gray-600">
Лист
</label>
<Select
value={target.sheet}
onValueChange={value =>
handleUpdateTarget(index, 'sheet', value)
}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="L">L (Левый)</SelectItem>
<SelectItem value="R">R (Правый)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="text-xs font-medium text-gray-600">
Ячейка
</label>
<Input
placeholder="A1, B5..."
value={target.cell}
onChange={e => handleCellInputChange(index, e.target.value)}
className="h-7 text-xs"
/>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveTarget(index)}
className="h-5 px-1 text-xs text-red-600 hover:bg-red-50 hover:text-red-700"
>
<Trash2 className="mr-1 h-2.5 w-2.5" />
Удалить
</Button>
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Target className="h-4 w-4 text-blue-600" />
<CardTitle className="text-sm">Целевые ячейки</CardTitle>
</div>
<Button
variant="outline"
size="sm"
onClick={handleAddTarget}
className="h-6 px-2 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
Добавить
</Button>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="max-h-48 space-y-2 overflow-y-auto">
{targets.map((target, index) => (
<div
key={index}
className="flex items-center gap-2 rounded-lg border bg-gray-50/50 p-2"
>
{/* Кнопки выбора листа */}
<div className="flex gap-1">
<Button
variant={target.sheet === 'L' ? 'default' : 'outline'}
size="sm"
onClick={() => handleUpdateTarget(index, 'sheet', 'L')}
className="h-6 w-7 p-0 text-xs"
>
L
</Button>
<Button
variant={target.sheet === 'R' ? 'default' : 'outline'}
size="sm"
onClick={() => handleUpdateTarget(index, 'sheet', 'R')}
className="h-6 w-7 p-0 text-xs"
>
R
</Button>
</div>
{/* Поле ввода ячейки */}
<Input
placeholder="A1"
value={target.cell}
onChange={e => handleCellInputChange(index, e.target.value)}
className="h-6 w-16 px-2 text-xs"
/>
{/* Кнопка удаления */}
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveTarget(index)}
className="ml-auto h-6 w-6 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
{targets.length === 0 && (
<div className="py-4 text-center">
<Target className="mx-auto mb-1 h-6 w-6 text-gray-300" />
<p className="text-xs text-gray-500">
Добавьте ячейки для связи с элементом
</p>
<p className="mt-1 text-xs text-gray-400">
Например: A1, B5, C10
</p>
</div>
)}
</div>
{targets.length > 0 && (
<div className="mt-2 flex items-center gap-1 text-xs text-gray-500">
<HelpCircle className="h-3 w-3" />
Всего ячеек: {targets.length}
</div>
))}
{targets.length === 0 && (
<p className="py-3 text-center text-xs text-gray-500">
Добавьте ячейки для связи с элементом
</p>
)}
</div>
</div>
</CardContent>
</Card>
)
}
@@ -156,42 +189,40 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
? getElementDefinition(element.type)
: null
if (elementDefinition && elementDefinition.Preview) {
return <elementDefinition.Preview config={element as any} />
}
// Fallback для неизвестных типов элементов
return (
<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">
{element.label && (
<label className="mb-2 block text-sm font-medium text-gray-700">
{element.label}
{element.required && <span className="ml-1 text-red-500">*</span>}
</label>
)}
<div className="rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
{element.type
? `Элемент типа "${element.type}" не найден`
: 'Выберите тип элемента'}
<Card className="h-full">
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Eye className="h-4 w-4 text-green-600" />
<CardTitle className="text-sm">Предпросмотр элемента</CardTitle>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="min-h-[200px] rounded-lg border border-gray-200 bg-white p-4">
{elementDefinition && elementDefinition.Preview ? (
<elementDefinition.Preview config={element as any} />
) : (
<div className="space-y-3">
{element.label && (
<label className="mb-2 block text-sm font-medium text-gray-700">
{element.label}
{element.required && (
<span className="ml-1 text-red-500">*</span>
)}
</label>
)}
{element.targetCells && element.targetCells.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{element.targetCells.map((target, index) => (
<span
key={index}
className="inline-block rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700"
>
{target.sheet}!{target.cell}
</span>
))}
</div>
)}
</div>
</div>
<div className="flex items-center justify-center gap-2 rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
<AlertTriangle className="h-4 w-4" />
{element.type
? `Элемент типа "${element.type}" не найден`
: 'Выберите тип элемента'}
</div>
</div>
)}
</div>
</CardContent>
</Card>
)
}
@@ -228,179 +259,113 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
}
const needsOptions =
formData.type === 'select' ||
formData.type === 'radio' ||
formData.type === 'button_group'
formData.type === 'select' || formData.type === 'button_group'
// Если есть определение элемента в реестре, используем его редактор
if (elementDefinition && elementDefinition.Editor) {
return (
<div className="grid grid-cols-2 gap-6">
{/* Форма настроек */}
<div className="space-y-4">
<div className="space-y-1.5">
<label className="text-sm font-medium">Тип элемента</label>
<Select
value={formData.type}
onValueChange={value => {
const newType = value as ElementType
const elementDefinition = getElementDefinition(newType)
setFormData(prev => {
const updates: Partial<TemplateElement> = {
type: newType,
// Сбрасываем targetCells при смене типа
targetCells: [],
}
// Применяем конфиг по умолчанию из определения элемента
if (elementDefinition && elementDefinition.defaultConfig) {
Object.assign(updates, elementDefinition.defaultConfig)
}
return { ...prev, ...updates }
})
}}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{getAvailableElementTypes().map(({ type, label, icon }) => (
<SelectItem key={type} value={type}>
<div className="flex items-center gap-2">
{icon}
{label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<label className="text-sm font-medium">Подпись</label>
<Input
placeholder="Название поля"
value={formData.label}
onChange={e =>
setFormData(prev => ({ ...prev, label: e.target.value }))
}
className="h-9"
/>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">Подсказка</label>
<Input
placeholder="Введите значение..."
value={formData.placeholder}
onChange={e =>
setFormData(prev => ({
...prev,
placeholder: e.target.value,
}))
}
className="h-9"
/>
</div>
</div>
<div className="flex items-center space-x-2 py-1">
<Checkbox
id="required"
checked={formData.required}
onCheckedChange={checked =>
setFormData(prev => ({ ...prev, required: !!checked }))
}
/>
<label htmlFor="required" className="text-sm font-medium">
Обязательное поле
</label>
</div>
<CellTargetEditor
targets={formData.targetCells || []}
onChange={targets =>
setFormData(prev => ({ ...prev, targetCells: targets }))
}
/>
{/* Специфичный редактор элемента */}
<elementDefinition.Editor
config={formData as any}
onChange={newConfig =>
setFormData(prev => ({ ...prev, ...newConfig }))
}
/>
</div>
{/* Live Preview */}
<div className="sticky top-0 flex min-h-[50vh] items-center">
<ElementPreview element={formData} />
</div>
</div>
)
}
// Fallback для старых элементов
return (
<div className="grid grid-cols-2 gap-6">
{/* Форма настроек */}
<div className="space-y-4">
<div className="space-y-1.5">
<label className="text-sm font-medium">Тип элемента</label>
<Select
value={formData.type}
onValueChange={value => {
const newType = value as ElementType
const elementDefinition = getElementDefinition(newType)
<div className="grid h-full grid-cols-1 gap-6 lg:grid-cols-2">
{/* Настройки элемента */}
<div className="max-h-[60vh] space-y-4 overflow-y-auto pr-2">
{/* Основные настройки */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Settings2 className="h-4 w-4 text-blue-600" />
<CardTitle className="text-sm">Основные настройки</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="flex items-center gap-2 text-sm font-medium">
<Type className="h-3 w-3" />
Тип элемента
</Label>
<Select
value={formData.type}
onValueChange={value => {
const newType = value as ElementType
const elementDefinition = getElementDefinition(newType)
setFormData(prev => {
const updates: Partial<TemplateElement> = {
type: newType,
// Сбрасываем targetCells при смене типа
targetCells: [],
setFormData(prev => {
const updates: Partial<TemplateElement> = {
type: newType,
targetCells: [],
}
if (elementDefinition && elementDefinition.defaultConfig) {
Object.assign(updates, elementDefinition.defaultConfig)
}
return { ...prev, ...updates }
})
}}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{getAvailableElementTypes().map(({ type, label, icon }) => (
<SelectItem key={type} value={type}>
<div className="flex items-center gap-2">
{icon}
{label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Label className="flex items-center gap-2 text-sm font-medium">
<Tag className="h-3 w-3" />
Подпись
</Label>
<Input
placeholder="Название поля"
value={formData.label}
onChange={e =>
setFormData(prev => ({ ...prev, label: e.target.value }))
}
className="h-9"
/>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2 text-sm font-medium">
<HelpCircle className="h-3 w-3" />
Подсказка
</Label>
<Input
placeholder="Введите значение..."
value={formData.placeholder}
onChange={e =>
setFormData(prev => ({
...prev,
placeholder: e.target.value,
}))
}
className="h-9"
/>
</div>
</div>
<div className="flex items-center space-x-2 py-1">
<Checkbox
id="required"
checked={formData.required}
onCheckedChange={checked =>
setFormData(prev => ({ ...prev, required: !!checked }))
}
/>
<Label htmlFor="required" className="text-sm font-medium">
Обязательное поле
</Label>
</div>
</CardContent>
</Card>
// Применяем конфиг по умолчанию из определения элемента
if (elementDefinition && elementDefinition.defaultConfig) {
Object.assign(updates, elementDefinition.defaultConfig)
}
return { ...prev, ...updates }
})
}}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{getAvailableElementTypes().map(({ type, label, icon }) => (
<SelectItem key={type} value={type}>
<div className="flex items-center gap-2">
{icon}
{label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">Подпись</label>
<Input
placeholder="Название поля"
value={formData.label}
onChange={e =>
setFormData(prev => ({ ...prev, label: e.target.value }))
}
className="h-9"
/>
</div>
{/* Целевые ячейки */}
<CellTargetEditor
targets={formData.targetCells || []}
onChange={targets =>
@@ -408,56 +373,94 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
}
/>
{/* Специфичный редактор элемента */}
{elementDefinition && elementDefinition.Editor && (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Settings2 className="h-4 w-4 text-purple-600" />
<CardTitle className="text-sm">
Дополнительные настройки
</CardTitle>
</div>
</CardHeader>
<CardContent>
<elementDefinition.Editor
config={formData as any}
onChange={newConfig =>
setFormData(prev => ({ ...prev, ...newConfig }))
}
/>
</CardContent>
</Card>
)}
{/* Варианты для select/radio/button_group */}
{needsOptions && (
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Варианты</label>
<Button
variant="outline"
size="sm"
onClick={handleAddOption}
className="h-7 px-2 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
Добавить
</Button>
</div>
<div className="max-h-28 space-y-1.5 overflow-y-auto">
{formData.options?.map((option, index) => (
<div key={index} className="flex gap-1.5">
<Input
placeholder="Значение"
value={option.value}
onChange={e =>
handleUpdateOption(index, 'value', e.target.value)
}
className="h-7 text-xs"
/>
<Input
placeholder="Подпись"
value={option.label}
onChange={e =>
handleUpdateOption(index, 'label', e.target.value)
}
className="h-7 text-xs"
/>
<Button
variant="ghost"
size="icon"
onClick={() => handleRemoveOption(index)}
className="h-7 w-7 text-red-600 hover:bg-red-50 hover:text-red-700"
>
<Trash2 className="h-3 w-3" />
</Button>
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Tag className="h-4 w-4 text-orange-600" />
<CardTitle className="text-sm">Варианты выбора</CardTitle>
</div>
))}
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleAddOption}
className="h-7 px-2 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
Добавить
</Button>
</div>
</CardHeader>
<CardContent>
<div className="max-h-32 space-y-2 overflow-y-auto">
{formData.options?.map((option, index) => (
<div key={index} className="flex gap-2">
<Input
placeholder="Значение"
value={option.value}
onChange={e =>
handleUpdateOption(index, 'value', e.target.value)
}
className="h-8 text-xs"
/>
<Input
placeholder="Подпись"
value={option.label}
onChange={e =>
handleUpdateOption(index, 'label', e.target.value)
}
className="h-8 text-xs"
/>
<Button
variant="ghost"
size="icon"
onClick={() => handleRemoveOption(index)}
className="h-8 w-8 text-red-600 hover:bg-red-50 hover:text-red-700"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
{(!formData.options || formData.options.length === 0) && (
<div className="py-4 text-center">
<Tag className="mx-auto mb-2 h-6 w-6 text-gray-300" />
<p className="text-xs text-gray-500">
Добавьте варианты выбора
</p>
</div>
)}
</div>
</CardContent>
</Card>
)}
</div>
{/* Live Preview */}
<div className="sticky top-0 flex min-h-[50vh] items-center">
{/* Предпросмотр */}
<div className="h-fit lg:sticky lg:top-0">
<ElementPreview element={formData} />
</div>
</div>
@@ -661,11 +664,6 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
resetForm()
}
// const reordered = (reorderedElements: TemplateElement[]) => {
// onElementsReorder(reorderedElements);
// onLayoutSettingsChange({ ...layoutSettings, showGrid: true });
// };
return (
<div className="flex h-full w-full flex-col bg-background">
<ElementFormulaBar
@@ -704,25 +702,46 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
{/* Диалог добавления элемента */}
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader>
<DialogTitle>Добавить новый элемент</DialogTitle>
<DialogContent className="max-h-[90vh] max-w-6xl overflow-hidden">
<DialogHeader className="pb-4">
<DialogTitle className="flex items-center gap-2 text-lg">
<Plus className="h-5 w-5 text-blue-600" />
Добавить новый элемент
</DialogTitle>
<DialogDescription>
Настройте параметры нового элемента формы.
Создайте и настройте новый элемент формы. Вы можете увидеть
предпросмотр справа.
</DialogDescription>
</DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 pt-4">
<Button onClick={() => setIsAddDialogOpen(false)} variant="outline">
Отмена
</Button>
<Button
onClick={handleAddElement}
disabled={disabled || !formData.label}
>
<Plus className="mr-2 h-4 w-4" />
Добавить элемент
</Button>
<div className="flex-1 overflow-hidden">
<ElementForm formData={formData} setFormData={setFormData} />
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between pt-2">
<div className="text-sm text-gray-500">
{!formData.label && (
<div className="flex items-center gap-1 text-amber-600">
<AlertTriangle className="h-3 w-3" />
Заполните обязательные поля
</div>
)}
</div>
<div className="flex gap-3">
<Button
onClick={() => setIsAddDialogOpen(false)}
variant="outline"
>
Отмена
</Button>
<Button
onClick={handleAddElement}
disabled={disabled || !formData.label}
className="min-w-[140px]"
>
<Plus className="mr-2 h-4 w-4" />
Добавить элемент
</Button>
</div>
</div>
</DialogContent>
</Dialog>
@@ -730,21 +749,43 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
{/* Диалог редактирования элемента */}
{editingElement && (
<Dialog open={!!editingElement} onOpenChange={() => handleCancelEdit()}>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader>
<DialogTitle>Редактировать элемент</DialogTitle>
<DialogContent className="max-h-[90vh] max-w-6xl overflow-hidden">
<DialogHeader className="pb-4">
<DialogTitle className="flex items-center gap-2 text-lg">
<Settings2 className="h-5 w-5 text-green-600" />
Редактировать элемент
</DialogTitle>
<DialogDescription>
Измените параметры элемента формы.
Измените параметры элемента "
{editingElement.label || editingElement.name}".
</DialogDescription>
</DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 pt-4">
<Button onClick={handleCancelEdit} variant="outline">
Отмена
</Button>
<Button onClick={handleUpdateElement} disabled={disabled}>
Сохранить изменения
</Button>
<div className="flex-1 overflow-hidden">
<ElementForm formData={formData} setFormData={setFormData} />
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between pt-2">
<div className="text-sm text-gray-500">
{!formData.label && (
<div className="flex items-center gap-1 text-amber-600">
<AlertTriangle className="h-3 w-3" />
Заполните обязательные поля
</div>
)}
</div>
<div className="flex gap-3">
<Button onClick={handleCancelEdit} variant="outline">
Отмена
</Button>
<Button
onClick={handleUpdateElement}
disabled={disabled || !formData.label}
className="min-w-[160px]"
>
<Settings2 className="mr-2 h-4 w-4" />
Сохранить изменения
</Button>
</div>
</div>
</DialogContent>
</Dialog>

View File

@@ -150,14 +150,16 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
>
{/* Controls (only visible when selected or hovered) */}
<div
className={`absolute right-1 top-1 flex items-center gap-1 ${
className={`absolute right-1 top-1 flex items-center gap-0.5 ${
isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-60'
}`}
>
{/* Индикатор ячеек */}
{element.targetCells && element.targetCells.length > 0 && (
<div className="group/cells relative">
<Grid className="h-3 w-3 cursor-help text-gray-500 opacity-60 transition-opacity hover:opacity-100" />
<div className="flex !h-5 !w-5 cursor-help items-center justify-center rounded hover:bg-gray-100">
<Grid className="!h-3 !w-3 text-gray-500" />
</div>
<div className="pointer-events-none absolute right-0 top-full z-50 mt-2 whitespace-nowrap rounded bg-gray-900 p-2 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover/cells:opacity-100">
<div className="mb-1 font-medium">Целевые ячейки:</div>
{element.targetCells.map((cell, i) => (
@@ -177,19 +179,19 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
<Button
variant="ghost"
size="icon"
className="h-4 w-4 cursor-pointer hover:bg-blue-100"
className="!h-5 !w-5 !min-w-0 cursor-pointer !p-0 hover:bg-blue-100"
onClick={handleEditClick}
>
<Edit className="h-2.5 w-2.5 text-blue-600" />
<Edit className="!h-3 !w-3 text-blue-600" />
</Button>
)}
<Button
variant="ghost"
size="icon"
className="h-4 w-4 cursor-pointer hover:bg-red-100"
className="!h-5 !w-5 !min-w-0 cursor-pointer !p-0 hover:bg-red-100"
onClick={handleDeleteClick}
>
<Trash2 className="h-2.5 w-2.5 text-red-600" />
<Trash2 className="!h-3 !w-3 text-red-600" />
</Button>
</div>
@@ -286,7 +288,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
onClick={handleCanvasClick}
>
<div
className="relative mx-auto my-8 select-none bg-white shadow-lg"
className="relative mx-auto my-8 select-none border border-gray-300 bg-white"
style={{ width: canvasSize.width, height: canvasSize.height }}
onClick={handleCanvasClick}
>