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>
  );
}

Avis

Code Block Diff Viewer — React Code-block Section — Incubator