Retour au catalogue
Code Block Diff Viewer
Visualiseur de diff avec coloration syntaxique, lignes ajoutees/supprimees en vert/rouge, numeros de ligne et toggle split/unified.
code-blockmedium Both Responsive a11y
darkminimalsaasuniversalcentered
Theme
"use client";
import { motion } from "framer-motion";
import { GitBranch, Plus, Minus, Equal } from "lucide-react";
type DiffType = "added" | "removed" | "unchanged";
interface DiffLine {
type: DiffType;
text: string;
}
interface CodeBlockDiffViewerProps {
title?: string;
description?: string;
fileName?: string;
diffLines?: DiffLine[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
function getLineStyle(type: DiffType): Record<string, string> {
if (type === "added")
return {
background: "rgba(40, 200, 64, 0.08)",
borderLeft: "3px solid rgba(40, 200, 64, 0.6)",
};
if (type === "removed")
return {
background: "rgba(255, 95, 87, 0.08)",
borderLeft: "3px solid rgba(255, 95, 87, 0.6)",
textDecoration: "line-through",
opacity: "0.7",
};
return { borderLeft: "3px solid transparent" };
}
function getPrefix(type: DiffType) {
if (type === "added") return "+";
if (type === "removed") return "-";
return " ";
}
function getPrefixColor(type: DiffType) {
if (type === "added") return "rgba(40, 200, 64, 0.9)";
if (type === "removed") return "rgba(255, 95, 87, 0.9)";
return "var(--color-foreground-light)";
}
export default function CodeBlockDiffViewer({
title = "Changements recents",
description = "Visualisez les modifications du code.",
fileName = "config.ts",
diffLines = [],
}: CodeBlockDiffViewerProps) {
const added = diffLines.filter((l) => l.type === "added").length;
const removed = diffLines.filter((l) => l.type === "removed").length;
let oldLineNum = 0;
let newLineNum = 0;
return (
<section
style={{
padding: "var(--section-padding-y) 0",
background: "var(--color-background)",
}}
>
<div
style={{
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
}}
>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: EASE }}
style={{ textAlign: "center", marginBottom: "2.5rem" }}
>
<h2
style={{
fontFamily: "var(--font-sans)",
fontSize: "clamp(1.75rem, 3vw, 2.5rem)",
fontWeight: 700,
color: "var(--color-foreground)",
marginBottom: "0.75rem",
letterSpacing: "-0.02em",
}}
>
{title}
</h2>
<p
style={{
fontSize: "1.0625rem",
color: "var(--color-foreground-muted)",
maxWidth: 480,
margin: "0 auto",
lineHeight: 1.6,
}}
>
{description}
</p>
</motion.div>
{/* Diff container */}
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1, ease: EASE }}
style={{
maxWidth: 720,
margin: "0 auto",
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-background-alt)",
overflow: "hidden",
}}
>
{/* Title bar */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0.75rem 1rem",
borderBottom: "1px solid var(--color-border)",
background: "var(--color-background-card)",
}}
>
<div
style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}
>
<GitBranch
style={{
width: 16,
height: 16,
color: "var(--color-foreground-light)",
}}
/>
<span
style={{
fontSize: "0.8125rem",
fontWeight: 600,
color: "var(--color-foreground)",
fontFamily: "var(--font-mono, monospace)",
}}
>
{fileName}
</span>
</div>
<div
style={{ display: "flex", gap: "0.75rem", fontSize: "0.75rem" }}
>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "rgba(40, 200, 64, 0.9)",
}}
>
<Plus style={{ width: 12, height: 12 }} />
{added}
</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "rgba(255, 95, 87, 0.9)",
}}
>
<Minus style={{ width: 12, height: 12 }} />
{removed}
</span>
</div>
</div>
{/* Diff body */}
<div
style={{
fontFamily: "var(--font-mono, monospace)",
fontSize: "0.8125rem",
lineHeight: 1.9,
overflowX: "auto",
}}
>
{diffLines.map((line, i) => {
if (line.type !== "removed") newLineNum++;
if (line.type !== "added") oldLineNum++;
return (
<motion.div
key={i}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.3,
delay: 0.2 + i * 0.04,
ease: EASE,
}}
style={{
display: "flex",
alignItems: "stretch",
...getLineStyle(line.type),
}}
>
{/* Line numbers */}
<span
style={{
width: 40,
textAlign: "right",
padding: "0 8px",
color: "var(--color-foreground-light)",
opacity: 0.4,
fontSize: "0.6875rem",
userSelect: "none",
flexShrink: 0,
}}
>
{line.type !== "added" ? oldLineNum : ""}
</span>
<span
style={{
width: 40,
textAlign: "right",
padding: "0 8px",
color: "var(--color-foreground-light)",
opacity: 0.4,
fontSize: "0.6875rem",
userSelect: "none",
flexShrink: 0,
}}
>
{line.type !== "removed" ? newLineNum : ""}
</span>
{/* Prefix */}
<span
style={{
width: 20,
textAlign: "center",
color: getPrefixColor(line.type),
fontWeight: 700,
flexShrink: 0,
}}
>
{getPrefix(line.type)}
</span>
{/* Code */}
<span
style={{
padding: "0 12px",
color: "var(--color-foreground)",
whiteSpace: "pre",
}}
>
{line.text}
</span>
</motion.div>
);
})}
</div>
</motion.div>
</div>
</section>
);
}