From 6df0f6c57326d8a306f7ce2f23ed86690bf792f1 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:25:59 +0800 Subject: [PATCH 1/3] fix: polish mobile differentials search --- src/app/globals.css | 57 +++++++++++++++++++ .../clinical-dashboard/differentials-home.tsx | 18 +++--- tests/ui-tools.spec.ts | 6 +- 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index fb181241..79a68363 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -769,6 +769,63 @@ summary::-webkit-details-marker { --footer-scrim-height: max(7rem, calc(var(--safe-area-bottom) + 5.5rem)); } +.differentials-mobile-compare-fab { + pointer-events: none; + position: fixed; + inset-inline: max(1rem, var(--safe-area-left)) max(1rem, var(--safe-area-right)); + bottom: max(5.35rem, calc(var(--safe-area-bottom) + 5.05rem)); + z-index: 50; + display: flex; + justify-content: center; +} + +.differentials-mobile-compare-fab__button { + pointer-events: auto; + display: inline-flex; + min-height: 3.25rem; + max-width: min(100%, 36rem); + align-items: center; + gap: 0.625rem; + border-radius: 9999px; + border: 1px solid color-mix(in srgb, var(--clinical-accent), white 14%); + background: + radial-gradient( + circle at 82% 0%, + color-mix(in srgb, var(--clinical-accent-contrast) 20%, transparent), + transparent 34% + ), + linear-gradient(135deg, color-mix(in srgb, var(--clinical-accent), white 7%), var(--clinical-accent)); + padding: 0.375rem 0.625rem 0.375rem 1.25rem; + color: var(--clinical-accent-contrast); + font-size: 0.95rem; + font-weight: 900; + letter-spacing: -0.01em; + box-shadow: + 0 18px 42px rgb(0 0 0 / 24%), + 0 0 0 1px color-mix(in srgb, var(--clinical-accent-contrast) 10%, transparent) inset; + touch-action: manipulation; + transition: + transform 160ms ease, + background-color 160ms ease, + box-shadow 160ms ease; +} + +.differentials-mobile-compare-fab__button:active { + transform: translateY(1px) scale(0.99); +} + +.differentials-mobile-compare-fab__button:focus-visible { + outline: 2px solid var(--focus); + outline-offset: 3px; +} + +.differentials-mobile-compare-fab__button--empty { + border-color: var(--border-strong); + background: color-mix(in srgb, var(--surface) 92%, transparent); + color: var(--text-muted); + font-weight: 800; +} + /* Bottom-docked composer: hint row + upward dropdown grow above the pill. */ .answer-footer-search-edge { overflow: visible; diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index dcc9caa0..d3186f0f 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -159,12 +159,12 @@ function DifferentialsMobileCompareBar({ const hasSelection = selectedCount > 0; return createPortal( -
+
{hasSelection ? ( Compare selected @@ -178,7 +178,7 @@ function DifferentialsMobileCompareBar({ ) : (

Tick results to compare @@ -801,7 +801,7 @@ function SearchResultsView({ // for comparison and drop stale ids whenever a new query changes the results // (render-time sync, matching the repo's set-state-in-render pattern). const resultSignature = results.map((result) => result.id).join("|"); - const [lastResultSignature, setLastResultSignature] = useState(resultSignature); + const [lastResultSignature, setLastResultSignature] = useState(""); if (lastResultSignature !== resultSignature) { setLastResultSignature(resultSignature); setKindFilter("all"); @@ -871,7 +871,7 @@ function SearchResultsView({ return (

{/* Query context lives here on every breakpoint — on phones this is the only place the submitted query is visible above the fold. */} @@ -885,10 +885,10 @@ function SearchResultsView({ />

- + Ranked from your imported differentials catalogue. Source counts reflect real matches from your indexed library. @@ -1034,7 +1034,7 @@ function SearchResultsView({ {!hasSourceEvidence && !sourcesChecked ? (

Sources not checked for this query yet. @@ -1043,7 +1043,7 @@ function SearchResultsView({ type="button" onClick={rerunSearch} disabled={loading} - className="inline-flex min-h-tap shrink-0 items-center gap-1.5 rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--surface)] px-2.5 text-xs font-extrabold text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] disabled:cursor-wait disabled:opacity-60" + className="inline-flex min-h-tap w-full shrink-0 items-center justify-center gap-1.5 rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--surface)] px-2.5 text-xs font-extrabold text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] disabled:cursor-wait disabled:opacity-60 min-[390px]:w-auto" > {loading ? "Searching…" : "Run source search"} diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 34c17bc1..ccc5e8d8 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1455,8 +1455,8 @@ test.describe("Clinical KB tools launcher", () => { await expect(compareAction).toBeVisible(); await expect(compareAction).toContainText("Compare selected"); - await scrollport.evaluate((element) => element.scrollTo({ top: 700, behavior: "instant" })); - await expect.poll(() => scrollport.evaluate((element) => element.scrollTop)).toBeGreaterThan(100); + await mainContent.evaluate((element) => element.scrollTo({ top: 700, behavior: "instant" })); + await expect.poll(() => mainContent.evaluate((element) => element.scrollTop)).toBeGreaterThan(100); await page.waitForTimeout(300); await expect(dock).not.toHaveAttribute("data-scroll-hidden", "true"); @@ -1492,6 +1492,8 @@ test.describe("Clinical KB tools launcher", () => { expect(compareGeometry.top).toBeGreaterThanOrEqual(0); expect(compareGeometry.bottom).toBeLessThanOrEqual(compareGeometry.viewportHeight); expect(compareGeometry.receivesPointer).toBe(true); + expect(compareGeometry.bottom).toBeLessThan(dockTop); + await expectNoPageHorizontalOverflow(page); // The result cards and compare bar remain in their non-desktop layout up // to 1023px, so the composer must keep providing the portal host on tablet. From fd178b9507e0dda9f81ae21d2f6185366266b674 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:28:33 +0800 Subject: [PATCH 2/3] Initial plan (#878) From 4d72d976b8b0139f56795676e5ce02a441e770a7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 18 Jul 2026 15:53:17 +0000 Subject: [PATCH 3/3] fix(differentials): keep phone results scrolling on #main-content overflow-x-hidden on the results canvas forced overflow-y:auto under the flex height cap, nesting a scrollport that left #main-content at scrollTop 0 and broke the mobile compare FAB scroll regression. Co-authored-by: BigSimmo --- src/components/clinical-dashboard/differentials-home.tsx | 6 +++++- tests/mobile-interaction-regressions.test.ts | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index d3186f0f..f5ba178f 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -871,7 +871,11 @@ function SearchResultsView({ return (