97 lines
2.4 KiB
TypeScript
97 lines
2.4 KiB
TypeScript
import type { ReactNode } from "react";
|
|
|
|
type StatusTone = "info" | "success" | "warning" | "danger" | "neutral";
|
|
|
|
const STATUS_PRESENTATION: Record<
|
|
string,
|
|
{ icon: string; label: string; tone: StatusTone }
|
|
> = {
|
|
OPEN: { icon: "○", label: "Open", tone: "info" },
|
|
IN_PROGRESS: { icon: "◐", label: "In progress", tone: "warning" },
|
|
COMPLETED: { icon: "✓", label: "Completed", tone: "success" },
|
|
CANCELLED: { icon: "—", label: "Cancelled", tone: "neutral" },
|
|
LIVE: { icon: "●", label: "Live", tone: "success" },
|
|
CONNECTING: { icon: "↻", label: "Connecting", tone: "info" },
|
|
OFFLINE: { icon: "○", label: "Offline · reconnecting", tone: "warning" },
|
|
ERROR: { icon: "!", label: "Connection error · retrying", tone: "danger" },
|
|
};
|
|
|
|
export function humanizeStatus(status: string): string {
|
|
return (
|
|
STATUS_PRESENTATION[status]?.label ??
|
|
status
|
|
.toLowerCase()
|
|
.split("_")
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(" ")
|
|
);
|
|
}
|
|
|
|
export function StatusBadge({
|
|
status,
|
|
label,
|
|
}: {
|
|
status: string;
|
|
label?: string;
|
|
}) {
|
|
const presentation = STATUS_PRESENTATION[status] ?? {
|
|
icon: "•",
|
|
label: humanizeStatus(status),
|
|
tone: "neutral" as const,
|
|
};
|
|
|
|
return (
|
|
<span className={`ul-status-badge ul-status-badge--${presentation.tone}`}>
|
|
<span className="ul-status-badge__icon" aria-hidden="true">
|
|
{presentation.icon}
|
|
</span>
|
|
<span>{label ?? presentation.label}</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function PageState({
|
|
kind,
|
|
title,
|
|
children,
|
|
}: {
|
|
kind: "loading" | "empty" | "error";
|
|
title: string;
|
|
children?: ReactNode;
|
|
}) {
|
|
const icon = kind === "error" ? "!" : kind === "empty" ? "□" : "…";
|
|
|
|
return (
|
|
<div
|
|
className={`ul-page-state ul-page-state--${kind}`}
|
|
role={kind === "error" ? "alert" : "status"}
|
|
>
|
|
<span className="ul-page-state__icon" aria-hidden="true">
|
|
{icon}
|
|
</span>
|
|
<div>
|
|
<strong>{title}</strong>
|
|
{children && <div className="ul-page-state__detail">{children}</div>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Metric({
|
|
label,
|
|
value,
|
|
detail,
|
|
}: {
|
|
label: string;
|
|
value: ReactNode;
|
|
detail?: ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="ul-metric">
|
|
<span className="ul-metric__label">{label}</span>
|
|
<strong className="ul-metric__value">{value}</strong>
|
|
{detail && <span className="ul-metric__detail">{detail}</span>}
|
|
</div>
|
|
);
|
|
}
|