README file from
GithubLimn
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
Quick start
bun install
just serve # starts Vite dev server at http://localhost:5173
Project structure
packages/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)
Development commands
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 checking
You 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
Architecture
- 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/)
Storage
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
File format
.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.
Keyboard shortcuts
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.
Obsidian plugin
Limn is available as an Obsidian community plugin that opens .limn files as interactive mind map views.
Install from the Community Plugins directory
- In Obsidian, open Settings -> Community plugins
- Click Browse, search for "Limn", and click Install
- Enable the plugin
Install via BRAT (beta / latest)
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.
Development
See OBSIDIAN-PLUGIN.md for local development setup, building, and architecture details.
Inline markdown
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) |