# Limn
[](https://github.com/tednaleid/limn/actions/workflows/ci.yml)
[](https://github.com/tednaleid/limn/actions/workflows/ci.yml)
[](https://www.typescriptlang.org/)
> **limn** /lim/ *verb* -- to outline in clear sharp detail : [delineate](https://www.merriam-webster.com/dictionary/limn)
A keyboard-first, offline-capable mind map progressive web app built with TypeScript, React, and SVG.
**Try it:** [tednaleid.github.io/limn](https://tednaleid.github.io/limn/)
## Quick start
```bash
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](https://github.com/casey/just):
```bash
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:
```bash
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=`)
- 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=` URL will stay in sync via BroadcastChannel
- Clearing browser data (IndexedDB) deletes all locally stored documents
- Use `Cmd+S` / `Cmd+O` to save/open `.limn` files 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](https://community.obsidian.md/plugins/limn) that opens `.limn` files as interactive mind map views.
### Install from the Community Plugins directory
1. In Obsidian, open Settings -> Community plugins
2. Click Browse, search for "Limn", and click Install
3. Enable the plugin
### Install via BRAT (beta / latest)
If you want the latest unreleased build, install via [BRAT](https://github.com/TfTHacker/obsidian42-brat) instead:
1. Install the BRAT plugin from Obsidian community plugins
2. In BRAT settings, click "Add Beta plugin" and enter: `tednaleid/limn`
3. Enable "Limn" in Settings -> Community plugins
BRAT will install the latest release and keep it updated automatically.
### Development
See [OBSIDIAN-PLUGIN.md](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~~` | ~~strikethrough~~ |
| `[text](url)` | clickable link (Cmd+Click or Cmd+Enter to follow) |