Retour au catalogue

Changelog Commit Log

Changelog style git log avec hashes de commits, indicateurs de branches et visualisation de merges.

changelogcomplex Both Responsive a11y
darkminimalsaasuniversalstacked
Theme
"use client";

import { motion } from "framer-motion";
import { GitCommit, GitMerge, GitBranch, User } from "lucide-react";

interface Commit {
  hash: string;
  message: string;
  author: string;
  date: string;
  branch: string;
  type: "commit" | "merge";
  mergeFrom?: string;
}

interface ChangelogCommitLogProps {
  title?: string;
  branch?: string;
  commits?: Commit[];
}

const EASE = [0.16, 1, 0.3, 1] as const;

function getPrefix(msg: string): { prefix: string; rest: string; color: string } {
  const match = msg.match(/^(feat|fix|perf|chore|docs|refactor|test|style):\s*(.*)/);
  if (!match) return { prefix: "", rest: msg, color: "var(--color-foreground-muted)" };
  const prefixColors: Record<string, string> = {
    feat: "var(--color-accent)",
    fix: "var(--color-foreground-muted)",
    perf: "var(--color-accent)",
    chore: "var(--color-foreground-muted)",
  };
  return { prefix: match[1], rest: match[2], color: prefixColors[match[1]] ?? "var(--color-foreground-muted)" };
}

export default function ChangelogCommitLog({
  title = "Journal des commits",
  branch = "main",
  commits = [],
}: ChangelogCommitLogProps) {
  return (
    <section style={{ paddingTop: "var(--section-padding-y)", paddingBottom: "var(--section-padding-y)", background: "var(--color-background)" }}>
      <div style={{ maxWidth: 780, margin: "0 auto", padding: "0 var(--container-padding-x)" }}>
        <motion.div
          initial={{ opacity: 0, y: 16 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ duration: 0.5, ease: EASE }}
          style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "2rem", flexWrap: "wrap", gap: "0.75rem" }}
        >
          <h2 style={{ fontFamily: "var(--font-sans)", fontSize: "clamp(1.5rem, 3vw, 2.25rem)", fontWeight: 700, color: "var(--color-foreground)" }}>{title}</h2>
          <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", padding: "0.35rem 0.85rem", borderRadius: "var(--radius-full)", background: "var(--color-background-alt)", border: "1px solid var(--color-border)" }}>
            <GitBranch style={{ width: 14, height: 14, color: "var(--color-accent)" }} />
            <span style={{ fontSize: "0.8125rem", fontWeight: 600, fontFamily: "monospace", color: "var(--color-foreground)" }}>{branch}</span>
          </div>
        </motion.div>

        <div style={{ position: "relative", borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)", overflow: "hidden", background: "var(--color-background-card)" }}>
          {/* Left line */}
          <div aria-hidden style={{ position: "absolute", left: 28, top: 0, bottom: 0, width: 2, background: "var(--color-border)" }} />

          {commits.map((commit, i) => {
            const { prefix, rest, color } = getPrefix(commit.message);
            const isMerge = commit.type === "merge";
            return (
              <motion.div
                key={commit.hash}
                initial={{ opacity: 0, x: -8 }}
                whileInView={{ opacity: 1, x: 0 }}
                viewport={{ once: true }}
                transition={{ duration: 0.4, delay: i * 0.04, ease: EASE }}
                style={{ display: "flex", gap: "1rem", padding: "1rem 1.25rem 1rem 0", marginLeft: "1rem", borderBottom: i < commits.length - 1 ? "1px solid var(--color-border)" : "none", position: "relative" }}
              >
                {/* Icon */}
                <div style={{ width: 24, display: "flex", justifyContent: "center", flexShrink: 0, paddingTop: 2, zIndex: 1 }}>
                  {isMerge ? (
                    <div style={{ width: 20, height: 20, borderRadius: "var(--radius-full)", background: "var(--color-accent)", display: "flex", alignItems: "center", justifyContent: "center" }}>
                      <GitMerge style={{ width: 12, height: 12, color: "var(--color-background)" }} />
                    </div>
                  ) : (
                    <div style={{ width: 20, height: 20, borderRadius: "var(--radius-full)", background: "var(--color-background-card)", border: "2px solid var(--color-border)", display: "flex", alignItems: "center", justifyContent: "center" }}>
                      <GitCommit style={{ width: 12, height: 12, color: "var(--color-foreground-muted)" }} />
                    </div>
                  )}
                </div>

                {/* Content */}
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.2rem", flexWrap: "wrap" }}>
                    <code style={{ fontSize: "0.75rem", fontFamily: "monospace", color: "var(--color-accent)", background: "var(--color-accent-subtle)", padding: "0.1rem 0.4rem", borderRadius: "var(--radius-sm)" }}>{commit.hash}</code>
                    {isMerge && commit.mergeFrom && (
                      <span style={{ fontSize: "0.6875rem", padding: "0.1rem 0.5rem", borderRadius: "var(--radius-full)", background: "var(--color-background-alt)", color: "var(--color-foreground-muted)", fontFamily: "monospace" }}>{commit.mergeFrom}</span>
                    )}
                  </div>
                  <p style={{ fontSize: "0.9375rem", color: "var(--color-foreground)", lineHeight: 1.4 }}>
                    {prefix && <span style={{ fontWeight: 700, color }}>{prefix}: </span>}
                    {rest}
                  </p>
                  <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", marginTop: "0.3rem", fontSize: "0.75rem", color: "var(--color-foreground-muted)" }}>
                    <span style={{ display: "flex", alignItems: "center", gap: "0.2rem" }}><User style={{ width: 11, height: 11 }} /> {commit.author}</span>
                    <span>{commit.date}</span>
                  </div>
                </div>
              </motion.div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

Avis

Changelog Commit Log — React Changelog Section — Incubator