Message scroller
A conversation that follows the newest message while you are at the end, stops when you scroll up, counts what arrives, and holds its place as older messages load.
Basic #
What is a recipe?
Pieces of style for each slot of a component, picked by its axes.
And a slot?
A named part of the component, which a style or a Mixin reaches.
Can I change one?
Extend the recipe, or add a rule in your app layer.
Which layer wins?
The app layer, after components and variants.
export const MessageScrollerStyle = forSlots(MessageScrollerSlots)(Recipes.MessageScroller({}))
const ConversationArea = forSlots(ScrollAreaSlots)(Recipes.ScrollArea({}))
const ChatControlStyle = forSlots(ButtonSlots)(
Recipes.Button({ tone: 'neutral', variant: 'outline' }),
)
/** A message of the conversation the scroller holds. */
export const ChatEntry = Schema.Struct({
id: Schema.String,
from: Schema.Literals(['user', 'assistant']),
text: Schema.String,
})
export type ChatEntry = typeof ChatEntry.Type
// Whether it follows the end is the scroller's Model, folded with
// `MessageScroller.update`; the application sends it `Appended` when a
// message lands at the end, and nothing when older ones load above.
export const messageScroller = <Message>(
style: NamedStyle<typeof MessageScrollerSlots>,
model: MessageScroller.Model,
entries: ReadonlyArray<ChatEntry>,
messages: {
readonly onMessage: (message: MessageScroller.Message) => Message
readonly onSend: Message
readonly onLoadOlder: Message
},
h: HtmlBuilder<Message>,
): Html =>
h.div(
[h.Style({ display: 'grid', gap: '0.75rem', inlineSize: 'min(32rem, 100%)' })],
[
h.div(
[h.Style({ blockSize: '20rem' })],
[
MessageScroller.view(
{
model,
label: 'Conversation with Ada',
content: [
ChatMessage.group(
{
items: entries.map(({ id, from, text }) => ({ key: id, from, content: text })),
style: ChatMessageGroupStyle,
messageStyle: ChatMessageStyle,
},
h,
),
],
onMessage: messages.onMessage,
style,
areaStyle: ConversationArea,
areaMixins: ScrollArea.overlayBars,
},
h,
),
],
),
h.div(
[h.Style({ display: 'flex', gap: '0.5rem' })],
[
Button.view({ label: 'Send', onClick: messages.onSend, style: ButtonStyle }, h),
Button.view(
{ label: 'Load older', onClick: messages.onLoadOlder, style: ChatControlStyle },
h,
),
],
),
],
)Usage
Each example above names the function that drew it, over the theme module Create a theme writes. The preview passes it the style picked above instead of its default.