mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: add sticky headers to the git diff (#22425)
<!-- If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting. -->
This commit is contained in:
@@ -268,7 +268,7 @@ const ChatMessageItem = memo<{
|
||||
</div>
|
||||
{fadeFromBottom && (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-12"
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 max-h-12"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to top, hsl(var(--surface-secondary)), transparent)",
|
||||
@@ -400,8 +400,10 @@ const StickyUserMessage: FC<{
|
||||
}) => {
|
||||
const [isStuck, setIsStuck] = useState(false);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [isTooTall, setIsTooTall] = useState(false);
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const updateFnRef = useRef<(() => void) | null>(null);
|
||||
|
||||
// useLayoutEffect so isStuck and --clip-h are both resolved
|
||||
// before the browser paints, avoiding a flash on load.
|
||||
@@ -440,9 +442,22 @@ const StickyUserMessage: FC<{
|
||||
|
||||
const MIN_HEIGHT = 72;
|
||||
let scrollerTop = scroller.getBoundingClientRect().top;
|
||||
let scrollerHeight = scroller.clientHeight;
|
||||
|
||||
const update = () => {
|
||||
const fullHeight = container.offsetHeight;
|
||||
|
||||
// Skip sticky behavior for messages that take up
|
||||
// most of the visible area — accounting for the
|
||||
// chat input and some breathing room.
|
||||
const tooTall = fullHeight > scrollerHeight * 0.75;
|
||||
setIsTooTall(tooTall);
|
||||
if (tooTall) {
|
||||
container.style.setProperty("--clip-h", `${fullHeight}px`);
|
||||
container.style.setProperty("--fade-opacity", "0");
|
||||
return;
|
||||
}
|
||||
|
||||
const sentinelTop = sentinel.getBoundingClientRect().top;
|
||||
const scrolledPast = scrollerTop - sentinelTop;
|
||||
|
||||
@@ -463,9 +478,11 @@ const StickyUserMessage: FC<{
|
||||
visible < fullHeight - 8 ? "1" : "0",
|
||||
);
|
||||
};
|
||||
updateFnRef.current = update;
|
||||
|
||||
const onResize = () => {
|
||||
scrollerTop = scroller.getBoundingClientRect().top;
|
||||
scrollerHeight = scroller.clientHeight;
|
||||
update();
|
||||
};
|
||||
|
||||
@@ -495,6 +512,16 @@ const StickyUserMessage: FC<{
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Re-run the height calculation synchronously whenever
|
||||
// isStuck changes so --clip-h is correct on the same frame
|
||||
// the overlay appears. Without this, the async
|
||||
// IntersectionObserver + RAF-throttled scroll handler can
|
||||
// leave a stale --clip-h for one paint.
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: isStuck is an intentional trigger
|
||||
useLayoutEffect(() => {
|
||||
updateFnRef.current?.();
|
||||
}, [isStuck]);
|
||||
|
||||
const handleEditUserMessage = onEditUserMessage
|
||||
? (messageId: number, text: string) => {
|
||||
onEditUserMessage(messageId, text);
|
||||
@@ -519,18 +546,19 @@ const StickyUserMessage: FC<{
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"relative sticky top-0 z-10 px-3 -mx-3 pt-2 pb-2",
|
||||
"relative px-3 -mx-3 pt-2 pb-2",
|
||||
!isTooTall && "sticky top-0 z-10",
|
||||
!isReady && "invisible",
|
||||
isStuck && "pointer-events-none",
|
||||
isStuck && !isTooTall && "pointer-events-none",
|
||||
)}
|
||||
>
|
||||
{/* Flow element: always in the DOM to preserve
|
||||
scroll layout. Hidden when stuck so the
|
||||
clipped overlay takes over visually. */}
|
||||
<div
|
||||
className={isStuck ? undefined : "pointer-events-auto"}
|
||||
className={isStuck && !isTooTall ? undefined : "pointer-events-auto"}
|
||||
style={
|
||||
isStuck
|
||||
isStuck && !isTooTall
|
||||
? { opacity: "calc(1 - var(--overlay-ready, 0))" }
|
||||
: undefined
|
||||
}
|
||||
@@ -548,7 +576,7 @@ const StickyUserMessage: FC<{
|
||||
sticky container. max-height + mask are driven
|
||||
entirely by the --clip-h CSS variable which the
|
||||
scroll handler sets on the container. */}
|
||||
{isStuck && (
|
||||
{isStuck && !isTooTall && (
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
|
||||
@@ -40,7 +40,15 @@ function formatPullRequestLabel(url: string): string {
|
||||
export const FilesChangedPanel: FC<FilesChangedPanelProps> = ({ chatId }) => {
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
const diffOptions = useMemo(() => getDiffViewerOptions(isDark), [isDark]);
|
||||
const diffOptions = useMemo(() => {
|
||||
const base = getDiffViewerOptions(isDark);
|
||||
return {
|
||||
...base,
|
||||
// Extend the base CSS to make file headers sticky so they
|
||||
// remain visible while scrolling through long diffs.
|
||||
unsafeCSS: `${base.unsafeCSS ?? ""} [data-diffs-header] { position: sticky; top: 0; z-index: 10; background-color: hsl(var(--surface-primary)) !important; }`,
|
||||
};
|
||||
}, [isDark]);
|
||||
|
||||
const diffStatusQuery = useQuery(chatDiffStatus(chatId));
|
||||
const diffContentsQuery = useQuery({
|
||||
|
||||
@@ -90,6 +90,14 @@ export default defineConfig({
|
||||
target: process.env.CODER_HOST || "http://localhost:3000",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
configure: (proxy) => {
|
||||
if (process.env.CODER_SESSION_TOKEN) {
|
||||
proxy.on("proxyReq", (proxyReq) => {
|
||||
proxyReq.setHeader(
|
||||
"Coder-Session-Token",
|
||||
process.env.CODER_SESSION_TOKEN!,
|
||||
);
|
||||
});
|
||||
}
|
||||
// Vite does not catch socket errors, and stops the webserver.
|
||||
// As /logs endpoint can return HTTP 4xx status, we need to embrace
|
||||
// Vite with a custom error handler to prevent from quitting.
|
||||
|
||||
Reference in New Issue
Block a user