Skip to content
Merged
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,3 +626,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-19 | cursor/hide-favourites-guest-mode-a26d (PR #934) | 04192653 | Final Favourites total guest-blackout review + merge readiness | No P0/P1. Fixed residual P2s: rankToolRecords now fails closed by default; mode/chip filters stop double-ORing demoMode; gated deep-link mode chrome falls back to Answer; MasterSearchHeader mode-menu DOM tests added. Guest discovery closed for mode menu, sidebar, cross-mode chips, Tools Saved, universal tools ranking, also-matches, prefetch. Intentional residual: direct /favourites signup gate; mockup routes. PR body appears overwritten by unrelated Sync PR policy content — agent posted corrective comment (403 on body edit). | Focused Vitest 37/37; verify:cheap green; verify:pr-local green (build + client-secret scan + RAG fixtures); browser guest Tools/sidebar/favourites proof earlier. No OpenAI/Supabase writes. |
| 2026-07-19 | cursor/pr-policy-body-cleanup-f46b (PR #942) + PR #933 closeout | 7c8e6aadf0890b143372fb96f13d9de47a416db9 | post-merge CI triage for #933 PR-policy red check | PR #933 product merge (`bd864de0`) already on main with green post-merge main CI (Static/Unit/Build/Production UI/SAST/Docker). Sole remaining red check on #933 was post-ready PR policy against a stale synced body with unchecked governance boxes (from leftover `PR_POLICY_BODY.md` introduced by #932). Token cannot edit merged PR bodies (403). Removed the stale template via #942 so Sync PR policy body no longer reapplies unchecked governance. Local composer regression 6/6 on main; reserve unit 11/11. No product regression. | Hosted #933 pre-merge + main push green; #942 required checks green then squash-merged; focused Chromium composer 6/6; reserve Vitest 11/11. No OpenAI/Supabase provider calls. |
| 2026-07-19 | cursor/documents-search-header-3eab / PR #936 | a7feaa3033180b672cfafaaaf75dc75088ebf052 | documents search header redesign final review + merge readiness | No remaining high-confidence P0-P1. Implemented identity-first results chrome, unified Sort/type-filter/Library toolbar, removed documents Also-in-library strip, relocated ScopeAndGovernanceNotice under controls, fixed Prettier CI failure and memo-busting empty warnings default, synced accurate PR policy body then removed the stale leftover, and repeatedly merged origin/main so squash auto-merge is not blocked behind/dirty. Hosted required checks including Production UI passed on the integrated head. | Local: typecheck/lint/format; focused Playwright documents `@critical` + deferred source/admin + forms sort persistence; design-system/icon-scale/maintainability; build + RAG fixtures; verify:pr-local units with known pdf-extraction-budget env artifact also on clean main. Hosted: PR policy, Static, Unit, Build, Production UI, Advisory UI, PR required green. No OpenAI/live Supabase writes. |
| 2026-07-19 | cursor/specifiers-results-ui-7850 (PR #941) | ef9bcf23bb93cbaddddeda3aedf08e6d037e2f18 | Specifiers results UI elevation final review + merge readiness | No high-confidence P0-P1. Ranking blurb removed; solid Aegean subnav kept vs soft family chips; diagnosis control content-sized with min-h-tap; match cards single Open hit target with motion-reduce and guarded typical language. Re-synced origin/main after #936 ledger append conflict. Residual risk: hosted Production UI must finish green on exact head; full verify:cheap still hits known container-only pdf-extraction-budget failures also seen on main. | Local: format:changed; lint/typecheck on touched files; design-system + icon-scale; vitest specifiers+reserve 10/10; Playwright ui-specifiers 5/5 including filter interactions; production build + client-bundle secret scan; offline RAG fixtures 36/21. PR marked ready for review. No OpenAI/live Supabase provider calls. |
181 changes: 174 additions & 7 deletions src/components/specifiers/specifier-ui.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import Link from "next/link";
import type { ComponentType, CSSProperties, ReactNode } from "react";
import { ArrowLeft, CheckCircle2, ChevronRight, Info, Minus, ShieldAlert, Tags } from "lucide-react";
import {
ArrowLeft,
ArrowRight,
CheckCircle2,
ChevronRight,
ChevronsUpDown,
Info,
Minus,
ShieldAlert,
Tags,
} from "lucide-react";

import { cn, eyebrowText, pageContainer } from "@/components/ui-primitives";
import type { SpecifierRecord } from "@/lib/specifiers";
import type { SpecifierFamily, SpecifierRecord } from "@/lib/specifiers";
import { specifierFamilies } from "@/lib/specifiers";
import type { SpecifierSourceStatus } from "@/lib/specifiers-search-index";

export const specifierCard =
Expand Down Expand Up @@ -51,10 +62,10 @@ export function SpecifierBreadcrumbs({ current }: { current?: string }) {

export function SpecifierSubnav({ active }: { active: "search" | "builder" | "compare" | "map" }) {
const items = [
{ id: "search" as const, label: "Find", href: "/specifiers" },
{ id: "builder" as const, label: "Build wording", href: "/specifiers/builder" },
{ id: "compare" as const, label: "Compare", href: "/specifiers/compare" },
{ id: "map" as const, label: "Map", href: "/specifiers/map" },
{ id: "search" as const, label: "Find", shortLabel: "Find", href: "/specifiers" },
{ id: "builder" as const, label: "Build wording", shortLabel: "Build", href: "/specifiers/builder" },
{ id: "compare" as const, label: "Compare", shortLabel: "Compare", href: "/specifiers/compare" },
{ id: "map" as const, label: "Map", shortLabel: "Map", href: "/specifiers/map" },
];

return (
Expand All @@ -66,6 +77,7 @@ export function SpecifierSubnav({ active }: { active: "search" | "builder" | "co
<Link
key={item.id}
href={item.href}
aria-label={item.label}
aria-current={active === item.id ? "page" : undefined}
className={cn(
"inline-flex min-h-tap shrink-0 items-center justify-center rounded-md px-3 text-xs font-bold transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] sm:text-sm",
Expand All @@ -74,13 +86,168 @@ export function SpecifierSubnav({ active }: { active: "search" | "builder" | "co
: "text-[color:var(--text-muted)] hover:bg-[color:var(--surface)] hover:text-[color:var(--text)]",
)}
>
{item.label}
<span className="sm:hidden" aria-hidden>
{item.shortLabel}
</span>
<span className="hidden sm:inline" aria-hidden>
{item.label}
</span>
</Link>
))}
</nav>
);
}

const familyChipBase =
"inline-flex min-h-tap shrink-0 items-center rounded-lg border px-3 text-xs font-semibold transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] sm:text-sm";
const familyChipActive =
"border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]";
const familyChipIdle =
"border-[color:var(--border)] bg-[color:var(--surface-raised)] text-[color:var(--text-muted)] hover:border-[color:var(--border-strong)] hover:text-[color:var(--text-heading)]";

export function SpecifierFamilyFilterChips({
value,
onChange,
}: {
value: "all" | SpecifierFamily;
onChange: (value: "all" | SpecifierFamily) => void;
}) {
return (
<div
role="group"
aria-label="Filter by specifier family"
className="polished-scroll flex w-full min-w-0 gap-1.5 overflow-x-auto sm:flex-1"
>
{specifierFamilies.map((option) => {
const active = value === option.id;
return (
<button
key={option.id}
type="button"
onClick={() => onChange(option.id)}
aria-pressed={active}
className={cn(familyChipBase, active ? familyChipActive : familyChipIdle)}
>
<span className="sm:hidden">{option.shortLabel}</span>
<span className="hidden sm:inline">{option.label}</span>
</button>
);
})}
</div>
);
}

export function SpecifierDiagnosisFilter({
value,
onChange,
options,
}: {
value: string;
onChange: (value: string) => void;
options: Array<{ value: string; label: string }>;
}) {
return (
<label
className={cn(
// Content-sized control: wide enough for “All diagnoses” without becoming a full-width field.
"relative inline-flex min-h-tap w-auto max-w-full shrink-0 items-center gap-1.5 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] py-1 pl-2.5 pr-7 text-xs font-bold shadow-[var(--shadow-inset)]",
"focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[color:var(--focus)]",
)}
>
<span className="shrink-0 text-[color:var(--text-soft)]">Diagnosis</span>
<select
value={value}
onChange={(event) => onChange(event.target.value)}
aria-label="Filter by diagnosis"
className="w-40 max-w-[min(100%,12rem)] cursor-pointer appearance-none bg-transparent text-xs font-bold text-[color:var(--text)] outline-none [-webkit-appearance:none] sm:w-44"
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<ChevronsUpDown
className="pointer-events-none absolute right-2 size-icon-sm text-[color:var(--text-soft)]"
aria-hidden
/>
</label>
);
}

export function SpecifierMatchCard({ record, isTopMatch }: { record: SpecifierRecord; isTopMatch: boolean }) {
const typicalLanguage = record.patientLanguage[0]?.trim();

return (
<article
className={cn(
specifierCard,
"group overflow-hidden transition hover:border-[color:var(--clinical-accent-border)] hover:shadow-[var(--shadow-soft)] motion-reduce:transition-none",
isTopMatch && "border-l-[3px] border-l-[color:var(--clinical-accent)]",
)}
>
<Link
href={`/specifiers/${record.slug}`}
aria-label={`Open ${record.name}`}
className="block focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)]"
>
<div className="grid gap-3 p-3.5 sm:grid-cols-[minmax(0,1fr)_minmax(13rem,0.5fr)] sm:items-start sm:gap-4 sm:p-5">
<div className="min-w-0">
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<span className="text-lg font-extrabold text-[color:var(--text-heading)] transition group-hover:text-[color:var(--clinical-accent)] motion-reduce:transition-none sm:text-xl">
{record.name}
</span>
{isTopMatch ? (
<span className="inline-flex min-h-6 items-center gap-1 rounded-md bg-[color:var(--success-soft)] px-2 text-2xs font-extrabold text-[color:var(--success)]">
<CheckCircle2 className="size-icon-xs" aria-hidden />
Top match
</span>
) : null}
</div>
<ArrowRight
className="mt-1 size-icon-md shrink-0 text-[color:var(--text-soft)] transition group-hover:translate-x-0.5 group-hover:text-[color:var(--clinical-accent)] motion-reduce:transition-none motion-reduce:group-hover:translate-x-0"
aria-hidden
/>
</div>
<p className="mt-1.5 max-w-3xl text-sm font-medium leading-6 text-[color:var(--text-muted)]">
{record.summary}
</p>
<div className="mt-2.5 flex flex-wrap gap-1.5">
<SpecifierFamilyBadge record={record} />
<DiagnosisChips values={record.appliesTo.slice(0, 2)} />
</div>
</div>

<div className="rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-subtle)] px-3 py-2.5 sm:mt-0.5">
<p className={eyebrowText}>Deciding signal</p>
<p className="mt-1 text-sm font-semibold leading-5 text-[color:var(--text-heading)]">
{record.clinicalSignal}
</p>
</div>
</div>

<div className="grid border-t border-[color:var(--border)] bg-[color:var(--surface-subtle)]/55 sm:grid-cols-2">
<div className="px-3 py-2.5 sm:px-5 sm:py-3">
<p className={eyebrowText}>Ask this</p>
<p className="mt-1 text-xs font-medium leading-5 text-[color:var(--text-muted)] sm:text-sm">
{record.decisionQuestion}
</p>
</div>
{typicalLanguage ? (
<div className="border-t border-[color:var(--border)] px-3 py-2.5 sm:border-l sm:border-t-0 sm:px-5 sm:py-3">
<p className={eyebrowText}>Typical language</p>
<p className="mt-1 text-xs font-medium leading-5 text-[color:var(--text-muted)] sm:text-sm">
&ldquo;{typicalLanguage}&rdquo;
</p>
</div>
) : null}
</div>
</Link>
</article>
);
}

export function SpecifierFamilyBadge({ record }: { record: SpecifierRecord }) {
return (
<span className="inline-flex min-h-7 items-center gap-1.5 rounded-md border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-2 text-xs font-bold text-[color:var(--clinical-accent)]">
Expand Down
Loading