{
  "id": "saas-dashboard",
  "name": "SaaS dashboard",
  "description": "A complete application dashboard with host-owned metrics, revenue charts, searchable customers, filters, pagination, and service callbacks.",
  "category": "Application",
  "blockIds": [
    "dashboard-01"
  ],
  "style": "clean-saas",
  "registryId": "recipe-saas-dashboard",
  "componentName": "SaasDashboardRecipe",
  "componentSource": "content/recipe-components/saas-dashboard.tsx",
  "installUsage": "import { useState, type ComponentProps } from \"react\";\nimport SaasDashboardRecipe from \"@/components/uiblocks/recipe-saas-dashboard/recipe-saas-dashboard\";\n\ntype CustomerQuery = NonNullable<ComponentProps<typeof SaasDashboardRecipe>[\"customerQuery\"]>;\n\nexport default function DashboardPage() {\n  const [customerQuery, setCustomerQuery] = useState<CustomerQuery>({\n    search: \"\", filters: { Plan: [], Status: [] }, page: 1, pageSize: 9,\n  });\n  return (\n    <SaasDashboardRecipe\n      mode=\"controlled\"\n      brand=\"Your product\"\n      title=\"Overview\"\n      metrics={[]}\n      chartData={[]}\n      customers={[]}\n      customerQuery={customerQuery}\n      onCustomerQueryChange={setCustomerQuery}\n    />\n  );\n}",
  "demoUsage": "import SaasDashboardRecipe from \"@/components/uiblocks/recipe-saas-dashboard/recipe-saas-dashboard\";\n\nexport default function DashboardDemo() {\n  return <SaasDashboardRecipe mode=\"demo\" brand=\"Lumen Labs\" />;\n}",
  "propsContract": [
    {
      "name": "mode, metrics, chartData, customers",
      "description": "Default controlled mode contains no sample metrics, chart points, or customers. Supply real data; explicit mode='demo' opts in to labeled illustrative records. An explicit empty array stays empty in either mode. chartData points use date, revenue, charges, and optional priorRevenue/priorCharges; no hourly data or prior values are invented for host data."
    },
    {
      "name": "customerQuery, onCustomerQueryChange, pagination, totalCustomers, customerFilterOptions",
      "description": "Query is {search, filters:{Plan:string[], Status:string[]}, page, pageSize}. Client mode filters and paginates supplied rows locally. Server mode renders supplied current-page rows unchanged and uses totalCustomers for pagination; the host must update rows, total, and query from its service. Search/filter changes reset page to 1. Supply customerFilterOptions={Plan:string[], Status:string[]} for service-wide values across all remote pages; supplied values, current-row values, and selected values remain available. Controlled fallback uses only actual rows and selected values, never demo choices. Never pass the full dataset as a remote page or filter it twice."
    },
    {
      "name": "period, dateRange, onPeriodChange, onDateRangeChange",
      "description": "Control day/week/month/year and {from,to} ISO date strings. Callbacks let the host fetch actual aggregations; chartData is already aggregated and is not synthesized or filtered again. Controls are disabled in controlled mode without their callback. Dates start blank until the host supplies a range."
    },
    {
      "name": "loading, error, onRetry, onExport, onCustomerAction",
      "description": "Provide real loading and error state. Retry/refresh delegates to onRetry; export receives the complete CustomerQuery; customer actions receive 'view' or 'contact' and the unchanged row. Handlers can return promises; rejected actions display their actual error and never report success. Business actions without a host handler are disabled in controlled mode. No automatic polling, export, mail, or network request is performed."
    },
    {
      "name": "brand, title, description, navigation, headerSlot, theme, className",
      "description": "Set real identity, copy, ordinary host navigation URLs, and account controls. The sidebar and layout retain the dashboard block's neutral surfaces, blue chart accent, spacing, and responsive customer table. Inherit the host theme or use a class-based dark variant."
    }
  ],
  "contentGuidelines": [
    "Use real reporting periods and shared host state for metrics, charts, customer query, and pagination. Never present the explicit demo records as production analytics.",
    "Supply loading, empty, error, and populated states. Status text must remain understandable without color; keep long customer names and email addresses readable on mobile.",
    "In server pagination mode, pass only the current page from the service and totalCustomers. Search and filter callbacks must reset the host page to 1 and fetch the new rows.",
    "Connect retry, export, and customer-action callbacks to your authenticated service or host router. Unconnected business controls remain disabled; the library includes no backend or billing integration.",
    "Keep the existing dashboard hierarchy, typography, neutral surfaces, and blue chart accent. Verify the installed page with long Chinese headings, sparse chart data, and mobile widths."
  ],
  "prompt": "Build a SaaS admin dashboard using UIBlocks saas-dashboard. Read get_recipe and get_preview, install recipe-saas-dashboard in one command, and import SaasDashboardRecipe. Keep the existing dashboard layout, type scale, neutral surfaces, and blue chart accent. Default to controlled mode with host-owned metrics, chartData, customers, period/dateRange, loading, error, and CustomerQuery. Connect onRetry, onExport, onCustomerAction, and query/date/period callbacks to the application's authenticated service and router. For server pagination, supply the current rows and totalCustomers without local filtering or slicing. Explicit mode='demo' is for labeled interface inspection only. Check empty/loading/error states, callback rejection, long Chinese copy, and mobile layout; preserve the host routes and aliases.",
  "preview": "/preview/recipes/saas-dashboard",
  "screenshot": "/previews/recipe-saas-dashboard.png",
  "screenshotMetadata": {
    "renderer": "Chromium",
    "id": "recipe-saas-dashboard",
    "image": "recipe-saas-dashboard.png",
    "sourceFiles": [
      "app/globals.css",
      "components/brand.tsx",
      "components/copy-button.tsx",
      "components/footer.tsx",
      "components/header.tsx",
      "components/recipes/recipe-preview.tsx",
      "components/registry-setup.tsx",
      "components/seo-jsonld.tsx",
      "components/theme-toggle.tsx",
      "components/ui/button.tsx",
      "components/ui/chart.tsx",
      "components/ui/dialog.tsx",
      "components/ui/dropdown-menu.tsx",
      "components/ui/input-group.tsx",
      "components/ui/input.tsx",
      "components/ui/separator.tsx",
      "components/ui/sheet.tsx",
      "components/ui/sidebar.tsx",
      "components/ui/skeleton.tsx",
      "components/ui/sonner.tsx",
      "components/ui/table.tsx",
      "components/ui/textarea.tsx",
      "components/ui/tooltip.tsx",
      "config.ts",
      "content/blocks-metadata.ts",
      "content/catalog.ts",
      "content/community-blocks.ts",
      "content/components/dashboard/dashboard-01/customers-table.tsx",
      "content/components/dashboard/dashboard-01/dashboard-charts.tsx",
      "content/components/marketing/faq-01.tsx",
      "content/components/marketing/features-01.tsx",
      "content/components/marketing/footer-01.tsx",
      "content/components/marketing/hero-01.tsx",
      "content/components/marketing/logo-cloud-01.tsx",
      "content/components/marketing/navigation-01.tsx",
      "content/components/marketing/pricing-01.tsx",
      "content/components/workspace/results-gallery-01.tsx",
      "content/components/workspace/task-queue-01.tsx",
      "content/components/workspace/upload-workflow-01.tsx",
      "content/components/workspace/usage-panel-01.tsx",
      "content/components/workspace/workspace-header-01.tsx",
      "content/declarations.ts",
      "content/recipe-components/saas-dashboard.tsx",
      "content/recipe-components/tool-landing.tsx",
      "content/recipe-components/tool-workspace.tsx",
      "content/recipes.ts",
      "hooks/use-mobile.ts",
      "hooks/use-registry-origin.ts",
      "lib/google-analytics.ts",
      "lib/utils.ts",
      "package-lock.json",
      "package.json",
      "postcss.config.mjs",
      "public/icon.svg",
      "public/opengraph-image.png",
      "scripts/capture-previews.mjs",
      "scripts/preview-provenance.mjs",
      "src/lib/head.ts",
      "src/routes/__root.tsx",
      "src/routes/preview.recipes.$recipeId.tsx",
      "vite.config.ts"
    ],
    "sourceHash": "becdcaf827a8e6c909890265aa18089c66c007f0b55a2debc1761e16edb45a05",
    "imageHash": "26a5fdca11f25d2ce59d8f5e0c7bec981b543f9d9e51ec55bb4729c7b64bf34f",
    "capturedAt": "2026-10-30T08:44:26.962Z",
    "viewport": {
      "width": 1200,
      "height": 800
    },
    "dimensions": {
      "width": 1200,
      "height": 1330
    },
    "state": "default"
  },
  "url": "/recipes/saas-dashboard",
  "registry": "/r/recipe-saas-dashboard.json",
  "install": {
    "direct": "npx shadcn@latest add https://uiblocks.org/r/recipe-saas-dashboard.json",
    "namespaced": "npx shadcn@latest add @uiblocks/recipe-saas-dashboard",
    "registryConfig": {
      "registries": {
        "@uiblocks": "https://uiblocks.org/r/{name}.json"
      }
    },
    "entry": "@components/uiblocks/recipe-saas-dashboard/recipe-saas-dashboard.tsx",
    "exportName": "default",
    "dependencies": [
      "@base-ui/react@1.9.0",
      "class-variance-authority@0.7.1",
      "cn@0.2.6",
      "date-fns@4.4.0",
      "lucide-react@0.577.0",
      "recharts@3.8.0"
    ],
    "requirements": {
      "react": ">=19",
      "tailwind": ">=4"
    },
    "setup": [
      {
        "id": "project",
        "title": "Open your existing React project",
        "description": "Use React 19+, TypeScript/TSX, and a working Tailwind CSS 4 build. Keep the project's existing router, dependencies, and global stylesheet."
      },
      {
        "id": "shadcn",
        "title": "Check components.json before installing",
        "description": "Reuse an existing components.json. Its aliases.components must resolve in TypeScript and the bundler, and tailwind.css must point to the stylesheet your app loads. Do not reinitialize an already configured project.",
        "command": "npx shadcn@latest init",
        "when": "Only if components.json is missing; run inside the existing app and follow its framework setup."
      },
      {
        "id": "styles",
        "title": "Include the installed sources in Tailwind",
        "description": "Keep the host's standard shadcn theme variables, Tailwind @theme mappings, and base border styles loaded (background, foreground, border, sidebar, and chart tokens). Ensure Tailwind scans the actual installed components directory. Restricted source(none), ignored paths, and monorepos may need an @source path relative to the stylesheet. For theme='dark', use a class-based dark variant in the host CSS."
      },
      {
        "id": "import",
        "title": "Import through your configured components alias",
        "description": "The @components/ entry is a registry target placeholder, not a JavaScript import. Replace the example's @/components prefix with components.json aliases.components, verify the file printed by the CLI, and mount it in an existing host route."
      }
    ],
    "usage": "import { useState, type ComponentProps } from \"react\";\nimport SaasDashboardRecipe from \"@/components/uiblocks/recipe-saas-dashboard/recipe-saas-dashboard\";\n\ntype CustomerQuery = NonNullable<ComponentProps<typeof SaasDashboardRecipe>[\"customerQuery\"]>;\n\nexport default function DashboardPage() {\n  const [customerQuery, setCustomerQuery] = useState<CustomerQuery>({\n    search: \"\", filters: { Plan: [], Status: [] }, page: 1, pageSize: 9,\n  });\n  return (\n    <SaasDashboardRecipe\n      mode=\"controlled\"\n      brand=\"Your product\"\n      title=\"Overview\"\n      metrics={[]}\n      chartData={[]}\n      customers={[]}\n      customerQuery={customerQuery}\n      onCustomerQueryChange={setCustomerQuery}\n    />\n  );\n}",
    "demoUsage": "import SaasDashboardRecipe from \"@/components/uiblocks/recipe-saas-dashboard/recipe-saas-dashboard\";\n\nexport default function DashboardDemo() {\n  return <SaasDashboardRecipe mode=\"demo\" brand=\"Lumen Labs\" />;\n}"
  },
  "compositionSource": {
    "language": "tsx",
    "componentName": "SaasDashboardRecipe",
    "referenceOnly": false,
    "installedEntry": "@components/uiblocks/recipe-saas-dashboard/recipe-saas-dashboard.tsx",
    "content": "\"use client\";\n\nimport {\n  LayoutDashboard,\n  RefreshCw,\n  Users,\n  ChartNoAxesCombined,\n} from \"lucide-react\";\nimport { useState, type ReactNode } from \"react\";\nimport {\n  CustomersTable,\n  demoCustomers,\n  type Customer,\n  type CustomerFilters,\n  type CustomerQuery,\n} from \"./shared/content/components/dashboard/dashboard-01/customers-table\";\nimport {\n  DashboardCharts,\n  type DashboardChartPoint,\n} from \"./shared/content/components/dashboard/dashboard-01/dashboard-charts\";\nimport { Button } from \"./shared/components/ui/button\";\nimport { Input } from \"./shared/components/ui/input\";\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarGroup,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarInset,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarProvider,\n  SidebarTrigger,\n  useSidebar,\n} from \"./shared/components/ui/sidebar\";\nimport { cn } from \"./shared/lib/utils\";\n\nexport type DashboardPeriod = \"day\" | \"week\" | \"month\" | \"year\";\nexport type DashboardDateRange = { from: string; to: string };\nexport type DashboardMetric = {\n  label: string;\n  value: string | number;\n  change?: string;\n  tone?: \"positive\" | \"negative\" | \"neutral\";\n};\nexport type SaasDashboardRecipeProps = {\n  /** Only explicit demo mode opts in to illustrative metrics, chart points and customers. */\n  mode?: \"controlled\" | \"demo\";\n  brand?: string;\n  title?: string;\n  description?: string;\n  navigation?: readonly { label: string; href: string }[];\n  headerSlot?: ReactNode;\n  metrics?: readonly DashboardMetric[];\n  chartData?: readonly DashboardChartPoint[];\n  customers?: readonly Customer[];\n  /** Full host filter values, independent of the current server page. Selected values stay available. */\n  customerFilterOptions?: Partial<CustomerFilters>;\n  period?: DashboardPeriod;\n  dateRange?: DashboardDateRange;\n  onPeriodChange?: (period: DashboardPeriod) => void | Promise<void>;\n  onDateRangeChange?: (range: DashboardDateRange) => void | Promise<void>;\n  customerQuery?: CustomerQuery;\n  onCustomerQueryChange?: (query: CustomerQuery) => void | Promise<void>;\n  /** In server mode customers must contain the already filtered current page. */\n  pagination?: \"client\" | \"server\";\n  totalCustomers?: number;\n  loading?: boolean;\n  error?: string;\n  onRetry?: () => void | Promise<void>;\n  onExport?: (query: CustomerQuery) => void | Promise<void>;\n  onCustomerAction?: (\n    action: \"view\" | \"contact\",\n    customer: Customer\n  ) => void | Promise<void>;\n  theme?: \"inherit\" | \"dark\";\n  className?: string;\n};\n\nconst emptyQuery: CustomerQuery = {\n  search: \"\",\n  filters: { Plan: [], Status: [] },\n  page: 1,\n  pageSize: 9,\n};\nconst demoRange = { from: \"2025-11-10\", to: \"2025-11-16\" };\nconst demoPeriodStarts: Record<DashboardPeriod, string> = {\n  day: \"2025-11-16\",\n  week: \"2025-11-10\",\n  month: \"2025-10-18\",\n  year: \"2024-11-17\",\n};\nconst demoMetrics: readonly DashboardMetric[] = [\n  { label: \"New customers\", value: \"1,284\", change: \"+8.2%\", tone: \"positive\" },\n  { label: \"Success rate\", value: \"97.4%\", change: \"+0.6%\", tone: \"positive\" },\n  { label: \"Failure rate\", value: \"2.6%\", change: \"-0.6%\", tone: \"negative\" },\n  { label: \"Refunds\", value: \"38\", change: \"0.0%\", tone: \"neutral\" },\n];\nconst defaultNavigation = [\n  { label: \"Overview\", href: \"#dashboard-overview\" },\n  { label: \"Customers\", href: \"#customers\" },\n  { label: \"Reports\", href: \"#revenue\" },\n];\n\n/** A host-owned dashboard using the same charts and table as the dashboard block. */\nexport default function SaasDashboardRecipe({\n  mode = \"controlled\",\n  brand = \"Your workspace\",\n  title = \"Overview\",\n  description,\n  navigation = defaultNavigation,\n  headerSlot,\n  metrics,\n  chartData,\n  customers,\n  customerFilterOptions,\n  period,\n  dateRange,\n  onPeriodChange,\n  onDateRangeChange,\n  customerQuery,\n  onCustomerQueryChange,\n  pagination = \"client\",\n  totalCustomers,\n  loading = false,\n  error,\n  onRetry,\n  onExport,\n  onCustomerAction,\n  theme = \"inherit\",\n  className,\n}: SaasDashboardRecipeProps) {\n  const demo = mode === \"demo\";\n  const [localPeriod, setLocalPeriod] = useState<DashboardPeriod>(\"week\");\n  const [localRange, setLocalRange] = useState<DashboardDateRange>(\n    demo ? demoRange : { from: \"\", to: \"\" }\n  );\n  const [localQuery, setLocalQuery] = useState<CustomerQuery>(emptyQuery);\n  const [callbackError, setCallbackError] = useState(\"\");\n  const [retrying, setRetrying] = useState(false);\n  const currentPeriod = period ?? localPeriod;\n  const currentRange = dateRange ?? localRange;\n  const currentQuery = customerQuery ?? localQuery;\n  const visibleMetrics = metrics ?? (demo ? demoMetrics : []);\n  const rows = customers ?? (demo ? demoCustomers : []);\n  const displayedError = callbackError || error;\n  const invoke = async (action: () => void | Promise<void>) => {\n    setCallbackError(\"\");\n    try {\n      await action();\n    } catch (failure) {\n      setCallbackError(\n        failure instanceof Error\n          ? failure.message\n          : \"The dashboard action could not be completed.\"\n      );\n    }\n  };\n  const changeRange = (next: DashboardDateRange) => {\n    if (!dateRange) setLocalRange(next);\n    if (onDateRangeChange) void invoke(() => onDateRangeChange(next));\n  };\n  const changeQuery = (next: CustomerQuery) => {\n    if (!customerQuery) setLocalQuery(next);\n    if (onCustomerQueryChange) void invoke(() => onCustomerQueryChange(next));\n  };\n  const retry = async () => {\n    if (!onRetry) return;\n    setRetrying(true);\n    await invoke(onRetry);\n    setRetrying(false);\n  };\n  return (\n    <div\n      className={cn(\n        theme === \"dark\" && \"dark\",\n        \"relative isolate [contain:layout_style]\",\n        className\n      )}\n      data-dashboard-mode={mode}\n    >\n      <SidebarProvider\n        defaultOpen={false}\n        className=\"min-h-dvh bg-background font-sans text-foreground antialiased [--chart-1:#3b82f6] [--chart-2:#d4d4d8] [--sidebar-width-icon:3.5rem]! [--sidebar-width:15.5rem]! dark:[--chart-2:#3f3f46]\"\n      >\n        <a\n          href=\"#dashboard-overview\"\n          className=\"sr-only focus:not-sr-only focus:absolute focus:left-2 focus:top-2 focus:z-50 focus:rounded-lg focus:bg-background focus:p-3\"\n        >\n          Skip to dashboard\n        </a>\n        <DashboardNavigation brand={brand} navigation={navigation} />\n        <SidebarInset className=\"min-w-0\" id=\"dashboard-overview\">\n          <header className=\"flex min-h-15 flex-wrap items-center justify-between gap-3 border-b px-4 py-3 lg:px-7\">\n            <div className=\"flex min-w-0 max-w-full items-start gap-3\">\n              <SidebarTrigger className=\"mt-0.5 shrink-0\" />\n              <div className=\"min-w-0\">\n                <h1 className=\"break-words text-lg font-semibold tracking-tight\">\n                  {title}\n                </h1>\n                {description && (\n                  <p className=\"mt-1 text-sm text-muted-foreground [overflow-wrap:anywhere]\">\n                    {description}\n                  </p>\n                )}\n              </div>\n            </div>\n            <div className=\"flex max-w-full flex-wrap items-center gap-2\">\n              <div\n                className=\"flex flex-wrap rounded-[9px] border bg-muted/50 p-0.5\"\n                role=\"group\"\n                aria-label=\"Chart period\"\n              >\n                {([\"day\", \"week\", \"month\", \"year\"] as const).map((value) => (\n                  <button\n                    key={value}\n                    type=\"button\"\n                    aria-pressed={currentPeriod === value}\n                    disabled={!demo && !onPeriodChange}\n                    className={cn(\n                      \"min-h-8 rounded-md px-3 text-xs capitalize disabled:cursor-not-allowed disabled:opacity-50\",\n                      currentPeriod === value &&\n                        \"bg-background font-medium shadow-xs\"\n                    )}\n                    onClick={() => {\n                      if (!period) setLocalPeriod(value);\n                      if (demo && !dateRange)\n                        setLocalRange({\n                          from: demoPeriodStarts[value],\n                          to: demoRange.to,\n                        });\n                      if (onPeriodChange)\n                        void invoke(() => onPeriodChange(value));\n                    }}\n                  >\n                    {value}\n                  </button>\n                ))}\n              </div>\n              <label className=\"min-w-0 text-xs text-muted-foreground\">\n                From\n                <Input\n                  aria-label=\"Dashboard start date\"\n                  type=\"date\"\n                  className=\"mt-1 h-8 w-36 max-w-full text-xs\"\n                  disabled={!demo && !onDateRangeChange}\n                  value={currentRange.from}\n                  max={currentRange.to || undefined}\n                  onChange={(event) =>\n                    changeRange({ ...currentRange, from: event.target.value })\n                  }\n                />\n              </label>\n              <label className=\"min-w-0 text-xs text-muted-foreground\">\n                To\n                <Input\n                  aria-label=\"Dashboard end date\"\n                  type=\"date\"\n                  className=\"mt-1 h-8 w-36 max-w-full text-xs\"\n                  disabled={!demo && !onDateRangeChange}\n                  value={currentRange.to}\n                  min={currentRange.from || undefined}\n                  onChange={(event) =>\n                    changeRange({ ...currentRange, to: event.target.value })\n                  }\n                />\n              </label>\n              <Button\n                size=\"sm\"\n                variant=\"outline\"\n                disabled={loading || retrying || !onRetry}\n                onClick={() => {\n                  void retry();\n                }}\n              >\n                <RefreshCw aria-hidden=\"true\" />\n                Refresh\n              </Button>\n              {headerSlot}\n            </div>\n          </header>\n          {demo && (\n            <p className=\"border-b bg-blue-50 px-4 py-2 text-xs text-blue-800 dark:bg-blue-950/30 dark:text-blue-200\">\n              Demo data · Illustrative metrics and customers. No service or\n              billing connection.\n            </p>\n          )}\n          {displayedError && (\n            <div\n              role=\"alert\"\n              className=\"flex flex-wrap items-center justify-between gap-3 border-b bg-red-50 px-4 py-3 text-sm text-red-800 dark:bg-red-950/30 dark:text-red-200\"\n            >\n              <span className=\"min-w-0 [overflow-wrap:anywhere]\">\n                {displayedError}\n              </span>\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                disabled={!onRetry || loading || retrying}\n                onClick={() => {\n                  void retry();\n                }}\n              >\n                Retry\n              </Button>\n            </div>\n          )}\n          {loading && (\n            <p\n              role=\"status\"\n              className=\"border-b px-4 py-3 text-sm text-muted-foreground\"\n            >\n              Loading dashboard data…\n            </p>\n          )}\n          <div className=\"@container/metrics border-b\" aria-busy={loading}>\n            {visibleMetrics.length ? (\n              <dl className=\"grid grid-cols-2 @3xl/metrics:grid-cols-4\">\n                {visibleMetrics.map((metric) => (\n                  <div\n                    key={metric.label}\n                    className=\"flex min-w-0 flex-col justify-center gap-2 border-b px-4 py-6 even:border-l @3xl/metrics:border-b-0 @3xl/metrics:not-first:border-l @3xl/metrics:px-7 [&:nth-last-child(-n+2)]:border-b-0\"\n                  >\n                    <dt className=\"break-words text-sm text-muted-foreground\">\n                      {metric.label}\n                    </dt>\n                    <dd className=\"flex flex-wrap items-baseline gap-2.5\">\n                      <span className=\"break-all text-[1.75rem] font-semibold tabular-nums tracking-tight\">\n                        {metric.value}\n                      </span>\n                      {metric.change && (\n                        <span\n                          className={cn(\n                            \"text-[13px] font-medium tabular-nums\",\n                            metric.tone === \"positive\"\n                              ? \"text-green-600 dark:text-green-400\"\n                              : metric.tone === \"negative\"\n                                ? \"text-red-600 dark:text-red-400\"\n                                : \"text-muted-foreground\"\n                          )}\n                        >\n                          {metric.change}\n                        </span>\n                      )}\n                    </dd>\n                  </div>\n                ))}\n              </dl>\n            ) : (\n              <p className=\"px-4 py-8 text-sm text-muted-foreground\">\n                No metrics supplied. Connect your dashboard data.\n              </p>\n            )}\n          </div>\n          <DashboardCharts\n            period={currentPeriod}\n            from={currentRange.from}\n            to={currentRange.to}\n            data={chartData ?? (demo ? undefined : [])}\n          />\n          <CustomersTable\n            customers={rows}\n            filterOptions={customerFilterOptions}\n            queryState={currentQuery}\n            onQueryChange={\n              onCustomerQueryChange ||\n              (!customerQuery && pagination === \"client\")\n                ? changeQuery\n                : undefined\n            }\n            pagination={pagination}\n            total={totalCustomers}\n            loading={loading}\n            requireActionHandlers={!demo}\n            onExport={onExport}\n            onCustomerAction={onCustomerAction}\n          />\n        </SidebarInset>\n      </SidebarProvider>\n    </div>\n  );\n}\n\nfunction DashboardNavigation({\n  brand,\n  navigation,\n}: {\n  brand: string;\n  navigation: NonNullable<SaasDashboardRecipeProps[\"navigation\"]>;\n}) {\n  const { setOpenMobile } = useSidebar();\n  return (\n    <Sidebar collapsible=\"icon\">\n      <SidebarHeader className=\"px-3 pb-5 pt-3.5\">\n        <SidebarMenu>\n          <SidebarMenuItem>\n            <SidebarMenuButton\n              render={\n                <a\n                  href=\"#dashboard-overview\"\n                  onClick={() => setOpenMobile(false)}\n                />\n              }\n              aria-label={brand}\n              className=\"h-9 gap-2 rounded-lg px-2 font-medium\"\n            >\n              <span className=\"flex size-5 shrink-0 items-center justify-center rounded-[5px] bg-blue-500 text-[11px] font-semibold text-white\">\n                {brand.slice(0, 1)}\n              </span>\n              <span className=\"truncate group-data-[collapsible=icon]:hidden\">\n                {brand}\n              </span>\n            </SidebarMenuButton>\n          </SidebarMenuItem>\n        </SidebarMenu>\n      </SidebarHeader>\n      <SidebarContent>\n        <SidebarGroup className=\"px-3 py-0\">\n          <SidebarGroupLabel className=\"px-2.5 text-[11px] tracking-wider\">\n            WORKSPACE\n          </SidebarGroupLabel>\n          <SidebarMenu className=\"gap-0.5\">\n            {navigation.map((item, index) => {\n              const Icon =\n                index === 0\n                  ? LayoutDashboard\n                  : index === 1\n                    ? Users\n                    : ChartNoAxesCombined;\n              return (\n                <SidebarMenuItem key={`${item.href}-${item.label}`}>\n                  <SidebarMenuButton\n                    className=\"h-9 gap-2.5 rounded-lg px-2.5\"\n                    isActive={index === 0}\n                    render={\n                      <a\n                        href={item.href}\n                        aria-label={item.label}\n                        onClick={() => setOpenMobile(false)}\n                      />\n                    }\n                  >\n                    <Icon aria-hidden=\"true\" />\n                    <span className=\"truncate group-data-[collapsible=icon]:hidden\">\n                      {item.label}\n                    </span>\n                  </SidebarMenuButton>\n                </SidebarMenuItem>\n              );\n            })}\n          </SidebarMenu>\n        </SidebarGroup>\n      </SidebarContent>\n    </Sidebar>\n  );\n}\n",
    "guidance": "Install this complete recipe with its single shadcn command, then import the component using your configured components alias and meta.entry. Matching blocks, helpers, and MIT notices are bundled. Pass your own copy, navigation, data, and callbacks through the published props. Recipes with meta.demoUsage require explicit demo mode for sample data or simulation; controlled mode delegates service work to your application."
  }
}
