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

Avis

Timeline Branching — React Timeline Section — Incubator