← All proposals

Suggestion Card

A card for use inside an agent `ChatMessage`:

Net-new componentOpen in Storybook

proposals/SuggestionCard/SuggestionCard.stories.tsx186 lines · View on GitHub
import { useState } from 'react';

import { FlexContainer } from '@pantheon-systems/pds-toolkit-react';
import { Meta, StoryObj } from '@storybook/react-vite';

import { SlotPlaceholder } from '../_story-helpers/SlotPlaceholder';
import { Button } from '../Button/Button';
import { ChatMessage } from '../ChatMessage/ChatMessage';
import {
	SuggestionCard,
	SuggestionCardState,
	SuggestionChange,
} from './SuggestionCard';

const CHANGE_ROWS = (
	<>
		<SuggestionChange
			after='The 2026 Championship'
			afterLabel='After'
			before='—'
			beforeLabel='Before'
			label='Eyebrow'
		/>
		<SuggestionChange
			after='Experience two weeks of elite tennis, electrifying fan moments, and championship glory. Be part of the 2026 Grand Slam Open.'
			afterLabel='After'
			before='—'
			beforeLabel='Before'
			label='Description'
		/>
	</>
);

const CONTENT = (
	<SlotPlaceholder label='children' style={{ minHeight: '6rem' }} />
);

const ACTIONS = (
	<>
		<Button label='Accept' variant='primary' />
		<Button label='Refine' variant='secondary' />
		<Button label='Dismiss' variant='subtle' />
	</>
);

const meta: Meta<typeof SuggestionCard> = {
	tags: ['new-pattern'],
	title: 'Proposals/Suggestion Card',
	component: SuggestionCard,
	parameters: {
		docs: {
			description: {
				component:
					'**New pattern:** not in PDS v2 today. An agent’s proposed changes in a conversation, with the buttons to decide. Built on `Panel`, with a title, a description, a list of before/after changes and an actions slot. It has a pending state and an accepted state.',
			},
		},
	},
	argTypes: {
		actions: { table: { category: 'Content' } },
		buttonSize: {
			control: 'select',
			options: ['xs', 's'],
			table: { category: 'Component configuration' },
		},
		children: { table: { category: 'Content' } },
		className: { table: { category: 'Base attributes' } },
		description: { control: 'text', table: { category: 'Content' } },
		headingLevel: {
			control: 'select',
			options: ['h2', 'h3', 'h4', 'h5', 'h6'],
			table: { category: 'Component configuration' },
		},
		headingSize: {
			control: 'select',
			options: ['m', 'l'],
			table: { category: 'Component configuration' },
		},
		padding: {
			control: 'select',
			options: ['none', 'condensed', 'standard', 'spacious'],
			table: { category: 'Component configuration' },
		},
		state: {
			control: 'select',
			options: ['pending', 'accepted'],
			table: { category: 'State' },
		},
		statusLabel: { control: 'text', table: { category: 'Content' } },
		title: { control: 'text', table: { category: 'Content' } },
	},
	args: {
		actions: ACTIONS,
		buttonSize: 'xs',
		children: CONTENT,
		description: 'Tighten and energize the hero copy for stronger impact.',
		headingLevel: 'h3',
		headingSize: 'm',
		padding: 'condensed',
		state: 'pending',
		statusLabel: 'Changes applied',
		title: 'Heading',
	},
};

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const WithChanges: Story = {
	args: { children: CHANGE_ROWS },
	parameters: {
		docs: {
			description: {
				story:
					'A worked example of the content area: `SuggestionChange` rows, each a field name with its before and after values. What goes in the content area is up to the screen.',
			},
		},
	},
};

export const Accepted: Story = {
	args: { state: 'accepted' },
	parameters: {
		docs: {
			description: {
				story:
					'Once the user accepts, set `state` to `accepted`. The actions are replaced by `statusLabel` (announced to screen readers), so the card in the history can’t be accepted twice. The changes stay visible. The final look of this state is still to be refined.',
			},
		},
	},
};

export const Interactive: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'Accept switches the card to `accepted`. Story-only wiring: the consumer owns the state.',
			},
		},
	},
	render: (args) => {
		const [state, setState] = useState<SuggestionCardState>('pending');
		return (
			<SuggestionCard
				{...args}
				actions={
					<>
						<Button
							label='Accept'
							onClick={() => setState('accepted')}
							variant='primary'
						/>
						<Button label='Refine' variant='secondary' />
						<Button label='Dismiss' variant='subtle' />
					</>
				}
				state={state}
			/>
		);
	},
};

export const InChat: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'The card as agent content inside a `ChatMessage`, after the agent’s text.',
			},
		},
	},
	render: (args) => (
		<div style={{ maxWidth: '22rem' }}>
			<ChatMessage label='Zappy'>
				<FlexContainer flexDirection='column' gap='l'>
					<p>Here are two changes to tighten the hero copy:</p>
					<SuggestionCard {...args} />
				</FlexContainer>
			</ChatMessage>
		</div>
	),
};