From 547ef429083660356b06730088196810cbe05949 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 06:52:33 +0000 Subject: [PATCH 1/2] feat(differentials): make result cards open on tap and lift their design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The differential result cards gave no visible answer to "where do I click to open this?". Only the title text was a link, the rest of the card was dead space, and the compare checkbox was the most prominent control on the card — so the one thing that looked tappable was not the thing that opens the record. - The whole card now opens the differential. The existing title link stretches a pseudo-element across the card rather than adding a second overlay anchor, so assistive technology still hears one link named by the title. The focus ring deliberately stays on the title: the shared `:focus-visible` rule in globals.css is unlayered and wins over any `outline-none` utility here, so ringing the pseudo-element too would stack two rings and break the "focus is singular" assertion in ui-smoke. - Every card carries an explicit "Open differential" / "Open presentation" affordance with a chevron, as visible text for that hit area. - The compare checkbox becomes a labelled Compare/Added pill on phone cards and moves into a footer action row opposite the open affordance, so the two actions read as two actions. Desktop rows keep the compact box in their trailing column. - A left urgency rail (emergent/urgent/investigations) makes triage legible before any text is read, and the cards pick up rounded-xl, a hover lift, and a wider trailing column so the open affordance fits on one line. Verified: the card body opens the record and the compare control toggles without navigating, at both phone and desktop widths. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017QSV1pJvt6QnprenaWFyza --- .../clinical-dashboard/differentials-home.tsx | 186 ++++++++++++++---- 1 file changed, 149 insertions(+), 37 deletions(-) diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index 8f7e0754a..57b36ab19 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -162,6 +162,20 @@ function statusTone(status: DifferentialRecord["status"]) { return "border-[color:var(--info-border)] bg-[color:var(--info-soft)] text-[color:var(--info)]"; } +/** + * The card's left urgency rail. Same status semantics as `statusTone`, reduced + * to a single solid fill so urgency is legible before any text is read. + */ +function statusRailTone(status: DifferentialRecord["status"]) { + if (status === "emergent") return "bg-[color:var(--danger-solid)]"; + if (status === "urgent") return "bg-[color:var(--warning)]"; + return "bg-[color:var(--info)]"; +} + +function openActionLabel(kind: DifferentialResult["kind"]) { + return kind === "presentation" ? "Open presentation" : "Open differential"; +} + function resultIcon(kind: DifferentialResult["kind"], slug: string) { if (kind === "presentation") return BrainCircuit; return candidateIconBySlug.find(([fragment]) => slug.includes(fragment))?.[1] ?? Stethoscope; @@ -273,11 +287,37 @@ function Chip({ children }: { children: string }) { ); } -function SelectionCheckbox({ selected, onChange, label }: { selected: boolean; onChange: () => void; label: string }) { +/** + * The compare toggle. It sits on the same card as the whole-card open link, so + * it carries its own word ("Compare"/"Added") rather than a bare tick: the two + * actions must never read as the same target. `relative z-10` keeps it above + * the card's stretched open link. + */ +function SelectionCheckbox({ + selected, + onChange, + label, + showLabel = true, +}: { + selected: boolean; + onChange: () => void; + label: string; + showLabel?: boolean; +}) { return ( ); } +/** + * Stretches the card's title link across the whole card, so anywhere that is + * not another control opens the record. It stays a pseudo-element on the single + * existing link rather than a second overlay anchor, so assistive technology + * still hears one link named by the title. + * + * The hit area is widened but the focus ring is deliberately NOT: the shared + * `:focus-visible` rule in `globals.css` is unlayered, so it wins over any + * `outline-none` utility here, and painting a second ring on the pseudo-element + * would leave two rings stacked — exactly what the "focus is singular" + * assertion in `tests/ui-smoke.spec.ts` forbids. Focus stays on the title. + */ +const stretchedOpenLinkClass = "after:absolute after:inset-0 after:z-0 after:rounded-[inherit] after:content-['']"; + +/** + * The visible "click here" label for the stretched card link. It is text, not a + * second anchor to the same href — duplicating the link would double every + * result in a screen-reader link list. + */ +function OpenAffordance({ + kind, + compact = false, + className, +}: { + kind: DifferentialResult["kind"]; + compact?: boolean; + className?: string; +}) { + return ( + + {compact ? "Open" : openActionLabel(kind)} + + + + + ); +} + +/** The card's left urgency rail, drawn inside the card's rounded clip. */ +function StatusRail({ status }: { status: DifferentialRecord["status"] }) { + return ( + + ); +} + function DesktopResultRow({ result, index, @@ -318,18 +414,22 @@ function DesktopResultRow({ return (
+ {index + 1} - +
{result.title} @@ -355,10 +455,15 @@ function DesktopResultRow({ {result.clinicalCues.length > 4 ? {`+${result.clinicalCues.length - 4}`} : null}
-
+
+
- {onToggle ? : } + {onToggle ? ( + + ) : ( + + )}
); } @@ -377,8 +482,9 @@ function MobileResultCard({ return (
+
{result.title} - {onToggle ? ( - - ) : ( - - )}
@@ -420,6 +524,10 @@ function MobileResultCard({ ))}
+
+ + {onToggle ? : null} +
); } @@ -497,13 +605,15 @@ function BestAnswerCard({ data-testid={compact ? "differential-best-answer" : "differential-best-match-card"} aria-label="Best differential match" className={cn( - "grid items-start gap-x-2.5 gap-y-3 rounded-lg border shadow-[var(--e1)]", + "group relative grid items-start gap-x-2.5 gap-y-3 overflow-hidden rounded-xl border shadow-[var(--e1)] transition", "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)]/45", + "hover:shadow-[var(--e2)]", compact - ? "grid-cols-[minmax(0,1fr)_var(--spacing-tap)] p-3" - : "grid-cols-[2.75rem_4.25rem_minmax(0,1fr)_7rem_var(--spacing-tap)] p-3.5", + ? "grid-cols-[minmax(0,1fr)] py-3 pl-4 pr-3" + : "grid-cols-[2.75rem_4.25rem_minmax(0,1fr)_10.75rem_auto] py-3.5 pl-4 pr-3.5", )} > + {!compact ? (
{compact ? ( @@ -546,33 +653,38 @@ function BestAnswerCard({ > {best.title} - - +
+ + + {compact ? : null} +
{best.scopeLabel ? (

{best.scopeLabel}

) : null} {!compact ? ( -
+
+
) : null} - {onToggle ? ( - - ) : compact ? ( - - ) : ( - - )} - {compact ? ( -
- -
+ {!compact ? ( + onToggle ? ( + + ) : ( + + ) ) : null} -
+
+ {compact ? ( +
+ + {onToggle ? : null} +
+ ) : null} ); } From d44ac43b80c648a9673f3000d0a46f78f3f7ee38 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 11:28:32 +0000 Subject: [PATCH 2/2] fix(differentials): drop the raw radius literal from the stretched card link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check:design-system-contract` ratchets raw radius literals per file and CI caught this one going from 0 to 1: `after:rounded-[inherit]` is an arbitrary value, not a token. The pseudo-element paints nothing — it only widens the card's hit area, and it already sits inside the card's own `overflow-hidden` rounded clip — so it needs no radius of its own. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017QSV1pJvt6QnprenaWFyza --- src/components/clinical-dashboard/differentials-home.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index 57b36ab19..f8998a8dd 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -359,7 +359,10 @@ function SelectionCheckbox({ * would leave two rings stacked — exactly what the "focus is singular" * assertion in `tests/ui-smoke.spec.ts` forbids. Focus stays on the title. */ -const stretchedOpenLinkClass = "after:absolute after:inset-0 after:z-0 after:rounded-[inherit] after:content-['']"; +// The pseudo-element paints nothing, so it needs no radius of its own — and a +// `rounded-[inherit]` arbitrary value is a raw radius literal the design-system +// contract ratchet counts against this file. +const stretchedOpenLinkClass = "after:absolute after:inset-0 after:z-0 after:content-['']"; /** * The visible "click here" label for the stretched card link. It is text, not a