05ccebad8d
- 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)
225 lines
8.0 KiB
TypeScript
225 lines
8.0 KiB
TypeScript
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;
|