Skip to content

Repository files navigation

Mindmaps - a simple mindmap app

CI

Demo: view here

Runs entirely in your browser.

No login | Easy Backups | Works offline | No tracking

Dump thoughts into a branching tree, rearrange them, come back later. No account, no server, no tracking. Your maps are stored in your browser's IndexedDB and the app makes no network requests at all after the page loads — open the Network tab and check.

Features: see here


What it doesn't do

Most of these would need the server this app's value depends on not having.

  • No accounts, sharing, collaboration, presence, or sync between devices
  • No cloud storage, backend, or serverless functions of any kind
  • No AI or LLM features
  • No analytics, telemetry, cookie banners, or onboarding tours
  • No PNG / SVG / PDF export
  • No images, attachments, or rich text inside nodes
  • No free-form node positioning — the layout is always computed
  • No themes beyond following your system's light/dark setting

If you want one of these, forking is the right answer; the licence permits it. Read CONTRIBUTING.md before opening an issue asking for one.

What it does

  • Fast to think in. Tab for a child, Enter for the next sibling.
  • Good on a phone. Built for a small screen first. Drag to rearrange, hold to lift, focus a single branch when the map outgrows the view.
  • Paste a list, get a branch. Select a node, paste an indented outline — bullets optional, any indentation — and it becomes a subtree, in one undo step.
  • In and out as JSON or Markdown. JSON keeps everything and is how a map moves between devices; Markdown is a lossy outline for getting text elsewhere. One map or all of them.
  • Hard to lose. Autosave, a crash-recovery draft, one snapshot per map per day for 30 days, and a one-button backup of everything to a single file.
  • Keyboard-complete on desktop, including reparenting.

The trade-offs

Local-only costs something:

  • Your maps are on one device, in one browser. They don't follow you. Restoring a backup elsewhere is how you move them.
  • Clearing site data deletes them. So does uninstalling the browser.
  • A browser short of disk space may evict them. Settings asks it not to via navigator.storage.persist() and reports whether it agreed — browsers often decline a first-time visitor.

Settings → Download a backup writes every map to one JSON file. For one map, the download button on its card in the maps list and ⋯ → Download as JSON in its header write the same format, and all of them go back in through Restore or import maps.

Running it

npm install
npm run dev
Script What it does
npm run dev Dev server
npm test Pure logic in node, behaviour in jsdom
npm run typecheck tsc, no emit
npm run build Type-check, build, prerender the content pages, write sitemap.xml and robots.txt
npm run verify Check the built output against the privacy claim (run after build)
npm run preview Serve the production build locally

How it's built

Vite + React 18 + TypeScript + Tailwind. Six production dependencies, no state-management library, and no graph library — the tidy-tree layout engine is hand-rolled, which is what makes the tapered branch edges possible.

npm audit reports one high-severity advisory against react-router. It is expected and unreachable here; don't npm audit fix --force, and see CONTRIBUTING.md for why.

src/
  lib/       idb.ts        promise wrapper over IndexedDB
             localStore.ts the ONLY module that reads or writes map data
             doc.ts        the document model, validateDoc, serialize
             layout.ts     pure tidy-tree engine + branch geometry
             vault.ts      the JSON backup format — a whole vault, or one map
             snapshots.ts  daily-snapshot rules
             markdown.ts   outline import/export, and the paste parser
             site.ts       SITE_URL and the indexed-page metadata
  state/     saveState.ts  the pure save machine (debounce, conflict, retry)
             useAutosave.ts the only impure half: clock and storage
             mapState.ts   document + history + edit session, as one reducer
  routes/    Landing, About · MapsList, MapView, ArchivedList, Settings
  prerender.tsx            renders the content pages to HTML at build time

How a change reaches the disk

Four hand-offs, each with a pure half that holds the rules and a thin impure half that owns the clock, the DOM or the store:

  1. mapState.ts turns the edit into a new MapDoc through reducer.ts and pushes it onto history.ts. useMapDoc is the React binding, and holds no rules of its own.
  2. saveState.ts decides whether to write and when — debounce, retry, one save in flight, pause on conflict — as a reducer plus a single nextAction(state, now).
  3. useAutosave.ts owns the timers, the four force-save triggers and the calls themselves. It decides nothing.
  4. localStore.ts writes, inside one transaction, bumping rev.

rev is the conflict token: a counter rather than a timestamp, so no clock skew or coarse resolution can confuse it. Every write pre-flights against it. A moved rev is only suspicion — two tabs that independently arrive at the same text are not in conflict — so a suspicion is settled by comparing the stored document against the last one confirmed written. Renaming a map deliberately does not bump rev, because the name is not part of the document.

Three stores, and the split between them matters:

Where What Why there
IndexedDB maps The row, its document serialized to a string The store of record. A string rather than an object, so the dirty check can compare bytes
IndexedDB snapshots One document per map per day, 30 kept For a mistake noticed days later. Written after a save, never awaited by one
localStorage mindmaps.draft.{id} The in-flight document The only API that can finish during pagehide. Cleared solely on a confirmed save

Five conventions that are load-bearing

  • localStore.ts is the only module that touches stored map data. A different backing store means editing one file.
  • Storage is injected into components, never imported. MapCanvas takes a SaveAdapter prop. That is the only reason its behaviour is testable, and every integration bug this project has had lived in that component tree.
  • Rules live in pure modules; timers, storage and the DOM live in a thin binding around them. layout.ts, saveState.ts, reparent.ts and friends have no side effects and are tested directly.
  • Layout measures what is actually painted. A hidden mirror predicts a node's size and a ResizeObserver on the real node overrides it. Never pin a node's width to the prediction — that makes the prediction unfalsifiable.
  • View state never mutates the document. Search reveals its matches by handing layout() a forceExpanded set for one pass, and focusing a branch passes a focusRoot the same way. Writing collapsed: false instead would make typing in the search box an edit — an undo entry, a dirty document and a stored write, for something the owner only wanted to look at.

Tests split by filename: *.test.ts runs in node, *.dom.test.tsx in jsdom. jsdom has no layout engine, so DOM tests prove behaviour and never geometry; geometry belongs in layout.test.ts.

Licence

MIT — see LICENSE.

About

A simple mindmap app - no login, easy backups, works offline, no tracking

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages