Spaces:
Paused
Paused
| /** | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β 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 ( | |
| <div className="rounded-lg border border-border/30 bg-background/50 overflow-hidden"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between px-4 py-2 bg-muted/30 border-b border-border/30"> | |
| <div className="flex items-center gap-3"> | |
| <Badge variant="outline" className="text-[10px] font-mono"> | |
| {language} | |
| </Badge> | |
| <div className="flex items-center gap-2 text-xs"> | |
| <span className="text-green-500 font-mono">+{diffSummary.additions}</span> | |
| <span className="text-red-500 font-mono">-{diffSummary.deletions}</span> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => setIsSplit(!isSplit)} | |
| className="h-7 px-2" | |
| > | |
| {isSplit ? ( | |
| <AlignJustify className="w-3 h-3 mr-1" /> | |
| ) : ( | |
| <SplitSquareHorizontal className="w-3 h-3 mr-1" /> | |
| )} | |
| <span className="text-[10px]">{isSplit ? 'Unified' : 'Split'}</span> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={copyNewCode} | |
| className="h-7 px-2" | |
| > | |
| {copied ? ( | |
| <Check className="w-3 h-3 text-green-500" /> | |
| ) : ( | |
| <Copy className="w-3 h-3" /> | |
| )} | |
| </Button> | |
| </div> | |
| </div> | |
| {/* File titles */} | |
| <div className={cn( | |
| 'grid border-b border-border/30 bg-muted/20', | |
| isSplit ? 'grid-cols-2' : 'grid-cols-1' | |
| )}> | |
| <div className="px-4 py-1.5 text-xs font-mono text-muted-foreground border-r border-border/30"> | |
| {oldTitle} | |
| </div> | |
| {isSplit && ( | |
| <div className="px-4 py-1.5 text-xs font-mono text-muted-foreground"> | |
| {newTitle} | |
| </div> | |
| )} | |
| </div> | |
| {/* Diff viewer */} | |
| <div className="max-h-[400px] overflow-auto"> | |
| <ReactDiffViewer | |
| oldValue={oldCode} | |
| newValue={newCode} | |
| splitView={isSplit} | |
| showDiffOnly={false} | |
| useDarkTheme={true} | |
| compareMethod={DiffMethod.WORDS} | |
| styles={darkModeStyles} | |
| hideLineNumbers={!showLineNumbers} | |
| leftTitle={undefined} | |
| rightTitle={undefined} | |
| extraLinesSurroundingDiff={3} | |
| /> | |
| </div> | |
| {/* Footer with stats */} | |
| <div className="px-4 py-2 bg-muted/20 border-t border-border/30 flex items-center justify-between text-[10px] text-muted-foreground"> | |
| <span> | |
| {oldCode.split('\n').length} β {newCode.split('\n').length} lines | |
| </span> | |
| <span className="font-mono"> | |
| {diffSummary.changes} change{diffSummary.changes !== 1 ? 's' : ''} detected | |
| </span> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default CodeDiffViewer; | |