import type { ReactNode } from 'react';
import { fmt, fmtDelta } from '@/lib/format';
import { levelById, levelFor, pressureColor, trendArrow, type LEVELS } from '@/lib/pressure';
import type { LevelId, Trend } from '@/lib/types';
export function Section({ id, label, title, right, children, className = '' }: { id?: string; label?: string; title?: string; right?: ReactNode; children: ReactNode; className?: string }) {
return (
{(label || title || right) && (
)}
{children}
);
}
export function LevelBadge({ level, value, size = 'sm' }: { level?: LevelId | string | null; value?: number | null; size?: 'xs' | 'sm' | 'md' }) {
const l = level ? levelById(level) : value != null ? levelFor(value) : undefined;
const color = l?.color ?? '#3A4756';
const cls = size === 'xs' ? 'text-[10px] tracking-[0.1em]' : size === 'md' ? 'text-[13px] tracking-[0.12em]' : 'text-[11px] tracking-[0.12em]';
return (
{l?.short ?? 'n/a'}
);
}
/** Pressure numeral coloured by level. */
export function PNum({ value, digits = 1, className = '' }: { value: number | null | undefined; digits?: number; className?: string }) {
return (
{fmt(value, digits)}
);
}
/** Signed delta with arrow; rising pressure is warm, falling is cool. */
export function Delta({ value, trend, digits = 1, suffix, className = '' }: { value: number | null | undefined; trend?: Trend; digits?: number; suffix?: string; className?: string }) {
if (value == null) return —;
const arrow = trendArrow(trend, value);
const color = value > 0.05 ? 'var(--p-stressed)' : value < -0.05 ? 'var(--p-calm)' : 'var(--ink-2)';
return (
{arrow} {fmtDelta(value, digits)}
{suffix ? {suffix} : null}
);
}
/** Thin horizontal bar 0–100 coloured by pressure (or a fixed colour). */
export function Bar({ value, max = 100, color, height = 3, className = '' }: { value: number | null | undefined; max?: number; color?: string; height?: number; className?: string }) {
const v = value == null ? 0 : Math.max(0, Math.min(max, value));
return (
);
}
export function ConfBar({ value, className = '' }: { value: number | null | undefined; className?: string }) {
return (
{value == null ? '—' : `${Math.round(value * 100)} %`}
);
}
/** Pure-SVG sparkline; renders on the server. Nulls break the line (missing data is shown as a gap, never interpolated). */
export 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 }) {
const nums = values.filter((v): v is number => v != null);
if (nums.length < 2) return ;
const min = bands ? 0 : Math.min(...nums);
const max = bands ? 100 : Math.max(...nums);
const span = max - min || 1;
const x = (i: number) => (i / (values.length - 1)) * (width - 2) + 1;
const y = (v: number) => height - 2 - ((v - min) / span) * (height - 4);
let d = '';
let pen = false;
values.forEach((v, i) => {
if (v == null) {
pen = false;
return;
}
d += `${pen ? 'L' : 'M'}${x(i).toFixed(1)},${y(v).toFixed(1)} `;
pen = true;
});
const last = [...values].reverse().find((v) => v != null);
const lastI = values.length - 1 - [...values].reverse().findIndex((v) => v != null);
return (
);
}
export function Empty({ children = 'Not observed' }: { children?: ReactNode }) {
return
{children}
;
}
export function Stat({ label, value, sub, className = '' }: { label: string; value: ReactNode; sub?: ReactNode; className?: string }) {
return (
{label}
{value}
{sub &&
{sub}
}
);
}
export function StatusDot({ status }: { status: string }) {
const color: Record = { 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)' };
return (
{status}
);
}
export function LevelLegend({ compact = false }: { compact?: boolean }) {
return (
{(
[
['calm', '≤10'],
['normal', '≤25'],
['elevated', '≤40'],
['stressed', '≤55'],
['high', '≤70'],
['severe', '≤85'],
['extreme', '≤100'],
] as [LevelId, string][]
).map(([id, range]) => {
const l = levelById(id) as (typeof LEVELS)[number];
return (
-
{l.short}
{!compact && {range}}
);
})}
-
not observed
);
}