2026-03-06 20:14:50 +07:00
|
|
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
2026-05-09 13:08:51 +07:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2026-03-06 20:14:50 +07:00
|
|
|
import { useEditorStore } from '../../store/useEditorStore';
|
2026-03-16 00:04:01 +07:00
|
|
|
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
|
|
|
|
import type { BoardKind } from '../../types/board';
|
|
|
|
|
import { BOARD_KIND_LABELS } from '../../types/board';
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject';
|
2026-03-06 20:14:50 +07:00
|
|
|
import './FileExplorer.css';
|
|
|
|
|
|
2026-03-06 20:24:03 +07:00
|
|
|
// SVG icons — same style as EditorToolbar (stroke-based, 16x16)
|
|
|
|
|
const IcoFile = () => (
|
2026-04-22 02:45:45 +07:00
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
2026-03-06 20:24:03 +07:00
|
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
|
|
|
<polyline points="14 2 14 8 20 8" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const IcoHeader = () => (
|
2026-04-22 02:45:45 +07:00
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
2026-03-06 20:24:03 +07:00
|
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
|
|
|
<polyline points="14 2 14 8 20 8" />
|
|
|
|
|
<line x1="9" y1="13" x2="15" y2="13" />
|
|
|
|
|
<line x1="9" y1="17" x2="13" y2="17" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const IcoNewFile = () => (
|
2026-04-22 02:45:45 +07:00
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
2026-03-06 20:24:03 +07:00
|
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
|
|
|
<polyline points="14 2 14 8 20 8" />
|
|
|
|
|
<line x1="12" y1="18" x2="12" y2="12" />
|
|
|
|
|
<line x1="9" y1="15" x2="15" y2="15" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-01 01:27:58 +07:00
|
|
|
const IcoNewWorkspace = () => (
|
|
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
|
|
|
|
<line x1="12" y1="11" x2="12" y2="17" />
|
|
|
|
|
<line x1="9" y1="14" x2="15" y2="14" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-06 20:24:03 +07:00
|
|
|
const IcoSave = () => (
|
2026-04-22 02:45:45 +07:00
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
2026-03-06 20:24:03 +07:00
|
|
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
|
|
|
|
|
<polyline points="17 21 17 13 7 13 7 21" />
|
|
|
|
|
<polyline points="7 3 7 8 15 8" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
const IcoOpen = () => (
|
|
|
|
|
// Folder with an "open / upload arrow" — matches Save visually (both
|
|
|
|
|
// are project-IO actions) but points the opposite way to signal load.
|
|
|
|
|
<svg
|
|
|
|
|
width="22"
|
|
|
|
|
height="22"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
|
|
|
|
<polyline points="12 11 12 17" />
|
|
|
|
|
<polyline points="9 14 12 11 15 14" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const IcoChevron = ({ open }: { open: boolean }) => (
|
|
|
|
|
<svg
|
2026-04-22 02:45:45 +07:00
|
|
|
width="12"
|
|
|
|
|
height="12"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2.5"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
2026-03-16 00:04:01 +07:00
|
|
|
style={{ transform: open ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.15s' }}
|
|
|
|
|
>
|
|
|
|
|
<polyline points="9 18 15 12 9 6" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Board emoji icons — mirrors BoardPickerModal
|
|
|
|
|
const BOARD_ICON: Record<BoardKind, string> = {
|
2026-04-22 02:45:45 +07:00
|
|
|
'arduino-uno': '⬤',
|
|
|
|
|
'arduino-nano': '▪',
|
|
|
|
|
'arduino-mega': '▬',
|
2026-03-16 00:04:01 +07:00
|
|
|
'raspberry-pi-pico': '◆',
|
2026-04-22 02:45:45 +07:00
|
|
|
'raspberry-pi-3': '⬛',
|
|
|
|
|
esp32: '⬡',
|
2026-03-16 00:04:01 +07:00
|
|
|
'esp32-s3': '⬡',
|
|
|
|
|
'esp32-c3': '⬡',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Color accent per board family
|
|
|
|
|
const BOARD_COLOR: Record<BoardKind, string> = {
|
2026-04-22 02:45:45 +07:00
|
|
|
'arduino-uno': '#4fc3f7',
|
|
|
|
|
'arduino-nano': '#4fc3f7',
|
|
|
|
|
'arduino-mega': '#4fc3f7',
|
2026-03-16 00:04:01 +07:00
|
|
|
'raspberry-pi-pico': '#ce93d8',
|
2026-04-22 02:45:45 +07:00
|
|
|
'raspberry-pi-3': '#ef9a9a',
|
|
|
|
|
esp32: '#a5d6a7',
|
2026-03-16 00:04:01 +07:00
|
|
|
'esp32-s3': '#a5d6a7',
|
|
|
|
|
'esp32-c3': '#a5d6a7',
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-06 20:24:03 +07:00
|
|
|
function FileIcon({ name }: { name: string }) {
|
2026-03-06 20:14:50 +07:00
|
|
|
const ext = name.split('.').pop()?.toLowerCase() ?? '';
|
2026-03-06 20:24:03 +07:00
|
|
|
if (['h', 'hpp'].includes(ext)) return <IcoHeader />;
|
|
|
|
|
return <IcoFile />;
|
2026-03-06 20:14:50 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ContextMenu {
|
|
|
|
|
fileId: string;
|
2026-03-16 00:04:01 +07:00
|
|
|
boardGroupId: string;
|
2026-03-06 20:14:50 +07:00
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 20:24:03 +07:00
|
|
|
interface FileExplorerProps {
|
|
|
|
|
onSaveClick: () => void;
|
2026-05-01 01:27:58 +07:00
|
|
|
onNewClick: () => void;
|
2026-03-06 20:24:03 +07:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 01:27:58 +07:00
|
|
|
export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewClick }) => {
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
// Hidden <input type="file"> we trigger via ref when the user clicks
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
// the Open project button. Accepts both .vlx (Velxio native) and .zip
|
|
|
|
|
// (Wokwi bundle); the dispatcher in utils/importProject.ts decides which
|
|
|
|
|
// loader to run based on the file extension. Kept outside React state so
|
|
|
|
|
// the change event still fires when the user picks the same file twice.
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
const handleOpenProjectClick = useCallback(() => {
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
fileInputRef.current?.click();
|
|
|
|
|
}, []);
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
const handleProjectFilePicked = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
const file = e.target.files?.[0];
|
|
|
|
|
// Reset so picking the SAME file again later still fires onchange.
|
|
|
|
|
e.target.value = '';
|
|
|
|
|
if (!file) return;
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
const friendlyName = file.name.toLowerCase().endsWith('.zip') ? 'Wokwi .zip' : '.vlx';
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
if (
|
|
|
|
|
!window.confirm(
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
`Load this ${friendlyName} project? Your current workspace will be replaced. ` +
|
|
|
|
|
`This cannot be undone.`,
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
const result = await importProjectFile(file);
|
|
|
|
|
// .zip needs the caller to apply the payload to the stores (we keep
|
|
|
|
|
// that asymmetry so the toolbar's import flow can also pop the
|
|
|
|
|
// install-libraries modal afterwards). Here in the file explorer we
|
|
|
|
|
// don't have that modal, so we apply the payload silently and just
|
|
|
|
|
// warn in the console if the project references uninstalled libs.
|
|
|
|
|
if (result.kind === 'zip') {
|
|
|
|
|
const { loadFiles } = useEditorStore.getState();
|
|
|
|
|
const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } =
|
|
|
|
|
useSimulatorStore.getState();
|
|
|
|
|
stopSimulation();
|
|
|
|
|
if (result.boardType) setBoardType(result.boardType);
|
|
|
|
|
setBoardPosition(result.boardPosition);
|
|
|
|
|
setComponents(result.components);
|
|
|
|
|
setWires(result.wires);
|
|
|
|
|
if (result.files.length > 0) loadFiles(result.files);
|
|
|
|
|
if (result.libraries.length > 0) {
|
|
|
|
|
console.warn(
|
|
|
|
|
'[FileExplorer] Imported Wokwi zip references libraries you may need to install:',
|
|
|
|
|
result.libraries,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
} catch (err) {
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
window.alert((err as Error).message);
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-05-09 13:08:51 +07:00
|
|
|
const { t } = useTranslation();
|
2026-04-22 02:45:45 +07:00
|
|
|
const {
|
|
|
|
|
fileGroups,
|
|
|
|
|
activeFileId,
|
|
|
|
|
activeGroupId,
|
|
|
|
|
openFile,
|
|
|
|
|
createFile,
|
|
|
|
|
deleteFile,
|
|
|
|
|
renameFile,
|
|
|
|
|
setActiveGroup,
|
|
|
|
|
} = useEditorStore();
|
2026-03-16 00:04:01 +07:00
|
|
|
const boards = useSimulatorStore((s) => s.boards);
|
|
|
|
|
const activeBoardId = useSimulatorStore((s) => s.activeBoardId);
|
|
|
|
|
const setActiveBoardId = useSimulatorStore((s) => s.setActiveBoardId);
|
|
|
|
|
|
2026-03-06 20:14:50 +07:00
|
|
|
const [contextMenu, setContextMenu] = useState<ContextMenu | null>(null);
|
|
|
|
|
const [renamingId, setRenamingId] = useState<string | null>(null);
|
|
|
|
|
const [renameValue, setRenameValue] = useState('');
|
2026-03-16 00:04:01 +07:00
|
|
|
// Track which board group is creating a file: boardGroupId or null
|
|
|
|
|
const [creatingInGroup, setCreatingInGroup] = useState<string | null>(null);
|
2026-03-06 20:14:50 +07:00
|
|
|
const [newFileName, setNewFileName] = useState('');
|
2026-03-16 00:04:01 +07:00
|
|
|
// Collapsed state per board ID
|
|
|
|
|
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
|
|
|
|
|
2026-03-06 20:14:50 +07:00
|
|
|
const renameInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
const newFileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (renamingId && renameInputRef.current) {
|
|
|
|
|
renameInputRef.current.focus();
|
|
|
|
|
renameInputRef.current.select();
|
|
|
|
|
}
|
|
|
|
|
}, [renamingId]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-03-16 00:04:01 +07:00
|
|
|
if (creatingInGroup && newFileInputRef.current) {
|
2026-03-06 20:14:50 +07:00
|
|
|
newFileInputRef.current.focus();
|
|
|
|
|
}
|
2026-03-16 00:04:01 +07:00
|
|
|
}, [creatingInGroup]);
|
2026-03-06 20:14:50 +07:00
|
|
|
|
|
|
|
|
// Close context menu on click outside
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!contextMenu) return;
|
|
|
|
|
const handler = () => setContextMenu(null);
|
|
|
|
|
document.addEventListener('click', handler);
|
|
|
|
|
return () => document.removeEventListener('click', handler);
|
|
|
|
|
}, [contextMenu]);
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const switchToBoard = useCallback(
|
|
|
|
|
(boardId: string, groupId: string) => {
|
|
|
|
|
setActiveBoardId(boardId);
|
|
|
|
|
// setActiveBoardId already calls setActiveGroup internally via the store
|
|
|
|
|
// but we make sure the editor group is also in sync
|
|
|
|
|
setActiveGroup(groupId);
|
|
|
|
|
},
|
|
|
|
|
[setActiveBoardId, setActiveGroup],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleFileClick = useCallback(
|
|
|
|
|
(fileId: string, boardId: string, groupId: string) => {
|
|
|
|
|
if (boardId !== activeBoardId) {
|
|
|
|
|
switchToBoard(boardId, groupId);
|
|
|
|
|
}
|
|
|
|
|
openFile(fileId);
|
|
|
|
|
},
|
|
|
|
|
[activeBoardId, switchToBoard, openFile],
|
|
|
|
|
);
|
2026-03-16 00:04:01 +07:00
|
|
|
|
|
|
|
|
const handleContextMenu = (e: React.MouseEvent, fileId: string, boardGroupId: string) => {
|
2026-03-06 20:14:50 +07:00
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
2026-03-16 00:04:01 +07:00
|
|
|
setContextMenu({ fileId, boardGroupId, x: e.clientX, y: e.clientY });
|
2026-03-06 20:14:50 +07:00
|
|
|
};
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const startRename = (fileId: string, groupId: string) => {
|
|
|
|
|
const files = fileGroups[groupId] ?? [];
|
2026-03-06 20:14:50 +07:00
|
|
|
const file = files.find((f) => f.id === fileId);
|
|
|
|
|
if (!file) return;
|
|
|
|
|
setRenamingId(fileId);
|
|
|
|
|
setRenameValue(file.name);
|
|
|
|
|
setContextMenu(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const commitRename = useCallback(() => {
|
|
|
|
|
if (renamingId && renameValue.trim()) {
|
|
|
|
|
renameFile(renamingId, renameValue.trim());
|
|
|
|
|
}
|
|
|
|
|
setRenamingId(null);
|
|
|
|
|
}, [renamingId, renameValue, renameFile]);
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const handleDelete = (fileId: string, groupId: string) => {
|
2026-03-06 20:14:50 +07:00
|
|
|
setContextMenu(null);
|
2026-03-16 00:04:01 +07:00
|
|
|
const files = fileGroups[groupId] ?? [];
|
2026-03-06 20:14:50 +07:00
|
|
|
if (files.length <= 1) return;
|
2026-05-09 13:08:51 +07:00
|
|
|
if (!window.confirm(t('editor.fileExplorer.confirmDelete'))) return;
|
2026-03-06 20:14:50 +07:00
|
|
|
deleteFile(fileId);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const startCreateFile = (boardId: string, groupId: string) => {
|
|
|
|
|
// Switch to this board first so createFile targets the right group
|
|
|
|
|
switchToBoard(boardId, groupId);
|
|
|
|
|
setCreatingInGroup(groupId);
|
2026-03-06 20:14:50 +07:00
|
|
|
setNewFileName('');
|
|
|
|
|
setContextMenu(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const commitCreateFile = useCallback(() => {
|
|
|
|
|
const name = newFileName.trim();
|
|
|
|
|
if (name) createFile(name);
|
2026-03-16 00:04:01 +07:00
|
|
|
setCreatingInGroup(null);
|
2026-03-06 20:14:50 +07:00
|
|
|
setNewFileName('');
|
|
|
|
|
}, [newFileName, createFile]);
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
const toggleCollapse = (boardId: string) => {
|
|
|
|
|
setCollapsed((prev) => ({ ...prev, [boardId]: !prev[boardId] }));
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-06 20:14:50 +07:00
|
|
|
return (
|
|
|
|
|
<div className="file-explorer">
|
|
|
|
|
<div className="file-explorer-header">
|
2026-05-09 13:08:51 +07:00
|
|
|
<span className="file-explorer-title">{t('editor.fileExplorer.workspace')}</span>
|
2026-03-06 20:24:03 +07:00
|
|
|
<div className="file-explorer-header-actions">
|
2026-05-01 01:27:58 +07:00
|
|
|
<button
|
|
|
|
|
className="file-explorer-new-btn"
|
2026-05-09 13:08:51 +07:00
|
|
|
title={t('editor.fileExplorer.newWorkspace')}
|
2026-05-01 01:27:58 +07:00
|
|
|
onClick={onNewClick}
|
|
|
|
|
>
|
|
|
|
|
<IcoNewWorkspace />
|
|
|
|
|
</button>
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
<button
|
|
|
|
|
className="file-explorer-save-btn"
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
title="Open project (.vlx Velxio or .zip Wokwi)"
|
|
|
|
|
onClick={handleOpenProjectClick}
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
>
|
|
|
|
|
<IcoOpen />
|
|
|
|
|
</button>
|
|
|
|
|
<input
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
type="file"
|
feat(import): unify project import — accept .vlx and .zip in both entry points
Velxio had two parallel import paths that confused users (reported on
Discord by AgUn / dmontero):
* Toolbar "Import a project from a .zip file" → Wokwi .zip only
* File-explorer "Open .vlx file" → Velxio .vlx only
If you exported a Velxio project as .vlx and tried to bring it back via
the toolbar Import button, you bounced off "wrong format" with no hint
that the .vlx loader was hiding behind the file-explorer save-bar.
Fix: introduce `utils/importProject.ts` as the single dispatcher. It
sniffs the extension and routes:
*.vlx → importVlxFile (writes directly to stores)
*.zip → importFromWokwiZip (returns a payload the caller applies,
so the toolbar can still trigger the
install-libraries modal afterwards)
Both UI entry points now go through the dispatcher with the same
`accept=".vlx,.zip,application/json,application/zip"` filter:
* Toolbar "Import project (.vlx Velxio or .zip Wokwi)"
* File-explorer "Open project (.vlx Velxio or .zip Wokwi)"
The toolbar tooltip is i18n-driven — updated EN + 8 other locales
(es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same
clarification.
Wokwi compatibility kept intact — the .zip path still resolves to
`importFromWokwiZip` and the same library-install modal pops if the
imported project lists libraries we don't have locally.
2026-05-23 01:55:06 +07:00
|
|
|
accept={PROJECT_FILE_ACCEPT}
|
|
|
|
|
onChange={handleProjectFilePicked}
|
feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.
New: utils/vlxFile.ts
- buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
editor + simulator stores.
- triggerDownloadVlx({ name? }) — anchor-click download with a safe
filename. Returns the filename actually used.
- parseVlxFile(File) — async reader + validator. Checks
format === "velxio-project", version <= 1, and the required
arrays/objects are present. Throws VlxParseError with a human-
readable message on any issue.
- importVlxFile(File) — convenience wrapper that parses AND calls
useSimulatorStore.loadProjectState() with the result.
Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).
lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.
components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.
Verified with both builds:
- OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
3 buttons (New, Open, Save).
- OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
SaveProjectModal as before. Open .vlx still works (independent
button, not part of the save flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:33:00 +07:00
|
|
|
style={{ display: 'none' }}
|
|
|
|
|
/>
|
2026-03-06 20:24:03 +07:00
|
|
|
<button
|
|
|
|
|
className="file-explorer-save-btn"
|
2026-05-09 13:08:51 +07:00
|
|
|
title={t('editor.fileExplorer.saveProject')}
|
2026-03-06 20:24:03 +07:00
|
|
|
onClick={onSaveClick}
|
|
|
|
|
>
|
|
|
|
|
<IcoSave />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-03-06 20:14:50 +07:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="file-explorer-list">
|
2026-03-16 00:04:01 +07:00
|
|
|
{boards.map((board) => {
|
|
|
|
|
const groupId = board.activeFileGroupId;
|
|
|
|
|
const groupFiles = fileGroups[groupId] ?? [];
|
|
|
|
|
const isActiveBoard = board.id === activeBoardId;
|
|
|
|
|
const isOpen = !collapsed[board.id];
|
|
|
|
|
const color = BOARD_COLOR[board.boardKind];
|
|
|
|
|
|
|
|
|
|
// Status dot color
|
|
|
|
|
const statusColor = board.running
|
|
|
|
|
? '#22c55e'
|
|
|
|
|
: board.compiledProgram
|
2026-04-22 02:45:45 +07:00
|
|
|
? '#f59e0b'
|
|
|
|
|
: '#6b7280';
|
2026-03-16 00:04:01 +07:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={board.id} className="fe-board-section">
|
|
|
|
|
{/* Board section header */}
|
|
|
|
|
<div
|
|
|
|
|
className={`fe-board-header${isActiveBoard ? ' fe-board-header-active' : ''}`}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
switchToBoard(board.id, groupId);
|
|
|
|
|
if (!isOpen) toggleCollapse(board.id);
|
2026-03-06 20:14:50 +07:00
|
|
|
}}
|
2026-05-09 13:08:51 +07:00
|
|
|
title={`${BOARD_KIND_LABELS[board.boardKind]} — ${t('editor.fileExplorer.clickToEdit')}`}
|
2026-03-16 00:04:01 +07:00
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
className="fe-collapse-btn"
|
2026-04-22 02:45:45 +07:00
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
toggleCollapse(board.id);
|
|
|
|
|
}}
|
2026-05-09 13:08:51 +07:00
|
|
|
title={isOpen ? t('editor.fileExplorer.collapse') : t('editor.fileExplorer.expand')}
|
2026-03-16 00:04:01 +07:00
|
|
|
>
|
|
|
|
|
<IcoChevron open={isOpen} />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<span className="fe-board-icon" style={{ color }}>
|
|
|
|
|
{BOARD_ICON[board.boardKind]}
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
<span className="fe-board-label">{BOARD_KIND_LABELS[board.boardKind]}</span>
|
|
|
|
|
|
|
|
|
|
<span
|
|
|
|
|
className="fe-status-dot"
|
|
|
|
|
style={{ background: statusColor }}
|
2026-05-09 13:08:51 +07:00
|
|
|
title={
|
|
|
|
|
board.running
|
|
|
|
|
? t('editor.fileExplorer.status.running')
|
|
|
|
|
: board.compiledProgram
|
|
|
|
|
? t('editor.fileExplorer.status.compiled')
|
|
|
|
|
: t('editor.fileExplorer.status.idle')
|
|
|
|
|
}
|
2026-03-16 00:04:01 +07:00
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* New file button — visible on hover */}
|
|
|
|
|
<button
|
|
|
|
|
className="fe-board-new-btn"
|
2026-05-09 13:08:51 +07:00
|
|
|
title={t('editor.fileExplorer.newFileInBoard')}
|
2026-04-22 02:45:45 +07:00
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
startCreateFile(board.id, groupId);
|
|
|
|
|
}}
|
2026-03-16 00:04:01 +07:00
|
|
|
>
|
|
|
|
|
<IcoNewFile />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Files under this board */}
|
|
|
|
|
{isOpen && (
|
|
|
|
|
<div className="fe-board-files">
|
|
|
|
|
{groupFiles.map((file) => {
|
|
|
|
|
const isActiveFile = isActiveBoard && file.id === activeFileId;
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={file.id}
|
|
|
|
|
className={`file-explorer-item fe-file-item${isActiveFile ? ' file-explorer-item-active' : ''}`}
|
|
|
|
|
onClick={() => handleFileClick(file.id, board.id, groupId)}
|
|
|
|
|
onContextMenu={(e) => handleContextMenu(e, file.id, groupId)}
|
2026-04-22 02:45:45 +07:00
|
|
|
onDoubleClick={() => {
|
|
|
|
|
switchToBoard(board.id, groupId);
|
|
|
|
|
startRename(file.id, groupId);
|
|
|
|
|
}}
|
2026-05-09 13:08:51 +07:00
|
|
|
title={`${file.name}${file.modified ? ` (${t('editor.fileExplorer.unsavedSuffix')})` : ''}`}
|
2026-03-16 00:04:01 +07:00
|
|
|
>
|
|
|
|
|
<span className="file-explorer-icon">
|
|
|
|
|
<FileIcon name={file.name} />
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
{renamingId === file.id ? (
|
|
|
|
|
<input
|
|
|
|
|
ref={renameInputRef}
|
|
|
|
|
className="file-explorer-rename-input"
|
|
|
|
|
value={renameValue}
|
|
|
|
|
onChange={(e) => setRenameValue(e.target.value)}
|
|
|
|
|
onBlur={commitRename}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter') commitRename();
|
|
|
|
|
if (e.key === 'Escape') setRenamingId(null);
|
|
|
|
|
}}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="file-explorer-name">{file.name}</span>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{file.modified && (
|
2026-05-09 13:08:51 +07:00
|
|
|
<span className="file-explorer-dot" title={t('editor.fileExplorer.unsavedChanges')} />
|
2026-03-16 00:04:01 +07:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
{/* Inline new-file input for this group */}
|
|
|
|
|
{creatingInGroup === groupId && (
|
|
|
|
|
<div className="file-explorer-item file-explorer-item-new fe-file-item">
|
|
|
|
|
<span className="file-explorer-icon">
|
|
|
|
|
<IcoFile />
|
|
|
|
|
</span>
|
|
|
|
|
<input
|
|
|
|
|
ref={newFileInputRef}
|
|
|
|
|
className="file-explorer-rename-input"
|
|
|
|
|
value={newFileName}
|
|
|
|
|
placeholder="filename.ino"
|
|
|
|
|
onChange={(e) => setNewFileName(e.target.value)}
|
|
|
|
|
onBlur={commitCreateFile}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter') commitCreateFile();
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
setCreatingInGroup(null);
|
|
|
|
|
setNewFileName('');
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
{/* Fallback: no boards yet */}
|
|
|
|
|
{boards.length === 0 && (
|
|
|
|
|
<div style={{ color: '#666', fontSize: 11, padding: '12px 12px', lineHeight: 1.5 }}>
|
2026-05-09 13:08:51 +07:00
|
|
|
{t('editor.fileExplorer.emptyState')}
|
2026-03-06 20:14:50 +07:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{contextMenu && (
|
|
|
|
|
<div
|
|
|
|
|
className="file-explorer-context-menu"
|
|
|
|
|
style={{ top: contextMenu.y, left: contextMenu.x }}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
2026-03-16 00:04:01 +07:00
|
|
|
<button onClick={() => startRename(contextMenu.fileId, contextMenu.boardGroupId)}>
|
2026-05-09 13:08:51 +07:00
|
|
|
{t('editor.fileExplorer.contextMenu.rename')}
|
2026-03-06 20:14:50 +07:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="ctx-delete"
|
2026-03-16 00:04:01 +07:00
|
|
|
onClick={() => handleDelete(contextMenu.fileId, contextMenu.boardGroupId)}
|
|
|
|
|
disabled={(fileGroups[contextMenu.boardGroupId] ?? []).length <= 1}
|
2026-03-06 20:14:50 +07:00
|
|
|
>
|
2026-05-09 13:08:51 +07:00
|
|
|
{t('editor.fileExplorer.contextMenu.delete')}
|
2026-03-06 20:14:50 +07:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|