Section
Team directory
Responsive directory of people with photos, roles and optional links.
npx -y github:anwarlemu/agentcard-brand-cli#release add team-directoryUsage
Use for teams, advisors, investors or partners. Use tone="blue" for the primary directory and the default tone for others. Provide current names and links.
- Color photos
- Round avatars
- Bold names
- More than one blue-bar window per page
- Cards with gaps between people instead of the ruled grid
Dependencies: window.
Example
import { TeamDirectory } from "@/components/site/team-directory";
// A neutral stand-in for a photo: initials on a pale block (the directory grays it as it does a photo).
function Initials({ name }: { name: string }) {
const letters = name
.split(" ")
.map((w) => w[0])
.slice(0, 2)
.join("");
return (
<span role="img" aria-label={`Photo of ${name}`} className="flex items-center justify-center bg-sky font-display text-body-sm text-fg-brand">
{letters}
</span>
);
}
const TEAM = [
{ name: "Ada Okafor", role: "CEO & Co-Founder" },
{ name: "Luis Romero", role: "CTO & Co-Founder" },
{ name: "Mara Lindqvist", role: "Member of Technical Staff" },
{ name: "Noor Haddad", role: "Member of Marketing Staff" },
];
const INVESTORS = [
["Priya Raman", "Co-founder, a fund"],
["Tom Becker", "CEO, a commerce platform"],
["Elena Ruiz", "CTO, a CRM"],
["Sam Wu", "CEO, an auth company"],
["Jonas Berg", "CEO, a cloud"],
["Ines Duarte", "Board, an AI lab"],
["Kofi Mensah", "CEO, a delivery app"],
["Hana Sato", "Design, a device maker"],
["Ravi Patel", "Payments"],
];
function YMark() {
return (
<svg viewBox="0 0 40 40" width="40" height="40" aria-hidden className="block size-10 flex-none">
<rect width="40" height="40" className="fill-brand" />
<path d="M13 11l7 11 7-11M20 22v8" className="fill-none stroke-white" strokeWidth="3" />
</svg>
);
}
export function Site() {
return (
<div className="mx-auto grid max-w-(--ds-layout-text) gap-16 px-5 py-16 sm:px-0">
<section aria-labelledby="team">
<h2 id="team" className="mb-3.5 font-display text-caption font-normal text-fg-brand">
Team
</h2>
<TeamDirectory name="team.dir" tone="blue" columns={2} people={TEAM.map((p) => ({ ...p, photo: <Initials name={p.name} /> }))} />
<p className="mt-4 text-body-sm text-fg-muted">
We are not hiring, but you can always send us an{" "}
<a href="mailto:support@agentcard.sh" className="text-fg-brand underline underline-offset-3">
email
</a>
.
</p>
</section>
<section aria-labelledby="investors">
<h2 id="investors" className="mb-3.5 font-display text-caption font-normal text-fg-brand">
Investors
</h2>
<TeamDirectory
name="investors.dir"
columns={3}
people={INVESTORS.map(([name, role]) => ({ name, role, photo: <Initials name={name} /> }))}
footer={[
{ logo: <YMark />, label: "Backed by an accelerator" },
{ logo: <span className="font-display text-h5 tracking-mark text-fg">NORTHFUND</span>, ariaLabel: "North Fund" },
]}
/>
</section>
</div>
);
}Source
import * as React from "react";
import { Window } from "@/components/ui/window";
import { cn } from "@/lib/utils";
// The About page's team and investors (agentcard.sh/about, .v7-about-sec): a window holding a ruled grid of people.
// Each cell is a gray photo, the name in the display face and the role in small muted display type; a cell that links
// out carries a blue arrow in its top right corner that nudges up and right as the cell lights sky under the pointer.
// The team sits two to a row in the blue-bar window, the investors three to a row in the sky one, with the backers
// (Y Combinator, Pioneer) as the last, two-column row. One column on phones.
type Person = {
name: React.ReactNode;
role?: React.ReactNode;
/** A photo: an image URL, or any node (an <img>, a Next Image). Shown in grayscale. */
photo?: React.ReactNode | string;
/** Alt text when `photo` is a URL; defaults to "Photo of <name>" when the name is a string. */
photoAlt?: string;
/** Makes the cell a link (opens in a new tab) with the arrow. */
href?: string;
};
type FooterCell = {
/** A logo or mark (an <img>, an inline SVG), shown as given. */
logo?: React.ReactNode;
/** Text beside the logo (Backed by Y Combinator), in muted body type. */
label?: React.ReactNode;
href?: string;
/** The link's accessible name when the cell has only a logo. */
ariaLabel?: string;
};
const COLUMNS = { 1: "sm:grid-cols-1", 2: "sm:grid-cols-2", 3: "sm:grid-cols-3" } as const;
/** White cells that finish a short last row, so the ruled grid never shows its line color as a gray block. */
function Fill({ count, columns }: { count: number; columns: number }) {
const left = (columns - (count % columns)) % columns;
return Array.from({ length: left }, (_, i) => <li key={`fill-${i}`} aria-hidden className="hidden bg-surface sm:block" />);
}
/** The outbound arrow in a link cell's corner. */
function Go() {
return (
<span
aria-hidden
className="absolute top-3 right-3.5 font-sans text-body-sm leading-none text-fg-brand transition-[translate] duration-150 ease-out group-hover/cell:translate-x-0.5 group-hover/cell:-translate-y-0.5 motion-reduce:transition-none motion-reduce:group-hover/cell:translate-0"
>
↗
</span>
);
}
function Cell({ href, ariaLabel, large, className, children }: { href?: string; ariaLabel?: string; large?: boolean; className?: string; children: React.ReactNode }) {
const box = cn("relative flex flex-1 items-center gap-3.5 p-4 text-inherit no-underline", large ? "min-h-18 sm:min-h-24" : "min-h-18 sm:min-h-21", className);
return (
<li className="flex bg-surface">
{href ? (
<a
href={href}
target="_blank"
rel="noreferrer"
aria-label={ariaLabel}
className={cn(
box,
"group/cell pr-9 outline-none transition-[background-color] duration-150 ease-out hover:bg-sky-wash focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring motion-reduce:transition-none",
)}
>
{children}
<Go />
</a>
) : (
<div className={box}>{children}</div>
)}
</li>
);
}
function Photo({ photo, alt, large }: { photo: Person["photo"]; alt: string; large?: boolean }) {
const size = large ? "size-16" : "size-10";
if (typeof photo === "string") return <img src={photo} alt={alt} width={large ? 64 : 40} height={large ? 64 : 40} className={cn("block flex-none object-cover grayscale", size)} />;
return <span className={cn("block flex-none overflow-hidden grayscale [&>*]:size-full [&_img]:object-cover", size)}>{photo}</span>;
}
type TeamDirectoryProps = Omit<React.ComponentProps<"div">, "children"> & {
/** The window bar's label (team.dir, investors.dir). */
name: React.ReactNode;
/** blue for the team (the window that matters), sky for the rest. */
tone?: "blue" | "sky";
/** People per row from the small breakpoint up; one on phones. Two cells larger photos (64px), three smaller (40px). */
columns?: 1 | 2 | 3;
people: Person[];
/** The last row: backers' logos and names, two to a row. */
footer?: FooterCell[];
};
function TeamDirectory({ name, tone = "sky", columns = 2, people, footer, className, ...props }: TeamDirectoryProps) {
const large = columns <= 2;
return (
<Window name={name} tone={tone} raised className={className} bodyClassName="overflow-hidden rounded-b-[calc(var(--ds-radius-card)-1px)]" {...props}>
<ul className={cn("m-0 grid list-none grid-cols-1 gap-px bg-line p-0", COLUMNS[columns])}>
{people.map((p, i) => {
const alt = p.photoAlt ?? (typeof p.name === "string" ? `Photo of ${p.name}` : "");
return (
<Cell key={i} href={p.href} large={large}>
{p.photo ? <Photo photo={p.photo} alt={alt} large={large} /> : null}
<span className="grid min-w-0 gap-1">
<b className="font-display text-body leading-snug font-medium">{p.name}</b>
{p.role ? <span className="font-display text-micro leading-4 tracking-label text-fg-muted">{p.role}</span> : null}
</span>
</Cell>
);
})}
<Fill count={people.length} columns={columns} />
</ul>
{footer?.length ? (
<ul className="m-0 grid list-none grid-cols-1 gap-px border-t border-line bg-line p-0 sm:grid-cols-2">
{footer.map((f, i) => (
<Cell key={i} href={f.href} ariaLabel={f.ariaLabel} className="[&_img]:block [&_img]:h-auto [&_img]:max-h-10 [&_img]:w-auto">
{f.logo}
{f.label ? <span className="text-body text-fg-muted">{f.label}</span> : null}
</Cell>
))}
<Fill count={footer.length} columns={2} />
</ul>
) : null}
</Window>
);
}
export { TeamDirectory };
export type { Person as TeamPerson, FooterCell as TeamFooterCell, TeamDirectoryProps };Reference: agentcard-website src/components/v7/company-page.tsx; src/app/globals.css:1808 (.v7-about); agentcard.sh/about (computed: .v7-about-sec, .v7-about-cell).