Retour au catalogue
Timeline Branching
Timeline style git avec branches, merges et commits visuels pour representer des flux paralleles.
timelinecomplex Both Responsive a11y
boldboldsaassaasstacked
Theme
"use client";
import { useRef } from "react";
import { motion, useInView } from "framer-motion";
import {
GitBranch,
GitCommit,
GitMerge,
GitPullRequest,
Tag,
CheckCircle2,
} from "lucide-react";
interface BranchCommit {
message: string;
author: string;
date: string;
type: "commit" | "merge" | "branch" | "tag" | "pr";
branch: string;
branchColor: string;
description?: string;
}
interface TimelineBranchingProps {
commits?: BranchCommit[];
title?: string;
subtitle?: string;
}
const typeIcons: Record<string, React.ElementType> = {
commit: GitCommit,
merge: GitMerge,
branch: GitBranch,
tag: Tag,
pr: GitPullRequest,
};
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];
export default function TimelineBranching({
commits = [],
title = "Historique des versions",
subtitle = "Toutes les branches de developpement",
}: TimelineBranchingProps) {
const sectionRef = useRef<HTMLDivElement>(null);
const isInView = useInView(sectionRef, { once: true, amount: 0.2 });
const branches = [...new Set(commits.map((c) => c.branch))];
return (
<section ref={sectionRef} className="py-20 px-6">
<div className="max-w-3xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, ease }}
className="text-center mb-12"
>
<h2
className="text-3xl font-bold mb-3"
style={{ color: "var(--color-foreground)" }}
>
{title}
</h2>
<p
className="text-base"
style={{ color: "var(--color-foreground-muted)" }}
>
{subtitle}
</p>
{/* Branch legend */}
<div className="flex flex-wrap justify-center gap-3 mt-6">
{branches.map((branch) => {
const commitForBranch = commits.find(
(c) => c.branch === branch
);
return (
<div
key={branch}
className="flex items-center gap-1.5 text-xs font-mono"
style={{ color: "var(--color-foreground-muted)" }}
>
<div
className="w-3 h-3 rounded-full"
style={{
background:
commitForBranch?.branchColor || "var(--color-accent)",
}}
/>
{branch}
</div>
);
})}
</div>
</motion.div>
{/* Git graph */}
<div className="relative">
{/* Central line */}
<div
className="absolute left-8 top-0 bottom-0 w-0.5"
style={{ background: "var(--color-border)" }}
/>
<div className="flex flex-col gap-1">
{commits.map((commit, i) => {
const Icon = typeIcons[commit.type] || GitCommit;
const isOff = commit.branch !== "main";
return (
<motion.div
key={i}
initial={{ opacity: 0, x: isOff ? 20 : -20 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.4, delay: i * 0.08, ease }}
className="relative flex items-start gap-4"
style={{ marginLeft: isOff ? 40 : 0 }}
>
{/* Branch connector */}
{isOff && (
<svg
className="absolute -left-10 top-4"
width="40"
height="24"
viewBox="0 0 40 24"
fill="none"
>
<path
d="M0 0 C10 0, 20 12, 40 12"
stroke={commit.branchColor}
strokeWidth="2"
fill="none"
strokeDasharray={
commit.type === "branch" ? "4 4" : "none"
}
/>
</svg>
)}
{/* Node */}
<div className="relative z-10 shrink-0">
<motion.div
className="w-8 h-8 rounded-full flex items-center justify-center"
style={{
background: commit.branchColor,
boxShadow: `0 0 0 4px var(--color-background)`,
}}
whileHover={{ scale: 1.2 }}
>
<Icon
size={16}
style={{ color: "var(--color-background)" }}
/>
</motion.div>
</div>
{/* Content */}
<motion.div
className="flex-1 pb-6 pt-0.5"
whileHover={{ x: 4 }}
>
<div className="flex items-center gap-2 flex-wrap">
<span
className="font-semibold text-sm"
style={{ color: "var(--color-foreground)" }}
>
{commit.message}
</span>
<span
className="text-[10px] px-2 py-0.5 rounded-full font-mono"
style={{
background: commit.branchColor + "22",
color: commit.branchColor,
border: `1px solid ${commit.branchColor}44`,
}}
>
{commit.branch}
</span>
{commit.type === "tag" && (
<span
className="text-[10px] px-2 py-0.5 rounded-full font-medium flex items-center gap-1"
style={{
background: "var(--color-accent-subtle)",
color: "var(--color-accent)",
}}
>
<CheckCircle2 size={10} /> Release
</span>
)}
</div>
{commit.description && (
<p
className="text-xs mt-1 leading-relaxed"
style={{ color: "var(--color-foreground-muted)" }}
>
{commit.description}
</p>
)}
<div
className="flex items-center gap-3 mt-1.5 text-[11px]"
style={{ color: "var(--color-foreground-light)" }}
>
<span>{commit.author}</span>
<span>{commit.date}</span>
</div>
</motion.div>
</motion.div>
);
})}
</div>
</div>
</div>
</section>
);
}