Vibecode SimpleMind
track this build4 phases, 12 steps, beginner friendly0%SimpleMind's solo core is compact: build a browser-based mind mapping editor with drag, resize, connect, keyboard shortcuts, autosave, and SVG or PNG export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on interaction polish, collaboration.
You are building a lean indie version of SimpleMind. 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 ===== # SimpleMind · indie build A local mind map editor whose hard problems are refusing to treat evictable browser storage as an archive, keeping the layout identical every time the same file is opened, and exporting a picture that still contains its fonts and images once the browser rasterises it. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with Vite, no backend at all, served from a static host or opened locally | a mind map does not need a server and adding one adds something that can be down | | Working store | IndexedDB with persistence requested and the granted answer displayed | storage that can be evicted is a cache whatever the documentation calls it | | Archive | One JSON file per map, written back through the file handle you opened | the file is the map and the browser is only where you edit it | | Layout | A deterministic tidy tree computed from the document, with manual positions stored per node | a layout that shifts when you reopen a file is a layout nobody trusts | | Export | SVG with fonts and images embedded as data, then PNG rasterised from that same SVG | an SVG inside an image element is not permitted to fetch anything external | ## 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; nothing runs server-side once it is built. - Get it: nodejs.org or a version manager. - Verify: node --version prints v22 or higher - [ ] **A browser with a file handle API and a storage persistence API** · free - Why: The archive design depends on writing back to a file the person chose, and the honesty design depends on being able to ask whether storage is persistent. - Get it: A current desktop browser; check the support tables for both APIs before committing. - Verify: Opening a file, editing and saving writes back to the same file without a download - [ ] **A font you intend to use, licensed for embedding** · free with an open licence - Why: The export embeds a subset of the font in the SVG, which is a use the font's licence either allows or does not, and finding out afterwards is the wrong order. - Get it: Choose a font whose licence permits embedding and keep the licence file with the project. - Verify: The licence text permits embedding and the file is in the repository - [ ] **A map with several hundred nodes** · free - Why: Layout determinism and layout speed are both invisible on a map with twelve nodes, and both are things people abandon an editor over. - Get it: Generate one, or import a large outline. - Verify: Layout time is measured and recorded and two layouts are identical ## 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: - Collaboration, cursors and comments. - High-fidelity import from proprietary formats. - A presentation mode. - Cloud sync. - 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 SimpleMind, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · SimpleMind 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 mind map editor that keeps every map in a file you own and exports a picture that still has its text and its pictures in it to replace SimpleMind. 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. ### Rule zero Browser storage is not durable, and an editor that pretends otherwise will eventually lose somebody's map without asking anyone. The storage standard is explicit: a local storage bucket's mode starts as best-effort, and a user agent under storage pressure should clear best-effort buckets. That includes your IndexedDB database. So the word 'saved' cannot refer to IndexedDB alone. Request persistence, check what the browser actually granted, and show the answer in the interface rather than assuming a yes. Which means the archive is a file. Every map is one JSON document the person can save to disk, written back through the file handle they opened where the browser supports it and offered as a download where it does not. The application asks for a file on first use rather than after the first loss. IndexedDB is the working copy and the crash recovery, never the only copy. There is no account, no server and no sync here. If the browser clears its storage and there is no file on disk, the map is gone, and the interface has to be honest about that from day one. ### Stack (fixed, do not substitute) - Runtime: React with Vite, no backend at all, served from a static host or opened locally · a mind map does not need a server and adding one adds something that can be down - Working store: IndexedDB with persistence requested and the granted answer displayed · storage that can be evicted is a cache whatever the documentation calls it - Archive: One JSON file per map, written back through the file handle you opened · the file is the map and the browser is only where you edit it - Layout: A deterministic tidy tree computed from the document, with manual positions stored per node · a layout that shifts when you reopen a file is a layout nobody trusts - Export: SVG with fonts and images embedded as data, then PNG rasterised from that same SVG · an SVG inside an image element is not permitted to fetch anything external ### Data model (create this before Phase 1) - document(format_version, id, title, created_at, updated_at, root_id) · the envelope, versioned from the first release - nodes(id, parent_id, ordinal, text, note, collapsed, style_id, manual_x, manual_y) · manual_x and manual_y stay null until somebody drags that node - links(id, from_node, to_node, kind, label) · the cross connections that make this a map rather than an outline - styles(id, name, fill, stroke, text_colour, font_family, font_size) · referenced by id so restyling a branch is one write - assets(id, kind, mime, bytes_base64, sha256) · images live inside the document, because an image referenced by path is an image that goes missing - snapshots(id, at, sha256, bytes) · the last few versions that opened cleanly, kept separately from the working copy - The whole document is one JSON object that round-trips byte-identically through save and load. If it does not, you have a serialisation format rather than a file format, and the difference shows up the first time you diff two versions. ### Phase 1 · A document that is a file Build: Create or open a map, hold the file handle, and write back on save. Keep the working copy in IndexedDB for crash recovery. Call the persistence request on first use, read back what was granted, and show it. Show a dirty indicator that means unwritten to the file, not unwritten to the database. Trap: Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. Done when: The persistence request runs on first use and the granted or denied answer is visible in the interface, tested with the request stubbed to deny. The dirty indicator reflects unwritten file state, not database state, asserted by editing with the file handle revoked. A map opened, edited and saved round-trips byte-identically when nothing was changed, tested. Clearing IndexedDB in devtools and reopening the file restores the map completely. Do not build yet: No editing shortcuts, layout work, exports or import formats yet. ### Phase 2 · Editing at the speed of thinking Build: Enter adds a sibling, Tab adds a child, arrows move focus through the tree, typing edits the focused node and Escape commits. Drag to reparent with a visible drop target. Undo and redo group edits by node and by a short idle window rather than by keystroke. Trap: One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. Done when: A ten node map can be built without touching the mouse, tested by a scripted key sequence. Undo after typing a title removes the whole title in one step, tested. Undo after a reparent restores the previous parent and ordinal and the layout follows, tested. Focus is always visible and every action has a keyboard route, checked against a keyboard-only pass over the whole interface. Do not build yet: No layout determinism work, exports or import formats yet. ### Phase 3 · A layout that holds still Build: Compute a tidy tree layout as a pure function of the document, balancing branches around the root. Nodes with manual positions are pinned and everything else flows around them. Collapsing a branch reflows without moving anything outside that branch more than it has to. Trap: A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. Done when: Laying out the same document twice produces identical coordinates to the last decimal, asserted in a test. A node with manual coordinates stays exactly where it was placed after adding a sibling elsewhere, tested. Collapsing and expanding a branch returns every other node to its previous position, tested. Layout for a five hundred node map completes fast enough to feel immediate, timed and recorded in the README. Do not build yet: No export, import or recovery work yet. ### Phase 4 · Export that still contains its text Build: Serialise the canvas to SVG with the font subset embedded as a data URI and every image embedded as a data URI. Rasterise that same SVG to PNG at a chosen scale through a canvas. Also export a plain Markdown outline, which is the format that survives everything. Trap: An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. Done when: An exported PNG rendered from a map using a custom font matches the on-screen text metrics, checked by comparing rendered widths rather than by eye. An exported SVG opened on a machine without that font still shows the correct face. An embedded image appears in both the SVG and the PNG, tested. The Markdown outline export round-trips structure and text, and reimporting it rebuilds the tree. Do not build yet: No recovery snapshots, interchange import or multi-map handling yet. ### Phase 5 · Recovery, interchange and the honest limits Build: Keep the last few snapshots that opened cleanly, keyed by content hash, separate from the working copy. Import and export at least one interchange format. Handle several maps in one folder. State plainly what this build does not do that the paid application does. Trap: A single autosave slot gets overwritten with the corrupted state the moment after a crash, so the recovery feature hands you exactly the broken document you were trying to escape; keep the last few snapshots that parsed cleanly, never overwrite the most recent good one, and offer recovery as a list with timestamps rather than as one silent restore that you cannot decline. Done when: A corrupted working copy does not overwrite the most recent good snapshot, tested by writing invalid JSON into the working store. Recovery is offered as a dated list and can be declined, leaving the file untouched. An interchange import produces the same structure on export, tested by a round-trip through the other format. The README lists what is absent, including collaboration, presentation mode and high-fidelity import from the paid tools. Do not build yet: Nothing after this. ### Out of scope (and why) - Collaboration, cursors and comments, which need a server, a conflict model and an account system, and are most of what the paid products charge for. - High-fidelity import from proprietary mind map formats, which is reverse engineering somebody's binary layout rather than a weekend of work. - A presentation mode that walks the map as slides, which is a second product hiding inside the first. - Cloud sync, because there is no server here on purpose and adding one changes every answer about where the data lives. ### README must contain - That browser storage can be cleared by the browser, what persistence was requested and granted, and that the file on disk is the archive - That the word saved refers to bytes written to a file, and what the dirty indicator means - That the layout is deterministic and the test which asserts it - That exports embed fonts and images because an SVG used as an image cannot fetch anything external - What is deliberately absent, including collaboration, presentation mode and proprietary format import ===== AGENTS.md ===== # Agent instructions · SimpleMind indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: React with Vite, no backend at all, served from a static host or opened locally, IndexedDB with persistence requested and the granted answer displayed, One JSON file per map, written back through the file handle you opened, A deterministic tidy tree computed from the document, with manual positions stored per node, SVG with fonts and images embedded as data, then PNG rasterised from that same SVG. 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 - Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. - One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. - A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. - An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. ===== BUILD_PLAN.md ===== # Build plan · SimpleMind A local mind map editor whose hard problems are refusing to treat evictable browser storage as an archive, keeping the layout identical every time the same file is opened, and exporting a picture that still contains its fonts and images once the browser rasterises it. 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 document that is a file Saving means a file on disk, and the interface says what the browser promised about the working copy. ### Steps 1. Request persistence and show what came back The interesting case is the browser saying no. Files: `src/storage/persist.ts`, `tests/persist.test.ts` 2. Hold the file handle and write back to it Falling back to a download where the browser has no handle. Files: `src/storage/file.ts` 3. Make the dirty indicator mean the file, not the cache That is the only meaning that protects anybody. Files: `src/ui/DirtyDot.tsx` ### Done when - [ ] The granted persistence answer is shown - [ ] Dirty means unwritten to the file - [ ] An unchanged map round-trips byte-identically - [ ] Clearing the database loses nothing that was saved ### Watch out - Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. ## Phase 2 · Editing at the speed of thinking A map can be built entirely from the keyboard and undo removes what a person would call one action. ### Steps 1. Group undo by node and idle window One action to a person is one entry in the stack. Files: `src/history/group.ts`, `tests/undo-grouping.test.ts` 2. Give every action a keyboard route This is a thinking tool, and the mouse is the slow path. Files: `src/keys.ts` 3. Make reparenting undo restore ordinal too Restoring the parent but not the position is a half undo. Files: `src/history/inverse.ts` ### Done when - [ ] A ten node map is built without the mouse - [ ] Undo removes a typed title in one step - [ ] Reparent undo restores parent and ordinal - [ ] Focus is visible and every action is reachable ### Watch out - One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. ## Phase 3 · A layout that holds still Opening the same file twice produces pixel-identical geometry. ### Steps 1. Make the layout pure, with an explicit child order No clock, no random, no map iteration order. Files: `src/layout/tidy.ts`, `tests/layout-determinism.test.ts` 2. Pin manual positions and flow around them The whole point of dragging a node is that it stays dragged. Files: `src/layout/pins.ts` 3. Time it on a large map and write the number down Five hundred nodes is a real map, not a stress test. Files: `tests/layout-perf.test.ts`, `README.md` ### Done when - [ ] Two layouts of one document are identical - [ ] Manual positions survive edits elsewhere - [ ] Collapse and expand restores positions - [ ] Large map layout time is measured and recorded ### Watch out - A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. ## Phase 4 · Export that still contains its text The exported SVG and PNG look like the screen, including fonts and embedded images. ### Steps 1. Embed the font subset and every image as data External references are not fetched when the SVG is an image. Files: `src/export/svg.ts`, `tests/export-font.test.ts` 2. Rasterise the same SVG rather than redrawing Two renderers is two sets of differences. Files: `src/export/png.ts` 3. Compare the export to the screen in a test Text width is the measurement that catches a missing font. Files: `tests/export-parity.test.ts` ### Done when - [ ] PNG text metrics match the screen - [ ] SVG renders correctly without the font installed - [ ] Embedded images appear in both exports - [ ] The Markdown outline round-trips ### Watch out - An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. ## Not in this build - Collaboration, cursors and comments. - High-fidelity import from proprietary formats. - A presentation mode. - Cloud sync. ## After v1, if you want it - A folder view across many maps with search over node text, which is where a personal map collection starts being useful - Per-branch styling presets, once you have found out which three styles you actually use - A print layout that paginates a large map sensibly, which is a genuinely hard problem and worth its own weekend ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Shown in the tab and in the export metadata. No secrets exist in this build. APP_TITLE=Maps # Required. How many clean snapshots to retain for recovery. They are small. SNAPSHOT_KEEP=5 # Required. Idle time before the working copy is written and undo entries are grouped. AUTOSAVE_IDLE_MS=800 # Required. Rasterisation scale for PNG export. Two is the sensible default. EXPORT_PNG_SCALE=2 # Required. Largest image that can be embedded in a document, since the file carries it. MAX_ASSET_BYTES=2000000
You are building a lean indie version of SimpleMind. 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 ===== # SimpleMind · indie build A local mind map editor whose hard problems are refusing to treat evictable browser storage as an archive, keeping the layout identical every time the same file is opened, and exporting a picture that still contains its fonts and images once the browser rasterises it. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with Vite, no backend at all, served from a static host or opened locally | a mind map does not need a server and adding one adds something that can be down | | Working store | IndexedDB with persistence requested and the granted answer displayed | storage that can be evicted is a cache whatever the documentation calls it | | Archive | One JSON file per map, written back through the file handle you opened | the file is the map and the browser is only where you edit it | | Layout | A deterministic tidy tree computed from the document, with manual positions stored per node | a layout that shifts when you reopen a file is a layout nobody trusts | | Export | SVG with fonts and images embedded as data, then PNG rasterised from that same SVG | an SVG inside an image element is not permitted to fetch anything external | ## 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; nothing runs server-side once it is built. - Get it: nodejs.org or a version manager. - Verify: node --version prints v22 or higher - [ ] **A browser with a file handle API and a storage persistence API** · free - Why: The archive design depends on writing back to a file the person chose, and the honesty design depends on being able to ask whether storage is persistent. - Get it: A current desktop browser; check the support tables for both APIs before committing. - Verify: Opening a file, editing and saving writes back to the same file without a download - [ ] **A font you intend to use, licensed for embedding** · free with an open licence - Why: The export embeds a subset of the font in the SVG, which is a use the font's licence either allows or does not, and finding out afterwards is the wrong order. - Get it: Choose a font whose licence permits embedding and keep the licence file with the project. - Verify: The licence text permits embedding and the file is in the repository - [ ] **A map with several hundred nodes** · free - Why: Layout determinism and layout speed are both invisible on a map with twelve nodes, and both are things people abandon an editor over. - Get it: Generate one, or import a large outline. - Verify: Layout time is measured and recorded and two layouts are identical ## 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: - Collaboration, cursors and comments. - High-fidelity import from proprietary formats. - A presentation mode. - Cloud sync. - 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 SimpleMind, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · SimpleMind 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 mind map editor that keeps every map in a file you own and exports a picture that still has its text and its pictures in it to replace SimpleMind. 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. ### Rule zero Browser storage is not durable, and an editor that pretends otherwise will eventually lose somebody's map without asking anyone. The storage standard is explicit: a local storage bucket's mode starts as best-effort, and a user agent under storage pressure should clear best-effort buckets. That includes your IndexedDB database. So the word 'saved' cannot refer to IndexedDB alone. Request persistence, check what the browser actually granted, and show the answer in the interface rather than assuming a yes. Which means the archive is a file. Every map is one JSON document the person can save to disk, written back through the file handle they opened where the browser supports it and offered as a download where it does not. The application asks for a file on first use rather than after the first loss. IndexedDB is the working copy and the crash recovery, never the only copy. There is no account, no server and no sync here. If the browser clears its storage and there is no file on disk, the map is gone, and the interface has to be honest about that from day one. ### Stack (fixed, do not substitute) - Runtime: React with Vite, no backend at all, served from a static host or opened locally · a mind map does not need a server and adding one adds something that can be down - Working store: IndexedDB with persistence requested and the granted answer displayed · storage that can be evicted is a cache whatever the documentation calls it - Archive: One JSON file per map, written back through the file handle you opened · the file is the map and the browser is only where you edit it - Layout: A deterministic tidy tree computed from the document, with manual positions stored per node · a layout that shifts when you reopen a file is a layout nobody trusts - Export: SVG with fonts and images embedded as data, then PNG rasterised from that same SVG · an SVG inside an image element is not permitted to fetch anything external ### Data model (create this before Phase 1) - document(format_version, id, title, created_at, updated_at, root_id) · the envelope, versioned from the first release - nodes(id, parent_id, ordinal, text, note, collapsed, style_id, manual_x, manual_y) · manual_x and manual_y stay null until somebody drags that node - links(id, from_node, to_node, kind, label) · the cross connections that make this a map rather than an outline - styles(id, name, fill, stroke, text_colour, font_family, font_size) · referenced by id so restyling a branch is one write - assets(id, kind, mime, bytes_base64, sha256) · images live inside the document, because an image referenced by path is an image that goes missing - snapshots(id, at, sha256, bytes) · the last few versions that opened cleanly, kept separately from the working copy - The whole document is one JSON object that round-trips byte-identically through save and load. If it does not, you have a serialisation format rather than a file format, and the difference shows up the first time you diff two versions. ### Phase 1 · A document that is a file Build: Create or open a map, hold the file handle, and write back on save. Keep the working copy in IndexedDB for crash recovery. Call the persistence request on first use, read back what was granted, and show it. Show a dirty indicator that means unwritten to the file, not unwritten to the database. Trap: Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. Done when: The persistence request runs on first use and the granted or denied answer is visible in the interface, tested with the request stubbed to deny. The dirty indicator reflects unwritten file state, not database state, asserted by editing with the file handle revoked. A map opened, edited and saved round-trips byte-identically when nothing was changed, tested. Clearing IndexedDB in devtools and reopening the file restores the map completely. Do not build yet: No editing shortcuts, layout work, exports or import formats yet. ### Phase 2 · Editing at the speed of thinking Build: Enter adds a sibling, Tab adds a child, arrows move focus through the tree, typing edits the focused node and Escape commits. Drag to reparent with a visible drop target. Undo and redo group edits by node and by a short idle window rather than by keystroke. Trap: One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. Done when: A ten node map can be built without touching the mouse, tested by a scripted key sequence. Undo after typing a title removes the whole title in one step, tested. Undo after a reparent restores the previous parent and ordinal and the layout follows, tested. Focus is always visible and every action has a keyboard route, checked against a keyboard-only pass over the whole interface. Do not build yet: No layout determinism work, exports or import formats yet. ### Phase 3 · A layout that holds still Build: Compute a tidy tree layout as a pure function of the document, balancing branches around the root. Nodes with manual positions are pinned and everything else flows around them. Collapsing a branch reflows without moving anything outside that branch more than it has to. Trap: A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. Done when: Laying out the same document twice produces identical coordinates to the last decimal, asserted in a test. A node with manual coordinates stays exactly where it was placed after adding a sibling elsewhere, tested. Collapsing and expanding a branch returns every other node to its previous position, tested. Layout for a five hundred node map completes fast enough to feel immediate, timed and recorded in the README. Do not build yet: No export, import or recovery work yet. ### Phase 4 · Export that still contains its text Build: Serialise the canvas to SVG with the font subset embedded as a data URI and every image embedded as a data URI. Rasterise that same SVG to PNG at a chosen scale through a canvas. Also export a plain Markdown outline, which is the format that survives everything. Trap: An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. Done when: An exported PNG rendered from a map using a custom font matches the on-screen text metrics, checked by comparing rendered widths rather than by eye. An exported SVG opened on a machine without that font still shows the correct face. An embedded image appears in both the SVG and the PNG, tested. The Markdown outline export round-trips structure and text, and reimporting it rebuilds the tree. Do not build yet: No recovery snapshots, interchange import or multi-map handling yet. ### Phase 5 · Recovery, interchange and the honest limits Build: Keep the last few snapshots that opened cleanly, keyed by content hash, separate from the working copy. Import and export at least one interchange format. Handle several maps in one folder. State plainly what this build does not do that the paid application does. Trap: A single autosave slot gets overwritten with the corrupted state the moment after a crash, so the recovery feature hands you exactly the broken document you were trying to escape; keep the last few snapshots that parsed cleanly, never overwrite the most recent good one, and offer recovery as a list with timestamps rather than as one silent restore that you cannot decline. Done when: A corrupted working copy does not overwrite the most recent good snapshot, tested by writing invalid JSON into the working store. Recovery is offered as a dated list and can be declined, leaving the file untouched. An interchange import produces the same structure on export, tested by a round-trip through the other format. The README lists what is absent, including collaboration, presentation mode and high-fidelity import from the paid tools. Do not build yet: Nothing after this. ### Out of scope (and why) - Collaboration, cursors and comments, which need a server, a conflict model and an account system, and are most of what the paid products charge for. - High-fidelity import from proprietary mind map formats, which is reverse engineering somebody's binary layout rather than a weekend of work. - A presentation mode that walks the map as slides, which is a second product hiding inside the first. - Cloud sync, because there is no server here on purpose and adding one changes every answer about where the data lives. ### README must contain - That browser storage can be cleared by the browser, what persistence was requested and granted, and that the file on disk is the archive - That the word saved refers to bytes written to a file, and what the dirty indicator means - That the layout is deterministic and the test which asserts it - That exports embed fonts and images because an SVG used as an image cannot fetch anything external - What is deliberately absent, including collaboration, presentation mode and proprietary format import ===== AGENTS.md ===== # Agent instructions · SimpleMind indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: React with Vite, no backend at all, served from a static host or opened locally, IndexedDB with persistence requested and the granted answer displayed, One JSON file per map, written back through the file handle you opened, A deterministic tidy tree computed from the document, with manual positions stored per node, SVG with fonts and images embedded as data, then PNG rasterised from that same SVG. 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 - Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. - One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. - A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. - An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. ===== BUILD_PLAN.md ===== # Build plan · SimpleMind A local mind map editor whose hard problems are refusing to treat evictable browser storage as an archive, keeping the layout identical every time the same file is opened, and exporting a picture that still contains its fonts and images once the browser rasterises it. 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 document that is a file Saving means a file on disk, and the interface says what the browser promised about the working copy. ### Steps 1. Request persistence and show what came back The interesting case is the browser saying no. Files: `src/storage/persist.ts`, `tests/persist.test.ts` 2. Hold the file handle and write back to it Falling back to a download where the browser has no handle. Files: `src/storage/file.ts` 3. Make the dirty indicator mean the file, not the cache That is the only meaning that protects anybody. Files: `src/ui/DirtyDot.tsx` ### Done when - [ ] The granted persistence answer is shown - [ ] Dirty means unwritten to the file - [ ] An unchanged map round-trips byte-identically - [ ] Clearing the database loses nothing that was saved ### Watch out - Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. ## Phase 2 · Editing at the speed of thinking A map can be built entirely from the keyboard and undo removes what a person would call one action. ### Steps 1. Group undo by node and idle window One action to a person is one entry in the stack. Files: `src/history/group.ts`, `tests/undo-grouping.test.ts` 2. Give every action a keyboard route This is a thinking tool, and the mouse is the slow path. Files: `src/keys.ts` 3. Make reparenting undo restore ordinal too Restoring the parent but not the position is a half undo. Files: `src/history/inverse.ts` ### Done when - [ ] A ten node map is built without the mouse - [ ] Undo removes a typed title in one step - [ ] Reparent undo restores parent and ordinal - [ ] Focus is visible and every action is reachable ### Watch out - One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. ## Phase 3 · A layout that holds still Opening the same file twice produces pixel-identical geometry. ### Steps 1. Make the layout pure, with an explicit child order No clock, no random, no map iteration order. Files: `src/layout/tidy.ts`, `tests/layout-determinism.test.ts` 2. Pin manual positions and flow around them The whole point of dragging a node is that it stays dragged. Files: `src/layout/pins.ts` 3. Time it on a large map and write the number down Five hundred nodes is a real map, not a stress test. Files: `tests/layout-perf.test.ts`, `README.md` ### Done when - [ ] Two layouts of one document are identical - [ ] Manual positions survive edits elsewhere - [ ] Collapse and expand restores positions - [ ] Large map layout time is measured and recorded ### Watch out - A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. ## Phase 4 · Export that still contains its text The exported SVG and PNG look like the screen, including fonts and embedded images. ### Steps 1. Embed the font subset and every image as data External references are not fetched when the SVG is an image. Files: `src/export/svg.ts`, `tests/export-font.test.ts` 2. Rasterise the same SVG rather than redrawing Two renderers is two sets of differences. Files: `src/export/png.ts` 3. Compare the export to the screen in a test Text width is the measurement that catches a missing font. Files: `tests/export-parity.test.ts` ### Done when - [ ] PNG text metrics match the screen - [ ] SVG renders correctly without the font installed - [ ] Embedded images appear in both exports - [ ] The Markdown outline round-trips ### Watch out - An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. ## Not in this build - Collaboration, cursors and comments. - High-fidelity import from proprietary formats. - A presentation mode. - Cloud sync. ## After v1, if you want it - A folder view across many maps with search over node text, which is where a personal map collection starts being useful - Per-branch styling presets, once you have found out which three styles you actually use - A print layout that paginates a large map sensibly, which is a genuinely hard problem and worth its own weekend ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Shown in the tab and in the export metadata. No secrets exist in this build. APP_TITLE=Maps # Required. How many clean snapshots to retain for recovery. They are small. SNAPSHOT_KEEP=5 # Required. Idle time before the working copy is written and undo entries are grouped. AUTOSAVE_IDLE_MS=800 # Required. Rasterisation scale for PNG export. Two is the sensible default. EXPORT_PNG_SCALE=2 # Required. Largest image that can be embedded in a document, since the file carries it. MAX_ASSET_BYTES=2000000
You are building a production product version of SimpleMind. 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 ===== # SimpleMind · product brief ## Problem SimpleMind's solo core is compact: build a browser-based mind mapping editor with drag, resize, connect, keyboard shortcuts, autosave, and SVG or PNG export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on interaction polish, collaboration. ## Product outcome A mind map editor where the file on disk is the archive and the browser is admitted to be a cache, where the same document always lays out identically, and where the exported picture still has its fonts and its images in it. ## 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 - Collaboration, cursors and comments. - High-fidelity import from proprietary formats. - A presentation mode. - Cloud sync. - high-fidelity import and export - presentation and enterprise controls - multiplayer cursors and comments - large template libraries ## Success criteria - The granted persistence answer is confirmed visible in the interface - An unchanged map is confirmed to round-trip byte-identically - Two layouts of one document are confirmed identical - Exported PNG text metrics are confirmed to match the screen - A corrupt working copy is confirmed not to overwrite a good snapshot ===== BRIEF.md ===== # Build brief · SimpleMind 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 mind map editor that keeps every map in a file you own and exports a picture that still has its text and its pictures in it to replace SimpleMind. 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. ### Rule zero Browser storage is not durable, and an editor that pretends otherwise will eventually lose somebody's map without asking anyone. The storage standard is explicit: a local storage bucket's mode starts as best-effort, and a user agent under storage pressure should clear best-effort buckets. That includes your IndexedDB database. So the word 'saved' cannot refer to IndexedDB alone. Request persistence, check what the browser actually granted, and show the answer in the interface rather than assuming a yes. Which means the archive is a file. Every map is one JSON document the person can save to disk, written back through the file handle they opened where the browser supports it and offered as a download where it does not. The application asks for a file on first use rather than after the first loss. IndexedDB is the working copy and the crash recovery, never the only copy. There is no account, no server and no sync here. If the browser clears its storage and there is no file on disk, the map is gone, and the interface has to be honest about that from day one. ### Stack (fixed, do not substitute) - Runtime: React with Vite, no backend at all, served from a static host or opened locally · a mind map does not need a server and adding one adds something that can be down - Working store: IndexedDB with persistence requested and the granted answer displayed · storage that can be evicted is a cache whatever the documentation calls it - Archive: One JSON file per map, written back through the file handle you opened · the file is the map and the browser is only where you edit it - Layout: A deterministic tidy tree computed from the document, with manual positions stored per node · a layout that shifts when you reopen a file is a layout nobody trusts - Export: SVG with fonts and images embedded as data, then PNG rasterised from that same SVG · an SVG inside an image element is not permitted to fetch anything external ### Data model (create this before Phase 1) - document(format_version, id, title, created_at, updated_at, root_id) · the envelope, versioned from the first release - nodes(id, parent_id, ordinal, text, note, collapsed, style_id, manual_x, manual_y) · manual_x and manual_y stay null until somebody drags that node - links(id, from_node, to_node, kind, label) · the cross connections that make this a map rather than an outline - styles(id, name, fill, stroke, text_colour, font_family, font_size) · referenced by id so restyling a branch is one write - assets(id, kind, mime, bytes_base64, sha256) · images live inside the document, because an image referenced by path is an image that goes missing - snapshots(id, at, sha256, bytes) · the last few versions that opened cleanly, kept separately from the working copy - The whole document is one JSON object that round-trips byte-identically through save and load. If it does not, you have a serialisation format rather than a file format, and the difference shows up the first time you diff two versions. ### Phase 1 · A document that is a file Build: Create or open a map, hold the file handle, and write back on save. Keep the working copy in IndexedDB for crash recovery. Call the persistence request on first use, read back what was granted, and show it. Show a dirty indicator that means unwritten to the file, not unwritten to the database. Trap: Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. Done when: The persistence request runs on first use and the granted or denied answer is visible in the interface, tested with the request stubbed to deny. The dirty indicator reflects unwritten file state, not database state, asserted by editing with the file handle revoked. A map opened, edited and saved round-trips byte-identically when nothing was changed, tested. Clearing IndexedDB in devtools and reopening the file restores the map completely. Do not build yet: No editing shortcuts, layout work, exports or import formats yet. ### Phase 2 · Editing at the speed of thinking Build: Enter adds a sibling, Tab adds a child, arrows move focus through the tree, typing edits the focused node and Escape commits. Drag to reparent with a visible drop target. Undo and redo group edits by node and by a short idle window rather than by keystroke. Trap: One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. Done when: A ten node map can be built without touching the mouse, tested by a scripted key sequence. Undo after typing a title removes the whole title in one step, tested. Undo after a reparent restores the previous parent and ordinal and the layout follows, tested. Focus is always visible and every action has a keyboard route, checked against a keyboard-only pass over the whole interface. Do not build yet: No layout determinism work, exports or import formats yet. ### Phase 3 · A layout that holds still Build: Compute a tidy tree layout as a pure function of the document, balancing branches around the root. Nodes with manual positions are pinned and everything else flows around them. Collapsing a branch reflows without moving anything outside that branch more than it has to. Trap: A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. Done when: Laying out the same document twice produces identical coordinates to the last decimal, asserted in a test. A node with manual coordinates stays exactly where it was placed after adding a sibling elsewhere, tested. Collapsing and expanding a branch returns every other node to its previous position, tested. Layout for a five hundred node map completes fast enough to feel immediate, timed and recorded in the README. Do not build yet: No export, import or recovery work yet. ### Phase 4 · Export that still contains its text Build: Serialise the canvas to SVG with the font subset embedded as a data URI and every image embedded as a data URI. Rasterise that same SVG to PNG at a chosen scale through a canvas. Also export a plain Markdown outline, which is the format that survives everything. Trap: An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. Done when: An exported PNG rendered from a map using a custom font matches the on-screen text metrics, checked by comparing rendered widths rather than by eye. An exported SVG opened on a machine without that font still shows the correct face. An embedded image appears in both the SVG and the PNG, tested. The Markdown outline export round-trips structure and text, and reimporting it rebuilds the tree. Do not build yet: No recovery snapshots, interchange import or multi-map handling yet. ### Phase 5 · Recovery, interchange and the honest limits Build: Keep the last few snapshots that opened cleanly, keyed by content hash, separate from the working copy. Import and export at least one interchange format. Handle several maps in one folder. State plainly what this build does not do that the paid application does. Trap: A single autosave slot gets overwritten with the corrupted state the moment after a crash, so the recovery feature hands you exactly the broken document you were trying to escape; keep the last few snapshots that parsed cleanly, never overwrite the most recent good one, and offer recovery as a list with timestamps rather than as one silent restore that you cannot decline. Done when: A corrupted working copy does not overwrite the most recent good snapshot, tested by writing invalid JSON into the working store. Recovery is offered as a dated list and can be declined, leaving the file untouched. An interchange import produces the same structure on export, tested by a round-trip through the other format. The README lists what is absent, including collaboration, presentation mode and high-fidelity import from the paid tools. Do not build yet: Nothing after this. ### Out of scope (and why) - Collaboration, cursors and comments, which need a server, a conflict model and an account system, and are most of what the paid products charge for. - High-fidelity import from proprietary mind map formats, which is reverse engineering somebody's binary layout rather than a weekend of work. - A presentation mode that walks the map as slides, which is a second product hiding inside the first. - Cloud sync, because there is no server here on purpose and adding one changes every answer about where the data lives. ### README must contain - That browser storage can be cleared by the browser, what persistence was requested and granted, and that the file on disk is the archive - That the word saved refers to bytes written to a file, and what the dirty indicator means - That the layout is deterministic and the test which asserts it - That exports embed fonts and images because an SVG used as an image cannot fetch anything external - What is deliberately absent, including collaboration, presentation mode and proprietary format import ===== ARCHITECTURE.md ===== # Architecture · SimpleMind ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with Vite, no backend at all, served from a static host or opened locally | a mind map does not need a server and adding one adds something that can be down | | Working store | IndexedDB with persistence requested and the granted answer displayed | storage that can be evicted is a cache whatever the documentation calls it | | Archive | One JSON file per map, written back through the file handle you opened | the file is the map and the browser is only where you edit it | | Layout | A deterministic tidy tree computed from the document, with manual positions stored per node | a layout that shifts when you reopen a file is a layout nobody trusts | | Export | SVG with fonts and images embedded as data, then PNG rasterised from that same SVG | an SVG inside an image element is not permitted to fetch anything external | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Document | The JSON envelope, versioning, round-trip fidelity. | Any schema, provided it round-trips byte-identically. | | Storage | File handles, the working copy, persistence requests, snapshots. | Any store, provided a file is still the archive. | | Editing | Keyboard model, undo grouping, structural edits. | Any interaction layer, provided undo matches human actions. | | Layout | The pure tidy tree and manual pins. | Any algorithm, provided it is a pure function of the document. | | Export | SVG serialisation, embedding, rasterisation, outline. | Any exporter, provided nothing external is referenced. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `APP_TITLE` · required · Shown in the tab and in the export metadata. No secrets exist in this build. - `SNAPSHOT_KEEP` · required · How many clean snapshots to retain for recovery. They are small. - `AUTOSAVE_IDLE_MS` · required · Idle time before the working copy is written and undo entries are grouped. - `EXPORT_PNG_SCALE` · required · Rasterisation scale for PNG export. Two is the sensible default. - `MAX_ASSET_BYTES` · required · Largest image that can be embedded in a document, since the file carries it. ## 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 · SimpleMind product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: React with Vite, no backend at all, served from a static host or opened locally, IndexedDB with persistence requested and the granted answer displayed, One JSON file per map, written back through the file handle you opened, A deterministic tidy tree computed from the document, with manual positions stored per node, SVG with fonts and images embedded as data, then PNG rasterised from that same SVG. - 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 - Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. - One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. - A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. - An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. - A single autosave slot gets overwritten with the corrupted state the moment after a crash, so the recovery feature hands you exactly the broken document you were trying to escape; keep the last few snapshots that parsed cleanly, never overwrite the most recent good one, and offer recovery as a list with timestamps rather than as one silent restore that you cannot decline. ===== MILESTONES.md ===== # Delivery milestones · SimpleMind Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A document that is a file Saving means a file on disk, and the interface says what the browser promised about the working copy. ### Steps 1. Request persistence and show what came back The interesting case is the browser saying no. Files: `src/storage/persist.ts`, `tests/persist.test.ts` 2. Hold the file handle and write back to it Falling back to a download where the browser has no handle. Files: `src/storage/file.ts` 3. Make the dirty indicator mean the file, not the cache That is the only meaning that protects anybody. Files: `src/ui/DirtyDot.tsx` ### Done when - [ ] The granted persistence answer is shown - [ ] Dirty means unwritten to the file - [ ] An unchanged map round-trips byte-identically - [ ] Clearing the database loses nothing that was saved ### Watch out - Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. ## M2 · Editing at the speed of thinking A map can be built entirely from the keyboard and undo removes what a person would call one action. ### Steps 1. Group undo by node and idle window One action to a person is one entry in the stack. Files: `src/history/group.ts`, `tests/undo-grouping.test.ts` 2. Give every action a keyboard route This is a thinking tool, and the mouse is the slow path. Files: `src/keys.ts` 3. Make reparenting undo restore ordinal too Restoring the parent but not the position is a half undo. Files: `src/history/inverse.ts` ### Done when - [ ] A ten node map is built without the mouse - [ ] Undo removes a typed title in one step - [ ] Reparent undo restores parent and ordinal - [ ] Focus is visible and every action is reachable ### Watch out - One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. ## M3 · A layout that holds still Opening the same file twice produces pixel-identical geometry. ### Steps 1. Make the layout pure, with an explicit child order No clock, no random, no map iteration order. Files: `src/layout/tidy.ts`, `tests/layout-determinism.test.ts` 2. Pin manual positions and flow around them The whole point of dragging a node is that it stays dragged. Files: `src/layout/pins.ts` 3. Time it on a large map and write the number down Five hundred nodes is a real map, not a stress test. Files: `tests/layout-perf.test.ts`, `README.md` ### Done when - [ ] Two layouts of one document are identical - [ ] Manual positions survive edits elsewhere - [ ] Collapse and expand restores positions - [ ] Large map layout time is measured and recorded ### Watch out - A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. ## M4 · Export that still contains its text The exported SVG and PNG look like the screen, including fonts and embedded images. ### Steps 1. Embed the font subset and every image as data External references are not fetched when the SVG is an image. Files: `src/export/svg.ts`, `tests/export-font.test.ts` 2. Rasterise the same SVG rather than redrawing Two renderers is two sets of differences. Files: `src/export/png.ts` 3. Compare the export to the screen in a test Text width is the measurement that catches a missing font. Files: `tests/export-parity.test.ts` ### Done when - [ ] PNG text metrics match the screen - [ ] SVG renders correctly without the font installed - [ ] Embedded images appear in both exports - [ ] The Markdown outline round-trips ### Watch out - An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. ## M5 · Recovery, interchange and the honest limits (production only) A crash costs nothing, a map can leave for another tool, and the README says what is not here. ### Steps 1. Snapshot only documents that parsed cleanly A snapshot of a broken document is not a backup. Files: `src/storage/snapshots.ts`, `tests/recovery.test.ts` 2. Offer recovery as a choice, never as a silent restore The person knows something you do not. Files: `src/ui/Recovery.tsx` 3. Round-trip one interchange format One that works beats three that nearly work. Files: `src/io/interchange.ts`, `tests/interchange.test.ts` ### Done when - [ ] A corrupt working copy never overwrites a good snapshot - [ ] Recovery is a dated list and can be declined - [ ] Interchange import and export round-trip - [ ] The README lists what is deliberately absent ### Watch out - A single autosave slot gets overwritten with the corrupted state the moment after a crash, so the recovery feature hands you exactly the broken document you were trying to escape; keep the last few snapshots that parsed cleanly, never overwrite the most recent good one, and offer recovery as a list with timestamps rather than as one silent restore that you cannot decline. ===== OPERATIONS.md ===== # Operations · SimpleMind ## Backup The files. That is the point of the design: back up the folder of JSON documents the same way you back up anything else on that machine, and the browser's copy becomes irrelevant. ## Restore Open the file. There is nothing else to restore, and a test that opens each backed-up document in a fresh browser profile is worth running once so you know the format really is self-contained. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local application this lives in the test suite: persistence granted or denied, layout determinism, export parity with the screen, and the round-trip test on every document format change. ## Incident checklist The incident here is a lost map, and it happens through evicted storage or a document that no longer parses. Snapshots cover the second, files cover the first, and the interface saying what the browser granted is what stops a person relying on the wrong one. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The granted persistence answer is confirmed visible in the interface - [ ] An unchanged map is confirmed to round-trip byte-identically - [ ] Two layouts of one document are confirmed identical - [ ] Exported PNG text metrics are confirmed to match the screen - [ ] A corrupt working copy is confirmed not to overwrite a good snapshot ## Launch constraint Do not market omitted SimpleMind 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. Shown in the tab and in the export metadata. No secrets exist in this build. APP_TITLE=Maps # Required. How many clean snapshots to retain for recovery. They are small. SNAPSHOT_KEEP=5 # Required. Idle time before the working copy is written and undo entries are grouped. AUTOSAVE_IDLE_MS=800 # Required. Rasterisation scale for PNG export. Two is the sensible default. EXPORT_PNG_SCALE=2 # Required. Largest image that can be embedded in a document, since the file carries it. MAX_ASSET_BYTES=2000000
# SimpleMind · indie build A local mind map editor whose hard problems are refusing to treat evictable browser storage as an archive, keeping the layout identical every time the same file is opened, and exporting a picture that still contains its fonts and images once the browser rasterises it. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with Vite, no backend at all, served from a static host or opened locally | a mind map does not need a server and adding one adds something that can be down | | Working store | IndexedDB with persistence requested and the granted answer displayed | storage that can be evicted is a cache whatever the documentation calls it | | Archive | One JSON file per map, written back through the file handle you opened | the file is the map and the browser is only where you edit it | | Layout | A deterministic tidy tree computed from the document, with manual positions stored per node | a layout that shifts when you reopen a file is a layout nobody trusts | | Export | SVG with fonts and images embedded as data, then PNG rasterised from that same SVG | an SVG inside an image element is not permitted to fetch anything external | ## 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; nothing runs server-side once it is built. - Get it: nodejs.org or a version manager. - Verify: node --version prints v22 or higher - [ ] **A browser with a file handle API and a storage persistence API** · free - Why: The archive design depends on writing back to a file the person chose, and the honesty design depends on being able to ask whether storage is persistent. - Get it: A current desktop browser; check the support tables for both APIs before committing. - Verify: Opening a file, editing and saving writes back to the same file without a download - [ ] **A font you intend to use, licensed for embedding** · free with an open licence - Why: The export embeds a subset of the font in the SVG, which is a use the font's licence either allows or does not, and finding out afterwards is the wrong order. - Get it: Choose a font whose licence permits embedding and keep the licence file with the project. - Verify: The licence text permits embedding and the file is in the repository - [ ] **A map with several hundred nodes** · free - Why: Layout determinism and layout speed are both invisible on a map with twelve nodes, and both are things people abandon an editor over. - Get it: Generate one, or import a large outline. - Verify: Layout time is measured and recorded and two layouts are identical ## 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: - Collaboration, cursors and comments. - High-fidelity import from proprietary formats. - A presentation mode. - Cloud sync. - 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 SimpleMind, and the README should say so rather than pretend.
# Build brief · SimpleMind 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 mind map editor that keeps every map in a file you own and exports a picture that still has its text and its pictures in it to replace SimpleMind. 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. ### Rule zero Browser storage is not durable, and an editor that pretends otherwise will eventually lose somebody's map without asking anyone. The storage standard is explicit: a local storage bucket's mode starts as best-effort, and a user agent under storage pressure should clear best-effort buckets. That includes your IndexedDB database. So the word 'saved' cannot refer to IndexedDB alone. Request persistence, check what the browser actually granted, and show the answer in the interface rather than assuming a yes. Which means the archive is a file. Every map is one JSON document the person can save to disk, written back through the file handle they opened where the browser supports it and offered as a download where it does not. The application asks for a file on first use rather than after the first loss. IndexedDB is the working copy and the crash recovery, never the only copy. There is no account, no server and no sync here. If the browser clears its storage and there is no file on disk, the map is gone, and the interface has to be honest about that from day one. ### Stack (fixed, do not substitute) - Runtime: React with Vite, no backend at all, served from a static host or opened locally · a mind map does not need a server and adding one adds something that can be down - Working store: IndexedDB with persistence requested and the granted answer displayed · storage that can be evicted is a cache whatever the documentation calls it - Archive: One JSON file per map, written back through the file handle you opened · the file is the map and the browser is only where you edit it - Layout: A deterministic tidy tree computed from the document, with manual positions stored per node · a layout that shifts when you reopen a file is a layout nobody trusts - Export: SVG with fonts and images embedded as data, then PNG rasterised from that same SVG · an SVG inside an image element is not permitted to fetch anything external ### Data model (create this before Phase 1) - document(format_version, id, title, created_at, updated_at, root_id) · the envelope, versioned from the first release - nodes(id, parent_id, ordinal, text, note, collapsed, style_id, manual_x, manual_y) · manual_x and manual_y stay null until somebody drags that node - links(id, from_node, to_node, kind, label) · the cross connections that make this a map rather than an outline - styles(id, name, fill, stroke, text_colour, font_family, font_size) · referenced by id so restyling a branch is one write - assets(id, kind, mime, bytes_base64, sha256) · images live inside the document, because an image referenced by path is an image that goes missing - snapshots(id, at, sha256, bytes) · the last few versions that opened cleanly, kept separately from the working copy - The whole document is one JSON object that round-trips byte-identically through save and load. If it does not, you have a serialisation format rather than a file format, and the difference shows up the first time you diff two versions. ### Phase 1 · A document that is a file Build: Create or open a map, hold the file handle, and write back on save. Keep the working copy in IndexedDB for crash recovery. Call the persistence request on first use, read back what was granted, and show it. Show a dirty indicator that means unwritten to the file, not unwritten to the database. Trap: Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. Done when: The persistence request runs on first use and the granted or denied answer is visible in the interface, tested with the request stubbed to deny. The dirty indicator reflects unwritten file state, not database state, asserted by editing with the file handle revoked. A map opened, edited and saved round-trips byte-identically when nothing was changed, tested. Clearing IndexedDB in devtools and reopening the file restores the map completely. Do not build yet: No editing shortcuts, layout work, exports or import formats yet. ### Phase 2 · Editing at the speed of thinking Build: Enter adds a sibling, Tab adds a child, arrows move focus through the tree, typing edits the focused node and Escape commits. Drag to reparent with a visible drop target. Undo and redo group edits by node and by a short idle window rather than by keystroke. Trap: One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. Done when: A ten node map can be built without touching the mouse, tested by a scripted key sequence. Undo after typing a title removes the whole title in one step, tested. Undo after a reparent restores the previous parent and ordinal and the layout follows, tested. Focus is always visible and every action has a keyboard route, checked against a keyboard-only pass over the whole interface. Do not build yet: No layout determinism work, exports or import formats yet. ### Phase 3 · A layout that holds still Build: Compute a tidy tree layout as a pure function of the document, balancing branches around the root. Nodes with manual positions are pinned and everything else flows around them. Collapsing a branch reflows without moving anything outside that branch more than it has to. Trap: A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. Done when: Laying out the same document twice produces identical coordinates to the last decimal, asserted in a test. A node with manual coordinates stays exactly where it was placed after adding a sibling elsewhere, tested. Collapsing and expanding a branch returns every other node to its previous position, tested. Layout for a five hundred node map completes fast enough to feel immediate, timed and recorded in the README. Do not build yet: No export, import or recovery work yet. ### Phase 4 · Export that still contains its text Build: Serialise the canvas to SVG with the font subset embedded as a data URI and every image embedded as a data URI. Rasterise that same SVG to PNG at a chosen scale through a canvas. Also export a plain Markdown outline, which is the format that survives everything. Trap: An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. Done when: An exported PNG rendered from a map using a custom font matches the on-screen text metrics, checked by comparing rendered widths rather than by eye. An exported SVG opened on a machine without that font still shows the correct face. An embedded image appears in both the SVG and the PNG, tested. The Markdown outline export round-trips structure and text, and reimporting it rebuilds the tree. Do not build yet: No recovery snapshots, interchange import or multi-map handling yet. ### Phase 5 · Recovery, interchange and the honest limits Build: Keep the last few snapshots that opened cleanly, keyed by content hash, separate from the working copy. Import and export at least one interchange format. Handle several maps in one folder. State plainly what this build does not do that the paid application does. Trap: A single autosave slot gets overwritten with the corrupted state the moment after a crash, so the recovery feature hands you exactly the broken document you were trying to escape; keep the last few snapshots that parsed cleanly, never overwrite the most recent good one, and offer recovery as a list with timestamps rather than as one silent restore that you cannot decline. Done when: A corrupted working copy does not overwrite the most recent good snapshot, tested by writing invalid JSON into the working store. Recovery is offered as a dated list and can be declined, leaving the file untouched. An interchange import produces the same structure on export, tested by a round-trip through the other format. The README lists what is absent, including collaboration, presentation mode and high-fidelity import from the paid tools. Do not build yet: Nothing after this. ### Out of scope (and why) - Collaboration, cursors and comments, which need a server, a conflict model and an account system, and are most of what the paid products charge for. - High-fidelity import from proprietary mind map formats, which is reverse engineering somebody's binary layout rather than a weekend of work. - A presentation mode that walks the map as slides, which is a second product hiding inside the first. - Cloud sync, because there is no server here on purpose and adding one changes every answer about where the data lives. ### README must contain - That browser storage can be cleared by the browser, what persistence was requested and granted, and that the file on disk is the archive - That the word saved refers to bytes written to a file, and what the dirty indicator means - That the layout is deterministic and the test which asserts it - That exports embed fonts and images because an SVG used as an image cannot fetch anything external - What is deliberately absent, including collaboration, presentation mode and proprietary format import
# Agent instructions · SimpleMind indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: React with Vite, no backend at all, served from a static host or opened locally, IndexedDB with persistence requested and the granted answer displayed, One JSON file per map, written back through the file handle you opened, A deterministic tidy tree computed from the document, with manual positions stored per node, SVG with fonts and images embedded as data, then PNG rasterised from that same SVG. 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 - Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. - One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. - A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. - An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked.
# Build plan · SimpleMind A local mind map editor whose hard problems are refusing to treat evictable browser storage as an archive, keeping the layout identical every time the same file is opened, and exporting a picture that still contains its fonts and images once the browser rasterises it. 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 document that is a file Saving means a file on disk, and the interface says what the browser promised about the working copy. ### Steps 1. Request persistence and show what came back The interesting case is the browser saying no. Files: `src/storage/persist.ts`, `tests/persist.test.ts` 2. Hold the file handle and write back to it Falling back to a download where the browser has no handle. Files: `src/storage/file.ts` 3. Make the dirty indicator mean the file, not the cache That is the only meaning that protects anybody. Files: `src/ui/DirtyDot.tsx` ### Done when - [ ] The granted persistence answer is shown - [ ] Dirty means unwritten to the file - [ ] An unchanged map round-trips byte-identically - [ ] Clearing the database loses nothing that was saved ### Watch out - Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. ## Phase 2 · Editing at the speed of thinking A map can be built entirely from the keyboard and undo removes what a person would call one action. ### Steps 1. Group undo by node and idle window One action to a person is one entry in the stack. Files: `src/history/group.ts`, `tests/undo-grouping.test.ts` 2. Give every action a keyboard route This is a thinking tool, and the mouse is the slow path. Files: `src/keys.ts` 3. Make reparenting undo restore ordinal too Restoring the parent but not the position is a half undo. Files: `src/history/inverse.ts` ### Done when - [ ] A ten node map is built without the mouse - [ ] Undo removes a typed title in one step - [ ] Reparent undo restores parent and ordinal - [ ] Focus is visible and every action is reachable ### Watch out - One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. ## Phase 3 · A layout that holds still Opening the same file twice produces pixel-identical geometry. ### Steps 1. Make the layout pure, with an explicit child order No clock, no random, no map iteration order. Files: `src/layout/tidy.ts`, `tests/layout-determinism.test.ts` 2. Pin manual positions and flow around them The whole point of dragging a node is that it stays dragged. Files: `src/layout/pins.ts` 3. Time it on a large map and write the number down Five hundred nodes is a real map, not a stress test. Files: `tests/layout-perf.test.ts`, `README.md` ### Done when - [ ] Two layouts of one document are identical - [ ] Manual positions survive edits elsewhere - [ ] Collapse and expand restores positions - [ ] Large map layout time is measured and recorded ### Watch out - A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. ## Phase 4 · Export that still contains its text The exported SVG and PNG look like the screen, including fonts and embedded images. ### Steps 1. Embed the font subset and every image as data External references are not fetched when the SVG is an image. Files: `src/export/svg.ts`, `tests/export-font.test.ts` 2. Rasterise the same SVG rather than redrawing Two renderers is two sets of differences. Files: `src/export/png.ts` 3. Compare the export to the screen in a test Text width is the measurement that catches a missing font. Files: `tests/export-parity.test.ts` ### Done when - [ ] PNG text metrics match the screen - [ ] SVG renders correctly without the font installed - [ ] Embedded images appear in both exports - [ ] The Markdown outline round-trips ### Watch out - An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. ## Not in this build - Collaboration, cursors and comments. - High-fidelity import from proprietary formats. - A presentation mode. - Cloud sync. ## After v1, if you want it - A folder view across many maps with search over node text, which is where a personal map collection starts being useful - Per-branch styling presets, once you have found out which three styles you actually use - A print layout that paginates a large map sensibly, which is a genuinely hard problem and worth its own weekend
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Shown in the tab and in the export metadata. No secrets exist in this build. APP_TITLE=Maps # Required. How many clean snapshots to retain for recovery. They are small. SNAPSHOT_KEEP=5 # Required. Idle time before the working copy is written and undo entries are grouped. AUTOSAVE_IDLE_MS=800 # Required. Rasterisation scale for PNG export. Two is the sensible default. EXPORT_PNG_SCALE=2 # Required. Largest image that can be embedded in a document, since the file carries it. MAX_ASSET_BYTES=2000000
# SimpleMind · product brief ## Problem SimpleMind's solo core is compact: build a browser-based mind mapping editor with drag, resize, connect, keyboard shortcuts, autosave, and SVG or PNG export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on interaction polish, collaboration. ## Product outcome A mind map editor where the file on disk is the archive and the browser is admitted to be a cache, where the same document always lays out identically, and where the exported picture still has its fonts and its images in it. ## 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 - Collaboration, cursors and comments. - High-fidelity import from proprietary formats. - A presentation mode. - Cloud sync. - high-fidelity import and export - presentation and enterprise controls - multiplayer cursors and comments - large template libraries ## Success criteria - The granted persistence answer is confirmed visible in the interface - An unchanged map is confirmed to round-trip byte-identically - Two layouts of one document are confirmed identical - Exported PNG text metrics are confirmed to match the screen - A corrupt working copy is confirmed not to overwrite a good snapshot
# Build brief · SimpleMind 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 mind map editor that keeps every map in a file you own and exports a picture that still has its text and its pictures in it to replace SimpleMind. 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. ### Rule zero Browser storage is not durable, and an editor that pretends otherwise will eventually lose somebody's map without asking anyone. The storage standard is explicit: a local storage bucket's mode starts as best-effort, and a user agent under storage pressure should clear best-effort buckets. That includes your IndexedDB database. So the word 'saved' cannot refer to IndexedDB alone. Request persistence, check what the browser actually granted, and show the answer in the interface rather than assuming a yes. Which means the archive is a file. Every map is one JSON document the person can save to disk, written back through the file handle they opened where the browser supports it and offered as a download where it does not. The application asks for a file on first use rather than after the first loss. IndexedDB is the working copy and the crash recovery, never the only copy. There is no account, no server and no sync here. If the browser clears its storage and there is no file on disk, the map is gone, and the interface has to be honest about that from day one. ### Stack (fixed, do not substitute) - Runtime: React with Vite, no backend at all, served from a static host or opened locally · a mind map does not need a server and adding one adds something that can be down - Working store: IndexedDB with persistence requested and the granted answer displayed · storage that can be evicted is a cache whatever the documentation calls it - Archive: One JSON file per map, written back through the file handle you opened · the file is the map and the browser is only where you edit it - Layout: A deterministic tidy tree computed from the document, with manual positions stored per node · a layout that shifts when you reopen a file is a layout nobody trusts - Export: SVG with fonts and images embedded as data, then PNG rasterised from that same SVG · an SVG inside an image element is not permitted to fetch anything external ### Data model (create this before Phase 1) - document(format_version, id, title, created_at, updated_at, root_id) · the envelope, versioned from the first release - nodes(id, parent_id, ordinal, text, note, collapsed, style_id, manual_x, manual_y) · manual_x and manual_y stay null until somebody drags that node - links(id, from_node, to_node, kind, label) · the cross connections that make this a map rather than an outline - styles(id, name, fill, stroke, text_colour, font_family, font_size) · referenced by id so restyling a branch is one write - assets(id, kind, mime, bytes_base64, sha256) · images live inside the document, because an image referenced by path is an image that goes missing - snapshots(id, at, sha256, bytes) · the last few versions that opened cleanly, kept separately from the working copy - The whole document is one JSON object that round-trips byte-identically through save and load. If it does not, you have a serialisation format rather than a file format, and the difference shows up the first time you diff two versions. ### Phase 1 · A document that is a file Build: Create or open a map, hold the file handle, and write back on save. Keep the working copy in IndexedDB for crash recovery. Call the persistence request on first use, read back what was granted, and show it. Show a dirty indicator that means unwritten to the file, not unwritten to the database. Trap: Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. Done when: The persistence request runs on first use and the granted or denied answer is visible in the interface, tested with the request stubbed to deny. The dirty indicator reflects unwritten file state, not database state, asserted by editing with the file handle revoked. A map opened, edited and saved round-trips byte-identically when nothing was changed, tested. Clearing IndexedDB in devtools and reopening the file restores the map completely. Do not build yet: No editing shortcuts, layout work, exports or import formats yet. ### Phase 2 · Editing at the speed of thinking Build: Enter adds a sibling, Tab adds a child, arrows move focus through the tree, typing edits the focused node and Escape commits. Drag to reparent with a visible drop target. Undo and redo group edits by node and by a short idle window rather than by keystroke. Trap: One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. Done when: A ten node map can be built without touching the mouse, tested by a scripted key sequence. Undo after typing a title removes the whole title in one step, tested. Undo after a reparent restores the previous parent and ordinal and the layout follows, tested. Focus is always visible and every action has a keyboard route, checked against a keyboard-only pass over the whole interface. Do not build yet: No layout determinism work, exports or import formats yet. ### Phase 3 · A layout that holds still Build: Compute a tidy tree layout as a pure function of the document, balancing branches around the root. Nodes with manual positions are pinned and everything else flows around them. Collapsing a branch reflows without moving anything outside that branch more than it has to. Trap: A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. Done when: Laying out the same document twice produces identical coordinates to the last decimal, asserted in a test. A node with manual coordinates stays exactly where it was placed after adding a sibling elsewhere, tested. Collapsing and expanding a branch returns every other node to its previous position, tested. Layout for a five hundred node map completes fast enough to feel immediate, timed and recorded in the README. Do not build yet: No export, import or recovery work yet. ### Phase 4 · Export that still contains its text Build: Serialise the canvas to SVG with the font subset embedded as a data URI and every image embedded as a data URI. Rasterise that same SVG to PNG at a chosen scale through a canvas. Also export a plain Markdown outline, which is the format that survives everything. Trap: An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. Done when: An exported PNG rendered from a map using a custom font matches the on-screen text metrics, checked by comparing rendered widths rather than by eye. An exported SVG opened on a machine without that font still shows the correct face. An embedded image appears in both the SVG and the PNG, tested. The Markdown outline export round-trips structure and text, and reimporting it rebuilds the tree. Do not build yet: No recovery snapshots, interchange import or multi-map handling yet. ### Phase 5 · Recovery, interchange and the honest limits Build: Keep the last few snapshots that opened cleanly, keyed by content hash, separate from the working copy. Import and export at least one interchange format. Handle several maps in one folder. State plainly what this build does not do that the paid application does. Trap: A single autosave slot gets overwritten with the corrupted state the moment after a crash, so the recovery feature hands you exactly the broken document you were trying to escape; keep the last few snapshots that parsed cleanly, never overwrite the most recent good one, and offer recovery as a list with timestamps rather than as one silent restore that you cannot decline. Done when: A corrupted working copy does not overwrite the most recent good snapshot, tested by writing invalid JSON into the working store. Recovery is offered as a dated list and can be declined, leaving the file untouched. An interchange import produces the same structure on export, tested by a round-trip through the other format. The README lists what is absent, including collaboration, presentation mode and high-fidelity import from the paid tools. Do not build yet: Nothing after this. ### Out of scope (and why) - Collaboration, cursors and comments, which need a server, a conflict model and an account system, and are most of what the paid products charge for. - High-fidelity import from proprietary mind map formats, which is reverse engineering somebody's binary layout rather than a weekend of work. - A presentation mode that walks the map as slides, which is a second product hiding inside the first. - Cloud sync, because there is no server here on purpose and adding one changes every answer about where the data lives. ### README must contain - That browser storage can be cleared by the browser, what persistence was requested and granted, and that the file on disk is the archive - That the word saved refers to bytes written to a file, and what the dirty indicator means - That the layout is deterministic and the test which asserts it - That exports embed fonts and images because an SVG used as an image cannot fetch anything external - What is deliberately absent, including collaboration, presentation mode and proprietary format import
# Architecture · SimpleMind ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with Vite, no backend at all, served from a static host or opened locally | a mind map does not need a server and adding one adds something that can be down | | Working store | IndexedDB with persistence requested and the granted answer displayed | storage that can be evicted is a cache whatever the documentation calls it | | Archive | One JSON file per map, written back through the file handle you opened | the file is the map and the browser is only where you edit it | | Layout | A deterministic tidy tree computed from the document, with manual positions stored per node | a layout that shifts when you reopen a file is a layout nobody trusts | | Export | SVG with fonts and images embedded as data, then PNG rasterised from that same SVG | an SVG inside an image element is not permitted to fetch anything external | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Document | The JSON envelope, versioning, round-trip fidelity. | Any schema, provided it round-trips byte-identically. | | Storage | File handles, the working copy, persistence requests, snapshots. | Any store, provided a file is still the archive. | | Editing | Keyboard model, undo grouping, structural edits. | Any interaction layer, provided undo matches human actions. | | Layout | The pure tidy tree and manual pins. | Any algorithm, provided it is a pure function of the document. | | Export | SVG serialisation, embedding, rasterisation, outline. | Any exporter, provided nothing external is referenced. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `APP_TITLE` · required · Shown in the tab and in the export metadata. No secrets exist in this build. - `SNAPSHOT_KEEP` · required · How many clean snapshots to retain for recovery. They are small. - `AUTOSAVE_IDLE_MS` · required · Idle time before the working copy is written and undo entries are grouped. - `EXPORT_PNG_SCALE` · required · Rasterisation scale for PNG export. Two is the sensible default. - `MAX_ASSET_BYTES` · required · Largest image that can be embedded in a document, since the file carries it. ## 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 · SimpleMind product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: React with Vite, no backend at all, served from a static host or opened locally, IndexedDB with persistence requested and the granted answer displayed, One JSON file per map, written back through the file handle you opened, A deterministic tidy tree computed from the document, with manual positions stored per node, SVG with fonts and images embedded as data, then PNG rasterised from that same SVG. - 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 - Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. - One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. - A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. - An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. - A single autosave slot gets overwritten with the corrupted state the moment after a crash, so the recovery feature hands you exactly the broken document you were trying to escape; keep the last few snapshots that parsed cleanly, never overwrite the most recent good one, and offer recovery as a list with timestamps rather than as one silent restore that you cannot decline.
# Delivery milestones · SimpleMind Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A document that is a file Saving means a file on disk, and the interface says what the browser promised about the working copy. ### Steps 1. Request persistence and show what came back The interesting case is the browser saying no. Files: `src/storage/persist.ts`, `tests/persist.test.ts` 2. Hold the file handle and write back to it Falling back to a download where the browser has no handle. Files: `src/storage/file.ts` 3. Make the dirty indicator mean the file, not the cache That is the only meaning that protects anybody. Files: `src/ui/DirtyDot.tsx` ### Done when - [ ] The granted persistence answer is shown - [ ] Dirty means unwritten to the file - [ ] An unchanged map round-trips byte-identically - [ ] Clearing the database loses nothing that was saved ### Watch out - Autosaving to IndexedDB and showing the word 'saved' is a promise the browser has not made, because the storage standard starts every bucket in best-effort mode and says an agent under storage pressure should clear best-effort buckets, so the map can vanish between sessions with no error anywhere; request persistence, display what the browser actually granted, and reserve the word 'saved' for bytes that reached a file. ## M2 · Editing at the speed of thinking A map can be built entirely from the keyboard and undo removes what a person would call one action. ### Steps 1. Group undo by node and idle window One action to a person is one entry in the stack. Files: `src/history/group.ts`, `tests/undo-grouping.test.ts` 2. Give every action a keyboard route This is a thinking tool, and the mouse is the slow path. Files: `src/keys.ts` 3. Make reparenting undo restore ordinal too Restoring the parent but not the position is a half undo. Files: `src/history/inverse.ts` ### Done when - [ ] A ten node map is built without the mouse - [ ] Undo removes a typed title in one step - [ ] Reparent undo restores parent and ordinal - [ ] Focus is visible and every action is reachable ### Watch out - One undo entry per keystroke means undo after typing a node title deletes a single letter, so the person hits it eleven times, overshoots into the previous action and loses a branch they wanted; group consecutive edits to the same node within an idle window into one entry, make every structural change its own entry, and test that undo after a reparent restores both the tree and the layout rather than only one of them. ## M3 · A layout that holds still Opening the same file twice produces pixel-identical geometry. ### Steps 1. Make the layout pure, with an explicit child order No clock, no random, no map iteration order. Files: `src/layout/tidy.ts`, `tests/layout-determinism.test.ts` 2. Pin manual positions and flow around them The whole point of dragging a node is that it stays dragged. Files: `src/layout/pins.ts` 3. Time it on a large map and write the number down Five hundred nodes is a real map, not a stress test. Files: `tests/layout-perf.test.ts`, `README.md` ### Done when - [ ] Two layouts of one document are identical - [ ] Manual positions survive edits elsewhere - [ ] Collapse and expand restores positions - [ ] Large map layout time is measured and recorded ### Watch out - A layout that iterates over a hash map, uses any randomness or depends on insertion order gives different coordinates for the same file on different runs, and because the difference is small people notice it as the map feeling unstable rather than as a bug they can report; sort children by explicit ordinal, make the layout a pure function with no clock and no random source, and assert in a test that laying out the same document twice produces identical numbers. ## M4 · Export that still contains its text The exported SVG and PNG look like the screen, including fonts and embedded images. ### Steps 1. Embed the font subset and every image as data External references are not fetched when the SVG is an image. Files: `src/export/svg.ts`, `tests/export-font.test.ts` 2. Rasterise the same SVG rather than redrawing Two renderers is two sets of differences. Files: `src/export/png.ts` 3. Compare the export to the screen in a test Text width is the measurement that catches a missing font. Files: `tests/export-parity.test.ts` ### Done when - [ ] PNG text metrics match the screen - [ ] SVG renders correctly without the font installed - [ ] Embedded images appear in both exports - [ ] The Markdown outline round-trips ### Watch out - An SVG placed in an image element is processed in one of the secure modes defined by the SVG conformance rules, which forbid resolving external references, so your linked web font and your linked images simply do not render and the PNG comes out in a fallback face with holes where the pictures were, while the same SVG opened directly in a tab looks perfect; embed the font subset and every image as data inside the SVG, and compare the exported PNG against the screen rather than trusting that it worked. ## M5 · Recovery, interchange and the honest limits (production only) A crash costs nothing, a map can leave for another tool, and the README says what is not here. ### Steps 1. Snapshot only documents that parsed cleanly A snapshot of a broken document is not a backup. Files: `src/storage/snapshots.ts`, `tests/recovery.test.ts` 2. Offer recovery as a choice, never as a silent restore The person knows something you do not. Files: `src/ui/Recovery.tsx` 3. Round-trip one interchange format One that works beats three that nearly work. Files: `src/io/interchange.ts`, `tests/interchange.test.ts` ### Done when - [ ] A corrupt working copy never overwrites a good snapshot - [ ] Recovery is a dated list and can be declined - [ ] Interchange import and export round-trip - [ ] The README lists what is deliberately absent ### Watch out - A single autosave slot gets overwritten with the corrupted state the moment after a crash, so the recovery feature hands you exactly the broken document you were trying to escape; keep the last few snapshots that parsed cleanly, never overwrite the most recent good one, and offer recovery as a list with timestamps rather than as one silent restore that you cannot decline.
# Operations · SimpleMind ## Backup The files. That is the point of the design: back up the folder of JSON documents the same way you back up anything else on that machine, and the browser's copy becomes irrelevant. ## Restore Open the file. There is nothing else to restore, and a test that opens each backed-up document in a fresh browser profile is worth running once so you know the format really is self-contained. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local application this lives in the test suite: persistence granted or denied, layout determinism, export parity with the screen, and the round-trip test on every document format change. ## Incident checklist The incident here is a lost map, and it happens through evicted storage or a document that no longer parses. Snapshots cover the second, files cover the first, and the interface saying what the browser granted is what stops a person relying on the wrong one. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The granted persistence answer is confirmed visible in the interface - [ ] An unchanged map is confirmed to round-trip byte-identically - [ ] Two layouts of one document are confirmed identical - [ ] Exported PNG text metrics are confirmed to match the screen - [ ] A corrupt working copy is confirmed not to overwrite a good snapshot ## Launch constraint Do not market omitted SimpleMind 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. Shown in the tab and in the export metadata. No secrets exist in this build. APP_TITLE=Maps # Required. How many clean snapshots to retain for recovery. They are small. SNAPSHOT_KEEP=5 # Required. Idle time before the working copy is written and undo entries are grouped. AUTOSAVE_IDLE_MS=800 # Required. Rasterisation scale for PNG export. Two is the sensible default. EXPORT_PNG_SCALE=2 # Required. Largest image that can be embedded in a document, since the file carries it. MAX_ASSET_BYTES=2000000
$ choose a build depth, inspect the files, then open the complete pack in your agent
SimpleMind: 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
SimpleMind pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free edition | $0 | $0 | Unlimited mind maps; virtually unlimited page size/elements; 15+ styles; local-device storage; no PDF/image export or cloud sync |
| pro single user, mac or windows | custom | — | 1 user on 1 desktop platform; unlimited mind maps; full export, media, links, custom styles and cloud sync |
| pro single user, mac + windows | custom | — | 1 user licensed on both Mac and Windows |
| pro family/team, mac or windows | custom | — | Up to 4 users on 1 desktop platform |
| pro family/team, mac + windows | custom | — | Up to 4 Mac users plus 4 Windows users |
| pro 10-user, mac or windows | custom | — | Up to 10 users on 1 desktop platform |
| pro 10-user, mac + windows | custom | — | Up to 10 Mac users plus 10 Windows users |
| pro mobile | custom | — | 1 mobile-platform license; full Pro features on iPhone/iPad or Android |
| site license | custom | — | Organization-wide Mac or Windows licensing; user/site caps custom |
free tierUnlimited mind maps; virtually unlimited page size and elements; 15+ styles; local-device storage only; no cloud sync or PDF/image export
billingone-time purchase only; no monthly or annual subscription
hidden costsA separate license is required for each platform unless buying a Mac+Windows bundle. Desktop prices exclude VAT; mobile storefront prices vary, and there is no discounted after-purchase cross-platform upgrade path stated.
verified 2026-08-12 · source ↗
Vibecode SimpleMind
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal SimpleMind replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does SimpleMind cost?
SimpleMind'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 SimpleMind?
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 SimpleMind?
Yes: Freeplane (Flexible local mind maps with links, notes, attributes, filters, and exports; no mobile app and considerably less visual restraint.) yEd (Not marketed as a mind mapper, but its free layouts and local graph files handle large, irregular maps unusually well.) The prompt is for when you want it exactly your way.