/** * ╔═══════════════════════════════════════════════════════════════════════════╗ * ║ CODE DIFF VIEWER ║ * ║═══════════════════════════════════════════════════════════════════════════║ * ║ Side-by-side or unified diff viewer for code changes ║ * ║ Part of the Liquid UI Arsenal ║ * ╚═══════════════════════════════════════════════════════════════════════════╝ */ import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; import { SplitSquareHorizontal, AlignJustify, Copy, Check } from 'lucide-react'; export interface CodeDiffViewerProps { oldCode: string; newCode: string; oldTitle?: string; newTitle?: string; language?: string; splitView?: boolean; showLineNumbers?: boolean; highlightLines?: number[]; summary?: { additions: number; deletions: number; changes: number; }; } // Dark mode styles matching WidgeTDC aesthetic const darkModeStyles = { variables: { dark: { diffViewerBackground: 'transparent', diffViewerColor: '#e5e5e5', addedBackground: 'rgba(34, 197, 94, 0.15)', addedColor: '#4ade80', removedBackground: 'rgba(239, 68, 68, 0.15)', removedColor: '#f87171', wordAddedBackground: 'rgba(34, 197, 94, 0.3)', wordRemovedBackground: 'rgba(239, 68, 68, 0.3)', addedGutterBackground: 'rgba(34, 197, 94, 0.1)', removedGutterBackground: 'rgba(239, 68, 68, 0.1)', gutterBackground: 'rgba(0, 0, 0, 0.2)', gutterBackgroundDark: 'rgba(0, 0, 0, 0.3)', highlightBackground: 'rgba(59, 130, 246, 0.2)', highlightGutterBackground: 'rgba(59, 130, 246, 0.1)', codeFoldGutterBackground: 'rgba(0, 0, 0, 0.2)', codeFoldBackground: 'rgba(0, 0, 0, 0.1)', emptyLineBackground: 'transparent', gutterColor: '#6b7280', addedGutterColor: '#4ade80', removedGutterColor: '#f87171', codeFoldContentColor: '#9ca3af', diffViewerTitleBackground: 'rgba(0, 0, 0, 0.3)', diffViewerTitleColor: '#e5e5e5', diffViewerTitleBorderColor: 'rgba(255, 255, 255, 0.1)', }, }, line: { padding: '4px 8px', fontSize: '12px', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', }, gutter: { padding: '0 8px', minWidth: '40px', }, marker: { padding: '0 4px', }, contentText: { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', fontSize: '12px', lineHeight: '1.5', }, }; export function CodeDiffViewer({ oldCode, newCode, oldTitle = 'Original', newTitle = 'Modified', language = 'typescript', splitView = true, showLineNumbers = true, highlightLines = [], summary, }: CodeDiffViewerProps) { const [isSplit, setIsSplit] = useState(splitView); const [copied, setCopied] = useState(false); const copyNewCode = async () => { await navigator.clipboard.writeText(newCode); setCopied(true); setTimeout(() => setCopied(false), 2000); }; // Calculate summary if not provided const diffSummary = summary || (() => { const oldLines = oldCode.split('\n'); const newLines = newCode.split('\n'); let additions = 0; let deletions = 0; // Simple diff calculation const oldSet = new Set(oldLines); const newSet = new Set(newLines); newLines.forEach(line => { if (!oldSet.has(line)) additions++; }); oldLines.forEach(line => { if (!newSet.has(line)) deletions++; }); return { additions, deletions, changes: additions + deletions }; })(); return (
{/* Header */}
{language}
+{diffSummary.additions} -{diffSummary.deletions}
{/* File titles */}
{oldTitle}
{isSplit && (
{newTitle}
)}
{/* Diff viewer */}
{/* Footer with stats */}
{oldCode.split('\n').length} → {newCode.split('\n').length} lines {diffSummary.changes} change{diffSummary.changes !== 1 ? 's' : ''} detected
); } export default CodeDiffViewer;