import { FileIcon } from "@/components/icons";
import { Box, ButtonLink, Label } from "@/components/ui";
import type { FileDiff } from "@/lib/git";
const MAX_RENDERED_LINES = 3000;
/** How many lines each file may render: what is left of the budget after the files above it. */
export function diffBudgets(files: FileDiff[]) {
return files.map((_, i) => MAX_RENDERED_LINES - files.slice(0, i).reduce((n, f) => n + f.lines.length, 0));
}
const lineStyles = {
add: { row: "bg-[var(--diff-add)]", num: "bg-[var(--diff-add-num)]", sign: "+" },
del: { row: "bg-[var(--diff-del)]", num: "bg-[var(--diff-del-num)]", sign: "-" },
ctx: { row: "", num: "", sign: " " },
};
/** The change as five lamps: green for the added share, red for the deleted, dark when nothing changed. */
function DiffStat({ additions, deletions }: { additions: number; deletions: number }) {
const total = additions + deletions;
const greens = total ? Math.round((additions / total) * 5) : 0;
return (
<span className="flex shrink-0 items-center gap-2 type-readout">
<span className="text-success-emphasis">+{additions}</span>
<span className="text-danger">−{deletions}</span>
<span aria-hidden className="flex gap-[2px]">
{Array.from({ length: 5 }, (_, i) => (
<span
key={i}
className={`size-2 rounded-[2.5px] ${i < greens ? "bg-success-emphasis" : i < (total ? 5 : 0) ? "bg-danger" : "material-well"}`}
/>
))}
</span>
</span>
);
}
/**
* One file's changes on a plate: its name and stat in a sticky header, then the lines. `viewHref`
* links the file as it is after the change.
*/
export function FileDiffView({ file, viewHref, budget }: { file: FileDiff; viewHref?: string; budget: number }) {
const name = file.status === "renamed" ? `${file.oldPath} → ${file.newPath}` : file.newPath;
const truncated = file.lines.length > budget;
return (
<section id={encodeURIComponent(file.newPath)} aria-label={name} className="mb-4 scroll-mt-20">
<Box className="overflow-clip">
<div className="rule-b sticky top-14 z-10 flex min-h-11 items-center gap-3 bg-[var(--mu-frost-strong)] py-1.5 pr-2 pl-4 backdrop-blur-md">
<DiffStat additions={file.additions} deletions={file.deletions} />
<span className="min-w-0 truncate font-mono text-[11.5px] font-medium text-ink">{name}</span>
{file.status !== "modified" && (
<Label tone={file.status === "added" ? "success" : file.status === "deleted" ? "danger" : "default"}>{file.status}</Label>
)}
{viewHref && file.status !== "deleted" && (
<ButtonLink
href={viewHref}
variant="invisible"
icon={<FileIcon width={14} height={14} />}
className="ml-auto shrink-0"
>
View File
</ButtonLink>
)}
</div>
{file.binary ? (
<p className="p-6 text-center type-ui text-ink3">Binary file not shown.</p>
) : file.lines.length === 0 ? (
<p className="p-6 text-center type-ui text-ink3">File mode or name changed only.</p>
) : truncated ? (
<p className="p-6 text-center type-ui text-ink3">Large diffs are not rendered by default.</p>
) : (
<div className="overflow-x-auto">
<table className="w-full border-collapse font-mono text-[11.5px] leading-[22px] text-ink">
<tbody>
{file.lines.map((line, i) =>
line.kind === "hunk" ? (
<tr key={i} className="bg-[var(--diff-hunk)] text-ink3">
<td colSpan={3} className="px-4 py-1.5">
{line.text}
</td>
</tr>
) : (
<tr key={i} className={lineStyles[line.kind].row}>
<td className={`w-12 min-w-12 select-none px-2 text-right text-ink3 tabular-nums ${lineStyles[line.kind].num}`}>
{line.oldNo}
</td>
<td className={`w-12 min-w-12 select-none px-2 text-right text-ink3 tabular-nums ${lineStyles[line.kind].num}`}>
{line.newNo}
</td>
<td className="px-2 whitespace-pre">
<span className="select-none text-ink3">{lineStyles[line.kind].sign} </span>
{line.text}
</td>
</tr>
),
)}
</tbody>
</table>
</div>
)}
</Box>
</section>
);
}