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 ( + + ); +} + +/* ------------------------------------------------------------------ */ +/* 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) => ( + + ))} +
+
+
+

+ Resumed: {items[active].query} +

+
+ + +
+

+ Continue +

+
    + {items.map((item, i) => ( +
  • + +
  • + ))} +
+
+ +
+ ); +} + +/* ------------------------------------------------------------------ */ +/* 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 + + ) : ( + + )} + + {on ? "Local protocols ranked in scope UI" : "All jurisdictions"} + +
+ + + +
+ {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. +

+
+ + +
+
+ + + {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} + + +
  • + ))} +
+ +
+ ) : ( +
+ 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) => ( +
  1. + + {i < steps.length - 1 ? ( + + ) : null} +
  2. + ))} +
+
+
+

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.

+
+ +
+
+

Dose and monitoring summary would render here.

+ +
+ + setOpen(false)} + title="Clozapine — source pack" + description="Key monitoring tables, PBS notes, and local protocols." + > + + + + ); +} + +/* ------------------------------------------------------------------ */ +/* 13 Evidence preview */ +/* ------------------------------------------------------------------ */ + +function EvidencePreviewStudy() { + return ( + +
+

+ + Selected evidence — answer still being verified +

+

+ Not final. Incomplete preview is discarded if verification fails. +

+
+ +
+
+
+
+

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

+ +
+

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 ( + + ); + })} +
+
+
+ + +
+
+
+ + ); +} + +/* ------------------------------------------------------------------ */ +/* 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. +

+
+ + +
+ {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 */ }