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 · mediumchartsrecharts
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>
)
}