# Dashboard Overview

> A product dashboard in three decisions: one number first, one large chart first, or the work list first with the numbers as one line.

A shadcn/ui block from the [@miseui](https://miseui.junsu4621.workers.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/miseui/dashboard-overview).

- Page, with a live preview: https://shadcn.exchange/r/miseui/dashboard-overview
- Screenshot: https://shadcn.exchange/shot/miseui/dashboard-overview/light.png
- Registry item: https://miseui.junsu4621.workers.dev/r/dashboard-overview.json
- Categories: blocks
- Listed: 2026-11-05, source last changed 2026-11-05

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/miseui/dashboard-overview.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `motion`
- Registry items: `utils`, `https://miseui.junsu4621.workers.dev/r/motion.json`, `https://miseui.junsu4621.workers.dev/r/button.json`, `https://miseui.junsu4621.workers.dev/r/chart.json`, `https://miseui.junsu4621.workers.dev/r/data-table.json`, `https://miseui.junsu4621.workers.dev/r/skeleton.json`, `https://miseui.junsu4621.workers.dev/r/status.json`, `https://miseui.junsu4621.workers.dev/r/tabs.json`

## Files

### `registry/miseui/blocks/dashboard-overview.tsx`

```tsx
import type * as React from "react"
import { ChartLine, LayoutDashboard, ReceiptText, Settings, Users } from "lucide-react"
import { cn } from "@/registry/miseui/lib/utils"
import { Tabs, TabsContent } from "@/registry/miseui/ui/tabs"
import { AnalyticsBody, AnalyticsSkeleton } from "./dashboard-overview-analytics"
import { DashboardContent, OverviewBody } from "./dashboard-overview-body"
import {
  DashboardRail,
  type DashboardActivity,
  type DashboardAttention,
  type DashboardMetric,
  type DashboardNavItem,
  type DashboardRange,
} from "./dashboard-overview-parts"
import { SAMPLE_ACTIVITY, SAMPLE_ATTENTION, SAMPLE_METRICS, SAMPLE_WORK } from "./dashboard-overview-sample"
import { RangeHeader, useRange } from "./dashboard-overview-shell"
import { DashboardSkeleton } from "./dashboard-overview-states"
import { WorkListBody, WorkListSkeleton, type DashboardWorkItem } from "./dashboard-overview-table"

type DashboardVariant = "overview" | "analytics" | "table"

const DEFAULT_NAV: readonly DashboardNavItem[] = [
  { id: "overview", label: "Overview", icon: LayoutDashboard },
  { id: "customers", label: "Customers", icon: Users },
  { id: "invoices", label: "Invoices", icon: ReceiptText },
  { id: "reports", label: "Reports", icon: ChartLine },
  { id: "settings", label: "Settings", icon: Settings },
]

interface DashboardOverviewProps extends Omit<React.ComponentProps<"div">, "title"> {
  /** `overview` one number first, `analytics` one large chart first, `table` the work list first. Anything else is `overview`. */
  variant?: DashboardVariant
  /** The first is the number the page is about; up to three more sit in a quieter row. An empty list shows the empty state. */
  metrics?: readonly DashboardMetric[]
  activity?: readonly DashboardActivity[]
  /** One thing that needs a person. Leave it out when nothing does. */
  attention?: DashboardAttention
  /** Shows the loading skeleton and locks the date range. */
  pending?: boolean
  /** Replaces the numbers with this message and a way to retry. */
  error?: string
  onRetry?: () => void
  range?: DashboardRange
  defaultRange?: DashboardRange
  onRangeChange?: (range: DashboardRange) => void
  product?: string
  nav?: readonly DashboardNavItem[]
  onNavigate?: (id: string) => void
  emptyActionLabel?: string
  onEmptyAction?: () => void
  /** `table` only: the rows to work through. */
  workItems?: readonly DashboardWorkItem[]
  /** `table` only: heads the page and names the table. */
  workTitle?: string
  /** `table` only: fires with the row and whether it is now handled. */
  onWorkAction?: (item: DashboardWorkItem, handled: boolean) => void
}

const SKELETON: Readonly<Record<DashboardVariant, React.ReactNode>> = {
  overview: <DashboardSkeleton />,
  analytics: <AnalyticsSkeleton />,
  table: <WorkListSkeleton />,
}

type DashboardPanelProps = Pick<
  DashboardOverviewProps,
  "metrics" | "activity" | "attention" | "error" | "onRetry" | "onEmptyAction" | "workItems" | "onWorkAction"
> & {
  shape: DashboardVariant
  range: DashboardRange
  pending: boolean
  emptyActionLabel: string
  workTitle: string
}

function DashboardPanel({ shape, range, pending, error, onRetry, metrics, activity, attention, emptyActionLabel, onEmptyAction, workItems, workTitle, onWorkAction }: DashboardPanelProps) {
  // Sample data only stands in when nothing real was passed, so it can never leak into a live dashboard.
  const shownMetrics = metrics ?? SAMPLE_METRICS[range]
  const content = {
    metrics: shownMetrics,
    activity: activity ?? (metrics ? [] : SAMPLE_ACTIVITY),
    attention: attention ?? (metrics ? undefined : SAMPLE_ATTENTION),
  }
  return (
    <DashboardContent
      pending={pending}
      error={error}
      onRetry={onRetry}
      empty={shownMetrics.length === 0}
      emptyActionLabel={emptyActionLabel}
      onEmptyAction={onEmptyAction}
      skeleton={SKELETON[shape]}
    >
      {shape === "analytics" ? <AnalyticsBody {...content} /> : null}
      {shape === "overview" ? <OverviewBody {...content} /> : null}
      {/* The sample list is already the failed payments, so the sample attention item would only repeat it. */}
      {shape === "table" ? <WorkListBody metrics={shownMetrics} items={workItems ?? (metrics ? [] : SAMPLE_WORK)} title={workTitle} attention={attention} onAction={onWorkAction} /> : null}
    </DashboardContent>
  )
}

function DashboardOverview({
  variant,
  metrics,
  activity,
  attention,
  pending = false,
  error,
  onRetry,
  range,
  defaultRange = "30d",
  onRangeChange,
  product = "Tallyroom",
  nav = DEFAULT_NAV,
  onNavigate,
  emptyActionLabel = "Connect billing",
  onEmptyAction,
  workItems,
  workTitle = "Payments to chase",
  onWorkAction,
  className,
  ...props
}: DashboardOverviewProps) {
  const [currentRange, selectRange] = useRange(range, defaultRange, onRangeChange)
  const shape: DashboardVariant = variant === "analytics" || variant === "table" ? variant : "overview"
  const panel = { shape, range: currentRange, pending, error, onRetry, metrics, activity, attention, emptyActionLabel, onEmptyAction, workItems, workTitle, onWorkAction }

  return (
    <div data-slot="dashboard-overview" data-variant={shape} className={cn("@container w-full bg-muted dark:bg-background", className)} {...props}>
      <div className={cn("flex min-w-0 flex-col", shape !== "analytics" && "@3xl:flex-row")}>
        <DashboardRail product={product} nav={nav} onNavigate={onNavigate} layout={shape === "analytics" ? "bar" : "rail"} touch={shape !== "overview"} />
        <Tabs value={currentRange} onValueChange={selectRange} className="min-w-0 flex-1 gap-6 p-4 @md:p-6 @3xl:p-8">
          <RangeHeader title={shape === "table" ? `${workTitle}.` : "Overview."} range={currentRange} pending={pending} touch={shape !== "overview"} />
          {/* Not a tab stop of its own: a focused panel has no visible ring, and its controls are reachable anyway. */}
          <TabsContent value={currentRange} tabIndex={-1} className="min-w-0">
            <DashboardPanel {...panel} />
          </TabsContent>
        </Tabs>
      </div>
    </div>
  )
}

export {
  DashboardOverview,
  type DashboardOverviewProps,
  type DashboardActivity,
  type DashboardAttention,
  type DashboardMetric,
  type DashboardRange,
  type DashboardVariant,
  type DashboardWorkItem,
}
```

### `registry/miseui/blocks/dashboard-overview-shell.tsx`

```tsx
import { useState } from "react"
import { cn } from "@/registry/miseui/lib/utils"
import { TabsList, TabsTrigger } from "@/registry/miseui/ui/tabs"
import type { DashboardRange } from "./dashboard-overview-parts"

const RANGE_LABELS: Readonly<Record<DashboardRange, string>> = { "7d": "7 days", "30d": "30 days", "90d": "90 days" }
const RANGES = Object.keys(RANGE_LABELS) as DashboardRange[]

interface RangeHeaderProps {
  title: string
  range: DashboardRange
  pending: boolean
  /** Grows the range tabs to 40 px under a finger. */
  touch?: boolean
}

function RangeHeader({ title, range, pending, touch = false }: RangeHeaderProps) {
  return (
    <div className="flex flex-wrap items-center justify-between gap-3">
      <h2 className="text-title text-xl">
        {title} <span className="text-muted-foreground">Last {RANGE_LABELS[range]}.</span>
      </h2>
      <TabsList aria-label="Date range" className={cn(touch && "pointer-coarse:h-12")}>
        {RANGES.map((value) => (
          <TabsTrigger key={value} value={value} disabled={pending} className="px-3">
            {RANGE_LABELS[value]}
          </TabsTrigger>
        ))}
      </TabsList>
    </div>
  )
}

function useRange(range: DashboardRange | undefined, defaultRange: DashboardRange, onChange?: (range: DashboardRange) => void) {
  const [internal, setInternal] = useState(defaultRange)
  const select = (next: string) => {
    setInternal(next as DashboardRange)
    onChange?.(next as DashboardRange)
  }
  return [range ?? internal, select] as const
}

export { RangeHeader, useRange }
```

### `registry/miseui/blocks/dashboard-overview-body.tsx`

```tsx
import type * as React from "react"
import { cn } from "@/registry/miseui/lib/utils"
import { Button } from "@/registry/miseui/ui/button"
import { AreaChart } from "./dashboard-overview-chart"
import {
  ActivityList,
  AttentionItem,
  ChangeLabel,
  PANEL_CLASS,
  SecondaryMetrics,
  type DashboardActivity,
  type DashboardAttention,
  type DashboardMetric,
} from "./dashboard-overview-parts"
import { DashboardMessage } from "./dashboard-overview-states"

/** One number first means the row under it stays short. A fourth metric belongs on its own page. */
const MAX_SECONDARY = 3

interface OverviewBodyProps {
  metrics: readonly DashboardMetric[]
  activity: readonly DashboardActivity[]
  attention?: DashboardAttention
}

function OverviewBody({ metrics, activity, attention }: OverviewBodyProps) {
  const [primary, ...rest] = metrics
  return (
    <div className="grid gap-4 @5xl:grid-cols-[minmax(0,1fr)_320px]">
      <div className="flex min-w-0 flex-col gap-4">
        <section className={cn(PANEL_CLASS, "flex flex-col gap-6 p-4 @md:p-6")}>
          <div className="flex flex-col gap-2">
            <p className="text-[13px] text-muted-foreground">{primary.label}</p>
            <p className="text-display text-5xl @2xl:text-6xl">{primary.value}</p>
            <ChangeLabel metric={primary} className="text-[15px]" />
          </div>
          <AreaChart label={primary.label} points={primary.series ?? []} format={primary.formatPoint} />
        </section>
        <SecondaryMetrics metrics={rest.slice(0, MAX_SECONDARY)} />
      </div>
      <div className="flex min-w-0 flex-col gap-4">
        {attention ? <AttentionItem key={attention.title} attention={attention} /> : null}
        <ActivityList activity={activity} />
      </div>
    </div>
  )
}

interface DashboardContentProps {
  pending: boolean
  error?: string
  onRetry?: () => void
  /** No metrics at all, which is the empty state in every design. */
  empty: boolean
  emptyActionLabel: string
  onEmptyAction?: () => void
  /** Same shape as the loaded design, so nothing jumps when the numbers land. */
  skeleton: React.ReactNode
  children: React.ReactNode
}

function DashboardContent({ pending, error, onRetry, empty, emptyActionLabel, onEmptyAction, skeleton, children }: DashboardContentProps) {
  if (pending) return skeleton
  if (error) {
    return (
      <DashboardMessage
        role="alert"
        title="These numbers did not load"
        description={error}
        action={<Button variant="outline" onClick={onRetry}>Try again</Button>}
      />
    )
  }
  if (empty) {
    return (
      <DashboardMessage
        title="No numbers to show yet"
        description="Connect a billing account and your revenue appears here within a few minutes."
        action={<Button variant="mise" onClick={onEmptyAction}>{emptyActionLabel}</Button>}
      />
    )
  }
  return children
}

export { DashboardContent, OverviewBody }
```

### `registry/miseui/blocks/dashboard-overview-analytics.tsx`

```tsx
import { useRef, useState } from "react"
import { cn } from "@/registry/miseui/lib/utils"
import { Chart } from "@/registry/miseui/ui/chart"
import { useElementWidth } from "@/registry/miseui/ui/chart-state"
import { Skeleton } from "@/registry/miseui/ui/skeleton"
import { ActivityList, AttentionItem, ChangeLabel, PANEL_CLASS, type DashboardActivity, type DashboardAttention, type DashboardMetric } from "./dashboard-overview-parts"

const MAX_METRICS = 4
const FALLBACK_WIDTH_PX = 640
/** Below this width a tall plot is mostly empty scrolling, and the strip above it already took the first screen. */
const TALL_FROM_PX = 560
const SHORT_HEIGHT_PX = 240
const TALL_HEIGHT_PX = 384
const STRIP_CLASS = "grid grid-cols-2 gap-1 @3xl:grid-cols-4"
const plainFormat = (value: number) => value.toLocaleString("en-US")

interface MetricTileProps {
  metric: DashboardMetric
  active: boolean
  onChoose?: () => void
}

/** A metric with nothing to draw stays a plain figure: a button that changes nothing is a lie. */
function MetricTile({ metric, active, onChoose }: MetricTileProps) {
  const content = (
    <>
      <span className="block text-[13px] text-pretty text-muted-foreground">{metric.label}</span>
      <span className="text-title mt-1 block text-xl @2xl:text-2xl">{metric.value}</span>
    </>
  )
  if (!onChoose) return <div className="min-w-0 rounded-xl p-3">{content}</div>
  return (
    <button
      type="button"
      aria-pressed={active}
      onClick={onChoose}
      className={cn(
        "min-w-0 rounded-xl p-3 text-start outline-none",
        "transition-colors duration-(--duration-fast) ease-(--ease-mise)",
        "focus-visible:ring-[3px] focus-visible:ring-ring/50",
        active ? "bg-muted dark:bg-accent" : "hover:bg-muted/60 dark:hover:bg-accent/60",
      )}
    >
      {content}
    </button>
  )
}

function useChosenMetric(metrics: readonly DashboardMetric[]) {
  const [chosenId, setChosenId] = useState<string | null>(null)
  const drawable = (metric: DashboardMetric) => (metric.series?.length ?? 0) > 0
  // A range or data change can take the chosen metric away, so this falls back instead of drawing nothing.
  const current = metrics.find((metric) => metric.id === chosenId && drawable(metric)) ?? metrics.find(drawable) ?? metrics[0]
  return { current, choose: setChosenId, drawable }
}

function LargeChart({ metric }: { metric: DashboardMetric }) {
  const frame = useRef<HTMLDivElement>(null)
  const width = useElementWidth(frame, FALLBACK_WIDTH_PX)
  const data = (metric.series ?? []).map((point) => ({ day: point.label, value: point.value }))
  return (
    <div ref={frame} className="min-w-0">
      {/* A line, because the kit's area scale starts at zero, which flattens a slowly moving total into a shelf. */}
      <Chart
        key={metric.id}
        variant="line"
        label={metric.label}
        data={data}
        index="day"
        series={[{ key: "value", label: metric.label }]}
        height={width < TALL_FROM_PX ? SHORT_HEIGHT_PX : TALL_HEIGHT_PX}
        formatValue={metric.formatPoint ?? plainFormat}
      />
    </div>
  )
}

interface AnalyticsBodyProps {
  metrics: readonly DashboardMetric[]
  activity: readonly DashboardActivity[]
  attention?: DashboardAttention
}

function AnalyticsBody({ metrics, activity, attention }: AnalyticsBodyProps) {
  const shown = metrics.slice(0, MAX_METRICS)
  const { current, choose, drawable } = useChosenMetric(shown)
  return (
    <div className="flex min-w-0 flex-col gap-4 pointer-coarse:[&_[data-slot=button]]:h-10">
      <section aria-label="Trend" className={cn(PANEL_CLASS, "flex flex-col gap-4 p-2 @md:p-4")}>
        <div role="group" aria-label="Metric on the chart" className={STRIP_CLASS}>
          {shown.map((metric) => (
            <MetricTile key={metric.id} metric={metric} active={metric.id === current.id} onChoose={drawable(metric) ? () => choose(metric.id) : undefined} />
          ))}
        </div>
        <div className="flex flex-col gap-3 px-2 pb-2 @md:px-2">
          <ChangeLabel metric={current} className="text-[13px]" />
          <LargeChart metric={current} />
        </div>
      </section>
      <div className="flex min-w-0 flex-col gap-4 @3xl:flex-row @3xl:items-start">
        {/* empty: the item can be put off, and a column reserved for it would stay behind as a blank. */}
        {attention ? (
          <div className="min-w-0 empty:hidden @3xl:order-last @3xl:w-80 @3xl:shrink-0">
            <AttentionItem key={attention.title} attention={attention} />
          </div>
        ) : null}
        <div className="min-w-0 flex-1">
          <ActivityList activity={activity} />
        </div>
      </div>
    </div>
  )
}

function AnalyticsSkeleton() {
  return (
    <div role="status" aria-label="Loading dashboard" className="flex flex-col gap-4">
      <div className={cn(PANEL_CLASS, "flex flex-col gap-4 p-2 @md:p-4")}>
        <div className={STRIP_CLASS}>
          {Array.from({ length: MAX_METRICS }, (_, index) => (
            <div key={index} className="flex flex-col gap-2 p-3">
              <Skeleton effect="wave" className="h-4 w-24" />
              <Skeleton effect="wave" className="h-6 w-20" />
            </div>
          ))}
        </div>
        <Skeleton effect="wave" className="mx-2 mb-2 h-60 rounded-xl @2xl:h-96" />
      </div>
      <div className={cn(PANEL_CLASS, "p-4")}>
        <Skeleton effect="wave" className="h-40 w-full" />
      </div>
    </div>
  )
}

export { AnalyticsBody, AnalyticsSkeleton }
```

### `registry/miseui/blocks/dashboard-overview-table.tsx`

```tsx
import { useState } from "react"
import { cn } from "@/registry/miseui/lib/utils"
import { Button } from "@/registry/miseui/ui/button"
import { DataTable, type DataTableColumn } from "@/registry/miseui/ui/data-table"
import { Skeleton } from "@/registry/miseui/ui/skeleton"
import { Status, type StatusTone } from "@/registry/miseui/ui/status"
import { AttentionItem, PANEL_CLASS, type DashboardAttention, type DashboardMetric } from "./dashboard-overview-parts"

interface DashboardWorkItem {
  id: string
  customer: string
  detail?: string
  status: string
  statusTone?: StatusTone
  /** Already formatted: this block never guesses a currency or a locale. */
  amount: string
  /** What the amount column sorts by. */
  amountValue?: number
  due: string
  /** What the due column sorts by, such as a timestamp. */
  dueValue?: number
  actionLabel: string
}

const MAX_NUMBERS = 4
const SKELETON_ROWS = 5

/** The list is the page, so the numbers give up their plate and sit on the ground as one sentence. */
function NumberLine({ metrics }: { metrics: readonly DashboardMetric[] }) {
  return (
    <dl aria-label="Key numbers" className="flex flex-wrap items-baseline gap-x-6 gap-y-2">
      {metrics.slice(0, MAX_NUMBERS).map((metric) => (
        <div key={metric.id} className="flex items-baseline gap-2 whitespace-nowrap">
          <dt className="order-last text-[13px] text-muted-foreground">{metric.label}</dt>
          <dd className={cn("text-[15px] font-medium tabular-nums", metric.changeTone === "negative" && "text-destructive")}>{metric.value}</dd>
          {metric.change ? <dd className="sr-only">{metric.change}</dd> : null}
        </div>
      ))}
    </dl>
  )
}

function workColumns(handled: readonly string[], toggle: (item: DashboardWorkItem) => void): DataTableColumn<DashboardWorkItem>[] {
  const isHandled = (item: DashboardWorkItem) => handled.includes(item.id)
  return [
    {
      key: "customer",
      header: "Customer",
      hideable: false,
      cell: (item) => (
        <span className="block min-w-0">
          <span className="block text-sm">{item.customer}</span>
          {item.detail ? <span className="block text-[13px] text-muted-foreground">{item.detail}</span> : null}
        </span>
      ),
    },
    {
      key: "status",
      header: "Status",
      value: (item) => (isHandled(item) ? "Handled" : item.status),
      cell: (item) => (
        <Status state="custom" tone={isHandled(item) ? "success" : (item.statusTone ?? "neutral")} className="text-[13px]">
          {isHandled(item) ? "Handled" : item.status}
        </Status>
      ),
    },
    { key: "amount", header: "Amount", numeric: true, filterable: false, value: (item) => item.amountValue ?? item.amount, cell: (item) => item.amount },
    { key: "due", header: "Due", filterable: false, value: (item) => item.dueValue ?? item.due, cell: (item) => <span className="whitespace-nowrap">{item.due}</span> },
    {
      key: "action",
      header: "Action",
      sortable: false,
      filterable: false,
      hideable: false,
      value: () => "",
      // The same button changes job and is never removed, so keyboard focus stays on the row just handled.
      cell: (item) => (
        <Button variant="outline" size="sm" aria-label={`${isHandled(item) ? "Undo" : item.actionLabel}: ${item.customer}`} className="pointer-coarse:h-10" onClick={() => toggle(item)}>
          {isHandled(item) ? "Undo" : item.actionLabel}
        </Button>
      ),
    },
  ]
}

interface WorkListBodyProps {
  metrics: readonly DashboardMetric[]
  items: readonly DashboardWorkItem[]
  title: string
  attention?: DashboardAttention
  /** Fires with the row and whether it is now handled. Update your data here. */
  onAction?: (item: DashboardWorkItem, handled: boolean) => void
}

function WorkListBody({ metrics, items, title, attention, onAction }: WorkListBodyProps) {
  const [handled, setHandled] = useState<readonly string[]>([])
  const toggle = (item: DashboardWorkItem) => {
    const next = !handled.includes(item.id)
    setHandled(next ? [...handled, item.id] : handled.filter((id) => id !== item.id))
    onAction?.(item, next)
  }
  const columns = workColumns(handled, toggle)
  return (
    <div className="flex min-w-0 flex-col gap-4">
      <NumberLine metrics={metrics} />
      {attention ? <AttentionItem key={attention.title} attention={attention} /> : null}
      {/* From @xl the table has room, so it earns a plate. Narrower it turns into cards, and a plate around cards is a plate on a plate. */}
      <div className="min-w-0 @xl:rounded-2xl @xl:border @xl:bg-background @xl:p-4 @xl:dark:bg-card">
        <DataTable label={title} variant="cards" rows={items} columns={columns} emptyText="Nothing needs you right now." labels={{ filter: "Filter this list" }} />
      </div>
    </div>
  )
}

function WorkListSkeleton() {
  return (
    <div role="status" aria-label="Loading dashboard" className="flex flex-col gap-4">
      <Skeleton effect="wave" className="h-5 w-full max-w-xl" />
      <div className={cn(PANEL_CLASS, "flex flex-col gap-4 p-4")}>
        <Skeleton effect="wave" className="h-9 w-full" />
        {Array.from({ length: SKELETON_ROWS }, (_, index) => (
          <Skeleton key={index} effect="wave" className="h-10 w-full" />
        ))}
      </div>
    </div>
  )
}

export { WorkListBody, WorkListSkeleton, type DashboardWorkItem }
```

### `registry/miseui/blocks/dashboard-overview-parts.tsx`

```tsx
import type * as React from "react"
import { useState } from "react"
import { TriangleAlert } from "lucide-react"
import { cn } from "@/registry/miseui/lib/utils"
import { Button } from "@/registry/miseui/ui/button"
import type { ChartPoint } from "./dashboard-overview-chart"

type DashboardRange = "7d" | "30d" | "90d"
type ChangeTone = "positive" | "negative" | "neutral"

interface DashboardMetric {
  id: string
  label: string
  /** Already formatted: this block never guesses a currency or a locale. */
  value: string
  change?: string
  /** Whether the change is good news, which its sign cannot tell: more churn is up and bad. */
  changeTone?: ChangeTone
  /** Drawn only for the first metric. */
  series?: readonly ChartPoint[]
  formatPoint?: (value: number) => string
}

interface DashboardActivity {
  id: string
  title: string
  detail?: string
  time: string
}

interface DashboardAttention {
  title: string
  description: string
  actionLabel: string
  onAction?: () => void
}

interface DashboardNavItem {
  id: string
  label: string
  icon: React.ComponentType<{ className?: string }>
}

const CAPTION_CLASS = "text-[13px] leading-relaxed text-muted-foreground"
/** A white panel on the pale canvas. Tinted from the ink, and off in dark mode, where it would read as a glow. */
const PANEL_CLASS =
  "rounded-2xl border bg-background dark:bg-card shadow-[0_1px_2px_color-mix(in_oklab,var(--foreground)_6%,transparent),0_16px_32px_-16px_color-mix(in_oklab,var(--foreground)_16%,transparent)] dark:shadow-none"
const TONE_CLASS: Record<ChangeTone, string> = {
  positive: "text-foreground",
  negative: "text-destructive",
  neutral: "text-muted-foreground",
}

function ChangeLabel({ metric, className }: { metric: DashboardMetric; className?: string }) {
  if (!metric.change) return null
  return <span className={cn(TONE_CLASS[metric.changeTone ?? "neutral"], className)}>{metric.change}</span>
}

/** Sized by position, not by a card: the first of these outranks the rest, and all of them sit under the chart. */
function SecondaryMetrics({ metrics }: { metrics: readonly DashboardMetric[] }) {
  if (metrics.length === 0) return null
  return (
    <dl className={cn(PANEL_CLASS, "grid gap-6 p-4 @md:grid-cols-[3fr_2fr_2fr] @md:p-6")}>
      {metrics.map((metric, index) => (
        <div key={metric.id} className={cn("flex flex-col gap-1", index > 0 && "@md:border-l @md:pl-6")}>
          <dt className={CAPTION_CLASS}>{metric.label}</dt>
          <dd className={cn("text-title", index === 0 ? "text-2xl" : "text-lg")}>{metric.value}</dd>
          {metric.change ? (
            <dd className="text-[13px]">
              <ChangeLabel metric={metric} />
            </dd>
          ) : null}
        </div>
      ))}
    </dl>
  )
}

function AttentionItem({ attention }: { attention: DashboardAttention }) {
  const [dismissed, setDismissed] = useState(false)
  if (dismissed) return null
  return (
    <section aria-label="Needs your attention" className={cn(PANEL_CLASS, "flex gap-3 p-4")}>
      <TriangleAlert aria-hidden="true" className="mt-1 size-4 shrink-0 text-destructive" />
      <div className="flex min-w-0 flex-col gap-1">
        <h3 className="text-[15px] font-medium">{attention.title}</h3>
        <p className={cn(CAPTION_CLASS, "text-pretty")}>{attention.description}</p>
        <div className="mt-2 flex flex-wrap gap-2">
          <Button variant="outline" size="sm" onClick={attention.onAction}>
            {attention.actionLabel}
          </Button>
          <Button variant="ghost" size="sm" onClick={() => setDismissed(true)}>
            Later
          </Button>
        </div>
      </div>
    </section>
  )
}

function ActivityList({ activity }: { activity: readonly DashboardActivity[] }) {
  return (
    <section className={cn(PANEL_CLASS, "flex flex-col gap-2 p-4")}>
      <h3 className="text-[15px] font-medium">Recent activity</h3>
      {activity.length === 0 ? (
        <p className={cn(CAPTION_CLASS, "py-3")}>Nothing has happened in this period.</p>
      ) : (
        <ul className="flex flex-col divide-y">
          {activity.map((item) => (
            <li key={item.id} className="flex items-start justify-between gap-3 py-3">
              <div className="min-w-0">
                <p className="text-[13px] leading-relaxed text-pretty">{item.title}</p>
                {item.detail ? <p className={CAPTION_CLASS}>{item.detail}</p> : null}
              </div>
              <span className={cn(CAPTION_CLASS, "shrink-0 whitespace-nowrap")}>{item.time}</span>
            </li>
          ))}
        </ul>
      )}
    </section>
  )
}

interface DashboardRailProps {
  product: string
  nav: readonly DashboardNavItem[]
  onNavigate?: (id: string) => void
  /** `rail` stands at the side from @3xl. `bar` stays on top at every width, giving the content the full width. */
  layout?: "rail" | "bar"
  /** Grows the items to 40 px under a finger. */
  touch?: boolean
}

/** A top bar under @3xl. The bar scrolls by itself, so five labels never push the page sideways on a phone. */
function DashboardRail({ product, nav, onNavigate, layout = "rail", touch = false }: DashboardRailProps) {
  const [active, setActive] = useState(nav[0]?.id ?? "")
  const rail = layout === "rail"
  const select = (id: string) => {
    setActive(id)
    onNavigate?.(id)
  }
  return (
    <div className={cn("flex min-w-0 flex-col gap-3 border-b p-4", rail ? "@3xl:w-56 @3xl:shrink-0 @3xl:gap-6 @3xl:border-r @3xl:border-b-0" : "@3xl:flex-row @3xl:items-center @3xl:gap-6 @3xl:px-8 @3xl:py-3")}>
      <p className={cn("px-3 text-[15px] font-medium", !rail && "@3xl:px-0")}>{product}</p>
      <nav aria-label="Main" className={cn("flex gap-1 overflow-x-auto", rail && "@3xl:flex-col @3xl:overflow-visible")}>
        {nav.map((item) => (
          <button
            key={item.id}
            type="button"
            aria-current={item.id === active ? "page" : undefined}
            onClick={() => select(item.id)}
            className={cn(
              "inline-flex h-9 shrink-0 items-center gap-3 rounded-md px-3 text-[13px] whitespace-nowrap outline-none",
              "transition-colors duration-(--duration-fast) ease-(--ease-mise)",
              "focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50",
              touch && "pointer-coarse:h-10",
              item.id === active ? "bg-background dark:bg-card text-foreground shadow-xs" : "text-muted-foreground hover:bg-background/60",
            )}
          >
            <item.icon className="size-4" />
            {item.label}
          </button>
        ))}
      </nav>
    </div>
  )
}

export {
  PANEL_CLASS,
  ActivityList,
  AttentionItem,
  ChangeLabel,
  DashboardRail,
  SecondaryMetrics,
  type DashboardActivity,
  type DashboardAttention,
  type DashboardMetric,
  type DashboardNavItem,
  type DashboardRange,
}
```

### `registry/miseui/blocks/dashboard-overview-states.tsx`

```tsx
import type * as React from "react"
import { cn } from "@/registry/miseui/lib/utils"
import { Skeleton } from "@/registry/miseui/ui/skeleton"
import { PANEL_CLASS } from "./dashboard-overview-parts"

/** Same panels as the loaded screen, so nothing jumps when the numbers land. */
function DashboardSkeleton() {
  return (
    <div role="status" aria-label="Loading dashboard" className="grid gap-4 @5xl:grid-cols-[minmax(0,1fr)_320px]">
      <div className="flex flex-col gap-4">
        <div className={cn(PANEL_CLASS, "flex flex-col gap-6 p-4 @md:p-6")}>
          <div className="flex flex-col gap-2">
            <Skeleton effect="wave" className="h-4 w-48" />
            <Skeleton effect="wave" className="h-12 w-56 rounded-xl @2xl:h-16 @2xl:w-72" />
            <Skeleton effect="wave" className="h-4 w-56" />
          </div>
          <Skeleton effect="wave" className="h-40 w-full rounded-xl @2xl:h-56" />
        </div>
        <div className={cn(PANEL_CLASS, "grid gap-6 p-4 @md:grid-cols-[3fr_2fr_2fr] @md:p-6")}>
          <Skeleton effect="wave" className="h-16 w-full" />
          <Skeleton effect="wave" className="h-16 w-full" />
          <Skeleton effect="wave" className="h-16 w-full" />
        </div>
      </div>
      <div className="flex flex-col gap-4">
        <div className={cn(PANEL_CLASS, "p-4")}>
          <Skeleton effect="wave" className="h-24 w-full" />
        </div>
        <div className={cn(PANEL_CLASS, "p-4")}>
          <Skeleton effect="wave" className="h-48 w-full" />
        </div>
      </div>
    </div>
  )
}

interface DashboardMessageProps {
  title: string
  description: string
  action?: React.ReactNode
  role?: "alert"
}

function DashboardMessage({ title, description, action, role }: DashboardMessageProps) {
  return (
    <div role={role} className={cn(PANEL_CLASS, "flex flex-col items-start gap-3 p-6 py-16 @md:p-12")}>
      <h3 className="text-title text-2xl text-balance">{title}</h3>
      <p className="max-w-md text-[15px] leading-relaxed text-pretty text-muted-foreground">{description}</p>
      {action ? <div className="mt-2">{action}</div> : null}
    </div>
  )
}

export { DashboardMessage, DashboardSkeleton }
```

### `registry/miseui/blocks/dashboard-overview-chart.tsx`

```tsx
import type * as React from "react"
import { useId, useState } from "react"
import { motion, useReducedMotion } from "motion/react"
import { SPRING_DEFAULT } from "@/registry/miseui/lib/motion"
import { cn } from "@/registry/miseui/lib/utils"

interface ChartPoint {
  label: string
  value: number
}

const VIEW_WIDTH = 640
const VIEW_HEIGHT = 200
/** Keeps a 2 px line off the top and bottom edge, where the SVG would clip half of it. */
const INSET_Y = 12
const PERCENT = 100
const FILL_TOP_OPACITY = 0.22
const CAPTION_CLASS = "text-[13px] text-muted-foreground"

/** A single reading still draws as a flat line, and a flat series must not divide by zero. */
function buildPaths(values: readonly number[]) {
  const drawn = values.length === 1 ? [values[0], values[0]] : values
  const min = Math.min(...drawn)
  const span = Math.max(...drawn) - min
  const ys = drawn.map((value) =>
    span === 0 ? VIEW_HEIGHT / 2 : INSET_Y + (1 - (value - min) / span) * (VIEW_HEIGHT - 2 * INSET_Y),
  )
  const line = ys
    .map((y, index) => `${index === 0 ? "M" : "L"}${((index / (ys.length - 1)) * VIEW_WIDTH).toFixed(1)} ${y.toFixed(1)}`)
    .join(" ")
  return { line, area: `${line} L${VIEW_WIDTH} ${VIEW_HEIGHT} L0 ${VIEW_HEIGHT} Z`, ys }
}

function shareOf(index: number, count: number) {
  return count <= 1 ? PERCENT : (index / (count - 1)) * PERCENT
}

interface ChartReadoutProps {
  point: ChartPoint
  left: number
  top: number
  format: (value: number) => string
}

/** Flips past the middle so the reading never leaves the chart. Instant on purpose: it follows a hand. */
function ChartReadout({ point, left, top, format }: ChartReadoutProps) {
  const flip = left > PERCENT / 2
  return (
    <>
      <span aria-hidden="true" className="pointer-events-none absolute inset-y-0 w-px bg-foreground/30" style={{ left: `${left}%` }} />
      <span
        aria-hidden="true"
        className="pointer-events-none absolute size-2 -translate-1/2 rounded-full bg-mise ring-2 ring-background"
        style={{ left: `${left}%`, top: `${top}%` }}
      />
      <span
        data-testid="chart-readout"
        className="pointer-events-none absolute top-0 z-10 rounded-md border bg-popover px-2 py-1 text-[13px] whitespace-nowrap shadow-xs"
        style={{ left: `${left}%`, transform: flip ? "translateX(calc(-100% - 8px))" : "translateX(8px)" }}
      >
        <span className="font-medium text-popover-foreground">{format(point.value)}</span>
        <span className="ml-2 text-muted-foreground">{point.label}</span>
      </span>
    </>
  )
}

interface ChartMarksProps {
  line: string
  area: string
}

function ChartMarks({ line, area }: ChartMarksProps) {
  const reduceMotion = useReducedMotion()
  const fillId = `area-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`
  return (
    <svg
      aria-hidden="true"
      viewBox={`0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`}
      preserveAspectRatio="none"
      className="absolute inset-0 size-full overflow-visible"
    >
      <defs>
        <linearGradient id={fillId} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" style={{ stopColor: "var(--mise)", stopOpacity: FILL_TOP_OPACITY }} />
          <stop offset="1" style={{ stopColor: "var(--mise)", stopOpacity: 0 }} />
        </linearGradient>
      </defs>
      {/* Keyed on the data, so a new range arrives as one soft change instead of a jump. */}
      <motion.g
        key={line}
        data-testid="chart-marks"
        data-reduced={reduceMotion ? "true" : "false"}
        initial={reduceMotion ? false : { opacity: 0 }}
        animate={{ opacity: 1 }}
        transition={SPRING_DEFAULT}
      >
        <path data-testid="chart-area" d={area} fill={`url(#${fillId})`} />
        <path
          data-testid="chart-line"
          d={line}
          fill="none"
          strokeWidth={2}
          strokeLinejoin="round"
          strokeLinecap="round"
          vectorEffect="non-scaling-stroke"
          className="stroke-mise"
        />
      </motion.g>
    </svg>
  )
}

/** The chart's numbers for anyone who cannot see or hover it. */
function ChartTable({ label, points, format }: { label: string; points: readonly ChartPoint[]; format: (value: number) => string }) {
  return (
    <table className="sr-only">
      <caption>{label}</caption>
      <tbody>
        {points.map((point) => (
          <tr key={point.label}>
            <th scope="row">{point.label}</th>
            <td>{format(point.value)}</td>
          </tr>
        ))}
      </tbody>
    </table>
  )
}

interface AreaChartProps extends Omit<React.ComponentProps<"figure">, "children"> {
  /** Names the chart for assistive tech. */
  label: string
  points: readonly ChartPoint[]
  format?: (value: number) => string
}

const defaultFormat = (value: number) => value.toLocaleString("en-US")

function AreaChart({ label, points, format = defaultFormat, className, ...props }: AreaChartProps) {
  const [active, setActive] = useState<number | null>(null)
  if (points.length === 0) return null

  const values = points.map((point) => point.value)
  const { line, area, ys } = buildPaths(values)
  const first = points[0]
  const last = points[points.length - 1]
  const count = points.length
  // After a switch to a shorter range the old index could point at nothing.
  const current = active !== null && active < count ? active : null

  const track = (event: React.PointerEvent<HTMLDivElement>) => {
    const rect = event.currentTarget.getBoundingClientRect()
    if (rect.width <= 0) return
    const share = (event.clientX - rect.left) / rect.width
    setActive(Math.min(count - 1, Math.max(0, Math.round(share * (count - 1)))))
  }

  return (
    <figure
      data-slot="area-chart"
      aria-label={`${label}, from ${format(first.value)} on ${first.label} to ${format(last.value)} on ${last.label}`}
      className={cn("flex flex-col gap-2", className)}
      {...props}
    >
      <div data-testid="chart-plot" className="relative h-40 touch-pan-y border-b @2xl:h-56" onPointerMove={track} onPointerLeave={() => setActive(null)}>
        <ChartMarks line={line} area={area} />
        {/* The scale does not start at zero, so the two ends of it are written down. */}
        <span className={cn(CAPTION_CLASS, "pointer-events-none absolute top-0 left-0")}>{format(Math.max(...values))}</span>
        <span className={cn(CAPTION_CLASS, "pointer-events-none absolute right-0 bottom-1")}>{format(Math.min(...values))}</span>
        {current !== null ? (
          <ChartReadout point={points[current]} left={shareOf(current, count)} top={(ys[current] / VIEW_HEIGHT) * PERCENT} format={format} />
        ) : null}
      </div>
      <div aria-hidden="true" className={cn(CAPTION_CLASS, "flex justify-between")}>
        <span>{first.label}</span>
        <span>{last.label}</span>
      </div>
      <ChartTable label={label} points={points} format={format} />
    </figure>
  )
}

export { AreaChart, type AreaChartProps, type ChartPoint }
```

### `registry/miseui/blocks/dashboard-overview-sample.ts`

```ts
import type { ChartPoint } from "./dashboard-overview-chart"
import type { DashboardActivity, DashboardAttention, DashboardMetric, DashboardRange } from "./dashboard-overview-parts"
import type { DashboardWorkItem } from "./dashboard-overview-table"

/** Everything in this file is invented sample data. Pass `metrics` and none of it is shown. */

const SAMPLE_LAST_DAY = Date.UTC(2026, 10, 2)
const DAY_MS = 86_400_000
const dayLabel = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" })

/** Dates counted back from a fixed day, so the sample renders the same on the server and in a test. */
function dated(values: readonly number[], stepDays: number): ChartPoint[] {
  return values.map((value, index) => ({
    label: dayLabel.format(SAMPLE_LAST_DAY - (values.length - 1 - index) * stepDays * DAY_MS),
    value,
  }))
}

const formatDollars = (value: number) => `$${value.toLocaleString("en-US")}`
const formatCount = (value: number) => value.toLocaleString("en-US")
const formatPercent = (value: number) => `${value}%`

const WEEK = [47702, 47702, 47886, 47886, 48066, 48132, 48216]
const MONTH = [
  46076, 46076, 46160, 46244, 46244, 46118, 46298, 46382, 46382, 46466, 46550, 46734, 46734, 46650, 46818, 46902, 46986,
  47170, 47170, 47086, 47254, 47338, 47422, 47618, 47618, 47702, 47702, 47886, 48066, 48216,
]
const QUARTER = [43318, 43612, 43780, 44102, 44018, 44496, 44910, 45238, 45154, 45702, 46076, 47170, 48216]

/** Only the analytics design draws these. Each ends on the figure its metric shows, so the chart and the number agree. */
const SUBSCRIPTIONS = {
  week: [1273, 1275, 1276, 1279, 1280, 1282, 1284],
  month: [1247, 1248, 1250, 1251, 1251, 1253, 1255, 1256, 1258, 1258, 1260, 1262, 1263, 1263, 1265, 1267, 1268, 1270, 1270, 1272, 1273, 1274, 1276, 1277, 1277, 1279, 1280, 1281, 1283, 1284],
  quarter: [1172, 1181, 1188, 1196, 1201, 1213, 1222, 1231, 1238, 1249, 1258, 1272, 1284],
}
const CONVERSION = {
  week: [23.4, 23.5, 23.5, 23.7, 23.8, 24, 24.1],
  month: [22.2, 22.2, 22.3, 22.3, 22.4, 22.4, 22.5, 22.5, 22.5, 22.6, 22.6, 22.7, 22.7, 22.8, 22.8, 22.8, 22.9, 22.9, 23, 23, 23, 23.1, 23.1, 23.2, 23.2, 23.2, 23.3, 23.3, 23.4, 23.4],
  quarter: [19.2, 19.4, 19.6, 19.7, 20, 20.2, 20.4, 20.6, 20.8, 21, 21.2, 21.5, 21.8],
}
const CHURN = {
  week: [0, 1, 0, 0, 0, 1, 0],
  month: [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0],
  quarter: [2, 3, 2, 3, 2, 2, 3, 2, 2, 3, 2, 3, 2],
}

interface RangeFigures {
  points: ChartPoint[]
  change: string
  subscriptions: string
  conversion: [string, string]
  churn: [string, string, DashboardMetric["changeTone"]]
  series: { subscriptions: ChartPoint[]; conversion: ChartPoint[]; churn: ChartPoint[] }
}

function metricsFor({ points, change, subscriptions, conversion, churn, series }: RangeFigures): DashboardMetric[] {
  return [
    {
      id: "mrr",
      label: "Monthly recurring revenue",
      value: "$48,216",
      change,
      changeTone: "positive",
      series: points,
      formatPoint: formatDollars,
    },
    { id: "subscriptions", label: "Active subscriptions", value: "1,284", change: subscriptions, changeTone: "positive", series: series.subscriptions, formatPoint: formatCount },
    { id: "conversion", label: "Trial to paid", value: conversion[0], change: conversion[1], changeTone: "positive", series: series.conversion, formatPoint: formatPercent },
    { id: "churn", label: "Accounts churned", value: churn[0], change: churn[1], changeTone: churn[2], series: series.churn, formatPoint: formatCount },
  ]
}

const SAMPLE_METRICS: Readonly<Record<DashboardRange, DashboardMetric[]>> = {
  "7d": metricsFor({
    points: dated(WEEK, 1),
    change: "+$514 (+1.1%) from last week",
    subscriptions: "+11 this week",
    conversion: ["24.1%", "+0.7 pts"],
    churn: ["2", "Same as last week", "neutral"],
    series: { subscriptions: dated(SUBSCRIPTIONS.week, 1), conversion: dated(CONVERSION.week, 1), churn: dated(CHURN.week, 1) },
  }),
  "30d": metricsFor({
    points: dated(MONTH, 1),
    change: "+$2,140 (+4.6%) from 30 days ago",
    subscriptions: "+37 in 30 days",
    conversion: ["23.4%", "+1.2 pts"],
    churn: ["9", "3 more than last period", "negative"],
    series: { subscriptions: dated(SUBSCRIPTIONS.month, 1), conversion: dated(CONVERSION.month, 1), churn: dated(CHURN.month, 1) },
  }),
  "90d": metricsFor({
    points: dated(QUARTER, 7),
    change: "+$4,898 (+11.3%) from 90 days ago",
    subscriptions: "+112 in 90 days",
    conversion: ["21.8%", "+2.6 pts"],
    churn: ["31", "4 fewer than last period", "positive"],
    series: { subscriptions: dated(SUBSCRIPTIONS.quarter, 7), conversion: dated(CONVERSION.quarter, 7), churn: dated(CHURN.quarter, 7) },
  }),
}

const SAMPLE_ACTIVITY: readonly DashboardActivity[] = [
  { id: "a1", title: "Pell Street Studio moved to Team", detail: "+$180 per month", time: "12 min ago" },
  { id: "a2", title: "Dunmore Dental started a trial", detail: "Invited 3 people", time: "1 hr ago" },
  { id: "a3", title: "Tern Harbor Books cancelled", detail: "-$84 per month, left a note", time: "4 hr ago" },
  { id: "a4", title: "Quill & Ash paid invoice 2291", detail: "$1,260, 11 days late", time: "Yesterday" },
  { id: "a5", title: "Orchard Row added 6 seats", detail: "+$126 per month", time: "Oct 31" },
]

const SAMPLE_ATTENTION: DashboardAttention = {
  title: "14 payments failed this week",
  description: "$1,092 is at risk. Automatic retries stop on Friday.",
  actionLabel: "Review payments",
}

const SAMPLE_WORK: readonly DashboardWorkItem[] = [
  { id: "w1", customer: "Pell Street Studio", detail: "Team plan", status: "Card declined", statusTone: "danger", amount: "$264", amountValue: 264, due: "Nov 1", dueValue: 1, actionLabel: "Retry" },
  { id: "w2", customer: "Dunmore Dental", detail: "Starter plan", status: "Card expired", statusTone: "danger", amount: "$84", amountValue: 84, due: "Oct 30", dueValue: -1, actionLabel: "Ask for a card" },
  { id: "w3", customer: "Quill & Ash", detail: "Invoice 2304", status: "11 days late", statusTone: "warning", amount: "$1,260", amountValue: 1260, due: "Oct 22", dueValue: -9, actionLabel: "Remind" },
  { id: "w4", customer: "Orchard Row", detail: "Team plan", status: "Retry on Friday", statusTone: "neutral", amount: "$126", amountValue: 126, due: "Nov 2", dueValue: 2, actionLabel: "Retry" },
  { id: "w5", customer: "Tern Harbor Books", detail: "Final invoice", status: "4 days late", statusTone: "warning", amount: "$84", amountValue: 84, due: "Oct 29", dueValue: -2, actionLabel: "Remind" },
]

export { SAMPLE_ACTIVITY, SAMPLE_ATTENTION, SAMPLE_METRICS, SAMPLE_WORK }
```
