Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1800,7 +1800,8 @@ summary::-webkit-details-marker {
padding-bottom: max(0.5rem, var(--safe-area-bottom));
}

.answer-footer-search-dock.document-mobile-search-edge.answer-footer-search-edge.document-mobile-search-compact {
.answer-footer-search-dock.document-mobile-search-edge.answer-footer-search-edge.document-mobile-search-compact,
.answer-footer-search-dock.dashboard-composer-edge.answer-footer-search-edge.document-mobile-search-compact {
padding-bottom: max(0.45rem, var(--safe-area-bottom));
}

Expand Down
30 changes: 15 additions & 15 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3242,10 +3242,6 @@ export function ClinicalDashboard({
const compactMobileModeHome =
centeredModeHome ||
((searchMode === "services" || searchMode === "forms") && !modeSearchSubmitted && !query.trim() && !loading);
// Submitted (non-answer) searches are result views, not mode homes: on phones
// the bottom composer drops its chip row and hugs the screen edge so results
// keep maximum vertical space. Mode homes keep the default chip-row layout.
const compactMobileBottomSearch = hasMobileBottomSearch && modeSearchSubmitted;
const differentialsCompareAddonActive =
searchMode === "differentials" && modeSearchSubmitted && Boolean(query.trim());
// Hidden dock pad must stay at 0.75rem — Safari toolbar safe-area recreates a blank band.
Expand All @@ -3255,7 +3251,6 @@ export function ClinicalDashboard({
searchMode,
hasAnswerFollowUps: answerFollowUpSuggestions.length > 0,
differentialsCompareAddonActive,
compactMobileBottomSearch,
}),
);
const renderDegradedNotice = () => (
Expand Down Expand Up @@ -3527,11 +3522,17 @@ export function ClinicalDashboard({
composerFollowUpSuggestionsDisabled={loading}
composerPlaceholder={searchMode === "answer" && latestAnswerQuery ? "Ask a follow-up..." : undefined}
mobileSearchPlacement={hasMobileBottomSearch ? "bottom" : "default"}
mobileBottomSearchVariant={compactMobileBottomSearch ? "compact" : "default"}
// Every phone dock is the compact single-row pill so content keeps
// maximum screen space (mode homes and result views alike).
mobileBottomSearchVariant="compact"
mobileBottomSearchAddonSlotId={
differentialsCompareAddonActive ? differentialsMobileCompareAddonSlotId : undefined
}
desktopHomeComposerSlotId={desktopHomeComposerSlotId}
// Only the answer home ("How can I help?") keeps the in-flow hero
// pill + privacy notice on phones; every other mode home docks the
// compact pill to the bottom edge below sm.
heroComposerBreakpoint={showAnswerHome ? "all" : "sm-up"}
// Answer view: the header overlays the scrolling <main> at every width
// (main reserves matching top padding) so content frosts under the
// glass bar, and it slides away/returns with scroll direction. Other
Expand Down Expand Up @@ -3575,11 +3576,10 @@ export function ClinicalDashboard({
// bottom-clamp guard in use-hide-on-scroll prevents false reveals.
"max-sm:pb-[var(--mobile-composer-reserve)] sm:mb-24"
: hasMobileBottomSearch
? // Mode homes keep the composer in the hero (in-flow at every
// width), so phones need no bottom-dock clearance on them.
compactMobileModeHome || showDesktopHomeComposer
? "mb-0"
: "max-sm:pb-[var(--mobile-composer-reserve)] sm:mb-0"
? // Phones dock the compact composer on every non-answer view
// (mode homes included), so they always reserve dock
// clearance; sm+ keeps the in-flow hero/sticky composers.
"max-sm:pb-[var(--mobile-composer-reserve)] sm:mb-0"
: "mb-0",
)}
>
Expand Down Expand Up @@ -3635,11 +3635,11 @@ export function ClinicalDashboard({
// keep the original generous padding.
"pb-4 sm:pb-36 lg:pb-40"
: hasMobileBottomSearch
? compactMobileModeHome
? // The <main> reserve clears the compact dock on phones, so
// content keeps only a small pad of its own.
compactMobileModeHome
? "pb-4 sm:pb-10 lg:pb-12"
: compactMobileBottomSearch || showDesktopHomeComposer
? "pb-8 sm:pb-10 lg:pb-12"
: "pb-32 sm:pb-10 lg:pb-12"
: "pb-8 sm:pb-10 lg:pb-12"
: "pb-8 sm:pb-10 lg:pb-12",
)}
>
Expand Down
15 changes: 8 additions & 7 deletions src/components/clinical-dashboard/global-search-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,8 @@ function GlobalStandaloneSearchShellClient({
!isDifferentialPresentationWorkflow &&
(!isInfoPage || isToolDetailWithFooterSearch(pathname));
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. Document viewer
// Standalone mode homes keep the hero composer from sm up but dock the
// compact pill on phones, so they reserve dock clearance too. Document viewer
// routes own their own floating composer, so the shell keeps only a small pad
// and lets DocumentViewer manage visible-dock clearance.
// Release the large bottom reserve only when the phone bottom composer is
Expand All @@ -365,7 +365,6 @@ function GlobalStandaloneSearchShellClient({
isStandaloneModeHome,
searchMode,
differentialsCompareAddonActive,
useCompactBottomSearch,
}),
);

Expand Down Expand Up @@ -714,16 +713,18 @@ function GlobalStandaloneSearchShellClient({
onCrossModeSearch={crossModeSearch}
headerVariant={isDifferentialPresentationWorkflow ? "workflow" : "default"}
mobileSearchPlacement="bottom"
// Submitted searches that stay in the shell (services results) are
// result views: compact the phone bottom composer so results keep
// maximum screen space. Mode homes keep the chip-row layout.
mobileBottomSearchVariant={useCompactBottomSearch ? "compact" : "default"}
// Every phone dock is the compact single-row pill so content keeps
// maximum screen space (mode homes and result views alike).
mobileBottomSearchVariant="compact"
mobileBottomSearchAddonSlotId={
differentialsCompareAddonActive ? differentialsMobileCompareAddonSlotId : undefined
}
desktopSearchPlacement={desktopSearchPlacement === "hero" && isStandaloneModeHome ? "hero" : "default"}
searchComposerVisible={shouldShowSearchComposer}
desktopHomeComposerSlotId={isStandaloneModeHome ? modeHomeDesktopComposerSlotId : undefined}
// Standalone mode homes keep the hero pill from sm up; phones get
// the compact bottom dock (only the answer home hero owns phones).
heroComposerBreakpoint="sm-up"
// 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 }}
Expand Down
79 changes: 49 additions & 30 deletions src/components/clinical-dashboard/master-search-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const phoneSearchLayoutMediaQuery = "(max-width: 639px)";
const scopeSheetMediaQuery = "(max-width: 1023px)";
const desktopHomeComposerMediaQuery = "(min-width: 1024px)";
const modeHomeComposerMediaQuery = "(min-width: 0px)";
const modeHomeComposerSmUpMediaQuery = "(min-width: 640px)";

function splitFilterText(value: string) {
return value
Expand Down Expand Up @@ -180,6 +181,7 @@ export function MasterSearchHeader({
desktopSearchPlacement = "default",
searchComposerVisible = true,
desktopHomeComposerSlotId,
heroComposerBreakpoint = "all",
mobileBottomSearchAddonSlotId,
mobileLeadingAction = "menu",
onMobileBack,
Expand Down Expand Up @@ -229,16 +231,20 @@ export function MasterSearchHeader({
composerFollowUpSuggestionsDisabled?: boolean;
headerVariant?: "default" | "workflow";
mobileSearchPlacement?: "default" | "bottom";
/** "compact" drops the phone footer chip row and hugs the bottom edge
* used by search/result views so results keep maximum screen space.
* Mode homes keep the default chip-row layout. */
/** "compact" drops the phone footer chip row and hugs the bottom edge so
* content keeps maximum screen space. Every phone dock uses it now; the
* "default" value remains for hosts that need the taller legacy dock. */
mobileBottomSearchVariant?: "default" | "compact";
desktopSearchPlacement?: "default" | "hero";
searchComposerVisible?: boolean;
/** Mode-home slot the composer portals into at every viewport width, so the
* search pill sits in the middle of the hero on phones as well as desktop
* instead of docking to the bottom edge. */
/** Mode-home slot the composer portals into so the search pill sits in the
* middle of the hero instead of docking to the bottom edge. Which widths the
* hero owns is controlled by `heroComposerBreakpoint`. */
desktopHomeComposerSlotId?: string;
/** Widths where the mode-home hero slot hosts the composer. "all" keeps the
* hero pill on phones too (the answer home); "sm-up" reserves the hero for
* sm+ widths and hands phones the compact bottom dock instead. */
heroComposerBreakpoint?: "all" | "sm-up";
/** Mobile/tablet slot rendered above the search pill for page-specific composer addons. */
mobileBottomSearchAddonSlotId?: string;
mobileLeadingAction?: "menu" | "back";
Expand Down Expand Up @@ -343,12 +349,14 @@ export function MasterSearchHeader({
const scrollHidden = hideOnScroll?.scrollHidden !== undefined ? hideOnScroll.scrollHidden : internalScrollHidden;
const headerChromeHidden =
scrollHidden && !modeMenuOpen && !actionMenuOpen && !scopeOpen && !scopeSheetOpen && !headerChromeFocused;
// Mode homes portal the composer into the hero slot at every width, so the
// phone bottom dock only exists when no hero slot is provided.
// Mode homes portal the composer into the hero slot. With "all" the hero owns
// every width (the answer home keeps its in-flow pill on phones); "sm-up"
// hero hosts hand phones the bottom dock instead.
const heroComposerOwnsPhones = Boolean(desktopHomeComposerSlotId) && heroComposerBreakpoint === "all";
const phoneBottomSearchDockActive =
usesPhoneSearchLayout &&
searchComposerVisible &&
!desktopHomeComposerSlotId &&
!heroComposerOwnsPhones &&
(isAnswerFooterComposer || mobileSearchPlacement === "bottom");
// Compare addon chrome lives inside the phone dock; hide/reveal with it so
// the search pill and Compare selected bar reclaim space together.
Expand Down Expand Up @@ -942,15 +950,20 @@ export function MasterSearchHeader({
// into it made React reconcile the portal against a container that another
// part of the tree had already removed, throwing a null-parentNode error.
// Because the host is stable, React's portal container never disappears.
// The slot is used at every viewport width — phones included — so mode
// homes keep the composer in the middle of the hero instead of docking it
// to the bottom edge.
// heroComposerBreakpoint decides which widths use the slot: "all" keeps the
// composer in the middle of the hero on phones too (the answer home), while
// "sm-up" leaves phones to the compact bottom dock and reserves the hero
// for sm+ widths.
const host = document.createElement("div");
// Layout-transparent so the composer lays out as a direct child of the slot.
host.style.display = "contents";

const mediaQuery = window.matchMedia(
desktopHomeComposerSlotId ? modeHomeComposerMediaQuery : desktopHomeComposerMediaQuery,
desktopHomeComposerSlotId
? heroComposerBreakpoint === "sm-up"
? modeHomeComposerSmUpMediaQuery
: modeHomeComposerMediaQuery
: desktopHomeComposerMediaQuery,
);

let retryTimeout: number | null = null;
Expand Down Expand Up @@ -1002,7 +1015,7 @@ export function MasterSearchHeader({
setDesktopHomeComposerFallback(false);
setDesktopHomeComposerHost(null);
};
}, [desktopHomeComposerSlotId]);
}, [desktopHomeComposerSlotId, heroComposerBreakpoint]);

const dismissModeMenu = useCallback(() => setModeMenuOpen(false), []);
function dismissScope(reason: "outside" | "escape") {
Expand Down Expand Up @@ -1320,7 +1333,6 @@ export function MasterSearchHeader({
const isDesktopHomeComposer = placement === "desktop-home";
const usesAnswerFooterStyle = isAnswerFooterComposer && !isDesktopHomeComposer;
const usesMobileBottomStyle = isMobileBottomComposer && !isDesktopHomeComposer;
const usesCompactMobileBottomStyle = usesMobileBottomStyle && mobileBottomSearchVariant === "compact";
const usesBottomComposerPlacement = usesAnswerFooterStyle || (usesMobileBottomStyle && usesPhoneSearchLayout);
// Sticky-top result composers (tablet+) share the footer chip layout so the
// pill + chip row looks identical across homes, results, and the answer dock.
Expand All @@ -1332,16 +1344,26 @@ export function MasterSearchHeader({
const ModeIdentityIcon = appModeIcons[searchMode];
const hasScopeFooterChip = searchMode === "answer" || searchMode === "documents" || searchMode === "forms";
const usesPhoneFooterDock = usesBottomComposerPlacement && usesPhoneSearchLayout;
const showsAnswerFollowUpRow = Boolean(
usesPhoneFooterDock &&
searchMode === "answer" &&
composerFollowUpSuggestions?.length &&
onPickComposerFollowUpSuggestion,
);
// Every phone dock is the compact single-row pill; the answer dock only
// keeps the taller default treatment while its follow-up chip row renders
// above the pill (the compact scrim would be too short for it).
const usesCompactMobileBottomStyle =
(usesMobileBottomStyle && mobileBottomSearchVariant === "compact") ||
(usesAnswerFooterStyle && usesPhoneFooterDock && !showsAnswerFollowUpRow);
// Differentials compare addon is dock chrome (search pill + Compare bar).
// Hide/reveal the whole dock together; do not pin for the addon slot.
const shouldHideBottomOnScroll = Boolean(hideOnScroll && usesPhoneFooterDock);
// Phone submitted non-answer result docks reserve pill-only scroll
// clearance (ClinicalDashboard / global-search-shell <main> padding via
// mobileComposerReserve), so an extra notice line would push the fixed
// dock over the last result. Those flows already showed the notice on
// their entry composer; answer docks keep it (their reserves were sized
// for the old taller notice-above-pill stack).
const showsComposerPrivacyNotice = searchMode === "answer" || !usesPhoneFooterDock;
// Phones show the APP-5 notice only on the home hero (the answer mode
// home's in-flow composer); every phone bottom dock is a compact
// result/entry pill without it, so content keeps maximum screen space.
// Tablet/desktop composers keep the site-wide notice everywhere.
const showsComposerPrivacyNotice = usesPhoneSearchLayout ? isDesktopHomeComposer : true;

const commandSurfacePlacement = usesBottomComposerPlacement ? "bottom-dock" : "inline";

Expand Down Expand Up @@ -1394,10 +1416,7 @@ export function MasterSearchHeader({
className="differentials-mobile-search-addon relative z-10 w-full empty:hidden"
/>
) : null}
{usesPhoneFooterDock &&
searchMode === "answer" &&
composerFollowUpSuggestions?.length &&
onPickComposerFollowUpSuggestion ? (
{showsAnswerFollowUpRow && composerFollowUpSuggestions?.length && onPickComposerFollowUpSuggestion ? (
<AnswerFollowUpSuggestions
suggestions={composerFollowUpSuggestions}
onPick={onPickComposerFollowUpSuggestion}
Expand Down Expand Up @@ -1548,10 +1567,10 @@ export function MasterSearchHeader({
</button>
</div>
</UniversalSearchCommandSurface>
{/* Single site-wide APP-5 privacy line: every composer variant (home
hero, answer dock, sticky search) renders exactly one compact
notice below the pill; no other surface may duplicate it. Phone
non-answer result docks skip it — see showsComposerPrivacyNotice. */}
{/* Single site-wide APP-5 privacy line: every tablet/desktop composer
variant renders exactly one compact notice below the pill; no other
surface may duplicate it. Phones show it only on the home hero —
see showsComposerPrivacyNotice. */}
{showsComposerPrivacyNotice ? (
<PrivacyInputNotice
id={composerPrivacyWarningId}
Expand Down
Loading