---
name: admin-panel
description: >
  Build production-grade admin panel dashboards in React that match the design quality of Supabase,
  Vercel, Linear, and Planetscale. Use this skill whenever the user asks for an admin panel,
  dashboard, control plane, management UI, back-office tool, or any internal product that needs
  to feel like a world-class SaaS. Trigger on phrases like "admin dashboard", "make it look like
  Vercel", "internal tool", "management interface", "clean dashboard", or "something like Supabase".
  This skill enforces a precise design system, component grammar, and the micro-decisions that
  separate amateur dashboards from professional product UI. Stack: Tailwind v4, shadcn/ui,
  HugeIcons, recharts.
---

# Admin Panel Skill

Produces React admin panels that feel like they were built by a product-design team, not assembled
from a template library. Quality reference bar: Supabase Studio, Vercel Dashboard, Linear, Railway.

---

## 0. Mandatory Pre-flight

Before writing a single line of JSX, run through this checklist mentally:

1. **Read the shadcn-ui SKILL** — theming, component APIs, and `cn()` utility conventions apply here.
2. **Confirm icon library**: This skill defaults to `hugeicons-react`. If the project's
   `components.json` specifies a different `iconLibrary`, use that instead.
3. **Confirm Tailwind version**: This skill targets Tailwind v4 (`@theme inline`, CSS variables,
   no `tailwind.config.js`). Adjust if the project is on v3.
4. **Choose dark or light mode** before touching color tokens. Default: dark mode.
5. **Pick one accent color** — never two. Never swap it mid-component.

---

## 1. Design Philosophy — Read This Before Touching JSX

The distance between a generic dashboard and a Vercel-quality one is not feature count.
It is **density, restraint, and intentionality**.

### The Core Aesthetic DNA

**Near-monochromatic.** Dark mode: near-black backgrounds (`oklch(8% 0 0)`), not navy or charcoal
soup. Light mode: off-whites (`oklch(98% 0 0)`), never pure `#fff`. One accent color — used for
interactive affordance only, never decoration.

**Borders beat shadows.** Separation is achieved with `1px solid border` at ~8% opacity.
Box-shadows, if used at all, are one pixel, one color stop, near-zero blur:
`0 1px 2px color-mix(in oklch, black 6%, transparent)`.

**Typography carries the information.** Body and data text: 13–14px. Muted labels: 11–12px.
Headings: 16–18px max on interior pages — not 28px hero headings. Numbers in tables must use
`tabular-nums` so columns stay aligned.

**Density is a product feature.** Padding is tight. Row height: 40px. Card padding: 16–20px.
Line-height: 1.4–1.5. This is not a consumer app — whitespace is not "breathing room", it is
wasted screen real estate.

**Status as signal, not decoration.** Badges are small, low-contrast pills. Active/healthy =
muted green chip. Error = muted red. Warning = muted amber. Never neon. The chip border is the
thing that makes it look premium.

**No decorative chrome.** No gradient hero banners on data pages. No large colorful icons. No
illustrations in the main content area. Every element must justify its presence with function.

### The Amateur vs. Professional Table

| Amateur                             | Professional (Vercel / Supabase)                     |
| ----------------------------------- | ---------------------------------------------------- |
| KPI cards with gradient backgrounds | KPI cards: flat bg, thin border, precise type        |
| Colorful sidebar icon set           | Monochrome icons; active = subtle bg fill only       |
| `box-shadow` on every card          | `border` only for card edges                         |
| Mixed font pairing, 3 weights       | One typeface, 3 weights max (400 / 500 / 600)        |
| Bright green "Active" badge         | `bg-green-950/60 text-green-400 border-green-800/40` |
| 5-color chart palette               | 1–2 hues, 2–3 shades each                            |
| Sidebar 280px wide                  | Sidebar 220px, icon-only collapse at 56px            |
| Table row padding 24px+             | Table row height 40px, px-4                          |
| Giant "Dashboard" H1                | Breadcrumb or 15px page context label                |
| Spinner overlay on loading table    | Skeleton rows, same height as real rows              |
| Empty state: just text              | Empty state: icon + title + description + CTA button |

---

## 2. Stack & Dependencies

```
tailwindcss@4          — Utility classes; @theme inline CSS variables
shadcn/ui              — Primitives: Table, Dialog, DropdownMenu, Tabs, Badge, Tooltip, Sheet
hugeicons-react        — Icon set (strokeWidth 1.5 default, size 16 default)
recharts               — Charts: AreaChart, LineChart, BarChart only (no Pie)
```

### HugeIcons Import Pattern

```tsx
import {
  Home01Icon,
  Settings01Icon,
  UserCircleIcon,
  Notification01Icon,
  Search01Icon,
  MoreHorizontalIcon,
  ArrowUp01Icon,
  ArrowDown01Icon,
  // ... etc
} from "hugeicons-react";

// Render — always size={16} strokeWidth={1.5} unless layout explicitly calls for larger
<Home01Icon size={16} strokeWidth={1.5} />;
```

**Icon sizing rules:**

- Sidebar nav: `size={16}` always
- Topbar actions: `size={16}`
- KPI card icon (top-right): `size={16}`
- Section headings / empty states: `size={20}` max
- Never use `size={24}` in a dense admin panel

### Font

Always load Geist. It is the closest publicly available match to Vercel's typeface.

```css
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&display=swap");

:root {
  font-family:
    "Geist",
    system-ui,
    -apple-system,
    sans-serif;
}
code,
pre,
kbd,
.font-mono {
  font-family: "Geist Mono", ui-monospace, monospace;
}
```

Banned fonts: `DM Sans`, `Poppins`, `Nunito`, `Raleway`, `Outfit` — consumer app fonts.
Acceptable fallback: `Inter` only if Geist is not loadable.

---

## 3. CSS Token System (Tailwind v4 / shadcn/ui)

Admin panels always use shadcn's CSS variable theming system. Override at the root level.
This is the canonical dark-mode token set for a professional admin panel:

```css
/* globals.css — dark mode override */
.dark {
  /* Backgrounds — layered from deepest to surface */
  --background: oklch(7% 0 0); /* page bg         #0a0a0a */
  --surface: oklch(10% 0 0); /* card / panel    #111 */
  --surface-2: oklch(13% 0 0); /* inset, hover    #161616 */

  /* Borders */
  --border: oklch(100% 0 0 / 8%); /* standard border */
  --border-hover: oklch(100% 0 0 / 14%); /* hover border */
  --border-focus: oklch(100% 0 0 / 22%); /* focus ring base */

  /* Text */
  --foreground: oklch(98% 0 0); /* primary */
  --muted-foreground: oklch(55% 0 0); /* labels, captions */
  --subtle-foreground: oklch(35% 0 0); /* tertiary, icons */

  /* Accent — pick ONE, never both */
  /* Supabase Green  */
  --accent: oklch(75% 0.18 160);
  /* Vercel Blue     */ /* --accent: oklch(60% 0.2 250); */

  --accent-muted: oklch(75% 0.18 160 / 12%); /* accent chip bg */
  --accent-border: oklch(75% 0.18 160 / 25%); /* accent chip border */

  /* Status chips */
  --status-active-bg: oklch(45% 0.14 145 / 15%);
  --status-active-text: oklch(75% 0.15 145);
  --status-active-border: oklch(55% 0.14 145 / 30%);

  --status-error-bg: oklch(45% 0.18 25 / 15%);
  --status-error-text: oklch(72% 0.18 25);
  --status-error-border: oklch(55% 0.18 25 / 30%);

  --status-warn-bg: oklch(60% 0.16 85 / 15%);
  --status-warn-text: oklch(82% 0.16 85);
  --status-warn-border: oklch(65% 0.16 85 / 30%);

  --status-idle-bg: oklch(40% 0 0 / 15%);
  --status-idle-text: oklch(62% 0 0);
  --status-idle-border: oklch(45% 0 0 / 30%);
}
```

**Token usage in components (always use semantic tokens, never raw hex):**

```tsx
// ✅ Correct
<div className="bg-background border-border text-foreground" />
<span className="text-muted-foreground text-[13px]" />

// ❌ Wrong
<div style={{ background: '#0a0a0a', color: '#fafafa' }} />
<span className="text-zinc-500" />
```

---

## 4. Layout Architecture

### Shell Structure (always this, never a different layout)

```
┌────────────────────────────────────────────────────────┐
│  Sidebar (220px)    │  Main content area               │
│                     │                                  │
│  [Logo]             │  [Topbar 56px]                   │
│  ─────────────────  │  ─────────────────────────────── │
│  Nav section label  │  [Page content — p-6]            │
│  Nav items          │   KPI row                        │
│                     │   Chart / Table                  │
│  ─────────────────  │   Secondary widget               │
│  [User / Settings]  │                                  │
└────────────────────────────────────────────────────────┘
```

### Shell Component Skeleton

```tsx
export function AdminShell({ children }: { children: React.ReactNode }) {
  return (
    <div className="bg-background text-foreground flex h-screen overflow-hidden">
      <Sidebar />
      <div className="flex flex-1 flex-col overflow-hidden">
        <Topbar />
        <main className="flex-1 overflow-y-auto p-6">{children}</main>
      </div>
    </div>
  );
}
```

---

## 5. Sidebar — Exact Specification

```tsx
// Rules:
// - Width: 220px expanded, 56px icon-only
// - Background: bg-background (same as page — border-r separates it)
// - Border-right: border-r border-border
// - Nav item height: h-8 (32px)
// - Nav item font: text-[13px] font-medium
// - Active state: bg-surface text-foreground — NO left-border stripe
// - Inactive state: text-muted-foreground hover:bg-surface/60 hover:text-foreground
// - Section label: text-[10px] tracking-widest uppercase text-subtle-foreground px-3 mb-1 mt-4

const NAV_SECTIONS = [
  {
    label: "OVERVIEW",
    items: [
      { icon: Home01Icon, href: "/dashboard", label: "Dashboard" },
      { icon: BarChart01Icon, href: "/analytics", label: "Analytics" },
    ],
  },
  {
    label: "MANAGEMENT",
    items: [
      { icon: UserGroupIcon, href: "/users", label: "Users", badge: "48" },
      { icon: Package01Icon, href: "/projects", label: "Projects" },
      { icon: Invoice01Icon, href: "/billing", label: "Billing" },
    ],
  },
  {
    label: "SYSTEM",
    items: [
      { icon: Settings01Icon, href: "/settings", label: "Settings" },
      { icon: LockIcon, href: "/security", label: "Security" },
    ],
  },
];

function NavItem({ icon: Icon, label, active, badge }: NavItemProps) {
  return (
    <button
      className={cn(
        "flex h-8 w-full items-center gap-2 rounded-md px-2 text-[13px] transition-colors duration-100",
        active
          ? "bg-surface text-foreground font-medium"
          : "text-muted-foreground hover:bg-surface/60 hover:text-foreground font-normal",
      )}
    >
      <Icon size={16} strokeWidth={1.5} className="shrink-0" />
      <span className="flex-1 truncate text-left">{label}</span>
      {badge && (
        <span className="bg-border rounded-full px-1.5 py-px text-[10px] font-medium leading-none">
          {badge}
        </span>
      )}
    </button>
  );
}
```

---

## 6. Topbar — Exact Specification

```tsx
// Rules:
// - Height: h-14 (56px) — never taller, never shorter
// - Border-bottom: border-b border-border
// - Background: bg-background (matches page)
// - No box-shadow
// - Left: breadcrumb or page title in text-[13px] font-medium
// - Right: search input → notification bell → avatar (28px circle)
// - Search: ghost input, placeholder "Search...", text-[13px]
// - Avatar: 28px, initials fallback, ring-1 ring-border

function Topbar({ breadcrumb }: { breadcrumb: string[] }) {
  return (
    <header className="border-border flex h-14 shrink-0 items-center gap-4 border-b px-6">
      {/* Breadcrumb */}
      <div className="flex items-center gap-1.5 text-[13px]">
        {breadcrumb.map((crumb, i) => (
          <span key={i} className="flex items-center gap-1.5">
            {i > 0 && <span className="text-subtle-foreground">/</span>}
            <span
              className={
                i < breadcrumb.length - 1
                  ? "text-muted-foreground"
                  : "text-foreground font-medium"
              }
            >
              {crumb}
            </span>
          </span>
        ))}
      </div>

      <div className="flex-1" />

      {/* Search */}
      <div className="relative">
        <Search01Icon
          size={14}
          strokeWidth={1.5}
          className="text-subtle-foreground absolute left-2.5 top-1/2 -translate-y-1/2"
        />
        <input
          placeholder="Search..."
          className="bg-surface border-border placeholder:text-subtle-foreground focus:border-border-focus h-8 w-48 rounded-md border pl-8 pr-3 text-[13px] transition-colors focus:outline-none focus:ring-0"
        />
      </div>

      {/* Notification Bell */}
      <button className="text-muted-foreground hover:bg-surface hover:text-foreground relative flex h-8 w-8 items-center justify-center rounded-md transition-colors">
        <Notification01Icon size={16} strokeWidth={1.5} />
        {/* Dot indicator — only when there are notifications */}
        <span className="bg-accent absolute right-1.5 top-1.5 h-1.5 w-1.5 rounded-full" />
      </button>

      {/* Avatar */}
      <button className="bg-surface border-border text-muted-foreground hover:border-border-hover flex h-7 w-7 items-center justify-center rounded-full border text-[11px] font-medium transition-colors">
        KL
      </button>
    </header>
  );
}
```

---

## 7. KPI / Metric Cards

```tsx
// Grid: 4 columns on desktop, 2 on tablet
// Card: bg-surface border border-border rounded-lg p-5
// Label: text-[12px] text-muted-foreground font-medium
// Value: text-[26px] font-semibold tabular-nums tracking-tight
// Delta: text-[12px] — green for positive, red for negative, muted for neutral
// Icon: top-right, size={16}, text-subtle-foreground — no colored icon bg
// Delta symbol: use Unicode ↑ ↓ (not arrow icons)

interface KPICardProps {
  label: string
  value: string
  delta: string         // e.g. "+12.4%"
  deltaType: 'up' | 'down' | 'neutral'
  icon: React.ComponentType<{ size?: number; strokeWidth?: number; className?: string }>
}

function KPICard({ label, value, delta, deltaType, icon: Icon }: KPICardProps) {
  return (
    <div className="bg-surface border border-border rounded-lg p-5">
      <div className="flex items-start justify-between mb-3">
        <span className="text-[12px] font-medium text-muted-foreground">{label}</span>
        <Icon size={16} strokeWidth={1.5} className="text-subtle-foreground" />
      </div>
      <div className="text-[26px] font-semibold tracking-tight tabular-nums text-foreground leading-none mb-2">
        {value}
      </div>
      <div className={cn(
        'text-[12px] font-medium',
        deltaType === 'up'      && 'text-[--status-active-text]',
        deltaType === 'down'    && 'text-[--status-error-text]',
        deltaType === 'neutral' && 'text-muted-foreground',
      )}>
        {deltaType === 'up' ? '↑' : deltaType === 'down' ? '↓' : '→'} {delta}
        <span className="text-subtle-foreground font-normal ml-1">vs last month</span>
      </div>
    </div>
  )
}

// Usage
const KPI_GRID = [
  { label: 'Total Revenue',   value: '$102,430', delta: '12.4%', deltaType: 'up',      icon: DollarCircleIcon },
  { label: 'Active Users',    value: '8,241',    delta: '3.1%',  deltaType: 'up',      icon: UserCircleIcon },
  { label: 'Deployments',     value: '1,034',    delta: '0.8%',  deltaType: 'neutral', icon: CloudUploadIcon },
  { label: 'Error Rate',      value: '0.42%',    delta: '0.12%', deltaType: 'down',    icon: AlertCircleIcon },
]

<div className="grid grid-cols-4 gap-4 mb-6">
  {KPI_GRID.map(k => <KPICard key={k.label} {...k} />)}
</div>
```

---

## 8. Data Tables

Tables are where quality is most visible. Follow this grammar exactly.

```tsx
// Column header: text-[11px] uppercase tracking-wide text-muted-foreground font-medium
// Row height: h-10 (40px)
// Row padding: px-4
// Hover: bg-surface-2/50 — not zebra striping
// Checkbox column: w-10
// Actions column: w-10, MoreHorizontalIcon, visible on row hover only
// Pagination: "Showing 1–10 of 48" in text-[13px] text-muted-foreground, Prev/Next buttons
// Loading: skeleton rows with animate-pulse, same height as real rows

function DataTable<T>({ columns, data, isLoading }: DataTableProps<T>) {
  return (
    <div className="bg-surface border-border overflow-hidden rounded-lg border">
      <table className="w-full text-[13px]">
        <thead>
          <tr className="border-border border-b">
            <th className="w-10 px-4 py-2.5">
              <input type="checkbox" className="border-border rounded" />
            </th>
            {columns.map((col) => (
              <th
                key={col.key}
                className="text-muted-foreground px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wide"
              >
                {col.label}
              </th>
            ))}
            <th className="w-10 px-4 py-2.5" /> {/* Actions */}
          </tr>
        </thead>
        <tbody>
          {isLoading
            ? Array.from({ length: 8 }).map((_, i) => (
                <tr key={i} className="border-border border-b last:border-0">
                  <td colSpan={columns.length + 2} className="h-10 px-4">
                    <div className="bg-surface-2 h-3 w-full animate-pulse rounded" />
                  </td>
                </tr>
              ))
            : data.map((row, i) => (
                <tr
                  key={i}
                  className="border-border hover:bg-surface-2/50 group border-b transition-colors last:border-0"
                >
                  <td className="h-10 w-10 px-4">
                    <input type="checkbox" className="border-border rounded" />
                  </td>
                  {columns.map((col) => (
                    <td key={col.key} className="text-foreground h-10 px-4">
                      {col.render ? col.render(row) : String(row[col.key])}
                    </td>
                  ))}
                  <td className="h-10 w-10 px-4">
                    <button className="text-muted-foreground hover:bg-border flex h-6 w-6 items-center justify-center rounded opacity-0 transition-opacity group-hover:opacity-100">
                      <MoreHorizontalIcon size={14} strokeWidth={1.5} />
                    </button>
                  </td>
                </tr>
              ))}
        </tbody>
      </table>

      {/* Pagination */}
      <div className="border-border flex items-center justify-between border-t px-4 py-3">
        <span className="text-muted-foreground text-[13px]">
          Showing <span className="text-foreground">1–10</span> of{" "}
          <span className="text-foreground">48</span>
        </span>
        <div className="flex items-center gap-2">
          <button className="border-border text-muted-foreground hover:bg-surface-2 hover:text-foreground h-7 rounded border px-3 text-[12px] font-medium transition-colors disabled:opacity-40">
            Previous
          </button>
          <button className="border-border text-muted-foreground hover:bg-surface-2 hover:text-foreground h-7 rounded border px-3 text-[12px] font-medium transition-colors">
            Next
          </button>
        </div>
      </div>
    </div>
  );
}
```

---

## 9. Status Badges

The badge border is the critical detail that makes this look premium. Never omit it.

```tsx
const STATUS_MAP = {
  active: {
    label: "Active",
    bg: "bg-[--status-active-bg]",
    text: "text-[--status-active-text]",
    border: "border-[--status-active-border]",
  },
  error: {
    label: "Error",
    bg: "bg-[--status-error-bg]",
    text: "text-[--status-error-text]",
    border: "border-[--status-error-border]",
  },
  warning: {
    label: "Warning",
    bg: "bg-[--status-warn-bg]",
    text: "text-[--status-warn-text]",
    border: "border-[--status-warn-border]",
  },
  inactive: {
    label: "Inactive",
    bg: "bg-[--status-idle-bg]",
    text: "text-[--status-idle-text]",
    border: "border-[--status-idle-border]",
  },
} as const;

type Status = keyof typeof STATUS_MAP;

function StatusBadge({ status }: { status: Status }) {
  const s = STATUS_MAP[status];
  return (
    <span
      className={cn(
        "inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium",
        s.bg,
        s.text,
        s.border,
      )}
    >
      {s.label}
    </span>
  );
}
```

---

## 10. Charts (Recharts)

```tsx
// Color rules:
// - Dark mode: single accent hue, 2 shades max
// - Primary line/bar: var(--accent) or hsl from accent token
// - Secondary: accent at 40% opacity
// - Grid lines: strokeDasharray="3 3", horizontal only, at ~6% opacity
// - Axis: no axis lines, no tick lines, text-[11px], text-muted-foreground
// - Tooltip: custom dark component (see below)
// - Area gradient: 15% top → 0% bottom (not 40% — too heavy)
// - Always wrap in ResponsiveContainer

// Realistic mock data — never flat arrays
const generateRealisticData = (days: number) =>
  Array.from({ length: days }, (_, i) => ({
    date: new Date(Date.now() - (days - i) * 86400000).toLocaleDateString(
      "en",
      { month: "short", day: "numeric" },
    ),
    value: Math.round(
      3000 + Math.sin(i / 3) * 800 + Math.random() * 400 + i * 22,
    ),
    secondary: Math.round(
      1200 + Math.sin(i / 4 + 1) * 300 + Math.random() * 150 + i * 8,
    ),
  }));

const chartData = generateRealisticData(30);

// Custom tooltip
function ChartTooltip({ active, payload, label }: any) {
  if (!active || !payload?.length) return null;
  return (
    <div className="bg-surface border-border rounded-md border px-3 py-2 text-[12px] shadow-sm">
      <p className="text-muted-foreground mb-1">{label}</p>
      {payload.map((p: any) => (
        <p key={p.dataKey} className="text-foreground font-medium tabular-nums">
          {p.name}: {p.value.toLocaleString()}
        </p>
      ))}
    </div>
  );
}

// Area chart (typical revenue / traffic)
<ResponsiveContainer width="100%" height={200}>
  <AreaChart data={chartData} margin={{ top: 4, right: 0, bottom: 0, left: 0 }}>
    <defs>
      <linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stopColor="var(--accent)" stopOpacity={0.15} />
        <stop offset="100%" stopColor="var(--accent)" stopOpacity={0} />
      </linearGradient>
    </defs>
    <CartesianGrid
      strokeDasharray="3 3"
      vertical={false}
      stroke="oklch(100% 0 0 / 6%)"
    />
    <XAxis
      dataKey="date"
      axisLine={false}
      tickLine={false}
      tick={{ fontSize: 11, fill: "oklch(55% 0 0)" }}
      interval="preserveStartEnd"
    />
    <YAxis
      axisLine={false}
      tickLine={false}
      tick={{ fontSize: 11, fill: "oklch(55% 0 0)" }}
      tickFormatter={(v) => `$${(v / 1000).toFixed(0)}k`}
      width={36}
    />
    <Tooltip
      content={<ChartTooltip />}
      cursor={{ stroke: "oklch(100% 0 0 / 8%)", strokeWidth: 1 }}
    />
    <Area
      type="monotone"
      dataKey="value"
      stroke="var(--accent)"
      strokeWidth={1.5}
      fill="url(#areaGrad)"
      dot={false}
    />
  </AreaChart>
</ResponsiveContainer>;
```

---

## 11. Activity Feed / Event Log

A common secondary widget. Use it when there's no second chart needed.

```tsx
const ACTIVITY_ITEMS = [
  {
    icon: UserCircleIcon,
    actor: "Sarah K.",
    action: "joined the workspace",
    time: "2m ago",
    accent: false,
  },
  {
    icon: CloudUploadIcon,
    actor: "main branch",
    action: "deployed to production",
    time: "18m ago",
    accent: true,
  },
  {
    icon: AlertCircleIcon,
    actor: "Edge function",
    action: "threw an error in /api",
    time: "1h ago",
    accent: false,
  },
];

function ActivityFeed() {
  return (
    <div className="bg-surface border-border overflow-hidden rounded-lg border">
      <div className="border-border flex items-center justify-between border-b px-5 py-4">
        <span className="text-foreground text-[13px] font-medium">
          Recent Activity
        </span>
        <button className="text-muted-foreground hover:text-foreground text-[12px] transition-colors">
          View all
        </button>
      </div>
      <div className="divide-border divide-y">
        {ACTIVITY_ITEMS.map((item, i) => (
          <div key={i} className="flex items-start gap-3 px-5 py-3.5">
            <div
              className={cn(
                "flex h-7 w-7 shrink-0 items-center justify-center rounded-full",
                item.accent ? "bg-accent-muted" : "bg-surface-2",
              )}
            >
              <item.icon
                size={14}
                strokeWidth={1.5}
                className={
                  item.accent ? "text-accent" : "text-muted-foreground"
                }
              />
            </div>
            <div className="min-w-0 flex-1">
              <p className="text-foreground text-[13px] leading-snug">
                <span className="font-medium">{item.actor}</span>{" "}
                <span className="text-muted-foreground">{item.action}</span>
              </p>
            </div>
            <span className="text-subtle-foreground shrink-0 text-[12px]">
              {item.time}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
}
```

---

## 12. Empty State

Every table or list view must have an empty state. Never show a blank panel.

```tsx
function EmptyState({
  icon: Icon,
  title,
  description,
  action,
}: {
  icon: React.ComponentType<any>;
  title: string;
  description: string;
  action?: { label: string; onClick: () => void };
}) {
  return (
    <div className="flex flex-col items-center justify-center px-4 py-16 text-center">
      <div className="bg-surface-2 mb-4 flex h-10 w-10 items-center justify-center rounded-full">
        <Icon size={18} strokeWidth={1.5} className="text-muted-foreground" />
      </div>
      <p className="text-foreground mb-1 text-[14px] font-medium">{title}</p>
      <p className="text-muted-foreground mb-5 max-w-xs text-[13px]">
        {description}
      </p>
      {action && (
        <button
          onClick={action.onClick}
          className="bg-foreground text-background h-8 rounded-md px-4 text-[13px] font-medium transition-opacity hover:opacity-90"
        >
          {action.label}
        </button>
      )}
    </div>
  );
}
```

---

## 13. Section Cards — Panel Header Pattern

Any card containing a chart or complex widget needs this standard header:

```tsx
function SectionCard({
  title,
  subtitle,
  action,
  children,
}: {
  title: string;
  subtitle?: string;
  action?: React.ReactNode;
  children: React.ReactNode;
}) {
  return (
    <div className="bg-surface border-border overflow-hidden rounded-lg border">
      <div className="border-border flex items-center gap-3 border-b px-5 py-4">
        <div className="flex-1">
          <p className="text-foreground text-[13px] font-medium">{title}</p>
          {subtitle && (
            <p className="text-muted-foreground mt-0.5 text-[12px]">
              {subtitle}
            </p>
          )}
        </div>
        {action}
      </div>
      <div className="p-5">{children}</div>
    </div>
  );
}
```

---

## 14. Interaction & Motion Rules

Supabase and Vercel animations are barely there. They use motion to confirm interactions,
not to entertain.

| Interaction          | Duration | Easing      | Property                         |
| -------------------- | -------- | ----------- | -------------------------------- |
| Nav item hover       | 100ms    | ease        | color, background                |
| Card hover           | 150ms    | ease        | border-color                     |
| Button hover         | 100ms    | ease        | opacity, background              |
| Dropdown open        | 100ms    | ease-out    | opacity + translateY(4px→0)      |
| Sidebar collapse     | 200ms    | ease        | width                            |
| Skeleton pulse       | 1.5s     | ease-in-out | opacity (Tailwind animate-pulse) |
| Toast / notification | 200ms    | ease-out    | opacity + translateY             |

**Never**: bounce, spring, rotate, parallax, stagger-reveal on admin data content.
**OK**: subtle fade-in on route change (100ms opacity), skeleton to content crossfade.

---

## 15. Default Page Structure

Unless specified otherwise, every admin panel page ships with:

1. `<AdminShell>` wrapping the full layout
2. `<Topbar breadcrumb={['Projects', 'Dashboard']} />`
3. KPI row — 4 metric cards in a `grid-cols-4` grid
4. Primary chart section — time-series in a `SectionCard`
5. `<DataTable>` — with realistic mock columns and data, status badges, skeleton states
6. Secondary widget — `<ActivityFeed>` OR a second smaller chart

Page title pattern: use the breadcrumb, not a giant `<h1>`. If a heading is needed,
keep it at `text-[15px] font-semibold` max.

---

## 16. Frequently Used HugeIcons Reference

Quick lookup for common admin panel needs. Always `size={16} strokeWidth={1.5}` unless noted.

```
Navigation:
  Home01Icon            Dashboard
  BarChart01Icon        Analytics
  UserGroupIcon         Users / Team
  Package01Icon         Projects / Products
  Invoice01Icon         Billing / Payments
  Settings01Icon        Settings
  LockIcon              Security
  Database01Icon        Data / Storage
  CloudIcon             Infrastructure

Topbar:
  Search01Icon          Search input
  Notification01Icon    Notification bell
  QuestionMarkIcon      Help

Tables / Actions:
  MoreHorizontalIcon    Row actions
  FilterIcon            Table filter
  ArrowUpDownIcon       Sort toggle
  DownloadCircleIcon    Export

KPI Cards:
  DollarCircleIcon      Revenue
  UserCircleIcon        Users / Members
  CloudUploadIcon       Deployments
  AlertCircleIcon       Errors / Issues
  TrendUp01Icon         Growth

Status / Feedback:
  CheckmarkCircle01Icon Success
  AlertCircleIcon       Warning
  CancelCircleIcon      Error
  Loading01Icon         Loading (animate-spin)
```

---

## 17. Quality Checklist (run before delivering any admin panel)

- [ ] Font is Geist (imported from Google Fonts or locally)
- [ ] All colors reference CSS variables — no hardcoded hex values
- [ ] Exactly one accent color used throughout
- [ ] Sidebar is exactly 220px, nav items 32px tall
- [ ] Topbar is exactly 56px tall
- [ ] KPI cards have flat bg + thin border, no gradients
- [ ] All deltas use ↑ ↓ → Unicode, not icon components
- [ ] Table rows are 40px tall with px-4 padding
- [ ] Row hover uses `bg-surface-2/50`, no zebra striping
- [ ] Status badges have all three classes: bg + text + border
- [ ] Charts use `ResponsiveContainer`, custom `ChartTooltip`, no legends unless 3+ series
- [ ] Area gradient stops at 15% top, 0% bottom
- [ ] Empty state exists for every table/list view
- [ ] Loading state uses skeleton rows, not a spinner
- [ ] HugeIcons at `size={16} strokeWidth={1.5}` (no exceptions without reason)
- [ ] All hover transitions are ≤150ms
- [ ] No `box-shadow` used for card separation (borders only)
- [ ] Mock data is realistic (sine wave variation), not flat arrays

---

## 18. Common Mistakes — Hard Stops

- ❌ Gradient backgrounds on KPI cards or sidebar
- ❌ `strokeWidth={2}` on HugeIcons (use 1.5)
- ❌ Sidebar wider than 240px
- ❌ Row padding taller than 44px
- ❌ Bright / saturated status colors (use the muted token system)
- ❌ Multiple accent colors
- ❌ Pie or donut charts (use bar charts for composition data)
- ❌ Box-shadow for card separation
- ❌ Font sizes above 14px for body / table text
- ❌ Animation duration above 200ms for micro-interactions
- ❌ Hardcoded hex or oklch values in JSX — always use CSS variable tokens
- ❌ Leaving a table panel without a loading skeleton and an empty state
- ❌ Giant H1 page headings — use breadcrumb or 15px labels
- ❌ Using `Inter` or `system-ui` without loading Geist first
- ❌ Legends on charts with only one data series
- ❌ Zebra row striping
- ❌ Left-border accent stripe on active nav items (use bg fill only)
