Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions src/app/globals.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand DownExpand Up@@ -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;
}
Expand All@@ -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);
Expand DownExpand Up@@ -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 {
Expand All@@ -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 {
Expand Down
10 changes: 4 additions & 6 deletions src/components/ClinicalDashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2487,7 +2487,7 @@
// URL search intentionally runs once when the selected mode can execute.
}, [canRunSearch, answerThreadBootstrapped]);

useEffect(() => {

Check warning on line 2490 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

React Hook useEffect has missing dependencies: 'executeSearch' and 'scopeFilters'. Either include them or remove the dependency array
const updateHash = () => {
const nextHash = normalizeNavigationHash(window.location.hash || "#search");
window.requestAnimationFrame(() => navigateMobileSection(nextHash, { updateHistory: false }));
Expand DownExpand Up@@ -3929,12 +3929,10 @@
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"
Expand Down
28 changes: 11 additions & 17 deletions src/components/applications-launcher-page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand DownExpand Up@@ -712,7 +712,7 @@ export function ApplicationsLauncherWorkspace({
return (
<main
data-testid="tools-hub"
aria-labelledby="tools-home-heading"
aria-labelledby="tools-home-title"
className={cn(
"mx-auto w-full max-w-[90rem] overflow-x-hidden px-4 pb-8 text-[color:var(--text)] sm:px-6 lg:px-8",
"pb-[calc(12rem+env(safe-area-inset-bottom))] sm:pb-8",
Expand All@@ -723,22 +723,16 @@ export function ApplicationsLauncherWorkspace({
<section
aria-label="Tools home"
data-testid="tools-home"
className="mx-auto grid max-w-5xl justify-items-center gap-5 text-center sm:gap-6"
className="mx-auto grid max-w-5xl justify-items-center gap-3.5 text-center sm:gap-6"
>
<span className="grid h-14 w-14 place-items-center rounded-2xl border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)] sm:h-16 sm:w-16">
<Grid2X2 className="h-7 w-7 sm:h-8 sm:w-8" aria-hidden />
</span>
<div className="grid gap-2">
<h1
id="tools-home-heading"
className="text-balance text-[2rem] font-extrabold leading-none tracking-normal text-[color:var(--text-heading)] sm:text-[2.7rem]"
>
{copy.heading}
</h1>
<p className="mx-auto max-w-xl text-pretty text-sm font-medium leading-6 text-[color:var(--text-muted)] sm:text-base">
{copy.description}
</p>
</div>
<ModeHomeHero
testId="tools-home"
title={copy.heading}
subtitle={copy.description}
icon={Grid2X2}
headingLevel={1}
compact
/>

{desktopComposerSlotId ? (
<div
Expand Down
2 changes: 1 addition & 1 deletion src/components/clinical-dashboard/differentials-home.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -952,7 +952,7 @@ export function DifferentialsHome({
}

return (
<div data-testid="differentials-home" className="mx-auto w-full max-w-6xl overflow-x-hidden px-1">
<div data-testid="differentials-home" className="w-full">
<ModeHomeTemplate
testId="differentials-home-template"
title="Differentials"
Expand Down
3 changes: 2 additions & 1 deletion src/components/clinical-dashboard/favourites-hub.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -159,13 +159,14 @@ export function FavouritesHub({

return (
<div data-testid="favourites-hub" className="mx-auto w-full max-w-6xl space-y-4 overflow-x-hidden sm:space-y-5">
<div className="mx-auto grid w-full max-w-5xl justify-items-center gap-5 pt-4 text-center sm:gap-6 sm:pt-7">
<div className="mx-auto grid w-full max-w-5xl justify-items-center gap-3.5 pt-4 text-center sm:gap-6 sm:pt-7">
<ModeHomeHero
testId="favourites-home"
title="Favourites"
subtitle="Keep trusted notes, sources, medication pages, and clinical sets ready for reuse."
icon={Heart}
headingLevel={headingLevel}
compact
/>

{desktopComposerSlotId ? (
Expand Down
12 changes: 6 additions & 6 deletions src/components/mode-home-page-skeleton.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,16 +9,16 @@ function SkeletonBlock({ className }: { className?: string }) {
export function ModeHomePageSkeleton() {
return (
<div
className="mx-auto grid w-full max-w-3xl justify-items-center gap-4 px-4 py-8"
className="mx-auto grid w-full max-w-[64rem] justify-items-center gap-3.5 px-4 py-8 sm:gap-6"
role="status"
aria-label="Loading"
>
<SkeletonBlock className="h-14 w-14 rounded-2xl sm:h-16 sm:w-16" />
<SkeletonBlock className="h-12 w-12 rounded-2xl sm:h-16 sm:w-16 lg:h-[4.75rem] lg:w-[4.75rem]" />
<div className="grid w-full justify-items-center gap-2">
<SkeletonBlock className="h-8 w-2/3 max-w-sm" />
<SkeletonBlock className="h-4 w-1/2 max-w-xs" />
<SkeletonBlock className="h-[1.7rem] w-2/3 max-w-sm sm:h-[2.6rem]" />
<SkeletonBlock className="h-5 w-1/2 max-w-xs" />
</div>
<SkeletonBlock className="mt-2 h-12 w-full max-w-xl rounded-xl" />
<SkeletonBlock className="mt-2 h-[3.25rem] w-full max-w-xl rounded-full sm:h-14" />
<div className="mt-4 grid w-full max-w-xl gap-3">
<SkeletonBlock className="h-16 w-full rounded-lg" />
<SkeletonBlock className="h-16 w-full rounded-lg" />
Expand All@@ -31,7 +31,7 @@ export function ModeHomePageSkeleton() {

export function ModeHomeRouteLoading() {
return (
<div className="flex min-h-[50vh] items-start justify-center bg-[color:var(--background)] pt-8">
<div className="grid min-h-[calc(100dvh-13.5rem-env(safe-area-inset-top)-env(safe-area-inset-bottom))] items-center justify-items-center bg-[color:var(--background)] sm:min-h-[calc(100dvh-4rem)]">
<ModeHomePageSkeleton />
</div>
);
Expand Down
8 changes: 7 additions & 1 deletion src/components/mode-home-template.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand All@@ -125,7 +131,7 @@ export function ModeHomeMain({
<main
data-testid={testId}
className={cn(
"grid min-h-[calc(100dvh-4rem)] items-center justify-items-center bg-[color:var(--background)] px-4 pb-4 pt-[clamp(1.25rem,4vh,2.25rem)] text-[color:var(--text)] sm:px-6 sm:pb-[clamp(1.75rem,5vh,3.25rem)] sm:pt-[clamp(1.75rem,5vh,3.25rem)] lg:px-8",
"grid min-h-[calc(100dvh-13.5rem-env(safe-area-inset-top)-env(safe-area-inset-bottom))] items-center justify-items-center bg-[color:var(--background)] px-4 pb-4 pt-[clamp(1.25rem,4vh,2.25rem)] text-[color:var(--text)] sm:min-h-[calc(100dvh-4rem)] sm:px-6 sm:pb-[clamp(1.75rem,5vh,3.25rem)] sm:pt-[clamp(1.75rem,5vh,3.25rem)] lg:px-8",
className,
)}
>
Expand Down
75 changes: 72 additions & 3 deletions tests/ui-tools.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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");
Expand All@@ -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 `<heroTestId>-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");
Expand DownExpand Up@@ -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 },
Expand DownExpand Up@@ -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 });
Expand All@@ -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);
Expand Down
Loading