Vectrum
Sign In
102 lines · 3.5 KB · TypeScript
import type { LedKind } from "@unlocalhosted/metalui";
import type { CommitStatus, JobStatus, RunStatus, StepStatus } from "@/lib/runs";

// Run, job and step states, drawn like the issue status glyphs (MetalUI's 24 grid, 1.7 stroke) and
// coloured like its LEDs: green passed, red failed, amber working or waiting, unlit (ink3) the rest.

export type AnyStatus = RunStatus | JobStatus | StepStatus | CommitStatus;

const AMBER = "text-[#c98a18]";

const COLORS: Record<AnyStatus, string> = {
  success: "text-green-deep",
  failure: "text-red",
  in_progress: AMBER,
  queued: AMBER,
  pending: "text-ink3",
  waiting: "text-ink3",
  cancelled: "text-ink3",
  skipped: "text-ink3",
};

const LABELS: Record<AnyStatus, string> = {
  success: "Success",
  failure: "Failure",
  in_progress: "In progress",
  queued: "Queued",
  pending: "Pending",
  waiting: "Waiting",
  cancelled: "Cancelled",
  skipped: "Skipped",
};

export const statusText = (status: AnyStatus) => LABELS[status];

/** The LED for a status beside its words (StatusBadge). */
export function statusLed(status: AnyStatus): LedKind {
  if (status === "success") return "live";
  if (status === "failure") return "failed";
  if (status === "in_progress" || status === "queued") return "waiting";
  return "off";
}

const R = 9.15;

export function RunStatusIcon({ status, size = 16, className = "" }: { status: AnyStatus; size?: number; className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      width={size}
      height={size}
      className={`shrink-0 ${COLORS[status]} ${className}`}
      role="img"
      aria-label={LABELS[status]}
    >
      <title>{LABELS[status]}</title>
      {status === "success" && (
        <>
          <circle cx="12" cy="12" r="10" fill="currentColor" />
          <path d="m7.6 12.3 3.1 3.1 5.7-6.2" fill="none" stroke="var(--canvas)" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" />
        </>
      )}
      {status === "failure" && (
        <>
          <circle cx="12" cy="12" r="10" fill="currentColor" />
          <path d="m8.7 8.7 6.6 6.6m0-6.6-6.6 6.6" fill="none" stroke="var(--canvas)" strokeWidth="1.9" strokeLinecap="round" />
        </>
      )}
      {status === "in_progress" && (
        <>
          <circle cx="12" cy="12" r={R} fill="none" stroke="currentColor" strokeWidth="1.7" opacity="0.3" />
          <path
            d={`M12 ${12 - R}a${R} ${R} 0 0 1 ${R} ${R}`}
            fill="none"
            stroke="currentColor"
            strokeWidth="1.9"
            strokeLinecap="round"
            className="origin-center animate-spin"
          />
          <circle cx="12" cy="12" r="3" fill="currentColor" />
        </>
      )}
      {(status === "queued" || status === "waiting" || status === "pending") && (
        <>
          <circle cx="12" cy="12" r={R} fill="none" stroke="currentColor" strokeWidth="1.7" strokeDasharray="2.6 2.19" />
          {status === "queued" && <circle cx="12" cy="12" r="3" fill="currentColor" />}
        </>
      )}
      {status === "cancelled" && (
        <>
          <circle cx="12" cy="12" r={R} fill="none" stroke="currentColor" strokeWidth="1.7" />
          <path d="m5.6 18.4 12.8-12.8" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
        </>
      )}
      {status === "skipped" && (
        <>
          <circle cx="12" cy="12" r={R} fill="none" stroke="currentColor" strokeWidth="1.7" opacity="0.6" />
          <path d="M8 12h8" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
        </>
      )}
    </svg>
  );
}