diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index e1c4145c0..95f375323 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -322,10 +322,15 @@ function MatchBadge({ label }: { label: string }) { ); } -function Chip({ children }: { children: string }) { +function Chip({ children, className }: { children: string; className?: string }) { return ( - - {children} + + {children} ); } @@ -451,42 +456,46 @@ function MobileResultCard({ selected: boolean; onToggle?: () => void; }) { - const Icon = result.icon; - return ( -
-
- +
+
+ {index + 1} - - -
-
- - {result.title} - + + {result.title} + +
-
-
+ {result.subtitle ? ( +

+ {result.subtitle} +

+ ) : null}
{onToggle ? : }
-
+
{result.tags.slice(0, 2).map((tag) => ( - {tag} + + {tag} + ))} - {result.tags.length > 2 ? {`+${result.tags.length - 2}`} : null} + {result.tags.length > 2 ? ( + {`+${result.tags.length - 2}`} + ) : null}
); @@ -539,12 +548,12 @@ function BestAnswerCard({

Best answer

-

+

- {best.title} + {best.title}

@@ -863,7 +872,7 @@ function SearchResultsView({ // this results canvas into a nested phone scrollport, stealing scroll from // #main-content. The fixed compare FAB and shell hide-on-scroll both assume // #main-content owns vertical scroll. - className="mx-auto grid w-full max-w-[86rem] min-w-0 gap-3 overflow-x-clip px-3 pb-[calc(10rem+env(safe-area-inset-bottom))] min-[390px]:gap-4 sm:px-4 lg:px-0 lg:pb-0" + className="mx-auto grid w-full max-w-[86rem] min-w-0 gap-3 overflow-x-clip px-4 pb-[calc(12.5rem+env(safe-area-inset-bottom))] min-[390px]:gap-4 sm:px-4 lg:px-0 lg:pb-0" > {/* Query context lives here on every breakpoint — on phones this is the only place the submitted query is visible above the fold. */} @@ -1059,6 +1068,11 @@ function SearchResultsView({ // Best-match styling remains tied to relevance while the row // number follows the user's chosen presentation order. const isBest = result.kind === best.kind && result.id === best.id; + // Phone list hides the best-answer duplicate, so ranks must + // skip that row or the first visible card starts at 2/3. + const mobileIndex = visibleResults + .slice(0, displayIndex) + .filter((candidate) => !(candidate.kind === best.kind && candidate.id === best.id)).length; return ( // The best answer is already featured above the phone list, // so its ranked duplicate only renders from the desktop @@ -1077,7 +1091,7 @@ function SearchResultsView({
toggleSelected(result.id) : undefined} /> diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 93309d929..717cc700e 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1448,6 +1448,36 @@ test.describe("Clinical KB tools launcher", () => { }); expect(badgeMetrics.height).toBeGreaterThanOrEqual(22); expect(badgeMetrics.scrollHeight).toBeLessThanOrEqual(badgeMetrics.height + 1); + + // Phone list hides the featured best answer, so ranks must start at 1. + const mobileCards = page.getByTestId("differential-mobile-result-card"); + await expect(mobileCards.first()).toBeVisible(); + await expect(mobileCards.first().getByTestId("differential-mobile-result-rank")).toHaveText("1"); + const ranks = await mobileCards.getByTestId("differential-mobile-result-rank").allTextContents(); + expect(ranks).toEqual(ranks.map((_, index) => String(index + 1))); + + // Status badge sits on its own meta row below the title, never beside it. + const titleBadgeLayout = await mobileCards.first().evaluate((card) => { + const title = card.querySelector("a span.line-clamp-2") ?? card.querySelector("a"); + const badge = card.querySelector('[data-testid="differential-status-badge"]'); + if (!title || !badge) return null; + const titleRect = title.getBoundingClientRect(); + const badgeRect = badge.getBoundingClientRect(); + return { titleBottom: titleRect.bottom, badgeTop: badgeRect.top }; + }); + expect(titleBadgeLayout).not.toBeNull(); + expect(titleBadgeLayout!.badgeTop).toBeGreaterThanOrEqual(titleBadgeLayout!.titleBottom - 1); + + const cardOverflow = await mobileCards.evaluateAll((cards) => + cards.map((card) => ({ + overflowX: card.scrollWidth > card.clientWidth + 1, + width: card.clientWidth, + })), + ); + for (const card of cardOverflow) { + expect(card.overflowX).toBe(false); + } + await expectNoPageHorizontalOverflow(page); }); @@ -1494,6 +1524,14 @@ test.describe("Clinical KB tools launcher", () => { await expect(compareAction).toContainText("Compare selected"); await expect(dock).not.toHaveAttribute("data-scroll-hidden", "true"); + await mainContent.evaluate((element) => element.scrollTo({ top: element.scrollHeight, behavior: "instant" })); + const lastResultBottom = await page + .getByTestId("differential-mobile-result-card") + .last() + .evaluate((element) => element.getBoundingClientRect().bottom); + const dockTop = await dock.evaluate((element) => element.getBoundingClientRect().top); + expect(lastResultBottom).toBeLessThanOrEqual(dockTop); + // Compare lives in the dock addon slot above the search pill. const revealedGeometry = await compareAction.evaluate((element) => { const rect = element.getBoundingClientRect(); @@ -1514,6 +1552,8 @@ test.describe("Clinical KB tools launcher", () => { expect(revealedGeometry.top).toBeGreaterThanOrEqual(revealedGeometry.dockTop!); expect(revealedGeometry.bottom).toBeLessThanOrEqual(revealedGeometry.dockBottom!); expect(revealedGeometry.receivesPointer).toBe(true); + // Last card must clear the floating compare CTA, not only the composer dock. + expect(lastResultBottom).toBeLessThanOrEqual(revealedGeometry.top); const mainPaddingBottom = await mainContent.evaluate((element) => Number.parseFloat(window.getComputedStyle(element).paddingBottom),