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)
This commit is contained in:
Agent Zero
2026-06-28 02:16:58 +02:00
parent 6e508fb68e
commit 05ccebad8d
89 changed files with 12577 additions and 1231 deletions
+224
View File
@@ -0,0 +1,224 @@
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;