mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-26 23:43:40 +00:00
Implements WebSocket-based TUIO protocol support to connect physical tangibles to presentation mode. When tangibles are placed on/removed from a touch screen, they trigger configured actions (label filtering or state switching). Features: - TUIO 1.1 and 2.0 protocol support with version selection - WebSocket connection management with real-time status - Test connection feature in configuration dialog - Persistent settings (WebSocket URL and protocol version) - Multi-tangible handling: union for filters, last-wins for states - Automatic connection in presentation mode Implementation: - TuioClientManager: Wrapper for tuio-client library with dual protocol support - WebsocketTuioReceiver: Custom OSC/WebSocket transport layer - useTuioIntegration: React hook bridging TUIO events to app stores - TuioConnectionConfig: Settings UI with real-time tangible detection - tuioStore: Zustand store with localStorage persistence Technical details: - TUIO 1.1 uses symbolId for hardware identification - TUIO 2.0 uses token.cId for hardware identification - Filter mode: Activates labels, union of all active tangibles - State mode: Switches timeline state, last tangible wins - Cleanup: Removes only labels no longer in use by any tangible - Unknown hardware IDs are silently ignored Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
589 lines
21 KiB
TypeScript
589 lines
21 KiB
TypeScript
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
|
import { useSettingsStore } from '../../stores/settingsStore';
|
|
import { useGraphWithHistory } from '../../hooks/useGraphWithHistory';
|
|
import { useDocumentHistory } from '../../hooks/useDocumentHistory';
|
|
import DocumentManager from '../Workspace/DocumentManager';
|
|
import NodeTypeConfigModal from '../Config/NodeTypeConfig';
|
|
import EdgeTypeConfigModal from '../Config/EdgeTypeConfig';
|
|
import LabelConfigModal from '../Config/LabelConfig';
|
|
import TangibleConfigModal from '../Config/TangibleConfig';
|
|
import TuioConnectionConfig from '../Config/TuioConnectionConfig';
|
|
import BibliographyConfigModal from '../Config/BibliographyConfig';
|
|
import InputDialog from '../Common/InputDialog';
|
|
import { useConfirm } from '../../hooks/useConfirm';
|
|
import { useShortcutLabels } from '../../hooks/useShortcutLabels';
|
|
import type { ExportOptions } from '../../utils/graphExport';
|
|
|
|
/**
|
|
* MenuBar Component
|
|
*
|
|
* Top-level menu bar with dropdown menus for:
|
|
* - File: Document and workspace operations
|
|
* - Edit: Configuration and settings
|
|
* - View: Display and navigation options
|
|
* - Help: Documentation and keyboard shortcuts
|
|
*/
|
|
|
|
interface MenuBarProps {
|
|
onOpenHelp?: () => void;
|
|
onFitView?: () => void;
|
|
onExport?: (format: 'png' | 'svg', options?: ExportOptions) => Promise<void>;
|
|
}
|
|
|
|
const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onExport }) => {
|
|
const [activeMenu, setActiveMenu] = useState<string | null>(null);
|
|
const [showDocumentManager, setShowDocumentManager] = useState(false);
|
|
const [showNodeConfig, setShowNodeConfig] = useState(false);
|
|
const [showEdgeConfig, setShowEdgeConfig] = useState(false);
|
|
const [showLabelConfig, setShowLabelConfig] = useState(false);
|
|
const [showTangibleConfig, setShowTangibleConfig] = useState(false);
|
|
const [showTuioConfig, setShowTuioConfig] = useState(false);
|
|
const [showBibliographyConfig, setShowBibliographyConfig] = useState(false);
|
|
const [showNewDocDialog, setShowNewDocDialog] = useState(false);
|
|
const [showNewFromTemplateDialog, setShowNewFromTemplateDialog] = useState(false);
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
const { confirm, ConfirmDialogComponent } = useConfirm();
|
|
const { getShortcutLabel } = useShortcutLabels();
|
|
|
|
const {
|
|
createDocument,
|
|
createDocumentFromTemplate,
|
|
activeDocumentId,
|
|
exportDocument,
|
|
importDocumentFromFile,
|
|
switchToDocument,
|
|
exportAllDocumentsAsZip,
|
|
exportWorkspace,
|
|
importWorkspace,
|
|
} = useWorkspaceStore();
|
|
|
|
const { clearGraph } = useGraphWithHistory();
|
|
const { undo, redo, canUndo, canRedo, undoDescription, redoDescription } = useDocumentHistory();
|
|
const { setPresentationMode } = useSettingsStore();
|
|
|
|
// Listen for custom event to close all menus (e.g., from graph canvas clicks, context menu opens)
|
|
useEffect(() => {
|
|
const handleCloseMenuEvent = (event: Event) => {
|
|
const customEvent = event as CustomEvent;
|
|
// Don't close if the event came from MenuBar itself (source: 'menubar')
|
|
if (customEvent.detail?.source !== 'menubar') {
|
|
setActiveMenu(null);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('closeAllMenus', handleCloseMenuEvent);
|
|
return () => window.removeEventListener('closeAllMenus', handleCloseMenuEvent);
|
|
}, []);
|
|
|
|
// Close menu when clicking outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
|
setActiveMenu(null);
|
|
}
|
|
};
|
|
|
|
if (activeMenu) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}
|
|
}, [activeMenu]);
|
|
|
|
const toggleMenu = useCallback((menuName: string) => {
|
|
setActiveMenu((current) => {
|
|
const newMenu = current === menuName ? null : menuName;
|
|
// When opening a menu (not closing), dispatch event to close context menus after state updates
|
|
if (newMenu !== null && current !== menuName) {
|
|
// Use setTimeout to dispatch after the render phase completes
|
|
setTimeout(() => {
|
|
window.dispatchEvent(new CustomEvent('closeAllMenus', { detail: { source: 'menubar' } }));
|
|
}, 0);
|
|
}
|
|
return newMenu;
|
|
});
|
|
}, []);
|
|
|
|
const closeMenu = useCallback(() => {
|
|
setActiveMenu(null);
|
|
}, []);
|
|
|
|
const handleNewDocument = useCallback(() => {
|
|
setShowNewDocDialog(true);
|
|
closeMenu();
|
|
}, [closeMenu]);
|
|
|
|
const handleConfirmNewDocument = useCallback((title: string) => {
|
|
createDocument(title);
|
|
setShowNewDocDialog(false);
|
|
}, [createDocument]);
|
|
|
|
const handleNewDocumentFromTemplate = useCallback(() => {
|
|
if (!activeDocumentId) {
|
|
alert('Please open a document first to use it as a template');
|
|
closeMenu();
|
|
return;
|
|
}
|
|
setShowNewFromTemplateDialog(true);
|
|
closeMenu();
|
|
}, [activeDocumentId, closeMenu]);
|
|
|
|
const handleConfirmNewFromTemplate = useCallback((title: string) => {
|
|
if (!activeDocumentId) return;
|
|
const newDocId = createDocumentFromTemplate(activeDocumentId, title);
|
|
if (newDocId) {
|
|
switchToDocument(newDocId);
|
|
}
|
|
setShowNewFromTemplateDialog(false);
|
|
}, [createDocumentFromTemplate, activeDocumentId, switchToDocument]);
|
|
|
|
const handleOpenDocumentManager = useCallback(() => {
|
|
setShowDocumentManager(true);
|
|
closeMenu();
|
|
}, [closeMenu]);
|
|
|
|
const handleImport = useCallback(async () => {
|
|
const newDocId = await importDocumentFromFile();
|
|
if (newDocId) {
|
|
switchToDocument(newDocId);
|
|
}
|
|
closeMenu();
|
|
}, [importDocumentFromFile, switchToDocument, closeMenu]);
|
|
|
|
const handleExport = useCallback(() => {
|
|
if (activeDocumentId) {
|
|
exportDocument(activeDocumentId);
|
|
}
|
|
closeMenu();
|
|
}, [activeDocumentId, exportDocument, closeMenu]);
|
|
|
|
const handleExportAll = useCallback(() => {
|
|
exportAllDocumentsAsZip();
|
|
closeMenu();
|
|
}, [exportAllDocumentsAsZip, closeMenu]);
|
|
|
|
const handleExportWorkspace = useCallback(() => {
|
|
exportWorkspace();
|
|
closeMenu();
|
|
}, [exportWorkspace, closeMenu]);
|
|
|
|
const handleImportWorkspace = useCallback(() => {
|
|
importWorkspace();
|
|
closeMenu();
|
|
}, [importWorkspace, closeMenu]);
|
|
|
|
const handleConfigureActors = useCallback(() => {
|
|
setShowNodeConfig(true);
|
|
closeMenu();
|
|
}, [closeMenu]);
|
|
|
|
const handleConfigureRelations = useCallback(() => {
|
|
setShowEdgeConfig(true);
|
|
closeMenu();
|
|
}, [closeMenu]);
|
|
|
|
const handleConfigureLabels = useCallback(() => {
|
|
setShowLabelConfig(true);
|
|
closeMenu();
|
|
}, [closeMenu]);
|
|
|
|
const handleConfigureTangibles = useCallback(() => {
|
|
setShowTangibleConfig(true);
|
|
closeMenu();
|
|
}, [closeMenu]);
|
|
|
|
const handleManageBibliography = useCallback(() => {
|
|
setShowBibliographyConfig(true);
|
|
closeMenu();
|
|
}, [closeMenu]);
|
|
|
|
const handleUndo = useCallback(() => {
|
|
undo();
|
|
closeMenu();
|
|
}, [undo, closeMenu]);
|
|
|
|
const handleRedo = useCallback(() => {
|
|
redo();
|
|
closeMenu();
|
|
}, [redo, closeMenu]);
|
|
|
|
const handleClearGraph = useCallback(async () => {
|
|
const confirmed = await confirm({
|
|
title: 'Clear Current Graph',
|
|
message: 'Are you sure you want to clear the current graph? This will remove all actors and relations from this document.',
|
|
confirmLabel: 'Clear Graph',
|
|
severity: 'danger',
|
|
});
|
|
if (confirmed) {
|
|
clearGraph();
|
|
}
|
|
closeMenu();
|
|
}, [clearGraph, closeMenu, confirm]);
|
|
|
|
const handleExportPNG = useCallback(async () => {
|
|
if (!onExport) return;
|
|
try {
|
|
await onExport('png');
|
|
closeMenu();
|
|
} catch (error) {
|
|
console.error('PNG export failed:', error);
|
|
alert('Failed to export graph as PNG');
|
|
}
|
|
}, [onExport, closeMenu]);
|
|
|
|
const handleExportSVG = useCallback(async () => {
|
|
if (!onExport) return;
|
|
try {
|
|
await onExport('svg');
|
|
closeMenu();
|
|
} catch (error) {
|
|
console.error('SVG export failed:', error);
|
|
alert('Failed to export graph as SVG');
|
|
}
|
|
}, [onExport, closeMenu]);
|
|
|
|
return (
|
|
<>
|
|
<div ref={menuRef} className="bg-white border-b border-gray-200 shadow-sm">
|
|
<div className="flex items-center px-4 py-1">
|
|
{/* File Menu */}
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => toggleMenu('file')}
|
|
className={`flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded transition-colors ${
|
|
activeMenu === 'file'
|
|
? 'bg-blue-100 text-blue-700'
|
|
: 'text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
File
|
|
<ExpandMoreIcon sx={{ fontSize: 16 }} />
|
|
</button>
|
|
|
|
{activeMenu === 'file' && (
|
|
<div className="absolute top-full left-0 mt-1 w-56 bg-white border border-gray-200 rounded-md shadow-lg py-1 z-50">
|
|
<button
|
|
onClick={handleNewDocument}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center justify-between"
|
|
>
|
|
<span>New Document</span>
|
|
{getShortcutLabel('new-document') && (
|
|
<span className="text-xs text-gray-400">{getShortcutLabel('new-document')}</span>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={handleNewDocumentFromTemplate}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
disabled={!activeDocumentId}
|
|
>
|
|
New from Current Template
|
|
</button>
|
|
<button
|
|
onClick={handleOpenDocumentManager}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center justify-between"
|
|
>
|
|
<span>Document Manager</span>
|
|
{getShortcutLabel('open-document-manager') && (
|
|
<span className="text-xs text-gray-400">{getShortcutLabel('open-document-manager')}</span>
|
|
)}
|
|
</button>
|
|
|
|
<hr className="my-1 border-gray-200" />
|
|
|
|
<button
|
|
onClick={handleImport}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
Import Document
|
|
</button>
|
|
<button
|
|
onClick={handleExport}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center justify-between"
|
|
>
|
|
<span>Export Document (JSON)</span>
|
|
{getShortcutLabel('save-document') && (
|
|
<span className="text-xs text-gray-400">{getShortcutLabel('save-document')}</span>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={handleExportPNG}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
disabled={!onExport || !activeDocumentId}
|
|
>
|
|
Export as PNG Image
|
|
</button>
|
|
<button
|
|
onClick={handleExportSVG}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
disabled={!onExport || !activeDocumentId}
|
|
>
|
|
Export as SVG Vector
|
|
</button>
|
|
<button
|
|
onClick={handleExportAll}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
Export All as ZIP
|
|
</button>
|
|
|
|
<hr className="my-1 border-gray-200" />
|
|
|
|
<button
|
|
onClick={handleExportWorkspace}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
Export Workspace
|
|
</button>
|
|
<button
|
|
onClick={handleImportWorkspace}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
Import Workspace
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Edit Menu */}
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => toggleMenu('edit')}
|
|
className={`flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded transition-colors ${
|
|
activeMenu === 'edit'
|
|
? 'bg-blue-100 text-blue-700'
|
|
: 'text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
Edit
|
|
<ExpandMoreIcon sx={{ fontSize: 16 }} />
|
|
</button>
|
|
|
|
{activeMenu === 'edit' && (
|
|
<div className="absolute top-full left-0 mt-1 w-56 bg-white border border-gray-200 rounded-md shadow-lg py-1 z-50">
|
|
<button
|
|
onClick={handleUndo}
|
|
disabled={!canUndo}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center justify-between disabled:opacity-40 disabled:cursor-not-allowed"
|
|
title={undoDescription ? `Undo: ${undoDescription}` : 'Undo'}
|
|
>
|
|
<span>Undo{undoDescription ? `: ${undoDescription}` : ''}</span>
|
|
{getShortcutLabel('undo') && (
|
|
<span className="text-xs text-gray-400">{getShortcutLabel('undo')}</span>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={handleRedo}
|
|
disabled={!canRedo}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center justify-between disabled:opacity-40 disabled:cursor-not-allowed"
|
|
title={redoDescription ? `Redo: ${redoDescription}` : 'Redo'}
|
|
>
|
|
<span>Redo{redoDescription ? `: ${redoDescription}` : ''}</span>
|
|
{getShortcutLabel('redo') && (
|
|
<span className="text-xs text-gray-400">{getShortcutLabel('redo')}</span>
|
|
)}
|
|
</button>
|
|
|
|
<hr className="my-1 border-gray-200" />
|
|
|
|
<button
|
|
onClick={handleConfigureActors}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
Configure Actor Types
|
|
</button>
|
|
<button
|
|
onClick={handleConfigureRelations}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
Configure Relation Types
|
|
</button>
|
|
<button
|
|
onClick={handleConfigureLabels}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
Configure Labels
|
|
</button>
|
|
<button
|
|
onClick={handleConfigureTangibles}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
Configure Tangibles
|
|
</button>
|
|
<button
|
|
onClick={handleManageBibliography}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
Manage Bibliography
|
|
</button>
|
|
|
|
<hr className="my-1 border-gray-200" />
|
|
|
|
<button
|
|
onClick={handleClearGraph}
|
|
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50"
|
|
>
|
|
Clear Current Graph
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* View Menu */}
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => toggleMenu('view')}
|
|
className={`flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded transition-colors ${
|
|
activeMenu === 'view'
|
|
? 'bg-blue-100 text-blue-700'
|
|
: 'text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
View
|
|
<ExpandMoreIcon sx={{ fontSize: 16 }} />
|
|
</button>
|
|
|
|
{activeMenu === 'view' && (
|
|
<div className="absolute top-full left-0 mt-1 w-56 bg-white border border-gray-200 rounded-md shadow-lg py-1 z-50">
|
|
<button
|
|
onClick={() => {
|
|
onFitView?.();
|
|
closeMenu();
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center justify-between"
|
|
>
|
|
<span>Fit View to Content</span>
|
|
{getShortcutLabel('fit-view') && (
|
|
<span className="text-xs text-gray-400">{getShortcutLabel('fit-view')}</span>
|
|
)}
|
|
</button>
|
|
|
|
{/* Presentation Mode */}
|
|
<button
|
|
onClick={() => {
|
|
setPresentationMode(true);
|
|
closeMenu();
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center justify-between"
|
|
>
|
|
<span>Presentation Mode</span>
|
|
<span className="text-xs text-gray-400">F11</span>
|
|
</button>
|
|
|
|
<div className="border-t border-gray-200 my-1" />
|
|
|
|
{/* TUIO Connection Settings */}
|
|
<button
|
|
onClick={() => {
|
|
setShowTuioConfig(true);
|
|
closeMenu();
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
TUIO Connection Settings...
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Help Menu */}
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => toggleMenu('help')}
|
|
className={`flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded transition-colors ${
|
|
activeMenu === 'help'
|
|
? 'bg-blue-100 text-blue-700'
|
|
: 'text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
Help
|
|
<ExpandMoreIcon sx={{ fontSize: 16 }} />
|
|
</button>
|
|
|
|
{activeMenu === 'help' && (
|
|
<div className="absolute top-full left-0 mt-1 w-56 bg-white border border-gray-200 rounded-md shadow-lg py-1 z-50">
|
|
<button
|
|
onClick={() => {
|
|
onOpenHelp?.();
|
|
closeMenu();
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center justify-between"
|
|
>
|
|
<span>Keyboard Shortcuts</span>
|
|
{getShortcutLabel('show-help') && (
|
|
<span className="text-xs text-gray-400">{getShortcutLabel('show-help')}</span>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modals */}
|
|
<DocumentManager
|
|
isOpen={showDocumentManager}
|
|
onClose={() => setShowDocumentManager(false)}
|
|
/>
|
|
<NodeTypeConfigModal
|
|
isOpen={showNodeConfig}
|
|
onClose={() => setShowNodeConfig(false)}
|
|
/>
|
|
<EdgeTypeConfigModal
|
|
isOpen={showEdgeConfig}
|
|
onClose={() => setShowEdgeConfig(false)}
|
|
/>
|
|
<LabelConfigModal
|
|
isOpen={showLabelConfig}
|
|
onClose={() => setShowLabelConfig(false)}
|
|
/>
|
|
<TangibleConfigModal
|
|
isOpen={showTangibleConfig}
|
|
onClose={() => setShowTangibleConfig(false)}
|
|
/>
|
|
<TuioConnectionConfig
|
|
isOpen={showTuioConfig}
|
|
onClose={() => setShowTuioConfig(false)}
|
|
/>
|
|
<BibliographyConfigModal
|
|
isOpen={showBibliographyConfig}
|
|
onClose={() => setShowBibliographyConfig(false)}
|
|
/>
|
|
|
|
{/* Input Dialogs */}
|
|
<InputDialog
|
|
isOpen={showNewDocDialog}
|
|
title="New Document"
|
|
message="Enter a name for the new document:"
|
|
placeholder="e.g., Team Analysis, Project Relationships..."
|
|
defaultValue="Untitled Analysis"
|
|
confirmLabel="Create"
|
|
onConfirm={handleConfirmNewDocument}
|
|
onCancel={() => setShowNewDocDialog(false)}
|
|
validateInput={(value) => {
|
|
if (!value.trim()) return 'Document name cannot be empty';
|
|
return null;
|
|
}}
|
|
/>
|
|
<InputDialog
|
|
isOpen={showNewFromTemplateDialog}
|
|
title="New Document from Template"
|
|
message="Enter a name for the new document:"
|
|
placeholder="e.g., Team Analysis, Project Relationships..."
|
|
defaultValue="Untitled Analysis"
|
|
confirmLabel="Create"
|
|
onConfirm={handleConfirmNewFromTemplate}
|
|
onCancel={() => setShowNewFromTemplateDialog(false)}
|
|
validateInput={(value) => {
|
|
if (!value.trim()) return 'Document name cannot be empty';
|
|
return null;
|
|
}}
|
|
/>
|
|
|
|
{/* Confirmation Dialog */}
|
|
{ConfirmDialogComponent}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default MenuBar;
|