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
9 changes: 8 additions & 1 deletion src/components/dsm/dsm-compare-chrome.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,14 @@ export function DsmCompareChrome({
changeLabel="Change diagnoses"
slotPlaceholder="Choose diagnosis"
icon={BookOpenCheck}
onCommit={(ids) => router.push(idsCompareHref("/dsm/compare", ids))}
onCommit={(ids) =>
router.push(
idsCompareHref(
"/dsm/compare",
ids.filter((id): id is string => Boolean(id)),
),
)
}
/>
);
}
259 changes: 114 additions & 145 deletions src/components/dsm/dsm-comparison-page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
import Link from "next/link";
import { ChevronRight, GitCompareArrows, ListChecks, ShieldCheck, X } from "lucide-react";
import { ArrowRight, HelpCircle } from "lucide-react";

import { idsCompareHref, type CompareCatalogItem, type CompareStarterChip } from "@/components/compare";
import { type CompareCatalogItem, type CompareStarterChip } from "@/components/compare";
import { DsmCompareChrome } from "@/components/dsm/dsm-compare-chrome";
import { DsmCompareRemoveLink } from "@/components/dsm/dsm-compare-remove-link";
import { DsmPageHeader } from "@/components/dsm/dsm-page-header";
import { cn, codeText, metadataPill, pageContainer } from "@/components/ui-primitives";
import { cn, codeText, eyebrowText, pageContainer } from "@/components/ui-primitives";
import { dsmCriteria, type DsmDiagnosis } from "@/lib/dsm";

function compareHref(diagnoses: DsmDiagnosis[]) {
return idsCompareHref(
"/dsm/compare",
diagnoses.map((diagnosis) => diagnosis.slug),
);
const BADGE_LETTERS = ["A", "B", "C"] as const;

function diagnosisHeaderGridClass(count: number) {
return count >= 3 ? "grid sm:grid-cols-3" : "grid sm:grid-cols-2";
}

function removeDiagnosisHref(diagnoses: DsmDiagnosis[], slug: string) {
const remaining = diagnoses.filter((diagnosis) => diagnosis.slug !== slug);
return remaining.length ? compareHref(remaining) : "/dsm/compare";
function comparisonRowGridClass(count: number) {
return count >= 3
? "grid border-b border-[color:var(--border)] last:border-b-0 sm:grid-cols-[10rem_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)]"
: "grid border-b border-[color:var(--border)] last:border-b-0 sm:grid-cols-[10rem_minmax(0,1fr)_minmax(0,1fr)]";
}

type ComparisonRow = {
Expand DownExpand Up@@ -87,163 +86,133 @@ export function DsmComparisonPage({
}) {
const rows = comparisonRows(diagnoses);
const chromeIds = selectedIds ?? diagnoses.map((diagnosis) => diagnosis.slug);
const comparisonLabel = diagnoses.map((diagnosis) => diagnosis.title).join(" compared with ");
const summaryById = new Map(catalog.map((item) => [item.id, item.snippet]));

return (
<div data-testid="dsm-comparison-page" className="min-h-full bg-[color:var(--background)] pb-8">
<DsmPageHeader
eyebrow="Diagnosis comparison"
title="Compare DSM diagnoses"
description="Review core criteria, course-defining features, specifiers, and differential flags side by side. This is a structured review aid, not a diagnostic score."
eyebrow=""
homeIcon={false}
icon={false}
title="Compare diagnoses"
description="Side-by-side criteria and differential flags."
className="[&>div]:py-3 [&>div]:sm:py-4"
/>

<div className={cn(pageContainer, "space-y-4 px-4 py-4 sm:px-6 sm:py-6 lg:px-8")}>
<DsmCompareChrome selectedIds={chromeIds} items={catalog} starters={starters} />

{diagnoses.length > 0 ? (
<section className="grid gap-2.5 md:grid-cols-3" aria-label="Selected diagnoses">
{diagnoses.map((diagnosis, index) => (
<article
key={diagnosis.slug}
className="relative rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-3.5 shadow-[var(--shadow-inset)]"
>
<div className="flex items-start gap-3 pr-8">
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-[color:var(--clinical-accent-soft)] text-sm font-extrabold text-[color:var(--clinical-accent)]">
{index + 1}
</span>
<div className="min-w-0">
<p className="text-2xs font-extrabold uppercase tracking-eyebrow text-[color:var(--text-muted)]">
{diagnosis.category.label}
</p>
<h2 className="mt-1 text-sm font-extrabold leading-5 text-[color:var(--text-heading)]">
{diagnosis.title}
</h2>
<span className={cn("mt-2 inline-flex", metadataPill, codeText)}>{diagnosis.icd_code}</span>
</div>
</div>
<DsmCompareRemoveLink
href={removeDiagnosisHref(diagnoses, diagnosis.slug)}
aria-label={`Remove ${diagnosis.title} from comparison`}
className="absolute right-2 top-2 grid h-tap w-tap place-items-center rounded-lg text-[color:var(--decoration-soft)] transition hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--danger)]"
>
<X className="h-4 w-4" aria-hidden />
</DsmCompareRemoveLink>
</article>
))}
</section>
) : null}

{diagnoses.length >= 2 ? (
<>
<section className="hidden overflow-hidden rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] shadow-[var(--shadow-soft)] md:block">
<div className="overflow-x-auto">
<table className="w-full min-w-[54rem] border-collapse text-left">
<caption className="sr-only">DSM diagnosis comparison</caption>
<thead>
<tr className="border-b border-[color:var(--border)] bg-[color:var(--surface-subtle)]">
<th
scope="col"
className="w-44 px-4 py-3 text-2xs font-extrabold uppercase tracking-eyebrow text-[color:var(--text-muted)]"
>
Compare
</th>
{diagnoses.map((diagnosis) => (
<th
key={diagnosis.slug}
scope="col"
className="px-4 py-3 text-sm font-extrabold text-[color:var(--text-heading)]"
>
{diagnosis.title}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-[color:var(--border)]">
{rows.map((row) => (
<tr key={row.label} className="align-top hover:bg-[color:var(--surface-subtle)]/55">
<th
scope="row"
className="bg-[color:var(--surface-subtle)]/65 px-4 py-3 text-xs font-extrabold text-[color:var(--text-heading)]"
>
{row.label}
</th>
{row.values.map((value, index) => (
<td
key={`${row.label}-${diagnoses[index]?.slug}`}
className={cn(
"px-4 py-3 text-xs font-medium leading-5 text-[color:var(--text-muted)]",
row.label === "ICD-10 code" && codeText,
)}
>
{value}
</td>
))}
</tr>
))}
</tbody>
</table>
<section
data-testid="dsm-comparison-ask-this"
className="rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-4 py-4 text-center sm:px-6"
>
<div className="mx-auto flex max-w-4xl items-start justify-center gap-2.5">
<HelpCircle className="mt-0.5 h-5 w-5 shrink-0 text-[color:var(--clinical-accent)]" aria-hidden />
<div>
<p className={cn(eyebrowText, "!text-[color:var(--clinical-accent)]")}>Ask this</p>
<p className="mt-1 text-base font-extrabold leading-6 text-[color:var(--text-heading)]">
Which diagnosis best fits duration, episodicity, and exclusions?
</p>
</div>
</div>
</section>

<section className="grid gap-3 md:hidden" aria-label="DSM diagnosis comparison cards">
{rows.map((row) => (
<article
key={row.label}
className="overflow-hidden rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] shadow-[var(--shadow-inset)]"
>
<h2 className="border-b border-[color:var(--border)] bg-[color:var(--surface-subtle)] px-3 py-2.5 text-xs font-extrabold uppercase tracking-label text-[color:var(--text-heading)]">
{row.label}
</h2>
<dl className="divide-y divide-[color:var(--border)]">
<section
data-testid="dsm-comparison-unified"
className="overflow-hidden rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] shadow-[var(--shadow-soft)]"
aria-label={comparisonLabel}
>
<div className={diagnosisHeaderGridClass(diagnoses.length)}>
{diagnoses.map((diagnosis, index) => (
<div
key={diagnosis.slug}
className={cn(
"grid gap-3 px-4 py-4 sm:px-5",
index > 0 && "border-t border-[color:var(--border)] sm:border-l sm:border-t-0",
)}
>
<div className="flex items-center gap-2">
<span className="grid h-7 w-7 place-items-center rounded-full bg-[color:var(--clinical-accent)] text-xs font-extrabold text-[color:var(--clinical-accent-contrast)]">
{BADGE_LETTERS[index]}
</span>
<h2 className="text-lg font-extrabold text-[color:var(--text-heading)]">{diagnosis.title}</h2>
</div>
<p className="text-sm font-medium leading-6 text-[color:var(--text-muted)]">
{summaryById.get(diagnosis.slug) ??
diagnosis.key_features[0]?.text ??
"Review the complete diagnostic record."}
</p>
<Link
href={`/dsm/diagnoses/${diagnosis.slug}`}
className="inline-flex min-h-tap items-center gap-2 rounded-md px-1 text-sm font-bold text-[color:var(--clinical-accent)] hover:underline"
>
Open record
<ArrowRight className="h-4 w-4" aria-hidden />
</Link>
</div>
))}
</div>

<div className="border-t border-[color:var(--border)]">
{rows.map((row) => (
<div key={row.label} className={comparisonRowGridClass(diagnoses.length)}>
<div className="bg-[color:var(--surface-subtle)] px-4 py-3 text-xs font-extrabold text-[color:var(--text-heading)] sm:flex sm:items-center">
{row.label}
</div>
{row.values.map((value, index) => (
<div key={`${row.label}-${diagnoses[index]?.slug}`} className="grid gap-1 px-3 py-3">
<dt className="text-xs font-extrabold text-[color:var(--clinical-accent)]">
{diagnoses[index]?.title}
</dt>
<dd
className={cn(
"text-xs font-medium leading-5 text-[color:var(--text-muted)]",
row.label === "ICD-10 code" && codeText,
)}
>
<div
key={`${row.label}-${diagnoses[index]?.slug}`}
className={cn(
"grid grid-cols-[1.75rem_minmax(0,1fr)] gap-2 px-4 py-3 text-sm font-medium leading-6 text-[color:var(--text-muted)]",
index > 0 && "border-t border-[color:var(--border)] sm:border-l sm:border-t-0",
row.label === "ICD-10 code" && codeText,
)}
>
<span className="grid h-7 w-7 place-items-center rounded-full bg-[color:var(--clinical-accent-soft)] text-xs font-extrabold text-[color:var(--clinical-accent)]">
{BADGE_LETTERS[index]}
</span>
<span className="line-clamp-4" title={value}>
{value}
</dd>
</span>
</div>
))}
</dl>
</article>
))}
</section>

<section className="grid gap-3 rounded-xl border border-[color:var(--info-border)] bg-[color:var(--info-soft)]/40 p-4 sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:items-center">
<span className="grid h-10 w-10 place-items-center rounded-lg bg-[color:var(--surface)] text-[color:var(--info)] shadow-[var(--shadow-inset)]">
<ShieldCheck className="h-5 w-5" aria-hidden />
</span>
<div>
<h2 className="text-sm font-extrabold text-[color:var(--text-heading)]">
Use the comparison to identify what still needs clarification
</h2>
<p className="mt-1 text-xs font-medium leading-5 text-[color:var(--text-muted)]">
Review duration, episodicity, exclusions, substance or medication effects, medical causes, and
functional impact in the complete records.
</p>
</div>
))}
</div>
<Link
href={`/dsm/diagnoses/${diagnoses[0].slug}/differentials`}
className="inline-flex min-h-tap items-center justify-center gap-2 rounded-lg border border-[color:var(--info-border)] bg-[color:var(--surface)] px-3 text-xs font-bold text-[color:var(--text-heading)]"

<div
className={cn(
"grid border-t border-[color:var(--border)] bg-[color:var(--surface-subtle)]",
diagnosisHeaderGridClass(diagnoses.length),
)}
>
Differential review
<ChevronRight className="h-4 w-4" aria-hidden />
</Link>
{diagnoses.map((diagnosis, index) => (
<div
key={`${diagnosis.slug}-differentials`}
className={cn(
"p-4 sm:p-5",
index > 0 && "border-t border-[color:var(--border)] sm:border-l sm:border-t-0",
)}
>
<Link
href={`/dsm/diagnoses/${diagnosis.slug}/differentials`}
className="inline-flex min-h-tap items-center gap-2 rounded-md px-1 text-sm font-bold text-[color:var(--clinical-accent)] hover:underline"
>
Differential review
<ArrowRight className="h-4 w-4" aria-hidden />
</Link>
</div>
))}
</div>
</section>
</>
) : null}

<footer className="flex flex-wrap items-center gap-2 border-t border-[color:var(--border)] pt-4 text-xs font-medium text-[color:var(--text-muted)]">
<GitCompareArrows className="h-4 w-4 text-[color:var(--clinical-accent)]" aria-hidden />
<span>Comparison is not ranked.</span>
<span aria-hidden>•</span>
<span>Open each diagnosis for complete criteria and documentation support.</span>
<ListChecks className="ml-auto hidden h-4 w-4 text-[color:var(--decoration-soft)] sm:block" aria-hidden />
<footer className="border-t border-[color:var(--border)] pt-4 text-center text-2xs font-medium leading-5 text-[color:var(--text-muted)]">
Structured review aid — not a diagnostic score. Open each record for complete criteria.
</footer>
</div>
</div>
Expand Down
14 changes: 11 additions & 3 deletions src/components/dsm/dsm-page-header.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { BookOpenCheck } from "lucide-react";
import { BookOpenCheck, type LucideIcon } from "lucide-react";
import type { ReactNode } from "react";

import { InformationPageBreadcrumbs } from "@/components/information-page-shell";
Expand All@@ -14,6 +14,8 @@ export function DsmPageHeader({
actions,
className,
breadcrumb = true,
homeIcon,
icon = BookOpenCheck,
}: {
eyebrow?: string;
title: string;
Expand All@@ -29,6 +31,8 @@ export function DsmPageHeader({
* header's title is a `<span>`.
*/
breadcrumb?: boolean;
homeIcon?: LucideIcon | false;
icon?: LucideIcon | false;
}) {
return (
<div className={cn("border-b border-[color:var(--border)] bg-[color:var(--surface)]", className)}>
Expand All@@ -37,13 +41,17 @@ export function DsmPageHeader({
`InformationPageBreadcrumbs` (now itself a `Breadcrumb`), so the
`PageHeader` below is not given a second `breadcrumb` of its own. */}
{breadcrumb ? (
<InformationPageBreadcrumbs home={{ label: "DSM-5 Diagnosis home", href: "/dsm" }} className="mb-3" />
<InformationPageBreadcrumbs
home={{ label: "DSM search", href: "/dsm" }}
homeIcon={homeIcon}
className="mb-3"
/>
) : null}
<PageHeader
eyebrow={eyebrow}
title={title}
description={description}
icon={BookOpenCheck}
icon={icon === false ? undefined : icon}
actions={actions}
meta={
code || category ? (
Expand Down
10 changes: 9 additions & 1 deletion src/components/information-page-shell.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,14 +84,22 @@ export function InformationPageBreadcrumbs({
crumbs = [],
current,
className,
/** Default ArrowLeft; pass false for text-only home crumb (e.g. compare pages with mode nav). */
homeIcon,
}: {
home: { label: string; href: string };
crumbs?: InformationPageCrumb[];
current?: string;
className?: string;
homeIcon?: LucideIcon | false;
}) {
const items: Crumb[] = [
{ label: home.label, href: home.href, icon: ArrowLeft, behavior: "history-back" },
{
label: home.label,
href: home.href,
icon: homeIcon === false ? undefined : (homeIcon ?? ArrowLeft),
behavior: "history-back",
},
...crumbs,
...(current ? [{ label: current }] : []),
];
Expand Down
Loading
Loading