60 lines
1.9 KiB
TypeScript
60 lines
1.9 KiB
TypeScript
|
|
import * as React from "react";
|
||
|
|
import { cn } from "../lib/cn";
|
||
|
|
|
||
|
|
export interface NavItem {
|
||
|
|
label: string;
|
||
|
|
icon?: React.ReactNode;
|
||
|
|
href?: string;
|
||
|
|
active?: boolean;
|
||
|
|
onClick?: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Dashboard chrome: fixed sidebar + sticky topbar + scrolling content. */
|
||
|
|
export function AppShell({
|
||
|
|
brand,
|
||
|
|
nav,
|
||
|
|
topbar,
|
||
|
|
children,
|
||
|
|
footer,
|
||
|
|
}: {
|
||
|
|
brand: React.ReactNode;
|
||
|
|
nav: NavItem[];
|
||
|
|
topbar?: React.ReactNode;
|
||
|
|
children: React.ReactNode;
|
||
|
|
footer?: React.ReactNode;
|
||
|
|
}) {
|
||
|
|
return (
|
||
|
|
<div className="flex min-h-screen bg-bg text-text">
|
||
|
|
<aside className="hidden w-60 shrink-0 flex-col border-r border-border bg-surface md:flex">
|
||
|
|
<div className="flex h-14 items-center px-5 font-display text-lg font-bold">{brand}</div>
|
||
|
|
<nav className="flex-1 space-y-0.5 px-3 py-3">
|
||
|
|
{nav.map((item) => {
|
||
|
|
const Comp = item.href ? "a" : "button";
|
||
|
|
return (
|
||
|
|
<Comp
|
||
|
|
key={item.label}
|
||
|
|
href={item.href}
|
||
|
|
onClick={item.onClick}
|
||
|
|
className={cn(
|
||
|
|
"flex w-full items-center gap-3 rounded px-3 py-2 text-sm font-medium transition-colors duration-fast [&_svg]:size-4",
|
||
|
|
item.active ? "bg-brand/15 text-brand" : "text-muted hover:bg-surface-2/60 hover:text-text"
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{item.icon}
|
||
|
|
{item.label}
|
||
|
|
</Comp>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</nav>
|
||
|
|
{footer && <div className="border-t border-border p-3 text-sm text-muted">{footer}</div>}
|
||
|
|
</aside>
|
||
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
||
|
|
<header className="sticky top-0 z-30 flex h-14 items-center justify-between gap-4 border-b border-border bg-bg/80 px-5 backdrop-blur">
|
||
|
|
{topbar}
|
||
|
|
</header>
|
||
|
|
<main className="flex-1 p-6">{children}</main>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|