- Add product vision/market research doc (VISION.md) - Set up shadcn/ui (radix + nova preset) with blue/orange theme, Outfit + Work Sans fonts - Landing page: hero with rank input, problem/steps sections, comparison table, FAQ - Data pipeline: CSV archive ingest (2021-2024) + live YÖK Atlas API refresh (2025) into SQLite (npm run ingest / refresh); zeros normalized to NULL - /sonuc page: hayal/dengeli/garanti buckets by COALESCE(sira2025, sira2024), score-type switcher, 2024→2025 trend indicators - Add ui-ux-pro-max and shadcn agent skills, shadcn MCP config, launch.json Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.8 KiB
Chat & Messaging
Components for conversation and chat UI. Compose these instead of hand-rolling bubbles, scroll containers, dividers, or attachment cards.
Install: npx shadcn@latest add message-scroller message bubble attachment marker
The same component names and props ship for both base and radix; only
composition differs (render vs asChild). See base-vs-radix.md.
Contents
- Scrollable threads use MessageScroller
- Message rows use Message
- Message surfaces use Bubble
- Attachments use Attachment
- System notes and dividers use Marker
- Streaming, anchoring, and jump-to-latest are built in
- Escape hatch: the scroller hooks
Scrollable threads use MessageScroller
A conversation that scrolls, follows new messages, restores position, or jumps
to a message uses MessageScroller. Don't build a raw overflow container with
manual scroll wiring, and don't reach for ScrollArea.
The parts nest in a fixed order. Every direct child of the content is wrapped in
a MessageScrollerItem so the scroller can measure, anchor, preserve position,
track visibility, and jump to it. MessageScrollerButton sits inside
MessageScroller, after the viewport.
Incorrect:
// Hand-rolled scroll container with manual stick-to-bottom logic.
<div ref={scrollRef} onScroll={handleScroll} className="flex-1 overflow-y-auto">
<div className="flex flex-col gap-6 p-4">
{messages.map((m) => (
<ChatMessage key={m.id} message={m} />
))}
</div>
</div>
Correct:
<MessageScrollerProvider autoScroll>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent>
{messages.map((message) => (
<MessageScrollerItem
key={message.id}
messageId={message.id}
scrollAnchor={message.role === "user"}
>
<Message align={message.role === "user" ? "end" : "start"}>
{/* ...message content... */}
</Message>
</MessageScrollerItem>
))}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
Message rows use Message
Message lays out a single row: avatar, header, content, footer, with
alignment. Group consecutive rows from one sender with MessageGroup. Don't
rebuild the row from flex divs.
align="end" is the current user's side; align="start" is everyone else.
<Message align="start">
<MessageAvatar>
<Avatar>
<AvatarImage src={sender.avatar} alt={sender.name} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
</MessageAvatar>
<MessageContent>
<MessageHeader>{sender.name}</MessageHeader>
<Bubble>
<BubbleContent>{text}</BubbleContent>
</Bubble>
<MessageFooter>{time}</MessageFooter>
</MessageContent>
</Message>
Message surfaces use Bubble
The colored message surface is Bubble + BubbleContent, never a styled div
with bg-muted / bg-primary and hand-managed corners.
variant:default,secondary,muted,tinted,outline,ghost,destructive.align:startorend(matches theMessageside).
BubbleReactions renders the reaction cluster. side (top | bottom) and
align (start | end) position it against the bubble. Don't lay reactions out
with absolutely-positioned Badges.
Incorrect:
<div className="w-fit rounded-2xl bg-primary px-3 py-2 text-primary-foreground">
{text}
</div>
Correct:
<Bubble variant="default" align="end">
<BubbleContent>{text}</BubbleContent>
<BubbleReactions side="bottom" align="end">
<Badge variant="secondary">👍 2</Badge>
</BubbleReactions>
</Bubble>
Attachments use Attachment
File and image attachments use Attachment, not Item or a custom card. It
carries upload state, so wire state to the real status rather than rendering a
separate spinner.
state:idle,uploading,processing,error,done.uploadingandprocessingapply theshimmeranimation to the title automatically.size:default,sm,xs.orientation:horizontal,vertical.- Use
AttachmentGroupto lay out several attachments in a scrolling row.
<Attachment state="done">
<AttachmentMedia variant="icon">
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>homepage-feedback.pdf</AttachmentTitle>
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction>
<DownloadIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
For an image, use <AttachmentMedia variant="image"> with an img child.
System notes and dividers use Marker
Status lines ("Sarah joined the conversation"), date dividers ("Today"), and
labeled separators are Marker, not a Separator plus a centered span.
variant:default(plain row),separator(centered label with rules on each side),border(bottom-bordered row).MarkerIconholds a leading icon;MarkerContentholds the label.
Incorrect:
<div className="flex items-center gap-3 py-2">
<Separator className="flex-1" />
<span className="text-xs text-muted-foreground">Today</span>
<Separator className="flex-1" />
</div>
Correct:
<Marker variant="separator">
<MarkerContent>Today</MarkerContent>
</Marker>
Streaming, anchoring, and jump-to-latest are built in
MessageScroller handles the behavior that chat UIs usually reinvent. Don't
write a useStickToBottom hook, a ResizeObserver, or manual scrollTop math.
- Follow the live edge while streaming.
MessageScrollerProviderwithautoScrollkeeps the view pinned to new content and yields the moment the user scrolls up. Streaming token updates that grow the last message are followed automatically. - Anchor a turn.
scrollAnchoron aMessageScrollerItemmarks the row to hold in view (typically the user's message that started the turn). - Jump to latest.
MessageScrollerButtonappears when the user scrolls away and scrolls back on click.direction="end"(default) ordirection="start". It is a self-managing control, so don't gate it behind your own scroll-position state.
For a "thinking…" indicator while the model generates, apply the shimmer
utility to text. Don't author a custom keyframe animation. See
styling.md.
Escape hatch: the scroller hooks
For behavior the parts don't expose, read state from the hooks rather than
re-implementing the scroller: useMessageScroller,
useMessageScrollerVisibility, and useMessageScrollerScrollable. They come
from the auto-installed @shadcn/react dependency, so there's nothing extra to
install. Reach for them only when composition can't express what you need.