Skip to content

Commit a6b8a1b

Browse files
committed
fix(bubble): stop painting the sender tint and bubble padding around a designed mail
1 parent fc999e0 commit a6b8a1b

3 files changed

Lines changed: 106 additions & 3 deletions

File tree

‎src/components/ChatBubble.tsx‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,13 @@ export interface ChatBubbleProps {
117117
onDownloadAttachment?: (attachment: Attachment, message: ChatMessage) => void;
118118
/** Controls shown at the bubble's outer edge on hover: menus, star, retry. */
119119
renderActions?: (message: ChatMessage) => ReactNode;
120-
/** Anything below the body, inside the bubble: a reply box, an AI notice. */
120+
/**
121+
* Anything below the body, inside the bubble: a reply box, an AI notice.
122+
*
123+
* A bubble holding a designed mail carries no padding of its own — the
124+
* document reaches the card's edge — so a footer that needs breathing room
125+
* has to bring it. Style it against `.sec-bubble--doc` if it must differ.
126+
*/
121127
renderFooter?: (message: ChatMessage) => ReactNode;
122128
className?: string;
123129
}
@@ -174,10 +180,15 @@ export function ChatBubble({
174180
);
175181

176182
const attachments = message.attachments ?? [];
183+
// A designed mail is not a chat line, it is a finished document: it brings
184+
// its own background, its own margins and often its own colour scheme. Tint
185+
// and pad it like a bubble and the reader gets a card inside a card. So a
186+
// framed body keeps the container and loses the costume.
187+
const isDocument = shape.kind === 'rich';
177188
// A framed body needs a sized containing block, so its bubble takes the full
178189
// column. A short inline one hugs its text, which is what makes a two-word
179190
// reply look like a two-word reply.
180-
const hug = shape.kind !== 'rich';
191+
const hug = !isDocument;
181192

182193
const rowClasses = ['sec-row', isMine ? 'sec-row--mine' : 'sec-row--theirs', className]
183194
.filter(Boolean)
@@ -187,6 +198,7 @@ export function ChatBubble({
187198
isMine ? 'sec-bubble--mine' : 'sec-bubble--theirs',
188199
compact ? '' : 'sec-bubble--tail',
189200
hug ? '' : 'sec-bubble--wide',
201+
isDocument ? 'sec-bubble--doc' : '',
190202
]
191203
.filter(Boolean)
192204
.join(' ');
@@ -244,7 +256,10 @@ export function ChatBubble({
244256
// The audit trail, in the DOM. "Why did part of my email disappear?"
245257
// is answerable with devtools instead of a rebuild.
246258
data-sec-applied={message.applied?.length ? message.applied.join(' ') : undefined}
247-
style={!isMine && color ? { backgroundColor: color.bubble } : undefined}
259+
// The sender tint is skipped for a document: it would sit behind the
260+
// mail's own background, visible only as a coloured rim. The avatar
261+
// and the sender name above the bubble carry that colour instead.
262+
style={!isMine && color && !isDocument ? { backgroundColor: color.bubble } : undefined}
248263
>
249264
<MessageBody
250265
message={message}

‎src/styles/index.css‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,60 @@
384384
width: 100%;
385385
}
386386

387+
/*
388+
* A framed body — a newsletter, an approval notice, any mail built as a page.
389+
*
390+
* It is rendered verbatim, which is the rule: designed mail is not reflowed.
391+
* But verbatim means it arrives with its own background and its own margins,
392+
* and a bubble that then tints and pads it produces a card inside a card. Two
393+
* or three of those in a row from one sender is the "heavy formatting looks
394+
* weird" report. So the bubble keeps only what a container owes the document:
395+
* an edge, a radius, and the clipping that makes the radius mean something.
396+
*/
397+
.sec-bubble--doc {
398+
padding: 0;
399+
/* Beats the inline sender tint only because the tint is skipped for a
400+
document in `ChatBubble`; this covers `--mine`, which is a class. */
401+
background: var(--sec-surface);
402+
box-shadow: none;
403+
/* The document fills the card edge to edge, so the card has to clip it —
404+
otherwise its corners square off over the border radius. */
405+
overflow: hidden;
406+
}
407+
408+
/* A document is its own surface, so the reader's own designed mail cannot
409+
inherit the brand-filled bubble's ink or its chip treatment. */
410+
.sec-bubble--doc.sec-bubble--mine {
411+
border-color: var(--sec-border);
412+
color: var(--sec-bubble-theirs-ink);
413+
}
414+
415+
.sec-bubble--doc.sec-bubble--mine .sec-chip {
416+
border-color: var(--sec-border);
417+
background: var(--sec-surface);
418+
color: var(--sec-ink);
419+
}
420+
421+
.sec-bubble--doc.sec-bubble--mine .sec-chip__clip,
422+
.sec-bubble--doc.sec-bubble--mine .sec-chip__size {
423+
color: var(--sec-muted);
424+
opacity: 1;
425+
}
426+
427+
/* Padding came off the bubble, so the parts that are ours rather than the
428+
sender's bring their own — and a rule to sit behind, so they read as the
429+
frame around the document instead of part of it. */
430+
.sec-bubble--doc .sec-attachments {
431+
margin-block-start: 0;
432+
padding: var(--sec-pad);
433+
border-block-start: 1px solid var(--sec-border);
434+
}
435+
436+
.sec-bubble--doc .sec-note--images {
437+
margin-block-end: 0;
438+
border-radius: 0;
439+
}
440+
387441
/* The squared corner points back at the avatar, and only on the FIRST bubble of
388442
a run — which is exactly where the avatar is. */
389443
.sec-bubble--tail.sec-bubble--theirs {

‎test/components/ChatBubble.test.tsx‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,40 @@ describe('ChatBubble', () => {
152152
expect(rich.container.querySelector('.sec-bubble')?.className).toContain('sec-bubble--wide');
153153
});
154154

155+
// Regression: a designed mail is rendered verbatim, so it arrives with its
156+
// own background. Paint the sender tint behind it and pad it like a chat
157+
// line and the reader sees a card inside a card — which is what a heavy HTML
158+
// mail looked like. The tint has to be dropped and `--doc` has to be on, or
159+
// the stylesheet has nothing to hang the padding removal off.
160+
it('drops the sender tint and the bubble padding for a designed mail', () => {
161+
const { container } = render(
162+
<ChatBubble
163+
message={chatMessage({ body: '<table><tr><td>Approve</td></tr></table>' })}
164+
color={COLOR}
165+
labels={DEFAULT_LABELS}
166+
{...FIXED}
167+
/>,
168+
);
169+
const bubble = container.querySelector('.sec-bubble') as HTMLElement;
170+
expect(bubble.className).toContain('sec-bubble--doc');
171+
expect(bubble.style.backgroundColor).toBe('');
172+
// The identity colour is not lost, it moves to where it can be seen.
173+
expect((container.querySelector('.sec-avatar') as HTMLElement).style.backgroundColor).toBe(
174+
COLOR.avatar,
175+
);
176+
});
177+
178+
// Regression: `--doc` is the framed-body marker, so an inline reply must
179+
// never carry it — a plain bubble with no padding collapses onto its text.
180+
it('keeps the tinted, padded bubble for an inline reply', () => {
181+
const { container } = render(
182+
<ChatBubble message={chatMessage()} color={COLOR} labels={DEFAULT_LABELS} {...FIXED} />,
183+
);
184+
const bubble = container.querySelector('.sec-bubble') as HTMLElement;
185+
expect(bubble.className).not.toContain('sec-bubble--doc');
186+
expect(bubble.style.backgroundColor).toBe(COLOR.bubble);
187+
});
188+
155189
// Regression: end-to-end cover for the "screenful of blank under the last
156190
// line" bug. The bubble must render the SHAPE's html, not `message.body` —
157191
// wire it to the original and the trailing empty wrappers come back, and a

0 commit comments

Comments
 (0)