diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx
index 5ae83ef5..7da4f6e9 100644
--- a/frontend/src/components/editor/EditorToolbar.tsx
+++ b/frontend/src/components/editor/EditorToolbar.tsx
@@ -23,7 +23,8 @@ import { LibraryManagerModal } from '../simulator/LibraryManagerModal';
import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal';
import { parseCompileResult } from '../../utils/compilationLogger';
import type { CompilationLog } from '../../utils/compilationLogger';
-import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip';
+import { exportToWokwiZip } from '../../utils/wokwiZip';
+import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject';
import { readFirmwareFile } from '../../utils/firmwareLoader';
import {
trackCompileCode,
@@ -903,7 +904,14 @@ export const EditorToolbar = ({
importInputRef.current.value = '';
if (!file) return;
try {
- const result = await importFromWokwiZip(file);
+ const result = await importProjectFile(file);
+ if (result.kind === 'vlx') {
+ // importVlxFile already wrote into the stores.
+ setMessage({ type: 'success', text: `Imported ${file.name}` });
+ return;
+ }
+ // .zip path: apply the parsed payload to the stores ourselves, then
+ // surface any missing libraries via the existing install modal.
const { loadFiles } = useEditorStore.getState();
const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } =
useSimulatorStore.getState();
@@ -1110,11 +1118,13 @@ export const EditorToolbar = ({
{centerSlot &&
{centerSlot}
}
- {/* Hidden file input for import (always present) */}
+ {/* Hidden file input for project import. Accepts both .vlx
+ (Velxio native) and .zip (Wokwi bundle); the dispatcher in
+ utils/importProject.ts picks the right loader by extension. */}
diff --git a/frontend/src/components/editor/FileExplorer.tsx b/frontend/src/components/editor/FileExplorer.tsx
index 57e33901..641ff425 100644
--- a/frontend/src/components/editor/FileExplorer.tsx
+++ b/frontend/src/components/editor/FileExplorer.tsx
@@ -4,7 +4,7 @@ import { useEditorStore } from '../../store/useEditorStore';
import { useSimulatorStore } from '../../store/useSimulatorStore';
import type { BoardKind } from '../../types/board';
import { BOARD_KIND_LABELS } from '../../types/board';
-import { importVlxFile, VlxParseError } from '../../utils/vlxFile';
+import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject';
import './FileExplorer.css';
// SVG icons — same style as EditorToolbar (stroke-based, 16x16)
@@ -173,29 +173,54 @@ interface FileExplorerProps {
export const FileExplorer: React.FC = ({ onSaveClick, onNewClick }) => {
// Hidden we trigger via ref when the user clicks
- // the Open .vlx button. Kept outside React state so the change event
- // can fire repeatedly even if the user picks the same file twice.
+ // 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.
const fileInputRef = useRef(null);
- const handleOpenVlxClick = useCallback(() => {
+ const handleOpenProjectClick = useCallback(() => {
fileInputRef.current?.click();
}, []);
- const handleVlxFilePicked = useCallback(async (e: React.ChangeEvent) => {
+ const handleProjectFilePicked = useCallback(async (e: React.ChangeEvent) => {
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';
if (
!window.confirm(
- 'Load this .vlx file? Your current workspace will be replaced. This cannot be undone.',
+ `Load this ${friendlyName} project? Your current workspace will be replaced. ` +
+ `This cannot be undone.`,
)
) {
return;
}
try {
- await importVlxFile(file);
+ 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,
+ );
+ }
+ }
} catch (err) {
- const msg = err instanceof VlxParseError ? err.message : (err as Error).message;
- window.alert(`Could not load .vlx file:\n\n${msg}`);
+ window.alert((err as Error).message);
}
}, []);
@@ -330,16 +355,16 @@ export const FileExplorer: React.FC = ({ onSaveClick, onNewCl