← All proposals

Agent avatar

Start from the PDS v2 Avatar (src/components/identity/Avatar, branch v2.x), renamed AgentAvatar (class pds-agent-avatar).

Net-new componentOpen in Storybook

proposals/AgentAvatar/AgentAvatar.tsx121 lines · View on GitHub
import {
	cloneElement,
	ComponentPropsWithoutRef,
	isValidElement,
	ReactNode,
} from 'react';

import { Icon } from '../Icon/Icon';

import './agent-avatar.css';

// Stand-in for the v2 `mergeClasses` util, which the published package does
// not export. The port uses the real one.
const mergeClasses = (classes: Array<string | null | undefined>) =>
	classes.filter(Boolean).join(' ');

export interface AgentAvatarProps extends ComponentPropsWithoutRef<'div'> {
	/**
	 * Aria label for the avatar when used as a link. This is required if using a link.
	 */
	ariaLabel?: string;
	/**
	 * Additional class names
	 */
	className?: string;
	/**
	 * Show the AI-gradient ring (halo) around the avatar. Without it the avatar
	 * is the dark disc and icon only.
	 */
	hasHalo?: boolean;
	/**
	 * Icon inside the avatar. `zappy` for Pantheon agents, `sparklesSolid` for other
	 * (third-party) agents.
	 */
	iconName?: 'sparklesSolid' | 'zappy';
	/**
	 * Screen-reader text that says this is an agent, for example "Agent". The
	 * visual treatment alone is not enough for assistive tech. Provide it
	 * translated.
	 */
	label: string;
	/**
	 * A link element using the router of your choice. Leave empty for no link.
	 */
	linkContent?: ReactNode;
	/**
	 * Avatar shape.
	 */
	shape?: 'circle' | 'square';
	/**
	 * Avatar size. `xs` = 16px, `s` = 20px (default), `m` = 24px, `l` = 32px,
	 * `xl` = 40px.
	 */
	size?: 'xs' | 's' | 'm' | 'l' | 'xl';
}

/**
 * Agent Avatar UI component
 */
export const AgentAvatar = ({
	ariaLabel,
	className,
	hasHalo = true,
	iconName = 'zappy',
	label,
	linkContent,
	shape = 'circle',
	size = 's',
	...props
}: AgentAvatarProps) => {
	// Set up classes.
	const baseClass = 'pds-agent-avatar';
	const sizeClass = `${baseClass}--${size}`;
	const shapeClass = shape !== 'circle' ? `${baseClass}--${shape}` : null;
	const haloClass = hasHalo ? `${baseClass}--halo` : null;

	// When used as a link, the link's aria-label names it instead.
	const agentContent = (
		<>
			<Icon
				className={`${baseClass}__icon`}
				iconName={iconName}
				aria-hidden='true'
			/>
			{!linkContent && <span className='visually-hidden'>{label}</span>}
		</>
	);

	const interactiveContent =
		linkContent && isValidElement(linkContent)
			? cloneElement(
					linkContent,
					Object.assign({}, linkContent.props, {
						className: `${baseClass}__content ${baseClass}__content--link`,
						'aria-label': ariaLabel ?? label,
					}),
					agentContent,
				)
			: null;

	// Render the output
	return (
		<div
			className={mergeClasses([
				baseClass,
				sizeClass,
				haloClass,
				shapeClass,
				className,
			])}
			{...props}
		>
			{linkContent ? (
				interactiveContent
			) : (
				<span className={`${baseClass}__content`}>{agentContent}</span>
			)}
		</div>
	);
};