A fast, offline-first outliner for hierarchical notes. Everything stays in your browser: there is no account and no server. Live at focus.ella.to.
- Outline: nest bullets, add multi-line notes, check items off, and zoom into any bullet. Undo and redo cover text and structure.
- Workspaces: keep separate outlines, for example personal and work. Lock a workspace with a password to encrypt it with AES-GCM; the key is derived with PBKDF2-SHA-256 at 600,000 iterations.
- Offline and installable: a service worker caches the app, and the web manifest lets you install it.
- Multiple tabs: edits made in one tab show up in the others.
- Your data: export a workspace, back up every workspace into one file, and restore it later.
Press ? in the app to see every keyboard shortcut.
Requires Bun.
bun install
bun run dev # start the dev server
bun run build # type-check and build into dist/
bun run preview # serve the production buildQuality checks (CI runs all of them):
bun run typecheck
bun run lint # ESLint, zero warnings allowed
bun run format:check # Prettier (bun run format to fix)
bun run test # unit tests (Vitest + fake-indexeddb)
bun run test:e2e # end-to-end tests (Playwright)The end-to-end tests need a browser. Run bunx playwright install chromium once, or reuse an
installed Chrome with PLAYWRIGHT_CHANNEL=chrome bun run test:e2e. The pwa project runs against a
production preview build to cover the service worker.
Notes are stored as an append-only event log in IndexedDB, with one log per workspace. Loading a workspace replays its events into a tree, and the tree is held in a MobX-State-Tree store that the React UI observes.
| Path | Responsibility |
|---|---|
src/lib/event-store.ts |
IndexedDB schema and migrations, atomic writes, change broadcast to other tabs |
src/lib/event-replayer.ts |
Pure function that turns events into a bullet tree |
src/lib/store/ |
Root store (root-store.ts), models, persistence strategy, encryption, export formats, cross-tab sync |
src/components/outline/ |
The outline editor: bullets, keyboard handling, focus and caret management |
src/components/workspace/ |
Workspace switcher and its dialogs (create, rename, lock, unlock, delete) |
src/components/layout/ |
Page chrome: header, search, menu, footer, banners, shortcuts |
src/components/ui/ |
Shared UI primitives (shadcn/ui, confirmation dialog, toasts) |
src/hooks/ |
Route sync, media queries, delayed focus |
e2e/ |
Playwright specs |
A few design rules keep the data safe:
- Every edit is written immediately. A burst of typing to one field updates its event in place, so the log stays small without delaying writes.
- Whole-log rewrites (import, lock, unlock, compaction) happen in a single IndexedDB transaction. They either complete fully or change nothing.
- A failed read never leads to a write, and a locked workspace never receives plaintext events, even from a stale tab.
- The URL is the source of truth for the open workspace and the zoomed bullet, so browser back and forward work as expected.
Pushing to main runs CI and deploys dist/ to GitHub Pages through
.github/workflows/deploy.yml. The build uses relative asset URLs, so it works at a domain root or
under a sub-path.