Build Ayoa
KINDAback to the verdict
A browser canvas for mind maps and tasks whose hard problems are an undo history people actually trust, keeping tasks and map nodes as one object rather than two that drift, and producing an export that renders correctly outside the application, which is where most home-built canvases quietly fail.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Builds the application and runs the test suite.
Get it nodejs.org, or your version manager. open ↗
Verify
node --version prints v22 or higher - have readyfree with an open licence
Why The export embeds the font bytes in the file, which is a licensing question as much as a technical one.
Get it An open licence family whose terms allow embedding, downloaded as a subsettable file.
Verify
The licence permits embedding and the file subsets without error - have readyfree
Why Export fidelity is only testable somewhere else, and a new browser tab is not somewhere else.
Get it An image viewer and a vector editor you already have.
Verify
The exported SVG opens in both with the intended typeface - decidefree
Why Every canvas project drifts towards collaboration, and deciding against it once is cheaper than deciding against it repeatedly.
Get it Write down that there is no server, no presence and no shared document, and what you would build instead if you needed one.
Verify
The decision is in the README and no networking code exists
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- documents(id, title, created_at, updated_at, schema_version) · the version is what lets an old file still open - nodes(id, doc_id, parent_id, text, x, y, width, height, colour, collapsed, style_json) - edges(id, doc_id, from_node, to_node, kind, label) · kind covers the hierarchy edge and free connections - task_fields(node_id, due_at, done_at, priority, assignee_label) · attached to a node, never a second object - commands(doc_id, ordinal, at, kind, payload_json, inverse_json, selection_before, selection_after) · the undo history, with the selection on both sides - assets(doc_id, id, sha256, mime, bytes, data) · images pasted into the map, stored inline because the export has to carry them - A node with task fields is still a node. There is no board document and no map document, only two views over the same nodes.
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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
VITE_APP_TITLE | required | Maps | Window title and export metadata. |
VITE_AUTOSAVE_DEBOUNCE_MS | required | 800 | The most work a crash can cost, in milliseconds. |
VITE_HISTORY_LIMIT | required | 500 | Commands retained before the oldest are dropped. |
VITE_RECOVERY_SNAPSHOTS | required | 5 | Size of the recovery ring kept in browser storage. |
VITE_EXPORT_REMINDER_DAYS | required | 7 | After this the last-export date turns into a prompt. |
The build, in order
A canvas that does not fight the pointer
Nodes can be created, moved, connected and edited with a pointer or a keyboard, and the geometry stays sane.
A saved layout should not remember how you were looking at it.
Files
src/canvas/coords.tstests/coords.test.tsOutlining speed is the only reason to use a mind map.
Files
src/canvas/keyboard.tsScreen-space hit testing drifts the moment you zoom.
Files
src/canvas/hit.ts
done when · tick each as it passeswatch out- Storing positions in screen coordinates makes every saved document depend on the zoom level it was edited at, so reopening at a different zoom scatters the map and no amount of later fixing recovers the intended layout. Store document coordinates, convert only at render and at hit test, and assert in the suite that no stored coordinate changes when the zoom does.
Undo that returns you to where you were
Every edit is undoable, typing collapses into one step, and undo restores the selection as well as the document.
Undo is a navigation as much as an edit.
Files
src/history/command.tstests/undo-selection.test.tsOne undo per keystroke is the same as no undo.
Files
src/history/coalesce.tsThe one direct mutation is the one that breaks undo.
Files
tests/no-direct-mutation.test.ts
done when · tick each as it passeswatch out- Undo that restores the document but not the selection makes a user press undo again to find where they were, and the second press removes an edit they wanted to keep, which is why home-built canvases feel untrustworthy. Store the selection on both sides of every command and restore it, then test that undo followed by redo leaves both document and selection identical.
A node that is also a task
Adding a due date to a node makes it appear on the board without creating a second object.
Two titles for one item is a synchronisation bug you chose.
Files
src/model/node.tsUndo should not care which view you were in.
Files
src/board/commands.tstests/board-undo.test.tsAn orphaned task is the second half of the same bug.
Files
src/model/delete.ts
done when · tick each as it passeswatch out- Creating a separate task record linked to a node gives you two titles that must be kept in step, and within a week the board and the map disagree about what the item is called, with no obvious winner. Keep one record and two views, and make the board's edits ordinary commands so undo works across both.
An export that opens correctly somewhere else
The exported SVG renders identically in a browser, an image viewer and whatever your colleague uses.
An SVG used as an image is not allowed to fetch anything.
Files
src/export/svg.tstests/no-external-refs.test.tsThen a broken export is visible before you send it.
Files
src/export/png.tsOpening a file from a later version should say so.
Files
src/io/json.ts
done when · tick each as it passeswatch out- Referencing a web font by URL from the exported SVG produces a file that looks perfect in your own tab and falls back to a default font everywhere else, because the SVG specification requires an SVG used in place of a raster image, including in an HTML image element, to be processed in secure static mode, where external references and scripting are disabled. Embed the font bytes in the file, and check the export by opening it in an image element rather than in a new tab.
Not losing the documentproduct builder
Browser storage is treated as a cache, the file on disk is the copy that matters, and a crash costs seconds.
Quota errors are silent until the work is already gone.
Files
src/storage/guarded.tstests/quota-failure.test.tsThe crash you did not predict is the one it covers.
Files
src/storage/recovery.tsA number going stale is a better reminder than a dialogue.
Files
src/components/ExportStatus.tsx
done when · tick each as it passeswatch out- Treating browser storage as the permanent home of the document means a cleared site setting, a private window or a full quota deletes work with no warning and no copy anywhere, and there is nothing you can do afterwards. Wrap every storage access so a failure surfaces at once, keep recovery snapshots, and put the date of the last file export where the user can see it going stale.
That is the whole plan for Ayoa. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Multiplayer editing and live cursors.
- Full-fidelity import of other mind map formats.
- Presentation mode and slide export.
- A template library.
- high-fidelity import and export
- presentation and enterprise controls
- multiplayer cursors and comments
- large template libraries
- Subsetting the embedded font to the glyphs actually used, which shrinks large exports considerably
- A printable layout that paginates a wide map, which is the one export people ask for next
- A diff view between two JSON exports, which is how you review changes to a shared map without a server
Need the files? The project pack on the verdict page hands your agent the whole brief · more diagrams.