mcpField
D
dashboard-builder

dashboard-builder

github.com/qwenlmデザイン・UI143105 回インストール23 週間前に更新
リポジトリ

dashboard-builder について

dashboard-builderは、データ可視化ダッシュボードを開発するチーム向けのスキルです。Next.jsやRechartsなどを使うUI指針とコンポーネント例を提供し、プロジェクトの初期化・起動スクリプトも含みます。

主な機能

  1. 01ダークテーマのカード型ダッシュボード設計
  2. 02KPI、データカード、数値アニメーションの例
  3. 03スクロール、ホバー、進捗アニメーションの指針
  4. 04API、データベース、静的データに対応
  5. 05プロジェクトの初期化と開発サーバー起動スクリプト

活用シーン

  1. 01大画面向けの運用ダッシュボード構築
  2. 02KPIや時系列データの可視化
  3. 03APIやデータベースのデータ連携
  4. 04Next.jsダッシュボードの迅速な初期化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/qwenlm/qwen-code-examples --skill dashboard-builder

インストール後、Claude Code で/dashboard-builderを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

ADDITIONAL_COMPONENTS.md
DATA_INTEGRATION.md
package.json
QUICKSTART.md
setup-and-run.sh
references/
big-screen-ui.md
data-api-integration.md
recharts-patterns.md

詳細

ダッシュボードビルダー

モダンな UI、豊富なビジュアライゼーション、動的なエフェクトを備えた、プロフェッショナルなデータ可視化ダッシュボードを構築します。

技術スタック

  • フロントエンド: Next.js 14+(App Router)+ React 18+
  • UI コンポーネント: shadcn/ui + Tailwind CSS
  • チャート: Recharts
  • アニメーション: Framer Motion
  • バックエンド: Express.js
  • データソース: 設定可能な API、データベース、または静的データ

デザイン要件

ビジュアルスタイル(プロフェッショナルな PC ダッシュボード)
  • ダークテーマと控えめなグラデーション(派手にしない)
  • すっきりとしたカードデザインに柔らかなシャドウとボーダー
  • プロフェッショナルなカラーパレット:
    • 背景: slate-900 (#0f172a) から slate-950 (#020617)
    • カード: slate-800/50 に控えめなボーダー
    • 成功: emerald-500 (#10b981)
    • 危険: rose-500 (#f43f5e)
    • アクセント: blue-500 (#3b82f6)
  • 大画面(4K、ウルトラワイド)に最適化した大きな文字
  • グリッドベースのレイアウトで明確な視覚的階層
アニメーション要件
  • 表示時のアニメーション: ページ読み込み時に控えめなフェードインと上方向へのスライド
  • スクロール連動アニメーション: カードが画面内に入るとアニメーション
  • 数値カウントアニメーション: 値が滑らかに変化
  • プログレスバーのアニメーション: イージング付きでバーが伸長
  • ホバーエフェクト: カードがわずかに浮き上がり、シャドウが付く
  • ライブデータのパルス: データ更新時に控えめなインジケーターを表示

プロジェクト構成

project-root/
├── app/
│   ├── layout.tsx
│   ├── page.tsx              # Landing/input page
│   └── dashboard/
│       └── page.tsx          # Results dashboard
├── components/
│   ├── ui/                   # shadcn components
│   ├── charts/
│   │   ├── LineChart.tsx
│   │   ├── BarChart.tsx
│   │   ├── AreaChart.tsx
│   │   └── PieChart.tsx
│   ├── dashboard/
│   │   ├── KPICard.tsx
│   │   ├── DataCard.tsx
│   │   ├── StatsPanel.tsx
│   │   └── DataTable.tsx
│   └── effects/
│       └── AnimatedNumber.tsx
├── lib/
│   ├── api.ts
│   ├── utils.ts
│   └── animations.ts
├── server/
│   ├── index.ts
│   ├── routes/
│   │   └── data.ts
│   └── services/
│       └── dataService.ts
└── styles/
    └── globals.css

PC 大画面レイアウト

4K ダッシュボードグリッド(3840x2160)
export default function DashboardPage() {
  return (
    <div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-900 to-slate-950 p-6">
      {/* Header */}
      <header className="flex items-center justify-between mb-8">
        <h1 className="text-4xl font-bold text-white">Dashboard</h1>
        <div className="flex items-center gap-4">
          <LiveIndicator />
          <span className="text-slate-400 text-lg">Last updated: {time}</span>
        </div>
      </header>

      {/* KPI Row */}
      <div className="grid grid-cols-4 gap-6 mb-8">
        <KPICard title="Metric 1" value={125000} prefix="$" trend={5.2} />
        <KPICard title="Metric 2" value={2340} prefix="$" trend={1.8} />
        <KPICard title="Metric 3" value={18.5} suffix="%" trend={0} />
        <KPICard title="Metric 4" value={12} trend={0} />
      </div>

      {/* Main Content */}
      <div className="grid grid-cols-12 gap-6">
        {/* Main Chart - 8 columns */}
        <div className="col-span-8">
          <Card className="h-[500px]">
            <CardHeader>
              <CardTitle>Main Metric Trend</CardTitle>
            </CardHeader>
            <CardContent>
              <LineChart data={metricData} />
            </CardContent>
          </Card>
        </div>

        {/* Side Panel - 4 columns */}
        <div className="col-span-4 space-y-6">
          <Card>
            <CardHeader><CardTitle>Top Items</CardTitle></CardHeader>
            <CardContent>
              <DataTable data={topItems} />
            </CardContent>
          </Card>
          <Card>
            <CardHeader><CardTitle>Comparison</CardTitle></CardHeader>
            <CardContent>
              <PieChart data={comparisonData} />
            </CardContent>
          </Card>
        </div>

        {/* Timeline Row - Full width */}
        <div className="col-span-12">
          <h2 className="text-2xl font-semibold text-white mb-4">Trend Analysis</h2>
          <div className="grid grid-cols-4 gap-6">
            {timelineData.map(item => (
              <DataCard key={item.period} {...item} />
            ))}
          </div>
        </div>
      </div>
    </div>
  )
}

コアコンポーネント

KPI カード
import { Card, CardContent } from "@/components/ui/card"
import { TrendingUp, TrendingDown, Minus } from "lucide-react"
import { AnimatedNumber } from "./AnimatedNumber"

interface KPICardProps {
  title: string
  value: number
  prefix?: string
  suffix?: string
  trend?: number
}

export function KPICard({ title, value, prefix = '', suffix = '', trend = 0 }: KPICardProps) {
  return (
    <Card className="bg-slate-800/50 border-slate-700/50">
      <CardContent className="pt-6">
        <p className="text-slate-400 text-sm font-medium mb-2">{title}</p>
        <div className="flex items-end justify-between">
          <span className="text-3xl font-bold text-white">
            {prefix}<AnimatedNumber value={value} />{suffix}
          </span>
          {trend !== 0 && (
            <span className={cn(
              "flex items-center text-sm font-medium",
              trend > 0 ? "text-emerald-500" : "text-rose-500"
            )}>
              {trend > 0 ? <TrendingUp className="w-4 h-4 mr-1" /> : <TrendingDown className="w-4 h-4 mr-1" />}
              {Math.abs(trend)}%
            </span>
          )}
        </div>
      </CardContent>
    </Card>
  )
}
データカード(汎用タイムラインカード)
'use client'
import { motion } from 'framer-motion'
import { useInView } from 'framer-motion'
import { useRef } from 'react'
import { Card, CardContent } from "@/components/ui/card"

interface DataCardProps {
  period: string
  currentValue: number
  baselineValue: number
  percentChange: number
  description: string
}

export function DataCard({ period, currentValue, baselineValue, percentChange, description }: DataCardProps) {
  const ref = useRef(null)
  const isInView = useInView(ref, { once: true, margin: "-50px" })
  const isPositive = percentChange >= 0

  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 30 }}
      animate={isInView ? { opacity: 1, y: 0 } : {}}
      transition={{ duration: 0.5, ease: "easeOut" }}
    >
      <Card className="bg-slate-800/50 border-slate-700/50 overflow-hidden">
        <div className={cn(
          "h-1",
          isPositive ? "bg-emerald-500" : "bg-rose-500"
        )} />
        <CardContent className="pt-6">
          <div className="flex items-baseline gap-2 mb-4">
            <span className="text-4xl font-bold text-white">{period}</span>
          </div>

          <div className="space-y-3">
            <div className="flex justify-between text-sm">
              <span className="text-slate-400">Baseline Value</span>
              <span className="text-slate-300">{baselineValue}</span>
            </div>
            <div className="flex justify-between text-sm">
              <span className="text-slate-400">Current Value</span>
              <span className="text-white font-medium">{currentValue}</span>
            </div>

            {/* Change percentage */}
            <div className={cn(
              "text-2xl font-bold text-center py-3 rounded-lg",
              isPositive ? "text-emerald-500 bg-emerald-500/10" : "text-rose-500 bg-rose-500/10"
            )}>
              {isPositive ? '+' : ''}{percentChange.toFixed(2)}%
            </div>

            {/* Progress bar */}
            <div className="h-1.5 bg-slate-700 rounded-full overflow-hidden">
              <motion.div
                initial={{ width: 0 }}
                animate={isInView ? { width: `${Math.min(Math.abs(percentChange), 100)}%` } : {}}
                transition={{ duration: 1, ease: "easeOut", delay: 0.3 }}
                className={cn(
                  "h-full rounded-full",
                  isPositive ? "bg-emerald-500" : "bg-rose-500"
                )}
              />
            </div>

            <p className="text-slate-400 text-sm">{description}</p>
          </div>
        </CardContent>
      </Card>
    </motion.div>
  )
}
アニメーション付き数値
'use client'
import { useEffect, useState } from 'react'
import { animate } from 'framer-motion'

interface AnimatedNumberProps {
  value: number
  decimals?: number
  duration?: number
}

export function AnimatedNumber({ value, decimals = 2, duration = 1 }: AnimatedNumberProps) {
  const [display, setDisplay] = useState(0)

  useEffect(() => {
    const controls = animate(display, value, {
      duration,
      ease: "easeOut",
      onUpdate: (v) => setDisplay(v)
    })
    return () => controls.stop()
  }, [value, duration])

  return <>{display.toFixed(decimals)}</>
}
ライブインジケーター
export function LiveIndicator() {
  return (
    <div className="flex items-center gap-2">
      <span className="relative flex h-3 w-3">
        <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
        <span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500" />
      </span>
      <span className="text-emerald-500 text-sm font-medium">LIVE</span>
    </div>
  )
}

自動化スクリプト

自動セットアップと起動

開発環境のセットアップを簡単にするため、次の処理を行う自動化スクリプトを用意しています。

  1. 必要な依存関係をすべて含む新しい Next.js プロジェクトを作成する
  2. shadcn/ui コンポーネントをインストールする
  3. サンプルのダッシュボードコンポーネントをセットアップする
  4. 開発サーバーを起動する

次のコマンドでセットアップスクリプトを実行します。

./setup-and-run.sh

スクリプトではプロジェクト名の入力が求められ、すべてのセットアップ手順が自動で実行された後、開発サーバーが http://localhost:3000 で起動します。

手動セットアップ

手動でセットアップする場合は、次の手順に従ってください。

  1. 新しい Next.js アプリを作成します。

    npx create-next-app@latest my-dashboard --typescript --tailwind --eslint
    
  2. 必要な依存関係をインストールします。

    cd my-dashboard
    npm install shadcn-ui recharts framer-motion lucide-react
    
  3. shadcn/ui を初期化し、コンポーネントを追加します。

    npx shadcn-ui@latest init
    npx shadcn-ui@latest add card button badge table scroll-area alert
    
  4. 開発サーバーを起動します。

    npm run dev
    

CSS スタイル

/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  --card: 222.2 84% 4.9%;
  --card-foreground: 210 40% 98%;
  --primary: 217.2 91.2% 59.8%;
  --primary-foreground: 222.2 47.4% 11.2%;
}

/* Big screen typography */
@media (min-width: 2560px) {
  html { font-size: 18px; }
}

@media (min-width: 3840px) {
  html { font-size: 20px; }
}

/* Smooth scrolling */
html {
  scroll-behavior: smooth;
}

/* Card hover effect */
.card-hover {
  @apply transition-all duration-300;
}

.card-hover:hover {
  @apply -translate-y-1 shadow-lg shadow-slate-900/50;
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

自動更新フック

'use client'
import { useEffect, useState, useCallback } from 'react'

export function useLiveData<T>(
  fetcher: () => Promise<T>,
  interval = 60000
) {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [isRefreshing, setIsRefreshing] = useState(false)
  const [lastUpdated, setLastUpdated] = useState<Date | null>(null)

  const refresh = useCallback(async () => {
    setIsRefreshing(true)
    try {
      const result = await fetcher()
      setData(result)
      setLastUpdated(new Date())
    } finally {
      setLoading(false)
      setTimeout(() => setIsRefreshing(false), 300)
    }
  }, [fetcher])

  useEffect(() => {
    refresh()
    const timer = setInterval(refresh, interval)
    return () => clearInterval(timer)
  }, [refresh, interval])

  return { data, loading, isRefreshing, lastUpdated, refresh }
}

リソース

コメント