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>
This commit is contained in:
Gergő Törcsvári 2026-06-20 08:24:14 +02:00
commit ce9ede1e9e
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
16 changed files with 743 additions and 81 deletions

View file

@ -60,6 +60,17 @@ export const PROJECT_SOURCE_KIND: ProjectSourceKind =
* "https://cdn.pcbjam.com/content/2.7.7/manifest.json". Required for "static". */
export const PROJECT_MANIFEST_URL = import.meta.env.VITE_PROJECT_MANIFEST_URL || null;
/**
* When "idb", loaded folders import into a browser-local (IndexedDB) project
* with its own /p/:slug URL — editable, persistent across visits, exported via
* Download .zip / per-file — instead of the in-page File System Access flow.
* The local store is layered (composite) alongside the configured remote/gallery
* source. Off by default (plain dev keeps disk write-back); build-demo.mjs turns
* it on for the demo. See lib/idb-project-store.ts + lib/project-source.ts.
*/
export const LOCAL_PROJECTS_ENABLED =
import.meta.env.VITE_LOCAL_PROJECTS === "idb";
import type { ProviderConfig, ProviderKind } from "@/wasm/collab";
import { remoteLibsSource } from "@/wasm/libs/remote-source";
import { scopedLibsSource } from "@/wasm/libs/scoped-source";