A native macOS writing application for seamless drafting and publishing
Version: 1.0 Last Updated: January 2026 Author: Pranav Hari
- Executive Summary
- Problem Statement
- Product Vision
- Core Requirements
- User Experience
- Technical Architecture
- Feature Specifications
- Publishing Workflow
- Phased Delivery
- Site Integration
- Open Questions
Nibandh (Hindi for "essay") is a native macOS desktop application that provides a distraction-free writing environment with direct publishing capabilities to a personal website. Built with Tauri (Rust + WebView), it offers the performance of a native app with the flexibility of web technologies.
- Streamlined Workflow: Draft → Edit → Publish in one application
- Native Performance: ~80MB memory footprint (vs 300MB+ for Electron)
- Offline-First: Full functionality without internet, sync when connected
- Site-Accurate Preview: See exactly how articles will appear before publishing
- Git-Native Publishing: Commits directly to your site repository
Apple Notes → Export PDF → AI Tool → Convert to Markdown → Manual Git Commit → Push
Issues:
- Multiple tool context switches
- AI conversion introduces formatting errors
- No preview of final rendering
- Manual git operations for every publish
- No draft versioning or sync between devices
Nibandh → Write → Preview → Publish (one click)
A single user (author) who:
- Writes long-form content (articles, essays, technical posts)
- Publishes to a personal Next.js/Markdown-based website
- Values native app performance and keyboard-driven workflows
- Works across multiple Mac devices
- Writing First: UI gets out of the way; content is the focus
- Predictable: What you see matches what gets published
- Fast: Instant launch, no loading spinners, responsive interactions
- Reliable: Never lose work; offline-capable; conflict-resistant
| Category | Requirement | Rationale |
|---|---|---|
| Platform | Tauri 2.x (Rust + WebView) | Native performance (~80MB), cross-platform potential |
| Editor Model | WYSIWYG blocks | Matches Notion mental model; no raw markdown exposure |
| Writing Style | Non-linear, block-based | User jumps between sections, rearranges content |
| Draft Storage | Markdown files on disk | Portable, editable with other tools if needed |
| Version Control | Auto-save every 5 min, last 10 versions | Safety net without manual intervention |
| Sync Strategy | Same repo, drafts branch |
Leverages existing git infrastructure |
| Git Auth | GitHub CLI (gh) |
Existing authentication, easy setup |
| Image Handling | Clipboard paste → /content/images/ |
Direct save to repo, no intermediate storage |
| Preview | Bundled renderer with site CSS | Accurate representation without running dev server |
| Organization | Flat list + tags + search | Simple mental model, powerful filtering |
| Theme | Manual light/dark toggle | User control, not system-dependent |
| Typography | Serif font (literary feel) | Georgia, Merriweather, or similar |
| Aspect | Specification |
|---|---|
| App Presence | Regular dock app + global hotkey |
| Global Hotkey | Cmd+Shift+N (customizable in settings) |
| Launch Behavior | Resume last opened draft |
| Window Model | Single main window with sidebar |
┌─────────────────────────────────────────────────────────────────┐
│ ● ○ ○ Nibandh ☀️/🌙 ─ □ x │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────────────────────────────────────┐ │
│ │ 🔍 Search │ │ │ │
│ ├─────────────┤ │ [Cover Image Upload Area] │ │
│ │ │ │ │ │
│ │ ALL DRAFTS │ │ ┌─ Metadata Panel ──────────────────────┐ │ │
│ │ │ │ │ Title: [________________] │ │ │
│ │ ○ Draft 1 │ │ │ Tags: [AI] [Tech] [+] │ │ │
│ │ ● Draft 2 ← │ │ │ Desc: [________________] │ │ │
│ │ ○ Draft 3 │ │ └───────────────────────────────────────┘ │ │
│ │ │ │ │ │
│ │ │ │ ───────────────────────────────────────── │ │
│ │ ───────────│ │ │ │
│ │ TAGS │ │ Type / for commands... │ │
│ │ #AI (3) │ │ │ │
│ │ #Tech (5) │ │ Your content blocks here... │ │
│ │ #Personal │ │ │ │
│ │ │ │ │ │
│ └─────────────┘ │ │ │
│ │ ┌─ Status Bar ─────────────────────────────┐│ │
│ │ │ 1,234 words · 6 min read · Saved 2m ago ││ │
│ │ └──────────────────────────────────────────┘│ │
│ │ [Preview] [Publish] │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Slash Commands (/ trigger):
/h1,/h2,/h3- Headings/p- Paragraph/list- Bullet list/numbered- Numbered list/quote- Blockquote/code- Code block (with language selector)/image- Image block/table- Table/callout- Callout/highlight box/divider- Horizontal rule/collapse- Collapsible section/youtube- YouTube embed/spotify- Spotify embed
Floating Toolbar (on text selection):
- Bold, Italic, Strikethrough
- Link, Code (inline)
- Highlight color
| Action | Shortcut |
|---|---|
| New draft | Cmd+N |
| Save (manual) | Cmd+S |
| Publish | Cmd+Shift+P |
| Preview | Cmd+P |
| Bold | Cmd+B |
| Italic | Cmd+I |
| Link | Cmd+K |
| Undo | Cmd+Z |
| Redo | Cmd+Shift+Z |
| Search drafts | Cmd+F |
| Toggle sidebar | Cmd+\ |
| Toggle dark mode | Cmd+Shift+D |
┌─────────────────────────────────────────────────────────────┐
│ Nibandh App │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Frontend (WebView) │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────────┐ │ │
│ │ │ React │ │ TipTap │ │ Tailwind CSS │ │ │
│ │ │ or Solid │ │ Editor │ │ + Theme │ │ │
│ │ └───────────┘ └───────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ Tauri IPC │
│ │ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Backend (Rust) │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────────┐ │ │
│ │ │ Git │ │ SQLite │ │ File System │ │ │
│ │ │ (git2-rs) │ │ (rusqlite)│ │ (std::fs) │ │ │
│ │ └───────────┘ └───────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| Layer | Recommended | Alternatives | Notes |
|---|---|---|---|
| Runtime | Tauri 2.x | - | Required for native packaging |
| Frontend Framework | React 18 | SolidJS, Svelte | React has best TipTap support |
| Editor | TipTap 2.x | Slate.js, ProseMirror | Block-based, extensible, active community |
| Styling | Tailwind CSS | CSS Modules | Matches site's existing stack |
| State Management | Zustand | Jotai, Redux Toolkit | Lightweight, TypeScript-friendly |
| Git Operations | git2-rs | Shell to gh CLI |
Native Rust bindings preferred |
| Database | rusqlite | sled | For version history storage |
| Markdown | pulldown-cmark | - | Rust markdown parser for preview |
nibandh/
├── src-tauri/ # Rust backend
│ ├── src/
│ │ ├── main.rs # App entry point
│ │ ├── commands/ # Tauri IPC commands
│ │ │ ├── drafts.rs # Draft CRUD operations
│ │ │ ├── git.rs # Git operations
│ │ │ ├── images.rs # Image handling
│ │ │ └── versions.rs # Version history
│ │ ├── storage/
│ │ │ ├── sqlite.rs # Version database
│ │ │ └── filesystem.rs # Draft file operations
│ │ └── git/
│ │ ├── sync.rs # Sync operations
│ │ └── publish.rs # Publishing flow
│ ├── tauri.conf.json # Tauri config
│ └── Cargo.toml
├── src/ # Frontend
│ ├── components/
│ │ ├── editor/
│ │ │ ├── Editor.tsx # Main TipTap editor
│ │ │ ├── Toolbar.tsx # Floating toolbar
│ │ │ ├── SlashMenu.tsx # Slash command palette
│ │ │ └── blocks/ # Custom block components
│ │ ├── sidebar/
│ │ │ ├── DraftList.tsx
│ │ │ ├── TagFilter.tsx
│ │ │ └── Search.tsx
│ │ ├── metadata/
│ │ │ ├── MetadataPanel.tsx
│ │ │ ├── CoverUpload.tsx
│ │ │ └── TagInput.tsx
│ │ └── publish/
│ │ ├── PreviewModal.tsx
│ │ ├── PublishDialog.tsx
│ │ └── DiffViewer.tsx
│ ├── hooks/
│ │ ├── useDrafts.ts
│ │ ├── useVersions.ts
│ │ └── useGit.ts
│ ├── stores/
│ │ ├── draftStore.ts
│ │ └── settingsStore.ts
│ ├── lib/
│ │ ├── markdown.ts # MD ↔ Block conversion
│ │ └── preview.ts # Preview rendering
│ └── styles/
│ ├── globals.css
│ └── editor.css
├── package.json
└── vite.config.ts
// Draft stored as markdown file with frontmatter
interface Draft {
id: string; // UUID
slug: string; // Filename (sans .md)
title: string;
date: string; // ISO date
tags: string[];
description: string;
cover?: string; // Image path
content: string; // Markdown body
createdAt: Date;
updatedAt: Date;
publishedAt?: Date;
isPublished: boolean;
}
// Version stored in SQLite
interface Version {
id: string;
draftId: string;
content: string; // Full markdown snapshot
createdAt: Date;
}
// App settings stored in SQLite or JSON
interface Settings {
repoPath: string; // Cloned repo location
repoUrl: string; // GitHub URL
theme: 'light' | 'dark';
globalHotkey: string;
lastOpenedDraftId?: string;
}Description: WYSIWYG editor using TipTap with custom block extensions.
Block Types:
| Block | Markdown Output | Notes |
|---|---|---|
| Heading 1-3 | # , ## , ### |
With anchor ID generation |
| Paragraph | Plain text | Default block type |
| Bullet List | - items |
Supports nesting |
| Numbered List | 1. items |
Auto-incrementing |
| Blockquote | > |
Styled with left border |
| Code Block | ``` lang |
Language selector dropdown |
| Inline Code | `code` |
Via toolbar or backticks |
| Image |  |
Clipboard paste support |
| Table | GFM tables | Column alignment support |
| Callout | > [!NOTE] |
GitHub-style callouts |
| Divider | --- |
Horizontal rule |
| Collapsible | <details> |
HTML passthrough |
| YouTube | [Embedded content](url) |
URL paste detection |
| Spotify | [Embedded content](url) |
URL paste detection |
Block Operations:
- Drag handle for reordering
- Block menu (duplicate, delete, convert type)
- Arrow key navigation between blocks
Paste from Clipboard:
- Detect image paste event
- Generate unique filename:
img_{timestamp}_{hash}.png - Save to
{repoPath}/content/images/ - Insert image block with path
../images/{filename}
Requirements:
- Support PNG, JPG, WebP, GIF
- Compress images over 1MB (optional, configurable)
- Show inline preview immediately
- Copy to
/site/public/images/on publish
Upload Flow:
- Dedicated drop zone in metadata panel
- Paste or drag image file
- Save as
cover_{slug}_{hash}.{ext} - Store path in frontmatter
coverfield
Display:
- Thumbnail preview in metadata panel
- "Remove cover" button
- Optional (clear "no cover" state available)
Fields:
| Field | Type | Validation |
|---|---|---|
| Title | Text input | Required, max 200 chars |
| Tags | Tag input | Autocomplete from existing, free-form entry |
| Description | Textarea | Optional, max 500 chars, shown in previews |
| Cover | Image upload | Optional |
| Slug | Auto-generated | Shown on publish confirmation |
Tag Autocomplete:
- Fetch existing tags from published articles
- Show suggestions as user types
- Allow creating new tags freely
Behavior:
- Save snapshot every 5 minutes if content changed
- Keep last 10 versions per draft
- Prune older versions automatically
Version Browser:
- Slide-out panel showing version history
- Timestamp + diff preview for each version
- "Restore this version" action
- Restored version becomes current (old current preserved as version)
Storage: SQLite database in app data directory
CREATE TABLE versions (
id TEXT PRIMARY KEY,
draft_id TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (draft_id) REFERENCES drafts(id)
);
CREATE INDEX idx_versions_draft ON versions(draft_id, created_at DESC);Strategy: Drafts branch in same repo
Sync Flow:
- On launch:
git fetch origin drafts - Check for remote changes
- If conflict: prefer local, backup remote as
{slug}_remote_{date}.md - On draft save: commit to local drafts branch
- On explicit sync or publish: push drafts branch
Offline Behavior:
- All operations work locally
- Sync queue stored for when connectivity returns
- Status indicator shows sync state
Implementation:
- SQLite FTS5 for fast text search
- Index: title, description, content, tags
- Update index on draft save
UI:
- Search bar in sidebar
- Real-time results as you type
- Highlight matches in results
- Filter by tag in conjunction with search
Display: Status bar at bottom of editor
Calculations:
- Word count: Split on whitespace, count
- Reading time:
Math.ceil(wordCount / 200)minutes - Character count (optional toggle)
Updates: Debounced (500ms) during typing
Bundled Preview Renderer:
- Uses site's actual CSS (bundled at build time)
- Renders markdown with same plugins:
- remark-gfm
- rehype-raw
- Shows accurate representation including:
- Cover image with gradient overlay
- Typography and spacing
- Code block styling
- Embed rendering
Preview Modal:
- Full-screen modal with rendered article
- Toggle between light/dark theme
- Close button returns to editor
Pre-Publish Checks:
- Validate required frontmatter (title, date, tags, description)
- Ensure all images exist in repo
- Generate/confirm slug
Publish Dialog:
┌─────────────────────────────────────────────────────┐
│ Ready to Publish │
├─────────────────────────────────────────────────────┤
│ │
│ Title: My Article Title │
│ Slug: my-article-title [Edit] │
│ Tags: AI, Tech │
│ │
│ ┌─ Changes ──────────────────────────────────────┐ │
│ │ + content/articles/my-article-title.md │ │
│ │ + content/images/img_12345.png │ │
│ │ M site/public/images/ (copied) │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ Commit Message: │
│ ┌────────────────────────────────────────────────┐ │
│ │ Add: My Article Title │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ [Cancel] [Publish to Main] │
└─────────────────────────────────────────────────────┘
Publish Steps:
- Copy images to
/site/public/images/ - Copy markdown to
/content/articles/{slug}.md - Stage files:
git add content/articles/{slug}.md content/images/* site/public/images/* - Commit with user-provided message
- Push to
mainbranch - Mark draft as published
- Poll GitHub Actions for build status
Implementation:
- After push, poll GitHub API for workflow runs
- Show status in app: "Building...", "Deployed!", "Failed"
- macOS notification on completion
Polling Strategy:
- Check every 10 seconds for 5 minutes
- Stop polling if status becomes terminal
- Use
gh api repos/{owner}/{repo}/actions/runsvia CLI
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Writing │────▶│ Preview │────▶│ Publish │
│ │ │ │ │ Dialog │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ Confirm │
│ Slug/Commit │
└──────┬───────┘
│
┌────────────────────────────┼────────────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Copy Images │ │ Copy Article │ │ Git Add │
│ to public/ │ │ to content/ │ │ & Commit │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ Git Push │
│ to main │
└──────┬───────┘
│
▼
┌──────────────┐
│ Poll GitHub │
│ Actions │
└──────┬───────┘
│
▼
┌──────────────┐
│ Notification │
│ "Deployed!" │
└──────────────┘
Goal: Core writing and publishing functionality
Features:
- Tauri app scaffold with React frontend
- Lexical editor with basic blocks (headings, paragraphs, lists, code, images)
- Frontmatter metadata panel (title, tags, description)
- Image paste from clipboard
- Publish flow with git commit/push
- Basic settings (repo path, theme toggle)
Not Included:
- Version history
- Sync between devices
- Search
- Advanced blocks (tables, callouts, embeds)
Goal: Complete core experience
Features:
- Multi-draft support with sidebar
- Auto-save versioning (5 min, 10 versions)
- Git-based sync (drafts branch)
- Bundled preview with site CSS (basic alignment with site styles)
- Full-text search
- Word count / reading time
- Cover image upload + reposition
- Tag autocomplete
- Slug confirmation on publish
Goal: Polish and advanced features
Features:
- Full block type support (tables, callouts, collapsibles, embeds) — partial support exists
- GitHub Actions build status notifications
- Global hotkey (
Cmd+Shift+N) - Onboarding flow (clone repo from URL)
-
ghCLI installation helper - Conflict resolution UI
- Settings panel (fonts, hotkeys, sync frequency)
- Performance optimizations
nextjs-notion-starter-kit/
├── site/
│ ├── content/
│ │ ├── articles/ # ← Published articles go here
│ │ │ └── {slug}.md
│ │ └── images/ # ← Article images go here
│ │ └── {image}.{ext}
│ ├── public/
│ │ └── images/ # ← Copied from content/images on publish
│ └── src/
│ └── ...
└── drafts/ # ← Drafts stored here (on drafts branch)
└── {slug}.md
---
title: "Article Title" # Required
date: "2026-01-15" # Required, ISO format
tags: ["AI", "Tech"] # Required, array
description: "Preview text" # Required, for meta/cards
cover: "/images/cover.webp" # Optional, path from public
draft: false # Optional, defaults false
---| Context | Path Format |
|---|---|
| In markdown body | ../images/{filename} (relative) |
| In frontmatter cover | /images/{filename} (absolute from public) |
| On disk (source) | {repo}/content/images/{filename} |
| On disk (public) | {repo}/site/public/images/{filename} |
Nibandh bundles the site's CSS variables for accurate preview:
:root {
--background: #F2F2F7;
--foreground: #000000;
--muted: #8E8E93;
--accent: #007AFF;
--border: #C6C6C8;
}
.dark {
--background: #000000;
--foreground: #FFFFFF;
--muted: #8E8E93;
--accent: #0A84FF;
--border: #38383A;
}Preview uses identical plugins to site:
remark-gfmfor tables, strikethrough, autolinksrehype-rawfor HTML passthrough (<details>, etc.)
┌─────────────────────────────────────────────────────┐
│ │
│ Welcome to Nibandh │
│ │
│ Let's set up your writing space │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ GitHub Repository URL │ │
│ │ [https://github.com/user/repo ]│ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Nibandh will clone this repository and use it │
│ for publishing your articles. │
│ │
│ [Continue →] │
└─────────────────────────────────────────────────────┘
If gh is not installed:
┌─────────────────────────────────────────────────────┐
│ │
│ GitHub CLI Required │
│ │
│ Nibandh uses the GitHub CLI for authentication. │
│ │
│ [Install GitHub CLI] │
│ │
│ This will run: brew install gh │
│ │
│ [Skip] [Install & Continue] │
└─────────────────────────────────────────────────────┘
After installation: gh auth login flow
-
Markdown ↔ Block Conversion Fidelity: How to handle edge cases where markdown doesn't round-trip perfectly through WYSIWYG?
-
Image Optimization: Should we auto-compress/convert images to WebP on paste? What about existing images?
-
Collaborative Future: Any future plans for real-time collaboration would require architectural changes. Document as out-of-scope?
-
Mobile Companion: Interest in iOS/iPadOS version for on-the-go writing? Would require separate spec.
-
Backup Strategy: Beyond git, should there be local Time Machine-style backups of all drafts?
| Term | Definition |
|---|---|
| Draft | Unpublished article stored on drafts branch |
| Published | Article merged to main and deployed |
| Block | Individual content unit in editor (paragraph, heading, etc.) |
| Frontmatter | YAML metadata at top of markdown file |
| Slug | URL-friendly filename derived from title |
For each feature, implementation should satisfy:
GIVEN [precondition]
WHEN [action]
THEN [expected result]
Example for Image Paste:
GIVEN I am editing a draft
WHEN I paste an image from clipboard (Cmd+V)
THEN the image is saved to content/images/
AND an image block appears in the editor
AND the block shows an inline preview
End of Specification