// Collapsible diff view for a single file in a commit. // Diff is fetched lazily on first expand to avoid loading large diffs upfront. import { useState } from 'react' import { ChevronRight, FilePlus, FileMinus, FileEdit } from 'lucide-react' import { cn } from '@/lib/utils' import { getCommitDiff } from '@/lib/gitApi' import type { FileDiff, DiffHunk } from '@/lib/gitApi' interface FileDiffViewProps { sha: string path: string oldPath?: string status: 'added' | 'modified' | 'deleted' | 'renamed' } const statusIcon = { added: , deleted: , modified: , renamed: , } const statusBadge = { added: 'A', deleted: 'D', modified: 'M', renamed: 'R' } export function FileDiffView({ sha, path, oldPath, status }: FileDiffViewProps) { const [open, setOpen] = useState(false) const [diff, setDiff] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) function toggle() { if (!open && diff === null && !loading) { setLoading(true) getCommitDiff(sha, path) .then(setDiff) .catch((e: Error) => setError(e.message)) .finally(() => setLoading(false)) } setOpen((v) => !v) } return (
{/* File header row — always visible, click to toggle */} {/* Diff body */} {open && (
{loading && (
Loading diff…
)} {error && (
Failed to load diff: {error}
)} {diff && ( diff.isBinary ? (
Binary file
) : diff.hunks.length === 0 ? (
No changes
) : ( diff.hunks.map((hunk, i) => ) ) )}
)}
) } function Hunk({ hunk }: { hunk: DiffHunk }) { return (
{/* Hunk header */}
@@ -{hunk.oldStart},{hunk.oldLines} +{hunk.newStart},{hunk.newLines} @@
{hunk.lines.map((line, i) => (
{/* Old line number */} {line.oldLine || ''} {/* New line number */} {line.newLine || ''} {/* Sign */} {line.type === 'added' ? '+' : line.type === 'deleted' ? '-' : ' '} {/* Content */}
            {line.content}
          
))}
) }