Retour au catalogue

Roadmap Card Board

Board Trello-style avec colonnes Now/Next/Later, cartes avec tags et indicateurs de priorite.

roadmapmedium Both Responsive a11y
playfulcorporatesaasagencyuniversalgrid
Theme
"use client";

import { motion } from "framer-motion";
import { ArrowUp, Layout } from "lucide-react";

interface BoardCard {
  title: string;
  tags: string[];
  votes: number;
}

interface BoardColumn {
  title: string;
  status: "now" | "next" | "later";
  cards: BoardCard[];
}

interface RoadmapCardBoardProps {
  title?: string;
  description?: string;
  columns?: BoardColumn[];
}

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

const statusDot: Record<string, string> = {
  now: "var(--color-accent)",
  next: "var(--color-foreground-muted)",
  later: "var(--color-border)",
};

export default function RoadmapCardBoard({
  title = "Ce qu'on prepare",
  description = "Notre roadmap publique, en toute transparence",
  columns = [],
}: RoadmapCardBoardProps) {
  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)" }}>
        <motion.div
          initial={{ opacity: 0, y: 16 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ duration: 0.5, ease: EASE }}
          style={{ textAlign: "center", marginBottom: "2.5rem" }}
        >
          <div style={{ display: "inline-flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.75rem" }}>
            <Layout style={{ width: 20, height: 20, color: "var(--color-accent)" }} />
            <h2 style={{ fontFamily: "var(--font-sans)", fontSize: "clamp(1.75rem, 3.5vw, 2.75rem)", fontWeight: 700, color: "var(--color-foreground)" }}>
              {title}
            </h2>
          </div>
          <p style={{ fontSize: "1.0625rem", color: "var(--color-foreground-muted)", maxWidth: "520px", margin: "0 auto" }}>
            {description}
          </p>
        </motion.div>

        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: "1.25rem" }}>
          {columns.map((col, ci) => (
            <motion.div
              key={ci}
              initial={{ opacity: 0, y: 20 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true }}
              transition={{ duration: 0.45, delay: ci * 0.1, ease: EASE }}
              style={{
                background: "var(--color-background-alt)",
                borderRadius: "var(--radius-lg)",
                border: "1px solid var(--color-border)",
                padding: "1.25rem",
              }}
            >
              <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "1rem" }}>
                <div style={{ width: 8, height: 8, borderRadius: "var(--radius-full)", background: statusDot[col.status] }} />
                <h3 style={{ fontFamily: "var(--font-sans)", fontSize: "0.9375rem", fontWeight: 700, color: "var(--color-foreground)" }}>
                  {col.title}
                </h3>
                <span style={{ fontSize: "0.75rem", color: "var(--color-foreground-muted)", marginLeft: "auto" }}>
                  {col.cards.length}
                </span>
              </div>

              <div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
                {col.cards.map((card, i) => (
                  <motion.div
                    key={i}
                    initial={{ opacity: 0, y: 8 }}
                    whileInView={{ opacity: 1, y: 0 }}
                    viewport={{ once: true }}
                    transition={{ duration: 0.3, delay: 0.15 + i * 0.05, ease: EASE }}
                    whileHover={{ y: -2 }}
                    style={{
                      padding: "0.875rem 1rem",
                      borderRadius: "var(--radius-md)",
                      background: "var(--color-background-card)",
                      border: "1px solid var(--color-border)",
                      cursor: "default",
                    }}
                  >
                    <div style={{ fontSize: "0.8125rem", fontWeight: 600, color: "var(--color-foreground)", marginBottom: "0.5rem" }}>
                      {card.title}
                    </div>
                    <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                      <div style={{ display: "flex", gap: "0.25rem", flexWrap: "wrap" }}>
                        {card.tags.map((tag, ti) => (
                          <span
                            key={ti}
                            style={{
                              fontSize: "0.625rem", fontWeight: 600, padding: "0.15rem 0.5rem",
                              borderRadius: "var(--radius-full)",
                              background: "var(--color-accent-subtle, var(--color-background-alt))",
                              color: "var(--color-accent)",
                            }}
                          >
                            {tag}
                          </span>
                        ))}
                      </div>
                      <span style={{ display: "flex", alignItems: "center", gap: "2px", fontSize: "0.6875rem", color: "var(--color-foreground-muted)" }}>
                        <ArrowUp style={{ width: 10, height: 10 }} />
                        {card.votes}
                      </span>
                    </div>
                  </motion.div>
                ))}
              </div>
            </motion.div>
          ))}
        </div>
      </div>
    </section>
  );
}

Avis

Roadmap Card Board — React Roadmap Section — Incubator