Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

TailwindCSS Migration Implementation Plan#

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Replace the ~1000 lines of custom CSS with TailwindCSS utility classes, while preserving the existing design and dark mode support.

Architecture:

  1. Install TailwindCSS as a dev dependency with PostCSS
  2. Create a minimal tailwind.config.js that extends the default theme with the existing color palette
  3. Keep a small base.css for critical custom styles that can't be replaced (e.g., CodeMirror overrides, Milkdown-specific styles)
  4. Replace HTML class names with Tailwind utility classes in templates
  5. Use @apply for complex patterns that appear multiple times

Tech Stack: TailwindCSS 4.x, PostCSS, existing esbuild for CSS bundling


Current CSS Inventory#

File Lines Purpose
static/css/style.css 427 Base styles, navbar, cards, buttons, forms, alerts
static/css/editor.css 419 Editor layout, toolbar, modals, split pane
static/css/markdown.css 84 Markdown preview rendering
static/css/diff.css 60 Diff view styling
Total 990

Chunk 1: Setup TailwindCSS#

Files:

  • Modify: /Users/johnluther/projects/diffdown/package.json

  • Create: /Users/johnluther/projects/diffdown/tailwind.config.js

  • Create: /Users/johnluther/projects/diffdown/postcss.config.js

  • Create: /Users/johnluther/projects/diffdown/src/styles/input.css

  • Step 1: Install TailwindCSS and dependencies

Run: npm install -D tailwindcss postcss autoprefixer

  • Step 2: Initialize Tailwind config
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./templates/**/*.html",
    "./static/**/*.js",
  ],
  darkMode: 'class', // Use data-theme attribute for dark mode
  theme: {
    extend: {
      colors: {
        bg: 'var(--bg)',
        'bg-card': 'var(--bg-card)',
        text: 'var(--text)',
        'text-muted': 'var(--text-muted)',
        border: 'var(--border)',
        primary: 'var(--primary)',
        'primary-hover': 'var(--primary-hover)',
        danger: 'var(--danger)',
        success: 'var(--success)',
        'code-bg': 'var(--code-bg)',
        'alert-error-bg': 'var(--alert-error-bg)',
        'alert-error-border': 'var(--alert-error-border)',
      },
      fontFamily: {
        sans: ['"Barlow"', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', 'sans-serif'],
        heading: ['"Lexend"', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', 'sans-serif'],
        mono: ['"JetBrains Mono"', '"Fira Code"', '"Cascadia Code"', 'monospace'],
      },
      borderRadius: {
        DEFAULT: '6px',
      },
    },
  },
  plugins: [],
}
  • Step 3: Create PostCSS config
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
  • Step 4: Create input.css for Tailwind directives
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Keep custom font import */
@import url(https://fonts.bunny.net/css?family=barlow:100,200,300,400,500,600|lexend:100,200,300,400,500,600,700,800,900);

/* CSS variables - kept for backward compatibility */
:root {
    --bg: #fafafa;
    --bg-card: #fff;
    --text: #1a1a2e;
    --text-muted: #6b7280;
    --border: #e5e7eb;
    --primary: #2563eb;
    --primary-hover: #1d4ed8;
    --danger: #dc2626;
    --success: #16a34a;
    --code-bg: #f3f4f6;
    --alert-error-bg: #fef2f2;
    --alert-error-border: #fecaca;
}

[data-theme="dark"] {
    --bg: #0d1117;
    --bg-card: #161b22;
    --text: #e6edf3;
    --text-muted: #ededed;
    --text-secondary: #d4d4d4;
    --border: #30363d;
    --code-bg: #1f2428;
    --primary: #388bfd;
    --primary-hover: #58a6ff;
    --danger: #f85149;
    --success: #3fb950;
    --alert-error-bg: #1c0608;
    --alert-error-border: #6e1c20;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #0d1117;
        --bg-card: #161b22;
        --text: #e6edf3;
        --text-muted: #ededed;
        --text-secondary: #d4d4d4;
        --border: #30363d;
        --code-bg: #1f2428;
        --primary: #388bfd;
        --primary-hover: #58a6ff;
        --danger: #f85149;
        --success: #3fb950;
        --alert-error-bg: #1c0608;
        --alert-error-border: #6e1c20;
    }
}
  • Step 5: Add build script to package.json
"scripts": {
  "build:css": "npx tailwindcss -i ./src/styles/input.css -o ./static/css/tailwind.css",
  "build:collab": "npx esbuild node_modules/prosemirror-collab/dist/index.js --bundle --format=esm --outfile=static/vendor/collab.js",
  "build": "npm run build:css && npm run build:collab"
}
  • Step 6: Build TailwindCSS

Run: npm run build:css

Expected: Creates static/css/tailwind.css with Tailwind utilities

  • Step 7: Commit
git add package.json tailwind.config.js postcss.config.js src/styles/input.css static/css/tailwind.css
git commit -m "feat: add TailwindCSS setup"

Chunk 2: Replace CSS in base.html#

Files:

  • Modify: /Users/johnluther/projects/diffdown/templates/base.html

  • Step 1: Replace navbar styles with Tailwind classes

Old:

<nav class="navbar">
    <a href="/" class="logo">
        <img src="/static/img/dd-logo.svg" alt="" width="22" height="28" style="vertical-align:middle;margin-right:0.4rem">Diffdown
    </a>
    <div class="nav-right">
        {{if .User}}
            <a href="/">Documents</a>
            <a href="/about">About</a>
            <span class="nav-user">{{.User.Name}}</span>
            <form method="post" action="/auth/logout" style="display:inline">
                <button type="submit" class="btn-link">Log out</button>
            </form>
        {{else}}
            <a href="/auth/login">Log in</a>
            <a href="/auth/register" class="btn btn-sm">Sign up</a>
            <a href="/about">About</a>
        {{end}}
        <button id="theme-toggle" class="btn-link" aria-label="Toggle dark mode" onclick="toggleTheme()" style="font-size:1.1rem;padding:0.25rem"></button>
    </div>
</nav>

New:

<nav class="flex items-center justify-between px-6 py-3 border-b border-[var(--border)]">
    <a href="/" class="flex items-center">
        <img src="/static/img/dd-logo.svg" alt="" width="22" height="28" class="align-middle mr-1">Diffdown
    </a>
    <div class="flex items-center gap-4">
        {{if .User}}
            <a href="/" class="text-[var(--primary)] hover:underline">Documents</a>
            <a href="/about" class="text-[var(--primary)] hover:underline">About</a>
            <span class="text-[var(--text-muted)]">{{.User.Name}}</span>
            <form method="post" action="/auth/logout">
                <button type="submit" class="text-[var(--primary)] bg-transparent border-none cursor-pointer hover:underline">Log out</button>
            </form>
        {{else}}
            <a href="/auth/login" class="text-[var(--primary)] hover:underline">Log in</a>
            <a href="/auth/register" class="px-3 py-1.5 text-sm bg-[var(--primary)] text-white rounded hover:bg-[var(--primary-hover)]">Sign up</a>
            <a href="/about" class="text-[var(--primary)] hover:underline">About</a>
        {{end}}
        <button id="theme-toggle" class="text-[var(--primary)] bg-transparent border-none cursor-pointer text-lg" aria-label="Toggle dark mode" onclick="toggleTheme()"></button>
    </div>
</nav>
  • Step 2: Replace main and alert styles

Old:

<main>
    {{if .Error}}
    <div class="alert alert-error">{{.Error}}</div>
    {{end}}
    {{block "content" .}}{{end}}
</main>

New:

<main class="p-6">
    {{if .Error}}
    <div class="p-4 mb-4 bg-[var(--alert-error-bg)] border border-[var(--alert-error-border)] rounded text-[var(--danger)]">{{.Error}}</div>
    {{end}}
    {{block "content" .}}{{end}}
</main>
  • Step 3: Update base.html to link tailwind.css instead of style.css

Old:

<link rel="stylesheet" href="/static/css/style.css">

New (keep style.css for now, we'll remove later):

<link rel="stylesheet" href="/static/css/tailwind.css">
<link rel="stylesheet" href="/static/css/style.css">
  • Step 4: Test locally

Run: make run Open: http://127.0.0.1:8080 Verify: Navbar, main content, and dark mode still work

  • Step 5: Commit
git add templates/base.html
git commit -m "refactor: convert base.html to TailwindCSS classes"

Chunk 3: Replace style.css (base styles)#

Files:

  • Modify: /Users/johnluther/projects/diffdown/templates/landing.html

  • Modify: /Users/johnluther/projects/diffdown/templates/documents.html

  • Modify: /Users/johnluther/projects/diffdown/templates/register.html

  • Modify: /Users/johnluther/projects/diffdown/templates/login.html

  • Step 1: Review style.css for reusable patterns

Run: cat static/css/style.css

Identify patterns to convert:

  • .btn, .btn-sm, .btn-outline → Tailwind utility classes

  • .alert, .alert-error → Already handled in base.html

  • .card → Tailwind card styles

  • .form-group, .form-label, .form-input → Tailwind form styles

  • Step 2: Convert landing.html

Read and convert elements to use Tailwind classes inline instead of custom CSS classes.

  • Step 3: Convert documents.html

Same approach for the documents dashboard page.

  • Step 4: Convert auth pages (register, login)

Convert form elements to use Tailwind classes.

  • Step 5: Test all pages

Run: make run Verify: Landing, documents, login, register pages render correctly with dark mode

  • Step 6: Commit
git add templates/
git commit -m "refactor: convert auth and dashboard pages to TailwindCSS"

Chunk 4: Replace editor.css (complex layout)#

Files:

  • Modify: /Users/johnluther/projects/diffdown/templates/document_edit.html

  • Modify: /Users/johnluther/projects/diffdown/templates/document_view.html

  • Step 1: Review editor.css

Key patterns:

  • .editor-page → Full height flex container

  • .editor-toolbar → Sticky top toolbar with flex layout

  • .editor-rich → Rich text editor container

  • .editor-split → Split pane layout (editor + preview)

  • .invite-modal → Modal overlay and box

  • Step 2: Convert document_edit.html toolbar

<!-- Old -->
<div class="editor-toolbar">
    <div class="breadcrumb">
        ...
    </div>
    <div class="toolbar-actions">
        ...
    </div>
</div>

<!-- New -->
<div class="sticky top-0 z-50 flex items-center justify-between px-4 py-2 border-b border-[var(--border)] bg-[var(--bg)]">
    <div class="flex items-center gap-2">
        ...
    </div>
    <div class="flex items-center gap-2">
        ...
    </div>
</div>
  • Step 3: Convert editor containers

  • Step 4: Convert modal components

  • Step 5: Test editor functionality

Run: make run Create/edit a document Verify: Rich mode, source mode, preview, toolbar all work

  • Step 6: Commit
git add templates/document_edit.html templates/document_view.html
git commit -m "refactor: convert editor pages to TailwindCSS classes"

Chunk 5: Replace markdown.css and diff.css#

Files:

  • Modify: /Users/johnluther/projects/diffdown/templates/document_view.html

  • Modify: /Users/johnluther/projects/diffdown/templates/landing.html

  • Create: Keep minimal overrides in a new file if needed

  • Step 1: Review markdown.css

Mostly Markdown rendering styles - can keep as-is for now with minimal overrides.

  • Step 2: Review diff.css

Diff view styles - can keep as-is.

  • Step 3: Link tailwind.css in view templates

Add <link rel="stylesheet" href="/static/css/tailwind.css"> to templates that don't have it.

  • Step 4: Test view pages

Run: make run View a document Verify: Markdown renders correctly with dark mode

  • Step 5: Commit
git add templates/
git commit -m "refactor: add TailwindCSS to view pages"

Chunk 6: Cleanup and final integration#

Files:

  • Delete: /Users/johnluther/projects/diffdown/static/css/style.css (after verifying no missing styles)

  • Delete: /Users/johnluther/projects/diffdown/static/css/editor.css (after verifying no missing styles)

  • Modify: /Users/johnluther/projects/diffdown/templates/base.html

  • Modify: /Users/johnluther/projects/diffdown/package.json

  • Step 1: Check for any remaining custom CSS references

Run: grep -r 'class="' templates/ | grep -E '\.(btn|alert|card|form)' | head -20

  • Step 2: Remove unused CSS files one by one

Start with style.css - comment out the link in base.html, test thoroughly, then delete.

  • Step 3: Keep minimal base.css for CodeMirror/Milkdown overrides

Create static/css/base.css for:

  • CodeMirror theme overrides

  • Milkdown-specific styles

  • Any other third-party library styles

  • Step 4: Update base.html to remove old CSS links

<link rel="stylesheet" href="/static/css/tailwind.css">
<link rel="stylesheet" href="/static/css/base.css">
  • Step 5: Add CSS build to existing build command
"scripts": {
  "build": "npm run build:css && npm run build:collab",
  "build:css": "npx tailwindcss -i ./src/styles/input.css -o ./static/css/tailwind.css --minify",
  "build:collab": "npx esbuild node_modules/prosemirror-collab/dist/index.js --bundle --format=esm --outfile=static/vendor/collab.js"
}
  • Step 6: Final testing

Run: make build && make run Test all pages:

  • Landing page

  • Login/register

  • Documents list

  • Document edit (rich + source mode)

  • Document view

  • About page

  • Dark mode toggle

  • Step 7: Commit final cleanup

git add -A
git commit -m "refactor: complete TailwindCSS migration, remove legacy CSS"

Testing Checklist#

  • Landing page displays correctly (logged out)
  • Login page renders with proper styling
  • Register page renders with proper styling
  • Dashboard shows document list with cards
  • Dark mode toggle works on all pages
  • Document editor loads in rich mode
  • Document editor toggles to source mode
  • Preview pane renders Markdown
  • Invite modal opens/closes properly
  • Document view renders Markdown
  • About page displays correctly
  • Navbar shows correct links for logged in/out users

Rollback Plan#

If issues arise:

  1. Keep old CSS files in a static/css/legacy/ folder
  2. Restore old link in base.html: <link rel="stylesheet" href="/static/css/style.css">
  3. Test before committing deletions

Notes#

  • TailwindCSS 4.x uses a different configuration approach - verify which version is installed
  • The data-theme attribute approach is already implemented, Tailwind's darkMode: 'class' should work with it
  • CodeMirror and Milkdown may need specific overrides in a separate CSS file
  • Consider using @layer components in input.css for complex custom patterns