Vectrum
Sign In
JjohannSoAdd wikisHistory
104 lines · 4.6 KB · TypeScript
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>
  );
}