opencommerceby Agentcard

Components

Message

One message: a 32px square avatar, the sender with the time in mono, the bubbles and a mono footer, mirrored for the agent's own.

npx -y github:anwarlemu/opencommerce-brand-cli#release add message --surface site

Run from the target React 19.2+ app root with Tailwind CSS 4 and the @/* source alias. Source and examples as JSON.

message.preview
JM
Jordan Miles
Order more printer paper for the office.
The usual, ten reams.
OA
Office agent
Ordered ten reams from Staples for $62.40. They arrive Thursday.
Delivered
Preview

Usage

use-it-for.md

Use for each turn of an agent conversation: MessageAvatar, then MessageContent holding MessageHeader (the name, then a time element), the Bubble parts and an optional MessageFooter (Delivered). Set align end for the agent's own messages. Stack turns in MessageGroup or MessageScroller.

avoid.md
  • Round avatars
  • Bold sender names
  • Timestamps in Inter

Exports: Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader. Read the examples for composition and props.

Example

examples/message.tsx
import { CheckCheck } from "lucide-react";

import { Bubble, BubbleContent, BubbleGroup } from "@/components/ui/bubble";
import { Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader } from "@/components/ui/message";

export function Site() {
  return (
    <MessageGroup className="mx-auto w-full max-w-xl">
      <Message>
        <MessageAvatar>JM</MessageAvatar>
        <MessageContent>
          <MessageHeader>
            Jordan Miles <time dateTime="2026-10-14T09:12">9:12 AM</time>
          </MessageHeader>
          <BubbleGroup>
            <Bubble variant="secondary">
              <BubbleContent>Order more printer paper for the office.</BubbleContent>
            </Bubble>
            <Bubble variant="secondary">
              <BubbleContent>The usual, ten reams.</BubbleContent>
            </Bubble>
          </BubbleGroup>
        </MessageContent>
      </Message>
      <Message align="end">
        <MessageAvatar>OA</MessageAvatar>
        <MessageContent>
          <MessageHeader>
            Office agent <time dateTime="2026-10-14T09:13">9:13 AM</time>
          </MessageHeader>
          <Bubble align="end">
            <BubbleContent>Ordered ten reams from Staples for $62.40. They arrive Thursday.</BubbleContent>
          </Bubble>
          <MessageFooter>
            <CheckCheck aria-hidden strokeWidth={1.5} />
            Delivered
          </MessageFooter>
        </MessageContent>
      </Message>
    </MessageGroup>
  );
}

Source

components/ui/message.tsx
import * as React from "react";

import { cn } from "@/lib/utils";

// One message in an agent conversation (extrapolated from avatar.tsx and the site's mono figures; computed): a 32px
// avatar square (the merchant tile's 6px corners on the 9 percent edge, initials in the display face, blue on the pale
// blue), the sender in 14px Inter at 500 with the time in the 11.5px platform mono at 64 percent ink beside it, the
// bubbles, and a mono footer (Delivered). The agent's own messages (align end) mirror to the right.
function MessageGroup({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="message-group" className={cn("flex min-w-0 flex-col gap-4", className)} {...props} />;
}

function Message({ className, align = "start", ...props }: React.ComponentProps<"div"> & { align?: "start" | "end" }) {
  return <div data-slot="message" data-align={align} className={cn("group/message relative flex w-full min-w-0 gap-2.5 font-sans text-body-sm data-[align=end]:flex-row-reverse", className)} {...props} />;
}

function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="message-avatar"
      className={cn(
        "flex size-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-card border border-line bg-blue-5 font-display text-caption leading-none text-fg-brand group-has-data-[slot=message-footer]/message:-translate-y-6 *:[img]:size-full *:[img]:object-cover",
        className,
      )}
      {...props}
    />
  );
}

function MessageContent({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="message-content" className={cn("flex w-full min-w-0 flex-col gap-1.5 break-words group-data-[align=end]/message:items-end", className)} {...props} />;
}

/** The sender and the time: the name in ink at 500, the time in the mono at 64 percent ink. */
function MessageHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="message-header"
      className={cn("flex max-w-full min-w-0 items-baseline gap-2 px-1 text-body-sm leading-tight font-medium text-fg group-data-[align=end]/message:flex-row-reverse [&_time]:font-mono [&_time]:text-bar [&_time]:font-normal [&_time]:text-fg-muted group-has-data-[variant=ghost]/message:px-0", className)}
      {...props}
    />
  );
}

function MessageFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="message-footer"
      className={cn("flex max-w-full min-w-0 items-center gap-1 px-1 font-mono text-bar leading-tight text-fg-muted group-data-[align=end]/message:justify-end group-has-data-[variant=ghost]/message:px-0 [&_svg:not([class*='size-'])]:size-3", className)}
      {...props}
    />
  );
}

export { Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader };

Reference: Extrapolated from opencommerce (avatar.tsx, mono figures).