SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
2.5 KB · 49 lines tsx
Raw Blame History
1'use client';2import { useEffect, useState } from 'react';3import { cn } from '@/lib/cn';4import { DASH, fmtAgo, fmtDateTime } from '@/lib/format';56/** Green live dot with optional pulse ring. */7export function Dot({ pulse = false, className, tone = 'live' }: { pulse?: boolean; className?: string; tone?: 'live' | 'warning' | 'danger' | 'muted' }) {8  return <span className={cn('dot', pulse && 'pulse', tone === 'warning' && 'bg-warning', tone === 'danger' && 'bg-danger', tone === 'muted' && 'bg-ink-3', className)} aria-hidden />;9}1011/**12 * "17 sec ago" that ticks every second for the first minutes, then every 30 s. Server renders the absolute UTC time13 * (identical on both sides); the relative label is applied after mount (`suppressHydrationWarning`).14 */15export function LiveAgo({ at, prefix = '', className, absoluteFallback = true, tick = 1000 }: { at: string | null | undefined; prefix?: string; className?: string; absoluteFallback?: boolean; tick?: number }) {16  const [now, setNow] = useState<number | null>(null);17  useEffect(() => {18    setNow(Date.now());19    const t = setInterval(() => setNow(Date.now()), tick);20    return () => clearInterval(t);21  }, [tick]);22  const label = now === null ? (absoluteFallback ? fmtDateTime(at) : DASH) : fmtAgo(at, now);23  return (24    <time dateTime={at ?? undefined} title={at ? fmtDateTime(at) : undefined} className={cn('tnum', className)} suppressHydrationWarning>25      {prefix}26      {label}27    </time>28  );29}3031/** "Live · updated 12 s ago" status line for panels fed by SSE/polling. */32export function LiveStatus({ updatedAt, connected = true, className }: { updatedAt: number | string | null; connected?: boolean; className?: string }) {33  const [now, setNow] = useState<number | null>(null);34  useEffect(() => {35    setNow(Date.now());36    const t = setInterval(() => setNow(Date.now()), 1000);37    return () => clearInterval(t);38  }, []);39  const ts = typeof updatedAt === 'number' ? updatedAt : updatedAt ? new Date(updatedAt).getTime() : null;40  const s = ts && now ? Math.max(0, Math.round((now - ts) / 1000)) : null;41  return (42    <span className={cn('inline-flex items-center gap-1.5 text-xs text-ink-3', className)} suppressHydrationWarning>43      <Dot pulse={connected} tone={connected ? 'live' : 'muted'} />44      <span className={cn('font-medium', connected ? 'text-positive' : 'text-ink-3')}>{connected ? 'Live' : 'Paused'}</span>45      {s !== null && <span className="tnum">· updated {s < 1 ? 'now' : `${s} s ago`}</span>}46    </span>47  );48}49