|
| 1 | +# Chat Layout |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +#### [View on MongoDB.design](https://www.mongodb.design/component/chat-layout/live-example/) |
| 6 | + |
| 7 | +## Installation |
| 8 | + |
| 9 | +### PNPM |
| 10 | + |
| 11 | +```shell |
| 12 | +pnpm add @lg-chat/chat-layout |
| 13 | +``` |
| 14 | + |
| 15 | +### Yarn |
| 16 | + |
| 17 | +```shell |
| 18 | +yarn add @lg-chat/chat-layout |
| 19 | +``` |
| 20 | + |
| 21 | +### NPM |
| 22 | + |
| 23 | +```shell |
| 24 | +npm install @lg-chat/chat-layout |
| 25 | +``` |
| 26 | + |
| 27 | +## Overview |
| 28 | + |
| 29 | +`@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. |
| 30 | + |
| 31 | +This package exports: |
| 32 | + |
| 33 | +- `ChatLayout`: The grid container and context provider |
| 34 | +- `ChatMain`: The primary content area of the chat interface, automatically positioned within the grid layout. |
| 35 | +- `ChatSideNav`: A compound component representing the side navigation, exposing subcomponents such as `ChatSideNav.Header`, `ChatSideNav.Content`, and `ChatSideNav.SideNavItem` for flexible composition. |
| 36 | +- `useChatLayoutContext`: Hook for accessing layout state |
| 37 | + |
| 38 | +## Examples |
| 39 | + |
| 40 | +### Basic |
| 41 | + |
| 42 | +```tsx |
| 43 | +import { useState } from 'react'; |
| 44 | +import { ChatLayout, ChatMain, ChatSideNav } from '@lg-chat/chat-layout'; |
| 45 | + |
| 46 | +function MyChatApp() { |
| 47 | + const [activeChatId, setActiveChatId] = useState('1'); |
| 48 | + |
| 49 | + const chatItems = [ |
| 50 | + { id: '1', name: 'MongoDB Atlas Setup', href: '/chat/1' }, |
| 51 | + { id: '2', name: 'Database Query Help', href: '/chat/2' }, |
| 52 | + { id: '3', name: 'Schema Design Discussion', href: '/chat/3' }, |
| 53 | + ]; |
| 54 | + |
| 55 | + const handleNewChat = () => { |
| 56 | + console.log('Start new chat'); |
| 57 | + }; |
| 58 | + |
| 59 | + return ( |
| 60 | + <ChatLayout> |
| 61 | + <ChatSideNav> |
| 62 | + <ChatSideNav.Header onClickNewChat={handleNewChat} /> |
| 63 | + <ChatSideNav.Content> |
| 64 | + {chatItems.map(({ href, id, item, name }) => ( |
| 65 | + <ChatSideNav.SideNavItem |
| 66 | + key={id} |
| 67 | + href={href} |
| 68 | + active={id === activeChatId} |
| 69 | + onClick={e => { |
| 70 | + e.preventDefault(); |
| 71 | + setActiveChatId(id); |
| 72 | + }} |
| 73 | + > |
| 74 | + {name} |
| 75 | + </ChatSideNav.SideNavItem> |
| 76 | + ))} |
| 77 | + </ChatSideNav.Content> |
| 78 | + </ChatSideNav> |
| 79 | + <ChatMain>{/* Main chat content here */}</ChatMain> |
| 80 | + </ChatLayout> |
| 81 | + ); |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +### With Initial State and Toggle Pinned Callback |
| 86 | + |
| 87 | +```tsx |
| 88 | +import { ChatLayout, ChatMain, ChatSideNav } from '@lg-chat/chat-layout'; |
| 89 | + |
| 90 | +function MyChatApp() { |
| 91 | + const handleTogglePinned = (isPinned: boolean) => { |
| 92 | + console.log('Side nav is now:', isPinned ? 'pinned' : 'collapsed'); |
| 93 | + }; |
| 94 | + |
| 95 | + return ( |
| 96 | + <ChatLayout initialIsPinned={false} onTogglePinned={handleTogglePinned}> |
| 97 | + <ChatSideNav>{/* Side nav subcomponents */}</ChatSideNav> |
| 98 | + <ChatMain>{/* Main chat content */}</ChatMain> |
| 99 | + </ChatLayout> |
| 100 | + ); |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +## Properties |
| 105 | + |
| 106 | +### ChatLayout |
| 107 | + |
| 108 | +| Prop | Type | Description | Default | |
| 109 | +| ------------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------- | ------- | |
| 110 | +| `children` | `ReactNode` | The content to render inside the grid layout (`ChatSideNav` and `ChatMain` components) | - | |
| 111 | +| `className` _(optional)_ | `string` | Custom CSS class to apply to the grid container | - | |
| 112 | +| `initialIsPinned` _(optional)_ | `boolean` | Initial state for whether the side nav is pinned (expanded) | `true` | |
| 113 | +| `onTogglePinned` _(optional)_ | `(isPinned: boolean) => void` | Callback fired when the side nav is toggled. Receives the new `isPinned` state as an argument | - | |
| 114 | + |
| 115 | +All other props are passed through to the underlying `<div>` element. |
| 116 | + |
| 117 | +### ChatMain |
| 118 | + |
| 119 | +| Prop | Type | Description | Default | |
| 120 | +| ---------- | ----------- | -------------------------- | ------- | |
| 121 | +| `children` | `ReactNode` | The main content to render | - | |
| 122 | + |
| 123 | +All other props are passed through to the underlying `<div>` element. |
| 124 | + |
| 125 | +**Note:** `ChatMain` must be used as a direct child of `ChatLayout` to work correctly within the grid system. |
| 126 | + |
| 127 | +### ChatSideNav |
| 128 | + |
| 129 | +| Prop | Type | Description | Default | |
| 130 | +| ------------------------ | ------------------------- | -------------------------------------------------------------- | ------- | |
| 131 | +| `children` | `ReactNode` | Should include `ChatSideNav.Header` and `ChatSideNav.Content`. | - | |
| 132 | +| `className` _(optional)_ | `string` | Root class name | - | |
| 133 | +| `...` | `HTMLElementProps<'nav'>` | Props spread on the root `<nav>` element | - | |
| 134 | + |
| 135 | +### ChatSideNav.Header |
| 136 | + |
| 137 | +| Prop | Type | Description | Default | |
| 138 | +| ----------------------------- | -------------------------------------- | ------------------------------------------- | ------- | |
| 139 | +| `onClickNewChat` _(optional)_ | `MouseEventHandler<HTMLButtonElement>` | Fired when the "New Chat" button is clicked | - | |
| 140 | +| `className` _(optional)_ | `string` | Header class name | - | |
| 141 | +| `...` | `HTMLElementProps<'div'>` | Props spread on the header container | - | |
| 142 | + |
| 143 | +### ChatSideNav.Content |
| 144 | + |
| 145 | +| Prop | Type | Description | Default | |
| 146 | +| ------------------------ | ------------------------- | ------------------------------------- | ------- | |
| 147 | +| `className` _(optional)_ | `string` | Content class name | - | |
| 148 | +| `...` | `HTMLElementProps<'div'>` | Props spread on the content container | - | |
| 149 | + |
| 150 | +### ChatSideNav.SideNavItem |
| 151 | + |
| 152 | +| Prop | Type | Description | Default | |
| 153 | +| ------------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | |
| 154 | +| `active` _(optional)_ | `boolean` | Whether or not the component should be rendered in an active state. When active, applies active styling and sets `aria-current="page"` | `false` | |
| 155 | +| `as` _(optional)_ | `React.ElementType` | When provided, the component will be rendered as the component or html tag indicated by this prop. Other additional props will be spread on the element. For example, `Link` or `a` tags can be supplied. Defaults to `'a'` | - | |
| 156 | +| `children` | `ReactNode` | Content that will be rendered inside the root-level element (typically the chat name) | - | |
| 157 | +| `className` _(optional)_ | `string` | Class name that will be applied to the root-level element | - | |
| 158 | +| `href` _(optional)_ | `string` | The URL that the hyperlink points to. When provided, the component will be rendered as an anchor element | - | |
| 159 | +| `onClick` _(optional)_ | `MouseEventHandler` | The event handler function for the 'onclick' event. Receives the associated `event` object as the first argument | - | |
| 160 | + |
| 161 | +## Context API |
| 162 | + |
| 163 | +### useChatLayoutContext |
| 164 | + |
| 165 | +Hook that returns the current chat layout context: |
| 166 | + |
| 167 | +```tsx |
| 168 | +const { |
| 169 | + isPinned, |
| 170 | + togglePin, |
| 171 | + isSideNavHovered, |
| 172 | + setIsSideNavHovered, |
| 173 | + shouldRenderExpanded, |
| 174 | +} = useChatLayoutContext(); |
| 175 | +``` |
| 176 | + |
| 177 | +**Returns:** |
| 178 | + |
| 179 | +| Property | Type | Description | |
| 180 | +| ---------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------- | |
| 181 | +| `isPinned` | `boolean` | Whether the side nav is currently pinned | |
| 182 | +| `togglePin` | `() => void` | Function to toggle the pinned state | |
| 183 | +| `isSideNavHovered` | `boolean` | Whether the side nav is currently being hovered | |
| 184 | +| `setIsSideNavHovered` | `(isHovered: boolean) => void` | Function to set the hover state of the side nav | |
| 185 | +| `shouldRenderExpanded` | `boolean` | Whether the side nav should render in expanded state. This is `true` when the nav is pinned OR hovered. | |
| 186 | + |
| 187 | +## Behavior |
| 188 | + |
| 189 | +### State Management |
| 190 | + |
| 191 | +- `ChatLayout` manages the `isPinned` and `isSideNavHovered` state internally and provides it to all descendants via `ChatLayoutContext` |
| 192 | +- `shouldRenderExpanded` is computed as `isPinned || isSideNavHovered` and provided in the context for convenience |
| 193 | +- When `togglePin` is called: |
| 194 | + 1. The `isPinned` state updates |
| 195 | + 2. Grid columns resize smoothly via CSS transition |
| 196 | + 3. The `onTogglePinned` callback fires (if provided) with the new state value |
| 197 | +- Descendant components can consume the context to: |
| 198 | + - Read the current `isPinned` state |
| 199 | + - Call `togglePin()` to toggle the sidebar |
| 200 | + - Read the current `isSideNavHovered` state |
| 201 | + - Call `setIsSideNavHovered()` to update the hover state |
| 202 | + - Use `shouldRenderExpanded` to determine if the side nav should render in expanded state |
0 commit comments