task(CAD-10): ribbon curation - 4 tabs (Start/Zeichnen/Ansicht/KI) statt 7, logische Werkzeug-Gruppen, Traversen+Stueckliste zusammen, Duplikate entfernt

This commit is contained in:
Agent Zero
2026-09-02 22:12:19 +02:00
parent 29234ea906
commit 47c029b4b6
5 changed files with 151 additions and 235 deletions
+100 -227
View File
@@ -1,19 +1,24 @@
import React from 'react'; import React from 'react';
import { getUIMode, useUIMode } from '../services/uiModeService'; import { useUIMode } from '../services/uiModeService';
import { useT } from '../i18n/strings';
import type { RibbonBarProps, RibbonTab } from '../types/ui.types'; import type { RibbonBarProps, RibbonTab } from '../types/ui.types';
import { pluginRegistry } from '../plugins'; import { pluginRegistry } from '../plugins';
import { V2_TOOLS_ENABLED } from '../kernel/featureFlags'; import { V2_TOOLS_ENABLED } from '../kernel/featureFlags';
const tabs: Array<{ id: RibbonTab; label: string; svg: React.ReactNode }> = [ const tabs: Array<{ id: RibbonTab; label: string; svg: React.ReactNode }> = [
{ id: 'start', label: 'Start', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg> }, { id: 'start', label: 'Start', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></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: 'draw', label: 'Zeichnen', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></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: '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> },
]; ];
/** V2-Werkzeug-Kategorien → Gruppen-Label im Zeichnen-Tab. */
const V2_GROUP_LABELS: Record<string, string> = {
canvas: 'Zeichnen & Ändern',
format: 'Bemaßung & Text',
insert: 'Bestuhlung & Traversen',
};
const RibbonBar: React.FC<RibbonBarProps> = ({ const RibbonBar: React.FC<RibbonBarProps> = ({
activeTab, onTabChange, onAction, activeTab, onTabChange, onAction,
canvasBgColor = '#1e1e2e', gridColor = '#3a3a4a', rulerEnabled = true, canvasBgColor = '#1e1e2e', gridColor = '#3a3a4a', rulerEnabled = true,
@@ -22,6 +27,46 @@ const RibbonBar: React.FC<RibbonBarProps> = ({
}) => { }) => {
const uiMode = useUIMode(); const uiMode = useUIMode();
/** Rendert die V2-Werkzeuge einer Kategorie als Button-Gruppe. */
const renderV2Group = (category: string) => {
const tools = pluginRegistry.getToolsV2()
.filter((tool) => tool.manifest.ribbonTab === category)
.filter((tool) => uiMode === 'pro' || (tool.manifest.tags ?? []).includes('basic'));
if (tools.length === 0) return null;
return (
<div className="ribbon-group">
<div className="ribbon-group-btns">
{tools.map((tool) => {
const isActive = onIsV2ToolActive?.(tool.manifest.id);
return (
<button
key={tool.manifest.id}
data-v2tool={tool.manifest.id}
title={tool.manifest.description ?? tool.manifest.label}
onClick={() => onAction(`v2tool:${tool.manifest.id}`)}
className={`ribbon-v2-btn${isActive ? ' active' : ''}`}
>
<span className="ribbon-v2-icon">{tool.manifest.icon}</span>
<span className="ribbon-v2-label">{tool.manifest.label}</span>
</button>
);
})}
{category === 'insert' && (
<button
className="ribbon-btn"
title="Stückliste Traversen"
onClick={() => onAction('bom')}
>
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3h18v18H3z"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M9 3v18"/></svg>
<span>Stückliste</span>
</button>
)}
</div>
<div className="ribbon-group-label">{V2_GROUP_LABELS[category] ?? category}</div>
</div>
);
};
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">
@@ -42,95 +87,58 @@ const RibbonBar: React.FC<RibbonBarProps> = ({
</div> </div>
<div className="ribbon-content" id="ribbon-content"> <div className="ribbon-content" id="ribbon-content">
{/* ===== V2 TOOLS (Task A4.13/A4.14: Standard aktiv — Opt-out via VITE_TOOL_DISPATCHER=legacy) ===== */}
{activeTab === 'canvas' && V2_TOOLS_ENABLED && (
<div style={{
display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center',
padding: '8px 4px', width: '100%',
borderBottom: '1px solid var(--color-border, #333)', marginBottom: '8px',
}}>
<span style={{ fontSize: '11px', color: 'var(--color-ki, #8a8aff)', marginRight: '4px' }}>V2:</span>
{pluginRegistry.getToolsV2()
.filter((tool) => uiMode === 'pro' || (tool.manifest.tags ?? []).includes('basic'))
.map((tool) => {
const isActive = onIsV2ToolActive?.(tool.manifest.id);
return (
<button
key={tool.manifest.id}
data-v2tool={tool.manifest.id}
title={tool.manifest.description ?? tool.manifest.label}
onClick={() => onAction(`v2tool:${tool.manifest.id}`)}
style={{
display: 'flex', alignItems: 'center', gap: '4px',
padding: '5px 9px', borderRadius: '4px', fontSize: '12px',
cursor: 'pointer', border: '1px solid var(--color-border, #333)',
background: isActive ? 'rgba(80,250,123,0.15)' : 'var(--color-bg-secondary, #2a2a3a)',
color: isActive ? '#50fa7b' : 'inherit',
fontWeight: isActive ? 600 : 400,
}}
>
<span style={{ fontFamily: 'monospace' }}>{tool.manifest.icon}</span>
<span>{tool.manifest.label}</span>
</button>
);
})}
</div>
)}
{/* ===== START TAB ===== */} {/* ===== START TAB ===== */}
{activeTab === 'start' && ( {activeTab === 'start' && (
<> <>
<div className="ribbon-group compact-mobile" data-ribbon-group="start"> <div className="ribbon-group">
<div className="ribbon-group-btns"> <div className="ribbon-group-btns">
<button className="ribbon-btn" title="Neues Projekt (Strg+N)" onClick={() => onAction('new')}> <button className="ribbon-btn" title="Neues Projekt (Strg+N)" onClick={() => onAction('new')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="15" x2="15" y2="15"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
<span>Neu</span> <span>Neu</span>
</button> </button>
<button className="ribbon-btn" title="Öffnen (Strg+O)" onClick={() => onAction('open')}> <button className="ribbon-btn" title="Öffnen (Strg+O)" onClick={() => onAction('open')}>
<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"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
<span>Öffnen</span> <span>Öffnen</span>
</button> </button>
<button className="ribbon-btn" title="Speichern (Strg+S)" onClick={() => onAction('save')}> <button className="ribbon-btn" title="Speichern (Strg+S)" onClick={() => onAction('save')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
<span>Speichern</span> <span>Speichern</span>
</button> </button>
<button className="ribbon-btn" title="Datei importieren (DXF, SVG, JSON)" onClick={() => onAction('import')}> <button className="ribbon-btn" title="Import (DXF, SVG, JSON)" onClick={() => onAction('import')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
<span>Import</span> <span>Import</span>
</button> </button>
<button className="ribbon-btn" title="Export als (DXF, SVG, PDF, PNG, JSON)" onClick={() => onAction('export')}> <button className="ribbon-btn" title="Export (DXF, SVG, PDF, PNG, JSON)" onClick={() => onAction('export')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
<span>Export</span> <span>Export</span>
</button> </button>
</div> </div>
<div className="ribbon-group-label">Datei</div> <div className="ribbon-group-label">Datei</div>
</div> </div>
<div className="ribbon-divider"></div>
<div className="ribbon-group"> <div className="ribbon-group">
<div className="ribbon-group-btns"> <div className="ribbon-group-btns">
<button className="ribbon-btn" title="Rückgängig (Strg+Z)" onClick={() => onAction('undo')}> <button className="ribbon-btn" title="Rückgängig (Strg+Z)" onClick={() => onAction('undo')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-15-6.7L3 13"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-15-6.7L3 13"/></svg>
<span>Undo</span> <span>Rückgängig</span>
</button> </button>
<button className="ribbon-btn" title="Wiederherstellen (Strg+Y)" onClick={() => onAction('redo')}> <button className="ribbon-btn" title="Wiederherstellen (Strg+Y)" onClick={() => onAction('redo')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 15-6.7L21 13"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 15-6.7L21 13"/></svg>
<span>Redo</span> <span>Wiederherstellen</span>
</button> </button>
<button className="ribbon-btn" title="Kopieren (Strg+C)" onClick={() => onAction('copy')}> <button className="ribbon-btn" title="Kopieren (Strg+C)" onClick={() => onAction('copy')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<span>Kopieren</span> <span>Kopieren</span>
</button> </button>
<button className="ribbon-btn" title="Einfügen (Strg+V)" onClick={() => onAction('paste')}> <button className="ribbon-btn" title="Einfügen (Strg+V)" onClick={() => onAction('paste')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/></svg>
<span>Einfügen</span> <span>Einfügen</span>
</button> </button>
<button className="ribbon-btn" title="Gruppieren (G)" onClick={() => onAction('group')}> <button className="ribbon-btn" title="Gruppieren (G)" onClick={() => onAction('group')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><path d="M10 10l4 4"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>
<span>Gruppieren</span> <span>Gruppieren</span>
</button> </button>
<button className="ribbon-btn" title="Degruppieren (Strg+G)" onClick={() => onAction('ungroup')}> <button className="ribbon-btn" title="Degruppieren (Strg+G)" onClick={() => onAction('ungroup')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1" strokeDasharray="2 2"/><rect x="14" y="14" width="7" height="7" rx="1" strokeDasharray="2 2"/><path d="M10 10l4 4" strokeDasharray="2 2"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
<span>Degruppieren</span> <span>Degruppieren</span>
</button> </button>
</div> </div>
@@ -139,198 +147,72 @@ const RibbonBar: React.FC<RibbonBarProps> = ({
</> </>
)} )}
{/* ===== INSERT TAB ===== */} {/* ===== ZEICHNEN TAB ===== */}
{activeTab === 'insert' && ( {activeTab === 'draw' && V2_TOOLS_ENABLED && (
<> <div className="ribbon-draw-content">
<div className="ribbon-group"> {renderV2Group('canvas')}
<div className="ribbon-group-btns"> {renderV2Group('format')}
<button className="ribbon-btn" title="Linie zeichnen" onClick={() => onAction('insert-line')}> {renderV2Group('insert')}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="19" x2="19" y2="5"/></svg>
<span>Linie</span>
</button>
<button className="ribbon-btn" title="Rechteck zeichnen" onClick={() => onAction('insert-rect')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="5" width="18" height="14" rx="1"/></svg>
<span>Rechteck</span>
</button>
<button className="ribbon-btn" title="Kreis zeichnen" onClick={() => onAction('insert-circle')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9"/></svg>
<span>Kreis</span>
</button>
<button className="ribbon-btn" title="Text einfügen" onClick={() => onAction('insert-text')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>
<span>Text</span>
</button>
<button className="ribbon-btn" title="Freihand zeichnen" onClick={() => onAction('insert-freehand')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 17s5-5 9-5 9 5 9 5"/><path d="M3 12s5-5 9-5 9 5 9 5"/></svg>
<span>Freihand</span>
</button>
</div> </div>
<div className="ribbon-group-label">Formen</div>
</div>
<div className="ribbon-divider"></div>
<div className="ribbon-group">
<div className="ribbon-group-btns">
<button className="ribbon-btn" title="Hintergrund importieren" onClick={() => onAction('background-import')}>
<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"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
<span>Hintergrund</span>
</button>
<button className="ribbon-btn" title="Bild einfügen" onClick={() => onAction('insert-image')}>
<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"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
<span>Bild</span>
</button>
</div>
<div className="ribbon-group-label">Objekt</div>
</div>
</>
)} )}
{/* ===== FORMAT TAB ===== */} {/* ===== ANSICHT TAB ===== */}
{activeTab === 'format' && (
<>
<div className="ribbon-group">
<div className="ribbon-group-btns">
<button className="ribbon-btn" title="Linienstärke" onClick={() => onAction('format-stroke-width')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="4" y1="12" x2="20" y2="12"/></svg>
<span>Linienstärke</span>
</button>
<button className="ribbon-btn" title="Linienfarbe" onClick={() => onAction('format-stroke-color')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9"/><line x1="3" y1="12" x2="21" y2="12"/></svg>
<span>Linienfarbe</span>
</button>
<button className="ribbon-btn" title="Füllfarbe" onClick={() => onAction('format-fill-color')}>
<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 3l18 18"/></svg>
<span>Füllfarbe</span>
</button>
<button className="ribbon-btn" title="Linientyp" onClick={() => onAction('format-stroke-style')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="4" y1="8" x2="20" y2="8"/><line x1="4" y1="16" x2="20" y2="16" stroke-dasharray="4 4"/></svg>
<span>Linientyp</span>
</button>
</div>
<div className="ribbon-group-label">Stil</div>
</div>
<div className="ribbon-divider"></div>
<div className="ribbon-group">
<div className="ribbon-group-btns">
<button className="ribbon-btn" title="Nach links ausrichten" onClick={() => onAction('format-align-left')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="4" y1="4" x2="4" y2="20"/><rect x="6" y="6" width="12" height="4"/><rect x="6" y="14" width="8" height="4"/></svg>
<span>Links</span>
</button>
<button className="ribbon-btn" title="Zentrieren" onClick={() => onAction('format-align-center')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="4" x2="12" y2="20"/><rect x="6" y="6" width="12" height="4"/><rect x="8" y="14" width="8" height="4"/></svg>
<span>Zentriert</span>
</button>
<button className="ribbon-btn" title="Nach rechts ausrichten" onClick={() => onAction('format-align-right')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="20" y1="4" x2="20" y2="20"/><rect x="6" y="6" width="12" height="4"/><rect x="10" y="14" width="8" height="4"/></svg>
<span>Rechts</span>
</button>
</div>
<div className="ribbon-group-label">Ausrichtung</div>
</div>
</>
)}
{/* ===== VIEW TAB ===== */}
{activeTab === 'view' && ( {activeTab === 'view' && (
<> <>
<div className="ribbon-group"> <div className="ribbon-group">
<div className="ribbon-group-btns"> <div className="ribbon-group-btns">
<button className="ribbon-btn" title="Zoom anpassen (F)" onClick={() => onAction('zoom-fit')}> <button className="ribbon-btn" title="Zoom anpassen (F)" onClick={() => onAction('zoom-fit')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><line x1="7" y1="12" x2="17" y2="12"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
<span>Zoom Fit</span> <span>Zoom Fit</span>
</button> </button>
<button className="ribbon-btn" title="100% (1:1)" onClick={() => onAction('zoom-100')}> <button className="ribbon-btn" title="100% (1:1)" onClick={() => onAction('zoom-100')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
<span>100%</span> <span>100%</span>
</button> </button>
<button className="ribbon-btn" title="Grid ein/aus (F7)" onClick={() => onAction('grid')}> <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" rx="0"/><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> <svg viewBox="0 0 24 24" width="14" height="14" 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="M3 15h18"/><path d="M9 3v18"/><path d="M15 3v18"/></svg>
<span>Grid</span> <span>Raster</span>
</button> </button>
<button className="ribbon-btn" title="Layer-Manager" onClick={() => onAction('layer')}> <button className="ribbon-btn" title="Layer-Manager" onClick={() => onAction('layer')}>
<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> <svg viewBox="0 0 24 24" width="14" height="14" 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>
<span>Layer</span> <span>Layer</span>
</button> </button>
</div> </div>
<div className="ribbon-group-label">Ansicht</div> <div className="ribbon-group-label">Ansicht</div>
</div> </div>
</>
)}
{/* ===== CANVAS TAB ===== */}
{activeTab === 'canvas' && (
<>
<div className="ribbon-group"> <div className="ribbon-group">
<div className="ribbon-group-btns"> <div className="ribbon-group-btns">
<div className="ribbon-color-picker" title="Hintergrundfarbe der Zeichenfläche"> <button className="ribbon-btn" title="Hintergrund-Grundriss importieren" onClick={() => onAction('background-import')}>
<div className="ribbon-color-swatch" style={{ background: canvasBgColor }}> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
<input <span>Hintergrund</span>
type="color" </button>
value={canvasBgColor} <button className="ribbon-btn" title="Bild einfügen" onClick={() => onAction('insert-image')}>
onChange={(e) => onCanvasBgColorChange?.(e.target.value)} <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
/> <span>Bild</span>
</button>
</div> </div>
<span className="ribbon-color-label">Hintergrund</span> <div className="ribbon-group-label">Hintergrund</div>
</div> </div>
<div className="ribbon-color-picker" title="Gitterfarbe">
<div className="ribbon-color-swatch" style={{ background: gridColor }}>
<input
type="color"
value={gridColor}
onChange={(e) => onGridColorChange?.(e.target.value)}
/>
</div>
<span className="ribbon-color-label">Gitter</span>
</div>
</div>
<div className="ribbon-group-label">Farben</div>
</div>
<div className="ribbon-divider"></div>
<div className="ribbon-group"> <div className="ribbon-group">
<div className="ribbon-group-btns"> <div className="ribbon-group-btns">
<button <label className="ribbon-color-label">Canvas</label>
className={`ribbon-btn${rulerEnabled ? ' active' : ''}`} <input type="color" value={canvasBgColor} onChange={(e) => onCanvasBgColorChange?.(e.target.value)} aria-label="Canvas-Hintergrundfarbe" />
title="Lineal ein/aus" <label className="ribbon-color-label">Raster</label>
aria-pressed={rulerEnabled} <input type="color" value={gridColor} onChange={(e) => onGridColorChange?.(e.target.value)} aria-label="Rasterfarbe" />
onClick={() => onToggleRuler?.()} <button className="ribbon-btn" title="Lineal ein/aus" onClick={() => onToggleRuler?.()}>
> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21.3 8.7l-7.6-7.6a1 1 0 0 0-1.4 0l-9 9a1 1 0 0 0 0 1.4l7.6 7.6a1 1 0 0 0 1.4 0l9-9a1 1 0 0 0 0-1.4z"/></svg>
<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> <span>Lineal</span>
</button> </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>
<div className="ribbon-group-label">Anzeige</div> <div className="ribbon-group-label">Darstellung</div>
</div> </div>
</>
)}
{/* ===== TOOLS TAB ===== */}
{activeTab === 'tools' && (
<>
<div className="ribbon-group"> <div className="ribbon-group">
<div className="ribbon-group-btns"> <div className="ribbon-group-btns">
<button className="ribbon-btn" title="Messwerkzeug" onClick={() => onAction('measure')}> <button className="ribbon-btn" title="Historie" onClick={() => onAction('history')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21.3 8.7 8.7 21.3a1 1 0 0 1-1.4 0L2.7 16.7a1 1 0 0 1 0-1.4L15.3 2.7a1 1 0 0 1 1.4 0l4.6 4.6a1 1 0 0 1 0 1.4z"/><path d="m7.5 10.5 2 2"/><path d="m10.5 7.5 2 2"/><path d="m13.5 4.5 2 2"/><path d="m4.5 13.5 2 2"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3v5h5"/><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"/><path d="M12 7v5l4 2"/></svg>
<span>Messen</span>
</button>
<button className="ribbon-btn" title="Plugins" onClick={() => onAction('plugins')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v6m0 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/><path d="M12 8a6 6 0 0 0-6 6c0 3 2 4 2 6h8c0-2 2-3 2-6a6 6 0 0 0-6-6z"/></svg>
<span>Plugins</span>
</button>
<button className="ribbon-btn" title="Historie (Strg+H)" onClick={() => onAction('history')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3v5h5"/><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"/><path d="M12 7v5l4 2"/></svg>
<span>Historie</span> <span>Historie</span>
</button> </button>
<button className="ribbon-btn" title="Gäste-Verwaltung" onClick={() => onAction('guests')}> <button className="ribbon-btn" title="Gäste-Verwaltung" onClick={() => onAction('guests')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span>Gäste</span> <span>Gäste</span>
</button> </button>
</div> </div>
@@ -344,18 +226,9 @@ const RibbonBar: React.FC<RibbonBarProps> = ({
<> <>
<div className="ribbon-group"> <div className="ribbon-group">
<div className="ribbon-group-btns"> <div className="ribbon-group-btns">
<button className="ribbon-btn" style={{ color: 'var(--color-ki)' }} title="KI Copilot (Ctrl+K)" onClick={() => onAction('ki')}> <span style={{ fontSize: '12px', color: 'var(--color-ki, #8a8aff)', padding: '4px 8px' }}>
<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> KI Copilot Befehle über die Befehlszeile unten (Strg+K)
<span>KI Copilot</span> </span>
</button>
<button className="ribbon-btn" style={{ color: 'var(--color-ki)' }} title="KI Zeichnen" onClick={() => onAction('ki-draw')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2 2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
<span>KI Zeichnen</span>
</button>
<button className="ribbon-btn" style={{ color: 'var(--color-ki)' }} title="KI Analysieren" onClick={() => onAction('ki-analyze')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/><path d="M3 5c0 1.66 4 3 9 3s9-1.34 9-3-4-3-9-3-9 1.34-9 3"/></svg>
<span>KI Analysieren</span>
</button>
</div> </div>
<div className="ribbon-group-label">Künstliche Intelligenz</div> <div className="ribbon-group-label">Künstliche Intelligenz</div>
</div> </div>
+43
View File
@@ -3077,3 +3077,46 @@ body.drawer-open { overflow: hidden; }
color: #fff; color: #fff;
border-color: rgba(255, 255, 255, 0.3); border-color: rgba(255, 255, 255, 0.3);
} }
/* ─── CAD-10: Zeichnen-Tab V2-Buttons ─── */
.ribbon-draw-content {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 4px 0;
width: 100%;
}
.ribbon-v2-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
min-width: 52px;
padding: 6px 8px;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
cursor: pointer;
color: var(--color-text, #d0d0d0);
}
.ribbon-v2-btn:hover {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.12);
}
.ribbon-v2-btn.active {
background: rgba(74, 144, 217, 0.15);
border-color: rgba(74, 144, 217, 0.5);
}
.ribbon-v2-icon {
font-size: 14px;
line-height: 1;
}
.ribbon-v2-label {
font-size: 9px;
color: var(--color-text-secondary, #9aa0a6);
text-align: center;
white-space: nowrap;
}
.ribbon-v2-btn.active .ribbon-v2-label {
color: #4a90d9;
}
+1 -1
View File
@@ -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' | 'canvas' | 'tools' | 'ki'; export type RibbonTab = 'start' | 'draw' | 'view' | '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';
+3 -3
View File
@@ -59,9 +59,9 @@ describe('RibbonBar', () => {
it('should call onTabChange when clicking a tab', () => { it('should call onTabChange when clicking a tab', () => {
const onTabChange = vi.fn(); const onTabChange = vi.fn();
render(<RibbonBar activeTab="start" onTabChange={onTabChange} onAction={() => {}} />); render(<RibbonBar activeTab="start" onTabChange={onTabChange} onAction={() => {}} />);
// Click the 'Format' tab (unique text) // CAD-10: Klick auf 'Zeichnen'
fireEvent.click(screen.getByText('Format')); fireEvent.click(screen.getByText('Zeichnen'));
expect(onTabChange).toHaveBeenCalledWith('format'); expect(onTabChange).toHaveBeenCalledWith('draw');
}); });
it('should call onAction when clicking an action button', () => { it('should call onAction when clicking an action button', () => {
+3 -3
View File
@@ -70,12 +70,12 @@ describe('H1: RibbonBar simple-Filter', () => {
registerBuiltinPlugins(); registerBuiltinPlugins();
pluginRegistry.initDefaults(); pluginRegistry.initDefaults();
const RibbonBar = (await import('../src/components/RibbonBar')).default; const RibbonBar = (await import('../src/components/RibbonBar')).default;
render(<RibbonBar activeTab="canvas" onTabChange={() => {}} onAction={() => {}} />); render(<RibbonBar activeTab="draw" onTabChange={() => {}} onAction={() => {}} />);
const all = screen.getAllByRole('button').length; const all = screen.getAllByRole('button').length;
expect(all).toBeGreaterThan(10); expect(all).toBeGreaterThan(10);
cleanup(); cleanup();
setUIMode('simple'); setUIMode('simple');
render(<RibbonBar activeTab="canvas" onTabChange={() => {}} onAction={() => {}} />); render(<RibbonBar activeTab="draw" onTabChange={() => {}} onAction={() => {}} />);
const simpleCount = screen.getAllByRole('button').length; const simpleCount = screen.getAllByRole('button').length;
expect(simpleCount).toBeLessThan(all); expect(simpleCount).toBeLessThan(all);
expect(simpleCount).toBeGreaterThan(0); expect(simpleCount).toBeGreaterThan(0);
@@ -88,7 +88,7 @@ describe('H1: RibbonBar simple-Filter', () => {
registerBuiltinPlugins(); registerBuiltinPlugins();
pluginRegistry.initDefaults(); pluginRegistry.initDefaults();
const RibbonBar = (await import('../src/components/RibbonBar')).default; const RibbonBar = (await import('../src/components/RibbonBar')).default;
render(<RibbonBar activeTab="canvas" onTabChange={() => {}} onAction={() => {}} />); render(<RibbonBar activeTab="draw" onTabChange={() => {}} onAction={() => {}} />);
const html = document.body.innerHTML; const html = document.body.innerHTML;
expect(html).toContain('data-v2tool="line"'); expect(html).toContain('data-v2tool="line"');
expect(html).not.toContain('data-v2tool="rotate"'); expect(html).not.toContain('data-v2tool="rotate"');