diff --git a/docs/site-map.md b/docs/site-map.md
index 6da4767960..62fbecd6bb 100644
--- a/docs/site-map.md
+++ b/docs/site-map.md
@@ -989,6 +989,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir
- `/mockups/calculators-popup-sheet` - Route discovered from app directory Source: `src/app/mockups/calculators-popup-sheet/page.tsx`.
- `/mockups/calculators-search` - Route discovered from app directory Source: `src/app/mockups/calculators-search/page.tsx`.
- `/mockups/calculators-search-page` - Route discovered from app directory Source: `src/app/mockups/calculators-search-page/page.tsx`.
+- `/mockups/clinician-workflow-features` - Route discovered from app directory Source: `src/app/mockups/clinician-workflow-features/page.tsx`.
- `/mockups/document-navigation-contract` - Route discovered from app directory Source: `src/app/mockups/document-navigation-contract/page.tsx`.
- `/mockups/document-navigation-final` - Route discovered from app directory Source: `src/app/mockups/document-navigation-final/page.tsx`.
- `/mockups/document-navigation-final-review` - Route discovered from app directory Source: `src/app/mockups/document-navigation-final-review/page.tsx`.
diff --git a/mockups/README.md b/mockups/README.md
index 690fd4e85e..60e47c4060 100644
--- a/mockups/README.md
+++ b/mockups/README.md
@@ -2,6 +2,15 @@
This folder collects notes for mockup routes that live under `src/app/mockups/`.
+## Clinician workflow features (2026-08)
+
+Interactive study for features 07–15 (Continue strip, Split reading, Prefer local, Refer, Forms pathway, Prescribing source pack, Evidence preview, Phone Filter sheet, Ward handoff):
+
+- Route: `/mockups/clinician-workflow-features`
+- Component: `src/components/clinician-workflow-features-mockups.tsx`
+- Uses design-system `Chip`, `Button`, `Sheet`, `VerificationNotice` and Clinical White tokens.
+- Shared search chrome is suppressed (study owns its frames and sheets).
+
## Authoritative route list
The generated route map in [`docs/site-map.md`](../docs/site-map.md) (mockups section) is the source of truth for runnable mockup URLs. Regenerate it after adding or removing mockup routes:
diff --git a/src/app/mockups/clinician-workflow-features/page.tsx b/src/app/mockups/clinician-workflow-features/page.tsx
new file mode 100644
index 0000000000..9305a11ca5
--- /dev/null
+++ b/src/app/mockups/clinician-workflow-features/page.tsx
@@ -0,0 +1,5 @@
+import { ClinicianWorkflowFeaturesMockupsPage } from "@/components/clinician-workflow-features-mockups";
+
+export default function ClinicianWorkflowFeaturesMockupRoute() {
+ return ;
+}
diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx
index 4a2c9d166f..c1fe1d8af8 100644
--- a/src/app/mockups/mockups-layout-client.tsx
+++ b/src/app/mockups/mockups-layout-client.tsx
@@ -36,6 +36,8 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
const isWarningConsolidationMockup = pathname === "/mockups/warning-consolidation";
const isWarningLineMockup = pathname === "/mockups/warning-line";
const isAnswerHomeProposalMockup = pathname === "/mockups/answer-home-proposal";
+ // Own feature frames + sheets; shared composer would compete with the study.
+ const isClinicianWorkflowFeaturesMockup = pathname === "/mockups/clinician-workflow-features";
// Draws its own sticky chrome + device frames for /privacy; shared shell would
// read as a second real header over the study.
const isPrivacyPageDirectionsMockup = pathname === "/mockups/privacy-page-directions";
@@ -88,6 +90,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
!isWarningConsolidationMockup &&
!isWarningLineMockup &&
!isAnswerHomeProposalMockup &&
+ !isClinicianWorkflowFeaturesMockup &&
!isPrivacyPageDirectionsMockup &&
!isPrivacyLiveSignalPerfectedMockup &&
!isSearchLensMenuMockup &&
@@ -104,6 +107,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
!isWarningConsolidationMockup &&
!isWarningLineMockup &&
!isAnswerHomeProposalMockup &&
+ !isClinicianWorkflowFeaturesMockup &&
!isPrivacyPageDirectionsMockup &&
!isPrivacyLiveSignalPerfectedMockup &&
!isSearchLensMenuMockup &&
diff --git a/src/components/clinician-workflow-features-mockups.tsx b/src/components/clinician-workflow-features-mockups.tsx
new file mode 100644
index 0000000000..f8b810a762
--- /dev/null
+++ b/src/components/clinician-workflow-features-mockups.tsx
@@ -0,0 +1,751 @@
+"use client";
+
+/**
+ * Design-scratch study: nine clinician-workflow UX features (#7–#15).
+ * Uses live design-system tokens and components (Chip, Button, Sheet,
+ * VerificationNotice). Mockups-only — not wired into production modes.
+ */
+
+import { useState, type ReactNode } from "react";
+import {
+ ArrowRight,
+ BookOpen,
+ Check,
+ ChevronRight,
+ ClipboardCopy,
+ FileText,
+ Filter,
+ History,
+ Hospital,
+ MapPin,
+ Pill,
+ Share2,
+ Sparkles,
+ TriangleAlert,
+ X,
+} from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { Chip } from "@/components/ui/chip";
+import { Sheet } from "@/components/ui/sheet";
+import { VerificationNotice } from "@/components/ui/verification-notice";
+import { cn, panelSubtle } from "@/components/ui-primitives";
+
+type FeatureId =
+ | "continue"
+ | "split-read"
+ | "prefer-local"
+ | "refer"
+ | "forms-path"
+ | "rx-pack"
+ | "evidence-preview"
+ | "filter-sheet"
+ | "handoff";
+
+const FEATURES: Array<{
+ id: FeatureId;
+ number: string;
+ title: string;
+ oneLiner: string;
+}> = [
+ { id: "continue", number: "07", title: "Continue strip", oneLiner: "Resume last clinical contexts in one tap." },
+ { id: "split-read", number: "08", title: "Split reading", oneLiner: "Claim beside highlighted source page." },
+ { id: "prefer-local", number: "09", title: "Prefer local", oneLiner: "WA / Perth source lens chip." },
+ { id: "refer", number: "10", title: "Refer action", oneLiner: "Answer → Services with query filled." },
+ { id: "forms-path", number: "11", title: "Forms pathway", oneLiner: "WA MH Act step strip." },
+ { id: "rx-pack", number: "12", title: "Prescribing pack", oneLiner: "Key sources drawer for a medicine." },
+ { id: "evidence-preview", number: "13", title: "Evidence preview", oneLiner: "Sources while answer verifies." },
+ { id: "filter-sheet", number: "14", title: "Phone Filter sheet", oneLiner: "One bottom-sheet filter pattern." },
+ { id: "handoff", number: "15", title: "Ward handoff pack", oneLiner: "Copy short cited handoff snippet." },
+];
+
+const focusRing =
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";
+
+function Frame({ label, children, phone }: { label: string; children: ReactNode; phone?: boolean }) {
+ return (
+
+
{label}
+
+ {children}
+
+
+ );
+}
+
+function FeatureNav({ active, onSelect }: { active: FeatureId; onSelect: (id: FeatureId) => void }) {
+ return (
+
+ {FEATURES.map((f) => {
+ const selected = f.id === active;
+ return (
+ onSelect(f.id)}
+ aria-pressed={selected}
+ className={cn(
+ "inline-flex min-h-12 items-center gap-2 rounded-lg border px-3 text-left text-sm font-semibold transition",
+ focusRing,
+ selected
+ ? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"
+ : "border-[color:var(--border)] bg-[color:var(--surface-raised)] text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)]",
+ )}
+ >
+ {f.number}
+ {f.title}
+
+ );
+ })}
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* 07 Continue strip */
+/* ------------------------------------------------------------------ */
+
+function ContinueStripStudy() {
+ const [active, setActive] = useState(0);
+ const items = [
+ { mode: "Answer", query: "Clozapine ANC threshold", source: "WA Clozapine Protocol" },
+ { mode: "Documents", query: "Lithium monitoring table", source: "EMHS Lithium Guide" },
+ { mode: "Prescribing", query: "Acamprosate renal dose", source: "AMH monograph" },
+ ];
+ return (
+
+
+
+
Continue
+
+ {items.map((item, i) => (
+ setActive(i)}
+ className={cn(
+ "min-h-12 shrink-0 rounded-lg border px-3 py-2 text-left transition",
+ focusRing,
+ i === active
+ ? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)]"
+ : "border-[color:var(--border)] bg-[color:var(--surface-raised)] hover:bg-[color:var(--surface)]",
+ )}
+ >
+
+
+ {item.mode}
+
+
+ {item.query}
+
+
+ {item.source}
+
+
+ ))}
+
+
+
+
+ Resumed: {items[active].query}
+
+
+
+
+
+
+ Continue
+
+
+ {items.map((item, i) => (
+
+ setActive(i)}
+ className={cn(
+ "flex min-h-12 w-full items-center gap-3 rounded-lg border px-3 py-2 text-left",
+ focusRing,
+ i === active
+ ? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)]"
+ : "border-[color:var(--border)] bg-[color:var(--surface-raised)]",
+ )}
+ >
+
+
+
+ {item.query}
+
+
+ {item.mode} · {item.source}
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* 08 Split reading */
+/* ------------------------------------------------------------------ */
+
+function SplitReadingStudy() {
+ return (
+
+
+
+
+ Claim
+
+
+ “Interrupt clozapine when ANC falls below{" "}
+ 1.5 × 10⁹/L and seek
+ haematology advice.”
+
+
Cited: WA Clozapine Protocol · p.12 · Table 3
+
+
+
+
+
+
+
Source preview
+
+ Verify here
+
+
+
+
+ Table 3 · ANC actions
+
+
+
+ ANC
+ Action
+
+
+ < 1.5
+ Interrupt + haematology
+
+
+ 1.5–2.0
+ Increase monitoring
+
+
+
+
+
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* 09 Prefer local */
+/* ------------------------------------------------------------------ */
+
+function PreferLocalStudy() {
+ const [on, setOn] = useState(true);
+ const authorities = ["WA Health", "EMHS", "SMHS", "PCH"];
+ return (
+
+
+
+
+ {on ? (
+ setOn(false)}
+ >
+ Prefer WA / Perth
+
+ ) : (
+ setOn(true)}>
+ Prefer local
+
+ )}
+
+ {on ? "Local protocols ranked in scope UI" : "All jurisdictions"}
+
+
+
+ {(on
+ ? [
+ { title: "EMHS Lithium Monitoring", tag: "EMHS", local: true },
+ { title: "WA Clozapine Protocol", tag: "WA Health", local: true },
+ { title: "SMHS Acute Sedation", tag: "SMHS", local: true },
+ ]
+ : [
+ { title: "NICE Depression NG222", tag: "NICE", local: false },
+ { title: "EMHS Lithium Monitoring", tag: "EMHS", local: true },
+ { title: "RANZCP Clinical Guideline", tag: "RANZCP", local: false },
+ ]
+ ).map((row) => (
+
+ {row.title}
+
+ {row.tag}
+
+
+ ))}
+
+
+
+
+ {authorities.map((a) => (
+
+ {a}
+
+ ))}
+
+
+ Optional UI scope only — does not change retrieval ranking formulas.
+
+
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* 10 Refer action */
+/* ------------------------------------------------------------------ */
+
+function ReferActionStudy() {
+ const [opened, setOpened] = useState(false);
+ return (
+
+
+
+
+ For Aboriginal and Torres Strait Islander people in crisis in WA, 13YARN provides 24/7 culturally safe
+ support.
+
+
+
+ Copy
+
+ setOpened(true)}>
+ Refer
+
+
+
+
+
+ {opened ? (
+
+
crisis ATSI phone WA
+
Services · 3 matches
+
+ {[
+ { name: "13YARN", note: "24/7 · National / WA accessible" },
+ { name: "Aboriginal Mental Health Liaison", note: "Hospital · EMHS" },
+ { name: "Crisis assessment (local CMH)", note: "Fremantle catchment" },
+ ].map((s) => (
+
+
+ {s.name}
+ {s.note}
+
+
+
+ ))}
+
+
setOpened(false)}>
+ Reset demo
+
+
+ ) : (
+
+ Tap Refer to open Services with the same query.
+
+ )}
+
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* 11 Forms pathway */
+/* ------------------------------------------------------------------ */
+
+function FormsPathwayStudy() {
+ const steps = [
+ { id: "4a", label: "Transport 4A", done: true },
+ { id: "4b", label: "Extension 4B", done: false, current: true },
+ { id: "psolis", label: "PSOLIS", done: false },
+ ];
+ return (
+
+
+
Pathway
+
+ {steps.map((step, i) => (
+
+
+ {step.done ? : null}
+ {i + 1}
+ {step.label}
+
+ {i < steps.length - 1 ? (
+
+ ) : null}
+
+ ))}
+
+
+
+
Form 4B — Extension
+
+ Continues involuntary transport authority. Confirm against the current WA MH Act form pack.
+
+
+
+ Form
+
+
+ WA MH Act
+
+
+
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* 12 Prescribing source pack */
+/* ------------------------------------------------------------------ */
+
+function PrescribingPackStudy() {
+ const [open, setOpen] = useState(false);
+ return (
+ <>
+
+
+
+
+
+ Clozapine
+
+
Confirm against source before prescribing.
+
+
setOpen(true)}>
+ Source pack
+
+
+
+
Dose and monitoring summary would render here.
+
+
+
+ setOpen(false)}
+ title="Clozapine — source pack"
+ description="Key monitoring tables, PBS notes, and local protocols."
+ >
+
+ {[
+ { title: "WA Clozapine Protocol · ANC table", kind: "Local protocol" },
+ { title: "AMH — monitoring schedule", kind: "Monograph" },
+ { title: "PBS Authority notes", kind: "PBS" },
+ ].map((row) => (
+
+
+
+ {row.title}
+ {row.kind}
+
+
+
+
+ ))}
+
+
+ >
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* 13 Evidence preview */
+/* ------------------------------------------------------------------ */
+
+function EvidencePreviewStudy() {
+ return (
+
+
+
+
+ Selected evidence — answer still being verified
+
+
+ Not final. Incomplete preview is discarded if verification fails.
+
+
+
+ {[
+ { title: "WA Clozapine Protocol", page: "p.12" },
+ { title: "EMHS Haematology pathway", page: "p.4" },
+ { title: "AMH Clozapine monograph", page: "Monitoring" },
+ ].map((s, i) => (
+
+
+ {i + 1}
+
+
+ {s.title}
+ {s.page}
+
+
+
+ ))}
+
+
+
+
Checking citations…
+
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* 14 Shared phone Filter sheet */
+/* ------------------------------------------------------------------ */
+
+function FilterSheetStudy() {
+ const [open, setOpen] = useState(true);
+ const [types, setTypes] = useState(["Guideline"]);
+ const toggle = (v: string) => setTypes((prev) => (prev.includes(v) ? prev.filter((x) => x !== v) : [...prev, v]));
+
+ return (
+
+
+
lithium monitoring
+
setOpen(true)}>
+ Filter
+ {types.length ? (
+
+ {types.length}
+
+ ) : null}
+
+
+ Results list would sit here.
+ setOpen(false)}
+ title="Filter"
+ description="Same sheet pattern across catalogue modes."
+ mobilePlacement="bottom"
+ mobileSize="content"
+ >
+
+
+
+ Document type
+
+
+ {["Guideline", "Table", "Protocol", "Form"].map((t) => {
+ const on = types.includes(t);
+ return (
+ toggle(t)}
+ className={cn(
+ "inline-flex min-h-12 items-center rounded-lg border px-3 text-sm font-semibold",
+ focusRing,
+ on
+ ? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"
+ : "border-[color:var(--border)] bg-[color:var(--surface-raised)] text-[color:var(--text-muted)]",
+ )}
+ >
+ {t}
+
+ );
+ })}
+
+
+
+ setTypes([])}>
+ Clear
+
+ setOpen(false)}>
+ Apply filters
+
+
+
+
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* 15 Ward handoff pack */
+/* ------------------------------------------------------------------ */
+
+function HandoffPackStudy() {
+ const [copied, setCopied] = useState(false);
+ const snippet = [
+ "HANDOFF (draft — not a clinical note)",
+ "Q: Clozapine ANC interrupt threshold",
+ "Summary: Interrupt below 1.5×10⁹/L; seek haematology.",
+ "Sources: (1) WA Clozapine Protocol p.12 (2) EMHS pathway p.4",
+ "Verify every claim against the linked source before acting.",
+ ].join("\n");
+
+ return (
+
+
+
+
+ Interrupt clozapine when ANC < 1.5 × 10⁹/L and seek haematology advice.
+
+
+
+ Copy answer
+
+ {
+ try {
+ await navigator.clipboard.writeText(snippet);
+ setCopied(true);
+ } catch {
+ /* demo-only — do not claim success if clipboard failed */
+ }
+ }}
+ >
+ Ward handoff
+
+
+ {copied ? (
+
+
+ Handoff snippet copied
+
+ ) : null}
+
+
+
+
+ {snippet}
+
+
+
+ );
+}
+
+function StudyBody({ id }: { id: FeatureId }) {
+ switch (id) {
+ case "continue":
+ return ;
+ case "split-read":
+ return ;
+ case "prefer-local":
+ return ;
+ case "refer":
+ return ;
+ case "forms-path":
+ return ;
+ case "rx-pack":
+ return ;
+ case "evidence-preview":
+ return ;
+ case "filter-sheet":
+ return ;
+ case "handoff":
+ return ;
+ }
+}
+
+export function ClinicianWorkflowFeaturesMockupsPage() {
+ const [active, setActive] = useState("continue");
+ const meta = FEATURES.find((f) => f.id === active)!;
+
+ return (
+
+
+
+ Design study · mockups only
+
+
+ Clinician workflow features
+
+
+ Interactive comps for features 07–15. Built with design-system tokens, Chip, Button, Sheet, and
+ VerificationNotice. Not production routes — use these to review UX before adoption PRs.
+
+
+
+
+
+
+
+
+
{meta.number}
+
+ {meta.title}
+
+
{meta.oneLiner}
+
+
+
+
+
+
+
+ Implementation notes
+
+
+ Clinical colour reserved for source/safety state — not decoration.
+ Prefer local is UI scope only; RAG ranking stays untouched.
+ Evidence preview must discard on verification failure (conservative degrade).
+ Phone Filter sheet must keep one-composer / search-chrome ownership rules.
+ Handoff pack is a draft snippet — never presented as a final clinical note.
+
+
+
+ );
+}
diff --git a/src/components/patient-safety-plan.tsx b/src/components/patient-safety-plan.tsx
index 80b9f74d20..5503956a31 100644
--- a/src/components/patient-safety-plan.tsx
+++ b/src/components/patient-safety-plan.tsx
@@ -21,7 +21,7 @@ import {
X,
type LucideIcon,
} from "lucide-react";
-import { useCallback, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { NavigationBackButton } from "@/components/navigation-back-button";
import { appModeHomeHref } from "@/lib/app-modes";
@@ -479,8 +479,20 @@ export function PatientSafetyPlan() {
// Per-instance id counter — avoids a module-level mutable that would persist
// across remounts; ids only need to be unique within this mounted plan.
const uidRef = useRef(0);
+ const copiedResetRef = useRef | null>(null);
const uid = useCallback((prefix: string) => `${prefix}-live-${uidRef.current++}`, []);
+ // Clear the copy-feedback timer on unmount so jsdom teardown / remount cannot
+ // fire setState after the environment tears down (`window is not defined`).
+ useEffect(() => {
+ return () => {
+ if (copiedResetRef.current != null) {
+ clearTimeout(copiedResetRef.current);
+ copiedResetRef.current = null;
+ }
+ };
+ }, []);
+
const addEntry = useCallback(
(key: StepKey, primary: string, secondary?: string) => {
setEntries((prev) => ({ ...prev, [key]: [...prev[key], { id: uid(key), primary, secondary }] }));
@@ -556,7 +568,11 @@ export function PatientSafetyPlan() {
try {
await navigator.clipboard.writeText(planText);
setCopied(true);
- window.setTimeout(() => setCopied(false), 1600);
+ if (copiedResetRef.current != null) clearTimeout(copiedResetRef.current);
+ copiedResetRef.current = setTimeout(() => {
+ copiedResetRef.current = null;
+ setCopied(false);
+ }, 1600);
} catch {
/* clipboard unavailable in some embeds — safe no-op */
}