feat: click-drag drawing, layer tree, settings modal, UI improvements

- interaction: click-drag drawing for 2-point tools (line, rect, circle, dimension, leader)
- interaction: preview rendering during drag with setPreviewElement
- interaction: handleDrawDown only sets phase=drawing on first point
- CanvasArea: sync activeLayerId to LayerManager
- CanvasArea: callback refresh useEffect prevents stale closures
- App.tsx: functional setElements updates for handleElementCreated/Deleted/Modified
- LayerPanel: tree structure with sub-layers (onReorder, onAddSubLayer)
- TreeView: drag-and-drop reordering support
- RightSidebar: onReorder, onAddSubLayer, onUpdateElement props
- PropertiesPanel: onUpdateElement prop
- SettingsModal: personal/password/language/theme/users/plugins/AI tabs
- styles.css: extensive UI styling additions
- types: updated RightSidebarProps, CanvasAreaProps, LayerPanelProps
This commit is contained in:
2026-06-27 14:12:37 +02:00
parent 39422a4348
commit 4303076ce4
17 changed files with 929 additions and 144 deletions
+63 -1
View File
@@ -6,6 +6,7 @@ interface TreeViewProps {
selectedId?: string | null;
onSelect: (id: string) => void;
onToggle?: (id: string) => void;
onReorder?: (draggedId: string, targetId: string, position: 'before' | 'after' | 'inside') => void;
renderIcon?: (node: TreeNode) => React.ReactNode;
renderActions?: (node: TreeNode) => React.ReactNode;
renderDetail?: (node: TreeNode) => React.ReactNode;
@@ -17,12 +18,16 @@ const TreeView: React.FC<TreeViewProps> = ({
selectedId,
onSelect,
onToggle,
onReorder,
renderIcon,
renderActions,
renderDetail,
draggable = false,
}) => {
const [expandedSet, setExpandedSet] = useState<Set<string>>(new Set());
const [draggedId, setDraggedId] = useState<string | null>(null);
const [dragOverId, setDragOverId] = useState<string | null>(null);
const [dragPosition, setDragPosition] = useState<'before' | 'after' | 'inside'>('before');
const toggleExpand = (id: string) => {
setExpandedSet((prev) => {
@@ -34,18 +39,75 @@ const TreeView: React.FC<TreeViewProps> = ({
onToggle?.(id);
};
const handleDragStart = (e: React.DragEvent, id: string) => {
if (!draggable || !onReorder) return;
setDraggedId(id);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', id);
};
const handleDragOver = (e: React.DragEvent, id: string) => {
if (!draggable || !onReorder || !draggedId || draggedId === id) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const rect = e.currentTarget.getBoundingClientRect();
const offset = e.clientY - rect.top;
const third = rect.height / 3;
if (offset < third) {
setDragPosition('before');
} else if (offset > third * 2) {
setDragPosition('after');
} else {
setDragPosition('inside');
}
setDragOverId(id);
};
const handleDragLeave = (_e: React.DragEvent, id: string) => {
if (!draggable || !onReorder) return;
if (dragOverId === id) {
setDragOverId(null);
}
};
const handleDrop = (e: React.DragEvent, id: string) => {
if (!draggable || !onReorder || !draggedId || draggedId === id) return;
e.preventDefault();
e.stopPropagation();
onReorder(draggedId, id, dragPosition);
setDraggedId(null);
setDragOverId(null);
};
const handleDragEnd = () => {
setDraggedId(null);
setDragOverId(null);
};
const renderNode = (node: TreeNode, level: number): React.ReactNode => {
const hasChildren = node.children && node.children.length > 0;
const isExpanded = expandedSet.has(node.id) || node.expanded;
const isSelected = selectedId === node.id;
const isDragOver = dragOverId === node.id;
const isDragging = draggedId === node.id;
let dragClass = '';
if (isDragOver && dragPosition === 'before') dragClass = ' tree-drag-before';
if (isDragOver && dragPosition === 'after') dragClass = ' tree-drag-after';
if (isDragOver && dragPosition === 'inside') dragClass = ' tree-drag-inside';
return (
<React.Fragment key={node.id}>
<div
className={`tree-node${isSelected ? ' active' : ''}`}
className={`tree-node${isSelected ? ' active' : ''}${dragClass}${isDragging ? ' tree-dragging' : ''}`}
style={{ paddingLeft: `${level * 16 + 8}px` }}
onClick={() => onSelect(node.id)}
draggable={draggable}
onDragStart={(e) => handleDragStart(e, node.id)}
onDragOver={(e) => handleDragOver(e, node.id)}
onDragLeave={(e) => handleDragLeave(e, node.id)}
onDrop={(e) => handleDrop(e, node.id)}
onDragEnd={handleDragEnd}
>
{hasChildren && (
<button