|
|
|
@@ -1,4 +1,4 @@
|
|
|
|
|
import React from 'react';
|
|
|
|
|
import React, { useCallback } from 'react';
|
|
|
|
|
import type { RightSidebarProps, RightPanel } from '../types/ui.types';
|
|
|
|
|
import PropertiesPanel from './PropertiesPanel';
|
|
|
|
|
import LayerPanel from './LayerPanel';
|
|
|
|
@@ -6,8 +6,11 @@ import BlockLibrary from './BlockLibrary';
|
|
|
|
|
import BlockLibraryTree from './BlockLibraryTree';
|
|
|
|
|
import KICopilot from './KICopilot';
|
|
|
|
|
|
|
|
|
|
const MIN_RIGHTBAR_WIDTH = 200;
|
|
|
|
|
const MAX_RIGHTBAR_WIDTH = 500;
|
|
|
|
|
|
|
|
|
|
const tabs: Array<{ id: RightPanel; label: string; svg: React.ReactNode; badge?: number }> = [
|
|
|
|
|
{ id: 'tool', label: 'Werkzeug', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg> },
|
|
|
|
|
{ id: 'tool', label: 'Einstellungen', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg> },
|
|
|
|
|
{ id: 'layer', label: 'Layer', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg> },
|
|
|
|
|
{ id: 'library', label: 'Bibliothek', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><line x1="3" y1="12" x2="21" y2="12"/></svg> },
|
|
|
|
|
{ id: 'ki', label: 'KI', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>, badge: 2 },
|
|
|
|
@@ -19,8 +22,8 @@ const RightSidebar: React.FC<RightSidebarProps> = ({
|
|
|
|
|
activeLayerId, onSelectLayer, onAddLayer, onToggleLayer,
|
|
|
|
|
onDeleteLayer, onRenameLayer, onDuplicateLayer, onToggleLock,
|
|
|
|
|
onReorder, onAddSubLayer,
|
|
|
|
|
onElementsDeleted, onToggleElementVisible,
|
|
|
|
|
elements,
|
|
|
|
|
onElementsDeleted, onToggleElementVisible,
|
|
|
|
|
elements,
|
|
|
|
|
onRenameBlock, onDuplicateBlock, onDeleteBlock, onSvgImport, onSaveGroupAsBlock,
|
|
|
|
|
onBlockCategoryChange, onBlockSearch, onDragBlock,
|
|
|
|
|
kiMessages, kiSuggestions, onKISend, onKISuggestionClick, kiLoading, onUpdateElement,
|
|
|
|
@@ -33,76 +36,117 @@ const RightSidebar: React.FC<RightSidebarProps> = ({
|
|
|
|
|
token,
|
|
|
|
|
unit = 'mm',
|
|
|
|
|
scaleFactor = 1,
|
|
|
|
|
rightbarWidth = 300,
|
|
|
|
|
onWidthChange,
|
|
|
|
|
}) => {
|
|
|
|
|
const handleResizeMouseDown = useCallback((e: React.MouseEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const startClientX = e.clientX;
|
|
|
|
|
const startWidth = rightbarWidth;
|
|
|
|
|
|
|
|
|
|
const handleMouseMove = (ev: MouseEvent) => {
|
|
|
|
|
const delta = startClientX - ev.clientX;
|
|
|
|
|
const newWidth = Math.min(MAX_RIGHTBAR_WIDTH, Math.max(MIN_RIGHTBAR_WIDTH, startWidth + delta));
|
|
|
|
|
document.documentElement.style.setProperty('--rightbar-w', newWidth + 'px');
|
|
|
|
|
if (onWidthChange) {
|
|
|
|
|
onWidthChange(newWidth);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleMouseUp = () => {
|
|
|
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
|
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
|
|
|
document.body.style.cursor = '';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.body.style.cursor = 'col-resize';
|
|
|
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
|
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
|
|
|
}, [rightbarWidth, onWidthChange]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<aside className={`rightbar${className ? ' ' + className : ''}${collapsed ? ' rightbar-collapsed' : ''}`} aria-label="Eigenschaften-Panels">
|
|
|
|
|
<div className="rightbar-header">
|
|
|
|
|
<span className="rightbar-title">Eigenschaften</span>
|
|
|
|
|
<button className="rightbar-toggle" aria-label="Rechte Leiste ein-/ausklappen" onClick={collapsed ? onToggleCollapse : onCollapse} title="Rechte Leiste ein-/ausklappen">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rightbar-tabs" role="tablist">
|
|
|
|
|
{tabs.map((tab) => (
|
|
|
|
|
<button
|
|
|
|
|
key={tab.id}
|
|
|
|
|
className={`rightbar-tab${activePanel === tab.id ? ' active' : ''}`}
|
|
|
|
|
data-panel={tab.id}
|
|
|
|
|
role="tab"
|
|
|
|
|
aria-selected={activePanel === tab.id}
|
|
|
|
|
onClick={() => onPanelChange(tab.id)}
|
|
|
|
|
>
|
|
|
|
|
{tab.svg}
|
|
|
|
|
<span>{tab.label}</span>
|
|
|
|
|
{tab.badge !== undefined && <span className="rightbar-tab-badge">{tab.badge}</span>}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="rightbar-content">
|
|
|
|
|
<div className={`rightbar-panel${activePanel === 'tool' ? ' active' : ''}`} data-panel-content="tool">
|
|
|
|
|
{activePanel === 'tool' && <PropertiesPanel selectedElement={selectedElement} layers={layers} unit={unit} scaleFactor={scaleFactor} onDelete={selectedElement && onElementsDeleted ? () => onElementsDeleted([selectedElement.id]) : undefined} onUpdateProperty={(key, value) => {
|
|
|
|
|
if (!selectedElement || !onUpdateElement) return;
|
|
|
|
|
const updated = { ...selectedElement };
|
|
|
|
|
if (key === 'x' || key === 'y' || key === 'width' || key === 'height') {
|
|
|
|
|
const numVal = typeof value === 'string' ? parseFloat(value.replace(/[^0-9.\-]/g, '')) || 0 : value as number;
|
|
|
|
|
if (key === 'x') updated.x = numVal;
|
|
|
|
|
else if (key === 'y') updated.y = numVal;
|
|
|
|
|
else if (key === 'width') updated.width = numVal;
|
|
|
|
|
else if (key === 'height') updated.height = numVal;
|
|
|
|
|
} else if (key === 'layerId') {
|
|
|
|
|
updated.layerId = value as string;
|
|
|
|
|
} else if (key === 'rotation') {
|
|
|
|
|
const rotVal = typeof value === 'string' ? parseFloat(value.replace(/[^0-9.\-]/g, '')) || 0 : value as number;
|
|
|
|
|
updated.properties = { ...updated.properties, rotation: rotVal };
|
|
|
|
|
} else {
|
|
|
|
|
updated.properties = { ...updated.properties, [key]: value };
|
|
|
|
|
}
|
|
|
|
|
onUpdateElement(updated);
|
|
|
|
|
}} />}
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`rightbar-panel${activePanel === 'layer' ? ' active' : ''}`} data-panel-content="layer">
|
|
|
|
|
{activePanel === 'layer' && <LayerPanel layers={layers} elements={elements} onElementsDeleted={onElementsDeleted} onToggleElementVisible={onToggleElementVisible} activeLayerId={activeLayerId} onSelectLayer={onSelectLayer ?? (() => {})} onSelectElement={onSelectElement} selectedElementIds={selectedElementIds} groups={groups} onAddLayer={onAddLayer ?? (() => {})} onToggleLayer={onToggleLayer ?? (() => {})} onDeleteLayer={onDeleteLayer} onRenameLayer={onRenameLayer} onDuplicateLayer={onDuplicateLayer} onToggleLock={onToggleLock} onReorder={onReorder} onAddSubLayer={onAddSubLayer} />}
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`rightbar-panel${activePanel === 'library' ? ' active' : ''}`} data-panel-content="library">
|
|
|
|
|
{activePanel === 'library' && (
|
|
|
|
|
<>
|
|
|
|
|
<BlockLibrary blocks={blocks} category="Alle" onCategoryChange={onBlockCategoryChange ?? (() => {})} onSearch={onBlockSearch ?? (() => {})} onDragBlock={onDragBlock ?? (() => {})} onRenameBlock={onRenameBlock} onDuplicateBlock={onDuplicateBlock} onDeleteBlock={onDeleteBlock} onSvgImport={onSvgImport} onSaveGroupAsBlock={onSaveGroupAsBlock} />
|
|
|
|
|
{token && (
|
|
|
|
|
<BlockLibraryTree
|
|
|
|
|
token={token}
|
|
|
|
|
onBlockDragStart={(_blockId, _blockData) => {
|
|
|
|
|
// Global blocks use a different drag type — handled in CanvasArea drop
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<div className="rightbar-resize-handle" onMouseDown={handleResizeMouseDown} />
|
|
|
|
|
<button className="rightbar-edge-toggle" aria-label="Rechte Leiste ein-/ausklappen" onClick={collapsed ? onToggleCollapse : onCollapse} title="Rechte Leiste ein-/ausklappen">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
{!collapsed && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="rightbar-header">
|
|
|
|
|
<span className="rightbar-title">Eigenschaften</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rightbar-tabs" role="tablist">
|
|
|
|
|
{tabs.map((tab) => (
|
|
|
|
|
<button
|
|
|
|
|
key={tab.id}
|
|
|
|
|
className={`rightbar-tab${activePanel === tab.id ? ' active' : ''}`}
|
|
|
|
|
data-panel={tab.id}
|
|
|
|
|
role="tab"
|
|
|
|
|
aria-selected={activePanel === tab.id}
|
|
|
|
|
onClick={() => onPanelChange(tab.id)}
|
|
|
|
|
>
|
|
|
|
|
{tab.svg}
|
|
|
|
|
<span>{tab.label}</span>
|
|
|
|
|
{tab.badge !== undefined && <span className="rightbar-tab-badge">{tab.badge}</span>}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rightbar-content">
|
|
|
|
|
<div className={`rightbar-panel${activePanel === 'tool' ? ' active' : ''}`} data-panel-content="tool">
|
|
|
|
|
{activePanel === 'tool' && selectedElement && onUpdateElement && (
|
|
|
|
|
<PropertiesPanel selectedElement={selectedElement} layers={layers} unit={unit} scaleFactor={scaleFactor} onDelete={selectedElement && onElementsDeleted ? () => onElementsDeleted([selectedElement.id]) : undefined} onUpdateProperty={(key, value) => {
|
|
|
|
|
if (!selectedElement || !onUpdateElement) return;
|
|
|
|
|
const updated = { ...selectedElement };
|
|
|
|
|
if (key === 'x' || key === 'y' || key === 'width' || key === 'height') {
|
|
|
|
|
const numVal = typeof value === 'string' ? parseFloat(value.replace(/[^0-9.\-.]/g, '')) || 0 : value as number;
|
|
|
|
|
if (key === 'x') updated.x = numVal;
|
|
|
|
|
else if (key === 'y') updated.y = numVal;
|
|
|
|
|
else if (key === 'width') updated.width = numVal;
|
|
|
|
|
else if (key === 'height') updated.height = numVal;
|
|
|
|
|
} else if (key === 'layerId') {
|
|
|
|
|
updated.layerId = value as string;
|
|
|
|
|
} else if (key === 'rotation') {
|
|
|
|
|
const rotVal = typeof value === 'string' ? parseFloat(value.replace(/[^0-9.\-.]/g, '')) || 0 : value as number;
|
|
|
|
|
updated.properties = { ...updated.properties, rotation: rotVal };
|
|
|
|
|
} else {
|
|
|
|
|
updated.properties = { ...updated.properties, [key]: value };
|
|
|
|
|
}
|
|
|
|
|
onUpdateElement(updated);
|
|
|
|
|
}} />
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`rightbar-panel${activePanel === 'ki' ? ' active' : ''}`} data-panel-content="ki">
|
|
|
|
|
{activePanel === 'ki' && <KICopilot messages={kiMessages ?? []} suggestions={kiSuggestions ?? []} onSend={onKISend ?? (() => {})} onSuggestionClick={onKISuggestionClick ?? (() => {})} loading={kiLoading} />}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{activePanel === 'tool' && !selectedElement && (
|
|
|
|
|
<div className="tool-settings-panel">
|
|
|
|
|
<p style={{ padding: '12px', color: 'var(--color-text-muted)', fontSize: '13px' }}>
|
|
|
|
|
Kein Objekt ausgewählt. Wählen Sie ein Objekt auf der Zeichenfläche um dessen Eigenschaften zu bearbeiten.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`rightbar-panel${activePanel === 'layer' ? ' active' : ''}`} data-panel-content="layer">
|
|
|
|
|
{activePanel === 'layer' && <LayerPanel layers={layers} elements={elements} onElementsDeleted={onElementsDeleted} onToggleElementVisible={onToggleElementVisible} activeLayerId={activeLayerId} onSelectLayer={onSelectLayer ?? (() => {})} onSelectElement={onSelectElement} selectedElementIds={selectedElementIds} groups={groups} onAddLayer={onAddLayer ?? (() => {})} onToggleLayer={onToggleLayer ?? (() => {})} onDeleteLayer={onDeleteLayer} onRenameLayer={onRenameLayer} onDuplicateLayer={onDuplicateLayer} onToggleLock={onToggleLock} onReorder={onReorder} onAddSubLayer={onAddSubLayer} />}
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`rightbar-panel${activePanel === 'library' ? ' active' : ''}`} data-panel-content="library">
|
|
|
|
|
{activePanel === 'library' && (
|
|
|
|
|
<>
|
|
|
|
|
<BlockLibrary blocks={blocks} category="Alle" onCategoryChange={onBlockCategoryChange ?? (() => {})} onSearch={onBlockSearch ?? (() => {})} onDragBlock={onDragBlock ?? (() => {})} onRenameBlock={onRenameBlock} onDuplicateBlock={onDuplicateBlock} onDeleteBlock={onDeleteBlock} onSvgImport={onSvgImport} onSaveGroupAsBlock={onSaveGroupAsBlock} />
|
|
|
|
|
{token && (
|
|
|
|
|
<BlockLibraryTree
|
|
|
|
|
token={token}
|
|
|
|
|
onBlockDragStart={(_blockId, _blockData) => {
|
|
|
|
|
// Global blocks use a different drag type — handled in CanvasArea drop
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`rightbar-panel${activePanel === 'ki' ? ' active' : ''}`} data-panel-content="ki">
|
|
|
|
|
{activePanel === 'ki' && <KICopilot messages={kiMessages ?? []} suggestions={kiSuggestions ?? []} onSend={onKISend ?? (() => {})} onSuggestionClick={onKISuggestionClick ?? (() => {})} loading={kiLoading} />}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</aside>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|