diff options
| author | soryu <soryu@soryu.co> | 2026-01-06 04:08:11 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-11 03:01:13 +0000 |
| commit | 8b17a175c3e7e27b789812eba4e3cd760beadb10 (patch) | |
| tree | 7864dcaa2fa9db47fdfd4e8bfdb0b1dde832aa33 /makima/frontend/src/components/mesh/OverlayDiffViewer.tsx | |
| parent | f79c416c58557d2f946aa5332989afdfa8c021cd (diff) | |
| download | soryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.tar.gz soryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.zip | |
Initial Control system
Diffstat (limited to 'makima/frontend/src/components/mesh/OverlayDiffViewer.tsx')
| -rw-r--r-- | makima/frontend/src/components/mesh/OverlayDiffViewer.tsx | 476 |
1 files changed, 476 insertions, 0 deletions
diff --git a/makima/frontend/src/components/mesh/OverlayDiffViewer.tsx b/makima/frontend/src/components/mesh/OverlayDiffViewer.tsx new file mode 100644 index 0000000..74059a0 --- /dev/null +++ b/makima/frontend/src/components/mesh/OverlayDiffViewer.tsx @@ -0,0 +1,476 @@ +import { useState, useMemo } from "react"; + +interface DiffLine { + type: "add" | "remove" | "context" | "header" | "hunk"; + content: string; + oldLineNumber?: number; + newLineNumber?: number; +} + +interface DiffFile { + path: string; + status: "added" | "modified" | "deleted" | "renamed"; + oldPath?: string; // For renames + additions: number; + deletions: number; + lines: DiffLine[]; +} + +interface OverlayDiffViewerProps { + diff: string; + changedFiles?: string[]; + loading?: boolean; + error?: string; + onClose?: () => void; + title?: string; +} + +function parseDiff(diffText: string): DiffFile[] { + if (!diffText.trim()) return []; + + const files: DiffFile[] = []; + const lines = diffText.split("\n"); + + let currentFile: DiffFile | null = null; + let oldLineNum = 0; + let newLineNum = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // File header (diff --git a/path b/path) + if (line.startsWith("diff --git")) { + if (currentFile) { + files.push(currentFile); + } + + // Extract paths + const match = line.match(/diff --git a\/(.+) b\/(.+)/); + const oldPath = match?.[1] || ""; + const newPath = match?.[2] || oldPath; + + currentFile = { + path: newPath, + oldPath: oldPath !== newPath ? oldPath : undefined, + status: "modified", + additions: 0, + deletions: 0, + lines: [], + }; + continue; + } + + if (!currentFile) continue; + + // New file indicator + if (line.startsWith("new file mode")) { + currentFile.status = "added"; + continue; + } + + // Deleted file indicator + if (line.startsWith("deleted file mode")) { + currentFile.status = "deleted"; + continue; + } + + // Rename indicator + if (line.startsWith("rename from") || line.startsWith("rename to")) { + currentFile.status = "renamed"; + continue; + } + + // Hunk header (@@ -1,3 +1,4 @@) + if (line.startsWith("@@")) { + const hunkMatch = line.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/); + if (hunkMatch) { + oldLineNum = parseInt(hunkMatch[1], 10); + newLineNum = parseInt(hunkMatch[2], 10); + } + currentFile.lines.push({ + type: "hunk", + content: line, + }); + continue; + } + + // Skip other headers (---, +++, index, etc.) + if ( + line.startsWith("---") || + line.startsWith("+++") || + line.startsWith("index ") || + line.startsWith("Binary files") + ) { + currentFile.lines.push({ + type: "header", + content: line, + }); + continue; + } + + // Diff content + if (line.startsWith("+")) { + currentFile.additions++; + currentFile.lines.push({ + type: "add", + content: line.substring(1), + newLineNumber: newLineNum++, + }); + } else if (line.startsWith("-")) { + currentFile.deletions++; + currentFile.lines.push({ + type: "remove", + content: line.substring(1), + oldLineNumber: oldLineNum++, + }); + } else if (line.startsWith(" ") || line === "") { + currentFile.lines.push({ + type: "context", + content: line.substring(1) || "", + oldLineNumber: oldLineNum++, + newLineNumber: newLineNum++, + }); + } + } + + if (currentFile) { + files.push(currentFile); + } + + return files; +} + +function DiffFileView({ file, collapsed, onToggle }: { file: DiffFile; collapsed: boolean; onToggle: () => void }) { + const statusColors: Record<DiffFile["status"], string> = { + added: "text-green-400 bg-green-400/10", + modified: "text-yellow-400 bg-yellow-400/10", + deleted: "text-red-400 bg-red-400/10", + renamed: "text-purple-400 bg-purple-400/10", + }; + + const statusLabels: Record<DiffFile["status"], string> = { + added: "A", + modified: "M", + deleted: "D", + renamed: "R", + }; + + return ( + <div className="border border-[rgba(117,170,252,0.2)] mb-2"> + {/* File header */} + <button + onClick={onToggle} + className="w-full flex items-center gap-2 px-3 py-2 bg-[rgba(0,0,0,0.2)] hover:bg-[rgba(0,0,0,0.3)] transition-colors text-left" + > + <span className="font-mono text-[10px] text-[#555]"> + {collapsed ? "▶" : "▼"} + </span> + <span + className={`px-1.5 py-0.5 font-mono text-[9px] font-bold ${statusColors[file.status]}`} + > + {statusLabels[file.status]} + </span> + <span className="font-mono text-sm text-[#dbe7ff] flex-1 truncate"> + {file.oldPath ? ( + <> + <span className="text-[#555]">{file.oldPath}</span> + <span className="text-[#75aafc] mx-1">→</span> + {file.path} + </> + ) : ( + file.path + )} + </span> + <span className="font-mono text-[10px]"> + {file.additions > 0 && ( + <span className="text-green-400 mr-2">+{file.additions}</span> + )} + {file.deletions > 0 && ( + <span className="text-red-400">-{file.deletions}</span> + )} + </span> + </button> + + {/* File content */} + {!collapsed && ( + <div className="overflow-x-auto"> + <table className="w-full font-mono text-xs"> + <tbody> + {file.lines.map((line, i) => { + if (line.type === "header" || line.type === "hunk") { + return ( + <tr + key={i} + className="bg-[rgba(117,170,252,0.05)]" + > + <td + colSpan={3} + className="px-2 py-0.5 text-[#75aafc] select-none" + > + {line.content} + </td> + </tr> + ); + } + + const bgColor = + line.type === "add" + ? "bg-green-400/10" + : line.type === "remove" + ? "bg-red-400/10" + : ""; + + const textColor = + line.type === "add" + ? "text-green-400" + : line.type === "remove" + ? "text-red-400" + : "text-[#9bc3ff]"; + + const prefix = + line.type === "add" + ? "+" + : line.type === "remove" + ? "-" + : " "; + + return ( + <tr key={i} className={bgColor}> + {/* Old line number */} + <td className="w-10 px-2 py-0 text-right text-[#555] select-none border-r border-[rgba(117,170,252,0.1)]"> + {line.type !== "add" ? line.oldLineNumber : ""} + </td> + {/* New line number */} + <td className="w-10 px-2 py-0 text-right text-[#555] select-none border-r border-[rgba(117,170,252,0.1)]"> + {line.type !== "remove" ? line.newLineNumber : ""} + </td> + {/* Content */} + <td className={`px-2 py-0 whitespace-pre ${textColor}`}> + <span className="select-none mr-1 text-[#555]"> + {prefix} + </span> + {line.content} + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + )} + </div> + ); +} + +export function OverlayDiffViewer({ + diff, + changedFiles, + loading, + error, + onClose, + title = "Overlay Changes", +}: OverlayDiffViewerProps) { + const [collapsedFiles, setCollapsedFiles] = useState<Set<string>>(new Set()); + const [showFullDiff, setShowFullDiff] = useState(true); + + const parsedFiles = useMemo(() => parseDiff(diff), [diff]); + + const toggleFile = (path: string) => { + setCollapsedFiles((prev) => { + const next = new Set(prev); + if (next.has(path)) { + next.delete(path); + } else { + next.add(path); + } + return next; + }); + }; + + const expandAll = () => setCollapsedFiles(new Set()); + const collapseAll = () => setCollapsedFiles(new Set(parsedFiles.map((f) => f.path))); + + // Calculate totals + const totals = useMemo(() => { + return parsedFiles.reduce( + (acc, file) => ({ + additions: acc.additions + file.additions, + deletions: acc.deletions + file.deletions, + files: acc.files + 1, + }), + { additions: 0, deletions: 0, files: 0 } + ); + }, [parsedFiles]); + + if (loading) { + return ( + <div className="panel p-4"> + <div className="flex items-center justify-center h-32"> + <div className="font-mono text-sm text-[#75aafc]"> + Loading diff... + </div> + </div> + </div> + ); + } + + if (error) { + return ( + <div className="panel p-4"> + <div className="flex items-center justify-between mb-3"> + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> + {title} + </div> + {onClose && ( + <button + onClick={onClose} + className="font-mono text-xs text-[#555] hover:text-[#9bc3ff]" + > + Close + </button> + )} + </div> + <div className="bg-red-400/10 border border-red-400/30 p-3 font-mono text-sm text-red-400"> + {error} + </div> + </div> + ); + } + + if (!diff.trim() && (!changedFiles || changedFiles.length === 0)) { + return ( + <div className="panel p-4"> + <div className="flex items-center justify-between mb-3"> + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> + {title} + </div> + {onClose && ( + <button + onClick={onClose} + className="font-mono text-xs text-[#555] hover:text-[#9bc3ff]" + > + Close + </button> + )} + </div> + <div className="text-center py-8 font-mono text-sm text-[#555]"> + No changes detected in overlay + </div> + </div> + ); + } + + return ( + <div className="panel flex flex-col max-h-[600px]"> + {/* Header */} + <div className="flex items-center justify-between p-3 border-b border-[rgba(117,170,252,0.2)] shrink-0"> + <div className="flex items-center gap-4"> + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> + {title} + </div> + <div className="font-mono text-[10px] text-[#75aafc]"> + {totals.files} file{totals.files !== 1 ? "s" : ""} changed + {totals.additions > 0 && ( + <span className="text-green-400 ml-2">+{totals.additions}</span> + )} + {totals.deletions > 0 && ( + <span className="text-red-400 ml-2">-{totals.deletions}</span> + )} + </div> + </div> + <div className="flex items-center gap-2"> + <button + onClick={expandAll} + className="font-mono text-[10px] text-[#555] hover:text-[#9bc3ff]" + > + Expand All + </button> + <button + onClick={collapseAll} + className="font-mono text-[10px] text-[#555] hover:text-[#9bc3ff]" + > + Collapse All + </button> + <button + onClick={() => setShowFullDiff(!showFullDiff)} + className="font-mono text-[10px] text-[#555] hover:text-[#9bc3ff]" + > + {showFullDiff ? "File List" : "Full Diff"} + </button> + {onClose && ( + <button + onClick={onClose} + className="font-mono text-xs text-[#555] hover:text-[#9bc3ff] ml-2" + > + Close + </button> + )} + </div> + </div> + + {/* Content */} + <div className="flex-1 overflow-y-auto p-3"> + {showFullDiff ? ( + // Full diff view + parsedFiles.length > 0 ? ( + parsedFiles.map((file) => ( + <DiffFileView + key={file.path} + file={file} + collapsed={collapsedFiles.has(file.path)} + onToggle={() => toggleFile(file.path)} + /> + )) + ) : ( + // Fallback to raw diff + <pre className="font-mono text-xs text-[#9bc3ff] whitespace-pre-wrap"> + {diff} + </pre> + ) + ) : ( + // File list view + <div className="space-y-1"> + {(changedFiles || parsedFiles.map((f) => f.path)).map((path) => { + const file = parsedFiles.find((f) => f.path === path); + return ( + <div + key={path} + className="flex items-center gap-2 px-2 py-1 hover:bg-[rgba(117,170,252,0.05)]" + > + {file && ( + <span + className={`px-1 py-0.5 font-mono text-[9px] ${ + file.status === "added" + ? "text-green-400 bg-green-400/10" + : file.status === "deleted" + ? "text-red-400 bg-red-400/10" + : file.status === "renamed" + ? "text-purple-400 bg-purple-400/10" + : "text-yellow-400 bg-yellow-400/10" + }`} + > + {file.status.charAt(0).toUpperCase()} + </span> + )} + <span className="font-mono text-sm text-[#dbe7ff] flex-1 truncate"> + {path} + </span> + {file && ( + <span className="font-mono text-[10px]"> + {file.additions > 0 && ( + <span className="text-green-400 mr-1">+{file.additions}</span> + )} + {file.deletions > 0 && ( + <span className="text-red-400">-{file.deletions}</span> + )} + </span> + )} + </div> + ); + })} + </div> + )} + </div> + </div> + ); +} |
