T20: Add export/import UI to RibbonBar.tsx

This commit is contained in:
2026-06-22 23:13:21 +00:00
parent e6da2a9b2f
commit 289ade187e
+131 -3
View File
@@ -1,12 +1,21 @@
import React, { useState } from 'react'; import React, { useState, useRef, useCallback } from 'react';
import type { YjsDocument } from '../crdt/YjsDocument';
import { exportDrawing, type ExportFormat } from '../services/exportService';
import { importDrawing, type ImportFormat, type ImportResult } from '../services/importService';
import './RibbonBar.css'; import './RibbonBar.css';
interface RibbonBarProps { interface RibbonBarProps {
onTabChange: (tab: string) => void; onTabChange: (tab: string) => void;
yjsDoc?: React.RefObject<YjsDocument | null>;
} }
const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange }) => { const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange, yjsDoc }) => {
const [activeTab, setActiveTab] = useState('draw'); const [activeTab, setActiveTab] = useState('draw');
const [showExportMenu, setShowExportMenu] = useState(false);
const [showImportMenu, setShowImportMenu] = useState(false);
const [importStatus, setImportStatus] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const pendingImportFormat = useRef<ImportFormat>('json');
const tabs = [ const tabs = [
{ id: 'draw', label: 'Draw', icon: '✏️' }, { id: 'draw', label: 'Draw', icon: '✏️' },
@@ -21,6 +30,57 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange }) => {
onTabChange(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 {
await exportDrawing(doc, format);
} 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);
// Reset input so same file can be selected again
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}, [yjsDoc]);
return ( return (
<div className="ribbon-bar" role="menubar" aria-label="CAD Functions"> <div className="ribbon-bar" role="menubar" aria-label="CAD Functions">
{tabs.map((tab) => ( {tabs.map((tab) => (
@@ -35,8 +95,76 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange }) => {
<span className="tab-label">{tab.label}</span> <span className="tab-label">{tab.label}</span>
</button> </button>
))} ))}
{/* Export/Import section */}
<div className="ribbon-divider" aria-hidden="true" />
<div className="ribbon-dropdown-wrapper">
<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">
<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>
{importStatus && (
<span className="ribbon-status" role="status" aria-live="polite">
{importStatus}
</span>
)}
<input
ref={fileInputRef}
type="file"
style={{ display: 'none' }}
onChange={handleFileSelected}
/>
</div> </div>
); );
}; };
export default RibbonBar; export default RibbonBar;