diff --git a/packages/web/src/app/(app)/components/banners/bannerHeightObserver.tsx b/packages/web/src/app/(app)/components/banners/bannerHeightObserver.tsx new file mode 100644 index 000000000..b465bc5fa --- /dev/null +++ b/packages/web/src/app/(app)/components/banners/bannerHeightObserver.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { useEffect, useRef } from "react"; + +const BANNER_HEIGHT_VAR = '--banner-height'; + +/** + * Measures the rendered height of the top banner and exposes it as the + * `--banner-height` CSS variable on the document root (0px when no banner is + * shown). Viewport-based layouts (e.g. the chat thread's `calc(100vh - ...)` + * sizing) subtract this so they don't overflow when a banner is present. + */ +export function BannerHeightObserver({ children }: { children: React.ReactNode }) { + const ref = useRef(null); + + useEffect(() => { + const element = ref.current; + if (!element) { + return; + } + + const root = document.documentElement; + const setHeight = (height: number) => { + root.style.setProperty(BANNER_HEIGHT_VAR, `${height}px`); + }; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + setHeight(entry.contentRect.height); + } + }); + resizeObserver.observe(element); + + return () => { + resizeObserver.disconnect(); + root.style.setProperty(BANNER_HEIGHT_VAR, '0px'); + }; + }, []); + + return
{children}
; +} diff --git a/packages/web/src/app/(app)/layout.tsx b/packages/web/src/app/(app)/layout.tsx index c05cbd38c..527c03817 100644 --- a/packages/web/src/app/(app)/layout.tsx +++ b/packages/web/src/app/(app)/layout.tsx @@ -26,6 +26,7 @@ import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch"; import { GitHubStarToast } from "./components/githubStarToast"; import { getLinkedAccounts } from "@/ee/features/sso/actions"; import { BannerSlot } from "./components/banners/bannerSlot"; +import { BannerHeightObserver } from "./components/banners/bannerHeightObserver"; import { getPermissionSyncStatus } from "../api/(server)/ee/permissionSyncStatus/api"; import { OrgRole } from "@sourcebot/db"; import { ServiceErrorException } from "@/lib/serviceError"; @@ -185,15 +186,17 @@ export default async function Layout(props: LayoutProps) { {sidebar}
- + + +
{children}
diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css index 710e6160e..a2878d321 100644 --- a/packages/web/src/app/globals.css +++ b/packages/web/src/app/globals.css @@ -7,6 +7,7 @@ @layer base { html { overflow-y: scroll; + --banner-height: 0px; } /* Hide scrollbar but keep functionality */ @@ -16,6 +17,7 @@ } :root { + --banner-height: 0px; --background: hsl(0 0% 99%); --background-secondary: hsl(0, 0%, 98%); --foreground: hsl(37, 84%, 5%); diff --git a/packages/web/src/ee/features/chat/components/chatThread/chatThreadListItem.tsx b/packages/web/src/ee/features/chat/components/chatThread/chatThreadListItem.tsx index d9ccb3d7c..d8776fe57 100644 --- a/packages/web/src/ee/features/chat/components/chatThread/chatThreadListItem.tsx +++ b/packages/web/src/ee/features/chat/components/chatThread/chatThreadListItem.tsx @@ -174,7 +174,7 @@ const ChatThreadListItemComponent = forwardRef { - const maxHeight = 'calc(100vh - 215px)'; + const maxHeight = 'calc(100vh - 215px - var(--banner-height, 0px))'; return { height: leftPanelHeight ? `min(${leftPanelHeight}px, ${maxHeight})` : maxHeight, @@ -323,7 +323,7 @@ const ChatThreadListItemComponent = forwardRef