2026-03-04 08:25:52 +07:00
/ * *
2026-03-05 08:05:23 +07:00
* Editor Page — main editor + simulator with resizable panels
2026-03-04 08:25:52 +07:00
* /
2026-03-16 00:04:01 +07:00
import React , { useRef , useState , useCallback , useEffect , lazy , Suspense } from 'react' ;
feat: electrical simulation via ngspice-WASM (eecircuit-engine)
Adds full SPICE-accurate electrical simulation to Velxio, behind a lazy-
loaded ⚡ toolbar toggle. Arduino / ESP32 / RP2040 sketches now co-simulate
with real analog behaviour: correct voltages on wires, real I–V curves on
LEDs, working potentiometers, NTC thermistors read by analogRead(), PWM
driving RC filters, transistors, op-amps, diodes, MOSFETs, etc.
Engine: eecircuit-engine (ngspice compiled to WebAssembly). Main bundle
stays at 2.4 MB; the 20 MB SPICE chunk only loads when the user activates
electrical mode. Disabled at build time via VITE_ELECTRICAL_SIM=false.
Frontend additions:
- simulation/spice/: SpiceEngine wrapper + lazy entry, NetlistBuilder with
UnionFind over wires, componentToSpice mapping (24 metadataIds incl.
real part numbers: 2N2222, 2N3055, BC547, IRF540, 2N7000, 1N4148,
1N4007, 1N4733, LEDs, NTC, op-amp ideal), CircuitScheduler with
debounced coalescing, AVRSpiceBridge for quasi-static co-simulation.
- store/useElectricalStore: Zustand slice, feature-flag aware.
- components/analog-ui/: ⚡ toolbar toggle + SVG voltage overlay.
- components/components-instruments/: Voltmeter, Ammeter probes.
- 62 tests (spice-*, netlist-builder, component-to-spice, instruments).
Sandbox (test/test_circuit/): 47-test validation sandbox that proved
the approach (hand-rolled MNA baseline + ngspice pipeline) before
porting to the app. Kept as reference.
Docs: docs/wiki/circuit-emulation-*.md (13 engineering pages covering
architecture, solvers, components, AVR bridge, gotchas, performance,
integration plan, API reference, appendix) + electrical-simulation-
user-guide.md (end-user facing).
Reference plan: test/test_circuit/plan/phase_8_velxio_implementation.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:11:54 +07:00
import { wireElectricalSolver } from '../simulation/spice/subscribeToStore' ;
2026-03-23 23:32:15 +07:00
import { useSEO } from '../utils/useSEO' ;
2026-03-04 08:25:52 +07:00
import { CodeEditor } from '../components/editor/CodeEditor' ;
import { EditorToolbar } from '../components/editor/EditorToolbar' ;
2026-03-06 20:14:50 +07:00
import { FileTabs } from '../components/editor/FileTabs' ;
import { FileExplorer } from '../components/editor/FileExplorer' ;
2026-03-16 00:04:01 +07:00
// Lazy-load Pi workspace so xterm.js isn't in the main bundle
const RaspberryPiWorkspace = lazy ( ( ) = >
2026-04-22 02:45:45 +07:00
import ( '../components/raspberry-pi/RaspberryPiWorkspace' ) . then ( ( m ) = > ( {
default : m . RaspberryPiWorkspace ,
} ) ) ,
2026-03-16 00:04:01 +07:00
) ;
2026-03-06 07:07:10 +07:00
import { CompilationConsole } from '../components/editor/CompilationConsole' ;
2026-03-04 08:25:52 +07:00
import { SimulatorCanvas } from '../components/simulator/SimulatorCanvas' ;
2026-03-05 16:56:14 +07:00
import { SerialMonitor } from '../components/simulator/SerialMonitor' ;
2026-03-11 22:09:24 +07:00
import { Oscilloscope } from '../components/simulator/Oscilloscope' ;
2026-03-06 20:14:50 +07:00
import { AppHeader } from '../components/layout/AppHeader' ;
import { SaveProjectModal } from '../components/layout/SaveProjectModal' ;
import { LoginPromptModal } from '../components/layout/LoginPromptModal' ;
2026-03-24 00:02:35 +07:00
import { GitHubStarBanner } from '../components/layout/GitHubStarBanner' ;
2026-05-01 01:27:58 +07:00
import { useSimulatorStore , DEFAULT_BOARD_POSITION } from '../store/useSimulatorStore' ;
2026-03-11 22:09:24 +07:00
import { useOscilloscopeStore } from '../store/useOscilloscopeStore' ;
2026-03-06 20:14:50 +07:00
import { useAuthStore } from '../store/useAuthStore' ;
2026-05-01 01:27:58 +07:00
import { useProjectStore } from '../store/useProjectStore' ;
2026-03-06 07:07:10 +07:00
import type { CompilationLog } from '../utils/compilationLogger' ;
2026-03-04 08:25:52 +07:00
import '../App.css' ;
2026-03-10 23:09:32 +07:00
const MOBILE_BREAKPOINT = 768 ;
2026-03-06 07:07:10 +07:00
const BOTTOM_PANEL_MIN = 80 ;
const BOTTOM_PANEL_MAX = 600 ;
const BOTTOM_PANEL_DEFAULT = 200 ;
2026-03-06 21:20:47 +07:00
const EXPLORER_MIN = 120 ;
const EXPLORER_MAX = 500 ;
const EXPLORER_DEFAULT = 210 ;
2026-03-06 07:07:10 +07:00
const resizeHandleStyle : React.CSSProperties = {
height : 5 ,
flexShrink : 0 ,
cursor : 'row-resize' ,
background : '#2a2d2e' ,
borderTop : '1px solid #3c3c3c' ,
borderBottom : '1px solid #3c3c3c' ,
} ;
2026-03-04 08:25:52 +07:00
export const EditorPage : React.FC = ( ) = > {
2026-03-23 23:32:15 +07:00
useSEO ( {
2026-03-23 23:38:56 +07:00
title : 'Multi-Board Simulator Editor — Arduino, ESP32, RP2040, RISC-V | Velxio' ,
2026-03-23 23:32:15 +07:00
description :
2026-03-23 23:38:56 +07:00
'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.' ,
2026-03-23 23:32:15 +07:00
url : 'https://velxio.dev/editor' ,
} ) ;
2026-03-05 08:05:23 +07:00
const [ editorWidthPct , setEditorWidthPct ] = useState ( 45 ) ;
const containerRef = useRef < HTMLDivElement > ( null ) ;
const resizingRef = useRef ( false ) ;
2026-03-05 16:56:14 +07:00
const serialMonitorOpen = useSimulatorStore ( ( s ) = > s . serialMonitorOpen ) ;
2026-03-16 00:04:01 +07:00
const activeBoardId = useSimulatorStore ( ( s ) = > s . activeBoardId ) ;
2026-04-22 02:45:45 +07:00
const activeBoardKind = useSimulatorStore (
( s ) = > s . boards . find ( ( b ) = > b . id === s . activeBoardId ) ? . boardKind ,
2026-03-16 00:04:01 +07:00
) ;
const isRaspberryPi3 = activeBoardKind === 'raspberry-pi-3' ;
2026-03-11 22:09:24 +07:00
const oscilloscopeOpen = useOscilloscopeStore ( ( s ) = > s . open ) ;
2026-03-06 07:07:10 +07:00
const [ consoleOpen , setConsoleOpen ] = useState ( false ) ;
const [ compileLogs , setCompileLogs ] = useState < CompilationLog [ ] > ( [ ] ) ;
const [ bottomPanelHeight , setBottomPanelHeight ] = useState ( BOTTOM_PANEL_DEFAULT ) ;
2026-03-06 20:14:50 +07:00
const [ saveModalOpen , setSaveModalOpen ] = useState ( false ) ;
const [ loginPromptOpen , setLoginPromptOpen ] = useState ( false ) ;
2026-03-24 00:02:35 +07:00
const [ showStarBanner , setShowStarBanner ] = useState ( false ) ;
feat: electrical simulation via ngspice-WASM (eecircuit-engine)
Adds full SPICE-accurate electrical simulation to Velxio, behind a lazy-
loaded ⚡ toolbar toggle. Arduino / ESP32 / RP2040 sketches now co-simulate
with real analog behaviour: correct voltages on wires, real I–V curves on
LEDs, working potentiometers, NTC thermistors read by analogRead(), PWM
driving RC filters, transistors, op-amps, diodes, MOSFETs, etc.
Engine: eecircuit-engine (ngspice compiled to WebAssembly). Main bundle
stays at 2.4 MB; the 20 MB SPICE chunk only loads when the user activates
electrical mode. Disabled at build time via VITE_ELECTRICAL_SIM=false.
Frontend additions:
- simulation/spice/: SpiceEngine wrapper + lazy entry, NetlistBuilder with
UnionFind over wires, componentToSpice mapping (24 metadataIds incl.
real part numbers: 2N2222, 2N3055, BC547, IRF540, 2N7000, 1N4148,
1N4007, 1N4733, LEDs, NTC, op-amp ideal), CircuitScheduler with
debounced coalescing, AVRSpiceBridge for quasi-static co-simulation.
- store/useElectricalStore: Zustand slice, feature-flag aware.
- components/analog-ui/: ⚡ toolbar toggle + SVG voltage overlay.
- components/components-instruments/: Voltmeter, Ammeter probes.
- 62 tests (spice-*, netlist-builder, component-to-spice, instruments).
Sandbox (test/test_circuit/): 47-test validation sandbox that proved
the approach (hand-rolled MNA baseline + ngspice pipeline) before
porting to the app. Kept as reference.
Docs: docs/wiki/circuit-emulation-*.md (13 engineering pages covering
architecture, solvers, components, AVR bridge, gotchas, performance,
integration plan, API reference, appendix) + electrical-simulation-
user-guide.md (end-user facing).
Reference plan: test/test_circuit/plan/phase_8_velxio_implementation.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:11:54 +07:00
// ── Electrical simulation subscriber (one-time, idempotent) ───────────────
useEffect ( ( ) = > {
const unsub = wireElectricalSolver ( ) ;
return unsub ;
} , [ ] ) ;
2026-03-24 00:02:35 +07:00
// ── 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 ) ;
} ;
2026-03-06 20:14:50 +07:00
const [ explorerOpen , setExplorerOpen ] = useState ( true ) ;
2026-03-06 21:20:47 +07:00
const [ explorerWidth , setExplorerWidth ] = useState ( EXPLORER_DEFAULT ) ;
2026-04-22 02:45:45 +07:00
const [ isMobile , setIsMobile ] = useState (
( ) = > window . matchMedia ( ` (max-width: ${ MOBILE_BREAKPOINT } px) ` ) . matches ,
) ;
2026-04-29 10:21:26 +07:00
// Slot element for SimulatorCanvas to portal its header into. When set, the
// canvas board selector / Serial / Scope / zoom / Add buttons render here
// instead of above the canvas — keeping the top bar a single full-width row
// that doesn't reflow when the editor/canvas splitter is dragged.
const [ canvasHeaderSlot , setCanvasHeaderSlot ] = useState < HTMLDivElement | null > ( null ) ;
2026-03-24 03:15:29 +07:00
// Default to 'code' on mobile — show the editor so users can write/view code
const [ mobileView , setMobileView ] = useState < 'code' | 'circuit' > ( 'code' ) ;
2026-03-06 20:14:50 +07:00
const user = useAuthStore ( ( s ) = > s . user ) ;
const handleSaveClick = useCallback ( ( ) = > {
if ( ! user ) {
setLoginPromptOpen ( true ) ;
} else {
setSaveModalOpen ( true ) ;
}
} , [ user ] ) ;
2026-05-01 01:27:58 +07:00
const handleNewClick = useCallback ( ( ) = > {
if (
! window . confirm (
'Start a new workspace? This clears every board, component, wire and file. This cannot be undone.' ,
)
) {
return ;
}
const sim = useSimulatorStore . getState ( ) ;
sim . boards . forEach ( ( b ) = > sim . stopBoard ( b . id ) ) ;
const ids = sim . boards . map ( ( b ) = > b . id ) ;
ids . forEach ( ( id ) = > sim . removeBoard ( id ) ) ;
sim . setComponents ( [ ] ) ;
sim . setWires ( [ ] ) ;
useProjectStore . getState ( ) . clearCurrentProject ( ) ;
const newId = useSimulatorStore
. getState ( )
. addBoard ( 'arduino-uno' , DEFAULT_BOARD_POSITION . x , DEFAULT_BOARD_POSITION . y ) ;
useSimulatorStore . getState ( ) . setActiveBoardId ( newId ) ;
} , [ ] ) ;
2026-03-10 23:09:32 +07:00
// 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 ) ;
} , [ ] ) ;
2026-03-06 20:14:50 +07:00
// 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 ] ) ;
2026-03-05 08:05:23 +07:00
2026-03-08 03:54:51 +07:00
// 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 = '' ;
} ;
} , [ ] ) ;
2026-03-05 08:05:23 +07:00
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 ) ;
} , [ ] ) ;
2026-04-22 02:45:45 +07:00
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 ] ,
) ;
2026-03-06 21:20:47 +07:00
2026-04-22 02:45:45 +07:00
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 ] ,
) ;
2026-03-06 21:20:47 +07:00
2026-03-04 08:25:52 +07:00
return (
< div className = "app" >
2026-03-06 20:24:03 +07:00
< AppHeader / >
2026-03-05 08:05:23 +07:00
2026-03-24 03:15:29 +07:00
{ /* ── Mobile tab bar (top, above panels) ── */ }
{ isMobile && (
< nav className = "mobile-tab-bar" >
< button
className = { ` mobile-tab-btn ${ mobileView === 'code' ? ' mobile-tab-btn--active' : '' } ` }
onClick = { ( ) = > setMobileView ( 'code' ) }
>
2026-04-22 02:45:45 +07:00
< svg
width = "16"
height = "16"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
2026-03-24 03:15:29 +07:00
< polyline points = "16 18 22 12 16 6" / >
< polyline points = "8 6 2 12 8 18" / >
< / svg >
< span > & lt ; / & g t ; C o d e < / s p a n >
< / button >
< button
className = { ` mobile-tab-btn ${ mobileView === 'circuit' ? ' mobile-tab-btn--active' : '' } ` }
onClick = { ( ) = > setMobileView ( 'circuit' ) }
>
2026-04-22 02:45:45 +07:00
< svg
width = "16"
height = "16"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
2026-03-24 03:15:29 +07:00
< rect x = "2" y = "7" width = "20" height = "14" rx = "2" / >
< path d = "M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" / >
< line x1 = "12" y1 = "12" x2 = "12" y2 = "16" / >
< line x1 = "10" y1 = "14" x2 = "14" y2 = "14" / >
< / svg >
< span > Circuit < / span >
< / button >
< / nav >
) }
2026-04-29 10:21:26 +07:00
{ / * ─ ─ U n i f i e d t o p t o o l b a r ( d e s k t o p o n l y ) ─ ─
Editor controls + canvas controls share a single full - width row so
the bar doesn ' t reflow when the editor / canvas splitter is dragged .
The canvas controls ( board selector , Serial , Scope , zoom , Add ) are
portaled into ` canvasHeaderSlot ` from inside SimulatorCanvas . * / }
{ ! isMobile && (
< div className = "unified-toolbar" >
< button
className = "explorer-toggle-btn unified-toolbar-explorer-toggle"
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 className = "unified-toolbar-editor" >
< EditorToolbar
consoleOpen = { consoleOpen }
setConsoleOpen = { setConsoleOpen }
compileLogs = { compileLogs }
setCompileLogs = { setCompileLogs }
centerSlot = { ! isRaspberryPi3 ? < FileTabs / > : null }
/ >
< / div >
< div className = "unified-toolbar-canvas" ref = { setCanvasHeaderSlot } / >
< / div >
) }
2026-03-05 08:05:23 +07:00
< div className = "app-container" ref = { containerRef } >
2026-03-06 20:14:50 +07:00
{ /* ── Editor side ── */ }
< div
className = "editor-panel"
2026-03-10 23:09:32 +07:00
style = { {
width : isMobile ? '100%' : ` ${ editorWidthPct } % ` ,
display : isMobile && mobileView !== 'code' ? 'none' : 'flex' ,
flexDirection : 'row' ,
} }
2026-03-06 20:14:50 +07:00
>
2026-03-06 21:20:47 +07:00
{ /* File explorer sidebar + resize handle */ }
{ explorerOpen && (
< >
2026-04-22 02:45:45 +07:00
< div
style = { { width : explorerWidth , flexShrink : 0 , display : 'flex' , overflow : 'hidden' } }
>
2026-05-01 01:27:58 +07:00
< FileExplorer onSaveClick = { handleSaveClick } onNewClick = { handleNewClick } / >
2026-03-06 21:20:47 +07:00
< / div >
2026-03-10 23:09:32 +07:00
{ ! isMobile && (
2026-04-22 02:45:45 +07:00
< div
className = "explorer-resize-handle"
onMouseDown = { handleExplorerResizeMouseDown }
/ >
2026-03-10 23:09:32 +07:00
) }
2026-03-06 21:20:47 +07:00
< / >
) }
2026-03-06 20:14:50 +07:00
{ /* Editor main area */ }
2026-04-22 02:45:45 +07:00
< div
style = { {
flex : 1 ,
display : 'flex' ,
flexDirection : 'column' ,
overflow : 'hidden' ,
minWidth : 0 ,
} }
>
2026-04-29 10:21:26 +07:00
{ / * M o b i l e - o n l y : e x p l o r e r t o g g l e + e d i t o r t o o l b a r i n s i d e t h e p a n e l .
On desktop these are hoisted into the unified top toolbar . * / }
{ isMobile && (
< 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' }
2026-04-22 02:45:45 +07:00
>
2026-04-29 10:21:26 +07:00
< 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 , minWidth : 0 } } >
< EditorToolbar
consoleOpen = { consoleOpen }
setConsoleOpen = { setConsoleOpen }
compileLogs = { compileLogs }
setCompileLogs = { setCompileLogs }
centerSlot = { ! isRaspberryPi3 ? < FileTabs / > : null }
/ >
< / div >
2026-03-06 07:07:10 +07:00
< / div >
2026-04-29 10:21:26 +07:00
) }
2026-03-06 20:14:50 +07:00
2026-03-16 00:04:01 +07:00
{ /* Editor area: Pi workspace or Monaco editor */ }
2026-03-06 20:14:50 +07:00
< div className = "editor-wrapper" style = { { flex : 1 , overflow : 'hidden' , minHeight : 0 } } >
2026-03-16 00:04:01 +07:00
{ isRaspberryPi3 && activeBoardId ? (
2026-04-22 02:45:45 +07:00
< Suspense
fallback = {
< div style = { { color : '#666' , padding : 16 , fontSize : 12 } } >
Loading Pi workspace …
< / div >
}
>
2026-03-16 00:04:01 +07:00
< RaspberryPiWorkspace boardId = { activeBoardId } / >
< / Suspense >
) : (
< CodeEditor / >
) }
2026-03-06 20:14:50 +07:00
< / 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 >
2026-03-04 08:25:52 +07:00
< / div >
2026-03-05 08:05:23 +07:00
2026-03-10 23:09:32 +07:00
{ /* Resize handle (desktop only) */ }
{ ! isMobile && (
< div className = "resize-handle" onMouseDown = { handleResizeMouseDown } >
< div className = "resize-handle-grip" / >
< / div >
) }
2026-03-05 08:05:23 +07:00
2026-03-06 20:14:50 +07:00
{ /* ── Simulator side ── */ }
< div
className = "simulator-panel"
2026-03-10 23:09:32 +07:00
style = { {
width : isMobile ? '100%' : ` ${ 100 - editorWidthPct } % ` ,
display : isMobile && mobileView !== 'circuit' ? 'none' : 'flex' ,
flexDirection : 'column' ,
} }
2026-03-06 20:14:50 +07:00
>
2026-03-06 07:07:10 +07:00
< div style = { { flex : 1 , overflow : 'hidden' , position : 'relative' , minHeight : 0 } } >
2026-04-29 10:21:26 +07:00
< SimulatorCanvas headerSlot = { ! isMobile ? canvasHeaderSlot : null } / >
2026-03-05 16:56:14 +07:00
< / div >
{ serialMonitorOpen && (
2026-03-06 07:07:10 +07:00
< >
< div
onMouseDown = { handleBottomPanelResizeMouseDown }
style = { resizeHandleStyle }
title = "Drag to resize"
/ >
< div style = { { height : bottomPanelHeight , flexShrink : 0 } } >
< SerialMonitor / >
< / div >
< / >
2026-03-05 16:56:14 +07:00
) }
2026-03-11 22:09:24 +07:00
{ oscilloscopeOpen && (
< >
< div
onMouseDown = { handleBottomPanelResizeMouseDown }
style = { resizeHandleStyle }
title = "Drag to resize"
/ >
< div style = { { height : bottomPanelHeight , flexShrink : 0 } } >
< Oscilloscope / >
< / div >
< / >
) }
2026-03-04 08:25:52 +07:00
< / div >
< / div >
2026-03-06 20:14:50 +07:00
{ saveModalOpen && < SaveProjectModal onClose = { ( ) = > setSaveModalOpen ( false ) } / > }
{ loginPromptOpen && < LoginPromptModal onClose = { ( ) = > setLoginPromptOpen ( false ) } / > }
2026-03-24 00:02:35 +07:00
{ showStarBanner && < GitHubStarBanner onClose = { handleDismissStarBanner } / > }
2026-03-04 08:25:52 +07:00
< / div >
) ;
} ;