# ChatThread

> Chat UI kit. A bottom-sticking message thread, messages with avatar, time, status and streaming, and an autosizing composer.

Source: https://docs.nasaqui.com/components/chat

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/chat.json
```

Three parts for a conversation. `ChatThread` scrolls and follows new messages. `ChatMessage` is one bubble with an
avatar, a time, a delivery status and an optional streaming state. `ChatComposer` is the message box. Message text can be
plain or [Markdown](https://docs.nasaqui.com/components/markdown), so model replies with lists, tables and code render properly.

## When to use

- Assistant or support chat, comment-style conversations, streaming model output.

## When not to use

- A feed of activity events: use `NotificationItem`.
- A single comment box with no thread: use `Textarea` from [field](https://docs.nasaqui.com/components/field).

## Import

```tsx
import { ChatComposer, ChatMessage, ChatThread } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { ChatComposer, ChatMessage, ChatThread } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Chat() {
  const [messages, setMessages] = useState([{ id: 1, side: "assistant" as const, text: "Hi! How can I help?" }]);
  return (
    <div className="flex h-96 flex-col rounded-surface border border-border">
      <ChatThread className="flex-1">
        {messages.map((m) => (
          <ChatMessage key={m.id} side={m.side} name={m.side === "user" ? "You" : "Assistant"} time={Date.now()}>
            {m.text}
          </ChatMessage>
        ))}
      </ChatThread>
      <div className="p-3 pt-0">
        <ChatComposer onSend={(text) => setMessages((m) => [...m, { id: m.length + 1, side: "user", text }])} />
      </div>
    </div>
  );
}
```

## Anatomy

```
ChatThread        data-slot="chat-thread"        wrapper
├─ scroller       role="log" aria-live="polite"  the scrolling region
│  └─ ChatMessage data-slot="chat-message"       data-side, data-status, data-streaming
│     ├─ Avatar   data-slot="avatar"
│     ├─ byline   name + DateTime (data-slot="date-time")
│     ├─ bubble   data-slot="chat-bubble"        Markdown or text; TypingIndicator while streaming
│     └─ status   data-slot="chat-status"        spinner / check / error + retry
└─ jump button    data-slot="chat-jump"          only when scrolled away from the bottom
ChatComposer      data-slot="chat-composer"
├─ attachments    data-slot="chat-attachments"   your slot
└─ Textarea (data-slot="textarea"), actions slot, send or stop Button
```

## API

### `ChatThread`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `string` | "Conversation" / "المحادثة" | Accessible name of the log. |
| `jumpLabel?` | `string` | "Jump to latest" / "الانتقال إلى الأحدث" | Name of the jump button. |
| `contentClassName?` | `string` | none | Classes for the inner message column (`p-4 gap-4` by default). |
| `className?` | `string` | none | On the wrapper. Give it a height (`flex-1`, `h-96`). |

It sticks to the bottom while the reader is within 48px of it, also as a message grows while streaming. Scroll up and it
stops following; the jump button returns you and resumes.

### `ChatMessage`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side?` | `"user" \| "assistant"` | `"assistant"` | `user` sits on the inline-end edge, `assistant` on inline-start. Mirrors in RTL. |
| `name?` | `string` | none | Byline and avatar initials. |
| `avatarSrc?` | `string` | none | Avatar image. |
| `avatar?` | `ReactNode` | none | Replaces the Avatar. |
| `time?` | `Date \| number \| string` | none | Shown as a localised time via `DateTime`. |
| `status?` | `"sending" \| "sent" \| "error"` | none | Delivery state under the bubble. |
| `onRetry?` | `() => void` | none | Shows a Retry button on `error`. |
| `format?` | `"text" \| "markdown"` | `"text"` | How a string child is rendered. |
| `streaming?` | `boolean` | `false` | Shows the typing indicator and sets `aria-busy`. |
| `children?` | `ReactNode` | none | Body. Empty plus `streaming` shows only the dots. |

### `ChatComposer`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value?` / `defaultValue?` | `string` | `""` | Controlled or uncontrolled text. |
| `onValueChange?` | `(value: string) => void` | none | Fires on every change and after a send (with `""`). |
| `onSend?` | `(text: string) => void` | none | Trimmed text, on Enter or the send button. |
| `onStop?` | `() => void` | none | With `streaming`, the send button becomes a stop button. |
| `streaming?` | `boolean` | `false` | Blocks sending. |
| `disabled?` | `boolean` | `false` | Disables the field and buttons. |
| `placeholder?` | `string` | "Write a message…" / "اكتب رسالة…" | |
| `label?` / `sendLabel?` / `stopLabel?` | `string` | "Message", "Send", "Stop" (Arabic built in) | Accessible names. |
| `maxRows?` | `number` | `8` | The field grows to this many lines, then scrolls. |
| `attachments?` | `ReactNode` | none | Slot above the field. |
| `actions?` | `ReactNode` | none | Slot before the send button. |

### `TypingIndicator`

`{ label?: string } & ComponentProps<"span">`. Three pulsing dots, `role="status"`, still under reduced motion.

## Examples

### Streaming with a stop button

```tsx
import { ChatComposer, ChatMessage, ChatThread } from "@fadymondy/nasaq/web";

export function Streaming({ text, busy, onStop }: { text: string; busy: boolean; onStop: () => void }) {
  return (
    <div className="flex h-96 flex-col">
      <ChatThread className="flex-1">
        <ChatMessage name="Assistant" streaming={busy} format="markdown">
          {text}
        </ChatMessage>
      </ChatThread>
      <ChatComposer streaming={busy} onStop={onStop} onSend={() => {}} />
    </div>
  );
}
```

### Arabic with a failed message

```tsx
import { ChatMessage } from "@fadymondy/nasaq/web";

export function Failed({ retry }: { retry: () => void }) {
  return (
    <ChatMessage side="user" name="فادي" status="error" onRetry={retry}>
      مرحبًا، هل الخدمة متاحة؟
    </ChatMessage>
  );
}
```

## Accessibility

| Key | Action |
| --- | --- |
| `Enter` | Sends (ignored while an IME composition is open) |
| `Shift+Enter` | New line |
| `Tab` | Thread, field, buttons; arrow keys scroll the focused thread |

- The thread is `role="log"` with `aria-live="polite"`, so new messages are announced without stealing focus.
- Sending and typing are `role="status"`; a failed message announces with `role="alert"`.
- Status is text plus an icon, never colour alone. Localise `label`, `sendLabel`, `stopLabel` when you pass your own.
- The streaming bubble sets `aria-busy` until `streaming` ends.

## RTL & i18n

- `user` and `assistant` use logical alignment (start and end) and mirror in Arabic. The send icon flips.
- Plain text and the field use `dir="auto"`; Markdown blocks orient themselves; code stays left-to-right.
- Times use the Nasaq digit set through `DateTime`.
- Built-in strings ship in English and Arabic and follow the Nasaq locale.

## Styling & tokens

- User bubble `bg-secondary`; assistant bubble `bg-card` with `border-border`; errors `border-nq-danger`.
- Composer focus ring uses `border-nq-focus` on the wrapper (`focus-within`).
- Target `data-side`, `data-status`, `data-streaming`, `data-disabled`. Extend with `className`; do not use raw hex.

## Do / Don't

- **Do** give the thread a bounded height so it can scroll.
- **Do** keep `onSend` fast: set `status="sending"` on the message, then `sent` or `error`.
- **Don't** put the composer inside the thread's scroller.
- **Don't** render untrusted HTML in bubbles; use `format="markdown"`, which drops raw HTML.

## Related

- [Markdown](https://docs.nasaqui.com/components/markdown) · [CodeBlock](https://docs.nasaqui.com/components/code-block) · [Avatar](https://docs.nasaqui.com/components/avatar) · [Field](https://docs.nasaqui.com/components/field) · [Button](https://docs.nasaqui.com/components/button)

## Lab

https://docs.nasaqui.com/?path=/docs/components-chat-chat--docs

## Code

### React

```tsx
import { ChatComposer, ChatMessage, ChatThread } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Chat() {
  const [messages, setMessages] = useState([{ id: 1, side: "assistant" as const, text: "Hi! How can I help?" }]);
  return (
    <div className="flex h-96 flex-col rounded-surface border border-border">
      <ChatThread className="flex-1">
        {messages.map((m) => (
          <ChatMessage key={m.id} side={m.side} name={m.side === "user" ? "You" : "Assistant"} time={Date.now()}>
            {m.text}
          </ChatMessage>
        ))}
      </ChatThread>
      <div className="p-3 pt-0">
        <ChatComposer onSend={(text) => setMessages((m) => [...m, { id: m.length + 1, side: "user", text }])} />
      </div>
    </div>
  );
}
```

### shadcn

```tsx
import { ChatComposer, ChatMessage, ChatThread } from "@/components/ui/chat";
import { useState } from "react";

export function Chat() {
  const [messages, setMessages] = useState([{ id: 1, side: "assistant" as const, text: "Hi! How can I help?" }]);
  return (
    <div className="flex h-96 flex-col rounded-surface border border-border">
      <ChatThread className="flex-1">
        {messages.map((m) => (
          <ChatMessage key={m.id} side={m.side} name={m.side === "user" ? "You" : "Assistant"} time={Date.now()}>
            {m.text}
          </ChatMessage>
        ))}
      </ChatThread>
      <div className="p-3 pt-0">
        <ChatComposer onSend={(text) => setMessages((m) => [...m, { id: m.length + 1, side: "user", text }])} />
      </div>
    </div>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqChatComposer, NqChatMessage, NqChatThread } from "@fadymondy/nasaq/vue";
import { ref } from "vue";

const messages = ref<{ id: number; side: "user" | "assistant"; text: string }[]>([{ id: 1, side: "assistant", text: "Hi! How can I help?" }]);
const send = (text: string) => messages.value.push({ id: messages.value.length + 1, side: "user", text });
</script>

<template>
  <div class="flex h-96 flex-col rounded-surface border border-border">
    <NqChatThread class="flex-1">
      <NqChatMessage v-for="m in messages" :key="m.id" :side="m.side" :name="m.side === 'user' ? 'You' : 'Assistant'" :time="Date.now()" :text="m.text" />
    </NqChatThread>
    <div class="p-3 pt-0">
      <NqChatComposer @send="send" />
    </div>
  </div>
</template>
```

### Blade

```blade
<div class="flex h-96 w-full max-w-xl flex-col rounded-surface border border-border">
    <x-nq::chat class="flex-1">
        <x-nq::chat.message side="assistant" name="Assistant" :time="now()->subMinutes(2)" text="Hi! How can I help?" />
        <x-nq::chat.message side="user" name="You" :time="now()->subMinute()" status="sent" text="Show me this month's spend." />
        <x-nq::chat.message side="assistant" name="Assistant" format="markdown" streaming text="Spend is **$38** so far" />
    </x-nq::chat>
    <div class="p-3 pt-0">
        <x-nq::chat.composer />
    </div>
</div>

{{-- chat-widget-offline --}}
<div class="relative h-[34rem] w-full">
    <x-nq::chat-widget placement="absolute" open :online="false" :agent="['name' => 'Layla']" :unread="2" :messages="[['id' => 'm1', 'from' => 'agent', 'text' => 'We are back at 9:00.', 'at' => now()->subHour()]]" />
</div>
```

### HTML + Alpine

```html
<div class="flex h-96 w-full max-w-xl flex-col rounded-surface border border-border">
    <div data-slot="chat-thread" x-data="nqChatThread" class="relative flex min-h-0 flex-col flex-1">
    <div x-ref="scroller" x-on:scroll="onScroll()" role="log" aria-live="polite" aria-relevant="additions" aria-label="Conversation" tabindex="0"
        class="min-h-0 flex-1 overflow-y-auto overscroll-contain outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus">
        <div x-ref="content" class="flex flex-col gap-4 p-4"><div data-slot="chat-message" data-side="assistant"          class="flex max-w-[85%] items-start gap-2 self-start">
            <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-6 text-[10px] rounded-full mt-0.5">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="Assistant" >A</span>
    </span>
        <div class="flex min-w-0 flex-col gap-1 items-start">
                    <div class="flex items-center gap-2 text-caption text-muted-foreground">
                <span class="text-label text-foreground">Assistant</span>                <time data-slot="date-time" datetime="2026-09-29T08:58:00+00:00" dir="auto"     class="tabular-nums [unicode-bidi:isolate]">8:58 AM</time>
            </div>
                <div data-slot="chat-bubble"             class="min-w-0 rounded-card px-3 py-2 text-body border border-border bg-card text-nq-fg-body">
                                                <p dir="auto" class="whitespace-pre-wrap text-start [overflow-wrap:anywhere]">Hi! How can I help?</p>
                                                </div>
            </div>
</div>
        <div data-slot="chat-message" data-side="user"  data-status="sent"          class="flex max-w-[85%] items-start gap-2 flex-row-reverse self-end">
            <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-6 text-[10px] rounded-full mt-0.5">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="You" >Y</span>
    </span>
        <div class="flex min-w-0 flex-col gap-1 items-end">
                    <div class="flex items-center gap-2 text-caption text-muted-foreground">
                <span class="text-label text-foreground">You</span>                <time data-slot="date-time" datetime="2026-09-29T08:59:00+00:00" dir="auto"     class="tabular-nums [unicode-bidi:isolate]">8:59 AM</time>
            </div>
                <div data-slot="chat-bubble"             class="min-w-0 rounded-card px-3 py-2 text-body bg-secondary text-secondary-foreground">
                                                <p dir="auto" class="whitespace-pre-wrap text-start [overflow-wrap:anywhere]">Show me this month&#039;s spend.</p>
                                                </div>
                    <div data-slot="chat-status" class="flex items-center gap-1.5 text-caption text-muted-foreground">
                                    <svg aria-hidden="true" class="size-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>                    <span>Sent</span>
                            </div>
            </div>
</div>
        <div data-slot="chat-message" data-side="assistant"   data-streaming         class="flex max-w-[85%] items-start gap-2 self-start">
            <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-6 text-[10px] rounded-full mt-0.5">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="Assistant" >A</span>
    </span>
        <div class="flex min-w-0 flex-col gap-1 items-start">
                    <div class="flex items-center gap-2 text-caption text-muted-foreground">
                <span class="text-label text-foreground">Assistant</span>                            </div>
                <div data-slot="chat-bubble"  aria-busy="true"             class="min-w-0 rounded-card px-3 py-2 text-body border border-border bg-card text-nq-fg-body">
                                                <div data-slot="markdown" class="flex min-w-0 flex-col gap-3 text-body text-nq-fg-body"><p data-slot="text" dir="auto" class="text-body text-nq-fg-body text-start">Spend is <strong class="font-semibold text-foreground">$38</strong> so far</p>
</div>
                                        <span data-slot="typing-indicator" role="status" class="items-center gap-1 py-1 mt-1 block">
            <span aria-hidden="true" style="animation-delay: 0ms" class="size-1.5 rounded-full bg-muted-foreground motion-safe:animate-pulse"></span>
            <span aria-hidden="true" style="animation-delay: 150ms" class="size-1.5 rounded-full bg-muted-foreground motion-safe:animate-pulse"></span>
            <span aria-hidden="true" style="animation-delay: 300ms" class="size-1.5 rounded-full bg-muted-foreground motion-safe:animate-pulse"></span>
        <span class="sr-only">Assistant is typing</span>
</span>
        </div>
            </div>
</div></div>
    </div>
    <button data-slot="chat-jump"
     type="button"                         x-show="away" x-cloak="x-cloak" x-on:click="jump()" aria-label="Jump to latest" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover size-control-sm p-0 absolute end-4 bottom-3 rounded-full shadow-sm">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M12 5v14"/>
  <path d="m19 12-7 7-7-7"/>
</svg></button>
</div>
    <div class="p-3 pt-0">
        <div data-slot="chat-composer" x-data="nqChatComposer('', JSON.parse('{\u0022streaming\u0022:false,\u0022stoppable\u0022:false,\u0022disabled\u0022:false,\u0022maxRows\u0022:8}'))" x-modelable="text"
    x-on:nq-chat-streaming="streaming = $event.detail.streaming"
        class="flex flex-col gap-2 rounded-card border border-input bg-card p-2 transition-colors duration-150 ease-nq focus-within:border-nq-focus data-disabled:opacity-50">
        <div class="flex items-end gap-2">
        <textarea data-slot="textarea"
                x-ref="field" x-model="text" x-on:keydown="key($event)" rows="1" dir="auto" placeholder="Write a message…" aria-label="Message" class="w-full min-w-0 rounded-control border-input text-body text-foreground transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] min-h-0 flex-1 resize-none border-0 bg-transparent px-2 py-1.5 focus-visible:outline-0"></textarea>
                        <button data-slot="button"
     type="button"                         x-show="! stopping()" x-on:click="send()" x-bind:disabled="! canSend()" aria-label="Send" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] size-control p-0">
        <svg aria-hidden="true" class="rtl:-scale-x-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/>
  <path d="m21.854 2.147-10.94 10.939"/>
</svg></button>
    </div>
</div>
    </div>
</div>

<!-- chat-widget-offline -->
<div class="relative h-[34rem] w-full">
    <div data-slot="chat-widget" x-data="nqChatWidget(JSON.parse('{\u0022open\u0022:true,\u0022maxFileMb\u0022:5,\u0022visitor\u0022:0,\u0022labels\u0022:{\u0022launcher\u0022:\u0022Open chat\u0022,\u0022close\u0022:\u0022Close chat\u0022,\u0022sendFailed\u0022:\u0022Could not send. Try again.\u0022,\u0022fileTooBig\u0022:\u0022{name} is larger than {mb} MB\u0022,\u0022removeFile\u0022:\u0022Remove {name}\u0022,\u0022formRequired\u0022:\u0022Required\u0022,\u0022formInvalidEmail\u0022:\u0022Enter a valid email address\u0022}}'))" x-modelable="open"
    x-bind:data-open="open ? '' : null" x-on:keydown="onKey($event)"
        class="z-50 flex flex-col items-end gap-3 absolute bottom-4 end-4">
    <section role="dialog" aria-labelledby="nq-cw-000001-title" x-show="open"         class="flex h-[min(34rem,calc(100dvh-7rem))] w-[min(24rem,calc(100vw-2rem))] flex-col overflow-hidden rounded-floating border border-border bg-card shadow-floating">
        <header class="flex items-center gap-3 bg-primary px-4 py-3 text-primary-foreground">
            <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-full">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="Layla" >L</span>
    </span>
            <div class="flex min-w-0 flex-1 flex-col">
                <h2 id="nq-cw-000001-title" dir="auto" class="truncate text-label">Chat with us</h2>
                <p class="flex items-center gap-1.5 truncate text-caption opacity-90">
                    <span aria-hidden="true" class="size-2 shrink-0 rounded-full bg-nq-line-strong"></span>
                    <span class="sr-only">Offline. </span>
                    <span dir="auto" class="truncate">We are away, leave a message</span>
                </p>
            </div>
            <button data-slot="button"
     type="button"                         x-on:click="close()" aria-label="Close chat" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 size-control-sm p-0 text-primary-foreground hover:bg-primary-foreground/15">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg></button>
        </header>
        <div data-slot="chat-thread" x-data="nqChatThread" x-on:nq-retry.stop="retry($event)" class="relative flex min-h-0 flex-col flex-1">
    <div x-ref="scroller" x-on:scroll="onScroll()" role="log" aria-live="polite" aria-relevant="additions" aria-label="Chat with us" tabindex="0"
        class="min-h-0 flex-1 overflow-y-auto overscroll-contain outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus">
        <div x-ref="content" class="flex flex-col gap-3 p-4"><div data-slot="chat-message" data-side="assistant"          class="flex max-w-[85%] items-start gap-2 self-start">
            <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-6 text-[10px] rounded-full mt-0.5">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="Layla" >L</span>
    </span>
        <div class="flex min-w-0 flex-col gap-1 items-start">
                    <div class="flex items-center gap-2 text-caption text-muted-foreground">
                <span class="text-label text-foreground">Layla</span>                            </div>
                <div data-slot="chat-bubble"             class="min-w-0 rounded-card px-3 py-2 text-body border border-border bg-card text-nq-fg-body">
                                                <p dir="auto" class="whitespace-pre-wrap text-start [overflow-wrap:anywhere]">We are not around right now. Leave your details and we will write back by email.</p>
                                                </div>
            </div>
</div>
                                                                <div data-slot="chat-message" data-side="assistant"          data-id="m1" class="flex max-w-[85%] items-start gap-2 self-start">
            <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-6 text-[10px] rounded-full mt-0.5">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="Layla" >L</span>
    </span>
        <div class="flex min-w-0 flex-col gap-1 items-start">
                    <div class="flex items-center gap-2 text-caption text-muted-foreground">
                <span class="text-label text-foreground">Layla</span>                <time data-slot="date-time" datetime="2026-09-29T08:00:00+00:00" dir="auto"     class="tabular-nums [unicode-bidi:isolate]">8:00 AM</time>
            </div>
                <div data-slot="chat-bubble"             class="min-w-0 rounded-card px-3 py-2 text-body border border-border bg-card text-nq-fg-body">
                                                <p dir="auto" class="whitespace-pre-wrap text-start [overflow-wrap:anywhere]">We are back at 9:00.</p>
                                                </div>
            </div>
</div>
            <template x-for="m in outbox" x-bind:key="m.id">
                <div data-slot="chat-message" data-side="user" x-bind:data-status="m.status" class="flex max-w-[85%] items-start gap-2 flex-row-reverse self-end">
                    <div class="flex min-w-0 flex-col gap-1 items-end">
                        <div data-slot="chat-bubble" class="min-w-0 rounded-card px-3 py-2 text-body bg-secondary text-secondary-foreground">
                            <p x-show="m.text" x-text="m.text" dir="auto" class="whitespace-pre-wrap text-start [overflow-wrap:anywhere]"></p>
                            <ul x-show="m.files.length" class="mt-2 flex flex-col gap-1.5">
                                <template x-for="name in m.files">
                                    <li><span dir="auto" class="inline-flex items-center gap-1 text-body-sm"><svg aria-hidden="true" class="size-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"/>
</svg><span x-text="name"></span></span></li>
                                </template>
                            </ul>
                        </div>
                        <div data-slot="chat-status" class="flex items-center gap-1.5 text-caption text-muted-foreground">
                            <span x-show="m.status === 'sending'" role="status">Sending…</span>
                            <span x-show="m.status === 'sent'" class="flex items-center gap-1.5"><svg aria-hidden="true" class="size-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg><span>Sent</span></span>
                        </div>
                    </div>
                </div>
            </template></div>
    </div>
    <button data-slot="chat-jump"
     type="button"                         x-show="away" x-cloak="x-cloak" x-on:click="jump()" aria-label="Jump to latest" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover size-control-sm p-0 absolute end-4 bottom-3 rounded-full shadow-sm">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M12 5v14"/>
  <path d="m19 12-7 7-7-7"/>
</svg></button>
</div>
        <p role="alert" x-show="error" x-text="error" style="display: none" class="px-4 pb-2 text-caption text-nq-danger-text"></p>
                    <div data-slot="chat-widget-sent" role="status" x-show="sent" style="display: none" class="flex flex-col items-center gap-2 border-t border-border p-6 text-center">
                <svg aria-hidden="true" class="size-8 text-nq-success-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m9 12 2 2 4-4"/>
</svg>                <p class="text-label text-foreground">Thanks, we got it</p>
                <p class="text-body-sm text-muted-foreground">We will reply to your email as soon as we are back.</p>
                <button data-slot="button"
     type="button"                         x-on:click="again()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground underline decoration-nq-line underline-offset-4 hover:decoration-current h-auto px-0">
        Send another</button>
            </div>
            <form data-slot="chat-widget-offline" novalidate x-show="! sent" x-on:submit.prevent="submitOffline()" class="flex flex-col gap-3 border-t border-border p-4">
                <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="nameBad" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Your name</label>
                    <input data-slot="input" type="text"
                    x-model="form.name" x-on:input="touch()" dir="auto" autocomplete="name" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control">
                    <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  class="text-caption text-nq-danger-text">Required</div>
</div>
                <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="emailBad" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Email</label>
                    <input data-slot="input" type="email"
     dir="ltr"                 x-model="form.email" x-on:input="touch()" autocomplete="email" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control text-start">
                    <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  class="text-caption text-nq-danger-text"><span x-text="emailError()">Required</span></div>
</div>
                <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="messageBad" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">How can we help?</label>
                    <textarea data-slot="textarea"
                x-model="form.message" x-on:input="touch()" dir="auto" rows="3" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] min-h-20 py-2"></textarea>
                    <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  class="text-caption text-nq-danger-text">Required</div>
</div>
                <p role="alert" x-show="offlineError" x-text="offlineError" style="display: none" class="text-caption text-nq-danger-text"></p>
                <button data-slot="button"
     type="submit"                         x-bind:disabled="busy" x-bind:aria-busy="busy ? &#039;true&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        Send message</button>
            </form>
            </section>
    <button data-slot="button"
     type="button"                         x-ref="launcher" x-on:click="toggle()" x-bind:aria-label="launcherLabel()" x-bind:aria-expanded="open ? &#039;true&#039; : &#039;false&#039;" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] p-0 relative size-14 rounded-full shadow-floating">
        <svg x-show="open" style="" aria-hidden="true" class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg>        <svg x-show="! open" style="display: none" aria-hidden="true" class="size-6 rtl:-scale-x-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"/>
</svg>                    <span x-show="! open"  style="display: none"  class="absolute -end-1 -top-1 grid h-5 min-w-5 place-items-center rounded-full bg-nq-danger px-1 text-caption tabular-nums text-primary-foreground">
                <span aria-hidden="true">2</span>
                <span class="sr-only">2 unread</span>
            </span></button>
</div>
</div>
```
