# Chat Message List

## Usage

Presentational message container with density context and infinite scroll support. Provides role="log" with aria-live="polite" for accessibility. A flex spacer pushes messages to the bottom when the list isn't full; set `align="top"` to start them at the top instead.

Rows go in the default slot: usually `ChatMessage` and `ChatSystemMessage`. `density` flows down to the rows that do not set their own. `gap` overrides the row gap alone, without touching padding, which is what LLM event streams with uneven spacing need.

`scrollToTopAction` runs when a sentinel at the top of the list enters the scroll parent, and a spinner stays up until the promise settles. Pair it with an ancestor that scrolls; inside `ChatLayout` the layout owns that scroll.

`isStreaming` marks the log `aria-busy`, so a screen reader waits and announces the finished message once instead of re-reading partial text on every token.

`align` only changes where a short list rests. `bottom`, the default, keeps the spacer so a short conversation sits just above the composer; `top` drops it for document-style transcripts. Once rows overflow the container the spacer collapses either way, so scrolling and the layout's auto-scroll-to-bottom behave the same in both modes.

```vue
<script setup lang="ts">
import { ChatMessage, ChatMessageBubble, ChatMessageList } from '@astryx-vue/core/Chat'
</script>

<template>
  <ChatMessageList :is-streaming="true">
    <ChatMessage sender="user">
      <ChatMessageBubble>How should I structure a monorepo?</ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <ChatMessageBubble variant="ghost">
        Use workspaces with a shared packages directory.
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

## Examples

Common configurations, variations, and states.

### Density

Side-by-side comparison of compact, balanced, and spacious densities. Use compact in sidebars or panels, balanced for most full-page chat, and spacious for long-form reading. Use gap when row spacing needs to differ from density.

```vue
<script setup lang="ts">
import type { AvatarSize } from '@astryx-vue/core/Avatar'
import type { ChatDensity } from '@astryx-vue/core/Chat'
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList } from '@astryx-vue/core/Chat'
import { Divider } from '@astryx-vue/core/Divider'

const agent = 'Agent'

const densities: { id: ChatDensity, label: string, avatar: AvatarSize }[] = [
  { id: 'compact', label: 'Compact', avatar: 'sm' },
  { id: 'balanced', label: 'Balanced', avatar: 'md' },
  { id: 'spacious', label: 'Spacious', avatar: 'md' },
]
</script>

<template>
  <div class="chat-preview-densities">
    <template v-for="(density, index) in densities" :key="density.id">
      <Divider v-if="index > 0" />
      <div>
        <p class="chat-preview__caption">
          {{ density.label }}
        </p>
        <ChatMessageList :density="density.id">
          <ChatMessage sender="user">
            <ChatMessageBubble>
              {{ 'How does density work?' }}
            </ChatMessageBubble>
          </ChatMessage>
          <ChatMessage sender="assistant">
            <template #avatar>
              <Avatar :name="agent" :size="density.avatar" />
            </template>
            <ChatMessageBubble>
              {{ 'Density provides default spacing at every level — message gap, bubble padding, and gap between child elements. Use gap to tune row spacing independently.' }}
            </ChatMessageBubble>
          </ChatMessage>
        </ChatMessageList>
      </div>
    </template>
  </div>
</template>
```

### Full Featured

Conversation showcasing system messages, multi-bubble grouping, markdown, code blocks, and metadata. Combines date dividers, ghost bubbles, grouped messages, and rich content in a single example.

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata, ChatSystemMessage } from '@astryx-vue/core/Chat'
import { CodeBlock } from '@astryx-vue/core/Code'

const agent = 'Agent'
const bubbleWidth = 'min(100%, 21.25rem)'

const snippet = `const [state, dispatch] = useReducer(
  (state, action) => ({
    ...state,
    [action.field]: action.value,
  }),
  { name: '', email: '' }
);`
</script>

<template>
  <ChatMessageList>
    <ChatSystemMessage variant="divider">
      Today
    </ChatSystemMessage>
    <ChatMessage sender="user">
      <div class="chat-preview__files">
        <span class="chat-preview__file">useReducer.ts</span>
        <span class="chat-preview__file">formState.ts</span>
      </div>
      <ChatMessageBubble>
        Can you review these files?
        <template #metadata>
          <ChatMessageMetadata status="read" timestamp="2:30 PM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatMessage sender="assistant">
      <template #avatar>
        <Avatar :name="agent" size="md" />
      </template>
      <ChatMessageBubble group="first" :width="bubbleWidth">
        Sure! Here's the key pattern from <strong>useReducer.ts</strong>:
      </ChatMessageBubble>
      <ChatMessageBubble group="last" :width="bubbleWidth">
        The reducer is <strong>pure and easy to test</strong> — pass in state and action, assert on the output.
      </ChatMessageBubble>
      <ChatMessageBubble variant="ghost" :width="bubbleWidth">
        <CodeBlock :code="snippet" language="tsx" width="100%" />
        <template #metadata>
          <ChatMessageMetadata timestamp="2:30 PM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
    <ChatSystemMessage>
      Agent shared a code snippet
    </ChatSystemMessage>
    <ChatMessage sender="user">
      <ChatMessageBubble>
        That's clean, thanks!
        <template #metadata>
          <ChatMessageMetadata status="delivered" timestamp="2:31 PM" />
        </template>
      </ChatMessageBubble>
    </ChatMessage>
  </ChatMessageList>
</template>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `'top' \| 'bottom'` | `'bottom'` | Vertical alignment when the list is shorter than its container. `'bottom'` fills the free space with a spacer, so a short conversation sits just above the composer; `'top'` leaves the spacer out, so messages start at the top. A list that already overflows scrolls the same way in both modes. |
| `density` | `ChatDensity` | `'balanced'` | Visual density: `'compact'`, `'balanced'`, or `'spacious'`. Flows to child messages through context. |
| `gap` | `SpacingStep` | density value | Gap between top-level rows, on the spacing scale. Overrides the density gap without changing padding. |
| `scrollToTopAction` | `() => Promise<void>` | — | Runs when a sentinel at the top of the list enters the scroll parent. Use it to load older messages. A spinner stays at the top until the promise settles. |
| `isStreaming` | `boolean` | `false` | Marks the log `aria-busy` while an assistant message is still growing, so a screen reader waits and announces the finished message once. |
| `label` | `string` | localized “Messages” | Accessible name of the log. |

## Slots

- `default`: message rows, usually `ChatMessage` and `ChatSystemMessage`.
- `emptyState`: shown when the default slot is omitted.
- `empty`: alias of `emptyState`.

## Exposed

- `element`: the root `<div role="log">` element.
