diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 4583a93dd..b0b050ff5 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -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. | diff --git a/src/components/specifiers/specifier-ui.tsx b/src/components/specifiers/specifier-ui.tsx index b7ccfdd07..09ab25f8f 100644 --- a/src/components/specifiers/specifier-ui.tsx +++ b/src/components/specifiers/specifier-ui.tsx @@ -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 = @@ -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 ( @@ -66,6 +77,7 @@ export function SpecifierSubnav({ active }: { active: "search" | "builder" | "co - {item.label} + + {item.shortLabel} + + + {item.label} + ))} ); } +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 ( +
+ {specifierFamilies.map((option) => { + const active = value === option.id; + return ( + + ); + })} +
+ ); +} + +export function SpecifierDiagnosisFilter({ + value, + onChange, + options, +}: { + value: string; + onChange: (value: string) => void; + options: Array<{ value: string; label: string }>; +}) { + return ( + + ); +} + +export function SpecifierMatchCard({ record, isTopMatch }: { record: SpecifierRecord; isTopMatch: boolean }) { + const typicalLanguage = record.patientLanguage[0]?.trim(); + + return ( +
+ +
+
+
+
+ + {record.name} + + {isTopMatch ? ( + + + Top match + + ) : null} +
+ +
+

+ {record.summary} +

+
+ + +
+
+ +
+

Deciding signal

+

+ {record.clinicalSignal} +

+
+
+ +
+
+

Ask this

+

+ {record.decisionQuestion} +

+
+ {typicalLanguage ? ( +
+

Typical language

+

+ “{typicalLanguage}” +

+
+ ) : null} +
+ +
+ ); +} + export function SpecifierFamilyBadge({ record }: { record: SpecifierRecord }) { return ( diff --git a/src/components/specifiers/specifiers-home-page.tsx b/src/components/specifiers/specifiers-home-page.tsx index 9d29bee15..f4f194c84 100644 --- a/src/components/specifiers/specifiers-home-page.tsx +++ b/src/components/specifiers/specifiers-home-page.tsx @@ -4,7 +4,6 @@ import Link from "next/link"; import { useMemo, useState } from "react"; import { ArrowRight, - CheckCircle2, ChevronRight, GitCompareArrows, ListChecks, @@ -17,10 +16,11 @@ import { import { ModeHomeMain, ModeHomeTemplate, ModeHomeVerificationFooter } from "@/components/mode-home-template"; import { CategoryTag, - DiagnosisChips, ReviewStatusBadge, SpecifierBreadcrumbs, - SpecifierFamilyBadge, + SpecifierDiagnosisFilter, + SpecifierFamilyFilterChips, + SpecifierMatchCard, SpecifierPageShell, SpecifierSafetyNote, SpecifierSubnav, @@ -29,7 +29,7 @@ import { import { cn, eyebrowText } from "@/components/ui-primitives"; import { appModeHomeHref } from "@/lib/app-modes"; import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; -import { searchSpecifiers, specifierFamilies, specifierSearchPresets, type SpecifierFamily } from "@/lib/specifiers"; +import { searchSpecifiers, specifierSearchPresets, type SpecifierFamily } from "@/lib/specifiers"; import { searchSpecifierCatalog, type SpecifierCatalogMatch } from "@/lib/specifiers-search-index"; // The curated set covers a small number of high-signal mood-episode specifiers. @@ -255,16 +255,12 @@ function SpecifierResults({ query }: { query: string }) { -
-
+
+

Specifier search

Matches for “{query}”

-

- Results ranked by text relevance: title, keywords, episode timing, and patient language. Open a result to - check exclusions and wording. -

{totalMatches} {totalMatches === 1 ? "match" : "matches"} @@ -273,44 +269,10 @@ function SpecifierResults({ query }: { query: string }) {

-
- {specifierFamilies.map((option) => { - const active = family === option.id; - return ( - - ); - })} -
- + +
{totalMatches === 0 ? ( @@ -318,73 +280,7 @@ function SpecifierResults({ query }: { query: string }) { ) : results.length > 0 ? (
{results.map(({ record }, index) => ( -
-
-
-
- - {record.name} - - {index === 0 ? ( - - - Top match - - ) : null} -
-

- {record.summary} -

-
- - -
-
- -
-

Deciding signal

-

- {record.clinicalSignal} -

-
- - - Open - - -
-
-
-

Ask this

-

- {record.decisionQuestion} -

-
-
-

Typical language

-

- “{record.patientLanguage[0]}” -

-
-
-
+ ))}
) : null} diff --git a/tests/ui-specifiers.spec.ts b/tests/ui-specifiers.spec.ts index 5da4ee90c..dfffc3867 100644 --- a/tests/ui-specifiers.spec.ts +++ b/tests/ui-specifiers.spec.ts @@ -75,11 +75,13 @@ test("searches clinical language without provenance fields and carries a result await expect(page).toHaveURL(/\/specifiers\?.*q=depressed(?:\+|%20)but(?:\+|%20)racing(?:\+|%20)thoughts.*run=1/); await expect(page.getByRole("heading", { name: /Matches for “depressed but racing thoughts”/ })).toBeVisible(); - await expect(page.getByText(/Results ranked by text relevance/i)).toBeVisible(); + await expect(page.getByText(/Results ranked by text relevance/i)).toHaveCount(0); await expect(page.getByText("Top match", { exact: true })).toBeVisible(); + await expect(page.getByRole("group", { name: "Filter by specifier family" })).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Filter by diagnosis" })).toBeVisible(); await expect(page.getByText("Best fit", { exact: true })).toHaveCount(0); await expect(page.getByText(/clinical fit/i)).toHaveCount(0); - await expect(page.getByRole("link", { name: "With mixed features", exact: true })).toBeVisible(); + await expect(page.getByRole("link", { name: "Open With mixed features" })).toBeVisible(); await expect(page.getByText("Source status", { exact: true })).toHaveCount(0); await expect(page.getByText("Source", { exact: true })).toHaveCount(0); @@ -101,11 +103,20 @@ test("keeps mobile search, filters, results, and the fixed composer usable", asy await gotoApp(page, "/specifiers?q=returns+every+winter&run=1"); await expect(page.getByRole("heading", { name: /Matches for “returns every winter”/ })).toBeVisible(); - await expect(page.getByRole("link", { name: "With seasonal pattern", exact: true })).toBeVisible(); + await expect(page.getByRole("link", { name: "Open With seasonal pattern" })).toBeVisible(); + await expect(page.getByRole("group", { name: "Filter by specifier family" })).toBeVisible(); await expect(page.getByRole("combobox", { name: "Filter by diagnosis" })).toBeVisible(); await expect(page.getByTestId("global-search-input").filter({ visible: true }).first()).toBeVisible(); await expect(page.getByText("Source status", { exact: true })).toHaveCount(0); await expect(page.getByText("Source", { exact: true })).toHaveCount(0); + + const courseFilter = page.getByRole("button", { name: /^(Course|Course and onset)$/ }); + await courseFilter.click(); + await expect(courseFilter).toHaveAttribute("aria-pressed", "true"); + await expect(page.getByRole("link", { name: "Open With seasonal pattern" })).toBeVisible(); + + await page.getByRole("combobox", { name: "Filter by diagnosis" }).selectOption("depressive"); + await expect(page.getByRole("link", { name: "Open With seasonal pattern" })).toBeVisible(); await expectNoHorizontalOverflow(page); await page.getByRole("link", { name: "Open With seasonal pattern" }).click();