Vibecode Whimsical
track this build5 phases, 15 steps, beginner friendly0%A constrained diagramming tool with nodes, connectors, sticky notes, and export is a very vibecodable core loop; polish and collaboration are what you lose.
You are building a lean indie version of Whimsical. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Whimsical · indie build An SVG canvas with pan and zoom, boxes you can type into, connectors that re-route when you move a node, one-key auto-layout for tree and flow diagrams, and PNG and SVG export that is pixel-honest at any zoom level. Documents live in the browser and export to a file you own. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite with TypeScript, no framework | a canvas application is one big stateful view, and a component tree fights you rather than helping | | Rendering | SVG in the DOM, not a canvas element | text stays selectable, export is the same tree you are already looking at, and hit testing is free | | Layout | ELK via elkjs for automatic arrangement | layered graph layout is a solved research problem and hand-rolling it produces crossing edges forever | | Storage | IndexedDB for documents, with file export and import | no account, no server, and the file is the thing you actually own | | Geometry | The SVG viewBox for pan and zoom | a CSS transform looks the same on screen and then betrays you at export time | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs Vite and the build. Nothing ships to a server, but the tooling needs it. - Get it: Download the LTS build from nodejs.org, or install with nvm. - Verify: node --version prints v22 or higher - [ ] **A current browser with developer tools** · free - Why: This is a browser application, and the SVG inspector is how you will debug geometry. - Get it: Chrome, Firefox or Safari, all current. Test in at least two, because SVG text metrics differ subtly. - Verify: You can inspect an SVG element and see its computed bounding box - [ ] **A second application that opens SVG files** · free - Why: The export check in Phase 5 is meaningless if the only thing you open the file in is the browser that made it. - Get it: Inkscape is free and strict, which makes it a good test. A design tool you already have works too. - Verify: You can open an SVG file and see whether its fonts survived - [ ] **A font you are allowed to embed** · free with an open licence - Why: Embedding a font in an exported file is redistribution, and not every licence permits it. - Get it: Pick an open licensed family such as Inter or Source Sans, and keep the licence file alongside it. Or convert text to paths on export and avoid the question. - Verify: The licence permits embedding, or your export outlines text instead ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Real-time collaboration and presence, which is why teams buy the paid plan. - The template library and the wireframe component kits. - Document embedding and workspace integrations. - Comment threads and review workflows on a diagram. - templates - multiplayer polish - docs integration - wireframe components - comments - sharing permissions If one of those is essential to you, that is the reason to keep paying for Whimsical, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Whimsical The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a diagram editor with connectors that stay attached, auto-layout, and exports that match the screen to replace Whimsical. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Vite with TypeScript, no framework · a canvas application is one big stateful view, and a component tree fights you rather than helping - Rendering: SVG in the DOM, not a canvas element · text stays selectable, export is the same tree you are already looking at, and hit testing is free - Layout: ELK via elkjs for automatic arrangement · layered graph layout is a solved research problem and hand-rolling it produces crossing edges forever - Storage: IndexedDB for documents, with file export and import · no account, no server, and the file is the thing you actually own - Geometry: The SVG viewBox for pan and zoom · a CSS transform looks the same on screen and then betrays you at export time ### Data model (create this before Phase 1) - docs(id, title, created_at, updated_at) - nodes(id, doc_id, kind, text, x, y, w, h, fill, stroke) · kind is box, ellipse, diamond or sticky - edges(id, doc_id, from_node, to_node, from_port, to_port, label, waypoints_json, style) - Ports are named anchors on a node, not absolute coordinates, so moving a node cannot detach an edge. - Selection, hover and the current tool are ephemeral view state and never persisted. ### Phase 1 · Canvas, pan, zoom and a node Build: An SVG element filling the viewport with a viewBox you control. Pan by adjusting the viewBox origin and zoom by adjusting its width and height. Create a rectangle on click, drag it, and edit its text in place. Convert pointer coordinates into canvas coordinates through one function and use that function everywhere. Trap: Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. Done when: Zooming to 400 percent, moving a node, and zooming back to 100 percent leaves the node exactly where you put it. Clicking at any zoom level selects the node under the cursor and not one nearby. Text entered into a node persists after a reload. Do not build yet: No connectors, no layout, no export. ### Phase 2 · Connectors that stay attached Build: Define named ports on each node. An edge stores the two node ids and port names, never absolute points. Render an orthogonal path that leaves each port perpendicular to its side. Recompute the path whenever either endpoint moves or resizes. Trap: Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. Done when: Moving a node re-routes its edges and no edge passes through the body of either node it connects. Deleting a node deletes its edges rather than leaving edges pointing at nothing. Resizing a node keeps the edge attached to the same side. Do not build yet: No automatic layout yet, and no export. ### Phase 3 · Automatic layout Build: Feed the graph to elkjs with a layered algorithm, take back positions, and animate nodes to their new places. Offer layout direction as a choice. Preserve manual positions as a separate mode so auto-layout is something you invoke rather than something that fights you. Trap: Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. Done when: A twenty node tree lays out with no overlapping node bounding boxes, verified by checking every pair rather than by looking. Layout is undoable in one step. Running layout twice on an unchanged graph produces the same positions both times. Do not build yet: No export yet. ### Phase 4 · Text that exports the way it looks Build: SVG has no text wrapping, so measure and lay out lines yourself into tspan elements with explicit positions. Grow node height to fit the text. Use the same layout function for the screen and for the export so they cannot disagree. Trap: A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. Done when: A node with a long label wraps to the same lines in the browser and in the exported SVG opened in a different application. Increasing text length grows the node rather than overflowing it. No text is clipped at any zoom level. Do not build yet: Export comes next, and this phase is what makes it honest. ### Phase 5 · Export and local documents Build: Export SVG by serialising the canvas with the viewBox set to the content bounds and fonts either embedded or converted to paths. Export PNG by rasterising that SVG at a chosen scale. Save documents in IndexedDB and support file save and open so nothing is trapped in one browser profile. Trap: An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. Done when: An exported SVG opens in a different application with the same fonts and the same line breaks. An exported PNG at two times scale has exactly double the pixel dimensions of the one times export. A document exported to a file and re-imported into a fresh browser profile is identical, including edges and layout. Do not build yet: No sharing or collaboration. ### Phase 6 · Sharing, and being honest about collaboration Build: A read-only share: export the document to a compact encoded form in a URL, or upload it to storage you own and serve a viewer page. State plainly that simultaneous editing is not supported rather than implementing something that loses edits. Trap: Real-time collaborative editing needs conflict-free replicated data types or operational transforms plus presence and reconnection handling. A naive last-write-wins sync over a socket looks like it works in a demo with one person and destroys work with two. Done when: A shared link renders the diagram read-only for someone who has never used the application. The share is versioned, so an edit after sharing does not silently change what a recipient sees unless you re-share. The interface says clearly that editing is single-user. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time multiplayer editing. It is the reason teams pay, and a naive version is worse than none because it loses work silently. - The template library. Templates are content, and content is accumulated rather than written. - Wireframe component kits, which are a design system with a maintainer behind them. - Document embedding and the integrations that make a diagram tool part of a workflow. ### README must contain - That pan and zoom run through the viewBox, and why that matters for export fidelity - That edges store node and port references rather than coordinates - How fonts are handled on export and how to verify a file elsewhere - Where documents are stored, and that exporting to a file is the only durable copy - That editing is single-user by design ===== AGENTS.md ===== # Agent instructions · Whimsical indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite with TypeScript, no framework, SVG in the DOM, not a canvas element, ELK via elkjs for automatic arrangement, IndexedDB for documents, with file export and import, The SVG viewBox for pan and zoom. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. - Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. - Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. - A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. - An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. ===== BUILD_PLAN.md ===== # Build plan · Whimsical An SVG canvas with pan and zoom, boxes you can type into, connectors that re-route when you move a node, one-key auto-layout for tree and flow diagrams, and PNG and SVG export that is pixel-honest at any zoom level. Documents live in the browser and export to a file you own. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Canvas, pan, zoom and a node You can create a box, move it, type in it, and the geometry is correct at any zoom. ### Steps 1. Set up the SVG and the viewBox transform One svg element, one root group, and a viewBox. Write screenToCanvas and canvasToScreen as the only coordinate conversion in the codebase. Files: `src/canvas.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Implement pan and zoom Pan on space-drag or middle-drag, zoom on wheel anchored at the cursor position so the point under the pointer stays put. That anchoring is what makes zoom feel right. Files: `src/viewport.ts` 3. Create, move and edit a node Click to create, drag to move with pointer events, double click to edit text using a foreignObject or an overlaid input positioned through canvasToScreen. Files: `src/node.ts` ### Done when - [ ] A node returns to the same canvas position after zooming out and back in - [ ] The point under the cursor stays fixed while wheel zooming - [ ] Clicking selects the node under the cursor at 25 percent and at 400 percent zoom - [ ] Node text survives a page reload ### Watch out - Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. ## Phase 2 · Connectors that stay attached Edges connect to nodes rather than to coordinates, and moving a node re-routes rather than breaking. ### Steps 1. Define ports and pick the best pair Four ports per node to start. When rendering, choose the port pair that gives the shortest sensible route rather than always using the same sides. Files: `src/ports.ts` 2. Route orthogonally and leave perpendicular A path that exits the port at right angles, turns at most twice for the common case, and rounds the corners slightly. Files: `src/route.ts` 3. Maintain referential integrity Deleting a node cascades to its edges. Write the test that deletes a node and asserts no orphan edge remains. Files: `src/doc.ts` ### Done when - [ ] Moving a node re-routes every attached edge - [ ] No edge crosses through the body of a node it connects - [ ] Deleting a node removes its edges and leaves no orphans - [ ] Resizing keeps edges attached to the correct side ### Watch out - Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. ## Phase 3 · Automatic layout One key press turns a tangle into a readable diagram. ### Steps 1. Add elkjs and map your graph to its format Nodes with widths and heights, edges with sources and targets. Its input shape is close to yours, which is why it is worth using. Files: `src/layout.ts` ```sh npm install elkjs ``` 2. Apply positions as one undoable action Compute all positions, then commit them in a single history entry so one undo restores the previous arrangement. Files: `src/history.ts` 3. Test for overlap programmatically After layout, assert no two node rectangles intersect. This catches configuration mistakes that look almost right. Files: `src/layout.test.ts` ### Done when - [ ] A twenty node tree lays out with no overlapping bounding boxes - [ ] One undo restores the pre-layout arrangement - [ ] Layout is deterministic for an unchanged graph - [ ] Changing direction from vertical to horizontal produces a sensible result ### Watch out - Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. ## Phase 4 · Text that exports the way it looks Wrapped labels look identical in the browser and in the exported file. ### Steps 1. Measure text properly Use a hidden SVG text element and getComputedTextLength, or a canvas measureText with the identical font stack. Guessing from character counts fails on proportional fonts. Files: `src/text.ts` 2. Lay out lines into tspans Break on word boundaries, position each tspan with explicit x and dy, and return the total height. Files: `src/text.ts` 3. Grow the node to fit Node height is derived from the laid-out text height plus padding, so a long label is never clipped. Files: `src/node.ts` ### Done when - [ ] A long label wraps identically in the browser and in an exported SVG opened elsewhere - [ ] Node height grows with text length - [ ] No text is clipped at any zoom level - [ ] The same layout function serves both the screen and the export ### Watch out - A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. ## Phase 5 · Export and local documents Diagrams leave the application as files that open correctly somewhere else. ### Steps 1. Export SVG with content bounds and embedded fonts Compute the bounding box of all content, set the viewBox to it, inline the styles, and embed or outline the font. Files: `src/export-svg.ts` 2. Rasterise to PNG at a chosen scale Draw the serialised SVG into a canvas at the requested scale and export a blob. Verify the pixel dimensions rather than trusting the scale parameter. Files: `src/export-png.ts` 3. Persist and move documents IndexedDB for the working set, plus save to file and open from file so a document survives a cleared browser. Files: `src/store.ts` ### Done when - [ ] An exported SVG opens elsewhere with correct fonts and line breaks - [ ] A two times PNG has exactly double the pixel dimensions of the one times - [ ] A document round-trips through a file into a fresh browser profile unchanged - [ ] Clearing site data loses nothing that was exported to a file ### Watch out - An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. ## Not in this build - Real-time collaboration and presence, which is why teams buy the paid plan. - The template library and the wireframe component kits. - Document embedding and workspace integrations. - Comment threads and review workflows on a diagram. ## After v1, if you want it - Sticky notes and freehand annotation, which need no new architecture - Diagram as code import, so a text description generates a first draft - A continuous integration check that renders fixture documents and diffs the output ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Optional. Shown in the page title. Cosmetic. VITE_APP_TITLE=Diagrams # Optional. Product phase only. Where shared documents are uploaded when they are too large for a URL. Leave unset to disable sharing. VITE_SHARE_ENDPOINT=https://share.example.com/upload # Optional. Above this the share switches from a link to an upload, because very long URLs break in real clients. VITE_MAX_SHARE_URL_BYTES=8000
You are building a lean indie version of Whimsical. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Whimsical · indie build An SVG canvas with pan and zoom, boxes you can type into, connectors that re-route when you move a node, one-key auto-layout for tree and flow diagrams, and PNG and SVG export that is pixel-honest at any zoom level. Documents live in the browser and export to a file you own. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite with TypeScript, no framework | a canvas application is one big stateful view, and a component tree fights you rather than helping | | Rendering | SVG in the DOM, not a canvas element | text stays selectable, export is the same tree you are already looking at, and hit testing is free | | Layout | ELK via elkjs for automatic arrangement | layered graph layout is a solved research problem and hand-rolling it produces crossing edges forever | | Storage | IndexedDB for documents, with file export and import | no account, no server, and the file is the thing you actually own | | Geometry | The SVG viewBox for pan and zoom | a CSS transform looks the same on screen and then betrays you at export time | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs Vite and the build. Nothing ships to a server, but the tooling needs it. - Get it: Download the LTS build from nodejs.org, or install with nvm. - Verify: node --version prints v22 or higher - [ ] **A current browser with developer tools** · free - Why: This is a browser application, and the SVG inspector is how you will debug geometry. - Get it: Chrome, Firefox or Safari, all current. Test in at least two, because SVG text metrics differ subtly. - Verify: You can inspect an SVG element and see its computed bounding box - [ ] **A second application that opens SVG files** · free - Why: The export check in Phase 5 is meaningless if the only thing you open the file in is the browser that made it. - Get it: Inkscape is free and strict, which makes it a good test. A design tool you already have works too. - Verify: You can open an SVG file and see whether its fonts survived - [ ] **A font you are allowed to embed** · free with an open licence - Why: Embedding a font in an exported file is redistribution, and not every licence permits it. - Get it: Pick an open licensed family such as Inter or Source Sans, and keep the licence file alongside it. Or convert text to paths on export and avoid the question. - Verify: The licence permits embedding, or your export outlines text instead ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Real-time collaboration and presence, which is why teams buy the paid plan. - The template library and the wireframe component kits. - Document embedding and workspace integrations. - Comment threads and review workflows on a diagram. - templates - multiplayer polish - docs integration - wireframe components - comments - sharing permissions If one of those is essential to you, that is the reason to keep paying for Whimsical, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Whimsical The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a diagram editor with connectors that stay attached, auto-layout, and exports that match the screen to replace Whimsical. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Vite with TypeScript, no framework · a canvas application is one big stateful view, and a component tree fights you rather than helping - Rendering: SVG in the DOM, not a canvas element · text stays selectable, export is the same tree you are already looking at, and hit testing is free - Layout: ELK via elkjs for automatic arrangement · layered graph layout is a solved research problem and hand-rolling it produces crossing edges forever - Storage: IndexedDB for documents, with file export and import · no account, no server, and the file is the thing you actually own - Geometry: The SVG viewBox for pan and zoom · a CSS transform looks the same on screen and then betrays you at export time ### Data model (create this before Phase 1) - docs(id, title, created_at, updated_at) - nodes(id, doc_id, kind, text, x, y, w, h, fill, stroke) · kind is box, ellipse, diamond or sticky - edges(id, doc_id, from_node, to_node, from_port, to_port, label, waypoints_json, style) - Ports are named anchors on a node, not absolute coordinates, so moving a node cannot detach an edge. - Selection, hover and the current tool are ephemeral view state and never persisted. ### Phase 1 · Canvas, pan, zoom and a node Build: An SVG element filling the viewport with a viewBox you control. Pan by adjusting the viewBox origin and zoom by adjusting its width and height. Create a rectangle on click, drag it, and edit its text in place. Convert pointer coordinates into canvas coordinates through one function and use that function everywhere. Trap: Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. Done when: Zooming to 400 percent, moving a node, and zooming back to 100 percent leaves the node exactly where you put it. Clicking at any zoom level selects the node under the cursor and not one nearby. Text entered into a node persists after a reload. Do not build yet: No connectors, no layout, no export. ### Phase 2 · Connectors that stay attached Build: Define named ports on each node. An edge stores the two node ids and port names, never absolute points. Render an orthogonal path that leaves each port perpendicular to its side. Recompute the path whenever either endpoint moves or resizes. Trap: Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. Done when: Moving a node re-routes its edges and no edge passes through the body of either node it connects. Deleting a node deletes its edges rather than leaving edges pointing at nothing. Resizing a node keeps the edge attached to the same side. Do not build yet: No automatic layout yet, and no export. ### Phase 3 · Automatic layout Build: Feed the graph to elkjs with a layered algorithm, take back positions, and animate nodes to their new places. Offer layout direction as a choice. Preserve manual positions as a separate mode so auto-layout is something you invoke rather than something that fights you. Trap: Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. Done when: A twenty node tree lays out with no overlapping node bounding boxes, verified by checking every pair rather than by looking. Layout is undoable in one step. Running layout twice on an unchanged graph produces the same positions both times. Do not build yet: No export yet. ### Phase 4 · Text that exports the way it looks Build: SVG has no text wrapping, so measure and lay out lines yourself into tspan elements with explicit positions. Grow node height to fit the text. Use the same layout function for the screen and for the export so they cannot disagree. Trap: A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. Done when: A node with a long label wraps to the same lines in the browser and in the exported SVG opened in a different application. Increasing text length grows the node rather than overflowing it. No text is clipped at any zoom level. Do not build yet: Export comes next, and this phase is what makes it honest. ### Phase 5 · Export and local documents Build: Export SVG by serialising the canvas with the viewBox set to the content bounds and fonts either embedded or converted to paths. Export PNG by rasterising that SVG at a chosen scale. Save documents in IndexedDB and support file save and open so nothing is trapped in one browser profile. Trap: An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. Done when: An exported SVG opens in a different application with the same fonts and the same line breaks. An exported PNG at two times scale has exactly double the pixel dimensions of the one times export. A document exported to a file and re-imported into a fresh browser profile is identical, including edges and layout. Do not build yet: No sharing or collaboration. ### Phase 6 · Sharing, and being honest about collaboration Build: A read-only share: export the document to a compact encoded form in a URL, or upload it to storage you own and serve a viewer page. State plainly that simultaneous editing is not supported rather than implementing something that loses edits. Trap: Real-time collaborative editing needs conflict-free replicated data types or operational transforms plus presence and reconnection handling. A naive last-write-wins sync over a socket looks like it works in a demo with one person and destroys work with two. Done when: A shared link renders the diagram read-only for someone who has never used the application. The share is versioned, so an edit after sharing does not silently change what a recipient sees unless you re-share. The interface says clearly that editing is single-user. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time multiplayer editing. It is the reason teams pay, and a naive version is worse than none because it loses work silently. - The template library. Templates are content, and content is accumulated rather than written. - Wireframe component kits, which are a design system with a maintainer behind them. - Document embedding and the integrations that make a diagram tool part of a workflow. ### README must contain - That pan and zoom run through the viewBox, and why that matters for export fidelity - That edges store node and port references rather than coordinates - How fonts are handled on export and how to verify a file elsewhere - Where documents are stored, and that exporting to a file is the only durable copy - That editing is single-user by design ===== AGENTS.md ===== # Agent instructions · Whimsical indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite with TypeScript, no framework, SVG in the DOM, not a canvas element, ELK via elkjs for automatic arrangement, IndexedDB for documents, with file export and import, The SVG viewBox for pan and zoom. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. - Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. - Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. - A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. - An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. ===== BUILD_PLAN.md ===== # Build plan · Whimsical An SVG canvas with pan and zoom, boxes you can type into, connectors that re-route when you move a node, one-key auto-layout for tree and flow diagrams, and PNG and SVG export that is pixel-honest at any zoom level. Documents live in the browser and export to a file you own. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Canvas, pan, zoom and a node You can create a box, move it, type in it, and the geometry is correct at any zoom. ### Steps 1. Set up the SVG and the viewBox transform One svg element, one root group, and a viewBox. Write screenToCanvas and canvasToScreen as the only coordinate conversion in the codebase. Files: `src/canvas.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Implement pan and zoom Pan on space-drag or middle-drag, zoom on wheel anchored at the cursor position so the point under the pointer stays put. That anchoring is what makes zoom feel right. Files: `src/viewport.ts` 3. Create, move and edit a node Click to create, drag to move with pointer events, double click to edit text using a foreignObject or an overlaid input positioned through canvasToScreen. Files: `src/node.ts` ### Done when - [ ] A node returns to the same canvas position after zooming out and back in - [ ] The point under the cursor stays fixed while wheel zooming - [ ] Clicking selects the node under the cursor at 25 percent and at 400 percent zoom - [ ] Node text survives a page reload ### Watch out - Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. ## Phase 2 · Connectors that stay attached Edges connect to nodes rather than to coordinates, and moving a node re-routes rather than breaking. ### Steps 1. Define ports and pick the best pair Four ports per node to start. When rendering, choose the port pair that gives the shortest sensible route rather than always using the same sides. Files: `src/ports.ts` 2. Route orthogonally and leave perpendicular A path that exits the port at right angles, turns at most twice for the common case, and rounds the corners slightly. Files: `src/route.ts` 3. Maintain referential integrity Deleting a node cascades to its edges. Write the test that deletes a node and asserts no orphan edge remains. Files: `src/doc.ts` ### Done when - [ ] Moving a node re-routes every attached edge - [ ] No edge crosses through the body of a node it connects - [ ] Deleting a node removes its edges and leaves no orphans - [ ] Resizing keeps edges attached to the correct side ### Watch out - Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. ## Phase 3 · Automatic layout One key press turns a tangle into a readable diagram. ### Steps 1. Add elkjs and map your graph to its format Nodes with widths and heights, edges with sources and targets. Its input shape is close to yours, which is why it is worth using. Files: `src/layout.ts` ```sh npm install elkjs ``` 2. Apply positions as one undoable action Compute all positions, then commit them in a single history entry so one undo restores the previous arrangement. Files: `src/history.ts` 3. Test for overlap programmatically After layout, assert no two node rectangles intersect. This catches configuration mistakes that look almost right. Files: `src/layout.test.ts` ### Done when - [ ] A twenty node tree lays out with no overlapping bounding boxes - [ ] One undo restores the pre-layout arrangement - [ ] Layout is deterministic for an unchanged graph - [ ] Changing direction from vertical to horizontal produces a sensible result ### Watch out - Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. ## Phase 4 · Text that exports the way it looks Wrapped labels look identical in the browser and in the exported file. ### Steps 1. Measure text properly Use a hidden SVG text element and getComputedTextLength, or a canvas measureText with the identical font stack. Guessing from character counts fails on proportional fonts. Files: `src/text.ts` 2. Lay out lines into tspans Break on word boundaries, position each tspan with explicit x and dy, and return the total height. Files: `src/text.ts` 3. Grow the node to fit Node height is derived from the laid-out text height plus padding, so a long label is never clipped. Files: `src/node.ts` ### Done when - [ ] A long label wraps identically in the browser and in an exported SVG opened elsewhere - [ ] Node height grows with text length - [ ] No text is clipped at any zoom level - [ ] The same layout function serves both the screen and the export ### Watch out - A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. ## Phase 5 · Export and local documents Diagrams leave the application as files that open correctly somewhere else. ### Steps 1. Export SVG with content bounds and embedded fonts Compute the bounding box of all content, set the viewBox to it, inline the styles, and embed or outline the font. Files: `src/export-svg.ts` 2. Rasterise to PNG at a chosen scale Draw the serialised SVG into a canvas at the requested scale and export a blob. Verify the pixel dimensions rather than trusting the scale parameter. Files: `src/export-png.ts` 3. Persist and move documents IndexedDB for the working set, plus save to file and open from file so a document survives a cleared browser. Files: `src/store.ts` ### Done when - [ ] An exported SVG opens elsewhere with correct fonts and line breaks - [ ] A two times PNG has exactly double the pixel dimensions of the one times - [ ] A document round-trips through a file into a fresh browser profile unchanged - [ ] Clearing site data loses nothing that was exported to a file ### Watch out - An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. ## Not in this build - Real-time collaboration and presence, which is why teams buy the paid plan. - The template library and the wireframe component kits. - Document embedding and workspace integrations. - Comment threads and review workflows on a diagram. ## After v1, if you want it - Sticky notes and freehand annotation, which need no new architecture - Diagram as code import, so a text description generates a first draft - A continuous integration check that renders fixture documents and diffs the output ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Optional. Shown in the page title. Cosmetic. VITE_APP_TITLE=Diagrams # Optional. Product phase only. Where shared documents are uploaded when they are too large for a URL. Leave unset to disable sharing. VITE_SHARE_ENDPOINT=https://share.example.com/upload # Optional. Above this the share switches from a link to an upload, because very long URLs break in real clients. VITE_MAX_SHARE_URL_BYTES=8000
You are building a production product version of Whimsical. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Whimsical · product brief ## Problem A constrained diagramming tool with nodes, connectors, sticky notes, and export is a very vibecodable core loop; polish and collaboration are what you lose. ## Product outcome A diagram editor that produces files which look the same everywhere, with connectors that survive editing, layout that is one keystroke, and an honest statement that it is single-user. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - React canvas/SVG library - local storage or backend - optional real-time sync - export library ## Explicit non-goals for v1 - Real-time collaboration and presence, which is why teams buy the paid plan. - The template library and the wireframe component kits. - Document embedding and workspace integrations. - Comment threads and review workflows on a diagram. - templates - multiplayer polish - docs integration - wireframe components - comments - sharing permissions ## Success criteria - The fixture document's exported SVG matches its stored reference - A two times PNG has exactly double the pixel dimensions - The overlap assertion passes after layout on the test graphs - An exported file opens correctly in a second application - The single-user limitation is visible in the interface ===== BRIEF.md ===== # Build brief · Whimsical The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a diagram editor with connectors that stay attached, auto-layout, and exports that match the screen to replace Whimsical. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Vite with TypeScript, no framework · a canvas application is one big stateful view, and a component tree fights you rather than helping - Rendering: SVG in the DOM, not a canvas element · text stays selectable, export is the same tree you are already looking at, and hit testing is free - Layout: ELK via elkjs for automatic arrangement · layered graph layout is a solved research problem and hand-rolling it produces crossing edges forever - Storage: IndexedDB for documents, with file export and import · no account, no server, and the file is the thing you actually own - Geometry: The SVG viewBox for pan and zoom · a CSS transform looks the same on screen and then betrays you at export time ### Data model (create this before Phase 1) - docs(id, title, created_at, updated_at) - nodes(id, doc_id, kind, text, x, y, w, h, fill, stroke) · kind is box, ellipse, diamond or sticky - edges(id, doc_id, from_node, to_node, from_port, to_port, label, waypoints_json, style) - Ports are named anchors on a node, not absolute coordinates, so moving a node cannot detach an edge. - Selection, hover and the current tool are ephemeral view state and never persisted. ### Phase 1 · Canvas, pan, zoom and a node Build: An SVG element filling the viewport with a viewBox you control. Pan by adjusting the viewBox origin and zoom by adjusting its width and height. Create a rectangle on click, drag it, and edit its text in place. Convert pointer coordinates into canvas coordinates through one function and use that function everywhere. Trap: Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. Done when: Zooming to 400 percent, moving a node, and zooming back to 100 percent leaves the node exactly where you put it. Clicking at any zoom level selects the node under the cursor and not one nearby. Text entered into a node persists after a reload. Do not build yet: No connectors, no layout, no export. ### Phase 2 · Connectors that stay attached Build: Define named ports on each node. An edge stores the two node ids and port names, never absolute points. Render an orthogonal path that leaves each port perpendicular to its side. Recompute the path whenever either endpoint moves or resizes. Trap: Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. Done when: Moving a node re-routes its edges and no edge passes through the body of either node it connects. Deleting a node deletes its edges rather than leaving edges pointing at nothing. Resizing a node keeps the edge attached to the same side. Do not build yet: No automatic layout yet, and no export. ### Phase 3 · Automatic layout Build: Feed the graph to elkjs with a layered algorithm, take back positions, and animate nodes to their new places. Offer layout direction as a choice. Preserve manual positions as a separate mode so auto-layout is something you invoke rather than something that fights you. Trap: Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. Done when: A twenty node tree lays out with no overlapping node bounding boxes, verified by checking every pair rather than by looking. Layout is undoable in one step. Running layout twice on an unchanged graph produces the same positions both times. Do not build yet: No export yet. ### Phase 4 · Text that exports the way it looks Build: SVG has no text wrapping, so measure and lay out lines yourself into tspan elements with explicit positions. Grow node height to fit the text. Use the same layout function for the screen and for the export so they cannot disagree. Trap: A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. Done when: A node with a long label wraps to the same lines in the browser and in the exported SVG opened in a different application. Increasing text length grows the node rather than overflowing it. No text is clipped at any zoom level. Do not build yet: Export comes next, and this phase is what makes it honest. ### Phase 5 · Export and local documents Build: Export SVG by serialising the canvas with the viewBox set to the content bounds and fonts either embedded or converted to paths. Export PNG by rasterising that SVG at a chosen scale. Save documents in IndexedDB and support file save and open so nothing is trapped in one browser profile. Trap: An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. Done when: An exported SVG opens in a different application with the same fonts and the same line breaks. An exported PNG at two times scale has exactly double the pixel dimensions of the one times export. A document exported to a file and re-imported into a fresh browser profile is identical, including edges and layout. Do not build yet: No sharing or collaboration. ### Phase 6 · Sharing, and being honest about collaboration Build: A read-only share: export the document to a compact encoded form in a URL, or upload it to storage you own and serve a viewer page. State plainly that simultaneous editing is not supported rather than implementing something that loses edits. Trap: Real-time collaborative editing needs conflict-free replicated data types or operational transforms plus presence and reconnection handling. A naive last-write-wins sync over a socket looks like it works in a demo with one person and destroys work with two. Done when: A shared link renders the diagram read-only for someone who has never used the application. The share is versioned, so an edit after sharing does not silently change what a recipient sees unless you re-share. The interface says clearly that editing is single-user. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time multiplayer editing. It is the reason teams pay, and a naive version is worse than none because it loses work silently. - The template library. Templates are content, and content is accumulated rather than written. - Wireframe component kits, which are a design system with a maintainer behind them. - Document embedding and the integrations that make a diagram tool part of a workflow. ### README must contain - That pan and zoom run through the viewBox, and why that matters for export fidelity - That edges store node and port references rather than coordinates - How fonts are handled on export and how to verify a file elsewhere - Where documents are stored, and that exporting to a file is the only durable copy - That editing is single-user by design ===== ARCHITECTURE.md ===== # Architecture · Whimsical ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite with TypeScript, no framework | a canvas application is one big stateful view, and a component tree fights you rather than helping | | Rendering | SVG in the DOM, not a canvas element | text stays selectable, export is the same tree you are already looking at, and hit testing is free | | Layout | ELK via elkjs for automatic arrangement | layered graph layout is a solved research problem and hand-rolling it produces crossing edges forever | | Storage | IndexedDB for documents, with file export and import | no account, no server, and the file is the thing you actually own | | Geometry | The SVG viewBox for pan and zoom | a CSS transform looks the same on screen and then betrays you at export time | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Viewport | Pan, zoom and every coordinate conversion. | Nothing else should compute coordinates. If a second place starts doing it, the export will disagree with the screen. | | Document | Nodes, edges, ports and referential integrity. | The document is a plain serialisable object, so persistence, sharing and export all read the same shape. | | Routing | Turning a node and port pair into a path. | A different routing strategy is a pure function swap with the same signature, testable against a set of fixed graphs. | | Layout | Automatic arrangement through elkjs. | dagre is a lighter alternative with fewer options; the mapping layer is small and the overlap test protects either choice. | | Export | SVG serialisation, font embedding and rasterisation. | Rasterisation could move to a server for very large diagrams, but the SVG path must stay client-side so exporting works offline. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_APP_TITLE` · optional · Shown in the page title. Cosmetic. - `VITE_SHARE_ENDPOINT` · optional · Product phase only. Where shared documents are uploaded when they are too large for a URL. Leave unset to disable sharing. - `VITE_MAX_SHARE_URL_BYTES` · optional · Above this the share switches from a link to an upload, because very long URLs break in real clients. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Whimsical product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Vite with TypeScript, no framework, SVG in the DOM, not a canvas element, ELK via elkjs for automatic arrangement, IndexedDB for documents, with file export and import, The SVG viewBox for pan and zoom. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. - Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. - Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. - A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. - An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. - Real-time collaborative editing needs conflict-free replicated data types or operational transforms plus presence and reconnection handling. A naive last-write-wins sync over a socket looks like it works in a demo with one person and destroys work with two. ===== MILESTONES.md ===== # Delivery milestones · Whimsical Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Canvas, pan, zoom and a node You can create a box, move it, type in it, and the geometry is correct at any zoom. ### Steps 1. Set up the SVG and the viewBox transform One svg element, one root group, and a viewBox. Write screenToCanvas and canvasToScreen as the only coordinate conversion in the codebase. Files: `src/canvas.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Implement pan and zoom Pan on space-drag or middle-drag, zoom on wheel anchored at the cursor position so the point under the pointer stays put. That anchoring is what makes zoom feel right. Files: `src/viewport.ts` 3. Create, move and edit a node Click to create, drag to move with pointer events, double click to edit text using a foreignObject or an overlaid input positioned through canvasToScreen. Files: `src/node.ts` ### Done when - [ ] A node returns to the same canvas position after zooming out and back in - [ ] The point under the cursor stays fixed while wheel zooming - [ ] Clicking selects the node under the cursor at 25 percent and at 400 percent zoom - [ ] Node text survives a page reload ### Watch out - Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. ## M2 · Connectors that stay attached Edges connect to nodes rather than to coordinates, and moving a node re-routes rather than breaking. ### Steps 1. Define ports and pick the best pair Four ports per node to start. When rendering, choose the port pair that gives the shortest sensible route rather than always using the same sides. Files: `src/ports.ts` 2. Route orthogonally and leave perpendicular A path that exits the port at right angles, turns at most twice for the common case, and rounds the corners slightly. Files: `src/route.ts` 3. Maintain referential integrity Deleting a node cascades to its edges. Write the test that deletes a node and asserts no orphan edge remains. Files: `src/doc.ts` ### Done when - [ ] Moving a node re-routes every attached edge - [ ] No edge crosses through the body of a node it connects - [ ] Deleting a node removes its edges and leaves no orphans - [ ] Resizing keeps edges attached to the correct side ### Watch out - Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. ## M3 · Automatic layout One key press turns a tangle into a readable diagram. ### Steps 1. Add elkjs and map your graph to its format Nodes with widths and heights, edges with sources and targets. Its input shape is close to yours, which is why it is worth using. Files: `src/layout.ts` ```sh npm install elkjs ``` 2. Apply positions as one undoable action Compute all positions, then commit them in a single history entry so one undo restores the previous arrangement. Files: `src/history.ts` 3. Test for overlap programmatically After layout, assert no two node rectangles intersect. This catches configuration mistakes that look almost right. Files: `src/layout.test.ts` ### Done when - [ ] A twenty node tree lays out with no overlapping bounding boxes - [ ] One undo restores the pre-layout arrangement - [ ] Layout is deterministic for an unchanged graph - [ ] Changing direction from vertical to horizontal produces a sensible result ### Watch out - Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. ## M4 · Text that exports the way it looks Wrapped labels look identical in the browser and in the exported file. ### Steps 1. Measure text properly Use a hidden SVG text element and getComputedTextLength, or a canvas measureText with the identical font stack. Guessing from character counts fails on proportional fonts. Files: `src/text.ts` 2. Lay out lines into tspans Break on word boundaries, position each tspan with explicit x and dy, and return the total height. Files: `src/text.ts` 3. Grow the node to fit Node height is derived from the laid-out text height plus padding, so a long label is never clipped. Files: `src/node.ts` ### Done when - [ ] A long label wraps identically in the browser and in an exported SVG opened elsewhere - [ ] Node height grows with text length - [ ] No text is clipped at any zoom level - [ ] The same layout function serves both the screen and the export ### Watch out - A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. ## M5 · Export and local documents Diagrams leave the application as files that open correctly somewhere else. ### Steps 1. Export SVG with content bounds and embedded fonts Compute the bounding box of all content, set the viewBox to it, inline the styles, and embed or outline the font. Files: `src/export-svg.ts` 2. Rasterise to PNG at a chosen scale Draw the serialised SVG into a canvas at the requested scale and export a blob. Verify the pixel dimensions rather than trusting the scale parameter. Files: `src/export-png.ts` 3. Persist and move documents IndexedDB for the working set, plus save to file and open from file so a document survives a cleared browser. Files: `src/store.ts` ### Done when - [ ] An exported SVG opens elsewhere with correct fonts and line breaks - [ ] A two times PNG has exactly double the pixel dimensions of the one times - [ ] A document round-trips through a file into a fresh browser profile unchanged - [ ] Clearing site data loses nothing that was exported to a file ### Watch out - An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. ## M6 · Sharing, and being honest about collaboration (production only) Other people can see a diagram, and you have not pretended to build multiplayer. ### Steps 1. Serialise compactly for sharing A compressed encoding of the document. If it exceeds a sane URL length, upload it instead of producing a link nobody can paste. Files: `src/share.ts` 2. Build a read-only viewer Same rendering code, no editing affordances, no keyboard shortcuts that mutate. Files: `viewer.html` 3. Say what is not supported One sentence in the interface and one in the README. Silence here reads as a promise you did not make. Files: `README.md` ### Done when - [ ] A shared link renders correctly for a first-time visitor - [ ] Editing after sharing does not change what a recipient already received - [ ] The viewer offers no editing affordances - [ ] The single-user limitation is stated in the interface, not only in documentation ### Watch out - Real-time collaborative editing needs conflict-free replicated data types or operational transforms plus presence and reconnection handling. A naive last-write-wins sync over a socket looks like it works in a demo with one person and destroys work with two. ===== OPERATIONS.md ===== # Operations · Whimsical ## Backup Documents live in IndexedDB, which the browser may clear without warning. File export is the backup, so make it obvious and frequent rather than buried in a menu. ## Restore Open the exported file. Because the file is the whole document, restore is the same operation as opening, which is the property worth protecting. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing to monitor server-side. Watch export fidelity instead: keep a fixture document and diff its exported SVG in continuous integration, so a rendering change that breaks export is caught immediately. ## Incident checklist The realistic incident is a browser storage eviction. The mitigation is habitual export, prompted by the application, plus a warning when a document has unsaved changes and has not been exported. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The fixture document's exported SVG matches its stored reference - [ ] A two times PNG has exactly double the pixel dimensions - [ ] The overlap assertion passes after layout on the test graphs - [ ] An exported file opens correctly in a second application - [ ] The single-user limitation is visible in the interface ## Launch constraint Do not market omitted Whimsical capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Optional. Shown in the page title. Cosmetic. VITE_APP_TITLE=Diagrams # Optional. Product phase only. Where shared documents are uploaded when they are too large for a URL. Leave unset to disable sharing. VITE_SHARE_ENDPOINT=https://share.example.com/upload # Optional. Above this the share switches from a link to an upload, because very long URLs break in real clients. VITE_MAX_SHARE_URL_BYTES=8000
# Whimsical · indie build An SVG canvas with pan and zoom, boxes you can type into, connectors that re-route when you move a node, one-key auto-layout for tree and flow diagrams, and PNG and SVG export that is pixel-honest at any zoom level. Documents live in the browser and export to a file you own. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite with TypeScript, no framework | a canvas application is one big stateful view, and a component tree fights you rather than helping | | Rendering | SVG in the DOM, not a canvas element | text stays selectable, export is the same tree you are already looking at, and hit testing is free | | Layout | ELK via elkjs for automatic arrangement | layered graph layout is a solved research problem and hand-rolling it produces crossing edges forever | | Storage | IndexedDB for documents, with file export and import | no account, no server, and the file is the thing you actually own | | Geometry | The SVG viewBox for pan and zoom | a CSS transform looks the same on screen and then betrays you at export time | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs Vite and the build. Nothing ships to a server, but the tooling needs it. - Get it: Download the LTS build from nodejs.org, or install with nvm. - Verify: node --version prints v22 or higher - [ ] **A current browser with developer tools** · free - Why: This is a browser application, and the SVG inspector is how you will debug geometry. - Get it: Chrome, Firefox or Safari, all current. Test in at least two, because SVG text metrics differ subtly. - Verify: You can inspect an SVG element and see its computed bounding box - [ ] **A second application that opens SVG files** · free - Why: The export check in Phase 5 is meaningless if the only thing you open the file in is the browser that made it. - Get it: Inkscape is free and strict, which makes it a good test. A design tool you already have works too. - Verify: You can open an SVG file and see whether its fonts survived - [ ] **A font you are allowed to embed** · free with an open licence - Why: Embedding a font in an exported file is redistribution, and not every licence permits it. - Get it: Pick an open licensed family such as Inter or Source Sans, and keep the licence file alongside it. Or convert text to paths on export and avoid the question. - Verify: The licence permits embedding, or your export outlines text instead ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Real-time collaboration and presence, which is why teams buy the paid plan. - The template library and the wireframe component kits. - Document embedding and workspace integrations. - Comment threads and review workflows on a diagram. - templates - multiplayer polish - docs integration - wireframe components - comments - sharing permissions If one of those is essential to you, that is the reason to keep paying for Whimsical, and the README should say so rather than pretend.
# Build brief · Whimsical The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a diagram editor with connectors that stay attached, auto-layout, and exports that match the screen to replace Whimsical. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Vite with TypeScript, no framework · a canvas application is one big stateful view, and a component tree fights you rather than helping - Rendering: SVG in the DOM, not a canvas element · text stays selectable, export is the same tree you are already looking at, and hit testing is free - Layout: ELK via elkjs for automatic arrangement · layered graph layout is a solved research problem and hand-rolling it produces crossing edges forever - Storage: IndexedDB for documents, with file export and import · no account, no server, and the file is the thing you actually own - Geometry: The SVG viewBox for pan and zoom · a CSS transform looks the same on screen and then betrays you at export time ### Data model (create this before Phase 1) - docs(id, title, created_at, updated_at) - nodes(id, doc_id, kind, text, x, y, w, h, fill, stroke) · kind is box, ellipse, diamond or sticky - edges(id, doc_id, from_node, to_node, from_port, to_port, label, waypoints_json, style) - Ports are named anchors on a node, not absolute coordinates, so moving a node cannot detach an edge. - Selection, hover and the current tool are ephemeral view state and never persisted. ### Phase 1 · Canvas, pan, zoom and a node Build: An SVG element filling the viewport with a viewBox you control. Pan by adjusting the viewBox origin and zoom by adjusting its width and height. Create a rectangle on click, drag it, and edit its text in place. Convert pointer coordinates into canvas coordinates through one function and use that function everywhere. Trap: Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. Done when: Zooming to 400 percent, moving a node, and zooming back to 100 percent leaves the node exactly where you put it. Clicking at any zoom level selects the node under the cursor and not one nearby. Text entered into a node persists after a reload. Do not build yet: No connectors, no layout, no export. ### Phase 2 · Connectors that stay attached Build: Define named ports on each node. An edge stores the two node ids and port names, never absolute points. Render an orthogonal path that leaves each port perpendicular to its side. Recompute the path whenever either endpoint moves or resizes. Trap: Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. Done when: Moving a node re-routes its edges and no edge passes through the body of either node it connects. Deleting a node deletes its edges rather than leaving edges pointing at nothing. Resizing a node keeps the edge attached to the same side. Do not build yet: No automatic layout yet, and no export. ### Phase 3 · Automatic layout Build: Feed the graph to elkjs with a layered algorithm, take back positions, and animate nodes to their new places. Offer layout direction as a choice. Preserve manual positions as a separate mode so auto-layout is something you invoke rather than something that fights you. Trap: Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. Done when: A twenty node tree lays out with no overlapping node bounding boxes, verified by checking every pair rather than by looking. Layout is undoable in one step. Running layout twice on an unchanged graph produces the same positions both times. Do not build yet: No export yet. ### Phase 4 · Text that exports the way it looks Build: SVG has no text wrapping, so measure and lay out lines yourself into tspan elements with explicit positions. Grow node height to fit the text. Use the same layout function for the screen and for the export so they cannot disagree. Trap: A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. Done when: A node with a long label wraps to the same lines in the browser and in the exported SVG opened in a different application. Increasing text length grows the node rather than overflowing it. No text is clipped at any zoom level. Do not build yet: Export comes next, and this phase is what makes it honest. ### Phase 5 · Export and local documents Build: Export SVG by serialising the canvas with the viewBox set to the content bounds and fonts either embedded or converted to paths. Export PNG by rasterising that SVG at a chosen scale. Save documents in IndexedDB and support file save and open so nothing is trapped in one browser profile. Trap: An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. Done when: An exported SVG opens in a different application with the same fonts and the same line breaks. An exported PNG at two times scale has exactly double the pixel dimensions of the one times export. A document exported to a file and re-imported into a fresh browser profile is identical, including edges and layout. Do not build yet: No sharing or collaboration. ### Phase 6 · Sharing, and being honest about collaboration Build: A read-only share: export the document to a compact encoded form in a URL, or upload it to storage you own and serve a viewer page. State plainly that simultaneous editing is not supported rather than implementing something that loses edits. Trap: Real-time collaborative editing needs conflict-free replicated data types or operational transforms plus presence and reconnection handling. A naive last-write-wins sync over a socket looks like it works in a demo with one person and destroys work with two. Done when: A shared link renders the diagram read-only for someone who has never used the application. The share is versioned, so an edit after sharing does not silently change what a recipient sees unless you re-share. The interface says clearly that editing is single-user. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time multiplayer editing. It is the reason teams pay, and a naive version is worse than none because it loses work silently. - The template library. Templates are content, and content is accumulated rather than written. - Wireframe component kits, which are a design system with a maintainer behind them. - Document embedding and the integrations that make a diagram tool part of a workflow. ### README must contain - That pan and zoom run through the viewBox, and why that matters for export fidelity - That edges store node and port references rather than coordinates - How fonts are handled on export and how to verify a file elsewhere - Where documents are stored, and that exporting to a file is the only durable copy - That editing is single-user by design
# Agent instructions · Whimsical indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite with TypeScript, no framework, SVG in the DOM, not a canvas element, ELK via elkjs for automatic arrangement, IndexedDB for documents, with file export and import, The SVG viewBox for pan and zoom. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. - Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. - Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. - A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. - An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application.
# Build plan · Whimsical An SVG canvas with pan and zoom, boxes you can type into, connectors that re-route when you move a node, one-key auto-layout for tree and flow diagrams, and PNG and SVG export that is pixel-honest at any zoom level. Documents live in the browser and export to a file you own. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Canvas, pan, zoom and a node You can create a box, move it, type in it, and the geometry is correct at any zoom. ### Steps 1. Set up the SVG and the viewBox transform One svg element, one root group, and a viewBox. Write screenToCanvas and canvasToScreen as the only coordinate conversion in the codebase. Files: `src/canvas.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Implement pan and zoom Pan on space-drag or middle-drag, zoom on wheel anchored at the cursor position so the point under the pointer stays put. That anchoring is what makes zoom feel right. Files: `src/viewport.ts` 3. Create, move and edit a node Click to create, drag to move with pointer events, double click to edit text using a foreignObject or an overlaid input positioned through canvasToScreen. Files: `src/node.ts` ### Done when - [ ] A node returns to the same canvas position after zooming out and back in - [ ] The point under the cursor stays fixed while wheel zooming - [ ] Clicking selects the node under the cursor at 25 percent and at 400 percent zoom - [ ] Node text survives a page reload ### Watch out - Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. ## Phase 2 · Connectors that stay attached Edges connect to nodes rather than to coordinates, and moving a node re-routes rather than breaking. ### Steps 1. Define ports and pick the best pair Four ports per node to start. When rendering, choose the port pair that gives the shortest sensible route rather than always using the same sides. Files: `src/ports.ts` 2. Route orthogonally and leave perpendicular A path that exits the port at right angles, turns at most twice for the common case, and rounds the corners slightly. Files: `src/route.ts` 3. Maintain referential integrity Deleting a node cascades to its edges. Write the test that deletes a node and asserts no orphan edge remains. Files: `src/doc.ts` ### Done when - [ ] Moving a node re-routes every attached edge - [ ] No edge crosses through the body of a node it connects - [ ] Deleting a node removes its edges and leaves no orphans - [ ] Resizing keeps edges attached to the correct side ### Watch out - Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. ## Phase 3 · Automatic layout One key press turns a tangle into a readable diagram. ### Steps 1. Add elkjs and map your graph to its format Nodes with widths and heights, edges with sources and targets. Its input shape is close to yours, which is why it is worth using. Files: `src/layout.ts` ```sh npm install elkjs ``` 2. Apply positions as one undoable action Compute all positions, then commit them in a single history entry so one undo restores the previous arrangement. Files: `src/history.ts` 3. Test for overlap programmatically After layout, assert no two node rectangles intersect. This catches configuration mistakes that look almost right. Files: `src/layout.test.ts` ### Done when - [ ] A twenty node tree lays out with no overlapping bounding boxes - [ ] One undo restores the pre-layout arrangement - [ ] Layout is deterministic for an unchanged graph - [ ] Changing direction from vertical to horizontal produces a sensible result ### Watch out - Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. ## Phase 4 · Text that exports the way it looks Wrapped labels look identical in the browser and in the exported file. ### Steps 1. Measure text properly Use a hidden SVG text element and getComputedTextLength, or a canvas measureText with the identical font stack. Guessing from character counts fails on proportional fonts. Files: `src/text.ts` 2. Lay out lines into tspans Break on word boundaries, position each tspan with explicit x and dy, and return the total height. Files: `src/text.ts` 3. Grow the node to fit Node height is derived from the laid-out text height plus padding, so a long label is never clipped. Files: `src/node.ts` ### Done when - [ ] A long label wraps identically in the browser and in an exported SVG opened elsewhere - [ ] Node height grows with text length - [ ] No text is clipped at any zoom level - [ ] The same layout function serves both the screen and the export ### Watch out - A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. ## Phase 5 · Export and local documents Diagrams leave the application as files that open correctly somewhere else. ### Steps 1. Export SVG with content bounds and embedded fonts Compute the bounding box of all content, set the viewBox to it, inline the styles, and embed or outline the font. Files: `src/export-svg.ts` 2. Rasterise to PNG at a chosen scale Draw the serialised SVG into a canvas at the requested scale and export a blob. Verify the pixel dimensions rather than trusting the scale parameter. Files: `src/export-png.ts` 3. Persist and move documents IndexedDB for the working set, plus save to file and open from file so a document survives a cleared browser. Files: `src/store.ts` ### Done when - [ ] An exported SVG opens elsewhere with correct fonts and line breaks - [ ] A two times PNG has exactly double the pixel dimensions of the one times - [ ] A document round-trips through a file into a fresh browser profile unchanged - [ ] Clearing site data loses nothing that was exported to a file ### Watch out - An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. ## Not in this build - Real-time collaboration and presence, which is why teams buy the paid plan. - The template library and the wireframe component kits. - Document embedding and workspace integrations. - Comment threads and review workflows on a diagram. ## After v1, if you want it - Sticky notes and freehand annotation, which need no new architecture - Diagram as code import, so a text description generates a first draft - A continuous integration check that renders fixture documents and diffs the output
# Copy to .env and fill in. Never commit .env; this file documents it. # Optional. Shown in the page title. Cosmetic. VITE_APP_TITLE=Diagrams # Optional. Product phase only. Where shared documents are uploaded when they are too large for a URL. Leave unset to disable sharing. VITE_SHARE_ENDPOINT=https://share.example.com/upload # Optional. Above this the share switches from a link to an upload, because very long URLs break in real clients. VITE_MAX_SHARE_URL_BYTES=8000
# Whimsical · product brief ## Problem A constrained diagramming tool with nodes, connectors, sticky notes, and export is a very vibecodable core loop; polish and collaboration are what you lose. ## Product outcome A diagram editor that produces files which look the same everywhere, with connectors that survive editing, layout that is one keystroke, and an honest statement that it is single-user. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - React canvas/SVG library - local storage or backend - optional real-time sync - export library ## Explicit non-goals for v1 - Real-time collaboration and presence, which is why teams buy the paid plan. - The template library and the wireframe component kits. - Document embedding and workspace integrations. - Comment threads and review workflows on a diagram. - templates - multiplayer polish - docs integration - wireframe components - comments - sharing permissions ## Success criteria - The fixture document's exported SVG matches its stored reference - A two times PNG has exactly double the pixel dimensions - The overlap assertion passes after layout on the test graphs - An exported file opens correctly in a second application - The single-user limitation is visible in the interface
# Build brief · Whimsical The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a diagram editor with connectors that stay attached, auto-layout, and exports that match the screen to replace Whimsical. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Vite with TypeScript, no framework · a canvas application is one big stateful view, and a component tree fights you rather than helping - Rendering: SVG in the DOM, not a canvas element · text stays selectable, export is the same tree you are already looking at, and hit testing is free - Layout: ELK via elkjs for automatic arrangement · layered graph layout is a solved research problem and hand-rolling it produces crossing edges forever - Storage: IndexedDB for documents, with file export and import · no account, no server, and the file is the thing you actually own - Geometry: The SVG viewBox for pan and zoom · a CSS transform looks the same on screen and then betrays you at export time ### Data model (create this before Phase 1) - docs(id, title, created_at, updated_at) - nodes(id, doc_id, kind, text, x, y, w, h, fill, stroke) · kind is box, ellipse, diamond or sticky - edges(id, doc_id, from_node, to_node, from_port, to_port, label, waypoints_json, style) - Ports are named anchors on a node, not absolute coordinates, so moving a node cannot detach an edge. - Selection, hover and the current tool are ephemeral view state and never persisted. ### Phase 1 · Canvas, pan, zoom and a node Build: An SVG element filling the viewport with a viewBox you control. Pan by adjusting the viewBox origin and zoom by adjusting its width and height. Create a rectangle on click, drag it, and edit its text in place. Convert pointer coordinates into canvas coordinates through one function and use that function everywhere. Trap: Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. Done when: Zooming to 400 percent, moving a node, and zooming back to 100 percent leaves the node exactly where you put it. Clicking at any zoom level selects the node under the cursor and not one nearby. Text entered into a node persists after a reload. Do not build yet: No connectors, no layout, no export. ### Phase 2 · Connectors that stay attached Build: Define named ports on each node. An edge stores the two node ids and port names, never absolute points. Render an orthogonal path that leaves each port perpendicular to its side. Recompute the path whenever either endpoint moves or resizes. Trap: Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. Done when: Moving a node re-routes its edges and no edge passes through the body of either node it connects. Deleting a node deletes its edges rather than leaving edges pointing at nothing. Resizing a node keeps the edge attached to the same side. Do not build yet: No automatic layout yet, and no export. ### Phase 3 · Automatic layout Build: Feed the graph to elkjs with a layered algorithm, take back positions, and animate nodes to their new places. Offer layout direction as a choice. Preserve manual positions as a separate mode so auto-layout is something you invoke rather than something that fights you. Trap: Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. Done when: A twenty node tree lays out with no overlapping node bounding boxes, verified by checking every pair rather than by looking. Layout is undoable in one step. Running layout twice on an unchanged graph produces the same positions both times. Do not build yet: No export yet. ### Phase 4 · Text that exports the way it looks Build: SVG has no text wrapping, so measure and lay out lines yourself into tspan elements with explicit positions. Grow node height to fit the text. Use the same layout function for the screen and for the export so they cannot disagree. Trap: A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. Done when: A node with a long label wraps to the same lines in the browser and in the exported SVG opened in a different application. Increasing text length grows the node rather than overflowing it. No text is clipped at any zoom level. Do not build yet: Export comes next, and this phase is what makes it honest. ### Phase 5 · Export and local documents Build: Export SVG by serialising the canvas with the viewBox set to the content bounds and fonts either embedded or converted to paths. Export PNG by rasterising that SVG at a chosen scale. Save documents in IndexedDB and support file save and open so nothing is trapped in one browser profile. Trap: An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. Done when: An exported SVG opens in a different application with the same fonts and the same line breaks. An exported PNG at two times scale has exactly double the pixel dimensions of the one times export. A document exported to a file and re-imported into a fresh browser profile is identical, including edges and layout. Do not build yet: No sharing or collaboration. ### Phase 6 · Sharing, and being honest about collaboration Build: A read-only share: export the document to a compact encoded form in a URL, or upload it to storage you own and serve a viewer page. State plainly that simultaneous editing is not supported rather than implementing something that loses edits. Trap: Real-time collaborative editing needs conflict-free replicated data types or operational transforms plus presence and reconnection handling. A naive last-write-wins sync over a socket looks like it works in a demo with one person and destroys work with two. Done when: A shared link renders the diagram read-only for someone who has never used the application. The share is versioned, so an edit after sharing does not silently change what a recipient sees unless you re-share. The interface says clearly that editing is single-user. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time multiplayer editing. It is the reason teams pay, and a naive version is worse than none because it loses work silently. - The template library. Templates are content, and content is accumulated rather than written. - Wireframe component kits, which are a design system with a maintainer behind them. - Document embedding and the integrations that make a diagram tool part of a workflow. ### README must contain - That pan and zoom run through the viewBox, and why that matters for export fidelity - That edges store node and port references rather than coordinates - How fonts are handled on export and how to verify a file elsewhere - Where documents are stored, and that exporting to a file is the only durable copy - That editing is single-user by design
# Architecture · Whimsical ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite with TypeScript, no framework | a canvas application is one big stateful view, and a component tree fights you rather than helping | | Rendering | SVG in the DOM, not a canvas element | text stays selectable, export is the same tree you are already looking at, and hit testing is free | | Layout | ELK via elkjs for automatic arrangement | layered graph layout is a solved research problem and hand-rolling it produces crossing edges forever | | Storage | IndexedDB for documents, with file export and import | no account, no server, and the file is the thing you actually own | | Geometry | The SVG viewBox for pan and zoom | a CSS transform looks the same on screen and then betrays you at export time | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Viewport | Pan, zoom and every coordinate conversion. | Nothing else should compute coordinates. If a second place starts doing it, the export will disagree with the screen. | | Document | Nodes, edges, ports and referential integrity. | The document is a plain serialisable object, so persistence, sharing and export all read the same shape. | | Routing | Turning a node and port pair into a path. | A different routing strategy is a pure function swap with the same signature, testable against a set of fixed graphs. | | Layout | Automatic arrangement through elkjs. | dagre is a lighter alternative with fewer options; the mapping layer is small and the overlap test protects either choice. | | Export | SVG serialisation, font embedding and rasterisation. | Rasterisation could move to a server for very large diagrams, but the SVG path must stay client-side so exporting works offline. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_APP_TITLE` · optional · Shown in the page title. Cosmetic. - `VITE_SHARE_ENDPOINT` · optional · Product phase only. Where shared documents are uploaded when they are too large for a URL. Leave unset to disable sharing. - `VITE_MAX_SHARE_URL_BYTES` · optional · Above this the share switches from a link to an upload, because very long URLs break in real clients. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Whimsical product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Vite with TypeScript, no framework, SVG in the DOM, not a canvas element, ELK via elkjs for automatic arrangement, IndexedDB for documents, with file export and import, The SVG viewBox for pan and zoom. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. - Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. - Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. - A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. - An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. - Real-time collaborative editing needs conflict-free replicated data types or operational transforms plus presence and reconnection handling. A naive last-write-wins sync over a socket looks like it works in a demo with one person and destroys work with two.
# Delivery milestones · Whimsical Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Canvas, pan, zoom and a node You can create a box, move it, type in it, and the geometry is correct at any zoom. ### Steps 1. Set up the SVG and the viewBox transform One svg element, one root group, and a viewBox. Write screenToCanvas and canvasToScreen as the only coordinate conversion in the codebase. Files: `src/canvas.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Implement pan and zoom Pan on space-drag or middle-drag, zoom on wheel anchored at the cursor position so the point under the pointer stays put. That anchoring is what makes zoom feel right. Files: `src/viewport.ts` 3. Create, move and edit a node Click to create, drag to move with pointer events, double click to edit text using a foreignObject or an overlaid input positioned through canvasToScreen. Files: `src/node.ts` ### Done when - [ ] A node returns to the same canvas position after zooming out and back in - [ ] The point under the cursor stays fixed while wheel zooming - [ ] Clicking selects the node under the cursor at 25 percent and at 400 percent zoom - [ ] Node text survives a page reload ### Watch out - Zooming with a CSS transform is easier for about an hour, and then every export, every hit test and every text position is off by the scale factor. Drive pan and zoom through the viewBox from the start so what you see is literally the SVG you will export. ## M2 · Connectors that stay attached Edges connect to nodes rather than to coordinates, and moving a node re-routes rather than breaking. ### Steps 1. Define ports and pick the best pair Four ports per node to start. When rendering, choose the port pair that gives the shortest sensible route rather than always using the same sides. Files: `src/ports.ts` 2. Route orthogonally and leave perpendicular A path that exits the port at right angles, turns at most twice for the common case, and rounds the corners slightly. Files: `src/route.ts` 3. Maintain referential integrity Deleting a node cascades to its edges. Write the test that deletes a node and asserts no orphan edge remains. Files: `src/doc.ts` ### Done when - [ ] Moving a node re-routes every attached edge - [ ] No edge crosses through the body of a node it connects - [ ] Deleting a node removes its edges and leaves no orphans - [ ] Resizing keeps edges attached to the correct side ### Watch out - Storing an edge as a pair of coordinates works until the first time someone moves a node, and then every diagram in every saved document is subtly wrong. Store the relationship and derive the geometry. ## M3 · Automatic layout One key press turns a tangle into a readable diagram. ### Steps 1. Add elkjs and map your graph to its format Nodes with widths and heights, edges with sources and targets. Its input shape is close to yours, which is why it is worth using. Files: `src/layout.ts` ```sh npm install elkjs ``` 2. Apply positions as one undoable action Compute all positions, then commit them in a single history entry so one undo restores the previous arrangement. Files: `src/history.ts` 3. Test for overlap programmatically After layout, assert no two node rectangles intersect. This catches configuration mistakes that look almost right. Files: `src/layout.test.ts` ### Done when - [ ] A twenty node tree lays out with no overlapping bounding boxes - [ ] One undo restores the pre-layout arrangement - [ ] Layout is deterministic for an unchanged graph - [ ] Changing direction from vertical to horizontal produces a sensible result ### Watch out - Running layout automatically after every edit makes the canvas feel possessed, because a node you just dragged jumps somewhere else. Make it an explicit action with an undo step. ## M4 · Text that exports the way it looks Wrapped labels look identical in the browser and in the exported file. ### Steps 1. Measure text properly Use a hidden SVG text element and getComputedTextLength, or a canvas measureText with the identical font stack. Guessing from character counts fails on proportional fonts. Files: `src/text.ts` 2. Lay out lines into tspans Break on word boundaries, position each tspan with explicit x and dy, and return the total height. Files: `src/text.ts` 3. Grow the node to fit Node height is derived from the laid-out text height plus padding, so a long label is never clipped. Files: `src/node.ts` ### Done when - [ ] A long label wraps identically in the browser and in an exported SVG opened elsewhere - [ ] Node height grows with text length - [ ] No text is clipped at any zoom level - [ ] The same layout function serves both the screen and the export ### Watch out - A foreignObject with an HTML div wraps text beautifully in the browser and is ignored or mangled by most SVG consumers, including Inkscape and many design tools. If you use foreignObject for editing, convert to positioned tspans for rendering and export. ## M5 · Export and local documents Diagrams leave the application as files that open correctly somewhere else. ### Steps 1. Export SVG with content bounds and embedded fonts Compute the bounding box of all content, set the viewBox to it, inline the styles, and embed or outline the font. Files: `src/export-svg.ts` 2. Rasterise to PNG at a chosen scale Draw the serialised SVG into a canvas at the requested scale and export a blob. Verify the pixel dimensions rather than trusting the scale parameter. Files: `src/export-png.ts` 3. Persist and move documents IndexedDB for the working set, plus save to file and open from file so a document survives a cleared browser. Files: `src/store.ts` ### Done when - [ ] An exported SVG opens elsewhere with correct fonts and line breaks - [ ] A two times PNG has exactly double the pixel dimensions of the one times - [ ] A document round-trips through a file into a fresh browser profile unchanged - [ ] Clearing site data loses nothing that was exported to a file ### Watch out - An exported SVG that references a web font renders with a fallback font on any machine that lacks it, which usually means every machine but yours. Embed the font as a data URL or convert text to paths, and check by opening the file in a different application. ## M6 · Sharing, and being honest about collaboration (production only) Other people can see a diagram, and you have not pretended to build multiplayer. ### Steps 1. Serialise compactly for sharing A compressed encoding of the document. If it exceeds a sane URL length, upload it instead of producing a link nobody can paste. Files: `src/share.ts` 2. Build a read-only viewer Same rendering code, no editing affordances, no keyboard shortcuts that mutate. Files: `viewer.html` 3. Say what is not supported One sentence in the interface and one in the README. Silence here reads as a promise you did not make. Files: `README.md` ### Done when - [ ] A shared link renders correctly for a first-time visitor - [ ] Editing after sharing does not change what a recipient already received - [ ] The viewer offers no editing affordances - [ ] The single-user limitation is stated in the interface, not only in documentation ### Watch out - Real-time collaborative editing needs conflict-free replicated data types or operational transforms plus presence and reconnection handling. A naive last-write-wins sync over a socket looks like it works in a demo with one person and destroys work with two.
# Operations · Whimsical ## Backup Documents live in IndexedDB, which the browser may clear without warning. File export is the backup, so make it obvious and frequent rather than buried in a menu. ## Restore Open the exported file. Because the file is the whole document, restore is the same operation as opening, which is the property worth protecting. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing to monitor server-side. Watch export fidelity instead: keep a fixture document and diff its exported SVG in continuous integration, so a rendering change that breaks export is caught immediately. ## Incident checklist The realistic incident is a browser storage eviction. The mitigation is habitual export, prompted by the application, plus a warning when a document has unsaved changes and has not been exported. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The fixture document's exported SVG matches its stored reference - [ ] A two times PNG has exactly double the pixel dimensions - [ ] The overlap assertion passes after layout on the test graphs - [ ] An exported file opens correctly in a second application - [ ] The single-user limitation is visible in the interface ## Launch constraint Do not market omitted Whimsical capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Optional. Shown in the page title. Cosmetic. VITE_APP_TITLE=Diagrams # Optional. Product phase only. Where shared documents are uploaded when they are too large for a URL. Leave unset to disable sharing. VITE_SHARE_ENDPOINT=https://share.example.com/upload # Optional. Above this the share switches from a link to an upload, because very long URLs break in real clients. VITE_MAX_SHARE_URL_BYTES=8000
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay for speed and tidy diagrams that teammates can edit without setup.
xtemplates
xmultiplayer polish
xdocs integration
xwireframe components
xcomments
xsharing permissions
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Whimsical →· no votes, no pay-to-list · just what's real
Whimsical pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | Unlimited private boards and members; 3 team boards, 3 teams, 10 guests, 5MB/file, 1GB storage, 7-day history, 20 AI credits/workspace total |
| pro | $12/user | $10/user | Unlimited team boards; 6 teams, 50 guests, 1GB/file, 100GB storage and 90-day history |
| business | $24/user | $20/user | Unlimited teams; starting at 200 guests, 10GB/file, starting at 2TB storage, unlimited history, SAML SSO and SCIM |
free tierUnlimited private boards and workspace members; 3 collaborative team boards, 3 teams, 10 guests, 2 free workspaces, 5MB/file, 1GB storage, 7-day history and 20 AI credits/workspace total
billingmonthly or annual (annual saves 17% / 2 months)
hidden costsEvery editor is charged separately in each upgraded workspace; viewers are free. Additional AI credits are sold but their price is not public. Adding/removing editors creates prorated charges/credits; custom legal/security terms require sales.
verified 2026-08-12 · source ↗
Vibecode Whimsical
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Whimsical replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Whimsical cost?
Whimsical costs about $12/month (Pro, checked 2026-07-30), which is $144 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Whimsical?
Honestly: templates; multiplayer polish; docs integration; wireframe components; comments; sharing permissions. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Whimsical?
Yes: Excalidraw (Excellent collaborative flowcharts, wireframes, and loose visual thinking; it does not pretend to be a proper document editor or structured mind mapper.) CryptPad (Private collaborative docs, whiteboards, diagrams, slides, and Kanban in one suite; rougher edges, fewer tasteful shortcuts, and no native mind-map mode.) AFFiNE (Docs, whiteboards, databases, mind maps, tasks, and presentation frames in one finished app; the free hosted limits are tolerable for one person or a tiny team.) All 3 curated free alternatives are at vibecodeit.com/whimsical/alternatives. The prompt is for when you want it exactly your way.