← All proposals

Chat Attachment

Net-new component: one attached item for chat.

Net-new componentOpen in Storybook

proposals/ChatAttachment/ChatAttachment.stories.tsx138 lines · View on GitHub
import { Meta, StoryObj } from '@storybook/react-vite';

import './chat-attachment-story.css';
import { SAMPLE_IMAGE } from '../_story-helpers/sample-image';
import { ChatAttachment, ChatAttachmentProps } from './ChatAttachment';

const meta: Meta<typeof ChatAttachment> = {
	tags: ['new-pattern', 'ready-for-dev'],
	title: 'Component Proposals/AI Chat/Chat Attachment',
	component: ChatAttachment,
	parameters: {
		docs: {
			description: {
				component:
					'**New pattern:** not in PDS v2 today. One attached file or image in a chat, for the `fieldHeader` slot of `ChatInput`. File chips are a fixed width and shorten long names in the middle; hover or focus shows the full name. The remove button shows on hover and focus (always on touch). `status="error"` marks a failed attachment.',
			},
		},
	},
	argTypes: {
		className: { table: { category: 'Base attributes' } },
		errorMessage: { control: 'text', table: { category: 'Content' } },
		fileName: { control: 'text', table: { category: 'Content' } },
		fileType: {
			control: 'select',
			options: ['file', 'image'],
			table: { category: 'Component configuration' },
		},
		onRemove: { table: { category: 'Events' } },
		previewUrl: { control: 'text', table: { category: 'Content' } },
		removeLabel: { control: 'text', table: { category: 'Content' } },
		status: {
			control: 'select',
			options: ['default', 'error'],
			table: { category: 'Component configuration' },
		},
	},
	args: {
		fileName: 'notes.md',
		fileType: 'file',
		onRemove: () => {},
		removeLabel: 'Remove notes.md',
	},
};

export default meta;

type Story = StoryObj<typeof meta>;

const LONG_NAME =
	'verylong-filename-example-when-you-hover-you-see-the-full123.md';

interface StateProps {
	args: ChatAttachmentProps;
	label: string;
	showRemove?: boolean;
}

// One state with its caption. `showRemove` holds the hover-only remove button
// open so the state can be seen without a pointer.
const State = ({ args, label, showRemove = false }: StateProps) => (
	<div className='chat-attachment-story__state'>
		<ChatAttachment
			{...args}
			className={showRemove ? 'chat-attachment-story--hover' : undefined}
			onRemove={showRemove ? (args.onRemove ?? (() => {})) : undefined}
		/>
		<span className='chat-attachment-story__label'>{label}</span>
	</div>
);

export const Default: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'A generic file (documents, text and other files). Hover, or tab to the chip, to see the full name and the remove button.',
			},
		},
	},
	render: (args) => (
		<div className='chat-attachment-story__states'>
			<State args={args} label='Default' />
			<State args={args} label='Hover or focus' showRemove />
			<State
				args={{
					...args,
					errorMessage: 'Upload failed',
					status: 'error',
				}}
				label='Error'
			/>
			<State
				args={{
					...args,
					errorMessage: 'Upload failed',
					status: 'error',
				}}
				label='Error, hover or focus'
				showRemove
			/>
			<State
				args={{ ...args, fileName: LONG_NAME }}
				label='Long name'
				showRemove
			/>
		</div>
	),
};

export const Image: Story = {
	args: {
		fileName: 'skyline.jpg',
		fileType: 'image',
		previewUrl: SAMPLE_IMAGE,
		removeLabel: 'Remove skyline.jpg',
	},
	parameters: {
		docs: {
			description: {
				story:
					'An image. It shows a thumbnail from `previewUrl`, or an image icon when there is no preview.',
			},
		},
	},
	render: (args) => (
		<div className='chat-attachment-story__states'>
			<State args={args} label='Default' />
			<State args={args} label='Hover or focus' showRemove />
			<State args={{ ...args, previewUrl: undefined }} label='No preview' />
			<State
				args={{ ...args, previewUrl: undefined }}
				label='No preview, hover or focus'
				showRemove
			/>
		</div>
	),
};