Documentation
The spec
What slate is, what it refuses to be, and the milestones in order.
A keyboard-first, plain-text note-taking app with omarchy's aesthetic: terminal-honest, monospace, zero chrome, instant.
Status: Draft v1 · Date: 2026-08-25 · Owner: Adam Davis
1. Summary
Slate is a local-first Markdown note-taking app. Your notes are .md files in a
folder you own. The app is a fast, beautiful, keyboard-driven lens over that folder.
It borrows Obsidian's data model (plain files, wikilinks, backlinks) but rejects Obsidian's complexity: no ribbon of 14 icons, no settings tree 6 levels deep, no plugin that requires a README to configure. It borrows omarchy's interface philosophy: one modal overlay, monospace type, tight rows, a single accent color, and a keybinding for everything.
One-line positioning: Obsidian's power, a text editor's speed, a terminal's calm.
2. Goals & Non-Goals
Goals
| # | Goal | Measured by |
|---|---|---|
| G1 | Extremely lightweight & responsive | Cold start < 400ms; keystroke→paint < 16ms; vault of 10k notes searchable in < 100ms |
| G2 | Simpler to pick up than Obsidian | A new user creates, links, and finds a note within 60s, without docs |
| G3 | Themes are a first-class feature | Theme = one file of tokens; live switch with no reload; ships 6 built-ins |
| G4 | Plugins are a first-class feature | Third-party plugin installable, sandboxed, cannot freeze the editor |
| G5 | Keyboard shortcuts at the forefront | Every action reachable without a mouse; shortcuts always visible in-context |
| G6 | A visual pleasure to work in | Live-preview editing; typography as the primary UI |
| G7 | Local .md files, user-owned |
No lock-in; folder works in any other editor unchanged |
| G8 | Desktop-ready without a rewrite | All platform I/O behind one adapter; Tauri shell drops in |
Non-Goals (v1)
- Real-time multiplayer / collaborative editing
- Cloud sync service (users bring iCloud/Dropbox/Syncthing/git)
- Mobile apps
- Canvas / whiteboard / kanban views
- WYSIWYG rich text that is not Markdown underneath
- Accounts, telemetry, or any network call the user did not initiate
3. Users
Primary — "The bounced Obsidian user." Technical or technical-adjacent. Tried
Obsidian, loved the file model, drowned in the interface and the plugin
archaeology. Lives in a terminal or an editor. Wants notes to feel like vim
feels: fast, quiet, and yours.
Secondary — "The tinkerer." Will write a theme in the first week and a plugin in the first month. Judges the app by how good its extension API is.
Tertiary — "The plain-text convert." Currently in Apple Notes/Notion. Wants out. Needs the app to be obvious on first open, and to never see a config file.
4. Design Language
Derived from omarchy / Hyprland / walker, adapted for a document surface.
Principles
- The document is the interface. Chrome collapses to nothing while typing.
- One overlay, many jobs. A single centered modal — the Palette — is the file switcher, command runner, search, theme picker, and plugin browser. Learn one surface, get the whole app. (This is the walker/launcher pattern from the refs.)
- Monospace as identity. UI text is monospace. It signals precision and gives the whole app a coherent, terminal-native rhythm.
- One accent, everything else neutral. Themes define a single accent used for selection, focus, active row, and links. No decorative color.
- Borders, not shadows. 1px hairlines and a single accent-colored focus ring. No blur-heavy glassmorphism, no drop shadows, no gradients.
- Density over padding. Rows ~32px. Tight, scannable, terminal-like.
- Shortcuts are labeled, always. Every menu row shows its key on the right. The app teaches itself; there is no shortcut cheatsheet to memorize separately.
- Motion is confirmation, never decoration. ≤120ms, opacity/transform only.
Layout
┌────────────────────────────────────────────────────────────────┐
│ ● vault-name note-title.md ⌘K ⌘P │ ← statusbar, 28px
├───────────────┬────────────────────────────────────────────────┤
│ │ │
│ sidebar │ editor │
│ (⌘B) │ live preview │
│ files │ max-width 68ch, centered │
│ tags │ │
│ search │ │
│ │ │
├───────────────┴────────────────────────────────────────────────┤
│ NORMAL 12 words ⎇ linked:3 backlinks:1 14:45 │ ← statusline
└────────────────────────────────────────────────────────────────┘
One dial with three stops, cycled from the chip in the top bar or from ⌘P:
standard is the whole app in a window; zen (⌘.) is the whole app
minus its chrome, with the top bar, the statusline and the sidebar all stepping
back until the pointer moves; full screen (⌘⌥V) is the whole app on the
whole display, chrome and sidebar included.
Those last two are different axes and must not be conflated. Zen is less UI. Full screen is more room. Full screen that eats your file tree is not full screen, it is zen with a bigger window — and it styles nothing of its own for the same reason: the layout that was right in a window is right on a display, and a shell that "helps" by padding the surface only pushes the scroll container off the right edge.
The browser's own full-screen key (F11, ⌃⌘F on macOS) is a different thing:
it resizes the browser window, not the document, so the app's mode does not
follow it and should not pretend to. What the app does follow is the Fullscreen
API — press esc out of it and the dial goes back to standard on its own.
And when the browser refuses the request, the mode reverts and says why, rather
than leaving the chrome hidden around a window that never changed.
Type & Color
- UI + editor:
JetBrains Mono→CaskaydiaCove→SF Mono→ui-monospace - Optional prose face for body text, per-theme opt-in (
iA Writer Quattrostyle) - Scale: 12 / 13 / 14 / 16 / 20 / 26px. Line-height 1.6 in prose, 1.35 in UI.
- Every color is a CSS custom property. No hex literal appears in a component.
5. Feature Requirements
5.1 Vault & Files — P0
- Open a local folder via File System Access API; handle persisted in IndexedDB so the vault reopens silently on next launch (permission re-prompt on cold grant).
- Recursive
.mdtree; folders as-is; no proprietary index file written to the vault. - Create / rename / move / delete note. Rename updates inbound
[[wikilinks]]. - External changes picked up (poll on focus in web; native FS watcher in Tauri).
- Frontmatter (YAML) parsed, editable, rendered as a collapsed chip.
- Attachments folder for pasted images.
5.2 Editor — P0
CodeMirror 6, single-pane live preview:
- Markdown syntax markers hide when the cursor is off the line and reveal when on it.
- Styled inline: headings, bold, italic, strike, code, blockquote, lists, task checkboxes (clickable), tables, horizontal rules, footnotes.
- Code blocks: syntax highlighting, language label, copy button.
[[triggers wikilink autocomplete;#triggers tag autocomplete;/triggers the slash-command menu (styled identically to the Palette).- Images and embeds render inline.
- Autosave, debounced 400ms, plus flush on blur/close. No save button, ever.
- Undo/redo history survives note switching within a session.
5.3 The Palette — P0
One overlay, invoked by mode:
| Key | Mode | Does |
|---|---|---|
⌘K |
Files | Fuzzy-jump to any note by name |
⌘P |
Commands | Every command in the app, incl. plugin-registered |
⌘⇧F |
Search | Full-text across the vault, with match previews |
⌘T |
Themes | Live-previews each theme as you arrow through it |
⌘⇧P |
Plugins | Browse / enable / disable / configure |
Type-ahead switches modes inline (> commands, # tags, / search). Arrow keys |
Enter; every row shows its shortcut on the right. This is the walker aesthetic from the reference screenshots, one-to-one.
5.4 Linking & Connections — P0/P1
[[wikilinks]]with autocomplete and alias support[[note|label]]— P0- Backlinks panel for the current note — P0
- Unlinked mentions — P1
- Tags,
#nested/tags, tag index — P0 - Fast contextual Connections pane — P1
5.5 Keyboard System — P0
- Full command registry: id, title, default binding, category, handler.
- Leader-key chords, omarchy-style:
Spacein normal context opens a which-key style hint menu —Space nnew note,Space ffind,Space ttheme. Discoverability without memorization. - Every binding remappable in
keymap.json; conflicts flagged at load. - No action is mouse-only.
5.6 Themes — P0
- A theme is one JSON file:
{ name, author, appearance, tokens{}, fonts{}, css? }. - ~40 semantic tokens (
--bg,--bg-elevated,--fg,--fg-muted,--accent,--border,--selection,--syntax-*). Applied to:root; switching is a single attribute swap, no reload, no flash. - Ships: Tokyo Night, Catppuccin Mocha, Gruvbox, Rose Pine, Nord, Everforest — the omarchy set.
- Optional
cssescape hatch for full custom styling. - Live theme preview while arrowing the theme palette.
5.7 Plugins — P0
- Sandboxed Web Worker. No DOM, no
window, no network unless the manifest declares hosts and the user grants them. - Versioned message API (
slate.v1). Capabilities:registerCommand— appears in the Palette, bindableregisterPanel— renders declarative UI blocks in a sidebar/modal slotregisterStatusItem— statusline segmentonNoteOpen / onNoteChange / onVaultChange— event hooksvault.read / write / list— scoped to declared globs in the manifestregisterMarkdownTransform— decorate the editor without touching the DOMsettingsschema → auto-generated settings UI, no plugin-authored forms
- Manifest declares
id, name, version, apiVersion, permissions, entry. - Watchdog: a worker that blocks >2s on a hook is terminated and reported. A plugin can never freeze the editor — this is the hard guarantee.
- Plugins load from
<vault>/.slate/plugins/<id>/. Install = drop a folder in.
5.8 Search — P0
- Incremental full-text index built in a worker on vault open.
- Fuzzy file-name matching (name > path > content ranking).
- Target: < 100ms query on a 10k-note vault.
6. Architecture
┌─────────────────────── UI (React 19 + Vite + TS) ────────────────────┐
│ Shell · Palette · Sidebar · Statusline · Settings │
│ CodeMirror 6 editor (live-preview extension) │
└────────────┬──────────────────┬──────────────────┬───────────────────┘
│ │ │
┌────────▼───────┐ ┌────────▼───────┐ ┌────────▼────────┐
│ Command │ │ Theme engine │ │ Plugin host │
│ registry + │ │ tokens→CSS │ │ Worker pool │
│ keymap │ │ vars │ │ + capability │
└────────┬───────┘ └────────────────┘ │ broker │
│ └────────┬────────┘
┌────────▼──────────────────────────────────────▼────────┐
│ Vault service (in-memory index) │
└────────────────────────┬───────────────────────────────┘
│ StorageAdapter (the seam)
┌──────────────┴───────────────┐
┌─────────▼──────────┐ ┌──────────▼──────────┐
│ FSAccessAdapter │ │ TauriFsAdapter │
│ (web, Chromium) │ │ (desktop, v2) │
└────────────────────┘ └─────────────────────┘
The seam that keeps desktop cheap: every filesystem touch goes through
StorageAdapter (~12 async methods). Shipping the Mac/PC app means implementing
that interface against Tauri's fs plugin and adding tauri.conf.json. No UI,
editor, plugin, or theme code changes. Global shortcuts, menu bar, native FS
watching, and auto-update are additive on the desktop side.
Stack
| Layer | Choice | Why |
|---|---|---|
| Build | Vite 7 | Instant HMR, tiny output |
| UI | React 19 + TypeScript | Adapter-friendly, hiring-friendly |
| Editor | CodeMirror 6 | The only serious live-preview base; 10x lighter than Monaco |
| State | Zustand | ~1KB, no provider tree |
| Markdown | remark/unified + Lezer | Shared AST for render and index |
| Search | Custom inverted index in a worker | No 200KB dependency |
| Desktop | Tauri v2 | 5-15MB installers vs Electron's 120MB |
Performance budget
| Metric | Budget |
|---|---|
| Cold start to editable | < 400ms |
| Note switch | < 50ms |
| Keystroke → paint | < 16ms (60fps) |
| Palette open → first result | < 30ms |
| JS bundle (gzipped) | < 250KB |
| Idle memory, 1k-note vault | < 120MB web / < 60MB Tauri |
7. Milestones
| M | Scope | Exit criteria |
|---|---|---|
| M1 — Shell | Layout, theme engine, 6 themes, Palette (files+commands), command registry, keymap | Navigate the whole app by keyboard; switch themes live |
| M2 — Vault | StorageAdapter + FS Access, file tree, CRUD, autosave, CodeMirror live preview | Real .md files edited round-trip on disk |
| M3 — Knowledge | Wikilinks, autocomplete, backlinks, tags, full-text search worker | 10k-note vault searches < 100ms |
| M4 — Extend | Plugin host, worker sandbox, capability broker, settings schema, 2 sample plugins | Third-party plugin runs and cannot freeze the editor |
| M5 — Desktop | Tauri adapter, global shortcuts, native watcher, signed builds | Mac .dmg + Windows .msi from one codebase |
| M6 — Polish | Zen mode, leader-key which-key, Connections, onboarding | 60s time-to-first-linked-note for a new user |
8. Open Questions
- Vim keybinding mode — v1 or v2? (CodeMirror has an official
@codemirror/vim.) - Should the plugin API be published as a typed npm package on day one?
- Sync guidance: ship a git-based sync plugin, or stay agnostic?
- Does the vault get a
.slate/config folder, or does config live outside it? (Current assumption:.slate/inside, so config travels with the vault.)