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
85 changes: 44 additions & 41 deletions docs/branch-review-ledger.md

Large diffs are not rendered by default.

128 changes: 128 additions & 0 deletions src/components/differentials/diagnosis-term-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import Link from "next/link";
import { ChevronRight } from "lucide-react";

import { cn } from "@/components/ui-primitives";

/** Client-safe href builder — keep snapshot/catalog imports out of this module. */
function differentialDiagnosisHref(slug: string): string {
return `/differentials/diagnoses/${slug}`;
}

export type DiagnosisTermTone = "danger" | "warning" | "accent" | "neutral";

const chipToneClass: Record<DiagnosisTermTone, { linked: string; unlinked: string }> = {
danger: {
linked:
"border-[color:var(--danger-border)] bg-[color:var(--surface)] text-[color:var(--danger)] hover:border-[color:var(--danger)] hover:underline",
unlinked: "border-[color:var(--danger-border)] bg-[color:var(--surface)] text-[color:var(--danger)]",
},
warning: {
linked:
"border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] text-[color:var(--warning)] hover:border-[color:var(--warning)] hover:underline",
unlinked: "border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] text-[color:var(--warning)]",
},
accent: {
linked:
"border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)] hover:border-[color:var(--clinical-accent)] hover:underline",
unlinked: "border-[color:var(--border)] bg-[color:var(--surface-subtle)] text-[color:var(--text-muted)]",
},
neutral: {
linked:
"border-[color:var(--border-strong)] bg-[color:var(--surface-subtle)] text-[color:var(--text-heading)] hover:border-[color:var(--clinical-accent)] hover:text-[color:var(--clinical-accent)] hover:underline",
unlinked: "border-[color:var(--border)] bg-[color:var(--surface-subtle)] text-[color:var(--text-muted)]",
},
};

const focusRing =
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";

type DiagnosisTermChipProps = {
label: string;
slug?: string | null;
tone?: DiagnosisTermTone;
className?: string;
};

/**
* Discrete diagnosis/risk chip. Links when a catalog slug is provided; otherwise
* renders a non-interactive chip so unlinked clinical risks stay readable.
*/
export function DiagnosisTermChip({ label, slug = null, tone = "neutral", className }: DiagnosisTermChipProps) {
const tones = chipToneClass[tone];
if (slug) {
return (
<Link
href={differentialDiagnosisHref(slug)}
aria-label={`Open diagnosis: ${label}`}
className={cn(
"inline-flex min-h-tap items-center gap-1 rounded-md border px-2.5 text-xs font-bold",
focusRing,
tones.linked,
className,
)}
>
{label}
<ChevronRight className="h-3.5 w-3.5 shrink-0" aria-hidden />
</Link>
);
}

return (
<span
className={cn(
"inline-flex min-h-tap items-center rounded-md border px-2.5 text-xs font-semibold",
tones.unlinked,
className,
)}
>
{label}
</span>
);
}

type DiagnosisTermInlineProps = {
label: string;
slug?: string | null;
className?: string;
};

/** Dense inline diagnosis link for comparison-table prose. */
export function DiagnosisTermInline({ label, slug = null, className }: DiagnosisTermInlineProps) {
if (!slug) {
return <span className={className}>{label}</span>;
}

return (
<Link
href={differentialDiagnosisHref(slug)}
aria-label={`Open diagnosis: ${label}`}
className={cn(
"font-semibold text-[color:var(--clinical-accent)] underline-offset-2 hover:underline",
focusRing,
className,
)}
>
{label}
</Link>
);
}

type DiagnosisTermInlineListProps = {
segments: ReadonlyArray<{ text: string; slug: string | null }>;
className?: string;
};

/** Render comma-separated inline segments, linking only resolved diagnoses. */
export function DiagnosisTermInlineList({ segments, className }: DiagnosisTermInlineListProps) {
if (segments.length === 0) return null;
return (
<span className={className}>
{segments.map((segment, index) => (
<span key={`${segment.text}-${index}`}>
{index > 0 ? <span className="text-[color:var(--text-muted)]">, </span> : null}
<DiagnosisTermInline label={segment.text} slug={segment.slug} />
</span>
))}
</span>
);
}
121 changes: 62 additions & 59 deletions src/components/differentials/differential-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import type { DifferentialRecordGovernance } from "@/components/clinical-dashboard/use-differential-catalog";
import { buildDifferentialSectionIndex } from "@/components/differentials/detail-section-index";
import { DiagnosisMapPanel } from "@/components/differentials/diagnosis-map-panel";
import { DiagnosisTermChip, DiagnosisTermInline } from "@/components/differentials/diagnosis-term-link";
import { CopyAfterReviewButton } from "@/components/differentials/differential-presentation-actions";
import { inPageActionRowClass as actionRowClass } from "@/components/in-page-nav/in-page-nav-classes";
import { InPageNavHeader } from "@/components/in-page-nav/in-page-nav-header";
Expand Down Expand Up @@ -132,56 +133,46 @@ const sectionItemIconClass: Partial<Record<DifferentialSection["tone"], string>>
*
* @param section - The section whose tone determines the item layout and styling
* @param items - The items to display
* @param overlapLinks - Maps overlap item labels to diagnosis slugs for linked items
* @param termLinks - Maps cleaned item labels to diagnosis slugs for linked items
* @returns The rendered section item list
*/
function SectionItems({
section,
items,
overlapLinks,
termLinks,
}: {
section: DifferentialSection;
items: string[];
overlapLinks: Record<string, string>;
termLinks: Record<string, string>;
}) {
if (section.tone === "action") {
return (
<ol className="grid gap-2">
{items.map((item, index) => (
<li key={item} className="grid grid-cols-[1.75rem_minmax(0,1fr)] items-start gap-2">
<span className="grid h-6 w-6 place-items-center rounded-full border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-2xs font-extrabold text-[color:var(--clinical-accent)]">
{index + 1}
</span>
<span className="pt-0.5 text-sm leading-6 text-[color:var(--text)]">{item}</span>
</li>
))}
{items.map((item, index) => {
const slug = termLinks[item] ?? null;
return (
<li key={item} className="grid grid-cols-[1.75rem_minmax(0,1fr)] items-start gap-2">
<span className="grid h-6 w-6 place-items-center rounded-full border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-2xs font-extrabold text-[color:var(--clinical-accent)]">
{index + 1}
</span>
<span className="pt-0.5 text-sm leading-6 text-[color:var(--text)]">
{slug ? <DiagnosisTermInline label={item} slug={slug} /> : item}
</span>
Comment thread
cursor[bot] marked this conversation as resolved.
</li>
);
})}
</ol>
);
}

if (section.tone === "overlap") {
return (
<ul className="flex flex-wrap gap-2">
{items.map((item) => {
const slug = overlapLinks[item];
return (
<li key={item}>
{slug ? (
<Link
href={`/differentials/diagnoses/${slug}`}
className="inline-flex min-h-tap items-center gap-1 rounded-md border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-2.5 text-xs font-bold text-[color:var(--clinical-accent)] hover:border-[color:var(--clinical-accent)]"
>
{item}
<ChevronRight className="h-3.5 w-3.5" aria-hidden />
</Link>
) : (
<span className="inline-flex min-h-tap items-center rounded-md border border-[color:var(--border)] bg-[color:var(--surface-subtle)] px-2.5 text-xs font-semibold text-[color:var(--text-muted)]">
{item}
</span>
)}
</li>
);
})}
{items.map((item) => (
<li key={item}>
<DiagnosisTermChip label={item} slug={termLinks[item] ?? null} tone="accent" />
</li>
))}
</ul>
);
}
Expand All @@ -195,18 +186,23 @@ function SectionItems({
"rounded-lg border border-[color:var(--danger-border)] bg-[color:var(--danger-soft)] p-3",
)}
>
{items.map((item) => (
<li key={item} className="flex items-start gap-2">
<Icon
className={cn(
"mt-0.5 h-4 w-4 shrink-0",
sectionItemIconClass[section.tone] ?? "text-[color:var(--text-muted)]",
)}
aria-hidden
/>
<span className="text-sm leading-6 text-[color:var(--text)]">{item}</span>
</li>
))}
{items.map((item) => {
const slug = termLinks[item] ?? null;
return (
<li key={item} className="flex items-start gap-2">
<Icon
className={cn(
"mt-0.5 h-4 w-4 shrink-0",
sectionItemIconClass[section.tone] ?? "text-[color:var(--text-muted)]",
)}
aria-hidden
/>
<span className="text-sm leading-6 text-[color:var(--text)]">
{slug ? <DiagnosisTermInline label={item} slug={slug} /> : item}
</span>
</li>
);
})}
</ul>
);
}
Expand All @@ -216,13 +212,13 @@ function SectionRow({
record,
open,
onOpenChange,
overlapLinks,
termLinks,
}: {
section: DifferentialSection;
record: DifferentialRecord;
open: boolean;
onOpenChange: (id: string, open: boolean) => void;
overlapLinks: Record<string, string>;
termLinks: Record<string, string>;
}) {
const Icon = sectionIcons[section.tone];
const meta = rowMeta[section.tone];
Expand Down Expand Up @@ -308,7 +304,7 @@ function SectionRow({
data-testid="differential-section-items"
className="border-t border-[color:var(--border)] px-3 pb-4 pt-3 sm:pl-[3.25rem] sm:pr-4"
>
<SectionItems section={section} items={items} overlapLinks={overlapLinks} />
<SectionItems section={section} items={items} termLinks={termLinks} />
</div>
</details>
);
Expand Down Expand Up @@ -378,9 +374,11 @@ function safetyFactGridClass(count: number): string {
function SafetySnapshot({
record,
onReviewMustNotMiss,
termLinks,
}: {
record: DifferentialRecord;
onReviewMustNotMiss: (() => void) | null;
termLinks: Record<string, string>;
}) {
const theme = snapshotThemes[record.status];
const facts = resolveSafetyFacts(record);
Expand Down Expand Up @@ -423,17 +421,18 @@ function SafetySnapshot({
Watch for
</span>
<div className="flex flex-wrap gap-1.5">
{tags.map((tag) => (
<span
key={tag}
className={cn(
"inline-flex min-h-6 items-center rounded-md border px-2 text-2xs font-semibold",
theme.chip,
)}
>
{cleanDifferentialItem(tag)}
</span>
))}
{tags.map((tag) => {
const cleaned = cleanDifferentialItem(tag);
return (
<DiagnosisTermChip
key={tag}
label={cleaned}
slug={termLinks[cleaned] ?? null}
tone="danger"
className={cn(!termLinks[cleaned] && theme.chip, "min-h-6 px-2 text-2xs")}
/>
);
})}
</div>
</div>
) : null}
Expand Down Expand Up @@ -1157,7 +1156,11 @@ export function DifferentialDetailPage({
>
{activeTab === "overview" ? (
<>
<SafetySnapshot record={record} onReviewMustNotMiss={reviewMustNotMiss} />
<SafetySnapshot
record={record}
onReviewMustNotMiss={reviewMustNotMiss}
termLinks={detailContext.termLinks ?? {}}
/>
<div className="overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] shadow-[var(--shadow-inset)]">
<div className="flex items-center justify-between gap-3 border-b border-[color:var(--border)] bg-[color:var(--surface-subtle)] px-3 sm:px-4">
<p className="text-xs font-extrabold uppercase tracking-eyebrow text-[color:var(--text-muted)]">
Expand Down Expand Up @@ -1186,7 +1189,7 @@ export function DifferentialDetailPage({
record={record}
open={openSections.has(section.id)}
onOpenChange={setSectionOpen}
overlapLinks={detailContext.overlapLinks}
termLinks={detailContext.termLinks ?? {}}
/>
))}
</div>
Expand Down
Loading
Loading