UX Standards — 06: Stats Summary Cards
Governs: Stat/summary cards that appear above the data table on list pages.
Parent rules: See 00-OVERVIEW-AND-CSS-RULES.md first.
Visual Anatomy
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Total Faxes │ │ Pending Review │ │ Completed Today │ │ Failed │
│ 1,247 [Icon] │ │ 84 [Icon] │ │ 203 [Icon] │ │ 12 [Icon] │
│ All time │ │ Needs attention │ │ Since midnight │ │ Requires action │
└──────────────────┘ └──────────────────┘ └──────────────────┘ └──────────────────┘
StatCard Component
// From src/components/doctype-list-page.tsx
interface StatCardConfig {
title: string;
value: string | number;
description?: string;
icon?: React.ComponentType<{ className?: string }>;
variant?: 'default' | 'green' | 'blue' | 'yellow' | 'red';
onClick?: () => void;
}
function StatCard({ title, value, description, icon: Icon, variant = 'default', onClick }: StatCardConfig) {
const variantClasses = {
default: 'text-foreground',
green: 'text-primary',
blue: 'text-blue-600 dark:text-blue-400',
yellow: 'text-yellow-600 dark:text-yellow-400',
red: 'text-destructive',
};
return (
<div
className={`rounded-lg border bg-card p-4 ${onClick ? 'cursor-pointer hover:bg-accent/50 transition-colors' : ''}`}
onClick={onClick}
>
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-muted-foreground">{title}</p>
{Icon && <Icon className={`h-5 w-5 ${variantClasses[variant]}`} />}
</div>
<div className="mt-2">
<p className={`text-2xl font-bold ${variantClasses[variant]}`}>{value}</p>
{description && (
<p className="mt-1 text-xs text-muted-foreground">{description}</p>
)}
</div>
</div>
);
}
Card Anatomy
┌─────────────────────────────────────────┐
│ rounded-lg border bg-card p-4 │
│ │
│ Title (text-sm font-medium muted) Icon │
│ │
│ 1,247 ← text-2xl font-bold │
│ All time ← text-xs muted │
└─────────────────────────────────────────┘
| Element | Spec |
|---|---|
| Container | rounded-lg border bg-card p-4 |
| Title | text-sm font-medium text-muted-foreground |
| Icon | h-5 w-5 — color from variant |
| Value | text-2xl font-bold — color from variant |
| Description | text-xs text-muted-foreground mt-1 |
| Hover (clickable) | cursor-pointer hover:bg-accent/50 transition-colors |
Color Variants
| Variant | Icon & Value Color | Use For |
|---|---|---|
default | text-foreground | Neutral counts (total, all) |
green | text-primary | Success, completed, active |
blue | text-blue-600 dark:text-blue-400 | In-progress, informational |
yellow | text-yellow-600 dark:text-yellow-400 | Warning, pending, attention needed |
red | text-destructive | Errors, failures, critical counts |
Note: blue and yellow still use Tailwind color classes here because no CSS var exists for these colors. Use dark: variants to support dark mode.
Grid Layout Rules
The grid auto-sizes based on how many cards are defined:
function getGridCols(count: number): string {
if (count <= 2) return 'grid-cols-1 md:grid-cols-2';
if (count === 3) return 'grid-cols-1 md:grid-cols-3';
if (count === 4) return 'grid-cols-2 md:grid-cols-4';
if (count === 5) return 'grid-cols-2 md:grid-cols-5';
return 'grid-cols-2 md:grid-cols-6'; // 6+
}
// Usage in DocTypeListPage:
{stats && (
<div className={`grid gap-4 ${getGridCols(stats.length)}`}>
{stats.map((stat) => <StatCard key={stat.title} {...stat} />)}
</div>
)}
| Card Count | Mobile | Desktop |
|---|---|---|
| 1–2 | 1 column | 2 columns |
| 3 | 1 column | 3 columns |
| 4 | 2 columns | 4 columns |
| 5 | 2 columns | 5 columns |
| 6+ | 2 columns | 6 columns |
Gap between cards: gap-4 (16px).
Positioning
Stats cards always appear as the first element in the content stack, before tabs and before the toolbar:
<div className="flex h-full flex-col space-y-4">
{/* 1. Stats cards — FIRST */}
{stats && (
<div className={`grid gap-4 ${getGridCols(stats.length)}`}>
{stats.map(stat => <StatCard key={stat.title} {...stat} />)}
</div>
)}
{/* 2. Tab navigation — SECOND (optional) */}
{preToolbarContent}
{/* 3. Toolbar — THIRD */}
<DataTableToolbar ... />
{/* 4. Table — FOURTH */}
<div className="flex min-h-0 flex-1 flex-col">
<DataTableSortable ... />
</div>
</div>
DocTypeListPage Integration
Pass stats as an array to DocTypeListPage:
<DocTypeListPage
table={table}
stats={[
{
title: 'Total Faxes',
value: data.totalCount.toLocaleString(),
description: 'All time',
icon: FileText,
variant: 'default',
},
{
title: 'Pending Review',
value: data.pendingCount,
description: 'Needs attention',
icon: Clock,
variant: 'yellow',
onClick: () => applyFilter('status', 'pending'), // optional
},
{
title: 'Completed Today',
value: data.completedToday,
description: 'Since midnight',
icon: CheckCircle,
variant: 'green',
},
{
title: 'Failed',
value: data.failedCount,
description: 'Requires action',
icon: AlertCircle,
variant: 'red',
},
]}
...
/>
Loading State
While data loads, render skeleton cards matching the expected count:
{isLoading ? (
<div className={`grid gap-4 ${getGridCols(4)}`}>
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="rounded-lg border bg-card p-4 space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-16" />
<Skeleton className="h-3 w-20" />
</div>
))}
</div>
) : (
<div className={`grid gap-4 ${getGridCols(stats.length)}`}>
{stats.map(stat => <StatCard key={stat.title} {...stat} />)}
</div>
)}
Clickable Stats (Filter Shortcut)
When a stat card is clickable, it applies a filter to the table below:
{
title: 'Pending Review',
value: 84,
icon: Clock,
variant: 'yellow',
onClick: () => {
// Apply a column filter on the table
table.getColumn('status')?.setFilterValue('pending');
// Or set a URL search param for server-side filtering
router.push('?status=pending');
}
}
Clickable cards receive cursor-pointer hover:bg-accent/50 transition-colors. Non-clickable cards have no pointer or hover state.
Violation Checklist
- Card container uses
rounded-lg border bg-card p-4 - Title uses
text-sm font-medium text-muted-foreground - Icon uses
h-5 w-5(not h-4 or h-6) - Value uses
text-2xl font-bold - Description uses
text-xs text-muted-foreground mt-1 - Grid uses
gap-4with correctgrid-cols-*count - Stats appear BEFORE tabs and BEFORE toolbar
- Color variants use
text-primary,text-destructive, or specified dark/light paired classes - Clickable cards have
cursor-pointer hover:bg-accent/50 transition-colors - Loading state shows skeleton cards of same count