limn /lim/ verb -- to outline in clear sharp detail : delineate
A keyboard-first, offline-capable mind map progressive web app built with TypeScript, React, and SVG.
Try it: tednaleid.github.io/limn
limn-demo.mp4
bun install
just serve # starts Vite dev server at http://localhost:5173packages/core/ # Framework-agnostic TS library (no React, no browser APIs)
packages/web/ # React web app (rendering, input handling, persistence)
packages/obsidian/ # Obsidian plugin (opens .limn files inside Obsidian)
All commands are available via just:
just # list all available commands
just install # install dependencies
just serve # start Vite dev server
just test # run all unit tests
just test-watch # run tests in watch mode
just test-file drag # run a specific test file (by name)
just lint # run ESLint
just coverage # run tests with coverage report
just check # run tests (with coverage) + lint + typecheck (CI check)
just build # build the Obsidian plugin (primary artifact)
just build-web # build the web PWA (used by the Pages deploy)
just typecheck # TypeScript type checkingYou can also use bun run directly:
bun run test # vitest
bun run dev # vite dev server
bun run lint # eslint
bun run build # build the Obsidian plugin
bun run build:web # build the web PWA- Editor is the sole source of truth for all state
- TestEditor enables testing all interactions without a browser
- Keyboard-first: Tab creates child, Enter edits, arrows navigate spatially,
;for EasyMotion jump - Diff-based undo/redo (snapshot capture, no Command classes)
- SVG rendering with pan/zoom viewport
- IndexedDB auto-save with cross-tab sync
- ZIP file format with embedded assets (
data.json+assets/)
All data is stored locally in the browser using IndexedDB. Nothing is sent to a server.
- Each document gets a unique ID shown in the URL hash (
#local-doc=<uuid>) - Opening the bare URL (no hash) reopens the last-edited document
- Opening a
#data=...URL decompresses the inline document, assigns it a fresh UUID, and saves it locally - The "New" menu item creates a new document with its own UUID
- Two tabs with the same
#local-doc=<uuid>URL will stay in sync via BroadcastChannel - Clearing browser data (IndexedDB) deletes all locally stored documents
- Use
Cmd+S/Cmd+Oto save/open.limnfiles for durable storage outside the browser
.limn files are ZIP bundles containing data.json and an assets/ directory for images. The current format version is 1.
- Schema definition:
packages/core/src/serialization/schema.ts - Golden fixture:
packages/core/src/serialization/fixtures/v1-complete.json - Migration pipeline:
packages/core/src/serialization/migration.ts
The format uses integer versions. When a file is opened, the migration pipeline in migration.ts upgrades it from its stored version to the current version. Post-migration, the result is validated against the Zod schema.
Navigation:
| Key | Action |
|---|---|
| Arrows / hjkl | Navigate between nodes |
; |
EasyMotion: labels appear on all visible nodes, type a label to jump |
| Cmd+Enter | Open link in selected node |
| Escape | Deselect |
Node Operations:
| Key | Action |
|---|---|
| Tab | Create child node |
| Enter | Edit selected node (or create root if nothing selected) |
| Shift+Enter | Create sibling node |
| Backspace | Delete node |
| Space | Toggle collapse |
| c / Shift+c | Cycle branch color forward / backward |
Structure:
| Key | Action |
|---|---|
| Alt+Up/Down or Alt+k/j | Reorder among siblings |
| Alt+Left/Right or Alt+h/l | Indent / Outdent |
| Shift+Tab | Detach node to root |
Alt+; |
Reparent to target (EasyMotion labels appear, type to attach) |
Positioning:
| Key | Action |
|---|---|
| Ctrl+Arrows / Ctrl+hjkl | Nudge node (20px) |
| Ctrl+Alt+Left/Right / Ctrl+Alt+h/l | Resize node width or image (20px) |
| r | Reflow children to computed layout |
Text Editing:
| Key | Action |
|---|---|
| Enter | Exit edit, create sibling |
| Tab | Exit edit, create child |
| Shift+Enter | Insert newline |
| Escape | Exit edit mode |
Global:
| Key | Action |
|---|---|
| Cmd+Z / Cmd+Shift+Z | Undo / Redo |
| Cmd+S / Cmd+Shift+S | Save / Save As |
| Cmd+O | Open file |
| Cmd+Shift+E | Export SVG |
| Cmd+= / Cmd+- | Zoom in / out |
| Cmd+0 | Zoom to fit |
| Cmd+1 | Zoom to selected node |
| Shift+Arrows / Shift+hjkl | Pan canvas |
| Shift+Alt+Arrows / Shift+Alt+hjkl | Pan canvas (fine) |
| m | Open menu |
| ? | Show keyboard shortcuts |
| Ctrl+Shift+K | Toggle keystroke overlay (for demos) |
Mouse:
| Action | Effect |
|---|---|
| Click node | Select node |
| Double-click node | Enter edit mode |
| Double-click canvas | Create new root node |
| Cmd+Click link | Open link in new tab |
| Drag node | Move node (reparent when dropped on another node) |
Note: On macOS, Ctrl+Arrow keys are bound to Mission Control by default (switching desktops). To use Ctrl+Arrow nudge and resize bindings, disable these in System Settings > Keyboard > Keyboard Shortcuts > Mission Control, or use the hjkl equivalents instead.
Limn is available as an Obsidian community plugin that opens .limn files as interactive mind map views.
- In Obsidian, open Settings -> Community plugins
- Click Browse, search for "Limn", and click Install
- Enable the plugin
If you want the latest unreleased build, install via BRAT instead:
- Install the BRAT plugin from Obsidian community plugins
- In BRAT settings, click "Add Beta plugin" and enter:
tednaleid/limn - Enable "Limn" in Settings -> Community plugins
BRAT will install the latest release and keep it updated automatically.
See OBSIDIAN-PLUGIN.md for local development setup, building, and architecture details.
Node text supports inline markdown formatting. Raw markdown is shown while editing; rendered formatting is shown in nav mode.
| Syntax | Result |
|---|---|
**bold** |
bold |
*italic* |
italic |
`code` |
code |
~~strikethrough~~ |
|
[text](url) |
clickable link (Cmd+Click or Cmd+Enter to follow) |