/** * 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 { Oscilloscope } from '../components/simulator/Oscilloscope'; import { AppHeader } from '../components/layout/AppHeader'; import { SaveProjectModal } from '../components/layout/SaveProjectModal'; import { LoginPromptModal } from '../components/layout/LoginPromptModal'; import { useSimulatorStore } from '../store/useSimulatorStore'; import { useOscilloscopeStore } from '../store/useOscilloscopeStore'; import { useAuthStore } from '../store/useAuthStore'; import type { CompilationLog } from '../utils/compilationLogger'; import '../App.css'; const MOBILE_BREAKPOINT = 768; 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(null); const resizingRef = useRef(false); const serialMonitorOpen = useSimulatorStore((s) => s.serialMonitorOpen); const oscilloscopeOpen = useOscilloscopeStore((s) => s.open); const [consoleOpen, setConsoleOpen] = useState(false); const [compileLogs, setCompileLogs] = useState([]); 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 [isMobile, setIsMobile] = useState(() => window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches); // Default to 'circuit' on mobile — the visual simulation is the primary content const [mobileView, setMobileView] = useState<'code' | 'circuit'>('circuit'); const user = useAuthStore((s) => s.user); const handleSaveClick = useCallback(() => { if (!user) { setLoginPromptOpen(true); } else { setSaveModalOpen(true); } }, [user]); // Track mobile breakpoint useEffect(() => { const mq = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`); const update = (e: MediaQueryListEvent | MediaQueryList) => { const mobile = e.matches; setIsMobile(mobile); if (mobile) setExplorerOpen(false); }; update(mq); mq.addEventListener('change', update); return () => mq.removeEventListener('change', update); }, []); // 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]); // Prevent body scroll on the editor page useEffect(() => { const html = document.documentElement; const body = document.body; html.style.overflow = 'hidden'; body.style.overflow = 'hidden'; window.scrollTo(0, 0); return () => { html.style.overflow = ''; body.style.overflow = ''; }; }, []); 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 (
{/* ── Editor side ── */}
{/* File explorer sidebar + resize handle */} {explorerOpen && ( <>
{!isMobile && (
)} )} {/* Editor main area */}
{/* Explorer toggle + toolbar */}
{/* File tabs */} {/* Monaco editor */}
{/* Console */} {consoleOpen && ( <>
setConsoleOpen(false)} logs={compileLogs} onClear={() => setCompileLogs([])} />
)}
{/* Resize handle (desktop only) */} {!isMobile && (
)} {/* ── Simulator side ── */}
{serialMonitorOpen && ( <>
)} {oscilloscopeOpen && ( <>
)}
{/* ── Mobile tab bar ── */} {isMobile && ( )} {saveModalOpen && setSaveModalOpen(false)} />} {loginPromptOpen && setLoginPromptOpen(false)} />}
); };