diff --git a/chat/chat-layout/README.md b/chat/chat-layout/README.md index 6f2a926f22..d1c999a6b7 100644 --- a/chat/chat-layout/README.md +++ b/chat/chat-layout/README.md @@ -26,21 +26,49 @@ npm install @lg-chat/chat-layout ## Overview -`@lg-chat/chat-layout` provides a CSS Grid-based layout system for building full-screen chat interfaces with a collapsible side nav. +`@lg-chat/chat-layout` provides a CSS Grid-based layout system for building full-screen chat interfaces with a side nav that can be collapsed or pinned. + +This package exports: + +- `ChatLayout`: The grid container and context provider +- `ChatMain`: Content area component that positions itself in the grid +- `useChatLayoutContext`: Hook for accessing layout state ## Examples +### Basic + +```tsx +import { ChatLayout, ChatMain } from '@lg-chat/chat-layout'; + +function MyChatApp() { + return ( + + {/* ChatSideNav will go here */} + +
Your chat content
+
+
+ ); +} +``` + +### With Initial State and Toggle Pinned Callback + ```tsx -import { ChatLayout } from '@lg-chat/chat-layout'; +import { ChatLayout, ChatMain } from '@lg-chat/chat-layout'; function MyChatApp() { - const handleToggle = (isPinned: boolean) => { + const handleTogglePinned = (isPinned: boolean) => { console.log('Side nav is now:', isPinned ? 'pinned' : 'collapsed'); }; return ( - - {/* ChatSideNav and ChatMain components will go here */} + + {/* ChatSideNav will go here */} + +
Your chat content
+
); } @@ -59,6 +87,16 @@ function MyChatApp() { All other props are passed through to the underlying `
` element. +### ChatMain + +| Prop | Type | Description | Default | +| ---------- | ----------- | -------------------------- | ------- | +| `children` | `ReactNode` | The main content to render | - | + +All other props are passed through to the underlying `
` element. + +**Note:** `ChatMain` must be used as a direct child of `ChatLayout` to work correctly within the grid system. + ## Context API ### useChatLayoutContext diff --git a/chat/chat-layout/package.json b/chat/chat-layout/package.json index ac788b8a79..3d3b26e0c6 100644 --- a/chat/chat-layout/package.json +++ b/chat/chat-layout/package.json @@ -33,6 +33,16 @@ "@leafygreen-ui/tokens": "workspace:^", "@lg-tools/test-harnesses": "workspace:^" }, + "peerDependencies": { + "@lg-chat/leafygreen-chat-provider": "workspace:^" + }, + "devDependencies": { + "@lg-chat/chat-window": "workspace:^", + "@lg-chat/input-bar": "workspace:^", + "@lg-chat/message": "workspace:^", + "@lg-chat/message-feed": "workspace:^", + "@lg-chat/title-bar": "workspace:^" + }, "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/chat/chat-layout", "repository": { "type": "git", diff --git a/chat/chat-layout/src/ChatLayout.stories.tsx b/chat/chat-layout/src/ChatLayout.stories.tsx index a1b6c90f4e..85108f5011 100644 --- a/chat/chat-layout/src/ChatLayout.stories.tsx +++ b/chat/chat-layout/src/ChatLayout.stories.tsx @@ -1,8 +1,19 @@ import React from 'react'; +import { ChatWindow } from '@lg-chat/chat-window'; +import { InputBar } from '@lg-chat/input-bar'; +import { + LeafyGreenChatProvider, + Variant, +} from '@lg-chat/leafygreen-chat-provider'; +import { Message } from '@lg-chat/message'; +import { MessageFeed } from '@lg-chat/message-feed'; +import { TitleBar } from '@lg-chat/title-bar'; import { StoryMetaType } from '@lg-tools/storybook-utils'; import { StoryFn, StoryObj } from '@storybook/react'; -import { ChatLayout, type ChatLayoutProps } from '.'; +import { css } from '@leafygreen-ui/emotion'; + +import { ChatLayout, type ChatLayoutProps, ChatMain } from '.'; const meta: StoryMetaType = { title: 'Composition/Chat/ChatLayout', @@ -10,10 +21,68 @@ const meta: StoryMetaType = { parameters: { default: 'LiveExample', }, + decorators: [ + Story => ( +
+ +
+ ), + ], }; export default meta; -const Template: StoryFn = props => ; +const sideNavPlaceholderStyles = css` + background-color: rgba(0, 0, 0, 0.05); + padding: 16px; + min-width: 200px; +`; + +const testMessages = [ + { + id: '1', + messageBody: 'Hello! How can I help you today?', + isSender: false, + }, + { + id: '2', + messageBody: 'I need help with my database query.', + }, + { + id: '3', + messageBody: + 'Sure! I can help with that. What specific issue are you encountering?', + isSender: false, + }, +]; + +const Template: StoryFn = props => ( + + +
ChatSideNav Placeholder
+ + + + + {testMessages.map(msg => ( + + ))} + + {}} /> + + +
+
+); export const LiveExample: StoryObj = { render: Template, diff --git a/chat/chat-layout/src/ChatLayout/ChatLayout.tsx b/chat/chat-layout/src/ChatLayout/ChatLayout.tsx index fbe818f86f..73f0386e84 100644 --- a/chat/chat-layout/src/ChatLayout/ChatLayout.tsx +++ b/chat/chat-layout/src/ChatLayout/ChatLayout.tsx @@ -6,9 +6,8 @@ import { ChatLayoutContext } from './ChatLayoutContext'; /** * ChatLayout is a context provider that manages the pinned state of the side nav - * and provides it to all child components. - * - * Context is primarily used by ChatSideNav and ChatMain. + * and provides it to all child components. It uses CSS Grid to control the layout + * and positioning the side nav and main content. */ export function ChatLayout({ children, diff --git a/chat/chat-layout/src/ChatLayout/ChatLayout.types.ts b/chat/chat-layout/src/ChatLayout/ChatLayout.types.ts index 0550d18606..f2d86c2bb4 100644 --- a/chat/chat-layout/src/ChatLayout/ChatLayout.types.ts +++ b/chat/chat-layout/src/ChatLayout/ChatLayout.types.ts @@ -1,19 +1,19 @@ -import { ComponentPropsWithRef, PropsWithChildren } from 'react'; +import { ComponentPropsWithRef } from 'react'; import { DarkModeProps } from '@leafygreen-ui/lib'; -export type ChatLayoutProps = ComponentPropsWithRef<'div'> & - DarkModeProps & - PropsWithChildren<{ - /** - * Initial state for whether the side nav is pinned (expanded). - * @default true - */ - initialIsPinned?: boolean; +export interface ChatLayoutProps + extends ComponentPropsWithRef<'div'>, + DarkModeProps { + /** + * Initial state for whether the side nav is pinned (expanded). + * @default true + */ + initialIsPinned?: boolean; - /** - * Callback fired when the side nav is toggled (pinned/unpinned). - * Receives the new `isPinned` state as an argument. - */ - onTogglePinned?: (isPinned: boolean) => void; - }>; + /** + * Callback fired when the side nav is toggled (pinned/unpinned). + * Receives the new `isPinned` state as an argument. + */ + onTogglePinned?: (isPinned: boolean) => void; +} diff --git a/chat/chat-layout/src/ChatMain/ChatMain.spec.tsx b/chat/chat-layout/src/ChatMain/ChatMain.spec.tsx new file mode 100644 index 0000000000..2f0376ad1e --- /dev/null +++ b/chat/chat-layout/src/ChatMain/ChatMain.spec.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { ChatLayout } from '../ChatLayout'; + +import { ChatMain } from '.'; + +describe('packages/chat-layout/ChatMain', () => { + describe('ChatMain', () => { + test('renders children', () => { + render( + + +
Main Content
+
+
, + ); + expect(screen.getByText('Main Content')).toBeInTheDocument(); + }); + + test('forwards HTML attributes to the div element', () => { + render( + + + Content + + , + ); + const element = screen.getByTestId('chat-main'); + expect(element).toHaveAttribute('aria-label', 'Chat content'); + }); + + test('forwards ref to the div element', () => { + const ref = React.createRef(); + render( + + Content + , + ); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + expect(ref.current?.tagName).toBe('DIV'); + }); + + test('applies custom className', () => { + render( + + + Content + + , + ); + const element = screen.getByTestId('chat-main'); + expect(element).toHaveClass('custom-class'); + }); + }); +}); diff --git a/chat/chat-layout/src/ChatMain/ChatMain.styles.ts b/chat/chat-layout/src/ChatMain/ChatMain.styles.ts new file mode 100644 index 0000000000..a8279dd2c4 --- /dev/null +++ b/chat/chat-layout/src/ChatMain/ChatMain.styles.ts @@ -0,0 +1,14 @@ +import { css, cx } from '@leafygreen-ui/emotion'; + +import { gridAreas } from '../constants'; + +const baseContainerStyles = css` + grid-area: ${gridAreas.main}; + display: flex; + flex-direction: column; + min-width: 0; + height: 100%; +`; + +export const getContainerStyles = ({ className }: { className?: string }) => + cx(baseContainerStyles, className); diff --git a/chat/chat-layout/src/ChatMain/ChatMain.tsx b/chat/chat-layout/src/ChatMain/ChatMain.tsx new file mode 100644 index 0000000000..7e62d19d72 --- /dev/null +++ b/chat/chat-layout/src/ChatMain/ChatMain.tsx @@ -0,0 +1,21 @@ +import React, { forwardRef } from 'react'; + +import { getContainerStyles } from './ChatMain.styles'; +import { ChatMainProps } from './ChatMain.types'; + +/** + * ChatMain represents the main content area of the chat layout. + * It automatically positions itself in the second column of the parent + * ChatLayout's CSS Grid, allowing the layout to control spacing for the sidebar. + */ +export const ChatMain = forwardRef( + ({ children, className, ...rest }, ref) => { + return ( +
+ {children} +
+ ); + }, +); + +ChatMain.displayName = 'ChatMain'; diff --git a/chat/chat-layout/src/ChatMain/ChatMain.types.ts b/chat/chat-layout/src/ChatMain/ChatMain.types.ts new file mode 100644 index 0000000000..9070c8ea0e --- /dev/null +++ b/chat/chat-layout/src/ChatMain/ChatMain.types.ts @@ -0,0 +1,7 @@ +import { ComponentPropsWithRef } from 'react'; + +import { DarkModeProps } from '@leafygreen-ui/lib'; + +export interface ChatMainProps + extends ComponentPropsWithRef<'div'>, + DarkModeProps {} diff --git a/chat/chat-layout/src/ChatMain/index.ts b/chat/chat-layout/src/ChatMain/index.ts new file mode 100644 index 0000000000..a212ee6fff --- /dev/null +++ b/chat/chat-layout/src/ChatMain/index.ts @@ -0,0 +1,2 @@ +export { ChatMain } from './ChatMain'; +export { type ChatMainProps } from './ChatMain.types'; diff --git a/chat/chat-layout/src/index.ts b/chat/chat-layout/src/index.ts index af5533d3db..8daa958273 100644 --- a/chat/chat-layout/src/index.ts +++ b/chat/chat-layout/src/index.ts @@ -4,3 +4,4 @@ export { type ChatLayoutProps, useChatLayoutContext, } from './ChatLayout'; +export { ChatMain, type ChatMainProps } from './ChatMain'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3897def354..b330dd06dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -349,9 +349,28 @@ importers: '@leafygreen-ui/tokens': specifier: workspace:^ version: link:../../packages/tokens + '@lg-chat/leafygreen-chat-provider': + specifier: workspace:^ + version: link:../leafygreen-chat-provider '@lg-tools/test-harnesses': specifier: workspace:^ version: link:../../tools/test-harnesses + devDependencies: + '@lg-chat/chat-window': + specifier: workspace:^ + version: link:../chat-window + '@lg-chat/input-bar': + specifier: workspace:^ + version: link:../input-bar + '@lg-chat/message': + specifier: workspace:^ + version: link:../message + '@lg-chat/message-feed': + specifier: workspace:^ + version: link:../message-feed + '@lg-chat/title-bar': + specifier: workspace:^ + version: link:../title-bar chat/chat-window: dependencies: