Repository Public
1'use client';2import type { RiskClass, RunStatus } from '@aos/shared';3import { Badge, Dot } from '@/components/ui/badge';4import { Tooltip } from '@/components/ui/tooltip';5import { RISK, RUN_STATUS } from '@/lib/status';6import { useNow } from '@/hooks/use-viewport';7import { DASH, elapsedSec, fmtDuration, fmtUsd } from '@/lib/format';8import { cn } from '@/lib/cn';910export function StatusBadge({ status, className }: { status: RunStatus; className?: string }) {11 const s = RUN_STATUS[status];12 return (13 <Tooltip content={s.hint}>14 <Badge tone={s.tone} className={className} data-testid="status-badge" data-status={status}>15 <Dot tone={s.tone} pulse={s.pulse} className="size-1.5" />16 {s.label}17 </Badge>18 </Tooltip>19 );20}21export function RiskBadge({ risk }: { risk: RiskClass | null }) {22 if (!risk) return <Badge tone="muted">Risk unknown</Badge>;23 const r = RISK[risk];24 return (25 <Tooltip content={r.hint}>26 <Badge tone={r.tone} data-testid="risk-badge" data-risk={risk}>{r.label}</Badge>27 </Tooltip>28 );29}3031/** Spend against a budget. Over 80 % turns amber, at or over the budget red. Real numbers only. */32export function BudgetBar({ spent, budget, className, label = 'Budget used' }: { spent: number | null | undefined; budget: number | null | undefined; className?: string; label?: string }) {33 const known = spent != null && budget != null && budget > 0;34 const f = known ? Math.max(0, Math.min(1, spent / budget)) : 0;35 const tone = !known ? 'bg-faint' : f >= 1 ? 'bg-error-vivid' : f >= 0.8 ? 'bg-warning-vivid' : 'bg-brand';36 return (37 <div38 role="progressbar"39 aria-label={label}40 aria-valuemin={0}41 aria-valuemax={known ? budget : undefined}42 aria-valuenow={known ? spent : undefined}43 aria-valuetext={known ? `${fmtUsd(spent)} of ${fmtUsd(budget)}` : 'unknown'}44 className={cn('h-1.5 w-full overflow-hidden rounded-full bg-surface-2', className)}45 >46 <div className={cn('h-full rounded-full transition-[width] duration-500', tone)} style={{ width: `${known ? Math.max(f * 100, spent > 0 ? 1.5 : 0) : 0}%` }} />47 </div>48 );49}50export function SpendOfBudget({ spent, budget, className }: { spent: number | null | undefined; budget: number | null | undefined; className?: string }) {51 return (52 <span className={cn('tnum whitespace-nowrap', className)} data-testid="spend-of-budget" data-spent={spent ?? ''}>53 <span className="font-semibold text-text">{fmtUsd(spent)}</span>54 <span className="text-muted"> / {fmtUsd(budget, { compact: true })}</span>55 </span>56 );57}5859/** Live elapsed time of something that started (ticks every second while it has not ended). */60export function Elapsed({ start, end, className }: { start: string | null | undefined; end: string | null | undefined; className?: string }) {61 const now = useNow(end ? 60_000 : 1000);62 const sec = elapsedSec(start, end, now);63 return <span className={cn('tnum', className)}>{sec == null ? DASH : fmtDuration(sec)}</span>;64}65