Skip to content

Repository files navigation

DraftPad

A local-first whiteboard and sketching editor. Rectangles, ellipses, diamonds, lines, arrows, text and freehand strokes on an infinite pannable canvas, with multiple named drawings, undo/redo, and PNG/SVG/JSON export.

Live at marcelerz.github.io/draft-pad.

Everything runs in the browser. There is no server and no account: drawings live in your own localStorage, and sharing is a compressed URL fragment.

Running it

npm install
npm run dev        # http://localhost:3000/draft-pad

The /draft-pad prefix is not a typo: the app is configured for the base path it is deployed under, so local and deployed builds are identical. See Deploying.

Script What it does
npm run dev Development server (Turbopack)
npm run build Static export to out/
npm start Serve the built out/ at http://localhost:3000/draft-pad
npm run typecheck tsc --noEmit
npm run lint ESLint
npm test Vitest

Deploying

next.config.ts sets output: 'export', so npm run build produces a static out/ directory that any file host can serve.

Every push to main publishes it to GitHub Pages, from the deploy job in .github/workflows/ci.yml. That job runs only after typecheck, lint and the test suite pass, so a red build cannot reach the site.

Because Pages serves this as a project site, the app lives under /draft-pad rather than at the domain root, and the export has to emit prefixed asset URLs. next.config.ts sets basePath: '/draft-pad' — that alone is enough. Next derives assetPrefix from it and Turbopack bakes the same prefix into the dynamic-chunk loader, so setting assetPrefix separately is redundant. trailingSlash is not needed either: there is a single route, Pages redirects /draft-pad to /draft-pad/, and out/404.html covers everything else.

To host it somewhere else, change basePath to that prefix, or to '' for a domain root.

Drawings do not travel between the two. localStorage is scoped to an origin, so anything drawn at localhost stays there and the hosted site starts empty. Use Export → JSON backup and import it if you want to move a drawing across. For the same reason DraftPad shares an origin with every other project page under marcelerz.github.io; its keys are all draftpad:-prefixed, which avoids collisions but is not isolation.

Share links stay private regardless of hosting: the scene rides in the URL fragment, which browsers never send to the server.

How it is put together

Area Location Notes
Rendering src/canvas/ Two stacked 2D canvases: a static scene layer and an interactive overlay for selection chrome. geometry.ts is shared with the SVG exporter so the two stay in step.
Tools src/tools/ BaseTool subclasses dispatched by ToolManager. SelectTool is an explicit state machine.
State src/state/store.ts Zustand. History lives in the store behind captureBefore / commit, so one gesture is one undo step.
Storage src/state/storage.ts Versioned, lz-string-compressed payloads with a plain-JSON manifest index. session.ts owns the autosave target and its gate.
UI src/components/ React 19 and Tailwind 4.

Things worth knowing before changing it

  • Adding an element type means adding a variant to DraftPadElement and then fixing the three compile errors assertNever produces, in ElementRenderer, HitTest and export.ts. That is deliberate: those switches used to have default: arms that silently swallowed new types.
  • Renderers are pure functions of their arguments. They take ThemeColors and viewport state rather than reading the store, which is what makes offscreen and test rendering possible.
  • Every mutation should go through the store, and gesture-scoped edits should bracket with captureBefore / commit rather than snapshotting on each pointermove.
  • Storage writes never throw. They return a SaveResult; surface it rather than swallowing it.

Testing

npm test runs Vitest against the pure modules — geometry, hit testing, resize maths, history semantics, scene normalisation and the storage round-trip, including a throwing localStorage. Rendering itself is not covered; changes there still want a look in the browser.

About

Hand-drawn style whiteboard, no server required

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages