Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/chat-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@lg-chat/chat-layout': minor
---

Initial release with `ChatLayout`, `ChatMain`, and `ChatSideNav`
- `ChatSideNav` is a compound component with the following subcomponents:
- `ChatSideNav.Header`
- `ChatSideNav.Content`
- `ChatSideNav.SideNavItem`
5 changes: 5 additions & 0 deletions .changeset/gold-goats-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lg-chat/input-bar': patch
---

Remove redundant `z-index: 2;` in `InputBar` content wrapping node.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ import Button from '@leafygreen-ui/button';
| [@lg-charts/legend](./charts/legend) | [![version](https://img.shields.io/npm/v/@lg-charts/legend)](https://www.npmjs.com/package/@lg-charts/legend) | ![downloads](https://img.shields.io/npm/dm/@lg-charts/legend?color=white) | [Live Example](http://mongodb.design/component/legend/live-example) |
| [@lg-charts/series-provider](./charts/series-provider) | [![version](https://img.shields.io/npm/v/@lg-charts/series-provider)](https://www.npmjs.com/package/@lg-charts/series-provider) | ![downloads](https://img.shields.io/npm/dm/@lg-charts/series-provider?color=white) | [Live Example](http://mongodb.design/component/series-provider/live-example) |
| [@lg-chat/avatar](./chat/avatar) | [![version](https://img.shields.io/npm/v/@lg-chat/avatar)](https://www.npmjs.com/package/@lg-chat/avatar) | ![downloads](https://img.shields.io/npm/dm/@lg-chat/avatar?color=white) | [Live Example](http://mongodb.design/component/avatar/live-example) |
| [@lg-chat/chat-layout](./chat/chat-layout) | [![version](https://img.shields.io/npm/v/@lg-chat/chat-layout)](https://www.npmjs.com/package/@lg-chat/chat-layout) | ![downloads](https://img.shields.io/npm/dm/@lg-chat/chat-layout?color=white) | [Live Example](http://mongodb.design/component/chat-layout/live-example) |
| [@lg-chat/chat-window](./chat/chat-window) | [![version](https://img.shields.io/npm/v/@lg-chat/chat-window)](https://www.npmjs.com/package/@lg-chat/chat-window) | ![downloads](https://img.shields.io/npm/dm/@lg-chat/chat-window?color=white) | [Live Example](http://mongodb.design/component/chat-window/live-example) |
| [@lg-chat/fixed-chat-window](./chat/fixed-chat-window) | [![version](https://img.shields.io/npm/v/@lg-chat/fixed-chat-window)](https://www.npmjs.com/package/@lg-chat/fixed-chat-window) | ![downloads](https://img.shields.io/npm/dm/@lg-chat/fixed-chat-window?color=white) | [Live Example](http://mongodb.design/component/fixed-chat-window/live-example) |
| [@lg-chat/input-bar](./chat/input-bar) | [![version](https://img.shields.io/npm/v/@lg-chat/input-bar)](https://www.npmjs.com/package/@lg-chat/input-bar) | ![downloads](https://img.shields.io/npm/dm/@lg-chat/input-bar?color=white) | [Live Example](http://mongodb.design/component/input-bar/live-example) |
Expand Down
202 changes: 202 additions & 0 deletions chat/chat-layout/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# Chat Layout

![npm (scoped)](https://img.shields.io/npm/v/@lg-chat/chat-layout.svg)

#### [View on MongoDB.design](https://www.mongodb.design/component/chat-layout/live-example/)

## Installation

### PNPM

```shell
pnpm add @lg-chat/chat-layout
```

### Yarn

```shell
yarn add @lg-chat/chat-layout
```

### NPM

```shell
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 side nav that can be collapsed or pinned.

This package exports:

- `ChatLayout`: The grid container and context provider
- `ChatMain`: The primary content area of the chat interface, automatically positioned within the grid layout.
- `ChatSideNav`: A compound component representing the side navigation, exposing subcomponents such as `ChatSideNav.Header`, `ChatSideNav.Content`, and `ChatSideNav.SideNavItem` for flexible composition.
- `useChatLayoutContext`: Hook for accessing layout state

## Examples

### Basic

```tsx
import { useState } from 'react';
import { ChatLayout, ChatMain, ChatSideNav } from '@lg-chat/chat-layout';

function MyChatApp() {
const [activeChatId, setActiveChatId] = useState('1');

const chatItems = [
{ id: '1', name: 'MongoDB Atlas Setup', href: '/chat/1' },
{ id: '2', name: 'Database Query Help', href: '/chat/2' },
{ id: '3', name: 'Schema Design Discussion', href: '/chat/3' },
];

const handleNewChat = () => {
console.log('Start new chat');
};

return (
<ChatLayout>
<ChatSideNav>
<ChatSideNav.Header onClickNewChat={handleNewChat} />
<ChatSideNav.Content>
{chatItems.map(({ href, id, item, name }) => (
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The destructured property item is not used in the map callback and appears to be a typo or leftover from refactoring. It should be removed from the destructuring.

Suggested change
{chatItems.map(({ href, id, item, name }) => (
{chatItems.map(({ href, id, name }) => (

Copilot uses AI. Check for mistakes.
<ChatSideNav.SideNavItem
key={id}
href={href}
active={id === activeChatId}
onClick={e => {
e.preventDefault();
setActiveChatId(id);
}}
>
{name}
</ChatSideNav.SideNavItem>
))}
</ChatSideNav.Content>
</ChatSideNav>
<ChatMain>{/* Main chat content here */}</ChatMain>
</ChatLayout>
);
}
```

### With Initial State and Toggle Pinned Callback

```tsx
import { ChatLayout, ChatMain, ChatSideNav } from '@lg-chat/chat-layout';

function MyChatApp() {
const handleTogglePinned = (isPinned: boolean) => {
console.log('Side nav is now:', isPinned ? 'pinned' : 'collapsed');
};

return (
<ChatLayout initialIsPinned={false} onTogglePinned={handleTogglePinned}>
<ChatSideNav>{/* Side nav subcomponents */}</ChatSideNav>
<ChatMain>{/* Main chat content */}</ChatMain>
</ChatLayout>
);
}
```

## Properties

### ChatLayout

| Prop | Type | Description | Default |
| ------------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------- | ------- |
| `children` | `ReactNode` | The content to render inside the grid layout (`ChatSideNav` and `ChatMain` components) | - |
| `className` _(optional)_ | `string` | Custom CSS class to apply to the grid container | - |
| `initialIsPinned` _(optional)_ | `boolean` | Initial state for whether the side nav is pinned (expanded) | `true` |
| `onTogglePinned` _(optional)_ | `(isPinned: boolean) => void` | Callback fired when the side nav is toggled. Receives the new `isPinned` state as an argument | - |

All other props are passed through to the underlying `<div>` element.

### ChatMain

| Prop | Type | Description | Default |
| ---------- | ----------- | -------------------------- | ------- |
| `children` | `ReactNode` | The main content to render | - |

All other props are passed through to the underlying `<div>` element.

**Note:** `ChatMain` must be used as a direct child of `ChatLayout` to work correctly within the grid system.

### ChatSideNav

| Prop | Type | Description | Default |
| ------------------------ | ------------------------- | -------------------------------------------------------------- | ------- |
| `children` | `ReactNode` | Should include `ChatSideNav.Header` and `ChatSideNav.Content`. | - |
| `className` _(optional)_ | `string` | Root class name | - |
| `...` | `HTMLElementProps<'nav'>` | Props spread on the root `<nav>` element | - |

### ChatSideNav.Header

| Prop | Type | Description | Default |
| ----------------------------- | -------------------------------------- | ------------------------------------------- | ------- |
| `onClickNewChat` _(optional)_ | `MouseEventHandler<HTMLButtonElement>` | Fired when the "New Chat" button is clicked | - |
| `className` _(optional)_ | `string` | Header class name | - |
| `...` | `HTMLElementProps<'div'>` | Props spread on the header container | - |

### ChatSideNav.Content

| Prop | Type | Description | Default |
| ------------------------ | ------------------------- | ------------------------------------- | ------- |
| `className` _(optional)_ | `string` | Content class name | - |
| `...` | `HTMLElementProps<'div'>` | Props spread on the content container | - |

### ChatSideNav.SideNavItem

| Prop | Type | Description | Default |
| ------------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `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` |
| `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'` | - |
| `children` | `ReactNode` | Content that will be rendered inside the root-level element (typically the chat name) | - |
| `className` _(optional)_ | `string` | Class name that will be applied to the root-level element | - |
| `href` _(optional)_ | `string` | The URL that the hyperlink points to. When provided, the component will be rendered as an anchor element | - |
| `onClick` _(optional)_ | `MouseEventHandler` | The event handler function for the 'onclick' event. Receives the associated `event` object as the first argument | - |

## Context API

### useChatLayoutContext

Hook that returns the current chat layout context:

```tsx
const {
isPinned,
togglePin,
isSideNavHovered,
setIsSideNavHovered,
shouldRenderExpanded,
} = useChatLayoutContext();
```

**Returns:**

| Property | Type | Description |
| ---------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `isPinned` | `boolean` | Whether the side nav is currently pinned |
| `togglePin` | `() => void` | Function to toggle the pinned state |
| `isSideNavHovered` | `boolean` | Whether the side nav is currently being hovered |
| `setIsSideNavHovered` | `(isHovered: boolean) => void` | Function to set the hover state of the side nav |
| `shouldRenderExpanded` | `boolean` | Whether the side nav should render in expanded state. This is `true` when the nav is pinned OR hovered. |

## Behavior

### State Management

- `ChatLayout` manages the `isPinned` and `isSideNavHovered` state internally and provides it to all descendants via `ChatLayoutContext`
- `shouldRenderExpanded` is computed as `isPinned || isSideNavHovered` and provided in the context for convenience
- When `togglePin` is called:
1. The `isPinned` state updates
2. Grid columns resize smoothly via CSS transition
3. The `onTogglePinned` callback fires (if provided) with the new state value
- Descendant components can consume the context to:
- Read the current `isPinned` state
- Call `togglePin()` to toggle the sidebar
- Read the current `isSideNavHovered` state
- Call `setIsSideNavHovered()` to update the hover state
- Use `shouldRenderExpanded` to determine if the side nav should render in expanded state
62 changes: 62 additions & 0 deletions chat/chat-layout/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

{
"name": "@lg-chat/chat-layout",
"version": "0.0.1",
"description": "LeafyGreen UI Kit Chat Layout",
"main": "./dist/umd/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"license": "Apache-2.0",
"exports": {
".": {
"require": "./dist/umd/index.js",
"import": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts"
},
"./testing": {
"require": "./dist/umd/testing/index.js",
"import": "./dist/esm/testing/index.js",
"types": "./dist/types/testing/index.d.ts"
}
},
"scripts": {
"build": "lg-build bundle",
"tsc": "lg-build tsc",
"docs": "lg-build docs"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@leafygreen-ui/avatar": "workspace:^",
"@leafygreen-ui/button": "workspace:^",
"@leafygreen-ui/compound-component": "workspace:^",
"@leafygreen-ui/emotion": "workspace:^",
"@leafygreen-ui/icon": "workspace:^",
"@leafygreen-ui/lib": "workspace:^",
"@leafygreen-ui/palette": "workspace:^",
"@leafygreen-ui/polymorphic": "workspace:^",
"@leafygreen-ui/tokens": "workspace:^",
"@leafygreen-ui/typography": "workspace:^",
"@lg-tools/test-harnesses": "workspace:^"
},
"peerDependencies": {
"@leafygreen-ui/leafygreen-provider": "workspace:^3.2.0 || workspace:^4.0.0 || workspace:^5.0.0",
"@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",
"url": "https://github.com/mongodb/leafygreen-ui"
},
"bugs": {
"url": "https://jira.mongodb.org/projects/LG/summary"
}
}
Loading
Loading