/** * Menubar for the canvas page: Project (Import/Export), Edit (Undo/Redo, Duplicate/Copy/Paste, Rename), View (Fit View, Minimap). */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarTrigger } from '@/components/ui/menubar' import { Kbd, KbdGroup } from '@/components/ui/kbd' import { usePlatform } from '@/app/kosmos/KosmosContext' import { ArrowLeft, ClipboardPaste, Copy, CopyPlus, Download, FolderOpen, Pencil, Redo2, Undo2 } from 'lucide-react' import { Input } from '@/components/ui/input' export type CanvasMenubarProps = { onImport: () => void onExport: () => void undo: () => void redo: () => void canUndo: boolean canRedo: boolean onDuplicate?: () => void onCopy?: () => void onPaste?: () => void canDuplicate?: boolean canCopy?: boolean onFitView?: () => void } const UNDO_KEYS = { key: 'z', shiftKey: false } const REDO_KEYS = { key: 'z', shiftKey: true } function matchKey(ev: KeyboardEvent, want: { key: string; shiftKey: boolean }) { const mod = ev.ctrlKey || ev.metaKey return ev.key.toLowerCase() === want.key && !!mod && !!ev.shiftKey === want.shiftKey } export function CanvasMenubar({ onImport, onExport, undo, redo, canUndo, canRedo, onDuplicate, onCopy, onPaste, canDuplicate = false, canCopy = false, onFitView, }: CanvasMenubarProps) { const { projectId } = useParams<{ projectId: string }>() const { projects, renameProject } = usePlatform() const projectName = useMemo( () => (projectId ? projects.find((p) => p.id === projectId)?.name ?? null : null), [projectId, projects] ) const [isRenamingProject, setIsRenamingProject] = useState(false) const [renameValue, setRenameValue] = useState('') const renameInputRef = useRef(null) const ignoreNextBlurRef = useRef(false) useEffect(() => { if (isRenamingProject) { setRenameValue(projectName ?? '') ignoreNextBlurRef.current = true // Delay focus so the Project dropdown can close first and not steal focus back (which would trigger blur) const t = setTimeout(() => { renameInputRef.current?.focus() renameInputRef.current?.select() }, 100) return () => clearTimeout(t) } }, [isRenamingProject, projectName]) const applyRename = useCallback(() => { if (!projectId || !renameProject) return const trimmed = renameValue.trim() if (trimmed) renameProject(projectId, trimmed) setIsRenamingProject(false) }, [projectId, renameProject, renameValue]) const cancelRename = useCallback(() => { setIsRenamingProject(false) }, []) const handleRenameBlur = useCallback(() => { if (ignoreNextBlurRef.current) { ignoreNextBlurRef.current = false return } applyRename() }, [applyRename]) useEffect(() => { const onKeyDown = (ev: KeyboardEvent) => { if (matchKey(ev, UNDO_KEYS)) { if (canUndo) { ev.preventDefault() ev.stopPropagation() undo() } return } if (matchKey(ev, REDO_KEYS)) { if (canRedo) { ev.preventDefault() ev.stopPropagation() redo() } } } window.addEventListener('keydown', onKeyDown, true) return () => window.removeEventListener('keydown', onKeyDown, true) }, [undo, redo, canUndo, canRedo]) return (
Project {projectId && ( <> setIsRenamingProject(true)} className="gap-2" > Rename )} Import… Export… Edit Undo ⌘ + Z Redo ⌘ + ⇧ + Z {(onDuplicate != null || onCopy != null || onPaste != null) && } {onDuplicate != null && ( Duplicate ⌘D )} {onCopy != null && ( Copy ⌘C )} {onPaste != null && ( Paste ⌘V )} View {onFitView && ( Fit View ⌘0 )} {projectId && (
{isRenamingProject ? ( setRenameValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault() applyRename() } else if (e.key === 'Escape') { e.preventDefault() cancelRename() } }} onBlur={handleRenameBlur} className="h-7 text-sm font-medium text-center font-serif" aria-label="Project name" /> ) : ( {projectName ?? 'Untitled'} )}
)}
) }