feat(frontend): DeployProgress UI, SerialMonitor BLE toggle, EmbedBridge improvements

- Add DeployProgress component (compile/transfer/flash progress bar)
- SerialMonitor: add BLE/Hardware deploy toggle support
- EditorToolbar: wire deploy state to progress
- EmbedBridge: improved message passing for embed mode
- useSimulatorStore: add deploy state tracking
- CodeEditor: disable stickyScroll, config tweaks
- nginx.conf: remove legacy redirect config
- package.json: dependency cleanup
This commit is contained in:
a2nr 2026-07-18 06:56:14 +07:00
parent 9402b6c40f
commit 867bbf3de9
14 changed files with 289 additions and 140 deletions

View File

@ -75,10 +75,6 @@ server {
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml application/rss+xml;
# Frontend SPA routing must be last so specific locations above take precedence
location = / {
return 301 /velxio/editor;
}
location / {
try_files $uri $uri/ /index.html;
}

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<!-- ═══════════════════════════════════════════════
PRIMARY SEO

View File

@ -28,7 +28,7 @@
"@types/node": "^24.11.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react": "^5.2.0",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/ui": "^4.0.18",
"eslint": "^9.39.1",
@ -38,77 +38,16 @@
"jsdom": "^28.1.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1",
"vite": "^7.3.6",
"vitest": "^4.0.18"
}
},
"../wokwi-libs/avr8js": {
"version": "0.21.0",
"license": "MIT",
"devDependencies": {
"@types/node": "^22.0.0",
"@wokwi/elements": "^1.9.1",
"eslint": "^9.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-json": "^4.0.1",
"eslint-plugin-prettier": "^5.5.5",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
"prettier": "^3.8.1",
"prettier-plugin-organize-imports": "^4.1.0",
"rimraf": "^6.1.2",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.55.0",
"vite": "^7.3.1",
"vitest": "^4.0.18"
},
"engines": {
"node": ">= 20.19.0",
"npm": ">= 10.0.0"
}
"version": "1.0.0"
},
"../wokwi-libs/wokwi-elements": {
"name": "@wokwi/elements",
"version": "1.9.2",
"license": "MIT",
"dependencies": {
"@types/react": ">=16",
"lit": "^3.0.0"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/runtime": "^7.15.3",
"@lit-labs/ssr": "^3.3.1",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.1",
"@storybook/addon-docs": "9.1.1",
"@storybook/web-components": "^9.1.1",
"@storybook/web-components-vite": "^9.1.1",
"@typescript-eslint/eslint-plugin": "^8.39.0",
"@typescript-eslint/parser": "^8.39.0",
"babel-loader": "^9.1.0",
"eslint": "^8.27.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-json": "^4.0.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-storybook": "^9.1.1",
"husky": "^8.0.0",
"hygen": "^6.1.0",
"lint-staged": "^10.2.11",
"prettier": "^3.5.3",
"react-is": "^16.13.1",
"rimraf": "^3.0.2",
"rollup": "^4.59.0",
"sharp": "^0.34.3",
"terser": "^5.43.1",
"typescript": "^5.9.0",
"vitest": "^3.2.4",
"web-component-analyzer": "^2.0.0"
},
"engines": {
"node": ">=20"
}
"version": "1.0.0"
},
"node_modules/@acemir/cssom": {
"version": "0.9.31",
@ -2176,9 +2115,9 @@
}
},
"node_modules/@vitejs/plugin-react": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz",
"integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
"integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2193,7 +2132,7 @@
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/@vitest/coverage-v8": {
@ -5007,13 +4946,13 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"version": "7.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
"integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.27.0",
"esbuild": "^0.27.0 || ^0.28.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",

View File

@ -40,7 +40,7 @@
"@types/node": "^24.11.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react": "^5.2.0",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/ui": "^4.0.18",
"eslint": "^9.39.1",
@ -50,7 +50,7 @@
"jsdom": "^28.1.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1",
"vite": "^7.3.6",
"vitest": "^4.0.18"
}
}

View File

@ -4,169 +4,169 @@
<url>
<loc>https://velxio.dev/</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>weekly</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://velxio.dev/editor</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/examples</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://velxio.dev/docs</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://velxio.dev/docs/intro</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://velxio.dev/docs/getting-started</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://velxio.dev/docs/emulator</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/esp32-emulation</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/riscv-emulation</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/rp2040-emulation</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/raspberry-pi3-emulation</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/components</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/architecture</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/wokwi-libs</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/mcp</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/setup</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://velxio.dev/docs/roadmap</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://velxio.dev/arduino-simulator</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/arduino-emulator</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/atmega328p-simulator</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://velxio.dev/arduino-mega-simulator</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://velxio.dev/esp32-simulator</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/esp32-s3-simulator</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://velxio.dev/esp32-c3-simulator</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://velxio.dev/raspberry-pi-pico-simulator</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/raspberry-pi-simulator</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://velxio.dev/v2</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/about</loc>
<lastmod>2026-04-01</lastmod>
<lastmod>2026-07-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>

View File

@ -14,7 +14,8 @@ body {
/* ── App shell ──────────────────────────────────── */
.app {
height: 100vh;
height: 100%; /* Fill iframe container — allows iframe scroll when content overflows */
min-height: 100dvh; /* But at least viewport height when no overflow */
display: flex;
flex-direction: column;
overflow: hidden;
@ -160,7 +161,8 @@ body {
.app-container {
flex: 1;
display: flex;
overflow: hidden;
overflow-y: auto; /* Allow scroll if content taller than container */
overflow-x: hidden;
position: relative;
}

View File

@ -41,26 +41,29 @@ export const CodeEditor = () => {
}, []);
return (
<div style={{ height: '100%', width: '100%' }}>
<Editor
// key forces a fresh editor instance per file (preserves undo/redo per file)
key={activeFileId}
height="100%"
language={activeFile ? getLanguage(activeFile.name) : 'cpp'}
theme={theme}
value={activeFile?.content ?? ''}
onChange={(value) => {
if (activeFileId) setFileContent(activeFileId, value || '');
}}
onMount={handleEditorMount}
options={{
minimap: { enabled: true },
fontSize,
automaticLayout: true,
scrollBeyondLastLine: false,
wordWrap: 'on',
}}
/>
<div style={{ height: '100%', width: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, minHeight: 0 }}>
<Editor
// key forces a fresh editor instance per file (preserves undo/redo per file)
key={activeFileId}
height="100%"
language={activeFile ? getLanguage(activeFile.name) : 'cpp'}
theme={theme}
value={activeFile?.content ?? ''}
onChange={(value) => {
if (activeFileId) setFileContent(activeFileId, value || '');
}}
onMount={handleEditorMount}
options={{
minimap: { enabled: true },
fontSize,
automaticLayout: true,
scrollBeyondLastLine: true,
wordWrap: 'on',
stickyScroll: { enabled: false },
}}
/>
</div>
</div>
);
};

View File

@ -0,0 +1,66 @@
/* DeployProgress — overlay bar shown below the editor toolbar during hardware deploy. */
.deploy-progress {
position: absolute;
left: 0;
right: 0;
bottom: -32px;
display: flex;
flex-direction: column;
gap: 6px;
padding: 6px 12px;
background: #1e1e1e;
border-bottom: 1px solid #333;
font-size: 12px;
z-index: 50;
}
.deploy-progress-header {
display: flex;
justify-content: space-between;
align-items: center;
color: #ccc;
gap: 8px;
}
.deploy-progress-message {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.deploy-progress-counter {
color: #888;
font-size: 11px;
font-family: monospace;
white-space: nowrap;
}
.deploy-progress-bar-container {
width: 100%;
height: 6px;
background: #333;
border-radius: 3px;
overflow: hidden;
}
.deploy-progress-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease, background 0.3s ease;
}
.deploy-progress-spinner {
width: 16px;
height: 16px;
border: 2px solid #333;
border-top-color: #3b82f6;
border-radius: 50%;
animation: deploy-spin 0.8s linear infinite;
margin: 2px auto;
}
@keyframes deploy-spin {
to { transform: rotate(360deg); }
}

View File

@ -0,0 +1,69 @@
import React from 'react';
import type { DeployState } from '../../types/deployer';
import './DeployProgress.css';
interface DeployProgressProps {
state: DeployState;
message: string;
completed: number;
total: number;
percent: number;
}
const STATE_GRADIENT: Record<string, string> = {
compiling: 'linear-gradient(90deg, #3b82f6, #60a5fa)', // blue
transferring: 'linear-gradient(90deg, #f59e0b, #fbbf24)', // amber/yellow
flashing: 'linear-gradient(90deg, #22c55e, #4ade80)', // green
serial_bridge:'linear-gradient(90deg, #8b5cf6, #a78bfa)', // purple
success: 'linear-gradient(90deg, #22c55e, #4ade80)', // green
};
const STATE_SPINNER_COLOR: Record<string, string> = {
compiling: '#3b82f6',
transferring: '#f59e0b',
flashing: '#22c55e',
serial_bridge:'#8b5cf6',
};
const DEFAULT_GRADIENT = 'linear-gradient(90deg, #3b82f6, #22c55e)';
const DEFAULT_SPINNER_COLOR = '#3b82f6';
export const DeployProgress: React.FC<DeployProgressProps> = ({
state,
message,
completed,
total,
percent,
}) => {
const showBar = total > 0 && (state === 'transferring' || state === 'flashing' || state === 'serial_bridge');
const showSpinner = state === 'compiling' || state === 'pairing' || (state === 'transferring' && total === 0);
const barGradient = STATE_GRADIENT[state] ?? DEFAULT_GRADIENT;
const spinnerColor = STATE_SPINNER_COLOR[state] ?? DEFAULT_SPINNER_COLOR;
return (
<div className="deploy-progress">
<div className="deploy-progress-header">
<span className="deploy-progress-message">{message}</span>
{showBar && (
<span className="deploy-progress-counter">
Chunk {completed}/{total} ({percent}%)
</span>
)}
</div>
{showBar && (
<div className="deploy-progress-bar-container">
<div
className="deploy-progress-bar-fill"
style={{ width: `${percent}%`, background: barGradient }}
/>
</div>
)}
{showSpinner && (
<div
className="deploy-progress-spinner"
style={{ borderTopColor: spinnerColor }}
/>
)}
</div>
);
};

View File

@ -147,6 +147,8 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
markCompiled();
setMissingLibHint(false);
embedBridge.notifyCompileResult(true);
// NEW: push hex to parent (embed mode only — EmbedBridge.send is no-op if not embedded)
embedBridge.notifyHexReady(activeBoardId, program, activeBoard?.boardKind ?? null);
} else {
const errText = result.error || result.stderr || 'Compile failed';
setMessage({ type: 'error', text: errText });

View File

@ -57,8 +57,21 @@ export const SerialMonitor: React.FC = () => {
const [inputValue, setInputValue] = useState('');
const [lineEnding, setLineEnding] = useState<'none' | 'nl' | 'cr' | 'both'>('nl');
const [autoscroll, setAutoscroll] = useState(true);
const [hardwareSerialActive, setHardwareSerialActive] = useState(false);
const outputRef = useRef<HTMLPreElement>(null);
// Subscribe to CustomEvents from EmbedBridge for hardware serial
useEffect(() => {
const onStart = () => setHardwareSerialActive(true);
const onStop = () => setHardwareSerialActive(false);
window.addEventListener('velxio-hardware-serial-start', onStart);
window.addEventListener('velxio-hardware-serial-stop', onStop);
return () => {
window.removeEventListener('velxio-hardware-serial-start', onStart);
window.removeEventListener('velxio-hardware-serial-stop', onStop);
};
}, []);
// Sync active tab to activeBoardId when it changes
useEffect(() => {
if (activeBoardId) setActiveTabId(activeBoardId);
@ -153,6 +166,20 @@ export const SerialMonitor: React.FC = () => {
{activeBoard?.serialBaudRate != null && activeBoard.serialBaudRate > 0 && (
<span style={styles.baudRate}>{activeBoard.serialBaudRate.toLocaleString()} baud</span>
)}
{hardwareSerialActive && (
<span style={{
color: '#4ec9b0',
fontSize: 11,
fontFamily: 'monospace',
background: '#1e1e1e',
border: '1px solid #4ec9b0',
borderRadius: 3,
padding: '1px 6px',
fontWeight: 600,
}}>
Hardware
</span>
)}
<label style={styles.autoscrollLabel}>
<input
type="checkbox"
@ -216,7 +243,11 @@ export const SerialMonitor: React.FC = () => {
return parts;
}
return text;
})() : (activeBoard?.running ? 'Waiting for serial data...\n' : 'Start simulation to see serial output.\n')}
})() : (activeBoard?.running ? (
hardwareSerialActive
? 'Hardware serial connected. Waiting for data...\n'
: 'Waiting for serial data...\n'
) : 'Start simulation to see serial output.\n')}
</pre>
{/* Input row */}

View File

@ -62,6 +62,7 @@ export const EditorPage: React.FC = () => {
enabled: params.get('embed') === 'true',
hideEditor: params.get('hideEditor') === 'true',
lockComponents: params.get('lockComponents') === 'true',
desktopLayout: params.get('desktopLayout') === 'true',
};
});
const [embedHideComponentPicker, setEmbedHideComponentPicker] = useState(false);
@ -146,8 +147,10 @@ export const EditorPage: React.FC = () => {
const [explorerOpen, setExplorerOpen] = useState(true);
const [explorerWidth, setExplorerWidth] = useState(EXPLORER_DEFAULT);
const [isMobile, setIsMobile] = useState(() => window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches);
// Embed mode on mobile: show circuit first (student needs to wire); standalone: show code
const [mobileView, setMobileView] = useState<'code' | 'circuit'>(embedMode.enabled ? 'circuit' : 'code');
// Embed mode with desktopLayout: show code first on mobile (developer playground)
const [mobileView, setMobileView] = useState<'code' | 'circuit'>(
embedMode.desktopLayout ? 'code' : embedMode.enabled ? 'circuit' : 'code'
);
const user = useAuthStore((s) => s.user);
const handleSaveClick = useCallback(() => {
@ -183,16 +186,17 @@ export const EditorPage: React.FC = () => {
return () => window.removeEventListener('keydown', handler);
}, [handleSaveClick]);
// Prevent body scroll on the editor page
// Prevent scroll gestures but allow viewport adjustment when keyboard appears
useEffect(() => {
const html = document.documentElement;
const body = document.body;
html.style.overflow = 'hidden';
body.style.overflow = 'hidden';
window.scrollTo(0, 0);
// Use touch-action: none to prevent scroll gestures without blocking
// browser viewport resize when virtual keyboard appears
html.style.touchAction = 'none';
body.style.touchAction = 'none';
return () => {
html.style.overflow = '';
body.style.overflow = '';
html.style.touchAction = '';
body.style.touchAction = '';
};
}, []);
@ -268,7 +272,7 @@ export const EditorPage: React.FC = () => {
{!embedMode.enabled && <AppHeader />}
{/* ── Mobile tab bar (top, above panels) ── */}
{isMobile && !(embedMode.enabled && embedMode.hideEditor) && (
{(isMobile || embedMode.desktopLayout) && !(embedMode.enabled && embedMode.hideEditor) && (
<nav className="mobile-tab-bar">
<button
className={`mobile-tab-btn${mobileView === 'code' ? ' mobile-tab-btn--active' : ''}`}

View File

@ -35,6 +35,11 @@ class EmbedBridge {
this.send('velxio:compile_result', { success });
}
/** Notify parent that compilation finished and hex is available. Push model. */
notifyHexReady(boardId: string | null, hex: string | null, boardKind: string | null) {
this.send('velxio:hex_ready', { boardId, hex, boardKind });
}
/** Parent received ready — stop broadcasting. */
private stopReadyBroadcast() {
if (this._readyInterval) {
@ -137,6 +142,27 @@ class EmbedBridge {
break;
}
case 'elemes:push_serial': {
const text = (event.data.text as string) || '';
const targetBoardId = (event.data.boardId as string) || null;
const store = useSimulatorStore.getState();
const boardId = targetBoardId ?? (store.activeBoardId as string | null);
if (boardId) {
store.appendSerialOutput(boardId, text);
}
break;
}
case 'elemes:start_hardware_serial': {
window.dispatchEvent(new CustomEvent('velxio-hardware-serial-start'));
break;
}
case 'elemes:stop_hardware_serial': {
window.dispatchEvent(new CustomEvent('velxio-hardware-serial-stop'));
break;
}
case 'elemes:ping': {
// Parent missed the initial ready signal — re-send it
this.notifyReady();

View File

@ -263,6 +263,7 @@ interface SimulatorState {
serialWriteToBoard: (boardId: string, text: string) => void;
clearSerialOutput: () => void;
clearBoardSerialOutput: (boardId: string) => void;
appendSerialOutput: (boardId: string, text: string) => void;
}
// ── Helper: create a simulator for a given board kind ─────────────────────
@ -1343,6 +1344,16 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
boards: s.boards.map((b) => b.id === boardId ? { ...b, serialOutput: '' } : b),
}));
},
appendSerialOutput: (boardId: string, text: string) => {
const isActive = get().activeBoardId === boardId;
set((s) => ({
...(isActive ? { serialOutput: s.serialOutput + text } : {}),
boards: s.boards.map((b) =>
b.id === boardId ? { ...b, serialOutput: (b.serialOutput ?? '') + text } : b
),
}));
},
};
});