chat.svelte Copy
< script lang = "ts" > import type { Snippet } from 'svelte' ; import type { HTMLAttributes } from 'svelte/elements' ; import { cn } from '$lib/utils.js' ; let { children, class : className, ... restProps } : HTMLAttributes < HTMLDivElement > & { children : Snippet } = $ props (); </ script > < div data-slot = "chat" class ={ cn ( 'flex min-h-0 flex-col overflow-hidden rounded-xl border border-border bg-background' , className )} { ... restProps } > {@ render children ()} </ div > chat-messages.svelte Copy
< script lang = "ts" > import type { Snippet } from 'svelte' ; import type { HTMLAttributes } from 'svelte/elements' ; import { cn } from '$lib/utils.js' ; let { children, class : className, role = 'log' , 'aria-live' : ariaLive = 'polite' , 'aria-relevant' : ariaRelevant = 'additions text' , ... restProps } : HTMLAttributes < HTMLDivElement > & { children : Snippet } = $ props (); </ script > < div data-slot = "chat-messages" { role } aria-live ={ariaLive} aria-relevant ={ariaRelevant} class ={ cn ( 'flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4' , className)} { ... restProps } > {@ render children ()} </ div > chat-message.svelte Copy
< script lang = "ts" > import type { Snippet } from 'svelte' ; import type { HTMLAttributes } from 'svelte/elements' ; import { cn } from '$lib/utils.js' ; export type ChatSide = 'left' | 'right' ; let { children, side = 'right' , class : className, ... restProps } : HTMLAttributes < HTMLDivElement > & { children : Snippet ; side ?: ChatSide } = $ props (); </ script > < div data-slot = "chat-message" data-side ={side} class ={ cn ( 'group/message flex w-full items-end gap-2' , side === 'left' ? 'justify-start' : 'justify-end' , className )} { ... restProps } > {@ render children ()} </ div > chat-content.svelte Copy
< script lang = "ts" > import type { Snippet } from 'svelte' ; import type { HTMLAttributes } from 'svelte/elements' ; import { cn } from '$lib/utils.js' ; let { children, class : className, ... restProps } : HTMLAttributes < HTMLDivElement > & { children : Snippet } = $ props (); </ script > < div data-slot = "chat-content" class ={ cn ( 'max-w-[80%] rounded-2xl bg-muted px-4 py-2.5 text-sm leading-relaxed wrap-break-word text-foreground shadow-xs' , 'group-data-[side=left]/message:rounded-bl-sm' , 'group-data-[side=right]/message:rounded-br-sm group-data-[side=right]/message:bg-primary group-data-[side=right]/message:text-primary-foreground' , className )} { ... restProps } > {@ render children ()} </ div > chat-composer.svelte Copy
< script lang = "ts" > import type { HTMLButtonAttributes, HTMLFormAttributes, HTMLTextareaAttributes } from 'svelte/elements' ; import { cn } from '$lib/utils.js' ; type ComposerProps = Omit < HTMLFormAttributes , 'onsubmit' > & { value ?: string ; onSend ?: ( message : string ) => void | Promise < void >; placeholder ?: string ; label ?: string ; sendLabel ?: string ; disabled ?: boolean ; clearOnSend ?: boolean ; submitOnEnter ?: boolean ; rows ?: number ; class ?: string ; textareaClass ?: string ; buttonClass ?: string ; textareaProps ?: Omit < HTMLTextareaAttributes , 'value' | 'class' | 'disabled' | 'placeholder' | 'rows' | 'onkeydown' >; buttonProps ?: Omit < HTMLButtonAttributes , 'type' | 'class' | 'disabled' >; }; let { value = $ bindable ( '' ), onSend, placeholder = 'Type a message…' , label = 'Message' , sendLabel = 'Send' , disabled = false , clearOnSend = true , submitOnEnter = true , rows = 1 , class : className, textareaClass, buttonClass, textareaProps, buttonProps, ... restProps } : ComposerProps = $ props (); const inputId = $props. id (); let submitting = $ state ( false ); const cannotSend = $ derived (disabled || submitting || value. trim (). length === 0 ); async function send ( event : SubmitEvent ) { event. preventDefault (); const message = value. trim (); if ( ! message || disabled || submitting) return ; submitting = true ; try { await onSend ?.(message); if (clearOnSend) value = '' ; } finally { submitting = false ; } } function handleKeydown ( event : KeyboardEvent ) { if ( ! submitOnEnter || event.key !== 'Enter' || event.shiftKey || event.isComposing || disabled || submitting ) { return ; } event. preventDefault (); (event.currentTarget as HTMLTextAreaElement ).form?. requestSubmit (); } </ script > < form { ... restProps } data-slot = "chat-composer" class ={ cn ( 'flex items-end gap-2 border-t border-border bg-background p-3' , className)} onsubmit ={send} > < label class = "sr-only" for ={inputId}>{label}</ label > < textarea { ... textareaProps } id ={inputId} data-slot = "chat-input" bind : value { rows } { placeholder } disabled ={disabled || submitting} onkeydown ={handleKeydown} class ={ cn ( 'max-h-40 min-h-10 flex-1 resize-none rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground shadow-xs outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50' , textareaClass )}></ textarea > < button { ... buttonProps } type = "submit" data-slot = "chat-send" disabled ={cannotSend} class ={ cn ( 'inline-flex h-10 shrink-0 items-center justify-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground shadow-xs transition-colors hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50' , buttonClass )} > {submitting ? 'Sending…' : sendLabel} </ button > </ form > index.ts Copy
import Root from './chat.svelte' ; import Messages from './chat-messages.svelte' ; import Message from './chat-message.svelte' ; import Content from './chat-content.svelte' ; import Composer from './chat-composer.svelte' ; export { Root, Messages, Message, Content, Composer }; export { Root as Chat, Messages as ChatMessages, Message as ChatMessage, Content as ChatContent, Composer as ChatComposer }; export type { ChatSide } from './chat-message.svelte' ;