velxio/frontend/src/components/editor/FileExplorer.tsx

919 lines
33 KiB
TypeScript
Raw Normal View History

import React, { useState, useRef, useEffect, useCallback } from 'react';
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';
import { useSimulatorStore } from '../../store/useSimulatorStore';
feat(custom-chip): newly-added programmable chip auto-gets an editable program; chaser-c goes board-less Two fixes from live testing feedback: 1. Adding a programmable chip (Z80/8080) from the gallery created NO program group — only the chip(s) from the example had one. Root cause: 'programmable' was detected by a non-empty programFile, but a fresh chip's programFile is empty until the user writes one. Now detection uses the canonical signal — chip.json's programTargets — via isProgrammableChip(). When such a chip lands with no program yet, the file explorer seeds an editable program.c (DEFAULT_CHIP_PROGRAM_C, a working walking-LED skeleton) into its own group and stamps programFile/programTarget onto the component so Compile/Run can build it. Behaviour/driver and predefined chips (no programTargets) still get no group — edited in the chip designer. 2. z80-led-chaser-c now runs board-less on a regulated power supply (no Arduino, mirroring z80-larson-no-board) — the Arduino only ever supplied 5V and added confusion. chaser.c stays the chip's editable program in its own section. - romCompileService: isProgrammableChip(), DEFAULT_CHIP_PROGRAM_FILE/_C. - FileExplorer: detect by programTargets; auto-seed program.c + persist programFile/programTarget for fresh chips. - examples-retro-intel: chaser-c -> board-less (psu + 8 resistors + 8 LEDs), drop the now-unused Arduino sketch const; fix a stale sdcc --code-loc comment. - Tests: board+chip case moved to z80-larson-scanner (still board-based); isProgrammableChip unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:52:11 +07:00
import {
isProgrammableChip,
targetForChip,
DEFAULT_CHIP_PROGRAM_FILE,
DEFAULT_CHIP_PROGRAM_C,
} from '../../services/romCompileService';
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';
import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject';
import './FileExplorer.css';
// SVG icons — same style as EditorToolbar (stroke-based, 16x16)
const IcoFile = () => (
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<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 = () => (
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<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 = () => (
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<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>
);
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>
);
const IcoSave = () => (
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<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>
);
const IcoChevron = ({ open }: { open: boolean }) => (
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
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>
);
// Board emoji icons — mirrors BoardPickerModal
const BOARD_ICON: Record<BoardKind, string> = {
'arduino-uno': '⬤',
'arduino-nano': '▪',
'arduino-mega': '▬',
'raspberry-pi-pico': '◆',
'raspberry-pi-3': '⬛',
esp32: '⬡',
'esp32-s3': '⬡',
'esp32-c3': '⬡',
'stm32-bluepill': '◈',
'stm32-blackpill': '◈',
'stm32-bluepill-f103cb': '◈',
'stm32-blackpill-f401': '◈',
'stm32-f4-discovery': '◈',
'stm32-olimex-h405': '◈',
'stm32-netduino-plus2': '◈',
'stm32-netduino2': '◈',
};
// Color accent per board family
const BOARD_COLOR: Record<BoardKind, string> = {
'arduino-uno': '#4fc3f7',
'arduino-nano': '#4fc3f7',
'arduino-mega': '#4fc3f7',
'raspberry-pi-pico': '#ce93d8',
'raspberry-pi-3': '#ef9a9a',
esp32: '#a5d6a7',
'esp32-s3': '#a5d6a7',
'esp32-c3': '#a5d6a7',
'stm32-bluepill': '#80cbc4',
'stm32-blackpill': '#b0bec5',
'stm32-bluepill-f103cb': '#80cbc4',
'stm32-blackpill-f401': '#b0bec5',
'stm32-f4-discovery': '#90caf9',
'stm32-olimex-h405': '#a5d6a7',
'stm32-netduino-plus2': '#ce93d8',
'stm32-netduino2': '#ce93d8',
};
function FileIcon({ name }: { name: string }) {
const ext = name.split('.').pop()?.toLowerCase() ?? '';
if (['h', 'hpp'].includes(ext)) return <IcoHeader />;
return <IcoFile />;
}
interface ContextMenu {
fileId: string;
boardGroupId: string;
x: number;
y: number;
}
interface FileExplorerProps {
onSaveClick: () => void;
onNewClick: () => void;
}
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
// 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);
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();
}, []);
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;
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(
`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 {
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) {
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
}
}, []);
const { t } = useTranslation();
const {
fileGroups,
activeFileId,
activeGroupId,
openFile,
createFile,
deleteFile,
renameFile,
setActiveGroup,
manifestViewBoardId,
setManifestView,
} = useEditorStore();
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);
feat(custom-chip): newly-added programmable chip auto-gets an editable program; chaser-c goes board-less Two fixes from live testing feedback: 1. Adding a programmable chip (Z80/8080) from the gallery created NO program group — only the chip(s) from the example had one. Root cause: 'programmable' was detected by a non-empty programFile, but a fresh chip's programFile is empty until the user writes one. Now detection uses the canonical signal — chip.json's programTargets — via isProgrammableChip(). When such a chip lands with no program yet, the file explorer seeds an editable program.c (DEFAULT_CHIP_PROGRAM_C, a working walking-LED skeleton) into its own group and stamps programFile/programTarget onto the component so Compile/Run can build it. Behaviour/driver and predefined chips (no programTargets) still get no group — edited in the chip designer. 2. z80-led-chaser-c now runs board-less on a regulated power supply (no Arduino, mirroring z80-larson-no-board) — the Arduino only ever supplied 5V and added confusion. chaser.c stays the chip's editable program in its own section. - romCompileService: isProgrammableChip(), DEFAULT_CHIP_PROGRAM_FILE/_C. - FileExplorer: detect by programTargets; auto-seed program.c + persist programFile/programTarget for fresh chips. - examples-retro-intel: chaser-c -> board-less (psu + 8 resistors + 8 LEDs), drop the now-unused Arduino sketch const; fix a stale sdcc --code-loc comment. - Tests: board+chip case moved to z80-larson-scanner (still board-based); isProgrammableChip unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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(
feat(custom-chip): newly-added programmable chip auto-gets an editable program; chaser-c goes board-less Two fixes from live testing feedback: 1. Adding a programmable chip (Z80/8080) from the gallery created NO program group — only the chip(s) from the example had one. Root cause: 'programmable' was detected by a non-empty programFile, but a fresh chip's programFile is empty until the user writes one. Now detection uses the canonical signal — chip.json's programTargets — via isProgrammableChip(). When such a chip lands with no program yet, the file explorer seeds an editable program.c (DEFAULT_CHIP_PROGRAM_C, a working walking-LED skeleton) into its own group and stamps programFile/programTarget onto the component so Compile/Run can build it. Behaviour/driver and predefined chips (no programTargets) still get no group — edited in the chip designer. 2. z80-led-chaser-c now runs board-less on a regulated power supply (no Arduino, mirroring z80-larson-no-board) — the Arduino only ever supplied 5V and added confusion. chaser.c stays the chip's editable program in its own section. - romCompileService: isProgrammableChip(), DEFAULT_CHIP_PROGRAM_FILE/_C. - FileExplorer: detect by programTargets; auto-seed program.c + persist programFile/programTarget for fresh chips. - examples-retro-intel: chaser-c -> board-less (psu + 8 resistors + 8 LEDs), drop the now-unused Arduino sketch const; fix a stale sdcc --code-loc comment. - Tests: board+chip case moved to z80-larson-scanner (still board-based); isProgrammableChip unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
);
feat(custom-chip): newly-added programmable chip auto-gets an editable program; chaser-c goes board-less Two fixes from live testing feedback: 1. Adding a programmable chip (Z80/8080) from the gallery created NO program group — only the chip(s) from the example had one. Root cause: 'programmable' was detected by a non-empty programFile, but a fresh chip's programFile is empty until the user writes one. Now detection uses the canonical signal — chip.json's programTargets — via isProgrammableChip(). When such a chip lands with no program yet, the file explorer seeds an editable program.c (DEFAULT_CHIP_PROGRAM_C, a working walking-LED skeleton) into its own group and stamps programFile/programTarget onto the component so Compile/Run can build it. Behaviour/driver and predefined chips (no programTargets) still get no group — edited in the chip designer. 2. z80-led-chaser-c now runs board-less on a regulated power supply (no Arduino, mirroring z80-larson-no-board) — the Arduino only ever supplied 5V and added confusion. chaser.c stays the chip's editable program in its own section. - romCompileService: isProgrammableChip(), DEFAULT_CHIP_PROGRAM_FILE/_C. - FileExplorer: detect by programTargets; auto-seed program.c + persist programFile/programTarget for fresh chips. - examples-retro-intel: chaser-c -> board-less (psu + 8 resistors + 8 LEDs), drop the now-unused Arduino sketch const; fix a stale sdcc --code-loc comment. - Tests: board+chip case moved to z80-larson-scanner (still board-based); isProgrammableChip unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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();
feat(custom-chip): newly-added programmable chip auto-gets an editable program; chaser-c goes board-less Two fixes from live testing feedback: 1. Adding a programmable chip (Z80/8080) from the gallery created NO program group — only the chip(s) from the example had one. Root cause: 'programmable' was detected by a non-empty programFile, but a fresh chip's programFile is empty until the user writes one. Now detection uses the canonical signal — chip.json's programTargets — via isProgrammableChip(). When such a chip lands with no program yet, the file explorer seeds an editable program.c (DEFAULT_CHIP_PROGRAM_C, a working walking-LED skeleton) into its own group and stamps programFile/programTarget onto the component so Compile/Run can build it. Behaviour/driver and predefined chips (no programTargets) still get no group — edited in the chip designer. 2. z80-led-chaser-c now runs board-less on a regulated power supply (no Arduino, mirroring z80-larson-no-board) — the Arduino only ever supplied 5V and added confusion. chaser.c stays the chip's editable program in its own section. - romCompileService: isProgrammableChip(), DEFAULT_CHIP_PROGRAM_FILE/_C. - FileExplorer: detect by programTargets; auto-seed program.c + persist programFile/programTarget for fresh chips. - examples-retro-intel: chaser-c -> board-less (psu + 8 resistors + 8 LEDs), drop the now-unused Arduino sketch const; fix a stale sdcc --code-loc comment. - Tests: board+chip case moved to z80-larson-scanner (still board-based); isProgrammableChip unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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;
feat(custom-chip): newly-added programmable chip auto-gets an editable program; chaser-c goes board-less Two fixes from live testing feedback: 1. Adding a programmable chip (Z80/8080) from the gallery created NO program group — only the chip(s) from the example had one. Root cause: 'programmable' was detected by a non-empty programFile, but a fresh chip's programFile is empty until the user writes one. Now detection uses the canonical signal — chip.json's programTargets — via isProgrammableChip(). When such a chip lands with no program yet, the file explorer seeds an editable program.c (DEFAULT_CHIP_PROGRAM_C, a working walking-LED skeleton) into its own group and stamps programFile/programTarget onto the component so Compile/Run can build it. Behaviour/driver and predefined chips (no programTargets) still get no group — edited in the chip designer. 2. z80-led-chaser-c now runs board-less on a regulated power supply (no Arduino, mirroring z80-larson-no-board) — the Arduino only ever supplied 5V and added confusion. chaser.c stays the chip's editable program in its own section. - romCompileService: isProgrammableChip(), DEFAULT_CHIP_PROGRAM_FILE/_C. - FileExplorer: detect by programTargets; auto-seed program.c + persist programFile/programTarget for fresh chips. - examples-retro-intel: chaser-c -> board-less (psu + 8 resistors + 8 LEDs), drop the now-unused Arduino sketch const; fix a stale sdcc --code-loc comment. - Tests: board+chip case moved to z80-larson-scanner (still board-based); isProgrammableChip unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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]);
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('');
// Track which board group is creating a file: boardGroupId or null
const [creatingInGroup, setCreatingInGroup] = useState<string | null>(null);
const [newFileName, setNewFileName] = useState('');
// Collapsed state per board ID
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
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);
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]);
useEffect(() => {
if (creatingInGroup && newFileInputRef.current) {
newFileInputRef.current.focus();
}
}, [creatingInGroup]);
// Close context menu on click outside
useEffect(() => {
if (!contextMenu) return;
const handler = () => setContextMenu(null);
document.addEventListener('click', handler);
return () => document.removeEventListener('click', handler);
}, [contextMenu]);
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],
);
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],
);
const handleContextMenu = (e: React.MouseEvent, fileId: string, boardGroupId: string) => {
e.preventDefault();
e.stopPropagation();
setContextMenu({ fileId, boardGroupId, x: e.clientX, y: e.clientY });
};
const startRename = (fileId: string, groupId: string) => {
const files = fileGroups[groupId] ?? [];
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]);
const handleDelete = (fileId: string, groupId: string) => {
setContextMenu(null);
const files = fileGroups[groupId] ?? [];
if (files.length <= 1) return;
if (!window.confirm(t('editor.fileExplorer.confirmDelete'))) return;
deleteFile(fileId);
};
const startCreateFile = (boardId: string, groupId: string) => {
// Switch to this board first so createFile targets the right group
switchToBoard(boardId, groupId);
setCreatingInGroup(groupId);
setNewFileName('');
setContextMenu(null);
};
const commitCreateFile = useCallback(() => {
const name = newFileName.trim();
if (name) createFile(name);
setCreatingInGroup(null);
setNewFileName('');
}, [newFileName, createFile]);
const toggleCollapse = (boardId: string) => {
setCollapsed((prev) => ({ ...prev, [boardId]: !prev[boardId] }));
};
return (
<div className="file-explorer">
<div className="file-explorer-header">
<span className="file-explorer-title">{t('editor.fileExplorer.workspace')}</span>
<div className="file-explorer-header-actions">
<button
className="file-explorer-new-btn"
title={t('editor.fileExplorer.newWorkspace')}
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"
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"
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' }}
/>
<button
className="file-explorer-save-btn"
title={t('editor.fileExplorer.saveProject')}
onClick={onSaveClick}
>
<IcoSave />
</button>
</div>
</div>
<div className="file-explorer-list">
{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
? '#f59e0b'
: '#6b7280';
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);
}}
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')}`}
>
<button
className="fe-collapse-btn"
onClick={(e) => {
e.stopPropagation();
toggleCollapse(board.id);
}}
title={isOpen ? t('editor.fileExplorer.collapse') : t('editor.fileExplorer.expand')}
>
<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>
)}
<span
className="fe-status-dot"
style={{ background: statusColor }}
title={
board.running
? t('editor.fileExplorer.status.running')
: board.compiledProgram
? t('editor.fileExplorer.status.compiled')
: t('editor.fileExplorer.status.idle')
}
/>
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>
<button
className="fe-board-new-btn"
title={t('editor.fileExplorer.newFileInBoard')}
onClick={(e) => {
e.stopPropagation();
startCreateFile(board.id, groupId);
}}
>
<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)}
onDoubleClick={() => {
switchToBoard(board.id, groupId);
startRename(file.id, groupId);
}}
title={`${file.name}${file.modified ? ` (${t('editor.fileExplorer.unsavedSuffix')})` : ''}`}
>
<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 && (
<span className="file-explorer-dot" title={t('editor.fileExplorer.unsavedChanges')} />
)}
</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>
)}
{/* velxio.json THIS board's declared library manifest
(compile scope), grouped with the board's code so it is
clear which board it belongs to. There is one per board.
Clicking switches to the board and opens the Library
Manager on its list. */}
<div
className={`file-explorer-item fe-file-item${
manifestViewBoardId === board.id ? ' file-explorer-item-active' : ''
}`}
onClick={() => {
switchToBoard(board.id, groupId);
// Open the READ-ONLY libraries.json view (not the modal).
// Library actions happen in the Library Manager modal.
setManifestView(board.id);
}}
title={`libraries.json — ${boardDisplayName(board)}'s declared libraries (read-only; manage from the Library Manager)`}
>
<span className="file-explorer-icon" style={{ color: '#ffd60a' }}>
<FileIcon name="libraries.json" />
</span>
<span className="file-explorer-name">libraries.json</span>
<span
style={{
marginLeft: 'auto',
fontSize: 10,
color: '#9d9d9d',
background: '#2d2d2d',
borderRadius: 8,
padding: '1px 7px',
}}
title={
board.libraries && board.libraries.length
? `${board.libraries.length} declared: ${board.libraries.join(', ')}`
: 'No libraries declared for this board'
}
>
{board.libraries?.length ?? 0}
</span>
</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 && (
<div style={{ color: '#666', fontSize: 11, padding: '12px 12px', lineHeight: 1.5 }}>
{t('editor.fileExplorer.emptyState')}
</div>
)}
</div>
{contextMenu && (
<div
className="file-explorer-context-menu"
style={{ top: contextMenu.y, left: contextMenu.x }}
onClick={(e) => e.stopPropagation()}
>
<button onClick={() => startRename(contextMenu.fileId, contextMenu.boardGroupId)}>
{t('editor.fileExplorer.contextMenu.rename')}
</button>
<button
className="ctx-delete"
onClick={() => handleDelete(contextMenu.fileId, contextMenu.boardGroupId)}
disabled={(fileGroups[contextMenu.boardGroupId] ?? []).length <= 1}
>
{t('editor.fileExplorer.contextMenu.delete')}
</button>
</div>
)}
</div>
);
};