/** * Editor Page — main editor + simulator with resizable panels */ import React, { useRef, useState, useCallback, useEffect, lazy, Suspense } from 'react'; import { useSEO } from '../utils/useSEO'; import { CodeEditor } from '../components/editor/CodeEditor'; import { EditorToolbar } from '../components/editor/EditorToolbar'; import { FileTabs } from '../components/editor/FileTabs'; import { FileExplorer } from '../components/editor/FileExplorer'; // Lazy-load Pi workspace so xterm.js isn't in the main bundle const RaspberryPiWorkspace = lazy(() => import('../components/raspberry-pi/RaspberryPiWorkspace').then((m) => ({ default: m.RaspberryPiWorkspace })) ); 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 { GitHubStarBanner } from '../components/layout/GitHubStarBanner'; 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 = () => { useSEO({ title: 'Multi-Board Simulator Editor — Arduino, ESP32, RP2040, RISC-V | Velxio', description: 'Write, compile and simulate Arduino, ESP32, Raspberry Pi Pico, ESP32-C3, and Raspberry Pi 3 code in your browser. 19 boards, 5 CPU architectures, 48+ components. Free and open-source.', url: 'https://velxio.dev/editor', }); const [editorWidthPct, setEditorWidthPct] = useState(45); const containerRef = useRef(null); const resizingRef = useRef(false); const serialMonitorOpen = useSimulatorStore((s) => s.serialMonitorOpen); const activeBoardId = useSimulatorStore((s) => s.activeBoardId); const activeBoardKind = useSimulatorStore((s) => s.boards.find((b) => b.id === s.activeBoardId)?.boardKind ); const isRaspberryPi3 = activeBoardKind === 'raspberry-pi-3'; 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 [showStarBanner, setShowStarBanner] = useState(false); // ── GitHub star prompt (show once: 2nd visit OR after 3 min) ────────────── useEffect(() => { const STAR_KEY = 'velxio_star_prompted'; const VISITS_KEY = 'velxio_editor_visits'; const FIRST_VISIT_KEY = 'velxio_editor_first_visit'; const THREE_MIN = 3 * 60 * 1000; if (localStorage.getItem(STAR_KEY)) return; // Increment visit counter const visits = parseInt(localStorage.getItem(VISITS_KEY) ?? '0', 10) + 1; localStorage.setItem(VISITS_KEY, String(visits)); // Record timestamp of first visit if (!localStorage.getItem(FIRST_VISIT_KEY)) { localStorage.setItem(FIRST_VISIT_KEY, String(Date.now())); } const firstVisit = parseInt(localStorage.getItem(FIRST_VISIT_KEY)!, 10); // Show immediately on second+ visit if (visits >= 2) { setShowStarBanner(true); return; } // Otherwise schedule after the 3-minute mark const elapsed = Date.now() - firstVisit; const delay = Math.max(0, THREE_MIN - elapsed); const timer = setTimeout(() => { if (!localStorage.getItem(STAR_KEY)) setShowStarBanner(true); }, delay); return () => clearTimeout(timer); }, []); const handleDismissStarBanner = () => { localStorage.setItem('velxio_star_prompted', '1'); setShowStarBanner(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 'code' on mobile — show the editor so users can write/view code const [mobileView, setMobileView] = useState<'code' | 'circuit'>('code'); 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 (
{/* ── Mobile tab bar (top, above panels) ── */} {isMobile && ( )}
{/* ── Editor side ── */}
{/* File explorer sidebar + resize handle */} {explorerOpen && ( <>
{!isMobile && (
)} )} {/* Editor main area */}
{/* Explorer toggle + toolbar */}
{/* File tabs — hidden when Pi workspace is active */} {!isRaspberryPi3 && } {/* Editor area: Pi workspace or Monaco editor */}
{isRaspberryPi3 && activeBoardId ? ( Loading Pi workspace…
}> ) : ( )}
{/* Console */} {consoleOpen && ( <>
setConsoleOpen(false)} logs={compileLogs} onClear={() => setCompileLogs([])} />
)}
{/* Resize handle (desktop only) */} {!isMobile && (
)} {/* ── Simulator side ── */}
{serialMonitorOpen && ( <>
)} {oscilloscopeOpen && ( <>
)}
{saveModalOpen && setSaveModalOpen(false)} />} {loginPromptOpen && setLoginPromptOpen(false)} />} {showStarBanner && }
); };