From ff816449fb3b7a2b910e0b481783eb42d583cbd0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 8 Jul 2026 18:21:46 +0000 Subject: [PATCH 1/2] fix(mobile): fully hide dashboard header on phone scroll down On phones the dashboard uses a collapse hide-on-scroll strategy where the header sits above an internally scrolling
. Sticky positioning fought the 0fr grid collapse, leaving ~19px of header chrome visible. Switch the header to relative positioning on max-sm for collapse hosts. Also wire scroll reporting through ClinicalDashboard main onScroll so hide state updates reliably from the real scroll container, keep the passive listener as a backup, and harden useHideOnScroll state tracking. Co-authored-by: BigSimmo --- src/components/ClinicalDashboard.tsx | 13 +- .../master-search-header.tsx | 30 +++-- .../clinical-dashboard/use-hide-on-scroll.ts | 119 ++++++++++++++---- 3 files changed, 128 insertions(+), 34 deletions(-) diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 46c9f3b1f..ecc06d7fd 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -23,7 +23,7 @@ import { WifiOff, Wrench, } from "lucide-react"; -import { type CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { type CSSProperties, type UIEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type DocumentDeleteResult } from "@/components/DocumentManagementActions"; import { extractSafetyFindings } from "@/lib/clinical-safety"; import { readLocalProjectIdentity, unsafeLocalProjectMessage } from "@/lib/local-project-identity"; @@ -77,6 +77,7 @@ import { import { AnswerEmptyState, AnswerSkeleton } from "@/components/clinical-dashboard/answer-status"; import { evidenceMapRowsFromRenderModel } from "@/components/clinical-dashboard/evidence-map-model"; import { MasterSearchHeader } from "@/components/clinical-dashboard/master-search-header"; +import { useScrollHideReporter } from "@/components/clinical-dashboard/use-hide-on-scroll"; import { SearchCommandProvider } from "@/components/clinical-dashboard/search-command-context"; import { answerRecovery, errorCopy } from "@/lib/ui-copy"; import { applicationsLauncherItemCount } from "@/components/applications-launcher-page"; @@ -669,6 +670,7 @@ export function ClinicalDashboard({ const router = useRouter(); const searchParams = useSearchParams(); const mainRef = useRef(null); + const phoneScrollHide = useScrollHideReporter(); const [bottomSearchScrollHidden, setBottomSearchScrollHidden] = useState(false); const composerInputRef = useRef(null); const scrollFrameRef = useRef(null); @@ -2653,6 +2655,11 @@ export function ClinicalDashboard({ }); } + function handleMainScroll(event: UIEvent) { + scheduleActiveSectionSync(); + phoneScrollHide.reportScroll(event.currentTarget.scrollTop); + } + async function copyText(action: string, text: string) { let copied = false; try { @@ -3115,7 +3122,7 @@ export function ClinicalDashboard({ heroComposerFromTablet={Boolean(desktopHomeComposerSlotId)} // Phone-only: the header sits above the internally scrolling
, // so hiding must collapse its layout space to hand it to content. - hideOnScroll={{ strategy: "collapse", containerRef: mainRef }} + hideOnScroll={{ strategy: "collapse", scrollHidden: phoneScrollHide.hidden, containerRef: mainRef }} onBottomComposerScrollHiddenChange={setBottomSearchScrollHidden} /> @@ -3123,7 +3130,7 @@ export function ClinicalDashboard({ id="main-content" ref={mainRef} tabIndex={-1} - onScroll={scheduleActiveSectionSync} + onScroll={handleMainScroll} className={cn( "min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain [-webkit-overflow-scrolling:touch] focus:outline-none", searchMode === "answer" diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index fcf9d2fa6..6cc3dbba2 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -245,7 +245,13 @@ export function MasterSearchHeader({ * layout space (host keeps the header above an internally scrolling element). * The phone bottom search composer hides in sync on search-mode pages. * `containerRef` points at the scrolling element; omit it to observe window scroll. */ - hideOnScroll?: { strategy: "overlay" | "collapse"; containerRef?: RefObject }; + hideOnScroll?: { + strategy: "overlay" | "collapse"; + containerRef?: RefObject; + scrollContainer?: HTMLElement | null; + /** Parent-owned hidden state for hosts that report scroll via React `onScroll`. */ + scrollHidden?: boolean; + }; /** Fired when the phone bottom search dock enters or leaves the scroll-hidden state. */ onBottomComposerScrollHiddenChange?: (hidden: boolean) => void; }) { @@ -288,10 +294,15 @@ export function MasterSearchHeader({ // into invisible controls). const [headerChromeFocused, setHeaderChromeFocused] = useState(false); const [composerChromeFocused, setComposerChromeFocused] = useState(false); - const scrollHidden = useHideOnScroll({ + const internalScrollHidden = useHideOnScroll({ containerRef: hideOnScroll?.containerRef, + scrollContainer: hideOnScroll?.scrollContainer, disabled: !hideOnScroll, }); + const scrollHidden = + hideOnScroll?.scrollHidden !== undefined + ? hideOnScroll.scrollHidden || internalScrollHidden + : internalScrollHidden; const headerChromeHidden = scrollHidden && !modeMenuOpen && !actionMenuOpen && !scopeOpen && !scopeSheetOpen && !headerChromeFocused; const phoneBottomSearchDockActive = @@ -1336,12 +1347,15 @@ export function MasterSearchHeader({