Redesign WatchLink application UI
This commit is contained in:
117
src/components/ui.tsx
Normal file
117
src/components/ui.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
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>;
|
||||
}
|
||||
Reference in New Issue
Block a user