A CLAUDE.md is just a markdown file at the root of your repo. Copy the content below into your own project's CLAUDE.md to give your agent the same context.
npx versuz@latest install joemccann-dillinger --kind=claude-mdcurl -o CLAUDE.md https://raw.githubusercontent.com/joemccann/dillinger/HEAD/CLAUDE.md<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 19, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #1873 | 2:32 PM | 🔵 | Next.js Migration Dependency Stack with Modern Equivalents | ~510 |
| #1861 | 2:25 PM | 🔵 | Next.js Config with Lucide-React Bundle Optimization | ~396 |
| #1850 | 2:24 PM | ✅ | Added Modal and Toast Z-Index Layers to Tailwind Configuration | ~354 |
| #1827 | 2:22 PM | 🔵 | Tailwind Configuration with Legacy Dillinger Design Tokens | ~463 |
</claude-mem-context>
# Dillinger Next.js - CLAUDE.md
> Cloud-enabled, mobile-ready, offline-storage-compatible Markdown editor built with Next.js 14.
## Quick Reference
```bash
npm run dev # Start dev server (http://localhost:3000)
npm run build # Production build
npm run start # Start production server
npm run lint # Run ESLint
```
## Tech Stack
| Category | Technology | Version |
|----------|------------|---------|
| Framework | Next.js (App Router) | 14.2.35 |
| Language | TypeScript (strict) | 5.x |
| UI | React | 18.x |
| Styling | Tailwind CSS | 3.4.1 |
| State | Zustand | 5.0.10 |
| Editor | Monaco Editor | 4.7.0 |
| Icons | Lucide React | 0.562.0 |
| Markdown | markdown-it + plugins | 14.1.0 |
## Architecture Overview
```
dillinger/
├── app/ # Next.js App Router
│ ├── api/ # API route handlers
│ │ ├── github/ # GitHub OAuth + file ops
│ │ ├── dropbox/ # Dropbox integration
│ │ ├── google-drive/ # Google Drive integration
│ │ ├── onedrive/ # Microsoft OneDrive
│ │ ├── bitbucket/ # Bitbucket integration
│ │ ├── medium/ # Medium publishing
│ │ └── export/ # PDF/HTML export
│ ├── layout.tsx # Root layout (providers)
│ ├── page.tsx # Main editor page
│ ├── globals.css # Global styles + Tailwind
│ ├── error.tsx # Error boundary
│ └── not-found.tsx # 404 page
├── components/
│ ├── editor/ # Monaco editor wrapper
│ ├── preview/ # Markdown preview pane
│ ├── navbar/ # Top navigation
│ ├── sidebar/ # Document list + integrations
│ ├── modals/ # OAuth dialogs (GitHub, Dropbox, etc.)
│ ├── providers/ # Context providers
│ └── ui/ # Reusable primitives (Toast, Skeleton)
├── hooks/ # Custom React hooks
│ ├── useGitHub.ts
│ ├── useDropbox.ts
│ ├── useGoogleDrive.ts
│ ├── useOneDrive.ts
│ ├── useBitbucket.ts
│ └── useMedium.ts
├── stores/
│ └── store.ts # Zustand store (documents, settings, UI)
├── lib/ # Utilities and helpers
└── types/ # TypeScript type definitions
```
## Code Conventions
### TypeScript
- **Strict mode enabled** - all code must pass strict type checking
- Use `interface` for object shapes, `type` for unions/intersections
- Prefer explicit return types on exported functions
- Use path alias `@/*` for imports from project root
```typescript
// Good
import { useAppStore } from '@/stores/store'
import { Document } from '@/types'
// Avoid
import { useAppStore } from '../../../stores/store'
```
### Components
- **Client Components**: Add `"use client"` directive at top for interactive components
- **Server Components**: Default for layouts and static content (no directive needed)
- **Naming**: PascalCase for components, camelCase for hooks/utilities
- **File naming**: Match component name (e.g., `EditorContainer.tsx` exports `EditorContainer`)
```typescript
// Client component pattern
"use client"
import { useState } from 'react'
interface Props {
initialValue: string
onChange: (value: string) => void
}
export function MyComponent({ initialValue, onChange }: Props) {
const [value, setValue] = useState(initialValue)
// ...
}
```
### Dynamic Imports
Use dynamic imports for:
- Components with browser-only APIs (Monaco, localStorage)
- Heavy components that aren't needed on initial load
- Components that cause hydration mismatches
```typescript
import dynamic from 'next/dynamic'
const Sidebar = dynamic(() => import('@/components/sidebar/Sidebar'), {
ssr: false, // Disable SSR for client-only components
})
```
### Styling with Tailwind
- Use design tokens from `tailwind.config.ts` (colors, spacing, z-index)
- Combine classes with `clsx` and `tailwind-merge` via the `cn()` utility
- Follow z-index scale: sidebar(1) < page(2) < editor(3) < preview(4) < overlay(5) < navbar(6) < settings(7) < modal(50) < toast(60)
```typescript
import { cn } from '@/lib/utils'
// Good - uses design tokens
<div className={cn(
"bg-bg-sidebar text-text-primary",
"w-sidebar p-gutter",
isActive && "bg-bg-highlight"
)} />
// Avoid - hardcoded values
<div className="bg-[#2B2F36] w-[270px] p-8" />
```
### Zustand State Management
- Store lives in `/stores/store.ts`
- Access state via hooks: `useAppStore(selector)`
- Always use selectors to prevent unnecessary re-renders
```typescript
// Good - selective subscription
const documents = useAppStore((state) => state.documents)
const addDocument = useAppStore((state) => state.addDocument)
// Avoid - subscribes to entire store
const store = useAppStore()
```
- **Persistence**: Store auto-persists to localStorage with 2s debounce
- **Hydration**: `StoreProvider` handles client-side hydration
### API Routes
- Use Next.js App Router route handlers (`route.ts`)
- Add `export const dynamic = "force-dynamic"` for OAuth routes
- Return `NextResponse.json()` with appropriate status codes
- Access env vars directly via `process.env`
```typescript
// app/api/example/route.ts
import { NextRequest, NextResponse } from 'next/server'
export const dynamic = "force-dynamic"
export async function GET(request: NextRequest) {
try {
const data = await fetchData()
return NextResponse.json({ data })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch' },
{ status: 500 }
)
}
}
```
### Custom Hooks
- One hook per integration (useGitHub, useDropbox, etc.)
- Hooks manage their own state and API calls
- Use `useRef` for callbacks to avoid stale closures
- Return loading/error states alongside data
```typescript
export function useGitHub() {
const [repos, setRepos] = useState<Repo[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchRepos = useCallback(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch('/api/github/repos')
const data = await res.json()
setRepos(data)
} catch (e) {
setError('Failed to fetch repos')
} finally {
setLoading(false)
}
}, [])
return { repos, loading, error, fetchRepos }
}
```
## Security Guidelines
### XSS Prevention
- **Always** sanitize user-generated HTML with DOMPurify before rendering
- Markdown preview uses `dangerouslySetInnerHTML` only after DOMPurify sanitization
```typescript
import DOMPurify from 'dompurify'
const sanitizedHtml = DOMPurify.sanitize(rawHtml)
```
### Environment Variables
- Store secrets in `.env.local` (never commit)
- Use `NEXT_PUBLIC_*` prefix only for client-accessible values
- Reference `.env.local.example` for required variables
### OAuth Security
- All OAuth flows use server-side route handlers
- Tokens should be stored in HTTP-only cookies (not localStorage)
- Validate redirect URIs match expected patterns
## Performance Best Practices
### Bundle Optimization
- Lucide icons are optimized via `next.config.mjs` experimental setting
- Import icons individually, not from barrel export
```typescript
// Good - tree-shakeable
import { FileText, Settings, Download } from 'lucide-react'
// Avoid - imports entire library
import * as Icons from 'lucide-react'
```
### Rendering Strategy
- Use Server Components for static content (layouts, headers)
- Use Client Components only when needed (interactivity, hooks, browser APIs)
- Add `loading.tsx` for route-level suspense boundaries
### Memoization
- Use `useMemo` for expensive computations (markdown rendering)
- Use `useCallback` for functions passed as props
- Avoid premature optimization - profile first
## Common Patterns
### Modal Pattern
```typescript
// Modals use Zustand for open/close state
const isSettingsOpen = useAppStore((state) => state.isSettingsOpen)
const toggleSettings = useAppStore((state) => state.toggleSettings)
```
### Toast Notifications
```typescript
import { useToast } from '@/components/providers/ToastProvider'
const { showToast } = useToast()
showToast('Document saved!', 'success')
```
### Keyboard Shortcuts
- Cmd/Ctrl + Shift + Z: Toggle zen mode
- Escape: Exit zen mode
- Monaco keybindings: Vim/Emacs modes supported via settings
## Testing
### Stack
| Tool | Purpose |
|------|---------|
| Vitest | Unit + integration tests |
| React Testing Library | Component rendering + interaction |
| Playwright | E2E browser automation |
| @vitest/coverage-v8 | Code coverage |
### Running Tests
```bash
npm run test:unit # Run unit/integration tests
npm run test:watch # Watch mode
npm run test:e2e # Build + run E2E (Playwright)
npm run test:e2e:headed # E2E in visible browser
npm run test # Unit + E2E
npm run verify # Lint + typecheck + unit + E2E
npx vitest run --coverage # Unit tests with coverage report
```
### Test Structure
```
tests/
├── lib/ # Pure utility unit tests
│ ├── cache.test.ts
│ ├── document.test.ts
│ ├── export.test.ts
│ ├── import.test.ts
│ ├── markdown.test.ts
│ └── utils.test.ts
├── store/
│ └── store.test.ts # Zustand store actions + persistence
├── hooks/
│ ├── useGitHub.test.ts # Integration tests with mocked fetch
│ └── useImageUpload.test.ts
├── components/ # React Testing Library component tests
│ ├── navbar.test.tsx
│ ├── settings-modal.test.tsx
│ ├── github-modal.test.tsx
│ ├── delete-confirm-modal.test.tsx
│ ├── document-title.test.tsx
│ ├── document-list.test.tsx
│ ├── toast.test.tsx
│ └── skeleton.test.tsx
├── routes/ # API route handler tests (@vitest-environment node)
│ ├── github.route.test.ts
│ ├── export-html.route.test.ts
│ ├── export-markdown.route.test.ts
│ ├── export-pdf.route.test.ts
│ ├── import-html-to-markdown.route.test.ts
│ └── upload-image.route.test.ts
└── e2e/ # Playwright browser tests
├── smoke.spec.ts
├── editor.spec.ts
├── settings-sidebar.spec.ts
├── import-export.spec.ts
└── logobar.spec.ts
```
### Coverage
Current coverage: **98% statements, 91% branches, 99.5% functions, 98% lines** (294 unit + 39 E2E tests).
### Writing Tests
- **File naming**: `*.test.ts` / `*.test.tsx` for Vitest, `*.spec.ts` for Playwright
- **Imports**: Use explicit imports (`import { describe, it, expect } from 'vitest'`), not globals
- **Store setup**: Reset store in `beforeEach` with `useStore.setState(initialState)`
- **Mocking**: Use `vi.mock()` for modules, `vi.spyOn(globalThis, 'fetch')` for API calls
- **Component rendering**: Wrap in providers if the component uses `useToast()`
- **API route tests**: Add `// @vitest-environment node` at the top, import handlers directly
- **E2E state seeding**: Use `page.addInitScript()` to populate localStorage before navigation
- **Async markdown**: The `renderMarkdown()` function is async — tests must await it
## Deployment
- **Platform**: Vercel (auto-deploys from GitHub)
- **Config**: `vercel.json` with `"framework": "nextjs"`
- **Environment**: Set all `.env.local` variables in Vercel dashboard
## Troubleshooting
### Hydration Mismatches
- Wrap browser-only code in `useEffect` or dynamic import with `ssr: false`
- Check for `typeof window !== 'undefined'` guards
### Monaco Editor Issues
- Monaco is loaded client-side only via dynamic import
- Ensure `@monaco-editor/react` is imported in client components
### OAuth Callback Failures
- Verify redirect URIs match exactly in provider dashboards
- Check `NEXT_PUBLIC_BASE_URL` matches deployment URL
- OAuth routes must have `dynamic = "force-dynamic"`
## Key Files Reference
| File | Purpose |
|------|---------|
| `stores/store.ts` | Zustand store definition |
| `tailwind.config.ts` | Design tokens, theme |
| `next.config.mjs` | Build configuration |
| `.env.local.example` | Required environment variables |
| `app/layout.tsx` | Root layout with providers |
| `components/editor/EditorContainer.tsx` | Main app shell |
| `.impeccable.md` | Design system & context |
| `vitest.config.ts` | Vitest test runner config |
| `vitest.setup.ts` | Test environment setup (mocks) |
| `playwright.config.ts` | E2E test config |
| `lib/cache.ts` | In-memory LRU cache for API routes |
## Design Context
### Users
Developers, technical writers, and content creators who need a distraction-free, cloud-connected markdown editor. They value speed, keyboard-driven workflows, and tools that stay out of the way.
### Brand Personality
**Focused. Capable. Understated.** The interface should feel like a precision instrument — confident and quiet, never flashy.
### Emotional Goal
**Calm focus.** The UI recedes; the content leads.
### Design Principles
1. **Content is king.** Every UI element exists to serve the writing experience.
2. **Quiet confidence.** Plum accent (#35D7BB) is the single bright voice in a neutral palette.
3. **Polished, not decorated.** Quality is in spacing, alignment, transitions, typography — never ornament.
4. **Progressive disclosure.** Show what's needed, hide what isn't.
5. **Accessible by default.** WCAG AA minimum. Focus rings, contrast, keyboard nav, reduced-motion.
### Theme Modes
Full support for light, dark, and system-preference modes. The plum accent (#35D7BB) remains constant across all themes. See `.impeccable.md` for full design system reference.