← All proposals

Badge

A copy of the PDS v2 Badge (pds-badge, same class names, props and CSS) with one addition: color='agentic-filled', the AI gradient as the fill, for denoting an agentic category.

Extends a PDS componentOpen in Storybook

proposals/Badge/Badge.tsx125 lines · View on GitHub
import { ComponentPropsWithoutRef, ReactNode } from 'react';

import { IconSize } from '@pantheon-systems/pds-toolkit-react';

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

import './badge.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 type BadgeColor =
	| 'agentic-filled'
	| 'ai-active'
	| 'brand'
	| 'brand-muted'
	| 'critical'
	| 'critical-muted'
	| 'discovery'
	| 'discovery-muted'
	| 'gold'
	| 'gold-muted'
	| 'indigo'
	| 'indigo-muted'
	| 'info'
	| 'info-muted'
	| 'ink'
	| 'ink-muted'
	| 'lime'
	| 'lime-muted'
	| 'orange'
	| 'orange-muted'
	| 'rose'
	| 'rose-muted'
	| 'silver'
	| 'silver-muted'
	| 'sky'
	| 'sky-muted'
	| 'slate'
	| 'slate-muted'
	| 'stone'
	| 'stone-muted'
	| 'success'
	| 'success-muted'
	| 'warning'
	| 'warning-muted';

export type BadgeSize = 'xs' | 's' | 'm';

/**
 * Maps each badge size to the closest-fitting Icon size, so a badge's icon
 * never has to be requested directly by consumers.
 */
const iconSizes: Record<BadgeSize, IconSize> = {
	xs: 'xs',
	s: 'xs',
	m: 'm',
};

/**
 * Badge component props
 */
export interface BadgeProps extends ComponentPropsWithoutRef<'span'> {
	/**
	 * Additional class names
	 */
	className?: string;
	/**
	 * Badge color
	 */
	color: BadgeColor;
	/**
	 * Optional leading icon. Sized to fit the badge's `size` and always
	 * colored to match the badge's foreground color.
	 */
	iconName?: IconProps['iconName'];
	/**
	 * Badge text or content.
	 */
	label: ReactNode;
	/**
	 * Badge size
	 */
	size?: BadgeSize;
}

/**
 * Badge UI component
 */
export const Badge = ({
	className,
	color = 'brand-muted',
	iconName,
	label,
	size = 's',
	...props
}: BadgeProps) => {
	const baseClass = 'pds-badge';
	const colorClass = `${baseClass}--${color}`;
	const sizeClass = `${baseClass}--${size}`;

	return (
		<span
			className={mergeClasses([
				baseClass,
				colorClass,
				sizeClass,
				className ?? '',
			])}
			{...props}
		>
			{iconName && (
				<Icon
					className={`${baseClass}__icon`}
					iconName={iconName}
					size={iconSizes[size]}
				/>
			)}
			<span className={`${baseClass}__label`}>{label}</span>
		</span>
	);
};