Design system
GitHub

Section

Product grid

Product overview cards with text, actions and media.

npx -y github:anwarlemu/agentcard-brand-cli#release add product-grid
Open alone
product-grid.preview
Preview

Usage

use-it-for.md

Pass products with title, description, action and media. Use layout="lead" for a full-width card and layout="half" for paired cards.

never.md
  • An odd number of half cards
  • A card without media
  • Two keys in one card
  • Media on a white ground instead of the stage

Dependencies: button.

Example

examples/product-grid.tsx
import { ArrowUpRight } from "lucide-react";

import { ProductGrid } from "@/components/site/product-grid";
import { Window } from "@/components/ui/window";

// Neutral stand-ins for the brand's pictures (3D cards, keycaps, screens), drawn from tokens so the layout reads.
function Cards() {
  return (
    <div className="relative h-56 w-80">
      <div className="absolute inset-x-6 top-16 h-32 -rotate-2 rounded-card border border-hairline bg-surface shadow-tile" />
      <div className="absolute inset-x-4 top-8 h-32 rotate-1 rounded-card bg-blue-mid shadow-tile" />
      <div className="absolute inset-x-2 top-0 flex h-32 -rotate-3 flex-col justify-between rounded-card bg-inverse p-4 text-on-brand shadow-tile">
        <span className="font-display text-body-sm">agentcard</span>
        <span className="self-end font-display text-caption">···· 4832</span>
      </div>
    </div>
  );
}

function Keys() {
  const tints = ["bg-tint-peach", "bg-sky", "bg-tint-teal", "bg-tint-rose", "bg-tint-sage"];
  return (
    <div className="grid grid-cols-5 gap-3 md:gap-4">
      {Array.from({ length: 15 }, (_, i) => (
        <div key={i} className={`size-10 rounded-card md:size-14 border border-hairline shadow-key ${tints[(i * 2) % tints.length]}`} />
      ))}
    </div>
  );
}

function Screen({ name, rows }: { name: string; rows: string[] }) {
  return (
    <Window name={name} className="w-full max-w-md -rotate-1">
      <ul className="grid gap-2 p-4 text-caption">
        {rows.map((row) => (
          <li key={row} className="flex items-center justify-between rounded-sm border border-hairline px-3 py-2">
            {row}
            <span className="rounded-chip bg-sky px-1.5 text-micro text-fg-brand">Active</span>
          </li>
        ))}
      </ul>
    </Window>
  );
}

export function Site() {
  return (
    <section className="mx-auto w-full max-w-(--ds-frame-column) py-20">
      <div className="mx-auto mb-18 max-w-(--ds-layout-heading) text-center max-lg:mb-10">
        <h2 className="font-display text-h2 leading-heading font-medium tracking-tighter">Explore our products</h2>
        <p className="mt-3.5 text-body-lg leading-6.5 tracking-tight opacity-70">Vault, Issuing, Voice and the Purchase Agent: what an agent needs to pay for things.</p>
      </div>
      <ProductGrid
        products={[
          { layout: "lead", title: "Vault", description: "Store customers’ cards and make purchases with agents safely.", action: { label: "Explore Vault", href: "https://www.agentcard.sh/vault" }, media: <Cards /> },
          {
            layout: "half",
            title: "Purchase Agent",
            description: "Integrate our agent into yours and enable purchases from Amazon, DoorDash and many other online stores.",
            action: { label: "Read the docs", href: "https://docs.agentcard.sh", icon: <ArrowUpRight /> },
            media: <Keys />,
          },
          { layout: "half", title: "Issuing", description: "Create one-time and multi-use cards for your agent.", action: { label: "Explore Issuing", href: "https://www.agentcard.sh/issuing" }, media: <Screen name="issuing.product" rows={["Agentcard ···· 4832", "Agentcard ···· 7719", "Agentcard ···· 2051"]} /> },
          { layout: "lead", title: "Voice", description: "Let customers approve their agent’s purchases by speaking, with no screen to look at.", action: { label: "Explore Voice", href: "https://cal.com/team/agentcard/agentcard-call" }, media: <Screen name="voice.product" rows={["Your usual from Rose Indian, $34.20", "Approved"]} /> },
        ]}
      />
    </section>
  );
}

Source

components/site/product-grid.tsx
import * as React from "react";

import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

// The products (agentcard.sh home, "Explore our products"; .v7-sol): large raised cards, white with 20px corners, a
// hairline and the hard 7px offset shadow, each holding a blue stage for the product's picture and its copy. A `lead`
// card spans the row, the stage on one side and the copy on the other (each lead turns the other way from the last);
// `half` cards pair up, stage above copy, their stages and copy lined up across the pair. Titles are the display face
// at 36px and 400, the line 19px at 60 percent ink, then the blue key. Below 1024px every card stacks, stage first.

type ProductAction = { label: React.ReactNode; href: string; icon?: React.ReactNode };

type ProductItem = {
  title: React.ReactNode;
  description: React.ReactNode;
  /** The card's key: a label and href (the blue key), or your own element. */
  action?: ProductAction | React.ReactNode;
  /** The product's picture on the blue stage: a screen, a window, a 3D object. */
  media: React.ReactNode;
  layout?: "lead" | "half";
};

const isAction = (a: ProductItem["action"]): a is ProductAction => typeof a === "object" && a !== null && !React.isValidElement(a) && "href" in a;

function Action({ action, className }: { action: ProductItem["action"]; className?: string }) {
  if (!action) return null;
  if (!isAction(action)) return <div className={className}>{action}</div>;
  return (
    <Button asChild className={className}>
      <a href={action.href}>
        {action.label}
        {action.icon}
      </a>
    </Button>
  );
}

// The stage: the blue ground under the brand's horizontal lines, darkened toward its edges.
function Stage({ children, className }: { children: React.ReactNode; className?: string }) {
  return (
    <div
      className={cn(
        "relative isolate flex min-h-80 items-center justify-center overflow-hidden rounded-card bg-brand p-[clamp(0.875rem,1.8vw,1.375rem)]",
        "before:absolute before:inset-0 before:-z-10 before:bg-[repeating-linear-gradient(0deg,color-mix(in_srgb,var(--ds-color-ink)_58%,transparent)_0_3px,transparent_3px_5px)]",
        "after:pointer-events-none after:absolute after:inset-0 after:-z-10 after:bg-[radial-gradient(ellipse_at_center,transparent_55%,color-mix(in_srgb,var(--ds-color-ink)_38%,transparent)),linear-gradient(90deg,color-mix(in_srgb,var(--ds-color-ink)_34%,transparent),transparent_16%,transparent_84%,color-mix(in_srgb,var(--ds-color-ink)_34%,transparent))]",
        className,
      )}
    >
      {children}
    </div>
  );
}

const CARD = "rounded-shell border border-hairline bg-surface p-3 text-fg shadow-raised max-lg:mx-auto max-lg:w-full max-lg:max-w-140";
const TITLE = "mb-3 font-display text-h2 leading-display font-normal tracking-tighter";
const LINE = "text-body-lg leading-snug tracking-tight opacity-60";

function LeadCard({ product, reverse }: { product: ProductItem; reverse: boolean }) {
  return (
    <li
      className={cn(
        CARD,
        "grid grid-cols-1 items-center lg:col-span-2 lg:gap-[clamp(2rem,5vw,4.5rem)]",
        reverse ? "lg:grid-cols-[minmax(0,1fr)_minmax(0,1.25fr)]" : "lg:grid-cols-[minmax(0,1.25fr)_minmax(0,1fr)]",
      )}
    >
      <Stage className={cn("self-stretch", reverse && "lg:order-2")}>{product.media}</Stage>
      <div className={cn("px-3 pt-5.5 pb-3 lg:px-0 lg:py-6", reverse ? "lg:pl-[clamp(1.25rem,3vw,3rem)]" : "lg:pr-[clamp(1.25rem,3vw,3rem)]")}>
        <h3 className={TITLE}>{product.title}</h3>
        <p className={cn(LINE, "max-w-[30ch]")}>{product.description}</p>
        <Action action={product.action} className="mt-5.5" />
      </div>
    </li>
  );
}

function HalfCard({ product }: { product: ProductItem }) {
  return (
    <li className={cn(CARD, "grid grid-cols-1 lg:row-span-2 lg:grid-rows-subgrid lg:gap-0")}>
      <Stage className="self-stretch py-[clamp(1.125rem,2vw,1.625rem)]">{product.media}</Stage>
      <div className="flex flex-col self-stretch px-3 pt-5.5 pb-3">
        <h3 className={TITLE}>{product.title}</h3>
        <p className={cn(LINE, "mb-9 max-w-[44ch]")}>{product.description}</p>
        {product.action && <Action action={product.action} className="mt-auto self-start" />}
      </div>
    </li>
  );
}

/** The product cards: leads across the row, halves in pairs. */
function ProductGrid({ products, className, ...props }: React.ComponentProps<"ul"> & { products: ProductItem[] }) {
  let leads = 0;
  return (
    <ul data-slot="product-grid" className={cn("grid grid-cols-1 gap-6 lg:grid-cols-2 lg:gap-8", className)} {...props}>
      {products.map((product, i) =>
        product.layout === "half" ? <HalfCard key={i} product={product} /> : <LeadCard key={i} product={product} reverse={leads++ % 2 === 1} />,
      )}
    </ul>
  );
}

export { ProductGrid, type ProductItem, type ProductAction };

Reference: agentcard-website src/components/v7/products-showcase.tsx (SolutionCard); agentcard.sh / (.v7-sol, .v7-sol-lead, .v7-sol-half, computed).