Data displayui
Activity feed
Timeline of who did what, and when.
- AL
Ada Lovelace deployed
acme-webto production - GH
Grace Hopper added the env var STRIPE_SECRET_KEY
Particular.Tech rotated the database password
Scheduled rotation, no action needed.- LT
Linus Torvalds invited mh@acme.dev as a Developer
Installation
npx shadcn@latest add @particular/activity-feednpm dependencies
- cn@^0.4.0
Installs these registry items
Source
import * as React from "react"
import { cn } from "cn"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { RelativeTime } from "@/components/ui/relative-time"
type ActivityItem = {
id: string
actor: { name: string; avatarUrl?: string }
/** Sentence fragment after the actor's name, e.g. "deployed api to production". */
action: React.ReactNode
timestamp: Date | string | number
/** Replaces the avatar, e.g. an icon for system events. */
icon?: React.ReactNode
detail?: React.ReactNode
}
function initials(name: string) {
return name
.split(/\s+/)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase() ?? "")
.join("")
}
function ActivityFeed({
items,
className,
...props
}: Omit<React.ComponentProps<"ol">, "children"> & { items: ActivityItem[] }) {
return (
<ol data-slot="activity-feed" className={cn("flex flex-col", className)} {...props}>
{items.map((item, index) => (
<li key={item.id} className="relative flex gap-3 pb-6 last:pb-0">
{index < items.length - 1 && (
<span aria-hidden className="absolute top-8 bottom-1 left-3.5 w-px -translate-x-1/2 bg-border" />
)}
{item.icon ? (
<span className="flex size-7 shrink-0 items-center justify-center rounded-full border bg-muted text-muted-foreground [&_svg]:size-3.5">
{item.icon}
</span>
) : (
<Avatar className="size-7">
{item.actor.avatarUrl && <AvatarImage src={item.actor.avatarUrl} alt="" />}
<AvatarFallback className="text-[0.625rem]">{initials(item.actor.name)}</AvatarFallback>
</Avatar>
)}
<div className="flex min-w-0 flex-1 flex-col gap-1 pt-1">
<p className="text-sm leading-tight">
<span className="font-medium">{item.actor.name}</span>{" "}
<span className="text-muted-foreground">{item.action}</span>
</p>
{item.detail && <div className="text-sm text-muted-foreground">{item.detail}</div>}
<RelativeTime date={item.timestamp} className="w-fit text-xs text-muted-foreground" />
</div>
</li>
))}
</ol>
)
}
export { ActivityFeed, type ActivityItem }