velxio/frontend/src/pages/EditorPage.tsx

244 lines
9.0 KiB
TypeScript

/**
* Editor Page — main editor + simulator with resizable panels
*/
import React, { useRef, useState, useCallback, useEffect } from 'react';
import { CodeEditor } from '../components/editor/CodeEditor';
import { EditorToolbar } from '../components/editor/EditorToolbar';
import { FileTabs } from '../components/editor/FileTabs';
import { FileExplorer } from '../components/editor/FileExplorer';
import { CompilationConsole } from '../components/editor/CompilationConsole';
import { SimulatorCanvas } from '../components/simulator/SimulatorCanvas';
import { SerialMonitor } from '../components/simulator/SerialMonitor';
import { AppHeader } from '../components/layout/AppHeader';
import { SaveProjectModal } from '../components/layout/SaveProjectModal';
import { LoginPromptModal } from '../components/layout/LoginPromptModal';
import { useSimulatorStore } from '../store/useSimulatorStore';
import { useAuthStore } from '../store/useAuthStore';
import type { CompilationLog } from '../utils/compilationLogger';
import '../App.css';
const BOTTOM_PANEL_MIN = 80;
const BOTTOM_PANEL_MAX = 600;
const BOTTOM_PANEL_DEFAULT = 200;
const EXPLORER_MIN = 120;
const EXPLORER_MAX = 500;
const EXPLORER_DEFAULT = 210;
const resizeHandleStyle: React.CSSProperties = {
height: 5,
flexShrink: 0,
cursor: 'row-resize',
background: '#2a2d2e',
borderTop: '1px solid #3c3c3c',
borderBottom: '1px solid #3c3c3c',
};
export const EditorPage: React.FC = () => {
const [editorWidthPct, setEditorWidthPct] = useState(45);
const containerRef = useRef<HTMLDivElement>(null);
const resizingRef = useRef(false);
const serialMonitorOpen = useSimulatorStore((s) => s.serialMonitorOpen);
const [consoleOpen, setConsoleOpen] = useState(false);
const [compileLogs, setCompileLogs] = useState<CompilationLog[]>([]);
const [bottomPanelHeight, setBottomPanelHeight] = useState(BOTTOM_PANEL_DEFAULT);
const [saveModalOpen, setSaveModalOpen] = useState(false);
const [loginPromptOpen, setLoginPromptOpen] = useState(false);
const [explorerOpen, setExplorerOpen] = useState(true);
const [explorerWidth, setExplorerWidth] = useState(EXPLORER_DEFAULT);
const user = useAuthStore((s) => s.user);
const handleSaveClick = useCallback(() => {
if (!user) {
setLoginPromptOpen(true);
} else {
setSaveModalOpen(true);
}
}, [user]);
// Ctrl+S shortcut
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
handleSaveClick();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [handleSaveClick]);
const handleResizeMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
resizingRef.current = true;
const handleMouseMove = (ev: MouseEvent) => {
if (!resizingRef.current || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const pct = ((ev.clientX - rect.left) / rect.width) * 100;
setEditorWidthPct(Math.max(20, Math.min(80, pct)));
};
const handleMouseUp = () => {
resizingRef.current = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}, []);
const handleBottomPanelResizeMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
const startY = e.clientY;
const startHeight = bottomPanelHeight;
const onMove = (ev: MouseEvent) => {
const delta = startY - ev.clientY;
setBottomPanelHeight(Math.max(BOTTOM_PANEL_MIN, Math.min(BOTTOM_PANEL_MAX, startHeight + delta)));
};
const onUp = () => {
document.body.style.cursor = '';
document.body.style.userSelect = '';
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.body.style.cursor = 'row-resize';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}, [bottomPanelHeight]);
const handleExplorerResizeMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
const startX = e.clientX;
const startWidth = explorerWidth;
const onMove = (ev: MouseEvent) => {
const delta = ev.clientX - startX;
setExplorerWidth(Math.max(EXPLORER_MIN, Math.min(EXPLORER_MAX, startWidth + delta)));
};
const onUp = () => {
document.body.style.cursor = '';
document.body.style.userSelect = '';
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}, [explorerWidth]);
return (
<div className="app">
<AppHeader />
<div className="app-container" ref={containerRef}>
{/* ── Editor side ── */}
<div
className="editor-panel"
style={{ width: `${editorWidthPct}%`, display: 'flex', flexDirection: 'row' }}
>
{/* File explorer sidebar + resize handle */}
{explorerOpen && (
<>
<div style={{ width: explorerWidth, flexShrink: 0, display: 'flex', overflow: 'hidden' }}>
<FileExplorer onSaveClick={handleSaveClick} />
</div>
<div className="explorer-resize-handle" onMouseDown={handleExplorerResizeMouseDown} />
</>
)}
{/* Editor main area */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minWidth: 0 }}>
{/* Explorer toggle + toolbar */}
<div style={{ display: 'flex', alignItems: 'stretch', flexShrink: 0 }}>
<button
className="explorer-toggle-btn"
onClick={() => setExplorerOpen((v) => !v)}
title={explorerOpen ? 'Hide file explorer' : 'Show file explorer'}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
</button>
<div style={{ flex: 1 }}>
<EditorToolbar
consoleOpen={consoleOpen}
setConsoleOpen={setConsoleOpen}
compileLogs={compileLogs}
setCompileLogs={setCompileLogs}
/>
</div>
</div>
{/* File tabs */}
<FileTabs />
{/* Monaco editor */}
<div className="editor-wrapper" style={{ flex: 1, overflow: 'hidden', minHeight: 0 }}>
<CodeEditor />
</div>
{/* Console */}
{consoleOpen && (
<>
<div
onMouseDown={handleBottomPanelResizeMouseDown}
style={resizeHandleStyle}
title="Drag to resize"
/>
<div style={{ height: bottomPanelHeight, flexShrink: 0 }}>
<CompilationConsole
isOpen={consoleOpen}
onClose={() => setConsoleOpen(false)}
logs={compileLogs}
onClear={() => setCompileLogs([])}
/>
</div>
</>
)}
</div>
</div>
{/* Resize handle */}
<div className="resize-handle" onMouseDown={handleResizeMouseDown}>
<div className="resize-handle-grip" />
</div>
{/* ── Simulator side ── */}
<div
className="simulator-panel"
style={{ width: `${100 - editorWidthPct}%`, display: 'flex', flexDirection: 'column' }}
>
<div style={{ flex: 1, overflow: 'hidden', position: 'relative', minHeight: 0 }}>
<SimulatorCanvas />
</div>
{serialMonitorOpen && (
<>
<div
onMouseDown={handleBottomPanelResizeMouseDown}
style={resizeHandleStyle}
title="Drag to resize"
/>
<div style={{ height: bottomPanelHeight, flexShrink: 0 }}>
<SerialMonitor />
</div>
</>
)}
</div>
</div>
{saveModalOpen && <SaveProjectModal onClose={() => setSaveModalOpen(false)} />}
{loginPromptOpen && <LoginPromptModal onClose={() => setLoginPromptOpen(false)} />}
</div>
);
};