# Dashboard

Default dashboard page with quick actions, stat cards, a new members carousel, and an activity feed.

## Installation

```bash
npx shadcn@latest add https://ui.uptoolkit.com/r/dashboard-page.json
```

[Registry JSON](https://ui.uptoolkit.com/r/dashboard-page.json)

## Preview

```tsx
import Page from "app/dashboard/page";
export function Preview() {
  return <Page />;
}
```


## Source

### page.tsx

```tsx
import { IconPlus, IconUsersGroup } from "@tabler/icons-react";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";

import {
  dashboardActivity,
  dashboardCommunityChannels,
  dashboardMembers,
  dashboardQuickActions,
  dashboardStats,
  type DashboardTone,
} from "@/lib/dashboard-data";
import { ActivityFeed, DashboardChrome, NewMembersCarousel } from "@/components/dashboard-panels";

/** Tile background and icon tint per tone, keyed by `DashboardTone`. */
const toneTileClasses = {
  orange:
    "bg-orange-50 text-orange-600 hover:bg-orange-100 dark:bg-orange-950/40 dark:text-orange-300 dark:hover:bg-orange-950/60",
  blue: "bg-blue-50 text-blue-600 hover:bg-blue-100 dark:bg-blue-950/40 dark:text-blue-300 dark:hover:bg-blue-950/60",
  green:
    "bg-emerald-50 text-emerald-600 hover:bg-emerald-100 dark:bg-emerald-950/40 dark:text-emerald-300 dark:hover:bg-emerald-950/60",
  amber:
    "bg-amber-50 text-amber-600 hover:bg-amber-100 dark:bg-amber-950/40 dark:text-amber-300 dark:hover:bg-amber-950/60",
  violet:
    "bg-violet-50 text-violet-600 hover:bg-violet-100 dark:bg-violet-950/40 dark:text-violet-300 dark:hover:bg-violet-950/60",
} as const satisfies Record<DashboardTone, string>;

/** Chip behind a stat icon. Same tones, without the hover state. */
const toneChipClasses = {
  orange: "bg-orange-50 text-orange-600 dark:bg-orange-950/40 dark:text-orange-300",
  blue: "bg-blue-50 text-blue-600 dark:bg-blue-950/40 dark:text-blue-300",
  green: "bg-emerald-50 text-emerald-600 dark:bg-emerald-950/40 dark:text-emerald-300",
  amber: "bg-amber-50 text-amber-600 dark:bg-amber-950/40 dark:text-amber-300",
  violet: "bg-violet-50 text-violet-600 dark:bg-violet-950/40 dark:text-violet-300",
} as const satisfies Record<DashboardTone, string>;

/**
 * Default dashboard page.
 *
 * The page stays a server component: it reads the fixtures and lays out the
 * sections, while `DashboardChrome`, `NewMembersCarousel`, and `ActivityFeed`
 * own the client state. Replace the imports from `dashboard-data` with your own
 * queries — nothing else here needs to change.
 */
export default function Page() {
  return (
    <DashboardChrome>
      <div className="mx-auto flex max-w-6xl flex-col gap-5">
        <h1 className="sr-only">Dashboard</h1>

        <Card>
          <CardHeader>
            <CardTitle className="flex items-center gap-2">
              <IconPlus aria-hidden="true" className="size-4 text-muted-foreground" />
              Quick actions
            </CardTitle>
          </CardHeader>
          <CardContent>
            <ul className="m-0 grid list-none gap-4 p-0 md:grid-cols-3">
              {dashboardQuickActions.map((action) => (
                <li key={action.id}>
                  <a
                    href={action.href}
                    className={cn(
                      "flex h-full flex-col items-center gap-1 rounded-xl px-4 py-6 text-center transition-colors",
                      toneTileClasses[action.tone],
                    )}
                  >
                    <action.icon aria-hidden="true" className="mb-1 size-5" />
                    <span className="font-medium">{action.label}</span>
                    <span className="text-sm opacity-80">{action.description}</span>
                  </a>
                </li>
              ))}
            </ul>
          </CardContent>
        </Card>

        <ul className="m-0 grid list-none gap-4 p-0 sm:grid-cols-2 lg:grid-cols-4">
          {dashboardStats.map((stat) => (
            <li key={stat.id}>
              <Card className="h-full">
                <CardContent className="flex items-center gap-3">
                  <span
                    aria-hidden="true"
                    className={cn(
                      "grid size-10 shrink-0 place-items-center rounded-lg",
                      toneChipClasses[stat.tone],
                    )}
                  >
                    <stat.icon className="size-5" />
                  </span>
                  <span className="flex min-w-0 flex-col">
                    <span className="font-heading text-2xl leading-tight font-semibold tabular-nums">
                      {stat.value}
                    </span>
                    <span className="truncate text-sm text-muted-foreground">{stat.label}</span>
                  </span>
                </CardContent>
              </Card>
            </li>
          ))}
        </ul>

        <NewMembersCarousel members={dashboardMembers} />

        <div className="grid gap-5 lg:grid-cols-[minmax(0,1.7fr)_minmax(0,1fr)]">
          <ActivityFeed entries={dashboardActivity} />

          <Card className="h-fit">
            <CardHeader>
              <CardTitle className="flex items-center gap-2">
                <IconUsersGroup aria-hidden="true" className="size-4 text-muted-foreground" />
                Join the community
              </CardTitle>
            </CardHeader>
            <CardContent>
              <ul className="m-0 flex list-none flex-col gap-3 p-0">
                {dashboardCommunityChannels.map((channel) => (
                  <li key={channel.id}>
                    <a
                      href={channel.href}
                      target="_blank"
                      rel="noreferrer"
                      className="flex items-center gap-3 rounded-xl border p-3 transition-colors hover:bg-muted/50"
                    >
                      <span
                        aria-hidden="true"
                        className="grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground"
                      >
                        <channel.icon className="size-4" />
                      </span>
                      <span className="flex min-w-0 flex-col">
                        <span className="font-medium">{channel.name}</span>
                        <span className="truncate text-sm text-muted-foreground">
                          {channel.description}
                        </span>
                      </span>
                    </a>
                  </li>
                ))}
              </ul>
            </CardContent>
          </Card>
        </div>
      </div>
    </DashboardChrome>
  );
}
```


### components/dashboard-panels.tsx

```tsx
"use client";

import {
  IconActivity,
  IconBriefcase,
  IconBuildingStore,
  IconChevronLeft,
  IconChevronRight,
  IconSchool,
  IconUsers,
  type Icon,
} from "@tabler/icons-react";
import * as React from "react";

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";

import { DashboardShell } from "@/components/dashboard-shell";
import {
  dashboardLocales,
  dashboardNav,
  dashboardUser,
  dashboardWorkspaces,
  type DashboardActivityEntry,
  type DashboardActivityKind,
  type DashboardMember,
} from "@/lib/dashboard-data";

/**
 * Client boundary around the shell.
 *
 * The shell takes callbacks for search, create, messages, and the language
 * switcher, so it has to be mounted from a client component. Keeping that in
 * one small wrapper lets the page itself stay a server component and pass its
 * sections in as `children`. Replace the handlers with your router, command
 * palette, and locale writer.
 */
function DashboardChrome({ children }: { children: React.ReactNode }) {
  const [workspaceId, setWorkspaceId] = React.useState(dashboardWorkspaces[0]?.id);
  const [locale, setLocale] = React.useState(dashboardLocales[0]?.value);
  const workspace =
    dashboardWorkspaces.find((entry) => entry.id === workspaceId) ?? dashboardWorkspaces[0];

  return (
    <DashboardShell
      workspace={workspace}
      workspaces={dashboardWorkspaces}
      onWorkspaceChange={setWorkspaceId}
      user={dashboardUser}
      nav={dashboardNav}
      locale={locale}
      locales={dashboardLocales}
      onLocaleChange={setLocale}
      messagesCount={1}
      notificationsCount={4}
      onSearch={() => {}}
      onCreateClick={() => {}}
      onMessagesClick={() => {}}
      onNotificationsClick={() => {}}
      onSupportClick={() => {}}
    >
      {children}
    </DashboardShell>
  );
}

type NewMembersCarouselProps = {
  members: readonly DashboardMember[];
  /** Cards per page. The dots below the track page through the list. */
  pageSize?: number;
};

/**
 * Paged grid of recently joined members.
 *
 * Pages rather than scrolls, so every card stays fully visible and the dots
 * map one-to-one to what a click will show.
 */
function NewMembersCarousel({ members, pageSize = 3 }: NewMembersCarouselProps) {
  const [page, setPage] = React.useState(0);
  const pageCount = Math.max(1, Math.ceil(members.length / pageSize));
  const currentPage = Math.min(page, pageCount - 1);
  const visible = members.slice(currentPage * pageSize, currentPage * pageSize + pageSize);

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <IconUsers aria-hidden="true" className="size-4 text-muted-foreground" />
          New members
        </CardTitle>
        {pageCount > 1 ? (
          <CardAction className="flex items-center gap-1">
            <Button
              variant="ghost"
              size="icon-sm"
              aria-label="Previous members"
              disabled={currentPage === 0}
              onClick={() => setPage(currentPage - 1)}
            >
              <IconChevronLeft aria-hidden="true" />
            </Button>
            <Button
              variant="ghost"
              size="icon-sm"
              aria-label="Next members"
              disabled={currentPage === pageCount - 1}
              onClick={() => setPage(currentPage + 1)}
            >
              <IconChevronRight aria-hidden="true" />
            </Button>
          </CardAction>
        ) : null}
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <ul className="m-0 grid list-none gap-4 p-0 sm:grid-cols-2 lg:grid-cols-3">
          {visible.map((member) => (
            <li key={member.id} className="flex flex-col items-center gap-3 rounded-xl border p-5">
              <Avatar size="lg" className="size-14">
                <AvatarImage src={member.avatarUrl} alt="" />
                <AvatarFallback>{member.name.slice(0, 2).toUpperCase()}</AvatarFallback>
              </Avatar>
              <span className="flex flex-col items-center gap-0.5 text-center">
                <span className="font-medium">{member.name}</span>
                <span className="text-sm text-muted-foreground">{member.role}</span>
              </span>
              <Button
                variant="outline"
                size="sm"
                nativeButton={false}
                render={<a href={member.href} />}
                className="w-full"
              >
                View profile
              </Button>
            </li>
          ))}
        </ul>

        {pageCount > 1 ? (
          <div className="flex items-center justify-center gap-1.5">
            {Array.from({ length: pageCount }, (_, index) => (
              <button
                key={index}
                type="button"
                aria-label={`Show members ${index + 1} of ${pageCount}`}
                aria-current={index === currentPage ? "true" : undefined}
                onClick={() => setPage(index)}
                className={cn(
                  "h-1.5 rounded-full transition-all",
                  index === currentPage
                    ? "w-5 bg-foreground"
                    : "w-1.5 bg-border hover:bg-border/80",
                )}
              />
            ))}
          </div>
        ) : null}
      </CardContent>
    </Card>
  );
}

const activityKindIcons = {
  training: IconSchool,
  marketplace: IconBuildingStore,
  job: IconBriefcase,
  member: IconUsers,
} as const satisfies Record<DashboardActivityKind, Icon>;

type ActivityTab = {
  id: string;
  label: string;
  icon: Icon;
  /** Kinds this tab keeps. `null` keeps everything. */
  kinds: readonly DashboardActivityKind[] | null;
};

const activityTabs: readonly ActivityTab[] = [
  { id: "all", label: "All", icon: IconActivity, kinds: null },
  { id: "marketplace", label: "Marketplace", icon: IconBuildingStore, kinds: ["marketplace"] },
  { id: "jobs", label: "Jobs", icon: IconBriefcase, kinds: ["job"] },
  { id: "members", label: "Members", icon: IconUsers, kinds: ["member"] },
];

/**
 * Activity feed with category tabs.
 *
 * Filtering happens client-side over the entries the page already loaded, so
 * switching tabs costs nothing. Move it to a search param and a per-tab query
 * once the feed is longer than a page.
 */
function ActivityFeed({ entries }: { entries: readonly DashboardActivityEntry[] }) {
  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2 text-base">
          <IconActivity aria-hidden="true" className="size-4 text-muted-foreground" />
          Activity
        </CardTitle>
      </CardHeader>
      <CardContent>
        <Tabs defaultValue="all" className="gap-4">
          <TabsList variant="line" className="flex-wrap">
            {activityTabs.map((tab) => (
              <TabsTrigger key={tab.id} value={tab.id}>
                <tab.icon data-icon="inline-start" aria-hidden="true" />
                {tab.label}
              </TabsTrigger>
            ))}
          </TabsList>

          {activityTabs.map((tab) => {
            const kinds = tab.kinds;
            const visible = kinds ? entries.filter((entry) => kinds.includes(entry.kind)) : entries;

            return (
              <TabsContent key={tab.id} value={tab.id}>
                {visible.length > 0 ? (
                  <ul className="m-0 flex list-none flex-col p-0">
                    {visible.map((entry) => {
                      const EntryIcon = activityKindIcons[entry.kind];

                      return (
                        <li key={entry.id} className="border-b last:border-b-0">
                          <a
                            href={entry.href}
                            className="flex items-center gap-3 rounded-lg px-1 py-3 transition-colors hover:bg-muted/50"
                          >
                            <span
                              aria-hidden="true"
                              className="grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground"
                            >
                              <EntryIcon className="size-4" />
                            </span>
                            <Badge variant="secondary" className="shrink-0">
                              {entry.label}
                            </Badge>
                            <span className="min-w-0 flex-1 truncate text-sm font-medium">
                              {entry.title}
                            </span>
                            <span className="shrink-0 text-xs text-muted-foreground">
                              {entry.timestamp}
                            </span>
                          </a>
                        </li>
                      );
                    })}
                  </ul>
                ) : (
                  <p className="m-0 py-6 text-center text-sm text-muted-foreground">
                    Nothing here yet.
                  </p>
                )}
              </TabsContent>
            );
          })}
        </Tabs>
      </CardContent>
    </Card>
  );
}

export { ActivityFeed, DashboardChrome, NewMembersCarousel };
```


### lib/dashboard-data.ts

```ts
import {
  IconBriefcase,
  IconBrandDiscord,
  IconBrandWhatsapp,
  IconBook2,
  IconBuildingStore,
  IconCertificate,
  IconClock,
  IconCreditCard,
  IconFolders,
  IconGift,
  IconId,
  IconLayoutGrid,
  IconLayoutKanban,
  IconRocket,
  IconSchool,
  IconSearch,
  IconSend,
  IconSettings,
  IconShieldCheck,
  IconTarget,
  IconTrophy,
  IconUser,
  IconUsers,
  IconUsersGroup,
  type Icon,
} from "@tabler/icons-react";

import type {
  DashboardShellLocale,
  DashboardShellNavGroup,
  DashboardShellUser,
  DashboardShellWorkspace,
} from "@/components/dashboard-shell";

/** Tint applied to an icon chip or tile. Keys map to classes in the page. */
export type DashboardTone = "orange" | "blue" | "green" | "amber" | "violet";

/** One tile in the quick actions card. */
export type DashboardQuickAction = {
  id: string;
  label: string;
  description: string;
  href: string;
  icon: Icon;
  tone: DashboardTone;
};

/** One counter in the stats row. */
export type DashboardStat = {
  id: string;
  label: string;
  value: number;
  icon: Icon;
  tone: DashboardTone;
};

/** A member card in the "new members" carousel. */
export type DashboardMember = {
  id: string;
  name: string;
  role: string;
  href: string;
  avatarUrl: string;
};

/**
 * Category an activity entry belongs to. Drives both the feed tabs and the
 * icon each row renders, so entries stay serializable across the server and
 * client boundary.
 */
export type DashboardActivityKind = "training" | "marketplace" | "job" | "member";

/** One row in the activity feed. */
export type DashboardActivityEntry = {
  id: string;
  kind: DashboardActivityKind;
  label: string;
  title: string;
  href: string;
  timestamp: string;
};

/** A chat or social channel in the community card. */
export type DashboardCommunityChannel = {
  id: string;
  name: string;
  description: string;
  href: string;
  icon: Icon;
};

export const dashboardWorkspace: DashboardShellWorkspace = {
  id: "my-space",
  name: "My Space",
  meta: "Personal",
};

export const dashboardWorkspaces: readonly DashboardShellWorkspace[] = [
  dashboardWorkspace,
  { id: "co-labs", name: "Co-Labs", meta: "Cooperative" },
  { id: "atlas-collective", name: "Atlas Collective", meta: "Cooperative" },
];

export const dashboardUser: DashboardShellUser = {
  name: "Alex Moreau",
  email: "alex@example.com",
  avatarUrl: "https://api.dicebear.com/9.x/thumbs/svg?seed=alex-moreau",
};

export const dashboardLocales: readonly DashboardShellLocale[] = [
  { value: "en", label: "English" },
  { value: "fr", label: "Français" },
];

export const dashboardNav: readonly DashboardShellNavGroup[] = [
  {
    label: "Explore",
    items: [
      { label: "Dashboard", href: "/dashboard", icon: IconLayoutGrid, active: true },
      { label: "Marketplace", href: "/marketplace", icon: IconBuildingStore },
      { label: "Jobs", href: "/jobs", icon: IconBriefcase },
      { label: "Missions", href: "/missions", icon: IconTarget },
      { label: "Members", href: "/members", icon: IconUsers },
      { label: "Academy", href: "/academy", icon: IconSchool },
      { label: "Projects", href: "/projects", icon: IconFolders },
      { label: "Resources", href: "/resources", icon: IconBook2 },
    ],
  },
  {
    label: "My space",
    items: [
      { label: "Timesheet", href: "/timesheet", icon: IconClock },
      { label: "My teams", href: "/teams", icon: IconUsersGroup },
      { label: "My projects", href: "/my-projects", icon: IconLayoutKanban },
      { label: "My applications", href: "/applications", icon: IconSend },
      { label: "Referral program", href: "/referrals", icon: IconGift },
      { label: "Edit profile", href: "/profile", icon: IconUser },
      { label: "Billing", href: "/billing", icon: IconCreditCard },
      { label: "My shares", href: "/shares", icon: IconCertificate },
      { label: "Member card", href: "/member-card", icon: IconId },
    ],
  },
  {
    label: "Administration",
    items: [
      { label: "Moderation", href: "/admin/moderation", icon: IconShieldCheck },
      { label: "Settings", href: "/admin/settings", icon: IconSettings },
    ],
  },
];

export const dashboardQuickActions: readonly DashboardQuickAction[] = [
  {
    id: "post-need",
    label: "Post a need",
    description: "Looking for talent?",
    href: "/marketplace/new",
    icon: IconSearch,
    tone: "orange",
  },
  {
    id: "post-job",
    label: "Post a job",
    description: "Hiring for your team?",
    href: "/jobs/new",
    icon: IconBriefcase,
    tone: "blue",
  },
  {
    id: "start-project",
    label: "Start a project",
    description: "Launch something new",
    href: "/projects/new",
    icon: IconLayoutKanban,
    tone: "green",
  },
];

export const dashboardStats: readonly DashboardStat[] = [
  { id: "members", label: "Members", value: 47, icon: IconUsers, tone: "blue" },
  { id: "projects", label: "Projects", value: 6, icon: IconRocket, tone: "green" },
  { id: "jobs", label: "Jobs", value: 25, icon: IconBriefcase, tone: "amber" },
  { id: "missions", label: "Missions", value: 5, icon: IconTrophy, tone: "violet" },
];

export const dashboardMembers: readonly DashboardMember[] = [
  {
    id: "nina-berg",
    name: "Nina Berg",
    role: "Member",
    href: "/members/nina-berg",
    avatarUrl: "https://api.dicebear.com/9.x/thumbs/svg?seed=nina-berg",
  },
  {
    id: "omar-diallo",
    name: "Omar Diallo",
    role: "Member",
    href: "/members/omar-diallo",
    avatarUrl: "https://api.dicebear.com/9.x/thumbs/svg?seed=omar-diallo",
  },
  {
    id: "june-park",
    name: "June Park",
    role: "Member",
    href: "/members/june-park",
    avatarUrl: "https://api.dicebear.com/9.x/thumbs/svg?seed=june-park",
  },
  {
    id: "luca-rossi",
    name: "Luca Rossi",
    role: "Member",
    href: "/members/luca-rossi",
    avatarUrl: "https://api.dicebear.com/9.x/thumbs/svg?seed=luca-rossi",
  },
  {
    id: "sofia-mendes",
    name: "Sofia Mendes",
    role: "Member",
    href: "/members/sofia-mendes",
    avatarUrl: "https://api.dicebear.com/9.x/thumbs/svg?seed=sofia-mendes",
  },
  {
    id: "tomas-novak",
    name: "Tomas Novak",
    role: "Member",
    href: "/members/tomas-novak",
    avatarUrl: "https://api.dicebear.com/9.x/thumbs/svg?seed=tomas-novak",
  },
];

export const dashboardActivity: readonly DashboardActivityEntry[] = [
  {
    id: "intro-to-cybersec",
    kind: "training",
    label: "Training",
    title: "Intro to Cybersec",
    href: "/academy/intro-to-cybersec",
    timestamp: "6 days ago",
  },
  {
    id: "design-system-audit",
    kind: "marketplace",
    label: "Marketplace",
    title: "Design system audit for a fintech app",
    href: "/marketplace/design-system-audit",
    timestamp: "1 week ago",
  },
  {
    id: "senior-platform-engineer",
    kind: "job",
    label: "Job",
    title: "Senior platform engineer, remote",
    href: "/jobs/senior-platform-engineer",
    timestamp: "1 week ago",
  },
  {
    id: "nina-berg-joined",
    kind: "member",
    label: "Member",
    title: "Nina Berg joined the cooperative",
    href: "/members/nina-berg",
    timestamp: "2 weeks ago",
  },
  {
    id: "accessibility-review",
    kind: "marketplace",
    label: "Marketplace",
    title: "Accessibility review for a public service portal",
    href: "/marketplace/accessibility-review",
    timestamp: "2 weeks ago",
  },
  {
    id: "data-engineer-contract",
    kind: "job",
    label: "Job",
    title: "Data engineer, 6 month contract",
    href: "/jobs/data-engineer-contract",
    timestamp: "3 weeks ago",
  },
];

export const dashboardCommunityChannels: readonly DashboardCommunityChannel[] = [
  {
    id: "discord",
    name: "Discord",
    description: "Chat with members in real time",
    href: "https://discord.com",
    icon: IconBrandDiscord,
  },
  {
    id: "whatsapp",
    name: "WhatsApp",
    description: "Join the members channel",
    href: "https://whatsapp.com",
    icon: IconBrandWhatsapp,
  },
];
```



## Usage

The default landing page for a signed-in workspace, rendered inside
[`dashboard-shell`](/layouts/dashboard-shell): a quick actions card, a row of stat counters, a paged
carousel of new members, and an activity feed with category tabs beside a community card.

```sh
npx shadcn@latest add @uptoolkit/dashboard-page
```

Installs `app/dashboard/page.tsx` plus the `DashboardChrome`, `NewMembersCarousel`, and
`ActivityFeed` client components, the `dashboard-data` fixtures, and the shell itself.

### Wiring it up

`page.tsx` is a server component. It reads the fixtures in `dashboard-data.ts` and lays out the
sections, so swapping in real data is one file:

```tsx
export default async function Page() {
  const [stats, members, activity] = await Promise.all([
    fetchWorkspaceStats(),
    fetchNewMembers({ limit: 6 }),
    fetchActivity({ limit: 20 }),
  ]);

  // ...pass each into the sections in place of dashboardStats, dashboardMembers, dashboardActivity
}
```

Icons stay out of the data that crosses into the client: activity rows carry a `kind` string and
`dashboard-panels.tsx` maps it to an icon, so the entries remain serializable. Keep that split when
you add a category.

### The shell boundary

The shell takes callbacks, so it is mounted from `DashboardChrome` rather than from the page. That
keeps the page a server component. `DashboardChrome`'s handlers are stubs — point `onSearch` at your
command palette, `onCreateClick` at your create menu, and `onLocaleChange` at wherever the locale is
persisted.

If every route under `/dashboard` shares the shell, lift `DashboardChrome` into
`app/dashboard/layout.tsx` and let this page return only its sections.

### Sections

`dashboard-data.ts` drives all four sections, and each one takes its records as a prop, so dropping
a section means deleting its element from `page.tsx` and its fixture. Quick action tiles and stat
chips pick their tint from a `tone` key mapped to classes at the top of `page.tsx` — add a tone by
adding one entry to each of the two records.

