From ea4a86f836c1485e2cb7811bd6fac4f4aa478171 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 9 Aug 2026 10:27:04 +0000 Subject: [PATCH 1/7] Compact therapy search result cards for denser scanning. Close the band/results gap, keep tags on one relevance-sorted row, move the favourite control beside the title, and summarise match-cell copy so cards use less vertical space on phone and desktop. Co-authored-by: BigSimmo --- src/components/therapy-compass/data/select.ts | 57 +++++++++ .../therapy-compass/screens/search-screen.tsx | 4 +- .../therapy-compass/therapy-card.tsx | 112 +++++++++--------- src/components/therapy-compass/ui.tsx | 13 +- tests/therapy-card-preview.test.ts | 45 +++++++ tests/therapy-compass-mode-wiring.test.ts | 2 + ...herapy-compass-responsive-contract.test.ts | 11 ++ 7 files changed, 183 insertions(+), 61 deletions(-) create mode 100644 tests/therapy-card-preview.test.ts diff --git a/src/components/therapy-compass/data/select.ts b/src/components/therapy-compass/data/select.ts index 49ae511ca3..723b4bc035 100644 --- a/src/components/therapy-compass/data/select.ts +++ b/src/components/therapy-compass/data/select.ts @@ -40,6 +40,63 @@ export function summarise(text: string | null, sentences = 1): string { return parts.slice(0, sentences).join(" ").trim(); } +/** + * Compact card copy: skip a leading sentence that merely restates the therapy + * name (common in clinicalSummary), then return up to `maxSentences` of the + * remainder. Empty when nothing useful remains. + */ +export function cardPreviewText( + text: string | null | undefined, + options: { exclude?: string | null; maxSentences?: number } = {}, +): string { + if (!text) return ""; + const maxSentences = options.maxSentences ?? 1; + const exclude = (options.exclude ?? "").trim().toLowerCase().replace(/[.]+$/, ""); + const parts = text + .split(/(?<=\.)\s+/) + .map((part) => part.trim()) + .filter(Boolean); + + const useful = parts.filter((part) => { + if (!exclude) return true; + const normalized = part.toLowerCase().replace(/[.]+$/, "").trim(); + return normalized !== exclude && !normalized.startsWith(`${exclude}.`); + }); + + return useful.slice(0, maxSentences).join(" ").trim(); +} + +/** + * Surface filter/query-relevant tags first so a one-row TagRow shows what + * matched the search rather than the catalogue's domain prefix. + */ +export function prioritiseTherapyTags( + tags: string[], + options: { query?: string; activeTags?: string[] } = {}, +): string[] { + if (!tags.length) return tags; + const active = new Set((options.activeTags ?? []).map((tag) => tag.toLowerCase())); + const tokens = (options.query ?? "") + .toLowerCase() + .split(/[^a-z0-9/+-]+/) + .map((token) => token.trim()) + .filter((token) => token.length >= 2); + + const rank = (tag: string) => { + const lower = tag.toLowerCase(); + if (active.has(lower)) return 0; + if (tokens.some((token) => lower === token || lower.includes(token) || token.includes(lower))) { + return 1; + } + return 2; + }; + + return tags + .map((tag, index) => ({ tag, index, rank: rank(tag) })) + .sort((a, b) => a.rank - b.rank || a.index - b.index) + .map((entry) => entry.tag); +} + export function reviewStatusMeta(status: string): { label: string; tone: "warning" | "success" | "neutral" } { if (status === "reviewed") return { label: "Reviewed", tone: "success" }; if (status === "needs_review") return { label: "Needs source review", tone: "warning" }; diff --git a/src/components/therapy-compass/screens/search-screen.tsx b/src/components/therapy-compass/screens/search-screen.tsx index 5b97ef15a3..dcc1c11a16 100644 --- a/src/components/therapy-compass/screens/search-screen.tsx +++ b/src/components/therapy-compass/screens/search-screen.tsx @@ -47,7 +47,7 @@ export function SearchScreen() { const [filterOpen, setFilterOpen] = useState(false); return ( -
+
} filterControls={ -
+
{QUICK_TAGS.map((tag) => { const on = b.search.tags.includes(tag); return ( diff --git a/src/components/therapy-compass/therapy-card.tsx b/src/components/therapy-compass/therapy-card.tsx index 1c936172d1..601f08d920 100644 --- a/src/components/therapy-compass/therapy-card.tsx +++ b/src/components/therapy-compass/therapy-card.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from "react"; import { useTcBindings } from "./bindings"; -import { summarise } from "./data/select"; +import { cardPreviewText, prioritiseTherapyTags, summarise } from "./data/select"; import type { Therapy } from "./data/types"; import { accentControl, outlineControl, therapyBtn } from "./controls"; import { @@ -22,71 +22,69 @@ import { Eyebrow, IconTile, TagRow } from "./ui"; export function ResultCard({ therapy }: { therapy: Therapy }) { const b = useTcBindings(); const inCompare = b.isInCompare(therapy.slug); + const subtitle = + cardPreviewText(therapy.clinicalSummary, { exclude: therapy.name }) || + cardPreviewText(therapy.bestUsedFor, { exclude: therapy.name }) || + ""; + const tags = prioritiseTherapyTags(therapy.tags.length ? therapy.tags : [therapy.category], { + query: b.search.query, + activeTags: b.search.tags, + }); + const whyMatched = + cardPreviewText(therapy.bestUsedFor || therapy.indications, { exclude: therapy.name }) || + "Relevant to the current search."; + const avoidModify = + summarise(therapy.contraindicationsOrCautions, 1) || "Check source and review status before clinical use."; + const bestFit = + cardPreviewText(therapy.targetSymptoms || therapy.patientPopulation || therapy.setting, { + exclude: therapy.name, + }) || "See record for population fit."; + return (
-
-
- -
-

- {therapy.name} -

-

- {summarise(therapy.clinicalSummary, 1) || therapy.bestUsedFor || therapy.category} -

- +
+
+ +
+
+

+ {therapy.name} +

+ +
+ {subtitle ? ( +

+ {subtitle} +

+ ) : ( +
+ )} +
- - - -
- -
- + + +
-
- +
-
-

- {therapy.name} -

- -
+

+ {therapy.name} +

{subtitle ? (

{subtitle} @@ -77,29 +78,39 @@ export function ResultCard({ therapy }: { therapy: Therapy }) {

-
-
diff --git a/tests/therapy-compass-responsive-contract.test.ts b/tests/therapy-compass-responsive-contract.test.ts index 4ad8161fb7..72d801560f 100644 --- a/tests/therapy-compass-responsive-contract.test.ts +++ b/tests/therapy-compass-responsive-contract.test.ts @@ -164,8 +164,9 @@ describe("Therapy Compass responsive contract", () => { expect(therapyCardSource).toContain("prioritiseTherapyTags"); expect(therapyCardSource).toContain("cardPreviewText"); expect(therapyCardSource).toContain("line-clamp-2"); - expect(therapyCardSource).toContain("sm:grid-cols-3"); - // Heart lives beside the title, not a third desktop column. + expect(therapyCardSource).toContain("grid-cols-3"); + // Favourite is pinned to the card corner; no heart-only desktop column. + expect(therapyCardSource).toContain("absolute top-3 right-3"); expect(therapyCardSource).not.toMatch(/sm:grid-cols-\[minmax\([^)]+\),1fr\)_minmax\([^)]+\),1\.35fr\)_auto\]/); }); From 3db839a6bb1f5b45fc55bb732d21b30551a506b0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 9 Aug 2026 11:20:26 +0000 Subject: [PATCH 3/7] Use an allowed z-index ladder rung for the favourite control. Co-authored-by: BigSimmo --- src/components/therapy-compass/therapy-card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/therapy-compass/therapy-card.tsx b/src/components/therapy-compass/therapy-card.tsx index 26057047f8..df31b58aec 100644 --- a/src/components/therapy-compass/therapy-card.tsx +++ b/src/components/therapy-compass/therapy-card.tsx @@ -47,7 +47,7 @@ export function ResultCard({ therapy }: { therapy: Therapy }) {
-
+
From 52f07d49f89e6c786c624ccbd38ae552818a2071 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 9 Aug 2026 13:24:30 +0000 Subject: [PATCH 6/7] Fix therapy card review findings for merge readiness. Keep TagRow +N unclipped, exclude title/alias restatements and title-prefixed prose from card previews, and apply field fallbacks after preview filtering. Co-authored-by: BigSimmo --- src/components/therapy-compass/data/select.ts | 21 ++++++++++---- .../therapy-compass/therapy-card.tsx | 10 ++++--- src/components/therapy-compass/ui.tsx | 28 ++++++++++++++----- tests/therapy-card-preview.test.ts | 28 +++++++++++++++++++ ...herapy-compass-responsive-contract.test.ts | 16 +++++++++++ 5 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/components/therapy-compass/data/select.ts b/src/components/therapy-compass/data/select.ts index 723b4bc035..cc60850dde 100644 --- a/src/components/therapy-compass/data/select.ts +++ b/src/components/therapy-compass/data/select.ts @@ -40,6 +40,21 @@ export function summarise(text: string | null, sentences = 1): string { return parts.slice(0, sentences).join(" ").trim(); } +/** + * True when a sentence is only a therapy-name restatement, or starts with that + * name before a word boundary (alias suffixes like `(CT)` / `, DT`, or prose + * such as "Behavioural activation is…"). Prefix-sharing words without a + * boundary ("Behavioural activationism") stay. + */ +function isExcludedTitleSentence(part: string, exclude: string): boolean { + if (!exclude) return false; + const normalized = part.toLowerCase().replace(/[.]+$/, "").trim(); + if (!normalized) return false; + if (normalized === exclude) return true; + const escaped = exclude.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(`^${escaped}\\b`).test(normalized); +} + /** * Compact card copy: skip a leading sentence that merely restates the therapy * name (common in clinicalSummary), then return up to `maxSentences` of the @@ -57,11 +72,7 @@ export function cardPreviewText( .map((part) => part.trim()) .filter(Boolean); - const useful = parts.filter((part) => { - if (!exclude) return true; - const normalized = part.toLowerCase().replace(/[.]+$/, "").trim(); - return normalized !== exclude && !normalized.startsWith(`${exclude}.`); - }); + const useful = parts.filter((part) => !isExcludedTitleSentence(part, exclude)); return useful.slice(0, maxSentences).join(" ").trim(); } diff --git a/src/components/therapy-compass/therapy-card.tsx b/src/components/therapy-compass/therapy-card.tsx index 35297d5ede..be01d2fd7c 100644 --- a/src/components/therapy-compass/therapy-card.tsx +++ b/src/components/therapy-compass/therapy-card.tsx @@ -32,14 +32,16 @@ export function ResultCard({ therapy }: { therapy: Therapy }) { activeTags: b.search.tags, }); const whyMatched = - cardPreviewText(therapy.bestUsedFor || therapy.indications, { exclude: therapy.name }) || + cardPreviewText(therapy.bestUsedFor, { exclude: therapy.name }) || + cardPreviewText(therapy.indications, { exclude: therapy.name }) || "Relevant to the current search."; const avoidModify = summarise(therapy.contraindicationsOrCautions, 1) || "Check source and review status before clinical use."; const bestFit = - cardPreviewText(therapy.targetSymptoms || therapy.patientPopulation || therapy.setting, { - exclude: therapy.name, - }) || "See record for population fit."; + cardPreviewText(therapy.targetSymptoms, { exclude: therapy.name }) || + cardPreviewText(therapy.patientPopulation, { exclude: therapy.name }) || + cardPreviewText(therapy.setting, { exclude: therapy.name }) || + "See record for population fit."; const sheetLabel = therapy.patientSheetAvailable ? "Patient sheet" : "Sheet unavailable"; const sheetShort = therapy.patientSheetAvailable ? "Sheet" : "No sheet"; diff --git a/src/components/therapy-compass/ui.tsx b/src/components/therapy-compass/ui.tsx index 6f422a256c..c0546dcb4d 100644 --- a/src/components/therapy-compass/ui.tsx +++ b/src/components/therapy-compass/ui.tsx @@ -81,14 +81,28 @@ export function TagRow({ }) { const shown = tags.slice(0, max); const extra = tags.length - shown.length; + const pills = shown.map((tag) => ( + + {tag} + + )); + const overflow = extra > 0 ? +{extra} : null; + + // Single-row cards: keep `+N` as a non-shrinking sibling so overflow-hidden + // clips long tags, not the indicator that more tags exist. + if (!wrap) { + return ( +
+
{pills}
+ {overflow ? {overflow} : null} +
+ ); + } + return ( -
- {shown.map((tag) => ( - - {tag} - - ))} - {extra > 0 ? +{extra} : null} +
+ {pills} + {overflow}
); } diff --git a/tests/therapy-card-preview.test.ts b/tests/therapy-card-preview.test.ts index 63219d05ae..54a3b36f23 100644 --- a/tests/therapy-card-preview.test.ts +++ b/tests/therapy-card-preview.test.ts @@ -12,6 +12,34 @@ describe("cardPreviewText", () => { ); }); + it("skips title restatements that include a parenthetical or comma alias", () => { + expect( + cardPreviewText("Cognitive Therapy (CT). Classic strongest uses are depression and anxiety disorders.", { + exclude: "Cognitive Therapy", + }), + ).toBe("Classic strongest uses are depression and anxiety disorders."); + + expect( + cardPreviewText("Dignity therapy, DT. Best in palliative care and advanced illness.", { + exclude: "Dignity therapy", + }), + ).toBe("Best in palliative care and advanced illness."); + }); + + it("skips a title-prefixed prose sentence while keeping unrelated prefix-sharing words", () => { + expect( + cardPreviewText("Behavioural activation is a structured approach. Prefer when avoidance dominates.", { + exclude: "Behavioural activation", + }), + ).toBe("Prefer when avoidance dominates."); + + expect( + cardPreviewText("Behavioural activationism remains distinct from the named therapy.", { + exclude: "Behavioural activation", + }), + ).toBe("Behavioural activationism remains distinct from the named therapy."); + }); + it("returns empty when every sentence is the excluded title", () => { const name = "Behavioural activation"; expect(cardPreviewText(`${name}.`, { exclude: name })).toBe(""); diff --git a/tests/therapy-compass-responsive-contract.test.ts b/tests/therapy-compass-responsive-contract.test.ts index bd79df5b81..520119c069 100644 --- a/tests/therapy-compass-responsive-contract.test.ts +++ b/tests/therapy-compass-responsive-contract.test.ts @@ -172,7 +172,23 @@ describe("Therapy Compass responsive contract", () => { expect(therapyCardSource).toContain("grid-cols-3"); // Favourite is pinned to the card corner; no heart-only desktop column. expect(therapyCardSource).toContain("absolute top-3 right-3"); + // Two-column card body waits for `md` so 640–700px viewports do not overflow. + expect(therapyCardSource).toContain("md:grid-cols-[minmax(240px,1fr)_minmax(320px,1.35fr)]"); + expect(therapyCardSource).not.toMatch(/sm:grid-cols-\[minmax\([^)]+\),1fr\)_minmax\([^)]+\),1\.35fr\)/); expect(therapyCardSource).not.toMatch(/sm:grid-cols-\[minmax\([^)]+\),1fr\)_minmax\([^)]+\),1\.35fr\)_auto\]/); + // Fallbacks run after preview filtering so a title-only first field can yield. + expect(therapyCardSource).toMatch( + /cardPreviewText\(therapy\.bestUsedFor,\s*\{\s*exclude:\s*therapy\.name\s*\}\)\s*\|\|/, + ); + expect(therapyCardSource).toMatch(/cardPreviewText\(therapy\.indications,\s*\{\s*exclude:\s*therapy\.name\s*\}\)/); + }); + + it("keeps the single-row TagRow overflow indicator unclipped", () => { + const uiSource = read(`${therapyPath}/ui.tsx`); + expect(uiSource).toContain("flex-nowrap gap-2 overflow-hidden"); + expect(uiSource).toContain("+{extra}"); + // `+N` is a shrink-0 sibling outside the clipping flex row. + expect(uiSource).toMatch(/overflow-hidden[\s\S]*?\{pills\}[\s\S]*?shrink-0[\s\S]*?\{overflow\}/); }); it("uses complete toggle semantics and preserves full-size control hit targets", () => { From 6c12d87a0e9150f2518e7dcf65cee8ff65a3c813 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 9 Aug 2026 13:24:59 +0000 Subject: [PATCH 7/7] Record PR 1783 babysit review in the branch-review ledger. Co-authored-by: BigSimmo --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 42e4de3e4d..5aa60107f1 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -850,3 +850,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-09 | claude/m3-token-debt-262-261 | 7bac3bd762b381cb25c9b2a15ef3bb7223d15b16 | PR #1780 review-and-fix | fixed P2 ratchet bypasses (arbitrary-property classes, CSS-consumer exemption anti-rot, modern CSS zero units); Bugbot clean; merge-tree clean; required CI was green on prior tip | vitest design-system-contract-utils 32/32; check:design-system-contract; mutation CSS-exemption fail→restore; verify:cheap PASS (549 files / 5933 tests); verify:pr-local stages PASS (test flake in design-system-adoption timed out once then 51/51 + full test 549/549 + check:rag:fixtures PASS); no provider gates | | 2026-08-09 | cursor/dsm-search-header-fix-15d6 | df088c766f1761496189ec09146aa54c23b1c012 | dsm-search-header | pass: removed catalogue page strip; ribbon + category filter match target | vitest dsm-search-empty-state; npm test 5857 passed; lint; typecheck; ensure phone /dsm/search?q=Delirium | | 2026-08-09 | claude/m3-token-debt-262-261 | fe75e6acade008e68f953e235cc035f2e5d9d216 | PR #1780 review-and-fix | fixed P2 ratchet bypasses; synced origin/main (#1775); Bugbot clean; merge-tree clean | vitest design-system-contract-utils 32/32; check:design-system-contract; mutation CSS-exemption; verify:cheap PASS 549/5933; verify:pr-local stages PASS after adoption flake retest; check:rag:fixtures PASS; no provider gates | +| 2026-08-09 | cursor/therapy-card-densify-e975 | 52f07d49f89e6c786c624ccbd38ae552818a2071 | PR 1783 babysit | fixed review threads: TagRow +N clip, title/alias preview exclusion, preview field fallbacks; Copilot md grid kept; CI re-triggered after Copilot tip | npm test: 5958 passed / 4 skipped |