2026-03-04 08:25:52 +07:00
/ * *
2026-03-05 08:05:23 +07:00
* Editor Page — main editor + simulator with resizable panels
2026-03-04 08:25:52 +07:00
* /
2026-03-16 00:04:01 +07:00
import React , { useRef , useState , useCallback , useEffect , lazy , Suspense } from 'react' ;
feat(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-03-04 08:25:52 +07:00
import { CodeEditor } from '../components/editor/CodeEditor' ;
import { EditorToolbar } from '../components/editor/EditorToolbar' ;
2026-03-06 20:14:50 +07:00
import { FileTabs } from '../components/editor/FileTabs' ;
import { FileExplorer } from '../components/editor/FileExplorer' ;
2026-03-16 00:04:01 +07:00
// Lazy-load Pi workspace so xterm.js isn't in the main bundle
const RaspberryPiWorkspace = lazy ( ( ) = >
2026-04-22 02:45:45 +07:00
import ( '../components/raspberry-pi/RaspberryPiWorkspace' ) . then ( ( m ) = > ( {
default : m . RaspberryPiWorkspace ,
} ) ) ,
2026-03-16 00:04:01 +07:00
) ;
2026-03-06 07:07:10 +07:00
import { CompilationConsole } from '../components/editor/CompilationConsole' ;
2026-03-04 08:25:52 +07:00
import { SimulatorCanvas } from '../components/simulator/SimulatorCanvas' ;
2026-03-05 16:56:14 +07:00
import { SerialMonitor } from '../components/simulator/SerialMonitor' ;
2026-03-11 22:09:24 +07:00
import { Oscilloscope } from '../components/simulator/Oscilloscope' ;
2026-03-06 20:14:50 +07:00
import { AppHeader } from '../components/layout/AppHeader' ;
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' ;
2026-05-01 01:27:58 +07:00
import { useSimulatorStore , DEFAULT_BOARD_POSITION } 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' ;
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' ;
2026-03-06 07:07:10 +07:00
import type { CompilationLog } from '../utils/compilationLogger' ;
2026-06-15 21:57:10 +07:00
import { isPiBoardKind } from '../types/board' ;
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-05-08 10:53:27 +07:00
const EXPLORER_MIN = 110 ;
2026-03-06 21:20:47 +07:00
const EXPLORER_MAX = 500 ;
2026-05-08 10:53:27 +07:00
const EXPLORER_DEFAULT = 165 ;
2026-03-06 21:20:47 +07:00
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-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-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-04-22 02:45:45 +07:00
const activeBoardKind = useSimulatorStore (
( s ) = > s . boards . find ( ( b ) = > b . id === s . activeBoardId ) ? . boardKind ,
2026-03-16 00:04:01 +07:00
) ;
2026-06-15 21:57:10 +07:00
// Pi 3/4/5 and Zero/1/2 all run the QEMU Linux workspace (terminal + Python),
// not the Arduino/Monaco editor. Pico (RP2040) is browser-emulated, not a Pi here.
const isLinuxPi = isPiBoardKind ( activeBoardKind ? ? '' ) ;
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-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 20:14:50 +07:00
const [ explorerOpen , setExplorerOpen ] = useState ( true ) ;
2026-03-06 21:20:47 +07:00
const [ explorerWidth , setExplorerWidth ] = useState ( EXPLORER_DEFAULT ) ;
2026-04-22 02:45:45 +07:00
const [ isMobile , setIsMobile ] = useState (
( ) = > window . matchMedia ( ` (max-width: ${ MOBILE_BREAKPOINT } px) ` ) . matches ,
) ;
2026-04-29 10:21:26 +07:00
// Slot element for SimulatorCanvas to portal its header into. When set, the
// canvas board selector / Serial / Scope / zoom / Add buttons render here
// instead of above the canvas — keeping the top bar a single full-width row
// that doesn't reflow when the editor/canvas splitter is dragged.
const [ canvasHeaderSlot , setCanvasHeaderSlot ] = useState < HTMLDivElement | null > ( null ) ;
2026-03-24 03:15:29 +07:00
// Default to 'code' on mobile — show the editor so users can write/view code
const [ mobileView , setMobileView ] = useState < 'code' | 'circuit' > ( 'code' ) ;
2026-03-06 20:14:50 +07:00
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-05-01 01:27:58 +07:00
const handleNewClick = useCallback ( ( ) = > {
if (
! window . confirm (
'Start a new workspace? This clears every board, component, wire and file. This cannot be undone.' ,
)
) {
return ;
}
const sim = useSimulatorStore . getState ( ) ;
sim . boards . forEach ( ( b ) = > sim . stopBoard ( b . id ) ) ;
const ids = sim . boards . map ( ( b ) = > b . id ) ;
ids . forEach ( ( id ) = > sim . removeBoard ( id ) ) ;
sim . setComponents ( [ ] ) ;
sim . setWires ( [ ] ) ;
useProjectStore . getState ( ) . clearCurrentProject ( ) ;
const newId = useSimulatorStore
. getState ( )
. addBoard ( 'arduino-uno' , DEFAULT_BOARD_POSITION . x , DEFAULT_BOARD_POSITION . y ) ;
useSimulatorStore . getState ( ) . setActiveBoardId ( newId ) ;
} , [ ] ) ;
2026-03-10 23:09:32 +07:00
// Track mobile breakpoint
useEffect ( ( ) = > {
const mq = window . matchMedia ( ` (max-width: ${ MOBILE_BREAKPOINT } px) ` ) ;
const update = ( e : MediaQueryListEvent | MediaQueryList ) = > {
const mobile = e . matches ;
setIsMobile ( mobile ) ;
if ( mobile ) setExplorerOpen ( false ) ;
} ;
update ( mq ) ;
mq . addEventListener ( 'change' , update ) ;
return ( ) = > mq . removeEventListener ( 'change' , update ) ;
} , [ ] ) ;
2026-03-06 20:14:50 +07:00
// Ctrl+S shortcut
useEffect ( ( ) = > {
const handler = ( e : KeyboardEvent ) = > {
if ( ( e . ctrlKey || e . metaKey ) && e . key === 's' ) {
e . preventDefault ( ) ;
handleSaveClick ( ) ;
}
} ;
window . addEventListener ( 'keydown' , handler ) ;
return ( ) = > window . removeEventListener ( 'keydown' , handler ) ;
} , [ handleSaveClick ] ) ;
2026-03-05 08:05:23 +07:00
2026-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 ;
const rect = containerRef . current . getBoundingClientRect ( ) ;
const pct = ( ( ev . clientX - rect . left ) / rect . width ) * 100 ;
setEditorWidthPct ( Math . max ( 20 , Math . min ( 80 , pct ) ) ) ;
} ;
const handleMouseUp = ( ) = > {
resizingRef . current = false ;
document . body . style . cursor = '' ;
document . body . style . userSelect = '' ;
document . removeEventListener ( 'mousemove' , handleMouseMove ) ;
document . removeEventListener ( 'mouseup' , handleMouseUp ) ;
} ;
document . body . style . cursor = 'col-resize' ;
document . body . style . userSelect = 'none' ;
document . addEventListener ( 'mousemove' , handleMouseMove ) ;
document . addEventListener ( 'mouseup' , handleMouseUp ) ;
} , [ ] ) ;
2026-04-22 02:45:45 +07:00
const handleBottomPanelResizeMouseDown = useCallback (
( e : React.MouseEvent ) = > {
e . preventDefault ( ) ;
const startY = e . clientY ;
const startHeight = bottomPanelHeight ;
const onMove = ( ev : MouseEvent ) = > {
const delta = startY - ev . clientY ;
setBottomPanelHeight (
Math . max ( BOTTOM_PANEL_MIN , Math . min ( BOTTOM_PANEL_MAX , startHeight + delta ) ) ,
) ;
} ;
const onUp = ( ) = > {
document . body . style . cursor = '' ;
document . body . style . userSelect = '' ;
document . removeEventListener ( 'mousemove' , onMove ) ;
document . removeEventListener ( 'mouseup' , onUp ) ;
} ;
document . body . style . cursor = 'row-resize' ;
document . body . style . userSelect = 'none' ;
document . addEventListener ( 'mousemove' , onMove ) ;
document . addEventListener ( 'mouseup' , onUp ) ;
} ,
[ bottomPanelHeight ] ,
) ;
2026-03-06 21:20:47 +07:00
2026-04-22 02:45:45 +07:00
const handleExplorerResizeMouseDown = useCallback (
( e : React.MouseEvent ) = > {
e . preventDefault ( ) ;
const startX = e . clientX ;
const startWidth = explorerWidth ;
const onMove = ( ev : MouseEvent ) = > {
const delta = ev . clientX - startX ;
setExplorerWidth ( Math . max ( EXPLORER_MIN , Math . min ( EXPLORER_MAX , startWidth + delta ) ) ) ;
} ;
const onUp = ( ) = > {
document . body . style . cursor = '' ;
document . body . style . userSelect = '' ;
document . removeEventListener ( 'mousemove' , onMove ) ;
document . removeEventListener ( 'mouseup' , onUp ) ;
} ;
document . body . style . cursor = 'col-resize' ;
document . body . style . userSelect = 'none' ;
document . addEventListener ( 'mousemove' , onMove ) ;
document . addEventListener ( 'mouseup' , onUp ) ;
} ,
[ explorerWidth ] ,
) ;
2026-03-06 21:20:47 +07:00
2026-03-04 08:25:52 +07:00
return (
< div className = "app" >
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
< AppHeader autoSave = { autoSave } / >
2026-03-05 08:05:23 +07:00
2026-03-24 03:15:29 +07:00
{ /* ── Mobile tab bar (top, above panels) ── */ }
{ isMobile && (
< nav className = "mobile-tab-bar" >
< button
className = { ` mobile-tab-btn ${ mobileView === 'code' ? ' mobile-tab-btn--active' : '' } ` }
onClick = { ( ) = > setMobileView ( 'code' ) }
>
2026-04-22 02:45:45 +07:00
< svg
width = "16"
height = "16"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
2026-03-24 03:15:29 +07:00
< polyline points = "16 18 22 12 16 6" / >
< polyline points = "8 6 2 12 8 18" / >
< / svg >
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
< span > & lt ; / & g t ; { t ( ' e d i t o r . s h e l l . c o d e ' ) } < / s p a n >
2026-03-24 03:15:29 +07:00
< / button >
< button
className = { ` mobile-tab-btn ${ mobileView === 'circuit' ? ' mobile-tab-btn--active' : '' } ` }
onClick = { ( ) = > setMobileView ( 'circuit' ) }
>
2026-04-22 02:45:45 +07:00
< svg
width = "16"
height = "16"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
2026-03-24 03:15:29 +07:00
< rect x = "2" y = "7" width = "20" height = "14" rx = "2" / >
< path d = "M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" / >
< line x1 = "12" y1 = "12" x2 = "12" y2 = "16" / >
< line x1 = "10" y1 = "14" x2 = "14" y2 = "14" / >
< / svg >
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
< span > { t ( 'editor.shell.circuit' ) } < / span >
2026-03-24 03:15:29 +07:00
< / button >
< / nav >
) }
2026-04-29 10:21:26 +07:00
{ / * ─ ─ U n i f i e d t o p t o o l b a r ( d e s k t o p o n l y ) ─ ─
Editor controls + canvas controls share a single full - width row so
the bar doesn ' t reflow when the editor / canvas splitter is dragged .
The canvas controls ( board selector , Serial , Scope , zoom , Add ) are
portaled into ` canvasHeaderSlot ` from inside SimulatorCanvas . * / }
{ ! isMobile && (
< div className = "unified-toolbar" >
< button
className = "explorer-toggle-btn unified-toolbar-explorer-toggle"
onClick = { ( ) = > setExplorerOpen ( ( v ) = > ! v ) }
title = { explorerOpen ? 'Hide file explorer' : 'Show file explorer' }
>
< svg
width = "16"
height = "16"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
< path d = "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" / >
< / svg >
< / button >
2026-05-08 10:53:27 +07:00
{ /* View-mode toggle: Code / Both / Circuit . Lets users hide a
pane to give the right - docked AI chat more breathing room .
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 = { {
display : 'flex' ,
gap : 1 ,
background : '#252526' ,
border : '1px solid #3c3c3c' ,
borderRadius : 4 ,
overflow : 'hidden' ,
alignSelf : 'center' ,
margin : '0 6px' ,
} }
>
{ (
[
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 = { {
background : viewMode === m . key ? '#0e639c' : 'transparent' ,
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 >
< span > { m . label } < / span >
< / button >
) ) }
< / div >
2026-04-29 10:21:26 +07:00
< div className = "unified-toolbar-editor" >
< EditorToolbar
consoleOpen = { consoleOpen }
setConsoleOpen = { setConsoleOpen }
compileLogs = { compileLogs }
setCompileLogs = { setCompileLogs }
2026-06-15 21:57:10 +07:00
centerSlot = { ! isLinuxPi ? < FileTabs / > : null }
2026-04-29 10:21:26 +07:00
/ >
< / div >
< div className = "unified-toolbar-canvas" ref = { setCanvasHeaderSlot } / >
< / div >
) }
2026-03-05 08:05:23 +07:00
< div className = "app-container" ref = { containerRef } >
2026-03-06 20:14:50 +07:00
{ /* ── Editor side ── */ }
< div
className = "editor-panel"
2026-03-10 23:09:32 +07:00
style = { {
2026-05-08 10:53:27 +07:00
width : isMobile
? '100%'
: viewMode === 'code'
? '100%'
: viewMode === 'circuit'
? '0%'
: ` ${ editorWidthPct } % ` ,
display :
( isMobile && mobileView !== 'code' ) || ( ! isMobile && viewMode === 'circuit' )
? '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
style = { { width : explorerWidth , flexShrink : 0 , display : 'flex' , overflow : 'hidden' } }
>
2026-05-01 01:27:58 +07:00
< FileExplorer onSaveClick = { handleSaveClick } onNewClick = { handleNewClick } / >
2026-03-06 21:20:47 +07:00
< / div >
2026-03-10 23:09:32 +07:00
{ ! isMobile && (
2026-04-22 02:45:45 +07:00
< div
className = "explorer-resize-handle"
onMouseDown = { handleExplorerResizeMouseDown }
/ >
2026-03-10 23:09:32 +07:00
) }
2026-03-06 21:20:47 +07:00
< / >
) }
2026-03-06 20:14:50 +07:00
{ /* Editor main area */ }
2026-04-22 02:45:45 +07:00
< div
style = { {
flex : 1 ,
display : 'flex' ,
flexDirection : 'column' ,
overflow : 'hidden' ,
minWidth : 0 ,
} }
>
2026-04-29 10:21:26 +07:00
{ / * M o b i l e - o n l y : e x p l o r e r t o g g l e + e d i t o r t o o l b a r i n s i d e t h e p a n e l .
On desktop these are hoisted into the unified top toolbar . * / }
{ isMobile && (
< div style = { { display : 'flex' , alignItems : 'stretch' , flexShrink : 0 } } >
< button
className = "explorer-toggle-btn"
onClick = { ( ) = > setExplorerOpen ( ( v ) = > ! v ) }
title = { explorerOpen ? 'Hide file explorer' : 'Show file explorer' }
2026-04-22 02:45:45 +07:00
>
2026-04-29 10:21:26 +07:00
< svg
width = "16"
height = "16"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
< path d = "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" / >
< / svg >
< / button >
< div style = { { flex : 1 , minWidth : 0 } } >
< EditorToolbar
consoleOpen = { consoleOpen }
setConsoleOpen = { setConsoleOpen }
compileLogs = { compileLogs }
setCompileLogs = { setCompileLogs }
2026-06-15 21:57:10 +07:00
centerSlot = { ! isLinuxPi ? < FileTabs / > : null }
2026-04-29 10:21:26 +07:00
/ >
< / div >
2026-03-06 07:07:10 +07:00
< / div >
2026-04-29 10:21:26 +07:00
) }
2026-03-06 20:14:50 +07:00
2026-03-16 00:04:01 +07:00
{ /* Editor area: Pi workspace or Monaco editor */ }
2026-03-06 20:14:50 +07:00
< div className = "editor-wrapper" style = { { flex : 1 , overflow : 'hidden' , minHeight : 0 } } >
2026-06-15 21:57:10 +07:00
{ isLinuxPi && activeBoardId ? (
2026-04-22 02:45:45 +07:00
< Suspense
fallback = {
< div style = { { color : '#666' , padding : 16 , fontSize : 12 } } >
Loading Pi workspace …
< / div >
}
>
2026-03-16 00:04:01 +07:00
< RaspberryPiWorkspace boardId = { activeBoardId } / >
< / Suspense >
) : (
< CodeEditor / >
) }
2026-03-06 20:14:50 +07:00
< / div >
{ /* Console */ }
{ consoleOpen && (
< >
< div
onMouseDown = { handleBottomPanelResizeMouseDown }
style = { resizeHandleStyle }
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) */ }
{ ! isMobile && viewMode === 'both' && (
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-05-08 10:53:27 +07:00
width : isMobile
? '100%'
: viewMode === 'circuit'
? '100%'
: viewMode === 'code'
? '0%'
: ` ${ 100 - editorWidthPct } % ` ,
display :
( 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-06-16 02:39:05 +07:00
{ showStarBanner && (
< GitHubStarBanner
onClose = { handleDismissStarBanner }
onStarClick = { handleStarClick }
round = { starRound }
/ >
) }
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 >
) ;
} ;