بلاک‌ها · پنل مدیریت

منوی کناری، کارت‌های آمار، نمودار فروش ماهانه با بازهٔ تاریخ و آخرین سفارش‌ها.

۱. نصب کامپوننت‌ها

این بلاک از ۱۹ آیتم دیگ استفاده می‌کند: avatarbadgebuttoncardchart-presetscheckboxcollapsiblecontrol-sizedropdown-menuiconslabelpaginationpersiansearch-fieldselectsidebarstat-tiletabletoggle-group

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/collapsible.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/sidebar.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/collapsible.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/sidebar.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/collapsible.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/sidebar.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/collapsible.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/sidebar.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

dashboard.tsx
"use client"

import * as React from "react"

import {
  AvatarGenderIcon,
  BadgeStarIcon,
  BarChartIcon,
  BellIcon,
  ChevronDownIcon,
  DashboardIcon,
  EnvelopeIcon,
  DigLogo,
  FilterIcon,
  GuideIcon,
  LogoutIcon,
  MoonIcon,
  SunIcon,
  PlusIcon,
  ReceiptIcon,
  SettingsIcon,
  ShieldCheckIcon,
  ShopIcon,
  TagIcon,
  TicketIcon,
  UsersIcon,
  XIcon,
} from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ComboChart } from "@/components/ui/chart-presets"
import { Checkbox } from "@/components/ui/checkbox"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"
import { SearchField } from "@/components/ui/search-field"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import {
  Sidebar,
  SidebarContent,
  SidebarBrand,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarSearch,
  SidebarTrigger,
} from "@/components/ui/sidebar"
import {
  StatTile,
  StatTileAction,
  StatTileHeader,
  StatTileLabel,
  StatTileList,
} from "@/components/ui/stat-tile"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { formatCurrency, formatJalali, formatNumber, formatRelativeTime, toFaDigits, toSearchKey } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"

/* ------------------------------------------------------------------ داده */

/** دادهٔ نمونهٔ قطعی برای هر بازه؛ در پروژهٔ واقعی همین سه فراخوانی API است. */
function salesFor(period: string) {
  const wave = (index: number, base: number, amp: number) => Math.round(base + amp * Math.sin(index * 0.9) + index * (amp / 6))
  if (period === "7") {
    return Array.from({ length: 7 }, (_, index) => ({
      label: `${toFaDigits(3 + index)} مهر`,
      revenue: wave(index, 52, 12),
      orders: wave(index, 165, 28),
    }))
  }
  if (period === "30") {
    // ۳۰ روز در ۱۰ دستهٔ سه‌روزه، تا میله‌ها خوانا بمانند.
    return Array.from({ length: 10 }, (_, index) => ({
      label: `${toFaDigits(1 + index * 3)} ${index < 3 ? "شهریور" : "مهر"}`,
      revenue: wave(index, 150, 30),
      orders: wave(index, 480, 70),
    }))
  }
  return Array.from({ length: 12 }, (_, index) => ({
    label: `هفتهٔ ${toFaDigits(index + 1)}`,
    revenue: wave(index, 360, 70),
    orders: wave(index, 1150, 160),
  }))
}


const QUICK = [
  { title: "محصولِ تازه", hint: "افزودن به فروشگاه", icon: <PlusIcon />, tone: "bg-primary-subtle text-primary-subtle-foreground" },
  { title: "ثبتِ سفارش", hint: "فروشِ حضوری", icon: <ReceiptIcon />, tone: "bg-info-subtle text-info-subtle-foreground" },
  { title: "کدِ تخفیف", hint: "کمپینِ تازه", icon: <TagIcon />, tone: "bg-warning-subtle text-warning-subtle-foreground" },
  { title: "گزارشِ فروش", hint: "خروجی اکسل", icon: <BarChartIcon />, tone: "bg-success-subtle text-success-subtle-foreground" },
]

const CUSTOMERS = [
  { name: "مهسا توکلی", gender: "female" as const, orders: 24, spent: "۱۲٫۴ میلیون" },
  { name: "کیان مرادی", gender: "male" as const, orders: 18, spent: "۹٫۸ میلیون" },
  { name: "نیلوفر احمدی", gender: "female" as const, orders: 11, spent: "۶٫۱ میلیون" },
  { name: "پرهام نوری", gender: "male" as const, orders: 7, spent: "۳٫۹ میلیون" },
]

type Status = "shipped" | "processing" | "delivered" | "pending"
const STATUS: Record<Status, { text: string; color: "warning" | "info" | "success" | "destructive" }> = {
  shipped: { text: "ارسال شد", color: "warning" },
  processing: { text: "در حال آماده‌سازی", color: "info" },
  delivered: { text: "تحویل شد", color: "success" },
  pending: { text: "در انتظار پرداخت", color: "destructive" },
}
const ORDERS: { id: string; customer: string; date: string; total: number; status: Status }[] = [
  { id: "1098", customer: "آرمیتا رستگار", date: "2026-10-01", total: 2_450_000, status: "shipped" },
  { id: "1097", customer: "کیان مرادی", date: "2026-09-30", total: 1_265_000, status: "processing" },
  { id: "1096", customer: "نیلوفر احمدی", date: "2026-09-29", total: 5_100_000, status: "delivered" },
  { id: "1095", customer: "سارا محمدی", date: "2026-09-28", total: 789_000, status: "pending" },
  { id: "1094", customer: "علی کریمی", date: "2026-09-28", total: 3_320_000, status: "delivered" },
  { id: "1093", customer: "مهسا نوری", date: "2026-09-27", total: 1_980_000, status: "shipped" },
  { id: "1092", customer: "رضا امینی", date: "2026-09-27", total: 640_000, status: "delivered" },
  { id: "1091", customer: "پریسا کاظمی", date: "2026-09-26", total: 4_250_000, status: "processing" },
  { id: "1090", customer: "حامد یزدانی", date: "2026-09-25", total: 2_100_000, status: "delivered" },
  { id: "1089", customer: "الهام صادقی", date: "2026-09-25", total: 990_000, status: "pending" },
  { id: "1088", customer: "بهراد فرهی", date: "2026-09-24", total: 3_760_000, status: "delivered" },
  { id: "1087", customer: "شیما رحیمی", date: "2026-09-23", total: 1_430_000, status: "shipped" },
]
const PAGE_SIZE = 5

const NOTIFICATIONS = [
  { id: 1, title: "سفارش تازه", body: "سفارش ۱۰۹۹ ثبت شد", time: "2026-10-03T10:00:00", icon: <ReceiptIcon />, tone: "bg-info-subtle text-info" },
  { id: 2, title: "موجودی رو به اتمام", body: "فقط ۷ عدد از هدفون پرو مانده", time: "2026-10-03T09:00:00", icon: <ShopIcon />, tone: "bg-warning-subtle text-warning" },
  { id: 3, title: "نظر مثبت", body: "«ارسال سریع و بسته‌بندی عالی»", time: "2026-10-03T08:00:00", icon: <BadgeStarIcon />, tone: "bg-success-subtle text-success" },
]

/* ------------------------------------------------------------------ بلاک */

export default function DashboardBlock() {
  const [period, setPeriod] = React.useState("30")
  const [showRevenue, setShowRevenue] = React.useState(true)
  const [showOrders, setShowOrders] = React.useState(true)
  const [query, setQuery] = React.useState("")
  const [selected, setSelected] = React.useState<string[]>([])
  const [notifications, setNotifications] = React.useState(NOTIFICATIONS)
  const [page, setPage] = React.useState(1)

  const sales = React.useMemo(() => salesFor(period), [period])
  const filtered = ORDERS.filter((order) => toSearchKey(order.customer + order.id).includes(toSearchKey(query)))
  const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
  const currentPage = Math.min(page, pageCount)
  const orders = filtered.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE)
  const allSelected = orders.length > 0 && orders.every((order) => selected.includes(order.id))

  return (
    <ControlSizeProvider size="xl" asChild>
    <SidebarProvider>
      <DashboardSidebar />

      <SidebarInset className="flex min-w-0 flex-col gap-6 bg-muted p-4 sm:p-6">
        <header className="flex flex-wrap items-center gap-3">
          <SidebarTrigger />
          <div className="flex flex-col gap-0.5">
            <h1 className="text-xl font-bold">سلام آرمیتا</h1>
            <p className="text-sm text-muted-foreground">امروز ۱۲ سفارشِ تازه منتظرِ آماده‌سازی است.</p>
          </div>
          <SearchField
            aria-label="جست‌وجو"
            placeholder="جست‌وجوی محصول، مشتری یا سفارش…"
            className="order-last w-full sm:order-none sm:ms-auto sm:w-80 [&_input]:bg-card [&>div]:bg-card"
          />
          <div className="ms-auto flex items-center gap-2 sm:ms-0">
            <Button variant="outline" size="icon" aria-label="اعلان‌ها، ۳ مورد تازه" className="relative bg-card">
              <BellIcon />
              <span aria-hidden className="absolute end-2 top-2 size-2 rounded-full bg-destructive" />
            </Button>
            {/* پروفایل در نوارِ بالا؛ تنظیمات و خروج در منوی همین آواتار. */}
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Button variant="ghost" className="gap-2 px-1.5" aria-label="منوی حساب">
                  <Avatar size="sm" alt="آرمیتا رستگار" src={stockImages.personWoman1.src} icon={<AvatarGenderIcon gender="female" />} className="ring-1 ring-border" />
                  <span className="hidden flex-col items-start leading-snug md:flex">
                    <span className="text-sm font-semibold">آرمیتا رستگار</span>
                    <span className="text-xs text-muted-foreground">مدیر فروشگاه</span>
                  </span>
                  <ChevronDownIcon className="text-muted-foreground" />
                </Button>
              </DropdownMenuTrigger>
              <DropdownMenuContent align="end" className="w-56">
                <DropdownMenuItem>
                  <SettingsIcon />
                  تنظیمات حساب
                </DropdownMenuItem>
                <DropdownMenuSeparator />
                <DropdownMenuItem variant="destructive">
                  <LogoutIcon />
                  خروج
                </DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          </div>
        </header>

        {/* دسترسی سریع: کارهای پرتکرار با یک کلیک، بالای همهٔ گزارش‌ها. */}
        <section aria-label="دسترسی سریع" className="grid grid-cols-2 gap-3 md:grid-cols-4">
          {QUICK.map((item) => (
            <Button
              key={item.title}
              variant="outline"
              className="h-auto justify-start gap-3 rounded-xl bg-card! p-4 text-start shadow-none hover:border-input"
            >
              <span aria-hidden className={`flex size-11 shrink-0 items-center justify-center rounded-xl [&_svg]:size-5 ${item.tone}`}>
                {item.icon}
              </span>
              <span className="flex min-w-0 flex-col gap-0.5">
                <span className="truncate text-sm font-semibold">{item.title}</span>
                <span className="truncate text-xs text-muted-foreground">{item.hint}</span>
              </span>
            </Button>
          ))}
        </section>


        <div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_22rem]">
          <Card>
            {/* کنترل‌ها روبه‌روی عنوان؛ در موبایل زیرِ آن می‌پیچند. */}
            <CardHeader className="flex flex-wrap items-center justify-between gap-3">
              <CardTitle>فروش و سفارش‌ها</CardTitle>
              <div className="flex flex-wrap items-center gap-4">
                <div className="flex items-center gap-2">
                  <Checkbox id="show-revenue" checked={showRevenue} onCheckedChange={(on) => setShowRevenue(on === true)} />
                  <Label htmlFor="show-revenue">فروش</Label>
                </div>
                <div className="flex items-center gap-2">
                  <Checkbox id="show-orders" checked={showOrders} onCheckedChange={(on) => setShowOrders(on === true)} />
                  <Label htmlFor="show-orders">سفارش</Label>
                </div>
                <Select value={period} onValueChange={(next) => setPeriod(next as string)}>
                  <SelectTrigger size="sm" aria-label="بازهٔ زمانی" className="w-28">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="7">۷ روز</SelectItem>
                    <SelectItem value="30">۳۰ روز</SelectItem>
                    <SelectItem value="90">۹۰ روز</SelectItem>
                  </SelectContent>
                </Select>
              </div>
            </CardHeader>
            <CardContent>
              <ComboChart
                className="h-80"
                data={sales.map((row) => ({
                  ...row,
                  revenue: showRevenue ? row.revenue : null,
                  orders: showOrders ? row.orders : null,
                }))}
                categoryKey="label"
                bar={{ key: "revenue", label: "فروش (میلیون تومان)", color: "var(--chart-1)" }}
                barFormatter={(value) => formatNumber(value)}
                line={{ key: "orders", label: "سفارش", color: "var(--chart-4)" }}
              />
            </CardContent>
          </Card>

          <section className="flex flex-col gap-4" aria-label="خلاصه">
            <StatTile>
              <StatTileHeader>
                <StatTileLabel className="text-base text-foreground">قیفِ خرید</StatTileLabel>
                <StatTileAction aria-label="جزئیات قیف خرید" />
              </StatTileHeader>
              <StatTileList
                items={[
                  { label: "بازدید", value: 100, display: "٪۱۰۰" },
                  { label: "دیدنِ محصول", value: 74, display: "٪۷۴" },
                  { label: "افزودن به سبد", value: 62, display: "٪۶۲" },
                  { label: "خریدِ موفق", value: 21, display: "٪۲۱" },
                ]}
              />
            </StatTile>
            <StatTile className="gap-3">
              <StatTileHeader>
                <StatTileLabel className="text-base text-foreground">مشتریانِ تازه</StatTileLabel>
                <Button variant="ghost" className="ms-auto">
                  همه
                </Button>
              </StatTileHeader>
              <ul className="flex flex-col gap-1">
                {CUSTOMERS.map((customer) => (
                  <li key={customer.name} className="flex items-center gap-3 rounded-xl p-2 hover:bg-muted">
                    <Avatar size="sm" alt={customer.name} icon={<AvatarGenderIcon gender={customer.gender} />} className="ring-1 ring-border" />
                    <div className="flex min-w-0 flex-1 flex-col gap-1">
                      <span className="truncate text-sm font-medium">{customer.name}</span>
                      <span className="text-xs text-muted-foreground">
                        {toFaDigits(customer.orders)} سفارش · {customer.spent}
                      </span>
                    </div>
                    <Button variant="ghost" size="icon" aria-label={`پیام به ${customer.name}`}>
                      <EnvelopeIcon />
                    </Button>
                  </li>
                ))}
              </ul>
            </StatTile>
          </section>
        </div>

        <div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_22rem]">
          <Card>
            <CardHeader className="flex flex-wrap items-center justify-between gap-3">
              <CardTitle>سفارش‌ها</CardTitle>
              {/* جست‌وجو کنارِ فیلتر، روبه‌روی عنوان؛ ابزارهای جدول یک‌جا. */}
              <div className="flex w-full items-center gap-2 sm:w-auto">
                <SearchField
                  aria-label="جست‌وجوی سفارش"
                  placeholder="شماره سفارش یا نام مشتری"
                  value={query}
                  onValueChange={(value) => {
                    setQuery(value)
                    setPage(1)
                  }}
                  className="min-w-0 flex-1 sm:w-64"
                />
                <Button variant="outline" size="icon" aria-label="فیلتر سفارش‌ها">
                  <FilterIcon />
                </Button>
              </div>
            </CardHeader>
            <CardContent className="flex flex-col gap-4">
              <div className="overflow-hidden rounded-lg border border-border">
                <Table stackOnMobile className="min-w-[36rem]">
                  <TableHeader className="bg-muted">
                    <TableRow>
                      <TableHead className="w-10">
                        <Checkbox
                          aria-label="انتخاب همه"
                          checked={allSelected}
                          onCheckedChange={(on) => setSelected(on === true ? orders.map((order) => order.id) : [])}
                        />
                      </TableHead>
                      <TableHead>شماره</TableHead>
                      <TableHead>مشتری</TableHead>
                      <TableHead>تاریخ</TableHead>
                      <TableHead className="text-end">مبلغ</TableHead>
                      <TableHead>وضعیت</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {orders.map((order) => (
                      <TableRow key={order.id} data-state={selected.includes(order.id) ? "selected" : undefined}>
                        <TableCell>
                          <Checkbox
                            aria-label={`انتخاب سفارش ${toFaDigits(order.id)}`}
                            checked={selected.includes(order.id)}
                            onCheckedChange={(on) =>
                              setSelected((list) => (on === true ? [...list, order.id] : list.filter((id) => id !== order.id)))
                            }
                          />
                        </TableCell>
                        <TableCell className="font-medium">
                          <bdi dir="ltr">#{toFaDigits(order.id)}</bdi>
                        </TableCell>
                        <TableCell>{order.customer}</TableCell>
                        <TableCell className="text-muted-foreground">{formatJalali(new Date(order.date))}</TableCell>
                        <TableCell className="text-end tabular-nums">{formatCurrency(order.total)}</TableCell>
                        <TableCell>
                          <Badge variant="soft" color={STATUS[order.status].color}>
                            {STATUS[order.status].text}
                          </Badge>
                        </TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                </Table>
              </div>
              <div className="flex flex-wrap items-center justify-between gap-3">
                <span className="text-sm text-muted-foreground">
                  {toFaDigits(filtered.length)} سفارش · صفحهٔ {toFaDigits(currentPage)} از {toFaDigits(pageCount)}
                </span>
                <Pagination className="mx-0 w-auto">
                  <PaginationContent>
                    <PaginationItem>
                      <PaginationPrevious
                        href="#"
                        aria-disabled={currentPage === 1}
                        onClick={(event) => {
                          event.preventDefault()
                          setPage(Math.max(1, currentPage - 1))
                        }}
                      />
                    </PaginationItem>
                    {Array.from({ length: pageCount }, (_, index) => index + 1).map((number) => (
                      <PaginationItem key={number}>
                        <PaginationLink
                          href="#"
                          isActive={number === currentPage}
                          onClick={(event) => {
                            event.preventDefault()
                            setPage(number)
                          }}
                        >
                          {toFaDigits(number)}
                        </PaginationLink>
                      </PaginationItem>
                    ))}
                    <PaginationItem>
                      <PaginationNext
                        href="#"
                        aria-disabled={currentPage === pageCount}
                        onClick={(event) => {
                          event.preventDefault()
                          setPage(Math.min(pageCount, currentPage + 1))
                        }}
                      />
                    </PaginationItem>
                  </PaginationContent>
                </Pagination>
              </div>
            </CardContent>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle>اعلان‌ها</CardTitle>
              <CardAction>
                <Badge variant="soft" color="destructive">
                  {toFaDigits(notifications.length)} تازه
                </Badge>
              </CardAction>
            </CardHeader>
            <CardContent className="flex flex-col gap-3">
              {notifications.length === 0 && (
                <p className="py-6 text-center text-sm text-muted-foreground">اعلانِ تازه‌ای نیست.</p>
              )}
              {notifications.map((item) => (
                <div key={item.id} className="flex items-start gap-3 rounded-lg border border-border bg-card p-3">
                  <span aria-hidden className={`flex size-9 shrink-0 items-center justify-center rounded-full [&_svg]:size-4 ${item.tone}`}>
                    {item.icon}
                  </span>
                  <div className="flex min-w-0 flex-1 flex-col gap-1">
                    <span className="text-sm font-semibold">{item.title}</span>
                    <span className="truncate text-xs text-muted-foreground">{item.body}</span>
                    <span className="text-xs text-muted-foreground">{formatRelativeTime(item.time)}</span>
                  </div>
                  <Button
                    variant="ghost"
                    size="icon"
                    aria-label={`بستن اعلان ${item.title}`}
                    onClick={() => setNotifications((list) => list.filter((entry) => entry.id !== item.id))}
                  >
                    <XIcon />
                  </Button>
                </div>
              ))}
              <Button variant="outline" fullWidth>
                مشاهدهٔ همه
              </Button>
            </CardContent>
          </Card>
        </div>
      </SidebarInset>
    </SidebarProvider>
    </ControlSizeProvider>
  )
}

/* ------------------------------------------------------------------ اجزا */

function DashboardSidebar() {
  return (
    <Sidebar collapsible="icon">
      <SidebarHeader className="gap-3 p-3 group-data-[collapsible=icon]:p-2">
        <SidebarBrand logo={<DigLogo />} title="فروشگاه دیگ" subtitle="پنل مدیریت" />
        <SidebarSearch placeholder="جست‌وجو در منو…" aria-label="جست‌وجو در منو" />
      </SidebarHeader>
      <SidebarContent className="gap-0 pt-4">
        <SidebarGroup>
          <SidebarGroupContent>
            <SidebarMenu>
              <NavItem icon={<DashboardIcon />} active>
                پیشخوان
              </NavItem>
              <NavItem icon={<ReceiptIcon />} badge={toFaDigits(12)}>
                سفارش‌ها
              </NavItem>
              <NavItem icon={<ShopIcon />}>محصولات</NavItem>
              <NavItem icon={<UsersIcon />}>مشتریان</NavItem>
              {/* زیرمنو جمع‌شونده و پیش‌فرض بسته؛ همیشه‌باز منو را شلوغ می‌کرد. */}
              <SidebarMenuItem>
                <Collapsible>
                  <CollapsibleTrigger asChild>
                    <SidebarMenuButton className="group/collapsible">
                      <BarChartIcon />
                      <span>تحلیل‌ها</span>
                      <Badge variant="soft" color="violet" size="sm" className="ms-auto">
                        ویژه
                      </Badge>
                      <ChevronDownIcon className="size-4! transition-transform group-data-[state=open]/collapsible:rotate-180" />
                    </SidebarMenuButton>
                  </CollapsibleTrigger>
                  <CollapsibleContent>
                    <SidebarMenuSub>
                      {["گزارش فروش", "عملکرد محصولات", "رفتار مشتریان"].map((title) => (
                        <SidebarMenuSubItem key={title}>
                          <SidebarMenuSubButton href="#">{title}</SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                      ))}
                    </SidebarMenuSub>
                  </CollapsibleContent>
                </Collapsible>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarGroupContent>
        </SidebarGroup>
        <SidebarGroup>
          <SidebarGroupContent>
            <SidebarMenu>
              <NavItem icon={<BellIcon />} badge={toFaDigits(6)} badgeTone="destructive">
                هشدار موجودی
              </NavItem>
              <NavItem icon={<ShieldCheckIcon />}>نقش‌ها و دسترسی‌ها</NavItem>
              <NavItem icon={<TicketIcon />}>پشتیبانی</NavItem>
              <NavItem icon={<GuideIcon />}>راهنما</NavItem>
            </SidebarMenu>
          </SidebarGroupContent>
        </SidebarGroup>
      </SidebarContent>
      <SidebarFooter className="gap-4 px-3 pt-3 pb-9 group-data-[collapsible=icon]:p-2">
        <ThemeSwitch />
        <p className="text-center text-xs text-placeholder group-data-[collapsible=icon]:hidden">نسخهٔ ۲٫۴٫۰ · به‌روزرسانی ۱۲ مهر</p>
      </SidebarFooter>
    </Sidebar>
  )
}

/**
 * سوییچِ روشن/تیره در پایینِ نوار. کلاسِ dark روی <html> را عوض می‌کند؛ در پروژه‌ای
 * که next-themes دارد، به‌جایش setTheme را صدا بزنید.
 */
function ThemeSwitch() {
  return (
    <>
      <ThemeSegmented />
      {/* حالتِ جمع: یک دکمهٔ آیکونی که بینِ دو پوسته جابه‌جا می‌کند. */}
      <ThemeIconToggle />
    </>
  )
}

function useHtmlTheme() {
  return React.useSyncExternalStore(
    (notify) => {
      const observer = new MutationObserver(notify)
      observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] })
      return () => observer.disconnect()
    },
    () => (document.documentElement.classList.contains("dark") ? "dark" : "light"),
    () => "light"
  )
}

function applyTheme(next: "light" | "dark") {
  document.documentElement.classList.toggle("dark", next === "dark")
  document.documentElement.classList.toggle("light", next === "light")
}

function ThemeIconToggle() {
  const theme = useHtmlTheme()
  return (
    <SidebarMenu className="hidden group-data-[collapsible=icon]:flex">
      <SidebarMenuItem>
        <SidebarMenuButton
          tooltip={theme === "dark" ? "پوستهٔ روشن" : "پوستهٔ تیره"}
          onClick={() => applyTheme(theme === "dark" ? "light" : "dark")}
        >
          {theme === "dark" ? <SunIcon /> : <MoonIcon />}
          <span>{theme === "dark" ? "پوستهٔ روشن" : "پوستهٔ تیره"}</span>
        </SidebarMenuButton>
      </SidebarMenuItem>
    </SidebarMenu>
  )
}

function ThemeSegmented() {
  const theme = useHtmlTheme()
  return (
    <ToggleGroup
      type="single"
      variant="segmented"
      fullWidth
      value={theme}
      onValueChange={(value) => {
        if (!value) return
        applyTheme(value as "light" | "dark")
      }}
      aria-label="پوستهٔ رنگی"
      className="group-data-[collapsible=icon]:hidden"
    >
      <ToggleGroupItem value="light" aria-label="پوستهٔ روشن">
        <SunIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="dark" aria-label="پوستهٔ تیره">
        <MoonIcon />
      </ToggleGroupItem>
    </ToggleGroup>
  )
}

function NavItem({
  icon,
  active = false,
  badge,
  badgeTone,
  children,
}: {
  icon: React.ReactNode
  active?: boolean
  badge?: string
  badgeTone?: "destructive"
  children: React.ReactNode
}) {
  return (
    <SidebarMenuItem>
      <SidebarMenuButton asChild isActive={active} tooltip={typeof children === "string" ? children : undefined}>
        <a href="#">
          {icon}
          <span>{children}</span>
        </a>
      </SidebarMenuButton>
      {badge && (
        <SidebarMenuBadge tone={badgeTone === "destructive" ? "destructive" : "default"}>
          {badge}
        </SidebarMenuBadge>
      )}
    </SidebarMenuItem>
  )
}

/** نمودارِ خطی کوچک برای کارتِ آمار؛ SVGِ سبک به‌جای یک نمونهٔ کاملِ نمودار. */