improve(ui): even out transcript turn and run block spacing (#128298)

* improve(ui): even out transcript turn and run block spacing

* fix(ui): keep expanded tool rows compact

* mock: fixtures (drop before land)

* test(ui): lock transcript spacing rhythm

* test(ui): cover expanded tool row spacing

* test(ui): cover touch footer turn spacing

* fix(ui): keep touch turn gaps stable with footer actions

* fix(ui): preserve persistent footer turn spacing

* fix(ui): preserve revealed footer turn spacing
This commit is contained in:
Vyctor H. Brzezowski
2026-08-23 14:19:23 -03:00
committed by GitHub
parent b0b7dec126
commit 09eb2f9934
2 changed files with 165 additions and 4 deletions
@@ -289,6 +289,80 @@ function completedWorkSpacingHtml() {
`;
}
function runBlockSpacingHtml() {
return `
<div class="chat-thread chat-thread--direct" role="log">
<div class="chat-thread-inner">
<div class="chat-group assistant chat-group--with-footer" data-run-turn>
<div class="chat-group-messages">
<div class="chat-bubble" data-run-block="text"><div class="chat-text">Opening text</div></div>
<div class="chat-bubble" data-run-block="detail"><div class="chat-text">Detail text</div></div>
<div class="chat-bubble chat-bubble--tool-shell" data-run-block="tool">Tool row</div>
<div class="chat-activity-group" data-run-block="list">
<div class="chat-activity-group__body">
<div class="chat-group-messages">
<div class="chat-bubble" data-expanded-row="text">Expanded detail</div>
<div class="chat-bubble chat-bubble--tool-shell" data-expanded-row="tool">Expanded tool row</div>
</div>
</div>
</div>
<div class="chat-activity-group chat-work-group" data-run-block="work">
<button class="chat-inline-disclosure chat-activity-group__summary" type="button">Worked for 10s</button>
<div class="chat-work-group__separator"></div>
</div>
</div>
<div class="chat-group-footer">
<span class="chat-sender-name">Assistant</span>
<div class="chat-group-footer-actions">
<button class="chat-copy-btn" type="button" aria-label="Copy">${iconSvg()}</button>
</div>
</div>
</div>
<div class="chat-group user chat-group--with-footer" data-next-turn>
<div class="chat-group-messages">
<div class="chat-bubble"><div class="chat-text">Next turn</div></div>
</div>
<div class="chat-group-footer"><span class="chat-sender-name">You</span></div>
</div>
<div class="chat-group user chat-group--with-footer" data-persistent-turn>
<div class="chat-group-messages">
<div class="chat-bubble"><div class="chat-text">Persistent identity turn</div></div>
</div>
<div class="chat-group-footer chat-group-footer--persistent-identity">
<span class="chat-sender-name">You</span>
<div class="chat-group-footer-actions">
<button class="chat-copy-btn" type="button" aria-label="Copy">${iconSvg()}</button>
</div>
</div>
</div>
<div class="chat-group assistant chat-group--with-footer" data-after-persistent-turn>
<div class="chat-group-messages">
<div class="chat-bubble"><div class="chat-text">After persistent identity</div></div>
</div>
<div class="chat-group-footer"><span class="chat-sender-name">Assistant</span></div>
</div>
<div class="chat-group user chat-group--with-footer chat-group--meta-revealed" data-revealed-persistent-turn>
<div class="chat-group-messages">
<div class="chat-bubble"><div class="chat-text">Revealed persistent identity</div></div>
</div>
<div class="chat-group-footer chat-group-footer--persistent-identity">
<span class="chat-sender-name">You</span>
<div class="chat-group-footer-actions">
<button class="chat-copy-btn" type="button" aria-label="Copy">${iconSvg()}</button>
</div>
</div>
</div>
<div class="chat-group assistant chat-group--with-footer" data-after-revealed-turn>
<div class="chat-group-messages">
<div class="chat-bubble"><div class="chat-text">After revealed identity</div></div>
</div>
<div class="chat-group-footer"><span class="chat-sender-name">Assistant</span></div>
</div>
</div>
</div>
`;
}
function chatFooterActionsHtml() {
return `
<div class="chat-group-footer-actions">
@@ -1307,6 +1381,63 @@ describeBrowserLayout.concurrent("chat responsive browser layout", () => {
}
});
it.each([
{ label: "desktop", width: 1366, hasTouch: false },
{ label: "mobile", width: 430, hasTouch: true },
])("keeps transcript turn and run block spacing on $label", async ({ width, hasTouch }) => {
const page = await openBrowserPage(width, 900, { hasTouch, isolated: true });
try {
await page.setContent(
`<!doctype html><html><head><style>${readUiCss()}</style></head><body>${runBlockSpacingHtml()}</body></html>`,
);
const gaps = await page.evaluate(() => {
const rect = (selector: string) =>
document.querySelector<HTMLElement>(selector)!.getBoundingClientRect();
const gap = (before: string, after: string) => rect(after).top - rect(before).bottom;
return {
intraTurn: gap('[data-run-block="text"]', '[data-run-block="detail"]'),
textToTool: gap('[data-run-block="detail"]', '[data-run-block="tool"]'),
toolToList: gap('[data-run-block="tool"]', '[data-run-block="list"]'),
listToWork: gap('[data-run-block="list"]', '[data-run-block="work"]'),
expandedTextToTool: gap('[data-expanded-row="text"]', '[data-expanded-row="tool"]'),
workedForSeparator: gap(
'[data-run-block="work"] > button',
".chat-work-group__separator",
),
turn: gap('[data-run-block="work"]', "[data-next-turn] .chat-bubble"),
persistentTurn: gap(
"[data-persistent-turn] .chat-bubble",
"[data-after-persistent-turn] .chat-bubble",
),
revealedPersistentTurn: gap(
"[data-revealed-persistent-turn] .chat-bubble",
"[data-after-revealed-turn] .chat-bubble",
),
simpleToPersistentTurn: gap(
"[data-next-turn] .chat-bubble",
"[data-persistent-turn] .chat-bubble",
),
};
});
expect(gaps).toEqual({
intraTurn: 2,
textToTool: 12,
toolToList: 12,
listToWork: 12,
expandedTextToTool: 6,
workedForSeparator: 0,
turn: 50,
persistentTurn: 50,
revealedPersistentTurn: 50,
simpleToPersistentTurn: 50,
});
} finally {
await closeBrowserPage(page);
}
});
it("insets only the bundled logo inside the unchanged avatar box", async () => {
const page = await openBrowserPage(430, 720);
try {
+34 -4
View File
@@ -3,15 +3,19 @@
============================================= */
/* Chat Group Layout - default (assistant/other on left). Groups without a
footer keep the shared 26px rhythm directly. Footer-bearing groups split
footer keep the shared turn gap directly. Footer-bearing groups split
message content and footer into separate rows so each row keeps the avatar
gutter while only the message row controls avatar alignment. */
.chat-group {
--chat-footer-row-offset: 26px;
--chat-message-column-max: var(--chat-message-max-width, min(900px, 68%));
--chat-run-block-gap: 12px;
--chat-touch-row-margin: 14px;
--chat-turn-gap: 50px;
display: flex;
gap: 10px;
align-items: flex-start;
padding-bottom: 26px;
padding-bottom: var(--chat-turn-gap);
margin-left: 4px;
margin-right: 16px;
}
@@ -55,7 +59,7 @@
grid-template-rows: auto auto;
gap: 2px 10px;
justify-content: start;
padding-bottom: 0;
padding-bottom: calc(var(--chat-turn-gap) - var(--chat-footer-row-offset));
}
.chat-group.user.chat-group--with-footer {
@@ -110,6 +114,21 @@
margin-block-start: var(--chat-message-type-boundary-gap);
}
.chat-group
> .chat-group-messages
> :is(
.chat-bubble--tool-shell + .chat-bubble:not(.chat-bubble--tool-shell),
.chat-bubble:not(.chat-bubble--tool-shell) + .chat-bubble--tool-shell,
.chat-bubble + .chat-activity-group,
.chat-activity-group + .chat-bubble,
.chat-work-group + .chat-bubble,
.chat-work-group + .chat-activity-group,
.chat-bubble + .chat-work-group,
.chat-activity-group + .chat-work-group
) {
margin-block-start: calc(var(--chat-run-block-gap) - 2px);
}
/* User messages align content right */
.chat-group.user .chat-group-messages {
align-items: flex-end;
@@ -746,11 +765,22 @@ img.chat-avatar.chat-avatar--logo {
/* Shared message renderers stay touch-readable by default. Transcript rows
opt into one-at-a-time disclosure through their owning thread. */
.chat-group {
--chat-footer-row-offset: 44px;
--chat-footer-disclosure-opacity: 1;
--chat-footer-disclosure-pointer-events: auto;
--chat-footer-detail-position: static;
padding-bottom: calc(var(--chat-turn-gap) - var(--chat-touch-row-margin));
margin-bottom: var(--chat-touch-row-margin);
}
.chat-group.chat-group--with-footer:has(
.chat-group-footer:not(.chat-group-footer--persistent-identity) .chat-group-footer-actions
),
.chat-group.chat-group--with-footer:is(.chat-group--meta-revealed, :focus-within):has(
.chat-group-footer--persistent-identity .chat-group-footer-actions
) {
padding-bottom: 0;
margin-bottom: 14px;
margin-bottom: 0;
}
.chat-group-footer {