SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
7.7 KB · 159 lines tsx
Raw Blame History
1import type { ReactNode } from 'react';2import { fmt, fmtDelta } from '@/lib/format';3import { levelById, levelFor, pressureColor, trendArrow, type LEVELS } from '@/lib/pressure';4import type { LevelId, Trend } from '@/lib/types';56export function Section({ id, label, title, right, children, className = '' }: { id?: string; label?: string; title?: string; right?: ReactNode; children: ReactNode; className?: string }) {7  return (8    <section id={id} className={`hairline pt-4 pb-6 ${className}`}>9      {(label || title || right) && (10        <header className="mb-3 flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">11          <div className="flex items-baseline gap-3">12            {label && <span className="label">{label}</span>}13            {title && <h2 className="text-[15px] font-medium text-ink">{title}</h2>}14          </div>15          {right && <div className="text-xs text-ink-2">{right}</div>}16        </header>17      )}18      {children}19    </section>20  );21}2223export function LevelBadge({ level, value, size = 'sm' }: { level?: LevelId | string | null; value?: number | null; size?: 'xs' | 'sm' | 'md' }) {24  const l = level ? levelById(level) : value != null ? levelFor(value) : undefined;25  const color = l?.color ?? '#3A4756';26  const cls = size === 'xs' ? 'text-[10px] tracking-[0.1em]' : size === 'md' ? 'text-[13px] tracking-[0.12em]' : 'text-[11px] tracking-[0.12em]';27  return (28    <span className={`inline-flex items-center gap-1.5 font-medium uppercase ${cls}`} style={{ color }}>29      <span className="inline-block size-1.5 rounded-full" style={{ background: color }} aria-hidden="true" />30      {l?.short ?? 'n/a'}31    </span>32  );33}3435/** Pressure numeral coloured by level. */36export function PNum({ value, digits = 1, className = '' }: { value: number | null | undefined; digits?: number; className?: string }) {37  return (38    <span className={`num ${className}`} style={{ color: value == null ? 'var(--ink-3)' : pressureColor(value) }}>39      {fmt(value, digits)}40    </span>41  );42}4344/** Signed delta with arrow; rising pressure is warm, falling is cool. */45export function Delta({ value, trend, digits = 1, suffix, className = '' }: { value: number | null | undefined; trend?: Trend; digits?: number; suffix?: string; className?: string }) {46  if (value == null) return <span className={`num text-ink-3 ${className}`}>—</span>;47  const arrow = trendArrow(trend, value);48  const color = value > 0.05 ? 'var(--p-stressed)' : value < -0.05 ? 'var(--p-calm)' : 'var(--ink-2)';49  return (50    <span className={`num ${className}`} style={{ color }}>51      {arrow} {fmtDelta(value, digits)}52      {suffix ? <span className="text-ink-3"> {suffix}</span> : null}53    </span>54  );55}5657/** Thin horizontal bar 0–100 coloured by pressure (or a fixed colour). */58export function Bar({ value, max = 100, color, height = 3, className = '' }: { value: number | null | undefined; max?: number; color?: string; height?: number; className?: string }) {59  const v = value == null ? 0 : Math.max(0, Math.min(max, value));60  return (61    <span className={`block w-full overflow-hidden bg-line/70 ${className}`} style={{ height }} aria-hidden="true">62      <span className="block h-full transition-[width] duration-700 ease-out" style={{ width: `${(v / max) * 100}%`, background: color ?? (value == null ? 'var(--ink-3)' : pressureColor(value)) }} />63    </span>64  );65}6667export function ConfBar({ value, className = '' }: { value: number | null | undefined; className?: string }) {68  return (69    <span className={`inline-flex items-center gap-2 ${className}`}>70      <Bar value={value == null ? null : value * 100} color="var(--accent)" className="w-14" />71      <span className="num text-xs text-ink-2">{value == null ? '—' : `${Math.round(value * 100)} %`}</span>72    </span>73  );74}7576/** Pure-SVG sparkline; renders on the server. Nulls break the line (missing data is shown as a gap, never interpolated). */77export function Sparkline({ values, width = 240, height = 36, color = 'var(--accent)', bands = false, strokeWidth = 1.25, className = '' }: { values: (number | null)[]; width?: number; height?: number; color?: string; bands?: boolean; strokeWidth?: number; className?: string }) {78  const nums = values.filter((v): v is number => v != null);79  if (nums.length < 2) return <svg width={width} height={height} className={className} aria-hidden="true" />;80  const min = bands ? 0 : Math.min(...nums);81  const max = bands ? 100 : Math.max(...nums);82  const span = max - min || 1;83  const x = (i: number) => (i / (values.length - 1)) * (width - 2) + 1;84  const y = (v: number) => height - 2 - ((v - min) / span) * (height - 4);85  let d = '';86  let pen = false;87  values.forEach((v, i) => {88    if (v == null) {89      pen = false;90      return;91    }92    d += `${pen ? 'L' : 'M'}${x(i).toFixed(1)},${y(v).toFixed(1)} `;93    pen = true;94  });95  const last = [...values].reverse().find((v) => v != null);96  const lastI = values.length - 1 - [...values].reverse().findIndex((v) => v != null);97  return (98    <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className={className} aria-hidden="true" preserveAspectRatio="none">99      <path d={d} fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinejoin="round" strokeLinecap="round" vectorEffect="non-scaling-stroke" />100      {last != null && <circle cx={x(lastI)} cy={y(last)} r={2} fill={color} />}101    </svg>102  );103}104105export function Empty({ children = 'Not observed' }: { children?: ReactNode }) {106  return <p className="py-6 text-center text-xs text-ink-3">{children}</p>;107}108109export function Stat({ label, value, sub, className = '' }: { label: string; value: ReactNode; sub?: ReactNode; className?: string }) {110  return (111    <div className={`min-w-0 ${className}`}>112      <div className="label truncate">{label}</div>113      <div className="num mt-0.5 text-[17px] leading-tight text-ink">{value}</div>114      {sub && <div className="mt-0.5 truncate text-[11px] text-ink-2">{sub}</div>}115    </div>116  );117}118119export function StatusDot({ status }: { status: string }) {120  const color: Record<string, string> = { online: 'var(--ok)', stale: 'var(--warn)', offline: 'var(--bad)', excluded: 'var(--ink-3)', detected: 'var(--p-elevated)', developing: 'var(--p-stressed)', active: 'var(--p-high)', recovering: 'var(--p-calm)', resolved: 'var(--ink-3)', ok: 'var(--ok)', degraded: 'var(--warn)' };121  return (122    <span className="inline-flex items-center gap-1.5 text-[11px] uppercase tracking-[0.1em]" style={{ color: color[status] ?? 'var(--ink-2)' }}>123      <span className="size-1.5 rounded-full" style={{ background: color[status] ?? 'var(--ink-2)' }} aria-hidden="true" />124      {status}125    </span>126  );127}128129export function LevelLegend({ compact = false }: { compact?: boolean }) {130  return (131    <ul className={`flex flex-wrap items-center gap-x-3 gap-y-1 ${compact ? 'text-[10px]' : 'text-[11px]'} text-ink-2`}>132      {(133        [134          ['calm', '≤10'],135          ['normal', '≤25'],136          ['elevated', '≤40'],137          ['stressed', '≤55'],138          ['high', '≤70'],139          ['severe', '≤85'],140          ['extreme', '≤100'],141        ] as [LevelId, string][]142      ).map(([id, range]) => {143        const l = levelById(id) as (typeof LEVELS)[number];144        return (145          <li key={id} className="inline-flex items-center gap-1.5">146            <span className="size-2 rounded-[1px]" style={{ background: l.color }} aria-hidden="true" />147            <span>{l.short}</span>148            {!compact && <span className="num text-ink-3">{range}</span>}149          </li>150        );151      })}152      <li className="inline-flex items-center gap-1.5">153        <span className="size-2 rounded-[1px] border border-line-2 bg-neutral" aria-hidden="true" />154        <span>not observed</span>155      </li>156    </ul>157  );158}159