diff --git a/data/repo-awareness-snapshot.json b/data/repo-awareness-snapshot.json
index a4e1ff436e..70c0684779 100644
--- a/data/repo-awareness-snapshot.json
+++ b/data/repo-awareness-snapshot.json
@@ -1,8 +1,8 @@
{
"version": "repo-awareness-snapshot-v1",
"captured_revision": {
- "sha": "1d9e1234e1c6c3d642efed81ebe65a6056d5d120",
- "committed_at": "2026-08-27T11:37:53+00:00"
+ "sha": "2cfbe5647dc919f367486a4d4c2102524329b7c6",
+ "committed_at": "2026-08-27T11:41:19+00:00"
},
"routes": {
"modes": [
@@ -3959,6 +3959,38 @@
"outcome": "Re-synced with origin/main after PR #2416 landed (mechanical conflict only in data/repo-awareness-snapshot.json, regenerated). Also found and fixed a real regression from the earlier sweep's conflict resolution: DsmCompareChrome's onCommit had dropped ids.filter(Boolean) that origin/main carries, producing ids=,bipolar-ii-disorder after removing the first of two selections instead of ids=bipolar-ii-disorder -- exactly the failure tests/ui-route-coverage.spec.ts caught in CI. Restored the filter to match main. Added the required Clinical Governance Preflight section to the PR body since the regenerated snapshot touches data/. No review threads outstanding.",
"checks": "npx vitest run tests/compare-catalog.test.ts tests/compare-ids-chrome.dom.test.tsx tests/compare-slot-strip.dom.test.tsx tests/therapy-compare-phone-layout.dom.test.tsx tests/therapy-compare-tray.dom.test.tsx tests/phone-dock-addon-contract.test.ts -- 57 passed; no provider-backed checks run"
},
+ {
+ "date": "2026-08-27",
+ "ref": "codex/therapy-pathways-redesign (PR #2413)",
+ "head": "05e00976ab04796d7f44593b692f46cb8919943e",
+ "scope": "Run PR follow-up: second merge-drift + PR-policy fix",
+ "outcome": "Re-synced with origin/main after PR #2416 landed (mechanical conflict only in data/repo-awareness-snapshot.json, regenerated); added the required Clinical Governance Preflight section to the PR body since the regenerated snapshot touches data/, a path scripts/pr-policy.mjs treats as clinical-risk. No review threads outstanding.",
+ "checks": "npx vitest run tests/therapy-pathways-mobile.dom.test.tsx tests/therapy-compass-responsive-contract.test.ts tests/playwright-pr-shards.test.ts -- 25 passed; no provider-backed checks run"
+ },
+ {
+ "date": "2026-08-27",
+ "ref": "codex/therapy-pathways-redesign (PR #2413)",
+ "head": "be2d9b0291965258e5933006dc9b914961bb0dc3",
+ "scope": "Run PR sweep: CI fix + threads + drift",
+ "outcome": "CI was already green pre-sweep (PR required: success on d3f39c4); branch was already current with main (0 behind), so no drift merge needed. Fixed 3 unresolved CodeRabbit review threads: (1) pathway/step 'linked steps' counts used total steps.length instead of counting only steps with a matched therapySlug — added pathwayLinkedStepCount() and wired it into pathway-review-label.ts, pathway-picker-sheet.tsx (x2), pathway-step-stack.tsx; (2) ui-therapy-pathways.spec.ts readCautionGeometry substituted +/-Infinity for a missing caution/dock element, masking a broken overlap assertion — now throws if either is absent; (3) the 'Change pathway' Playwright test only re-clicked the already-active Anxiety pathway row, which could pass with a broken selectPathway — now selects Mood pathway, asserts the URL/heading changed, then switches back to Anxiety before the anxiety-scoped scroll assertions. All 3 threads replied with commit SHA and resolved.",
+ "checks": "npx vitest run tests/therapy-pathways-mobile.dom.test.tsx tests/therapy-compass-responsive-contract.test.ts tests/playwright-pr-shards.test.ts tests/therapy-compass-pathways.test.ts -- 34 passed; npm run lint -- exit 0 (gate-receipts pass); npm run typecheck -- exit 0, run foreground twice, gate-receipts recorded pass for typecheck:internal (5613 input files); npm run format -- applied (1 file reformatted, amended into commit); no provider-backed checks run"
+ },
+ {
+ "date": "2026-08-27",
+ "ref": "codex/therapy-pathways-redesign (PR #2413)",
+ "head": "d2bbc44f084707d8844485c67c8cd76d57e04b39",
+ "scope": "Run PR sweep: CI fix + threads + drift",
+ "outcome": "Before: mergeable_state dirty (conflict vs main), all real CI green on prior head (19fca0d), 0 unresolved review threads (4 PR comments were all automated bot usage-limit/status noise, no actionable findings). After: merged origin/main (b183dc65) into branch; only conflicts were generated files docs/design-system/COMPONENTS.md and docs/design-system/adoption-manifest.json, resolved by regenerating via 'node scripts/generate-design-system-adoption.mjs --write' (no manual edits); PR mergeability check now succeeds. No review threads needed action (none existed). No CI fix needed beyond the merge itself.",
+ "checks": "npx vitest run tests/therapy-pathways-mobile.dom.test.tsx tests/therapy-compass-responsive-contract.test.ts tests/playwright-pr-shards.test.ts -- 25 passed; npm run check:design-system-adoption -- design system adoption checked: 55 components, 76 roots; npm run check:playwright-pr-shards -- PR UI shard parity OK: 31 production specs across 3 groups; pre-commit hook ran docs:check-inventory and docs:check-index -- both current. No provider-backed checks run."
+ },
+ {
+ "date": "2026-08-27",
+ "ref": "codex/therapy-pathways-redesign (PR #2413)",
+ "head": "f8537abbef36b5e9f72eb1c745f565253a276153",
+ "scope": "Run PR sweep: CI fix + threads + drift",
+ "outcome": "Before: PR required failing (Static PR checks red on check:repo-awareness-snapshot — committed data/repo-awareness-snapshot.json review_state was stale after prior sweep's ledger/docs commits); 0 unresolved review threads; branch already contained origin/main (no drift). After: ran npm run snapshot:repo-awareness to regenerate the snapshot, verified npm run check:repo-awareness-snapshot and npm run format:changed pass, committed and pushed f8537ab. All other required jobs (Build, Unit coverage, Production UI critical, Safety and config checks, Production UI shards 1-3, Lighthouse budget) were already green before this fix and untouched by it. No review threads existed to address. No main-sync needed (branch already up to date).",
+ "checks": "npm run check:repo-awareness-snapshot (PASS, reproduces prior CI failure then passes after regen); npm run format:changed (PASS); npm run snapshot:repo-awareness (regenerated data/repo-awareness-snapshot.json); no provider-backed checks run"
+ },
{
"date": "2026-08-27",
"ref": "codex/tooling-hooks-ci-invariants",
@@ -25001,8 +25033,8 @@
}
],
"counts": {
- "records": 2639,
- "refs": 1610
+ "records": 2643,
+ "refs": 1611
}
}
}
diff --git a/docs/branch-review-records/093e552fb22a9e87c899dd093a3c133dd13a262ad6b2fc4fbb5b5a6deee67d93.record.md b/docs/branch-review-records/093e552fb22a9e87c899dd093a3c133dd13a262ad6b2fc4fbb5b5a6deee67d93.record.md
new file mode 100644
index 0000000000..c4e5a834b0
--- /dev/null
+++ b/docs/branch-review-records/093e552fb22a9e87c899dd093a3c133dd13a262ad6b2fc4fbb5b5a6deee67d93.record.md
@@ -0,0 +1 @@
+| 2026-08-27 | codex/therapy-pathways-redesign (PR #2413) | 05e00976ab04796d7f44593b692f46cb8919943e | Run PR follow-up: second merge-drift + PR-policy fix | Re-synced with origin/main after PR #2416 landed (mechanical conflict only in data/repo-awareness-snapshot.json, regenerated); added the required Clinical Governance Preflight section to the PR body since the regenerated snapshot touches data/, a path scripts/pr-policy.mjs treats as clinical-risk. No review threads outstanding. | npx vitest run tests/therapy-pathways-mobile.dom.test.tsx tests/therapy-compass-responsive-contract.test.ts tests/playwright-pr-shards.test.ts -- 25 passed; no provider-backed checks run |
diff --git a/docs/branch-review-records/7895a1814c28b2fe53fd6877a91f0bc86535eee70ed77c22032f6989646a2a1d.record.md b/docs/branch-review-records/7895a1814c28b2fe53fd6877a91f0bc86535eee70ed77c22032f6989646a2a1d.record.md
new file mode 100644
index 0000000000..1ecd07784f
--- /dev/null
+++ b/docs/branch-review-records/7895a1814c28b2fe53fd6877a91f0bc86535eee70ed77c22032f6989646a2a1d.record.md
@@ -0,0 +1 @@
+| 2026-08-27 | codex/therapy-pathways-redesign (PR #2413) | f8537abbef36b5e9f72eb1c745f565253a276153 | Run PR sweep: CI fix + threads + drift | Before: PR required failing (Static PR checks red on check:repo-awareness-snapshot — committed data/repo-awareness-snapshot.json review_state was stale after prior sweep's ledger/docs commits); 0 unresolved review threads; branch already contained origin/main (no drift). After: ran npm run snapshot:repo-awareness to regenerate the snapshot, verified npm run check:repo-awareness-snapshot and npm run format:changed pass, committed and pushed f8537ab. All other required jobs (Build, Unit coverage, Production UI critical, Safety and config checks, Production UI shards 1-3, Lighthouse budget) were already green before this fix and untouched by it. No review threads existed to address. No main-sync needed (branch already up to date). | npm run check:repo-awareness-snapshot (PASS, reproduces prior CI failure then passes after regen); npm run format:changed (PASS); npm run snapshot:repo-awareness (regenerated data/repo-awareness-snapshot.json); no provider-backed checks run |
diff --git a/docs/branch-review-records/ad2f2cc55ed454d781f0501ef25a67e6f7bbaf869908014dfceacbcd2b8838d4.record.md b/docs/branch-review-records/ad2f2cc55ed454d781f0501ef25a67e6f7bbaf869908014dfceacbcd2b8838d4.record.md
new file mode 100644
index 0000000000..cb6b6e6697
--- /dev/null
+++ b/docs/branch-review-records/ad2f2cc55ed454d781f0501ef25a67e6f7bbaf869908014dfceacbcd2b8838d4.record.md
@@ -0,0 +1 @@
+| 2026-08-27 | codex/therapy-pathways-redesign (PR #2413) | be2d9b0291965258e5933006dc9b914961bb0dc3 | Run PR sweep: CI fix + threads + drift | CI was already green pre-sweep (PR required: success on d3f39c4); branch was already current with main (0 behind), so no drift merge needed. Fixed 3 unresolved CodeRabbit review threads: (1) pathway/step 'linked steps' counts used total steps.length instead of counting only steps with a matched therapySlug — added pathwayLinkedStepCount() and wired it into pathway-review-label.ts, pathway-picker-sheet.tsx (x2), pathway-step-stack.tsx; (2) ui-therapy-pathways.spec.ts readCautionGeometry substituted +/-Infinity for a missing caution/dock element, masking a broken overlap assertion — now throws if either is absent; (3) the 'Change pathway' Playwright test only re-clicked the already-active Anxiety pathway row, which could pass with a broken selectPathway — now selects Mood pathway, asserts the URL/heading changed, then switches back to Anxiety before the anxiety-scoped scroll assertions. All 3 threads replied with commit SHA and resolved. | npx vitest run tests/therapy-pathways-mobile.dom.test.tsx tests/therapy-compass-responsive-contract.test.ts tests/playwright-pr-shards.test.ts tests/therapy-compass-pathways.test.ts -- 34 passed; npm run lint -- exit 0 (gate-receipts pass); npm run typecheck -- exit 0, run foreground twice, gate-receipts recorded pass for typecheck:internal (5613 input files); npm run format -- applied (1 file reformatted, amended into commit); no provider-backed checks run |
diff --git a/docs/branch-review-records/d99361a8c9a53049ee45227f0230020721e9aebb18ee7548804835c510538765.record.md b/docs/branch-review-records/d99361a8c9a53049ee45227f0230020721e9aebb18ee7548804835c510538765.record.md
new file mode 100644
index 0000000000..af8850eb0c
--- /dev/null
+++ b/docs/branch-review-records/d99361a8c9a53049ee45227f0230020721e9aebb18ee7548804835c510538765.record.md
@@ -0,0 +1 @@
+| 2026-08-27 | codex/therapy-pathways-redesign (PR #2413) | d2bbc44f084707d8844485c67c8cd76d57e04b39 | Run PR sweep: CI fix + threads + drift | Before: mergeable_state dirty (conflict vs main), all real CI green on prior head (19fca0d), 0 unresolved review threads (4 PR comments were all automated bot usage-limit/status noise, no actionable findings). After: merged origin/main (b183dc65) into branch; only conflicts were generated files docs/design-system/COMPONENTS.md and docs/design-system/adoption-manifest.json, resolved by regenerating via 'node scripts/generate-design-system-adoption.mjs --write' (no manual edits); PR mergeability check now succeeds. No review threads needed action (none existed). No CI fix needed beyond the merge itself. | npx vitest run tests/therapy-pathways-mobile.dom.test.tsx tests/therapy-compass-responsive-contract.test.ts tests/playwright-pr-shards.test.ts -- 25 passed; npm run check:design-system-adoption -- design system adoption checked: 55 components, 76 roots; npm run check:playwright-pr-shards -- PR UI shard parity OK: 31 production specs across 3 groups; pre-commit hook ran docs:check-inventory and docs:check-index -- both current. No provider-backed checks run. |
diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md
index 1f29f11b69..d32e94fa73 100644
--- a/docs/design-system/COMPONENTS.md
+++ b/docs/design-system/COMPONENTS.md
@@ -976,7 +976,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `AnswerFooter` | answer | yes | yes | inherited-global-root | yes | no | 1 |
| `AsyncButton` | controls | yes | yes | inherited-global-root | yes | no | 4 |
| `Breadcrumb` | layout | yes | yes | inherited-global-root | yes | no | 1 |
-| `Button` | controls | yes | yes | inherited-global-root | yes | no | 17 |
+| `Button` | controls | yes | yes | inherited-global-root | yes | no | 19 |
| `Checkbox` | controls | yes | yes | no | yes | no | 0 |
| `Chip` | controls | yes | yes | inherited-global-root | yes | no | 5 |
| `ChoiceChip` | controls | yes | yes | inherited-global-root | yes | no | 4 |
@@ -1012,7 +1012,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `SearchField` | controls | yes | yes | no | yes | no | 0 |
| `SegmentedControl` | controls | yes | yes | inherited-global-root | yes | no | 8 |
| `Select` | controls | yes | yes | inherited-global-root | yes | no | 2 |
-| `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 31 |
+| `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 32 |
| `Skeleton` | feedback | yes | yes | inherited-global-root | yes | no | 6 |
| `SourceDesignationBadge` | source | yes | yes | inherited-global-root | yes | no | 4 |
| `SourceProvenance` | source | yes | yes | inherited-global-root | yes | no | 1 |
diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json
index 05be5ff13c..4005a3d674 100644
--- a/docs/design-system/adoption-manifest.json
+++ b/docs/design-system/adoption-manifest.json
@@ -281,6 +281,8 @@
"src/components/AccessibleTable.tsx",
"src/components/clinical-dashboard/signed-image.tsx",
"src/components/privacy-quiet-signal-page.tsx",
+ "src/components/therapy-compass/pathway-picker-sheet.tsx",
+ "src/components/therapy-compass/pathway-step-stack.tsx",
"src/components/therapy-compass/recommend-scenario-control.tsx",
"src/components/therapy-compass/record/compare-action.tsx",
"src/components/therapy-compass/screens/brief-screen.tsx",
@@ -300,6 +302,8 @@
"src/components/AccessibleTable.tsx",
"src/components/clinical-dashboard/signed-image.tsx",
"src/components/privacy-quiet-signal-page.tsx",
+ "src/components/therapy-compass/pathway-picker-sheet.tsx",
+ "src/components/therapy-compass/pathway-step-stack.tsx",
"src/components/therapy-compass/recommend-scenario-control.tsx",
"src/components/therapy-compass/record/compare-action.tsx",
"src/components/therapy-compass/screens/brief-screen.tsx",
@@ -1663,6 +1667,7 @@
"src/components/forms/form-priority-facts-section.tsx",
"src/components/in-page-nav/in-page-nav-header.tsx",
"src/components/mode-nav/mode-nav.tsx",
+ "src/components/therapy-compass/pathway-picker-sheet.tsx",
"src/components/therapy-compass/recommend-scenario-control.tsx",
"src/components/therapy-compass/record/key-facts.tsx",
"src/components/therapy-compass/therapy-compare-tray.tsx",
@@ -1697,6 +1702,7 @@
"src/components/forms/form-priority-facts-section.tsx",
"src/components/in-page-nav/in-page-nav-header.tsx",
"src/components/mode-nav/mode-nav.tsx",
+ "src/components/therapy-compass/pathway-picker-sheet.tsx",
"src/components/therapy-compass/recommend-scenario-control.tsx",
"src/components/therapy-compass/record/key-facts.tsx",
"src/components/therapy-compass/therapy-compare-tray.tsx",
diff --git a/playwright.config.ts b/playwright.config.ts
index 8b02d37438..9bd6a26a04 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -23,7 +23,7 @@ const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
// `tests/playwright-project-isolation.test.ts` asserts every such file on disk is
// matched here.
const productionSpecPattern =
- /.*(?:answer-progress-ui-smoke|dsm-ui-smoke|ui-(smoke|stress|accessibility|caring-contacts-workspace|clinical-ask|dictionary|document-canvas|tools|overlap|universal-search|specifiers|formulation(?:-result-cards)?|forms-section-nav|chrome-scroll|therapy-nav-scroll|mode-nav-density|phone-motion|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts/;
+ /.*(?:answer-progress-ui-smoke|dsm-ui-smoke|ui-(smoke|stress|accessibility|caring-contacts-workspace|clinical-ask|dictionary|document-canvas|tools|overlap|universal-search|specifiers|formulation(?:-result-cards)?|forms-section-nav|chrome-scroll|therapy-nav-scroll|therapy-pathways|mode-nav-density|phone-motion|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts/;
const mockupSpecPattern =
/.*ui-(answer-chat-perfected-mockup|care-plan-mockup|caring-contact-mockup|document-image-status-mockup|document-top-navigation-mockup|sidebar-live-mockup|therapy-navigation-mockup|tools|tools-collapse|tools-search-mode-mockup|tools-task-directory|ward-management|ward-coordinator|ward-roles|ward-discharges)\.spec\.ts/;
const mockupTag = /@mockup/;
@@ -31,7 +31,7 @@ const mockupTag = /@mockup/;
export default defineConfig({
testDir: "./tests",
testMatch:
- /.*(?:answer-progress-ui-smoke|dsm-ui-smoke|ui-(smoke|stress|accessibility|answer-chat-perfected-mockup|care-plan-mockup|caring-contact-mockup|caring-contacts-workspace|clinical-ask|dictionary|document-canvas|document-image-status-mockup|document-top-navigation-mockup|sidebar-live-mockup|therapy-navigation-mockup|tools|tools-collapse|tools-search-mode-mockup|tools-task-directory|ward-(?:management|coordinator|roles|discharges)|overlap|universal-search|specifiers|formulation(?:-result-cards)?|forms-section-nav|chrome-scroll|therapy-nav-scroll|mode-nav-density|phone-motion|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts/,
+ /.*(?:answer-progress-ui-smoke|dsm-ui-smoke|ui-(smoke|stress|accessibility|answer-chat-perfected-mockup|care-plan-mockup|caring-contact-mockup|caring-contacts-workspace|clinical-ask|dictionary|document-canvas|document-image-status-mockup|document-top-navigation-mockup|sidebar-live-mockup|therapy-navigation-mockup|tools|tools-collapse|tools-search-mode-mockup|tools-task-directory|ward-(?:management|coordinator|roles|discharges)|overlap|universal-search|specifiers|formulation(?:-result-cards)?|forms-section-nav|chrome-scroll|therapy-nav-scroll|therapy-pathways|mode-nav-density|phone-motion|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts/,
timeout: 60_000,
retries: 0,
// Fail the run if a stray `test.only` is committed: otherwise it silently
diff --git a/scripts/playwright-pr-shards.mjs b/scripts/playwright-pr-shards.mjs
index 0784808898..a1305b1ad9 100644
--- a/scripts/playwright-pr-shards.mjs
+++ b/scripts/playwright-pr-shards.mjs
@@ -19,7 +19,7 @@ import { childProcessExitCode } from "./child-process-result.mjs";
/** Same matcher as playwright.config.ts `productionSpecPattern` (keep in sync). */
export const productionSpecFilePattern =
- /^(?:answer-progress-ui-smoke|dsm-ui-smoke|ui-(?:smoke|stress|accessibility|caring-contacts-workspace|clinical-ask|dictionary|document-canvas|tools|overlap|universal-search|specifiers|formulation(?:-result-cards)?|forms-section-nav|chrome-scroll|therapy-nav-scroll|mode-nav-density|phone-motion|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts$/;
+ /^(?:answer-progress-ui-smoke|dsm-ui-smoke|ui-(?:smoke|stress|accessibility|caring-contacts-workspace|clinical-ask|dictionary|document-canvas|tools|overlap|universal-search|specifiers|formulation(?:-result-cards)?|forms-section-nav|chrome-scroll|therapy-nav-scroll|therapy-pathways|mode-nav-density|phone-motion|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts$/;
/**
* One source of truth for shard membership and its latest hosted timing sample.
@@ -44,6 +44,9 @@ export const prUiSpecProfiles = Object.freeze([
// with hosted evidence at the next timing refresh. Moved here from shard 2 by
// Task 19 to offset the re-measured Caring Contacts workspace spec.
{ file: "tests/ui-phone-motion.spec.ts", shard: 1, fullSeconds: 5.0, criticalSeconds: 0 },
+ // Added with the Therapy Pathways mobile picker redesign; measured locally at
+ // ~2 tests. Placed on shard 1 to keep post-critical spread within the 10s ceiling.
+ { file: "tests/ui-therapy-pathways.spec.ts", shard: 1, fullSeconds: 2.0, criticalSeconds: 0 },
{ file: "tests/ui-phone-scroll-routes.spec.ts", shard: 2, fullSeconds: 129.6, criticalSeconds: 0 },
{ file: "tests/ui-phone-scroll.spec.ts", shard: 2, fullSeconds: 66.3, criticalSeconds: 0 },
diff --git a/src/components/therapy-compass/pathway-picker-sheet.tsx b/src/components/therapy-compass/pathway-picker-sheet.tsx
new file mode 100644
index 0000000000..8cc22a5170
--- /dev/null
+++ b/src/components/therapy-compass/pathway-picker-sheet.tsx
@@ -0,0 +1,223 @@
+"use client";
+
+import { useId, useMemo, useRef, useState } from "react";
+import { Search, Waypoints } from "lucide-react";
+
+import { InteractiveRow } from "@/components/ui/interactive-row";
+import { Sheet } from "@/components/ui/sheet";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/components/ui-primitives";
+
+import type { Pathway } from "./data/types";
+import {
+ matchesPathwayFilter,
+ pathwayLinkedStepCount,
+ pathwayReviewBadgeClass,
+ pathwayReviewLabel,
+ pathwayRowAccessibleName,
+} from "./pathway-review-label";
+
+type PathwayListRailProps = {
+ pathways: Pathway[];
+ activeSlug: string;
+ onSelect: (slug: string) => void;
+};
+
+export function PathwayListRail({ pathways, activeSlug, onSelect }: PathwayListRailProps) {
+ return (
+
+
Pathways
+
+ {pathways.map((pathway) => {
+ const active = pathway.slug === activeSlug;
+ const reviewed = pathway.reviewStatus === "reviewed";
+ return (
+ onSelect(pathway.slug)}
+ >
+
+
+
+
+ {pathway.name}
+
+ {pathway.clinicalProblem ?? pathway.summary ?? "Therapy workflow"}
+
+
+
+ {pathwayLinkedStepCount(pathway)} linked steps
+
+
+ {pathwayReviewLabel(pathway)}
+
+
+
+
+ );
+ })}
+
+
+ Pathways are generated from imported therapy records.
+
+
+ );
+}
+
+type PathwayPickerSheetProps = {
+ pathways: Pathway[];
+ activePathway: Pathway;
+ onSelect: (slug: string) => void;
+};
+
+export function PathwayMobileBar({ pathways, activePathway, onSelect }: PathwayPickerSheetProps) {
+ const [sheetOpen, setSheetOpen] = useState(false);
+ const [filter, setFilter] = useState("");
+ const panelId = useId();
+ const filterId = useId();
+ const triggerRef = useRef(null);
+ const reviewed = activePathway.reviewStatus === "reviewed";
+ const filteredPathways = useMemo(
+ () => pathways.filter((pathway) => matchesPathwayFilter(pathway, filter)),
+ [pathways, filter],
+ );
+
+ const closeSheet = () => {
+ setSheetOpen(false);
+ setFilter("");
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
{activePathway.name}
+
+ {pathwayReviewLabel(activePathway)}
+
+
+
+ {activePathway.clinicalProblem ?? activePathway.summary ?? "Therapy workflow"}
+
+
+
+
setSheetOpen(true)}
+ >
+ Change pathway
+
+
+
+
+ Select a clinical problem workflow. {pathways.length} pathways available.
+
+ }
+ portal
+ placement="responsive-right"
+ id={panelId}
+ testId="therapy-pathway-picker-panel"
+ footer={
+
+ Done
+
+ }
+ >
+
+
+ setFilter(event.target.value)}
+ placeholder="Filter pathways…"
+ aria-label="Filter pathways"
+ className="w-full min-h-tap rounded-md border border-[color:var(--border)] bg-[color:var(--surface)] py-0 pr-3 pl-9 text-sm-minus text-[color:var(--text)]"
+ />
+
+
+ {filteredPathways.length === 0 ? (
+
No pathways match that filter.
+ ) : null}
+ {filteredPathways.map((pathway) => {
+ const active = pathway.slug === activePathway.slug;
+ const pathwayReviewed = pathway.reviewStatus === "reviewed";
+ return (
+
{
+ onSelect(pathway.slug);
+ closeSheet();
+ }}
+ >
+
+
+
+
+ {pathway.name}
+
+ {pathway.clinicalProblem ?? pathway.summary ?? "Therapy workflow"}
+
+
+
+ {pathwayLinkedStepCount(pathway)} linked steps
+
+
+ {pathwayReviewLabel(pathway)}
+
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/src/components/therapy-compass/pathway-review-label.ts b/src/components/therapy-compass/pathway-review-label.ts
new file mode 100644
index 0000000000..aecae9e8ff
--- /dev/null
+++ b/src/components/therapy-compass/pathway-review-label.ts
@@ -0,0 +1,33 @@
+import type { Pathway } from "./data/types";
+
+export function pathwayLinkedStepCount(pathway: Pick): number {
+ return pathway.steps.filter((step) => Boolean(step.therapySlug)).length;
+}
+
+export function pathwayRowAccessibleName(pathway: Pathway, active: boolean): string {
+ const parts = [pathway.name, `${pathwayLinkedStepCount(pathway)} linked steps`, pathwayReviewLabel(pathway)];
+ if (active) parts.push("currently selected");
+ return parts.join(", ");
+}
+
+export function matchesPathwayFilter(pathway: Pathway, query: string): boolean {
+ const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
+ if (tokens.length === 0) return true;
+ const haystack = [pathway.name, pathway.clinicalProblem, pathway.summary, pathwayReviewLabel(pathway)]
+ .filter(Boolean)
+ .join(" ")
+ .toLowerCase();
+ return tokens.every((token) => haystack.includes(token));
+}
+
+export function pathwayReviewLabel(pathway: Pick): string {
+ if (pathway.reviewStatus === "reviewed") return "Reviewed";
+ if (pathway.incomplete) return "Incomplete";
+ return "Needs review";
+}
+
+export function pathwayReviewBadgeClass(reviewed: boolean): string {
+ return reviewed
+ ? "border-[color:var(--success-border)] bg-[color:var(--success-bg)] text-[color:var(--success-text)]"
+ : "border-[color:var(--warning-border)] bg-[color:var(--warning-bg)] text-[color:var(--warning-text)]";
+}
diff --git a/src/components/therapy-compass/pathway-step-stack.tsx b/src/components/therapy-compass/pathway-step-stack.tsx
new file mode 100644
index 0000000000..e0d75464ef
--- /dev/null
+++ b/src/components/therapy-compass/pathway-step-stack.tsx
@@ -0,0 +1,197 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { ChevronRight, Scale, Waypoints } from "lucide-react";
+
+import { cardSurface } from "@/components/card-recipes";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/components/ui-primitives";
+
+import type { Pathway, PathwayStep, Therapy } from "./data/types";
+import { pathwayLinkedStepCount } from "./pathway-review-label";
+
+type PathwayStepStackProps = {
+ steps: PathwayStep[];
+ bySlug: Map;
+ onOpenRecord: (slug: string) => void;
+};
+
+function StepCard({
+ step,
+ index,
+ last,
+ therapy,
+ onOpenRecord,
+ layout,
+}: {
+ step: PathwayStep;
+ index: number;
+ last: boolean;
+ therapy: Therapy | undefined;
+ onOpenRecord: (slug: string) => void;
+ layout: "mobile" | "desktop";
+}) {
+ const title = therapy?.name ?? step.label ?? "Therapy step";
+ const description = step.description ?? therapy?.bestUsedFor ?? "Review fit, contraindications and source status.";
+ const roleLabel = step.label ?? "STEP";
+
+ if (layout === "mobile") {
+ return (
+
+ {!last ? (
+
+ ) : null}
+
+
+ {index + 1}
+
+
+
+
{title}
+
+ {roleLabel}
+
+
+
{description}
+
+
+ {therapy ? (
+ onOpenRecord(therapy.slug)}>
+ Open record
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+
+ {index + 1}
+
+ {!last ? : null}
+
+
+
+
+
+
+
{title}
+
{description}
+
+
+ {roleLabel}
+
+ {therapy ? (
+
onOpenRecord(therapy.slug)}>
+ Open record
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+export function PathwayStepStack({ steps, bySlug, onOpenRecord }: PathwayStepStackProps) {
+ return (
+ <>
+
+ {steps.map((step, index) => {
+ const therapy = step.therapySlug ? bySlug.get(step.therapySlug) : undefined;
+ return (
+
+ );
+ })}
+
+
+ {steps.map((step, index) => {
+ const therapy = step.therapySlug ? bySlug.get(step.therapySlug) : undefined;
+ return (
+
+ );
+ })}
+
+ >
+ );
+}
+
+export function PathwayDetailHeader({ pathway, reviewBadge }: { pathway: Pathway; reviewBadge?: ReactNode }) {
+ return (
+
+
+
+
+
+
+
{pathway.name}
+ {reviewBadge}
+
+
+ {pathway.summary ??
+ "A source-linked workflow for reviewing therapy options, delivery constraints and cautions before choosing a next step."}
+
+
+
+ {pathwayLinkedStepCount(pathway)} linked therapy steps
+
+
+
+ );
+}
diff --git a/src/components/therapy-compass/screens/pathways-screen.tsx b/src/components/therapy-compass/screens/pathways-screen.tsx
index 65dae23192..64534c80c3 100644
--- a/src/components/therapy-compass/screens/pathways-screen.tsx
+++ b/src/components/therapy-compass/screens/pathways-screen.tsx
@@ -1,7 +1,7 @@
"use client";
import { useMemo } from "react";
-import { Check, ChevronRight, Copy, FileText, ListChecks, Scale, TriangleAlert, Waypoints } from "lucide-react";
+import { Check, Copy, FileText, ListChecks, TriangleAlert, Waypoints } from "lucide-react";
import { cardSurface } from "@/components/card-recipes";
import { Chip } from "@/components/ui/chip";
@@ -10,19 +10,17 @@ import { cn, pageContainer } from "@/components/ui-primitives";
import { Button } from "@/components/ui/button";
import { useTcBindings } from "../bindings";
-import { InteractiveRow } from "@/components/ui/interactive-row";
-import type { Therapy } from "../data/types";
import { LoadingState } from "../ui";
import { useClipboard } from "../use-clipboard";
+import { PathwayMobileBar, PathwayListRail } from "../pathway-picker-sheet";
+import { PathwayDetailHeader, PathwayStepStack } from "../pathway-step-stack";
+import { pathwayReviewBadgeClass, pathwayReviewLabel } from "../pathway-review-label";
export function PathwaysScreen() {
const b = useTcBindings();
const bySlug = useMemo(() => new Map(b.therapies.map((t) => [t.slug, t])), [b.therapies]);
const pathway = b.selectedPathway;
- // Header counts. Summed here rather than in the header markup so the chips
- // stay declarative and the totals survive a data shape that grows a step.
const linkedStepCount = useMemo(() => b.pathways.reduce((total, p) => total + p.steps.length, 0), [b.pathways]);
- // Called before the early return below, because it is a hook.
const { copied, copy } = useClipboard();
if (b.loading || !pathway) return ;
@@ -31,10 +29,8 @@ export function PathwaysScreen() {
pathway.steps
.map((step) => (step.therapySlug ? bySlug.get(step.therapySlug) : undefined))
.find((therapy) => therapy?.patientSheetAvailable)?.slug ?? null;
- // Every other copy affordance in this mode goes through `useClipboard` ->
- // `@/lib/copy-to-clipboard`. This one reached for `navigator.clipboard`
- // directly, so it had no fallback path, treated a rejected write as success,
- // and gave the reader no confirmation that anything had been copied.
+ const reviewed = pathway.reviewStatus === "reviewed";
+
const copyPathway = () => {
const lines = pathway.steps.map((st, i) => {
const name = (st.therapySlug ? bySlug.get(st.therapySlug)?.name : null) ?? st.label ?? "Step";
@@ -45,21 +41,8 @@ export function PathwaysScreen() {
return (
- {/* The Review queue lived here as the page's one action. It is a curation
- surface — which records still need source review — not something a
- clinician reads a pathway to reach, so the header no longer carries it;
- `/therapy-compass/review` still serves it directly. What replaces it is
- scale, not another control: the two counts say how much of the
- catalogue this page covers, and the accent chip is the only colour a
- phone gets here, since `PageHeader` hides its icon tile below `sm`.
-
- The description loses "generated from imported therapy records"
- because that provenance is already stated twice further down the same
- screen — the note under the pathway list and the caution banner — and
- on a 390px viewport it was the line that wrapped into the drawer
- handle. */}
-
- {/* pathway list */}
-
-
Pathways
-
- {b.pathways.map((p) => {
- const active = p.slug === pathway.slug;
- return (
- b.selectPathway(p.slug)}>
-
-
-
-
- {p.name}
-
- {p.clinicalProblem ?? p.summary ?? "Therapy workflow"}
-
-
- {p.steps.length} linked steps
-
- {p.reviewStatus === "reviewed" ? "Reviewed" : p.incomplete ? "Incomplete" : "Needs review"}
-
-
-
-
- );
- })}
-
-
- Pathways are generated from imported therapy records.
-
-
+
- {/* pathway detail */}
-
-
-
-
-
-
-
-
{pathway.name}
+
+
+
+
+
+
- {pathway.reviewStatus === "reviewed" ? "Reviewed" : "Needs review"}
+ {pathwayReviewLabel(pathway)}
-
-
- {pathway.summary ??
- "A source-linked workflow for reviewing therapy options, delivery constraints and cautions before choosing a next step."}
-
-
-
- {pathway.steps.length} linked therapy steps
-
-
+ }
+ />
-
- {pathway.steps.map((step, i) => {
- const therapy: Therapy | undefined = step.therapySlug ? bySlug.get(step.therapySlug) : undefined;
- const last = i === pathway.steps.length - 1;
- return (
-
-
- {i + 1}
-
-
-
-
-
-
-
- {therapy?.name ?? step.label ?? "Therapy step"}
-
-
- {step.description ?? therapy?.bestUsedFor ?? "Review fit, contraindications and source status."}
-
-
-
- {step.label ?? "STEP"}
-
- {therapy ? (
-
b.open(therapy.slug)}>
- Open record
-
- ) : (
-
- )}
-
-
- );
- })}
-
+
-
+
-
+
Clinical caution — decision support generated from imported records.
-
+
{pathway.cautions ??
"Review source status, missing fields and patient-specific factors before clinical use."}
-
-
+
+
{copied === "pathway" ? "Copied" : "Copy pathway"}
firstLinkedSlug && b.openSheet(firstLinkedSlug)}
disabled={!firstLinkedSlug}
>
diff --git a/tests/therapy-compass-responsive-contract.test.ts b/tests/therapy-compass-responsive-contract.test.ts
index 7d5d3fd3c3..ee3ebdb690 100644
--- a/tests/therapy-compass-responsive-contract.test.ts
+++ b/tests/therapy-compass-responsive-contract.test.ts
@@ -23,6 +23,8 @@ const compareSource = read(`${therapyPath}/screens/compare-screen.tsx`);
const recommendSource = read(`${therapyPath}/screens/recommend-screen.tsx`);
const recommendFieldsSource = read(`${therapyPath}/recommend-scenario-fields.tsx`);
const pathwaysSource = read(`${therapyPath}/screens/pathways-screen.tsx`);
+const pathwayStepStackSource = read(`${therapyPath}/pathway-step-stack.tsx`);
+const pathwayPickerSource = read(`${therapyPath}/pathway-picker-sheet.tsx`);
const briefSource = read(`${therapyPath}/screens/brief-screen.tsx`);
const sheetsSource = read(`${therapyPath}/screens/sheets-screen.tsx`);
const otherSource = read(`${therapyPath}/screens/other-screen.tsx`);
@@ -185,7 +187,13 @@ describe("Therapy Compass responsive contract", () => {
1,
);
expect(responsiveStackCount(pathwaysSource)).toBeGreaterThanOrEqual(1);
- expect(pathwaysSource).toContain("therapy-pathway-list");
+ expect(pathwayPickerSource).toContain("therapy-pathway-list");
+ expect(pathwayPickerSource).toContain('data-testid="therapy-pathway-picker"');
+ expect(pathwayStepStackSource).toContain('data-testid="therapy-pathway-steps"');
+ expect(pathwaysSource).toContain('data-testid="therapy-pathway-caution"');
+ expect(pathwayPickerSource).toContain("sm:hidden");
+ expect(pathwayPickerSource).toContain("hidden border-r");
+ expect(pathwaysSource).not.toContain("overflow-hidden");
expect(responsiveStackCount(briefSource)).toBeGreaterThanOrEqual(1);
expect(responsiveStackCount(sheetsSource)).toBeGreaterThanOrEqual(1);
expect(sheetsSource).toContain("max-sm:static");
diff --git a/tests/therapy-pathways-mobile.dom.test.tsx b/tests/therapy-pathways-mobile.dom.test.tsx
new file mode 100644
index 0000000000..2317c7df9f
--- /dev/null
+++ b/tests/therapy-pathways-mobile.dom.test.tsx
@@ -0,0 +1,186 @@
+import { render, screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { TcProvider } from "@/components/therapy-compass/bindings";
+import { PathwaysScreen } from "@/components/therapy-compass/screens/pathways-screen";
+import type { Pathway, Therapy } from "@/components/therapy-compass/data/types";
+
+const navigation = vi.hoisted(() => ({
+ pathname: "/therapy-compass/pathways",
+ search: "pathway=anxiety-pathway",
+}));
+
+vi.mock("next/navigation", () => ({
+ usePathname: () => navigation.pathname,
+ useSearchParams: () => new URLSearchParams(navigation.search),
+ useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }),
+}));
+
+vi.mock("@/components/account-data-provider", () => ({
+ useAccountData: () => ({ isAuthenticated: false, isSaved: () => false, setFavourite: vi.fn(async () => true) }),
+}));
+
+const therapy = (slug: string, name: string): Therapy => ({
+ slug,
+ name,
+ category: "Skills based",
+ clinicalSummary: `${name} summary`,
+ bestUsedFor: `${name} best fit`,
+ indications: "Anxiety",
+ contraindicationsOrCautions: "Use with care.",
+ deliverySteps: "1. Orient the patient",
+ patientExplanation: "A practical therapy skill.",
+ sourceNotes: null,
+ targetSymptoms: "Anxiety",
+ patientPopulation: "Adults",
+ setting: "Outpatient",
+ sessionLength: "50 minutes",
+ timeRequired: "5 minutes",
+ complexity: "Low",
+ mechanism: "Skills practice",
+ briefVersion: null,
+ fifteenMinuteVersion: null,
+ fullSessionVersion: null,
+ homework: null,
+ materials: null,
+ commonPitfalls: null,
+ alternatives: null,
+ relatedTherapies: null,
+ evidenceLevel: "Review source",
+ evidenceNotes: null,
+ limitations: null,
+ references: null,
+ reviewStatus: "reviewed",
+ confidenceLevel: null,
+ contentOrigin: null,
+ patientSheetAvailable: true,
+ briefInterventionAvailable: true,
+ sourceCompleteness: null,
+ indexCompleteness: null,
+ reviewCompleteness: null,
+ tags: ["Anxiety"],
+ warnings: [],
+ aliases: [],
+ sources: [],
+ patientSheetTemplates: [],
+ clinicianScripts: [],
+ reviewChecklist: null,
+});
+
+const pathways: Pathway[] = [
+ {
+ slug: "anxiety-pathway",
+ name: "Anxiety pathway",
+ clinicalProblem: "Anxiety",
+ summary: "Workflow for anxiety presentations.",
+ cautions: "Confirm acuity and patient preference before clinical use.",
+ incomplete: true,
+ reviewStatus: "needs_review",
+ steps: [
+ {
+ therapySlug: "cognitive-behavioural-therapy-cbt",
+ label: "Initial option",
+ description:
+ "Strongest broad evidence-backed uses are depression and anxiety disorders with long descriptive copy that should remain readable on a phone without being clamped away.",
+ },
+ {
+ therapySlug: "graded-exposure",
+ label: "Alternative option",
+ description: "Useful when avoidance is the main maintaining factor.",
+ },
+ ],
+ },
+ {
+ slug: "mood-pathway",
+ name: "Mood pathway",
+ clinicalProblem: "Mood",
+ summary: "Workflow for mood presentations.",
+ cautions: "Use as decision support only.",
+ incomplete: false,
+ reviewStatus: "reviewed",
+ steps: [
+ {
+ therapySlug: "cognitive-behavioural-therapy-cbt",
+ label: "Initial option",
+ description: "First-line for depression.",
+ },
+ ],
+ },
+];
+
+vi.mock("@/components/therapy-compass/data/use-therapy-data", () => ({
+ useTherapyData: () => ({
+ data: {
+ therapies: [
+ therapy("cognitive-behavioural-therapy-cbt", "Cognitive behavioural therapy (CBT)"),
+ therapy("graded-exposure", "Graded exposure"),
+ ],
+ pathways,
+ reference: { categories: [], tags: [], measures: [] },
+ },
+ loading: false,
+ error: null,
+ retry: vi.fn(),
+ }),
+}));
+
+function renderPathways() {
+ return render(
+
+
+ ,
+ );
+}
+
+afterEach(() => {
+ navigation.search = "pathway=anxiety-pathway";
+});
+
+describe("Therapy pathways on a phone", () => {
+ it("exposes the mobile picker, step stack, and caution markers", () => {
+ renderPathways();
+
+ expect(screen.getByTestId("therapy-pathway-picker")).toBeInTheDocument();
+ expect(screen.getByTestId("therapy-pathway-steps")).toBeInTheDocument();
+ expect(screen.getByTestId("therapy-pathway-caution")).toBeInTheDocument();
+ });
+
+ it("keeps step open-record actions in the accessibility tree", () => {
+ renderPathways();
+ const steps = within(screen.getByTestId("therapy-pathway-steps"));
+
+ expect(steps.getAllByRole("button", { name: "Open record" })).toHaveLength(2);
+ });
+
+ it("renders full step descriptions on the mobile stack", () => {
+ renderPathways();
+ const steps = within(screen.getByTestId("therapy-pathway-steps"));
+ const description = steps.getByText(/Strongest broad evidence-backed uses are depression and anxiety disorders/i);
+
+ expect(description).toBeInTheDocument();
+ expect(description.className).not.toContain("line-clamp-2");
+ });
+
+ it("opens the pathway picker sheet from the compact bar", async () => {
+ const user = userEvent.setup();
+ renderPathways();
+
+ await user.click(screen.getByRole("button", { name: "Change pathway" }));
+ const panel = screen.getByTestId("therapy-pathway-picker-panel");
+ expect(panel).toBeInTheDocument();
+ expect(within(panel).getByRole("button", { name: /Mood pathway/i })).toBeInTheDocument();
+ });
+
+ it("filters pathways in the picker sheet", async () => {
+ const user = userEvent.setup();
+ renderPathways();
+
+ await user.click(screen.getByRole("button", { name: "Change pathway" }));
+ const panel = screen.getByTestId("therapy-pathway-picker-panel");
+ await user.type(within(panel).getByRole("textbox", { name: "Filter pathways" }), "mood");
+
+ expect(within(panel).getByRole("button", { name: /Mood pathway/i })).toBeInTheDocument();
+ expect(within(panel).queryByRole("button", { name: /Anxiety pathway/i })).not.toBeInTheDocument();
+ });
+});
diff --git a/tests/ui-therapy-pathways.spec.ts b/tests/ui-therapy-pathways.spec.ts
new file mode 100644
index 0000000000..2554c925a8
--- /dev/null
+++ b/tests/ui-therapy-pathways.spec.ts
@@ -0,0 +1,122 @@
+import { expect, test, type Page } from "playwright/test";
+import { resolve } from "node:path";
+
+import { appendPrimaryScrollSpacer, scrollPrimarySurface } from "./playwright-scroll";
+
+const phoneViewport = { width: 390, height: 844 };
+const desktopViewport = { width: 1280, height: 900 };
+
+async function blockExternalRequests(page: Page) {
+ await page.route("**/*", async (route) => {
+ const url = new URL(route.request().url());
+ if (
+ (url.protocol === "http:" || url.protocol === "https:") &&
+ !["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname)
+ ) {
+ await route.abort("blockedbyclient");
+ return;
+ }
+ await route.fallback();
+ });
+}
+
+async function installTherapyFixtures(page: Page) {
+ await page.route("**/therapy-compass-data/*.json", async (route) => {
+ const filename = new URL(route.request().url()).pathname.split("/").at(-1) ?? "";
+ if (!/^(?:therapies(?:-index)?\.[a-f0-9]{16}|pathways|reference)\.json$/.test(filename)) {
+ await route.abort("blockedbyclient");
+ return;
+ }
+ await route.fulfill({
+ contentType: "application/json",
+ path: resolve(process.cwd(), "public", "therapy-compass-data", filename),
+ });
+ });
+}
+
+async function gotoPathways(page: Page, pathway = "anxiety-pathway") {
+ await page.goto(`/therapy-compass/pathways?pathway=${pathway}`, { waitUntil: "domcontentloaded" });
+ await expect(page.locator("#main-content").first()).toBeVisible({ timeout: 15_000 });
+ await page.addStyleTag({
+ content: ":root{--safe-area-top:59px !important;--safe-area-bottom:34px !important;}",
+ });
+ await page.waitForTimeout(700);
+}
+
+async function readCautionGeometry(page: Page) {
+ return page.evaluate(() => {
+ const caution = document.querySelector('[data-testid="therapy-pathway-caution"]');
+ const dock = document.querySelector(".answer-footer-search-dock");
+ if (!caution || !dock) {
+ throw new Error("Expected both the pathway caution band and the answer footer search dock to be present");
+ }
+ const cautionRect = caution.getBoundingClientRect();
+ const dockRect = dock.getBoundingClientRect();
+ return {
+ cautionBottom: cautionRect.bottom,
+ dockTop: dockRect.top,
+ reserve: getComputedStyle(document.querySelector("#main-content")!)
+ .getPropertyValue("--mobile-composer-reserve")
+ .trim(),
+ };
+ });
+}
+
+test.beforeEach(async ({ page }) => {
+ await blockExternalRequests(page);
+ await installTherapyFixtures(page);
+});
+
+test("phone pathways picker opens, anxiety steps scroll above composer, caution stays visible", async ({ page }) => {
+ await page.emulateMedia({ reducedMotion: "no-preference" });
+ await page.setViewportSize(phoneViewport);
+ await gotoPathways(page);
+
+ const picker = page.getByTestId("therapy-pathway-picker");
+ await expect(picker).toBeVisible();
+ await expect(page.getByTestId("therapy-pathway-steps")).toBeVisible();
+ await expect(page.locator(".therapy-pathway-list")).toBeHidden();
+
+ // Selecting a different pathway must actually change the active pathway, not just close the
+ // sheet — clicking the already-active row would pass a check that only asserts the panel closed.
+ await page.getByRole("button", { name: "Change pathway" }).click();
+ await expect(page.getByTestId("therapy-pathway-picker-panel")).toBeVisible();
+ await page.getByRole("button", { name: "Mood pathway" }).click();
+ await expect(page.getByTestId("therapy-pathway-picker-panel")).toBeHidden();
+ await expect(page).toHaveURL(/pathway=mood-pathway/);
+ await expect(picker.getByText("Mood pathway", { exact: false }).first()).toBeVisible();
+
+ // Switch back to Anxiety pathway so the remaining steps/caution/composer assertions below,
+ // which are scoped to the anxiety fixture data, stay valid.
+ await page.getByRole("button", { name: "Change pathway" }).click();
+ await expect(page.getByTestId("therapy-pathway-picker-panel")).toBeVisible();
+ await page.getByRole("button", { name: "Anxiety pathway" }).click();
+ await expect(page.getByTestId("therapy-pathway-picker-panel")).toBeHidden();
+ await expect(page).toHaveURL(/pathway=anxiety-pathway/);
+
+ const lastStep = page.getByTestId("therapy-pathway-steps").getByRole("button", { name: "Open record" }).last();
+ await expect(lastStep).toBeVisible();
+
+ await appendPrimaryScrollSpacer(page, { heightPx: 2400, testId: "therapy-pathways-scroll-spacer" });
+ for (const offset of [80, 160, 240, 320, 400, 520, 640]) {
+ await scrollPrimarySurface(page, offset);
+ }
+
+ await lastStep.scrollIntoViewIfNeeded();
+ const caution = page.getByTestId("therapy-pathway-caution");
+ await expect(caution).toBeVisible();
+
+ const geometry = await readCautionGeometry(page);
+ expect(geometry.cautionBottom).toBeLessThanOrEqual(geometry.dockTop + 1);
+});
+
+test("desktop pathways keeps list and detail visible together", async ({ page }) => {
+ await page.setViewportSize(desktopViewport);
+ await gotoPathways(page);
+
+ await expect(page.locator(".therapy-pathway-list")).toBeVisible();
+ await expect(page.getByTestId("therapy-pathway-steps-desktop")).toBeVisible();
+ await expect(page.getByTestId("therapy-pathway-picker")).toBeHidden();
+ await expect(page.getByRole("heading", { name: "Anxiety pathway", level: 2 })).toBeVisible();
+ await expect(page.getByTestId("therapy-pathway-caution")).toBeVisible();
+});