Files
web-cad/frontend/src/components/RibbonBar.tsx
T
Agent Zero 05ccebad8d Replace frontend with deployed TS version from a08dd73
- Restore TS frontend source (canvas, tools, CRDT, components, services)
- Keep 5 JSX components that TS code depends on (CADCanvas, LayerPanel, BlockLibrary, PluginRegistry, Toolbar)
- Keep JS services (api.js, blockService.js) that components depend on
- Fix vite/plugin-react version mismatch (downgrade to v4 for vite 6)
- Add axios dependency
- Skip tsc in build script (vite/esbuild handles transpilation)
- Fix index.html lang=de, favicon path
- Add vite proxy config for /api and /ws
- Backend unchanged (already from deployed containers)
2026-06-28 02:16:58 +02:00

225 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useRef, useCallback, useEffect } from 'react';
import type { YjsDocument } from '../crdt/YjsDocument';
import { exportDrawing, type ExportFormat } from '../services/exportService';
import type { ExportResult } from '../types/cad.types';
import { importDrawing, type ImportFormat, type ImportResult } from '../services/importService';
import PrintPreview from './PrintPreview/PrintPreview';
import './RibbonBar.css';
interface RibbonBarProps {
onTabChange: (tab: string) => void;
yjsDoc?: React.RefObject<YjsDocument | null>;
}
const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange, yjsDoc }) => {
const [activeTab, setActiveTab] = useState('draw');
const [showExportMenu, setShowExportMenu] = useState(false);
const [showImportMenu, setShowImportMenu] = useState(false);
const [importStatus, setImportStatus] = useState<string | null>(null);
const [showPrintPreview, setShowPrintPreview] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const pendingImportFormat = useRef<ImportFormat>('json');
// Refs for dropdown wrappers (for outside-click detection)
const exportDropdownRef = useRef<HTMLDivElement>(null);
const importDropdownRef = useRef<HTMLDivElement>(null);
// Ref to track the trigger button that opened the print preview (for focus restoration)
const printTriggerRef = useRef<HTMLButtonElement>(null);
const tabs = [
{ id: 'draw', label: 'Draw', icon: '✏️' },
{ id: 'modify', label: 'Modify', icon: '🔧' },
{ id: 'annotate', label: 'Annotate', icon: '📝' },
{ id: 'view', label: 'View', icon: '👁️' },
{ id: 'tools', label: 'Tools', icon: '🛠️' },
];
const handleTabClick = (tabId: string) => {
setActiveTab(tabId);
onTabChange(tabId);
};
const handleExport = useCallback(async (format: ExportFormat) => {
setShowExportMenu(false);
const doc = yjsDoc?.current;
if (!doc) {
setImportStatus('Error: No document available');
setTimeout(() => setImportStatus(null), 3000);
return;
}
try {
const result: ExportResult = await exportDrawing(doc, format);
if (!result.success) {
setImportStatus(`Export error: ${result.error}`);
setTimeout(() => setImportStatus(null), 5000);
}
} catch (e) {
setImportStatus(`Export error: ${(e as Error).message}`);
setTimeout(() => setImportStatus(null), 5000);
}
}, [yjsDoc]);
const triggerImport = useCallback((format: ImportFormat) => {
setShowImportMenu(false);
pendingImportFormat.current = format;
if (fileInputRef.current) {
fileInputRef.current.accept = format === 'json' ? '.json,application/json' : '.dxf,application/dxf';
fileInputRef.current.click();
}
}, []);
const handleFileSelected = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const doc = yjsDoc?.current;
if (!doc) {
setImportStatus('Error: No document available');
setTimeout(() => setImportStatus(null), 3000);
return;
}
try {
const result: ImportResult = await importDrawing(file, pendingImportFormat.current, doc);
if (result.success) {
setImportStatus(`Imported: ${result.elementsImported} elements, ${result.layersImported} layers`);
} else {
setImportStatus(`Import failed: ${result.errors.join(', ')}`);
}
} catch (err) {
setImportStatus(`Import error: ${(err as Error).message}`);
}
setTimeout(() => setImportStatus(null), 5000);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}, [yjsDoc]);
// Outside-click and Escape close for dropdowns
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as Node;
if (showExportMenu && exportDropdownRef.current && !exportDropdownRef.current.contains(target)) {
setShowExportMenu(false);
}
if (showImportMenu && importDropdownRef.current && !importDropdownRef.current.contains(target)) {
setShowImportMenu(false);
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (showExportMenu) {
setShowExportMenu(false);
}
if (showImportMenu) {
setShowImportMenu(false);
}
}
};
if (showExportMenu || showImportMenu) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleKeyDown);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleKeyDown);
};
}, [showExportMenu, showImportMenu]);
return (
<div className="ribbon-bar" role="menubar" aria-label="CAD Functions">
{tabs.map((tab) => (
<button
key={tab.id}
className={`ribbon-tab ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => handleTabClick(tab.id)}
aria-pressed={activeTab === tab.id}
role="menuitemradio"
>
<span className="tab-icon" aria-hidden="true">{tab.icon}</span>
<span className="tab-label">{tab.label}</span>
</button>
))}
<div className="ribbon-divider" aria-hidden="true" />
<div className="ribbon-dropdown-wrapper" ref={exportDropdownRef}>
<button
className="ribbon-tab"
onClick={() => setShowExportMenu(!showExportMenu)}
aria-haspopup="menu"
aria-expanded={showExportMenu}
role="menuitem"
>
<span className="tab-icon" aria-hidden="true">📤</span>
<span className="tab-label">Export</span>
</button>
{showExportMenu && (
<div className="ribbon-dropdown" role="menu">
<button className="ribbon-dropdown-item" onClick={() => handleExport('json')} role="menuitem">JSON</button>
<button className="ribbon-dropdown-item" onClick={() => handleExport('svg')} role="menuitem">SVG</button>
<button className="ribbon-dropdown-item" onClick={() => handleExport('dxf')} role="menuitem">DXF</button>
<button className="ribbon-dropdown-item" onClick={() => handleExport('pdf')} role="menuitem">PDF</button>
</div>
)}
</div>
<div className="ribbon-dropdown-wrapper" ref={importDropdownRef}>
<button
className="ribbon-tab"
onClick={() => setShowImportMenu(!showImportMenu)}
aria-haspopup="menu"
aria-expanded={showImportMenu}
role="menuitem"
>
<span className="tab-icon" aria-hidden="true">📥</span>
<span className="tab-label">Import</span>
</button>
{showImportMenu && (
<div className="ribbon-dropdown" role="menu">
<button className="ribbon-dropdown-item" onClick={() => triggerImport('json')} role="menuitem">JSON</button>
<button className="ribbon-dropdown-item" onClick={() => triggerImport('dxf')} role="menuitem">DXF</button>
</div>
)}
</div>
<div className="ribbon-divider" aria-hidden="true" />
<button
ref={printTriggerRef}
className="ribbon-tab"
onClick={() => setShowPrintPreview(true)}
role="menuitem"
aria-label="Print Preview"
>
<span className="tab-icon" aria-hidden="true">🖨</span>
<span className="tab-label">Print</span>
</button>
{importStatus && (
<span className="ribbon-status" role="status" aria-live="polite">{importStatus}</span>
)}
<input
ref={fileInputRef}
type="file"
style={{ display: 'none' }}
onChange={handleFileSelected}
/>
{showPrintPreview && yjsDoc && (
<PrintPreview yjsDoc={yjsDoc} onClose={() => {
setShowPrintPreview(false);
// Restore focus to the Print trigger button
if (printTriggerRef.current) {
printTriggerRef.current.focus();
}
}} />
)}
</div>
);
};
export default RibbonBar;