diff --git a/docs/site-map.md b/docs/site-map.md index d6d22b81..2596e3e5 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 0ee3e07a..17f1dabd 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 5c6b998c..529681aa 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 5abf7581..2ab4ec16 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 daf39496..256e4c1a 100644 --- a/src/app/factsheets/search/page.tsx +++ b/src/app/factsheets/search/page.tsx @@ -1,9 +1,25 @@ 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 ; +// App Router hands repeated query params (?q=a&q=b) through as string[]; collapse to +// the first value the way the DSM/Services search routes do, so a malformed link +// filters on one value instead of throwing on `.trim()`. +function firstValue(value?: string | string[]) { + return Array.isArray(value) ? value[0] : value; +} + +export default async function FactsheetsSearchRoute({ + searchParams, +}: { + searchParams: Promise<{ q?: string | string[]; category?: string | string[] }>; +}) { + const params = await searchParams; + const query = (firstValue(params.q) ?? "").trim(); + const category = firstValue(params.category); + const results = filterFactsheets(query, category); + return ; } diff --git a/src/app/globals.css b/src/app/globals.css index fb181241..92ece9c6 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 8c2ad598..6724cd98 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 979dc30a..b347d4c1 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 6f64db99..a24ac694 100644 --- a/src/components/factsheets/factsheet-detail-page.tsx +++ b/src/components/factsheets/factsheet-detail-page.tsx @@ -1,109 +1,786 @@ "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 { 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, +} from "@/components/factsheets/factsheets-data"; +import { factsheetGlyph } 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))`; +} + +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 ( -
- -