diff --git a/src/app/therapy-compass/layout.tsx b/src/app/therapy-compass/layout.tsx index b479dacc..b691df24 100644 --- a/src/app/therapy-compass/layout.tsx +++ b/src/app/therapy-compass/layout.tsx @@ -3,14 +3,14 @@ import { Suspense, type ReactNode } from "react"; import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; import { TherapyCompassWorkspace } from "@/components/therapy-compass"; -// Therapy Compass keeps the universal header + rail but provides its own primary -// search surface (the in-tool therapy search), so the shared search composer is -// suppressed here. The workspace is mounted at the layout level so the therapy -// dataset and interaction state are shared across every /therapy-compass/* route, -// while each route renders its own screen into the workspace's main content. +// Therapy Compass uses the same universal header, rail, and responsive search +// composer as the other mode homes. The workspace is mounted at the layout level +// so the therapy dataset and interaction state are shared across every +// /therapy-compass/* route, while each route renders its own screen into the +// workspace's main content. export default function TherapyCompassLayout({ children }: { children: ReactNode }) { return ( - + {/* The workspace provider reads useSearchParams; an explicit boundary lets the route family prerender on its own, independent of the shell's Suspense. */} diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index b347d4c1..33dbc82c 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -250,6 +250,7 @@ function GlobalStandaloneSearchShellClient({ (searchMode === "specifiers" && pathname === "/specifiers") || (searchMode === "formulation" && pathname === "/formulation") || (searchMode === "factsheets" && pathname === "/factsheets") || + (searchMode === "therapy-compass" && pathname === "/therapy-compass") || (searchMode === "tools" && pathname === "/tools")); const isDifferentialPresentationWorkflow = pathname.startsWith("/differentials/presentations"); const shouldShowDesktopSidebar = !hideDesktopSidebar; diff --git a/src/components/therapy-compass/nav.tsx b/src/components/therapy-compass/nav.tsx index c5161d85..752bbef1 100644 --- a/src/components/therapy-compass/nav.tsx +++ b/src/components/therapy-compass/nav.tsx @@ -1,7 +1,6 @@ "use client"; import { useTcBindings } from "./bindings"; -import { AlertIcon } from "./icons"; import { s } from "./style-utils"; /** @@ -9,13 +8,12 @@ import { s } from "./style-utils"; * * The design shipped its own left sidebar, but inside this app that role is * already filled by the universal rail, so the bespoke rail is dropped. Its - * eight destinations are kept reachable through a horizontal, repo-idiomatic - * pill nav that sits at the top of the content (sticky under the global + * core destinations are kept reachable through a horizontal, repo-idiomatic + * pill nav that sits at the top of non-home content (sticky under the global * header) and scrolls horizontally on narrow viewports. */ export function TherapyCompassNav() { const b = useTcBindings(); - const reviewCount = b.reviewCount; return (
- {/* Compact tool identity (the design's top-bar brand, slimmed down) */} -
- - - - - - - - - - Therapy - - Source-grounded decision support - -
- {/* Screen nav — horizontal, scrollable */} - - {reviewCount > 0 ? ( - - ) : null}
); } diff --git a/src/components/therapy-compass/screens/home-screen.tsx b/src/components/therapy-compass/screens/home-screen.tsx index 4b67e09d..066495ce 100644 --- a/src/components/therapy-compass/screens/home-screen.tsx +++ b/src/components/therapy-compass/screens/home-screen.tsx @@ -1,12 +1,11 @@ "use client"; -import { useState, type ReactNode } from "react"; +import { FileText, Network, Search, ShieldCheck, Sparkles, Waypoints } from "lucide-react"; + +import { ModeHomeMain, ModeHomeTemplate, ModeHomeVerificationFooter } from "@/components/mode-home-template"; +import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; import { useTcBindings } from "../bindings"; -import { commandControl, linkButton } from "../controls"; -import type { Therapy } from "../data/types"; -import { ChevronRightIcon, CompassIcon, FileTextIcon, PathwayIcon, ScaleIcon, SearchIcon, SparkleIcon } from "../icons"; -import { s } from "../style-utils"; const SUGGESTIONS = [ "Anxiety in outpatient care", @@ -15,233 +14,53 @@ const SUGGESTIONS = [ "5-minute grounding", "Relapse prevention", ]; -const FEATURED_SLUGS = [ - "cognitive-behavioural-therapy-cbt", - "behavioural-activation", - "dialectical-behaviour-therapy-dbt", - "eye-movement-desensitisation-and-reprocessing-emdr", - "acceptance-and-commitment-therapy-act", - "interpersonal-psychotherapy-ipt", -]; export function HomeScreen() { const b = useTcBindings(); - const [query, setLocalQuery] = useState(""); - - const bySlug = new Map(b.therapies.map((t) => [t.slug, t])); - const featured: Therapy[] = FEATURED_SLUGS.map((sl) => bySlug.get(sl)).filter((t): t is Therapy => Boolean(t)); - const featuredList = (featured.length ? featured : b.therapies).slice(0, 6); - const pathways = b.pathways.slice(0, 3); - - const submit = () => b.submitQuery(query); - - return ( -
-
- - - -

- What therapy are you looking for? -

-

- Search {b.therapies.length || "200+"} source-grounded therapy records by problem, symptom, skill or population - — or jump into a clinical pathway. -

-
- -
- - setLocalQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") submit(); - }} - placeholder="Search problem, symptom, therapy, skill, population…" - aria-label="Search therapies" - style={s( - `width:100%;height:58px;padding:0 130px 0 50px;border:1px solid var(--border-strong);border-radius:15px;background:var(--surface);color:var(--text);font-size:16px;font-family:inherit;outline:none;box-shadow:var(--shadow-soft);`, - )} - /> - -
- -
- {SUGGESTIONS.map((sugg) => ( - - ))} -
- - {/* quick tools */} -
- - - -
- - {/* pathways */} -
-

Key clinical pathways

- -
-
- {pathways.map((p) => ( - - ))} -
- - {/* therapies */} -
-

- Frequently used therapies -

- -
-
- {featuredList.map((t) => ( - - ))} -
-
- ); -} -function QuickTool({ - icon: Icon, - title, - body, - onClick, -}: { - icon: (p: { size?: number; strokeWidth?: number }) => ReactNode; - title: string; - body: string; - onClick: () => void; -}) { return ( - + + ({ + label: suggestion, + onClick: () => b.submitQuery(suggestion), + icon: Network, + }))} + footer={ + + } + /> + ); } diff --git a/src/components/therapy-compass/workspace.tsx b/src/components/therapy-compass/workspace.tsx index 6ae79cea..b1a73141 100644 --- a/src/components/therapy-compass/workspace.tsx +++ b/src/components/therapy-compass/workspace.tsx @@ -1,6 +1,7 @@ "use client"; import type { ReactNode } from "react"; +import { usePathname } from "next/navigation"; import { ShieldCheck } from "lucide-react"; import { ModeHomeVerificationFooter } from "@/components/mode-home-template"; @@ -61,13 +62,31 @@ function TherapyCompassDataError() { ); } -function TherapyCompassMain({ children }: { children: ReactNode }) { +function TherapyCompassMain({ + children, + showFooter, + asMain, +}: { + children: ReactNode; + showFooter: boolean; + /** Home renders ModeHomeMain; keep a non-main shell so landmarks are not nested. */ + asMain: boolean; +}) { const b = useTcBindings(); + // Home normally leaves
to ModeHomeMain. On catalogue load failure the + // home child (and its landmark) is replaced by TherapyCompassDataError, so + // promote this shell to
for the error state only. + const useMainLandmark = asMain || Boolean(b.error); + const Tag = useMainLandmark ? "main" : "div"; + // Home padding comes from ModeHomeMain; non-home routes keep the workspace gutter. + // Error-on-home still needs the non-home gutter so the alert isn't flush. + const style = asMain || b.error ? s(`min-width:0;padding:32px 40px 40px;`) : s(`min-width:0;`); return ( -
+ {b.error ? : children} - -
+ {/* Home uses ModeHomeTemplate's verification footer; skip the workspace copy there. */} + {showFooter ? : null} + ); } @@ -77,10 +96,13 @@ function TherapyCompassMain({ children }: { children: ReactNode }) { * are shared across every `/therapy-compass/*` route, while each route renders * its own screen into the workspace's main content. The design's bespoke left * rail is dropped in favour of the app's universal rail; its destinations live in - * the horizontal in-content nav under the global header, and the content closes + * the horizontal in-content nav under the global header, and non-home routes close * with the universal clinical verification footer. */ export function TherapyCompassWorkspace({ children }: { children: ReactNode }) { + const pathname = usePathname(); + const isHome = pathname === "/therapy-compass"; + return ( @@ -88,8 +110,10 @@ export function TherapyCompassWorkspace({ children }: { children: ReactNode }) { className="tc-root" style={s(`min-height:calc(100dvh - 4rem);background:var(--surface-chrome);color:var(--text);`)} > - - {children} + {isHome ? null : } + + {children} + ); diff --git a/tests/therapy-compass-mode-wiring.test.ts b/tests/therapy-compass-mode-wiring.test.ts index 9b1493d5..25a892be 100644 --- a/tests/therapy-compass-mode-wiring.test.ts +++ b/tests/therapy-compass-mode-wiring.test.ts @@ -46,4 +46,33 @@ describe("Therapy Compass production-mode wiring", () => { expect(bindingsSrc).toMatch(/resolveRoute\(pathname\)/); expect(bindingsSrc).toMatch(/searchParams\.get\("q"\)/); }); + + it("keeps a single main landmark on the therapy home route", () => { + const workspaceSrc = readFileSync( + new URL("../src/components/therapy-compass/workspace.tsx", import.meta.url), + "utf8", + ); + const homeSrc = readFileSync( + new URL("../src/components/therapy-compass/screens/home-screen.tsx", import.meta.url), + "utf8", + ); + // Home uses ModeHomeMain; workspace must not wrap home in a second
. + expect(homeSrc).toMatch(/ModeHomeMain/); + expect(workspaceSrc).toMatch(/asMain=\{!isHome\}/); + expect(workspaceSrc).toContain("const useMainLandmark = asMain || Boolean(b.error);"); + expect(workspaceSrc).toContain('const Tag = useMainLandmark ? "main" : "div"'); + }); + + it("wires therapy-compass home into the shared desktop composer portal", () => { + const shellSrc = readFileSync( + new URL("../src/components/clinical-dashboard/global-search-shell.tsx", import.meta.url), + "utf8", + ); + const homeSrc = readFileSync( + new URL("../src/components/therapy-compass/screens/home-screen.tsx", import.meta.url), + "utf8", + ); + expect(homeSrc).toContain("desktopComposerSlotId={modeHomeDesktopComposerSlotId}"); + expect(shellSrc).toContain('searchMode === "therapy-compass" && pathname === "/therapy-compass"'); + }); });