# Technical Documentation: joemccann/dillinger

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/joemccann/dillinger)

### Overview (`README.md`)

<p align="center">
  <img src="https://github.com/user-attachments/assets/885053fb-b002-4168-98f5-56e583829983" alt="Dillinger Banner" width="100%" />
</p>

<h1 align="center">Dillinger</h1>

<p align="center">
  <strong>The last Markdown editor you'll ever need.</strong>
</p>

<p align="center">
  A modern, cloud-native Markdown editor built on Next.js — designed for speed, focus, and seamless sync.
</p>

<p align="center">
  <a href="#quick-start"><strong>Get Started</strong></a> ·
  <a href="#features"><strong>Features</strong></a> ·
  <a href="#cloud-integrations"><strong>Integrations</strong></a> ·
  <a href="#deployment"><strong>Deploy</strong></a>
</p>

---

## ✨ Why Dillinger

Most markdown editors are either:
- bloated  
- offline-only  
- or lack serious developer ergonomics  

**Dillinger fixes that.**

It combines:
- ⚡ VS Code-grade editing  
- ☁️ Native cloud storage  
- 🧘 Distraction-free UX  
- 🔄 Real-time rendering  

---

## ⚡ Quick Start

```bash
npm install
npm run dev
````

Open → [http://localhost:3000](http://localhost:3000)

---

## 🧱 Requirements

* Node.js 18+
* npm / yarn / pnpm / bun

---

## 🚀 Features

### Editor Experience

* Monaco Editor (VS Code core)
* Syntax highlighting for Markdown
* Image paste support

### Live Workflow

* Real-time preview
* Scroll sync
* Instant feedback loop

### Focus Mode

* Zen Mode (`Cmd/Ctrl + Shift + Z`)
* Fullscreen, distraction-free writing

### File Handling

* Drag & drop `.md`, `.txt`, `.markdown`
* Auto-save (local persistence)

### Export

* Markdown
* HTML
* PDF

### Cloud Integrations

* GitHub
* Google Drive
* Dropbox
* OneDrive
* Bitbucket

---

## 🔐 Environment Setup

Create:

```bash
.env.local
```

### Template

```bash
# Google Drive
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://localhost:3000/api/google-drive/callback

# OneDrive
ONEDRIVE_CLIENT_ID=
ONEDRIVE_CLIENT_SECRET=
ONEDRIVE_REDIRECT_URI=http://localhost:3000/api/onedrive/callback

# Bitbucket
BITBUCKET_CLIENT_ID=
BITBUCKET_CLIENT_SECRET=
BITBUCKET_REDIRECT_URI=http://localhost:3000/api/bitbucket/callback

# GitHub
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_REDIRECT_URI=http://localhost:3000/api/github/callback

# Dropbox
DROPBOX_CLIENT_ID=
DROPBOX_CLIENT_SECRET=
DROPBOX_REDIRECT_URI=http://localhost:3000/api/dropbox/callback
```

---

## ☁️ Cloud Integrations

Each provider requires OAuth configuration.

### Google Drive

* Enable Drive API
* Create OAuth credentials
* Redirect:

```
/api/google-drive/callback
```

### OneDrive

* Azure App Registration
* Permissions:

  * Files.ReadWrite
  * User.Read

### Bitbucket

* OAuth consumer
* Repo read/write access

### GitHub

* OAuth App
* Callback:

```
/api/github/callback
```

### Dropbox

* Scoped app
* Enable:

  * files.metadata.read
  * files.content.write

---

## 🚢 Deployment

Update all OAuth callbacks:

```bash
https://yourdomain.com/api/{provider}/callback
```

Then:

```bash
npm run build
npm start
```

---

## 🏗 Tech Stack

| Layer     | Technology   |
| --------- | ------------ |
| Framework | Next.js 14   |
| Editor    | Monaco       |
| Styling   | Tailwind CSS |
| State     | Zustand      |
| Icons     | Lucide       |

---

## 📦 Scripts

```bash
npm run dev              # Start dev server
npm run build            # Production build
npm start                # Start production server
npm run lint             # ESLint
npm run test             # Unit + E2E tests
npm run test:unit        # Vitest unit/integration tests
npm run test:watch       # Vitest watch mode
npm run test:e2e         # Playwright E2E tests
npm run test:e2e:headed  # E2E in visible browser
npm run verify           # Lint + typecheck + all tests
```

---

## 🧪 Testing

294 unit/integration tests (Vitest) + 39 E2E tests (Playwright) with 98% code coverage.

```bash
npm run test:unit                # Run all unit tests
npx vitest run --coverage        # With coverage report
npm run test:e2e:headed          # E2E in visible browser
```

Tests live in `tests/` organized by type: `lib/`, `store/`, `hooks/`, `components/`, `routes/`, and `e2e/`.

---

## 🧭 Philosophy

Dillinger is built around a simple idea:

> Writing tools should disappear.

No friction.
No clutter.
Just flow.

---

## 🤝 Contributing

PRs welcome. Open an issue first for major changes.

---

## 📄 License

MIT

---

### Overview (`packages/cli/README.md`)

# @dillinger/cli

Command-line interface for Dillinger markdown editor.

## Install

```bash
npm install @dillinger/cli
```

## Setup

```bash
export DILLINGER_API_KEY="your-api-key"
```

## Usage

```bash
# Render markdown to HTML
dlg render README.md

# Export to PDF
dlg export README.md --format pdf -o output.pdf

# Export to styled HTML
dlg export README.md --format html

# Convert HTML to markdown
dlg convert page.html

# Pipe from stdin
cat README.md | dlg render
echo "<h1>Hello</h1>" | dlg convert
```

---

### Overview (`packages/mcp/README.md`)

# @dillinger/mcp

MCP (Model Context Protocol) server for Dillinger. Lets LLMs use Dillinger as a native markdown tool.

## Setup

```bash
npm install
npm run build
```

## Usage with Claude Desktop

Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "dillinger": {
      "command": "node",
      "args": ["/path/to/packages/mcp/dist/index.js"],
      "env": {
        "DILLINGER_API_KEY": "your-api-key",
        "DILLINGER_URL": "https://dillinger.io"
      }
    }
  }
}
```

## Available Tools

| Tool | Description |
|------|-------------|
| `render_markdown` | Markdown to HTML with full plugin support |
| `export_pdf` | Markdown to PDF document |
| `export_html` | Markdown to styled HTML document |
| `convert_html_to_markdown` | HTML to clean markdown |

---

### AGENTS (`docs/AGENTS.md`)

# Agent Learnings - Next.js Migration

This document captures key learnings and debugging insights from the Next.js migration project.

---

## Vercel Deployment - Subdirectory Configuration

**Problem:** Vercel was deploying the old Express.js app at the repository root instead of the Next.js app in `next-app/` subdirectory.

**Solution:**
1. Set **Root Directory** to `next-app` in Vercel Dashboard (Settings → General)
2. Create `vercel.json` inside `next-app/` directory (not repository root)
3. Minimal config works best:
   ```json
   {
     "$schema": "https://openapi.vercel.sh/vercel.json",
     "framework": "nextjs"
   }
   ```

**Key Insight:** When Root Directory is set to a subdirectory, Vercel looks for `vercel.json` INSIDE that subdirectory, not at the repository root. The framework detection works automatically once the config file is in the right place.

---

## Next.js API Routes - Static Generation Errors

**Problem:** Production build failed with errors like:
```
Dynamic server usage: Route /api/bitbucket/branches couldn't be rendered statically because it used `cookies`
```

**Root Cause:** Next.js tries to statically pre-render API routes during build by default. Routes that use `cookies()` or other dynamic server features must be explicitly marked as dynamic.

**Solution:** Add to the top of every API route file:
```typescript
export const dynamic = "force-dynamic";
```

**Files affected:** All 45 API route files in:
- `app/api/google-drive/`
- `app/api/onedrive/`
- `app/api/github/`
- `app/api/dropbox/`
- `app/api/bitbucket/`
- `app/api/medium/`
- `app/api/export/`
- `app/api/upload/`

**Implementation:** Used bash script to add export to all route files:
```bash
for file in $(find app/api -name "route.ts"); do
  echo 'export const dynamic = "force-dynamic";
' | cat - "$file" > /tmp/tempfile && mv /tmp/tempfile "$file"
done
```

---

## Google Drive OAuth Scopes

**Problem 1:** Files API returned empty array even though user had `.md` files in Drive.

**Root Cause:** Using `https://www.googleapis.com/auth/drive.file` scope only grants access to files created by the app itself.

**Solution:** Changed to `https://www.googleapis.com/auth/drive` scope in `app/api/google-drive/oauth/route.ts` to access all user files.

**Problem 2:** Status endpoint returned 401 "missing authentication credential".

**Root Cause:** Missing `openid` and `email` scopes required for `/oauth2/v2/userinfo` endpoint.

**Solution:** Updated scope to:
```typescript
scope: "openid email https://www.googleapis.com/auth/drive"
```

---

## Google Drive - Preventing Duplicate Files

**Problem:** Saving a file with the same name created duplicates instead of overwriting.

**Root Cause:** Google Drive allows duplicate filenames by default. The save route only updated files when an explicit `fileId` was provided.

**Solution:** Modified `app/api/google-drive/save/route.ts` to:
1. Check if file with same name exists in target folder
2. If exists, use that file ID to update it
3. If not, create new file

**Implementation:**
```typescript
if (!targetFileId) {
  const query = `name = '${fileName}' and '${parentId}' in parents and trashed = false`;
  const searchResponse = await fetch(
    `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`,
    { headers: { Authorization: `Bearer ${accessToken}` } }
  );

  if (searchResponse.ok) {
    const searchData = await searchResponse.json();
    if (searchData.files?.length > 0) {
      targetFileId = searchData.files[0].id;
    }
  }
}

// Use PATCH if targetFileId exists, POST if creating new
const method = targetFileId ? "PATCH" : "POST";
```

---

## OneDrive OAuth Fixes

**Problem 1:** OAuth callback failed with "invalid_request - redirect_uri mismatch" for consumer accounts.

**Solution:** Changed OAuth endpoint from `/common/` to `/consumers/` in `app/api/onedrive/oauth/route.ts`:
```typescript
const authUrl = `https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?${params}`;
```

**Problem 2:** Graph API returned 401 Unauthorized.

**Solution:** Added `User.Read` scope:
```typescript
scope: "User.Read Files.ReadWrite.All offline_access"
```

**Problem 3:** Files API returned "Operation not supported" when using `$filter`.

**Solution:** Removed `$filter` query parameter (not supported on personal OneDrive) and implemented server-side filtering:
```typescript
const files = data.value.filter((item) => {
  return item.folder || item.name.toLowerCase().endsWith('.md');
});
```

---

## Bitbucket OAuth Fixes

**Problem 1:** Token exchange failed with "redirect_uri does not match".

**Solution:** Added `redirect_uri` parameter to token exchange POST body in `app/api/bitbucket/callback/route.ts`.

**Problem 2:** API returned 403 "credentials lack required privilege scopes".

**Solution:** Added account and repository scopes:
```typescript
scope: "account repository"
```

**Note:** User must also enable these permissions in Bitbucket OAuth app settings.

**Problem 3:** Files API returned "Resource not found" when listing directory contents.

**Solution:** Added trailing slash to directory paths:
```typescript
const apiPath = path ? `/${path}/` : "/";
const url = `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}/src/${branch}${apiPath}`;
```

---

## React SSR/Hydration Issues

**Problem:** `Cannot read properties of null (reading 'useContext')` error during build.

**Root Cause:** StoreProvider was calling `useStore()` hook during render, creating closure issues between server and client.

**Solution:** Changed to use `useStore.getState()` in `useEffect` instead:
```typescript
const hasHydrated = useRef(false);

useEffect(() => {
  if (!hasHydrated.current) {
    useStore.getState().hydrate();
    hasHydrated.current = true;
  }
}, []);
```

---

## Scroll Sync Feature

**Problem:** Scroll sync toggle didn't work - enabling it didn't sync scrolling.

**Root Cause:** Scroll listener was set up in `handleMount` callback, which captured stale `settings` value in closure.

**Solution:** Moved listener setup to `useEffect` with `settings.enableScrollSync` dependency in `components/editor/MonacoEditor.tsx`:
```typescript
useEffect(() => {
  const editor = editorRef.current;
  if (!editor) return;

  const disposable = editor.onDidScrollChange(() => {
    if (settings.enableScrollSync) {
      const scrollTop = editor.getScrollTop();
      const scrollHeight = editor.getScrollHeight() - editor.getLayoutInfo().height;
      const percent = scrollHeight > 0 ? scrollTop / scrollHeight : 0;
      setEditorScrollPercent(percent);
    }
  });

  return () => disposable.dispose();
}, [settings.enableScrollSync, setEditorScrollPercent]);
```

---

## Dark Mode Preview

**Problem:** Night mode only affected Monaco editor, not the markdown preview panel.

**Solution:** Added conditional dark class to preview in `components/preview/MarkdownPreview.tsx`:
```typescript
className={`preview-html h-full overflow-auto p-6 ${
  settings.enableNightMode ? 'dark bg-[#1e1e1e]' : 'bg-bg-primary'
}`}
```

And added comprehensive dark mode CSS to `app/globals.css`:
```css
.dark.preview-html {
  color: #d4d4d4;
}

.dark.preview-html code {
  background: #2d2d2d;
  color: #d4d4d4;
}

.dark.preview-html pre {
  background: #2d2d2d;
}
/* ... more dark mode styles */
```

---

## Production Deployment Checklist

Created comprehensive checklist at `docs/PRODUCTION-OAUTH-CHECKLIST.md` covering:
- All 6 OAuth service configurations
- Redirect URI updates for production domain
- Environment variable setup in Vercel
- Testing checklist after deployment

**Key Reminder:** Most OAuth apps allow multiple redirect URIs, so you can add production URLs without removing localhost for local development.

---

## Common Patterns

### OAuth Scope Issues
- Always check provider documentation for required scopes
- `drive.file` vs `drive` - understand access levels
- Some APIs require `openid` and `email` for userinfo endpoints
- Personal vs organizational accounts may have different requirements

### API Route Best Practices
- Mark all routes using `cookies()` as `force-dynamic`
- Use server-side filtering when provider API doesn't support it
- Always include `redirect_uri` in OAuth token exchange
- Handle trailing slashes carefully in REST APIs

### Vercel Subdirectory Deployment
1. Set Root Directory in dashboard
2. Put `vercel.json` in that subdirectory
3. Let framework auto-detection work
4. Avoid custom build commands unless necessary

---

## References

- [Next.js Dynamic Server Usage](https://nextjs.org/docs/messages/dynamic-server-error)
- [Vercel Root Directory Configuration](https://vercel.com/docs/projects/project-configuration#root-directory)
- [Google Drive API Scopes](https://developers.google.com/drive/api/guides/api-specific-auth)
- [Microsoft Graph API Permissions](https://learn.microsoft.com/en-us/graph/permissions-reference)
- [Bitbucket OAuth](https://developer.atlassian.com/cloud/bitbucket/oauth-2/)

---

### CLAUDE (`docs/CLAUDE.md`)

<claude-mem-context>
# Recent Activity



*No recent activity*
</claude-mem-context>

---

### PRODUCTION OAUTH CHECKLIST (`docs/PRODUCTION-OAUTH-CHECKLIST.md`)

# Production OAuth Configuration Checklist

Before deploying the Next.js app to production, update ALL OAuth applications with production URLs.

## Production URL Format
Your production redirect URIs will be:
- Base URL: `https://your-production-domain.com` (or `https://dillinger.vercel.app`)
- Callback pattern: `https://your-production-domain.com/api/{service}/callback`

---

## Google Drive

- [ ] Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
- [ ] Select your OAuth 2.0 Client ID
- [ ] Add production Authorized redirect URI:
  ```
  https://your-production-domain.com/api/google-drive/callback
  ```
- [ ] Update Vercel environment variables:
  ```
  GOOGLE_CLIENT_ID=your_client_id
  GOOGLE_CLIENT_SECRET=your_client_secret
  NEXT_PUBLIC_APP_URL=https://your-production-domain.com
  ```

---

## OneDrive (Microsoft)

- [ ] Go to [Azure Portal](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade)
- [ ] Select your app registration
- [ ] Go to "Authentication" → "Add a platform" → "Web"
- [ ] Add production redirect URI:
  ```
  https://your-production-domain.com/api/onedrive/callback
  ```
- [ ] Update Vercel environment variables:
  ```
  ONEDRIVE_CLIENT_ID=your_client_id
  ONEDRIVE_CLIENT_SECRET=your_client_secret
  NEXT_PUBLIC_APP_URL=https://your-production-domain.com
  ```

---

## GitHub

- [ ] Go to [GitHub Developer Settings](https://github.com/settings/developers)
- [ ] Select your OAuth App (or create new one for production)
- [ ] Update Authorization callback URL:
  ```
  https://your-production-domain.com/api/github/callback
  ```
- [ ] Update Vercel environment variables:
  ```
  GITHUB_CLIENT_ID=your_client_id
  GITHUB_CLIENT_SECRET=your_client_secret
  NEXT_PUBLIC_APP_URL=https://your-production-domain.com
  ```

---

## Dropbox

- [ ] Go to [Dropbox App Console](https://www.dropbox.com/developers/apps)
- [ ] Select your app
- [ ] Under "OAuth 2" → "Redirect URIs", add:
  ```
  https://your-production-domain.com/api/dropbox/callback
  ```
- [ ] Update Vercel environment variables:
  ```
  DROPBOX_APP_KEY=your_app_key
  DROPBOX_APP_SECRET=your_app_secret
  NEXT_PUBLIC_APP_URL=https://your-production-domain.com
  ```

---

## Bitbucket

- [ ] Go to [Bitbucket OAuth Consumers](https://bitbucket.org/account/settings/app-passwords/)
- [ ] Select your OAuth consumer
- [ ] Update Callback URL:
  ```
  https://your-production-domain.com/api/bitbucket/callback
  ```
- [ ] Update Vercel environment variables:
  ```
  BITBUCKET_CLIENT_ID=your_client_id
  BITBUCKET_CLIENT_SECRET=your_client_secret
  NEXT_PUBLIC_APP_URL=https://your-production-domain.com
  ```

---

## Vercel Environment Variables

In Vercel dashboard, set all environment variables for **Production** environment:

```bash
# Google Drive
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# OneDrive
ONEDRIVE_CLIENT_ID=
ONEDRIVE_CLIENT_SECRET=

# GitHub
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

# Dropbox
DROPBOX_APP_KEY=
DROPBOX_APP_SECRET=

# Bitbucket
BITBUCKET_CLIENT_ID=
BITBUCKET_CLIENT_SECRET=

# App URL (CRITICAL - used for all OAuth redirects)
NEXT_PUBLIC_APP_URL=https://your-production-domain.com

# Node Environment
NODE_ENV=production
```

---

## Testing Checklist (After Production Deploy)

After updating all OAuth apps and deploying to production:

- [ ] Test Google Drive: Connect → List Files → Save File → Import File
- [ ] Test OneDrive: Connect → List Files → Save File → Import File
- [ ] Test GitHub: Connect → List Repos → List Files → Save File
- [ ] Test Dropbox: Connect → List Files → Save File → Import File
- [ ] Test Bitbucket: Connect → List Repos → List Files → Save File
- [ ] Test all export formats: HTML, Markdown, PDF
- [ ] Test drag & drop file import
- [ ] Test image upload
- [ ] Test zen mode toggle
- [ ] Test scroll sync toggle
- [ ] Test night mode toggle

---

## Important Notes

1. **NEXT_PUBLIC_APP_URL is critical** - All OAuth callbacks use this to construct redirect URIs
2. **You can keep localhost URLs** in OAuth apps for local development alongside production URLs
3. **OAuth apps allow multiple redirect URIs** - add production without removing localhost
4. **Some providers require review** - If you added new scopes, some OAuth apps may need re-approval
5. **Clear cookies** - After deploying, users may need to reconnect their accounts with new tokens

---

## Quick Reference: All Callback URLs

```
https://your-production-domain.com/api/google-drive/callback
https://your-production-domain.com/api/onedrive/callback
https://your-production-domain.com/api/github/callback
https://your-production-domain.com/api/dropbox/callback
https://your-production-domain.com/api/bitbucket/callback
```

---

### Plans 2026 01 19 Nextjs Migration Design (`docs/plans/2026-01-19-nextjs-migration-design.md`)

# Dillinger Next.js Migration Design

**Date:** 2026-01-19
**Status:** Approved
**Scope:** Full stack migration from Angular.js to Next.js App Router

---

## Overview

Migrate the Dillinger markdown editor from Angular.js 1.8.3 to Next.js 14 with App Router, maintaining 100% functional parity and visual consistency.

## Key Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Framework | Next.js 14 (App Router) | Modern React, Vercel-optimized, API routes |
| Editor | Monaco Editor | Better maintained than Ace, VS Code familiar |
| Persistence | localStorage only | Matches original, no database needed |
| Cloud integrations | GitHub + Dropbox initially | Core services first, pattern for others |
| Markdown | markdown-it + plugins | Identical rendering output |
| Styling | Tailwind CSS | Design tokens extracted from SCSS |
| State | Zustand | Simple, performant, localStorage middleware |
| Export | Server-side PDF | md-to-pdf via API routes |

---

## Project Structure

```
dillinger-next/
├── app/
│   ├── layout.tsx              # Root layout with providers
│   ├── page.tsx                # Main editor page
│   ├── globals.css             # Tailwind + custom CSS
│   └── api/
│       ├── export/
│       │   ├── html/route.ts
│       │   ├── pdf/route.ts
│       │   └── markdown/route.ts
│       ├── convert/
│       │   └── html-to-md/route.ts
│       ├── github/
│       │   ├── oauth/route.ts
│       │   ├── callback/route.ts
│       │   ├── orgs/route.ts
│       │   ├── repos/route.ts
│       │   ├── branches/route.ts
│       │   ├── files/route.ts
│       │   └── save/route.ts
│       └── dropbox/
│           ├── oauth/route.ts
│           ├── callback/route.ts
│           ├── files/route.ts
│           └── save/route.ts
├── components/
│   ├── editor/
│   │   ├── MonacoEditor.tsx
│   │   └── EditorToolbar.tsx
│   ├── preview/
│   │   └── MarkdownPreview.tsx
│   ├── sidebar/
│   │   ├── Sidebar.tsx
│   │   └── DocumentList.tsx
│   ├── navbar/
│   │   └── Navbar.tsx
│   ├── modals/
│   │   ├── GitHubModal.tsx
│   │   ├── DropboxModal.tsx
│   │   ├── DeleteModal.tsx
│   │   └── SettingsModal.tsx
│   └── ui/
│       ├── Toast.tsx
│       ├── Button.tsx
│       ├── Modal.tsx
│       └── Switch.tsx
├── hooks/
│   ├── useDocuments.ts
│   ├── useUserSettings.ts
│   ├── useGitHub.ts
│   ├── useDropbox.ts
│   └── useMarkdown.ts
├── stores/
│   └── store.ts
├── lib/
│   ├── markdown.ts
│   └── storage.ts
└── tailwind.config.ts
```

---

## State Management

### Zustand Store

```typescript
interface Document {
  id: string;
  title: string;
  body: string;
  createdAt: Date;
  github?: {
    sha: string;
    path: string;
    repo: string;
    owner: string;
    branch: string;
  };
}

interface UserSettings {
  enableAutoSave: boolean;
  enableWordsCount: boolean;
  enableScrollSync: boolean;
  tabSize: number;
  keybindings: 'default' | 'vim' | 'emacs';
  enableNightMode: boolean;
}

interface AppState {
  documents: Document[];
  currentDocument: Document | null;
  settings: UserSettings;
  sidebarOpen: boolean;
  settingsOpen: boolean;
  previewVisible: boolean;
  zenMode: boolean;

  // Actions
  createDocument: () => void;
  selectDocument: (id: string) => void;
  deleteDocument: (id: string) => void;
  updateDocumentBody: (body: string) => void;
  updateDocumentTitle: (title: string) => void;
  updateSettings: (settings: Partial<UserSettings>) => void;
  hydrate: () => void;
  persist: () => void;
}
```

### Persistence

- `hydrate()` loads from localStorage on mount
- `persist()` saves to localStorage (debounced 2 seconds)
- Keys: `files`, `currentDocument`, `profileV3` (compatible with original)

---

## Tailwind Configuration

Design tokens extracted from legacy SCSS `_config.scss`:

```typescript
const config: Config = {
  content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        plum: '#35D7BB',
        'bg-primary': '#ffffff',
        'bg-sidebar': '#2B2F36',
        'bg-navbar': '#373D49',
        'bg-highlight': '#1D212A',
        'bg-button-save': '#4A5261',
        'text-primary': '#373D49',
        'text-invert': '#ffffff',
        'text-muted': '#A0AABF',
        'border-light': '#E8E8E8',
        'border-settings': '#4F535B',
        'icon-default': '#D3DAEA',
        'dropdown-link': '#D0D6E2',
        'switchery': '#4B5363',
      },
      fontFamily: {
        sans: ['"Source Sans Pro"', 'sans-serif'],
        serif: ['Georgia', 'Cambria', 'serif'],
        mono: ['"Ubuntu Mono"', 'Monaco', 'monospace'],
      },
      spacing: {
        sidebar: '270px',
        gutter: '32px',
      },
      zIndex: {
        sidebar: '1',
        page: '2',
        editor: '3',
        preview: '4',
        overlay: '5',
        navbar: '6',
        settings: '7',
      },
    },
  },
};
```

---

## Component Architecture

### Main Page Layout

```tsx
export default function EditorPage() {
  return (
    <div className="h-screen flex overflow-hidden">
      <Sidebar />
      <main className="flex-1 flex flex-col">
        <Navbar />
        <DocumentTitle />
        <div className="flex-1 flex">
          <ResizablePanel>
            <MonacoEditor />
          </ResizablePanel>
          <ResizablePanel>
            <MarkdownPreview />
          </ResizablePanel>
        </div>
      </main>
      <ModalProvider />
    </div>
  );
}
```

### Component Types

All major components are Client Components (`'use client'`) due to:
- Monaco Editor (browser API)
- localStorage access
- Event handlers and interactivity

---

## Monaco Editor Configuration

```tsx
<Editor
  height="100%"
  language="markdown"
  theme={settings.enableNightMode ? 'vs-dark' : 'vs'}
  value={currentDocument?.body ?? ''}
  onChange={handleChange}
  options={{
    fontSize: 14,
    fontFamily: '"Ubuntu Mono", Monaco, monospace',
    lineHeight: 24,
    wordWrap: 'on',
    minimap: { enabled: false },
    lineNumbers: 'off',
    tabSize: settings.tabSize,
    scrollBeyondLastLine: false,
    automaticLayout: true,
  }}
/>
```

### Keybindings

- Default: Standard Monaco
- Vim: `monaco-vim` package
- Emacs: `monaco-emacs` package

---

## Markdown Rendering

```typescript
import MarkdownIt from 'markdown-it';
// + 8 plugins: abbr, checkbox, deflist, footnote, ins, mark, sub, sup, toc
// + highlight.js for code syntax
// + KaTeX for math

export const md = new MarkdownIt({
  html: true,
  linkify: true,
  typographer: true,
  highlight: (str, lang) => hljs.highlight(str, { language: lang }).value,
})
  .use(markdownItAbbr)
  .use(markdownItCheckbox)
  // ... all plugins
```

---

## API Routes

### Export Routes

| Route | Method | Purpose |
|-------|--------|---------|
| `/api/export/pdf` | POST | Generate PDF via md-to-pdf |
| `/api/export/html` | POST | Generate styled HTML |
| `/api/export/markdown` | POST | Return raw markdown |
| `/api/convert/html-to-md` | POST | Convert HTML to Markdown |

### GitHub Routes

| Route | Method | Purpose |
|-------|--------|---------|
| `/api/github/oauth` | GET | Initiate OAuth flow |
| `/api/github/callback` | GET | OAuth callback, set cookie |
| `/api/github/orgs` | GET | List user organizations |
| `/api/github/repos` | GET | List repositories |
| `/api/github/branches` | GET | List branches |
| `/api/github/files` | POST | List/fetch files |
| `/api/github/save` | POST | Save file to repo |

### Dropbox Routes

Same pattern as GitHub.

### Token Storage

OAuth tokens stored in HTTP-only cookies:
- Secure, not accessible to JavaScript
- Automatically sent with API requests
- No session database required

---

## Notifications

Custom Toast context replacing Angular's diNotify:

```tsx
const { notify } = useToast();
notify('Document saved successfully');
notify('Uploading...', 5000);  // Custom duration
notify('Processing...', 0);    // Persistent
```

---

## Integration Hooks

```typescript
function useGitHub() {
  return {
    isAuthenticated,
    orgs, repos, branches, files,
    current: { owner, repo, branch, path, sha },
    authenticate,
    fetchOrgs,
    fetchRepos,
    fetchBranches,
    fetchFiles,
    saveFile,
  };
}
```

---

## Deferred Features

To be added in future iterations:
- Google Drive integration
- OneDrive integration
- Medium integration
- Bitbucket integration
- Zen mode (fullscreen editing)
- Image upload to Dropbox
- Scroll sync between editor and preview

---

## Dependencies

### Production

```json
{
  "next": "^14.0.0",
  "@monaco-editor/react": "^4.6.0",
  "markdown-it": "^14.0.0",
  "markdown-it-*": "various plugins",
  "highlight.js": "^11.0.0",
  "katex": "^0.16.0",
  "zustand": "^4.4.0",
  "lucide-react": "^0.300.0",
  "md-to-pdf": "^5.0.0",
  "breakdance": "^4.0.0"
}
```

### Development

```json
{
  "typescript": "^5.0.0",
  "tailwindcss": "^3.4.0",
  "@types/node": "^20.0.0",
  "@types/react": "^18.0.0"
}
```

---

## Migration Compatibility

localStorage keys maintained for seamless migration:
- `files` - Document array
- `currentDocument` - Active document
- `profileV3` - User settings

Users' existing documents will be preserved when switching to the new app.

---

### Plans 2026 01 19 Nextjs Migration Phase2 (`docs/plans/2026-01-19-nextjs-migration-phase2.md`)

# Dillinger Next.js Migration - Phase 2: Cloud Integrations

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add GitHub and Dropbox OAuth integrations for importing and saving markdown files to cloud services.

**Architecture:** OAuth tokens stored in HTTP-only cookies, API routes handle all cloud service communication, React hooks manage client-side state for file browsing modals.

**Tech Stack:** Next.js API Routes, GitHub REST API, Dropbox SDK, HTTP-only cookies for token storage

---

## Prerequisites

- Phase 1 complete (core editor working)
- Environment variables configured for OAuth:
  - `GITHUB_CLIENT_ID`
  - `GITHUB_CLIENT_SECRET`
  - `DROPBOX_APP_KEY`
  - `DROPBOX_APP_SECRET`
  - `NEXT_PUBLIC_BASE_URL` (e.g., `http://localhost:3000`)

---

## Phase 2.1: Environment Setup

### Task 2.1.1: Create Environment Configuration

**Files:**
- Create: `next-app/.env.local.example`
- Create: `next-app/.env.local`
- Modify: `next-app/.gitignore`

**Step 1: Create environment example file**

Create `next-app/.env.local.example`:

```bash
# GitHub OAuth
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret

# Dropbox OAuth
DROPBOX_APP_KEY=your_dropbox_app_key
DROPBOX_APP_SECRET=your_dropbox_app_secret

# App URL (for OAuth callbacks)
NEXT_PUBLIC_BASE_URL=http://localhost:3000
```

**Step 2: Create actual .env.local (user fills in values)**

```bash
cp .env.local.example .env.local
```
User must fill in actual OAuth credentials.

**Step 3: Ensure .env.local is gitignored**

Verify `.gitignore` contains:
```
.env*.local
```

**Step 4: Commit**

```bash
git add .env.local.example && git commit -m "docs: add environment variables example for OAuth"
```

---

## Phase 2.2: GitHub Integration

### Task 2.2.1: Create GitHub OAuth Routes

**Files:**
- Create: `next-app/app/api/github/oauth/route.ts`
- Create: `next-app/app/api/github/callback/route.ts`

**Step 1: Create OAuth initiation route**

Create `next-app/app/api/github/oauth/route.ts`:

```typescript
import { NextResponse } from "next/server";

export async function GET() {
  const clientId = process.env.GITHUB_CLIENT_ID;
  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;

  if (!clientId || !baseUrl) {
    return NextResponse.json(
      { error: "GitHub OAuth not configured" },
      { status: 500 }
    );
  }

  const redirectUri = `${baseUrl}/api/github/callback`;
  const scope = "repo,user";

  const authUrl = new URL("https://github.com/login/oauth/authorize");
  authUrl.searchParams.set("client_id", clientId);
  authUrl.searchParams.set("redirect_uri", redirectUri);
  authUrl.searchParams.set("scope", scope);

  return NextResponse.redirect(authUrl.toString());
}
```

**Step 2: Create OAuth callback route**

Create `next-app/app/api/github/callback/route.ts`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 3: Commit**

```bash
git add app/api/github/oauth/route.ts app/api/github/callback/route.ts && git commit -m "feat: add GitHub OAuth routes"
```

---

### Task 2.2.2: Create GitHub Status and Unlink Routes

**Files:**
- Create: `next-app/app/api/github/status/route.ts`
- Create: `next-app/app/api/github/unlink/route.ts`

**Step 1: Create status check route**

Create `next-app/app/api/github/status/route.ts`:

```typescript
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function GET() {
  const cookieStore = await cookies();
  const token = cookieStore.get("github_token")?.value;

  if (!token) {
    return NextResponse.json({ connected: false });
  }

  try {
    // Verify token is still valid by fetching user
    const response = await fetch("https://api.github.com/user", {
      headers: {
        Authorization: `Bearer ${token}`,
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      return NextResponse.json({ connected: false });
    }

    const user = await response.json();

    return NextResponse.json({
      connected: true,
      user: {
        login: user.login,
        name: user.name,
        avatar_url: user.avatar_url,
      },
    });
  } catch (error) {
    return NextResponse.json({ connected: false });
  }
}
```

**Step 2: Create unlink route**

Create `next-app/app/api/github/unlink/route.ts`:

```typescript
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function POST() {
  const cookieStore = await cookies();
  cookieStore.delete("github_token");

  return NextResponse.json({ success: true });
}
```

**Step 3: Commit**

```bash
git add app/api/github/status/route.ts app/api/github/unlink/route.ts && git commit -m "feat: add GitHub status and unlink routes"
```

---

### Task 2.2.3: Create GitHub Data Routes

**Files:**
- Create: `next-app/app/api/github/orgs/route.ts`
- Create: `next-app/app/api/github/repos/route.ts`
- Create: `next-app/app/api/github/branches/route.ts`
- Create: `next-app/app/api/github/files/route.ts`

**Step 1: Create orgs route**

Create `next-app/app/api/github/orgs/route.ts`:

```typescript
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function GET() {
  const cookieStore = await cookies();
  const token = cookieStore.get("github_token")?.value;

  if (!token) {
    return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
  }

  try {
    // Fetch user first
    const userResponse = await fetch("https://api.github.com/user", {
      headers: {
        Authorization: `Bearer ${token}`,
        Accept: "application/vnd.github.v3+json",
      },
    });

    const user = await userResponse.json();

    // Fetch organizations
    const orgsResponse = await fetch("https://api.github.com/user/orgs", {
      headers: {
        Authorization: `Bearer ${token}`,
        Accept: "application/vnd.github.v3+json",
      },
    });

    const orgs = await orgsResponse.json();

    // Prepend user as first "org" option
    const allOrgs = [{ login: user.login, type: "user" }, ...orgs];

    return NextResponse.json(allOrgs);
  } catch (error) {
    console.error("GitHub orgs error:", error);
    return NextResponse.json({ error: "Failed to fetch organizations" }, { status: 500 });
  }
}
```

**Step 2: Create repos route**

Create `next-app/app/api/github/repos/route.ts`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 3: Create branches route**

Create `next-app/app/api/github/branches/route.ts`:

```typescript
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function GET(request: NextRequest) {
  const cookieStore = await cookies();
  const token = cookieStore.get("github_token")?.value;

  if (!token) {
    return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
  }

  const searchParams = request.nextUrl.searchParams;
  const owner = searchParams.get("owner");
  const repo = searchParams.get("repo");

  if (!owner || !repo) {
    return NextResponse.json({ error: "Owner and repo are required" }, { status: 400 });
  }

  try {
    const response = await fetch(
      `https://api.github.com/repos/${owner}/${repo}/branches`,
      {
        headers: {
          Authorization: `Bearer ${token}`,
          Accept: "application/vnd.github.v3+json",
        },
      }
    );

    if (!response.ok) {
      throw new Error(`GitHub API error: ${response.status}`);
    }

    const branches = await response.json();

    return NextResponse.json(
      branches.map((branch: any) => ({
        name: branch.name,
        sha: branch.commit.sha,
      }))
    );
  } catch (error) {
    console.error("GitHub branches error:", error);
    return NextResponse.json({ error: "Failed to fetch branches" }, { status: 500 });
  }
}
```

**Step 4: Create files route**

Create `next-app/app/api/github/files/route.ts`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 5: Commit**

```bash
git add app/api/github/orgs/route.ts app/api/github/repos/route.ts app/api/github/branches/route.ts app/api/github/files/route.ts && git commit -m "feat: add GitHub data routes (orgs, repos, branches, files)"
```

---

### Task 2.2.4: Create GitHub Save Route

**Files:**
- Create: `next-app/app/api/github/save/route.ts`

**Step 1: Create save route**

Create `next-app/app/api/github/save/route.ts`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add app/api/github/save/route.ts && git commit -m "feat: add GitHub save route"
```

---

### Task 2.2.5: Create GitHub Hook

**Files:**
- Create: `next-app/hooks/useGitHub.ts`

**Step 1: Create GitHub hook**

Create `next-app/hooks/useGitHub.ts`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add hooks/useGitHub.ts && git commit -m "feat: add useGitHub hook for GitHub integration"
```

---

### Task 2.2.6: Create GitHub Modal

**Files:**
- Create: `next-app/components/modals/GitHubModal.tsx`

**Step 1: Create GitHub modal component**

Create `next-app/components/modals/GitHubModal.tsx`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add components/modals/GitHubModal.tsx && git commit -m "feat: add GitHubModal component"
```

---

## Phase 2.3: Dropbox Integration

### Task 2.3.1: Install Dropbox SDK

**Step 1: Install dependency**

```bash
npm install dropbox
```

**Step 2: Commit**

```bash
git add package.json package-lock.json && git commit -m "feat: add Dropbox SDK dependency"
```

---

### Task 2.3.2: Create Dropbox OAuth Routes

**Files:**
- Create: `next-app/app/api/dropbox/oauth/route.ts`
- Create: `next-app/app/api/dropbox/callback/route.ts`
- Create: `next-app/app/api/dropbox/status/route.ts`
- Create: `next-app/app/api/dropbox/unlink/route.ts`

**Step 1: Create OAuth initiation route**

Create `next-app/app/api/dropbox/oauth/route.ts`:

```typescript
import { NextResponse } from "next/server";
import { Dropbox } from "dropbox";

export async function GET() {
  const clientId = process.env.DROPBOX_APP_KEY;
  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;

  if (!clientId || !baseUrl) {
    return NextResponse.json(
      { error: "Dropbox OAuth not configured" },
      { status: 500 }
    );
  }

  const redirectUri = `${baseUrl}/api/dropbox/callback`;

  const dbx = new Dropbox({ clientId });
  const authUrl = await dbx.auth.getAuthenticationUrl(
    redirectUri,
    undefined,
    "code",
    "offline",
    undefined,
    undefined,
    false
  );

  return NextResponse.redirect(authUrl.toString());
}
```

**Step 2: Create callback route**

Create `next-app/app/api/dropbox/callback/route.ts`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 3: Create status route**

Create `next-app/app/api/dropbox/status/route.ts`:

```typescript
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { Dropbox } from "dropbox";

export async function GET() {
  const cookieStore = await cookies();
  const tokenCookie = cookieStore.get("dropbox_token")?.value;

  if (!tokenCookie) {
    return NextResponse.json({ connected: false });
  }

  try {
    const { access_token } = JSON.parse(tokenCookie);

    const dbx = new Dropbox({ accessToken: access_token });
    const account = await dbx.usersGetCurrentAccount();

    return NextResponse.json({
      connected: true,
      user: {
        name: account.result.name.display_name,
        email: account.result.email,
      },
    });
  } catch (error) {
    return NextResponse.json({ connected: false });
  }
}
```

**Step 4: Create unlink route**

Create `next-app/app/api/dropbox/unlink/route.ts`:

```typescript
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function POST() {
  const cookieStore = await cookies();
  cookieStore.delete("dropbox_token");

  return NextResponse.json({ success: true });
}
```

**Step 5: Commit**

```bash
git add app/api/dropbox/ && git commit -m "feat: add Dropbox OAuth routes"
```

---

### Task 2.3.3: Create Dropbox Data Routes

**Files:**
- Create: `next-app/app/api/dropbox/files/route.ts`
- Create: `next-app/app/api/dropbox/save/route.ts`

**Step 1: Create files route**

Create `next-app/app/api/dropbox/files/route.ts`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Create save route**

Create `next-app/app/api/dropbox/save/route.ts`:

```typescript
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { Dropbox } from "dropbox";

export async function POST(request: NextRequest) {
  const cookieStore = await cookies();
  const tokenCookie = cookieStore.get("dropbox_token")?.value;

  if (!tokenCookie) {
    return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
  }

  try {
    const { path, content } = await request.json();

    if (!path || content === undefined) {
      return NextResponse.json(
        { error: "Path and content are required" },
        { status: 400 }
      );
    }

    const { access_token } = JSON.parse(tokenCookie);
    const dbx = new Dropbox({ accessToken: access_token });

    // Ensure path starts with /
    const filePath = path.startsWith("/") ? path : `/${path}`;

    const response = await dbx.filesUpload({
      path: filePath,
      contents: content,
      mode: { ".tag": "overwrite" },
    });

    return NextResponse.json({
      success: true,
      path: response.result.path_lower,
      name: response.result.name,
    });
  } catch (error) {
    console.error("Dropbox save error:", error);
    return NextResponse.json({ error: "Failed to save file" }, { status: 500 });
  }
}
```

**Step 3: Commit**

```bash
git add app/api/dropbox/files/route.ts app/api/dropbox/save/route.ts && git commit -m "feat: add Dropbox files and save routes"
```

---

### Task 2.3.4: Create Dropbox Hook

**Files:**
- Create: `next-app/hooks/useDropbox.ts`

**Step 1: Create Dropbox hook**

Create `next-app/hooks/useDropbox.ts`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add hooks/useDropbox.ts && git commit -m "feat: add useDropbox hook for Dropbox integration"
```

---

### Task 2.3.5: Create Dropbox Modal

**Files:**
- Create: `next-app/components/modals/DropboxModal.tsx`

**Step 1: Create Dropbox modal component**

Create `next-app/components/modals/DropboxModal.tsx`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add components/modals/DropboxModal.tsx && git commit -m "feat: add DropboxModal component"
```

---

## Phase 2.4: Integration with Main App

### Task 2.4.1: Update Sidebar with Cloud Services

**Files:**
- Modify: `next-app/components/sidebar/Sidebar.tsx`

**Step 1: Update Sidebar to include cloud service buttons**

Update `next-app/components/sidebar/Sidebar.tsx`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add components/sidebar/Sidebar.tsx && git commit -m "feat: integrate GitHub and Dropbox into Sidebar"
```

---

### Task 2.4.2: Final Verification

**Step 1: Run development server**

```bash
npm run dev
```

**Step 2: Manual testing checklist**

- [ ] GitHub OAuth flow works (link/unlink)
- [ ] Can browse GitHub orgs → repos → branches → files
- [ ] Can import markdown file from GitHub
- [ ] Can save document to GitHub (new file and update existing)
- [ ] Dropbox OAuth flow works (link/unlink)
- [ ] Can browse Dropbox folders and files
- [ ] Can import markdown file from Dropbox
- [ ] Can save document to Dropbox
- [ ] All Phase 1 features still work

**Step 3: Run build**

```bash
npm run build
```
Expected: Build succeeds without errors

**Step 4: Final commit**

```bash
git add -A && git commit -m "feat: complete Phase 2 - GitHub and Dropbox integrations"
```

---

## Summary

Phase 2 adds cloud integrations:

**Completed:**
- GitHub OAuth flow (connect/disconnect)
- GitHub file browsing (orgs → repos → branches → files)
- GitHub import and save
- Dropbox OAuth flow (connect/disconnect)
- Dropbox file browsing
- Dropbox import and save
- Updated Sidebar with Services, Import, and Save sections

**Environment Variables Required:**
```
GITHUB_CLIENT_ID=xxx
GITHUB_CLIENT_SECRET=xxx
DROPBOX_APP_KEY=xxx
DROPBOX_APP_SECRET=xxx
NEXT_PUBLIC_BASE_URL=http://localhost:3000
```

**Deferred to Phase 3:**
- Google Drive integration
- OneDrive integration
- Medium integration
- Bitbucket integration
- Resizable editor/preview panels
- Vim/Emacs keybindings
- Word count display

---

### Plans 2026 01 19 Nextjs Migration (`docs/plans/2026-01-19-nextjs-migration.md`)

# Dillinger Next.js Migration Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Migrate Dillinger from Angular.js to Next.js 14 with App Router, maintaining 100% functional parity.

**Architecture:** Single-page editor app with Zustand state management, Monaco Editor, markdown-it rendering, and Next.js API routes for exports and OAuth. All state persisted to localStorage.

**Tech Stack:** Next.js 14, TypeScript, Tailwind CSS, Zustand, Monaco Editor, markdown-it, Lucide React

---

## Phase 1: Project Setup

### Task 1.1: Initialize Next.js Project

**Files:**
- Create: `next-app/` (new Next.js project)
- Create: `next-app/package.json`
- Create: `next-app/tsconfig.json`
- Create: `next-app/tailwind.config.ts`

**Step 1: Create Next.js app with TypeScript and Tailwind**

```bash
cd /Users/joemccann/dev/vercel/dillinger/.worktrees/nextjs-migration
npx create-next-app@latest next-app --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*" --use-npm
```

**Step 2: Verify project created**

```bash
ls next-app/
```
Expected: `app/`, `public/`, `package.json`, `tsconfig.json`, `tailwind.config.ts`

**Step 3: Commit**

```bash
cd next-app && git add -A && git commit -m "feat: initialize Next.js 14 project with TypeScript and Tailwind"
```

---

### Task 1.2: Install Core Dependencies

**Files:**
- Modify: `next-app/package.json`

**Step 1: Install editor and state management**

```bash
cd /Users/joemccann/dev/vercel/dillinger/.worktrees/nextjs-migration/next-app
npm install @monaco-editor/react zustand lucide-react
```

**Step 2: Install markdown rendering stack**

```bash
npm install markdown-it markdown-it-abbr markdown-it-checkbox markdown-it-deflist markdown-it-footnote markdown-it-ins markdown-it-mark markdown-it-sub markdown-it-sup markdown-it-toc-done-right highlight.js katex
```

**Step 3: Install types**

```bash
npm install -D @types/markdown-it
```

**Step 4: Verify installation**

```bash
npm ls @monaco-editor/react zustand markdown-it
```
Expected: All packages listed without errors

**Step 5: Commit**

```bash
git add package.json package-lock.json && git commit -m "feat: add core dependencies (Monaco, Zustand, markdown-it)"
```

---

### Task 1.3: Configure Tailwind with Design Tokens

**Files:**
- Modify: `next-app/tailwind.config.ts`

**Step 1: Update Tailwind config with Dillinger design tokens**

Replace contents of `next-app/tailwind.config.ts`:

```typescript
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  darkMode: "class",
  theme: {
    extend: {
      colors: {
        plum: "#35D7BB",
        "bg-primary": "#ffffff",
        "bg-sidebar": "#2B2F36",
        "bg-navbar": "#373D49",
        "bg-highlight": "#1D212A",
        "bg-button-save": "#4A5261",
        "text-primary": "#373D49",
        "text-invert": "#ffffff",
        "text-muted": "#A0AABF",
        "border-light": "#E8E8E8",
        "border-settings": "#4F535B",
        "icon-default": "#D3DAEA",
        "dropdown-link": "#D0D6E2",
        switchery: "#4B5363",
      },
      fontFamily: {
        sans: ['"Source Sans Pro"', '"Helvetica Neue"', "Helvetica", "Arial", "sans-serif"],
        serif: ["Georgia", "Cambria", "serif"],
        mono: ['"Ubuntu Mono"', "Monaco", "monospace"],
      },
      spacing: {
        sidebar: "270px",
        gutter: "32px",
      },
      zIndex: {
        sidebar: "1",
        page: "2",
        editor: "3",
        preview: "4",
        overlay: "5",
        navbar: "6",
        settings: "7",
      },
    },
  },
  plugins: [],
};

export default config;
```

**Step 2: Add Google Fonts to layout**

Update `next-app/app/layout.tsx`:

```typescript
import type { Metadata } from "next";
import { Source_Sans_3 } from "next/font/google";
import "./globals.css";

const sourceSans = Source_Sans_3({
  subsets: ["latin"],
  variable: "--font-source-sans",
});

export const metadata: Metadata = {
  title: "Dillinger - Online Markdown Editor",
  description: "The last Markdown editor you'll ever need",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={`${sourceSans.variable} font-sans`}>{children}</body>
    </html>
  );
}
```

**Step 3: Verify build**

```bash
npm run build
```
Expected: Build succeeds

**Step 4: Commit**

```bash
git add tailwind.config.ts app/layout.tsx && git commit -m "feat: configure Tailwind with Dillinger design tokens"
```

---

## Phase 2: State Management

### Task 2.1: Create Zustand Store Types

**Files:**
- Create: `next-app/lib/types.ts`

**Step 1: Create types file**

Create `next-app/lib/types.ts`:

```typescript
export interface Document {
  id: string;
  title: string;
  body: string;
  createdAt: string;
  github?: {
    sha: string;
    path: string;
    repo: string;
    owner: string;
    branch: string;
  };
}

export interface UserSettings {
  enableAutoSave: boolean;
  enableWordsCount: boolean;
  enableCharactersCount: boolean;
  enableScrollSync: boolean;
  tabSize: number;
  keybindings: "default" | "vim" | "emacs";
  enableNightMode: boolean;
  enableGitHubComment: boolean;
}

export const DEFAULT_SETTINGS: UserSettings = {
  enableAutoSave: true,
  enableWordsCount: true,
  enableCharactersCount: true,
  enableScrollSync: false,
  tabSize: 4,
  keybindings: "default",
  enableNightMode: false,
  enableGitHubComment: true,
};

export const DEFAULT_DOCUMENT_BODY = `# Welcome to Dillinger

Dillinger is a cloud-enabled, mobile-ready, offline-storage compatible,
AngularJS-powered HTML5 Markdown editor.

- Type some Markdown on the left
- See HTML in the right
- Magic

## Features

- Import a HTML file and watch it magically convert to Markdown
- Drag and drop images (requires your Dropbox account be linked)
- Import and save files from GitHub, Dropbox, Google Drive and One Drive
- Drag and drop markdown and HTML files into Dillinger
- Export documents as Markdown, HTML and PDF
`;
```

**Step 2: Commit**

```bash
git add lib/types.ts && git commit -m "feat: add TypeScript types for documents and settings"
```

---

### Task 2.2: Create Zustand Store

**Files:**
- Create: `next-app/stores/store.ts`

**Step 1: Create store with persistence**

Create `next-app/stores/store.ts`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add stores/store.ts && git commit -m "feat: add Zustand store with document and settings management"
```

---

### Task 2.3: Create Store Provider

**Files:**
- Create: `next-app/components/providers/StoreProvider.tsx`

**Step 1: Create provider component**

Create directory and file `next-app/components/providers/StoreProvider.tsx`:

```typescript
"use client";

import { useEffect } from "react";
import { useStore } from "@/stores/store";

export function StoreProvider({ children }: { children: React.ReactNode }) {
  const hydrate = useStore((state) => state.hydrate);

  useEffect(() => {
    hydrate();
  }, [hydrate]);

  return <>{children}</>;
}
```

**Step 2: Update layout to use provider**

Update `next-app/app/layout.tsx`:

```typescript
import type { Metadata } from "next";
import { Source_Sans_3 } from "next/font/google";
import { StoreProvider } from "@/components/providers/StoreProvider";
import "./globals.css";

const sourceSans = Source_Sans_3({
  subsets: ["latin"],
  variable: "--font-source-sans",
});

export const metadata: Metadata = {
  title: "Dillinger - Online Markdown Editor",
  description: "The last Markdown editor you'll ever need",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={`${sourceSans.variable} font-sans`}>
        <StoreProvider>{children}</StoreProvider>
      </body>
    </html>
  );
}
```

**Step 3: Verify build**

```bash
npm run build
```
Expected: Build succeeds

**Step 4: Commit**

```bash
git add components/providers/StoreProvider.tsx app/layout.tsx && git commit -m "feat: add StoreProvider for state hydration"
```

---

## Phase 3: Markdown Rendering

### Task 3.1: Create Markdown Library

**Files:**
- Create: `next-app/lib/markdown.ts`

**Step 1: Create markdown-it configuration**

Create `next-app/lib/markdown.ts`:

```typescript
import MarkdownIt from "markdown-it";
import markdownItAbbr from "markdown-it-abbr";
import markdownItCheckbox from "markdown-it-checkbox";
import markdownItDeflist from "markdown-it-deflist";
import markdownItFootnote from "markdown-it-footnote";
import markdownItIns from "markdown-it-ins";
import markdownItMark from "markdown-it-mark";
import markdownItSub from "markdown-it-sub";
import markdownItSup from "markdown-it-sup";
import hljs from "highlight.js";

export const md = new MarkdownIt({
  html: true,
  linkify: true,
  typographer: true,
  highlight: (str: string, lang: string): string => {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value;
      } catch {
        // Fall through to default
      }
    }
    return ""; // Use external default escaping
  },
})
  .use(markdownItAbbr)
  .use(markdownItCheckbox)
  .use(markdownItDeflist)
  .use(markdownItFootnote)
  .use(markdownItIns)
  .use(markdownItMark)
  .use(markdownItSub)
  .use(markdownItSup);

export function renderMarkdown(content: string): string {
  return md.render(content);
}
```

**Step 2: Commit**

```bash
git add lib/markdown.ts && git commit -m "feat: add markdown-it configuration with all plugins"
```

---

### Task 3.2: Create Preview Component

**Files:**
- Create: `next-app/components/preview/MarkdownPreview.tsx`
- Modify: `next-app/app/globals.css`

**Step 1: Create preview component**

Create `next-app/components/preview/MarkdownPreview.tsx`:

```typescript
"use client";

import { useMemo } from "react";
import { useStore } from "@/stores/store";
import { renderMarkdown } from "@/lib/markdown";

export function MarkdownPreview() {
  const currentDocument = useStore((state) => state.currentDocument);

  const html = useMemo(() => {
    if (!currentDocument?.body) return "";
    return renderMarkdown(currentDocument.body);
  }, [currentDocument?.body]);

  return (
    <div
      id="preview"
      className="preview-html h-full overflow-auto p-6 bg-bg-primary"
      dangerouslySetInnerHTML={{ __html: html }}
    />
  );
}
```

**Step 2: Add preview styles to globals.css**

Append to `next-app/app/globals.css`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 3: Verify build**

```bash
npm run build
```
Expected: Build succeeds

**Step 4: Commit**

```bash
git add components/preview/MarkdownPreview.tsx app/globals.css && git commit -m "feat: add MarkdownPreview component with styling"
```

---

## Phase 4: Monaco Editor

### Task 4.1: Create Monaco Editor Component

**Files:**
- Create: `next-app/components/editor/MonacoEditor.tsx`

**Step 1: Create editor component**

Create `next-app/components/editor/MonacoEditor.tsx`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add components/editor/MonacoEditor.tsx && git commit -m "feat: add Monaco Editor component with auto-save"
```

---

## Phase 5: UI Components

### Task 5.1: Create Toast Notification System

**Files:**
- Create: `next-app/components/ui/Toast.tsx`

**Step 1: Create toast component and context**

Create `next-app/components/ui/Toast.tsx`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Add animation to Tailwind config**

Update `next-app/tailwind.config.ts` to add animation:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 3: Add ToastProvider to layout**

Update `next-app/app/layout.tsx`:

```typescript
import type { Metadata } from "next";
import { Source_Sans_3 } from "next/font/google";
import { StoreProvider } from "@/components/providers/StoreProvider";
import { ToastProvider } from "@/components/ui/Toast";
import "./globals.css";

const sourceSans = Source_Sans_3({
  subsets: ["latin"],
  variable: "--font-source-sans",
});

export const metadata: Metadata = {
  title: "Dillinger - Online Markdown Editor",
  description: "The last Markdown editor you'll ever need",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={`${sourceSans.variable} font-sans`}>
        <StoreProvider>
          <ToastProvider>{children}</ToastProvider>
        </StoreProvider>
      </body>
    </html>
  );
}
```

**Step 4: Commit**

```bash
git add components/ui/Toast.tsx tailwind.config.ts app/layout.tsx && git commit -m "feat: add Toast notification system"
```

---

### Task 5.2: Create Sidebar Component

**Files:**
- Create: `next-app/components/sidebar/Sidebar.tsx`
- Create: `next-app/components/sidebar/DocumentList.tsx`

**Step 1: Create DocumentList component**

Create `next-app/components/sidebar/DocumentList.tsx`:

```typescript
"use client";

import { useStore } from "@/stores/store";
import { FileText } from "lucide-react";

export function DocumentList() {
  const documents = useStore((state) => state.documents);
  const currentDocument = useStore((state) => state.currentDocument);
  const selectDocument = useStore((state) => state.selectDocument);

  return (
    <ul className="space-y-1">
      {documents.map((doc) => (
        <li key={doc.id}>
          <button
            onClick={() => selectDocument(doc.id)}
            className={`w-full text-left px-3 py-2 rounded flex items-center gap-2 text-sm transition-colors ${
              currentDocument?.id === doc.id
                ? "bg-bg-highlight text-text-invert"
                : "text-dropdown-link hover:bg-bg-highlight/50"
            }`}
          >
            <FileText size={16} />
            <span className="truncate">{doc.title}</span>
          </button>
        </li>
      ))}
    </ul>
  );
}
```

**Step 2: Create Sidebar component**

Create `next-app/components/sidebar/Sidebar.tsx`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 3: Commit**

```bash
git add components/sidebar/ && git commit -m "feat: add Sidebar with document list and actions"
```

---

### Task 5.3: Create Navbar Component

**Files:**
- Create: `next-app/components/navbar/Navbar.tsx`

**Step 1: Create navbar component**

Create `next-app/components/navbar/Navbar.tsx`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add components/navbar/Navbar.tsx && git commit -m "feat: add Navbar with export dropdown and toggles"
```

---

### Task 5.4: Create Document Title Component

**Files:**
- Create: `next-app/components/editor/DocumentTitle.tsx`

**Step 1: Create editable title component**

Create `next-app/components/editor/DocumentTitle.tsx`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add components/editor/DocumentTitle.tsx && git commit -m "feat: add editable DocumentTitle component"
```

---

### Task 5.5: Create Settings Modal

**Files:**
- Create: `next-app/components/modals/SettingsModal.tsx`

**Step 1: Create settings modal component**

Create `next-app/components/modals/SettingsModal.tsx`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add components/modals/SettingsModal.tsx && git commit -m "feat: add SettingsModal component"
```

---

## Phase 6: Main Page Assembly

### Task 6.1: Create Main Editor Page

**Files:**
- Modify: `next-app/app/page.tsx`

**Step 1: Assemble the main page**

Update `next-app/app/page.tsx`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Verify dev server works**

```bash
npm run dev
```
Expected: App runs at http://localhost:3000 with editor and preview visible

**Step 3: Commit**

```bash
git add app/page.tsx && git commit -m "feat: assemble main editor page with all components"
```

---

## Phase 7: API Routes

### Task 7.1: Create Markdown Export Route

**Files:**
- Create: `next-app/app/api/export/markdown/route.ts`

**Step 1: Create markdown export API**

Create `next-app/app/api/export/markdown/route.ts`:

```typescript
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  try {
    const { markdown, title } = await request.json();

    if (!markdown) {
      return NextResponse.json(
        { error: "Markdown content is required" },
        { status: 400 }
      );
    }

    const filename = `${title || "document"}.md`;

    return new NextResponse(markdown, {
      headers: {
        "Content-Type": "text/markdown",
        "Content-Disposition": `attachment; filename="${filename}"`,
      },
    });
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to export markdown" },
      { status: 500 }
    );
  }
}
```

**Step 2: Commit**

```bash
git add app/api/export/markdown/route.ts && git commit -m "feat: add markdown export API route"
```

---

### Task 7.2: Create HTML Export Route

**Files:**
- Create: `next-app/app/api/export/html/route.ts`

**Step 1: Create HTML export API**

Create `next-app/app/api/export/html/route.ts`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Commit**

```bash
git add app/api/export/html/route.ts && git commit -m "feat: add HTML export API route with styling"
```

---

### Task 7.3: Create PDF Export Route

**Files:**
- Create: `next-app/app/api/export/pdf/route.ts`
- Modify: `next-app/package.json` (add md-to-pdf)

**Step 1: Install md-to-pdf**

```bash
npm install md-to-pdf
```

**Step 2: Create PDF export API**

Create `next-app/app/api/export/pdf/route.ts`:

```typescript
import { NextRequest, NextResponse } from "next/server";
import { mdToPdf } from "md-to-pdf";

export async function POST(request: NextRequest) {
  try {
    const { markdown, title } = await request.json();

    if (!markdown) {
      return NextResponse.json(
        { error: "Markdown content is required" },
        { status: 400 }
      );
    }

    const pdf = await mdToPdf(
      { content: markdown },
      {
        pdf_options: {
          format: "A4",
          margin: { top: "20mm", bottom: "20mm", left: "20mm", right: "20mm" },
        },
      }
    );

    if (!pdf || !pdf.content) {
      throw new Error("PDF generation failed");
    }

    const filename = `${title || "document"}.pdf`;

    return new NextResponse(pdf.content, {
      headers: {
        "Content-Type": "application/pdf",
        "Content-Disposition": `attachment; filename="${filename}"`,
      },
    });
  } catch (error) {
    console.error("PDF export error:", error);
    return NextResponse.json(
      { error: "Failed to export PDF" },
      { status: 500 }
    );
  }
}
```

**Step 3: Commit**

```bash
git add app/api/export/pdf/route.ts package.json package-lock.json && git commit -m "feat: add PDF export API route using md-to-pdf"
```

---

## Phase 8: Final Integration & Testing

### Task 8.1: Verify Full Application

**Step 1: Run development server**

```bash
npm run dev
```

**Step 2: Manual testing checklist**

- [ ] App loads with default document
- [ ] Can edit markdown in Monaco editor
- [ ] Preview updates in real-time
- [ ] Can create new document
- [ ] Can switch between documents
- [ ] Can delete document (if more than one)
- [ ] Can rename document
- [ ] Can toggle preview visibility
- [ ] Settings modal opens/closes
- [ ] Night mode toggle works
- [ ] Export to Markdown works
- [ ] Export to HTML works
- [ ] Export to PDF works
- [ ] Documents persist across page reload

**Step 3: Run build**

```bash
npm run build
```
Expected: Build succeeds without errors

**Step 4: Final commit**

```bash
git add -A && git commit -m "feat: complete Dillinger Next.js migration - Phase 1"
```

---

## Summary

This plan covers Phase 1 of the migration:

**Completed:**
- Next.js project setup with TypeScript and Tailwind
- Zustand state management with localStorage persistence
- Monaco Editor integration
- markdown-it rendering with all plugins
- Sidebar with document list
- Navbar with export dropdown
- Settings modal
- Export API routes (Markdown, HTML, PDF)

**Deferred to Phase 2:**
- GitHub OAuth integration
- Dropbox OAuth integration
- Resizable panels
- Vim/Emacs keybindings
- Word count display
- Scroll sync

---

### Plans 2026 01 19 Phase3 Design (`docs/plans/2026-01-19-phase3-design.md`)

# Phase 3: Complete Feature Parity - Design Document

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Date:** 2026-01-19
**Status:** Approved
**Goal:** Complete the Dillinger Next.js migration with all deferred features

---

## Overview

Phase 3 delivers full functional parity with the Angular.js original by implementing:

1. **Google Drive integration** - OAuth, browse folders, import/save .md files
2. **OneDrive integration** - Same pattern as Google Drive
3. **Medium integration** - Publish documents to Medium as drafts
4. **Bitbucket integration** - Same pattern as GitHub
5. **Zen mode** - Editor-only fullscreen for distraction-free writing
6. **Scroll sync** - Preview follows editor scroll position (on by default)
7. **File handling** - Drag & drop .md import, image upload to cloud

**Implementation order:**
1. Google Drive (establishes patterns for other cloud services)
2. Editor UX (Zen mode, scroll sync)
3. OneDrive, Bitbucket, Medium
4. File handling (drag & drop, image upload)

---

## 1. Google Drive Integration

### OAuth Flow

- Register app in Google Cloud Console with Drive API enabled
- Scopes: `https://www.googleapis.com/auth/drive.file`
- Redirect URI: `/api/google-drive/callback`
- Token storage: HTTP-only cookies (matches Dropbox/GitHub pattern)

### API Routes

```
/api/google-drive/
├── oauth/route.ts      # Initiates OAuth, redirects to Google
├── callback/route.ts   # Exchanges code for tokens, sets cookie
├── status/route.ts     # Returns connection status
├── files/route.ts      # GET: list folders/files, POST: get file content
├── save/route.ts       # POST: create or update file
└── unlink/route.ts     # POST: clear tokens, disconnect
```

### Implementation

Direct REST API calls (no SDK) to avoid Next.js server compatibility issues.

### Hook Interface

```typescript
interface UseGoogleDrive {
  isConnected: boolean;
  files: DriveFile[];
  currentPath: string;
  pathHistory: string[];
  connect: () => void;
  disconnect: () => void;
  fetchFiles: (folderId?: string) => Promise<void>;
  fetchFileContent: (fileId: string) => Promise<FileContent | null>;
  saveFile: (name: string, content: string, folderId?: string) => Promise<boolean>;
  navigateToFolder: (folderId: string) => void;
  navigateBack: () => void;
}
```

### Modal UI

Reuse DropboxModal structure:
- Folder browser with back navigation
- File list (folders and .md files)
- Save mode: filename input field
- Consistent styling with existing modals

---

## 2. Editor UX

### Zen Mode

**State:** `zenMode: boolean` in Zustand store (not persisted)

**Activation:**
- Keyboard shortcut: `Cmd/Ctrl + Shift + Z`
- Navbar button with expand icon

**Behavior:**
- Hides: sidebar, navbar, document title bar, preview panel
- Shows: Monaco editor centered, max-width 800px
- Exit: `Escape` key or × button in corner

**Implementation:**
```typescript
// EditorContainer.tsx
if (zenMode) {
  return (
    <div className="h-dvh bg-bg-primary flex items-center justify-center">
      <div className="w-full max-w-3xl h-full py-12 px-4 relative">
        <button
          onClick={() => setZenMode(false)}
          className="absolute top-4 right-4 text-text-muted hover:text-text-invert"
          aria-label="Exit zen mode"
        >
          <X size={20} />
        </button>
        <MonacoEditor className="h-full" />
      </div>
    </div>
  );
}
```

### Scroll Sync

**State:** `scrollSync: boolean` in settings (persisted, default `true`)

**Behavior:**
- Editor scroll position syncs to preview proportionally
- Formula: `(editorScrollTop / editorScrollHeight) * previewScrollHeight`
- Debounced at 50ms to prevent jank

**Implementation:**
- Monaco `onDidScrollChange` event triggers sync
- Preview container ref receives calculated scroll position
- Settings toggle to disable

---

## 3. OneDrive Integration

Follows Google Drive pattern exactly:

- OAuth via Microsoft Identity Platform
- Scopes: `Files.ReadWrite`
- API routes: `/api/onedrive/{oauth,callback,status,files,save,unlink}`
- Hook: `useOneDrive()` with identical interface
- Modal: Same folder browser UI pattern

---

## 4. Bitbucket Integration

Follows GitHub pattern:

- OAuth via Atlassian/Bitbucket
- Multi-step navigation: Workspaces → Repos → Branches → Files
- API routes: `/api/bitbucket/{oauth,callback,status,workspaces,repos,branches,files,save,unlink}`
- Hook: `useBitbucket()` mirrors `useGitHub()` interface
- Modal: Same multi-step UI as GitHubModal

---

## 5. Medium Integration

Different pattern - publish only:

- OAuth via Medium API
- Single action: "Publish to Medium" in export dropdown
- Creates draft post (user publishes on Medium)
- API routes: `/api/medium/{oauth,callback,status,publish,unlink}`

**Hook interface:**
```typescript
interface UseMedium {
  isConnected: boolean;
  connect: () => void;
  disconnect: () => void;
  publish: (title: string, content: string, tags?: string[]) => Promise<boolean>;
}
```

**Modal:** Simple form with title, tags input, publish button.

---

## 6. File Handling

### Drag & Drop Import

**Behavior:**
- Drop zone covers entire editor area
- Accepts: `.md`, `.markdown`, `.txt` files
- Creates new document with file content and filename as title
- Visual feedback: overlay on drag enter

**Implementation:**
```typescript
const handleDrop = (e: DragEvent) => {
  const file = e.dataTransfer.files[0];
  if (file && /\.(md|markdown|txt)$/i.test(file.name)) {
    const reader = new FileReader();
    reader.onload = () => {
      createDocument();
      updateDocumentTitle(file.name.replace(/\.(md|markdown|txt)$/i, ''));
      updateDocumentBody(reader.result as string);
    };
    reader.readAsText(file);
  }
};
```

### Image Upload

**Behavior:**
- Paste or drag image into editor
- Modal: "Upload to [service]?" with service selector
- Uploads to selected cloud service
- Inserts markdown: `` at cursor

**Requirements:**
- At least one cloud service must be connected
- API route per service: `/api/[service]/upload-image`
- Returns shareable URL

**Fallback:** If no service connected, show message prompting user to connect one.

---

## Store Updates

```typescript
interface AppState {
  // Existing...

  // Phase 3 additions
  zenMode: boolean;
  setZenMode: (enabled: boolean) => void;
}

interface Settings {
  // Existing...

  // Phase 3 additions
  scrollSync: boolean;  // default: true
}
```

---

## UI Updates

### Sidebar Services Section

Add to existing services list:
- Google Drive (with connect/unlink)
- OneDrive (with connect/unlink)
- Medium (with connect/unlink)
- Bitbucket (with connect/unlink)

### Sidebar Import/Save Sections

Add entries for each new service in "Import from" and "Save to" sections.

### Navbar

- Add Zen mode toggle button (expand icon)
- Add "Publish to Medium" in export dropdown (when connected)

### Settings Modal

- Add "Scroll Sync" toggle (default on)

---

## Environment Variables

```env
# Google Drive
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# OneDrive
ONEDRIVE_CLIENT_ID=
ONEDRIVE_CLIENT_SECRET=

# Medium
MEDIUM_CLIENT_ID=
MEDIUM_CLIENT_SECRET=

# Bitbucket
BITBUCKET_CLIENT_ID=
BITBUCKET_CLIENT_SECRET=
```

---

## Success Criteria

- [ ] All four cloud services authenticate and sync files
- [ ] Zen mode hides all UI, Escape exits
- [ ] Scroll sync works smoothly, can be disabled
- [ ] Drag & drop imports .md files correctly
- [ ] Image upload works with connected services
- [ ] All features accessible via keyboard
- [ ] TypeScript compiles without errors
- [ ] ESLint passes

---

### Plans 2026 01 19 Phase3 Implementation (`docs/plans/2026-01-19-phase3-implementation.md`)

# Phase 3 Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Implement all Phase 3 features: Google Drive, OneDrive, Bitbucket, Medium integrations, Zen mode, scroll sync, and file handling.

**Architecture:** Follow established patterns from Phase 2 - OAuth via API routes with HTTP-only cookies, React hooks for service state, modal UI for file browsing. Direct REST API calls (no SDKs) for cloud services.

**Tech Stack:** Next.js 14 App Router, TypeScript, Zustand, Tailwind CSS, Monaco Editor

**Verification:** After each task, run `npx tsc --noEmit && npm run lint` to verify. Start dev server with `npm run dev` for manual testing.

---

## Part 1: Google Drive Integration

### Task 1: Add Google Drive Environment Variables

**Files:**
- Modify: `.env.local`
- Modify: `app/api/` (reference only)

**Step 1: Add environment variables to .env.local**

```env
# Google Drive OAuth
GOOGLE_CLIENT_ID=your_client_id_here
GOOGLE_CLIENT_SECRET=your_client_secret_here
```

**Step 2: Verify TypeScript still compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add -A
git commit -m "chore: add Google Drive env variables placeholder"
```

---

### Task 2: Create Google Drive OAuth Route

**Files:**
- Create: `app/api/google-drive/oauth/route.ts`

**Step 1: Create the OAuth initiation route**

```typescript
import { NextResponse } from "next/server";

export async function GET() {
  const clientId = process.env.GOOGLE_CLIENT_ID;

  if (!clientId) {
    return NextResponse.json(
      { error: "Google Drive not configured" },
      { status: 500 }
    );
  }

  const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/api/google-drive/callback`;

  const params = new URLSearchParams({
    client_id: clientId,
    redirect_uri: redirectUri,
    response_type: "code",
    scope: "https://www.googleapis.com/auth/drive.file",
    access_type: "offline",
    prompt: "consent",
  });

  const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;

  return NextResponse.redirect(authUrl);
}
```

**Step 2: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add app/api/google-drive/oauth/route.ts
git commit -m "feat: add Google Drive OAuth initiation route"
```

---

### Task 3: Create Google Drive Callback Route

**Files:**
- Create: `app/api/google-drive/callback/route.ts`

**Step 1: Create the callback route**

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add app/api/google-drive/callback/route.ts
git commit -m "feat: add Google Drive OAuth callback route"
```

---

### Task 4: Create Google Drive Status Route

**Files:**
- Create: `app/api/google-drive/status/route.ts`

**Step 1: Create the status route**

```typescript
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function GET() {
  try {
    const cookieStore = await cookies();
    const tokenCookie = cookieStore.get("google_drive_token");

    if (!tokenCookie) {
      return NextResponse.json({ connected: false });
    }

    const tokens = JSON.parse(tokenCookie.value);

    // Verify token is still valid by calling userinfo
    const response = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
      headers: {
        Authorization: `Bearer ${tokens.access_token}`,
      },
    });

    if (!response.ok) {
      // Token expired or invalid
      return NextResponse.json({ connected: false });
    }

    const userInfo = await response.json();

    return NextResponse.json({
      connected: true,
      email: userInfo.email,
    });
  } catch (error) {
    console.error("Google Drive status error:", error);
    return NextResponse.json({ connected: false });
  }
}
```

**Step 2: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add app/api/google-drive/status/route.ts
git commit -m "feat: add Google Drive status route"
```

---

### Task 5: Create Google Drive Unlink Route

**Files:**
- Create: `app/api/google-drive/unlink/route.ts`

**Step 1: Create the unlink route**

```typescript
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function POST() {
  try {
    const cookieStore = await cookies();
    cookieStore.delete("google_drive_token");

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error("Google Drive unlink error:", error);
    return NextResponse.json(
      { error: "Failed to unlink Google Drive" },
      { status: 500 }
    );
  }
}
```

**Step 2: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add app/api/google-drive/unlink/route.ts
git commit -m "feat: add Google Drive unlink route"
```

---

### Task 6: Create Google Drive Files Route

**Files:**
- Create: `app/api/google-drive/files/route.ts`

**Step 1: Create the files route**

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add app/api/google-drive/files/route.ts
git commit -m "feat: add Google Drive files route"
```

---

### Task 7: Create Google Drive Save Route

**Files:**
- Create: `app/api/google-drive/save/route.ts`

**Step 1: Create the save route**

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add app/api/google-drive/save/route.ts
git commit -m "feat: add Google Drive save route"
```

---

### Task 8: Create useGoogleDrive Hook

**Files:**
- Create: `hooks/useGoogleDrive.ts`

**Step 1: Create the hook**

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add hooks/useGoogleDrive.ts
git commit -m "feat: add useGoogleDrive hook"
```

---

### Task 9: Create GoogleDriveModal Component

**Files:**
- Create: `components/modals/GoogleDriveModal.tsx`

**Step 1: Create the modal (copy pattern from DropboxModal)**

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Step 2: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add components/modals/GoogleDriveModal.tsx
git commit -m "feat: add GoogleDriveModal component"
```

---

### Task 10: Integrate Google Drive into Sidebar

**Files:**
- Modify: `components/sidebar/Sidebar.tsx`

**Step 1: Add imports and state**

Add to imports at top of file:
```typescript
import { useGoogleDrive } from "@/hooks/useGoogleDrive";
import { GoogleDriveModal } from "@/components/modals/GoogleDriveModal";
import { HardDrive } from "lucide-react";
```

**Step 2: Add hook and modal state inside Sidebar component**

After existing hook calls:
```typescript
const googleDrive = useGoogleDrive();

const [googleDriveModal, setGoogleDriveModal] = useState<{ open: boolean; mode: "import" | "save" }>({
  open: false,
  mode: "import",
});
```

**Step 3: Add Google Drive to Services section**

In the Services section (inside `{servicesOpen && ...}`), add after Dropbox ServiceButton:
```typescript
<ServiceButton
  icon={<HardDrive size={16} />}
  label="Google Drive"
  connected={googleDrive.isConnected}
  onConnect={googleDrive.connect}
  onDisconnect={googleDrive.disconnect}
/>
```

**Step 4: Add Google Drive to Import section**

In the Import section (inside `{importOpen && ...}`), add:
```typescript
<button
  onClick={() => setGoogleDriveModal({ open: true, mode: "import" })}
  className="w-full flex items-center gap-2 py-2 px-2 text-dropdown-link hover:text-text-invert text-sm rounded hover:bg-bg-highlight
             focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-plum"
>
  <HardDrive size={16} />
  <span>Google Drive</span>
</button>
```

**Step 5: Add Google Drive to Save section**

In the Save section (inside `{saveOpen && ...}`), add:
```typescript
<button
  onClick={() => setGoogleDriveModal({ open: true, mode: "save" })}
  className="w-full flex items-center gap-2 py-2 px-2 text-dropdown-link hover:text-text-invert text-sm rounded hover:bg-bg-highlight
             focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-plum"
>
  <HardDrive size={16} />
  <span>Google Drive</span>
</button>
```

**Step 6: Add modal to render**

After existing modals (before closing `</>`):
```typescript
<GoogleDriveModal
  isOpen={googleDriveModal.open}
  onClose={() => setGoogleDriveModal({ ...googleDriveModal, open: false })}
  mode={googleDriveModal.mode}
/>
```

**Step 7: Verify TypeScript compiles and lint passes**

Run: `npx tsc --noEmit && npm run lint`
Expected: No errors

**Step 8: Commit**

```bash
git add components/sidebar/Sidebar.tsx
git commit -m "feat: integrate Google Drive into sidebar"
```

---

## Part 2: Editor UX (Zen Mode & Scroll Sync)

### Task 11: Add Zen Mode and Scroll Sync to Store

**Files:**
- Modify: `stores/store.ts`

**Step 1: Add zen mode state**

Add to the AppState interface:
```typescript
zenMode: boolean;
setZenMode: (enabled: boolean) => void;
```

Add to Settings interface (if not already there, add scrollSync):
```typescript
scrollSync: boolean;
```

**Step 2: Add to initial state and actions**

In the create function, add to initial state:
```typescript
zenMode: false,
```

Add to settings default:
```typescript
scrollSync: true,
```

Add action:
```typescript
setZenMode: (enabled) => set({ zenMode: enabled }),
```

**Step 3: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 4: Commit**

```bash
git add stores/store.ts
git commit -m "feat: add zenMode and scrollSync to store"
```

---

### Task 12: Implement Zen Mode in EditorContainer

**Files:**
- Modify: `components/editor/EditorContainer.tsx`

**Step 1: Add zen mode imports and state**

Add to imports:
```typescript
import { X, Maximize2 } from "lucide-react";
```

In EditorContent, add:
```typescript
const zenMode = useStore((state) => state.zenMode);
const setZenMode = useStore((state) => state.setZenMode);
```

**Step 2: Add keyboard shortcut effect**

Add useEffect for keyboard shortcut:
```typescript
useEffect(() => {
  const handleKeyDown = (e: KeyboardEvent) => {
    // Cmd/Ctrl + Shift + Z for zen mode
    if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === "z") {
      e.preventDefault();
      setZenMode(!zenMode);
    }
    // Escape to exit zen mode
    if (e.key === "Escape" && zenMode) {
      setZenMode(false);
    }
  };

  document.addEventListener("keydown", handleKeyDown);
  return () => document.removeEventListener("keydown", handleKeyDown);
}, [zenMode, setZenMode]);
```

**Step 3: Add zen mode render**

Before the main return, add:
```typescript
if (zenMode) {
  return (
    <div className="h-dvh bg-bg-primary flex items-center justify-center">
      <div className="w-full max-w-3xl h-full py-12 px-4 relative">
        <button
          onClick={() => setZenMode(false)}
          className="absolute top-4 right-4 text-text-muted hover:text-text-invert transition-colors rounded
                     focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-plum"
          aria-label="Exit zen mode"
        >
          <X size={24} />
        </button>
        <div className="h-full border border-border-light rounded-lg overflow-hidden">
          <MonacoEditor />
        </div>
      </div>
    </div>
  );
}
```

**Step 4: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 5: Commit**

```bash
git add components/editor/EditorContainer.tsx
git commit -m "feat: implement zen mode in editor"
```

---

### Task 13: Add Zen Mode Button to Navbar

**Files:**
- Modify: `components/navbar/Navbar.tsx`

**Step 1: Add imports**

Add to imports:
```typescript
import { Maximize2 } from "lucide-react";
```

**Step 2: Add zen mode state**

Add to component:
```typescript
const setZenMode = useStore((state) => state.setZenMode);
```

**Step 3: Add zen mode button**

Add button in the navbar (before settings button):
```typescript
<button
  onClick={() => setZenMode(true)}
  aria-label="Enter zen mode"
  className="text-icon-default hover:text-plum transition-colors rounded
             focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-plum"
>
  <Maximize2 size={20} />
</button>
```

**Step 4: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 5: Commit**

```bash
git add components/navbar/Navbar.tsx
git commit -m "feat: add zen mode button to navbar"
```

---

### Task 14: Implement Scroll Sync

**Files:**
- Modify: `components/editor/MonacoEditor.tsx`
- Modify: `components/preview/MarkdownPreview.tsx`
- Modify: `stores/store.ts` (add scroll position)

**Step 1: Add scroll position to store**

In stores/store.ts, add to AppState:
```typescript
editorScrollPercent: number;
setEditorScrollPercent: (percent: number) => void;
```

Add initial state:
```typescript
editorScrollPercent: 0,
```

Add action:
```typescript
setEditorScrollPercent: (percent) => set({ editorScrollPercent: percent }),
```

**Step 2: Update MonacoEditor to emit scroll position**

In MonacoEditor.tsx, add to imports:
```typescript
import { useStore } from "@/stores/store";
```

Add state:
```typescript
const settings = useStore((state) => state.settings);
const setEditorScrollPercent = useStore((state) => state.setEditorScrollPercent);
```

In handleEditorDidMount, add scroll listener:
```typescript
editor.onDidScrollChange((e) => {
  if (settings.scrollSync) {
    const scrollTop = e.scrollTop;
    const scrollHeight = editor.getScrollHeight() - editor.getLayoutInfo().height;
    const percent = scrollHeight > 0 ? scrollTop / scrollHeight : 0;
    setEditorScrollPercent(percent);
  }
});
```

**Step 3: Update MarkdownPreview to sync scroll**

In MarkdownPreview.tsx, add:
```typescript
const containerRef = useRef<HTMLDivElement>(null);
const settings = useStore((state) => state.settings);
const editorScrollPercent = useStore((state) => state.editorScrollPercent);
```

Add useEffect for scroll sync:
```typescript
useEffect(() => {
  if (!settings.scrollSync || !containerRef.current) return;

  const container = containerRef.current;
  const scrollHeight = container.scrollHeight - container.clientHeight;
  container.scrollTop = scrollHeight * editorScrollPercent;
}, [editorScrollPercent, settings.scrollSync]);
```

Add ref to container div:
```typescript
<div ref={containerRef} className="...existing classes...">
```

**Step 4: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 5: Commit**

```bash
git add stores/store.ts components/editor/MonacoEditor.tsx components/preview/MarkdownPreview.tsx
git commit -m "feat: implement scroll sync between editor and preview"
```

---

### Task 15: Add Scroll Sync Toggle to Settings

**Files:**
- Modify: `components/modals/SettingsModal.tsx`

**Step 1: Add scroll sync toggle**

Add after existing toggles:
```typescript
{/* Scroll Sync */}
<SettingToggle
  id="scroll-sync"
  label="Scroll Sync"
  checked={settings.scrollSync}
  onChange={(v) => updateSettings({ scrollSync: v })}
/>
```

**Step 2: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add components/modals/SettingsModal.tsx
git commit -m "feat: add scroll sync toggle to settings"
```

---

## Part 3: Remaining Integrations

### Task 16: Create OneDrive Integration (Following Google Drive Pattern)

**Files:**
- Create: `app/api/onedrive/oauth/route.ts`
- Create: `app/api/onedrive/callback/route.ts`
- Create: `app/api/onedrive/status/route.ts`
- Create: `app/api/onedrive/unlink/route.ts`
- Create: `app/api/onedrive/files/route.ts`
- Create: `app/api/onedrive/save/route.ts`
- Create: `hooks/useOneDrive.ts`
- Create: `components/modals/OneDriveModal.tsx`
- Modify: `components/sidebar/Sidebar.tsx`

Follow the exact same pattern as Google Drive, replacing:
- API endpoints: Microsoft Graph API (`graph.microsoft.com`)
- OAuth URL: `login.microsoftonline.com`
- Scopes: `Files.ReadWrite`
- Cookie name: `onedrive_token`
- Icon: Use `CloudCog` from lucide-react

**Commit after each major piece:**
```bash
git commit -m "feat: add OneDrive OAuth routes"
git commit -m "feat: add OneDrive files and save routes"
git commit -m "feat: add useOneDrive hook"
git commit -m "feat: add OneDriveModal component"
git commit -m "feat: integrate OneDrive into sidebar"
```

---

### Task 17: Create Bitbucket Integration (Following GitHub Pattern)

**Files:**
- Create: `app/api/bitbucket/oauth/route.ts`
- Create: `app/api/bitbucket/callback/route.ts`
- Create: `app/api/bitbucket/status/route.ts`
- Create: `app/api/bitbucket/unlink/route.ts`
- Create: `app/api/bitbucket/workspaces/route.ts`
- Create: `app/api/bitbucket/repos/route.ts`
- Create: `app/api/bitbucket/branches/route.ts`
- Create: `app/api/bitbucket/files/route.ts`
- Create: `app/api/bitbucket/save/route.ts`
- Create: `hooks/useBitbucket.ts`
- Create: `components/modals/BitbucketModal.tsx`
- Modify: `components/sidebar/Sidebar.tsx`

Follow GitHub pattern with Bitbucket API:
- OAuth URL: `bitbucket.org/site/oauth2`
- API: `api.bitbucket.org/2.0`
- Uses "workspaces" instead of "orgs"

**Commit after each major piece.**

---

### Task 18: Create Medium Integration (Publish Only)

**Files:**
- Create: `app/api/medium/oauth/route.ts`
- Create: `app/api/medium/callback/route.ts`
- Create: `app/api/medium/status/route.ts`
- Create: `app/api/medium/unlink/route.ts`
- Create: `app/api/medium/publish/route.ts`
- Create: `hooks/useMedium.ts`
- Create: `components/modals/MediumPublishModal.tsx`
- Modify: `components/navbar/Navbar.tsx` (add to export dropdown)
- Modify: `components/sidebar/Sidebar.tsx` (add to services)

Simpler pattern - publish only:
- OAuth via Medium API
- Publish creates draft post
- No file browsing needed

**Commit after each major piece.**

---

## Part 4: File Handling

### Task 19: Implement Drag & Drop Import

**Files:**
- Modify: `components/editor/EditorContainer.tsx`

**Step 1: Add drag and drop state and handlers**

Add state:
```typescript
const [isDragging, setIsDragging] = useState(false);
```

Add to store usage:
```typescript
const createDocument = useStore((state) => state.createDocument);
const updateDocumentBody = useStore((state) => state.updateDocumentBody);
const updateDocumentTitle = useStore((state) => state.updateDocumentTitle);
```

Add handlers:
```typescript
const handleDragEnter = (e: React.DragEvent) => {
  e.preventDefault();
  setIsDragging(true);
};

const handleDragLeave = (e: React.DragEvent) => {
  e.preventDefault();
  if (e.currentTarget === e.target) {
    setIsDragging(false);
  }
};

const handleDragOver = (e: React.DragEvent) => {
  e.preventDefault();
};

const handleDrop = (e: React.DragEvent) => {
  e.preventDefault();
  setIsDragging(false);

  const file = e.dataTransfer.files[0];
  if (file && /\.(md|markdown|txt)$/i.test(file.name)) {
    const reader = new FileReader();
    reader.onload = () => {
      createDocument();
      updateDocumentTitle(file.name.replace(/\.(md|markdown|txt)$/i, ""));
      updateDocumentBody(reader.result as string);
      notify("File imported");
    };
    reader.readAsText(file);
  }
};
```

**Step 2: Add drag overlay to render**

Wrap the main content div and add overlay:
```typescript
<div
  onDragEnter={handleDragEnter}
  onDragLeave={handleDragLeave}
  onDragOver={handleDragOver}
  onDrop={handleDrop}
  className="h-dvh flex overflow-hidden relative"
>
  {isDragging && (
    <div className="absolute inset-0 z-overlay bg-plum/20 flex items-center justify-center pointer-events-none">
      <div className="bg-bg-navbar text-text-invert px-6 py-4 rounded-lg shadow-lg">
        Drop file to import
      </div>
    </div>
  )}
  {/* ...existing content... */}
</div>
```

**Step 3: Verify TypeScript compiles**

Run: `npx tsc --noEmit`
Expected: No errors

**Step 4: Commit**

```bash
git add components/editor/EditorContainer.tsx
git commit -m "feat: implement drag and drop file import"
```

---

### Task 20: Implement Image Upload

**Files:**
- Create: `app/api/google-drive/upload-image/route.ts`
- Create: `app/api/dropbox/upload-image/route.ts`
- Create: `components/modals/ImageUploadModal.tsx`
- Modify: `components/editor/MonacoEditor.tsx`

**Step 1: Create image upload API route for Google Drive**

```typescript
// app/api/google-drive/upload-image/route.ts
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function POST(request: NextRequest) {
  const cookieStore = await cookies();
  const tokenCookie = cookieStore.get("google_drive_token");

  if (!tokenCookie) {
    return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
  }

  const tokens = JSON.parse(tokenCookie.value);
  const formData = await request.formData();
  const file = formData.get("file") as File;

  if (!file) {
    return NextResponse.json({ error: "No file provided" }, { status: 400 });
  }

  // Upload to Google Drive and get shareable link
  // ... implementation

  return NextResponse.json({ url: shareableUrl });
}
```

**Step 2: Create ImageUploadModal**

Modal that shows when user pastes an image, lets them select which service to upload to.

**Step 3: Add paste handler to MonacoEditor**

Detect image paste, show modal, insert markdown link.

**Step 4: Verify and commit**

```bash
git add app/api/google-drive/upload-image/route.ts app/api/dropbox/upload-image/route.ts
git add components/modals/ImageUploadModal.tsx components/editor/MonacoEditor.tsx
git commit -m "feat: implement image upload to cloud services"
```

---

## Final Task: Verification

### Task 21: Final Verification

**Step 1: Run full verification**

```bash
npx tsc --noEmit && npm run lint
```
Expected: No errors

**Step 2: Start dev server and test**

```bash
npm run dev
```

Test checklist:
- [ ] Google Drive: connect, browse, import, save
- [ ] Zen mode: activate with Cmd+Shift+Z, exit with Escape
- [ ] Scroll sync: editor scroll moves preview
- [ ] Drag & drop: drop .md file imports it
- [ ] OneDrive: connect, browse, import, save
- [ ] Bitbucket: connect, navigate repos, import, save
- [ ] Medium: connect, publish document
- [ ] Image upload: paste image, select service, inserts link

**Step 3: Final commit**

```bash
git add -A
git commit -m "feat: complete Phase 3 implementation"
```

---

## Summary

Phase 3 adds:
- 4 new cloud integrations (Google Drive, OneDrive, Bitbucket, Medium)
- Zen mode for distraction-free writing
- Scroll sync between editor and preview
- Drag & drop file import
- Image upload to cloud services

Total new files: ~25
Modified files: ~10
Estimated tasks: 21

---

### Plans CLAUDE (`docs/plans/CLAUDE.md`)

<claude-mem-context>
# Recent Activity



### Jan 19, 2026

| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #1660 | 10:51 AM | ✅ | Phase 2 Implementation Plan for GitHub and Dropbox Cloud Integrations | ~748 |
| #1557 | 10:27 AM | ⚖️ | Comprehensive Next.js Migration Implementation Plan Created | ~685 |
</claude-mem-context>

---

--- METRICS ---
- Files Extracted: 12
- Estimated Token Budget: ~24583 tokens
- Canonical Reference: https://codewiki.google/github.com/joemccann/dillinger
