reference files updated

This commit is contained in:
2026-02-11 22:48:31 +00:00
parent 192d629125
commit cfd0283c78
9 changed files with 1649 additions and 59 deletions
+397
View File
@@ -143,3 +143,400 @@ The breadcrumb updates as the user navigates deeper (e.g., expanding a consultat
- **Investigations:** Click to expand results.
- **Documents:** Click to expand preview.
- **Referrals:** No sub-navigation.
---
## Design Guidance (from /frontend-design)
### Aesthetic Direction
**Clinical Institutional Precision** — The NHS Patient Administration System (PAS) header bar, faithfully reproduced as personal branding. This is not a "medical theme" website. It is a clinical system UI that happens to contain career data instead of patient data. The fidelity to real NHS IT systems (EMIS Web, SystmOne, Lorenzo) is the entire point.
- **Tone**: Utilitarian, institutional, information-dense. No decoration. No gradients. No shadows. The beauty is in the data density, the pipe separators, the monospaced identifiers, the surname-first convention, the green status dot.
- **Typography Discipline**:
- Inter at 600 weight for the patient name — the anchor element
- Geist Mono for structured identifiers (NHS Number, DOB) — monospaced data feels like it came from a database
- Inter at normal weight for demographic text
- The pipe character `|` as a data separator is a deliberate NHS PAS convention
### Design System Tokens
| Token | Value | Usage |
|-------|-------|-------|
| NHS Blue | `#005EB8` | Primary accent, buttons, active states, borders |
| Banner Background | `#334155` (slate-700) | Patient banner background — exact EMIS Web header shade |
| Sidebar Background | `#1E293B` | Dark navigation panel |
| Content Background | `#F5F7FA` | Main content area |
| Border | `#E5E7EB` | 1px solid borders |
| Border Radius | `4px` | All UI elements |
| Green Status | `#22C55E` | Active status dot |
| Font Text | `Inter` | All text content |
| Font Data | `Geist Mono` | Monospaced identifiers |
### Key Design Decisions
1. **220px Sidebar Width**: Fixed, always visible on desktop. No hamburger menu. This is how clinical systems work — persistent direct access.
2. **Alt+1-7 Keyboard Shortcuts**: Each sidebar item has a keyboard shortcut for power users. Arrow key navigation and `/` for search focus.
3. **CV-Friendly Navigation Labels**: Not clinical jargon. The metaphor lives in the layout, not the labels:
- Summary (ClipboardList icon)
- Experience (FileText)
- Skills (Pill)
- Achievements (AlertTriangle)
- Projects (FlaskConical)
- Education (FolderOpen)
- Contact (Send)
4. **Scroll-Triggered Banner Condensation**:
- Full banner: 80px height with three rows (name, demographics, contact/actions)
- Condensed: 48px sticky after 100px scroll, single line
- 200ms smooth transition
- IntersectionObserver for performance
5. **Navigation Item States**:
- Default: white text at 70% opacity, transparent background
- Hover: white text at 100%, background `rgba(255,255,255,0.08)`
- Active: white text at 100%, 3px NHS blue left border, background `rgba(255,255,255,0.12)`, Inter 600 weight
6. **Interface Materialization Animations** (PMRInterface):
- Patient banner slides down (200ms ease-out)
- Sidebar slides from left (250ms ease-out, 50ms delay)
- Content fades in (300ms, 100ms delay after sidebar)
- View switching is INSTANT — no crossfade or slide between views
7. **Mobile Adaptations**:
- Banner collapses to minimal: `CHARLWOOD, A (Mr) | 2211810 | dot`
- Overflow menu for actions
- Bottom nav bar (56px height with safe area padding)
- Sidebar becomes icon-only (56px) with tooltips on tablet
### Implementation Patterns
#### PatientBanner Component Structure
```tsx
// Main container with IntersectionObserver sentinel
<>
<div ref={sentinelRef} className="h-0 w-full absolute top-0" aria-hidden="true" />
<header
className={`
sticky top-0 z-40 w-full
bg-pmr-banner border-b border-slate-600
transition-all duration-200 ease-out
${shouldCondense ? 'h-12' : 'h-20'}
`}
role="banner"
>
{shouldCondense ? <CondensedBanner /> : <FullBanner />}
</header>
</>
```
#### useScrollCondensation Hook
```tsx
export function useScrollCondensation(options: UseScrollCondensationOptions = {}) {
const { threshold = 100 } = options
const [isCondensed, setIsCondensed] = useState(false)
const sentinelRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const sentinel = sentinelRef.current
if (!sentinel) return
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries
setIsCondensed(!entry.isIntersecting)
},
{
rootMargin: `-${threshold}px 0px 0px 0px`,
threshold: 0,
}
)
observer.observe(sentinel)
return () => observer.disconnect()
}, [threshold])
return { isCondensed, sentinelRef }
}
```
#### ClinicalSidebar Navigation Items
```tsx
const navItems: NavItem[] = [
{ id: 'summary', label: 'Summary', icon: <ClipboardList size={18} /> },
{ id: 'consultations', label: 'Experience', icon: <FileText size={18} /> },
{ id: 'medications', label: 'Skills', icon: <Pill size={18} /> },
{ id: 'problems', label: 'Achievements', icon: <AlertTriangle size={18} /> },
{ id: 'investigations', label: 'Projects', icon: <FlaskConical size={18} /> },
{ id: 'documents', label: 'Education', icon: <FolderOpen size={18} /> },
{ id: 'referrals', label: 'Contact', icon: <Send size={18} /> },
]
// Item styling pattern
<button
className={`
w-full h-[44px] px-4 flex items-center gap-3
font-inter text-[14px] font-medium
transition-all duration-150
${isActive
? 'text-white bg-white/[0.12] border-l-[3px] border-pmr-nhsblue font-semibold'
: 'text-white/70 hover:text-white hover:bg-white/[0.08] border-l-[3px] border-transparent'
}
`}
>
<span className="w-[18px] h-[18px]">{icon}</span>
<span>{label}</span>
</button>
```
#### PMRInterface Layout
```tsx
// Main layout structure
<div className="flex h-screen overflow-hidden">
{/* Fixed sidebar */}
<ClinicalSidebar
activeView={activeView}
onViewChange={handleViewChange}
isTablet={isTablet}
/>
{/* Main content area */}
<div className="flex-1 flex flex-col min-w-0">
{/* Sticky patient banner */}
<PatientBanner isMobile={isMobile} isTablet={isTablet} />
{/* Scrollable content */}
<main className="flex-1 overflow-y-auto bg-pmr-content p-6">
{/* View content renders here */}
</main>
</div>
</div>
```
#### Action Button Pattern
```tsx
// Outlined buttons that fill on hover
<button
className="
px-3 py-1.5
text-pmr-nhsblue text-sm font-medium
border border-pmr-nhsblue rounded-[4px]
transition-all duration-150
hover:bg-pmr-nhsblue hover:text-white
"
>
Download CV
</button>
```
### Mobile Considerations
- **Banner**: Shows only name (truncated), NHS number, and status dot
- **Overflow Menu**: Three-dot menu reveals hidden actions (Download CV, Email, LinkedIn)
- **Bottom Nav**: 56px fixed bottom bar with safe area padding for notched devices
- **Touch Targets**: All interactive elements minimum 44px for accessibility
### Accessibility Requirements
- All navigation items keyboard accessible
- Active state has visual indicator (NHS blue left border)
- Reduced motion support: disable animations when `prefers-reduced-motion` is set
- Focus visible states on all interactive elements
- ARIA labels for icon-only buttons
---
## Additional Implementation Notes (from Agent Analysis)
### PatientBanner Component Refinements
#### Animation Improvements
- Replace raw CSS `transition-all duration-200` with Framer Motion's `AnimatePresence` and `motion.div` for smoother layout animations
- Enable cross-fade content between full and condensed banner states
- Use `motion.div` with `initial`, `animate`, `exit` props for content swapping
#### Badge Styling
- Current: `rounded-sm` — Change to true pill shape: `rounded-full` for "Open to opportunities" badge
- Blue pill shape per NHS design system
#### NHS Number Tooltip
- Replace native `title` attribute with custom styled tooltip
- Use Framer Motion for controlled hover reveal
- Tooltip text: "GPhC Registration Number"
#### Mobile Overflow Menu
- Current: raw `useState` toggle with no animation
- Use `AnimatePresence` for enter/exit animations
- Three-dot menu button triggers slide-down panel
#### Action Button Hover States
```tsx
// Outlined buttons with NHS blue that fill on hover
className="
px-3 py-1.5
text-[#005EB8] text-sm font-medium
border border-[#005EB8] rounded-[4px]
transition-all duration-150
hover:bg-[#005EB8] hover:text-white
"
```
### ClinicalSidebar Keyboard Navigation
#### Alt+1-7 Shortcuts Implementation
```tsx
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.altKey && e.key >= '1' && e.key <= '7') {
const index = parseInt(e.key) - 1
const view = navItems[index]
if (view) onViewChange(view.id)
}
if (e.key === '/') {
e.preventDefault()
searchInputRef.current?.focus()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [onViewChange])
```
#### Arrow Key Navigation
- Up/Down arrows navigate between sidebar items
- Focus trap within sidebar when using keyboard
- Visual focus indicator matches hover state
### PMRInterface Layout Structure
#### Materialization Animation Sequence
```tsx
// Staggered entrance animations
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.05,
delayChildren: 0.1
}
}
}
// Patient banner: slides down (200ms ease-out)
const bannerVariants = {
hidden: { y: -80, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { duration: 0.2, ease: 'easeOut' }
}
}
// Sidebar: slides from left (250ms ease-out, 50ms delay)
const sidebarVariants = {
hidden: { x: -220, opacity: 0 },
visible: {
x: 0,
opacity: 1,
transition: { duration: 0.25, ease: 'easeOut', delay: 0.05 }
}
}
// Content: fades in (300ms, 100ms delay after sidebar)
const contentVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { duration: 0.3, delay: 0.15 }
}
}
```
#### View Switching Performance
- Views switch INSTANTLY — no crossfade or slide between views
- Content updates immediately on hash change
- No transition/animation between different view components
- Only initial materialization has animation
### Breadcrumb Component Pattern
```tsx
interface BreadcrumbProps {
currentView: ViewId
expandedItem?: { name: string; type: string }
}
// View name mapping (CV-friendly names)
const viewLabels: Record<ViewId, string> = {
summary: 'Summary',
consultations: 'Experience',
medications: 'Skills',
problems: 'Achievements',
investigations: 'Projects',
documents: 'Education',
referrals: 'Contact'
}
// Styling: Inter 400, 13px, gray-400
// Chevron separators using Lucide ChevronRight
// Clickable links navigate back
```
### Mobile Bottom Navigation
```tsx
// 56px height with safe area padding
<div className="fixed bottom-0 left-0 right-0 h-14 bg-pmr-sidebar border-t border-slate-700 pb-safe">
<div className="flex justify-around items-center h-full px-4">
{navItems.map((item) => (
<button
key={item.id}
className={`
flex flex-col items-center gap-1
${isActive ? 'text-pmr-nhsblue' : 'text-white/60'}
`}
>
{item.icon}
<span className="text-[10px]">{item.label}</span>
</button>
))}
</div>
</div>
```
### TypeScript Types Reference
```tsx
// ViewId type for navigation
export type ViewId =
| 'summary'
| 'consultations'
| 'medications'
| 'problems'
| 'investigations'
| 'documents'
| 'referrals'
// Patient data structure
export interface Patient {
name: string // 'CHARLWOOD, Andrew (Mr)'
displayName: string // 'Andrew Charlwood'
dob: string // '14/02/1993'
nhsNumber: string // '221 181 0'
nhsNumberTooltip: string // 'GPhC Registration Number'
address: string // 'Norwich, NR1'
phone: string
email: string
linkedin: string
status: 'Active' | 'Inactive'
badge?: string // 'Open to opportunities'
}
```