Skip to content
Merged
8 changes: 5 additions & 3 deletions src/components/clinical-dashboard/global-search-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -726,9 +726,11 @@ function GlobalStandaloneSearchShellClient({
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"
// Standalone mode homes keep the in-flow hero pill at every width,
// phones included — the composer sits in the middle of the hero and
// scrolls with the content, matching the answer home rather than
// docking to the bottom edge.
heroComposerBreakpoint="all"
// 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
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ export function resolveShellVisibleMobileComposerReserve(input: {
// DocumentViewer owns its dock; shell keeps only the hidden-size pad.
return input.documentViewerOwnedRoute ? mobileComposerHiddenReserve : mobileComposerIdleReserve;
}
// Standalone mode homes keep the hero composer from sm up but dock the
// compact pill on phones, so they reserve the same clearance as result docks.
if (input.isStandaloneModeHome) return mobileComposerVisibleReserve.shellDock;
// Standalone mode homes keep the in-flow hero pill at every width (phones
// included), so the composer sits in the content flow rather than docking to
// the bottom edge. Reserve only the idle content pad — matching the answer
// home hero — so no empty band opens below the pill.
if (input.isStandaloneModeHome) return mobileComposerIdleReserve;
if (input.searchMode === "answer") return mobileComposerVisibleReserve.shellAnswer;
if (input.differentialsCompareAddonActive) return mobileComposerVisibleReserve.differentialsCompare;
return mobileComposerVisibleReserve.shellDock;
Expand Down
4 changes: 2 additions & 2 deletions tests/mobile-composer-reserve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe("mobile composer reserve contract", () => {
).toBe(mobileComposerHiddenReserve);
});

it("reserves compact dock clearance on standalone mode homes (phone dock, hero from sm up)", () => {
it("keeps only the idle content pad on standalone mode homes (in-flow hero pill, no dock)", () => {
expect(
resolveShellVisibleMobileComposerReserve({
shouldShowSearchComposer: true,
Expand All @@ -50,7 +50,7 @@ describe("mobile composer reserve contract", () => {
searchMode: "services",
differentialsCompareAddonActive: false,
}),
).toBe(mobileComposerVisibleReserve.shellDock);
).toBe(mobileComposerIdleReserve);
});

it("uses the compact dock reserve for every non-answer dashboard dock (mode homes included)", () => {
Expand Down
7 changes: 5 additions & 2 deletions tests/ui-phone-scroll.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ import { expect, test, type Page } from "playwright/test";
* page.emulateMedia({ reducedMotion: "no-preference" }).
*/

// Standalone mode homes share GlobalSearchShell's phone scroller and compact
// bottom dock (list mirrors global-search-shell.tsx isStandaloneModeHome).
// Standalone mode homes share GlobalSearchShell's phone scroller. They keep the
// in-flow hero pill on phones (the composer sits in the hero and scrolls with the
// content — no bottom dock), while the sticky header still collapses on scroll;
// this sweep guards that the scroll geometry stays stable through that collapse.
// (list mirrors global-search-shell.tsx isStandaloneModeHome).
const modeHomeRoutes = [
"/formulation",
"/dsm",
Expand Down
49 changes: 23 additions & 26 deletions tests/ui-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ test.describe("Clinical KB tools launcher", () => {
await expectNoPageHorizontalOverflow(page);
});

test("phone mode homes dock the compact shared search at the bottom edge", async ({ page }) => {
test("phone mode homes keep the in-flow hero pill, matching the answer home", async ({ page }) => {
await mockAnswerDashboardApi(page);
await page.setViewportSize({ width: 390, height: 820 });

Expand All @@ -614,27 +614,29 @@ test.describe("Clinical KB tools launcher", () => {
await expect(page.getByTestId(home.testId)).toBeVisible();
await expect(visibleGlobalSearchInput(page)).toHaveCount(1, { timeout: 15_000 });

// Phones dock the composer as the compact single-row pill; the hero slot
// stays empty below sm (it hosts the pill again from the sm breakpoint).
const dock = page.locator('form.answer-footer-search-dock[data-footer-variant="compact"]');
await expect(dock, home.path).toHaveCount(1);
await expect(dock.getByTestId("global-search-input")).toBeVisible();
await expect(page.locator(".mode-home-composer-slot").getByTestId("global-search-input")).toHaveCount(0);
// The composer sits in the middle of the hero (in-flow) at phone width too,
// not docked to the bottom edge: it renders inside the mode-home composer
// slot and there is no fixed bottom dock.
await expect(page.locator(".mode-home-composer-slot").getByTestId("global-search-input"), home.path).toHaveCount(
1,
);
await expect(page.locator("form.answer-footer-search-dock"), home.path).toHaveCount(0);

const metrics = await globalSearchComposerMetrics(page);
const metrics = await globalSearchComposerMetrics(page, home.testId);
expect(metrics, home.path).not.toBeNull();
expect(metrics?.position).toBe("fixed");
expect(metrics?.formBottom ?? 0).toBeGreaterThanOrEqual(820 - 48);
// In-flow (not viewport-fixed), scrolls with the content, and stays within
// the phone column. The pill class is shared with the dock, so it still
// reads answer-footer-search-pill.
expect(metrics?.position, home.path).not.toBe("fixed");
expect(metrics?.formWidth ?? 0).toBeLessThanOrEqual(390);
expect(metrics?.pillClassName).toContain("answer-footer-search-pill");
// Compact docks drop the chip row and the privacy notice on phones.
await expect(page.locator(".answer-footer-search-chip:visible")).toHaveCount(0);
await expect(page.getByTestId("answer-composer-privacy-warning")).toHaveCount(0);

// Phone dock composers must not cover the page with the universal sheet.
const dockInput = dock.getByTestId("global-search-input");
await dockInput.click();
await dockInput.press("ArrowDown");
// The APP-5 privacy notice rides the hero pill on phones too (as on desktop).
await expect(page.getByTestId("answer-composer-privacy-warning"), home.path).toBeVisible();

// The in-flow composer must not cover the page with the universal sheet.
const heroInput = page.locator(".mode-home-composer-slot").getByTestId("global-search-input");
await heroInput.click();
await heroInput.press("ArrowDown");
await expect(page.locator(".universal-command-dropdown:visible")).toHaveCount(0);
await expect(page.getByRole("listbox")).toHaveCount(0);
await expectNoPageHorizontalOverflow(page);
Expand Down Expand Up @@ -664,14 +666,9 @@ test.describe("Clinical KB tools launcher", () => {
await expect(page.getByTestId(home.testId)).toBeVisible();
// The composer must never vanish: exactly one visible search input.
await expect(visibleGlobalSearchInput(page)).toHaveCount(1, { timeout: 15_000 });
if (viewport.name === "phone" && home.path !== "/?mode=answer") {
// Phones dock the standalone mode-home composer at the bottom edge.
await expect(page.locator("form.answer-footer-search-dock").getByTestId("global-search-input")).toBeVisible();
} else {
// The hero owns the composer: the answer home at every width, and
// every mode home from the sm breakpoint up.
await expect(page.getByTestId(home.testId).getByTestId("global-search-input")).toBeVisible();
}
// The hero owns the composer at every width: the answer home and every
// standalone mode home keep the in-flow hero pill, phones included.
await expect(page.getByTestId(home.testId).getByTestId("global-search-input")).toBeVisible();
});
}
}
Expand Down