# Limn [![CI](https://github.com/tednaleid/limn/actions/workflows/ci.yml/badge.svg)](https://github.com/tednaleid/limn/actions/workflows/ci.yml) [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/tednaleid/41b99760446766c22cc7143632db43c6/raw/limn-coverage.json)](https://github.com/tednaleid/limn/actions/workflows/ci.yml) [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue)](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) |