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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-18 | claude/patient-factsheets-search-regression-8iyvnd | 5c93cffd7e4cfa876e3926d8b40c4ae9bb0a84da | src/components/factsheets/factsheets-home-page.tsx,src/components/factsheets/factsheets-data.ts,src/components/factsheets/factsheets-icons.ts,tests/mode-home-loading-contract.test.ts | approved | verify:pr-local (all 10 checks passed), live Playwright screenshot check at 390x844 |
11 changes: 1 addition & 10 deletions src/components/factsheets/factsheets-data.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -627,7 +627,7 @@ export const factsheets: Factsheet[] = [

const bySlug = new Map(factsheets.map((sheet) => [sheet.slug, sheet]));

/** Sheets shown on the home "Start with a factsheet" grid, in curated order. */
/** Curated slugs worth promoting elsewhere (e.g. related-content surfaces); kept in order. */
export const featuredFactsheetSlugs = ["sertraline", "depression", "gad", "ssri", "cbt", "lithium-monitoring"];

const relatedMap: Record<string, string[]> = {
Expand All@@ -649,10 +649,6 @@ export function factsheetSlugs(): string[] {
return factsheets.map((sheet) => sheet.slug);
}

export function featuredFactsheets(): Factsheet[] {
return featuredFactsheetSlugs.map((slug) => bySlug.get(slug)).filter((sheet): sheet is Factsheet => Boolean(sheet));
}

/** Server-driven filter for the search page: optional query + optional category. */
export function filterFactsheets(query: string, category?: string): Factsheet[] {
const q = query.trim().toLowerCase();
Expand DownExpand Up@@ -680,11 +676,6 @@ export function sameTopicFactsheets(slug: string): Factsheet[] {
return factsheets.filter((entry) => entry.category === sheet.category && entry.slug !== sheet.slug);
}

/** Count of sheets per category, for the home browse pills. */
export function categoryCount(category: FactsheetCategory): number {
return factsheets.filter((sheet) => sheet.category === category).length;
}

// ---- Print + table-of-contents projections -------------------------------------------------

export type PrintBlock =
Expand Down
144 changes: 15 additions & 129 deletions src/components/factsheets/factsheets-home-page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,140 +1,26 @@
import Link from "next/link";
import { ArrowRight, BookOpenText, Clock } from "lucide-react";
import { BookOpenText } from "lucide-react";

import { categoryCount, factsheetCategories, featuredFactsheets } from "@/components/factsheets/factsheets-data";
import { cardAccentEdge, cardInteractive, focusRing } from "@/components/card-recipes";
import { factsheetCategoryGlyph, factsheetGlyph } from "@/components/factsheets/factsheets-icons";
import { DesktopComposerPortalSlot } from "@/components/desktop-composer-portal-slot";
import { ModeHomeHero, ModeHomeVerificationFooter } from "@/components/mode-home-template";
import { cn, eyebrowText } from "@/components/ui-primitives";
import { FACTSHEET_CATEGORY_IDENTITY } from "@/lib/category-identity";
import { ModeHomeMain, ModeHomeTemplate, ModeHomeVerificationFooter } from "@/components/mode-home-template";
import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";

export function FactsheetsHomePage() {
const featured = featuredFactsheets();

return (
<main
data-testid="factsheets-home-main"
className="flex w-full flex-1 flex-col items-center bg-[color:var(--background)] px-0 pb-10 pt-[clamp(1.25rem,4vh,2.25rem)] text-[color:var(--text)] sm:px-6 sm:pt-[clamp(1.75rem,5vh,3.25rem)] lg:px-8"
>
<div className="mode-home-template mx-auto flex w-full max-w-none flex-col items-center gap-5 px-0 text-center sm:max-w-[62rem] sm:gap-6">
<ModeHomeHero
testId="factsheets-home"
title="Factsheets"
subtitle="Plain-language patient handouts."
icon={BookOpenText}
/>

{/* The universal composer portals itself into this slot on the mode home (hero placement). */}
<DesktopComposerPortalSlot
id={modeHomeDesktopComposerSlotId}
className="mode-home-composer-slot hidden w-full px-4 sm:px-0 [&:not(:empty)]:block"
/>

<section aria-label="Browse by topic" className="grid w-full gap-2.5 px-4 sm:px-0">
<p className={cn(eyebrowText, "text-center sm:text-left")}>Browse by topic</p>
<div className="flex flex-wrap justify-center gap-2 sm:justify-start">
{factsheetCategories.map((category) => {
return (
<Link
key={category}
href={`/factsheets/search?category=${encodeURIComponent(category)}`}
data-category-accent={FACTSHEET_CATEGORY_IDENTITY[category].accent}
className={cn(
"inline-flex min-h-tap items-center gap-2 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-raised)] px-3 text-sm font-semibold text-[color:var(--text)] shadow-[var(--e1)] transition hover:border-[color:var(--cat-border)] hover:bg-[color:var(--surface-subtle)] motion-reduce:transition-none forced-colors:border",
focusRing,
)}
>
<span className="grid h-5 w-5 shrink-0 place-items-center rounded-full bg-[color:var(--cat-soft)] text-[color:var(--cat-accent)]">
{factsheetCategoryGlyph(category, "size-icon-sm")}
</span>
{category}
<span className="text-2xs font-bold tabular-nums text-[color:var(--text-muted)]">
{categoryCount(category)}
</span>
</Link>
);
})}
</div>
</section>

<section
aria-labelledby="factsheets-featured-title"
className="grid w-full gap-3.5 border-t border-[color:var(--border)] px-4 pt-6 text-left sm:px-0"
>
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h2
id="factsheets-featured-title"
className="text-lg font-extrabold tracking-tight text-[color:var(--text-heading)]"
>
Start with a factsheet
</h2>
<p className="mt-1 text-sm-minus font-medium text-[color:var(--text-muted)]">
Dated demonstration content, written in plain language.
</p>
</div>
<Link
href="/factsheets/search"
className="inline-flex min-h-tap items-center gap-1.5 rounded-lg text-sm font-bold text-[color:var(--clinical-accent)] transition hover:text-[color:var(--clinical-accent-hover)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
>
View all sheets
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>

<div className="grid gap-3.5 sm:grid-cols-2 lg:grid-cols-3">
{featured.map((sheet) => {
return (
<Link
key={sheet.slug}
href={`/factsheets/${sheet.slug}`}
data-testid="factsheets-featured-card"
// Accent arrives through the shared attribute rather than three
// inline `style` objects. An inline value cannot be remapped by
// the dark or forced-colors blocks, so the old card carried its
// light-mode tint into high contrast.
data-category-accent={FACTSHEET_CATEGORY_IDENTITY[sheet.category].accent}
className={cn(cardInteractive, cardAccentEdge, "flex flex-col p-4")}
>
<div className="flex items-center justify-between">
<span className="grid h-10 w-10 place-items-center rounded-lg border border-[color:var(--cat-border)] bg-[color:var(--cat-soft)] text-[color:var(--cat-accent)] forced-colors:border">
{factsheetGlyph(sheet.icon, "size-icon-lg")}
</span>
<span className="rounded-md border border-[color:var(--cat-border)] bg-[color:var(--cat-soft)] px-2 py-1 text-2xs font-bold text-[color:var(--cat-accent)] forced-colors:border">
{sheet.category}
</span>
</div>
<h3 className="mt-4 text-lg font-semibold leading-6 text-[color:var(--text-heading)] transition group-hover:text-[color:var(--cat-accent)] motion-reduce:transition-none">
{sheet.title}
{sheet.brand ? (
<span className="font-medium text-[color:var(--text-muted)]"> {sheet.brand}</span>
) : null}
</h3>
<p className="mt-2 flex-1 text-pretty text-sm-minus leading-5 text-[color:var(--text-muted)]">
{sheet.summary}
</p>
<div className="mt-4 flex items-center gap-3">
<span className="inline-flex items-center gap-1.5 text-2xs font-bold text-[color:var(--text-muted)]">
<Clock className="h-3.5 w-3.5" aria-hidden="true" />
Updated {sheet.reviewedOn}
</span>
<span className="text-xs text-[color:var(--text-muted)]">{sheet.readTime}</span>
</div>
</Link>
);
})}
</div>
</section>

<div className="w-full px-4 pt-1 sm:px-0">
<ModeHomeMain testId="factsheets-home-main">
<ModeHomeTemplate
testId="factsheets-home"
title="Factsheets"
subtitle="Plain-language patient handouts."
icon={BookOpenText}
actions={[]}
actionsLabel="Factsheets actions"
desktopComposerSlotId={modeHomeDesktopComposerSlotId}
footer={
<ModeHomeVerificationFooter
label="Demonstration patient information"
body="Connect only governance-approved content before publication"
/>
</div>
</div>
</main>
}
/>
</ModeHomeMain>
);
}
16 changes: 1 addition & 15 deletions src/components/factsheets/factsheets-icons.ts
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
import {
Activity,
BrainCircuit,
ClipboardList,
CloudRain,
Droplet,
HeartPulse,
Expand All@@ -15,7 +13,7 @@ import {
} from "lucide-react";
import { createElement } from "react";

import type { FactsheetCategory, FactsheetIconKey } from "@/components/factsheets/factsheets-data";
import type { FactsheetIconKey } from "@/components/factsheets/factsheets-data";

/** Stable icon-key → Lucide component map for per-sheet icons. */
const factsheetIcons: Record<FactsheetIconKey, LucideIcon> = {
Expand All@@ -32,22 +30,10 @@ const factsheetIcons: Record<FactsheetIconKey, LucideIcon> = {
droplet: Droplet,
};

/** Category browse icons for the home topic pills. */
const factsheetCategoryIcons: Record<FactsheetCategory, LucideIcon> = {
Medications: Pill,
Conditions: BrainCircuit,
Therapies: MessagesSquare,
"Tests & procedures": ClipboardList,
};

/**
* Render a Lucide glyph without binding a capitalised component to a render-body
* local (which `react-hooks/static-components` forbids).
*/
export function factsheetGlyph(icon: FactsheetIconKey, className: string) {
return createElement(factsheetIcons[icon], { className, "aria-hidden": "true" });
}

export function factsheetCategoryGlyph(category: FactsheetCategory, className: string) {
return createElement(factsheetCategoryIcons[category], { className, "aria-hidden": "true" });
}
16 changes: 16 additions & 0 deletions tests/mode-home-loading-contract.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,6 +93,22 @@ describe("mode-home loading contract", () => {
expect(source).toContain("pathname === THERAPY_HOME");
});

it("routes Factsheets' mode home body through the shared ModeHomeTemplate composer host", () => {
// A hand-rolled composer slot (as Factsheets briefly had) omits the SSR
// data-composer-reserve/min-h reserve that ModeHomeTemplate provides, which
// regresses the shared "one composer host" contract — see
// docs/search-chrome-behaviour.md Invariant 15.
const factsheetsSource = readFileSync(
join(process.cwd(), "src/components/factsheets/factsheets-home-page.tsx"),
"utf8",
);
expect(factsheetsSource).toMatch(
/import\s*\{[^}]*ModeHomeTemplate[^}]*\}\s*from\s*"@\/components\/mode-home-template"/,
);
expect(factsheetsSource).toMatch(/<ModeHomeTemplate\b/);
expect(factsheetsSource).not.toMatch(/DesktopComposerPortalSlot/);
});

it("keeps mode-home route loading top-aligned on phones", () => {
const skeletonSource = readFileSync(join(process.cwd(), "src/components/mode-home-page-skeleton.tsx"), "utf8");
expect(skeletonSource).toContain("items-start");
Expand Down
Loading