From eeec2d9a06a2d1219bb0837cadc9c3bd596bbdde Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 19 Jul 2026 02:49:06 +0800 Subject: [PATCH 1/3] fix(ui): release mobile chrome scroll reserve --- .../clinical-dashboard/global-search-shell.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 331812ca..81294d04 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -260,7 +260,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 +271,15 @@ function GlobalStandaloneSearchShellClient({ : useCompactBottomSearch ? "calc(5.5rem + env(safe-area-inset-bottom))" : "calc(9rem + env(safe-area-inset-bottom))"; + // When phone chrome hides on downward scroll, release the large bottom + // composer reserve as well. Otherwise long content reaches the end of the + // internal scrollport while a toolbar-sized blank band remains between the + // page and Safari's collapsed address bar. Keep only a small safe-area + // breathing room so the site content, not the background, fills the revealed + // edge-to-edge viewport. + const mobileComposerReserve = phoneScrollHide.hidden + ? "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 From 294f02972d88009a668e0d6d6c78dba82f65393c Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:11:47 +0000 Subject: [PATCH 2/3] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- .../clinical-dashboard/global-search-shell.tsx | 13 ++++++++----- .../clinical-dashboard/master-search-header.tsx | 8 ++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 81294d04..f0cdde36 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -164,6 +164,7 @@ function GlobalStandaloneSearchShellClient({ useEffect(() => { reportPhoneScrollHideRef.current = phoneScrollHide.reportScroll; }, [phoneScrollHide.reportScroll]); + const [bottomComposerHidden, setBottomComposerHidden] = useState(false); const visibleShellModes = useMemo(() => { const modes = visibleAppModeDefinitions(); if (!availableModeIds?.length) return modes; @@ -271,13 +272,14 @@ function GlobalStandaloneSearchShellClient({ : useCompactBottomSearch ? "calc(5.5rem + env(safe-area-inset-bottom))" : "calc(9rem + env(safe-area-inset-bottom))"; - // When phone chrome hides on downward scroll, release the large bottom - // composer reserve as well. Otherwise long content reaches the end of the - // internal scrollport while a toolbar-sized blank band remains between the - // page and Safari's collapsed address bar. Keep only a small safe-area + // When the bottom composer is actually hidden (accounting for scroll-hidden + // state plus all menu/focus conditions that keep it visible), release the + // large bottom composer reserve. Otherwise long content reaches the end of + // the internal scrollport while a toolbar-sized blank band remains between + // the page and Safari's collapsed address bar. Keep only a small safe-area // breathing room so the site content, not the background, fills the revealed // edge-to-edge viewport. - const mobileComposerReserve = phoneScrollHide.hidden + const mobileComposerReserve = bottomComposerHidden ? "max(0.75rem, env(safe-area-inset-bottom))" : visibleMobileComposerReserve; @@ -586,6 +588,7 @@ function GlobalStandaloneSearchShellClient({ // collapses the header/composer to hand space back to content. hideOnScroll={{ strategy: "collapse", scrollHidden: phoneScrollHide.hidden }} queryInputAutoFocus={searchParams.get("focus") === "1"} + onBottomComposerHiddenChange={setBottomComposerHidden} /> diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index cd0c1d6a..0d80d117 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -184,6 +184,7 @@ export function MasterSearchHeader({ mobileLeadingAction = "menu", onMobileBack, hideOnScroll, + onBottomComposerHiddenChange, }: { demoMode: boolean; documents: ClinicalDocument[]; @@ -258,6 +259,9 @@ export function MasterSearchHeader({ /** Parent-owned hidden state for hosts that report scroll via React `onScroll`. */ scrollHidden?: boolean; }; + /** Callback invoked when the bottom composer visibility changes (accounting for + * scroll-hidden state plus all the menu/focus conditions that keep it visible). */ + onBottomComposerHiddenChange?: (hidden: boolean) => void; }) { const visibleAppModeOptions = defaultVisibleAppModeOptions; const trimmedQuery = query.trim(); @@ -333,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)); From d0ead491dee56de62e1072b80822d21ea51083c8 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 19 Jul 2026 03:14:27 +0800 Subject: [PATCH 3/3] fix(ui): gate mobile reserve on actual bottom composer hide Only shrink --mobile-composer-reserve when MasterSearchHeader reports the bottom dock is hidden, so pinned compare addons and open menus keep content clearance. Co-authored-by: Cursor --- .../clinical-dashboard/global-search-shell.tsx | 16 +++++++++------- .../clinical-dashboard/master-search-header.tsx | 7 +++++++ tests/clinical-dashboard-merge-artifacts.test.ts | 4 +++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 81294d04..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]); @@ -271,13 +272,13 @@ function GlobalStandaloneSearchShellClient({ : useCompactBottomSearch ? "calc(5.5rem + env(safe-area-inset-bottom))" : "calc(9rem + env(safe-area-inset-bottom))"; - // When phone chrome hides on downward scroll, release the large bottom - // composer reserve as well. Otherwise long content reaches the end of the - // internal scrollport while a toolbar-sized blank band remains between the - // page and Safari's collapsed address bar. Keep only a small safe-area - // breathing room so the site content, not the background, fills the revealed - // edge-to-edge viewport. - const mobileComposerReserve = phoneScrollHide.hidden + // 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; @@ -585,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 cd0c1d6a..8ca2f6b7 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -184,6 +184,7 @@ export function MasterSearchHeader({ mobileLeadingAction = "menu", onMobileBack, hideOnScroll, + onBottomComposerHiddenChange, }: { demoMode: boolean; documents: ClinicalDocument[]; @@ -258,6 +259,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(); @@ -333,6 +336,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", () => {