diff --git a/packages/react-chat/src/components/AgentCard/AgentCard.story.tsx b/packages/react-chat/src/components/AgentCard/AgentCard.story.tsx new file mode 100644 index 0000000000..a82b0ed7f4 --- /dev/null +++ b/packages/react-chat/src/components/AgentCard/AgentCard.story.tsx @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import AgentCard from '.'; + +const meta: Meta = { + title: 'Components/AgentCard', + component: AgentCard, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Simple: Story = { + args: { + children:
Sample content inside the AgentCard
, + }, +}; + +export const WithComplexContent: Story = { + args: { + children: ( +
+

Dashboard Title

+

This is a sample dashboard card with multiple elements inside.

+ +
+ ), + }, +}; + +/** + * @see {@link https://voiceflow.github.io/react-chat/?path=/story/components-dashboard-card--simple} + */ diff --git a/packages/react-chat/src/components/AgentCard/__tests__/index.test.tsx b/packages/react-chat/src/components/AgentCard/__tests__/index.test.tsx new file mode 100644 index 0000000000..35a7b6873a --- /dev/null +++ b/packages/react-chat/src/components/AgentCard/__tests__/index.test.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it } from 'vitest'; +import '@testing-library/jest-dom'; + +import { ClassName } from '../../../constants'; +import AgentCard from '..'; + +describe('AgentCard', () => { + const testContent = 'Test Content'; + + it('renders children correctly', () => { + render({testContent}); + expect(screen.getByText(testContent)).toBeInTheDocument(); + }); + + it('passes through additional props', () => { + const testId = 'test-agent-card'; + const ariaLabel = 'Agent Card'; + render({testContent}); + + const card = screen.getByTestId(testId); + expect(card).toHaveAttribute('aria-label', ariaLabel); + }); + + it('applies custom className', () => { + const customClass = 'custom-class'; + const { container } = render({testContent}); + expect(container.firstChild).toHaveClass(customClass); + expect(container.firstChild).toHaveClass(ClassName.AGENT_CARD); + }); + + it('maintains correct styling', () => { + const { container } = render({testContent}); + const card = container.firstChild as HTMLElement; + + expect(card).toHaveStyle({ + borderRadius: '6px', + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + overflow: 'hidden', + backgroundColor: 'rgb(255, 255, 255)', // $white + }); + }); + + it('applies box shadow correctly', () => { + const { container } = render({testContent}); + const card = container.firstChild as HTMLElement; + + const boxShadow = window.getComputedStyle(card).boxShadow; + expect(boxShadow).toContain('0px 1px 3px 1px'); + expect(boxShadow).toContain('0px 4px 8px -6px'); + expect(boxShadow).toContain('0px 1px 5px -4px'); + expect(boxShadow).toContain('0px 0px 0px 1px'); + expect(boxShadow).toContain('0px 1px 0px 0px'); + }); + + it('handles complex nested content', () => { + const complexContent = ( +
+

Title

+

Description

+ +
+ ); + + render({complexContent}); + const content = screen.getByTestId('complex-content'); + expect(content).toBeInTheDocument(); + expect(screen.getByText('Title')).toBeInTheDocument(); + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('handles empty content gracefully', () => { + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + expect(container.firstChild).toHaveStyle({ + display: 'flex', + flexDirection: 'column', + }); + }); + + it('maintains layout with different content types', () => { + const { rerender, container } = render(Short text); + expect(container.firstChild).toHaveStyle({ + display: 'flex', + flexDirection: 'column', + }); + + rerender( + +
+

Large Content

+

With multiple paragraphs

+

And more content

+
+
+ ); + + expect(container.firstChild).toHaveStyle({ + display: 'flex', + flexDirection: 'column', + }); + }); +}); diff --git a/packages/react-chat/src/components/AgentCard/index.tsx b/packages/react-chat/src/components/AgentCard/index.tsx new file mode 100644 index 0000000000..151a5ef6ec --- /dev/null +++ b/packages/react-chat/src/components/AgentCard/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import { Container } from './styled'; + +export interface AgentCardProps extends React.HTMLAttributes { + /** + * The content to be rendered inside the card. + */ + children?: React.ReactNode; +} + +const AgentCard: React.FC = ({ children, ...props }) => ( + {children} +); + +/** + * A flexible card component with a rounded rectangle shape and subtle shadow effects. + */ +export default Object.assign(AgentCard, { + Container, +}); diff --git a/packages/react-chat/src/components/AgentCard/styled.ts b/packages/react-chat/src/components/AgentCard/styled.ts new file mode 100644 index 0000000000..6e20862e83 --- /dev/null +++ b/packages/react-chat/src/components/AgentCard/styled.ts @@ -0,0 +1,21 @@ +import { ClassName } from '@/constants'; +import { tagFactory } from '@/hocs'; +import { styled } from '@/styles'; + +const tag = tagFactory(ClassName.AGENT_CARD); + +export const Container = styled(tag('div'), { + borderRadius: '6px', + boxShadow: ` + 0px 1px 3px 1px #161A1E03, + 0px 4px 8px -6px #161A1E14, + 0px 1px 5px -4px #161A1E14, + 0px 0px 0px 1px #161A1E0A, + 0px 1px 0px 0px #161A1E05 + `, + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + overflow: 'hidden', + backgroundColor: '$white', +}); diff --git a/packages/react-chat/src/constants.ts b/packages/react-chat/src/constants.ts index 33f988464d..19828a1806 100644 --- a/packages/react-chat/src/constants.ts +++ b/packages/react-chat/src/constants.ts @@ -29,6 +29,7 @@ export enum ClassName { PROACTIVE_CLOSE = 'vfrc-proactive-close', PROACTIVE_MESSAGE = 'vfrc-proactive-message', PROACTIVE = 'vfrc-proactive', + AGENT_CARD = 'vfrc-agent-card', } export const DEVICE_INFO = Bowser.parse(window.navigator.userAgent);