feat(ui): overlay hover meta and even out chat thread rhythm (#104201)

* feat(ui): overlay hover meta and even out chat thread rhythm

* fix(ui): keep the overlay footer single-line so it fits the rhythm gap

* fix(ui): wrap the revealed footer in narrow lanes and gate hidden-overlay hit-testing

* test(ui): travel through the group before hovering pointer-gated footers

* fix(ui): keep the streaming footer sender and time on one shrinkable row
This commit is contained in:
Peter Steinberger
2026-07-11 10:15:28 -07:00
committed by GitHub
parent 0a10b1408a
commit e5578f6652
3 changed files with 48 additions and 8 deletions

View File

@@ -614,6 +614,9 @@ describeBrowserLayout.concurrent("chat responsive browser layout", () => {
});
expect(await context.isVisible()).toBe(false);
// Travel like a real pointer: the footer overlay is pointer-gated until
// the group is hovered, so enter through the message body first.
await page.locator(".chat-text").first().hover();
await page.locator(".msg-meta__summary").hover();
expect(await context.isVisible()).toBe(true);
const hoverLayout = await page.evaluate(() => {
@@ -635,6 +638,7 @@ describeBrowserLayout.concurrent("chat responsive browser layout", () => {
await page.mouse.move(0, 0);
expect(await context.isVisible()).toBe(false);
await page.locator(".chat-text").first().hover();
await page.locator(".msg-meta__summary").click();
await page.mouse.move(0, 0);
expect(await details.getAttribute("open")).toBe("");

View File

@@ -619,8 +619,10 @@ export function renderStreamGroup(parts: StreamGroupPart[], opts: StreamGroupOpt
${footerStartedAt !== null
? html`
<div class="chat-group-footer">
<span class="chat-sender-name">${name}</span>
${renderChatTimestamp(footerStartedAt)}
<div class="chat-group-footer__meta">
<span class="chat-sender-name">${name}</span>
${renderChatTimestamp(footerStartedAt)}
</div>
</div>
`
: nothing}

View File

@@ -2,12 +2,16 @@
GROUPED CHAT LAYOUT (Slack-style)
============================================= */
/* Chat Group Layout - default (assistant/other on left) */
/* Chat Group Layout - default (assistant/other on left). The bottom PADDING
is the thread's rhythm unit and doubles as the reveal zone for the
overlaid hover footer: padding (not margin) keeps the gap inside the
group's hover box so the pointer can travel to the footer without it
vanishing mid-way. */
.chat-group {
display: flex;
gap: 10px;
align-items: flex-start;
margin-bottom: 14px;
padding-bottom: 26px;
margin-left: 4px;
margin-right: 16px;
}
@@ -19,6 +23,7 @@
}
.chat-group-messages {
position: relative; /* anchors the overlaid hover footer */
display: flex;
flex-direction: column;
gap: 2px;
@@ -43,17 +48,27 @@
}
/* Footer at bottom of message group (role + time). Hidden until the group is
hovered/focused so the thread reads as a clean stream; opacity (not
display) keeps the height reserved and avoids layout shift on reveal. */
hovered/focused so the thread reads as a clean stream; overlaid into the
group's bottom margin (no reserved flow height) so message rhythm stays
even. Touch devices restore the in-flow always-visible footer below. */
.chat-group-footer {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1; /* revealed overlay paints above the next group's leading edge */
display: flex;
/* Wrap instead of clipping in narrow lanes (extreme chatMessageMaxWidth,
slim split panes): a wrapped revealed footer may transiently overlap the
next group, but every control stays reachable. Hidden overlays must not
intercept pointer input, so hit-testing is gated on the reveal. */
flex-wrap: wrap;
gap: 8px;
align-items: center;
width: 100%;
min-width: 0;
margin-top: 4px;
margin-top: 1px;
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease-out;
}
@@ -61,6 +76,7 @@
.chat-group:focus-within .chat-group-footer,
.chat-group-footer:has(details.msg-meta[open]) {
opacity: 1;
pointer-events: auto;
}
.chat-group-footer__meta {
@@ -89,10 +105,16 @@
margin-top: 4px;
}
/* Single line, ellipsized: the footer overlays a fixed 26px rhythm gap, so
long configurable sender names must never wrap it taller than the gap. */
.chat-sender-name {
font-weight: 500;
font-size: 12px;
color: var(--muted);
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-group-timestamp {
@@ -339,8 +361,20 @@ img.chat-avatar {
@media (hover: none),
(max-width: 768px),
(max-width: 932px) and (max-height: 500px) and (orientation: landscape) {
/* No hover to reveal an overlay: restore the in-flow always-visible footer
and the tighter rhythm that accounts for its height. */
.chat-group {
padding-bottom: 0;
margin-bottom: 14px;
}
.chat-group-footer {
position: static;
flex-wrap: wrap;
width: 100%;
margin-top: 4px;
opacity: 1;
pointer-events: auto;
}
.chat-group-footer-actions button,