Local-first markdown notes app for macOS and GNU/Linux. Tauri 2 (Rust) + React 19 + Vite + Tailwind v4 + Tiptap 3 + Zustand.
bun tauri dev— run the app (vite on port 1420 + cargo)bun run build— typecheck (tsc) + vite production buildbunx tsc --noEmit— typecheck onlycd src-tauri && cargo test— Rust unit testsbun run release: full signed and notarized macOS release (scripts/release.sh).github/workflows/release-linux.yml: signed x86_64 AppImage and Debian release
Rust owns the filesystem; the frontend is UI + state only. All IO goes through typed Tauri commands (src/lib/ipc.ts is the only file that touches invoke).
User picks any folder as a vault:
<vault>/— plain.mdfiles live directly in the selected vault root, filename = title, no IDs. Folders are real folders..markd/remains reserved app data. Portable plain-markdown. Frontmatter is optional: Markd preserves external YAML and only authors flat properties after an explicit user action in the Properties UI.<vault>/.markd/— app data:todos.json,bookmarks.json,assets/(pasted images).- Vault path + theme persist in the app config dir (
config.json).
Notes are addressed by path relative to the vault root (e.g. projects/app.md), never by ID. Deletes go to OS trash. External edits are picked up on window focus (dirty editor wins).
One module per concern, each with unit tests; keep files under ~300 lines:
error.rs—AppError/AppResult, serialized as{kind, message}to the frontendvault.rs— layout, tree scan, rel-path resolution (rejects traversal)notes.rs— CRUD, rename/move with collision suffixing ("name 2")search.rs— case-insensitive title+content search, title hits ranked firsttodos.rs/bookmarks.rs— JSON stores in.markd/link_meta.rs— fetch page title / og:image / favicon (reqwest + scraper)assets.rs— save pasted images into.markd/assets/commands.rs— thin#[tauri::command]wrappers only;lib.rs— wiring only
Blocking dialogs (blocking_pick_folder) must run in async commands via spawn_blocking — on the main thread they deadlock the app.
stores/— zustand:vault(tree, view, theme, recents),tabs(open note tabs; active = derived fromvault.view),todos,bookmarks,uicomponents/— by feature:layout/,tree/,editor/,todos/,bookmarks/,palette/,settings/,welcome/,ui/- Editor: Tiptap with
contentType: "markdown"; autosave debounced 500ms, flush on unmount; images stored as vault-relative paths, rendered via asset protocol - Tabs:
NotesWorkspacekeeps one live editor per open tab, inactive panes hidden viadisplay:none— tab switch is a CSS toggle, never a remount/re-parse. Keep it that way. - Session:
lib/session.tspersists per-vault UI layout (open tabs, active view, todo/bookmark tag filters) to localStorage keyed by vault root, restoring it on launch. Tag filters live in thetodos/bookmarksstores (not component state) so they're subscribable. - Page links: internal note links are plain markdown links whose href is a vault-relative note path (
[Title](projects/app.md)).lib/noteLinks.tsconverts href↔rel and rewrites[[wiki]]/[[target|alias]]syntax (on type + on load) into those markdown links; clicking one opens the note;/linkslash command +NoteLinkPickeralso insert them. - Frontmatter:
lib/frontmatter.tssplits a leading---YAML block off the body on load and re-attaches it on save.NotePropertiescan add, edit, and remove flat text or list properties while preserving unrelated YAML. The editor body never contains the raw YAML.
- Strict monochrome: only the semantic tokens in
styles.css(bg,panel,sunken,ink,muted,faint,line,hover,active,invert…).sunkenis the recessed surface (tab strip), darker thanpanel. Never hardcode colors;dangeris the sole exception, for destructive actions. - Dark mode =
.darkclass on<html>; themes: system/light/dark. - Active/selected rows use inverted style (
bg-invert text-invert-ink) — the signature look. - Motion: 100–160ms ease-out only. Fonts: Inter Variable (UI), JetBrains Mono (code).
- No autocorrect anywhere: global
focusinhook inmain.tsxhandles inputs; editor sets its own attrs.
Animated components come from the beui registry (the user's own library). A compatibility layer is already in place so pulled components inherit our monochrome theme unchanged:
src/lib/utils.tsexportscn()(clsx + tailwind-merge) — what beui/shadcn files import. Our own components usecx().src/lib/ease.ts— shared motion tokens (SPRING_PANEL,EASE_OUT, …).styles.cssmirrors--ease-outand defines the.pressutility.styles.cssmaps shadcn semantic tokens (--color-background,--color-foreground,--color-card,--color-border,--color-muted-foreground,--color-destructive,--color-border-strong, …) onto our palette, sobg-background,border-border,text-muted-foregroundetc. resolve to our monochrome look in both themes.components.jsonregisters the@beuiregistry (https://beui.dev/r/{name}.json).
Two ways to add one:
- beui MCP (preferred):
get_component <slug>returns every file's contents; write them undersrc/components/…. Shared files (lib/ease.ts,lib/utils.ts) already exist — don't overwrite. - shadcn CLI:
bunx --bun shadcn add @beui/<slug>.
Our Modal (components/ui/Modal.tsx) is built on the same tokens; keep new dialogs on it for one motion language.
- Never add "Co-Authored-By" or any AI attribution to commits or PRs.
- Commit messages: conventional commits, subject ≤50 chars where possible.
- Don't reintroduce: sticky notes, note IDs, plugin-fs. Never add frontmatter automatically; only explicit actions in the Properties UI may author it.
[[wiki]]is accepted as input but stored as standard markdown[title](path.md)links.