diff --git a/src/components/therapy-compass/bindings.tsx b/src/components/therapy-compass/bindings.tsx index 9e95fc6db..31dc2dac4 100644 --- a/src/components/therapy-compass/bindings.tsx +++ b/src/components/therapy-compass/bindings.tsx @@ -16,7 +16,7 @@ import { import type { Pathway, ReferenceData, Therapy } from "./data/types"; const KNOWN_SCREENS = ["search", "detail", "compare", "recommend", "pathways", "brief", "home", "sheets"] as const; -const MAX_COMPARE = 4; +export const MAX_COMPARE = 4; type SheetSectionKey = "about" | "steps" | "practice" | "coping" | "contacts"; diff --git a/src/components/therapy-compass/icons.tsx b/src/components/therapy-compass/icons.tsx index 0b2606e7f..400fb504e 100644 --- a/src/components/therapy-compass/icons.tsx +++ b/src/components/therapy-compass/icons.tsx @@ -135,6 +135,7 @@ export const CopyIcon = makeIcon( >, ); +export const CheckIcon = makeIcon(, 2); export const PrinterIcon = makeIcon( , ); diff --git a/src/components/therapy-compass/screens/brief-screen.tsx b/src/components/therapy-compass/screens/brief-screen.tsx index a624f6128..738a500a1 100644 --- a/src/components/therapy-compass/screens/brief-screen.tsx +++ b/src/components/therapy-compass/screens/brief-screen.tsx @@ -5,9 +5,10 @@ import { useMemo, useState } from "react"; import { useTcBindings } from "../bindings"; import { commandControl, outlineControl } from "../controls"; import { parseSteps, summarise } from "../data/select"; -import { AlertIcon, CopyIcon, ExternalLinkIcon, FileTextIcon, SaveIcon, SearchIcon } from "../icons"; +import { AlertIcon, CheckIcon, CopyIcon, ExternalLinkIcon, FileTextIcon, SearchIcon } from "../icons"; import { s } from "../style-utils"; import { LoadingState } from "../ui"; +import { useClipboard } from "../use-clipboard"; const CHECKLIST = [ "Confirm the primary problem", @@ -20,6 +21,7 @@ export function BriefScreen() { const b = useTcBindings(); const t = b.selectedTherapy; const [filter, setFilter] = useState(""); + const { copied, copy } = useClipboard(); const briefTherapies = useMemo( () => @@ -41,6 +43,20 @@ export function BriefScreen() { t.briefVersion : t.briefVersion; const steps = parseSteps(durationText, 6); + const interventionText = [ + `${t.name} — ${durationLabel} intervention`, + "", + ...steps.map((st, i) => `${i + 1}. ${st}`), + ...(t.clinicianScripts.length + ? [ + "", + "Clinician script:", + ...t.clinicianScripts + .slice(0, 2) + .map((c) => (c.scriptType ? `${c.scriptType}: ${c.body ?? ""}` : (c.body ?? ""))), + ] + : []), + ].join("\n"); return ( @@ -231,12 +247,13 @@ export function BriefScreen() { copy(step, `step-${i}`)} title="Copy step" style={s( - `display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--text-soft);flex:none;cursor:pointer;`, + `display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border:1px solid ${copied === `step-${i}` ? "var(--clinical-accent-border)" : "var(--border)"};border-radius:8px;background:var(--surface);color:${copied === `step-${i}` ? "var(--clinical-accent)" : "var(--text-soft)"};flex:none;cursor:pointer;`, )} > - + {copied === `step-${i}` ? : } @@ -315,13 +332,14 @@ export function BriefScreen() { - - - Copy intervention - - - - Save brief + copy(interventionText, "intervention")} + style={s(outlineControl + "height:46px;")} + > + {copied === "intervention" ? : } + {copied === "intervention" ? "Copied" : "Copy intervention"} { if (b.cmpTab === "priorities") return ROWS.filter((r) => r.priority); @@ -78,6 +81,16 @@ export function CompareScreen() { return ROWS; }, [b.cmpTab, items]); + const copySet = () => + copy( + [ + `Therapy comparison — ${items.map((t) => t.name).join(" vs ")}`, + "", + ...ROWS.map((r) => `${r.label}: ${items.map((t) => r.get(t)).join(" | ")}`), + ].join("\n"), + "set", + ); + const cols = `minmax(180px,1.1fr) ${items.map(() => "minmax(160px,1fr)").join(" ")}`; const dense = b.density === "dense"; const cellPad = dense ? "11px 16px" : "15px 20px"; @@ -115,9 +128,15 @@ export function CompareScreen() { Dense - - - Save set + + {copied === "set" ? : } + {copied === "set" ? "Copied" : "Copy set"} Clear @@ -310,12 +329,13 @@ function SummaryCell({ function AddPicker() { const b = useTcBindings(); const [q, setQ] = useState(""); + const atLimit = b.compareSlugs.length >= MAX_COMPARE; const matches = useMemo(() => { - if (!q.trim()) return []; + if (atLimit || !q.trim()) return []; return searchTherapies(b.therapies, { query: q, tags: [], briefOnly: false, sheetOnly: false, reviewedOnly: false }) .filter((t) => !b.isInCompare(t.slug)) .slice(0, 6); - }, [q, b]); + }, [q, b, atLimit]); return ( @@ -324,10 +344,13 @@ function AddPicker() { setQ(e.target.value)} - placeholder="Add a therapy to the comparison…" + disabled={atLimit} + placeholder={ + atLimit ? "Maximum of 4 selected — remove one to add another" : "Add a therapy to the comparison…" + } aria-label="Add a therapy to compare" style={s( - `width:100%;height:46px;padding:0 14px 0 40px;border:1px dashed var(--border-strong);border-radius:12px;background:var(--surface);color:var(--text);font-size:14px;font-family:inherit;outline:none;`, + `width:100%;height:46px;padding:0 14px 0 40px;border:1px dashed var(--border-strong);border-radius:12px;background:var(--surface);color:var(--text);font-size:14px;font-family:inherit;outline:none;${atLimit ? "opacity:0.6;cursor:not-allowed;" : ""}`, )} /> diff --git a/src/components/therapy-compass/screens/detail-screen.tsx b/src/components/therapy-compass/screens/detail-screen.tsx index 55b2d2f0e..96f4fc93a 100644 --- a/src/components/therapy-compass/screens/detail-screen.tsx +++ b/src/components/therapy-compass/screens/detail-screen.tsx @@ -4,7 +4,7 @@ import type { ReactNode } from "react"; import { useTcBindings } from "../bindings"; import { card, heroCard, outlineControl } from "../controls"; -import { complexityLabel, parseSteps } from "../data/select"; +import { complexityLabel, parseSteps, summarise } from "../data/select"; import type { Therapy } from "../data/types"; import { AlertIcon, @@ -16,7 +16,6 @@ import { DatabaseIcon, FileTextIcon, InfoIcon, - MessageIcon, PersonIcon, ScaleIcon, ShieldIcon, @@ -99,13 +98,13 @@ export function DetailScreen() { icon={ShieldIcon} eyebrow="USE WHEN" tone="accent" - text={t.bestUsedFor || t.indications || "See clinical record."} + text={summarise(t.bestUsedFor || t.indications, 1) || "See clinical record."} /> - {t.mechanism ? : null} {steps.length ? ( @@ -204,7 +202,6 @@ export function DetailScreen() { At a glance - React.ReactNode; + icon: (p: { size?: number; strokeWidth?: number }) => ReactNode; title: string; body: string; onClick: () => void; diff --git a/src/components/therapy-compass/screens/recommend-screen.tsx b/src/components/therapy-compass/screens/recommend-screen.tsx index d08b60b4e..2f39a9795 100644 --- a/src/components/therapy-compass/screens/recommend-screen.tsx +++ b/src/components/therapy-compass/screens/recommend-screen.tsx @@ -1,18 +1,40 @@ "use client"; +import type { ReactNode } from "react"; + import { useTcBindings } from "../bindings"; import { commandControl, outlineControl } from "../controls"; import { RECOMMEND_CONSTRAINTS, summarise } from "../data/select"; -import { ArrowRightIcon, SaveIcon, SearchIcon, ShieldIcon, SparkleIcon } from "../icons"; +import { ArrowRightIcon, CheckIcon, CopyIcon, SearchIcon, ShieldIcon, SparkleIcon } from "../icons"; import { s } from "../style-utils"; import { LoadingState } from "../ui"; +import { useClipboard } from "../use-clipboard"; export function RecommendScreen() { const b = useTcBindings(); + const { copied, copy } = useClipboard(); const ranked = b.recommendations; const top = ranked[0]?.therapy; const rest = ranked.slice(1, 6); + const copyShortlist = () => + copy( + [ + "Recommendation shortlist", + b.recQuery.trim() ? `Question: ${b.recQuery.trim()}` : "", + b.recConstraints.length + ? `Constraints: ${RECOMMEND_CONSTRAINTS.filter((c) => b.recConstraints.includes(c.key)) + .map((c) => c.label) + .join(", ")}` + : "", + "", + ...ranked.map((r, i) => `${i + 1}. ${r.therapy.name}`), + ] + .filter(Boolean) + .join("\n"), + "shortlist", + ); + return ( @@ -85,9 +107,15 @@ export function RecommendScreen() { `display:flex;align-items:center;justify-content:space-between;margin-top:18px;gap:12px;flex-wrap:wrap;`, )} > - - - Save workflow + + {copied === "shortlist" ? : } + {copied === "shortlist" ? "Copied" : "Copy shortlist"} @@ -266,7 +294,7 @@ function MatchCell({ eyebrow: string; text: string; tone?: "accent"; - children?: React.ReactNode; + children?: ReactNode; }) { const bg = tone === "accent" ? "var(--clinical-accent-soft)" : "var(--surface)"; const head = tone === "accent" ? "var(--clinical-accent-hover)" : "var(--clinical-accent)"; diff --git a/src/components/therapy-compass/screens/search-screen.tsx b/src/components/therapy-compass/screens/search-screen.tsx index 4c0625ee4..32828c326 100644 --- a/src/components/therapy-compass/screens/search-screen.tsx +++ b/src/components/therapy-compass/screens/search-screen.tsx @@ -2,7 +2,7 @@ import { useTcBindings } from "../bindings"; import { outlineControl, softControl } from "../controls"; -import { SearchIcon, SearchXIcon, SlidersIcon, XIcon } from "../icons"; +import { SearchIcon, SearchXIcon, XIcon } from "../icons"; import { s } from "../style-utils"; import { EmptyState, LoadingState } from "../ui"; import { ResultCard } from "../therapy-card"; @@ -39,10 +39,6 @@ export function SearchScreen() { )} /> - - - Filters - diff --git a/src/components/therapy-compass/screens/sheets-screen.tsx b/src/components/therapy-compass/screens/sheets-screen.tsx index 07a6d7593..7e9aad567 100644 --- a/src/components/therapy-compass/screens/sheets-screen.tsx +++ b/src/components/therapy-compass/screens/sheets-screen.tsx @@ -1,11 +1,10 @@ "use client"; -import { useMemo, useState } from "react"; +import { useMemo, useState, type ReactNode } from "react"; import { useTcBindings } from "../bindings"; -import { outlineControl } from "../controls"; import { parseSteps, searchTherapies } from "../data/select"; -import { ChevronDownIcon, PrinterIcon, SaveIcon, ScaleIcon, SearchIcon } from "../icons"; +import { ChevronDownIcon, PrinterIcon, ScaleIcon, SearchIcon } from "../icons"; import { s } from "../style-utils"; import { LoadingState } from "../ui"; @@ -44,10 +43,6 @@ export function SheetsScreen() { - - - Save draft - (null); + const timer = useRef | null>(null); + + useEffect( + () => () => { + if (timer.current) clearTimeout(timer.current); + }, + [], + ); + + const copy = useCallback( + (text: string, key = "default") => { + if (!copyText(text)) return; + setCopied(key); + if (timer.current) clearTimeout(timer.current); + timer.current = setTimeout(() => setCopied(null), resetMs); + }, + [resetMs], + ); + + return { copied, copy }; +}