From 341c3c9dd1c7a2e4dc3b0afc3f730243b6c67850 Mon Sep 17 00:00:00 2001
From: Claude
Date: Mon, 24 Aug 2026 10:40:50 +0000
Subject: [PATCH 1/7] Add three phone mockups for the therapy comparison picker
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The phone compare screen opens with four empty A/B/C/D slot cards, a
`Change therapies` button and an `Add therapies to compare` empty state —
three affordances for one outcome, with the primary one pushed below the
fold by ~1,100px of placeholders. The toolbar (density, Copy set, Clear)
renders disabled above it all, and the payoff is a 720px-minimum table in
a horizontal scroller.
Three design-scratch directions, each a live phone prototype:
- A `therapy-compare-progressive` — Add as you go. Empty slots are never
drawn; one button and three starter pairs, then a row per therapy chosen.
- B `therapy-compare-sheet` — Build the set in one sheet. Full-height
multi-select with search, category chips and a draft that commits once.
- C `therapy-compare-tray` — Carry a compare tray. Selection leaves the
page body for a persistent bottom tray filled from search and records.
All three share the same fixtures, a per-field stacked comparison in place
of the sideways table, and the same six-defect brief. Mockup-only arbitrary
Tailwind values are inlined: globals.css excludes `*mockup*` sources, so a
utility used nowhere else in `src` is never emitted and silently collapses
the frame (the shipped therapy-navigation mockups have the same issue).
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01WKxKU17WQqryM8iNhpyJk7
---
docs/site-map.md | 3 +
src/app/mockups/mockups-layout-client.tsx | 7 +
.../therapy-compare-progressive/page.tsx | 12 +
.../mockups/therapy-compare-sheet/page.tsx | 13 +
src/app/mockups/therapy-compare-tray/page.tsx | 13 +
.../progressive.tsx | 391 ++++++++++++
.../therapy-compare-picker-mockups/shared.tsx | 581 ++++++++++++++++++
.../therapy-compare-picker-mockups/sheet.tsx | 472 ++++++++++++++
.../therapy-compare-picker-mockups/tray.tsx | 438 +++++++++++++
9 files changed, 1930 insertions(+)
create mode 100644 src/app/mockups/therapy-compare-progressive/page.tsx
create mode 100644 src/app/mockups/therapy-compare-sheet/page.tsx
create mode 100644 src/app/mockups/therapy-compare-tray/page.tsx
create mode 100644 src/components/therapy-compare-picker-mockups/progressive.tsx
create mode 100644 src/components/therapy-compare-picker-mockups/shared.tsx
create mode 100644 src/components/therapy-compare-picker-mockups/sheet.tsx
create mode 100644 src/components/therapy-compare-picker-mockups/tray.tsx
diff --git a/docs/site-map.md b/docs/site-map.md
index 33f300dc51..06cfd15d8e 100644
--- a/docs/site-map.md
+++ b/docs/site-map.md
@@ -1240,6 +1240,9 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir
- `/mockups/settings-search-general` - Route discovered from app directory Source: `src/app/mockups/settings-search-general/page.tsx`.
- `/mockups/settings-search-privacy` - Route discovered from app directory Source: `src/app/mockups/settings-search-privacy/page.tsx`.
- `/mockups/sidebar-live` - Route discovered from app directory Source: `src/app/mockups/sidebar-live/page.tsx`.
+- `/mockups/therapy-compare-progressive` - Route discovered from app directory Source: `src/app/mockups/therapy-compare-progressive/page.tsx`.
+- `/mockups/therapy-compare-sheet` - Route discovered from app directory Source: `src/app/mockups/therapy-compare-sheet/page.tsx`.
+- `/mockups/therapy-compare-tray` - Route discovered from app directory Source: `src/app/mockups/therapy-compare-tray/page.tsx`.
- `/mockups/therapy-navigation-context` - Route discovered from app directory Source: `src/app/mockups/therapy-navigation-context/page.tsx`.
- `/mockups/therapy-navigation-dock` - Route discovered from app directory Source: `src/app/mockups/therapy-navigation-dock/page.tsx`.
- `/mockups/therapy-navigation-rail` - Route discovered from app directory Source: `src/app/mockups/therapy-navigation-rail/page.tsx`.
diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx
index baeddb5b94..2457811ba7 100644
--- a/src/app/mockups/mockups-layout-client.tsx
+++ b/src/app/mockups/mockups-layout-client.tsx
@@ -74,6 +74,11 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
// These studies render their own top bar and composer inside each device
// frame. Suppress shared chrome so it cannot be mistaken for the concept.
const isTherapyNavigationMockup = pathname.startsWith("/mockups/therapy-navigation-");
+ // These three draw a complete phone — universal header, page body and the
+ // edge-to-edge composer — inside every frame, because the selection surface
+ // under study is defined by what sits between those two. Shared chrome above
+ // them would read as a second real header and a second real search bar.
+ const isTherapyComparePickerMockup = pathname.startsWith("/mockups/therapy-compare-");
// The calculators search page owns its own search input (top on desktop, docked
// at the bottom on phones), so the shared universal composer is suppressed here
// to avoid a second, floating search bar.
@@ -124,6 +129,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
!isServicesFilterOptionsMockup &&
!isFilterSheetRestyleMockup &&
!isTherapyNavigationMockup &&
+ !isTherapyComparePickerMockup &&
!isWarningConsolidationMockup &&
!isWarningLineMockup &&
!isAnswerHomeProposalMockup &&
@@ -149,6 +155,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
!isFilterSheetRestyleMockup &&
!isPhoneInPageNavigationMockup &&
!isTherapyNavigationMockup &&
+ !isTherapyComparePickerMockup &&
!isWarningConsolidationMockup &&
!isWarningLineMockup &&
!isAnswerHomeProposalMockup &&
diff --git a/src/app/mockups/therapy-compare-progressive/page.tsx b/src/app/mockups/therapy-compare-progressive/page.tsx
new file mode 100644
index 0000000000..f689f60b67
--- /dev/null
+++ b/src/app/mockups/therapy-compare-progressive/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+
+import { TherapyCompareProgressiveMockups } from "@/components/therapy-compare-picker-mockups/progressive";
+
+export const metadata: Metadata = {
+ title: "Therapy comparison picker A · Add as you go - Clinical KB",
+ description: "Direction A for the phone therapy-comparison picker: Add as you go, shown as a live phone prototype.",
+};
+
+export default function TherapyCompareProgressiveMockupPage() {
+ return ;
+}
diff --git a/src/app/mockups/therapy-compare-sheet/page.tsx b/src/app/mockups/therapy-compare-sheet/page.tsx
new file mode 100644
index 0000000000..fa3da8e87e
--- /dev/null
+++ b/src/app/mockups/therapy-compare-sheet/page.tsx
@@ -0,0 +1,13 @@
+import type { Metadata } from "next";
+
+import { TherapyCompareSheetMockups } from "@/components/therapy-compare-picker-mockups/sheet";
+
+export const metadata: Metadata = {
+ title: "Therapy comparison picker B · Build the set in one sheet - Clinical KB",
+ description:
+ "Direction B for the phone therapy-comparison picker: Build the set in one sheet, shown as a live phone prototype.",
+};
+
+export default function TherapyCompareSheetMockupPage() {
+ return ;
+}
diff --git a/src/app/mockups/therapy-compare-tray/page.tsx b/src/app/mockups/therapy-compare-tray/page.tsx
new file mode 100644
index 0000000000..4d28d275b2
--- /dev/null
+++ b/src/app/mockups/therapy-compare-tray/page.tsx
@@ -0,0 +1,13 @@
+import type { Metadata } from "next";
+
+import { TherapyCompareTrayMockups } from "@/components/therapy-compare-picker-mockups/tray";
+
+export const metadata: Metadata = {
+ title: "Therapy comparison picker C · Carry a compare tray - Clinical KB",
+ description:
+ "Direction C for the phone therapy-comparison picker: Carry a compare tray, shown as a live phone prototype.",
+};
+
+export default function TherapyCompareTrayMockupPage() {
+ return ;
+}
diff --git a/src/components/therapy-compare-picker-mockups/progressive.tsx b/src/components/therapy-compare-picker-mockups/progressive.tsx
new file mode 100644
index 0000000000..56bcd1f570
--- /dev/null
+++ b/src/components/therapy-compare-picker-mockups/progressive.tsx
@@ -0,0 +1,391 @@
+"use client";
+
+import { ArrowLeft, Copy, Plus, Search, Trash2, X } from "lucide-react";
+import { useState } from "react";
+
+import { cn } from "@/components/ui-primitives";
+
+import {
+ AddedToast,
+ ComparisonPreview,
+ CurrentDefects,
+ MAX_COMPARE,
+ MockupSection,
+ MockupShell,
+ NoteCard,
+ PhoneComposer,
+ PhoneFrame,
+ PhoneTopBar,
+ ReviewPill,
+ SLOT_LETTERS,
+ SlotBadge,
+ THERAPY_COUNT,
+ bySlug,
+ filterTherapies,
+ focusRing,
+ useCompareSet,
+} from "./shared";
+
+/* ------------------------------------------------------------------ *
+ * Direction A — Add as you go.
+ *
+ * The screen shows what you have chosen and one way to add the next one.
+ * Zero selections means zero cards: a single primary button and two starter
+ * pairs. The slot letters still exist — they are what the comparison keys
+ * off — but they are minted as you fill them, never drawn empty.
+ * ------------------------------------------------------------------ */
+
+const STARTERS: Array<{ label: string; slugs: [string, string] }> = [
+ { label: "CBT vs ACT", slugs: ["cognitive-behavioural-therapy-cbt", "acceptance-and-commitment-therapy-act"] },
+ { label: "DBT vs MBT", slugs: ["dialectical-behaviour-therapy-dbt", "mentalisation-based-treatment-mbt"] },
+ {
+ label: "EMDR vs CPT",
+ slugs: ["eye-movement-desensitisation-and-reprocessing-emdr", "cognitive-processing-therapy-cpt"],
+ },
+];
+
+function ProgressivePhone() {
+ const set = useCompareSet();
+ const [picking, setPicking] = useState(false);
+ const [query, setQuery] = useState("");
+ const [toast, setToast] = useState(null);
+ const results = filterTherapies(query, "All");
+ const count = set.selected.length;
+
+ function choose(slug: string) {
+ set.add(slug);
+ setPicking(false);
+ setQuery("");
+ const therapy = bySlug(slug);
+ setToast(therapy ? `${therapy.short} added as ${SLOT_LETTERS[count] ?? "next"}` : null);
+ window.setTimeout(() => setToast(null), 1600);
+ }
+
+ return (
+
+
+
+
+ {/* Title block: one line, and a count that only exists once it counts. */}
+
+
+
Compare therapies
+
+ {count === 0
+ ? "Add two or more to see fit, cautions and delivery side by side."
+ : count === 1
+ ? "Add one more to start the comparison."
+ : `${count} of ${MAX_COMPARE} · fit, cautions, delivery and evidence.`}
+
+
+ {count > 0 ? (
+
+ ) : null}
+
+
+ {/* Chosen therapies — one row each, nothing more. */}
+ {count > 0 ? (
+
+ {set.items.map((therapy, index) => (
+
+
+
+
{therapy.name}
+
+ {therapy.category}
+
+
+
+
+
+
+
+ ))}
+
+ ) : null}
+
+ {/* The single add affordance. Primary while empty, quiet once filling. */}
+ {set.full ? (
+
+ {MAX_COMPARE} of {MAX_COMPARE} — remove one to swap in another
+
+ ) : (
+
+ )}
+
+ {/* Starter pairs — only while they can still fit. */}
+ {count === 0 ? (
+
+
+ Or start from a pair
+
+
+ {STARTERS.map((starter) => (
+
+ ))}
+
+
+ You can also add from a search result or from any therapy record — the set follows you.
+
+
+ ) : null}
+
+ {/* Everything that only makes sense with a comparison on screen. */}
+ {count >= 2 ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+
+
+
+
+
+ {/* Full-screen picker. One tap adds and returns. */}
+ {picking ? (
+
+ The phone below is live — tap it. Comparison copy is placeholder text for layout only, not clinical content;
+ every record in the real catalogue is still needs review, which is why each card says so.
+
+
+
+ {children}
+
+
+ );
+}
+
+export const currentDefects: Array<{ title: string; body: string }> = [
+ {
+ title: "Four empty slots before a single choice",
+ body: "The screen opens with A, B, C and D already drawn as cards. Nothing is selected, so all four are dead placeholders that push the only real action — the one that adds a therapy — below the fold. Four is the ceiling, not the plan.",
+ },
+ {
+ title: "Three controls that all do the same thing",
+ body: "Each empty slot card is tappable, `Change therapies` sits under them, and `Add therapies to compare` sits under that. Three affordances, one outcome, and the biggest and most prominent of them is the one you reach last.",
+ },
+ {
+ title: "The toolbar is disabled at the moment you see it",
+ body: "Comfortable/Dense, Copy set and Clear all render at full strength above the fold with nothing to act on. Density in particular decides how a wide table breathes — a decision that has no meaning until there is a table, and none at all on a 390 px screen.",
+ },
+ {
+ title: "The count is chrome, not feedback",
+ body: "`0 of 4 selected` is a static badge in the header, detached from the slots it counts, and in the captured screen it sits partly under the page's own edge control.",
+ },
+ {
+ title: "The slot cards do not fill the phone",
+ body: "The strip is `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`, but on the phone the cards still stop short of the right edge while the buttons beneath them run full width — so the column looks broken rather than deliberate.",
+ },
+ {
+ title: "The payoff is a sideways-scrolling table",
+ body: "Once two are chosen, the comparison is a `min-w-[720px]` table inside a horizontal scroller. On a 390 px viewport that is two thirds of a column at a time, with the field labels scrolling away from the values they label.",
+ },
+];
+
+export function CurrentDefects() {
+ return (
+
+
+ {currentDefects.map((defect) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/therapy-compare-picker-mockups/sheet.tsx b/src/components/therapy-compare-picker-mockups/sheet.tsx
new file mode 100644
index 0000000000..df687e6769
--- /dev/null
+++ b/src/components/therapy-compare-picker-mockups/sheet.tsx
@@ -0,0 +1,472 @@
+"use client";
+
+import { Check, Copy, Layers, Pencil, Plus, Scale, Search, X } from "lucide-react";
+import { useState } from "react";
+
+import { cn } from "@/components/ui-primitives";
+
+import {
+ CATEGORY_FILTERS,
+ ComparisonPreview,
+ CurrentDefects,
+ MAX_COMPARE,
+ MockupSection,
+ MockupShell,
+ NoteCard,
+ PhoneComposer,
+ PhoneFrame,
+ PhoneTopBar,
+ SLOT_LETTERS,
+ THERAPY_COUNT,
+ bySlug,
+ filterTherapies,
+ focusRing,
+ useCompareSet,
+} from "./shared";
+
+/* ------------------------------------------------------------------ *
+ * Direction B — Build the set in one sheet.
+ *
+ * The page body never holds a picker. Choosing is one full-height sheet:
+ * search, category filter, a multi-select list, and a docked bar that
+ * counts what you have and commits the whole set in one action. You return
+ * to the comparison, not to another empty slot.
+ * ------------------------------------------------------------------ */
+
+function SheetPhone() {
+ const set = useCompareSet();
+ const [open, setOpen] = useState(false);
+ const [draft, setDraft] = useState([]);
+ const [query, setQuery] = useState("");
+ const [category, setCategory] = useState("All");
+ const results = filterTherapies(query, category);
+
+ function openSheet() {
+ setDraft(set.selected);
+ setQuery("");
+ setCategory("All");
+ setOpen(true);
+ }
+
+ function toggleDraft(slug: string) {
+ setDraft((current) => {
+ if (current.includes(slug)) return current.filter((entry) => entry !== slug);
+ return current.length >= MAX_COMPARE ? current : [...current, slug];
+ });
+ }
+
+ function commit() {
+ set.setSelected(draft);
+ setOpen(false);
+ }
+
+ const count = set.selected.length;
+ const draftItems = draft
+ .map((slug) => bySlug(slug))
+ .filter((entry): entry is NonNullable => Boolean(entry));
+
+ return (
+
+
+
+
+
Compare therapies
+
+ {count === 0 ? (
+ <>
+
+ Choose two to four therapies and see fit, cautions, delivery and evidence together.
+
+
+
+
+
+ What you get
+
+
+
· Cautions first, then best fit, time and clinician skill
+
· Every field links back to the source record
+
· Copy the whole set as text for a letter or note
+
+
+ >
+ ) : (
+ <>
+ {/* The whole selection as one wrapping line of pills. */}
+
+
+
+ );
+}
From 767f38814947eacb53ba8097ffb95bc998a4776b Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Mon, 24 Aug 2026 10:49:13 +0000
Subject: [PATCH 2/7] fix(mockups): keep Direction C tray compare reachable and
honest
Leave compare when Empty or a row remove drops the tray below two, and
dim leftover Plus controls at the four-therapy ceiling so they no-op
visibly instead of looking live.
---
.../therapy-compare-picker-mockups/tray.tsx | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/src/components/therapy-compare-picker-mockups/tray.tsx b/src/components/therapy-compare-picker-mockups/tray.tsx
index e33262d4ec..181ccf56d2 100644
--- a/src/components/therapy-compare-picker-mockups/tray.tsx
+++ b/src/components/therapy-compare-picker-mockups/tray.tsx
@@ -77,6 +77,7 @@ function TrayPhone() {
{
+ set.clear();
+ setView("browse");
+ }}
aria-label="Empty the compare tray"
className={cn(
"inline-flex min-h-9 items-center gap-1 rounded-lg px-2 text-3xs font-bold text-[color:var(--text-muted)]",
@@ -195,7 +202,10 @@ function TrayPhone() {
set.remove(therapy.slug)}
+ onClick={() => {
+ set.remove(therapy.slug);
+ if (count <= 2) setView("browse");
+ }}
aria-label={`Remove ${therapy.short}`}
className={cn(
"grid h-11 w-11 shrink-0 place-items-center rounded-lg text-[color:var(--text-muted)]",
From c135aaa1c6f501c34c25a157dd879f8a5d83bee3 Mon Sep 17 00:00:00 2001
From: Claude
Date: Mon, 24 Aug 2026 10:52:01 +0000
Subject: [PATCH 3/7] Fix two Bugbot findings in the compare-tray mockup
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both confirmed against the running prototype before fixing.
1. Emptying or trimming the tray below two while reading a comparison left
the compare view mounted over a `ComparisonPreview` that renders null —
a blank screen, in the one direction whose whole claim is that an empty
compare screen is unreachable. The view is now derived (`showCompare`)
rather than latched, so dropping below two falls back to browse.
2. At four, the result-row plus controls still looked live and `add`
silently no-opped. They now carry `aria-disabled`, an explanatory title
and label, and 40% opacity, matching what the sheet and progressive
pickers already did; the tray heading reads `4 of 4 · full`. Focus order
is preserved — no native `disabled`, per the repo's wiring convention.
Also replaces the bare `opacity-40`/`opacity-45` dimming in all three
directions with inline styles. Production only ever uses those utilities
behind `disabled:`, so the bare form was never emitted and every "blocked"
row rendered at full strength — the same mockup-only utility trap as the
frame heights. Verified in Chromium: 0.4 / 0.4 / 0.45 computed, tabIndex 0,
`aria-disabled=true`, and Playwright refuses to click the tray's fifth
control ("element is not enabled").
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01WKxKU17WQqryM8iNhpyJk7
---
.../progressive.tsx | 3 ++-
.../therapy-compare-picker-mockups/sheet.tsx | 3 ++-
.../therapy-compare-picker-mockups/tray.tsx | 19 ++++++++++++++++---
3 files changed, 20 insertions(+), 5 deletions(-)
diff --git a/src/components/therapy-compare-picker-mockups/progressive.tsx b/src/components/therapy-compare-picker-mockups/progressive.tsx
index 56bcd1f570..ccede720dc 100644
--- a/src/components/therapy-compare-picker-mockups/progressive.tsx
+++ b/src/components/therapy-compare-picker-mockups/progressive.tsx
@@ -241,7 +241,8 @@ function ProgressivePhone() {
type="button"
onClick={() => (already ? undefined : choose(therapy.slug))}
aria-disabled={already || undefined}
- className={cn("flex w-full items-center gap-2 py-3 text-left", focusRing, already && "opacity-45")}
+ className={cn("flex w-full items-center gap-2 py-3 text-left", focusRing)}
+ style={already ? { opacity: 0.45 } : undefined}
>
diff --git a/src/components/therapy-compare-picker-mockups/sheet.tsx b/src/components/therapy-compare-picker-mockups/sheet.tsx
index df687e6769..b6b68414d1 100644
--- a/src/components/therapy-compare-picker-mockups/sheet.tsx
+++ b/src/components/therapy-compare-picker-mockups/sheet.tsx
@@ -247,7 +247,8 @@ function SheetPhone() {
onClick={() => (blocked ? undefined : toggleDraft(therapy.slug))}
aria-pressed={ticked}
aria-disabled={blocked || undefined}
- className={cn("flex w-full items-center gap-3 py-3 text-left", focusRing, blocked && "opacity-40")}
+ className={cn("flex w-full items-center gap-3 py-3 text-left", focusRing)}
+ style={blocked ? { opacity: 0.4 } : undefined}
>
query.trim() ? true : BROWSE_RESULTS.includes(therapy.slug),
);
+ // The claim this direction makes is that a compare screen is unreachable
+ // without a set, so emptying the tray while reading one has to fall back to
+ // browse rather than leave a blank comparison mounted.
+ const showCompare = view === "compare" && count >= 2;
return (
- {view === "browse" ? (
+ {!showCompare ? (
<>
@@ -90,12 +94,19 @@ function TrayPhone() {
(held ? set.remove(therapy.slug) : set.add(therapy.slug))}
+ onClick={() => {
+ if (held) set.remove(therapy.slug);
+ else if (!set.full) set.add(therapy.slug);
+ }}
aria-pressed={held}
+ aria-disabled={!held && set.full ? true : undefined}
+ title={!held && set.full ? `Tray full — remove one to add ${therapy.short}` : undefined}
aria-label={
held
? `Remove ${therapy.short} from the compare tray`
- : `Add ${therapy.short} to the compare tray`
+ : set.full
+ ? `Cannot add ${therapy.short} — the compare tray already holds ${MAX_COMPARE}`
+ : `Add ${therapy.short} to the compare tray`
}
className={cn(
"grid h-11 w-11 shrink-0 place-items-center rounded-full border",
@@ -104,6 +115,7 @@ function TrayPhone() {
? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent-hover)]"
: "border-[color:var(--border-strong)] text-[color:var(--clinical-accent)]",
)}
+ style={!held && set.full ? { opacity: 0.4 } : undefined}
>
{held ? (
@@ -166,6 +178,7 @@ function TrayPhone() {
Date: Mon, 24 Aug 2026 10:59:38 +0000
Subject: [PATCH 4/7] Draw the phone composer in the compare-tray prototype
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Direction C's own notes say the tray has to negotiate the bottom-chrome
contract with the edge-to-edge composer, but neither of its frames rendered
`PhoneComposer` — unlike Directions A and B. The prototype therefore hid
the single collision the direction most needs judging on, and showed it
with more viewport than production would allow.
The tray now sits in flow directly above the composer in both frames, to
scale, so the ~134 px of permanent bottom chrome is visible rather than
assumed away. The list padding drops from `pb-28` to `pb-4` because the
tray no longer overlays the content it sat on top of. The `warn` note is
rewritten to point at what the frames now show instead of describing it.
Re-verified in Chromium: composer present, compare view still reachable at
two, Empty still returns to browse, no page or console errors.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01WKxKU17WQqryM8iNhpyJk7
---
.../therapy-compare-picker-mockups/tray.tsx | 20 ++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/src/components/therapy-compare-picker-mockups/tray.tsx b/src/components/therapy-compare-picker-mockups/tray.tsx
index bd249baf7c..f9959255f5 100644
--- a/src/components/therapy-compare-picker-mockups/tray.tsx
+++ b/src/components/therapy-compare-picker-mockups/tray.tsx
@@ -12,6 +12,7 @@ import {
MockupSection,
MockupShell,
NoteCard,
+ PhoneComposer,
PhoneFrame,
PhoneTopBar,
ReviewPill,
@@ -59,7 +60,7 @@ function TrayPhone() {
return (
@@ -78,7 +79,7 @@ function TrayPhone() {
/>
-
+
{results.map((therapy) => {
const held = set.selected.includes(therapy.slug);
const blocked = !held && set.full;
@@ -135,7 +136,7 @@ function TrayPhone() {
>
) : (
-
+
setView("browse")}
@@ -167,7 +168,9 @@ function TrayPhone() {
)}
- {/* The tray. Present on every Therapy screen, above the composer. */}
+ {/* The tray. Present on every Therapy screen, and drawn in flow directly
+ above the real phone composer so the bottom-chrome stack this
+ direction has to negotiate is visible rather than assumed away. */}
{expanded ? (
) : null}
-
+
);
}
@@ -407,7 +413,7 @@ export function TherapyCompareTrayMockups() {
Date: Tue, 25 Aug 2026 03:41:14 +0000
Subject: [PATCH 5/7] Add the perfected compare-tray direction
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The tray was the strongest of the three ideas and the weakest execution.
This is the same model with its three real costs removed, added as a fourth
direction rather than replacing the original so the two can be compared.
- The tray does not exist until something is in it. An empty Therapy screen
is exactly what it is today: one composer and nothing more. The original
drew a 68px bar with four hollow dots before any choice had been made —
permanent furniture advertising a feature nobody was using yet.
- Tray and composer are one bottom stack that translates off together on
scroll-down and returns on scroll-up, with the content reserve going to
zero while hidden. That is the `docs/search-chrome-behaviour.md` contract
the original only described in prose.
- You can add from a therapy record, not just from a list. Tapping a name
opens the record and its primary action fills the tray without losing
your place. That is the entire argument for carrying a set, and the
original never showed it.
- Arrival is legible: the filled slot marks itself as it lands and the row
control flips to a tick, so a state change 600px from the thumb still
reads. Dropped entirely under `prefers-reduced-motion`.
The fixture list grows from 10 to 20 records because a short list cannot
demonstrate scroll-hide honestly — releasing the reserve shortens the
content, which un-scrolls a short list, which reveals the chrome again. The
real catalogue is 205 records and never does this; an 8-row mockup did, and
the loop was visible before the list was lengthened.
Verified in Chromium: no tray at zero; reserve 138px -> 12px and the stack
translating 126px on scroll-down, both restored on scroll-up; record-add
moving the tray to 3 of 4; compare reachable at two and unreachable below
it; no page or console errors.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01WKxKU17WQqryM8iNhpyJk7
---
docs/site-map.md | 1 +
.../therapy-compare-perfected/page.tsx | 13 +
.../therapy-compare-picker-mockups/shared.tsx | 98 ++-
.../tray-perfected.tsx | 643 ++++++++++++++++++
4 files changed, 754 insertions(+), 1 deletion(-)
create mode 100644 src/app/mockups/therapy-compare-perfected/page.tsx
create mode 100644 src/components/therapy-compare-picker-mockups/tray-perfected.tsx
diff --git a/docs/site-map.md b/docs/site-map.md
index e1ddae32c9..98d2aa85e4 100644
--- a/docs/site-map.md
+++ b/docs/site-map.md
@@ -1244,6 +1244,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir
- `/mockups/settings-search-general` - Route discovered from app directory Source: `src/app/mockups/settings-search-general/page.tsx`.
- `/mockups/settings-search-privacy` - Route discovered from app directory Source: `src/app/mockups/settings-search-privacy/page.tsx`.
- `/mockups/sidebar-live` - Route discovered from app directory Source: `src/app/mockups/sidebar-live/page.tsx`.
+- `/mockups/therapy-compare-perfected` - Route discovered from app directory Source: `src/app/mockups/therapy-compare-perfected/page.tsx`.
- `/mockups/therapy-compare-progressive` - Route discovered from app directory Source: `src/app/mockups/therapy-compare-progressive/page.tsx`.
- `/mockups/therapy-compare-sheet` - Route discovered from app directory Source: `src/app/mockups/therapy-compare-sheet/page.tsx`.
- `/mockups/therapy-compare-tray` - Route discovered from app directory Source: `src/app/mockups/therapy-compare-tray/page.tsx`.
diff --git a/src/app/mockups/therapy-compare-perfected/page.tsx b/src/app/mockups/therapy-compare-perfected/page.tsx
new file mode 100644
index 0000000000..1a85624414
--- /dev/null
+++ b/src/app/mockups/therapy-compare-perfected/page.tsx
@@ -0,0 +1,13 @@
+import type { Metadata } from "next";
+
+import { TherapyComparePerfectedMockups } from "@/components/therapy-compare-picker-mockups/tray-perfected";
+
+export const metadata: Metadata = {
+ title: "Therapy comparison picker C+ · Perfected tray - Clinical KB",
+ description:
+ "The perfected compare-tray direction: no tray until a set exists, one bottom stack that hides with the composer, and adding from a therapy record.",
+};
+
+export default function TherapyComparePerfectedMockupPage() {
+ return ;
+}
diff --git a/src/components/therapy-compare-picker-mockups/shared.tsx b/src/components/therapy-compare-picker-mockups/shared.tsx
index c84ccf5f36..a53481dbdf 100644
--- a/src/components/therapy-compare-picker-mockups/shared.tsx
+++ b/src/components/therapy-compare-picker-mockups/shared.tsx
@@ -143,6 +143,96 @@ export const THERAPIES: TherapyFixture[] = [
complexity: "Can be delivered by trained non-specialists",
caution: "Source review required — open the record.",
},
+ {
+ slug: "mindfulness-based-cognitive-therapy-mbct",
+ name: "Mindfulness-Based Cognitive Therapy (MBCT)",
+ short: "MBCT",
+ category: "Standard Talking Therapies",
+ fit: "Recurrent depression in remission; relapse prevention.",
+ time: "8 weekly sessions · group",
+ complexity: "Trained therapist",
+ caution: "Source review required — open the record.",
+ },
+ {
+ slug: "schema-therapy",
+ name: "Schema Therapy",
+ short: "Schema",
+ category: "Personality Disorder Therapies",
+ fit: "Longstanding relational patterns unresponsive to shorter work.",
+ time: "12–24 months",
+ complexity: "Accredited training and supervision",
+ caution: "Source review required — open the record.",
+ },
+ {
+ slug: "solution-focused-brief-therapy",
+ name: "Solution-focused brief therapy",
+ short: "SFBT",
+ category: "Community & Casework Support",
+ fit: "Goal-directed work where the presenting problem is circumscribed.",
+ time: "3–6 sessions",
+ complexity: "Can be delivered by trained non-specialists",
+ caution: "Source review required — open the record.",
+ },
+ {
+ slug: "family-intervention-for-psychosis",
+ name: "Family Intervention for Psychosis",
+ short: "FIp",
+ category: "Psychosis & Rehabilitation Therapies",
+ fit: "Relapse prevention where family contact is high.",
+ time: "10+ sessions over 3 months",
+ complexity: "Full team and supervision",
+ caution: "Source review required — open the record.",
+ },
+ {
+ slug: "social-skills-training-sst",
+ name: "Social Skills Training (SST)",
+ short: "SST",
+ category: "Psychosis & Rehabilitation Therapies",
+ fit: "Functional deficits in role and social performance.",
+ time: "Programme · weekly group",
+ complexity: "Trained facilitator",
+ caution: "Source review required — open the record.",
+ },
+ {
+ slug: "cognitive-remediation-therapy-crt",
+ name: "Cognitive Remediation Therapy (CRT)",
+ short: "CRT",
+ category: "Psychosis & Rehabilitation Therapies",
+ fit: "Cognitive impairment limiting rehabilitation or return to work.",
+ time: "20–40 sessions",
+ complexity: "Trained facilitator",
+ caution: "Source review required — open the record.",
+ },
+ {
+ slug: "contingency-management-cm",
+ name: "Contingency Management (CM)",
+ short: "CM",
+ category: "Substance Use Therapies",
+ fit: "Stimulant and opioid use where abstinence can be verified.",
+ time: "Programme · 12+ weeks",
+ complexity: "Service-level protocol required",
+ caution: "Source review required — open the record.",
+ },
+ {
+ slug: "brief-supportive-psychotherapy",
+ name: "Brief supportive psychotherapy",
+ short: "BSP",
+ category: "Foundational & Engagement Therapies",
+ fit: "Engagement and stabilisation where structured work is premature.",
+ time: "Flexible · 20–50 min",
+ complexity: "Any trained clinician",
+ caution: "Source review required — open the record.",
+ },
+ {
+ slug: "problem-management-plus-pm",
+ name: "Problem Management Plus / PM+",
+ short: "PM+",
+ category: "Self-Help & Digital Therapies",
+ fit: "Common mental disorders in low-resource or stepped-care settings.",
+ time: "5 sessions · 90 min",
+ complexity: "Can be delivered by trained non-specialists",
+ caution: "Source review required — open the record.",
+ },
];
export const CATEGORY_FILTERS = [
@@ -466,11 +556,17 @@ export const directions = [
name: "Carry a compare tray",
tagline: "Selection lives in a bottom tray you fill from anywhere, never in the page.",
},
+ {
+ slug: "therapy-compare-perfected",
+ letter: "C+",
+ name: "The tray, perfected",
+ tagline: "C with its three real costs removed. The one to judge.",
+ },
];
function DirectionSwitcher({ current }: { current: string }) {
return (
-