diff --git a/docs/site-map.md b/docs/site-map.md index 838108de7c..2e335f073a 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -1002,6 +1002,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` - `/mockups/favourites-set-navigator` - Route discovered from app directory Source: `src/app/mockups/favourites-set-navigator/page.tsx`. - `/mockups/medication-prescribing` - Route discovered from app directory Source: `src/app/mockups/medication-prescribing/page.tsx`. - `/mockups/mode-dropdown` - Route discovered from app directory Source: `src/app/mockups/mode-dropdown/page.tsx`. +- `/mockups/patient-safety-plan` - Route discovered from app directory Source: `src/app/mockups/patient-safety-plan/page.tsx`. - `/mockups/recent-searches-bottom` - Route discovered from app directory Source: `src/app/mockups/recent-searches-bottom/page.tsx`. - `/mockups/settings-search-clinical` - Route discovered from app directory Source: `src/app/mockups/settings-search-clinical/page.tsx`. - `/mockups/settings-search-general` - Route discovered from app directory Source: `src/app/mockups/settings-search-general/page.tsx`. diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx index ed508daadb..e9a3407fed 100644 --- a/src/app/mockups/mockups-layout-client.tsx +++ b/src/app/mockups/mockups-layout-client.tsx @@ -17,6 +17,10 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { // at the bottom on phones), so the shared universal composer is suppressed here // to avoid a second, floating search bar. const isCalculatorsSearchPageMockup = pathname === "/mockups/calculators-search-page"; + // The safety-plan generator is a full builder workspace with its own primary + // surfaces, so it hides the shared bottom search composer (same treatment as + // the tool and favourites mockups). + const isSafetyPlanMockup = pathname === "/mockups/patient-safety-plan"; return ( diff --git a/src/app/mockups/patient-safety-plan/page.tsx b/src/app/mockups/patient-safety-plan/page.tsx new file mode 100644 index 0000000000..1590ded1ec --- /dev/null +++ b/src/app/mockups/patient-safety-plan/page.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; + +import { PatientSafetyPlanMockup } from "@/components/patient-safety-plan-mockup"; + +export const metadata: Metadata = { + title: "Safety Plan Generator Mockup - Clinical KB", + description: + "Clinician-facing patient safety-plan generator mockup: build the Stanley-Brown six steps with a live patient copy to print or share.", +}; + +export default function PatientSafetyPlanMockupRoute() { + return ; +} diff --git a/src/components/patient-safety-plan-mockup.tsx b/src/components/patient-safety-plan-mockup.tsx new file mode 100644 index 0000000000..a2d52bee67 --- /dev/null +++ b/src/components/patient-safety-plan-mockup.tsx @@ -0,0 +1,829 @@ +"use client"; + +import { + Activity, + Anchor, + Check, + CheckCheck, + ChevronRight, + ClipboardCopy, + Heart, + Users, + HandHelping, + Phone, + PhoneCall, + Plus, + Printer, + RotateCcw, + ShieldCheck, + Sparkles, + Stethoscope, + X, + type LucideIcon, +} from "lucide-react"; +import { useCallback, useMemo, useState } from "react"; + +import { + cn, + clinicalDivider, + eyebrowText, + fieldLabel, + fieldControlPlain, + metadataPill, + panelSubtle, + primaryControl, + toneNeutral, + toneSuccess, +} from "@/components/ui-primitives"; + +/* + * Patient Safety Plan generator — design-exploration mockup. + * + * A clinician builds an evidence-based safety plan *with* the patient (the + * Stanley-Brown Safety Planning Intervention, six prioritised steps), and a + * live patient-facing preview updates as they type — ready to print, save as + * PDF, or hand over. Sample content is seeded so the layout reads fully; every + * field is editable. Australian English + AU crisis resources throughout, per + * the Clinical KB (en-AU) voice. All chrome is token-driven so light/dark, + * reduced-motion and forced-colors follow the shared design system. + */ + +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; + +const softButton = cn( + "inline-flex min-h-10 items-center justify-center gap-2 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-raised)] px-3 text-sm-minus font-bold text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)]", + focusRing, +); + +type StepKind = "list" | "contact"; +type StepKey = "warning" | "coping" | "people" | "support" | "professional" | "environment"; + +type Entry = { id: string; primary: string; secondary?: string }; + +interface StepDef { + key: StepKey; + step: number; + icon: LucideIcon; + /** Clinician-facing card title in the builder. */ + builderTitle: string; + /** Patient-facing, first-person title in the plan. */ + patientTitle: string; + /** Short guidance for the clinician. */ + helper: string; + kind: StepKind; + primaryPlaceholder: string; + secondaryPlaceholder?: string; + emptyHint: string; +} + +const STEPS: StepDef[] = [ + { + key: "warning", + step: 1, + icon: Activity, + builderTitle: "Warning signs", + patientTitle: "Signs a tough time might be building", + helper: "Thoughts, feelings, images or situations that come up before a crisis.", + kind: "list", + primaryPlaceholder: "e.g. Not sleeping for a couple of nights", + emptyHint: "We’ll list these together.", + }, + { + key: "coping", + step: 2, + icon: Anchor, + builderTitle: "Things I can do on my own", + patientTitle: "Things I can do on my own to settle", + helper: "Internal coping strategies — no one else needed. Grounding, movement, distraction.", + kind: "list", + primaryPlaceholder: "e.g. Slow breathing — 4 in, 6 out", + emptyHint: "Add a few calming strategies.", + }, + { + key: "people", + step: 3, + icon: Users, + builderTitle: "People & places that help me feel calm", + patientTitle: "People and places that take my mind off things", + helper: "Social settings and company that distract — not for asking for help yet.", + kind: "list", + primaryPlaceholder: "e.g. Sit in the local library", + emptyHint: "Add people or places that help.", + }, + { + key: "support", + step: 4, + icon: HandHelping, + builderTitle: "People I can ask for help", + patientTitle: "People I can reach out to for support", + helper: "Trusted people the patient would talk to when struggling. Add how to reach them.", + kind: "contact", + primaryPlaceholder: "Name & relationship", + secondaryPlaceholder: "Phone or how to reach them", + emptyHint: "Add at least one trusted person.", + }, + { + key: "professional", + step: 5, + icon: Stethoscope, + builderTitle: "Professionals & crisis lines", + patientTitle: "Professionals and services I can call", + helper: "Clinician, crisis team and 24/7 lines. Confirm the numbers for your service.", + kind: "contact", + primaryPlaceholder: "Service or clinician", + secondaryPlaceholder: "Phone / hours", + emptyHint: "Add clinical contacts and a 24/7 line.", + }, + { + key: "environment", + step: 6, + icon: ShieldCheck, + builderTitle: "Making my space safer", + patientTitle: "Making my space safer", + helper: "Means-safety steps — put time and distance between the patient and anything harmful.", + kind: "list", + primaryPlaceholder: "e.g. A friend holds my medication", + emptyHint: "Agree practical means-safety steps.", + }, +]; + +const SEED: Record = { + warning: [ + { id: "w1", primary: "Not sleeping for a couple of nights" }, + { id: "w2", primary: "Withdrawing from friends and letting messages pile up" }, + { id: "w3", primary: "Thoughts that people would be better off without me" }, + { id: "w4", primary: "Drinking more than usual" }, + ], + coping: [ + { id: "c1", primary: "Walk around the block with music on" }, + { id: "c2", primary: "Cold water on my face, then slow breathing (4 in, 6 out)" }, + { id: "c3", primary: "Make a cup of tea and step out into the garden" }, + ], + people: [ + { id: "p1", primary: "Text my sister and sit with her" }, + { id: "p2", primary: "Take Biscuit to the dog park" }, + { id: "p3", primary: "Work in the local library where it’s quiet" }, + ], + support: [ + { id: "s1", primary: "Priya — my sister", secondary: "0400 000 000" }, + { id: "s2", primary: "Jordan — close friend", secondary: "0400 111 222" }, + ], + professional: [ + { id: "pr1", primary: "Dr Nguyen — GP", secondary: "Rosewood Clinic · (02) 0000 0000" }, + { id: "pr2", primary: "Community mental health crisis team", secondary: "1800 000 000 · 24/7" }, + { id: "pr3", primary: "Lifeline", secondary: "13 11 14 · 24/7" }, + ], + environment: [ + { id: "e1", primary: "Priya keeps my medication and hands it out weekly" }, + { id: "e2", primary: "Leave anything I could misuse with a mate for now" }, + { id: "e3", primary: "Skip alcohol on the days I’m feeling low" }, + ], +}; + +const SEED_REASONS: Entry[] = [ + { id: "r1", primary: "My dog, Biscuit" }, + { id: "r2", primary: "Finishing my apprenticeship" }, + { id: "r3", primary: "The camping trip with mates in spring" }, + { id: "r4", primary: "Being there for my niece" }, +]; + +let seq = 0; +const uid = (prefix: string) => `${prefix}-live-${seq++}`; + +/* ---------- small building blocks ---------- */ + +function AddRow({ + kind, + primaryPlaceholder, + secondaryPlaceholder, + onAdd, +}: { + kind: StepKind; + primaryPlaceholder: string; + secondaryPlaceholder?: string; + onAdd: (primary: string, secondary?: string) => void; +}) { + const [primary, setPrimary] = useState(""); + const [secondary, setSecondary] = useState(""); + + const submit = () => { + const trimmed = primary.trim(); + if (!trimmed) return; + onAdd(trimmed, secondary.trim() || undefined); + setPrimary(""); + setSecondary(""); + }; + + return ( +
+ setPrimary(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + submit(); + } + }} + placeholder={primaryPlaceholder} + aria-label={primaryPlaceholder} + className={cn(fieldControlPlain, "min-h-10")} + /> + {kind === "contact" ? ( + setSecondary(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + submit(); + } + }} + placeholder={secondaryPlaceholder} + aria-label={secondaryPlaceholder} + className={cn(fieldControlPlain, "min-h-10")} + /> + ) : null} + +
+ ); +} + +function EntryChip({ entry, kind, onRemove }: { entry: Entry; kind: StepKind; onRemove: () => void }) { + return ( +
  • +
    +

    {entry.primary}

    + {kind === "contact" && entry.secondary ? ( +

    +

    + ) : null} +
    + +
  • + ); +} + +function StepBuilderCard({ + def, + entries, + onAdd, + onRemove, +}: { + def: StepDef; + entries: Entry[]; + onAdd: (primary: string, secondary?: string) => void; + onRemove: (id: string) => void; +}) { + const Icon = def.icon; + const filled = entries.length > 0; + + return ( +
    +
    + + +
    +
    + + Step {def.step} + + {filled ? ( + + ) : null} +
    +

    {def.builderTitle}

    +

    {def.helper}

    +
    + {entries.length} +
    + + {entries.length ? ( +
      + {entries.map((entry) => ( + onRemove(entry.id)} /> + ))} +
    + ) : ( +

    + {def.emptyHint} +

    + )} + + +
    + ); +} + +/* ---------- patient-facing preview ---------- */ + +function PreviewStep({ def, entries }: { def: StepDef; entries: Entry[] }) { + const Icon = def.icon; + return ( +
  • + + {def.step} + +
    +
    +
    + {entries.length ? ( +
      + {entries.map((entry) => ( +
    • +
    • + ))} +
    + ) : ( +

    + To be completed together. +

    + )} +
    +
  • + ); +} + +/* ---------- root ---------- */ + +export function PatientSafetyPlanMockup() { + const [entries, setEntries] = useState>(SEED); + const [reasons, setReasons] = useState(SEED_REASONS); + const [patient, setPatient] = useState(""); + const [planDate, setPlanDate] = useState(""); + const [mobileTab, setMobileTab] = useState<"build" | "preview">("build"); + const [copied, setCopied] = useState(false); + const [finalised, setFinalised] = useState(false); + + const addEntry = useCallback((key: StepKey, primary: string, secondary?: string) => { + setEntries((prev) => ({ ...prev, [key]: [...prev[key], { id: uid(key), primary, secondary }] })); + setFinalised(false); + }, []); + + const removeEntry = useCallback((key: StepKey, id: string) => { + setEntries((prev) => ({ ...prev, [key]: prev[key].filter((entry) => entry.id !== id) })); + setFinalised(false); + }, []); + + const filledSteps = useMemo(() => STEPS.filter((step) => entries[step.key].length > 0).length, [entries]); + const ready = filledSteps === STEPS.length; + + const planText = useMemo(() => { + const lines: string[] = [ + "MY SAFETY PLAN", + patient ? `For: ${patient}` : "", + planDate ? `Date: ${planDate}` : "", + "", + ]; + for (const step of STEPS) { + lines.push(`${step.step}. ${step.patientTitle}`); + const rows = entries[step.key]; + if (rows.length) { + for (const row of rows) lines.push(` • ${row.primary}${row.secondary ? ` — ${row.secondary}` : ""}`); + } else { + lines.push(" • (to be completed)"); + } + lines.push(""); + } + if (reasons.length) { + lines.push("MY REASONS FOR LIVING"); + for (const reason of reasons) lines.push(` • ${reason.primary}`); + lines.push(""); + } + lines.push("In an emergency: call 000 or go to your nearest Emergency Department."); + lines.push("24/7 support: Lifeline 13 11 14 · Suicide Call Back Service 1300 659 467."); + return lines.filter((line, index, all) => !(line === "" && all[index - 1] === "")).join("\n"); + }, [entries, patient, planDate, reasons]); + + const copyPlan = async () => { + try { + await navigator.clipboard.writeText(planText); + setCopied(true); + window.setTimeout(() => setCopied(false), 1600); + } catch { + /* clipboard unavailable in some embeds — mockup-safe no-op */ + } + }; + + const printPlan = () => { + if (typeof window !== "undefined") window.print(); + }; + + const clearAll = () => { + setEntries({ warning: [], coping: [], people: [], support: [], professional: [], environment: [] }); + setReasons([]); + setFinalised(false); + }; + + return ( +
    + {/* Tool header */} +
    +
    +
    + + +
    +

    Clinical KB · Clinical tool

    +

    + Safety plan generator +

    +

    + Build an evidence-based safety plan with your patient — the six prioritised steps — then print, + save as PDF, or hand over a copy. +

    +
    +
    + +
    +
    + Plan progress +
    +
    + {STEPS.map((step) => ( + + ))} +
    + + {ready ? "Ready to share" : `${filledSteps}/${STEPS.length} steps`} + +
    +
    + + +
    +
    +
    + + {/* Mobile pane switch */} +
    +
    + {(["build", "preview"] as const).map((tab) => ( + + ))} +
    +
    + +
    + {/* ---------- Builder ---------- */} +
    +
    +

    + Build the plan +

    + +
    + + {/* Patient context */} +
    +
    + + { + setPatient(event.target.value); + setFinalised(false); + }} + placeholder="e.g. Sam R." + className={fieldControlPlain} + /> +
    +
    + + { + setPlanDate(event.target.value); + setFinalised(false); + }} + placeholder="e.g. 12 Aug 2026" + className={fieldControlPlain} + /> +
    +
    + + {STEPS.map((def) => ( + addEntry(def.key, primary, secondary)} + onRemove={(id) => removeEntry(def.key, id)} + /> + ))} + + {/* Reasons for living */} +
    +
    + + +
    +

    + Reasons for living +

    +

    + The people, plans and things that matter — worth coming back to. +

    +
    + {reasons.length} +
    + {reasons.length ? ( +
      + {reasons.map((reason) => ( +
    • + {reason.primary} + +
    • + ))} +
    + ) : null} + { + setReasons((prev) => [...prev, { id: uid("reason"), primary }]); + setFinalised(false); + }} + /> +
    +
    + + {/* ---------- Preview ---------- */} +
    + {/* Preview toolbar */} +
    +

    + Patient copy +

    +
    + + +
    +
    + + {finalised ? ( +
    +
    + ) : null} + + {/* The plan document */} +
    +
    +
    +
    +

    + Keeping myself safe +

    +
    + {patient ? patient : "Name: ________________"} + {planDate ? planDate : "Date: ____________"} +
    +

    + When things get hard or I’m having thoughts of suicide, I’ll work through these steps in order. I can + start anywhere — even one step can help. +

    +
    + +
      + {STEPS.map((def) => ( + + ))} +
    + + {reasons.length ? ( +
    +
    +
    +

    + {reasons.map((reason) => reason.primary).join(" · ")} +

    +
    + ) : null} + + {/* Emergency escalation */} +
    +
    +
    + + +

    +

    +
    + +
    +

    + Based on the Stanley–Brown Safety Planning Intervention (Stanley & Brown, 2012). +

    +

    + A supportive plan built collaboratively — not a substitute for clinical risk assessment. Confirm crisis + numbers for your local service. +

    +
    +
    + +

    +

    +
    +
    +
    + ); +}