spb/internetpressure
Public
TypeScript 36.3%
Python 31.8%
Go 18%
JavaScript 9.8%
Shell 1.9%
SQL 1.4%
CSS 0.5%
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