SPB Git

Repository Public

bits.tsx 2.8 KB 42 lines tsx

1import { level } from "./format";23const FILL = { normal: "bg-viz-cpu", warn: "bg-viz-warn", danger: "bg-viz-danger" } as const;4const TRACK = { normal: "bg-viz-track", warn: "bg-viz-warn-track", danger: "bg-viz-danger-track" } as const;56/** Utilisation meter. The fill carries severity; the printed value carries the reading, so colour is never the only channel. */7/** `severity` is for memory pressure only: a busy CPU or GPU is the cluster doing its job, not a warning. */8export function Meter({ label, value, text, shortText, muted = false, severity = false }: { label: string; value: number | null; text: string; shortText?: string; muted?: boolean; severity?: boolean }) {9  const lv = value === null || !severity ? "normal" : level(value);10  return (11    <div>12      <div className="flex items-baseline justify-between gap-2">13        <span className="font-mono text-[0.6875rem] uppercase tracking-[0.08em] text-mute">{label}</span>14        <span className={`num whitespace-nowrap font-mono text-[0.8125rem] ${muted ? "text-mute" : "text-fg"}`}>{shortText ? <><span className="sm:hidden">{shortText}</span><span className="hidden sm:inline">{text}</span></> : text}</span>15      </div>16      <div className={`mt-1.5 h-1.5 overflow-hidden rounded-full ${value === null ? "bg-surface-2" : TRACK[lv]}`} role="meter" aria-label={label} aria-valuemin={0} aria-valuemax={100} aria-valuenow={value ?? 0} aria-valuetext={text}>17        {value !== null ? <div className={`meter-fill h-full rounded-full ${FILL[lv]}`} style={{ width: `${Math.max(value, 1.5)}%` }} /> : null}18      </div>19    </div>20  );21}2223/** One-hour CPU trace: muted line, current reading marked in the series colour. Decorative; the value is printed beside it. */24export function Sparkline({ values, width = 120, height = 28 }: { values: number[]; width?: number; height?: number }) {25  if (values.length < 2) return <svg width={width} height={height} aria-hidden="true" />;26  const max = Math.max(20, ...values);27  const x = (i: number) => 3 + (i / (values.length - 1)) * (width - 6);28  const y = (v: number) => height - 3 - (v / max) * (height - 6);29  const d = values.map((v, i) => `${i ? "L" : "M"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join("");30  const last = values.length - 1;31  return (32    <svg viewBox={`0 0 ${width} ${height}`} width={width} height={height} aria-hidden="true" focusable="false" className="max-w-full">33      <path d={d} fill="none" stroke="#9aa5b4" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" />34      <circle cx={x(last)} cy={y(values[last])} r="3" fill="var(--color-viz-cpu)" stroke="var(--color-surface)" strokeWidth="1.5" />35    </svg>36  );37}3839export function StatusDot({ state }: { state: "on" | "off" | "warn" }) {40  return <span className="live-dot" data-state={state} aria-hidden="true" />;41}42
Files main
Menu

Theme