"use client";
import { Loader2, X } from "lucide-react";
import { useEffect, useId, type ReactNode } from "react";
export const cx = (...a: (string | false | null | undefined)[]) => a.filter(Boolean).join(" ");
export function Card({ title, subtitle, actions, children, className, id }: { title?: ReactNode; subtitle?: ReactNode; actions?: ReactNode; children: ReactNode; className?: string; id?: string }) {
return (
{(title || actions) && (
{title &&
{title}
}
{subtitle &&
{subtitle}
}
{actions && {actions}
}
)}
{children}
);
}
export function Tile({ label, value, sub, tone }: { label: string; value: ReactNode; sub?: ReactNode; tone?: "up" | "down" | "warn" | "muted" }) {
const t = tone === "up" ? "text-up" : tone === "down" ? "text-down" : tone === "warn" ? "text-warn" : tone === "muted" ? "text-fg-muted" : "text-fg";
return (
{label}
{value}
{sub &&
{sub}
}
);
}
type BtnProps = React.ButtonHTMLAttributes & { variant?: "primary" | "ghost" | "danger" | "outline"; size?: "sm" | "md"; loading?: boolean };
export function Button({ variant = "outline", size = "md", loading, className, children, disabled, ...rest }: BtnProps) {
const v =
variant === "primary" ? "bg-accent text-white hover:bg-accent/90 border-transparent" :
variant === "danger" ? "bg-down text-white hover:bg-down/90 border-transparent" :
variant === "ghost" ? "border-transparent hover:bg-bg-sub text-fg-muted hover:text-fg" :
"border-line-strong bg-bg-sub hover:bg-line text-fg";
const s = size === "sm" ? "h-7 px-2.5 text-xs" : "h-9 px-3.5 text-sm";
return (
);
}
export function Field({ label, hint, children, htmlFor }: { label: ReactNode; hint?: ReactNode; children: ReactNode; htmlFor?: string }) {
return (
);
}
const inputCls = "h-9 w-full rounded-md border border-line-strong bg-bg px-2.5 text-sm text-fg placeholder:text-fg-dim focus:border-accent";
export function Input(props: React.InputHTMLAttributes) {
return ;
}
export function Select(props: React.SelectHTMLAttributes) {
return ;
}
export function Textarea(props: React.TextareaHTMLAttributes) {
return ;
}
export function Badge({ children, tone = "muted", className }: { children: ReactNode; tone?: "up" | "down" | "warn" | "muted" | "accent" | "live"; className?: string }) {
const t = {
up: "bg-up/15 text-up border-up/30",
down: "bg-down/15 text-down border-down/30",
warn: "bg-warn/15 text-warn border-warn/30",
muted: "bg-bg-sub text-fg-muted border-line-strong",
accent: "bg-accent-soft text-accent border-accent/40",
live: "bg-live text-white border-live",
}[tone];
return {children};
}
export function Dot({ ok, className }: { ok: boolean | null | undefined; className?: string }) {
return ;
}
export function Spinner({ label = "Loading…" }: { label?: string }) {
return (
{label}
);
}
export function Empty({ title, children }: { title: string; children?: ReactNode }) {
return (
{title}
{children &&
{children}
}
);
}
export function ErrorBox({ error }: { error: unknown }) {
const msg = error instanceof Error ? error.message : String(error);
return {msg}
;
}
export function Table({ children, className }: { children: ReactNode; className?: string }) {
return (
);
}
export function Th({ children, className, align = "left", scope = "col" }: { children?: ReactNode; className?: string; align?: "left" | "right" | "center"; scope?: "col" | "row" }) {
return {children} | ;
}
export function Td({ children, className, align = "left", num, title }: { children?: ReactNode; className?: string; align?: "left" | "right" | "center"; num?: boolean; title?: string }) {
return {children} | ;
}
export function Dialog({ open, onClose, title, children, footer }: { open: boolean; onClose: () => void; title: string; children: ReactNode; footer?: ReactNode }) {
const id = useId();
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onClose]);
if (!open) return null;
return (
e.stopPropagation()}>
{children}
{footer &&
}
);
}
export function PageHeader({ title, subtitle, actions }: { title: string; subtitle?: ReactNode; actions?: ReactNode }) {
return (
{title}
{subtitle &&
{subtitle}
}
{actions &&
{actions}
}
);
}
export function KV({ items }: { items: [ReactNode, ReactNode][] }) {
return (
{items.map(([k, v], i) => (
- {k}
- {v}
))}
);
}