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
19 changes: 8 additions & 11 deletions src/app/globals.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -2574,21 +2574,18 @@ html[data-motion="reduced"] .source-capsule-hit[aria-expanded="true"]:hover .sou
}

@media print {
html.factsheets-printing body * {
visibility: hidden !important;
}

html.factsheets-printing .factsheet-print-sheet,
html.factsheets-printing .factsheet-print-sheet * {
visibility: visible !important;
/* Print only the portaled clean sheet. Removing every other <body> subtree from
* layout flow (display:none — NOT visibility:hidden) means the full-height shell
* chrome (sidebar <aside>, composer) and the tall screen article no longer
* paginate into trailing blank PDF pages. The print sheet keeps normal static
* flow inside its portal so it paginates correctly across pages. */
html.factsheets-printing body > *:not(.factsheet-print-portal) {
display: none !important;
}

html.factsheets-printing .factsheet-print-portal,
html.factsheets-printing .factsheet-print-sheet {
display: block !important;
position: absolute;
left: 0;
top: 0;
width: 100%;
}
}

Expand Down
13 changes: 12 additions & 1 deletion src/components/clinical-dashboard/master-search-header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -484,7 +484,9 @@ export function MasterSearchHeader({
? "formulation"
: searchMode === "tools"
? "tools"
: "answer";
: searchMode === "factsheets"
? "factsheets"
: "answer";
const actionMenuItems = modeActionItemsFor(actionMenuSetId);
const actionMenuTitle = selectedAppMode.label;
const actionMenuSubtitle = searchMode === "answer" ? "Source-backed mode" : selectedAppMode.description;
Expand DownExpand Up@@ -578,6 +580,15 @@ export function MasterSearchHeader({
onOpenSourcePdf?.();
return;
}
if (actionId === "factsheets-search") {
onSearchModeChange("factsheets");
return;
}
if (actionId === "factsheets-browse") {
onSearchModeChange("factsheets");
onQueryChange("");
return;
}
if (actionId === "services-search") {
onSearchModeChange("services");
return;
Expand Down
23 changes: 21 additions & 2 deletions src/components/clinical-dashboard/mode-action-popup.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ import {
import { createPortal } from "react-dom";
import {
Activity,
BookOpenText,
TriangleAlert,
CalendarDays,
Check,
Expand DownExpand Up@@ -59,7 +60,8 @@ export type ModeActionSetId =
| "dsm"
| "specifiers"
| "formulation"
| "prescribing";
| "prescribing"
| "factsheets";
export type ModeActionPlacement = "up" | "down";

type IntegratedSurfaceLayout = {
Expand DownExpand Up@@ -131,7 +133,9 @@ export type ModeActionId =
| "formulation-search"
| "formulation-builder"
| "formulation-compare"
| "formulation-map";
| "formulation-map"
| "factsheets-search"
| "factsheets-browse";

export type ModeActionItem = {
id: ModeActionId;
Expand DownExpand Up@@ -329,6 +333,21 @@ const modeActionSets = {
},
{ id: "medication-access", label: "Documentation", description: "Required forms and eligibility", icon: Lock },
],
factsheets: [
{
id: "factsheets-search",
label: "Search factsheets",
description: "Find a medicine, condition, therapy, or test",
icon: Search,
primary: true,
},
{
id: "factsheets-browse",
label: "Browse all sheets",
description: "Open the full patient-information library",
icon: BookOpenText,
},
],
} as const satisfies Record<ModeActionSetId, readonly ModeActionItem[]>;

export function modeActionItemsFor(setId: ModeActionSetId): readonly ModeActionItem[] {
Expand Down
57 changes: 48 additions & 9 deletions src/components/factsheets/factsheet-detail-page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,10 +15,12 @@ import {
TriangleAlert,
Zap,
} from "lucide-react";
import { useEffect, useState, type ReactNode } from "react";
import { useEffect, useState, useSyncExternalStore, type ReactNode } from "react";
import { createPortal } from "react-dom";

import {
categoryTheme,
FACTSHEET_DEMO_NOTICE,
printBlocks,
relatedFactsheets,
sameTopicFactsheets,
Expand DownExpand Up@@ -48,6 +50,14 @@ export function FactsheetDetailPage({ factsheet }: { factsheet: Factsheet }) {
const [saved, setSaved] = useState(false);
const [saveNotice, setSaveNotice] = useState("");
const [copied, setCopied] = useState(false);
// The print sheet is portaled to <body> so print can isolate it from the shell
// chrome; the portal is client-only, gated behind a mount flag. useSyncExternalStore
// is the lint-safe way to flip false→true on hydration without setState-in-effect.
const mounted = useSyncExternalStore(
() => () => undefined,
() => true,
() => false,
);

const related = relatedFactsheets(factsheet.slug);
const moreInTopic = sameTopicFactsheets(factsheet.slug);
Expand DownExpand Up@@ -329,10 +339,13 @@ export function FactsheetDetailPage({ factsheet }: { factsheet: Factsheet }) {
</div>
</section>

<p className="mt-6 border-t border-[color:var(--border)] pt-3.5 text-xs leading-5 text-[color:var(--text-soft)]">
This sheet is general information, not personal medical advice. Always follow the instructions from your
own doctor or pharmacist.
</p>
<div className="mt-6 border-t border-[color:var(--border)] pt-3.5">
<p className="text-xs font-bold text-[color:var(--warning)]">{FACTSHEET_DEMO_NOTICE}</p>
<p className="mt-1.5 text-xs leading-5 text-[color:var(--text-soft)]">
This sheet is general information, not personal medical advice. Always follow the instructions from your
own doctor or pharmacist.
</p>
</div>
</article>

{/* sidebar */}
Expand DownExpand Up@@ -391,8 +404,17 @@ export function FactsheetDetailPage({ factsheet }: { factsheet: Factsheet }) {
</div>
</div>

{/* print-only clean A4 sheet */}
<FactsheetPrintSheet factsheet={factsheet} blocks={blocks} />
{/* Print-only clean A4 sheet, portaled to <body> so print can remove every
other body subtree from layout flow (see globals.css factsheets-printing
rules) — otherwise the hidden shell chrome paginates into blank pages. */}
{mounted
? createPortal(
<div className="factsheet-print-portal">
<FactsheetPrintSheet factsheet={factsheet} blocks={blocks} />
</div>,
document.body,
)
: null}
</>
);
}
Expand DownExpand Up@@ -715,6 +737,22 @@ function FactsheetPrintSheet({ factsheet, blocks }: { factsheet: Factsheet; bloc
className="factsheet-print-sheet"
style={{ maxWidth: "720px", margin: "0 auto", padding: "8px", color: "#111", fontFamily: "var(--font-sans)" }}
>
<div
style={{
border: "1.5px solid #b42318",
background: "#fef3f2",
color: "#b42318",
fontSize: "11px",
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.04em",
padding: "6px 10px",
borderRadius: "6px",
marginBottom: "12px",
}}
>
Sample — not for clinical use
</div>
<div style={{ borderBottom: "2px solid var(--clinical-accent)", paddingBottom: "12px", marginBottom: "18px" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span
Expand DownExpand Up@@ -808,8 +846,9 @@ function FactsheetPrintSheet({ factsheet, blocks }: { factsheet: Factsheet; bloc
lineHeight: 1.5,
}}
>
This sheet is general information, not personal medical advice. Always follow the instructions from your own
doctor or pharmacist. In an emergency call 000.
<strong style={{ color: "#b42318" }}>{FACTSHEET_DEMO_NOTICE}</strong> This sheet is general information, not
personal medical advice. Always follow the instructions from your own doctor or pharmacist. In an emergency call
000.
</p>
</div>
);
Expand Down
8 changes: 8 additions & 0 deletions src/components/factsheets/factsheets-data.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,14 @@
* patient information before treating sheets as locally approved.
*/

/**
* Demonstration/governance status shown on-screen and preserved in the printed /
* exported take-away, so a handout is never mistaken for approved local patient
* information. Kept as a single source of truth for the disclaimer copy.
*/
export const FACTSHEET_DEMO_NOTICE =
"Demonstration content — not clinician-approved. Governance-approved patient information must replace it before any clinical use or sharing.";

export type FactsheetCategory = "Medications" | "Conditions" | "Therapies" | "Tests & procedures";

export type FactsheetKind = "medRich" | "medLite" | "condition" | "therapy" | "procedure";
Expand Down
8 changes: 8 additions & 0 deletions tests/factsheets-data.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";

import {
FACTSHEET_DEMO_NOTICE,
factsheetCategories,
factsheets,
factsheetSlugs,
Expand All@@ -20,6 +21,13 @@ describe("factsheet library", () => {
expect(findFactsheet("unknown-factsheet")).toBeUndefined();
});

it("exposes a demo/governance notice for on-screen and printed take-aways", () => {
// The exported handout must never read as approved local patient information.
expect(FACTSHEET_DEMO_NOTICE).toMatch(/demonstration/i);
expect(FACTSHEET_DEMO_NOTICE).toMatch(/not clinician-approved/i);
expect(FACTSHEET_DEMO_NOTICE).toMatch(/before any clinical use/i);
});

it("has unique slugs and complete governance metadata on every sheet", () => {
const slugs = new Set<string>();
for (const sheet of factsheets) {
Expand Down