Skip to content

Analytics dashboard

A complete app shell, not just a card grid: a collapsible sidebar with sectioned navigation (Nav) and an account menu (Dropdown), a header with search and a user menu, a merged balance-plus-wallet card beside two stat cards, an earnings chart, savings-goal progress bars, and a transactions table — every one of them a real oks-ui component wired together, not a static mockup.

analytics-dashboard.tsx

Built with

How it’s built

  1. 01The sidebar is a real Nav — items is a tree of { key, label, icon, badge, isSection, children }, selectedKey/onItemSelect control the active row, and isCollapsed switches the whole thing to an icon-only rail. The small dot beside the active label is real too, not CSS on [data-active] — Nav's renderItem prop hands back { isActive, content, props } for full control per row.
  2. 02Nav's own CSS sets a fixed width: 15rem on itself regardless of its container (--oks-nav-width) — inside a flex column narrower than that, it silently overflows the sidebar sideways. Fixed the same way as any other width override in this project: classNames={{ base: "w-full" }}, which wins over the library's own :where()-scoped rule on specificity alone.
  3. 03This is a real app shell, not a page that happens to have a sidebar: the outer wrapper has a bounded height (h-[46rem]) and overflow-hidden, the sidebar and header get their own overflow-y-auto/shrink-0, and only the middle content column scrolls. Without that, the whole thing — sidebar included — would scroll away as one long document, defeating the point of persistent nav. The sidebar-collapse toggle lives in the header (before the search field), not inside the sidebar itself.
  4. 04Settings / Help desk / Log out, and every kebab ("⋯") menu, and the currency selector are all real Dropdowns — none of them were wired up in an earlier pass, which meant clicking them did nothing. Dropdown's own wrapper is display: inline-block by default (shrink-to-fit) — classNames={{ content: "block w-full" }} plus className="w-full" on Dropdown itself is what makes the account-menu trigger actually span the sidebar's full width instead of hugging its own content.
  5. 05Account balance and My wallet are one card, not two separate ones stacked in different grid rows — @3xl:row-span-2 on that single card is what lets it sit beside Total expenses/Total savings (row 1) and Overview (row 2). The two subsections are separated by spacing alone, deliberately with no border-t rule between them — a hard divider would visually re-split what's meant to read as one continuous section.
  6. 06Send money / Request money use flex-1 on both buttons for a genuine 50/50 split, instead of each button sizing to its own label's width and ending up visibly uneven.
  7. 07The account avatar is the user's own photo (src=), and the sidebar/header logo is the real /logo.svg static asset — this is the maintainer's own branded copy of the pattern, not stock photography or a fictional placeholder person.
  8. 08The header search is a real FormFieldSet type="search" with startIcon/endIcon (two separate <kbd> chips), not a styled <input> — classNames={{ control: "border-transparent bg-paper-sunken" }} swaps its default bordered box for a filled, borderless one.
  9. 09Every delta ("+4.8%", "-1.6%") is a real Chip with a directional arrow icon in endContent — a shared DeltaChip helper keeps the up/down colour and icon in one place.
  10. 10The overflow menus are a shared MoreButton — a real Dropdown behind a Button isIconOnly variant="ghost" (no border, matching the reference's minimal treatment) with a few generic actions, reused on both stat cards, each wallet card, the savings plan, the overview chart, and each transaction row.
  11. 11Account balance / Total expenses / Total savings sit in one row (grid-cols-[1.25fr_1fr_1fr]) with items-start, not the grid default align-items: stretch — stretching would force the shorter stat cards (or the merged card) to match a sibling's height instead of each keeping its own natural size, which is exactly what caused dead space in an earlier pass.
  12. 12Wallet status ("Active"/"Inactive") is plain coloured text through --oks-color-<role>-600, not a filled Chip — a deliberately lighter treatment than the Chip used for transaction status.
  13. 13Every icon badge is a real Avatar with icon=, coloured via the color prop — each transaction row uses a different semantic colour so the list reads as varied activity, not one repeated icon.
  14. 14Every heading is a real PageTitle, always with classNames={{ base: "flex-col items-start" }} — PageTitle's own wrapper is display: inline-flex with no set direction, built for a compact inline icon+title, not a stacked heading.
  15. 15The sidebar is hidden below the @3xl container width so the pattern still reads correctly scaled down inside a gallery-card thumbnail, rather than squeezing a 240px rail into a few dozen pixels.
  16. 16Table's classNames={{ cell: "whitespace-nowrap" }} keeps every cell on one line — without it, "Software license" or a date wraps across two or three lines the moment the column narrows. Table's own container already has overflow: auto built in, so once cells can't wrap, a table wider than its card scrolls horizontally instead — the fix is stopping the wrap, not adding scroll that was already there.
  17. 17My savings plan carries four goals, each with its own semantic color driving both the Avatar icon badge and the Progress bar — varied colour reads as varied activity, and keeps the card from looking sparse beside the taller Recent transactions table.
  18. 18Every direct child of the scrollable content body carries shrink-0. A flex child defaults to flex-shrink: 1, so without it the browser silently compresses these sections to fit the available space instead of letting them overflow — and the parent's overflow-y-auto never has anything to scroll.

Source

Three files · copy & paste

Styling: this source uses Tailwind utility classes and this site’s colour tokens (for example text-ink, bg-paper and border-line). The oks-ui components render without them; swap those classes for your own.

analytics-dashboard.tsx
"use client";

import { type ReactNode, useState } from "react";
import { Avatar } from "oks-ui/avatar";
import { Button } from "oks-ui/button";
import { Card, CardBody, CardHeader } from "oks-ui/card";
import { Chart } from "oks-ui/chart";
import { Chip } from "oks-ui/chip";
import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from "oks-ui/dropdown";
import { FormFieldSet } from "oks-ui/form-field-set";
import type { NavItemData } from "oks-ui/nav";
import { PageTitle } from "oks-ui/page-title";
import { Progress } from "oks-ui/progress";
import { Table } from "oks-ui/table";
import "oks-ui/avatar.css";
import "oks-ui/button.css";
import "oks-ui/card.css";
import "oks-ui/chart.css";
import "oks-ui/chip.css";
import "oks-ui/dropdown.css";
import "oks-ui/form-field-set.css";
import "oks-ui/page-title.css";
import "oks-ui/progress.css";
import "oks-ui/table.css";
import { AppShell } from "./AppShell";
import {
  ArrowDownLeftIcon,
  ArrowDownRightIcon,
  ArrowsRightLeftIcon,
  ArrowUpRightIcon,
  BuildingIcon,
  CalendarIcon,
  ChevronDownIcon,
  ClockIcon,
  CreditCardIcon,
  DownloadIcon,
  GlobeIcon,
  GridIcon,
  HelpCircleIcon,
  MailIcon,
  MessageCircleIcon,
  MoreIcon,
  PieChartIcon,
  PlusIcon,
  ReceiptIcon,
  SearchIcon,
  SendIcon,
} from "./icons";

const NAV_ITEMS: NavItemData[] = [
  {
    key: "main",
    label: "Main menu",
    isSection: true,
    children: [
      { key: "dashboard", label: "Dashboard", icon: <GridIcon size={18} /> },
      { key: "analytics", label: "Analytics", icon: <PieChartIcon size={18} />, badge: 20 },
      { key: "transactions", label: "Transactions", icon: <ArrowsRightLeftIcon size={18} /> },
      { key: "invoices", label: "Invoices", icon: <ReceiptIcon size={18} /> },
    ],
  },
  {
    key: "features",
    label: "Features",
    isSection: true,
    children: [
      { key: "recurring", label: "Recurring", icon: <ClockIcon size={18} />, badge: 16 },
      { key: "subscriptions", label: "Subscriptions", icon: <CreditCardIcon size={18} /> },
      { key: "feedback", label: "Feedback", icon: <MessageCircleIcon size={18} /> },
    ],
  },
  // Settings / Help desk / Log out live in the account menu at the bottom
  // of the sidebar (opened from the profile footer), not as permanent nav
  // rows — they're account actions, not app destinations.
];

const wallets = [
  { flag: "🇺🇸", code: "USD", balance: "$22,678.00", status: "Active" as const },
  { flag: "🇪🇺", code: "EUR", balance: "€14,320.00", status: "Active" as const },
  { flag: "🇬🇧", code: "GBP", balance: "£9,540.00", status: "Active" as const },
  { flag: "🇯🇵", code: "JPY", balance: "¥1,240,000", status: "Inactive" as const },
];

const CURRENCIES = wallets.map((w) => ({ flag: w.flag, code: w.code }));

const earnings = [
  { month: "Jan", value: 21400 }, { month: "Feb", value: 24800 }, { month: "Mar", value: 19600 },
  { month: "Apr", value: 27200 }, { month: "May", value: 16800 }, { month: "Jun", value: 25400 },
  { month: "Jul", value: 18200 }, { month: "Aug", value: 34900 }, { month: "Sep", value: 23100 },
  { month: "Oct", value: 15600 }, { month: "Nov", value: 29800 }, { month: "Dec", value: 20200 },
];

const savingsGoals = [
  { id: 1, icon: <PieChartIcon size={18} />, label: "Investment goal", saved: "$15,600", target: "$25,000", pct: 62, color: "primary" as const },
  { id: 2, icon: <ReceiptIcon size={18} />, label: "Emergency fund", saved: "$8,400", target: "$12,000", pct: 70, color: "danger" as const },
  { id: 3, icon: <GlobeIcon size={18} />, label: "Vacation fund", saved: "$3,200", target: "$6,000", pct: 53, color: "info" as const },
  { id: 4, icon: <BuildingIcon size={18} />, label: "Home renovation", saved: "$11,400", target: "$20,000", pct: 57, color: "success" as const },
];

type TxnRow = {
  id: number;
  activity: string;
  icon: ReactNode;
  color: "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "default";
  date: string;
  amount: string;
  status: "Success" | "Pending" | "Failed";
} & Record<string, unknown>;

const transactions: TxnRow[] = [
  { id: 1, activity: "Software license", icon: <CreditCardIcon size={16} />, color: "secondary" as const, date: "Wed, 10 Jun-2026", amount: "$89.00", status: "Success" },
  { id: 2, activity: "Mobile app purchase", icon: <GridIcon size={16} />, color: "info" as const, date: "Tue, 09 Jun-2026", amount: "$46.50", status: "Success" },
  { id: 3, activity: "Grocery purchase", icon: <ReceiptIcon size={16} />, color: "warning" as const, date: "Sun, 07 Jun-2026", amount: "$132.00", status: "Success" },
  { id: 4, activity: "Freelance payment", icon: <ArrowDownLeftIcon size={16} />, color: "success" as const, date: "Fri, 05 Jun-2026", amount: "$2,150.00", status: "Success" },
  { id: 5, activity: "Subscription renewal", icon: <ClockIcon size={16} />, color: "default" as const, date: "Wed, 03 Jun-2026", amount: "$14.99", status: "Pending" },
  { id: 6, activity: "Wire transfer", icon: <ArrowsRightLeftIcon size={16} />, color: "danger" as const, date: "Mon, 01 Jun-2026", amount: "$500.00", status: "Failed" },
];

const statusTone: Record<TxnRow["status"], "success" | "warning" | "danger"> = {
  Success: "success",
  Pending: "warning",
  Failed: "danger",
};

const DeltaChip = ({ delta, up }: { delta: string; up: boolean }) => {
  return (
    <Chip
      size="sm"
      variant="soft"
      color={up ? "success" : "danger"}
      endContent={up ? <ArrowUpRightIcon size={12} /> : <ArrowDownRightIcon size={12} />}
    >
      {delta}
    </Chip>
  );
};

const MoreButton = ({ label, size = "sm" }: { label: string; size?: "xs-sm" | "sm" }) => {
  return (
    <Dropdown placement="bottom-end">
      <DropdownTrigger>
        <Button isIconOnly aria-label={label} variant="ghost" radius="full" size={size}>
          <MoreIcon size={size === "xs-sm" ? 14 : 16} />
        </Button>
      </DropdownTrigger>
      <DropdownMenu aria-label={label}>
        <DropdownItem key="view">View details</DropdownItem>
        <DropdownItem key="export">Export</DropdownItem>
        <DropdownItem key="dismiss" classNames={{ title: "text-[color:var(--oks-color-danger-700)]" }}>
          Dismiss
        </DropdownItem>
      </DropdownMenu>
    </Dropdown>
  );
};

const StatCard = ({
  icon,
  title,
  value,
  delta,
  up,
}: {
  icon: ReactNode;
  title: string;
  value: string;
  delta: string;
  up: boolean;
}) => {
  return (
    <Card shadow="sm" className="h-full">
      <CardBody className="flex flex-col gap-4">
        <div className="flex items-center justify-between gap-3">
          <div className="flex items-center gap-2.5">
            <Avatar icon={icon as never} color="primary" radius="md" size="sm" />
            <PageTitle
              as="h3"
              title={title}
              classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
            />
          </div>
          <MoreButton label={`${title} options`} />
        </div>
        <div>
          <p className="text-[1.7rem] font-bold tracking-tight text-ink">{value}</p>
          <div className="mt-2 flex items-center gap-2 text-[0.8rem] text-ink-muted">
            <DeltaChip delta={delta} up={up} />
            from last month
          </div>
        </div>
      </CardBody>
    </Card>
  );
};

const AnalyticsDashboard = () => {
  const [selectedKey, setSelectedKey] = useState("dashboard");
  const [currency, setCurrency] = useState("USD");
  const activeCurrency = CURRENCIES.find((c) => c.code === currency) ?? CURRENCIES[0];

  return (
    <AppShell
      navItems={NAV_ITEMS}
      selectedNavKey={selectedKey}
      onSelectedNavKeyChange={setSelectedKey}
      headerLeft={
        <FormFieldSet
          type="search"
          name="search"
          label="Search"
          placeholder="Search"
          startIcon={<SearchIcon size={16} />}
          endIcon={
            <span className="flex items-center gap-1" aria-hidden="true">
              <kbd className="rounded-[var(--r-sm)] bg-paper px-1.5 py-0.5 text-[0.68rem] font-medium text-ink-faint">⌘</kbd>
              <kbd className="rounded-[var(--r-sm)] bg-paper px-1.5 py-0.5 text-[0.68rem] font-medium text-ink-faint">K</kbd>
            </span>
          }
          classNames={{
            base: "w-full max-w-xs",
            label: "sr-only",
            control: "border-transparent bg-paper-sunken",
          }}
        />
      }
      headerExtra={
        <>
          <Button isIconOnly aria-label="Help" variant="ghost" radius="full" size="sm">
            <HelpCircleIcon size={18} />
          </Button>
          <Button isIconOnly aria-label="Messages" variant="ghost" radius="full" size="sm">
            <MailIcon size={18} />
          </Button>
        </>
      }
    >
      {/* welcome banner. shrink-0 on every direct child of this
          scroll body: a flex child defaults to flex-shrink:1 and
          gets silently compressed to fit its allocated space instead
          of overflowing, so without this the parent's overflow-y-auto
          never sees anything to scroll and content gets clipped by
          the outer overflow-hidden shell instead. */}
      <div className="flex shrink-0 flex-wrap items-start justify-between gap-4">
        <PageTitle
          as="h2"
          title="Welcome back, Omkar"
          subtitle="Monitor and control what happens with your money today."
          classNames={{
            base: "flex-col items-start",
            title: "text-2xl font-bold tracking-tight text-ink",
            subtitle: "text-[0.9rem] text-ink-muted",
          }}
        />
        <div className="flex items-center gap-2">
          <Button variant="bordered" radius="full" size="sm" startContent={<CalendarIcon size={14} />}>
            Fri, 12 June 2026
          </Button>
          <Button color="primary" radius="full" size="sm" startContent={<DownloadIcon size={14} />}>
            Export
          </Button>
        </div>
      </div>

      {/* One grid, not two stacked ones: Account balance + My wallet is
          a SINGLE card spanning both rows (@3xl:row-span-2), sitting
          beside Total expenses / Total savings (row 1) and Overview
          (row 2, spanning the same two narrower columns). Splitting
          "Account balance" and "My wallet" into separate cards was the
          actual bug — they're one continuous section in the
          reference, not two. */}
      <div className="grid shrink-0 items-start gap-5 @3xl:grid-cols-[1.25fr_1fr_1fr] @3xl:grid-rows-[auto_auto]">
        <Card shadow="sm" className="@3xl:row-span-2">
          <CardBody className="flex flex-col gap-5">
            <div className="flex items-center justify-between gap-3">
              <div className="flex items-center gap-2.5">
                <Avatar icon={<CreditCardIcon size={18} />} color="primary" radius="md" size="sm" />
                <PageTitle
                  as="h3"
                  title="Account balance"
                  classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
                />
              </div>
              <Dropdown placement="bottom-end">
                <DropdownTrigger>
                  <Button
                    variant="bordered"
                    radius="full"
                    size="xs"
                    startContent={<span aria-hidden="true">{activeCurrency.flag}</span>}
                    endContent={<ChevronDownIcon size={14} />}
                  >
                    {activeCurrency.code}
                  </Button>
                </DropdownTrigger>
                <DropdownMenu
                  aria-label="Choose currency"
                  selectionMode="single"
                  selectedKeys={[currency]}
                  onAction={(key) => setCurrency(String(key))}
                >
                  {CURRENCIES.map((c) => (
                    <DropdownItem key={c.code} startContent={<span aria-hidden="true">{c.flag}</span>}>
                      {c.code}
                    </DropdownItem>
                  ))}
                </DropdownMenu>
              </Dropdown>
            </div>
            <div>
              <p className="text-[2rem] font-bold tracking-tight text-ink">$48,920.15</p>
              <div className="mt-2 flex items-center gap-2 text-[0.8rem] text-ink-muted">
                <DeltaChip delta="+4.8%" up />
                from last month
              </div>
            </div>
            {/* flex-1 on both: a real 50/50 split instead of each
                button hugging its own label width. */}
            <div className="flex gap-2">
              <Button color="primary" radius="full" size="xs" className="flex-1" startContent={<SendIcon size={12} />}>
                Send money
              </Button>
              <Button variant="bordered" radius="full" size="xs" className="flex-1" startContent={<ArrowDownLeftIcon size={12} />}>
                Request money
              </Button>
            </div>

            <div className="mt-1 flex flex-col gap-3">
              <div className="flex items-center justify-between">
                <PageTitle
                  as="h3"
                  title="My wallet"
                  classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
                />
                <Button variant="bordered" radius="full" size="xs" startContent={<PlusIcon size={12} />}>
                  Add new
                </Button>
              </div>
              <div className="grid grid-cols-2 gap-3">
                {wallets.map((w) => (
                  <div key={w.code} className="relative rounded-[var(--r-md)] bg-paper-sunken p-3.5">
                    <div className="absolute right-1.5 top-1.5">
                      <MoreButton label={`${w.code} wallet options`} size="xs-sm" />
                    </div>
                    <div className="flex items-center gap-2 text-[0.85rem] font-medium text-ink">
                      <span aria-hidden="true">{w.flag}</span>
                      {w.code}
                    </div>
                    <p className="mt-2 text-[1.05rem] font-semibold text-ink">{w.balance}</p>
                    <p
                      className="mt-2 text-[0.8rem] font-medium"
                      style={{ color: `var(--oks-color-${w.status === "Active" ? "success" : "warning"}-700)` }}
                    >
                      {w.status}
                    </p>
                  </div>
                ))}
              </div>
            </div>
          </CardBody>
        </Card>

        <StatCard icon={<ArrowsRightLeftIcon size={18} />} title="Total expenses" value="$12,340.60" delta="-1.6%" up={false} />
        <StatCard icon={<PieChartIcon size={18} />} title="Total savings" value="$22,150.40" delta="+6.2%" up />

        <Card shadow="sm" className="@3xl:col-span-2">
          <CardHeader className="flex flex-wrap items-center justify-between gap-2">
            <PageTitle
              as="h3"
              title="Overview"
              classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
            />
            <div className="flex items-center gap-3">
              <span className="flex items-center gap-1.5 text-[0.78rem] text-ink-muted">
                <span
                  aria-hidden="true"
                  className="h-2 w-2 rounded-full"
                  style={{ background: "var(--oks-color-primary-600)" }}
                />
                Earnings
              </span>
              <Button variant="ghost" size="sm" endContent={<ChevronDownIcon size={14} />}>
                This year
              </Button>
              <MoreButton label="Overview options" />
            </div>
          </CardHeader>
          <CardBody>
            <Chart type="column" data={earnings} x="month" series="value" legend={false} height={220} />
          </CardBody>
        </Card>
      </div>

      {/* My savings plan / Recent transactions — same 3-column template
          again, so all three rows share one consistent set of column
          edges down the page. */}
      <div className="grid shrink-0 gap-5 @3xl:grid-cols-[1.25fr_1fr_1fr]">
        <Card shadow="sm">
          <CardHeader className="flex items-center justify-between">
            <PageTitle
              as="h3"
              title="My savings plan"
              classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
            />
            <MoreButton label="Savings plan options" />
          </CardHeader>
          <CardBody className="flex flex-col gap-6">
            {savingsGoals.map((g) => (
              <div key={g.id} className="flex flex-col gap-2.5">
                <div className="flex items-center gap-2.5">
                  <Avatar icon={g.icon} color={g.color} radius="md" size="sm" />
                  <p className="text-[0.85rem] font-medium text-ink">{g.label}</p>
                </div>
                <div className="flex items-center justify-between text-[0.85rem]">
                  <span className="text-ink-muted">
                    {g.saved} / {g.target}
                  </span>
                  <span className="font-semibold text-ink">{g.pct}%</span>
                </div>
                {/* aria-hidden: the label + "$saved / $target" + "N%" above
                    already say everything this bar would announce again. */}
                <Progress value={g.pct} color={g.color} size="sm" aria-hidden="true" />
              </div>
            ))}
          </CardBody>
        </Card>

        <Card shadow="sm" className="@3xl:col-span-2">
          <CardHeader className="flex items-center justify-between">
            <PageTitle
              as="h3"
              title="Recent transactions"
              classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
            />
            <Button variant="bordered" size="sm">Filter</Button>
          </CardHeader>
          <CardBody>
            <Table
              aria-label="Recent transactions"
              classNames={{ cell: "whitespace-nowrap" }}
              columns={[
                {
                  key: "activity",
                  header: "Activity",
                  render: (r: TxnRow) => (
                    <div className="flex items-center gap-2.5">
                      <Avatar icon={r.icon as never} color={r.color} radius="md" size="sm" />
                      <span className="font-medium text-ink">{r.activity}</span>
                    </div>
                  ),
                },
                { key: "date", header: "Date" },
                { key: "amount", header: "Amount", align: "end" },
                {
                  key: "status",
                  header: "Status",
                  render: (r: TxnRow) => (
                    <Chip size="sm" variant="soft" color={statusTone[r.status]}>
                      {r.status}
                    </Chip>
                  ),
                },
                {
                  key: "actions",
                  header: "",
                  ariaLabel: "Actions",
                  align: "end",
                  render: (r: TxnRow) => <MoreButton label={`${r.activity} options`} size="xs-sm" />,
                },
              ]}
              rows={transactions}
              getRowKey={(r) => r.id}
            />
          </CardBody>
        </Card>
      </div>
    </AppShell>
  );
};

export default AnalyticsDashboard;

It imports ./icons, so save this next to it:

icons.tsx
export const ArrowDownLeftIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M17 7 7 17" />
      <path d="M17 17H7V7" />
    </svg>
  );
};

export const ArrowDownRightIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2.5}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M7 7l10 10" />
      <path d="M17 7v10H7" />
    </svg>
  );
};

export const ArrowsRightLeftIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="m17 2 4 4-4 4" />
      <path d="M3 6h18" />
      <path d="m7 22-4-4 4-4" />
      <path d="M21 18H3" />
    </svg>
  );
};

export const ArrowUpRightIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2.5}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M7 17 17 7" />
      <path d="M7 7h10v10" />
    </svg>
  );
};

export const BuildingIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M6 22V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v18" />
      <path d="M2 22h20" />
      <path d="M9 6h1M14 6h1M9 10h1M14 10h1M9 14h1M14 14h1" />
      <path d="M10 22v-4h4v4" />
    </svg>
  );
};

// Icons below this line were added for Nova's rebuild into a fan-community
// events platform -- same "no icon library dependency" convention above.

export const CalendarIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <rect x="3" y="5" width="18" height="16" rx="2" />
      <path d="M3 10h18M8 3v4M16 3v4" />
    </svg>
  );
};

export const ChevronDownIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="m6 9 6 6 6-6" />
    </svg>
  );
};

export const ClockIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <circle cx="12" cy="12" r="9" />
      <path d="M12 7v5l3 3" />
    </svg>
  );
};

export const CreditCardIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <rect x="2" y="5" width="20" height="14" rx="2" />
      <path d="M2 10h20" />
    </svg>
  );
};

export const DownloadIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M12 3v12M7 10l5 5 5-5M5 21h14" />
    </svg>
  );
};

export const GlobeIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <circle cx="12" cy="12" r="10" />
      <path d="M2 12h20" />
      <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10Z" />
    </svg>
  );
};

export const GridIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <rect x="3" y="3" width="7" height="7" rx="1.5" />
      <rect x="14" y="3" width="7" height="7" rx="1.5" />
      <rect x="3" y="14" width="7" height="7" rx="1.5" />
      <rect x="14" y="14" width="7" height="7" rx="1.5" />
    </svg>
  );
};

export const HelpCircleIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <circle cx="12" cy="12" r="9" />
      <path d="M9.5 9a2.5 2.5 0 0 1 4.8 1c0 1.5-2.3 1.8-2.3 3.5" />
      <path d="M12 17.5h.01" />
    </svg>
  );
};

// Icons below this line were added for Horizon's rebuild into a business
// back-office (Finance/Accounts/HR and Payroll/Peoples/Contracts) --
// same "no icon library dependency" convention as everything above.

export const MailIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <rect x="2" y="4" width="20" height="16" rx="2" />
      <path d="m2 7 10 6 10-6" />
    </svg>
  );
};

export const MessageCircleIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5Z" />
    </svg>
  );
};

export const MoreIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
    >
      <circle cx="5" cy="12" r="1.6" />
      <circle cx="12" cy="12" r="1.6" />
      <circle cx="19" cy="12" r="1.6" />
    </svg>
  );
};

export const PieChartIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M21.21 15.89A10 10 0 1 1 8 2.83" />
      <path d="M22 12A10 10 0 0 0 12 2v10z" />
    </svg>
  );
};

export const PlusIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M12 5v14M5 12h14" />
    </svg>
  );
};

export const ReceiptIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M5 3v18l2-1.5L9 21l2-1.5L13 21l2-1.5L17 21l2-1.5V3l-2 1.5L15 3l-2 1.5L11 3 9 4.5 7 3 5 4.5Z" />
      <path d="M8 8h8M8 12h8" />
    </svg>
  );
};

export const SearchIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <circle cx="11" cy="11" r="7" />
      <path d="M21 21l-4.3-4.3" />
    </svg>
  );
};

export const SendIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="m22 2-7 20-4-9-9-4Z" />
      <path d="M22 2 11 13" />
    </svg>
  );
};

// Icons below this line were added for Slate's rebuild into a personal
// finance & invoicing app -- same "no icon library dependency" convention
// above.

export const BellIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M6 8a6 6 0 0 1 12 0c0 4 1.5 5.5 2 6H4c.5-.5 2-2 2-6Z" />
      <path d="M10 20a2 2 0 0 0 4 0" />
    </svg>
  );
};

export const SettingsIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <circle cx="12" cy="12" r="3" />
      <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1.08-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c.26.42.7.7 1.19.83l.32.08c.6.15 1 .68 1 1.3v0" />
    </svg>
  );
};

It imports ./AppShell, so save this next to it:

AppShell.tsx
"use client";

import { useState, type ReactNode } from "react";
import { Avatar } from "oks-ui/avatar";
import { Button } from "oks-ui/button";
import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from "oks-ui/dropdown";
import { FormFieldSet } from "oks-ui/form-field-set";
import { Nav } from "oks-ui/nav";
import type { NavItemData } from "oks-ui/nav";
import "oks-ui/avatar.css";
import "oks-ui/button.css";
import "oks-ui/dropdown.css";
import "oks-ui/form-field-set.css";
import "oks-ui/nav.css";
import { BellIcon, HelpCircleIcon, SearchIcon, SettingsIcon } from "./icons";

// Two overlapping chevrons — a standard "collapse sidebar" glyph, not a
// reproduction of any specific icon set's artwork.
const CollapseIcon = () => {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true">
      <path d="M6 3 2 8l4 5" />
      <path d="M12 3 8 8l4 5" />
    </svg>
  );
};

const LogOutIcon = ({ size = 16 }: { size?: number }) => {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M6 14H3.5a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1H6" />
      <path d="M10.5 11 14 8l-3.5-3" />
      <path d="M14 8H6" />
    </svg>
  );
};

export type AppShellProps = {
  /** The sidebar tree — differs per pattern to match its app context (a Kanban tool's nav reads differently from an invoicing tool's). */
  navItems: NavItemData[];
  /** Omitted when the pattern being shown isn't one of the permanent nav rows (e.g. a Settings page reached from the account menu, not the sidebar itself). */
  selectedNavKey?: string;
  /** Lets a pattern make its own Nav row genuinely clickable (analytics-dashboard highlights whichever item — or account-menu action — was clicked last) instead of a fixed selectedNavKey. */
  onSelectedNavKeyChange?: (key: string) => void;
  /** Left side of the header, after the collapse toggle. Defaults to a plain search field; command-palette-shell overrides this with its ⌘K trigger button instead. */
  headerLeft?: ReactNode;
  /** Right side of the header, after the standard bell. Defaults to nothing extra. */
  headerExtra?: ReactNode;
  /** The scrollable main content (everything below the header). */
  children: ReactNode;
  /** Extra classes for the scrollable content wrapper — e.g. to drop the default padding for a pattern that wants edge-to-edge content. */
  contentClassName?: string;
};

/**
 * The one persistent sidebar/header app shell every "App screens" pattern on
 * this site shares. Pulled out of each individual pattern file after the
 * per-file copies drifted (one ended up with a different account name than
 * the rest) — the account identity (Omkar Sahu) and the structural chrome
 * (collapse toggle, Nav wiring, account Dropdown, bell) now live in exactly
 * one place, while each pattern still supplies its own contextual nav items
 * and can override the header's search slot when it needs to (the command
 * palette trigger, for instance).
 */
export const AppShell = ({
  navItems,
  selectedNavKey,
  onSelectedNavKeyChange,
  headerLeft,
  headerExtra,
  children,
  contentClassName,
}: AppShellProps) => {
  const [collapsed, setCollapsed] = useState(false);

  return (
    // @container has to live on its OWN wrapper, not the element whose
    // height is conditioned by @3xl: — an element can't query its own
    // container for its own styling (circular per the CSS Containment
    // spec), so @3xl:h-[46rem] would silently be evaluated against the
    // wrong (ancestor) container and never apply.
    <div className="@container w-full">
      <div className="flex h-[32rem] w-full overflow-hidden bg-paper @3xl:h-[46rem]">
        <aside
          role="none"
          className={`hidden h-full shrink-0 flex-col gap-6 overflow-y-auto overflow-x-hidden border-r border-line bg-paper-raised p-4 @3xl:flex ${collapsed ? "w-[4.5rem]" : "w-60"}`}
        >
          <div className="flex items-center px-1">
            {/* Two files, swapped by theme — logo.svg carries dark ink and would
                vanish against the dark sidebar. eslint-disable: fixed static
                assets already sized for this slot; next/image's runtime cost
                isn't worth it here. */}
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img src="/logo.svg" alt="oks-ui" className="oks-logo-for-light h-8 w-8 shrink-0" />
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img src="/logo-light.svg" alt="oks-ui" className="oks-logo-for-dark h-8 w-8 shrink-0" />
          </div>

          <Nav
            aria-label="Main"
            items={navItems}
            selectedKey={selectedNavKey}
            onItemSelect={onSelectedNavKeyChange ? (item) => onSelectedNavKeyChange(item.key) : undefined}
            isCollapsed={collapsed}
            classNames={{ base: "w-full", sectionTitle: collapsed ? "sr-only" : undefined }}
            renderItem={({ isActive, content, props }) => (
              <button type="button" {...props} className={`${props.className} flex items-center gap-1.5`}>
                {!collapsed ? (
                  <span
                    aria-hidden="true"
                    className="h-1.5 w-1.5 shrink-0 rounded-full"
                    style={{ background: isActive ? "var(--oks-color-primary-600)" : "transparent" }}
                  />
                ) : null}
                {content}
              </button>
            )}
          />

          <Dropdown placement="top-start" className="mt-auto w-full" classNames={{ content: "block w-full" }}>
            <DropdownTrigger>
              <Button
                variant="ghost"
                fullWidth
                className={`gap-2.5 p-2 ${collapsed ? "justify-center" : "justify-start"}`}
              >
                <Avatar src="/avatar-omkar.jpg" name="Omkar Sahu" size="sm" radius="full" />
                {!collapsed ? (
                  <span className="min-w-0 flex-1 text-left">
                    <span className="block truncate text-[0.85rem] font-medium text-ink">Omkar Sahu</span>
                    <span className="block truncate text-[0.75rem] text-ink-muted">me@oks-ui.com</span>
                  </span>
                ) : null}
              </Button>
            </DropdownTrigger>
            <DropdownMenu
              aria-label="Account menu"
              onAction={onSelectedNavKeyChange ? (key) => onSelectedNavKeyChange(String(key)) : undefined}
            >
              <DropdownItem key="settings" startContent={<SettingsIcon size={16} />}>
                Settings
              </DropdownItem>
              <DropdownItem key="help" startContent={<HelpCircleIcon size={16} />} showDivider>
                Help desk
              </DropdownItem>
              <DropdownItem key="logout" startContent={<LogOutIcon size={16} />} classNames={{ title: "text-[color:var(--oks-color-danger-700)]" }}>
                Log out
              </DropdownItem>
            </DropdownMenu>
          </Dropdown>
        </aside>

        <div className="flex min-w-0 flex-1 flex-col overflow-hidden">
          <header className="flex shrink-0 flex-wrap items-center justify-between gap-3 border-b border-line px-5 py-3.5 @3xl:px-8">
            <div className="flex min-w-0 flex-1 items-center gap-3">
              <Button
                isIconOnly
                variant="ghost"
                size="sm"
                onClick={() => setCollapsed((c) => !c)}
                aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
                className="hidden @3xl:flex"
              >
                <CollapseIcon />
              </Button>
              {headerLeft ?? (
                <FormFieldSet
                  type="search"
                  name="headerSearch"
                  label="Search"
                  placeholder="Search"
                  startIcon={<SearchIcon size={16} />}
                  classNames={{ base: "w-full max-w-xs", label: "sr-only", control: "border-transparent bg-paper-sunken" }}
                />
              )}
            </div>
            <div className="flex items-center gap-3">
              {headerExtra}
              <Button isIconOnly aria-label="Notifications" variant="ghost" radius="full" size="sm" className="relative">
                <BellIcon size={18} />
                <span aria-hidden="true" className="absolute right-2 top-2 h-1.5 w-1.5 rounded-full" style={{ background: "var(--oks-color-danger-500)" }} />
              </Button>
              <Avatar src="/avatar-omkar.jpg" name="Omkar Sahu" size="sm" radius="full" />
            </div>
          </header>

          <div className={contentClassName ?? "flex flex-1 flex-col gap-5 overflow-y-auto p-5 @3xl:p-8"}>{children}</div>
        </div>
      </div>
    </div>
  );
};