Live Feed

Rolling activity stream for surfacing incidents, deploys, and operational signals in real time. Install Live Feed from the VLLNT UI registry with the shadcn CLI.

Report a bug

Rolling activity stream for surfacing incidents, deploys, and operational signals in real time. Part of the data family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

Add Live Feed to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/live-feed.json

Source

"use client"; import * as React from "react"; import { normalizeDate } from "../../lib/format"; import { useLiveDate } from "../../lib/use-live-date"; import { cn } from "../../lib/utils"; import { Badge } from "../badge/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../card/card"; import { SeverityBadge, type SeverityBadgeLevel, } from "../severity-badge/severity-badge"; export type LiveFeedEvent = { id: string; message?: string; severity: SeverityBadgeLevel; source?: string; timestamp: Date | number | string; title: string; }; export type LiveFeedProps = React.ComponentPropsWithoutRef<"div"> & { description?: string; emptyLabel?: string; events: LiveFeedEvent[]; maxItems?: number; now?: Date | number | string; tickMs?: number; title?: string; }; const SECOND_MS = 1000; const MINUTE_MS = 60 * SECOND_MS; const HOUR_MS = 60 * MINUTE_MS; const DAY_MS = 24 * HOUR_MS; function formatRelative(eventDate: Date, now: Date): string { const deltaMs = now.getTime() - eventDate.getTime(); if (deltaMs < 5 * SECOND_MS) { return "just now"; } if (deltaMs < MINUTE_MS) { return `${Math.floor(deltaMs / SECOND_MS)}s ago`; } if (deltaMs < HOUR_MS) { return `${Math.floor(deltaMs / MINUTE_MS)}m ago`; } if (deltaMs < DAY_MS) { return `${Math.floor(deltaMs / HOUR_MS)}h ago`; } if (deltaMs < 7 * DAY_MS) { return `${Math.floor(deltaMs / DAY_MS)}d ago`; } return SHORT_DATE_FORMATTER.format(eventDate); } const SHORT_DATE_FORMATTER = new Intl.DateTimeFormat("en-US", { day: "numeric", month: "short", }); const ABSOLUTE_FORMATTER = new Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit", month: "short", second: "2-digit", }); function formatAbsolute(eventDate: Date): string { return ABSOLUTE_FORMATTER.format(eventDate); } function sortEventsDesc(events: LiveFeedEvent[]): LiveFeedEvent[] { return [...events].sort( (a, b) => normalizeDate(b.timestamp).getTime() - normalizeDate(a.timestamp).getTime(), ); } function LiveFeedRow({ event, isLatest, now, }: { event: LiveFeedEvent; isLatest: boolean; now: Date; }) { const eventDate = normalizeDate(event.timestamp); return ( <li className="flex gap-3 border-b border-border/60 py-3 last:border-b-0"> <div className="pt-1"> <SeverityBadge level={event.severity} pulse={isLatest ? event.severity === "critical" : undefined} tone="soft" /> </div> <div className="min-w-0 flex-1"> <div className="flex items-baseline justify-between gap-2"> <p className="truncate text-sm font-medium">{event.title}</p> <time className="whitespace-nowrap text-xs text-muted-foreground" dateTime={eventDate.toISOString()} title={formatAbsolute(eventDate)} > {formatRelative(eventDate, now)} </time> </div> {event.message ? ( <p className="mt-0.5 text-sm text-muted-foreground"> {event.message} </p> ) : null} {event.source ? ( <p className="mt-1 text-xs uppercase tracking-[0.14em] text-muted-foreground"> {event.source} </p> ) : null} </div> </li> ); } export const LiveFeed = ({ className, description, emptyLabel = "No events yet", events, maxItems = 50, now, ref, tickMs = 30_000, title = "Live feed", ...props }: LiveFeedProps & { ref?: React.Ref<HTMLDivElement> }) => { const liveNow = useLiveDate(now, tickMs); const visibleEvents = React.useMemo( () => sortEventsDesc(events).slice(0, maxItems), [events, maxItems], ); return ( <Card className={cn("shadow-sm", className)} ref={ref} {...props}> <CardHeader className="flex flex-row items-start justify-between gap-3 gap-y-0 pb-3"> <div className="space-y-1"> <CardTitle className="text-base">{title}</CardTitle> {description ? ( <CardDescription>{description}</CardDescription> ) : null} </div> <Badge variant="outline"> <span aria-hidden="true" className="relative mr-1.5 inline-flex size-2" > <span className="absolute inline-flex size-2 animate-ping rounded-full bg-emerald-500 opacity-70" /> <span className="relative inline-flex size-2 rounded-full bg-emerald-500" /> </span>{" "} Live </Badge> </CardHeader> <CardContent className="pt-0"> {visibleEvents.length === 0 ? ( <div className="py-8 text-center text-sm text-muted-foreground"> {emptyLabel} </div> ) : ( <ul className="max-h-[360px] divide-y divide-border/60 overflow-y-auto"> {visibleEvents.map((event, index) => ( <LiveFeedRow event={event} isLatest={index === 0} key={event.id} now={liveNow} /> ))} </ul> )} </CardContent> </Card> ); }; LiveFeed.displayName = "LiveFeed";

Stories

Explore every variant and state in the interactive Storybook:

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Dependencies

  • @vllnt/ui@^0.3.0