The visible mind mapping + tasks loop is buildable, but a credible replacement needs more than the first screen. Ayoa earns its keep through interaction polish, collaboration, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Ayoa. 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 ===== # Ayoa · indie build 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. Estimated effort: **multi-day**. 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 | React with TypeScript, built with Vite, running entirely in the browser | no server means no accounts and no hosting, and this is a single-person tool | | Storage | IndexedDB for documents, with an explicit file export as the durable copy | browser storage is convenient and is not a backup | | Model | One node type that can carry task fields, never a separate task record | two records for one thing is how a map and a board stop agreeing | | History | An append-only command log with coalescing, not snapshots of the document | snapshots of a large map are slow and lose the selection people expect back | | Export | Self-contained SVG with fonts embedded, rasterised through the same path | an SVG in an image element cannot fetch anything, which decides the whole export design | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the application and runs the test suite. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A typeface you are licensed to embed** · free 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 - [ ] **Two applications that are not your browser tab** · free - 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 - [ ] **What a single-user canvas will never do** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Ayoa, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Ayoa 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 local mind map and task board whose exports still look right in an application you did not write to replace Ayoa. 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: React with TypeScript, built with Vite, running entirely in the browser · no server means no accounts and no hosting, and this is a single-person tool - Storage: IndexedDB for documents, with an explicit file export as the durable copy · browser storage is convenient and is not a backup - Model: One node type that can carry task fields, never a separate task record · two records for one thing is how a map and a board stop agreeing - History: An append-only command log with coalescing, not snapshots of the document · snapshots of a large map are slow and lose the selection people expect back - Export: Self-contained SVG with fonts embedded, rasterised through the same path · an SVG in an image element cannot fetch anything, which decides the whole export design ### Data model (create this before Phase 1) - 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. ### Phase 1 · A canvas that does not fight the pointer Build: An infinite pan and zoom canvas with nodes positioned in document coordinates and converted at render. Create a child with one key, a sibling with another, edit text in place, and drag to connect. Hit testing in document space, not screen space. Keep the zoom factor out of every stored coordinate. Trap: 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. Done when: Zooming and panning leave every stored node coordinate unchanged, asserted in the suite. A child and a sibling can each be created with a single key from a selected node, and focus lands in the new node's editor. Hit testing selects the topmost node under the pointer at any zoom, tested at three zoom levels. Dragging a node moves its subtree or only itself according to a stated rule, and the rule is the same with the keyboard. Do not build yet: No undo, no tasks, no export, no persistence beyond the session. ### Phase 2 · Undo that returns you to where you were Build: Represent every mutation as a command with its inverse. Coalesce consecutive text edits on the same node within a short window into one command. Store the selection before and after each command and restore it on undo and redo. Cap the history and drop the oldest rather than the newest. Trap: 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. Done when: Undo then redo returns the document and the selection to exactly their prior states, asserted by deep comparison in the suite. Typing a word produces one undo step rather than one per keystroke, tested with a simulated typing sequence. Every mutation in the codebase goes through the command layer, verified by a test that no reducer mutates state directly. The history cap drops the oldest entries and the interface says when it has. Do not build yet: No tasks, no export, no persistence yet. ### Phase 3 · A node that is also a task Build: Task fields attached to the node record. A board view filtering and grouping the same nodes by due date and done state, where editing text in either view edits the same node. Completing a task from the board is a command in the same history as a canvas edit. Trap: 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. Done when: Renaming a node in the board view changes the same record shown on the canvas, tested through the store. Completing a task from the board is undoable from the canvas, tested. No table in the schema holds a task title, asserted by inspection and by a schema test. Deleting a node removes its task fields and it disappears from the board in the same command. Do not build yet: No export, no import, no persistence work. ### Phase 4 · An export that opens correctly somewhere else Build: Serialise the document to SVG with every font embedded as a base64 data URL inside an inline style, and every pasted image embedded as a data URL too. Rasterise to PNG by drawing that same self-contained SVG, so anything broken in the SVG is visibly broken in the PNG rather than only in the file you sent. Export and import the document as JSON with its schema version. Trap: 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. Done when: The exported SVG contains no external URL of any kind, asserted by a test that scans the output for http references. The same SVG rendered in an image element shows the intended typeface, checked visually and by a size comparison against a reference render. PNG export draws the identical serialised SVG rather than reading the live canvas, verified by review. JSON export and import round-trip a document with images, tasks and free connections, with identical node counts and positions. Do not build yet: No autosave or recovery work yet. ### Phase 5 · Not losing the document Build: Autosave to browser storage on a debounce and after every command batch. Detect a storage quota failure and tell the user immediately rather than at the next reload. Keep a small ring of recovery snapshots. Prompt for a file export at a stated interval, and show when the last file export happened. Trap: 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. Done when: A simulated storage quota failure surfaces a visible message at the moment of the write, not at reload, tested by stubbing the storage layer to throw. Reloading after a forced crash restores the document from the most recent recovery snapshot with at most the debounce interval lost, tested. The date of the last file export is displayed and turns into a prompt after the configured interval. Opening the application with storage unavailable still allows editing and exporting, with a clear banner. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer editing with live cursors, which needs a server, a conflict model and presence, and is most of a different product. - Importing other mind mapping formats with full fidelity, since each one is a reverse engineering project and a partial import that silently drops nodes is worse than no import. - Presentation mode and slide export, which is a second layout engine attached to a canvas that does not need one. - A template library, which is content rather than software and ages faster than the code around it. ### README must contain - That the exported SVG is self-contained because an SVG used as an image cannot resolve external references - That browser storage is a cache and the exported file is the copy that survives a cleared setting - How undo restores selection as well as content, and what the history cap is - That a node carrying task fields is one record, and that the board and the canvas are two views of it - Where documents live, how to export everything, and how to open an export from an older schema version ===== AGENTS.md ===== # Agent instructions · Ayoa indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: React with TypeScript, built with Vite, running entirely in the browser, IndexedDB for documents, with an explicit file export as the durable copy, One node type that can carry task fields, never a separate task record, An append-only command log with coalescing, not snapshots of the document, Self-contained SVG with fonts embedded, rasterised through the same path. 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 - 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 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Ayoa 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · 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. ### Steps 1. Keep document coordinates free of zoom A saved layout should not remember how you were looking at it. Files: `src/canvas/coords.ts`, `tests/coords.test.ts` 2. Make create-child and create-sibling single keys Outlining speed is the only reason to use a mind map. Files: `src/canvas/keyboard.ts` 3. Hit test in document space Screen-space hit testing drifts the moment you zoom. Files: `src/canvas/hit.ts` ### Done when - [ ] Zoom and pan do not alter stored coordinates - [ ] Child and sibling creation are single keys - [ ] Hit testing is correct at several zoom levels - [ ] Subtree drag follows one stated rule ### Watch 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. ## Phase 2 · 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. ### Steps 1. Put the selection on both sides of every command Undo is a navigation as much as an edit. Files: `src/history/command.ts`, `tests/undo-selection.test.ts` 2. Coalesce typing within a short window One undo per keystroke is the same as no undo. Files: `src/history/coalesce.ts` 3. Route every mutation through the command layer The one direct mutation is the one that breaks undo. Files: `tests/no-direct-mutation.test.ts` ### Done when - [ ] Undo and redo restore document and selection - [ ] Typing collapses into one undo step - [ ] No mutation bypasses the command layer - [ ] History capping drops the oldest and says so ### Watch 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. ## Phase 3 · 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. ### Steps 1. Attach task fields to the node record Two titles for one item is a synchronisation bug you chose. Files: `src/model/node.ts` 2. Make board edits ordinary commands Undo should not care which view you were in. Files: `src/board/commands.ts`, `tests/board-undo.test.ts` 3. Delete node and task fields together An orphaned task is the second half of the same bug. Files: `src/model/delete.ts` ### Done when - [ ] A board rename changes the canvas node - [ ] Board completion is undoable from the canvas - [ ] No separate task title exists in the schema - [ ] Deleting a node clears it from the board ### Watch 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. ## Phase 4 · An export that opens correctly somewhere else The exported SVG renders identically in a browser, an image viewer and whatever your colleague uses. ### Steps 1. Embed fonts and images in the SVG itself An SVG used as an image is not allowed to fetch anything. Files: `src/export/svg.ts`, `tests/no-external-refs.test.ts` 2. Rasterise the exported SVG, not the live canvas Then a broken export is visible before you send it. Files: `src/export/png.ts` 3. Version the JSON and refuse a newer schema Opening a file from a later version should say so. Files: `src/io/json.ts` ### Done when - [ ] The exported SVG has no external references - [ ] The intended typeface survives in an image element - [ ] PNG is rasterised from the exported SVG - [ ] JSON round-trips images, tasks and connections ### Watch 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 in this build - Multiplayer editing and live cursors. - Full-fidelity import of other mind map formats. - Presentation mode and slide export. - A template library. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Window title and export metadata. VITE_APP_TITLE=Maps # Required. The most work a crash can cost, in milliseconds. VITE_AUTOSAVE_DEBOUNCE_MS=800 # Required. Commands retained before the oldest are dropped. VITE_HISTORY_LIMIT=500 # Required. Size of the recovery ring kept in browser storage. VITE_RECOVERY_SNAPSHOTS=5 # Required. After this the last-export date turns into a prompt. VITE_EXPORT_REMINDER_DAYS=7
You are building a lean indie version of Ayoa. 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 ===== # Ayoa · indie build 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. Estimated effort: **multi-day**. 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 | React with TypeScript, built with Vite, running entirely in the browser | no server means no accounts and no hosting, and this is a single-person tool | | Storage | IndexedDB for documents, with an explicit file export as the durable copy | browser storage is convenient and is not a backup | | Model | One node type that can carry task fields, never a separate task record | two records for one thing is how a map and a board stop agreeing | | History | An append-only command log with coalescing, not snapshots of the document | snapshots of a large map are slow and lose the selection people expect back | | Export | Self-contained SVG with fonts embedded, rasterised through the same path | an SVG in an image element cannot fetch anything, which decides the whole export design | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the application and runs the test suite. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A typeface you are licensed to embed** · free 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 - [ ] **Two applications that are not your browser tab** · free - 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 - [ ] **What a single-user canvas will never do** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Ayoa, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Ayoa 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 local mind map and task board whose exports still look right in an application you did not write to replace Ayoa. 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: React with TypeScript, built with Vite, running entirely in the browser · no server means no accounts and no hosting, and this is a single-person tool - Storage: IndexedDB for documents, with an explicit file export as the durable copy · browser storage is convenient and is not a backup - Model: One node type that can carry task fields, never a separate task record · two records for one thing is how a map and a board stop agreeing - History: An append-only command log with coalescing, not snapshots of the document · snapshots of a large map are slow and lose the selection people expect back - Export: Self-contained SVG with fonts embedded, rasterised through the same path · an SVG in an image element cannot fetch anything, which decides the whole export design ### Data model (create this before Phase 1) - 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. ### Phase 1 · A canvas that does not fight the pointer Build: An infinite pan and zoom canvas with nodes positioned in document coordinates and converted at render. Create a child with one key, a sibling with another, edit text in place, and drag to connect. Hit testing in document space, not screen space. Keep the zoom factor out of every stored coordinate. Trap: 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. Done when: Zooming and panning leave every stored node coordinate unchanged, asserted in the suite. A child and a sibling can each be created with a single key from a selected node, and focus lands in the new node's editor. Hit testing selects the topmost node under the pointer at any zoom, tested at three zoom levels. Dragging a node moves its subtree or only itself according to a stated rule, and the rule is the same with the keyboard. Do not build yet: No undo, no tasks, no export, no persistence beyond the session. ### Phase 2 · Undo that returns you to where you were Build: Represent every mutation as a command with its inverse. Coalesce consecutive text edits on the same node within a short window into one command. Store the selection before and after each command and restore it on undo and redo. Cap the history and drop the oldest rather than the newest. Trap: 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. Done when: Undo then redo returns the document and the selection to exactly their prior states, asserted by deep comparison in the suite. Typing a word produces one undo step rather than one per keystroke, tested with a simulated typing sequence. Every mutation in the codebase goes through the command layer, verified by a test that no reducer mutates state directly. The history cap drops the oldest entries and the interface says when it has. Do not build yet: No tasks, no export, no persistence yet. ### Phase 3 · A node that is also a task Build: Task fields attached to the node record. A board view filtering and grouping the same nodes by due date and done state, where editing text in either view edits the same node. Completing a task from the board is a command in the same history as a canvas edit. Trap: 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. Done when: Renaming a node in the board view changes the same record shown on the canvas, tested through the store. Completing a task from the board is undoable from the canvas, tested. No table in the schema holds a task title, asserted by inspection and by a schema test. Deleting a node removes its task fields and it disappears from the board in the same command. Do not build yet: No export, no import, no persistence work. ### Phase 4 · An export that opens correctly somewhere else Build: Serialise the document to SVG with every font embedded as a base64 data URL inside an inline style, and every pasted image embedded as a data URL too. Rasterise to PNG by drawing that same self-contained SVG, so anything broken in the SVG is visibly broken in the PNG rather than only in the file you sent. Export and import the document as JSON with its schema version. Trap: 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. Done when: The exported SVG contains no external URL of any kind, asserted by a test that scans the output for http references. The same SVG rendered in an image element shows the intended typeface, checked visually and by a size comparison against a reference render. PNG export draws the identical serialised SVG rather than reading the live canvas, verified by review. JSON export and import round-trip a document with images, tasks and free connections, with identical node counts and positions. Do not build yet: No autosave or recovery work yet. ### Phase 5 · Not losing the document Build: Autosave to browser storage on a debounce and after every command batch. Detect a storage quota failure and tell the user immediately rather than at the next reload. Keep a small ring of recovery snapshots. Prompt for a file export at a stated interval, and show when the last file export happened. Trap: 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. Done when: A simulated storage quota failure surfaces a visible message at the moment of the write, not at reload, tested by stubbing the storage layer to throw. Reloading after a forced crash restores the document from the most recent recovery snapshot with at most the debounce interval lost, tested. The date of the last file export is displayed and turns into a prompt after the configured interval. Opening the application with storage unavailable still allows editing and exporting, with a clear banner. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer editing with live cursors, which needs a server, a conflict model and presence, and is most of a different product. - Importing other mind mapping formats with full fidelity, since each one is a reverse engineering project and a partial import that silently drops nodes is worse than no import. - Presentation mode and slide export, which is a second layout engine attached to a canvas that does not need one. - A template library, which is content rather than software and ages faster than the code around it. ### README must contain - That the exported SVG is self-contained because an SVG used as an image cannot resolve external references - That browser storage is a cache and the exported file is the copy that survives a cleared setting - How undo restores selection as well as content, and what the history cap is - That a node carrying task fields is one record, and that the board and the canvas are two views of it - Where documents live, how to export everything, and how to open an export from an older schema version ===== AGENTS.md ===== # Agent instructions · Ayoa indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: React with TypeScript, built with Vite, running entirely in the browser, IndexedDB for documents, with an explicit file export as the durable copy, One node type that can carry task fields, never a separate task record, An append-only command log with coalescing, not snapshots of the document, Self-contained SVG with fonts embedded, rasterised through the same path. 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 - 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 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Ayoa 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · 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. ### Steps 1. Keep document coordinates free of zoom A saved layout should not remember how you were looking at it. Files: `src/canvas/coords.ts`, `tests/coords.test.ts` 2. Make create-child and create-sibling single keys Outlining speed is the only reason to use a mind map. Files: `src/canvas/keyboard.ts` 3. Hit test in document space Screen-space hit testing drifts the moment you zoom. Files: `src/canvas/hit.ts` ### Done when - [ ] Zoom and pan do not alter stored coordinates - [ ] Child and sibling creation are single keys - [ ] Hit testing is correct at several zoom levels - [ ] Subtree drag follows one stated rule ### Watch 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. ## Phase 2 · 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. ### Steps 1. Put the selection on both sides of every command Undo is a navigation as much as an edit. Files: `src/history/command.ts`, `tests/undo-selection.test.ts` 2. Coalesce typing within a short window One undo per keystroke is the same as no undo. Files: `src/history/coalesce.ts` 3. Route every mutation through the command layer The one direct mutation is the one that breaks undo. Files: `tests/no-direct-mutation.test.ts` ### Done when - [ ] Undo and redo restore document and selection - [ ] Typing collapses into one undo step - [ ] No mutation bypasses the command layer - [ ] History capping drops the oldest and says so ### Watch 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. ## Phase 3 · 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. ### Steps 1. Attach task fields to the node record Two titles for one item is a synchronisation bug you chose. Files: `src/model/node.ts` 2. Make board edits ordinary commands Undo should not care which view you were in. Files: `src/board/commands.ts`, `tests/board-undo.test.ts` 3. Delete node and task fields together An orphaned task is the second half of the same bug. Files: `src/model/delete.ts` ### Done when - [ ] A board rename changes the canvas node - [ ] Board completion is undoable from the canvas - [ ] No separate task title exists in the schema - [ ] Deleting a node clears it from the board ### Watch 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. ## Phase 4 · An export that opens correctly somewhere else The exported SVG renders identically in a browser, an image viewer and whatever your colleague uses. ### Steps 1. Embed fonts and images in the SVG itself An SVG used as an image is not allowed to fetch anything. Files: `src/export/svg.ts`, `tests/no-external-refs.test.ts` 2. Rasterise the exported SVG, not the live canvas Then a broken export is visible before you send it. Files: `src/export/png.ts` 3. Version the JSON and refuse a newer schema Opening a file from a later version should say so. Files: `src/io/json.ts` ### Done when - [ ] The exported SVG has no external references - [ ] The intended typeface survives in an image element - [ ] PNG is rasterised from the exported SVG - [ ] JSON round-trips images, tasks and connections ### Watch 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 in this build - Multiplayer editing and live cursors. - Full-fidelity import of other mind map formats. - Presentation mode and slide export. - A template library. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Window title and export metadata. VITE_APP_TITLE=Maps # Required. The most work a crash can cost, in milliseconds. VITE_AUTOSAVE_DEBOUNCE_MS=800 # Required. Commands retained before the oldest are dropped. VITE_HISTORY_LIMIT=500 # Required. Size of the recovery ring kept in browser storage. VITE_RECOVERY_SNAPSHOTS=5 # Required. After this the last-export date turns into a prompt. VITE_EXPORT_REMINDER_DAYS=7
You are building a production product version of Ayoa. 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 ===== # Ayoa · product brief ## Problem The visible mind mapping + tasks loop is buildable, but a credible replacement needs more than the first screen. Ayoa earns its keep through interaction polish, collaboration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A canvas where the map and the board are one set of objects, undo returns you to where you were, and the exported file renders correctly on a machine that has never seen your application. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - modern browser - no account required ## Explicit non-goals for v1 - 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 ## Success criteria - Exports are confirmed to contain no external reference - Undo and redo are confirmed to restore selection as well as content - No stored coordinate is confirmed to change with zoom - A board rename is confirmed to change the canvas node - A storage quota failure is confirmed to surface immediately ===== BRIEF.md ===== # Build brief · Ayoa 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 local mind map and task board whose exports still look right in an application you did not write to replace Ayoa. 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: React with TypeScript, built with Vite, running entirely in the browser · no server means no accounts and no hosting, and this is a single-person tool - Storage: IndexedDB for documents, with an explicit file export as the durable copy · browser storage is convenient and is not a backup - Model: One node type that can carry task fields, never a separate task record · two records for one thing is how a map and a board stop agreeing - History: An append-only command log with coalescing, not snapshots of the document · snapshots of a large map are slow and lose the selection people expect back - Export: Self-contained SVG with fonts embedded, rasterised through the same path · an SVG in an image element cannot fetch anything, which decides the whole export design ### Data model (create this before Phase 1) - 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. ### Phase 1 · A canvas that does not fight the pointer Build: An infinite pan and zoom canvas with nodes positioned in document coordinates and converted at render. Create a child with one key, a sibling with another, edit text in place, and drag to connect. Hit testing in document space, not screen space. Keep the zoom factor out of every stored coordinate. Trap: 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. Done when: Zooming and panning leave every stored node coordinate unchanged, asserted in the suite. A child and a sibling can each be created with a single key from a selected node, and focus lands in the new node's editor. Hit testing selects the topmost node under the pointer at any zoom, tested at three zoom levels. Dragging a node moves its subtree or only itself according to a stated rule, and the rule is the same with the keyboard. Do not build yet: No undo, no tasks, no export, no persistence beyond the session. ### Phase 2 · Undo that returns you to where you were Build: Represent every mutation as a command with its inverse. Coalesce consecutive text edits on the same node within a short window into one command. Store the selection before and after each command and restore it on undo and redo. Cap the history and drop the oldest rather than the newest. Trap: 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. Done when: Undo then redo returns the document and the selection to exactly their prior states, asserted by deep comparison in the suite. Typing a word produces one undo step rather than one per keystroke, tested with a simulated typing sequence. Every mutation in the codebase goes through the command layer, verified by a test that no reducer mutates state directly. The history cap drops the oldest entries and the interface says when it has. Do not build yet: No tasks, no export, no persistence yet. ### Phase 3 · A node that is also a task Build: Task fields attached to the node record. A board view filtering and grouping the same nodes by due date and done state, where editing text in either view edits the same node. Completing a task from the board is a command in the same history as a canvas edit. Trap: 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. Done when: Renaming a node in the board view changes the same record shown on the canvas, tested through the store. Completing a task from the board is undoable from the canvas, tested. No table in the schema holds a task title, asserted by inspection and by a schema test. Deleting a node removes its task fields and it disappears from the board in the same command. Do not build yet: No export, no import, no persistence work. ### Phase 4 · An export that opens correctly somewhere else Build: Serialise the document to SVG with every font embedded as a base64 data URL inside an inline style, and every pasted image embedded as a data URL too. Rasterise to PNG by drawing that same self-contained SVG, so anything broken in the SVG is visibly broken in the PNG rather than only in the file you sent. Export and import the document as JSON with its schema version. Trap: 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. Done when: The exported SVG contains no external URL of any kind, asserted by a test that scans the output for http references. The same SVG rendered in an image element shows the intended typeface, checked visually and by a size comparison against a reference render. PNG export draws the identical serialised SVG rather than reading the live canvas, verified by review. JSON export and import round-trip a document with images, tasks and free connections, with identical node counts and positions. Do not build yet: No autosave or recovery work yet. ### Phase 5 · Not losing the document Build: Autosave to browser storage on a debounce and after every command batch. Detect a storage quota failure and tell the user immediately rather than at the next reload. Keep a small ring of recovery snapshots. Prompt for a file export at a stated interval, and show when the last file export happened. Trap: 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. Done when: A simulated storage quota failure surfaces a visible message at the moment of the write, not at reload, tested by stubbing the storage layer to throw. Reloading after a forced crash restores the document from the most recent recovery snapshot with at most the debounce interval lost, tested. The date of the last file export is displayed and turns into a prompt after the configured interval. Opening the application with storage unavailable still allows editing and exporting, with a clear banner. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer editing with live cursors, which needs a server, a conflict model and presence, and is most of a different product. - Importing other mind mapping formats with full fidelity, since each one is a reverse engineering project and a partial import that silently drops nodes is worse than no import. - Presentation mode and slide export, which is a second layout engine attached to a canvas that does not need one. - A template library, which is content rather than software and ages faster than the code around it. ### README must contain - That the exported SVG is self-contained because an SVG used as an image cannot resolve external references - That browser storage is a cache and the exported file is the copy that survives a cleared setting - How undo restores selection as well as content, and what the history cap is - That a node carrying task fields is one record, and that the board and the canvas are two views of it - Where documents live, how to export everything, and how to open an export from an older schema version ===== ARCHITECTURE.md ===== # Architecture · Ayoa ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with TypeScript, built with Vite, running entirely in the browser | no server means no accounts and no hosting, and this is a single-person tool | | Storage | IndexedDB for documents, with an explicit file export as the durable copy | browser storage is convenient and is not a backup | | Model | One node type that can carry task fields, never a separate task record | two records for one thing is how a map and a board stop agreeing | | History | An append-only command log with coalescing, not snapshots of the document | snapshots of a large map are slow and lose the selection people expect back | | Export | Self-contained SVG with fonts embedded, rasterised through the same path | an SVG in an image element cannot fetch anything, which decides the whole export design | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Canvas | Document coordinates, hit testing and keyboard creation. | Any renderer, provided stored coordinates stay independent of zoom. | | History | Commands, inverses, coalescing and selection. | Any implementation, provided selection is restored with the document. | | Model | Nodes, edges and attached task fields. | Not swappable for a separate task entity, which is the drift bug. | | Export | Self-contained SVG, PNG from that SVG, versioned JSON. | Any serialiser, provided the output contains no external reference. | | Storage | Autosave, quota failures and recovery snapshots. | Any store, provided a failure is reported at write time. | ## 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` · required · Window title and export metadata. - `VITE_AUTOSAVE_DEBOUNCE_MS` · required · The most work a crash can cost, in milliseconds. - `VITE_HISTORY_LIMIT` · required · Commands retained before the oldest are dropped. - `VITE_RECOVERY_SNAPSHOTS` · required · Size of the recovery ring kept in browser storage. - `VITE_EXPORT_REMINDER_DAYS` · required · After this the last-export date turns into a prompt. ## 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 · Ayoa product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: React with TypeScript, built with Vite, running entirely in the browser, IndexedDB for documents, with an explicit file export as the durable copy, One node type that can carry task fields, never a separate task record, An append-only command log with coalescing, not snapshots of the document, Self-contained SVG with fonts embedded, rasterised through the same path. - 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 - 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 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. - 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. - 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. - 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. ===== MILESTONES.md ===== # Delivery milestones · Ayoa Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · 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. ### Steps 1. Keep document coordinates free of zoom A saved layout should not remember how you were looking at it. Files: `src/canvas/coords.ts`, `tests/coords.test.ts` 2. Make create-child and create-sibling single keys Outlining speed is the only reason to use a mind map. Files: `src/canvas/keyboard.ts` 3. Hit test in document space Screen-space hit testing drifts the moment you zoom. Files: `src/canvas/hit.ts` ### Done when - [ ] Zoom and pan do not alter stored coordinates - [ ] Child and sibling creation are single keys - [ ] Hit testing is correct at several zoom levels - [ ] Subtree drag follows one stated rule ### Watch 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. ## M2 · 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. ### Steps 1. Put the selection on both sides of every command Undo is a navigation as much as an edit. Files: `src/history/command.ts`, `tests/undo-selection.test.ts` 2. Coalesce typing within a short window One undo per keystroke is the same as no undo. Files: `src/history/coalesce.ts` 3. Route every mutation through the command layer The one direct mutation is the one that breaks undo. Files: `tests/no-direct-mutation.test.ts` ### Done when - [ ] Undo and redo restore document and selection - [ ] Typing collapses into one undo step - [ ] No mutation bypasses the command layer - [ ] History capping drops the oldest and says so ### Watch 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. ## M3 · 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. ### Steps 1. Attach task fields to the node record Two titles for one item is a synchronisation bug you chose. Files: `src/model/node.ts` 2. Make board edits ordinary commands Undo should not care which view you were in. Files: `src/board/commands.ts`, `tests/board-undo.test.ts` 3. Delete node and task fields together An orphaned task is the second half of the same bug. Files: `src/model/delete.ts` ### Done when - [ ] A board rename changes the canvas node - [ ] Board completion is undoable from the canvas - [ ] No separate task title exists in the schema - [ ] Deleting a node clears it from the board ### Watch 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. ## M4 · An export that opens correctly somewhere else The exported SVG renders identically in a browser, an image viewer and whatever your colleague uses. ### Steps 1. Embed fonts and images in the SVG itself An SVG used as an image is not allowed to fetch anything. Files: `src/export/svg.ts`, `tests/no-external-refs.test.ts` 2. Rasterise the exported SVG, not the live canvas Then a broken export is visible before you send it. Files: `src/export/png.ts` 3. Version the JSON and refuse a newer schema Opening a file from a later version should say so. Files: `src/io/json.ts` ### Done when - [ ] The exported SVG has no external references - [ ] The intended typeface survives in an image element - [ ] PNG is rasterised from the exported SVG - [ ] JSON round-trips images, tasks and connections ### Watch 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. ## M5 · Not losing the document (production only) Browser storage is treated as a cache, the file on disk is the copy that matters, and a crash costs seconds. ### Steps 1. Wrap every storage access and surface failures now Quota errors are silent until the work is already gone. Files: `src/storage/guarded.ts`, `tests/quota-failure.test.ts` 2. Keep a ring of recovery snapshots The crash you did not predict is the one it covers. Files: `src/storage/recovery.ts` 3. Show the age of the last file export A number going stale is a better reminder than a dialogue. Files: `src/components/ExportStatus.tsx` ### Done when - [ ] A storage failure is reported immediately - [ ] A forced crash recovers from a snapshot - [ ] The last export date is shown and prompts - [ ] Editing works with storage unavailable ### Watch 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. ===== OPERATIONS.md ===== # Operations · Ayoa ## Backup The JSON export, on a schedule you keep. Browser storage is not a backup and the application says so on its own front page. ## Restore Import the JSON into an empty application and confirm node count, task count and one image render. An import that silently drops a node type is the failure this check exists to catch. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: no external reference in an export, no stored coordinate changed by zoom, no mutation outside the command layer, and a quota failure surfaced at write time. ## Incident checklist The incident is lost work, and it comes from browser storage rather than from a bug in the canvas. Recovery snapshots and a visible last-export date are the two things that turn it from a loss into an inconvenience. 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 - [ ] Exports are confirmed to contain no external reference - [ ] Undo and redo are confirmed to restore selection as well as content - [ ] No stored coordinate is confirmed to change with zoom - [ ] A board rename is confirmed to change the canvas node - [ ] A storage quota failure is confirmed to surface immediately ## Launch constraint Do not market omitted Ayoa 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. # Required. Window title and export metadata. VITE_APP_TITLE=Maps # Required. The most work a crash can cost, in milliseconds. VITE_AUTOSAVE_DEBOUNCE_MS=800 # Required. Commands retained before the oldest are dropped. VITE_HISTORY_LIMIT=500 # Required. Size of the recovery ring kept in browser storage. VITE_RECOVERY_SNAPSHOTS=5 # Required. After this the last-export date turns into a prompt. VITE_EXPORT_REMINDER_DAYS=7
# Ayoa · indie build 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. Estimated effort: **multi-day**. 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 | React with TypeScript, built with Vite, running entirely in the browser | no server means no accounts and no hosting, and this is a single-person tool | | Storage | IndexedDB for documents, with an explicit file export as the durable copy | browser storage is convenient and is not a backup | | Model | One node type that can carry task fields, never a separate task record | two records for one thing is how a map and a board stop agreeing | | History | An append-only command log with coalescing, not snapshots of the document | snapshots of a large map are slow and lose the selection people expect back | | Export | Self-contained SVG with fonts embedded, rasterised through the same path | an SVG in an image element cannot fetch anything, which decides the whole export design | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the application and runs the test suite. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A typeface you are licensed to embed** · free 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 - [ ] **Two applications that are not your browser tab** · free - 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 - [ ] **What a single-user canvas will never do** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Ayoa, and the README should say so rather than pretend.
# Build brief · Ayoa 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 local mind map and task board whose exports still look right in an application you did not write to replace Ayoa. 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: React with TypeScript, built with Vite, running entirely in the browser · no server means no accounts and no hosting, and this is a single-person tool - Storage: IndexedDB for documents, with an explicit file export as the durable copy · browser storage is convenient and is not a backup - Model: One node type that can carry task fields, never a separate task record · two records for one thing is how a map and a board stop agreeing - History: An append-only command log with coalescing, not snapshots of the document · snapshots of a large map are slow and lose the selection people expect back - Export: Self-contained SVG with fonts embedded, rasterised through the same path · an SVG in an image element cannot fetch anything, which decides the whole export design ### Data model (create this before Phase 1) - 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. ### Phase 1 · A canvas that does not fight the pointer Build: An infinite pan and zoom canvas with nodes positioned in document coordinates and converted at render. Create a child with one key, a sibling with another, edit text in place, and drag to connect. Hit testing in document space, not screen space. Keep the zoom factor out of every stored coordinate. Trap: 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. Done when: Zooming and panning leave every stored node coordinate unchanged, asserted in the suite. A child and a sibling can each be created with a single key from a selected node, and focus lands in the new node's editor. Hit testing selects the topmost node under the pointer at any zoom, tested at three zoom levels. Dragging a node moves its subtree or only itself according to a stated rule, and the rule is the same with the keyboard. Do not build yet: No undo, no tasks, no export, no persistence beyond the session. ### Phase 2 · Undo that returns you to where you were Build: Represent every mutation as a command with its inverse. Coalesce consecutive text edits on the same node within a short window into one command. Store the selection before and after each command and restore it on undo and redo. Cap the history and drop the oldest rather than the newest. Trap: 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. Done when: Undo then redo returns the document and the selection to exactly their prior states, asserted by deep comparison in the suite. Typing a word produces one undo step rather than one per keystroke, tested with a simulated typing sequence. Every mutation in the codebase goes through the command layer, verified by a test that no reducer mutates state directly. The history cap drops the oldest entries and the interface says when it has. Do not build yet: No tasks, no export, no persistence yet. ### Phase 3 · A node that is also a task Build: Task fields attached to the node record. A board view filtering and grouping the same nodes by due date and done state, where editing text in either view edits the same node. Completing a task from the board is a command in the same history as a canvas edit. Trap: 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. Done when: Renaming a node in the board view changes the same record shown on the canvas, tested through the store. Completing a task from the board is undoable from the canvas, tested. No table in the schema holds a task title, asserted by inspection and by a schema test. Deleting a node removes its task fields and it disappears from the board in the same command. Do not build yet: No export, no import, no persistence work. ### Phase 4 · An export that opens correctly somewhere else Build: Serialise the document to SVG with every font embedded as a base64 data URL inside an inline style, and every pasted image embedded as a data URL too. Rasterise to PNG by drawing that same self-contained SVG, so anything broken in the SVG is visibly broken in the PNG rather than only in the file you sent. Export and import the document as JSON with its schema version. Trap: 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. Done when: The exported SVG contains no external URL of any kind, asserted by a test that scans the output for http references. The same SVG rendered in an image element shows the intended typeface, checked visually and by a size comparison against a reference render. PNG export draws the identical serialised SVG rather than reading the live canvas, verified by review. JSON export and import round-trip a document with images, tasks and free connections, with identical node counts and positions. Do not build yet: No autosave or recovery work yet. ### Phase 5 · Not losing the document Build: Autosave to browser storage on a debounce and after every command batch. Detect a storage quota failure and tell the user immediately rather than at the next reload. Keep a small ring of recovery snapshots. Prompt for a file export at a stated interval, and show when the last file export happened. Trap: 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. Done when: A simulated storage quota failure surfaces a visible message at the moment of the write, not at reload, tested by stubbing the storage layer to throw. Reloading after a forced crash restores the document from the most recent recovery snapshot with at most the debounce interval lost, tested. The date of the last file export is displayed and turns into a prompt after the configured interval. Opening the application with storage unavailable still allows editing and exporting, with a clear banner. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer editing with live cursors, which needs a server, a conflict model and presence, and is most of a different product. - Importing other mind mapping formats with full fidelity, since each one is a reverse engineering project and a partial import that silently drops nodes is worse than no import. - Presentation mode and slide export, which is a second layout engine attached to a canvas that does not need one. - A template library, which is content rather than software and ages faster than the code around it. ### README must contain - That the exported SVG is self-contained because an SVG used as an image cannot resolve external references - That browser storage is a cache and the exported file is the copy that survives a cleared setting - How undo restores selection as well as content, and what the history cap is - That a node carrying task fields is one record, and that the board and the canvas are two views of it - Where documents live, how to export everything, and how to open an export from an older schema version
# Agent instructions · Ayoa indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: React with TypeScript, built with Vite, running entirely in the browser, IndexedDB for documents, with an explicit file export as the durable copy, One node type that can carry task fields, never a separate task record, An append-only command log with coalescing, not snapshots of the document, Self-contained SVG with fonts embedded, rasterised through the same path. 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 - 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 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. - 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. - 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.
# Build plan · Ayoa 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · 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. ### Steps 1. Keep document coordinates free of zoom A saved layout should not remember how you were looking at it. Files: `src/canvas/coords.ts`, `tests/coords.test.ts` 2. Make create-child and create-sibling single keys Outlining speed is the only reason to use a mind map. Files: `src/canvas/keyboard.ts` 3. Hit test in document space Screen-space hit testing drifts the moment you zoom. Files: `src/canvas/hit.ts` ### Done when - [ ] Zoom and pan do not alter stored coordinates - [ ] Child and sibling creation are single keys - [ ] Hit testing is correct at several zoom levels - [ ] Subtree drag follows one stated rule ### Watch 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. ## Phase 2 · 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. ### Steps 1. Put the selection on both sides of every command Undo is a navigation as much as an edit. Files: `src/history/command.ts`, `tests/undo-selection.test.ts` 2. Coalesce typing within a short window One undo per keystroke is the same as no undo. Files: `src/history/coalesce.ts` 3. Route every mutation through the command layer The one direct mutation is the one that breaks undo. Files: `tests/no-direct-mutation.test.ts` ### Done when - [ ] Undo and redo restore document and selection - [ ] Typing collapses into one undo step - [ ] No mutation bypasses the command layer - [ ] History capping drops the oldest and says so ### Watch 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. ## Phase 3 · 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. ### Steps 1. Attach task fields to the node record Two titles for one item is a synchronisation bug you chose. Files: `src/model/node.ts` 2. Make board edits ordinary commands Undo should not care which view you were in. Files: `src/board/commands.ts`, `tests/board-undo.test.ts` 3. Delete node and task fields together An orphaned task is the second half of the same bug. Files: `src/model/delete.ts` ### Done when - [ ] A board rename changes the canvas node - [ ] Board completion is undoable from the canvas - [ ] No separate task title exists in the schema - [ ] Deleting a node clears it from the board ### Watch 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. ## Phase 4 · An export that opens correctly somewhere else The exported SVG renders identically in a browser, an image viewer and whatever your colleague uses. ### Steps 1. Embed fonts and images in the SVG itself An SVG used as an image is not allowed to fetch anything. Files: `src/export/svg.ts`, `tests/no-external-refs.test.ts` 2. Rasterise the exported SVG, not the live canvas Then a broken export is visible before you send it. Files: `src/export/png.ts` 3. Version the JSON and refuse a newer schema Opening a file from a later version should say so. Files: `src/io/json.ts` ### Done when - [ ] The exported SVG has no external references - [ ] The intended typeface survives in an image element - [ ] PNG is rasterised from the exported SVG - [ ] JSON round-trips images, tasks and connections ### Watch 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 in this build - Multiplayer editing and live cursors. - Full-fidelity import of other mind map formats. - Presentation mode and slide export. - A template library. ## After v1, if you want it - 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
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Window title and export metadata. VITE_APP_TITLE=Maps # Required. The most work a crash can cost, in milliseconds. VITE_AUTOSAVE_DEBOUNCE_MS=800 # Required. Commands retained before the oldest are dropped. VITE_HISTORY_LIMIT=500 # Required. Size of the recovery ring kept in browser storage. VITE_RECOVERY_SNAPSHOTS=5 # Required. After this the last-export date turns into a prompt. VITE_EXPORT_REMINDER_DAYS=7
# Ayoa · product brief ## Problem The visible mind mapping + tasks loop is buildable, but a credible replacement needs more than the first screen. Ayoa earns its keep through interaction polish, collaboration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A canvas where the map and the board are one set of objects, undo returns you to where you were, and the exported file renders correctly on a machine that has never seen your application. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - modern browser - no account required ## Explicit non-goals for v1 - 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 ## Success criteria - Exports are confirmed to contain no external reference - Undo and redo are confirmed to restore selection as well as content - No stored coordinate is confirmed to change with zoom - A board rename is confirmed to change the canvas node - A storage quota failure is confirmed to surface immediately
# Build brief · Ayoa 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 local mind map and task board whose exports still look right in an application you did not write to replace Ayoa. 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: React with TypeScript, built with Vite, running entirely in the browser · no server means no accounts and no hosting, and this is a single-person tool - Storage: IndexedDB for documents, with an explicit file export as the durable copy · browser storage is convenient and is not a backup - Model: One node type that can carry task fields, never a separate task record · two records for one thing is how a map and a board stop agreeing - History: An append-only command log with coalescing, not snapshots of the document · snapshots of a large map are slow and lose the selection people expect back - Export: Self-contained SVG with fonts embedded, rasterised through the same path · an SVG in an image element cannot fetch anything, which decides the whole export design ### Data model (create this before Phase 1) - 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. ### Phase 1 · A canvas that does not fight the pointer Build: An infinite pan and zoom canvas with nodes positioned in document coordinates and converted at render. Create a child with one key, a sibling with another, edit text in place, and drag to connect. Hit testing in document space, not screen space. Keep the zoom factor out of every stored coordinate. Trap: 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. Done when: Zooming and panning leave every stored node coordinate unchanged, asserted in the suite. A child and a sibling can each be created with a single key from a selected node, and focus lands in the new node's editor. Hit testing selects the topmost node under the pointer at any zoom, tested at three zoom levels. Dragging a node moves its subtree or only itself according to a stated rule, and the rule is the same with the keyboard. Do not build yet: No undo, no tasks, no export, no persistence beyond the session. ### Phase 2 · Undo that returns you to where you were Build: Represent every mutation as a command with its inverse. Coalesce consecutive text edits on the same node within a short window into one command. Store the selection before and after each command and restore it on undo and redo. Cap the history and drop the oldest rather than the newest. Trap: 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. Done when: Undo then redo returns the document and the selection to exactly their prior states, asserted by deep comparison in the suite. Typing a word produces one undo step rather than one per keystroke, tested with a simulated typing sequence. Every mutation in the codebase goes through the command layer, verified by a test that no reducer mutates state directly. The history cap drops the oldest entries and the interface says when it has. Do not build yet: No tasks, no export, no persistence yet. ### Phase 3 · A node that is also a task Build: Task fields attached to the node record. A board view filtering and grouping the same nodes by due date and done state, where editing text in either view edits the same node. Completing a task from the board is a command in the same history as a canvas edit. Trap: 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. Done when: Renaming a node in the board view changes the same record shown on the canvas, tested through the store. Completing a task from the board is undoable from the canvas, tested. No table in the schema holds a task title, asserted by inspection and by a schema test. Deleting a node removes its task fields and it disappears from the board in the same command. Do not build yet: No export, no import, no persistence work. ### Phase 4 · An export that opens correctly somewhere else Build: Serialise the document to SVG with every font embedded as a base64 data URL inside an inline style, and every pasted image embedded as a data URL too. Rasterise to PNG by drawing that same self-contained SVG, so anything broken in the SVG is visibly broken in the PNG rather than only in the file you sent. Export and import the document as JSON with its schema version. Trap: 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. Done when: The exported SVG contains no external URL of any kind, asserted by a test that scans the output for http references. The same SVG rendered in an image element shows the intended typeface, checked visually and by a size comparison against a reference render. PNG export draws the identical serialised SVG rather than reading the live canvas, verified by review. JSON export and import round-trip a document with images, tasks and free connections, with identical node counts and positions. Do not build yet: No autosave or recovery work yet. ### Phase 5 · Not losing the document Build: Autosave to browser storage on a debounce and after every command batch. Detect a storage quota failure and tell the user immediately rather than at the next reload. Keep a small ring of recovery snapshots. Prompt for a file export at a stated interval, and show when the last file export happened. Trap: 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. Done when: A simulated storage quota failure surfaces a visible message at the moment of the write, not at reload, tested by stubbing the storage layer to throw. Reloading after a forced crash restores the document from the most recent recovery snapshot with at most the debounce interval lost, tested. The date of the last file export is displayed and turns into a prompt after the configured interval. Opening the application with storage unavailable still allows editing and exporting, with a clear banner. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer editing with live cursors, which needs a server, a conflict model and presence, and is most of a different product. - Importing other mind mapping formats with full fidelity, since each one is a reverse engineering project and a partial import that silently drops nodes is worse than no import. - Presentation mode and slide export, which is a second layout engine attached to a canvas that does not need one. - A template library, which is content rather than software and ages faster than the code around it. ### README must contain - That the exported SVG is self-contained because an SVG used as an image cannot resolve external references - That browser storage is a cache and the exported file is the copy that survives a cleared setting - How undo restores selection as well as content, and what the history cap is - That a node carrying task fields is one record, and that the board and the canvas are two views of it - Where documents live, how to export everything, and how to open an export from an older schema version
# Architecture · Ayoa ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with TypeScript, built with Vite, running entirely in the browser | no server means no accounts and no hosting, and this is a single-person tool | | Storage | IndexedDB for documents, with an explicit file export as the durable copy | browser storage is convenient and is not a backup | | Model | One node type that can carry task fields, never a separate task record | two records for one thing is how a map and a board stop agreeing | | History | An append-only command log with coalescing, not snapshots of the document | snapshots of a large map are slow and lose the selection people expect back | | Export | Self-contained SVG with fonts embedded, rasterised through the same path | an SVG in an image element cannot fetch anything, which decides the whole export design | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Canvas | Document coordinates, hit testing and keyboard creation. | Any renderer, provided stored coordinates stay independent of zoom. | | History | Commands, inverses, coalescing and selection. | Any implementation, provided selection is restored with the document. | | Model | Nodes, edges and attached task fields. | Not swappable for a separate task entity, which is the drift bug. | | Export | Self-contained SVG, PNG from that SVG, versioned JSON. | Any serialiser, provided the output contains no external reference. | | Storage | Autosave, quota failures and recovery snapshots. | Any store, provided a failure is reported at write time. | ## 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` · required · Window title and export metadata. - `VITE_AUTOSAVE_DEBOUNCE_MS` · required · The most work a crash can cost, in milliseconds. - `VITE_HISTORY_LIMIT` · required · Commands retained before the oldest are dropped. - `VITE_RECOVERY_SNAPSHOTS` · required · Size of the recovery ring kept in browser storage. - `VITE_EXPORT_REMINDER_DAYS` · required · After this the last-export date turns into a prompt. ## 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 · Ayoa product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: React with TypeScript, built with Vite, running entirely in the browser, IndexedDB for documents, with an explicit file export as the durable copy, One node type that can carry task fields, never a separate task record, An append-only command log with coalescing, not snapshots of the document, Self-contained SVG with fonts embedded, rasterised through the same path. - 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 - 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 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. - 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. - 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. - 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.
# Delivery milestones · Ayoa Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · 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. ### Steps 1. Keep document coordinates free of zoom A saved layout should not remember how you were looking at it. Files: `src/canvas/coords.ts`, `tests/coords.test.ts` 2. Make create-child and create-sibling single keys Outlining speed is the only reason to use a mind map. Files: `src/canvas/keyboard.ts` 3. Hit test in document space Screen-space hit testing drifts the moment you zoom. Files: `src/canvas/hit.ts` ### Done when - [ ] Zoom and pan do not alter stored coordinates - [ ] Child and sibling creation are single keys - [ ] Hit testing is correct at several zoom levels - [ ] Subtree drag follows one stated rule ### Watch 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. ## M2 · 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. ### Steps 1. Put the selection on both sides of every command Undo is a navigation as much as an edit. Files: `src/history/command.ts`, `tests/undo-selection.test.ts` 2. Coalesce typing within a short window One undo per keystroke is the same as no undo. Files: `src/history/coalesce.ts` 3. Route every mutation through the command layer The one direct mutation is the one that breaks undo. Files: `tests/no-direct-mutation.test.ts` ### Done when - [ ] Undo and redo restore document and selection - [ ] Typing collapses into one undo step - [ ] No mutation bypasses the command layer - [ ] History capping drops the oldest and says so ### Watch 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. ## M3 · 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. ### Steps 1. Attach task fields to the node record Two titles for one item is a synchronisation bug you chose. Files: `src/model/node.ts` 2. Make board edits ordinary commands Undo should not care which view you were in. Files: `src/board/commands.ts`, `tests/board-undo.test.ts` 3. Delete node and task fields together An orphaned task is the second half of the same bug. Files: `src/model/delete.ts` ### Done when - [ ] A board rename changes the canvas node - [ ] Board completion is undoable from the canvas - [ ] No separate task title exists in the schema - [ ] Deleting a node clears it from the board ### Watch 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. ## M4 · An export that opens correctly somewhere else The exported SVG renders identically in a browser, an image viewer and whatever your colleague uses. ### Steps 1. Embed fonts and images in the SVG itself An SVG used as an image is not allowed to fetch anything. Files: `src/export/svg.ts`, `tests/no-external-refs.test.ts` 2. Rasterise the exported SVG, not the live canvas Then a broken export is visible before you send it. Files: `src/export/png.ts` 3. Version the JSON and refuse a newer schema Opening a file from a later version should say so. Files: `src/io/json.ts` ### Done when - [ ] The exported SVG has no external references - [ ] The intended typeface survives in an image element - [ ] PNG is rasterised from the exported SVG - [ ] JSON round-trips images, tasks and connections ### Watch 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. ## M5 · Not losing the document (production only) Browser storage is treated as a cache, the file on disk is the copy that matters, and a crash costs seconds. ### Steps 1. Wrap every storage access and surface failures now Quota errors are silent until the work is already gone. Files: `src/storage/guarded.ts`, `tests/quota-failure.test.ts` 2. Keep a ring of recovery snapshots The crash you did not predict is the one it covers. Files: `src/storage/recovery.ts` 3. Show the age of the last file export A number going stale is a better reminder than a dialogue. Files: `src/components/ExportStatus.tsx` ### Done when - [ ] A storage failure is reported immediately - [ ] A forced crash recovers from a snapshot - [ ] The last export date is shown and prompts - [ ] Editing works with storage unavailable ### Watch 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.
# Operations · Ayoa ## Backup The JSON export, on a schedule you keep. Browser storage is not a backup and the application says so on its own front page. ## Restore Import the JSON into an empty application and confirm node count, task count and one image render. An import that silently drops a node type is the failure this check exists to catch. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: no external reference in an export, no stored coordinate changed by zoom, no mutation outside the command layer, and a quota failure surfaced at write time. ## Incident checklist The incident is lost work, and it comes from browser storage rather than from a bug in the canvas. Recovery snapshots and a visible last-export date are the two things that turn it from a loss into an inconvenience. 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 - [ ] Exports are confirmed to contain no external reference - [ ] Undo and redo are confirmed to restore selection as well as content - [ ] No stored coordinate is confirmed to change with zoom - [ ] A board rename is confirmed to change the canvas node - [ ] A storage quota failure is confirmed to surface immediately ## Launch constraint Do not market omitted Ayoa 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. # Required. Window title and export metadata. VITE_APP_TITLE=Maps # Required. The most work a crash can cost, in milliseconds. VITE_AUTOSAVE_DEBOUNCE_MS=800 # Required. Commands retained before the oldest are dropped. VITE_HISTORY_LIMIT=500 # Required. Size of the recovery ring kept in browser storage. VITE_RECOVERY_SNAPSHOTS=5 # Required. After this the last-export date turns into a prompt. VITE_EXPORT_REMINDER_DAYS=7
$ choose a build depth, inspect the files, then open the complete pack in your agent
Ayoa: The shapes are easy; customers pay for low-friction editing, reliable exports, collaboration, and a canvas that never fights them.
xhigh-fidelity import and export
xpresentation and enterprise controls
xmultiplayer cursors and comments
xlarge template libraries
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Ayoa pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | 10 mind maps; whiteboards and task boards are view-only; 0 AI credits |
| ultimate | $17/user | $13/user | Unlimited mind maps, whiteboards and task boards |
free tier10 mind maps; whiteboards/tasks view-only; no AI credits; personal use only
billingmonthly + annual; 3-year option also offered
hidden costsCommercial use requires Ultimate. The account begins with a 7-day Ultimate trial, after which non-paying accounts fall back to the restricted Free plan.
verified 2026-08-12 · source ↗
Vibecode Ayoa
Kinda. The core of Ayoa is buildable in a weekend with the prompt on this page, but there are real gaps: high-fidelity import and export, presentation and enterprise controls. Read the honest list above before committing.
How much does Ayoa cost?
Ayoa's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Ayoa?
Honestly: high-fidelity import and export; presentation and enterprise controls; multiplayer cursors and comments; large template libraries. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Ayoa?
Yes: AFFiNE (A surprisingly complete docs-and-whiteboards workspace; the free plan allows unlimited local workspaces, but cloud history and team size are capped.) The prompt is for when you want it exactly your way.