Files
BoxGenerator/src/components/LayoutStep.tsx
Халимов Рустам 454e6cf822 9
2026-01-11 15:14:29 +03:00

502 lines
26 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 } from 'lucide-react';
interface Props {
config: AppConfig;
splits: LayoutSplits;
onChange: (splits: LayoutSplits) => void;
}
type EditMode = 'lines' | 'cells';
type Axis = 'x' | 'y';
type Limits = { min: number; max: number };
type LimitMap = Record<string, Limits>;
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);
const [phantomMainAxis, setPhantomMainAxis] = useState<Axis | null>(null);
const [hoveredMainSplit, setHoveredMainSplit] = useState<{ axis: Axis; index: number } | null>(null);
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);
// -- 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 wallThick = config.wallThickness || 1.2;
const aspectRatio = drawerD / drawerW;
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]);
// -- HELPER: Get Selected --
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();
// --- SOLVER (Копия из geometryGenerator) ---
const solveWallLimits = (parts: Partition[]): LimitMap => {
const limits: LimitMap = {};
parts.forEach(p => { limits[p.id] = { min: 0, max: 1 }; });
for (let pass = 0; pass < 4; pass++) {
parts.forEach(target => {
let newMin = 0;
let newMax = 1;
const center = target.offset;
parts.forEach(obstacle => {
if (target.id === obstacle.id || target.axis === obstacle.axis) return;
const obsMin = limits[obstacle.id].min;
const obsMax = limits[obstacle.id].max;
// Используем >= и <= для надежности
if (target.offset >= obsMin - 0.001 && target.offset <= obsMax + 0.001) {
if (obstacle.offset < center) newMin = Math.max(newMin, obstacle.offset);
else if (obstacle.offset > center) newMax = Math.min(newMax, obstacle.offset);
}
});
limits[target.id] = { min: newMin, max: newMax };
});
}
return limits;
};
// --- RAYCASTING (Исправленный поиск свободного места) ---
const getCursorBox = (lx: number, ly: number, parts: Partition[], limitMap: LimitMap) => {
let minX = 0, maxX = 1;
let minY = 0, maxY = 1;
parts.forEach(p => {
const { min: pMin, max: pMax } = limitMap[p.id];
if (pMax - pMin < 0.001) return;
// Используем те же допуски, что и в Solver
const EPSILON = 0.001;
if (p.axis === 'x') {
// Вертикальная преграда (X)
// Проверяем, перекрывает ли она Y курсора
if (ly >= pMin - EPSILON && ly <= pMax + EPSILON) {
if (p.offset < lx) minX = Math.max(minX, p.offset);
if (p.offset > lx) maxX = Math.min(maxX, p.offset);
}
} else {
// Горизонтальная преграда (Y)
// Проверяем, перекрывает ли она X курсора
if (lx >= pMin - EPSILON && lx <= pMax + EPSILON) {
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 };
};
// Поиск соседей для отображения размеров
const getNeighborOffsets = (offset: number, crossPos: number, axis: Axis, parts: Partition[], limitMap: LimitMap) => {
let min = 0;
let max = 1;
const EPSILON = 0.001;
parts.forEach(p => {
if (p.axis === axis) {
const { min: pMin, max: pMax } = limitMap[p.id];
if (crossPos >= pMin - EPSILON && crossPos <= pMax + EPSILON) {
if (p.offset < offset) min = Math.max(min, p.offset);
if (p.offset > offset) max = Math.min(max, p.offset);
}
}
});
return { min, max };
};
// -- 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 HANDLERS --
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;
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) setPhantomMainAxis(Math.min(nx, 1-nx) < Math.min(ny, 1-ny) ? 'y' : 'x');
else setPhantomMainAxis(null);
}
} else {
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 limitMap = solveWallLimits(parts);
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 { min, max } = limitMap[p.id];
if (p.axis === 'x') {
if (ly >= min && ly <= max && Math.abs(lx - p.offset) < SNAP * (aspectRatio > 1 ? 1 : aspectRatio)) found = p;
} else {
if (lx >= min && lx <= max && Math.abs(ly - p.offset) < SNAP * (aspectRatio < 1 ? 1 : 1/aspectRatio)) found = p;
}
}
if (found) {
setHoveredPartition({ id: found.id, cellKey: key });
} else {
// --- ВАЖНО: Получаем корректные границы с учетом Solver ---
const box = getCursorBox(lx, ly, parts, limitMap);
const distL = lx - box.minX; const distR = box.maxX - lx;
const distT = ly - box.minY; const distB = box.maxY - ly;
// Выбираем ось перпендикулярно ближайшей стороне
const minD = Math.min(distL, distR, distT, distB);
const newAxis = (minD === distL || minD === distR) ? 'x' : 'y';
const width = box.maxX - box.minX;
const height = box.maxY - box.minY;
if ((newAxis === 'y' && height > 0.05) || (newAxis === 'x' && width > 0.05)) {
const offset = newAxis === 'x' ? lx : ly;
const min = newAxis === 'x' ? box.minY : box.minX;
const max = newAxis === 'x' ? box.maxY : box.maxX;
setPhantomPartition({ axis: newAxis, 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 isAnySelected = parts.some(p => p.id === selectedPartitionId);
const realW = (x2 - x1) * drawerW;
const realD = (y2 - y1) * drawerD;
// ВАЖНО: Используем solver для отрисовки
const limitMap = solveWallLimits(parts);
// Label
if (parts.length === 0) {
const labelX = cellX + cellW / 2;
const labelY = cellY + cellH / 2;
const textW = Math.max(0, realW - wallThick).toFixed(0);
const textD = Math.max(0, realD - wallThick).toFixed(0);
if (cellH > 40 && cellW > 60) {
elements.push(
<text key={`label-${key}`} x={labelX} y={labelY} textAnchor="middle" dominantBaseline="middle" className="fill-slate-300 font-mono text-[16px] font-bold pointer-events-none select-none opacity-80" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.8)' }}>
{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"/>}
{isAnySelected && mode === 'cells' && <rect x={cellX} y={cellY} width={cellW} height={cellH} fill="transparent" stroke="#a855f7" strokeWidth="2" className="pointer-events-none opacity-50"/>}
{parts.map(p => {
const { min, max } = limitMap[p.id];
if (max - min < 0.001) return null;
let lx1, ly1, lx2, ly2;
let dist1 = 0, dist2 = 0;
let midX, midY;
const isVertical = p.axis === 'x';
if (isVertical) {
const px = cellX + (cellW * p.offset);
lx1 = px; lx2 = px;
ly1 = cellY + (cellH * min); ly2 = cellY + (cellH * max);
const cy = (min + max) / 2;
const box = getCursorBox(p.offset, cy, parts, limitMap);
dist1 = Math.abs((p.offset - box.minX) * realW) - wallThick;
dist2 = Math.abs((box.maxX - p.offset) * realW) - wallThick;
midX = px; midY = (ly1 + ly2) / 2;
} else {
const py = cellY + (cellH * p.offset);
ly1 = py; ly2 = py;
lx1 = cellX + (cellW * min); lx2 = cellX + (cellW * max);
const cx = (min + max) / 2;
const box = getCursorBox(cx, p.offset, parts, limitMap);
dist1 = Math.abs((p.offset - box.minY) * realD) - wallThick;
dist2 = Math.abs((box.maxY - p.offset) * realD) - wallThick;
midX = (lx1 + lx2) / 2; midY = py;
}
const isSel = selectedPartitionId === p.id;
const isHov = hoveredPartition?.id === p.id;
const textOffset = 12;
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>
<g className="pointer-events-none select-none font-mono text-[14px] font-bold fill-white" style={{ textShadow: '0px 0px 3px #000' }}>
{isVertical ? (
<>
<text x={midX - textOffset} y={midY} textAnchor="end" dominantBaseline="middle">{Math.max(0, dist1).toFixed(0)}</text>
<text x={midX + textOffset} y={midY} textAnchor="start" dominantBaseline="middle">{Math.max(0, dist2).toFixed(0)}</text>
</>
) : (
<>
<text x={midX} y={midY - textOffset} textAnchor="middle" dominantBaseline="auto">{Math.max(0, dist1).toFixed(0)}</text>
<text x={midX} y={midY + textOffset * 2} textAnchor="middle" dominantBaseline="auto">{Math.max(0, dist2).toFixed(0)}</text>
</>
)}
</g>
</g>
);
})}
{isHovered && phantomPartition && !hoveredPartition && !dragging && (
<g className="pointer-events-none opacity-60">
{(() => {
const { min, max } = phantomPartition;
let fx1, fy1, fx2, fy2;
if (phantomPartition.axis === 'x') {
const px = cellX + (cellW * phantomPartition.offset);
fx1 = px; fx2 = px; fy1 = cellY + (cellH * min); fy2 = cellY + (cellH * max);
} else {
const py = cellY + (cellH * phantomPartition.offset);
fy1 = py; fy2 = py; fx1 = cellX + (cellW * min); fx2 = cellX + (cellW * max);
}
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">
<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>
<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>
<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" 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()}
{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>
))}
{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>
))}
{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>
{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>
)}
</div>
</div>
);
};