SPB Git

Repository Public

bits.tsx 3.0 KB 65 lines tsx

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
Files main
Menu

Theme