feat: logo link to dashboard + ribbon tab Zeichenfläche + settings System tab + ruler
This commit is contained in:
@@ -105,6 +105,11 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
const [gridSize, setGridSize] = useState<number>(20);
|
const [gridSize, setGridSize] = useState<number>(20);
|
||||||
const [scaleFactor, setScaleFactor] = useState<number>(1);
|
const [scaleFactor, setScaleFactor] = useState<number>(1);
|
||||||
|
|
||||||
|
// Canvas appearance
|
||||||
|
const [canvasBgColor, setCanvasBgColor] = useState<string>('#1e1e2e');
|
||||||
|
const [gridColor, setGridColor] = useState<string>('#3a3a4a');
|
||||||
|
const [rulerEnabled, setRulerEnabled] = useState<boolean>(true);
|
||||||
|
|
||||||
// Right sidebar
|
// Right sidebar
|
||||||
const [activeRightPanel, setActiveRightPanel] = useState<RightPanel>('tool');
|
const [activeRightPanel, setActiveRightPanel] = useState<RightPanel>('tool');
|
||||||
const [selectedElement, setSelectedElement] = useState<CADElement | null>(null);
|
const [selectedElement, setSelectedElement] = useState<CADElement | null>(null);
|
||||||
@@ -1527,11 +1532,18 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
onOpenRightDrawer={() => setMobileRightOpen(true)}
|
onOpenRightDrawer={() => setMobileRightOpen(true)}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
onUnitChange={handleUnitChange}
|
onUnitChange={handleUnitChange}
|
||||||
|
onNavigateBack={onNavigateBack}
|
||||||
/>
|
/>
|
||||||
<RibbonBar
|
<RibbonBar
|
||||||
activeTab={activeRibbonTab}
|
activeTab={activeRibbonTab}
|
||||||
onTabChange={setActiveRibbonTab}
|
onTabChange={setActiveRibbonTab}
|
||||||
onAction={handleRibbonAction}
|
onAction={handleRibbonAction}
|
||||||
|
canvasBgColor={canvasBgColor}
|
||||||
|
gridColor={gridColor}
|
||||||
|
rulerEnabled={rulerEnabled}
|
||||||
|
onCanvasBgColorChange={setCanvasBgColor}
|
||||||
|
onGridColorChange={setGridColor}
|
||||||
|
onToggleRuler={() => setRulerEnabled((p) => !p)}
|
||||||
/>
|
/>
|
||||||
<div className={`app-body${leftSidebarCollapsed ? ' left-collapsed' : ''}${rightSidebarCollapsed ? ' right-collapsed' : ''}`}>
|
<div className={`app-body${leftSidebarCollapsed ? ' left-collapsed' : ''}${rightSidebarCollapsed ? ' right-collapsed' : ''}`}>
|
||||||
<LeftSidebar
|
<LeftSidebar
|
||||||
@@ -1593,6 +1605,9 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
unit={unit}
|
unit={unit}
|
||||||
gridSize={gridSize}
|
gridSize={gridSize}
|
||||||
scaleFactor={scaleFactor}
|
scaleFactor={scaleFactor}
|
||||||
|
canvasBgColor={canvasBgColor}
|
||||||
|
gridColor={gridColor}
|
||||||
|
rulerEnabled={rulerEnabled}
|
||||||
/>
|
/>
|
||||||
<RightSidebar
|
<RightSidebar
|
||||||
activePanel={activeRightPanel}
|
activePanel={activeRightPanel}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export interface RenderOptions {
|
|||||||
unit?: 'mm' | 'cm' | 'm';
|
unit?: 'mm' | 'cm' | 'm';
|
||||||
scaleFactor?: number;
|
scaleFactor?: number;
|
||||||
showGridLabels?: boolean;
|
showGridLabels?: boolean;
|
||||||
|
canvasBgColor?: string;
|
||||||
|
gridColor?: string;
|
||||||
|
rulerEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectionState {
|
export interface SelectionState {
|
||||||
@@ -73,6 +76,9 @@ export class RenderEngine {
|
|||||||
backgroundOffsetY: 0,
|
backgroundOffsetY: 0,
|
||||||
backgroundRotation: 0,
|
backgroundRotation: 0,
|
||||||
backgroundOpacity: 0.5,
|
backgroundOpacity: 0.5,
|
||||||
|
canvasBgColor: '#1e1e2e',
|
||||||
|
gridColor: '#3a3a4a',
|
||||||
|
rulerEnabled: true,
|
||||||
};
|
};
|
||||||
this.selection = {
|
this.selection = {
|
||||||
selectedIds: new Set(),
|
selectedIds: new Set(),
|
||||||
@@ -145,12 +151,12 @@ export class RenderEngine {
|
|||||||
const w = this.canvas.width / this.dpr;
|
const w = this.canvas.width / this.dpr;
|
||||||
const h = this.canvas.height / this.dpr;
|
const h = this.canvas.height / this.dpr;
|
||||||
this.ctx.save();
|
this.ctx.save();
|
||||||
this.ctx.fillStyle = '#1e1e2e';
|
this.ctx.fillStyle = this.options.canvasBgColor || '#1e1e2e';
|
||||||
this.ctx.fillRect(0, 0, w, h);
|
this.ctx.fillRect(0, 0, w, h);
|
||||||
|
|
||||||
if (this.options.showGrid) this.drawGrid(w, h);
|
if (this.options.showGrid) this.drawGrid(w, h);
|
||||||
if (this.options.backgroundSrc) this.drawBackground();
|
if (this.options.backgroundSrc) this.drawBackground();
|
||||||
|
this.drawRuler(w, h);
|
||||||
const viewport = this.zoomPan.getViewport();
|
const viewport = this.zoomPan.getViewport();
|
||||||
const visibleElements = this.spatialIndex.search({
|
const visibleElements = this.spatialIndex.search({
|
||||||
minX: viewport.minX, minY: viewport.minY,
|
minX: viewport.minX, minY: viewport.minY,
|
||||||
@@ -190,7 +196,7 @@ export class RenderEngine {
|
|||||||
const offsetX = this.zoomPan.getTransform().e % gridSize;
|
const offsetX = this.zoomPan.getTransform().e % gridSize;
|
||||||
const offsetY = this.zoomPan.getTransform().f % gridSize;
|
const offsetY = this.zoomPan.getTransform().f % gridSize;
|
||||||
|
|
||||||
this.ctx.strokeStyle = '#2a2a3e';
|
this.ctx.strokeStyle = this.options.gridColor || '#3a3a4a';
|
||||||
this.ctx.lineWidth = 1;
|
this.ctx.lineWidth = 1;
|
||||||
this.ctx.beginPath();
|
this.ctx.beginPath();
|
||||||
for (let x = offsetX; x < w; x += gridSize) {
|
for (let x = offsetX; x < w; x += gridSize) {
|
||||||
@@ -208,7 +214,8 @@ export class RenderEngine {
|
|||||||
if (majorSize >= 20) {
|
if (majorSize >= 20) {
|
||||||
const majOffX = this.zoomPan.getTransform().e % majorSize;
|
const majOffX = this.zoomPan.getTransform().e % majorSize;
|
||||||
const majOffY = this.zoomPan.getTransform().f % majorSize;
|
const majOffY = this.zoomPan.getTransform().f % majorSize;
|
||||||
this.ctx.strokeStyle = '#33334a';
|
// Major lines use a brighter variant of gridColor
|
||||||
|
this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 15);
|
||||||
this.ctx.beginPath();
|
this.ctx.beginPath();
|
||||||
for (let x = majOffX; x < w; x += majorSize) {
|
for (let x = majOffX; x < w; x += majorSize) {
|
||||||
this.ctx.moveTo(x, 0);
|
this.ctx.moveTo(x, 0);
|
||||||
@@ -272,6 +279,105 @@ export class RenderEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private lightenColor(hex: string, percent: number): string {
|
||||||
|
const num = parseInt(hex.replace('#', ''), 16);
|
||||||
|
if (isNaN(num)) return hex;
|
||||||
|
const r = Math.min(255, (num >> 16) + Math.round(255 * percent / 100));
|
||||||
|
const g = Math.min(255, ((num >> 8) & 0x00FF) + Math.round(255 * percent / 100));
|
||||||
|
const b = Math.min(255, (num & 0x0000FF) + Math.round(255 * percent / 100));
|
||||||
|
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawRuler(w: number, h: number): void {
|
||||||
|
if (!this.options.rulerEnabled) return;
|
||||||
|
const scale = this.zoomPan.getScale();
|
||||||
|
const transform = this.zoomPan.getTransform();
|
||||||
|
const unit = this.options.unit || 'mm';
|
||||||
|
const scaleFactor = this.options.scaleFactor || 1;
|
||||||
|
const rulerSize = 20;
|
||||||
|
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.fillStyle = this.options.canvasBgColor || '#1e1e2e';
|
||||||
|
this.ctx.fillRect(0, 0, w, rulerSize);
|
||||||
|
this.ctx.fillRect(0, 0, rulerSize, h);
|
||||||
|
|
||||||
|
this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 30);
|
||||||
|
this.ctx.lineWidth = 1;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(0, rulerSize);
|
||||||
|
this.ctx.lineTo(w, rulerSize);
|
||||||
|
this.ctx.moveTo(rulerSize, 0);
|
||||||
|
this.ctx.lineTo(rulerSize, h);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
// Determine tick spacing in world units (aim for ~50px between major ticks)
|
||||||
|
const targetPx = 50;
|
||||||
|
const worldPerPx = 1 / scale;
|
||||||
|
const rawTickSpacing = targetPx * worldPerPx;
|
||||||
|
const niceSpacings = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000];
|
||||||
|
let tickSpacing = niceSpacings[0];
|
||||||
|
for (const ns of niceSpacings) {
|
||||||
|
if (ns >= rawTickSpacing) { tickSpacing = ns; break; }
|
||||||
|
tickSpacing = ns;
|
||||||
|
}
|
||||||
|
const tickPx = tickSpacing * scale;
|
||||||
|
|
||||||
|
this.ctx.fillStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 50);
|
||||||
|
this.ctx.font = '9px sans-serif';
|
||||||
|
this.ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
|
// X-axis ruler (top)
|
||||||
|
this.ctx.textAlign = 'center';
|
||||||
|
const xStartWorld = Math.floor((-transform.e / scale) / tickSpacing) * tickSpacing;
|
||||||
|
for (let i = 0; ; i++) {
|
||||||
|
const screenX = transform.e + (xStartWorld + i * tickSpacing) * scale;
|
||||||
|
if (screenX < rulerSize) continue;
|
||||||
|
if (screenX > w) break;
|
||||||
|
// Tick mark
|
||||||
|
this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 30);
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(screenX, rulerSize - 6);
|
||||||
|
this.ctx.lineTo(screenX, rulerSize);
|
||||||
|
this.ctx.stroke();
|
||||||
|
// Label
|
||||||
|
const worldVal = (xStartWorld + i * tickSpacing) * scaleFactor;
|
||||||
|
let label: string;
|
||||||
|
switch (unit) {
|
||||||
|
case 'mm': label = `${worldVal.toFixed(0)}`; break;
|
||||||
|
case 'cm': label = `${(worldVal / 10).toFixed(0)}`; break;
|
||||||
|
case 'm': label = `${(worldVal / 1000).toFixed(2)}`; break;
|
||||||
|
}
|
||||||
|
this.ctx.fillText(label, screenX, rulerSize / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y-axis ruler (left)
|
||||||
|
this.ctx.textAlign = 'right';
|
||||||
|
this.ctx.textBaseline = 'middle';
|
||||||
|
const yStartWorld = Math.floor((-transform.f / scale) / tickSpacing) * tickSpacing;
|
||||||
|
for (let i = 0; ; i++) {
|
||||||
|
const screenY = transform.f + (yStartWorld + i * tickSpacing) * scale;
|
||||||
|
if (screenY < rulerSize) continue;
|
||||||
|
if (screenY > h) break;
|
||||||
|
this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 30);
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(rulerSize - 6, screenY);
|
||||||
|
this.ctx.lineTo(rulerSize, screenY);
|
||||||
|
this.ctx.stroke();
|
||||||
|
const worldVal = (yStartWorld + i * tickSpacing) * scaleFactor;
|
||||||
|
let label: string;
|
||||||
|
switch (unit) {
|
||||||
|
case 'mm': label = `${worldVal.toFixed(0)}`; break;
|
||||||
|
case 'cm': label = `${(worldVal / 10).toFixed(0)}`; break;
|
||||||
|
case 'm': label = `${(worldVal / 1000).toFixed(2)}`; break;
|
||||||
|
}
|
||||||
|
this.ctx.fillText(label, rulerSize - 2, screenY);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.restore();
|
||||||
|
this.ctx.textAlign = 'start';
|
||||||
|
this.ctx.textBaseline = 'alphabetic';
|
||||||
|
}
|
||||||
|
|
||||||
private drawBackground(): void {
|
private drawBackground(): void {
|
||||||
// Background image rendering with transform
|
// Background image rendering with transform
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
onToggleGrid, onToggleOrtho, onToggleSnap, onZoomIn, onZoomOut, onZoomFit, onTextEdit, onCommandTrigger, blocks, onBlockDrop, onSelectionChange, externalSelectionIds, selectedTemplate, bgConfig, remoteCursors, zoomCommand,
|
onToggleGrid, onToggleOrtho, onToggleSnap, onZoomIn, onZoomOut, onZoomFit, onTextEdit, onCommandTrigger, blocks, onBlockDrop, onSelectionChange, externalSelectionIds, selectedTemplate, bgConfig, remoteCursors, zoomCommand,
|
||||||
groups,
|
groups,
|
||||||
unit = 'mm', gridSize = 20, scaleFactor = 1,
|
unit = 'mm', gridSize = 20, scaleFactor = 1,
|
||||||
|
canvasBgColor = '#1e1e2e', gridColor = '#3a3a4a', rulerEnabled = true,
|
||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const zoomPanRef = useRef<ZoomPanController | null>(null);
|
const zoomPanRef = useRef<ZoomPanController | null>(null);
|
||||||
@@ -213,6 +214,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
renderEngine.setOptions({ showSnapPoints: snapEnabled });
|
renderEngine.setOptions({ showSnapPoints: snapEnabled });
|
||||||
renderEngine.setOptions({ showOrtho: orthoEnabled });
|
renderEngine.setOptions({ showOrtho: orthoEnabled });
|
||||||
renderEngine.setOptions({ gridSize, unit, scaleFactor, showGridLabels: true });
|
renderEngine.setOptions({ gridSize, unit, scaleFactor, showGridLabels: true });
|
||||||
|
renderEngine.setOptions({ canvasBgColor, gridColor, rulerEnabled });
|
||||||
interaction.setSnapEnabled(snapEnabled);
|
interaction.setSnapEnabled(snapEnabled);
|
||||||
interaction.setOrthoEnabled(orthoEnabled);
|
interaction.setOrthoEnabled(orthoEnabled);
|
||||||
interaction.setPolarEnabled(polarEnabled);
|
interaction.setPolarEnabled(polarEnabled);
|
||||||
@@ -220,7 +222,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
interaction.setGridSize(gridSize);
|
interaction.setGridSize(gridSize);
|
||||||
renderEngine.render();
|
renderEngine.render();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [gridEnabled, snapEnabled, orthoEnabled, polarEnabled, unit, gridSize, scaleFactor]);
|
}, [gridEnabled, snapEnabled, orthoEnabled, polarEnabled, unit, gridSize, scaleFactor, canvasBgColor, gridColor, rulerEnabled]);
|
||||||
|
|
||||||
// Sync active tool
|
// Sync active tool
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ const tabs: Array<{ id: RibbonTab; label: string; svg: React.ReactNode }> = [
|
|||||||
{ id: 'insert', label: 'Einfügen', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg> },
|
{ id: 'insert', label: 'Einfügen', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg> },
|
||||||
{ id: 'format', label: 'Format', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/></svg> },
|
{ id: 'format', label: 'Format', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/></svg> },
|
||||||
{ id: 'view', label: 'Ansicht', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg> },
|
{ id: 'view', label: 'Ansicht', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg> },
|
||||||
|
{ id: 'canvas', label: 'Zeichenfläche', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 3v18"/></svg> },
|
||||||
{ id: 'tools', label: 'Extras', 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: 'tools', label: 'Extras', 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: '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> },
|
{ 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> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const RibbonBar: React.FC<RibbonBarProps> = ({ activeTab, onTabChange, onAction }) => {
|
const RibbonBar: React.FC<RibbonBarProps> = ({
|
||||||
|
activeTab, onTabChange, onAction,
|
||||||
|
canvasBgColor = '#1e1e2e', gridColor = '#3a3a4a', rulerEnabled = true,
|
||||||
|
onCanvasBgColorChange, onGridColorChange, onToggleRuler,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<nav className="ribbon" role="navigation" aria-label="Hauptwerkzeuge">
|
<nav className="ribbon" role="navigation" aria-label="Hauptwerkzeuge">
|
||||||
<div className="ribbon-tabs" role="tablist">
|
<div className="ribbon-tabs" role="tablist">
|
||||||
@@ -215,6 +220,56 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ activeTab, onTabChange, onAction
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ===== CANVAS TAB ===== */}
|
||||||
|
{activeTab === 'canvas' && (
|
||||||
|
<>
|
||||||
|
<div className="ribbon-group">
|
||||||
|
<div className="ribbon-group-btns">
|
||||||
|
<div className="ribbon-color-control" title="Hintergrundfarbe der Zeichenfläche">
|
||||||
|
<label className="ribbon-color-label">Hintergrund</label>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={canvasBgColor}
|
||||||
|
onChange={(e) => onCanvasBgColorChange?.(e.target.value)}
|
||||||
|
style={{ width: '32px', height: '32px', border: '1px solid var(--color-border)', borderRadius: '4px', cursor: 'pointer', padding: 0, background: 'none' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ribbon-color-control" title="Gitterfarbe">
|
||||||
|
<label className="ribbon-color-label">Gitterfarbe</label>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={gridColor}
|
||||||
|
onChange={(e) => onGridColorChange?.(e.target.value)}
|
||||||
|
style={{ width: '32px', height: '32px', border: '1px solid var(--color-border)', borderRadius: '4px', cursor: 'pointer', padding: 0, background: 'none' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ribbon-group-label">Farben</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ribbon-divider"></div>
|
||||||
|
|
||||||
|
<div className="ribbon-group">
|
||||||
|
<div className="ribbon-group-btns">
|
||||||
|
<button
|
||||||
|
className={`ribbon-btn${rulerEnabled ? ' active' : ''}`}
|
||||||
|
title="Lineal ein/aus"
|
||||||
|
aria-pressed={rulerEnabled}
|
||||||
|
onClick={() => onToggleRuler?.()}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3h18v18H3z"/><path d="M3 8h3"/><path d="M3 13h3"/><path d="M3 18h3"/><path d="M8 3v3"/><path d="M13 3v3"/><path d="M18 3v3"/></svg>
|
||||||
|
<span>Lineal</span>
|
||||||
|
</button>
|
||||||
|
<button className="ribbon-btn" title="Raster ein/aus (F7)" onClick={() => onAction('grid')}>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>
|
||||||
|
<span>Raster</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="ribbon-group-label">Anzeige</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ===== TOOLS TAB ===== */}
|
{/* ===== TOOLS TAB ===== */}
|
||||||
{activeTab === 'tools' && (
|
{activeTab === 'tools' && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface SettingsModalProps {
|
|||||||
onScaleFactorChange?: (factor: number) => void;
|
onScaleFactorChange?: (factor: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingsTab = 'personal' | 'password' | 'language' | 'theme' | 'units' | 'users' | 'plugins' | 'ai';
|
type SettingsTab = 'personal' | 'password' | 'system' | 'theme' | 'units' | 'users' | 'plugins' | 'ai';
|
||||||
|
|
||||||
const SettingsModal: React.FC<SettingsModalProps> = ({
|
const SettingsModal: React.FC<SettingsModalProps> = ({
|
||||||
open, onClose,
|
open, onClose,
|
||||||
@@ -57,7 +57,7 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
|
|||||||
const tabs: Array<{ id: SettingsTab; label: string; adminOnly?: boolean }> = [
|
const tabs: Array<{ id: SettingsTab; label: string; adminOnly?: boolean }> = [
|
||||||
{ id: 'personal', label: 'Persönlich' },
|
{ id: 'personal', label: 'Persönlich' },
|
||||||
{ id: 'password', label: 'Passwort' },
|
{ id: 'password', label: 'Passwort' },
|
||||||
{ id: 'language', label: 'Sprache' },
|
{ id: 'system', label: 'System' },
|
||||||
{ id: 'theme', label: 'Theme' },
|
{ id: 'theme', label: 'Theme' },
|
||||||
{ id: 'units', label: 'Einheiten' },
|
{ id: 'units', label: 'Einheiten' },
|
||||||
{ id: 'users', label: 'Benutzer', adminOnly: true },
|
{ id: 'users', label: 'Benutzer', adminOnly: true },
|
||||||
@@ -118,7 +118,7 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
|
|||||||
<button className="settings-btn" onClick={handlePasswordChange}>Passwort ändern</button>
|
<button className="settings-btn" onClick={handlePasswordChange}>Passwort ändern</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'language':
|
case 'system':
|
||||||
return (
|
return (
|
||||||
<div className="settings-tab-content">
|
<div className="settings-tab-content">
|
||||||
<label className="settings-label">Sprache</label>
|
<label className="settings-label">Sprache</label>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { UnitType } from '../utils/format';
|
|||||||
const Topbar: React.FC<TopbarProps> = ({
|
const Topbar: React.FC<TopbarProps> = ({
|
||||||
projectName, savedStatus, onUndo, onRedo, onThemeToggle, theme,
|
projectName, savedStatus, onUndo, onRedo, onThemeToggle, theme,
|
||||||
onOpenSettings, onOpenLeftDrawer, onOpenRightDrawer,
|
onOpenSettings, onOpenLeftDrawer, onOpenRightDrawer,
|
||||||
unit = 'mm', onUnitChange,
|
unit = 'mm', onUnitChange, onNavigateBack,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<header className="topbar" role="banner">
|
<header className="topbar" role="banner">
|
||||||
@@ -13,10 +13,17 @@ const Topbar: React.FC<TopbarProps> = ({
|
|||||||
<button className="hamburger-btn" aria-label="Werkzeuge öffnen" onClick={onOpenLeftDrawer}>
|
<button className="hamburger-btn" aria-label="Werkzeuge öffnen" onClick={onOpenLeftDrawer}>
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div className="app-logo" aria-hidden="true">
|
<button
|
||||||
|
className="app-logo"
|
||||||
|
aria-label="Zum Dashboard"
|
||||||
|
onClick={onNavigateBack}
|
||||||
|
style={{ cursor: onNavigateBack ? 'pointer' : 'default', border: 'none', background: 'none', padding: 0, display: 'flex', alignItems: 'center', gap: '4px', transition: 'opacity 0.15s' }}
|
||||||
|
onMouseEnter={(e) => { if (onNavigateBack) e.currentTarget.style.opacity = '0.7'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.opacity = '1'; }}
|
||||||
|
>
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3h18v18H3z"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3h18v18H3z"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>
|
||||||
</div>
|
<span className="app-name">web-cad</span>
|
||||||
<span className="app-name">web-cad</span>
|
</button>
|
||||||
<span style={{ color: 'var(--color-border-strong)', margin: '0 4px' }}>|</span>
|
<span style={{ color: 'var(--color-border-strong)', margin: '0 4px' }}>|</span>
|
||||||
<button className="project-name" aria-label="Projekt wechseln">
|
<button className="project-name" aria-label="Projekt wechseln">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><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"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><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"/></svg>
|
||||||
@@ -70,10 +77,6 @@ const Topbar: React.FC<TopbarProps> = ({
|
|||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<span style={{ width: '1px', height: '20px', background: 'var(--color-border)', margin: '0 4px' }}></span>
|
<span style={{ width: '1px', height: '20px', background: 'var(--color-border)', margin: '0 4px' }}></span>
|
||||||
<select className="lang-select" aria-label="Sprache wählen" defaultValue="de">
|
|
||||||
<option value="de">DE</option>
|
|
||||||
<option value="en">EN</option>
|
|
||||||
</select>
|
|
||||||
<button className="icon-btn-top mobile-drawer-toggle" aria-label="Panel öffnen" title="Panel öffnen" onClick={onOpenRightDrawer}>
|
<button className="icon-btn-top mobile-drawer-toggle" aria-label="Panel öffnen" title="Panel öffnen" onClick={onOpenRightDrawer}>
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="15" y1="3" x2="15" y2="21"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="15" y1="3" x2="15" y2="21"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { UserCursor } from '../crdt';
|
|||||||
import type { UnitType } from '../utils/format';
|
import type { UnitType } from '../utils/format';
|
||||||
|
|
||||||
export type ViewMode = '2d' | 'iso' | 'front' | 'top';
|
export type ViewMode = '2d' | 'iso' | 'front' | 'top';
|
||||||
export type RibbonTab = 'start' | 'insert' | 'format' | 'view' | 'tools' | 'ki';
|
export type RibbonTab = 'start' | 'insert' | 'format' | 'view' | 'canvas' | 'tools' | 'ki';
|
||||||
export type RightPanel = 'tool' | 'layer' | 'library' | 'ki';
|
export type RightPanel = 'tool' | 'layer' | 'library' | 'ki';
|
||||||
export type Theme = 'light' | 'dark';
|
export type Theme = 'light' | 'dark';
|
||||||
export type DrawerTab = 'tool' | 'layer' | 'library' | 'ki';
|
export type DrawerTab = 'tool' | 'layer' | 'library' | 'ki';
|
||||||
@@ -70,6 +70,7 @@ export interface TopbarProps {
|
|||||||
onOpenRightDrawer?: () => void;
|
onOpenRightDrawer?: () => void;
|
||||||
unit?: UnitType;
|
unit?: UnitType;
|
||||||
onUnitChange?: (unit: UnitType) => void;
|
onUnitChange?: (unit: UnitType) => void;
|
||||||
|
onNavigateBack?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsModalProps {
|
export interface SettingsModalProps {
|
||||||
@@ -87,6 +88,12 @@ export interface RibbonBarProps {
|
|||||||
activeTab: RibbonTab;
|
activeTab: RibbonTab;
|
||||||
onTabChange: (tab: RibbonTab) => void;
|
onTabChange: (tab: RibbonTab) => void;
|
||||||
onAction: (action: string) => void;
|
onAction: (action: string) => void;
|
||||||
|
canvasBgColor?: string;
|
||||||
|
gridColor?: string;
|
||||||
|
rulerEnabled?: boolean;
|
||||||
|
onCanvasBgColorChange?: (color: string) => void;
|
||||||
|
onGridColorChange?: (color: string) => void;
|
||||||
|
onToggleRuler?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LeftSidebarProps {
|
export interface LeftSidebarProps {
|
||||||
@@ -151,6 +158,9 @@ export interface CanvasAreaProps {
|
|||||||
unit?: UnitType;
|
unit?: UnitType;
|
||||||
gridSize?: number;
|
gridSize?: number;
|
||||||
scaleFactor?: number;
|
scaleFactor?: number;
|
||||||
|
canvasBgColor?: string;
|
||||||
|
gridColor?: string;
|
||||||
|
rulerEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RightSidebarProps {
|
export interface RightSidebarProps {
|
||||||
|
|||||||
Reference in New Issue
Block a user