shadimage
7ovr
blockdashboardsource: ok · hit

Dashboard

Logged-in dashboard app template with a collapsible sidebar and command palette top bar wrapping stat cards, an interactive area chart, and a sortable, paginated data table.

npx shadcn add "https://7ovr.com/r/dashboard.json"
free

Where it goes

statssidebardashboard-shelldata-tablekanban

Interaction

animated-counteraccordion

Use cases

dashboard

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
chartsrecharts
ui-primitives@base-ui/react

Registry dependencies

avatarbadgebuttoncardchartcheckboxcommanddropdown-menuinputkbdselectsheetsidebarsonnertableutils

Files6 · source cached 10/15/2026, 1:35:23 AM

registry/templates/dashboard/install-page.tsx
import { Toaster } from "@/components/ui/sonner"
import AppShell from "@/components/dashboard/app-shell"
import Charts from "@/components/dashboard/charts"
import Dashboard from "@/components/dashboard/dashboard"
import DataTable from "@/components/dashboard/table"

export default function DashboardTemplate() {
  return (
    <AppShell>
      <Toaster />
      <div className="mx-auto w-full max-w-6xl space-y-6">
        <Dashboard />
        <Charts />
        <DataTable />
      </div>
    </AppShell>
  )
}
registry/templates/dashboard/install-route.tsx
export { default } from "@/components/dashboard/page"
registry/templates/dashboard/components/app-shell.tsx
"use client"

import * as React from "react"
import { toast } from "sonner"

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"
import { Kbd, KbdGroup } from "@/components/ui/kbd"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from "@/components/ui/sidebar"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

type IconProps = { className?: string; size?: number | string }

const navItems = [
  {
    label: "Overview",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="LayoutGrid"
        tabler="IconLayoutGrid"
        hugeicons="LayoutGridIcon"
        phosphor="GridFour"
        remixicon="RiLayoutGridLine"
        {...p}
      />
    ),
  },
  {
    label: "Analytics",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="ChartBar"
        tabler="IconChartBar"
        hugeicons="BarChartIcon"
        phosphor="ChartBar"
        remixicon="RiBarChartBoxLine"
        {...p}
      />
    ),
  },
  {
    label: "Projects",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="Folder"
        tabler="IconFolder"
        hugeicons="FolderIcon"
        phosphor="Folder"
        remixicon="RiFolder3Line"
        {...p}
      />
    ),
  },
  {
    label: "Team",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="Users"
        tabler="IconUsersGroup"
        hugeicons="UserGroupIcon"
        phosphor="UsersThree"
        remixicon="RiTeamLine"
        {...p}
      />
    ),
  },
  {
    label: "Settings",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="Settings"
        tabler="IconSettings"
        hugeicons="SettingsIcon"
        phosphor="GearSix"
        remixicon="RiSettings3Line"
        {...p}
      />
    ),
  },
]

const notifications = [
  {
    id: 1,
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="Rocket"
        tabler="IconRocket"
        hugeicons="RocketIcon"
        phosphor="Rocket"
        remixicon="RiRocketLine"
        {...p}
      />
    ),
    title: "Deployment succeeded",
    detail: "acme-web v2.4 is live in production.",
    time: "2 Min Ago",
    unread: true,
    tone: "primary" as const,
  },
  {
    id: 2,
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="UserPlus"
        tabler="IconUserPlus"
        hugeicons="UserAddIcon"
        phosphor="UserPlus"
        remixicon="RiUserAddLine"
        {...p}
      />
    ),
    title: "New team member",
    detail: "Priya Nair accepted your invite.",
    time: "1 Hour Ago",
    unread: true,
    tone: "primary" as const,
  },
  {
    id: 3,
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="CircleAlert"
        tabler="IconAlertCircle"
        hugeicons="AlertCircleIcon"
        phosphor="WarningCircle"
        remixicon="RiErrorWarningLine"
        {...p}
      />
    ),
    title: "Usage at 80%",
    detail: "You're approaching your monthly request limit.",
    time: "3 Hours Ago",
    unread: false,
    tone: "destructive" as const,
  },
]

const recentItems = [
  {
    label: "Acme Web Redesign",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="Folder"
        tabler="IconFolder"
        hugeicons="FolderIcon"
        phosphor="Folder"
        remixicon="RiFolder3Line"
        {...p}
      />
    ),
  },
  {
    label: "Q3 Analytics report",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="ChartBar"
        tabler="IconChartBar"
        hugeicons="BarChartIcon"
        phosphor="ChartBar"
        remixicon="RiBarChartBoxLine"
        {...p}
      />
    ),
  },
  {
    label: "Priya Nair",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="User"
        tabler="IconUser"
        hugeicons="UserIcon"
        phosphor="User"
        remixicon="RiUser3Line"
        {...p}
      />
    ),
  },
]

const commands = [
  {
    label: "Go to Overview",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="LayoutGrid"
        tabler="IconLayoutGrid"
        hugeicons="LayoutGridIcon"
        phosphor="GridFour"
        remixicon="RiLayoutGridLine"
        {...p}
      />
    ),
    shortcut: ["G", "O"],
  },
  {
    label: "Go to Analytics",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="ChartBar"
        tabler="IconChartBar"
        hugeicons="BarChartIcon"
        phosphor="ChartBar"
        remixicon="RiBarChartBoxLine"
        {...p}
      />
    ),
    shortcut: ["G", "A"],
  },
  {
    label: "Go to Projects",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="Folder"
        tabler="IconFolder"
        hugeicons="FolderIcon"
        phosphor="Folder"
        remixicon="RiFolder3Line"
        {...p}
      />
    ),
    shortcut: ["G", "P"],
  },
  {
    label: "Go to Team",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="Users"
        tabler="IconUsersGroup"
        hugeicons="UserGroupIcon"
        phosphor="UsersThree"
        remixicon="RiTeamLine"
        {...p}
      />
    ),
    shortcut: ["G", "T"],
  },
  {
    label: "Invite a teammate",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="UserPlus"
        tabler="IconUserPlus"
        hugeicons="UserAddIcon"
        phosphor="UserPlus"
        remixicon="RiUserAddLine"
        {...p}
      />
    ),
    shortcut: ["I"],
  },
  {
    label: "Open settings",
    icon: (p: IconProps) => (
      <IconPlaceholder
        lucide="Settings"
        tabler="IconSettings"
        hugeicons="SettingsIcon"
        phosphor="GearSix"
        remixicon="RiSettings3Line"
        {...p}
      />
    ),
    shortcut: ["⌘", ","],
  },
]

export default function AppShell({ children }: { children: React.ReactNode }) {
  const [activeNav, setActiveNav] = React.useState("Overview")
  const [commandOpen, setCommandOpen] = React.useState(false)
  const [notes, setNotes] = React.useState(notifications)
  const unreadCount = notes.filter((n) => n.unread).length
  const markAllRead = () =>
    setNotes((prev) => prev.map((n) => ({ ...n, unread: false })))

  React.useEffect(() => {
    function onKeyDown(event: KeyboardEvent) {
      if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
        event.preventDefault()
        setCommandOpen((open) => !open)
      }
    }
    document.addEventListener("keydown", onKeyDown)
    return () => document.removeEventListener("keydown", onKeyDown)
  }, [])

  return (
    <SidebarProvider className="min-h-svh bg-muted/30 text-foreground">
      <Sidebar collapsible="icon">
        <SidebarHeader>
          <div className="flex h-10 items-center gap-2 px-1 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0">
            <svg
              viewBox="0 0 24 24"
              fill="currentColor"
              aria-hidden="true"
              className="size-5 shrink-0 text-foreground"
            >
              <rect
                x="3"
                y="3"
                width="8"
                height="8"
                transform="rotate(-6 7 7)"
              />
              <rect
                x="3"
                y="13"
                width="8"
                height="8"
                transform="rotate(5 7 17)"
              />
              <rect
                x="13"
                y="13"
                width="8"
                height="8"
                transform="rotate(-4 17 17)"
              />
              <rect
                x="13"
                y="3"
                width="8"
                height="8"
                transform="rotate(15 17 7)"
              />
            </svg>
            <span className="truncate text-base font-semibold group-data-[collapsible=icon]:hidden">
              Acme
            </span>
          </div>
        </SidebarHeader>

        <SidebarContent>
          <SidebarGroup>
            <SidebarMenu>
              {navItems.map((item) => (
                <SidebarMenuItem key={item.label}>
                  <SidebarMenuButton
                    isActive={activeNav === item.label}
                    tooltip={item.label}
                    onClick={() => setActiveNav(item.label)}
                  >
                    <item.icon />
                    <span>{item.label}</span>
                  </SidebarMenuButton>
                </SidebarMenuItem>
              ))}
            </SidebarMenu>
          </SidebarGroup>
        </SidebarContent>

        <SidebarFooter>
          <SidebarMenu>
            <SidebarMenuItem>
              <AccountMenu
                trigger={
                  <SidebarMenuButton
                    size="lg"
                    tooltip="Account"
                    className="gap-2 group-data-[collapsible=icon]:justify-center"
                  >
                    <Avatar size="sm">
                      <AvatarImage
                        src="https://i.pravatar.cc/150?img=15"
                        alt="Avery Cole"
                        className="grayscale"
                      />
                      <AvatarFallback>AC</AvatarFallback>
                    </Avatar>
                    <span className="flex min-w-0 flex-col group-data-[collapsible=icon]:hidden">
                      <span className="truncate text-xs font-medium">
                        Avery Cole
                      </span>
                      <span className="truncate text-xs text-muted-foreground">
                        avery@acme.com
                      </span>
                    </span>
                  </SidebarMenuButton>
                }
              />
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarFooter>
      </Sidebar>

      <SidebarInset>
        <header className="flex h-16 shrink-0 items-center gap-3 border-b border-border bg-background px-4 sm:px-6">
          <SidebarTrigger className="-ml-1" />

          <button
            type="button"
            onClick={() => setCommandOpen(true)}
            className="relative flex h-9 w-full max-w-sm items-center gap-2 rounded-lg border border-border bg-background px-3 text-sm text-muted-foreground transition-colors hover:bg-muted/50"
          >
            <IconPlaceholder
              lucide="Search"
              tabler="IconSearch"
              hugeicons="SearchIcon"
              phosphor="MagnifyingGlass"
              remixicon="RiSearchLine"
              className="size-4 shrink-0"
              aria-hidden="true"
            />
            <span className="flex-1 text-left">Search projects, people...</span>
            <KbdGroup className="hidden sm:inline-flex">
              <Kbd>⌘</Kbd>
              <Kbd>K</Kbd>
            </KbdGroup>
          </button>

          <div className="ml-auto flex items-center gap-2">
            <Sheet>
              <SheetTrigger
                render={
                  <Button
                    variant="ghost"
                    size="icon"
                    aria-label="Notifications"
                    className="relative"
                  >
                    <IconPlaceholder
                      lucide="Bell"
                      tabler="IconNotification"
                      hugeicons="NotificationIcon"
                      phosphor="Notification"
                      remixicon="RiNotification3Line"
                      className="size-5"
                    />
                    {unreadCount > 0 && (
                      <span className="absolute -top-1 -right-1 flex size-4 items-center justify-center rounded-md bg-primary text-[9px] leading-none font-bold text-primary-foreground ring-2 ring-background">
                        {unreadCount}
                      </span>
                    )}
                  </Button>
                }
              />
              <SheetContent className="w-full sm:max-w-sm">
                <SheetHeader className="gap-0.5 border-b border-border">
                  <SheetTitle>Notifications</SheetTitle>
                  <SheetDescription>
                    {unreadCount > 0
                      ? `${unreadCount} unread updates`
                      : "You're all caught up"}
                  </SheetDescription>
                </SheetHeader>
                <div className="flex flex-1 flex-col overflow-y-auto">
                  {notes.map((note) => (
                    <button
                      key={note.id}
                      type="button"
                      onClick={() =>
                        setNotes((prev) =>
                          prev.map((n) =>
                            n.id === note.id ? { ...n, unread: false } : n
                          )
                        )
                      }
                      className={[
                        "flex items-start gap-3 border-b border-border px-4 py-3 text-left transition-colors hover:bg-muted/50 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring",
                        note.unread ? "bg-muted/30" : "",
                      ]
                        .filter(Boolean)
                        .join(" ")}
                    >
                      <span
                        className={[
                          "mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md",
                          note.tone === "destructive"
                            ? "bg-destructive/10 text-destructive"
                            : "bg-primary/10 text-primary",
                        ].join(" ")}
                      >
                        <note.icon className="size-4" aria-hidden="true" />
                      </span>
                      <div className="flex min-w-0 flex-1 flex-col gap-0.5">
                        <div className="flex items-center justify-between gap-2">
                          <p className="text-sm font-medium text-foreground">
                            {note.title}
                          </p>
                          <span className="shrink-0 text-[10px] text-muted-foreground tabular-nums">
                            {note.time}
                          </span>
                        </div>
                        <p className="text-xs text-muted-foreground">
                          {note.detail}
                        </p>
                      </div>
                      {note.unread && (
                        <span
                          className="mt-1.5 size-2 shrink-0 bg-primary"
                          role="img"
                          aria-label="Unread"
                        />
                      )}
                    </button>
                  ))}
                </div>
                <div className="flex items-center justify-between border-t border-border p-3">
                  <Button
                    variant="ghost"
                    size="sm"
                    className="text-xs text-muted-foreground hover:text-foreground"
                    onClick={markAllRead}
                    disabled={unreadCount === 0}
                  >
                    Mark all read
                  </Button>
                  <Button
                    variant="ghost"
                    size="sm"
                    className="text-xs text-muted-foreground hover:text-foreground"
                    onClick={() =>
                      toast("Notifications", {
                        description: "Opening all notifications.",
                      })
                    }
                  >
                    View all
                  </Button>
                </div>
              </SheetContent>
            </Sheet>

            <AccountMenu
              trigger={
                <Button variant="ghost" size="icon" aria-label="Account menu">
                  <Avatar>
                    <AvatarImage
                      src="https://i.pravatar.cc/150?img=15"
                      alt="Avery Cole"
                      className="grayscale"
                    />
                    <AvatarFallback>AC</AvatarFallback>
                  </Avatar>
                </Button>
              }
            />
          </div>
        </header>

        <div className="flex-1 overflow-auto p-4 sm:p-6">{children}</div>
      </SidebarInset>

      <CommandDialog open={commandOpen} onOpenChange={setCommandOpen}>
        <Command>
          <CommandInput placeholder="Type a command or search..." />
          <CommandList>
            <CommandEmpty>No results found.</CommandEmpty>
            <CommandGroup heading="Recent">
              {recentItems.map((item) => (
                <CommandItem
                  key={item.label}
                  value={item.label}
                  onSelect={() => {
                    setCommandOpen(false)
                    toast(item.label, { description: "Opening recent item." })
                  }}
                >
                  <item.icon />
                  <span>{item.label}</span>
                </CommandItem>
              ))}
            </CommandGroup>
            <CommandSeparator />
            <CommandGroup heading="Commands">
              {commands.map((command) => (
                <CommandItem
                  key={command.label}
                  value={command.label}
                  onSelect={() => {
                    setCommandOpen(false)
                    toast(command.label, { description: "Command executed." })
                  }}
                >
                  <command.icon />
                  <span>{command.label}</span>
                  <CommandShortcut>
                    <KbdGroup>
                      {command.shortcut.map((key) => (
                        <Kbd key={key}>{key}</Kbd>
                      ))}
                    </KbdGroup>
                  </CommandShortcut>
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </CommandDialog>
    </SidebarProvider>
  )
}

function AccountMenu({ trigger }: { trigger: React.ReactElement }) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={trigger} />
      <DropdownMenuContent align="end" className="w-48">
        <div className="flex flex-col px-2 py-1.5">
          <span className="text-xs font-medium text-foreground">
            Avery Cole
          </span>
          <span className="text-xs text-muted-foreground">avery@acme.com</span>
        </div>
        <DropdownMenuSeparator />
        <DropdownMenuItem
          onClick={() =>
            toast("Account", { description: "Opening your account." })
          }
        >
          <IconPlaceholder
            lucide="User"
            tabler="IconUser"
            hugeicons="UserIcon"
            phosphor="User"
            remixicon="RiUser3Line"
          />
          Account
        </DropdownMenuItem>
        <DropdownMenuItem
          onClick={() =>
            toast("Settings", { description: "Opening your settings." })
          }
        >
          <IconPlaceholder
            lucide="Settings"
            tabler="IconSettings"
            hugeicons="SettingsIcon"
            phosphor="GearSix"
            remixicon="RiSettings3Line"
          />
          Settings
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem
          variant="destructive"
          onClick={() =>
            toast.success("Logged out", { description: "See you soon." })
          }
        >
          <IconPlaceholder
            lucide="LogOut"
            tabler="IconLogout"
            hugeicons="Logout01Icon"
            phosphor="SignOut"
            remixicon="RiLogoutBoxRLine"
          />
          Log Out
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
registry/templates/dashboard/components/charts.tsx
"use client"

import * as React from "react"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
import { Badge } from "@/components/ui/badge"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

type RangeKey = "7d" | "30d" | "90d"

type DataPoint = {
  label: string
  visitors: number
  signups: number
}

const datasets: Record<RangeKey, DataPoint[]> = {
  "7d": [
    { label: "Mon", visitors: 4_120, signups: 312 },
    { label: "Tue", visitors: 4_780, signups: 348 },
    { label: "Wed", visitors: 5_240, signups: 401 },
    { label: "Thu", visitors: 4_960, signups: 377 },
    { label: "Fri", visitors: 6_310, signups: 489 },
    { label: "Sat", visitors: 5_870, signups: 452 },
    { label: "Sun", visitors: 5_120, signups: 398 },
  ],
  "30d": [
    { label: "Week 1", visitors: 28_400, signups: 2_140 },
    { label: "Week 2", visitors: 31_900, signups: 2_480 },
    { label: "Week 3", visitors: 34_600, signups: 2_710 },
    { label: "Week 4", visitors: 39_200, signups: 3_060 },
  ],
  "90d": [
    { label: "Jan", visitors: 96_400, signups: 7_320 },
    { label: "Feb", visitors: 104_800, signups: 8_010 },
    { label: "Mar", visitors: 121_300, signups: 9_540 },
  ],
}

const ranges: { key: RangeKey; label: string }[] = [
  { key: "7d", label: "7 Days" },
  { key: "30d", label: "30 Days" },
  { key: "90d", label: "90 Days" },
]

const chartConfig = {
  visitors: {
    label: "Visitors",
    color: "var(--primary)",
  },
  signups: {
    label: "Signups",
    color: "var(--muted-foreground)",
  },
} satisfies ChartConfig
const series = [
  { key: "visitors", label: "Visitors" },
  { key: "signups", label: "Signups" },
] as const

function formatTick(value: number | string): string {
  const n = Number(value)
  if (n >= 10_000) return `${Math.round(n / 1000)}k`
  if (n >= 1000) return `${(n / 1000).toFixed(1)}k`
  return String(n)
}

function summarize(data: DataPoint[], key: keyof Omit<DataPoint, "label">) {
  const total = data.reduce((sum, point) => sum + point[key], 0)
  const first = data[0]?.[key] ?? 0
  const last = data[data.length - 1]?.[key] ?? 0
  const deltaPct = first === 0 ? 0 : ((last - first) / first) * 100
  return { total, deltaPct, up: deltaPct >= 0 }
}

export default function Charts() {
  const [range, setRange] = React.useState<RangeKey>("30d")
  const data = datasets[range]

  return (
    <section className="w-full">
      <Card className="w-full">
        <CardHeader className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
          <div className="space-y-1.5">
            <CardTitle>Traffic Overview</CardTitle>
            <CardDescription>
              Visitors and signups for Acme, last {range.replace("d", " days")}
            </CardDescription>
          </div>
          <Select
            value={range}
            onValueChange={(value) => setRange(value as RangeKey)}
            items={ranges.map((r) => ({ value: r.key, label: r.label }))}
          >
            <SelectTrigger
              className="w-28 shrink-0"
              aria-label="Select date range"
            >
              <SelectValue />
            </SelectTrigger>
            <SelectContent alignItemWithTrigger={false}>
              {ranges.map((r) => (
                <SelectItem key={r.key} value={r.key}>
                  {r.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </CardHeader>
        <CardContent className="grid gap-8 md:grid-cols-2 md:gap-10">
          {series.map(({ key, label }) => {
            const { total, deltaPct, up } = summarize(data, key)
            return (
              <div key={key} className="flex flex-col gap-4">
                <div className="flex items-baseline gap-2">
                  <span className="text-xs font-semibold tracking-widest text-muted-foreground uppercase">
                    {label}
                  </span>
                  <span className="ml-auto text-2xl font-semibold tabular-nums">
                    {total.toLocaleString("en-US")}
                  </span>
                  <Badge
                    variant={up ? "secondary" : "destructive"}
                    className="gap-1"
                  >
                    {up ? (
                      <IconPlaceholder
                        lucide="ArrowUp"
                        tabler="IconArrowUp"
                        hugeicons="ArrowUpIcon"
                        phosphor="ArrowUp"
                        remixicon="RiArrowUpLine"
                        data-icon="inline-start"
                        className="size-3"
                      />
                    ) : (
                      <IconPlaceholder
                        lucide="ArrowDown"
                        tabler="IconArrowDown"
                        hugeicons="ArrowDownIcon"
                        phosphor="ArrowDown"
                        remixicon="RiArrowDownLine"
                        data-icon="inline-start"
                        className="size-3"
                      />
                    )}
                    {up ? "+" : ""}
                    {deltaPct.toFixed(1)}%
                  </Badge>
                </div>

                <ChartContainer config={chartConfig} className="h-56 w-full">
                  <AreaChart data={data} margin={{ left: 0, right: 8, top: 8 }}>
                    <defs>
                      <linearGradient
                        id={`fill-${key}`}
                        x1="0"
                        y1="0"
                        x2="0"
                        y2="1"
                      >
                        <stop
                          offset="5%"
                          stopColor={`var(--color-${key})`}
                          stopOpacity={0.35}
                        />
                        <stop
                          offset="95%"
                          stopColor={`var(--color-${key})`}
                          stopOpacity={0.02}
                        />
                      </linearGradient>
                    </defs>
                    <CartesianGrid vertical={false} />
                    <XAxis
                      dataKey="label"
                      tickLine={false}
                      axisLine={false}
                      tickMargin={8}
                    />
                    <YAxis
                      tickLine={false}
                      axisLine={false}
                      tickMargin={8}
                      width={44}
                      tickFormatter={formatTick}
                    />
                    <ChartTooltip
                      cursor={false}
                      content={
                        <ChartTooltipContent
                          formatter={(value, name) => (
                            <span className="flex w-full justify-between gap-4">
                              <span className="text-muted-foreground capitalize">
                                {String(name)}
                              </span>
                              <span className="font-medium tabular-nums">
                                {Number(value).toLocaleString("en-US")}
                              </span>
                            </span>
                          )}
                        />
                      }
                    />
                    <Area
                      dataKey={key}
                      type="monotone"
                      stroke={`var(--color-${key})`}
                      strokeWidth={2}
                      fill={`url(#fill-${key})`}
                    />
                  </AreaChart>
                </ChartContainer>
              </div>
            )
          })}
        </CardContent>
      </Card>
    </section>
  )
}
registry/templates/dashboard/components/dashboard.tsx
"use client"

import * as React from "react"
import { Bar, BarChart, CartesianGrid, LabelList, XAxis } from "recharts"

import { Badge } from "@/components/ui/badge"
import {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

type Range = "7d" | "30d" | "90d" | "12m"

const RANGES: { value: Range; label: string }[] = [
  { value: "7d", label: "Last 7 Days" },
  { value: "30d", label: "Last 30 Days" },
  { value: "90d", label: "Last 90 Days" },
  { value: "12m", label: "Last 12 Months" },
]

const STATS_BY_RANGE: Record<
  Range,
  { label: string; value: string; delta: string; up: boolean }[]
> = {
  "7d": [
    { label: "Revenue", value: "$11.4k", delta: "+3.1%", up: true },
    { label: "Active Users", value: "2,140", delta: "+1.2%", up: true },
    { label: "Conversion", value: "3.31%", delta: "+0.2%", up: true },
    { label: "Avg. Session", value: "4m 02s", delta: "-0.4%", up: false },
  ],
  "30d": [
    { label: "Revenue", value: "$48.2k", delta: "+12.5%", up: true },
    { label: "Active Users", value: "8,941", delta: "+4.3%", up: true },
    { label: "Conversion", value: "3.24%", delta: "-0.8%", up: false },
    { label: "Avg. Session", value: "4m 12s", delta: "+1.1%", up: true },
  ],
  "90d": [
    { label: "Revenue", value: "$142.7k", delta: "+9.8%", up: true },
    { label: "Active Users", value: "26,304", delta: "+7.6%", up: true },
    { label: "Conversion", value: "3.18%", delta: "-1.4%", up: false },
    { label: "Avg. Session", value: "4m 26s", delta: "+2.3%", up: true },
  ],
  "12m": [
    { label: "Revenue", value: "$571.0k", delta: "+22.1%", up: true },
    { label: "Active Users", value: "98,612", delta: "+18.4%", up: true },
    { label: "Conversion", value: "3.02%", delta: "-2.9%", up: false },
    { label: "Avg. Session", value: "4m 41s", delta: "+5.0%", up: true },
  ],
}

const chartData = [
  { month: "Jan", sessions: 1840 },
  { month: "Feb", sessions: 2120 },
  { month: "Mar", sessions: 1760 },
  { month: "Apr", sessions: 2580 },
  { month: "May", sessions: 2340 },
  { month: "Jun", sessions: 3120 },
  { month: "Jul", sessions: 2210 },
  { month: "Aug", sessions: 2860 },
  { month: "Sep", sessions: 2480 },
  { month: "Oct", sessions: 3340 },
  { month: "Nov", sessions: 2720 },
  { month: "Dec", sessions: 3010 },
]

const chartConfig = {
  sessions: { label: "Sessions", color: "var(--primary)" },
} satisfies ChartConfig

export default function Dashboard() {
  const [range, setRange] = React.useState<Range>("30d")
  const stats = STATS_BY_RANGE[range]

  return (
    <section className="w-full">
      <div className="w-full">
        <div className="flex items-center justify-between gap-4">
          <h1 className="font-heading text-2xl font-bold tracking-tight">
            Overview
          </h1>
          <Select
            items={RANGES}
            value={range}
            onValueChange={(value) => setRange(value as Range)}
          >
            <SelectTrigger aria-label="Date range">
              <SelectValue />
            </SelectTrigger>
            <SelectContent alignItemWithTrigger={false}>
              {RANGES.map((r) => (
                <SelectItem key={r.value} value={r.value}>
                  {r.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>

        <div className="mt-6 grid grid-cols-2 gap-4 md:grid-cols-4">
          {stats.map((stat) => (
            <Card key={stat.label}>
              <CardHeader>
                <CardDescription>{stat.label}</CardDescription>
                <CardTitle className="text-3xl font-bold tracking-tight">
                  {stat.value}
                </CardTitle>
              </CardHeader>
              <CardContent className="flex items-center gap-2">
                <Badge
                  variant={stat.up ? "secondary" : "outline"}
                  className={
                    stat.up
                      ? undefined
                      : "border-destructive/40 text-destructive"
                  }
                >
                  {stat.up ? (
                    <IconPlaceholder
                      lucide="ArrowUpRight"
                      tabler="IconArrowUpRight"
                      hugeicons="ArrowUpRightIcon"
                      phosphor="ArrowUpRight"
                      remixicon="RiArrowRightUpLine"
                      data-icon="inline-start"
                      aria-hidden="true"
                    />
                  ) : (
                    <IconPlaceholder
                      lucide="ArrowDownRight"
                      tabler="IconArrowDownRight"
                      hugeicons="ArrowDownRightIcon"
                      phosphor="ArrowDownRight"
                      remixicon="RiArrowRightDownLine"
                      data-icon="inline-start"
                      aria-hidden="true"
                    />
                  )}
                  {stat.delta}
                </Badge>
                <span className="hidden text-xs text-muted-foreground sm:inline">
                  Vs Last Period
                </span>
              </CardContent>
            </Card>
          ))}
        </div>

        <Card className="mt-4">
          <CardHeader>
            <CardTitle>Monthly traffic</CardTitle>
            <CardDescription>Sessions over the last 12 months</CardDescription>
            <CardAction>
              <a
                href="#"
                className="flex items-center gap-1 text-sm font-medium text-muted-foreground"
              >
                View Report
                <IconPlaceholder
                  lucide="ArrowRight"
                  tabler="IconArrowRight"
                  hugeicons="ArrowRightIcon"
                  phosphor="ArrowRight"
                  remixicon="RiArrowRightLine"
                  className="size-4"
                  aria-hidden="true"
                />
              </a>
            </CardAction>
          </CardHeader>
          <CardContent>
            <ChartContainer config={chartConfig} className="h-48 w-full">
              <BarChart
                accessibilityLayer
                data={chartData}
                margin={{ top: 24 }}
              >
                <CartesianGrid vertical={false} />
                <XAxis
                  dataKey="month"
                  tickLine={false}
                  axisLine={false}
                  tickMargin={8}
                />
                <ChartTooltip
                  cursor={false}
                  content={<ChartTooltipContent />}
                />
                <Bar
                  dataKey="sessions"
                  fill="var(--color-sessions)"
                  maxBarSize={48}
                >
                  <LabelList
                    dataKey="sessions"
                    position="top"
                    offset={8}
                    className="fill-muted-foreground"
                    fontSize={11}
                    formatter={(value) =>
                      (Number(value) / 1000).toFixed(1) + "k"
                    }
                  />
                </Bar>
              </BarChart>
            </ChartContainer>
          </CardContent>
        </Card>
      </div>
    </section>
  )
}
registry/templates/dashboard/components/table.tsx
"use client"

import * as React from "react"
import {
  sortFn_datetime,
  type ColumnDef,
  type ColumnFiltersState,
  type SortingState,
  flexRender,
  tableFeatures,
  useTable,
  createSortedRowModel,
  rowSortingFeature,
  columnFilteringFeature,
  createFilteredRowModel,
  createPaginatedRowModel,
  rowPaginationFeature,
  rowSelectionFeature,
  columnVisibilityFeature,
  type ColumnVisibilityState,
} from "@tanstack/react-table"

const TABLE_FEATURES = tableFeatures({
  rowSortingFeature,
  columnFilteringFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  columnVisibilityFeature,
  sortedRowModel: createSortedRowModel(),
  filteredRowModel: createFilteredRowModel(),
  paginatedRowModel: createPaginatedRowModel(),
  sortFns: {
    datetime: sortFn_datetime,
  },
})
import { toast } from "sonner"

import { cn } from "@/lib/utils"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

type Status = "Active" | "Invited" | "Inactive"
type Role = "Admin" | "Editor" | "Viewer"

type Member = {
  id: string
  name: string
  initials: string
  avatar: string
  email: string
  status: Status
  role: Role
  joined: string
}

const statusVariant: Record<Status, "default" | "secondary" | "outline"> = {
  Active: "default",
  Invited: "secondary",
  Inactive: "outline",
}

const roleClass: Record<Role, string> = {
  Admin: "text-foreground font-medium",
  Editor: "text-muted-foreground",
  Viewer: "text-muted-foreground",
}

const COLUMN_LABELS: Record<string, string> = {
  name: "Member",
  status: "Status",
  role: "Role",
  joined: "Joined",
}

const members: Member[] = [
  {
    id: "m-01",
    name: "Ada Lovelace",
    initials: "AL",
    avatar: "https://i.pravatar.cc/80?img=47",
    email: "ada@acme.io",
    status: "Active",
    role: "Admin",
    joined: "2026-06-12",
  },
  {
    id: "m-02",
    name: "Alan Turing",
    initials: "AT",
    avatar: "https://i.pravatar.cc/80?img=11",
    email: "alan@acme.io",
    status: "Active",
    role: "Editor",
    joined: "2026-06-10",
  },
  {
    id: "m-03",
    name: "Grace Hopper",
    initials: "GH",
    avatar: "https://i.pravatar.cc/80?img=45",
    email: "grace@acme.io",
    status: "Invited",
    role: "Editor",
    joined: "2026-06-08",
  },
  {
    id: "m-04",
    name: "Linus Pauling",
    initials: "LP",
    avatar: "https://i.pravatar.cc/80?img=12",
    email: "linus@acme.io",
    status: "Inactive",
    role: "Viewer",
    joined: "2026-05-29",
  },
  {
    id: "m-05",
    name: "Katherine Johnson",
    initials: "KJ",
    avatar: "https://i.pravatar.cc/80?img=49",
    email: "katherine@acme.io",
    status: "Active",
    role: "Viewer",
    joined: "2026-05-21",
  },
  {
    id: "m-06",
    name: "Edsger Dijkstra",
    initials: "ED",
    avatar: "https://i.pravatar.cc/80?img=13",
    email: "edsger@acme.io",
    status: "Active",
    role: "Admin",
    joined: "2026-05-18",
  },
  {
    id: "m-07",
    name: "Barbara Liskov",
    initials: "BL",
    avatar: "https://i.pravatar.cc/80?img=44",
    email: "barbara@acme.io",
    status: "Active",
    role: "Editor",
    joined: "2026-05-14",
  },
  {
    id: "m-08",
    name: "Tim Berners-Lee",
    initials: "TB",
    avatar: "https://i.pravatar.cc/80?img=14",
    email: "tim@acme.io",
    status: "Invited",
    role: "Viewer",
    joined: "2026-05-09",
  },
  {
    id: "m-09",
    name: "Margaret Hamilton",
    initials: "MH",
    avatar: "https://i.pravatar.cc/80?img=48",
    email: "margaret@acme.io",
    status: "Active",
    role: "Editor",
    joined: "2026-05-04",
  },
  {
    id: "m-10",
    name: "Donald Knuth",
    initials: "DK",
    avatar: "https://i.pravatar.cc/80?img=15",
    email: "donald@acme.io",
    status: "Inactive",
    role: "Viewer",
    joined: "2026-04-28",
  },
  {
    id: "m-11",
    name: "Radia Perlman",
    initials: "RP",
    avatar: "https://i.pravatar.cc/80?img=43",
    email: "radia@acme.io",
    status: "Active",
    role: "Admin",
    joined: "2026-04-22",
  },
  {
    id: "m-12",
    name: "Ken Thompson",
    initials: "KT",
    avatar: "https://i.pravatar.cc/80?img=16",
    email: "ken@acme.io",
    status: "Active",
    role: "Editor",
    joined: "2026-04-19",
  },
  {
    id: "m-13",
    name: "Hedy Lamarr",
    initials: "HL",
    avatar: "https://i.pravatar.cc/80?img=41",
    email: "hedy@acme.io",
    status: "Invited",
    role: "Viewer",
    joined: "2026-04-15",
  },
  {
    id: "m-14",
    name: "Dennis Ritchie",
    initials: "DR",
    avatar: "https://i.pravatar.cc/80?img=17",
    email: "dennis@acme.io",
    status: "Active",
    role: "Editor",
    joined: "2026-04-11",
  },
  {
    id: "m-15",
    name: "Shafi Goldwasser",
    initials: "SG",
    avatar: "https://i.pravatar.cc/80?img=40",
    email: "shafi@acme.io",
    status: "Active",
    role: "Viewer",
    joined: "2026-04-07",
  },
  {
    id: "m-16",
    name: "John McCarthy",
    initials: "JM",
    avatar: "https://i.pravatar.cc/80?img=18",
    email: "john@acme.io",
    status: "Inactive",
    role: "Viewer",
    joined: "2026-04-02",
  },
  {
    id: "m-17",
    name: "Frances Allen",
    initials: "FA",
    avatar: "https://i.pravatar.cc/80?img=39",
    email: "frances@acme.io",
    status: "Active",
    role: "Admin",
    joined: "2026-03-29",
  },
  {
    id: "m-18",
    name: "Vint Cerf",
    initials: "VC",
    avatar: "https://i.pravatar.cc/80?img=19",
    email: "vint@acme.io",
    status: "Active",
    role: "Editor",
    joined: "2026-03-24",
  },
  {
    id: "m-19",
    name: "Adele Goldberg",
    initials: "AG",
    avatar: "https://i.pravatar.cc/80?img=38",
    email: "adele@acme.io",
    status: "Invited",
    role: "Viewer",
    joined: "2026-03-20",
  },
  {
    id: "m-20",
    name: "Bjarne Stroustrup",
    initials: "BS",
    avatar: "https://i.pravatar.cc/80?img=20",
    email: "bjarne@acme.io",
    status: "Active",
    role: "Editor",
    joined: "2026-03-16",
  },
  {
    id: "m-21",
    name: "Karen Spärck Jones",
    initials: "KS",
    avatar: "https://i.pravatar.cc/80?img=36",
    email: "karen@acme.io",
    status: "Active",
    role: "Viewer",
    joined: "2026-03-11",
  },
  {
    id: "m-22",
    name: "Brian Kernighan",
    initials: "BK",
    avatar: "https://i.pravatar.cc/80?img=51",
    email: "brian@acme.io",
    status: "Inactive",
    role: "Viewer",
    joined: "2026-03-06",
  },
  {
    id: "m-23",
    name: "Sophie Wilson",
    initials: "SW",
    avatar: "https://i.pravatar.cc/80?img=35",
    email: "sophie@acme.io",
    status: "Active",
    role: "Editor",
    joined: "2026-03-01",
  },
  {
    id: "m-24",
    name: "Guido van Rossum",
    initials: "GR",
    avatar: "https://i.pravatar.cc/80?img=52",
    email: "guido@acme.io",
    status: "Active",
    role: "Admin",
    joined: "2026-02-24",
  },
  {
    id: "m-25",
    name: "Lynn Conway",
    initials: "LC",
    avatar: "https://i.pravatar.cc/80?img=34",
    email: "lynn@acme.io",
    status: "Invited",
    role: "Viewer",
    joined: "2026-02-19",
  },
  {
    id: "m-26",
    name: "Ralph Merkle",
    initials: "RM",
    avatar: "https://i.pravatar.cc/80?img=53",
    email: "ralph@acme.io",
    status: "Active",
    role: "Editor",
    joined: "2026-02-13",
  },
  {
    id: "m-27",
    name: "Carol Shaw",
    initials: "CS",
    avatar: "https://i.pravatar.cc/80?img=32",
    email: "carol@acme.io",
    status: "Active",
    role: "Viewer",
    joined: "2026-02-08",
  },
  {
    id: "m-28",
    name: "Niklaus Wirth",
    initials: "NW",
    avatar: "https://i.pravatar.cc/80?img=54",
    email: "niklaus@acme.io",
    status: "Inactive",
    role: "Viewer",
    joined: "2026-02-02",
  },
  {
    id: "m-29",
    name: "Mary Allen Wilkes",
    initials: "MW",
    avatar: "https://i.pravatar.cc/80?img=31",
    email: "mary@acme.io",
    status: "Active",
    role: "Admin",
    joined: "2026-01-27",
  },
  {
    id: "m-30",
    name: "Leslie Lamport",
    initials: "LL",
    avatar: "https://i.pravatar.cc/80?img=55",
    email: "leslie@acme.io",
    status: "Active",
    role: "Editor",
    joined: "2026-01-21",
  },
]

const dateFmt = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "2-digit",
  year: "numeric",
})

function formatDate(value: string) {
  const parsed = new Date(value)
  return Number.isNaN(parsed.getTime()) ? value : dateFmt.format(parsed)
}

function SortIcon({ sorted }: { sorted: false | "asc" | "desc" }) {
  if (sorted === "asc")
    return (
      <IconPlaceholder
        lucide="ArrowUp"
        tabler="IconArrowUp"
        hugeicons="ArrowUpIcon"
        phosphor="ArrowUp"
        remixicon="RiArrowUpLine"
        className="size-3.5"
        aria-hidden="true"
      />
    )
  if (sorted === "desc")
    return (
      <IconPlaceholder
        lucide="ArrowDown"
        tabler="IconArrowDown"
        hugeicons="ArrowDownIcon"
        phosphor="ArrowDown"
        remixicon="RiArrowDownLine"
        className="size-3.5"
        aria-hidden="true"
      />
    )
  return (
    <IconPlaceholder
      lucide="ChevronsUpDown"
      tabler="IconArrowsVertical"
      hugeicons="ArrowUpDownIcon"
      phosphor="CaretUpDown"
      remixicon="RiExpandUpDownLine"
      className="size-3.5 text-muted-foreground/60"
      aria-hidden="true"
    />
  )
}

const columns: ColumnDef<typeof TABLE_FEATURES, Member>[] = [
  {
    id: "select",
    enableSorting: false,
    enableHiding: false,
    header: ({ table }) => (
      <Checkbox
        checked={table.getIsAllPageRowsSelected()}
        indeterminate={
          table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()
        }
        onCheckedChange={(checked) =>
          table.toggleAllPageRowsSelected(checked === true)
        }
        aria-label="Select all members on this page"
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(checked) => row.toggleSelected(checked === true)}
        aria-label={`Select ${row.original.name}`}
      />
    ),
  },
  {
    accessorKey: "name",
    header: ({ column }) => (
      <button
        type="button"
        onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
        className="-mx-1 inline-flex items-center gap-1 rounded-md px-1 text-xs font-semibold tracking-widest text-muted-foreground uppercase transition-colors hover:text-foreground"
      >
        Member
        <SortIcon sorted={column.getIsSorted()} />
      </button>
    ),
    filterFn: (row, _id, value: string) => {
      const q = value.toLowerCase()
      return (
        row.original.name.toLowerCase().includes(q) ||
        row.original.email.toLowerCase().includes(q)
      )
    },
    cell: ({ row }) => {
      const member = row.original
      return (
        <div className="flex min-w-0 items-center gap-3">
          <Avatar size="sm" className="shrink-0 border border-border">
            <AvatarImage
              src={member.avatar}
              alt={member.name}
              className="grayscale"
            />
            <AvatarFallback className="text-xs">
              {member.initials}
            </AvatarFallback>
          </Avatar>
          <div className="min-w-0">
            <p className="truncate text-sm leading-tight font-medium">
              {member.name}
            </p>
            <p className="truncate text-xs text-muted-foreground">
              {member.email}
            </p>
          </div>
        </div>
      )
    },
  },
  {
    accessorKey: "status",
    enableSorting: false,
    header: () => (
      <span className="text-xs font-semibold tracking-widest text-muted-foreground uppercase">
        Status
      </span>
    ),
    cell: ({ row }) => (
      <Badge variant={statusVariant[row.original.status]} className="text-xs">
        {row.original.status}
      </Badge>
    ),
  },
  {
    accessorKey: "role",
    header: ({ column }) => (
      <button
        type="button"
        onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
        className="-mx-1 inline-flex items-center gap-1 rounded-md px-1 text-xs font-semibold tracking-widest text-muted-foreground uppercase transition-colors hover:text-foreground"
      >
        Role
        <SortIcon sorted={column.getIsSorted()} />
      </button>
    ),
    cell: ({ row }) => (
      <span className={cn("text-sm", roleClass[row.original.role])}>
        {row.original.role}
      </span>
    ),
  },
  {
    accessorKey: "joined",
    sortFn: "datetime",
    header: ({ column }) => (
      <button
        type="button"
        onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
        className="-mx-1 ml-auto inline-flex items-center gap-1 rounded-md px-1 text-xs font-semibold tracking-widest text-muted-foreground uppercase transition-colors hover:text-foreground"
      >
        Joined
        <SortIcon sorted={column.getIsSorted()} />
      </button>
    ),
    cell: ({ row }) => (
      <span className="block text-right text-xs text-muted-foreground tabular-nums">
        {formatDate(row.original.joined)}
      </span>
    ),
  },
  {
    id: "actions",
    enableSorting: false,
    enableHiding: false,
    header: () => <span className="sr-only">Actions</span>,
    cell: ({ row }) => (
      <div className="flex justify-end">
        <DropdownMenu>
          <DropdownMenuTrigger
            render={
              <Button
                variant="ghost"
                size="icon-sm"
                aria-label={`Actions for ${row.original.name}`}
              >
                <IconPlaceholder
                  lucide="Ellipsis"
                  tabler="IconDots"
                  hugeicons="MoreHorizontalIcon"
                  phosphor="DotsThree"
                  remixicon="RiMoreLine"
                  className="size-4"
                  aria-hidden="true"
                />
              </Button>
            }
          />
          <DropdownMenuContent align="end" className="w-40">
            <DropdownMenuItem>
              <IconPlaceholder
                lucide="User"
                tabler="IconUser"
                hugeicons="UserIcon"
                phosphor="User"
                remixicon="RiUserLine"
                aria-hidden="true"
              />
              View Profile
            </DropdownMenuItem>
            <DropdownMenuItem>
              <IconPlaceholder
                lucide="Pencil"
                tabler="IconPencil"
                hugeicons="PencilIcon"
                phosphor="Pencil"
                remixicon="RiPencilLine"
                aria-hidden="true"
              />
              Edit
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem variant="destructive">
              <IconPlaceholder
                lucide="Trash"
                tabler="IconTrash"
                hugeicons="Delete02Icon"
                phosphor="Trash"
                remixicon="RiDeleteBinLine"
                aria-hidden="true"
              />
              Remove
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </div>
    ),
  },
]

export default function DataTable() {
  const [sorting, setSorting] = React.useState<SortingState>([
    { id: "joined", desc: true },
  ])
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
    []
  )
  const [columnVisibility, setColumnVisibility] =
    React.useState<ColumnVisibilityState>({})
  const [rowSelection, setRowSelection] = React.useState({})
  const [data, setData] = React.useState(members)

  const table = useTable({
    features: TABLE_FEATURES,
    data,
    columns,
    getRowId: (row) => row.id,
    state: { sorting, columnFilters, columnVisibility, rowSelection },
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    initialState: { pagination: { pageIndex: 0, pageSize: 6 } },
  })

  const nameFilter = (table.getColumn("name")?.getFilterValue() as string) ?? ""
  const selectedCount = table.getFilteredSelectedRowModel().rows.length
  const totalCount = table.getFilteredRowModel().rows.length
  const pageCount = table.getPageCount()

  function handleRemove() {
    const selectedIds = new Set(
      table.getFilteredSelectedRowModel().rows.map((row) => row.id)
    )
    setData((prev) => prev.filter((row) => !selectedIds.has(row.id)))
    table.resetRowSelection()
    toast("Members removed", {
      description: `${selectedIds.size} ${
        selectedIds.size === 1 ? "member" : "members"
      } removed from the workspace.`,
    })
  }

  return (
    <section className="w-full">
      <div className="w-full rounded-xl border border-border bg-card">
        <div className="flex flex-col gap-4 border-b border-border px-6 py-5 sm:flex-row sm:items-center sm:justify-between">
          <div className="flex items-center gap-3">
            <div className="flex size-9 items-center justify-center rounded-md border border-border bg-card text-muted-foreground">
              <IconPlaceholder
                lucide="UserCog"
                tabler="IconShield"
                hugeicons="ShieldUserIcon"
                phosphor="Shield"
                remixicon="RiShieldUserLine"
                className="size-4"
                aria-hidden="true"
              />
            </div>
            <div>
              <h2 className="font-heading text-lg leading-tight font-semibold tracking-tight">
                Team Members
              </h2>
              <p className="text-sm text-muted-foreground">
                {data.length} members across 3 workspaces
              </p>
            </div>
          </div>
          <div className="flex w-full flex-wrap items-center gap-2 sm:w-auto">
            <div className="relative flex-1 sm:flex-none">
              <IconPlaceholder
                lucide="Search"
                tabler="IconSearch"
                hugeicons="SearchIcon"
                phosphor="MagnifyingGlass"
                remixicon="RiSearchLine"
                className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground"
                aria-hidden="true"
              />
              <Input
                type="search"
                value={nameFilter}
                onChange={(event) =>
                  table.getColumn("name")?.setFilterValue(event.target.value)
                }
                placeholder="Search members..."
                className="h-7 w-full pl-8 text-sm sm:w-48"
                aria-label="Search members by name or email"
              />
            </div>
            <DropdownMenu>
              <DropdownMenuTrigger
                render={
                  <Button
                    variant="outline"
                    size="sm"
                    aria-label="Toggle columns"
                  >
                    <IconPlaceholder
                      lucide="Columns"
                      tabler="IconColumns"
                      hugeicons="LayoutLeftIcon"
                      phosphor="Columns"
                      remixicon="RiLayoutColumnLine"
                      className="size-3.5"
                      aria-hidden="true"
                    />
                    View
                  </Button>
                }
              />
              <DropdownMenuContent align="end" className="w-40">
                <DropdownMenuGroup>
                  <DropdownMenuLabel>Toggle Columns</DropdownMenuLabel>
                  <DropdownMenuSeparator />
                  {table
                    .getAllColumns()
                    .filter((column) => column.getCanHide())
                    .map((column) => (
                      <DropdownMenuCheckboxItem
                        key={column.id}
                        checked={column.getIsVisible()}
                        onCheckedChange={(checked) =>
                          column.toggleVisibility(checked === true)
                        }
                        closeOnClick={false}
                      >
                        {COLUMN_LABELS[column.id] ?? column.id}
                      </DropdownMenuCheckboxItem>
                    ))}
                </DropdownMenuGroup>
              </DropdownMenuContent>
            </DropdownMenu>
            <Button
              size="sm"
              onClick={() =>
                toast("Invitation sent", {
                  description: "They will appear here once they accept.",
                })
              }
            >
              <IconPlaceholder
                lucide="Plus"
                tabler="IconPlus"
                hugeicons="Add01Icon"
                phosphor="Plus"
                remixicon="RiAddLine"
                className="mr-1 size-3.5"
                aria-hidden="true"
              />
              Invite
            </Button>
          </div>
        </div>

        {selectedCount > 0 && (
          <div className="flex flex-wrap items-center justify-between gap-3 border-b border-border bg-muted/40 px-6 py-2.5">
            <div className="flex items-center gap-2">
              <span className="text-sm font-medium text-foreground tabular-nums">
                {selectedCount} Selected
              </span>
              <Button
                variant="ghost"
                size="xs"
                className="text-muted-foreground hover:text-foreground"
                onClick={() => table.resetRowSelection()}
              >
                Clear
              </Button>
            </div>
            <div className="flex flex-wrap items-center gap-2">
              <Button
                variant="outline"
                size="sm"
                onClick={() =>
                  toast("Export started", {
                    description: `Exporting ${selectedCount} members to CSV.`,
                  })
                }
              >
                <IconPlaceholder
                  lucide="Download"
                  tabler="IconDownload"
                  hugeicons="DownloadIcon"
                  phosphor="Download"
                  remixicon="RiDownloadLine"
                  className="size-3.5"
                  aria-hidden="true"
                />
                Export
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() =>
                  toast("Role updated", {
                    description: `Changed the role for ${selectedCount} members.`,
                  })
                }
              >
                <IconPlaceholder
                  lucide="UserCog"
                  tabler="IconUserCog"
                  hugeicons="UserSettingsIcon"
                  phosphor="UserGear"
                  remixicon="RiUserSettingsLine"
                  className="size-3.5"
                  aria-hidden="true"
                />
                Change role
              </Button>
              <Button
                variant="outline"
                size="sm"
                className="text-destructive hover:text-destructive"
                onClick={handleRemove}
              >
                <IconPlaceholder
                  lucide="Trash"
                  tabler="IconTrash"
                  hugeicons="Delete02Icon"
                  phosphor="Trash"
                  remixicon="RiDeleteBinLine"
                  className="size-3.5"
                  aria-hidden="true"
                />
                Remove
              </Button>
            </div>
          </div>
        )}

        <div>
          <Table>
            <TableHeader>
              {table.getHeaderGroups().map((headerGroup) => (
                <TableRow
                  key={headerGroup.id}
                  className="border-b border-border bg-muted/40 hover:bg-muted/40"
                >
                  {headerGroup.headers.map((header) => (
                    <TableHead
                      key={header.id}
                      className={cn(
                        "h-9",
                        header.column.id === "select" && "w-10 pl-6",
                        header.column.id === "name" && "pl-3",
                        header.column.id === "joined" && "text-right",
                        header.column.id === "actions" && "w-10 pr-6"
                      )}
                    >
                      {header.isPlaceholder
                        ? null
                        : flexRender(
                            header.column.columnDef.header,
                            header.getContext()
                          )}
                    </TableHead>
                  ))}
                </TableRow>
              ))}
            </TableHeader>
            <TableBody>
              {table.getRowModel().rows.length ? (
                table.getRowModel().rows.map((row) => (
                  <TableRow
                    key={row.id}
                    data-state={row.getIsSelected() ? "selected" : undefined}
                    className="border-b border-border transition-colors duration-100 last:border-b-0 hover:bg-muted/30"
                  >
                    {row.getVisibleCells().map((cell) => (
                      <TableCell
                        key={cell.id}
                        className={cn(
                          "py-3",
                          cell.column.id === "select" && "pl-6",
                          cell.column.id === "name" && "pl-3",
                          cell.column.id === "actions" && "pr-6"
                        )}
                      >
                        {flexRender(
                          cell.column.columnDef.cell,
                          cell.getContext()
                        )}
                      </TableCell>
                    ))}
                  </TableRow>
                ))
              ) : (
                <TableRow className="hover:bg-transparent">
                  <TableCell
                    colSpan={columns.length}
                    className="h-24 text-center text-sm text-muted-foreground"
                  >
                    No members match your search.
                  </TableCell>
                </TableRow>
              )}
            </TableBody>
          </Table>

          <div className="flex items-center justify-between gap-4 rounded-b-xl border-t border-border bg-muted/20 px-6 py-3">
            <p className="text-xs text-muted-foreground">
              <span className="font-medium text-foreground">{totalCount}</span>{" "}
              {totalCount === 1 ? "Result" : "Results"}
            </p>
            <div className="flex items-center gap-1.5">
              <Button
                variant="outline"
                size="icon"
                className="size-7"
                onClick={() => table.previousPage()}
                disabled={!table.getCanPreviousPage()}
                aria-label="Previous page"
              >
                <IconPlaceholder
                  lucide="ChevronLeft"
                  tabler="IconChevronLeft"
                  hugeicons="ArrowLeft01Icon"
                  phosphor="CaretLeft"
                  remixicon="RiArrowLeftSLine"
                  className="size-3.5"
                  aria-hidden="true"
                />
              </Button>
              <span className="px-1 text-xs text-muted-foreground tabular-nums">
                Page {table.state.pagination.pageIndex + 1} of{" "}
                {Math.max(pageCount, 1)}
              </span>
              <Button
                variant="outline"
                size="icon"
                className="size-7"
                onClick={() => table.nextPage()}
                disabled={!table.getCanNextPage()}
                aria-label="Next page"
              >
                <IconPlaceholder
                  lucide="ChevronRight"
                  tabler="IconChevronRight"
                  hugeicons="ArrowRight01Icon"
                  phosphor="CaretRight"
                  remixicon="RiArrowRightSLine"
                  className="size-3.5"
                  aria-hidden="true"
                />
              </Button>
            </div>
          </div>
        </div>
      </div>
    </section>
  )
}

More from 7ovr

Browse all components in this library →