From 2a1508da56328f5b4abb501188452b1cd9b0dcfa Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 15:25:09 +0000 Subject: [PATCH 1/6] Rebuild Patient Factsheets as a first-class mode from the imported design Replace the placeholder factsheet pages (3 generic "sample" sheets in a bespoke local shell) with the imported Patient Factsheets design, wired into the app's universal shell and registered as a proper app mode. Data & content - Rewrite factsheets-data.ts: discriminated Factsheet model with 5 kinds (medRich/medLite/condition/therapy/procedure), 8 plain-language sheets with cited Australian sources, category theming, and print/TOC projections. - Add factsheets-icons.ts mapping icon keys + categories to Lucide glyphs. Pages (rebuilt to the design, using Tailwind + design tokens so dark mode, forced-colors and reduced-motion all work) - Home: shared ModeHomeHero + hero composer slot + browse-by-topic pills + colour-coded featured cards + governance verification footer. - Search: server-driven results, category filter chips, List/Cards toggle, empty state, content-status note. - Detail: category hero band, five kind-specific layouts, sources, more-in-topic, related sheets, sticky sidebar, Easy-read/Standard toggle, Save, and Download PDF that prints a clean A4 sheet (globals.css print rule hides the shell chrome). - [slug] route gains generateStaticParams + dynamicParams=false. Mode registration & shared shell - Register "factsheets" in app-modes (id, definition, namespace-isolated, /factsheets/search query special-case), app-mode-icons, the universal-search domain map, route-ownership, the standalone-home + prefetch in global-search-shell, and the sidebar nav. - Swap factsheets/layout.tsx to GlobalSearchShell; delete the bespoke FactsheetShell. All factsheet routes now share the universal header/sidebar/composer, and the composer drives /factsheets/search. Tests & docs - Rewrite factsheets-data.test.ts for the new model; extend app-modes.test.ts with factsheets coverage; regenerate docs/site-map.md. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01U9NNpe5W616C4XvjRgW6pV --- docs/site-map.md | 1 + scripts/generate-site-map.ts | 1 + src/app/factsheets/[slug]/page.tsx | 8 +- src/app/factsheets/layout.tsx | 8 +- src/app/factsheets/search/page.tsx | 12 +- src/app/globals.css | 28 + .../clinical-dashboard/ClinicalSidebar.tsx | 1 + .../global-search-shell.tsx | 2 + .../factsheets/factsheet-detail-page.tsx | 821 ++++++++++++++++-- src/components/factsheets/factsheet-shell.tsx | 37 - src/components/factsheets/factsheets-data.ts | 674 +++++++++++++- .../factsheets/factsheets-home-page.tsx | 234 ++--- src/components/factsheets/factsheets-icons.ts | 44 + .../factsheets/factsheets-search-page.tsx | 333 ++++--- src/lib/app-mode-icons.ts | 2 + src/lib/app-modes.ts | 30 +- src/lib/search-route-ownership.ts | 1 + src/lib/universal-search-mode-context.ts | 3 + tests/app-modes.test.ts | 21 + tests/factsheets-data.test.ts | 71 +- 20 files changed, 1935 insertions(+), 397 deletions(-) delete mode 100644 src/components/factsheets/factsheet-shell.tsx create mode 100644 src/components/factsheets/factsheets-icons.ts diff --git a/docs/site-map.md b/docs/site-map.md index d6d22b812..2596e3e50 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -54,6 +54,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` - `/?mode=prescribing` - Medication mode. Search kind: `documents`. Query example: `/?mode=prescribing&q=acamprosate+renal+dose&focus=1&run=1`. - `/?mode=tools` - Tools mode. Search kind: `tools`. Query example: `/?mode=tools&q=medications&focus=1&run=1`. - `/therapy-compass` - Therapy mode. Search kind: `tools`. Query example: `/therapy-compass?q=behavioural+activation&focus=1&run=1`. +- `/factsheets` - Factsheets mode. Search kind: `tools`. Query example: `/factsheets/search?q=sertraline&focus=1&run=1`. ## Mode page index diff --git a/scripts/generate-site-map.ts b/scripts/generate-site-map.ts index 0ee3e07ac..17f1dabd0 100644 --- a/scripts/generate-site-map.ts +++ b/scripts/generate-site-map.ts @@ -263,6 +263,7 @@ function renderModeRoutes() { prescribing: appModeHomeHref("prescribing", { query: "acamprosate renal dose", focus: true, run: true }), tools: appModeHomeHref("tools", { query: "medications", focus: true, run: true }), "therapy-compass": appModeHomeHref("therapy-compass", { query: "behavioural activation", focus: true, run: true }), + factsheets: appModeHomeHref("factsheets", { query: "sertraline", focus: true, run: true }), }; return appModeDefinitions.map((mode) => diff --git a/src/app/factsheets/[slug]/page.tsx b/src/app/factsheets/[slug]/page.tsx index 5c6b998cd..529681aad 100644 --- a/src/app/factsheets/[slug]/page.tsx +++ b/src/app/factsheets/[slug]/page.tsx @@ -2,7 +2,13 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { FactsheetDetailPage } from "@/components/factsheets/factsheet-detail-page"; -import { findFactsheet } from "@/components/factsheets/factsheets-data"; +import { factsheetSlugs, findFactsheet } from "@/components/factsheets/factsheets-data"; + +export const dynamicParams = false; + +export function generateStaticParams() { + return factsheetSlugs().map((slug) => ({ slug })); +} export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { const { slug } = await params; diff --git a/src/app/factsheets/layout.tsx b/src/app/factsheets/layout.tsx index 5abf7581e..2ab4ec162 100644 --- a/src/app/factsheets/layout.tsx +++ b/src/app/factsheets/layout.tsx @@ -1,7 +1,11 @@ import type { ReactNode } from "react"; -import { FactsheetShell } from "@/components/factsheets/factsheet-shell"; +import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; export default function FactsheetsLayout({ children }: { children: ReactNode }) { - return {children}; + return ( + + {children} + + ); } diff --git a/src/app/factsheets/search/page.tsx b/src/app/factsheets/search/page.tsx index daf394964..637603d0b 100644 --- a/src/app/factsheets/search/page.tsx +++ b/src/app/factsheets/search/page.tsx @@ -1,9 +1,17 @@ import type { Metadata } from "next"; +import { filterFactsheets } from "@/components/factsheets/factsheets-data"; import { FactsheetsSearchPage } from "@/components/factsheets/factsheets-search-page"; export const metadata: Metadata = { title: "Search Patient Information | Clinical KB" }; -export default function FactsheetsSearchRoute() { - return ; +export default async function FactsheetsSearchRoute({ + searchParams, +}: { + searchParams: Promise<{ q?: string; category?: string }>; +}) { + const { q, category } = await searchParams; + const query = (q ?? "").trim(); + const results = filterFactsheets(query, category); + return ; } diff --git a/src/app/globals.css b/src/app/globals.css index fb181241c..92ece9c67 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2403,3 +2403,31 @@ html[data-motion="reduced"] .source-capsule-hit[aria-expanded="true"]:hover .sou text-decoration: underline; } } + +/* Patient factsheet print sheet. + * The clean A4 sheet is hidden on screen. When the user hits Download PDF / Print, + * the detail component toggles `factsheets-printing` on ; in print we then hide + * every rendered element and reveal ONLY the print sheet, so the shell chrome + * (sidebar, composer, action bar) never appears in the exported PDF. */ +.factsheet-print-sheet { + display: none; +} + +@media print { + html.factsheets-printing body * { + visibility: hidden !important; + } + + html.factsheets-printing .factsheet-print-sheet, + html.factsheets-printing .factsheet-print-sheet * { + visibility: visible !important; + } + + html.factsheets-printing .factsheet-print-sheet { + display: block !important; + position: absolute; + left: 0; + top: 0; + width: 100%; + } +} diff --git a/src/components/clinical-dashboard/ClinicalSidebar.tsx b/src/components/clinical-dashboard/ClinicalSidebar.tsx index 8c2ad598a..6724cd985 100644 --- a/src/components/clinical-dashboard/ClinicalSidebar.tsx +++ b/src/components/clinical-dashboard/ClinicalSidebar.tsx @@ -83,6 +83,7 @@ const sidebarToolItems = [ { id: "prescribing", label: "Medication", icon: Pill, href: "/?mode=prescribing" }, { id: "tools", label: "Tools", icon: Wrench, href: "/?mode=tools" }, { id: "therapy-compass", label: "Therapy", icon: appModeIcons["therapy-compass"], href: "/therapy-compass" }, + { id: "factsheets", label: "Factsheets", icon: appModeIcons.factsheets, href: "/factsheets" }, ] as const; // Drop any tool whose id is a dev-only app mode from the production nav. Non-mode diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 979dc30a5..b347d4c1c 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -249,6 +249,7 @@ function GlobalStandaloneSearchShellClient({ (searchMode === "dsm" && pathname === "/dsm") || (searchMode === "specifiers" && pathname === "/specifiers") || (searchMode === "formulation" && pathname === "/formulation") || + (searchMode === "factsheets" && pathname === "/factsheets") || (searchMode === "tools" && pathname === "/tools")); const isDifferentialPresentationWorkflow = pathname.startsWith("/differentials/presentations"); const shouldShowDesktopSidebar = !hideDesktopSidebar; @@ -342,6 +343,7 @@ function GlobalStandaloneSearchShellClient({ router.prefetch("/dsm"); router.prefetch("/specifiers"); router.prefetch("/formulation"); + router.prefetch("/factsheets"); } function openGuide() { diff --git a/src/components/factsheets/factsheet-detail-page.tsx b/src/components/factsheets/factsheet-detail-page.tsx index 6f64db99b..b25375999 100644 --- a/src/components/factsheets/factsheet-detail-page.tsx +++ b/src/components/factsheets/factsheet-detail-page.tsx @@ -1,109 +1,768 @@ "use client"; import Link from "next/link"; -import { ArrowLeft, Check, Copy, FileText, Printer, Share2 } from "lucide-react"; -import { useRef, useState } from "react"; +import { + ArrowLeft, + ArrowUpRight, + Bookmark, + Check, + ChevronRight, + Clock, + Download, + HeartHandshake, + Printer, + Share2, + TriangleAlert, + Zap, +} from "lucide-react"; +import { createElement, useState, type ReactNode } from "react"; -import type { Factsheet } from "@/components/factsheets/factsheets-data"; -import { Sheet } from "@/components/ui/sheet"; -import { cn, floatingControl, metadataPill, primaryControl, quietPanel } from "@/components/ui-primitives"; +import { + categoryTheme, + printBlocks, + relatedFactsheets, + sameTopicFactsheets, + tocFor, + type Factsheet, + type FactsheetIconKey, +} from "@/components/factsheets/factsheets-data"; +import { factsheetIcon } from "@/components/factsheets/factsheets-icons"; +import { cn, toneDanger, toneWarning } from "@/components/ui-primitives"; + +function accentBorder(accent: string) { + return `color-mix(in srgb, ${accent} 35%, var(--surface))`; +} + +// Render a sheet's Lucide glyph without binding a capitalised component to a render-body +// local (which `react-hooks/static-components` forbids). aria-hidden is set explicitly. +function factsheetGlyph(icon: FactsheetIconKey, className: string) { + return createElement(factsheetIcon(icon), { className, "aria-hidden": "true" }); +} + +function Heading({ children }: { children: ReactNode }) { + return

{children}

; +} export function FactsheetDetailPage({ factsheet }: { factsheet: Factsheet }) { - const [shareOpen, setShareOpen] = useState(false); - const [copyStatus, setCopyStatus] = useState<"idle" | "copied" | "error">("idle"); - const shareButtonRef = useRef(null); + const theme = categoryTheme(factsheet.category); + const [readingLevel, setReadingLevel] = useState<"easy" | "standard">("easy"); + const [saved, setSaved] = useState(false); + const [copied, setCopied] = useState(false); + + const related = relatedFactsheets(factsheet.slug); + const moreInTopic = sameTopicFactsheets(factsheet.slug); + const toc = tocFor(factsheet); + const blocks = printBlocks(factsheet); + + function downloadPdf() { + if (typeof document === "undefined") return; + const root = document.documentElement; + root.classList.add("factsheets-printing"); + const cleanup = () => root.classList.remove("factsheets-printing"); + window.addEventListener("afterprint", cleanup, { once: true }); + window.print(); + } async function copyLink() { try { await navigator.clipboard.writeText(window.location.href); - setCopyStatus("copied"); + setCopied(true); + window.setTimeout(() => setCopied(false), 2200); } catch { - setCopyStatus("error"); + setCopied(false); } } return ( -
- -
From ccb7c30250a10bb34b8a25267846fdc83f009286 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:02:19 +0800 Subject: [PATCH 6/6] Initial plan (#889)