ham7a311

Activity Stream

Component

About

An activity log/feed where the vertical gap between events scales with the time elapsed between them, grouping bursts of activity and visually marking quiet periods, with day headers, grouped/folded repeat events, and an expandable detail per event.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Activity Stream to your project and fits it to your theme.

Code

How Activity Stream is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import * as React from "react";
import { ActivityStream } from "@/components/ui/activity-stream";
import type { Activity } from "@/components/ui/activity-stream";

const M = 60_000;

function seed(now: number): Activity[] {
  const at = (minsAgo: number) => now - minsAgo * M;
  const log = (lines: string[]) => (
    <pre className="m-0 whitespace-pre-wrap font-mono text-[11.5px] leading-[1.6] opacity-80">
      {lines.join("\n")}
    </pre>
  );
  return [
    {
      id: "e1",
      at: at(3),
      actor: "Mia",
      kind: "deploy",
      text: (
        <>
          deployed <em>nimbus-web</em> to Production
        </>
      ),
      status: { label: "Ready", tone: "ok" },
      detail: log([
        "7f3c2a1 · Move search to edge runtime",
        "Built in 1m 42s · 38 routes",
        "✓ nimbus.app",
      ]),
    },
    {
      id: "e2",
      at: at(9),
      actor: "CI",
      kind: "approve",
      text: (
        <>
          checks passed on <em>PR #212</em>
        </>
      ),
      status: { label: "12 / 12", tone: "ok" },
    },
    {
      id: "e3",
      at: at(14),
      actor: "Mia",
      kind: "comment",
      text: (
        <>
          commented on <em>Search architecture notes</em>
        </>
      ),
      detail: (
        <p className="m-0 max-w-[52ch] leading-relaxed">
          "Edge cache keys need the workspace id, otherwise the two apps will
          share facets. Adding that before we flip the flag."
        </p>
      ),
    },
    {
      id: "e4",
      at: at(16),
      actor: "Mia",
      kind: "status",
      text: (
        <>
          moved <em>PRJ-212</em> from In progress to Review
        </>
      ),
    },
    {
      id: "e5",
      at: at(24),
      actor: "Mia",
      kind: "upload",
      groupKey: "upload-atlas",
      groupText: (n) => (
        <>
          uploaded {n} files to <em>Atlas</em>
        </>
      ),
      text: (
        <>
          uploaded <em>floor-c-plan.pdf</em>
        </>
      ),
    },
    {
      id: "e6",
      at: at(26),
      actor: "Mia",
      kind: "upload",
      groupKey: "upload-atlas",
      groupText: (n) => (
        <>
          uploaded {n} files to <em>Atlas</em>
        </>
      ),
      text: (
        <>
          uploaded <em>room-index.csv</em>
        </>
      ),
    },
    {
      id: "e7",
      at: at(27),
      actor: "Mia",
      kind: "upload",
      groupKey: "upload-atlas",
      groupText: (n) => (
        <>
          uploaded {n} files to <em>Atlas</em>
        </>
      ),
      text: (
        <>
          uploaded <em>signage-photos.zip</em>
        </>
      ),
    },
    {
      id: "e8",
      at: at(31),
      actor: "Mia",
      kind: "upload",
      groupKey: "upload-atlas",
      groupText: (n) => (
        <>
          uploaded {n} files to <em>Atlas</em>
        </>
      ),
      text: (
        <>
          uploaded <em>entrances.geojson</em>
        </>
      ),
    },
    {
      id: "e9",
      at: at(214),
      actor: "CI",
      kind: "deploy",
      text: (
        <>
          deployed <em>worker-mail</em> to Production
        </>
      ),
      status: { label: "Failed", tone: "bad" },
      detail: log([
        "2ad9cc3 · Template for invoice reminders",
        "error: Cannot find module './reminder.mjml'",
        "✗ Rolled back to e3a7713",
      ]),
    },
    {
      id: "e10",
      at: at(222),
      actor: "Mia",
      kind: "assign",
      text: (
        <>
          assigned <em>PRJ-205 · Invoice reminders send twice</em> to themself
        </>
      ),
    },
    {
      id: "e11",
      at: at(300),
      actor: "Design Team",
      kind: "approve",
      text: (
        <>
          approved the <em>Q3 roadmap</em>
        </>
      ),
      status: { label: "Approved", tone: "ok" },
    },
    {
      id: "e12",
      at: at(24 * 60 + 40),
      actor: "Mia",
      kind: "status",
      text: (
        <>
          closed <em>PRJ-41 · Room search index</em>
        </>
      ),
      status: { label: "Done", tone: "neutral" },
    },
    {
      id: "e13",
      at: at(24 * 60 + 55),
      actor: "CI",
      kind: "deploy",
      text: (
        <>
          deployed <em>nimbus-api</em> to Staging
        </>
      ),
      status: { label: "Ready", tone: "ok" },
    },
    {
      id: "e14",
      at: at(24 * 60 + 300),
      actor: "Mia",
      kind: "comment",
      text: (
        <>
          commented on <em>Draft v4 — methods</em>
        </>
      ),
      detail: (
        <p className="m-0 max-w-[52ch] leading-relaxed">
          "Section 3.2 still reads like a list. Rewriting it as a narrative of
          the survey rounds."
        </p>
      ),
    },
  ];
}

const INCOMING: Omit<Activity, "id" | "at">[] = [
  {
    actor: "CI",
    kind: "approve",
    text: (
      <>
        checks passed on <em>PR #214</em>
      </>
    ),
    status: { label: "12 / 12", tone: "ok" },
  },
  {
    actor: "Mia",
    kind: "comment",
    text: (
      <>
        replied in <em>PRJ-209 · Deploy previews</em>
      </>
    ),
    detail: (
      <p className="m-0 leading-relaxed">
        "Reproduced on the monorepo fixture — it's the 10 MB manifest limit."
      </p>
    ),
  },
  {
    actor: "Mia",
    kind: "deploy",
    text: (
      <>
        deployed <em>search-edge</em> to Preview
      </>
    ),
    status: { label: "Building", tone: "warn" },
  },
];

export default function ActivityStreamDemo() {
  const [now, setNow] = React.useState(() => Date.now());
  const base = React.useMemo(() => seed(now), []); // eslint-disable-line react-hooks/exhaustive-deps
  const [events, setEvents] = React.useState<Activity[]>(base);
  const [n, setN] = React.useState(0);

  const push = () => {
    const t = Date.now();
    setNow(t);
    setEvents((e) => [
      { ...INCOMING[n % INCOMING.length], id: `new${n}`, at: t },
      ...e,
    ]);
    setN((x) => x + 1);
  };

  React.useEffect(() => {
    const t = window.setInterval(() => setNow(Date.now()), 30_000);
    return () => window.clearInterval(t);
  }, []);

  return (
    <div
      className="flex h-full min-h-[640px] w-full justify-center overflow-auto bg-[#fbfaf6] px-5 py-8 text-[#1b1a17]"
      style={{ fontFamily: "Geist, ui-sans-serif, system-ui" }}
    >
      <div className="w-full max-w-[720px]">
        <div className="mb-5 flex flex-wrap items-center gap-2">
          <h1 className="m-0 mr-auto text-[20px] font-semibold tracking-[-0.02em]">
            Project activity
          </h1>
          <button
            type="button"
            onClick={push}
            className="h-8 rounded-lg px-3 text-[12.5px] ring-1 ring-black/10"
          >
            Simulate new event
          </button>
        </div>
        <ActivityStream
          theme="paper"
          label="Project activity"
          now={now}
          events={events}
        />
      </div>
    </div>
  );
}

More by ham7a311

View all
  • Route Globe

    Route Globe

    16
  • Wave Mesh

    Wave Mesh

    11
  • Glass Tiles

    Glass Tiles

    8
  • Glass Carousel

    Glass Carousel

    7
  • Uptime Ribbon

    Uptime Ribbon

    6
  • Wind Map

    Wind Map

    6
  • Streak Field Stats

    Streak Field Stats

    5
  • Time Window

    Time Window

    5