diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index 018b94cf5..3b888af2a 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -125,10 +125,10 @@ function routeWithQuery(path: string, query: string, selectedIds?: Set) } /** - * Phone-only floating compare action. Portals into the bottom search dock's - * addon slot so it stays anchored just above the composer pill (and hides - * with it on scroll), but renders as a self-contained floating pill so it - * reads as a batch-selection action rather than composer chrome. + * Mobile/tablet floating compare action. Portals into the search composer's + * addon slot so it stays anchored beside the active result controls, but + * renders as a self-contained floating pill so it reads as a batch-selection + * action rather than composer chrome. */ function DifferentialsMobileCompareBar({ selectedCount, diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 9be51e3af..a08259790 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -45,7 +45,7 @@ import { } from "@/lib/app-modes"; import { isLocalNoAuthMode } from "@/lib/client-env"; import { documentsSearchHref } from "@/lib/document-flow-routes"; -import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; +import { differentialsMobileCompareAddonSlotId, modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; import { readSearchNavigationContext, type SearchNavigationOptions } from "@/lib/search-navigation-context"; import type { SearchScopeFilters } from "@/lib/search-scope"; import { useAuthSession } from "@/lib/supabase/client"; @@ -241,6 +241,8 @@ function GlobalStandaloneSearchShellClient({ const isDocumentSearchMockupRoute = pathname.startsWith("/mockups/document-search"); const isDocumentCommandSearchView = pathname === "/documents/search" && requestedQuery.length > 0; const useCompactBottomSearch = hasSubmittedModeSearch || isDocumentCommandSearchView; + const differentialsCompareAddonActive = + pathname === "/differentials" && searchMode === "differentials" && hasSubmittedModeSearch; // Registry and local decision-support modes own their submitted-search views on their // standalone routes; the shell must not swap them to the dashboard. On the // home route the dashboard always renders, so these exclusions only apply @@ -283,9 +285,11 @@ function GlobalStandaloneSearchShellClient({ ? "2rem" : searchMode === "answer" ? "calc(9rem + env(safe-area-inset-bottom))" - : useCompactBottomSearch - ? "calc(5.5rem + env(safe-area-inset-bottom))" - : "calc(9rem + env(safe-area-inset-bottom))"; + : differentialsCompareAddonActive + ? "calc(8.75rem + env(safe-area-inset-bottom))" + : useCompactBottomSearch + ? "calc(5.5rem + env(safe-area-inset-bottom))" + : "calc(9rem + env(safe-area-inset-bottom))"; useEffect(() => { // Re-derive the mode and query from the URL, but only when the search string @@ -551,6 +555,9 @@ function GlobalStandaloneSearchShellClient({ // result views: compact the phone bottom composer so results keep // maximum screen space. Mode homes keep the chip-row layout. mobileBottomSearchVariant={useCompactBottomSearch ? "compact" : "default"} + mobileBottomSearchAddonSlotId={ + differentialsCompareAddonActive ? differentialsMobileCompareAddonSlotId : undefined + } desktopSearchPlacement={desktopSearchPlacement === "hero" && isStandaloneModeHome ? "hero" : "default"} searchComposerVisible={shouldShowSearchComposer} desktopHomeComposerSlotId={isStandaloneModeHome ? modeHomeDesktopComposerSlotId : undefined} diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 8f9537929..8da025690 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -235,7 +235,7 @@ export function MasterSearchHeader({ * search pill sits in the middle of the hero on phones as well as desktop * instead of docking to the bottom edge. */ desktopHomeComposerSlotId?: string; - /** Phone-only slot rendered above the bottom search pill for page-specific dock addons. */ + /** Mobile/tablet slot rendered above the search pill for page-specific composer addons. */ mobileBottomSearchAddonSlotId?: string; mobileLeadingAction?: "menu" | "back"; onMobileBack?: () => void; @@ -321,7 +321,9 @@ export function MasterSearchHeader({ searchComposerVisible && !desktopHomeComposerSlotId && (isAnswerFooterComposer || mobileSearchPlacement === "bottom"); - const bottomComposerScrollHiddenActive = Boolean(hideOnScroll && phoneBottomSearchDockActive); + const bottomComposerScrollHiddenActive = Boolean( + hideOnScroll && phoneBottomSearchDockActive && !mobileBottomSearchAddonSlotId, + ); const bottomComposerHidden = bottomComposerScrollHiddenActive && scrollHidden && @@ -1188,7 +1190,11 @@ export function MasterSearchHeader({ const ModeIdentityIcon = appModeIcons[searchMode]; const hasScopeFooterChip = searchMode === "answer" || searchMode === "documents" || searchMode === "forms"; const usesPhoneFooterDock = usesBottomComposerPlacement && usesPhoneSearchLayout; - const shouldHideBottomOnScroll = Boolean(hideOnScroll && usesPhoneFooterDock); + // A differential comparison is a persistent batch action: hiding its host + // dock on downward scroll makes the CTA slide under mobile browser chrome + // and disables pointer events just when users finish reviewing the list. + // Keep that dock pinned while the header can still collapse independently. + const shouldHideBottomOnScroll = Boolean(hideOnScroll && usesPhoneFooterDock && !mobileBottomSearchAddonSlotId); // Phone submitted non-answer result docks reserve pill-only scroll // clearance (ClinicalDashboard
margins / global-search-shell // mobileComposerReserve), so an extra notice line would push the fixed @@ -1237,7 +1243,7 @@ export function MasterSearchHeader({ )} > {usesBottomComposerPlacement ?