feat(standalone): browser-local (IndexedDB) virtual projects
Loading a folder now imports a COPY into a browser-local IndexedDB project with
its own /p/:slug URL (original disk files untouched): edits persist to IDB across
visits, and the project exports via Download .zip or per-file. Gated by
VITE_LOCAL_PROJECTS=idb (on for the demo; off keeps the File System Access
write-back flow), so it's configurable per deployment.
Modular, swappable project sources behind the shared ProjectSource interface,
each self-describing via a SourceDescriptor whose kind is shown verbatim in the
UI — "Local (this browser)", "Remote · read-only", "Remote · editable" — on the
home page, the project view, and inside the editor, so the user always knows
whether/how Save persists:
- remote → REST backend (remote-rw)
- static → CDN gallery (remote-ro), saves download
- local (new) → idbProjectStore, writable IDB store
A composite layers the local store over the configured remote/gallery source,
routing per slug so imported/saved projects and the gallery share one namespace.
New, dependency-free (matching sync-client's raw-IDB ethos):
- lib/idb-project-store.ts raw IndexedDB store + create/delete/rename/export
- lib/zip.ts store-only ZIP writer (verified via system unzip)
- lib/import-folder.ts FSA/webkitdirectory folder → in-memory bytes
- lib/project-source-shared.ts source descriptors + deterministic uuid
- components/SourceChip, components/LocalProjectsSection
Home lists local projects (open/export/rename/delete). Verified: typecheck,
project-source tests, prod build, and IDB key-range project isolation in-browser
(prefix-colliding slugs don't leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:24:14 +02:00
|
|
|
import * as React from "react";
|
|
|
|
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
|
|
|
|
import { useQueryClient } from "@tanstack/react-query";
|
feat(web): wire pl_editor + symbol_editor tools (open, wizard-skip, UI)
Add the two newly WASM-ported editors to the web app the same way as the
existing tools:
- pl_editor (drawing-sheet, .kicad_wks): PL_EDITOR_FRAME overrides
OpenProjectFiles, so it gets the generic kicadOpenFile embind hook
(wasm/bindings/pl_editor_embind.cpp) for deterministic open. Mapped
.kicad_wks -> pl_editor in EXTENSION_TOOL.
- symbol_editor (symbol library): SYMBOL_EDIT_FRAME does NOT override
OpenProjectFiles, so it's treated as file-less (boot standalone, opens
libraries via its own UI). Added to FILELESS_TOOLS.
Both boot through single_top.cpp's STARTWIZARD, so both seed config to skip the
first-run wizard (TOOL_NEEDS_CONFIG_SEED) and get a /usr/bin/<binary> argv0.
contract: add to TOOLS, plus a TOOL_LABELS map for friendly names. The project
UI now renders file-less launch links generically from FILELESS_TOOLS and
per-file "Open in <label>" links from EXTENSION_TOOL (auto file-type detection),
so adding a tool needs no UI edits.
Verified in-browser: pl_editor opens a .kicad_wks (renders the sheet),
symbol_editor boots wizard-free; both with 0 console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:49:04 +02:00
|
|
|
import {
|
|
|
|
|
EXTENSION_TOOL,
|
|
|
|
|
FILELESS_TOOLS,
|
|
|
|
|
TOOL_LABELS,
|
|
|
|
|
type Tool,
|
2026-06-29 15:52:45 +02:00
|
|
|
projectPath,
|
|
|
|
|
projectToolPath,
|
2026-06-09 12:35:46 +02:00
|
|
|
} from "@pcbjam/shared";
|
2026-06-20 10:02:36 +02:00
|
|
|
import {
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
Download,
|
|
|
|
|
ExternalLink,
|
|
|
|
|
Loader2,
|
|
|
|
|
Trash2,
|
|
|
|
|
} from "lucide-react";
|
feat(standalone): browser-local (IndexedDB) virtual projects
Loading a folder now imports a COPY into a browser-local IndexedDB project with
its own /p/:slug URL (original disk files untouched): edits persist to IDB across
visits, and the project exports via Download .zip or per-file. Gated by
VITE_LOCAL_PROJECTS=idb (on for the demo; off keeps the File System Access
write-back flow), so it's configurable per deployment.
Modular, swappable project sources behind the shared ProjectSource interface,
each self-describing via a SourceDescriptor whose kind is shown verbatim in the
UI — "Local (this browser)", "Remote · read-only", "Remote · editable" — on the
home page, the project view, and inside the editor, so the user always knows
whether/how Save persists:
- remote → REST backend (remote-rw)
- static → CDN gallery (remote-ro), saves download
- local (new) → idbProjectStore, writable IDB store
A composite layers the local store over the configured remote/gallery source,
routing per slug so imported/saved projects and the gallery share one namespace.
New, dependency-free (matching sync-client's raw-IDB ethos):
- lib/idb-project-store.ts raw IndexedDB store + create/delete/rename/export
- lib/zip.ts store-only ZIP writer (verified via system unzip)
- lib/import-folder.ts FSA/webkitdirectory folder → in-memory bytes
- lib/project-source-shared.ts source descriptors + deterministic uuid
- components/SourceChip, components/LocalProjectsSection
Home lists local projects (open/export/rename/delete). Verified: typecheck,
project-source tests, prod build, and IDB key-range project isolation in-browser
(prefix-colliding slugs don't leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:24:14 +02:00
|
|
|
import { fetchFileBytes, useProject, useSourceDescriptor } from "@/lib/api";
|
|
|
|
|
import { downloadBytes } from "@/lib/download";
|
|
|
|
|
import { localProjectStore } from "@/lib/project-source";
|
|
|
|
|
import { zipFiles } from "@/lib/zip";
|
2026-06-01 16:30:29 +02:00
|
|
|
import { Button } from "@/components/ui/button";
|
feat(standalone): browser-local (IndexedDB) virtual projects
Loading a folder now imports a COPY into a browser-local IndexedDB project with
its own /p/:slug URL (original disk files untouched): edits persist to IDB across
visits, and the project exports via Download .zip or per-file. Gated by
VITE_LOCAL_PROJECTS=idb (on for the demo; off keeps the File System Access
write-back flow), so it's configurable per deployment.
Modular, swappable project sources behind the shared ProjectSource interface,
each self-describing via a SourceDescriptor whose kind is shown verbatim in the
UI — "Local (this browser)", "Remote · read-only", "Remote · editable" — on the
home page, the project view, and inside the editor, so the user always knows
whether/how Save persists:
- remote → REST backend (remote-rw)
- static → CDN gallery (remote-ro), saves download
- local (new) → idbProjectStore, writable IDB store
A composite layers the local store over the configured remote/gallery source,
routing per slug so imported/saved projects and the gallery share one namespace.
New, dependency-free (matching sync-client's raw-IDB ethos):
- lib/idb-project-store.ts raw IndexedDB store + create/delete/rename/export
- lib/zip.ts store-only ZIP writer (verified via system unzip)
- lib/import-folder.ts FSA/webkitdirectory folder → in-memory bytes
- lib/project-source-shared.ts source descriptors + deterministic uuid
- components/SourceChip, components/LocalProjectsSection
Home lists local projects (open/export/rename/delete). Verified: typecheck,
project-source tests, prod build, and IDB key-range project isolation in-browser
(prefix-colliding slugs don't leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:24:14 +02:00
|
|
|
import { SourceChip } from "@/components/SourceChip";
|
2026-06-27 07:59:09 +02:00
|
|
|
import { ToolGrid } from "@/components/ToolGrid";
|
|
|
|
|
import { FileTree } from "@/components/FileTree";
|
2026-06-20 10:02:36 +02:00
|
|
|
import { NewFileDialog } from "@/components/NewFileDialog";
|
2026-06-01 16:30:29 +02:00
|
|
|
|
|
|
|
|
function toolForPath(path: string): Tool | null {
|
|
|
|
|
const dot = path.lastIndexOf(".");
|
|
|
|
|
if (dot < 0) return null;
|
2026-06-09 12:35:46 +02:00
|
|
|
return EXTENSION_TOOL[path.slice(dot).toLowerCase()] ?? null;
|
2026-06-01 16:30:29 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-09 12:35:46 +02:00
|
|
|
/**
|
2026-06-27 07:59:09 +02:00
|
|
|
* Project-centric view (mirrors the closed app): an iconed tool launcher (same
|
|
|
|
|
* cards as the home page) + the project's files as a navigable folder tree.
|
|
|
|
|
* Opening a read-only GALLERY project auto-"moves it to local" — it's copied
|
|
|
|
|
* into a writable browser-local project at the same slug, so the composite source
|
|
|
|
|
* shadows the gallery and this view re-resolves as a local, editable project.
|
2026-06-09 12:35:46 +02:00
|
|
|
*/
|
|
|
|
|
export function ProjectView() {
|
2026-06-29 15:52:45 +02:00
|
|
|
const { scope = "", name: slug = "" } = useParams();
|
feat(standalone): browser-local (IndexedDB) virtual projects
Loading a folder now imports a COPY into a browser-local IndexedDB project with
its own /p/:slug URL (original disk files untouched): edits persist to IDB across
visits, and the project exports via Download .zip or per-file. Gated by
VITE_LOCAL_PROJECTS=idb (on for the demo; off keeps the File System Access
write-back flow), so it's configurable per deployment.
Modular, swappable project sources behind the shared ProjectSource interface,
each self-describing via a SourceDescriptor whose kind is shown verbatim in the
UI — "Local (this browser)", "Remote · read-only", "Remote · editable" — on the
home page, the project view, and inside the editor, so the user always knows
whether/how Save persists:
- remote → REST backend (remote-rw)
- static → CDN gallery (remote-ro), saves download
- local (new) → idbProjectStore, writable IDB store
A composite layers the local store over the configured remote/gallery source,
routing per slug so imported/saved projects and the gallery share one namespace.
New, dependency-free (matching sync-client's raw-IDB ethos):
- lib/idb-project-store.ts raw IndexedDB store + create/delete/rename/export
- lib/zip.ts store-only ZIP writer (verified via system unzip)
- lib/import-folder.ts FSA/webkitdirectory folder → in-memory bytes
- lib/project-source-shared.ts source descriptors + deterministic uuid
- components/SourceChip, components/LocalProjectsSection
Home lists local projects (open/export/rename/delete). Verified: typecheck,
project-source tests, prod build, and IDB key-range project isolation in-browser
(prefix-colliding slugs don't leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:24:14 +02:00
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const qc = useQueryClient();
|
2026-06-01 16:30:29 +02:00
|
|
|
const { data, isLoading, error } = useProject(slug);
|
feat(standalone): browser-local (IndexedDB) virtual projects
Loading a folder now imports a COPY into a browser-local IndexedDB project with
its own /p/:slug URL (original disk files untouched): edits persist to IDB across
visits, and the project exports via Download .zip or per-file. Gated by
VITE_LOCAL_PROJECTS=idb (on for the demo; off keeps the File System Access
write-back flow), so it's configurable per deployment.
Modular, swappable project sources behind the shared ProjectSource interface,
each self-describing via a SourceDescriptor whose kind is shown verbatim in the
UI — "Local (this browser)", "Remote · read-only", "Remote · editable" — on the
home page, the project view, and inside the editor, so the user always knows
whether/how Save persists:
- remote → REST backend (remote-rw)
- static → CDN gallery (remote-ro), saves download
- local (new) → idbProjectStore, writable IDB store
A composite layers the local store over the configured remote/gallery source,
routing per slug so imported/saved projects and the gallery share one namespace.
New, dependency-free (matching sync-client's raw-IDB ethos):
- lib/idb-project-store.ts raw IndexedDB store + create/delete/rename/export
- lib/zip.ts store-only ZIP writer (verified via system unzip)
- lib/import-folder.ts FSA/webkitdirectory folder → in-memory bytes
- lib/project-source-shared.ts source descriptors + deterministic uuid
- components/SourceChip, components/LocalProjectsSection
Home lists local projects (open/export/rename/delete). Verified: typecheck,
project-source tests, prod build, and IDB key-range project isolation in-browser
(prefix-colliding slugs don't leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:24:14 +02:00
|
|
|
const { data: descriptor } = useSourceDescriptor(slug);
|
|
|
|
|
const isLocal = descriptor?.kind === "local";
|
2026-06-27 07:59:09 +02:00
|
|
|
const isRemoteRo = descriptor?.kind === "remote-ro";
|
|
|
|
|
const store = localProjectStore();
|
feat(standalone): browser-local (IndexedDB) virtual projects
Loading a folder now imports a COPY into a browser-local IndexedDB project with
its own /p/:slug URL (original disk files untouched): edits persist to IDB across
visits, and the project exports via Download .zip or per-file. Gated by
VITE_LOCAL_PROJECTS=idb (on for the demo; off keeps the File System Access
write-back flow), so it's configurable per deployment.
Modular, swappable project sources behind the shared ProjectSource interface,
each self-describing via a SourceDescriptor whose kind is shown verbatim in the
UI — "Local (this browser)", "Remote · read-only", "Remote · editable" — on the
home page, the project view, and inside the editor, so the user always knows
whether/how Save persists:
- remote → REST backend (remote-rw)
- static → CDN gallery (remote-ro), saves download
- local (new) → idbProjectStore, writable IDB store
A composite layers the local store over the configured remote/gallery source,
routing per slug so imported/saved projects and the gallery share one namespace.
New, dependency-free (matching sync-client's raw-IDB ethos):
- lib/idb-project-store.ts raw IndexedDB store + create/delete/rename/export
- lib/zip.ts store-only ZIP writer (verified via system unzip)
- lib/import-folder.ts FSA/webkitdirectory folder → in-memory bytes
- lib/project-source-shared.ts source descriptors + deterministic uuid
- components/SourceChip, components/LocalProjectsSection
Home lists local projects (open/export/rename/delete). Verified: typecheck,
project-source tests, prod build, and IDB key-range project isolation in-browser
(prefix-colliding slugs don't leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:24:14 +02:00
|
|
|
const [busy, setBusy] = React.useState(false);
|
2026-06-20 10:02:36 +02:00
|
|
|
const [newFileTool, setNewFileTool] = React.useState<Tool | null>(null);
|
2026-06-27 07:59:09 +02:00
|
|
|
const [forkError, setForkError] = React.useState<string | null>(null);
|
|
|
|
|
const forkStartedRef = React.useRef(false);
|
|
|
|
|
|
|
|
|
|
// Auto "move to local": a read-only gallery project is copied into a writable
|
|
|
|
|
// browser-local project at the SAME slug. The composite source then routes this
|
|
|
|
|
// /p/:slug to the local copy (local wins), and invalidating the queries re-
|
|
|
|
|
// resolves this view as local — no button, no URL change. Runs once.
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!isRemoteRo || !store || !data || forkStartedRef.current) return;
|
|
|
|
|
forkStartedRef.current = true;
|
|
|
|
|
void (async () => {
|
|
|
|
|
try {
|
|
|
|
|
if (!(await store.hasProject(slug))) {
|
|
|
|
|
const files = await Promise.all(
|
|
|
|
|
data.files.map(async (f) => ({
|
|
|
|
|
path: f.path,
|
|
|
|
|
bytes: await fetchFileBytes(slug, f.path),
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
await store.createProject(data.project.name, files, { slug });
|
|
|
|
|
}
|
|
|
|
|
await qc.invalidateQueries({ queryKey: ["local-projects"] });
|
|
|
|
|
await qc.invalidateQueries({ queryKey: ["source-descriptor", slug] });
|
|
|
|
|
await qc.invalidateQueries({ queryKey: ["project", slug] });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setForkError(e instanceof Error ? e.message : String(e));
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
}, [isRemoteRo, store, data, slug, qc]);
|
feat(standalone): browser-local (IndexedDB) virtual projects
Loading a folder now imports a COPY into a browser-local IndexedDB project with
its own /p/:slug URL (original disk files untouched): edits persist to IDB across
visits, and the project exports via Download .zip or per-file. Gated by
VITE_LOCAL_PROJECTS=idb (on for the demo; off keeps the File System Access
write-back flow), so it's configurable per deployment.
Modular, swappable project sources behind the shared ProjectSource interface,
each self-describing via a SourceDescriptor whose kind is shown verbatim in the
UI — "Local (this browser)", "Remote · read-only", "Remote · editable" — on the
home page, the project view, and inside the editor, so the user always knows
whether/how Save persists:
- remote → REST backend (remote-rw)
- static → CDN gallery (remote-ro), saves download
- local (new) → idbProjectStore, writable IDB store
A composite layers the local store over the configured remote/gallery source,
routing per slug so imported/saved projects and the gallery share one namespace.
New, dependency-free (matching sync-client's raw-IDB ethos):
- lib/idb-project-store.ts raw IndexedDB store + create/delete/rename/export
- lib/zip.ts store-only ZIP writer (verified via system unzip)
- lib/import-folder.ts FSA/webkitdirectory folder → in-memory bytes
- lib/project-source-shared.ts source descriptors + deterministic uuid
- components/SourceChip, components/LocalProjectsSection
Home lists local projects (open/export/rename/delete). Verified: typecheck,
project-source tests, prod build, and IDB key-range project isolation in-browser
(prefix-colliding slugs don't leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:24:14 +02:00
|
|
|
|
|
|
|
|
const exportZip = async () => {
|
|
|
|
|
if (!store) return;
|
|
|
|
|
setBusy(true);
|
|
|
|
|
try {
|
|
|
|
|
downloadBytes(`${slug}.zip`, zipFiles(await store.readFiles(slug)));
|
|
|
|
|
} finally {
|
|
|
|
|
setBusy(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const downloadOne = async (path: string) => {
|
|
|
|
|
downloadBytes(path, await fetchFileBytes(slug, path));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const remove = async () => {
|
|
|
|
|
if (!store || !data) return;
|
|
|
|
|
if (!window.confirm(`Delete "${data.project.name}" from this browser? This can't be undone.`))
|
|
|
|
|
return;
|
|
|
|
|
await store.deleteProject(slug);
|
|
|
|
|
await qc.invalidateQueries({ queryKey: ["local-projects"] });
|
|
|
|
|
navigate("/");
|
|
|
|
|
};
|
2026-06-01 16:30:29 +02:00
|
|
|
|
2026-06-27 07:59:09 +02:00
|
|
|
// A tool launched from the grid: file-less tools (gerber/calculator + the
|
|
|
|
|
// symbol/footprint editors, which load the full project lib set) open project-
|
|
|
|
|
// scoped with a full reload; document tools create a templated file first.
|
|
|
|
|
const launchTool = (tool: Tool) => {
|
|
|
|
|
if (FILELESS_TOOLS.has(tool)) {
|
2026-06-29 15:52:45 +02:00
|
|
|
window.location.assign(projectToolPath(scope, slug, tool));
|
2026-06-27 07:59:09 +02:00
|
|
|
} else {
|
|
|
|
|
setNewFileTool(tool);
|
2026-06-20 10:02:36 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-27 07:59:09 +02:00
|
|
|
const renderFileActions = (path: string) => {
|
|
|
|
|
const tool = toolForPath(path);
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{isLocal && (
|
|
|
|
|
<button
|
|
|
|
|
className="inline-flex items-center gap-1 rounded-md border px-2.5 py-1.5 text-sm hover:bg-accent"
|
|
|
|
|
title="Download this file"
|
|
|
|
|
onClick={() => void downloadOne(path)}
|
|
|
|
|
>
|
|
|
|
|
<Download size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{tool && (
|
|
|
|
|
<a
|
|
|
|
|
className="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm hover:bg-accent"
|
2026-06-29 15:52:45 +02:00
|
|
|
href={projectPath(scope, slug, path)}
|
2026-06-27 07:59:09 +02:00
|
|
|
>
|
|
|
|
|
<ExternalLink size={14} /> Open in {TOOL_LABELS[tool]}
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const movingToLocal = isRemoteRo && !!store && !forkError;
|
|
|
|
|
|
2026-06-01 16:30:29 +02:00
|
|
|
return (
|
|
|
|
|
<div className="container py-10">
|
|
|
|
|
<Button asChild variant="ghost" size="sm" className="mb-4">
|
|
|
|
|
<Link to="/">
|
2026-06-09 12:35:46 +02:00
|
|
|
<ArrowLeft /> Home
|
2026-06-01 16:30:29 +02:00
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{isLoading && (
|
|
|
|
|
<p className="flex items-center gap-2 text-muted-foreground">
|
|
|
|
|
<Loader2 className="animate-spin" /> loading…
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2026-06-09 12:35:46 +02:00
|
|
|
{error && <p className="text-destructive">{(error as Error).message}</p>}
|
2026-06-01 16:30:29 +02:00
|
|
|
|
|
|
|
|
{data && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="mb-6">
|
feat(standalone): browser-local (IndexedDB) virtual projects
Loading a folder now imports a COPY into a browser-local IndexedDB project with
its own /p/:slug URL (original disk files untouched): edits persist to IDB across
visits, and the project exports via Download .zip or per-file. Gated by
VITE_LOCAL_PROJECTS=idb (on for the demo; off keeps the File System Access
write-back flow), so it's configurable per deployment.
Modular, swappable project sources behind the shared ProjectSource interface,
each self-describing via a SourceDescriptor whose kind is shown verbatim in the
UI — "Local (this browser)", "Remote · read-only", "Remote · editable" — on the
home page, the project view, and inside the editor, so the user always knows
whether/how Save persists:
- remote → REST backend (remote-rw)
- static → CDN gallery (remote-ro), saves download
- local (new) → idbProjectStore, writable IDB store
A composite layers the local store over the configured remote/gallery source,
routing per slug so imported/saved projects and the gallery share one namespace.
New, dependency-free (matching sync-client's raw-IDB ethos):
- lib/idb-project-store.ts raw IndexedDB store + create/delete/rename/export
- lib/zip.ts store-only ZIP writer (verified via system unzip)
- lib/import-folder.ts FSA/webkitdirectory folder → in-memory bytes
- lib/project-source-shared.ts source descriptors + deterministic uuid
- components/SourceChip, components/LocalProjectsSection
Home lists local projects (open/export/rename/delete). Verified: typecheck,
project-source tests, prod build, and IDB key-range project isolation in-browser
(prefix-colliding slugs don't leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:24:14 +02:00
|
|
|
<h1 className="flex flex-wrap items-center gap-3 text-2xl font-semibold tracking-tight">
|
2026-06-01 16:30:29 +02:00
|
|
|
{data.project.name}
|
feat(standalone): browser-local (IndexedDB) virtual projects
Loading a folder now imports a COPY into a browser-local IndexedDB project with
its own /p/:slug URL (original disk files untouched): edits persist to IDB across
visits, and the project exports via Download .zip or per-file. Gated by
VITE_LOCAL_PROJECTS=idb (on for the demo; off keeps the File System Access
write-back flow), so it's configurable per deployment.
Modular, swappable project sources behind the shared ProjectSource interface,
each self-describing via a SourceDescriptor whose kind is shown verbatim in the
UI — "Local (this browser)", "Remote · read-only", "Remote · editable" — on the
home page, the project view, and inside the editor, so the user always knows
whether/how Save persists:
- remote → REST backend (remote-rw)
- static → CDN gallery (remote-ro), saves download
- local (new) → idbProjectStore, writable IDB store
A composite layers the local store over the configured remote/gallery source,
routing per slug so imported/saved projects and the gallery share one namespace.
New, dependency-free (matching sync-client's raw-IDB ethos):
- lib/idb-project-store.ts raw IndexedDB store + create/delete/rename/export
- lib/zip.ts store-only ZIP writer (verified via system unzip)
- lib/import-folder.ts FSA/webkitdirectory folder → in-memory bytes
- lib/project-source-shared.ts source descriptors + deterministic uuid
- components/SourceChip, components/LocalProjectsSection
Home lists local projects (open/export/rename/delete). Verified: typecheck,
project-source tests, prod build, and IDB key-range project isolation in-browser
(prefix-colliding slugs don't leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:24:14 +02:00
|
|
|
{descriptor && <SourceChip descriptor={descriptor} />}
|
2026-06-01 16:30:29 +02:00
|
|
|
</h1>
|
2026-06-29 15:52:45 +02:00
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{data.project.scope}/projects/{data.project.slug}
|
|
|
|
|
</p>
|
2026-06-01 16:30:29 +02:00
|
|
|
</div>
|
|
|
|
|
|
2026-06-27 07:59:09 +02:00
|
|
|
{movingToLocal ? (
|
|
|
|
|
<p className="flex items-center gap-2 text-muted-foreground">
|
|
|
|
|
<Loader2 className="animate-spin" /> Preparing your editable copy…
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{forkError && (
|
|
|
|
|
<p className="mb-4 text-sm text-destructive">
|
|
|
|
|
Couldn't prepare a local copy: {forkError}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Tools — same iconed launcher as the home page. */}
|
|
|
|
|
<section className="mb-8">
|
|
|
|
|
<h2 className="mb-3 text-lg font-medium">Tools</h2>
|
|
|
|
|
<ToolGrid onLaunch={launchTool} />
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{/* Project-level actions (local projects). */}
|
|
|
|
|
{isLocal && (
|
|
|
|
|
<div className="mb-6 flex flex-wrap gap-3">
|
|
|
|
|
<Button variant="outline" size="sm" disabled={busy} onClick={() => void exportZip()}>
|
|
|
|
|
{busy ? <Loader2 className="animate-spin" size={15} /> : <Download size={15} />}
|
|
|
|
|
Download .zip
|
|
|
|
|
</Button>
|
2026-06-20 10:02:36 +02:00
|
|
|
<Button
|
2026-06-27 07:59:09 +02:00
|
|
|
variant="ghost"
|
2026-06-20 10:02:36 +02:00
|
|
|
size="sm"
|
2026-06-27 07:59:09 +02:00
|
|
|
className="text-destructive hover:text-destructive"
|
|
|
|
|
onClick={() => void remove()}
|
2026-06-20 10:02:36 +02:00
|
|
|
>
|
2026-06-27 07:59:09 +02:00
|
|
|
<Trash2 size={15} /> Delete
|
2026-06-20 10:02:36 +02:00
|
|
|
</Button>
|
2026-06-01 16:30:29 +02:00
|
|
|
</div>
|
2026-06-27 07:59:09 +02:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<h2 className="mb-3 text-lg font-medium">
|
|
|
|
|
Files ({data.files.length})
|
|
|
|
|
</h2>
|
|
|
|
|
<FileTree
|
|
|
|
|
files={data.files}
|
|
|
|
|
renderActions={renderFileActions}
|
|
|
|
|
emptyText="No files yet — use the Tools above to create one."
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-06-20 10:02:36 +02:00
|
|
|
|
|
|
|
|
<NewFileDialog
|
|
|
|
|
tool={newFileTool}
|
|
|
|
|
onClose={() => setNewFileTool(null)}
|
|
|
|
|
project={{ slug, existingPaths: data.files.map((f) => f.path) }}
|
|
|
|
|
/>
|
2026-06-01 16:30:29 +02:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|