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