feat(sprint-3): refresh responsive UlHub portal
This commit is contained in:
96
web/components/PortalUi.tsx
Normal file
96
web/components/PortalUi.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user