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

  1. The document is the interface. Chrome collapses to nothing while typing.
  2. 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.)
  3. Monospace as identity. UI text is monospace. It signals precision and gives the whole app a coherent, terminal-native rhythm.
  4. One accent, everything else neutral. Themes define a single accent used for selection, focus, active row, and links. No decorative color.
  5. Borders, not shadows. 1px hairlines and a single accent-colored focus ring. No blur-heavy glassmorphism, no drop shadows, no gradients.
  6. Density over padding. Rows ~32px. Tight, scannable, terminal-like.
  7. 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.
  8. 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 MonoCaskaydiaCoveSF Monoui-monospace
  • Optional prose face for body text, per-theme opt-in (iA Writer Quattro style)
  • 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 .md tree; 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: Space in normal context opens a which-key style hint menu — Space n new note, Space f find, Space t theme. 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 css escape 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, bindable
    • registerPanel — renders declarative UI blocks in a sidebar/modal slot
    • registerStatusItem — statusline segment
    • onNoteOpen / onNoteChange / onVaultChange — event hooks
    • vault.read / write / list — scoped to declared globs in the manifest
    • registerMarkdownTransform — decorate the editor without touching the DOM
    • settings schema → 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

  1. Vim keybinding mode — v1 or v2? (CodeMirror has an official @codemirror/vim.)
  2. Should the plugin API be published as a typed npm package on day one?
  3. Sync guidance: ship a git-based sync plugin, or stay agnostic?
  4. Does the vault get a .slate/ config folder, or does config live outside it? (Current assumption: .slate/ inside, so config travels with the vault.)