Skip to content

Data table view

The list-screen pattern, done as a real app screen: the same persistent sidebar/header shell as the other App screens patterns, a row of status Tabs acting as a live filter (not separate content panels), a toolbar with a removable sort pill, filter Dropdowns, and search, and a genuine sortable/selectable Table with AvatarGroup recipients and a Pagination footer.

data-table-view.tsx

Built with

How it’s built

  1. 01The status row is a real Tabs, driving onSelectionChange to filter the same Table below instead of swapping between separate content panels — a legitimate Tabs use even without one panel per tab, as long as something visibly responds to the selection.
  2. 02Each status tab's title is whitespace-nowrap and the tablist scrolls (overflow-x-auto) instead of wrapping — without it, a longer label like "For Approval" wraps onto two lines and the whole row becomes uneven.
  3. 03Recipients use a real AvatarGroup (max={3}) — it overlaps the avatars and collapses the overflow into a "+N" once past max, the standard "who's on this" cluster, not a hand-rolled -space-x stack.
  4. 04Selection is controlled — selectedKeys / onSelectionChange — so a bulk-action bar can react to it.
  5. 05Sorting is client-side until you pass onSortChange, at which point it becomes your server's job.
  6. 06The sidebar and header are the same bounded-height app shell as analytics-dashboard: overflow-hidden on the outer wrapper, independent scroll on the sidebar, shrink-0 on the header, so only the documents content itself scrolls — and every direct child of that scrollable body carries shrink-0 too, or a flex child's default flex-shrink: 1 would silently compress it to fit instead of letting it overflow.

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.

data-table-view.tsx
"use client";

import { useMemo, useState } from "react";
import { Avatar, AvatarGroup } from "oks-ui/avatar";
import { Button } from "oks-ui/button";
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 { Pagination, PaginationSummary } from "oks-ui/pagination";
import { Table } from "oks-ui/table";
import { Tab, Tabs } from "oks-ui/tabs";
import "oks-ui/avatar.css";
import "oks-ui/button.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/pagination.css";
import "oks-ui/table.css";
import "oks-ui/tabs.css";
import { AppShell } from "./AppShell";
import {
  ChevronDownIcon,
  ClockIcon,
  FileTextIcon,
  GridIcon,
  HelpCircleIcon,
  MailIcon,
  MessageCircleIcon,
  MoreIcon,
  PieChartIcon,
  PlusIcon,
  ReceiptIcon,
  SendIcon,
  SparkleIcon,
  XIcon,
} from "./icons";

// A simple open eye — "Viewed", not a reproduction of any icon set.
const EyeIcon = ({ size = 16 }: { size?: number }) => {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M1 8s2.5-4.5 7-4.5S15 8 15 8s-2.5 4.5-7 4.5S1 8 1 8Z" />
      <circle cx="8" cy="8" r="2" />
    </svg>
  );
};

// A single looping arrow — "For approval / in progress".
const RefreshIcon = ({ size = 16 }: { size?: number }) => {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M13.5 8A5.5 5.5 0 0 1 3.9 11.6M2.5 8A5.5 5.5 0 0 1 12.1 4.4" />
      <path d="M12.1 1.8v2.6h-2.6M3.9 14.2v-2.6h2.6" />
    </svg>
  );
};

// A checkmark inside a ring — "Sign completed".
const CheckCircleIcon = ({ size = 16 }: { size?: number }) => {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <circle cx="8" cy="8" r="6.5" />
      <path d="M5.3 8.2 7.2 10l3.5-4" />
    </svg>
  );
};

// A funnel — "More filters".
const FilterIcon = ({ size = 16 }: { size?: number }) => {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M2 3h12l-4.5 5.5v4L6.5 14v-5.5Z" />
    </svg>
  );
};

// A simple folder — "Manage folders".
const FolderIcon = ({ size = 16 }: { size?: number }) => {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M2 4.5a1 1 0 0 1 1-1h3l1.5 1.5H13a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1Z" />
    </svg>
  );
};

// Three horizontal lines — the "list view" half of the grid/list toggle.
const ListViewIcon = ({ size = 16 }: { size?: number }) => {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" aria-hidden="true">
      <path d="M3 4h10M3 8h10M3 12h10" />
    </svg>
  );
};

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} /> },
      { key: "documents", label: "Documents", icon: <FileTextIcon 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} /> },
      { key: "feedback", label: "Feedback", icon: <MessageCircleIcon size={18} /> },
    ],
  },
];

const STATUS_TABS = [
  { key: "all", label: "All Documents", icon: <GridIcon size={15} /> },
  { key: "draft", label: "Draft", icon: <FileTextIcon size={15} /> },
  { key: "approval", label: "For Approval", icon: <RefreshIcon size={15} /> },
  { key: "sent", label: "Sent", icon: <SendIcon size={15} /> },
  { key: "viewed", label: "Viewed", icon: <EyeIcon size={15} /> },
  { key: "suggest", label: "Suggest Edits", icon: <SparkleIcon size={15} /> },
  { key: "completed", label: "Sign Completed", icon: <CheckCircleIcon size={15} /> },
  { key: "expired", label: "Expired/Declined", icon: <XIcon size={15} /> },
];

type DocStatus = "Draft" | "Completed" | "Sent" | "Expired";

type DocRow = {
  id: number;
  name: string;
  createdAt: string;
  lastActivity: string;
  recipients: string[];
  status: DocStatus;
} & Record<string, unknown>;

const DOCUMENTS: DocRow[] = [
  { id: 1, name: "Sample Sales Proposal", createdAt: "Created: Jan 16, 2026, 9:18 AM", lastActivity: "Jan 24, 2026, 9:18 AM", recipients: ["Nora Chen", "Marcus Lee", "Ada Lovelace"], status: "Draft" },
  { id: 2, name: "Start exploring here! (Product guide)", createdAt: "Created: Jan 15, 2026, 9:18 AM", lastActivity: "Jan 23, 2026, 9:18 AM", recipients: ["Grace Hopper", "Alan Turing"], status: "Completed" },
  { id: 3, name: "Memorandum of Understanding", createdAt: "Created: Jan 14, 2026, 9:18 AM", lastActivity: "Jan 23, 2026, 9:18 AM", recipients: ["Margaret Hamilton"], status: "Completed" },
  { id: 4, name: "Employment Contract", createdAt: "Created: Jan 13, 2026, 9:20 AM", lastActivity: "Jan 22, 2026, 9:18 AM", recipients: ["Barbara Liskov", "Radia Perlman", "Ada Lovelace", "Grace Hopper"], status: "Sent" },
  { id: 5, name: "Purchase Agreement", createdAt: "Created: Jan 12, 2026, 9:18 AM", lastActivity: "Jan 21, 2026, 10:24 AM", recipients: ["Alan Turing", "Nora Chen"], status: "Draft" },
  { id: 6, name: "Lease Agreement", createdAt: "Created: Jan 10, 2026, 11:18 AM", lastActivity: "Jan 21, 2026, 10:22 AM", recipients: ["Marcus Lee", "Grace Hopper", "Radia Perlman"], status: "Completed" },
  { id: 7, name: "Copyright Registration", createdAt: "Created: Jan 10, 2026, 11:18 AM", lastActivity: "Jan 21, 2026, 10:20 AM", recipients: ["Margaret Hamilton"], status: "Sent" },
  { id: 8, name: "As-is Bill of Sale", createdAt: "Created: Jan 10, 2026, 10:18 AM", lastActivity: "Jan 20, 2026, 10:18 AM", recipients: ["Ada Lovelace", "Barbara Liskov"], status: "Expired" },
  { id: 9, name: "Service Agreement", createdAt: "Created: Jan 10, 2026, 9:18 AM", lastActivity: "Jan 20, 2026, 9:15 AM", recipients: ["Nora Chen", "Alan Turing"], status: "Draft" },
  { id: 10, name: "Non-Disclosure Agreement (NDA)", createdAt: "Created: Jan 10, 2026, 9:16 AM", lastActivity: "Jan 20, 2026, 8:18 AM", recipients: ["Marcus Lee"], status: "Completed" },
];

// A handful of recipients get a real photo — two are the same stock-free
// portrait assets the AvatarGroup recipe already uses elsewhere on this
// site, three are AI-synthesized (non-real-person) faces — the rest fall
// back to initials, exactly like that recipe's own photo/initials mix.
const RECIPIENT_PHOTOS: Record<string, string> = {
  "Nora Chen": "/hero/person-woman.jpg",
  "Marcus Lee": "/hero/person-man.jpg",
  "Ada Lovelace": "/avatars/synthetic-1.jpg",
  "Grace Hopper": "/avatars/synthetic-2.jpg",
  "Margaret Hamilton": "/avatars/synthetic-3.jpg",
};

const statusTone: Record<DocStatus, "default" | "success" | "info" | "danger"> = {
  Draft: "default",
  Completed: "success",
  Sent: "info",
  Expired: "danger",
};

const PAGE_SIZE = 10;

const RowMenu = ({ label }: { label: string }) => {
  return (
    <Dropdown placement="bottom-end">
      <DropdownTrigger>
        <Button isIconOnly aria-label={label} variant="ghost" radius="full" size="sm">
          <MoreIcon size={16} />
        </Button>
      </DropdownTrigger>
      <DropdownMenu aria-label={label}>
        <DropdownItem key="rename">Rename</DropdownItem>
        <DropdownItem key="duplicate">Duplicate</DropdownItem>
        <DropdownItem key="delete" classNames={{ title: "text-[color:var(--oks-color-danger-700)]" }}>
          Delete
        </DropdownItem>
      </DropdownMenu>
    </Dropdown>
  );
};

const DataTableView = () => {
  const [status, setStatus] = useState("all");
  const [query, setQuery] = useState("");
  const [page, setPage] = useState(1);
  const [selected, setSelected] = useState<Set<number>>(new Set());
  const [sortBy, setSortBy] = useState<string | null>("Last Updated Desc");

  const filtered = useMemo(() => {
    const byStatus =
      status === "all"
        ? DOCUMENTS
        : DOCUMENTS.filter((d) => d.status.toLowerCase() === status.replace("approval", "sent").toLowerCase() || d.status.toLowerCase() === status);
    return byStatus.filter((d) => d.name.toLowerCase().includes(query.toLowerCase()));
  }, [status, query]);

  const pageRows = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

  return (
    <AppShell
      navItems={NAV_ITEMS}
      selectedNavKey="documents"
      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>
        </>
      }
    >
      <div className="flex shrink-0 flex-wrap items-center justify-between gap-3">
        <PageTitle
          as="h2"
          title="My Documents"
          classNames={{ base: "flex-col items-start", title: "text-2xl font-bold tracking-tight text-ink" }}
        />
        <div className="flex items-center gap-2">
          <Dropdown placement="bottom-end">
            <DropdownTrigger>
              <Button variant="bordered" radius="full" size="sm" endContent={<ChevronDownIcon size={14} />}>
                Export
              </Button>
            </DropdownTrigger>
            <DropdownMenu aria-label="Export options">
              <DropdownItem key="csv">Export as CSV</DropdownItem>
              <DropdownItem key="pdf">Export as PDF</DropdownItem>
            </DropdownMenu>
          </Dropdown>
          <Button color="primary" radius="full" size="sm" startContent={<PlusIcon size={14} />}>
            New Document
          </Button>
        </div>
      </div>

      {/* Status Tabs — a real Tabs, not a set of styled buttons, so
          switching between them is roving-tabindex and arrow-key
          accessible for free. */}
      <Tabs
        aria-label="Filter by status"
        variant="underlined"
        selectedKey={status}
        onSelectionChange={(key) => {
          setStatus(String(key));
          setPage(1);
        }}
        classNames={{ base: "shrink-0", tabList: "overflow-x-auto", tab: "shrink-0" }}
      >
        {STATUS_TABS.map((t) => (
          <Tab
            key={t.key}
            title={
              <span className="flex items-center gap-1.5 whitespace-nowrap">
                {t.icon}
                {t.label}
              </span>
            }
          />
        ))}
      </Tabs>

      {/* toolbar */}
      <div className="flex shrink-0 flex-wrap items-center justify-between gap-3">
        <div className="flex flex-wrap items-center gap-2">
          {sortBy ? (
            <Chip
              size="sm"
              variant="bordered"
              color="primary"
              onClose={() => setSortBy(null)}
              closeIcon={<XIcon size={12} />}
            >
              Sort By: {sortBy}
            </Chip>
          ) : null}

          <Dropdown placement="bottom-start">
            <DropdownTrigger>
              <Button variant="bordered" size="sm" endContent={<ChevronDownIcon size={14} />}>
                Name
              </Button>
            </DropdownTrigger>
            <DropdownMenu aria-label="Filter by name" selectionMode="single">
              <DropdownItem key="az">A → Z</DropdownItem>
              <DropdownItem key="za">Z → A</DropdownItem>
            </DropdownMenu>
          </Dropdown>

          <Dropdown placement="bottom-start">
            <DropdownTrigger>
              <Button variant="bordered" size="sm" endContent={<ChevronDownIcon size={14} />}>
                Date
              </Button>
            </DropdownTrigger>
            <DropdownMenu aria-label="Filter by date" selectionMode="single">
              <DropdownItem key="newest">Newest first</DropdownItem>
              <DropdownItem key="oldest">Oldest first</DropdownItem>
            </DropdownMenu>
          </Dropdown>

          <Button variant="ghost" size="sm" startContent={<FilterIcon size={14} />}>
            More Filters
          </Button>
        </div>

        <div className="flex flex-wrap items-center gap-2">
          <FormFieldSet
            type="search"
            name="q"
            label="Search"
            placeholder="Search…"
            value={query}
            onChange={(v: unknown) => {
              setQuery(String(v ?? ""));
              setPage(1);
            }}
            classNames={{ base: "w-40", label: "sr-only" }}
          />
          <Button variant="ghost" size="sm" startContent={<FolderIcon size={14} />}>
            Manage Folders
          </Button>
          <Button isIconOnly aria-label="Grid view" variant="ghost" size="sm">
            <GridIcon size={16} />
          </Button>
          <Button isIconOnly aria-label="List view" variant="soft" color="primary" size="sm">
            <ListViewIcon size={16} />
          </Button>
        </div>
      </div>

      <Table
        aria-label="My documents"
        classNames={{ cell: "whitespace-nowrap" }}
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={(keys) => setSelected(keys as Set<number>)}
        columns={[
          {
            key: "name",
            header: "Name",
            render: (r) => (
              <div className="flex items-center gap-2.5">
                <Avatar icon={<FileTextIcon size={16} />} color="default" radius="md" size="sm" />
                <div className="min-w-0">
                  <p className="truncate font-medium text-ink">{r.name}</p>
                  <p className="truncate text-[0.78rem] text-ink-faint">{r.createdAt}</p>
                </div>
              </div>
            ),
          },
          { key: "lastActivity", header: "Last Activity", sortable: true, sortValue: (r) => new Date(r.lastActivity) },
          {
            key: "recipients",
            header: "Recipients",
            render: (r) => (
              <AvatarGroup max={3}>
                {r.recipients.map((name) => (
                  <Avatar key={name} src={RECIPIENT_PHOTOS[name]} name={name} size="sm" radius="full" />
                ))}
              </AvatarGroup>
            ),
          },
          {
            key: "status",
            header: "Status",
            render: (r) => (
              <Chip size="sm" variant="soft" color={statusTone[r.status]}>
                {r.status}
              </Chip>
            ),
          },
          {
            key: "actions",
            header: "",
            ariaLabel: "Actions",
            align: "end",
            render: (r) => (
              <div className="flex items-center justify-end gap-1.5">
                <Button variant="bordered" size="sm">
                  View
                </Button>
                <RowMenu label={`${r.name} options`} />
              </div>
            ),
          },
        ]}
        rows={pageRows}
        getRowKey={(r) => r.id}
      />

      <div className="flex flex-wrap items-center justify-between gap-3">
        <PaginationSummary page={page} pageSize={PAGE_SIZE} total={filtered.length} />
        <Pagination page={page} pageCount={Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))} onChange={setPage} />
      </div>
    </AppShell>
  );
};

export default DataTableView;

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

icons.tsx
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 FileTextIcon = ({ 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="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5z" />
      <path d="M14 2v6h6" />
      <path d="M9 13h6M9 17h6M9 9h2" />
    </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 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 SparkleIcon = ({ 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 3c.6 3.2 1.9 4.9 5.5 5.5-3.6.6-4.9 2.3-5.5 5.5-.6-3.2-1.9-4.9-5.5-5.5C10.1 7.9 11.4 6.2 12 3Z" />
      <path d="M19 15c.3 1.4.9 2.1 2.3 2.3-1.4.3-2.1.9-2.3 2.3-.3-1.4-.9-2.1-2.3-2.3 1.4-.3 2.1-.9 2.3-2.3Z" />
    </svg>
  );
};

export const XIcon = ({ 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="M18 6 6 18M6 6l12 12" />
    </svg>
  );
};

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 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 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>
  );
};