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-07-29 00:43:20 +07:00
import React , { useRef , useState , useCallback , useEffect } from 'react' ;
feat(i18n): translate AdminPage + UserProfile + Pricing + EditorPage shell (Editor block 10 — final cleanup)
Closes the Phase 2 i18n rollout. Every visitor- and user-facing
surface velxio renders in normal use now reads from t().
AdminPage (admin-only)
- Header (panel title, logout) and the four tabs (Dashboard /
Users / Projects / Boards).
- Setup screen for first-admin creation (title, body, password
fields + mismatch error + create-admin button).
- Not-admin gate page.
- EditUserModal (title, four labels, admin/active toggles,
cancel/save).
- UsersTab: search placeholder, count pluralisation, all 12
table columns, Activity / Edit / Delete actions, empty state,
delete-confirm prompt with username interpolation.
- ProjectsTab: search placeholder, count pluralisation, all 9
table columns, public/private badge labels, delete action +
confirm with project-name interpolation, empty state.
- All error messages (load failed / save failed / delete failed)
fall back through t().
UserProfilePage
- "New project" CTA, loading + empty + not-found states,
"Private" project badge, "Copy shareable link" tooltip.
- The /editor link uses localize() so /es/<username>'s "New
project" button stays in Spanish.
PricingPlaceholder
- Title + the two paragraphs (self-hosted note + hosted Pro
tier note + GitHub source note). Inline links wrapped via
the Trans component so the link surface stays clickable in
every locale without each translation having to re-write the
HTML.
EditorPage shell
- Mobile bottom-tab labels (Code / Circuit), file-explorer
toggle (Show / Hide), View mode aria-label, view-mode
segmented control labels (Code / Both / Circuit), and the
three "Drag to resize" handle tooltips on the panel splitters.
Translations
- en.json hand-curated for the new keys.
- All 8 non-English locales auto-translated via the existing
`npm run translate:i18n` pipeline (DeepSeek, ~5 min for the
whole bundle, sameShape() validates each output before write).
This closes Phase 2 of i18n. Phase 3 (DocsPage prose, AboutPage
long-form paragraphs, the 15 SEO landing pages) is deliberately
deferred — Docs/About are best handled by extracting the prose
into JSON keys and running the same script, while the SEO pages
are intentionally optimised for English keyword targeting and
should not be machine-translated en masse.
2026-05-09 22:49:13 +07:00
import { useTranslation } from 'react-i18next' ;
2026-05-16 02:46:34 +07:00
import { startSimulation } from '../simulation/spice/start' ;
2026-03-23 23:32:15 +07:00
import { useSEO } from '../utils/useSEO' ;
2026-07-18 13:24:06 +07:00
import { getLocaleFromPath , localizedPath } from '../i18n/path' ;
2026-07-17 11:27:28 +07:00
import { restoreStashedWorkspace } from '../utils/workspaceDraft' ;
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 { 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
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' ;
refactor(oss-split): remove auth/admin/profile frontend from OSS
Phase 3 of the OSS / pro split — frontend side. Phase 2 already moved
the auth/DB stack out of the OSS backend; this commit does the same
for the React app. After this, the OSS image is editor + simulator
+ landing + docs only.
What moved to the private overlay (pro/frontend/src/pro/):
pages/{Login,Register,ForgotPassword,ResetPassword}Page.tsx
pages/{Admin,UserProfile,Project,ProjectById}Page.tsx
components/admin/{AdminBoardsTab,AdminDashboardTab,UserActivityModal}.tsx
components/layout/{SaveProjectModal,LoginPromptModal}.tsx
services/{authService,adminService}.ts
store/useAuthStore.ts
hooks/autoSaveImpl.ts
New seams added so OSS components stay decoupled:
* lib/proRoutes.ts — registerProRoutes()/useProRoutes() via
useSyncExternalStore. mountPro() injects the moved pages at runtime;
App.tsx subscribes to the registry, so registration after the
initial render re-renders without a Not-Found flash.
* lib/proSession.ts — registerSessionCheck()/triggerSessionCheck().
App.tsx fires this on mount instead of useAuthStore.checkSession();
pure OSS no-ops.
* lib/proSaveAction.ts — installSaveActionImpl()/triggerSaveAction().
EditorPage's Save button dispatches through this; the overlay
decides whether to show SaveProjectModal or LoginPromptModal based
on auth state. In OSS without an overlay it's a no-op today; in
Phase 4 of the split it becomes the .vlx Export entry point.
OSS-side rewrites:
* App.tsx drops the 8 page imports + 8 route entries; uses
triggerSessionCheck() instead of useAuthStore directly.
* AppHeader.tsx drops the user/login/register block entirely. The
header-auth slot (introduced in Phase 1) now stays empty in OSS
and gets filled by the overlay's portal mount.
* EditorPage.tsx drops useAuthStore + SaveProjectModal +
LoginPromptModal imports. The Save handler is now triggerSaveAction().
* LandingPage.tsx drops the dead UserMenu component (defined but
never rendered) + its useAuthStore imports.
* main.tsx drops the side-effect import of hooks/autoSaveImpl — the
impl lives in pro now and self-registers via mountPro().
Build config:
* vite.config.ts adds @velxio alias → src/. Lets the overlay import
upstream modules (lib/proRoutes etc.) by stable name regardless of
whether it's symlinked (local dev) or COPYed (Docker).
* preserveSymlinks now gated on VITE_PRO_BUILD only (not on serve
mode). Needed so Rollup keeps the overlay logically inside src/pro/
during local junction-based builds.
Build verification:
* OSS-only: 20-ish routes, no /login, /admin, /:username — 285 SEO
pages prerendered. Bundle drops ~80-120 KB.
* OSS + overlay: full 38 routes (30 upstream + 8 from registerProRoutes),
HeaderAuth dropdown injected via slot, save action wired to the
overlay's modal flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:31:12 +07:00
import { triggerSaveAction } from '../lib/proSaveAction' ;
2026-03-24 00:02:35 +07:00
import { GitHubStarBanner } from '../components/layout/GitHubStarBanner' ;
feat(news): announce with a toast, open the modal only on request
Entering the editor could stack two overlays: the starter-template
dialog opened on mount, then four seconds later the "What's new" modal
landed on top of it. Two dimmed backdrops over each other, and the
smaller, *less* important surface in front — the z-indexes said it
outright, news 10000 against the starter's 9000.
The delay made it worse rather than gentler. Arriving after the user has
started reading the templates is the most disruptive moment available;
appearing instantly would at least have let them clear it before
engaging.
The rule this restores: a modal is for what BLOCKS, and an announcement
never blocks. News now slides into the corner as a toast that dims
nothing, steals no focus (role="status", polite), and retires by itself.
The full post is one click behind "Read more" — a modal the user asked
for is a categorically different thing from one thrown at them — and
stays reachable from Help ▸ What's new once the toast is gone.
Worth noting the audience argument too: the starter dialog only fires on
a PRISTINE visit, i.e. for someone brand new, and "what's new" is
meaningless without a baseline to compare against. Returning users, the
ones it is actually written for, arrive with a project loaded and never
saw the collision.
Renamed NewsModal → NewsAnnouncer: it is not a modal by default any more,
and leaving the old name would have mislead the next reader.
Strings are new keys under `news.*`, filled in for all nine locales — the
previous modal had "What's new" and "Got it" hardcoded in English.
Toast sits at z-index 8000, deliberately BELOW the starter dialog, so if
the two ever coincide again the blocking task keeps the foreground.
2026-08-11 00:52:55 +07:00
import { NewsAnnouncer } from '../components/layout/NewsAnnouncer' ;
2026-08-08 01:11:30 +07:00
import { useSimulatorStore } from '../store/useSimulatorStore' ;
2026-05-08 10:53:27 +07:00
import { useEditorStore } from '../store/useEditorStore' ;
2026-05-14 21:44:05 +07:00
import { useCompileLogsStore } from '../store/useCompileLogsStore' ;
2026-03-11 22:09:24 +07:00
import { useOscilloscopeStore } from '../store/useOscilloscopeStore' ;
2026-05-01 01:27:58 +07:00
import { useProjectStore } from '../store/useProjectStore' ;
2026-08-08 02:07:48 +07:00
import {
NewProjectDialog ,
clearWorkspaceForStarter ,
} from '../components/editor/NewProjectDialog' ;
feat: persist multi-board projects + add auto-save
The project save/load pipeline only persisted a single `board_type`, so
multi-board workspaces silently lost every board except the active one
on save, and wires referencing the dropped boards' IDs orphaned to the
canvas corner on reload. An audit of the production backup found 74/306
projects (24%) with at least one orphaned wire and 174/301 non-trivial
projects whose code was still the default Blink template — strong signal
that users save once and never re-save.
Backend
- Add `boards_json` column on `projects` with idempotent ALTER TABLE in
the lifespan migration list.
- New `FileGroup` schema + `file_groups` array on
ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat.
- `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via
`read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on
read; legacy single-list `files` only updates the active group, leaving
other boards' files intact.
- `_persist_files_from_body` honors file_groups → files → code priority.
Frontend
- `useSimulatorStore.addBoard` accepts an optional `explicitId` so
saved board IDs can be restored verbatim (wires reference IDs literally).
- New `loadProjectState({boards, fileGroups, components, wires,
activeBoardId})` action: tears down current boards, recreates from the
payload, restores file groups atomically, recalculates wire positions
on the next frame, and refreshes the Interconnect.
- `useEditorStore.replaceFileGroups` for atomic multi-group restore.
- `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through
`buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects
by synthesising a default board from `board_type`).
Auto-save (#useAutoSaveProject hook)
- 2.5s debounced silent PUT triggered ONLY when an authenticated user
has a `currentProject` with a UUID. State hash detects real changes
vs. UI-only churn; baseline is reset on project load so the just-loaded
state isn't immediately re-saved.
- `beforeunload` flush via `fetch keepalive: true` (supports PUT +
credentials, survives unload).
- Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error).
Backfill script (one-off, idempotent)
- `backend/scripts/backfill_boards_2026_05.py` populates `boards_json`
for legacy projects. Heuristic per project, based on which board IDs
the wires reference:
Case A — wires only ref 'arduino-uno' but board_type ≠ uno:
rename id→board_type and rewrite wire endpoints.
Case B — single-board normal: keep verbatim.
Case C — multi-board: recreate one board per distinct ref, infer
kind by stripping trailing -N suffix.
Also moves any flat files into the active board's group subdir.
Stdlib-only, runs from host or `docker exec`.
Docker
- `Dockerfile.standalone` now copies `backend/scripts/` into the image
so the backfill is callable via `docker exec velxio-app python
/app/scripts/backfill_boards_2026_05.py --apply`.
Verified locally on the restored production backup (363 projects):
33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans.
Re-running the script after apply skips all 363 (idempotent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
import { useAutoSaveProject } from '../hooks/useAutoSaveProject' ;
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
import { registerEditorCommand } from '../lib/editorCommands' ;
2026-08-12 01:12:32 +07:00
import { whenNewsClear } from '../lib/newsGate' ;
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
import { EditorMenuBar } from '../components/editor/EditorMenuBar' ;
2026-08-29 10:35:49 +07:00
import { embedBridge } from '../services/EmbedBridge' ;
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-08-18 09:54:49 +07:00
const EXPLORER_MIN = 100 ;
2026-03-06 21:20:47 +07:00
const EXPLORER_MAX = 500 ;
fix(editor): the workspace panel stacks its actions, and the toolbar stops growing a hole
Two places where the editor bar spent width on nothing.
The file explorer put its label and its buttons on one line: WORKSPACE next
to new/open/save, and every board section's name next to rename / new file /
new folder plus the status dot. Between them those set a floor under the
panel's width, and the board name — the one thing a user reads there — got
whatever was left ("M5 Cardput..." in a 150px pane). Both headers now stack:
the label owns its row, the actions sit above (board) or below (workspace),
and the status dot rides in the actions row, where it costs the name
nothing. The actions row keeps its height reserved whether or not the
buttons are showing, so hovering a section never shoves the files below it
down. Default pane width 150 -> 124px; the freed px go to the code editor.
The toolbar's center slot is a flexible spacer that pins the right icons to
the far right, and nothing renders into it (no caller passes centerSlot). So
whenever the bar had width to spare — closing the docked AI chat is exactly
that — the spacer ate it: 191px of black between Reset and Libraries on a
1600px window. Empty, it no longer grows, so the editor controls stay packed
and the surplus collects at the one boundary that already carries a divider,
in front of the canvas controls (which belong over the canvas pane).
2026-08-18 23:42:33 +07:00
// Narrow by default: the explorer rows are compact and both headers stack
// their actions ABOVE their label instead of beside it (see
// FileExplorer.css), so nothing has to share a line — 124px fits a board
// name and typical file names, and every px saved here goes to the code
// editor.
const EXPLORER_DEFAULT = 124 ;
2026-03-06 21:20:47 +07:00
2026-08-08 01:11:30 +07:00
// Once per full page load: the pristine-visit starter dialog must not pop
// again when the user closes it and later navigates away from and back to
// /editor within the same SPA session.
let starterDialogShownThisLoad = false ;
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 = ( ) = > {
feat(i18n): translate AdminPage + UserProfile + Pricing + EditorPage shell (Editor block 10 — final cleanup)
Closes the Phase 2 i18n rollout. Every visitor- and user-facing
surface velxio renders in normal use now reads from t().
AdminPage (admin-only)
- Header (panel title, logout) and the four tabs (Dashboard /
Users / Projects / Boards).
- Setup screen for first-admin creation (title, body, password
fields + mismatch error + create-admin button).
- Not-admin gate page.
- EditUserModal (title, four labels, admin/active toggles,
cancel/save).
- UsersTab: search placeholder, count pluralisation, all 12
table columns, Activity / Edit / Delete actions, empty state,
delete-confirm prompt with username interpolation.
- ProjectsTab: search placeholder, count pluralisation, all 9
table columns, public/private badge labels, delete action +
confirm with project-name interpolation, empty state.
- All error messages (load failed / save failed / delete failed)
fall back through t().
UserProfilePage
- "New project" CTA, loading + empty + not-found states,
"Private" project badge, "Copy shareable link" tooltip.
- The /editor link uses localize() so /es/<username>'s "New
project" button stays in Spanish.
PricingPlaceholder
- Title + the two paragraphs (self-hosted note + hosted Pro
tier note + GitHub source note). Inline links wrapped via
the Trans component so the link surface stays clickable in
every locale without each translation having to re-write the
HTML.
EditorPage shell
- Mobile bottom-tab labels (Code / Circuit), file-explorer
toggle (Show / Hide), View mode aria-label, view-mode
segmented control labels (Code / Both / Circuit), and the
three "Drag to resize" handle tooltips on the panel splitters.
Translations
- en.json hand-curated for the new keys.
- All 8 non-English locales auto-translated via the existing
`npm run translate:i18n` pipeline (DeepSeek, ~5 min for the
whole bundle, sameShape() validates each output before write).
This closes Phase 2 of i18n. Phase 3 (DocsPage prose, AboutPage
long-form paragraphs, the 15 SEO landing pages) is deliberately
deferred — Docs/About are best handled by extracting the prose
into JSON keys and running the same script, while the SEO pages
are intentionally optimised for English keyword targeting and
should not be machine-translated en masse.
2026-05-09 22:49:13 +07:00
const { t } = useTranslation ( ) ;
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' ,
} ) ;
feat: persist multi-board projects + add auto-save
The project save/load pipeline only persisted a single `board_type`, so
multi-board workspaces silently lost every board except the active one
on save, and wires referencing the dropped boards' IDs orphaned to the
canvas corner on reload. An audit of the production backup found 74/306
projects (24%) with at least one orphaned wire and 174/301 non-trivial
projects whose code was still the default Blink template — strong signal
that users save once and never re-save.
Backend
- Add `boards_json` column on `projects` with idempotent ALTER TABLE in
the lifespan migration list.
- New `FileGroup` schema + `file_groups` array on
ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat.
- `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via
`read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on
read; legacy single-list `files` only updates the active group, leaving
other boards' files intact.
- `_persist_files_from_body` honors file_groups → files → code priority.
Frontend
- `useSimulatorStore.addBoard` accepts an optional `explicitId` so
saved board IDs can be restored verbatim (wires reference IDs literally).
- New `loadProjectState({boards, fileGroups, components, wires,
activeBoardId})` action: tears down current boards, recreates from the
payload, restores file groups atomically, recalculates wire positions
on the next frame, and refreshes the Interconnect.
- `useEditorStore.replaceFileGroups` for atomic multi-group restore.
- `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through
`buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects
by synthesising a default board from `board_type`).
Auto-save (#useAutoSaveProject hook)
- 2.5s debounced silent PUT triggered ONLY when an authenticated user
has a `currentProject` with a UUID. State hash detects real changes
vs. UI-only churn; baseline is reset on project load so the just-loaded
state isn't immediately re-saved.
- `beforeunload` flush via `fetch keepalive: true` (supports PUT +
credentials, survives unload).
- Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error).
Backfill script (one-off, idempotent)
- `backend/scripts/backfill_boards_2026_05.py` populates `boards_json`
for legacy projects. Heuristic per project, based on which board IDs
the wires reference:
Case A — wires only ref 'arduino-uno' but board_type ≠ uno:
rename id→board_type and rewrite wire endpoints.
Case B — single-board normal: keep verbatim.
Case C — multi-board: recreate one board per distinct ref, infer
kind by stripping trailing -N suffix.
Also moves any flat files into the active board's group subdir.
Stdlib-only, runs from host or `docker exec`.
Docker
- `Dockerfile.standalone` now copies `backend/scripts/` into the image
so the backfill is callable via `docker exec velxio-app python
/app/scripts/backfill_boards_2026_05.py --apply`.
Verified locally on the restored production backup (363 projects):
33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans.
Re-running the script after apply skips all 363 (idempotent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
// Silent auto-save for the loaded project (only fires when authed AND
// currentProject has a UUID — see useAutoSaveProject for the gating rules).
const autoSave = useAutoSaveProject ( ) ;
2026-08-29 10:35:49 +07:00
// 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 ( ( ) = > {
2026-09-05 09:41:12 +07:00
if ( embedMode . lockComponents && typeof setLockComponents === 'function' ) {
2026-08-29 10:35:49 +07:00
setLockComponents ( true ) ;
}
} , [ embedMode . lockComponents , setLockComponents ] ) ;
useEffect ( ( ) = > {
if ( ! embedMode . enabled ) return ;
const handler = ( e : Event ) = > {
const detail = ( e as CustomEvent ) . detail || { } ;
2026-09-05 09:41:12 +07:00
if ( detail . lockComponents !== undefined && typeof setLockComponents === 'function' ) {
setLockComponents ( detail . lockComponents ) ;
}
2026-08-29 10:35:49 +07:00
} ;
window . addEventListener ( 'velxio-embed-mode' , handler ) ;
embedBridge . notifyReady ( ) ;
return ( ) = > window . removeEventListener ( 'velxio-embed-mode' , handler ) ;
2026-09-05 09:41:12 +07:00
} , [ embedMode . enabled , setLockComponents ] ) ;
2026-08-29 10:35:49 +07:00
2026-03-05 08:05:23 +07:00
const [ editorWidthPct , setEditorWidthPct ] = useState ( 45 ) ;
2026-05-08 10:53:27 +07:00
// Desktop-only 3-way layout switch (code-only / circuit-only / both).
// Lets users hide a pane to give the right-docked chat more room.
const viewMode = useEditorStore ( ( s ) = > s . viewMode ) ;
const setViewMode = useEditorStore ( ( s ) = > s . setViewMode ) ;
2026-08-18 05:17:38 +07:00
const explorerOpen = useEditorStore ( ( s ) = > s . explorerOpen ) ;
const setExplorerOpen = useEditorStore ( ( s ) = > s . setExplorerOpen ) ;
const toggleExplorer = useEditorStore ( ( s ) = > s . toggleExplorer ) ;
2026-03-05 08:05:23 +07:00
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-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 ) ;
2026-05-14 21:44:05 +07:00
// compileLogs live in a Zustand store so the velxio-pro agent overlay
// (mounted in a separate React tree via slotMounter) can subscribe and
// build a "diagnose this failure" prompt without prop-drilling.
const compileLogs = useCompileLogsStore ( ( s ) = > s . logs ) ;
const setCompileLogs = useCompileLogsStore ( ( s ) = > s . setLogs ) ;
2026-03-06 07:07:10 +07:00
const [ bottomPanelHeight , setBottomPanelHeight ] = useState ( BOTTOM_PANEL_DEFAULT ) ;
2026-03-24 00:02:35 +07:00
const [ showStarBanner , setShowStarBanner ] = useState ( false ) ;
2026-06-16 02:39:05 +07:00
const [ starRound , setStarRound ] = useState < 1 | 2 > ( 1 ) ;
2026-03-24 00:02:35 +07:00
2026-05-16 02:46:34 +07:00
// ── Electrical simulation (one-time mount) ────────────────────────────────
// `startSimulation()` is the single entry point: it constructs the
// CircuitSimulationService, mounts the ADC bridge, and subscribes
// PinManager → service.handleMcuEdge. No more legacy paths — the
// WASM ngspice (via NgSpiceWorkerAdapter) is the only solver.
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
useEffect ( ( ) = > {
2026-05-16 02:46:34 +07:00
return startSimulation ( ) ;
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
} , [ ] ) ;
2026-07-17 11:27:28 +07:00
// Restore an in-progress workspace stashed before a login redirect, so a
// user who was building something and signed in lands back on their circuit
// (not the empty starter board). One-shot; see utils/workspaceDraft.
useEffect ( ( ) = > {
restoreStashedWorkspace ( ) ;
} , [ ] ) ;
2026-08-08 01:11:30 +07:00
const [ showNewProjectDialog , setShowNewProjectDialog ] = useState ( false ) ;
2026-08-08 11:24:47 +07:00
// Pristine-visit starter dialog: a bare /editor landing still holds the
2026-08-08 01:11:30 +07:00
// hardcoded Uno + LED starter (see useSimulatorStore INITIAL_BOARD /
2026-08-08 11:24:47 +07:00
// builtin components). Clear it and offer the template picker over an
// EMPTY canvas instead of silently dropping the user into the Arduino
// blink (cancelling the dialog leaves a blank workspace). Guarded so it
2026-08-08 02:07:48 +07:00
// never fires over a loaded project/example URL, a restored login draft
// (both leave the stores non-pristine), or twice per page load. Declared
// AFTER the restoreStashedWorkspace effect — mount order is what makes
// the pristine check see the restored draft.
2026-08-12 01:12:32 +07:00
//
// Sequenced BEHIND the news announcement: the canvas is cleared right
// away, but the dialog itself waits until NewsAnnouncer reports the
// announcement flow is done (nothing to show, or its modal was closed).
// 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.
2026-08-08 01:11:30 +07:00
useEffect ( ( ) = > {
2026-08-29 10:35:49 +07:00
if ( embedMode . enabled ) return ;
2026-08-08 01:11:30 +07:00
if ( starterDialogShownThisLoad ) return ;
const locale = getLocaleFromPath ( window . location . pathname ) ;
2026-08-18 22:22:47 +07:00
// /editor is prerendered, so a fresh load arrives as /editor/ (nginx
// adds the slash); client-side navigation lands on /editor. Both count.
if ( window . location . pathname . replace ( /\/+$/ , '' ) !== localizedPath ( '/editor' , locale ) ) return ;
2026-08-08 01:11:30 +07:00
if ( window . location . search ) return ;
if ( useProjectStore . getState ( ) . currentProject ) return ;
const sim = useSimulatorStore . getState ( ) ;
const pristine =
sim . boards . length === 1 &&
sim . boards [ 0 ] . id === 'arduino-uno' &&
sim . components . length === 2 &&
sim . components . every ( ( c ) = > c . id === 'led_builtin' || c . id === 'r_builtin' ) ;
if ( ! pristine ) return ;
starterDialogShownThisLoad = true ;
2026-08-08 02:07:48 +07:00
clearWorkspaceForStarter ( ) ;
2026-08-12 01:12:32 +07:00
let cancelled = false ;
void whenNewsClear ( 2500 ) . then ( ( ) = > {
if ( ! cancelled ) setShowNewProjectDialog ( true ) ;
} ) ;
return ( ) = > {
cancelled = true ;
} ;
2026-08-08 01:11:30 +07:00
} , [ ] ) ;
2026-06-16 02:39:05 +07:00
// ── GitHub star prompt (show twice at most: 2nd visit OR after 3 min) ──────
// Three localStorage flags drive this:
// velxio_star_prompted → dismissed the first ask
// velxio_star_prompted_v2 → dismissed the follow-up ask (stop forever)
// velxio_star_clicked → clicked through to the repo (stop forever)
// Anyone who dismissed the first ask WITHOUT clicking through gets one
// follow-up (round 2) with a stronger message; clicking the repo link at
// any time opts them out permanently.
2026-03-24 00:02:35 +07:00
useEffect ( ( ) = > {
const STAR_KEY = 'velxio_star_prompted' ;
2026-06-16 02:39:05 +07:00
const STAR_KEY_V2 = 'velxio_star_prompted_v2' ;
const STAR_CLICKED_KEY = 'velxio_star_clicked' ;
2026-03-24 00:02:35 +07:00
const VISITS_KEY = 'velxio_editor_visits' ;
const FIRST_VISIT_KEY = 'velxio_editor_first_visit' ;
const THREE_MIN = 3 * 60 * 1000 ;
2026-06-16 02:39:05 +07:00
// Never bother people who already starred or already saw the follow-up.
if ( localStorage . getItem ( STAR_CLICKED_KEY ) ) return ;
if ( localStorage . getItem ( STAR_KEY_V2 ) ) return ;
// Round 2 = they dismissed the first ask (without clicking through).
const round = localStorage . getItem ( STAR_KEY ) ? 2 : 1 ;
setStarRound ( round ) ;
2026-03-24 00:02:35 +07:00
// 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 ( ( ) = > {
2026-06-16 02:39:05 +07:00
if ( ! localStorage . getItem ( STAR_CLICKED_KEY ) && ! localStorage . getItem ( STAR_KEY_V2 ) ) {
setShowStarBanner ( true ) ;
}
2026-03-24 00:02:35 +07:00
} , delay ) ;
return ( ) = > clearTimeout ( timer ) ;
} , [ ] ) ;
const handleDismissStarBanner = ( ) = > {
2026-06-16 02:39:05 +07:00
// First dismiss → mark round 1; second dismiss → mark round 2 (stop forever).
if ( localStorage . getItem ( 'velxio_star_prompted' ) ) {
localStorage . setItem ( 'velxio_star_prompted_v2' , '1' ) ;
} else {
localStorage . setItem ( 'velxio_star_prompted' , '1' ) ;
}
setShowStarBanner ( false ) ;
} ;
const handleStarClick = ( ) = > {
// They went to the repo — opt them out of any further prompts.
localStorage . setItem ( 'velxio_star_clicked' , '1' ) ;
2026-03-24 00:02:35 +07:00
setShowStarBanner ( false ) ;
} ;
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
2026-08-29 10:35:49 +07:00
const [ mobileView , setMobileView ] = useState < 'code' | 'circuit' > (
embedMode . desktopLayout ? 'code' : embedMode . enabled ? 'circuit' : 'code'
) ;
2026-03-06 20:14:50 +07:00
refactor(oss-split): remove auth/admin/profile frontend from OSS
Phase 3 of the OSS / pro split — frontend side. Phase 2 already moved
the auth/DB stack out of the OSS backend; this commit does the same
for the React app. After this, the OSS image is editor + simulator
+ landing + docs only.
What moved to the private overlay (pro/frontend/src/pro/):
pages/{Login,Register,ForgotPassword,ResetPassword}Page.tsx
pages/{Admin,UserProfile,Project,ProjectById}Page.tsx
components/admin/{AdminBoardsTab,AdminDashboardTab,UserActivityModal}.tsx
components/layout/{SaveProjectModal,LoginPromptModal}.tsx
services/{authService,adminService}.ts
store/useAuthStore.ts
hooks/autoSaveImpl.ts
New seams added so OSS components stay decoupled:
* lib/proRoutes.ts — registerProRoutes()/useProRoutes() via
useSyncExternalStore. mountPro() injects the moved pages at runtime;
App.tsx subscribes to the registry, so registration after the
initial render re-renders without a Not-Found flash.
* lib/proSession.ts — registerSessionCheck()/triggerSessionCheck().
App.tsx fires this on mount instead of useAuthStore.checkSession();
pure OSS no-ops.
* lib/proSaveAction.ts — installSaveActionImpl()/triggerSaveAction().
EditorPage's Save button dispatches through this; the overlay
decides whether to show SaveProjectModal or LoginPromptModal based
on auth state. In OSS without an overlay it's a no-op today; in
Phase 4 of the split it becomes the .vlx Export entry point.
OSS-side rewrites:
* App.tsx drops the 8 page imports + 8 route entries; uses
triggerSessionCheck() instead of useAuthStore directly.
* AppHeader.tsx drops the user/login/register block entirely. The
header-auth slot (introduced in Phase 1) now stays empty in OSS
and gets filled by the overlay's portal mount.
* EditorPage.tsx drops useAuthStore + SaveProjectModal +
LoginPromptModal imports. The Save handler is now triggerSaveAction().
* LandingPage.tsx drops the dead UserMenu component (defined but
never rendered) + its useAuthStore imports.
* main.tsx drops the side-effect import of hooks/autoSaveImpl — the
impl lives in pro now and self-registers via mountPro().
Build config:
* vite.config.ts adds @velxio alias → src/. Lets the overlay import
upstream modules (lib/proRoutes etc.) by stable name regardless of
whether it's symlinked (local dev) or COPYed (Docker).
* preserveSymlinks now gated on VITE_PRO_BUILD only (not on serve
mode). Needed so Rollup keeps the overlay logically inside src/pro/
during local junction-based builds.
Build verification:
* OSS-only: 20-ish routes, no /login, /admin, /:username — 285 SEO
pages prerendered. Bundle drops ~80-120 KB.
* OSS + overlay: full 38 routes (30 upstream + 8 from registerProRoutes),
HeaderAuth dropdown injected via slot, save action wired to the
overlay's modal flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:31:12 +07:00
// Save is dispatched to the pro overlay, which inspects auth state and
// shows the right modal (Save vs Login prompt). In OSS without the
// overlay this is a no-op today and becomes the .vlx Export entry
// point in Phase 4 of the OSS split.
2026-03-06 20:14:50 +07:00
const handleSaveClick = useCallback ( ( ) = > {
refactor(oss-split): remove auth/admin/profile frontend from OSS
Phase 3 of the OSS / pro split — frontend side. Phase 2 already moved
the auth/DB stack out of the OSS backend; this commit does the same
for the React app. After this, the OSS image is editor + simulator
+ landing + docs only.
What moved to the private overlay (pro/frontend/src/pro/):
pages/{Login,Register,ForgotPassword,ResetPassword}Page.tsx
pages/{Admin,UserProfile,Project,ProjectById}Page.tsx
components/admin/{AdminBoardsTab,AdminDashboardTab,UserActivityModal}.tsx
components/layout/{SaveProjectModal,LoginPromptModal}.tsx
services/{authService,adminService}.ts
store/useAuthStore.ts
hooks/autoSaveImpl.ts
New seams added so OSS components stay decoupled:
* lib/proRoutes.ts — registerProRoutes()/useProRoutes() via
useSyncExternalStore. mountPro() injects the moved pages at runtime;
App.tsx subscribes to the registry, so registration after the
initial render re-renders without a Not-Found flash.
* lib/proSession.ts — registerSessionCheck()/triggerSessionCheck().
App.tsx fires this on mount instead of useAuthStore.checkSession();
pure OSS no-ops.
* lib/proSaveAction.ts — installSaveActionImpl()/triggerSaveAction().
EditorPage's Save button dispatches through this; the overlay
decides whether to show SaveProjectModal or LoginPromptModal based
on auth state. In OSS without an overlay it's a no-op today; in
Phase 4 of the split it becomes the .vlx Export entry point.
OSS-side rewrites:
* App.tsx drops the 8 page imports + 8 route entries; uses
triggerSessionCheck() instead of useAuthStore directly.
* AppHeader.tsx drops the user/login/register block entirely. The
header-auth slot (introduced in Phase 1) now stays empty in OSS
and gets filled by the overlay's portal mount.
* EditorPage.tsx drops useAuthStore + SaveProjectModal +
LoginPromptModal imports. The Save handler is now triggerSaveAction().
* LandingPage.tsx drops the dead UserMenu component (defined but
never rendered) + its useAuthStore imports.
* main.tsx drops the side-effect import of hooks/autoSaveImpl — the
impl lives in pro now and self-registers via mountPro().
Build config:
* vite.config.ts adds @velxio alias → src/. Lets the overlay import
upstream modules (lib/proRoutes etc.) by stable name regardless of
whether it's symlinked (local dev) or COPYed (Docker).
* preserveSymlinks now gated on VITE_PRO_BUILD only (not on serve
mode). Needed so Rollup keeps the overlay logically inside src/pro/
during local junction-based builds.
Build verification:
* OSS-only: 20-ish routes, no /login, /admin, /:username — 285 SEO
pages prerendered. Bundle drops ~80-120 KB.
* OSS + overlay: full 38 routes (30 upstream + 8 from registerProRoutes),
HeaderAuth dropdown injected via slot, save action wired to the
overlay's modal flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:31:12 +07:00
triggerSaveAction ( ) ;
} , [ ] ) ;
2026-03-06 20:14:50 +07:00
2026-08-08 01:11:30 +07:00
// "New workspace" opens the starter-template dialog. Destruction moved
// from here into the dialog's selection handler — Cancel/ESC now leaves
// the current workspace untouched, so the old confirm dialog is gone.
const handleNewClick = useCallback ( ( ) = > {
setShowNewProjectDialog ( true ) ;
} , [ ] ) ;
2026-05-01 01:27:58 +07:00
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-08-18 05:17:38 +07:00
} , [ setExplorerOpen ] ) ;
2026-03-10 23:09:32 +07:00
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
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
// File-menu commands owned by this page. Registered here (not in the
// menu) so the menu never duplicates the confirm-dialog / autosave logic.
useEffect ( ( ) = > {
const offSave = registerEditorCommand ( 'project.save' , handleSaveClick ) ;
const offNew = registerEditorCommand ( 'project.new' , ( ) = > {
void handleNewClick ( ) ;
} ) ;
2026-08-18 05:17:38 +07:00
const offExplorer = registerEditorCommand ( 'view.toggleExplorer' , ( ) = > toggleExplorer ( ) ) ;
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
return ( ) = > {
offSave ( ) ;
offNew ( ) ;
feat(editor): View and Language menus — full desktop-menu parity
The menubar now matches the desktop app's native menu set: File, Edit,
View, Language, Help.
View collects what the user asked to reach from a menu: Compile (Ctrl+B),
Run, Stop, Reset up top; the panel toggles — File Explorer, Output
Console, Serial Monitor, Oscilloscope/Logic Analyzer — in the middle
(store-backed ones render a live check, like a real desktop menu); and
the canvas view actions (center, zoom) move here from Edit, which goes
back to being undo/redo only, as menus have always worked.
Language lists the nine locales with the current one checked, switching
through the same switchLocale path the header globe uses — so language is
reachable from the menubar regardless of the account state, on top of
living in the signed-in account menu.
Run/Stop/Compile/Reset and the console toggle register through the
editorCommands seam from EditorToolbar (their handlers close over its
state); the explorer toggle from EditorPage; serial and scope call their
stores directly, same as undo/redo.
2026-07-31 02:05:55 +07:00
offExplorer ( ) ;
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
} ;
2026-08-18 05:17:38 +07:00
} , [ handleSaveClick , handleNewClick , toggleExplorer ] ) ;
feat(editor): menus File/Edit en la cabecera y editor sin nav de marketing
La queja: demasiados botones en una sola fila, y en pantallas pequenas se
solapan — medido, no impresion: a 1024px Code/Both/Circuit pisaban a
Compile/Run/Stop por 9-20px y Add pisaba el chat por 29px, con el idioma y
el usuario cortados fuera del viewport.
Tres piezas:
1. En el editor, el nav de marketing (Home/Docs/Pricing/...) sobra: ya
estas dentro, y cuesta exactamente el ancho que le falta a la toolbar.
AppHeader gana la variante editorMenu — mismo mecanismo que ya usa la
build de escritorio (VITE_DESKTOP) — que oculta el nav y pinta un menu
File/Edit junto al logo. El logo sigue llevando a inicio; idioma,
autoguardado, Share y usuario se quedan.
2. File/Edit al estilo escritorio: lo que se usa cada minuto sigue siendo
boton; lo que se usa unas veces por sesion va al menu. File: nuevo
workspace, nuevo fichero, abrir, guardar, importar, exportar, BOM,
imagen del esquema. Edit: undo/redo (con su estado real del historial),
centrar vista, zoom. Las acciones viven en cierres de cuatro
componentes distintos, asi que hay un registro id->handler
(lib/editorCommands, mismo patron de seam que registerProExamples o
registerBoardBuiltins): cada dueno registra al montar y el menu invoca
por id; un item sin dueno montado se pinta deshabilitado, que ademas es
la verdad. El registro sobrevive al remontaje StrictMode (el cleanup
viejo no borra al sucesor) y tiene test de eso.
3. La toolbar adelgaza: fuera Undo/Redo (Ctrl+Z/Y siguen), fuera los
botones inline de Import/Export y sus gemelos responsivos del More
(~70px recuperados); el More queda solo con los extras pro (firmware,
GitHub, share, grabacion). El CSS responsivo de los botones retirados
se va con ellos.
Suite en verde (2300) y build del overlay verificado desde este arbol.
2026-07-30 21:26:36 +07:00
2026-05-08 22:11:53 +07:00
// Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z — canvas undo/redo. Skipped when the
// user is typing in any input/textarea/contenteditable so the Monaco
// editor's per-file history (and the AI chat composer, etc.) keep
// working untouched.
useEffect ( ( ) = > {
const handler = ( e : KeyboardEvent ) = > {
const t = e . target as HTMLElement | null ;
if ( t ) {
const tag = t . tagName ;
if ( tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || t . isContentEditable ) {
return ;
}
}
if ( ! ( e . ctrlKey || e . metaKey ) ) return ;
const k = e . key . toLowerCase ( ) ;
const sim = useSimulatorStore . getState ( ) ;
if ( k === 'z' && ! e . shiftKey ) {
e . preventDefault ( ) ;
sim . undo ( ) ;
} else if ( k === 'y' || ( k === 'z' && e . shiftKey ) ) {
e . preventDefault ( ) ;
sim . redo ( ) ;
}
} ;
window . addEventListener ( 'keydown' , handler ) ;
return ( ) = > window . removeEventListener ( 'keydown' , handler ) ;
} , [ ] ) ;
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 ;
2026-08-14 21:34:22 +07:00
const el = containerRef . current ;
const rect = el . getBoundingClientRect ( ) ;
// The panels' percentage widths resolve against the container's CONTENT
// box, but getBoundingClientRect() returns the border box — and overlays
// may reserve space by padding .app-container (e.g. a docked side panel).
// Measuring against the padded box makes the handle jump away from the
// cursor, so strip borders and padding first.
const cs = getComputedStyle ( el ) ;
const padLeft = parseFloat ( cs . paddingLeft ) || 0 ;
const padRight = parseFloat ( cs . paddingRight ) || 0 ;
const contentLeft = rect . left + el . clientLeft + padLeft ;
const contentWidth = el . clientWidth - padLeft - padRight ;
if ( contentWidth <= 0 ) return ;
const pct = ( ( ev . clientX - contentLeft ) / contentWidth ) * 100 ;
2026-03-05 08:05:23 +07:00
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-07-30 23:56:24 +07:00
/ * ─ ─ U n i f i e d t o o l b a r ( d e s k t o p ) ─ ─
Editor controls + canvas controls in one strip ; the canvas side is
portaled into ` canvasHeaderSlot ` from SimulatorCanvas . Since the
marketing nav left the editor header , the header ' s middle is empty
space — so this strip now rides INSIDE the AppHeader row ( via the
editorToolbar prop ) instead of being a second bar : one 44 px row where
there used to be 44 + 38 . On narrow widths the strip wraps internally
and the header grows ; the docked AI chat is avoided by the same
padding - right the strip always had . * /
2026-07-31 03:56:19 +07:00
/ * A c c o u n t b l o c k . F u s e d a s t h e e x p l o r e r p a n e l ' s f o o t e r s o a l o n g f i l e
tree scrolls ABOVE it instead of disappearing underneath a floating
box ; when the explorer is collapsed it falls back to the small fixed
corner box ( avatar only there — no room for a name ) . * /
2026-07-31 01:54:24 +07:00
const accountBlock = ! isMobile ? (
2026-07-31 03:56:19 +07:00
// Just the account button. Language lives in the menubar's Language
// menu (for everyone) and inside the account menu (for signed-in
// users) — the standalone globe crowded the footer for no gain.
< div data - velxio - slot = "header-auth" style = { { display : 'contents' } } / >
2026-07-31 01:54:24 +07:00
) : undefined ;
2026-07-30 23:56:24 +07:00
const unifiedToolbar = ! isMobile ? (
2026-04-29 10:21:26 +07:00
< div className = "unified-toolbar" >
2026-07-31 05:46:08 +07:00
{ /* View-mode toggle: explorer | Code / Both / Circuit — one
segmented group so the four pane switches read as one control .
2026-05-08 10:53:27 +07:00
Hidden on mobile — there ' s already a code / circuit toggle in
the mobile bottom - nav . * / }
< div
role = "group"
feat(i18n): translate AdminPage + UserProfile + Pricing + EditorPage shell (Editor block 10 — final cleanup)
Closes the Phase 2 i18n rollout. Every visitor- and user-facing
surface velxio renders in normal use now reads from t().
AdminPage (admin-only)
- Header (panel title, logout) and the four tabs (Dashboard /
Users / Projects / Boards).
- Setup screen for first-admin creation (title, body, password
fields + mismatch error + create-admin button).
- Not-admin gate page.
- EditUserModal (title, four labels, admin/active toggles,
cancel/save).
- UsersTab: search placeholder, count pluralisation, all 12
table columns, Activity / Edit / Delete actions, empty state,
delete-confirm prompt with username interpolation.
- ProjectsTab: search placeholder, count pluralisation, all 9
table columns, public/private badge labels, delete action +
confirm with project-name interpolation, empty state.
- All error messages (load failed / save failed / delete failed)
fall back through t().
UserProfilePage
- "New project" CTA, loading + empty + not-found states,
"Private" project badge, "Copy shareable link" tooltip.
- The /editor link uses localize() so /es/<username>'s "New
project" button stays in Spanish.
PricingPlaceholder
- Title + the two paragraphs (self-hosted note + hosted Pro
tier note + GitHub source note). Inline links wrapped via
the Trans component so the link surface stays clickable in
every locale without each translation having to re-write the
HTML.
EditorPage shell
- Mobile bottom-tab labels (Code / Circuit), file-explorer
toggle (Show / Hide), View mode aria-label, view-mode
segmented control labels (Code / Both / Circuit), and the
three "Drag to resize" handle tooltips on the panel splitters.
Translations
- en.json hand-curated for the new keys.
- All 8 non-English locales auto-translated via the existing
`npm run translate:i18n` pipeline (DeepSeek, ~5 min for the
whole bundle, sameShape() validates each output before write).
This closes Phase 2 of i18n. Phase 3 (DocsPage prose, AboutPage
long-form paragraphs, the 15 SEO landing pages) is deliberately
deferred — Docs/About are best handled by extracting the prose
into JSON keys and running the same script, while the SEO pages
are intentionally optimised for English keyword targeting and
should not be machine-translated en masse.
2026-05-09 22:49:13 +07:00
aria - label = { t ( 'editor.shell.viewMode' ) }
2026-05-08 10:53:27 +07:00
className = "view-mode-toggle"
style = { {
2026-08-18 05:17:38 +07:00
// display comes from App.css (flex; none on a narrow bar or mobile).
2026-07-31 05:46:08 +07:00
// Never squeezed below its buttons: flexbox used to crush
2026-07-30 21:43:46 +07:00
// this block to a third of its width and clip two of them.
flexShrink : 0 ,
2026-05-08 10:53:27 +07:00
gap : 1 ,
background : '#252526' ,
border : '1px solid #3c3c3c' ,
borderRadius : 4 ,
overflow : 'hidden' ,
alignSelf : 'center' ,
margin : '0 6px' ,
} }
>
2026-07-31 05:46:08 +07:00
< button
2026-08-18 05:17:38 +07:00
onClick = { ( ) = > toggleExplorer ( ) }
2026-07-31 05:46:08 +07:00
aria - pressed = { explorerOpen }
title = { explorerOpen ? t ( 'editor.menu.hideExplorer' , 'Hide file explorer' ) : t ( 'editor.menu.showExplorer' , 'Show file explorer' ) }
style = { {
background : explorerOpen ? 'var(--color-action-primary)' : 'transparent' ,
color : explorerOpen ? 'white' : '#aaa' ,
border : 'none' ,
height : 28 ,
padding : '0 10px' ,
display : 'flex' ,
alignItems : 'center' ,
cursor : 'pointer' ,
} }
>
< svg
width = "13"
height = "13"
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 = { { width : 1 , background : '#3c3c3c' , alignSelf : 'stretch' } } / >
2026-05-08 10:53:27 +07:00
{ (
[
feat(i18n): translate AdminPage + UserProfile + Pricing + EditorPage shell (Editor block 10 — final cleanup)
Closes the Phase 2 i18n rollout. Every visitor- and user-facing
surface velxio renders in normal use now reads from t().
AdminPage (admin-only)
- Header (panel title, logout) and the four tabs (Dashboard /
Users / Projects / Boards).
- Setup screen for first-admin creation (title, body, password
fields + mismatch error + create-admin button).
- Not-admin gate page.
- EditUserModal (title, four labels, admin/active toggles,
cancel/save).
- UsersTab: search placeholder, count pluralisation, all 12
table columns, Activity / Edit / Delete actions, empty state,
delete-confirm prompt with username interpolation.
- ProjectsTab: search placeholder, count pluralisation, all 9
table columns, public/private badge labels, delete action +
confirm with project-name interpolation, empty state.
- All error messages (load failed / save failed / delete failed)
fall back through t().
UserProfilePage
- "New project" CTA, loading + empty + not-found states,
"Private" project badge, "Copy shareable link" tooltip.
- The /editor link uses localize() so /es/<username>'s "New
project" button stays in Spanish.
PricingPlaceholder
- Title + the two paragraphs (self-hosted note + hosted Pro
tier note + GitHub source note). Inline links wrapped via
the Trans component so the link surface stays clickable in
every locale without each translation having to re-write the
HTML.
EditorPage shell
- Mobile bottom-tab labels (Code / Circuit), file-explorer
toggle (Show / Hide), View mode aria-label, view-mode
segmented control labels (Code / Both / Circuit), and the
three "Drag to resize" handle tooltips on the panel splitters.
Translations
- en.json hand-curated for the new keys.
- All 8 non-English locales auto-translated via the existing
`npm run translate:i18n` pipeline (DeepSeek, ~5 min for the
whole bundle, sameShape() validates each output before write).
This closes Phase 2 of i18n. Phase 3 (DocsPage prose, AboutPage
long-form paragraphs, the 15 SEO landing pages) is deliberately
deferred — Docs/About are best handled by extracting the prose
into JSON keys and running the same script, while the SEO pages
are intentionally optimised for English keyword targeting and
should not be machine-translated en masse.
2026-05-09 22:49:13 +07:00
{ key : 'code' , label : t ( 'editor.shell.code' ) , path : 'M16 18l6-6-6-6M8 6l-6 6 6 6' } ,
{ key : 'both' , label : t ( 'editor.shell.both' ) , path : 'M3 3h7v18H3zM14 3h7v18h-7z' } ,
{ key : 'circuit' , label : t ( 'editor.shell.circuit' ) , path : 'M5 12h14M12 5v14' } ,
2026-05-08 10:53:27 +07:00
] as const
) . map ( ( m ) = > (
< button
key = { m . key }
onClick = { ( ) = > setViewMode ( m . key ) }
aria - pressed = { viewMode === m . key }
style = { {
2026-07-31 05:46:08 +07:00
background : viewMode === m . key ? 'var(--color-action-primary)' : 'transparent' ,
2026-05-08 10:53:27 +07:00
color : viewMode === m . key ? 'white' : '#aaa' ,
border : 'none' ,
height : 28 ,
padding : '0 10px' ,
display : 'flex' ,
alignItems : 'center' ,
gap : 4 ,
cursor : 'pointer' ,
fontSize : 12 ,
fontFamily : 'inherit' ,
} }
>
< svg
width = "13"
height = "13"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
< path d = { m . path } / >
< / svg >
fix(editor): stop unified toolbar controls overlapping on a narrow bar
The top bar packs three zones onto one row: the view-mode toggle, the editor
actions (Compile/Run/Stop/...), and the canvas controls (board selector,
Serial, Scope, zoom, Add) portaled in from SimulatorCanvas. The editor zone
was flex:1 min-width:0 while the canvas zone was fixed-width, so when the bar
narrowed - mainly when the right-docked AI chat opens - the editor zone shrank
below its own buttons and painted them over the board selector / Serial /
Scope. The existing collapse logic was keyed to the viewport (@media 768px),
so it never fired on a wide screen with the chat open.
Make the shared bar a container-query context and collapse every zone by the
bar's own width instead of the viewport: view-mode labels drop to icons first,
then Serial/Scope/Add labels and the board selector ellipsizes, then the
component count and finally the zoom buttons (wheel-zoom still works). Floor
the editor zone at its collapsed content width so it can never underflow and
overlap; past that the lower-priority canvas controls yield toward the right
edge instead. Verified across bar widths 660-1140px with the AI chat open:
overlap eliminated, dropdown menus still render un-clipped.
2026-07-03 12:32:14 +07:00
< span className = "vm-label" > { m . label } < / span >
2026-05-08 10:53:27 +07:00
< / button >
) ) }
< / div >
2026-04-29 10:21:26 +07:00
< div className = "unified-toolbar-editor" >
< EditorToolbar
consoleOpen = { consoleOpen }
setConsoleOpen = { setConsoleOpen }
compileLogs = { compileLogs }
setCompileLogs = { setCompileLogs }
/ >
< / div >
< div className = "unified-toolbar-canvas" ref = { setCanvasHeaderSlot } / >
< / div >
2026-07-30 23:56:24 +07:00
) : undefined ;
return (
< div className = "app" >
2026-09-11 11:02:34 +07:00
{ ! embedMode . enabled ? (
2026-08-29 10:35:49 +07:00
< AppHeader
editorMenu = { ! isMobile ? < EditorMenuBar / > : undefined }
editorToolbar = { unifiedToolbar }
/ >
2026-09-11 11:02:34 +07:00
) : (
! isMobile && ! embedMode . hideEditor && unifiedToolbar
2026-08-29 10:35:49 +07:00
) }
2026-07-30 23:56:24 +07:00
{ /* ── Mobile tab bar (top, above panels) ── */ }
2026-09-11 11:02:34 +07:00
{ isMobile && ! ( embedMode . enabled && embedMode . hideEditor ) && (
2026-07-30 23:56:24 +07:00
< nav className = "mobile-tab-bar" >
< button
className = { ` mobile-tab-btn ${ mobileView === 'code' ? ' mobile-tab-btn--active' : '' } ` }
onClick = { ( ) = > setMobileView ( 'code' ) }
>
< svg
width = "16"
height = "16"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
< polyline points = "16 18 22 12 16 6" / >
< polyline points = "8 6 2 12 8 18" / >
< / svg >
< span > & lt ; / & g t ; { t ( ' e d i t o r . s h e l l . c o d e ' ) } < / s p a n >
< / button >
< button
className = { ` mobile-tab-btn ${ mobileView === 'circuit' ? ' mobile-tab-btn--active' : '' } ` }
onClick = { ( ) = > setMobileView ( 'circuit' ) }
>
< svg
width = "16"
height = "16"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
< 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 > { t ( 'editor.shell.circuit' ) } < / span >
< / button >
< / nav >
2026-04-29 10:21:26 +07:00
) }
2026-07-30 23:56:24 +07:00
2026-08-08 11:24:47 +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 = { {
2026-08-29 10:35:49 +07:00
width : ( embedMode . enabled && embedMode . hideEditor )
? '0%'
: isMobile
2026-05-08 10:53:27 +07:00
? '100%'
: viewMode === 'code'
? '100%'
: viewMode === 'circuit'
? '0%'
: ` ${ editorWidthPct } % ` ,
display :
2026-08-29 10:35:49 +07:00
( embedMode . enabled && embedMode . hideEditor ) ||
( isMobile && mobileView !== 'code' ) ||
( ! isMobile && viewMode === 'circuit' )
2026-05-08 10:53:27 +07:00
? 'none'
: 'flex' ,
2026-03-10 23:09:32 +07:00
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
2026-07-31 01:54:24 +07:00
style = { {
width : explorerWidth ,
flexShrink : 0 ,
display : 'flex' ,
flexDirection : 'column' ,
overflow : 'hidden' ,
} }
2026-04-22 02:45:45 +07:00
>
2026-07-31 01:54:24 +07:00
< div style = { { flex : 1 , minHeight : 0 , display : 'flex' , overflow : 'hidden' } } >
2026-08-18 07:26:35 +07:00
< FileExplorer onSaveClick = { handleSaveClick } onNewClick = { handleNewClick } autoSave = { autoSave } / >
2026-07-31 01:54:24 +07:00
< / div >
{ accountBlock && (
< div className = "explorer-account-footer" > { accountBlock } < / div >
) }
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
2026-08-08 11:24:47 +07:00
{ ! explorerOpen && accountBlock && (
2026-07-31 01:54:24 +07:00
< div className = "editor-corner-box" > { accountBlock } < / div >
) }
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"
2026-08-18 05:17:38 +07:00
onClick = { ( ) = > toggleExplorer ( ) }
2026-07-31 02:18:37 +07:00
title = { explorerOpen ? t ( 'editor.menu.hideExplorer' , 'Hide file explorer' ) : t ( 'editor.menu.showExplorer' , '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 }
/ >
< / 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-07-29 00:43:20 +07:00
{ / * E d i t o r a r e a — M o n a c o f o r e v e r y b o a r d , Q E M U - L i n u x i n c l u d e d .
Pi boards edit script . py here like any other board edits its
sketch ; their interactive terminal lives in the bottom serial
panel ( SerialMonitor renders an xterm for Pi kinds ) . The old
RaspberryPiWorkspace ( own file tree + own editor + upload
button ) confused users with three competing file surfaces . * / }
2026-03-06 20:14:50 +07:00
< div className = "editor-wrapper" style = { { flex : 1 , overflow : 'hidden' , minHeight : 0 } } >
2026-07-29 00:43:20 +07:00
< CodeEditor / >
2026-03-06 20:14:50 +07:00
< / div >
{ /* Console */ }
{ consoleOpen && (
< >
< div
onMouseDown = { handleBottomPanelResizeMouseDown }
style = { resizeHandleStyle }
feat(i18n): translate AdminPage + UserProfile + Pricing + EditorPage shell (Editor block 10 — final cleanup)
Closes the Phase 2 i18n rollout. Every visitor- and user-facing
surface velxio renders in normal use now reads from t().
AdminPage (admin-only)
- Header (panel title, logout) and the four tabs (Dashboard /
Users / Projects / Boards).
- Setup screen for first-admin creation (title, body, password
fields + mismatch error + create-admin button).
- Not-admin gate page.
- EditUserModal (title, four labels, admin/active toggles,
cancel/save).
- UsersTab: search placeholder, count pluralisation, all 12
table columns, Activity / Edit / Delete actions, empty state,
delete-confirm prompt with username interpolation.
- ProjectsTab: search placeholder, count pluralisation, all 9
table columns, public/private badge labels, delete action +
confirm with project-name interpolation, empty state.
- All error messages (load failed / save failed / delete failed)
fall back through t().
UserProfilePage
- "New project" CTA, loading + empty + not-found states,
"Private" project badge, "Copy shareable link" tooltip.
- The /editor link uses localize() so /es/<username>'s "New
project" button stays in Spanish.
PricingPlaceholder
- Title + the two paragraphs (self-hosted note + hosted Pro
tier note + GitHub source note). Inline links wrapped via
the Trans component so the link surface stays clickable in
every locale without each translation having to re-write the
HTML.
EditorPage shell
- Mobile bottom-tab labels (Code / Circuit), file-explorer
toggle (Show / Hide), View mode aria-label, view-mode
segmented control labels (Code / Both / Circuit), and the
three "Drag to resize" handle tooltips on the panel splitters.
Translations
- en.json hand-curated for the new keys.
- All 8 non-English locales auto-translated via the existing
`npm run translate:i18n` pipeline (DeepSeek, ~5 min for the
whole bundle, sameShape() validates each output before write).
This closes Phase 2 of i18n. Phase 3 (DocsPage prose, AboutPage
long-form paragraphs, the 15 SEO landing pages) is deliberately
deferred — Docs/About are best handled by extracting the prose
into JSON keys and running the same script, while the SEO pages
are intentionally optimised for English keyword targeting and
should not be machine-translated en masse.
2026-05-09 22:49:13 +07:00
title = { t ( 'editor.shell.dragResize' ) }
2026-03-06 20:14:50 +07:00
/ >
< 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-05-08 10:53:27 +07:00
{ /* Resize handle (desktop only, and only when both panes are visible) */ }
2026-08-29 10:35:49 +07:00
{ ! isMobile && viewMode === 'both' && ! ( embedMode . enabled && embedMode . hideEditor ) && (
2026-03-10 23:09:32 +07:00
< 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 = { {
2026-08-29 10:35:49 +07:00
width : ( embedMode . enabled && embedMode . hideEditor )
? '100%'
: isMobile
2026-05-08 10:53:27 +07:00
? '100%'
: viewMode === 'circuit'
? '100%'
: viewMode === 'code'
? '0%'
: ` ${ 100 - editorWidthPct } % ` ,
2026-08-29 10:35:49 +07:00
display : ( embedMode . enabled && embedMode . hideEditor )
? 'flex'
: ( isMobile && mobileView !== 'circuit' ) || ( ! isMobile && viewMode === 'code' )
? 'none'
: 'flex' ,
2026-03-10 23:09:32 +07:00
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 }
feat(i18n): translate AdminPage + UserProfile + Pricing + EditorPage shell (Editor block 10 — final cleanup)
Closes the Phase 2 i18n rollout. Every visitor- and user-facing
surface velxio renders in normal use now reads from t().
AdminPage (admin-only)
- Header (panel title, logout) and the four tabs (Dashboard /
Users / Projects / Boards).
- Setup screen for first-admin creation (title, body, password
fields + mismatch error + create-admin button).
- Not-admin gate page.
- EditUserModal (title, four labels, admin/active toggles,
cancel/save).
- UsersTab: search placeholder, count pluralisation, all 12
table columns, Activity / Edit / Delete actions, empty state,
delete-confirm prompt with username interpolation.
- ProjectsTab: search placeholder, count pluralisation, all 9
table columns, public/private badge labels, delete action +
confirm with project-name interpolation, empty state.
- All error messages (load failed / save failed / delete failed)
fall back through t().
UserProfilePage
- "New project" CTA, loading + empty + not-found states,
"Private" project badge, "Copy shareable link" tooltip.
- The /editor link uses localize() so /es/<username>'s "New
project" button stays in Spanish.
PricingPlaceholder
- Title + the two paragraphs (self-hosted note + hosted Pro
tier note + GitHub source note). Inline links wrapped via
the Trans component so the link surface stays clickable in
every locale without each translation having to re-write the
HTML.
EditorPage shell
- Mobile bottom-tab labels (Code / Circuit), file-explorer
toggle (Show / Hide), View mode aria-label, view-mode
segmented control labels (Code / Both / Circuit), and the
three "Drag to resize" handle tooltips on the panel splitters.
Translations
- en.json hand-curated for the new keys.
- All 8 non-English locales auto-translated via the existing
`npm run translate:i18n` pipeline (DeepSeek, ~5 min for the
whole bundle, sameShape() validates each output before write).
This closes Phase 2 of i18n. Phase 3 (DocsPage prose, AboutPage
long-form paragraphs, the 15 SEO landing pages) is deliberately
deferred — Docs/About are best handled by extracting the prose
into JSON keys and running the same script, while the SEO pages
are intentionally optimised for English keyword targeting and
should not be machine-translated en masse.
2026-05-09 22:49:13 +07:00
title = { t ( 'editor.shell.dragResize' ) }
2026-03-06 07:07:10 +07:00
/ >
< 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 }
feat(i18n): translate AdminPage + UserProfile + Pricing + EditorPage shell (Editor block 10 — final cleanup)
Closes the Phase 2 i18n rollout. Every visitor- and user-facing
surface velxio renders in normal use now reads from t().
AdminPage (admin-only)
- Header (panel title, logout) and the four tabs (Dashboard /
Users / Projects / Boards).
- Setup screen for first-admin creation (title, body, password
fields + mismatch error + create-admin button).
- Not-admin gate page.
- EditUserModal (title, four labels, admin/active toggles,
cancel/save).
- UsersTab: search placeholder, count pluralisation, all 12
table columns, Activity / Edit / Delete actions, empty state,
delete-confirm prompt with username interpolation.
- ProjectsTab: search placeholder, count pluralisation, all 9
table columns, public/private badge labels, delete action +
confirm with project-name interpolation, empty state.
- All error messages (load failed / save failed / delete failed)
fall back through t().
UserProfilePage
- "New project" CTA, loading + empty + not-found states,
"Private" project badge, "Copy shareable link" tooltip.
- The /editor link uses localize() so /es/<username>'s "New
project" button stays in Spanish.
PricingPlaceholder
- Title + the two paragraphs (self-hosted note + hosted Pro
tier note + GitHub source note). Inline links wrapped via
the Trans component so the link surface stays clickable in
every locale without each translation having to re-write the
HTML.
EditorPage shell
- Mobile bottom-tab labels (Code / Circuit), file-explorer
toggle (Show / Hide), View mode aria-label, view-mode
segmented control labels (Code / Both / Circuit), and the
three "Drag to resize" handle tooltips on the panel splitters.
Translations
- en.json hand-curated for the new keys.
- All 8 non-English locales auto-translated via the existing
`npm run translate:i18n` pipeline (DeepSeek, ~5 min for the
whole bundle, sameShape() validates each output before write).
This closes Phase 2 of i18n. Phase 3 (DocsPage prose, AboutPage
long-form paragraphs, the 15 SEO landing pages) is deliberately
deferred — Docs/About are best handled by extracting the prose
into JSON keys and running the same script, while the SEO pages
are intentionally optimised for English keyword targeting and
should not be machine-translated en masse.
2026-05-09 22:49:13 +07:00
title = { t ( 'editor.shell.dragResize' ) }
2026-03-11 22:09:24 +07:00
/ >
< 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
2026-08-29 10:35:49 +07:00
{ ! embedMode . enabled && showStarBanner && (
2026-06-16 02:39:05 +07:00
< GitHubStarBanner
onClose = { handleDismissStarBanner }
onStarClick = { handleStarClick }
round = { starRound }
/ >
) }
2026-08-29 10:35:49 +07:00
{ ! embedMode . enabled && < NewsAnnouncer / > }
{ ! embedMode . enabled && (
< NewProjectDialog
isOpen = { showNewProjectDialog }
onClose = { ( ) = > setShowNewProjectDialog ( false ) }
/ >
) }
2026-05-08 10:53:27 +07:00
{ / * S l o t r e s e r v e d f o r t h e p r i v a t e p r o o v e r l a y ( e . g . a g e n t c h a t p a n e l ) .
Self - hosted builds without an overlay see nothing here . * / }
< div data - velxio - slot = "agent-chat" / >
2026-03-04 08:25:52 +07:00
< / div >
) ;
} ;