d-ui logod-ui
データ表示

Timeline

縦型タイムラインで時系列イベントを表示するコンポーネント。

インストール

npx shadcn add https://d-ui.daigo-suhara.com/registry/timeline.json

サンプル

  1. プロジェクト開始

    2024/01
  2. ベータリリース

    2024/03

    初回テスト公開

  3. バグ修正

    2024/04

    クリティカルな問題を解決

  4. 本番リリース

    2024/06

使い方

import { Timeline } from "@/components/ui/timeline";

export default function Example() {
  return (
    <Timeline items={[
      { title: "プロジェクト開始", date: "2024/01", variant: "success" },
      { title: "ベータリリース", description: "初回テスト完了", date: "2024/03" },
      { title: "本番リリース", date: "2024/06", variant: "success" },
    ]} />
  );
}

プロパティ

items必須
TimelineItem[]

表示するアイテムの配列

ソースコード

import * as React from "react";
import { cn } from "@/lib/utils";

export interface TimelineItem {
  id?: string;
  title: string;
  description?: string;
  date?: string;
  icon?: React.ReactNode;
  variant?: "default" | "success" | "warning" | "danger";
}

interface TimelineProps {
  items: TimelineItem[];
  className?: string;
}

const variantDot: Record<string, string> = {
  default: "bg-primary",
  success: "bg-emerald-500",
  warning: "bg-amber-500",
  danger: "bg-red-500",
};

const variantRing: Record<string, string> = {
  default: "ring-primary/20",
  success: "ring-emerald-500/20",
  warning: "ring-amber-500/20",
  danger: "ring-red-500/20",
};

export function Timeline({ items, className }: TimelineProps) {
  return (
    <ol className={cn("relative space-y-0", className)}>
      {items.map((item, i) => {
        const variant = item.variant ?? "default";
        const isLast = i === items.length - 1;
        return (
          <li key={item.id ?? i} className="relative flex gap-4 pb-6 last:pb-0">
            {/* Line */}
            {!isLast && (
              <div className="absolute left-[15px] top-8 bottom-0 w-px bg-border" />
            )}
            {/* Dot */}
            <div className="relative z-10 mt-1 flex h-8 w-8 shrink-0 items-center justify-center">
              <div
                className={cn(
                  "flex h-8 w-8 items-center justify-center rounded-full ring-4",
                  variantRing[variant]
                )}
              >
                {item.icon ? (
                  <div className={cn("h-4 w-4", `text-${variantDot[variant].replace("bg-", "")}`)}>
                    {item.icon}
                  </div>
                ) : (
                  <div className={cn("h-2.5 w-2.5 rounded-full", variantDot[variant])} />
                )}
              </div>
            </div>
            {/* Content */}
            <div className="flex-1 min-w-0 pt-1">
              <div className="flex items-baseline justify-between gap-2 mb-0.5">
                <p className="text-sm font-medium leading-none">{item.title}</p>
                {item.date && (
                  <span className="shrink-0 text-xs text-muted-foreground">
                    {item.date}
                  </span>
                )}
              </div>
              {item.description && (
                <p className="mt-1 text-sm text-muted-foreground leading-relaxed">
                  {item.description}
                </p>
              )}
            </div>
          </li>
        );
      })}
    </ol>
  );
}