Communitygithub.com

0necontroller/skills

デザインシステム、レイアウトアーキテクチャ、微妙なグラデーションの統計カード、折りたたみ可能なサイドバー、パンくずリスト、日付フィルター、そしてモダンダッシュボード用のTanStackデータテーブルを設計します。

skills とは?

このスキルは、Next.js(App Router)、Tailwind CSS(v3またはv4)、shadcn/uiを使用して、プロフェッショナルで応答性の高い管理ダッシュボードと分析ダッシュボードを構築するための包括的なガイドラインと実装パターンを提供します。具体的には、レイアウトアーキテクチャ、微妙なグラデーションを使用した統計カード、折りたたみ可能なアイコンサイドバーの構築、階層的なパンくずリスト、日付フィルター、TanStack Tableを活用した高度なデータテーブル(フィルタリング、ソート、ページネーション、行選択を含む)の実装に焦点を当てています。このスキルは、shadcn/uiのセットアップ確認、必要なコンポーネントのインストール(`npx shadcn@latest add`経由)、コアnpmパッケージ(`@tanstack/react-table`、`date-fns`、`lucide-react`)の検証を含む、前提条件の段階的なチェックから始まります。次に、CSS変数に基づいたデザインシステムとテーマの基礎を確立し、コンポーネントの実装に進みます。これには、サイドバープロバイダー、折りたたみ可能なナビゲーション、アクティブ状態、通知カウンターを含む完全な管理レイアウトが含まれます。すべてのコードパターンは、すぐにコピーして使用できるように設計されており、shadcn/uiのコンポーネント(Sidebar、Breadcrumb、Card、Popover、Calendar、Tableなど)とユーティリティファーストのTailwind CSSクラスを統合して、メンテナンス性が高く視覚的に一貫性のあるダッシュボードを実現します。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/0necontroller/skills/tree/HEAD/skills/dashboard-ui

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

ダッシュボードUIスキル

Next.js(App Router)、Tailwind CSS(v3またはv4)、およびshadcn/uiを使用して、ハイエンドで応答性の高い管理・分析ダッシュボードを構築するための、包括的なガイドライン、コンポーネントアーキテクチャ、およびコピー&ペースト可能な実装パターン。


1. 前提条件とshadcn/uiコンポーネントのインストール

ダッシュボードコンポーネントを生成または変更する前に、AIエージェントは以下の前提条件チェックを実行する必要があります。

ステップ1: shadcn/uiのセットアップを確認する (components.json)

プロジェクトルートに components.json が存在するか確認します。

  • components.json がない場合: コンポーネントを盲目的に作成またはインストールしようとしないでください。ユーザーに次のように促します。

    "shadcn/uiの設定(components.json)がこのプロジェクトに見つかりませんでした。最初にshadcn/uiを初期化してください(例:npx shadcn@latest init)。これにより、コンポーネントディレクトリとパスエイリアスが適切に設定されます。"

  • components.json が存在する場合: コンポーネントの確認に進みます。

ステップ2: 必要なshadcn/uiコンポーネントをインストールする

指定されたUIコンポーネントディレクトリ(例:@/components/ui/)に必要なプリミティブが存在するか確認します。不足しているコンポーネントに対して npx shadcn@latest add <component> を実行します。

# コアレイアウトとサイドバー
npx shadcn@latest add sidebar collapsible separator sheet tooltip avatar dropdown-menu

# ページヘッダーとパンくずリスト
npx shadcn@latest add breadcrumb badge button

# 統計&分析カード
npx shadcn@latest add card skeleton

# 日付フィルター
npx shadcn@latest add popover calendar

# データテーブルとフォームコントロール
npx shadcn@latest add table input select

または、ダッシュボードに必要なすべてのコンポーネントを一度にインストールします。

npx shadcn@latest add sidebar collapsible separator sheet tooltip avatar dropdown-menu breadcrumb badge button card skeleton popover calendar table input select

ステップ3: コアnpmパッケージを確認してインストールする

このスキルのすべてのデータテーブルは、shadcn/uiの <Table> と組み合わせた TanStack Table(@tanstack/react-table) で構築されています。以下のパッケージが package.json に存在することを確認してください。

  • @tanstack/react-table: テーブルの状態管理、カラム定義、ページネーション、ソート。
  • date-fns: 日付フィルターのための日付の解析、フォーマット、計算。
  • lucide-react: 標準的なアイコンセット。

不足している場合は、インストールします。

npm install @tanstack/react-table date-fns lucide-react
# または: pnpm add @tanstack/react-table date-fns lucide-react
# または: bun add @tanstack/react-table date-fns lucide-react

2. デザインシステムとテーマの基礎

すべてのダッシュボードコンポーネントは、セマンティックなCSS変数と、微妙で高いコントラストのビジュアルトークンに依存しています。

テーマトークン (globals.css)

:root {
  --background: 0 0% 100%;
  --foreground: 0 0% 3.9%;
  --card: 0 0% 100%;
  --card-foreground: 0 0% 3.9%;
  --popover: 0 0% 100%;
  --popover-foreground: 0 0% 3.9%;
  --primary: 330 43% 46%;
  --primary-foreground: 0 0% 98%;
  --muted: 0 0% 96.1%;
  --muted-foreground: 0 0% 45.1%;
  --border: 0 0% 89.8%;
  --radius: 0.5rem;

  /* サイドバートークン */
  --sidebar: hsl(0 0% 98%);
  --sidebar-foreground: hsl(240 5.3% 26.1%);
  --sidebar-primary: hsl(240 5.9% 10%);
  --sidebar-primary-foreground: hsl(0 0% 98%);
  --sidebar-accent: hsl(240 4.8% 95.9%);
  --sidebar-accent-foreground: hsl(240 5.9% 10%);
  --sidebar-border: hsl(220 13% 91%);
  --sidebar-ring: hsl(217.2 91.2% 59.8%);
}

.dark {
  --background: 0 0% 3.9%;
  --foreground: 0 0% 98%;
  --card: 0 0% 3.9%;
  --card-foreground: 0 0% 98%;
  --muted: 0 0% 14.9%;
  --muted-foreground: 0 0% 63.9%;
  --border: 0 0% 14.9%;

  /* ダークサイドバートークン */
  --sidebar: hsl(240 5.9% 10%);
  --sidebar-foreground: hsl(240 4.8% 95.9%);
  --sidebar-primary: hsl(224.3 76.3% 48%);
  --sidebar-accent: hsl(240 3.7% 15.9%);
  --sidebar-accent-foreground: hsl(240 4.8% 95.9%);
  --sidebar-border: hsl(240 3.7% 15.9%);
}

3. 管理レイアウトとサイドバーアーキテクチャ

2.1 エントリポイントレイアウト (app/admin/layout.tsx)

ルートの管理レイアウトは、ページを SidebarProvider でラップし、折りたたみ可能な AppSidebar を挿入し、モバイルトリガーでヘッダーを制御します。

import { AppSidebar } from '@/components/admin/sidebar/app-sidebar';
import {
  SidebarInset,
  SidebarProvider,
  SidebarTrigger
} from '@/components/ui/sidebar';

export default function AdminLayout({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <AppSidebar />
      <SidebarInset>
        {/* モバイルヘッダー:小画面では上部固定ブラー、デスクトップでは透明 */}
        <header className="bg-background/95 fixed top-0 right-0 left-0 z-20 flex h-16 shrink-0 items-center gap-2 border-b shadow-xs backdrop-blur transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:static md:border-none md:bg-transparent md:shadow-none">
          <div className="flex w-full items-center justify-between gap-2 px-4">
            <SidebarTrigger className="-ml-1" />
            <div className="font-semibold text-sm tracking-tight md:hidden">
              Admin Portal
            </div>
            <div className="w-8 md:hidden" />
          </div>
        </header>

        {/* メインコンテンツエリア */}
        <main className="flex flex-1 flex-col gap-4 p-4 pt-20 md:pt-0">
          {children}
        </main>
      </SidebarInset>
    </SidebarProvider>
  );
}

2.2 アプリサイドバー (components/admin/sidebar/app-sidebar.tsx)

variant="inset" と collapsible="icon" を使用して、アイコンのみのドックに折りたたみます。

'use client';

import {
  PieChart,
  ShoppingBag,
  Users,
  Settings2,
  Package
} from 'lucide-react';
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarHeader,
  SidebarRail
} from '@/components/ui/sidebar';
import { NavMain } from './nav-main';
import { NavUser } from './nav-user';
import { TeamSwitcher } from './team-switcher';

const navData = [
  {
    title: 'ダッシュボード',
    url: '/admin',
    icon: PieChart,
    items: [{ title: '概要', url: '/admin' }]
  },
  {
    title: '注文',
    url: '/admin/orders',
    icon: ShoppingBag,
    items: [
      { title: 'すべての注文', url: '/admin/orders' },
      { title: '取引', url: '/admin/orders/transactions' }
    ]
  },
  {
    title: '在庫',
    url: '/admin/inventory',
    icon: Package,
    items: [
      { title: '製品', url: '/admin/inventory/products' },
      { title: '在庫僅少', url: '/admin/inventory/low-stock' }
    ]
  },
  {
    title: 'ユーザー',
    url: '/admin/users',
    icon: Users,
    items: [{ title: '顧客', url: '/admin/users' }]
  },
  {
    title: '設定',
    url: '/admin/settings',
    icon: Settings2,
    items: [{ title: '一般', url: '/admin/settings' }]
  }
];

export function AppSidebar(props: React.ComponentProps<typeof Sidebar>) {
  return (
    <Sidebar variant="inset" collapsible="icon" {...props}>
      <SidebarHeader>
        <TeamSwitcher />
      </SidebarHeader>
      <SidebarContent>
        <NavMain items={navData} />
      </SidebarContent>
      <SidebarFooter>
        <NavUser user={{ name: '管理者', email: 'admin@company.com' }} />
      </SidebarFooter>
      <SidebarRail />
    </Sidebar>
  );
}

2.3 アクティブ状態とカウンターピル付き折りたたみ可能ナビ (nav-main.tsx)

アクティブなリンクを強調表示し、シェブロンを滑らかに回転させ、tabular-nums を使用して通知カウンターを表示します。

'use client';

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { ChevronRight, type LucideIcon } from 'lucide-react';
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger
} from '@/components/ui/collapsible';
import {
  SidebarGroup,
  SidebarGroupLabel,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem
} from '@/components/ui/sidebar';

interface NavItem {
  title: string;
  url: string;
  icon?: LucideIcon;
  count?: number;
  items?: { title: string; url: string; count?: number }[];
}

export function NavMain({ items }: { items: NavItem[] }) {
  const pathname = usePathname();

  const isPathActive = (target: string) => {
    if (!target) return false;
    if (pathname === target) return true;
    if (target === '/admin') return false;
    return pathname.startsWith(`${target}/`);
  };

  return (
    <SidebarGroup>
      <SidebarGroupLabel>プラットフォーム</SidebarGroupLabel>
      <SidebarMenu>
        {items.map((item) => {
          const subItemActive = item.items?.some((s) => isPathActive(s.url));
          const isActive = subItemActive || isPathActive(item.url);

          return (
            <Collapsible
              key={item.title}
              asChild
              defaultOpen={isActive}
              className="group/collapsible"
            >
              <SidebarMenuItem>
                <CollapsibleTrigger asChild>
                  <SidebarMenuButton tooltip={item.title} isActive={isActive}>
                    {item.icon && <item.icon />}
                    <span>{item.title}</span>
                    {item.count && item.count > 0 && (
                      <span className="bg-primary/10 text-primary mr-1 ml-auto flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-[11px] font-semibold tabular-nums">
                        {item.count}
                      </span>
                    )}
                    <ChevronRight
                      className={`transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 ${
                        item.count ? '' : 'ml-auto'
                      }`}
                    />
                  </SidebarMenuButton>
                </CollapsibleTrigger>
                <CollapsibleContent>
                  <SidebarMenuSub>
                    {item.items?.map((subItem) => {
                      const subIsActive = isPathActive(subItem.url);
                      return (
                        <SidebarMenuSubItem key={subItem.title}>
                          <SidebarMenuSubButton asChild isActive={subIsActive}>
                            <Link
                              href={subItem.url}
                              className="flex w-full items-center justify-between"
                            >
                              <span>{subItem.title}</span>
                              {subItem.count && subItem.count > 0 && (
                                <span className="bg-primary ml-auto flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-medium text-white tabular-nums">
                                  {subItem.count}
                                </span>
                              )}
                            </Link>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                      );
                    })}
                  </SidebarMenuSub>
                </CollapsibleContent>
              </SidebarMenuItem>
            </Collapsible>
          );
        })}
      </SidebarMenu>
    </SidebarGroup>
  );
}

4. ページヘッダー、パンくずリスト、タイトル階層

すべてのページは、階層的なパンくずリスト、アクセス可能な <h1> タイトル、サブタイトル、およびアクション/フィルターツールバーを組み合わせた、クリーンなヘッダー行で始まります。

import Link from 'next/link';
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator
} from '@/components/ui/breadcrumb';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';

interface PageHeaderProps {
  breadcrumbs?: { title: string; href?: string }[];
  title: string;
  subtitle?: string;
  statusBadge?: React.ReactNode;
  children?: React.ReactNode; // アクションボタンまたはフィルター
}

export function PageHeader({
  breadcrumbs,
  title,
  subtitle,
  statusBadge,
  children
}: PageHeaderProps) {
  return (
    <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
      <div>
        {breadcrumbs && breadcrumbs.length > 0 && (
          <Breadcrumb className="mb-2">
            <BreadcrumbList>
              {breadcrumbs.map((crumb, idx) => {
                const isLast = idx === breadcrumbs.length - 1;
                return (
                  <span key={crumb.title} className="inline-flex items-center gap-1.5">
                    <BreadcrumbItem>
                      {isLast || !crumb.href ? (
                        <BreadcrumbPage>{crumb.title}</BreadcrumbPage>
                      ) : (
                        <BreadcrumbLink asChild>
                          <Link href={crumb.href}>{crumb.title}</Link>
                        </BreadcrumbLink>
                      )}
                    </BreadcrumbItem>
                    {!isLast && <BreadcrumbSeparator />}
                  </span>
                );
              })}
            </BreadcrumbList>
          </Breadcrumb>
        )}

        <div className="flex items-center gap-3">
          <h1 className="text-2xl font-semibold tracking-tight md:text-3xl md:font-bold">
            {title}
          </h1>
          {statusBadge}
        </div>

        {subtitle && (
          <p className="text-muted-foreground text-sm mt-1">
            {subtitle}
          </p>
        )}
      </div>

      {children && (
        <div className="flex flex-wrap items-center gap-2">
          {children}
        </div>
      )}
    </div>
  );
}

5. 微妙なグラデーションの統計カード

4.1 グラデーションとコンテナクエリパターン

視覚的な乱雑さを抑えながらハイエンドな外観を実現するには、以下のようにします。

  1. ライトモードでは、親のワイルドカードセレクタ *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:bg-gradient-to-t を適用します。
  2. ダークモードでは、クリーンでマットなカードのために dark:*:data-[slot=card]:bg-card にフォールバックします。
  3. カードに @container/card を追加して、タイトルのタイポグラフィを動的にスケーリングします(@[250px]/card:text-3xl)。
  4. トレンドインジケーターは abs...

…[翻訳のため一部省略]

関連スキル