// Adapted from jalcoui (MIT) — github.com/jal-co/ui 'use client'; import * as React from 'react'; import type { Language, PrismTheme } from 'prism-react-renderer'; import { Virtuoso } from 'react-virtuoso'; import { cn } from '@djangocfg/ui-core/lib'; import { DIFF_TONE_CLASSES, type DiffLine } from '../types'; import { DiffLineContent } from './DiffLineContent'; interface SplitViewProps { lines: DiffLine[]; numWidth: number; language: Language | null; prismTheme: PrismTheme; } // See UnifiedView for context — same threshold + viewport size keeps both // layouts behaviorally aligned. const VIRTUALIZE_THRESHOLD = 100; const VIRTUAL_VIEWPORT_HEIGHT = 480; /** * Align consecutive `removed` / `added` runs into a pair of columns. * Pure-add rows leave the left column empty; pure-remove rows leave the * right column empty. Context rows duplicate on both sides. * * The pairing is the same shape jalcoui ships — we keep it so downstream * consumers' visual baseline doesn't move. */ function pairLines(lines: DiffLine[]): Array<{ left: DiffLine | null; right: DiffLine | null; }> { const pairs: Array<{ left: DiffLine | null; right: DiffLine | null }> = []; let i = 0; while (i < lines.length) { const line = lines[i]; if (line.type === 'context') { pairs.push({ left: line, right: line }); i++; continue; } if (line.type === 'removed') { const removed: DiffLine[] = []; while (i < lines.length && lines[i].type === 'removed') { removed.push(lines[i]); i++; } const added: DiffLine[] = []; while (i < lines.length && lines[i].type === 'added') { added.push(lines[i]); i++; } const maxLen = Math.max(removed.length, added.length); for (let j = 0; j < maxLen; j++) { pairs.push({ left: j < removed.length ? removed[j] : null, right: j < added.length ? added[j] : null, }); } continue; } // line.type === 'added' with no preceding removed run pairs.push({ left: null, right: line }); i++; } return pairs; } function sidePrefix(side: 'old' | 'new', type: DiffLine['type'] | undefined) { if (!type || type === 'context') return ' '; if (side === 'old' && type === 'removed') return '-'; if (side === 'new' && type === 'added') return '+'; return ' '; } interface HalfProps { line: DiffLine | null; side: 'old' | 'new'; numWidth: number; language: Language | null; prismTheme: PrismTheme; } function Half({ line, side, numWidth, language, prismTheme }: HalfProps) { const tone = line ? DIFF_TONE_CLASSES[line.type] : DIFF_TONE_CLASSES.context; const gutterCh = `calc(${numWidth}ch + 1rem)`; return (
{(side === 'old' ? line?.oldNumber : line?.newNumber) ?? ''}
{sidePrefix(side, line?.type)}
{line ? ( ) : ( ' ' )}
); } interface PairRowProps { pair: { left: DiffLine | null; right: DiffLine | null }; numWidth: number; language: Language | null; prismTheme: PrismTheme; } function PairRow({ pair, numWidth, language, prismTheme }: PairRowProps) { return (
); } /** * Side-by-side diff. Left half = old, right half = new. Each row is a * single grid wrapping two `` halves so a `` list can * virtualize the whole stack with naturally synchronized scrolling. * * Both halves receive the SAME globally-computed `numWidth` so the two * gutter columns are pixel-identical regardless of which side has the * longer line numbers. */ export function SplitView({ lines, numWidth, language, prismTheme, }: SplitViewProps) { const pairs = React.useMemo(() => pairLines(lines), [lines]); if (pairs.length > VIRTUALIZE_THRESHOLD) { return (
( )} increaseViewportBy={{ top: 200, bottom: 400 }} />
); } return (
{pairs.map((pair, idx) => ( ))}
); }