Files
BoxGenerator/src/components/LayoutStep.tsx
Халимов Рустам 57a5695c2a Add size
2026-01-11 00:09:49 +03:00

531 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef, useState, useMemo } from 'react';
import { AppConfig, LayoutSplits, Partition } from '../types';
import { Grid, MousePointer2, Trash2, RotateCcw, X, Move, Settings2, Plus } from 'lucide-react';
interface Props {
config: AppConfig;
splits: LayoutSplits;
onChange: (splits: LayoutSplits) => void;
}
type EditMode = 'lines' | 'cells';
type Axis = 'x' | 'y';
type DragTarget =
| { type: 'main'; axis: Axis; index: number }
| { type: 'partition'; cellKey: string; id: string; axis: Axis };
export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
const svgRef = useRef<SVGSVGElement>(null);
// -- STATE --
const [mode, setMode] = useState<EditMode>('lines');
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const [dragging, setDragging] = useState<DragTarget | null>(null);
const [isButtonHovered, setIsButtonHovered] = useState(false);
// Main Grid Hover
const [phantomMainAxis, setPhantomMainAxis] = useState<Axis | null>(null);
const [hoveredMainSplit, setHoveredMainSplit] = useState<{ axis: Axis; index: number } | null>(null);
// Partition Hover
const [hoveredCell, setHoveredCell] = useState<{ i: number; j: number } | null>(null);
const [hoveredPartition, setHoveredPartition] = useState<{ id: string; cellKey: string } | null>(null);
const [phantomPartition, setPhantomPartition] = useState<{ axis: Axis; offset: number; min: number; max: number } | null>(null);
const [selectedPartitionId, setSelectedPartitionId] = useState<string | null>(null);
// -- SAFE DATA --
const safeX = Array.isArray(splits?.x) ? splits.x : [];
const safeY = Array.isArray(splits?.y) ? splits.y : [];
const safePartitions = splits?.partitions || {};
const drawerW = Math.max(1, config.drawer.width || 300);
const drawerD = Math.max(1, config.drawer.depth || 400);
const aspectRatio = drawerD / drawerW;
const thickness = config.wallThickness || 1.2;
const viewBoxW = 1000;
const viewBoxH = viewBoxW * aspectRatio;
const sortedX = useMemo(() => [0, ...safeX, 1].sort((a, b) => a - b), [safeX]);
const sortedY = useMemo(() => [0, ...safeY, 1].sort((a, b) => a - b), [safeY]);
// -- HELPERS --
const getSelectedPartition = () => {
if (!selectedPartitionId) return null;
for (const key in safePartitions) {
const part = safePartitions[key].find(p => p.id === selectedPartitionId);
if (part) return { key, part };
}
return null;
};
const selectedData = getSelectedPartition();
// Найти ближайшие границы для перегородки (или точки)
const getNearestNeighbors = (lx: number, ly: number, axis: Axis, parts: Partition[]) => {
let min = 0;
let max = 1;
parts.forEach(p => {
// Игнорируем саму себя, если это проверка для существующей стенки (но здесь мы передаем lx/ly точки)
const pMin = p.min ?? 0;
const pMax = p.max ?? 1;
// Если мы ищем границы по X (для вертикальной стенки или пространства слева/справа)
// То нас интересуют другие ВЕРТИКАЛЬНЫЕ стенки (axis === 'x'), которые перекрывают нас по Y
if (axis === 'x') {
if (p.axis === 'x') {
// Перекрываются ли диапазоны высоты?
// ly для точки - это просто координата. Для стенки это min-max.
// Здесь мы ищем границы для точки (lx, ly).
// Стенка P идет от pMin до pMax по Y.
if (ly > pMin && ly < pMax) {
if (p.offset < lx) min = Math.max(min, p.offset);
if (p.offset > lx) max = Math.min(max, p.offset);
}
}
} else {
// Ищем границы по Y (для горизонтальной стенки)
// Нас интересуют ГОРИЗОНТАЛЬНЫЕ стенки
if (p.axis === 'y') {
if (lx > pMin && lx < pMax) {
if (p.offset < ly) min = Math.max(min, p.offset);
if (p.offset > ly) max = Math.min(max, p.offset);
}
}
}
});
return { min, max };
};
// Расчет границ для НОВОЙ линии (перпендикулярной)
const getHoveredBoundaries = (lx: number, ly: number, parts: Partition[]) => {
let minX = 0, maxX = 1;
let minY = 0, maxY = 1;
parts.forEach(p => {
const pMin = p.min ?? 0;
const pMax = p.max ?? 1;
if (p.axis === 'x') {
if (ly >= pMin && ly <= pMax) {
if (p.offset < lx) minX = Math.max(minX, p.offset);
if (p.offset > lx) maxX = Math.min(maxX, p.offset);
}
} else {
if (lx >= pMin && lx <= pMax) {
if (p.offset < ly) minY = Math.max(minY, p.offset);
if (p.offset > ly) maxY = Math.min(maxY, p.offset);
}
}
});
return { minX, maxX, minY, maxY };
};
// -- ACTIONS --
const createPartition = (i: number, j: number, axis: Axis, offset: number, min: number, max: number) => {
const key = `${i}-${j}`;
const current = safePartitions[key] || [];
const newPart: Partition = {
id: Math.random().toString(36).substr(2, 9),
axis, offset, min, max,
height: config.drawer.height || 80,
rounded: false
};
onChange({ ...splits, partitions: { ...safePartitions, [key]: [...current, newPart] } });
setSelectedPartitionId(newPart.id);
};
const updatePartition = (key: string, id: string, updates: Partial<Partition>) => {
const current = safePartitions[key] || [];
const updated = current.map(p => p.id === id ? { ...p, ...updates } : p);
onChange({ ...splits, partitions: { ...safePartitions, [key]: updated } });
};
const removePartition = (key: string, id: string) => {
const current = safePartitions[key] || [];
onChange({ ...splits, partitions: { ...safePartitions, [key]: current.filter(p => p.id !== id) } });
if (selectedPartitionId === id) setSelectedPartitionId(null);
setHoveredPartition(null);
};
const removeMainSplit = (axis: Axis, index: number) => {
const newSplits = { ...splits, x: [...safeX], y: [...safeY] };
newSplits[axis] = newSplits[axis].filter((_, i) => i !== index);
onChange(newSplits);
setHoveredMainSplit(null);
setDragging(null);
};
// -- MOUSE HANDLER --
const handleMouseMove = (e: React.MouseEvent) => {
if (!svgRef.current) return;
const rect = svgRef.current.getBoundingClientRect();
if (rect.width === 0) return;
const nx = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
const ny = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
setMousePos({ x: nx, y: ny });
if (dragging) {
if (dragging.type === 'main') {
const newSplits = { ...splits, x: [...safeX], y: [...safeY] };
const val = dragging.axis === 'x' ? nx : ny;
newSplits[dragging.axis][dragging.index] = val;
onChange(newSplits);
} else {
const [iStr, jStr] = dragging.cellKey.split('-');
const i = parseInt(iStr); const j = parseInt(jStr);
const cellX1 = sortedX[i]; const cellX2 = sortedX[i+1];
const cellY1 = sortedY[j]; const cellY2 = sortedY[j+1];
let newOffset = 0;
// Текущие границы перетаскиваемой стенки (чтобы не выехать за пределы соседей)
// Для простоты пока используем границы ячейки (0-1), так как сложный пересчет соседей в реалтайме - это дорого
// Но визуально мы ограничим 2-98%
if (dragging.axis === 'x') {
newOffset = (nx - cellX1) / (cellX2 - cellX1);
} else {
newOffset = (ny - cellY1) / (cellY2 - cellY1);
}
newOffset = Math.max(0.02, Math.min(0.98, newOffset));
if (!isNaN(newOffset)) {
updatePartition(dragging.cellKey, dragging.id, { offset: newOffset });
}
}
return;
}
if (isButtonHovered) return;
if (mode === 'lines') {
setHoveredMainSplit(null);
const SNAP = 0.015;
if (nx > SNAP && nx < 1 - SNAP && ny > SNAP && ny < 1 - SNAP) {
const closeToX = safeX.some(val => Math.abs(nx - val) < SNAP);
const closeToY = safeY.some(val => Math.abs(ny - val) < SNAP);
if (!closeToX && !closeToY) {
const distRight = 1 - nx; const distBottom = 1 - ny;
setPhantomMainAxis(Math.min(nx, distRight) < Math.min(ny, distBottom) ? 'y' : 'x');
} else { setPhantomMainAxis(null); }
}
} else {
// Cells Mode
setHoveredPartition(null);
setPhantomPartition(null);
setHoveredCell(null);
let cellIdx = null;
for (let i = 0; i < sortedX.length - 1; i++) {
if (nx >= sortedX[i] && nx <= sortedX[i+1]) {
for (let j = 0; j < sortedY.length - 1; j++) {
if (ny >= sortedY[j] && ny <= sortedY[j+1]) {
cellIdx = { i, j };
break;
}
}
}
}
if (cellIdx) {
setHoveredCell(cellIdx);
const key = `${cellIdx.i}-${cellIdx.j}`;
const parts = safePartitions[key] || [];
const cx1 = sortedX[cellIdx.i]; const cx2 = sortedX[cellIdx.i+1];
const cy1 = sortedY[cellIdx.j]; const cy2 = sortedY[cellIdx.j+1];
const cw = cx2 - cx1; const ch = cy2 - cy1;
const lx = (nx - cx1) / cw;
const ly = (ny - cy1) / ch;
let found = null;
const SNAP = 0.05;
for (const p of parts) {
const pMin = p.min ?? 0; const pMax = p.max ?? 1;
if (p.axis === 'x') {
if (ly >= pMin && ly <= pMax && Math.abs(lx - p.offset) < SNAP * (aspectRatio > 1 ? 1 : aspectRatio)) found = p;
} else {
if (lx >= pMin && lx <= pMax && Math.abs(ly - p.offset) < SNAP * (aspectRatio < 1 ? 1 : 1/aspectRatio)) found = p;
}
}
if (found) {
setHoveredPartition({ id: found.id, cellKey: key });
} else {
const bounds = getHoveredBoundaries(lx, ly, parts);
const distL = lx - bounds.minX; const distR = bounds.maxX - lx;
const distT = ly - bounds.minY; const distB = bounds.maxY - ly;
const minX = Math.min(distL, distR);
const minY = Math.min(distT, distB);
const axis = minX < minY ? 'y' : 'x';
const width = bounds.maxX - bounds.minX;
const height = bounds.maxY - bounds.minY;
if ((axis === 'y' && height > 0.1) || (axis === 'x' && width > 0.1)) {
const offset = axis === 'x' ? lx : ly;
const min = axis === 'x' ? bounds.minY : bounds.minX;
const max = axis === 'x' ? bounds.maxY : bounds.maxX;
setPhantomPartition({ axis, offset, min, max });
}
}
}
}
};
const handleMouseDown = (e: React.MouseEvent) => {
if (isButtonHovered) return;
if (mode === 'lines') {
if (hoveredMainSplit) {
if (e.button === 0) setDragging({ type: 'main', ...hoveredMainSplit });
else if (e.button === 2) removeMainSplit(hoveredMainSplit.axis, hoveredMainSplit.index);
} else if (phantomMainAxis) {
const val = phantomMainAxis === 'x' ? mousePos.x : mousePos.y;
const newSplits = { ...splits, x: [...safeX], y: [...safeY] };
newSplits[phantomMainAxis] = [...newSplits[phantomMainAxis], val];
onChange(newSplits);
setDragging({ type: 'main', axis: phantomMainAxis, index: newSplits[phantomMainAxis].length - 1 });
}
} else {
if (hoveredPartition) {
const key = hoveredPartition.cellKey;
const parts = safePartitions[key] || [];
const part = parts.find(p => p.id === hoveredPartition.id);
if (part) {
if (e.button === 0) {
setDragging({ type: 'partition', cellKey: key, id: part.id, axis: part.axis });
setSelectedPartitionId(part.id);
} else if (e.button === 2) {
removePartition(key, part.id);
}
}
} else if (hoveredCell && phantomPartition) {
if (e.button === 0) {
createPartition(hoveredCell.i, hoveredCell.j, phantomPartition.axis, phantomPartition.offset, phantomPartition.min, phantomPartition.max);
}
} else {
setSelectedPartitionId(null);
}
}
};
// --- RENDER ---
const renderCellsAndPartitions = () => {
const elements = [];
for (let i = 0; i < sortedX.length - 1; i++) {
for (let j = 0; j < sortedY.length - 1; j++) {
const x1 = sortedX[i]; const x2 = sortedX[i + 1];
const y1 = sortedY[j]; const y2 = sortedY[j + 1];
if (y2 === undefined || x2 === undefined) continue;
const cellX = x1 * viewBoxW; const cellY = y1 * viewBoxH;
const cellW = (x2 - x1) * viewBoxW; const cellH = (y2 - y1) * viewBoxH;
const key = `${i}-${j}`;
const isHovered = hoveredCell?.i === i && hoveredCell?.j === j && mode === 'cells';
const parts = safePartitions[key] || [];
const isSelected = parts.some(p => p.id === selectedPartitionId);
// Реальные размеры ячейки в мм
const realW = (x2 - x1) * drawerW;
const realD = (y2 - y1) * drawerD;
// --- ЛЕЙБЛ РАЗМЕРА ---
// Если перегородок нет, показываем общий размер по центру
if (parts.length === 0) {
const labelX = cellX + cellW / 2;
const labelY = cellY + cellH / 2;
// Учитываем толщину внешних стенок (или соседних)
// Упрощенно: вычитаем толщину (1 раз, т.к. 0.5 с каждой стороны)
const textW = Math.max(0, realW - thickness).toFixed(0);
const textD = Math.max(0, realD - thickness).toFixed(0);
// Рисуем текст только если есть место
if (cellH > 40 && cellW > 60) {
elements.push(
<text key={`label-${key}`} x={labelX} y={labelY} textAnchor="middle" dominantBaseline="middle" className="fill-slate-400 font-mono text-[10px] pointer-events-none select-none opacity-60">
{textW} × {textD}
</text>
);
}
}
elements.push(
<g key={key}>
{isHovered && <rect x={cellX} y={cellY} width={cellW} height={cellH} fill="rgba(16, 185, 129, 0.05)" stroke="#10b981" strokeWidth="2" strokeDasharray="4,4" className="pointer-events-none"/>}
{parts.map(p => {
const pMin = p.min ?? 0; const pMax = p.max ?? 1;
let lx1, ly1, lx2, ly2; // Координаты линии
let tx, ty; // Координаты текста
let dist1 = 0, dist2 = 0; // Расстояния до соседей (в мм)
// Находим соседей для расчета размеров
const neighbors = getNearestNeighbors(p.offset, (pMin + pMax)/2, p.axis, parts);
if (p.axis === 'x') {
const px = cellX + (cellW * p.offset);
lx1 = px; lx2 = px;
ly1 = cellY + (cellH * pMin); ly2 = cellY + (cellH * pMax);
// Размеры слева и справа
const leftRatio = p.offset - neighbors.min;
const rightRatio = neighbors.max - p.offset;
dist1 = (leftRatio * realW) - thickness; // Left
dist2 = (rightRatio * realW) - thickness; // Right
tx = px;
ty = (ly1 + ly2) / 2;
} else {
const py = cellY + (cellH * p.offset);
ly1 = py; ly2 = py;
lx1 = cellX + (cellW * pMin); lx2 = cellX + (cellW * pMax);
// Размеры сверху и снизу
const topRatio = p.offset - neighbors.min;
const botRatio = neighbors.max - p.offset;
dist1 = (topRatio * realD) - thickness; // Top
dist2 = (botRatio * realD) - thickness; // Bottom
tx = (lx1 + lx2) / 2;
ty = py;
}
const isSel = selectedPartitionId === p.id;
const isHov = hoveredPartition?.id === p.id;
return (
<g key={p.id}>
{/* Сама линия */}
<g onDoubleClick={(e) => { e.stopPropagation(); removePartition(key, p.id); }}>
<line x1={lx1} y1={ly1} x2={lx2} y2={ly2} stroke="transparent" strokeWidth="40" />
<line x1={lx1} y1={ly1} x2={lx2} y2={ly2} stroke={isSel ? "#a855f7" : (isHov ? "#d8b4fe" : "#7e22ce")} strokeWidth={isSel ? 6 : 4} strokeLinecap="round" />
</g>
{/* Текст размеров (Слева/Справа или Сверху/Снизу) */}
<text x={tx} y={ty} className="pointer-events-none select-none font-mono text-[9px] fill-white font-bold" textAnchor="middle" dominantBaseline="middle">
{p.axis === 'x' ? (
<>
<tspan dx="-12" fill="#94a3b8">{dist1.toFixed(0)}</tspan>
<tspan dx="24" fill="#94a3b8">{dist2.toFixed(0)}</tspan>
</>
) : (
<>
<tspan x={tx} dy="-8" fill="#94a3b8">{dist1.toFixed(0)}</tspan>
<tspan x={tx} dy="16" fill="#94a3b8">{dist2.toFixed(0)}</tspan>
</>
)}
</text>
</g>
);
})}
{isHovered && phantomPartition && !hoveredPartition && !dragging && (
<g className="pointer-events-none opacity-60">
{(() => {
const pMin = phantomPartition.min; const pMax = phantomPartition.max;
let fx1, fy1, fx2, fy2;
if (phantomPartition.axis === 'x') {
const px = cellX + (cellW * phantomPartition.offset);
fx1 = px; fx2 = px; fy1 = cellY + (cellH * pMin); fy2 = cellY + (cellH * pMax);
} else {
const py = cellY + (cellH * phantomPartition.offset);
fy1 = py; fy2 = py; fx1 = cellX + (cellW * pMin); fx2 = cellX + (cellW * pMax);
}
return <line x1={fx1} y1={fy1} x2={fx2} y2={fy2} stroke="#34d399" strokeWidth="3" strokeDasharray="6,6"/>;
})()}
</g>
)}
</g>
);
}
}
return elements;
};
return (
<div className="bg-slate-900 p-4 rounded-xl shadow-lg border border-slate-800 h-full flex flex-col relative overflow-hidden">
{/* HEADER */}
<div className="flex justify-between items-center mb-2 z-20 shrink-0">
<h2 className="text-lg font-bold flex items-center gap-2 text-primary">
<Grid size={20} /> 2. Редактор макета
</h2>
<div className="flex bg-slate-800 p-1 rounded-lg border border-slate-700">
<button onClick={() => { setMode('lines'); setSelectedPartitionId(null); }} className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${mode === 'lines' ? 'bg-primary text-white shadow' : 'text-gray-400 hover:text-gray-200'}`}><Move size={14}/> Границы</button>
<button onClick={() => setMode('cells')} className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${mode === 'cells' ? 'bg-primary text-white shadow' : 'text-gray-400 hover:text-gray-200'}`}><Grid size={14}/> Внутри ячеек</button>
</div>
<button onClick={() => onChange({ x: [], y: [], partitions: {} })} className="px-3 py-1.5 text-xs text-red-400 border border-slate-700 rounded hover:bg-slate-800 transition-colors flex items-center gap-1"><RotateCcw size={14} /> Сброс</button>
</div>
{/* INFO */}
<div className="bg-slate-800/50 rounded-lg px-3 py-2 mb-2 flex items-center gap-3 text-[11px] text-gray-300 border border-slate-700/50 shrink-0">
<MousePointer2 size={14} className="text-primary" />
{mode === 'lines' ? (
<div className="flex gap-3"><span><b className="text-blue-400">ЛКМ:</b> Линия</span><span><b className="text-red-400">ПКМ:</b> Удалить</span></div>
) : (
<div className="flex gap-3"><span><b className="text-green-400">ЛКМ в ячейке:</b> Стенка</span><span><b className="text-purple-400">Драг:</b> Двигать</span><span><b className="text-red-400">2КМ:</b> Удалить</span></div>
)}
</div>
{/* CANVAS */}
<div className="flex-1 bg-slate-800/30 rounded-lg flex flex-col items-center justify-center relative overflow-hidden border border-slate-700/50 min-h-0 w-full">
<div className="relative w-full h-full flex items-center justify-center p-4">
<div className="relative shadow-2xl bg-[#1e293b] border border-slate-600 rounded-sm overflow-hidden"
style={{ width: aspectRatio > 1 ? 'auto' : '100%', height: aspectRatio > 1 ? '100%' : 'auto', aspectRatio: `${1/aspectRatio}`, maxHeight: '100%', maxWidth: '100%', cursor: mode === 'lines' ? (dragging ? 'grabbing' : hoveredMainSplit ? 'col-resize' : 'crosshair') : (dragging ? 'grabbing' : hoveredPartition ? 'grab' : hoveredCell ? 'crosshair' : 'default') }}
>
<svg ref={svgRef} viewBox={`0 0 ${viewBoxW} ${viewBoxH}`} className="w-full h-full touch-none block"
preserveAspectRatio="none"
onMouseMove={handleMouseMove} onMouseDown={handleMouseDown} onMouseUp={() => setDragging(null)} onMouseLeave={() => setDragging(null)} onContextMenu={(e) => e.preventDefault()}
>
<defs><pattern id="grid" width="50" height="50" patternUnits="userSpaceOnUse"><path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(255,255,255,0.03)" strokeWidth="1"/></pattern></defs>
<rect width="100%" height="100%" fill="url(#grid)" />
{renderCellsAndPartitions()}
{/* Main Grid X */}
{safeX.map((x, i) => (
<g key={`x-${i}`} onMouseMove={(e) => { if(mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredMainSplit({axis: 'x', index: i}); setPhantomMainAxis(null); }}} onDoubleClick={(e) => { e.stopPropagation(); removeMainSplit('x', i); }}>
<line x1={x * viewBoxW} y1="0" x2={x * viewBoxW} y2="100%" stroke="transparent" strokeWidth="40" className={mode === 'lines' ? "cursor-col-resize" : ""} />
<line x1={x * viewBoxW} y1="0" x2={x * viewBoxW} y2="100%" stroke={hoveredMainSplit?.axis === 'x' && hoveredMainSplit.index === i ? "#f59e0b" : "#64748b"} strokeWidth={hoveredMainSplit?.axis === 'x' && hoveredMainSplit.index === i ? 6 : 4} className="pointer-events-none" />
</g>
))}
{/* Main Grid Y */}
{safeY.map((y, i) => (
<g key={`y-${i}`} onMouseMove={(e) => { if(mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredMainSplit({axis: 'y', index: i}); setPhantomMainAxis(null); }}} onDoubleClick={(e) => { e.stopPropagation(); removeMainSplit('y', i); }}>
<line x1="0" y1={y * viewBoxH} x2="100%" y2={y * viewBoxH} stroke="transparent" strokeWidth="40" className={mode === 'lines' ? "cursor-row-resize" : ""} />
<line x1="0" y1={y * viewBoxH} x2="100%" y2={y * viewBoxH} stroke={hoveredMainSplit?.axis === 'y' && hoveredMainSplit.index === i ? "#f59e0b" : "#64748b"} strokeWidth={hoveredMainSplit?.axis === 'y' && hoveredMainSplit.index === i ? 6 : 4} className="pointer-events-none" />
</g>
))}
{/* Phantoms */}
{mode === 'lines' && !hoveredMainSplit && !dragging && phantomMainAxis && (
<line x1={phantomMainAxis === 'x' ? mousePos.x * viewBoxW : 0} y1={phantomMainAxis === 'y' ? mousePos.y * viewBoxH : 0} x2={phantomMainAxis === 'x' ? mousePos.x * viewBoxW : '100%'} y2={phantomMainAxis === 'y' ? mousePos.y * viewBoxH : '100%'} stroke="#3b82f6" strokeWidth="4" strokeDasharray="8,8" className="pointer-events-none opacity-50"/>
)}
</svg>
</div>
</div>
{/* SIDEBAR */}
{mode === 'cells' && selectedData && (
<div className="absolute top-0 right-0 bottom-0 w-72 bg-slate-900 border-l border-slate-700 p-4 shadow-2xl flex flex-col z-30 animate-in slide-in-from-right duration-200">
<div className="flex justify-between items-center mb-6"><h3 className="text-sm font-bold text-white flex items-center gap-2">Настройки стенки</h3><button onClick={() => setSelectedPartitionId(null)} className="text-gray-400 hover:text-white"><X size={20}/></button></div>
<div className="bg-slate-800 p-4 rounded border border-slate-700 space-y-6">
<div>
<div className="flex justify-between text-xs text-gray-300 mb-2"><span>Высота</span> <span className="font-mono bg-slate-900 px-1.5 py-0.5 rounded text-xs">{selectedData.part.height} мм</span></div>
<input type="range" min="5" max={config.drawer.height || 100} step="1" value={selectedData.part.height} onChange={(e) => updatePartition(selectedData.key, selectedData.part.id, { height: parseFloat(e.target.value) })} className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-purple-500"/>
</div>
<div className="flex items-center justify-between">
<label htmlFor="rounded-check" className="text-xs text-gray-300 cursor-pointer select-none">Скруглить края</label>
<input type="checkbox" id="rounded-check" checked={selectedData.part.rounded} onChange={(e) => updatePartition(selectedData.key, selectedData.part.id, { rounded: e.target.checked })} className="w-4 h-4 rounded bg-slate-700 border-slate-600 text-purple-500 focus:ring-0 cursor-pointer"/>
</div>
<button onClick={() => removePartition(selectedData.key, selectedData.part.id)} className="w-full py-2 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/30 rounded text-xs flex items-center justify-center gap-2 transition-colors mt-4"><Trash2 size={14}/> Удалить</button>
</div>
<div className="mt-auto text-[10px] text-gray-500 text-center leading-relaxed">Выделите стенку для настройки.<br/>Двойной клик удаляет её.</div>
</div>
)}
</div>
</div>
);
};