════════════════════════════════════════════════════════════════════════ ARCHITECTURE Gnomad Webcanvas · docs/ARCHITECTURE.md ════════════════════════════════════════════════════════════════════════ ARCHITECTURE — GNOMAD WEBCANVAS =============================== Version: 0.1.0-beta.2 Last updated: June 2026 ──────────────────────────────────────── SYSTEM CONTEXT -------------- Gnomad Webcanvas is a live HTML/CSS/JS editor with instant preview. Users write code in Monaco panes, see output in a sandboxed iframe, and persist or share work via localStorage, native files, or URL hash. [code] ┌──────────────────────────────────────────────────────────────────┐ │ User (designer / developer) │ └────────────────────────────┬─────────────────────────────────────┘ │ ┌───────────────────┴───────────────────┐ │ │ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────┐ │ Browser (Vite) │ │ Tauri Desktop │ │ localStorage │ │ native dialogs │ │ ZIP export │ │ filesystem I/O │ └──────────┬──────────┘ └──────────┬──────────┘ │ │ └──────────────┬──────────────────────┘ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ React 19 SPA (TypeScript) │ │ App · Workspace · TopNavbar · PreviewFrame · FloatingTools │ │ Zustand store · platformBridge · parseHtmlFile · assembleSource · shareUrl │ │ preferences (theme persistence) │ └────────────────────────────┬─────────────────────────────────────┘ │ ┌──────────────┴──────────────┐ ▼ ▼ Monaco Editor (3 panes) Preview iframe (srcDoc) HTML · CSS · JS CDN libs · fonts · console ──────────────────────────────────────── LAYER RESPONSIBILITIES ---------------------- Layer | Owns | Must not own React UI | Layout, editor UX, preview assembly, sharing | Direct filesystem without bridge Zustand store | Code, projects, preview settings | Persistence format (delegated to utils) Platform bridge | Open/Save abstraction | UI rendering Tauri plugins | Native dialogs, file read/write | Editor logic Preview iframe | User HTML/CSS/JS execution | Parent DOM access (sandboxed) ──────────────────────────────────────── COMPONENT HIERARCHY ------------------- [code] App ├── TopNavbar — projects, templates, libraries, export, share ├── Workspace │ ├── Group (react-resizable-panels) │ │ ├── Panel[] — CodeEditorPane × 3 (html, css, js) │ │ └── Panel — PreviewFrame │ │ ├── iframe (assembled srcDoc) │ │ └── ConsolePanel │ └── ResizeHandle └── FloatingTools ├── ColorPicker ├── CssGenerator └── FontPairings ──────────────────────────────────────── DATA FLOW — LIVE PREVIEW ------------------------ [code] User types in Monaco │ ▼ editorStore.setCode(pane, code) isDirty = true │ ▼ useDebounce (500 ms) │ ▼ assembleSource(html, css, js, libraries, fonts) │ ▼ PreviewFrame sets iframe srcDoc │ ▼ User JS runs in iframe ──postMessage──► ConsolePanel Force refresh: previewKey increment bypasses debounce/cache when user presses Ctrl/Cmd+Enter. Pause: When previewPaused is true, assembledSource returns null — iframe retains last rendered document. ──────────────────────────────────────── DATA FLOW — PERSISTENCE ----------------------- Web [code] editorStore change │ ▼ App useEffect (debounce 1 s) │ ▼ projectManager.saveProjectData(id, payload) │ ▼ localStorage Desktop [code] Ctrl/Cmd+S │ ▼ platformBridge.saveProject(html, css, js, path?) │ ▼ assembleFullHtml() → writeTextFile via Tauri fs plugin Open: Native dialog → readTextFile → parseHtmlFile() (DOMParser) extracts styles, inline scripts, and body HTML. Implementation: src/utils/parseHtmlFile.ts — uses DOMParser + querySelectorAll, not regex. Covered by 8 unit tests. ──────────────────────────────────────── HYDRATION PRIORITY (STARTUP) ---------------------------- On first mount, App.tsx loads state in order: 1. URL hash — shared link (decodeProjectFromHash) 2. Legacy migration — old localStorage keys → new project format 3. Existing projects — first project in index Hash wins so shared links always load correctly even if local projects exist. ──────────────────────────────────────── PLATFORM BRIDGE --------------- [typescript] interface PlatformBridge { isDesktop: boolean; openProject(): Promise; saveProject(html, css, js, filePath?): Promise; saveProjectAs(html, css, js): Promise; } Method | Web | Desktop openProject | Returns null | Open dialog → parse HTML saveProject | Returns null | Write to path or prompt Save As saveProjectAs | Returns null | Save dialog → write file Detection: '__TAURI_INTERNALS__' in window ──────────────────────────────────────── SHARE URL ENCODING ------------------ [code] Project state → JSON → lz-string compress → URL hash Payload fields: html, css, js, optional libs, fp (font pairing), hf/bf (custom fonts). ──────────────────────────────────────── SECURITY BOUNDARY ----------------- Surface | Trust level Editor UI | Trusted (app code) Preview iframe | Untrusted (user code) — sandboxed, no parent access CDN scripts | Third-party — user opt-in via library toggle File I/O | User-selected paths only (Tauri dialog scope) See SECURITY_MODEL.md for details. ──────────────────────────────────────── TAURI CONFIGURATION ------------------- Setting | Value Product name | Gnomad Webcanvas Identifier | com.liveview.notepad Default window | 1400×900, min 800×500 Bundle targets | all (deb, rpm, msi, dmg, etc.) CSP | null (preview requires inline scripts) Capabilities grant fs read/write and dialog open/save on the main window only. ──────────────────────────────────────── EXTENSION POINTS ---------------- Future feature | Likely touch points Custom Tauri commands | src-tauri/src/lib.rs, platformBridge.ts Auto-updater | tauri.conf.json plugins, release workflow Multi-file projects | projectManager.ts, Rust workspace commands Collaboration | Backend or CRDT layer (not in v0.1) ──────────────────────────────────────── Built with ❤️ by Gnomad Studio 🦙 ════════════════════════════════════════════════════════════════════════ Built with ❤️ by Gnomad Studio 🦙 https://gnomadstudio.org ════════════════════════════════════════════════════════════════════════