Chat Transcript
The small parts every transcript repeats: the day a run of messages belongs to, the clock beside a turn, a human's message with its links made clickable, pictures that open in the lightbox, files that can only be named, and a turn that never reached the server. Assemble them inside message-scroller, message and bubble.
Day divider and timestamp
`startsNewDay(previous, current)` says when to draw the divider, `dayLabelOf` says what it reads: Today, Yesterday, or the date. `ChatTimestamp` is the clock beside a turn, and it never fades: an action you go looking for can hide until hover, context you read cannot.
Usage
Import the parts you need from the component's folder.
import {
ChatDayDivider,
ChatTimestamp,
ChatText,
ChatPictures,
ChatFileChips,
ChatFailedTurn,
ChatUnreadDot,
dayLabelOf,
startsNewDay,
midnightOf,
formatMessageTime,
relativeTimeOf,
linkSegments,
} from "@/components/ui/chat";Text with links
A human's message is text, not markdown. `ChatText` splits it into segments and builds an element for each URL, so whatever someone typed stays text and nothing they wrote is parsed as markup. Trailing punctuation stays with the sentence.
Pictures and file chips
Pictures are shown, not named. One picture is an `Attachment` card sized to the photo, several go through the lightbox's own grid, which shows the first four and says +2 for the rest. Either way the lightbox is what opens.
Anything that cannot be drawn, a document or an image whose bytes are gone, goes through `ChatFileChips`: an icon picked from the extension, the name, and the type and size underneath.
Download affordance
Give an item a `downloadUrl` and it gets a download button. A picture and a named file are the same `Attachment` in two orientations, so one structure serves both: `AttachmentActions` sits inline after the title on `orientation="horizontal"`, and becomes an absolute overlay at top-3 right-3 on `orientation="vertical"`. Neither component writes a button of its own.
On the picture, `AttachmentTrigger` covers the card to open the lightbox and sits under the actions, so the download stays separately clickable. A grid of several pictures gets no per-tile button: the lightbox already carries one.
Failed turn
A turn that never reached the server keeps the reader's own words where they wrote them, says what went wrong, and offers Retry and Discard. The host owns the message and decides what retrying means.
Unread dot
For a launcher whose panel is shut while something arrived. A dot rather than a count: on a small round button a number needs a size the style guide does not allow for a value, and the next action is the same either way. It positions itself against the nearest positioned ancestor, so give the launcher `relative`.
Anatomy
Import all parts and piece them together.
<script setup>
import {
ChatDayDivider,
ChatFailedTurn,
ChatFileChips,
ChatPictures,
ChatText,
ChatTimestamp,
ChatUnreadDot,
} from "@/components/ui/chat";
</script>
<template>
<ChatDayDivider />
<ChatPictures />
<ChatFileChips />
<ChatText />
<ChatTimestamp />
<ChatFailedTurn />
<ChatUnreadDot />
</template>API Reference
ChatDayDivider
ChatTimestamp
ChatText
ChatPictures
ChatFileChips
ChatFailedTurn
ChatUnreadDot
Helpers
Accessibility
Keyboard shortcuts and ARIA behavior.
- ChatDayDivider is a role=separator carrying the day as its aria-label, so the date is announced as a boundary rather than read as a stray line of text.
- ChatText builds an element per URL instead of setting innerHTML, so nothing anyone types is parsed as markup. Links open in a new tab with rel=noopener noreferrer.
- ChatTimestamp stays visible rather than appearing on hover, so the clock is readable without a pointer.
- ChatPictures labels each image with its file name, falling back to 'Attached image' when a message did not carry one.
- ChatUnreadDot is aria-hidden and pointer-events-none. It marks a launcher that already has its own label, so announce the unread state on that button.