diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 5211d335..446994c5 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -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 }} diff --git a/src/components/clinical-dashboard/mobile-composer-reserve.ts b/src/components/clinical-dashboard/mobile-composer-reserve.ts index 9b794bcd..e4a19c25 100644 --- a/src/components/clinical-dashboard/mobile-composer-reserve.ts +++ b/src/components/clinical-dashboard/mobile-composer-reserve.ts @@ -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; diff --git a/tests/mobile-composer-reserve.test.ts b/tests/mobile-composer-reserve.test.ts index 732f27b3..92afcc60 100644 --- a/tests/mobile-composer-reserve.test.ts +++ b/tests/mobile-composer-reserve.test.ts @@ -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, @@ -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)", () => { diff --git a/tests/ui-phone-scroll.spec.ts b/tests/ui-phone-scroll.spec.ts index c9afad82..49528976 100644 --- a/tests/ui-phone-scroll.spec.ts +++ b/tests/ui-phone-scroll.spec.ts @@ -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", diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 38edf578..469b07ec 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -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 }); @@ -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); @@ -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(); }); } }