SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
3.3 KB · 53 lines tsx
Raw Blame History
1import type { ReactNode } from 'react';2import { cn } from '@/lib/format';34/** Operator labels for health statuses (SPEC §12): UP · DEGRADED · BROKEN · DISABLED · MAINTENANCE. */5const LABELS: Record<string, { label: string; cls: string }> = {6  healthy: { label: 'UP', cls: 'bg-gain-bg text-gain' },7  degraded: { label: 'DEGRADED', cls: 'bg-alert-bg text-alert' },8  failing: { label: 'BROKEN', cls: 'bg-loss-bg text-loss' },9  paused: { label: 'DISABLED', cls: 'bg-inset text-muted' },10  disabled: { label: 'DISABLED', cls: 'bg-inset text-muted' },11  maintenance: { label: 'MAINTENANCE', cls: 'bg-index-bg text-index' },12  unknown: { label: 'UNKNOWN', cls: 'bg-inset text-subtle' },13};1415export function HealthLabel({ status, small = true }: { status: string | null | undefined; small?: boolean }) {16  const l = LABELS[status ?? 'unknown'] ?? LABELS.unknown!;17  return <span className={cn('inline-flex rounded-sm font-semibold tracking-wide', small ? 'px-1.5 py-0.5 text-[10px]' : 'px-2 py-1 text-xs', l.cls)}>{l.label}</span>;18}1920/** Source favicon (Google s2 service, falls back to an initial). */21export function SourceLogo({ domain, name, logoUrl, size = 18 }: { domain: string | null | undefined; name: string; logoUrl?: string | null; size?: number }) {22  const src = logoUrl ?? (domain ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=${size * 2}` : null);23  if (!src) return <span className="inline-flex h-[18px] w-[18px] items-center justify-center rounded-sm bg-inset text-[10px] font-semibold text-muted">{name.slice(0, 1).toUpperCase()}</span>;24  // eslint-disable-next-line @next/next/no-img-element25  return <img src={src} alt="" width={size} height={size} loading="lazy" className="inline-block rounded-sm bg-white/5" />;26}2728export function Pct({ value, digits = 0 }: { value: unknown; digits?: number }) {29  const x = Number(value);30  return <>{Number.isFinite(x) ? `${(x * 100).toFixed(digits)}%` : '—'}</>;31}3233/** Thin progress bar for backfill campaigns. */34export function Progress({ percent, status }: { percent: number | null | undefined; status?: string | null }) {35  const p = percent === null || percent === undefined ? null : Math.max(0, Math.min(100, Number(percent)));36  return (37    <div className="min-w-[72px]">38      <div className="h-1.5 w-full overflow-hidden rounded-full bg-inset">39        <div className={cn('h-full rounded-full', status === 'completed' ? 'bg-gain' : status === 'failed' ? 'bg-loss' : status === 'paused' ? 'bg-alert' : 'bg-index')} style={{ width: `${p ?? (status === 'running' ? 8 : 0)}%` }} />40      </div>41      <div className="mt-0.5 text-[10px] text-subtle">{p === null ? (status ?? '—') : `${p.toFixed(0)}% · ${status ?? ''}`}</div>42    </div>43  );44}4546export function Json({ value, className }: { value: unknown; className?: string }) {47  return <pre className={cn('max-h-[70vh] overflow-auto rounded-md border border-border bg-sunken p-3 font-mono text-[11px] leading-relaxed', className)}>{typeof value === 'string' ? value : JSON.stringify(value, null, 2)}</pre>;48}4950export function Chip({ children, tone = 'neutral' }: { children: ReactNode; tone?: 'neutral' | 'index' | 'alert' }) {51  return <span className={cn('inline-flex rounded-sm px-1.5 py-0.5 text-[10px]', tone === 'index' ? 'bg-index-bg text-index' : tone === 'alert' ? 'bg-alert-bg text-alert' : 'bg-inset text-muted')}>{children}</span>;52}53