chat-layout
v1.0.0
Published
Canvas-based layout primitives for chat and timeline UIs.
Readme
chat-layout
Canvas-based layout primitives for chat and timeline UIs.
The current v2-style APIs are:
Flex: row/column layoutFlexItem: explicitgrow/shrink/alignSelfPlace: place a single child atstart/center/endMultilineText: text layout with logicalalignor physicalphysicalAlignChatRenderer+ListState: virtualized chat renderingmemoRenderItem/memoRenderItemBy: item render memoization
Quick example
Use Flex to build structure, FlexItem to control resize behavior, and Place to align the final bubble:
const bubble = new RoundedBox(
new MultilineText(item.content, {
lineHeight: 20,
font: "16px system-ui",
style: "black",
align: "start",
}),
{ top: 6, bottom: 6, left: 10, right: 10, radii: 8, fill: "#ccc" },
);
const row = new Flex(
[
avatar,
new FlexItem(bubble, { grow: 1, shrink: 1 }),
],
{ direction: "row", gap: 4, reverse: item.sender === "A" },
);
return new Place(row, {
align: item.sender === "A" ? "end" : "start",
});See example/chat.ts for a full chat example.
Layout notes
Flexhandles the main axis only. It shrink-wraps on the cross axis unless you opt into stretch behavior.maxWidth/maxHeightlimit measurement, but do not automatically make children fill the cross axis.- Use
alignItems: "stretch"oralignSelf: "stretch"when a child should fill the computed cross size. Placeis the simplest way to align a single bubble left, center, or right.MultilineText.alignuses logical values:start,center,end.MultilineText.physicalAlignuses physical values:left,center,right.TextandMultilineTextpreserve blank lines and edge whitespace by default. Usewhitespace: "trim-and-collapse"if you want cleanup.TextandMultilineTextdefault tooverflowWrap: "break-word", which preserves compatibility-first min-content sizing for shrink layouts.- Use
overflowWrap: "anywhere"when long unspaced strings should contribute grapheme-level breakpoints to min-content sizing. Textsupportsoverflow: "ellipsis"withellipsisPosition: "start" | "end" | "middle"when measured under a finitemaxWidth.MultilineTextsupportsoverflow: "ellipsis"together withmaxLines; values below1are treated as1.
Text ellipsis
Single-line Text can ellipsize at the start, end, or middle when a finite width constraint is present:
const title = new Text("Extremely long thread title that should not blow out the row", {
lineHeight: 20,
font: "16px system-ui",
style: "#111",
overflow: "ellipsis",
ellipsisPosition: "middle",
});Multi-line MultilineText can cap the visible line count and convert the last visible line to an end ellipsis:
const preview = new MultilineText(reply.content, {
lineHeight: 16,
font: "13px system-ui",
style: "#444",
align: "start",
overflowWrap: "anywhere",
overflow: "ellipsis",
maxLines: 2,
});Notes:
- Ellipsis is only inserted when the node is measured under a finite
maxWidthand content actually overflows that constraint. MultilineTextonly supports end ellipsis on the last visible line; start/middle ellipsis are intentionally single-line only.maxLinesdefaults to unlimited, and values below1are clamped to1.overflowWrap: "break-word"keeps the current min-content behavior;overflowWrap: "anywhere"lets long unspaced strings shrink inside flex layouts such as chat bubbles.- Current
measureMinContent()behavior stays compatibility-first: ellipsis affects constrained measurement/drawing, but does not lower the min-content shrink floor by itself.
Shrink behavior
FlexItemOptions.shrinkdefaults to0, so old layouts keep their previous behavior unless you opt in.- Shrink only applies when there is a finite main-axis constraint and total content size overflows it.
- Overflow is redistributed by
shrink * basis; todaybasisis internal-only and always"auto". - Custom nodes can implement
measureMinContent()for better shrink results. - Known limitation: column shrink with
MultilineTextdoes not clip drawing by itself.
Migration notes
- Use
memoRenderItemBy(keyOf, renderItem)when list items are primitives. FlexItemexposesgrow,shrink, andalignSelf;basisis no longer public.MultilineTextnow usesalign/physicalAligninstead ofalignment.ListState.positionusesundefinedfor the renderer default anchor.- Use
list.resetScroll()orlist.setAnchor(index, offset)instead of assigningNumber.NaN.
Development
Install dependencies:
bun installType-check:
bun run typecheckBuild distributable files:
bun run distBuild the chat example:
bun run example