SPB Git

Repository Public

diff-view.tsx 3.6 KB 69 lines tsx

1'use client';2import { useState } from 'react';3import { FileDiff } from 'lucide-react';4import type { DiffFile, DiffLine } from '@/lib/diff';5import { cn } from '@/lib/cn';67const ROW: Record<DiffLine['type'], string> = {8  ctx: 'text-text-2',9  add: 'bg-diff-add text-text',10  del: 'bg-diff-del text-text',11  meta: 'text-muted italic',12};13const SIGN: Record<DiffLine['type'], string> = { ctx: ' ', add: '+', del: '-', meta: '\\' };14const SIGN_TONE: Record<DiffLine['type'], string> = { ctx: 'text-faint', add: 'text-success', del: 'text-error', meta: 'text-muted' };15const MAX_LINES = 400;1617/** Unified diff view: line numbers, +/- gutter, added/removed colouring. Content is rendered as text only. */18export function DiffView({ files, className }: { files: DiffFile[]; className?: string }) {19  const [all, setAll] = useState(false);20  const total = files.reduce((n, f) => n + f.hunks.reduce((m, h) => m + h.lines.length, 0), 0);21  let budget = all ? Number.POSITIVE_INFINITY : MAX_LINES;22  return (23    <div className={cn('flex flex-col gap-2', className)} data-testid="diff-view">24      {files.map((f, fi) => (25        <div key={fi} className="overflow-hidden rounded-sm border border-line-2 bg-surface">26          <div className="flex items-center gap-2 border-b border-line bg-surface-2 px-3 py-1.5 text-xs">27            <FileDiff className="size-3.5 shrink-0 text-muted" aria-hidden />28            <span className="min-w-0 flex-1 truncate font-mono text-[12px] font-medium text-text" title={f.path ?? undefined}>{f.path ?? f.oldPath ?? 'changes'}</span>29            {f.note && <span className="shrink-0 text-[11px] text-muted">{f.note}</span>}30            <span className="tnum shrink-0 font-mono text-[11px]">31              <span className="text-success" data-testid="diff-added">+{f.added}</span> <span className="text-error" data-testid="diff-removed">−{f.removed}</span>32            </span>33          </div>34          <div className="overflow-x-auto">35            <table className="w-full border-collapse font-mono text-[12px] leading-[1.55]">36              <tbody>37                {f.hunks.map((h, hi) => {38                  const lines = h.lines.slice(0, Math.max(0, budget));39                  budget -= lines.length;40                  return [41                    h.header && lines.length > 0 ? (42                      <tr key={`h${hi}`} className="bg-surface-2/60 text-muted">43                        <td colSpan={4} className="px-3 py-0.5 text-[11px]">{h.header}</td>44                      </tr>45                    ) : null,46                    ...lines.map((l, li) => (47                      <tr key={`${hi}.${li}`} className={ROW[l.type]} data-line={l.type}>48                        <td className="tnum w-9 select-none border-r border-line px-1.5 text-right text-[11px] text-faint">{l.oldNo ?? ''}</td>49                        <td className="tnum w-9 select-none border-r border-line px-1.5 text-right text-[11px] text-faint">{l.newNo ?? ''}</td>50                        <td className={cn('w-5 select-none pl-2 text-center font-bold', SIGN_TONE[l.type])} aria-hidden>{SIGN[l.type]}</td>51                        <td className="whitespace-pre pr-3">{l.text === '' ? ' ' : l.text}</td>52                      </tr>53                    )),54                  ];55                })}56              </tbody>57            </table>58          </div>59        </div>60      ))}61      {!all && total > MAX_LINES && (62        <button onClick={() => setAll(true)} className="self-start rounded-sm px-2 py-1 text-xs font-medium text-accent hover:bg-hover pointer-coarse:min-h-11">63          Show all {total} lines64        </button>65      )}66    </div>67  );68}69
Files main
Menu

Theme