Build Whimsical

YESreplaces $12/mosaves $144/yrback to the verdict

0%0 of 19 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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 weekendthe files for this build are in the project pack

RuntimeVite with TypeScript, no frameworkRenderingSVG in the DOM, not a canvas elementLayoutELK via elkjs for automatic arrangementStorageIndexedDB for documents, with file export and importGeometryThe SVG viewBox for pan and zoom

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    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. open ↗

    Verify node --version prints v22 or higher

  2. installfree

    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

  3. installfree

    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. open ↗

    Verify You can open an SVG file and see whether its fonts survived

  4. decidefree 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

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- 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.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
VITE_APP_TITLEoptionalDiagramsShown in the page title. Cosmetic.
VITE_SHARE_ENDPOINToptionalhttps://share.example.com/uploadProduct phase only. Where shared documents are uploaded when they are too large for a URL. Leave unset to disable sharing.
VITE_MAX_SHARE_URL_BYTESoptional8000Above this the share switches from a link to an upload, because very long URLs break in real clients.

The build, in order

  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.

    1. 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

      terminal
      npm create vite@latest . -- --template vanilla-ts
    2. 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. 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 · tick each as it passes
    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.
  2. Connectors that stay attached

    Edges connect to nodes rather than to coordinates, and moving a node re-routes rather than breaking.

    1. 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. 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. 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 · tick each as it passes
    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.
  3. Automatic layout

    One key press turns a tangle into a readable diagram.

    1. 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

      terminal
      npm install elkjs
    2. Compute all positions, then commit them in a single history entry so one undo restores the previous arrangement.

      Files src/history.ts

    3. After layout, assert no two node rectangles intersect. This catches configuration mistakes that look almost right.

      Files src/layout.test.ts

    done when · tick each as it passes
    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.
  4. Text that exports the way it looks

    Wrapped labels look identical in the browser and in the exported file.

    1. 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. Break on word boundaries, position each tspan with explicit x and dy, and return the total height.

      Files src/text.ts

    3. 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 · tick each as it passes
    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.
  5. Export and local documents

    Diagrams leave the application as files that open correctly somewhere else.

    1. 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. 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. 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 · tick each as it passes
    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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more diagrams.