'use client';
import { useEffect, useState } from 'react';
import { cn } from '@/lib/cn';
import { DASH, fmtAgo, fmtDateTime } from '@/lib/format';
/** Green live dot with optional pulse ring. */
export function Dot({ pulse = false, className, tone = 'live' }: { pulse?: boolean; className?: string; tone?: 'live' | 'warning' | 'danger' | 'muted' }) {
return ;
}
/**
* "17 sec ago" that ticks every second for the first minutes, then every 30 s. Server renders the absolute UTC time
* (identical on both sides); the relative label is applied after mount (`suppressHydrationWarning`).
*/
export function LiveAgo({ at, prefix = '', className, absoluteFallback = true, tick = 1000 }: { at: string | null | undefined; prefix?: string; className?: string; absoluteFallback?: boolean; tick?: number }) {
const [now, setNow] = useState(null);
useEffect(() => {
setNow(Date.now());
const t = setInterval(() => setNow(Date.now()), tick);
return () => clearInterval(t);
}, [tick]);
const label = now === null ? (absoluteFallback ? fmtDateTime(at) : DASH) : fmtAgo(at, now);
return (
);
}
/** "Live · updated 12 s ago" status line for panels fed by SSE/polling. */
export function LiveStatus({ updatedAt, connected = true, className }: { updatedAt: number | string | null; connected?: boolean; className?: string }) {
const [now, setNow] = useState(null);
useEffect(() => {
setNow(Date.now());
const t = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(t);
}, []);
const ts = typeof updatedAt === 'number' ? updatedAt : updatedAt ? new Date(updatedAt).getTime() : null;
const s = ts && now ? Math.max(0, Math.round((now - ts) / 1000)) : null;
return (
{connected ? 'Live' : 'Paused'}
{s !== null && · updated {s < 1 ? 'now' : `${s} s ago`}}
);
}