feat(elemes): wire EmbedBridge, embedMode URL params, and auth/projects routers for LMS v3.0.4

This commit is contained in:
a2nr 2026-08-29 10:35:49 +07:00
parent af72e7cc3f
commit bf457e17b8
4 changed files with 77 additions and 20 deletions

View File

@ -112,6 +112,13 @@ app.include_router(flash.router, prefix="/api/flash", tags=["flash"])
# them out of upstream entirely — they now live under the private overlay's
# pro/backend/app/api/routes/ and are registered by register_pro(app)
# below. The OSS image carries none of them: anonymous, stateless.
try:
from app.api.routes.auth import router as auth_router
from app.api.routes.projects import router as projects_router
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
app.include_router(projects_router, prefix="/api", tags=["projects"])
except ImportError:
pass
# WebSockets
from app.api.routes import simulation

View File

@ -113,6 +113,11 @@
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://velxio.dev/examples/esp32-micropython-external-library/</loc>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://velxio.dev/examples/esp32-idf-blink/</loc>
<changefreq>monthly</changefreq>

View File

@ -5,6 +5,7 @@ import './index.css';
// useTranslation() always resolves against a live instance. Must come
// before App.
import './i18n';
import './services/EmbedBridge';
import { markProExamplesSettled } from './data/examples';
import { markProRoutesSettled } from './lib/proRoutes';

View File

@ -34,6 +34,7 @@ import { useAutoSaveProject } from '../hooks/useAutoSaveProject';
import { registerEditorCommand } from '../lib/editorCommands';
import { whenNewsClear } from '../lib/newsGate';
import { EditorMenuBar } from '../components/editor/EditorMenuBar';
import { embedBridge } from '../services/EmbedBridge';
import type { CompilationLog } from '../utils/compilationLogger';
import '../App.css';
@ -79,6 +80,35 @@ export const EditorPage: React.FC = () => {
// currentProject has a UUID — see useAutoSaveProject for the gating rules).
const autoSave = useAutoSaveProject();
// Embed mode: detect ?embed=true and optional flags from URL
const [embedMode] = useState(() => {
const params = new URLSearchParams(window.location.search);
return {
enabled: params.get('embed') === 'true',
hideEditor: params.get('hideEditor') === 'true',
lockComponents: params.get('lockComponents') === 'true',
desktopLayout: params.get('desktopLayout') === 'true',
};
});
const setLockComponents = useSimulatorStore((s) => s.setLockComponents);
useEffect(() => {
if (embedMode.lockComponents) {
setLockComponents(true);
}
}, [embedMode.lockComponents, setLockComponents]);
useEffect(() => {
if (!embedMode.enabled) return;
const handler = (e: Event) => {
const detail = (e as CustomEvent).detail || {};
if (detail.lockComponents !== undefined) setLockComponents(detail.lockComponents);
};
window.addEventListener('velxio-embed-mode', handler);
embedBridge.notifyReady();
return () => window.removeEventListener('velxio-embed-mode', handler);
}, [embedMode.enabled]);
const [editorWidthPct, setEditorWidthPct] = useState(45);
// Desktop-only 3-way layout switch (code-only / circuit-only / both).
// Lets users hide a pane to give the right-docked chat more room.
@ -136,6 +166,7 @@ export const EditorPage: React.FC = () => {
// The 2.5s bound applies only while the news decision is pending, so a
// dead feed can't hold the dialog hostage — see lib/newsGate.ts.
useEffect(() => {
if (embedMode.enabled) return;
if (starterDialogShownThisLoad) return;
const locale = getLocaleFromPath(window.location.pathname);
// /editor is prerendered, so a fresh load arrives as /editor/ (nginx
@ -237,7 +268,9 @@ export const EditorPage: React.FC = () => {
// that doesn't reflow when the editor/canvas splitter is dragged.
const [canvasHeaderSlot, setCanvasHeaderSlot] = useState<HTMLDivElement | null>(null);
// Default to 'code' on mobile — show the editor so users can write/view code
const [mobileView, setMobileView] = useState<'code' | 'circuit'>('code');
const [mobileView, setMobileView] = useState<'code' | 'circuit'>(
embedMode.desktopLayout ? 'code' : embedMode.enabled ? 'circuit' : 'code'
);
// Save is dispatched to the pro overlay, which inspects auth state and
// shows the right modal (Save vs Login prompt). In OSS without the
@ -550,13 +583,15 @@ export const EditorPage: React.FC = () => {
return (
<div className="app">
<AppHeader
editorMenu={!isMobile ? <EditorMenuBar /> : undefined}
editorToolbar={unifiedToolbar}
/>
{!embedMode.enabled && (
<AppHeader
editorMenu={!isMobile ? <EditorMenuBar /> : undefined}
editorToolbar={unifiedToolbar}
/>
)}
{/* ── Mobile tab bar (top, above panels) ── */}
{isMobile && (
{(isMobile || embedMode.desktopLayout) && !(embedMode.enabled && embedMode.hideEditor) && (
<nav className="mobile-tab-bar">
<button
className={`mobile-tab-btn${mobileView === 'code' ? ' mobile-tab-btn--active' : ''}`}
@ -607,7 +642,9 @@ export const EditorPage: React.FC = () => {
<div
className="editor-panel"
style={{
width: isMobile
width: (embedMode.enabled && embedMode.hideEditor)
? '0%'
: isMobile
? '100%'
: viewMode === 'code'
? '100%'
@ -615,7 +652,9 @@ export const EditorPage: React.FC = () => {
? '0%'
: `${editorWidthPct}%`,
display:
(isMobile && mobileView !== 'code') || (!isMobile && viewMode === 'circuit')
(embedMode.enabled && embedMode.hideEditor) ||
(isMobile && mobileView !== 'code') ||
(!isMobile && viewMode === 'circuit')
? 'none'
: 'flex',
flexDirection: 'row',
@ -728,7 +767,7 @@ export const EditorPage: React.FC = () => {
</div>
{/* Resize handle (desktop only, and only when both panes are visible) */}
{!isMobile && viewMode === 'both' && (
{!isMobile && viewMode === 'both' && !(embedMode.enabled && embedMode.hideEditor) && (
<div className="resize-handle" onMouseDown={handleResizeMouseDown}>
<div className="resize-handle-grip" />
</div>
@ -738,17 +777,20 @@ export const EditorPage: React.FC = () => {
<div
className="simulator-panel"
style={{
width: isMobile
width: (embedMode.enabled && embedMode.hideEditor)
? '100%'
: isMobile
? '100%'
: viewMode === 'circuit'
? '100%'
: viewMode === 'code'
? '0%'
: `${100 - editorWidthPct}%`,
display:
(isMobile && mobileView !== 'circuit') || (!isMobile && viewMode === 'code')
? 'none'
: 'flex',
display: (embedMode.enabled && embedMode.hideEditor)
? 'flex'
: (isMobile && mobileView !== 'circuit') || (!isMobile && viewMode === 'code')
? 'none'
: 'flex',
flexDirection: 'column',
}}
>
@ -782,18 +824,20 @@ export const EditorPage: React.FC = () => {
</div>
</div>
{showStarBanner && (
{!embedMode.enabled && showStarBanner && (
<GitHubStarBanner
onClose={handleDismissStarBanner}
onStarClick={handleStarClick}
round={starRound}
/>
)}
<NewsAnnouncer />
<NewProjectDialog
isOpen={showNewProjectDialog}
onClose={() => setShowNewProjectDialog(false)}
/>
{!embedMode.enabled && <NewsAnnouncer />}
{!embedMode.enabled && (
<NewProjectDialog
isOpen={showNewProjectDialog}
onClose={() => setShowNewProjectDialog(false)}
/>
)}
{/* Slot reserved for the private pro overlay (e.g. agent chat panel).
Self-hosted builds without an overlay see nothing here. */}
<div data-velxio-slot="agent-chat" />