118 lines
2.8 KiB
TypeScript
118 lines
2.8 KiB
TypeScript
import { clsx } from "clsx";
|
|
|
|
export function PageHeader({
|
|
title,
|
|
description,
|
|
actions,
|
|
meta
|
|
}: {
|
|
title: string;
|
|
description?: string;
|
|
actions?: React.ReactNode;
|
|
meta?: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<header className="page-header">
|
|
<div className="title-block">
|
|
{meta ? <div className="status-row">{meta}</div> : null}
|
|
<h1>{title}</h1>
|
|
{description ? <p>{description}</p> : null}
|
|
</div>
|
|
{actions ? <div className="toolbar">{actions}</div> : null}
|
|
</header>
|
|
);
|
|
}
|
|
|
|
export function Panel({
|
|
title,
|
|
eyebrow,
|
|
actions,
|
|
children,
|
|
className
|
|
}: {
|
|
title?: string;
|
|
eyebrow?: string;
|
|
actions?: React.ReactNode;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<section className={clsx("panel", className)}>
|
|
{title || actions || eyebrow ? (
|
|
<div className="panel-header">
|
|
<div>
|
|
{eyebrow ? <span className="eyebrow">{eyebrow}</span> : null}
|
|
{title ? <h2>{title}</h2> : null}
|
|
</div>
|
|
{actions ? <div className="toolbar compact">{actions}</div> : null}
|
|
</div>
|
|
) : null}
|
|
<div className="panel-body">{children}</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function MetricTile({
|
|
label,
|
|
value,
|
|
detail,
|
|
tone = "neutral",
|
|
icon
|
|
}: {
|
|
label: string;
|
|
value: React.ReactNode;
|
|
detail?: string;
|
|
tone?: "neutral" | "good" | "warn" | "danger" | "info";
|
|
icon?: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div className={clsx("metric-tile", tone)}>
|
|
<div className="metric-top">
|
|
<span>{label}</span>
|
|
{icon ? <span className="metric-icon">{icon}</span> : null}
|
|
</div>
|
|
<strong>{value}</strong>
|
|
{detail ? <p>{detail}</p> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Tabs({ items, active }: { items: Array<{ label: string; href: string; count?: number }>; active: string }) {
|
|
return (
|
|
<nav className="tabs" aria-label="View tabs">
|
|
{items.map((item) => (
|
|
<a className={clsx("tab", active === item.label && "active")} href={item.href} key={item.label}>
|
|
{item.label}
|
|
{typeof item.count === "number" ? <span className="tab-count">{item.count}</span> : null}
|
|
</a>
|
|
))}
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
export function EmptyState({
|
|
title,
|
|
description,
|
|
action
|
|
}: {
|
|
title: string;
|
|
description?: string;
|
|
action?: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="empty-state">
|
|
<strong>{title}</strong>
|
|
{description ? <span>{description}</span> : null}
|
|
{action ? <div className="toolbar compact">{action}</div> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function StatusDot({ tone = "neutral", label }: { tone?: string; label: string }) {
|
|
return <span className={clsx("status-dot", tone)}>{label}</span>;
|
|
}
|
|
|
|
export function DataTable({ children }: { children: React.ReactNode }) {
|
|
return <div className="table-wrap">{children}</div>;
|
|
}
|