From 030c23cd2a9e0f667b3b21b0ed21538f06f63ff3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 13:59:20 +0000 Subject: [PATCH] Standardize mode home sizing and slim the search pill for phones All eight mode homes now render the shared compact ModeHomeHero at identical sizes: Favourites gains the compact flag, Tools swaps its bespoke hero markup for the shared component, and Differentials drops its extra max-w-6xl wrapper. Short mode homes centre their hero and search block mid-screen on phones (both dashboard-hosted and standalone routes), replacing the top-anchored treatment and the phone-wrong 4rem header calc. The composer pill slims from 3.8rem to 3.25rem (3.5rem from sm) with a flat action button and lighter shadow, and the route-loading skeleton now matches the compact hero so pages no longer jump on load. The mobile Playwright loop gains Documents and Medication entries plus a hero-parity test across all eight modes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JrqMMyWff7uicA1v9gUhfx --- src/app/globals.css | 24 +++--- src/components/ClinicalDashboard.tsx | 10 +-- src/components/applications-launcher-page.tsx | 28 +++---- .../clinical-dashboard/differentials-home.tsx | 2 +- .../clinical-dashboard/favourites-hub.tsx | 3 +- src/components/mode-home-page-skeleton.tsx | 12 +-- src/components/mode-home-template.tsx | 8 +- tests/ui-tools.spec.ts | 75 ++++++++++++++++++- 8 files changed, 115 insertions(+), 47 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 409fa258a1..a3c92c5171 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -592,15 +592,14 @@ summary::-webkit-details-marker { } .answer-footer-search-pill { - min-height: 3.8rem; + min-height: 3.25rem; gap: 0.375rem; border: 1px solid color-mix(in srgb, var(--border-strong) 80%, transparent); background: color-mix(in srgb, var(--surface) 97%, transparent); - padding-inline: 0.5rem; + padding-inline: 0.375rem; box-shadow: 0 1px 2px rgb(16 24 40 / 4%), - 0 6px 16px rgb(16 24 40 / 7%), - 0 20px 48px rgb(16 24 40 / 12%); + 0 8px 24px rgb(16 24 40 / 8%); backdrop-filter: blur(18px) saturate(140%); -webkit-backdrop-filter: blur(18px) saturate(140%); transition: @@ -1127,7 +1126,8 @@ summary::-webkit-details-marker { .answer-footer-search-action { height: 2.75rem; width: 2.75rem; - border: 1px solid var(--border); + border: 1px solid transparent; + background: transparent; color: var(--text-muted); box-shadow: none; } @@ -1148,15 +1148,15 @@ summary::-webkit-details-marker { .answer-footer-search-divider { display: none; - height: 2.25rem; + height: 2rem; width: 1px; flex: 0 0 auto; background: color-mix(in srgb, var(--border-strong) 58%, transparent); } .answer-footer-search-send { - height: 2.8rem; - width: 2.8rem; + height: 2.75rem; + width: 2.75rem; background: var(--clinical-accent); color: var(--clinical-accent-contrast); box-shadow: 0 3px 10px color-mix(in srgb, var(--clinical-accent) 32%, transparent); @@ -1416,9 +1416,9 @@ summary::-webkit-details-marker { } .answer-footer-search-pill { - min-height: 3.8rem; + min-height: 3.5rem; gap: 0.5rem; - padding-inline: 0.625rem; + padding-inline: 0.5rem; } .answer-footer-search-input { @@ -1427,8 +1427,8 @@ summary::-webkit-details-marker { .answer-footer-search-action, .answer-footer-search-send { - height: 3.3rem; - width: 3.3rem; + height: 3rem; + width: 3rem; } .answer-footer-search-divider { diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 19de0de082..14b89a0e75 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3929,12 +3929,10 @@ export function ClinicalDashboard({ className={cn( "min-h-[calc(100dvh-12.5rem)] sm:min-h-[calc(100dvh-11rem)]", centeredModeHome || showAnswerHome - ? // On tall phones the centred home leans slightly toward the - // bottom composer (matches the committed vertical-weighting - // guard); short phones skip the bias so content still fits. - // Mobile uses top alignment so the integrated action menu is - // not clipped by the dead space below vertically centred homes. - "grid w-full place-items-center max-sm:place-content-start max-sm:justify-items-center max-sm:pt-[clamp(0.75rem,3vh,2rem)] max-sm:[@media(min-height:800px)]:pt-[5vh]" + ? // Phones centre the home block mid-screen, matching the + // standalone-route homes; the pop-up action surface picks + // its own up/down placement so it stays unclipped either way. + "grid w-full place-items-center max-sm:pt-2" : activeModeResultKind === "tools" || activeModeResultKind === "favourites" || activeModeResultKind === "differentials" diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx index f8b9fbf271..6039ba6056 100644 --- a/src/components/applications-launcher-page.tsx +++ b/src/components/applications-launcher-page.tsx @@ -24,7 +24,7 @@ import { } from "lucide-react"; import { type FormEvent, useEffect, useMemo, useState } from "react"; -import { ModeHomeVerificationFooter } from "@/components/mode-home-template"; +import { ModeHomeHero, ModeHomeVerificationFooter } from "@/components/mode-home-template"; import { cn } from "@/components/ui-primitives"; import { toolCatalogRecords, @@ -712,7 +712,7 @@ export function ApplicationsLauncherWorkspace({ return (
- - - -
-

- {copy.heading} -

-

- {copy.description} -

-
+ {desktopComposerSlotId ? (
+
-
+
{desktopComposerSlotId ? ( diff --git a/src/components/mode-home-page-skeleton.tsx b/src/components/mode-home-page-skeleton.tsx index 459f5bc44c..d5e0046099 100644 --- a/src/components/mode-home-page-skeleton.tsx +++ b/src/components/mode-home-page-skeleton.tsx @@ -9,16 +9,16 @@ function SkeletonBlock({ className }: { className?: string }) { export function ModeHomePageSkeleton() { return (
- +
- - + +
- +
@@ -31,7 +31,7 @@ export function ModeHomePageSkeleton() { export function ModeHomeRouteLoading() { return ( -
+
); diff --git a/src/components/mode-home-template.tsx b/src/components/mode-home-template.tsx index 5a3d36e0d9..cf140fb0f6 100644 --- a/src/components/mode-home-template.tsx +++ b/src/components/mode-home-template.tsx @@ -111,6 +111,12 @@ export function ModeHomeHero({ /** * Standalone-route wrapper that mirrors the dashboard's vertically centred * mode homes: full-height, centred content, no fixed bottom composer reserve. + * The phone min-height nets out the real phone chrome — the ~4.25rem sticky + * header plus the shell's 9rem bottom-composer reserve on `#main-content` + * (see `global-mockup-search-shell.tsx`) — so short homes centre in the space + * that is actually visible instead of against the 4rem desktop-header calc, + * which over-measures on phones and forced a phantom scrollbar. sm+ keeps the + * original calc where the header really is 4rem. */ export function ModeHomeMain({ testId, @@ -125,7 +131,7 @@ export function ModeHomeMain({
diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index f466e56cc6..b93a995917 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -413,7 +413,10 @@ test.describe("Clinical KB tools launcher", () => { expect(searchBox).not.toBeNull(); expect(headingBox).not.toBeNull(); expect((headingBox?.y ?? 0) + (headingBox?.height ?? 0)).toBeLessThan(searchBox?.y ?? 0); + // Short homes centre their hero+search block mid-screen on phones, so the + // search midpoint should land in a centred band rather than hug an edge. expect((searchBox?.y ?? 0) + (searchBox?.height ?? 0) / 2).toBeLessThan(820 * 0.72); + expect((searchBox?.y ?? 0) + (searchBox?.height ?? 0) / 2).toBeGreaterThan(820 * 0.2); const metrics = await globalSearchComposerMetrics(page, home.testId); expect(metrics).not.toBeNull(); expect(metrics?.position).not.toBe("fixed"); @@ -426,6 +429,65 @@ test.describe("Clinical KB tools launcher", () => { } }); + test("all mode home heroes share identical sizing on mobile", async ({ page }) => { + test.setTimeout(150_000); + await mockAnswerDashboardApi(page); + await page.setViewportSize({ width: 390, height: 820 }); + + // Every mode home renders the shared compact ModeHomeHero, so the icon box + // and type scale must be identical across modes. Baseline: Answer. + let baseline: { iconWidth: number; iconHeight: number; headingFontSize: number; subtitleFontSize: number } | null = + null; + + for (const home of [ + { path: "/?mode=answer", testId: "answer-empty-state", heroTestId: "answer-empty-state" }, + { path: "/?mode=documents", testId: "document-search-empty-state", heroTestId: "document-search-empty-state" }, + { path: "/?mode=prescribing", testId: "medication-home", heroTestId: "medication-home" }, + { path: "/?mode=favourites", testId: "favourites-hub", heroTestId: "favourites-home" }, + { path: "/?mode=tools", testId: "tools-home", heroTestId: "tools-home" }, + { path: "/services", testId: "services-home", heroTestId: "services-home-template" }, + { path: "/forms", testId: "forms-home", heroTestId: "forms-home-template" }, + { path: "/differentials", testId: "differentials-home", heroTestId: "differentials-home-template" }, + ] as const) { + await gotoLauncher(page, home.path); + const homeRegion = page.getByTestId(home.testId); + await expect(homeRegion).toBeVisible(); + + const icon = homeRegion.locator(".mode-home-icon").first(); + await expect(icon).toBeVisible(); + const iconBox = await icon.boundingBox(); + expect(iconBox, `${home.path} hero icon`).not.toBeNull(); + + // ModeHomeHero gives its heading the deterministic id `-title` + // (role/name lookups can collide with sr-only section headings). + const heading = page.locator(`#${home.heroTestId}-title`); + await expect(heading).toBeVisible(); + const headingFontSize = await heading.evaluate((el) => Number.parseFloat(getComputedStyle(el).fontSize)); + const subtitle = heading.locator("xpath=following-sibling::p[1]"); + await expect(subtitle).toBeVisible(); + const subtitleFontSize = await subtitle.evaluate((el) => Number.parseFloat(getComputedStyle(el).fontSize)); + + const metrics = { + iconWidth: Math.round(iconBox?.width ?? 0), + iconHeight: Math.round(iconBox?.height ?? 0), + headingFontSize, + subtitleFontSize, + }; + if (!baseline) { + baseline = metrics; + // Compact hero mobile scale: 3rem icon, 1.6rem heading, 0.875rem subtitle. + expect(metrics.iconWidth).toBe(48); + expect(metrics.iconHeight).toBe(48); + expect(metrics.headingFontSize).toBeCloseTo(25.6, 1); + expect(metrics.subtitleFontSize).toBeCloseTo(14, 1); + } else { + expect(metrics, `${home.path} hero metrics`).toEqual(baseline); + } + + await expectNoPageHorizontalOverflow(page); + } + }); + test("phone bottom-dock search opens the command surface above the pill", async ({ page }) => { await page.setViewportSize({ width: 390, height: 820 }); await gotoLauncher(page, "/services?q=13YARN&focus=1&run=1"); @@ -461,6 +523,13 @@ test.describe("Clinical KB tools launcher", () => { for (const home of [ { path: "/?mode=answer", testId: "answer-empty-state", heading: "How can I help?", headingLevel: 2 }, + { path: "/?mode=documents", testId: "document-search-empty-state", heading: "Documents", headingLevel: 2 }, + { + path: "/?mode=prescribing", + testId: "medication-home", + heading: "Medication prescribing", + headingLevel: 2, + }, { path: "/services", testId: "services-home", heading: "Find a service", headingLevel: 1 }, { path: "/forms", testId: "forms-home", heading: "What do you need from forms?", headingLevel: 1 }, { path: "/differentials", testId: "differentials-home", heading: "Differentials", headingLevel: 1 }, @@ -1005,7 +1074,7 @@ test.describe("Responsive layout guards", () => { }); } - test("prescribing mode home top-aligns on phones but centres on tablet", async ({ page }) => { + test("prescribing mode home centres above the phone composer and balances on tablet", async ({ page }) => { async function verticalWeighting(width: number) { // Tall viewport exaggerates the free space so the anchor is unambiguous. await page.setViewportSize({ width, height: 900 }); @@ -1020,8 +1089,8 @@ test.describe("Responsive layout guards", () => { }); } - // Phone (< sm): content is top-aligned so integrated action menus are not - // clipped by dead space below vertically centred homes. + // Phone (< sm): the home block centres within the space above the bottom + // composer reserve, so it sits mid-screen leaning toward the top edge. const phone = await verticalWeighting(375); expect(phone).not.toBeNull(); expect(phone?.topGap ?? 0).toBeLessThan(phone?.bottomGap ?? 0);