2026-03-06 20:14:50 +07:00
|
|
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
2026-05-09 13:08:51 +07:00
|
|
|
import { useTranslation } from 'react-i18next';
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
import { useEditorStore, chipFileGroupId } from '../../store/useEditorStore';
|
2026-03-16 00:04:01 +07:00
|
|
|
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
2026-06-04 03:52:11 +07:00
|
|
|
import {
|
|
|
|
|
isProgrammableChip,
|
|
|
|
|
targetForChip,
|
|
|
|
|
DEFAULT_CHIP_PROGRAM_FILE,
|
|
|
|
|
DEFAULT_CHIP_PROGRAM_C,
|
|
|
|
|
} from '../../services/romCompileService';
|
2026-03-16 00:04:01 +07:00
|
|
|
import type { BoardKind } from '../../types/board';
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
import { boardDisplayName } from '../../types/board';
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject';
|
2026-03-06 20:14:50 +07:00
|
|
|
import './FileExplorer.css';
|
|
|
|
|
|
2026-03-06 20:24:03 +07:00
|
|
|
// SVG icons — same style as EditorToolbar (stroke-based, 16x16)
|
|
|
|
|
const IcoFile = () => (
|
2026-04-22 02:45:45 +07:00
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
2026-03-06 20:24:03 +07:00
|
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
|
|
|
<polyline points="14 2 14 8 20 8" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const IcoHeader = () => (
|
2026-04-22 02:45:45 +07:00
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
2026-03-06 20:24:03 +07:00
|
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
|
|
|
<polyline points="14 2 14 8 20 8" />
|
|
|
|
|
<line x1="9" y1="13" x2="15" y2="13" />
|
|
|
|
|
<line x1="9" y1="17" x2="13" y2="17" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const IcoNewFile = () => (
|
2026-04-22 02:45:45 +07:00
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
2026-03-06 20:24:03 +07:00
|
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
|
|
|
<polyline points="14 2 14 8 20 8" />
|
|
|
|
|
<line x1="12" y1="18" x2="12" y2="12" />
|
|
|
|
|
<line x1="9" y1="15" x2="15" y2="15" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-01 01:27:58 +07:00
|
|
|
const IcoNewWorkspace = () => (
|
|
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
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" />
|
|
|
|
|
<line x1="12" y1="11" x2="12" y2="17" />
|
|
|
|
|
<line x1="9" y1="14" x2="15" y2="14" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-06 20:24:03 +07:00
|
|
|
const IcoSave = () => (
|
2026-04-22 02:45:45 +07:00
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
2026-03-06 20:24:03 +07:00
|
|
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
|
|
|
|
|
<polyline points="17 21 17 13 7 13 7 21" />
|
|
|
|
|
<polyline points="7 3 7 8 15 8" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
const IcoOpen = () => (
|
|
|
|
|
// Folder with an "open / upload arrow" — matches Save visually (both
|
|
|
|
|
// are project-IO actions) but points the opposite way to signal load.
|
|
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
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" />
|
|
|
|
|
<polyline points="12 11 12 17" />
|
|
|
|
|
<polyline points="9 14 12 11 15 14" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const IcoChevron = ({ open }: { open: boolean }) => (
|
|
|
|
|
<svg
|
2026-04-22 02:45:45 +07:00
|
|
|
width="12"
|
|
|
|
|
height="12"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2.5"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
2026-03-16 00:04:01 +07:00
|
|
|
style={{ transform: open ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.15s' }}
|
|
|
|
|
>
|
|
|
|
|
<polyline points="9 18 15 12 9 6" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
// Pencil icon — the rename affordance on a board/chip section header.
|
|
|
|
|
const IcoPencil = () => (
|
|
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<path d="M12 20h9" />
|
|
|
|
|
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
// Integrated-circuit (chip) icon — a DIP package with pins. Marks a
|
|
|
|
|
// programmable custom-chip's program section, distinct from board sections.
|
|
|
|
|
const IcoChip = () => (
|
|
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<rect x="7" y="7" width="10" height="10" rx="1" />
|
|
|
|
|
<line x1="10" y1="3" x2="10" y2="7" />
|
|
|
|
|
<line x1="14" y1="3" x2="14" y2="7" />
|
|
|
|
|
<line x1="10" y1="17" x2="10" y2="21" />
|
|
|
|
|
<line x1="14" y1="17" x2="14" y2="21" />
|
|
|
|
|
<line x1="3" y1="10" x2="7" y2="10" />
|
|
|
|
|
<line x1="3" y1="14" x2="7" y2="14" />
|
|
|
|
|
<line x1="17" y1="10" x2="21" y2="10" />
|
|
|
|
|
<line x1="17" y1="14" x2="21" y2="14" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
// Board emoji icons — mirrors BoardPickerModal
|
|
|
|
|
const BOARD_ICON: Record<BoardKind, string> = {
|
2026-04-22 02:45:45 +07:00
|
|
|
'arduino-uno': '⬤',
|
|
|
|
|
'arduino-nano': '▪',
|
|
|
|
|
'arduino-mega': '▬',
|
2026-03-16 00:04:01 +07:00
|
|
|
'raspberry-pi-pico': '◆',
|
2026-04-22 02:45:45 +07:00
|
|
|
'raspberry-pi-3': '⬛',
|
|
|
|
|
esp32: '⬡',
|
2026-03-16 00:04:01 +07:00
|
|
|
'esp32-s3': '⬡',
|
|
|
|
|
'esp32-c3': '⬡',
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
'stm32-bluepill': '◈',
|
|
|
|
|
'stm32-blackpill': '◈',
|
feat(boards): add 6 STM32 boards (F4 Discovery, Olimex H405, Netduino 2/+2, Pill variants)
Adds stm32-f4-discovery, stm32-olimex-h405, stm32-netduino-plus2, stm32-netduino2, stm32-blackpill-f401 and stm32-bluepill-f103cb, mapped to existing qemu-lcgamboa machines (netduinoplus2, olimex-stm32-h405, netduino2, stm32vldiscovery). A generic inline board renderer (no SVG) draws the Discovery/Olimex/Netduino boards from a header pin layout; the Pill variants reuse the Blue/Black Pill SVGs. Per-board onboard-LED pin and polarity via STM32_LED. One blink+serial example per board.
tsc --noEmit clean; all new FQBN pnum variants present in STM32 core 2.12.0; worker smoke tests pass for the new machines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 10:59:04 +07:00
|
|
|
'stm32-bluepill-f103cb': '◈',
|
|
|
|
|
'stm32-blackpill-f401': '◈',
|
|
|
|
|
'stm32-f4-discovery': '◈',
|
|
|
|
|
'stm32-olimex-h405': '◈',
|
|
|
|
|
'stm32-netduino-plus2': '◈',
|
|
|
|
|
'stm32-netduino2': '◈',
|
2026-03-16 00:04:01 +07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Color accent per board family
|
|
|
|
|
const BOARD_COLOR: Record<BoardKind, string> = {
|
2026-04-22 02:45:45 +07:00
|
|
|
'arduino-uno': '#4fc3f7',
|
|
|
|
|
'arduino-nano': '#4fc3f7',
|
|
|
|
|
'arduino-mega': '#4fc3f7',
|
2026-03-16 00:04:01 +07:00
|
|
|
'raspberry-pi-pico': '#ce93d8',
|
2026-04-22 02:45:45 +07:00
|
|
|
'raspberry-pi-3': '#ef9a9a',
|
|
|
|
|
esp32: '#a5d6a7',
|
2026-03-16 00:04:01 +07:00
|
|
|
'esp32-s3': '#a5d6a7',
|
|
|
|
|
'esp32-c3': '#a5d6a7',
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
'stm32-bluepill': '#80cbc4',
|
|
|
|
|
'stm32-blackpill': '#b0bec5',
|
feat(boards): add 6 STM32 boards (F4 Discovery, Olimex H405, Netduino 2/+2, Pill variants)
Adds stm32-f4-discovery, stm32-olimex-h405, stm32-netduino-plus2, stm32-netduino2, stm32-blackpill-f401 and stm32-bluepill-f103cb, mapped to existing qemu-lcgamboa machines (netduinoplus2, olimex-stm32-h405, netduino2, stm32vldiscovery). A generic inline board renderer (no SVG) draws the Discovery/Olimex/Netduino boards from a header pin layout; the Pill variants reuse the Blue/Black Pill SVGs. Per-board onboard-LED pin and polarity via STM32_LED. One blink+serial example per board.
tsc --noEmit clean; all new FQBN pnum variants present in STM32 core 2.12.0; worker smoke tests pass for the new machines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 10:59:04 +07:00
|
|
|
'stm32-bluepill-f103cb': '#80cbc4',
|
|
|
|
|
'stm32-blackpill-f401': '#b0bec5',
|
|
|
|
|
'stm32-f4-discovery': '#90caf9',
|
|
|
|
|
'stm32-olimex-h405': '#a5d6a7',
|
|
|
|
|
'stm32-netduino-plus2': '#ce93d8',
|
|
|
|
|
'stm32-netduino2': '#ce93d8',
|
2026-03-16 00:04:01 +07:00
|
|
|
};
|
|
|
|
|
|
2026-03-06 20:24:03 +07:00
|
|
|
function FileIcon({ name }: { name: string }) {
|
2026-03-06 20:14:50 +07:00
|
|
|
const ext = name.split('.').pop()?.toLowerCase() ?? '';
|
2026-03-06 20:24:03 +07:00
|
|
|
if (['h', 'hpp'].includes(ext)) return <IcoHeader />;
|
|
|
|
|
return <IcoFile />;
|
2026-03-06 20:14:50 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ContextMenu {
|
|
|
|
|
fileId: string;
|
2026-03-16 00:04:01 +07:00
|
|
|
boardGroupId: string;
|
2026-03-06 20:14:50 +07:00
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 20:24:03 +07:00
|
|
|
interface FileExplorerProps {
|
|
|
|
|
onSaveClick: () => void;
|
2026-05-01 01:27:58 +07:00
|
|
|
onNewClick: () => void;
|
2026-03-06 20:24:03 +07:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 01:27:58 +07:00
|
|
|
export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewClick }) => {
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
// Hidden <input type="file"> we trigger via ref when the user clicks
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
// the Open project button. Accepts both .vlx (Velxio native) and .zip
|
|
|
|
|
// (Wokwi bundle); the dispatcher in utils/importProject.ts decides which
|
|
|
|
|
// loader to run based on the file extension. Kept outside React state so
|
|
|
|
|
// the change event still fires when the user picks the same file twice.
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
const handleOpenProjectClick = useCallback(() => {
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
fileInputRef.current?.click();
|
|
|
|
|
}, []);
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
const handleProjectFilePicked = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
const file = e.target.files?.[0];
|
|
|
|
|
// Reset so picking the SAME file again later still fires onchange.
|
|
|
|
|
e.target.value = '';
|
|
|
|
|
if (!file) return;
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
const friendlyName = file.name.toLowerCase().endsWith('.zip') ? 'Wokwi .zip' : '.vlx';
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
if (
|
|
|
|
|
!window.confirm(
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
`Load this ${friendlyName} project? Your current workspace will be replaced. ` +
|
|
|
|
|
`This cannot be undone.`,
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
const result = await importProjectFile(file);
|
|
|
|
|
// .zip needs the caller to apply the payload to the stores (we keep
|
|
|
|
|
// that asymmetry so the toolbar's import flow can also pop the
|
|
|
|
|
// install-libraries modal afterwards). Here in the file explorer we
|
|
|
|
|
// don't have that modal, so we apply the payload silently and just
|
|
|
|
|
// warn in the console if the project references uninstalled libs.
|
|
|
|
|
if (result.kind === 'zip') {
|
|
|
|
|
const { loadFiles } = useEditorStore.getState();
|
|
|
|
|
const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } =
|
|
|
|
|
useSimulatorStore.getState();
|
|
|
|
|
stopSimulation();
|
|
|
|
|
if (result.boardType) setBoardType(result.boardType);
|
|
|
|
|
setBoardPosition(result.boardPosition);
|
|
|
|
|
setComponents(result.components);
|
|
|
|
|
setWires(result.wires);
|
|
|
|
|
if (result.files.length > 0) loadFiles(result.files);
|
|
|
|
|
if (result.libraries.length > 0) {
|
|
|
|
|
console.warn(
|
|
|
|
|
'[FileExplorer] Imported Wokwi zip references libraries you may need to install:',
|
|
|
|
|
result.libraries,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
} catch (err) {
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
window.alert((err as Error).message);
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-05-09 13:08:51 +07:00
|
|
|
const { t } = useTranslation();
|
2026-04-22 02:45:45 +07:00
|
|
|
const {
|
|
|
|
|
fileGroups,
|
|
|
|
|
activeFileId,
|
|
|
|
|
activeGroupId,
|
|
|
|
|
openFile,
|
|
|
|
|
createFile,
|
|
|
|
|
deleteFile,
|
|
|
|
|
renameFile,
|
|
|
|
|
setActiveGroup,
|
|
|
|
|
} = useEditorStore();
|
2026-03-16 00:04:01 +07:00
|
|
|
const boards = useSimulatorStore((s) => s.boards);
|
|
|
|
|
const activeBoardId = useSimulatorStore((s) => s.activeBoardId);
|
|
|
|
|
const setActiveBoardId = useSimulatorStore((s) => s.setActiveBoardId);
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
const updateBoard = useSimulatorStore((s) => s.updateBoard);
|
|
|
|
|
const updateComponent = useSimulatorStore((s) => s.updateComponent);
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
const components = useSimulatorStore((s) => s.components);
|
|
|
|
|
|
2026-06-04 03:52:11 +07:00
|
|
|
// Programmable custom-chips (CPU emulators whose chip.json declares
|
|
|
|
|
// programTargets) own a program the user can edit — a ROM source / C —
|
|
|
|
|
// shown as its own section below the boards. Behaviour/driver chips and
|
|
|
|
|
// predefined chips declare no programTargets and don't appear here (they're
|
|
|
|
|
// edited in the chip designer).
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
const programmableChips = components.filter(
|
2026-06-04 03:52:11 +07:00
|
|
|
(c) => c.metadataId === 'custom-chip' && isProgrammableChip(c.properties as Record<string, unknown>),
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
);
|
|
|
|
|
|
2026-06-04 03:52:11 +07:00
|
|
|
// Ensure each programmable chip has an editable program AND its editor group.
|
|
|
|
|
// loadExample seeds groups from an example's files; THIS is the path for a
|
|
|
|
|
// chip dropped fresh from the gallery (and older projects): a fresh chip has
|
|
|
|
|
// no program yet, so seed a default program.c the user can edit and persist
|
|
|
|
|
// programFile/programTarget onto the component so Compile/Run can build it.
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
useEffect(() => {
|
|
|
|
|
const ed = useEditorStore.getState();
|
2026-06-04 03:52:11 +07:00
|
|
|
const updateComponent = useSimulatorStore.getState().updateComponent;
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
for (const chip of programmableChips) {
|
|
|
|
|
const gid = chipFileGroupId(chip.id);
|
|
|
|
|
if (ed.fileGroups[gid]) continue;
|
2026-06-04 03:52:11 +07:00
|
|
|
const props = chip.properties as Record<string, unknown>;
|
|
|
|
|
const existing = String(props.programFile ?? '').trim();
|
|
|
|
|
if (existing) {
|
|
|
|
|
// Chip already names its program (e.g. an example) — seed from its
|
|
|
|
|
// saved source if any, else empty (loadExample usually filled it).
|
|
|
|
|
ed.createFileGroup(gid, [
|
|
|
|
|
{ name: existing, content: String(props.programSource ?? '') },
|
|
|
|
|
]);
|
|
|
|
|
} else {
|
|
|
|
|
// Fresh chip from the gallery — give it a starter program.c and
|
|
|
|
|
// remember its target CPU for the ROM compiler.
|
|
|
|
|
const target = targetForChip(String(props.chipJson ?? '{}'));
|
|
|
|
|
updateComponent(chip.id, {
|
|
|
|
|
properties: { ...props, programFile: DEFAULT_CHIP_PROGRAM_FILE, programTarget: target },
|
|
|
|
|
});
|
|
|
|
|
ed.createFileGroup(gid, [
|
|
|
|
|
{ name: DEFAULT_CHIP_PROGRAM_FILE, content: DEFAULT_CHIP_PROGRAM_C },
|
|
|
|
|
]);
|
|
|
|
|
}
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
}
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [components]);
|
2026-03-16 00:04:01 +07:00
|
|
|
|
2026-03-06 20:14:50 +07:00
|
|
|
const [contextMenu, setContextMenu] = useState<ContextMenu | null>(null);
|
|
|
|
|
const [renamingId, setRenamingId] = useState<string | null>(null);
|
|
|
|
|
const [renameValue, setRenameValue] = useState('');
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
// Inline rename of a SECTION header (a board or a chip). Kept separate from
|
|
|
|
|
// file rename (renamingId) so the two never collide.
|
|
|
|
|
const [renamingSection, setRenamingSection] = useState<{
|
|
|
|
|
id: string;
|
|
|
|
|
kind: 'board' | 'chip';
|
|
|
|
|
} | null>(null);
|
|
|
|
|
const [sectionRenameValue, setSectionRenameValue] = useState('');
|
2026-03-16 00:04:01 +07:00
|
|
|
// Track which board group is creating a file: boardGroupId or null
|
|
|
|
|
const [creatingInGroup, setCreatingInGroup] = useState<string | null>(null);
|
2026-03-06 20:14:50 +07:00
|
|
|
const [newFileName, setNewFileName] = useState('');
|
2026-03-16 00:04:01 +07:00
|
|
|
// Collapsed state per board ID
|
|
|
|
|
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
|
|
|
|
|
2026-03-06 20:14:50 +07:00
|
|
|
const renameInputRef = useRef<HTMLInputElement>(null);
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
const sectionRenameInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
// Set true by Escape so the input's onBlur (which fires when Escape unmounts
|
|
|
|
|
// the input) discards instead of committing the typed value.
|
|
|
|
|
const sectionRenameCancelledRef = useRef(false);
|
2026-03-06 20:14:50 +07:00
|
|
|
const newFileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (renamingId && renameInputRef.current) {
|
|
|
|
|
renameInputRef.current.focus();
|
|
|
|
|
renameInputRef.current.select();
|
|
|
|
|
}
|
|
|
|
|
}, [renamingId]);
|
|
|
|
|
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (renamingSection && sectionRenameInputRef.current) {
|
|
|
|
|
sectionRenameInputRef.current.focus();
|
|
|
|
|
sectionRenameInputRef.current.select();
|
|
|
|
|
}
|
|
|
|
|
}, [renamingSection]);
|
|
|
|
|
|
|
|
|
|
const startBoardRename = useCallback((board: { id: string; name?: string; boardKind: BoardKind }) => {
|
|
|
|
|
sectionRenameCancelledRef.current = false;
|
|
|
|
|
setRenamingSection({ id: board.id, kind: 'board' });
|
|
|
|
|
setSectionRenameValue(boardDisplayName(board));
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const startChipRename = useCallback((chipId: string, currentName: string) => {
|
|
|
|
|
sectionRenameCancelledRef.current = false;
|
|
|
|
|
setRenamingSection({ id: chipId, kind: 'chip' });
|
|
|
|
|
setSectionRenameValue(currentName);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const cancelSectionRename = useCallback(() => {
|
|
|
|
|
sectionRenameCancelledRef.current = true;
|
|
|
|
|
setRenamingSection(null);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const commitSectionRename = useCallback(() => {
|
|
|
|
|
// Escape cancelled this edit (it unmounts the input, firing onBlur) — discard.
|
|
|
|
|
if (sectionRenameCancelledRef.current) {
|
|
|
|
|
sectionRenameCancelledRef.current = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const target = renamingSection;
|
|
|
|
|
if (target) {
|
|
|
|
|
const value = sectionRenameValue.trim();
|
|
|
|
|
if (target.kind === 'board') {
|
|
|
|
|
// Empty clears the custom name -> boardDisplayName falls back to kind.
|
|
|
|
|
updateBoard(target.id, { name: value });
|
|
|
|
|
} else {
|
|
|
|
|
const comp = useSimulatorStore.getState().components.find((c) => c.id === target.id);
|
|
|
|
|
if (comp) {
|
|
|
|
|
updateComponent(target.id, {
|
|
|
|
|
properties: { ...comp.properties, chipName: value || 'Custom Chip' },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setRenamingSection(null);
|
|
|
|
|
}, [renamingSection, sectionRenameValue, updateBoard, updateComponent]);
|
|
|
|
|
|
2026-03-06 20:14:50 +07:00
|
|
|
useEffect(() => {
|
2026-03-16 00:04:01 +07:00
|
|
|
if (creatingInGroup && newFileInputRef.current) {
|
2026-03-06 20:14:50 +07:00
|
|
|
newFileInputRef.current.focus();
|
|
|
|
|
}
|
2026-03-16 00:04:01 +07:00
|
|
|
}, [creatingInGroup]);
|
2026-03-06 20:14:50 +07:00
|
|
|
|
|
|
|
|
// Close context menu on click outside
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!contextMenu) return;
|
|
|
|
|
const handler = () => setContextMenu(null);
|
|
|
|
|
document.addEventListener('click', handler);
|
|
|
|
|
return () => document.removeEventListener('click', handler);
|
|
|
|
|
}, [contextMenu]);
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const switchToBoard = useCallback(
|
|
|
|
|
(boardId: string, groupId: string) => {
|
|
|
|
|
setActiveBoardId(boardId);
|
|
|
|
|
// setActiveBoardId already calls setActiveGroup internally via the store
|
|
|
|
|
// but we make sure the editor group is also in sync
|
|
|
|
|
setActiveGroup(groupId);
|
|
|
|
|
},
|
|
|
|
|
[setActiveBoardId, setActiveGroup],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleFileClick = useCallback(
|
|
|
|
|
(fileId: string, boardId: string, groupId: string) => {
|
|
|
|
|
if (boardId !== activeBoardId) {
|
|
|
|
|
switchToBoard(boardId, groupId);
|
|
|
|
|
}
|
|
|
|
|
openFile(fileId);
|
|
|
|
|
},
|
|
|
|
|
[activeBoardId, switchToBoard, openFile],
|
|
|
|
|
);
|
2026-03-16 00:04:01 +07:00
|
|
|
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
// Chip program groups aren't tied to a board — switching to one just makes
|
|
|
|
|
// the chip's group active in the editor (no activeBoardId change).
|
|
|
|
|
const switchToChip = useCallback(
|
|
|
|
|
(groupId: string) => {
|
|
|
|
|
setActiveGroup(groupId);
|
|
|
|
|
},
|
|
|
|
|
[setActiveGroup],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleChipFileClick = useCallback(
|
|
|
|
|
(fileId: string, groupId: string) => {
|
|
|
|
|
if (groupId !== activeGroupId) switchToChip(groupId);
|
|
|
|
|
openFile(fileId);
|
|
|
|
|
},
|
|
|
|
|
[activeGroupId, switchToChip, openFile],
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const handleContextMenu = (e: React.MouseEvent, fileId: string, boardGroupId: string) => {
|
2026-03-06 20:14:50 +07:00
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
2026-03-16 00:04:01 +07:00
|
|
|
setContextMenu({ fileId, boardGroupId, x: e.clientX, y: e.clientY });
|
2026-03-06 20:14:50 +07:00
|
|
|
};
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const startRename = (fileId: string, groupId: string) => {
|
|
|
|
|
const files = fileGroups[groupId] ?? [];
|
2026-03-06 20:14:50 +07:00
|
|
|
const file = files.find((f) => f.id === fileId);
|
|
|
|
|
if (!file) return;
|
|
|
|
|
setRenamingId(fileId);
|
|
|
|
|
setRenameValue(file.name);
|
|
|
|
|
setContextMenu(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const commitRename = useCallback(() => {
|
|
|
|
|
if (renamingId && renameValue.trim()) {
|
|
|
|
|
renameFile(renamingId, renameValue.trim());
|
|
|
|
|
}
|
|
|
|
|
setRenamingId(null);
|
|
|
|
|
}, [renamingId, renameValue, renameFile]);
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const handleDelete = (fileId: string, groupId: string) => {
|
2026-03-06 20:14:50 +07:00
|
|
|
setContextMenu(null);
|
2026-03-16 00:04:01 +07:00
|
|
|
const files = fileGroups[groupId] ?? [];
|
2026-03-06 20:14:50 +07:00
|
|
|
if (files.length <= 1) return;
|
2026-05-09 13:08:51 +07:00
|
|
|
if (!window.confirm(t('editor.fileExplorer.confirmDelete'))) return;
|
2026-03-06 20:14:50 +07:00
|
|
|
deleteFile(fileId);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const startCreateFile = (boardId: string, groupId: string) => {
|
|
|
|
|
// Switch to this board first so createFile targets the right group
|
|
|
|
|
switchToBoard(boardId, groupId);
|
|
|
|
|
setCreatingInGroup(groupId);
|
2026-03-06 20:14:50 +07:00
|
|
|
setNewFileName('');
|
|
|
|
|
setContextMenu(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const commitCreateFile = useCallback(() => {
|
|
|
|
|
const name = newFileName.trim();
|
|
|
|
|
if (name) createFile(name);
|
2026-03-16 00:04:01 +07:00
|
|
|
setCreatingInGroup(null);
|
2026-03-06 20:14:50 +07:00
|
|
|
setNewFileName('');
|
|
|
|
|
}, [newFileName, createFile]);
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const toggleCollapse = (boardId: string) => {
|
|
|
|
|
setCollapsed((prev) => ({ ...prev, [boardId]: !prev[boardId] }));
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-06 20:14:50 +07:00
|
|
|
return (
|
|
|
|
|
<div className="file-explorer">
|
|
|
|
|
<div className="file-explorer-header">
|
2026-05-09 13:08:51 +07:00
|
|
|
<span className="file-explorer-title">{t('editor.fileExplorer.workspace')}</span>
|
2026-03-06 20:24:03 +07:00
|
|
|
<div className="file-explorer-header-actions">
|
2026-05-01 01:27:58 +07:00
|
|
|
<button
|
|
|
|
|
className="file-explorer-new-btn"
|
2026-05-09 13:08:51 +07:00
|
|
|
title={t('editor.fileExplorer.newWorkspace')}
|
2026-05-01 01:27:58 +07:00
|
|
|
onClick={onNewClick}
|
|
|
|
|
>
|
|
|
|
|
<IcoNewWorkspace />
|
|
|
|
|
</button>
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
<button
|
|
|
|
|
className="file-explorer-save-btn"
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
title="Open project (.vlx Velxio or .zip Wokwi)"
|
|
|
|
|
onClick={handleOpenProjectClick}
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
>
|
|
|
|
|
<IcoOpen />
|
|
|
|
|
</button>
|
|
|
|
|
<input
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
type="file"
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
accept={PROJECT_FILE_ACCEPT}
|
|
|
|
|
onChange={handleProjectFilePicked}
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
style={{ display: 'none' }}
|
|
|
|
|
/>
|
2026-03-06 20:24:03 +07:00
|
|
|
<button
|
|
|
|
|
className="file-explorer-save-btn"
|
2026-05-09 13:08:51 +07:00
|
|
|
title={t('editor.fileExplorer.saveProject')}
|
2026-03-06 20:24:03 +07:00
|
|
|
onClick={onSaveClick}
|
|
|
|
|
>
|
|
|
|
|
<IcoSave />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-03-06 20:14:50 +07:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="file-explorer-list">
|
2026-03-16 00:04:01 +07:00
|
|
|
{boards.map((board) => {
|
|
|
|
|
const groupId = board.activeFileGroupId;
|
|
|
|
|
const groupFiles = fileGroups[groupId] ?? [];
|
|
|
|
|
const isActiveBoard = board.id === activeBoardId;
|
|
|
|
|
const isOpen = !collapsed[board.id];
|
|
|
|
|
const color = BOARD_COLOR[board.boardKind];
|
|
|
|
|
|
|
|
|
|
// Status dot color
|
|
|
|
|
const statusColor = board.running
|
|
|
|
|
? '#22c55e'
|
|
|
|
|
: board.compiledProgram
|
2026-04-22 02:45:45 +07:00
|
|
|
? '#f59e0b'
|
|
|
|
|
: '#6b7280';
|
2026-03-16 00:04:01 +07:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={board.id} className="fe-board-section">
|
|
|
|
|
{/* Board section header */}
|
|
|
|
|
<div
|
|
|
|
|
className={`fe-board-header${isActiveBoard ? ' fe-board-header-active' : ''}`}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
switchToBoard(board.id, groupId);
|
|
|
|
|
if (!isOpen) toggleCollapse(board.id);
|
2026-03-06 20:14:50 +07:00
|
|
|
}}
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
title={`${boardDisplayName(board)} — ${t('editor.fileExplorer.clickToEdit')}`}
|
2026-03-16 00:04:01 +07:00
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
className="fe-collapse-btn"
|
2026-04-22 02:45:45 +07:00
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
toggleCollapse(board.id);
|
|
|
|
|
}}
|
2026-05-09 13:08:51 +07:00
|
|
|
title={isOpen ? t('editor.fileExplorer.collapse') : t('editor.fileExplorer.expand')}
|
2026-03-16 00:04:01 +07:00
|
|
|
>
|
|
|
|
|
<IcoChevron open={isOpen} />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<span className="fe-board-icon" style={{ color }}>
|
|
|
|
|
{BOARD_ICON[board.boardKind]}
|
|
|
|
|
</span>
|
|
|
|
|
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
{renamingSection?.id === board.id && renamingSection.kind === 'board' ? (
|
|
|
|
|
<input
|
|
|
|
|
ref={sectionRenameInputRef}
|
|
|
|
|
className="file-explorer-rename-input fe-section-rename-input"
|
|
|
|
|
value={sectionRenameValue}
|
|
|
|
|
onChange={(e) => setSectionRenameValue(e.target.value)}
|
|
|
|
|
onBlur={commitSectionRename}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter') commitSectionRename();
|
|
|
|
|
if (e.key === 'Escape') cancelSectionRename();
|
|
|
|
|
}}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<span
|
|
|
|
|
className="fe-board-label"
|
|
|
|
|
onDoubleClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
startBoardRename(board);
|
|
|
|
|
}}
|
|
|
|
|
title="Double-click to rename"
|
|
|
|
|
>
|
|
|
|
|
{boardDisplayName(board)}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-03-16 00:04:01 +07:00
|
|
|
|
|
|
|
|
<span
|
|
|
|
|
className="fe-status-dot"
|
|
|
|
|
style={{ background: statusColor }}
|
2026-05-09 13:08:51 +07:00
|
|
|
title={
|
|
|
|
|
board.running
|
|
|
|
|
? t('editor.fileExplorer.status.running')
|
|
|
|
|
: board.compiledProgram
|
|
|
|
|
? t('editor.fileExplorer.status.compiled')
|
|
|
|
|
: t('editor.fileExplorer.status.idle')
|
|
|
|
|
}
|
2026-03-16 00:04:01 +07:00
|
|
|
/>
|
|
|
|
|
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
{/* Rename + new-file buttons — visible on hover */}
|
|
|
|
|
<button
|
|
|
|
|
className="fe-board-new-btn"
|
|
|
|
|
title="Rename board (or double-click the name)"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
startBoardRename(board);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<IcoPencil />
|
|
|
|
|
</button>
|
2026-03-16 00:04:01 +07:00
|
|
|
<button
|
|
|
|
|
className="fe-board-new-btn"
|
2026-05-09 13:08:51 +07:00
|
|
|
title={t('editor.fileExplorer.newFileInBoard')}
|
2026-04-22 02:45:45 +07:00
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
startCreateFile(board.id, groupId);
|
|
|
|
|
}}
|
2026-03-16 00:04:01 +07:00
|
|
|
>
|
|
|
|
|
<IcoNewFile />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Files under this board */}
|
|
|
|
|
{isOpen && (
|
|
|
|
|
<div className="fe-board-files">
|
|
|
|
|
{groupFiles.map((file) => {
|
|
|
|
|
const isActiveFile = isActiveBoard && file.id === activeFileId;
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={file.id}
|
|
|
|
|
className={`file-explorer-item fe-file-item${isActiveFile ? ' file-explorer-item-active' : ''}`}
|
|
|
|
|
onClick={() => handleFileClick(file.id, board.id, groupId)}
|
|
|
|
|
onContextMenu={(e) => handleContextMenu(e, file.id, groupId)}
|
2026-04-22 02:45:45 +07:00
|
|
|
onDoubleClick={() => {
|
|
|
|
|
switchToBoard(board.id, groupId);
|
|
|
|
|
startRename(file.id, groupId);
|
|
|
|
|
}}
|
2026-05-09 13:08:51 +07:00
|
|
|
title={`${file.name}${file.modified ? ` (${t('editor.fileExplorer.unsavedSuffix')})` : ''}`}
|
2026-03-16 00:04:01 +07:00
|
|
|
>
|
|
|
|
|
<span className="file-explorer-icon">
|
|
|
|
|
<FileIcon name={file.name} />
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
{renamingId === file.id ? (
|
|
|
|
|
<input
|
|
|
|
|
ref={renameInputRef}
|
|
|
|
|
className="file-explorer-rename-input"
|
|
|
|
|
value={renameValue}
|
|
|
|
|
onChange={(e) => setRenameValue(e.target.value)}
|
|
|
|
|
onBlur={commitRename}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter') commitRename();
|
|
|
|
|
if (e.key === 'Escape') setRenamingId(null);
|
|
|
|
|
}}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="file-explorer-name">{file.name}</span>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{file.modified && (
|
2026-05-09 13:08:51 +07:00
|
|
|
<span className="file-explorer-dot" title={t('editor.fileExplorer.unsavedChanges')} />
|
2026-03-16 00:04:01 +07:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
{/* Inline new-file input for this group */}
|
|
|
|
|
{creatingInGroup === groupId && (
|
|
|
|
|
<div className="file-explorer-item file-explorer-item-new fe-file-item">
|
|
|
|
|
<span className="file-explorer-icon">
|
|
|
|
|
<IcoFile />
|
|
|
|
|
</span>
|
|
|
|
|
<input
|
|
|
|
|
ref={newFileInputRef}
|
|
|
|
|
className="file-explorer-rename-input"
|
|
|
|
|
value={newFileName}
|
|
|
|
|
placeholder="filename.ino"
|
|
|
|
|
onChange={(e) => setNewFileName(e.target.value)}
|
|
|
|
|
onBlur={commitCreateFile}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter') commitCreateFile();
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
setCreatingInGroup(null);
|
|
|
|
|
setNewFileName('');
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
{/* Programmable custom-chip program sections — one per chip, each its
|
|
|
|
|
own collapsible group (the chip's ROM source / C), separate from the
|
|
|
|
|
board sketch above. */}
|
|
|
|
|
{programmableChips.map((chip) => {
|
|
|
|
|
const groupId = chipFileGroupId(chip.id);
|
|
|
|
|
const groupFiles = fileGroups[groupId] ?? [];
|
|
|
|
|
if (groupFiles.length === 0) return null;
|
|
|
|
|
const isActiveGroup = activeGroupId === groupId;
|
|
|
|
|
const isOpen = !collapsed[chip.id];
|
|
|
|
|
const chipName =
|
|
|
|
|
String((chip.properties as Record<string, unknown>)?.chipName ?? '').trim() ||
|
|
|
|
|
'Custom Chip';
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={chip.id} className="fe-board-section">
|
|
|
|
|
<div
|
|
|
|
|
className={`fe-board-header${isActiveGroup ? ' fe-board-header-active' : ''}`}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
switchToChip(groupId);
|
|
|
|
|
if (!isOpen) toggleCollapse(chip.id);
|
|
|
|
|
}}
|
|
|
|
|
title={`${chipName} — ${t('editor.fileExplorer.clickToEdit')}`}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
className="fe-collapse-btn"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
toggleCollapse(chip.id);
|
|
|
|
|
}}
|
|
|
|
|
title={isOpen ? t('editor.fileExplorer.collapse') : t('editor.fileExplorer.expand')}
|
|
|
|
|
>
|
|
|
|
|
<IcoChevron open={isOpen} />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<span className="fe-board-icon" style={{ color: '#c4b5fd' }}>
|
|
|
|
|
<IcoChip />
|
|
|
|
|
</span>
|
|
|
|
|
|
feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.
- BoardInstance gains an optional user ; boardDisplayName(board) resolver
(name || kind label) routes every INSTANCE-label surface: file-explorer
section header, compile console (EditorToolbar), canvas selector/tooltip/
context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
properties. Enter commits, Escape cancels (cancel-flag ref guards the
unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
(resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
only seeds the blank defaults (My Chip / Custom Chip); loading an example
relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
(early return before a useCallback).
Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:22:25 +07:00
|
|
|
{renamingSection?.id === chip.id && renamingSection.kind === 'chip' ? (
|
|
|
|
|
<input
|
|
|
|
|
ref={sectionRenameInputRef}
|
|
|
|
|
className="file-explorer-rename-input fe-section-rename-input"
|
|
|
|
|
value={sectionRenameValue}
|
|
|
|
|
onChange={(e) => setSectionRenameValue(e.target.value)}
|
|
|
|
|
onBlur={commitSectionRename}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter') commitSectionRename();
|
|
|
|
|
if (e.key === 'Escape') cancelSectionRename();
|
|
|
|
|
}}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<span
|
|
|
|
|
className="fe-board-label"
|
|
|
|
|
onDoubleClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
startChipRename(chip.id, chipName);
|
|
|
|
|
}}
|
|
|
|
|
title="Double-click to rename"
|
|
|
|
|
>
|
|
|
|
|
{chipName}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!(renamingSection?.id === chip.id && renamingSection.kind === 'chip') && (
|
|
|
|
|
<button
|
|
|
|
|
className="fe-board-new-btn"
|
|
|
|
|
title="Rename chip (or double-click the name)"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
startChipRename(chip.id, chipName);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<IcoPencil />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
feat(custom-chip): program lives in its own editor group, not the board sketch
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:07:56 +07:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isOpen && (
|
|
|
|
|
<div className="fe-board-files">
|
|
|
|
|
{groupFiles.map((file) => {
|
|
|
|
|
const isActiveFile = isActiveGroup && file.id === activeFileId;
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={file.id}
|
|
|
|
|
className={`file-explorer-item fe-file-item${isActiveFile ? ' file-explorer-item-active' : ''}`}
|
|
|
|
|
onClick={() => handleChipFileClick(file.id, groupId)}
|
|
|
|
|
title={`${file.name}${file.modified ? ` (${t('editor.fileExplorer.unsavedSuffix')})` : ''}`}
|
|
|
|
|
>
|
|
|
|
|
<span className="file-explorer-icon">
|
|
|
|
|
<FileIcon name={file.name} />
|
|
|
|
|
</span>
|
|
|
|
|
<span className="file-explorer-name">{file.name}</span>
|
|
|
|
|
{file.modified && (
|
|
|
|
|
<span className="file-explorer-dot" title={t('editor.fileExplorer.unsavedChanges')} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
{/* Fallback: nothing on the canvas yet */}
|
|
|
|
|
{boards.length === 0 && programmableChips.length === 0 && (
|
2026-03-16 00:04:01 +07:00
|
|
|
<div style={{ color: '#666', fontSize: 11, padding: '12px 12px', lineHeight: 1.5 }}>
|
2026-05-09 13:08:51 +07:00
|
|
|
{t('editor.fileExplorer.emptyState')}
|
2026-03-06 20:14:50 +07:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{contextMenu && (
|
|
|
|
|
<div
|
|
|
|
|
className="file-explorer-context-menu"
|
|
|
|
|
style={{ top: contextMenu.y, left: contextMenu.x }}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
2026-03-16 00:04:01 +07:00
|
|
|
<button onClick={() => startRename(contextMenu.fileId, contextMenu.boardGroupId)}>
|
2026-05-09 13:08:51 +07:00
|
|
|
{t('editor.fileExplorer.contextMenu.rename')}
|
2026-03-06 20:14:50 +07:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="ctx-delete"
|
2026-03-16 00:04:01 +07:00
|
|
|
onClick={() => handleDelete(contextMenu.fileId, contextMenu.boardGroupId)}
|
|
|
|
|
disabled={(fileGroups[contextMenu.boardGroupId] ?? []).length <= 1}
|
2026-03-06 20:14:50 +07:00
|
|
|
>
|
2026-05-09 13:08:51 +07:00
|
|
|
{t('editor.fileExplorer.contextMenu.delete')}
|
2026-03-06 20:14:50 +07:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|