- Add file "viewed" checkmarks with progress tracking (X/26 viewed in header + sidebar) - Add directory-grouped file tree in sidebar with review progress bar - Add sticky file headers that stay visible when scrolling through long diffs - Add file change type badges (NEW/DELETED/RENAMED) with colored left borders - Add syntax highlighting via shiki with lazy loading and progressive enhancement - Add merge confirmation dropdown showing unresolved comments + viewed progress - Make Approve & Merge button prominently green, Request Changes styled with error tokens - Show full branch names instead of aggressively truncated text - Add always-visible comment dots on lines with comments, subtle hover on others - Improve hunk headers with two-tone @@ display and context function highlighting - Add review progress bar to sidebar with file-level viewed state
170 lines
5.0 KiB
TypeScript
170 lines
5.0 KiB
TypeScript
import { useState, useMemo } from "react";
|
|
import {
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Plus,
|
|
Minus,
|
|
CheckCircle2,
|
|
Circle,
|
|
} from "lucide-react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import type { FileDiff, FileChangeType, DiffLine, ReviewComment } from "./types";
|
|
import { HunkRows } from "./HunkRows";
|
|
import { useHighlightedFile } from "./use-syntax-highlight";
|
|
|
|
const changeTypeBadge: Record<
|
|
FileChangeType,
|
|
{ label: string; classes: string } | null
|
|
> = {
|
|
added: {
|
|
label: "NEW",
|
|
classes:
|
|
"bg-status-success-bg text-status-success-fg border-status-success-border",
|
|
},
|
|
deleted: {
|
|
label: "DELETED",
|
|
classes:
|
|
"bg-status-error-bg text-status-error-fg border-status-error-border",
|
|
},
|
|
renamed: {
|
|
label: "RENAMED",
|
|
classes:
|
|
"bg-status-active-bg text-status-active-fg border-status-active-border",
|
|
},
|
|
modified: null,
|
|
};
|
|
|
|
const leftBorderClass: Record<FileChangeType, string> = {
|
|
added: "border-l-2 border-l-status-success-fg",
|
|
deleted: "border-l-2 border-l-status-error-fg",
|
|
renamed: "border-l-2 border-l-status-active-fg",
|
|
modified: "border-l-2 border-l-primary/40",
|
|
};
|
|
|
|
interface FileCardProps {
|
|
file: FileDiff;
|
|
comments: ReviewComment[];
|
|
onAddComment: (
|
|
filePath: string,
|
|
lineNumber: number,
|
|
lineType: DiffLine["type"],
|
|
body: string,
|
|
) => void;
|
|
onResolveComment: (commentId: string) => void;
|
|
onUnresolveComment: (commentId: string) => void;
|
|
isViewed?: boolean;
|
|
onToggleViewed?: () => void;
|
|
}
|
|
|
|
export function FileCard({
|
|
file,
|
|
comments,
|
|
onAddComment,
|
|
onResolveComment,
|
|
onUnresolveComment,
|
|
isViewed = false,
|
|
onToggleViewed = () => {},
|
|
}: FileCardProps) {
|
|
const [expanded, setExpanded] = useState(true);
|
|
const commentCount = comments.length;
|
|
const badge = changeTypeBadge[file.changeType];
|
|
|
|
// Flatten all hunk lines for syntax highlighting
|
|
const allLines = useMemo(
|
|
() => file.hunks.flatMap((h) => h.lines),
|
|
[file.hunks],
|
|
);
|
|
const tokenMap = useHighlightedFile(file.newPath, allLines);
|
|
|
|
return (
|
|
<div className="rounded-lg border border-border overflow-hidden">
|
|
{/* File header — sticky so it stays visible when scrolling */}
|
|
<button
|
|
className={`sticky top-0 z-10 flex w-full items-center gap-2 px-3 py-2 bg-muted hover:bg-muted/90 text-left text-sm font-mono transition-colors ${leftBorderClass[file.changeType]}`}
|
|
onClick={() => setExpanded(!expanded)}
|
|
>
|
|
{expanded ? (
|
|
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
) : (
|
|
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
)}
|
|
<span className="truncate flex-1 flex items-center gap-2">
|
|
{file.newPath}
|
|
{badge && (
|
|
<span
|
|
className={`inline-flex text-[9px] font-semibold uppercase px-1.5 py-0 rounded border leading-4 ${badge.classes}`}
|
|
>
|
|
{badge.label}
|
|
</span>
|
|
)}
|
|
</span>
|
|
<span className="flex items-center gap-2 shrink-0 text-xs">
|
|
{/* Viewed toggle */}
|
|
<span
|
|
role="checkbox"
|
|
aria-checked={isViewed}
|
|
tabIndex={0}
|
|
className="cursor-pointer"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggleViewed();
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
onToggleViewed();
|
|
}
|
|
}}
|
|
>
|
|
{isViewed ? (
|
|
<CheckCircle2 className="h-4 w-4 text-status-success-fg" />
|
|
) : (
|
|
<Circle className="h-4 w-4 text-muted-foreground" />
|
|
)}
|
|
</span>
|
|
{file.additions > 0 && (
|
|
<span className="flex items-center gap-0.5 text-diff-add-fg">
|
|
<Plus className="h-3 w-3" />
|
|
{file.additions}
|
|
</span>
|
|
)}
|
|
{file.deletions > 0 && (
|
|
<span className="flex items-center gap-0.5 text-diff-remove-fg">
|
|
<Minus className="h-3 w-3" />
|
|
{file.deletions}
|
|
</span>
|
|
)}
|
|
{commentCount > 0 && (
|
|
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
|
|
{commentCount}
|
|
</Badge>
|
|
)}
|
|
</span>
|
|
</button>
|
|
|
|
{/* Diff content */}
|
|
{expanded && (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-xs font-mono border-collapse">
|
|
<tbody>
|
|
{file.hunks.map((hunk, hi) => (
|
|
<HunkRows
|
|
key={hi}
|
|
hunk={hunk}
|
|
filePath={file.newPath}
|
|
comments={comments}
|
|
onAddComment={onAddComment}
|
|
onResolveComment={onResolveComment}
|
|
onUnresolveComment={onUnresolveComment}
|
|
tokenMap={tokenMap}
|
|
/>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|