From 2be346144c9722e76e556fcef823b25ad6e7b941 Mon Sep 17 00:00:00 2001 From: A Charlwood Date: Fri, 13 Feb 2026 00:51:23 +0000 Subject: [PATCH] Task 8: Rebuild ConsultationsView (Experience view) Rebuilt from ref-consultations.md spec with Clinical Luxury styling: - Framer Motion height-only expand/collapse (no opacity fade) - font-ui (Elvaro Grotesque) throughout, Geist Mono for dates/codes - 3px left border color-coded by employer (NHS blue / Tesco teal) - Multi-layered card shadows (shadow-pmr) - Blue tint hover state (#EFF6FF) - H/E/P section headers: uppercase, 12px, letter-spacing 0.05em - Coded entries in Geist Mono with bracket codes - Single-expand accordion behavior - Chevron rotation via Framer Motion - Proper font sizes per spec (13px body, 15px titles, 12px codes) - Focus-visible ring on entry buttons Co-Authored-By: Claude Opus 4.6 --- src/components/views/ConsultationsView.tsx | 180 ++++++++++----------- 1 file changed, 89 insertions(+), 91 deletions(-) diff --git a/src/components/views/ConsultationsView.tsx b/src/components/views/ConsultationsView.tsx index 18ab8e9..e36bfa3 100644 --- a/src/components/views/ConsultationsView.tsx +++ b/src/components/views/ConsultationsView.tsx @@ -1,8 +1,11 @@ -import { useState, useRef, useEffect } from 'react' +import { useState } from 'react' +import { motion, AnimatePresence } from 'framer-motion' import { ChevronDown } from 'lucide-react' import { consultations } from '@/data/consultations' import type { Consultation, ViewId } from '@/types/pmr' +// ─── Props ────────────────────────────────────────────────────────────────── + interface ConsultationsViewProps { onNavigate?: (view: ViewId, itemId?: string) => void initialExpandedId?: string @@ -10,6 +13,7 @@ interface ConsultationsViewProps { export function ConsultationsView({ initialExpandedId }: ConsultationsViewProps) { const [expandedId, setExpandedId] = useState(initialExpandedId ?? null) + const prefersReducedMotion = typeof window !== 'undefined' ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false @@ -21,10 +25,10 @@ export function ConsultationsView({ initialExpandedId }: ConsultationsViewProps) return (
-

- Consultation History +

+ Consultation Journal

- + {consultations.length} entries
@@ -44,6 +48,8 @@ export function ConsultationsView({ initialExpandedId }: ConsultationsViewProps) ) } +// ─── Consultation Entry ───────────────────────────────────────────────────── + interface ConsultationEntryProps { consultation: Consultation isExpanded: boolean @@ -57,168 +63,159 @@ function ConsultationEntry({ onToggle, prefersReducedMotion, }: ConsultationEntryProps) { - const contentRef = useRef(null) - const expandedContentRef = useRef(null) - const [height, setHeight] = useState(isExpanded ? undefined : 0) - - useEffect(() => { - if (prefersReducedMotion) { - setHeight(isExpanded ? undefined : 0) - return - } - - if (isExpanded) { - const timer = setTimeout(() => { - setHeight(undefined) - }, 200) - return () => clearTimeout(timer) - } - setHeight(0) - }, [isExpanded, prefersReducedMotion]) - - useEffect(() => { - if (isExpanded && expandedContentRef.current) { - expandedContentRef.current.focus() - } - }, [isExpanded]) - const keyCodedEntry = consultation.codedEntries[0] return (
+ {/* Collapsed header — always visible */} -
+ {/* Expandable content — height-only animation, NO opacity fade */} + {isExpanded && ( - + + + )} -
+
) } +// ─── Status Dot ───────────────────────────────────────────────────────────── + interface StatusDotProps { isCurrent: boolean } function StatusDot({ isCurrent }: StatusDotProps) { return ( - + ) } +// ─── Expanded Content ─────────────────────────────────────────────────────── + interface ExpandedContentProps { consultation: Consultation - prefersReducedMotion: boolean - contentRef: React.RefObject } -function ExpandedContent({ consultation, prefersReducedMotion, contentRef }: ExpandedContentProps) { - const opacity = prefersReducedMotion ? 1 : undefined - const transition = prefersReducedMotion ? 'none' : 'opacity 150ms ease-out' - +function ExpandedContent({ consultation }: ExpandedContentProps) { return ( -
-
+
+
+ {/* Duration */}
- Duration: - {consultation.duration} + Duration: + + {consultation.duration} +
+ {/* HISTORY */} HISTORY -

+

{consultation.history}

+ {/* EXAMINATION */} EXAMINATION
    {consultation.examination.map((item, index) => ( -
  • +
  • - - {item} + {item}
  • ))}
+ {/* PLAN */} PLAN
    {consultation.plan.map((item, index) => ( -
  • +
  • - - {item} + {item}
  • ))}
+ {/* CODED ENTRIES */} CODED ENTRIES
{consultation.codedEntries.map(entry => ( - + ))}
@@ -226,14 +223,18 @@ function ExpandedContent({ consultation, prefersReducedMotion, contentRef }: Exp ) } +// ─── Section Header ───────────────────────────────────────────────────────── + function SectionHeader({ children }: { children: React.ReactNode }) { return ( -

+

{children}

) } +// ─── Coded Entry ──────────────────────────────────────────────────────────── + interface CodedEntryProps { code: string description: string @@ -241,11 +242,8 @@ interface CodedEntryProps { function CodedEntry({ code, description }: CodedEntryProps) { return ( -
- - [{code}] - - {description} +
+ [{code}] {description}
) }