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