ダッシュボード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 グラデーションとコンテナクエリパターン
視覚的な乱雑さを抑えながらハイエンドな外観を実現するには、以下のようにします。
- ライトモードでは、親のワイルドカードセレクタ
*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:bg-gradient-to-tを適用します。 - ダークモードでは、クリーンでマットなカードのために
dark:*:data-[slot=card]:bg-cardにフォールバックします。 - カードに
@container/cardを追加して、タイトルのタイポグラフィを動的にスケーリングします(@[250px]/card:text-3xl)。 - トレンドインジケーターは abs...
…[翻訳のため一部省略]