fix(webui): soften composer scroll boundary

This commit is contained in:
Xubin Ren
2026-08-27 10:45:40 +08:00
parent 701926eba1
commit cb7b640d36
3 changed files with 36 additions and 4 deletions
@@ -744,8 +744,8 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
ref={messageRegionRef}
data-testid="thread-message-region"
className={cn(
"thread-viewport-scrollbar row-start-1 flex min-h-0 min-w-0 flex-col",
"scroll-auto justify-start overflow-x-hidden px-3 pb-4 pt-4 sm:px-4",
"thread-message-viewport thread-viewport-scrollbar row-start-1 flex min-h-0 min-w-0 flex-col",
"scroll-auto justify-start overflow-x-hidden px-3 pb-0 pt-4 sm:px-4",
"[overflow-anchor:none] [scrollbar-width:none]",
"[&::-webkit-scrollbar]:hidden",
hasVerticalOverflow ? "overflow-y-auto" : "overflow-hidden",
@@ -768,6 +768,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
onQuoteSelection={onQuoteSelection}
/>
</div>
<div aria-hidden className="thread-message-end-gap shrink-0" />
<div ref={bottomRef} aria-hidden className="h-px shrink-0" />
</div>
) : (
@@ -808,7 +809,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
}}
className={cn(
"row-start-2 z-10 w-full",
hasMessages ? "relative bg-background" : "relative self-center",
hasMessages ? "thread-composer-dock relative" : "relative self-center",
)}
>
<div
+28
View File
@@ -377,6 +377,7 @@
* bottom without a remount or a transform clone.
*/
.thread-layout {
--thread-composer-fade-height: 2.25rem;
grid-template-rows: minmax(min-content, 1fr) auto 0fr;
transition: grid-template-rows 220ms ease-out;
}
@@ -392,6 +393,33 @@
}
}
/**
* Keep the transcript and composer visually continuous. The scrollport owns
* the fade because overflow clips its contents before an adjacent overlay
* can soften the edge. The matching in-flow gap keeps the last line fully
* visible once the user reaches the bottom.
*/
.thread-message-viewport {
-webkit-mask-image: linear-gradient(
to bottom,
#000 0,
#000 calc(100% - var(--thread-composer-fade-height)),
transparent 100%
);
mask-image: linear-gradient(
to bottom,
#000 0,
#000 calc(100% - var(--thread-composer-fade-height)),
transparent 100%
);
}
.thread-message-end-gap {
height: var(--thread-composer-fade-height);
}
.thread-composer-dock {
background: hsl(var(--background));
}
@keyframes composer-status-strip-enter {
0% {
max-height: 0;
+4 -1
View File
@@ -259,7 +259,8 @@ describe("ThreadViewport", () => {
const messageRegion = screen.getByTestId("thread-message-region");
expect(messageRegion).toHaveClass("justify-start");
expect(messageRegion).not.toHaveClass("justify-end");
expect(messageRegion).toHaveClass("pb-4");
expect(messageRegion).toHaveClass("thread-message-viewport");
expect(messageRegion).toHaveClass("pb-0");
expect(messageRegion.className).not.toContain("5rem");
});
@@ -316,7 +317,9 @@ describe("ThreadViewport", () => {
expect(scroller).not.toContainElement(composerDock);
expect(scroller.parentElement).toContainElement(composerDock);
expect(composerDock).toHaveClass("relative");
expect(composerDock).toHaveClass("thread-composer-dock");
expect(composerDock).not.toHaveClass("sticky");
expect(scroller.querySelector(".thread-message-end-gap")).toBeInTheDocument();
expect(scroller.lastElementChild).toHaveClass("h-px", "shrink-0");
});