Files
ulweb/web/components/PortalUi.tsx
2026-08-20 22:00:38 -05:00

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>
);
}