Repository Public
1/** Formatting helpers. Null/undefined always renders as an em dash: the UI never invents values. */2export const DASH = '—';34export function fmtBytes(n: number | null | undefined): string {5 if (n == null || !Number.isFinite(n)) return DASH;6 if (n < 1024) return `${n} B`;7 const units = ['KiB', 'MiB', 'GiB', 'TiB'];8 let v = n / 1024;9 let i = 0;10 while (v >= 1024 && i < units.length - 1) {11 v /= 1024;12 i++;13 }14 return `${v >= 100 ? v.toFixed(0) : v.toFixed(1)} ${units[i]}`;15}16export function fmtNum(n: number | null | undefined, digits = 0, unit = ''): string {17 if (n == null || !Number.isFinite(n)) return DASH;18 return `${n.toLocaleString('en-CA', { minimumFractionDigits: digits, maximumFractionDigits: digits })}${unit}`;19}20export function fmtPct(n: number | null | undefined, digits = 0): string {21 return n == null || !Number.isFinite(n) ? DASH : `${n.toFixed(digits)} %`;22}23export function fmtTime(iso: string | number | null | undefined): string {24 if (iso == null) return DASH;25 const d = new Date(iso);26 return Number.isNaN(d.getTime()) ? DASH : d.toLocaleTimeString('en-CA', { hour12: false });27}28export function fmtDateTime(iso: string | number | null | undefined): string {29 if (iso == null) return DASH;30 const d = new Date(iso);31 if (Number.isNaN(d.getTime())) return DASH;32 return `${d.toLocaleDateString('en-CA')} ${d.toLocaleTimeString('en-CA', { hour12: false })}`;33}34export function fmtRelative(iso: string | number | null | undefined, nowMs = Date.now()): string {35 if (iso == null) return DASH;36 const t = new Date(iso).getTime();37 if (Number.isNaN(t)) return DASH;38 const s = Math.round((nowMs - t) / 1000);39 if (s < 5) return 'just now';40 if (s < 60) return `${s} s ago`;41 if (s < 3600) return `${Math.floor(s / 60)} min ago`;42 if (s < 86400) return `${Math.floor(s / 3600)} h ago`;43 if (s < 86400 * 30) return `${Math.floor(s / 86400)} d ago`;44 return new Date(t).toLocaleDateString('en-CA');45}46export function fmtDuration(sec: number | null | undefined): string {47 if (sec == null || !Number.isFinite(sec) || sec < 0) return DASH;48 const s = Math.floor(sec);49 if (s < 60) return `${s} s`;50 if (s < 3600) return `${Math.floor(s / 60)} min ${String(s % 60).padStart(2, '0')} s`;51 if (s < 86400) return `${Math.floor(s / 3600)} h ${String(Math.floor((s % 3600) / 60)).padStart(2, '0')} min`;52 return `${Math.floor(s / 86400)} d ${Math.floor((s % 86400) / 3600)} h`;53}54/** US dollars. Small amounts keep 3–4 decimals so that a cheap turn never reads "$0.00". */55export function fmtUsd(n: number | null | undefined, opts: { compact?: boolean } = {}): string {56 if (n == null || !Number.isFinite(n)) return DASH;57 const a = Math.abs(n);58 const digits = opts.compact ? (a >= 100 ? 0 : 2) : a === 0 || a >= 1 ? 2 : a >= 0.01 ? 3 : 4;59 return `$${n.toLocaleString('en-CA', { minimumFractionDigits: digits, maximumFractionDigits: digits })}`;60}61/** Token counts: 1 234 → "1.2k", 2 500 000 → "2.50M". */62export function fmtTokens(n: number | null | undefined): string {63 if (n == null || !Number.isFinite(n)) return DASH;64 if (n < 1000) return String(Math.round(n));65 if (n < 1e6) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`;66 return `${(n / 1e6).toFixed(n < 1e7 ? 2 : 1)}M`;67}68export function fmtMs(ms: number | null | undefined): string {69 if (ms == null || !Number.isFinite(ms) || ms < 0) return DASH;70 if (ms < 1000) return `${Math.round(ms)} ms`;71 if (ms < 60_000) return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)} s`;72 return fmtDuration(ms / 1000);73}74/** Seconds between two instants (`end` null ⇒ now); null when the start is unknown. */75export function elapsedSec(start: string | null | undefined, end: string | null | undefined, nowMs = Date.now()): number | null {76 if (!start) return null;77 const a = new Date(start).getTime();78 const b = end ? new Date(end).getTime() : nowMs;79 return Number.isNaN(a) || Number.isNaN(b) ? null : Math.max(0, (b - a) / 1000);80}81export function truncate(s: string, max: number): string {82 return s.length <= max ? s : `${s.slice(0, max - 1)}…`;83}84export const isMac = (): boolean => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);85