Vectrum
Sign In
80 lines · 3.0 KB · TypeScript
import Link from "next/link";
import type { ReactNode } from "react";
import { Surface } from "@unlocalhosted/metalui";
import { ActorAvatar, ActorName } from "./agent";
import { Markdown } from "./markdown";
import { Label } from "./ui";
import { formatDate, timeAgo } from "@/lib/format";

// The parts of an issue's or pull request's conversation: comments on raised plates beside their
// author, and events as lines on the activity rail.

export function CommentBox({
  author,
  agent,
  createdAt,
  body,
  verb = "commented",
  badge,
  children,
}: {
  author: string;
  agent?: string | null;
  createdAt: number;
  body: string;
  verb?: ReactNode;
  badge?: ReactNode;
  children?: ReactNode;
}) {
  return (
    <div className="flex gap-3">
      <Link href={`/${author}`} className="shrink-0">
        <ActorAvatar name={author} agent={agent} size={32} />
      </Link>
      <Surface material="raise-lite" radius="plate" className="min-w-0 flex-1">
        <div className="rule-b flex min-h-10 flex-wrap items-center gap-x-2 gap-y-1 px-4 py-2 type-ui">
          <ActorName person={author} agent={agent} />
          <span className="type-label engraved" title={formatDate(createdAt)}>
            {verb} {timeAgo(createdAt)}
          </span>
          {badge && <span className="ml-auto">{badge}</span>}
        </div>
        {body.trim() ? (
          <div className="px-5 py-4">
            <Markdown>{body}</Markdown>
          </div>
        ) : (
          !children && <p className="px-5 py-4 type-body text-ink3 italic">No description.</p>
        )}
        {children && <div className={body.trim() ? "rule-t space-y-3 px-4 py-3" : "space-y-3 px-4 py-3"}>{children}</div>}
      </Surface>
    </div>
  );
}

/** A point on the activity rail: a small sunk well on the page, holding the event's glyph or an unlit dot. */
export function TimelineDot({ children }: { children?: ReactNode }) {
  return (
    <span className="well flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-full -ml-[3px]">
      {children ?? <span className="h-1.5 w-1.5 rounded-full bg-ink3" />}
    </span>
  );
}

/** One line on the rail: who did what, when. */
export function TimelineEvent({ icon, actor, agent, at, children }: { icon?: ReactNode; actor: string | null; agent?: string | null; at: number; children: ReactNode }) {
  return (
    <p className="relative flex flex-wrap items-center gap-x-1.5 gap-y-1 pl-2.5 type-body text-ink2">
      <TimelineDot>{icon}</TimelineDot>
      {actor ? <ActorName person={actor} agent={agent} /> : <strong className="font-medium text-ink">Someone</strong>} {children} ·{" "}
      <span title={formatDate(at)}>{timeAgo(at)}</span>
    </p>
  );
}

/** The rail the timeline hangs on. */
export function Timeline({ children }: { children: ReactNode }) {
  return <div className="relative space-y-4 before:absolute before:top-2 before:bottom-2 before:left-4 before:w-px before:bg-line">{children}</div>;
}

export const AuthorBadge = () => <Label tone="accent">Author</Label>;