diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 331812ca..a8524a85 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -161,6 +161,7 @@ function GlobalStandaloneSearchShellClient({ const [mainElement, setMainElement] = useState(null); const phoneScrollHide = useScrollHideReporter(); const reportPhoneScrollHideRef = useRef(phoneScrollHide.reportScroll); + const [bottomComposerHidden, setBottomComposerHidden] = useState(false); useEffect(() => { reportPhoneScrollHideRef.current = phoneScrollHide.reportScroll; }, [phoneScrollHide.reportScroll]); @@ -260,7 +261,7 @@ function GlobalStandaloneSearchShellClient({ const reservesFloatingComposer = shouldShowSearchComposer && !isStandaloneModeHome; // Standalone mode homes portal the composer into the hero (in-flow at every // width), so phones need no bottom-dock clearance there. - const mobileComposerReserve = !shouldShowSearchComposer + const visibleMobileComposerReserve = !shouldShowSearchComposer ? "2rem" : isStandaloneModeHome ? "2rem" @@ -271,6 +272,15 @@ function GlobalStandaloneSearchShellClient({ : useCompactBottomSearch ? "calc(5.5rem + env(safe-area-inset-bottom))" : "calc(9rem + env(safe-area-inset-bottom))"; + // Release the large bottom reserve only when the phone bottom composer is + // actually hidden (MasterSearchHeader's bottomComposerHidden). Header-only + // scroll-hide, pinned compare addons, open menus/sheets, and composer focus + // keep the full reserve so content does not slide under a still-visible dock. + // When the dock does hide, keep only a small safe-area breathing room so + // content — not the background — fills the revealed edge-to-edge viewport. + const mobileComposerReserve = bottomComposerHidden + ? "max(0.75rem, env(safe-area-inset-bottom))" + : visibleMobileComposerReserve; useEffect(() => { // Re-derive the mode and query from the URL, but only when the search string @@ -576,6 +586,7 @@ function GlobalStandaloneSearchShellClient({ // Phone-only: #main-content owns vertical scroll, so hide-on-scroll // collapses the header/composer to hand space back to content. hideOnScroll={{ strategy: "collapse", scrollHidden: phoneScrollHide.hidden }} + onBottomComposerHiddenChange={setBottomComposerHidden} queryInputAutoFocus={searchParams.get("focus") === "1"} /> diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 48fc07de..7837d9c3 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -185,6 +185,7 @@ export function MasterSearchHeader({ mobileLeadingAction = "menu", onMobileBack, hideOnScroll, + onBottomComposerHiddenChange, }: { demoMode: boolean; documents: ClinicalDocument[]; @@ -259,6 +260,8 @@ export function MasterSearchHeader({ /** Parent-owned hidden state for hosts that report scroll via React `onScroll`. */ scrollHidden?: boolean; }; + /** Notify hosts when the phone bottom composer is actually hidden (not merely scrolled). */ + onBottomComposerHiddenChange?: (hidden: boolean) => void; }) { const visibleAppModeOptions = defaultVisibleAppModeOptions; const trimmedQuery = query.trim(); @@ -334,6 +337,10 @@ export function MasterSearchHeader({ !scopeSheetOpen && !composerChromeFocused; + useEffect(() => { + onBottomComposerHiddenChange?.(bottomComposerHidden); + }, [bottomComposerHidden, onBottomComposerHiddenChange]); + useEffect(() => { if (!loading || !commandDropdownOpen) return undefined; const frame = window.requestAnimationFrame(() => setCommandDropdownOpen(false)); diff --git a/tests/clinical-dashboard-merge-artifacts.test.ts b/tests/clinical-dashboard-merge-artifacts.test.ts index 7e4a2aca..90cf1df2 100644 --- a/tests/clinical-dashboard-merge-artifacts.test.ts +++ b/tests/clinical-dashboard-merge-artifacts.test.ts @@ -87,8 +87,10 @@ describe("ClinicalDashboard merge-artifact guards", () => { }); it("reserves phone space for the fixed mode-home composer", () => { - expect(globalSearchShellSource).toContain("const mobileComposerReserve = !shouldShowSearchComposer"); + expect(globalSearchShellSource).toContain("const visibleMobileComposerReserve = !shouldShowSearchComposer"); + expect(globalSearchShellSource).toContain("const mobileComposerReserve = bottomComposerHidden"); expect(globalSearchShellSource).not.toContain("const mobileComposerReserve = !reservesFloatingComposer"); + expect(globalSearchShellSource).not.toContain("const mobileComposerReserve = phoneScrollHide.hidden"); }); it("does not reintroduce the obsolete output-mode copy helper", () => {