# UX Improvements Plan — GP Clinical System Theme Polish ## Status Key - [ ] Not started - [x] Complete --- ## Improvement 1: Restructure Profile Summary Text **Status:** [x] Complete **File:** `src/components/tiles/PatientSummaryTile.tsx`, `src/data/profile-content.ts` **Current state:** `PatientSummaryTile` line 129 renders `summaryText` (from `getProfileSummaryText()`) as a single `
` — an 80+ word paragraph wall. **Plan:** 1. In `PatientSummaryTile.tsx`, replace the single `
{summaryText}
` with a structured clinical layout: - **Presenting Complaint** (1–2 sentence summary): "Healthcare leader combining clinical pharmacy expertise with proficiency in Python, SQL, and data analytics. Currently leading population health analytics for NHS Norfolk & Waveney ICB, serving 1.2 million." - **Structured fields** below, rendered as a 2-column grid of label/value pairs: | Label | Value | |-------|-------| | Specialisation | Population Health Analytics & Medicines Optimisation | | Current System | NHS Norfolk & Waveney ICB | | Population | 1.2 million | | Focus Areas | Prescribing analytics, financial modelling, algorithm design, data pipelines | | Key Achievement | £14.6M+ efficiency programmes identified | 2. **Styling approach:** - Brief summary: same `profileTextStyles` (15px, line-height 1.65, `--text-primary`) - Structured fields grid: 2-column CSS grid (`grid-template-columns: auto 1fr`), gap 6px 16px - Labels: `12px uppercase, letter-spacing 0.06em, color: var(--text-tertiary), font-family: var(--font-geist-mono)` — matching existing `fieldLabelStyle` from LastConsultationCard - Values: `13px, font-weight 600, color: var(--text-primary)` — matching existing `fieldValueStyle` from LastConsultationCard - A thin `border-top: 1px solid var(--border-light)` with `padding-top: 14px, margin-top: 14px` separating the summary from the fields 3. **Data source:** Extract structured fields into `profile-content.ts` as a new `structuredProfile` object within `profileContent.profile`. Keep `patientSummaryNarrative` for backward compatibility but add: ```ts structuredProfile: { presentingComplaint: '...', fields: [ { label: 'Specialisation', value: '...' }, { label: 'Current System', value: '...' }, // etc. ] } ``` 4. **Mobile:** Grid goes single-column (`1fr`) at `< 480px`. Use CSS class `profile-fields-grid` with media query. **Verify:** Profile reads as structured clinical data, not a LinkedIn About. Labels match the field label aesthetic used in LastConsultationCard. --- ## Improvement 2: Surface Impact Metrics on Project Cards **Status:** [x] Complete **File:** `src/components/tiles/ProjectsTile.tsx` **Current state:** `ProjectItem` renders thumbnail, name, year, tech stack, skills, status pill — but never touches `project.resultSummary`. The `Investigation` type has `resultSummary: string` with data like "14,000 patients identified", "£2.6M savings". **Plan:** 1. In `ProjectItem` component (around line 170, after the name/year row), add a `resultSummary` display: ```tsx {project.resultSummary && (
{project.resultSummary}
)} ``` 2. Place it between the name row and the tech stack row — immediately after the `
` that wraps project name + year (after line 169). 3. All 6 investigations have `resultSummary`, so it will always show. But the conditional guard is good practice. **Verify:** Each project card shows a bold stat line. Numbers like "14,000 patients identified" are immediately scannable. --- ## Improvement 3: Add Prominent Contact/Download CV CTA **Status:** [x] Complete **File:** `src/components/tiles/PatientSummaryTile.tsx` **Current state:** Contact actions only exist in CommandPalette (`Ctrl+K`). `profile-content.ts` has URLs: `mailto:andy@charlwood.xyz`, `linkedin.com/in/andycharlwood`, `github.com/andycharlwood`. Download CV exists as a quick action type `'download'`. **Plan:** 1. Add a compact action bar below the structured profile fields, above the KPI section. Use a horizontal flex row with 4 buttons: Email, LinkedIn, GitHub, Download CV. 2. **Styling** — match GP system "action buttons" aesthetic: - Container: `display: flex, gap: 8px, flexWrap: wrap, marginTop: 16px, marginBottom: 4px` - Each button: `display: inline-flex, alignItems: center, gap: 6px, padding: '6px 12px', fontSize: '12px', fontWeight: 600, fontFamily: 'var(--font-geist-mono)', letterSpacing: '0.03em', textTransform: 'uppercase', borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--accent)', cursor: 'pointer', transition: '...', textDecoration: 'none'` - Hover: `background: var(--accent-light), borderColor: var(--accent-border)` - Icons: `Mail`, `Linkedin`, `Github`, `Download` from lucide-react, size 13 3. **Links:** - Email → `mailto:andy@charlwood.xyz` - LinkedIn → `https://linkedin.com/in/andycharlwood` (target `_blank`, `rel="noopener noreferrer"`) - GitHub → `https://github.com/andycharlwood` (target `_blank`, `rel="noopener noreferrer"`) - Download CV → trigger the same download logic as CommandPalette (check what it does — likely opens a PDF URL or triggers a download). For now, link to `/AndrewCharlwood_CV.pdf` or check existing download action. If no PDF exists, use a `mailto:` with subject "CV Request" as fallback, or omit. 4. Render as `` tags styled as buttons (not ` {/* Right arrow */} ``` 5. **Hover effect** on arrows: `opacity 0.7 → 1` on hover, match the existing `FullscreenButton` pattern. 6. **Existing hover pause** still works — `onMouseEnter/Leave` on the viewport div pauses the rAF loop. Arrow clicks set `isPausedRef = true` with their own 6s resume timer. If user hovers viewport area after clicking arrow, hover pause takes over. On mouse leave, if the 6s timer hasn't elapsed, the arrow's timer still holds the pause. - Need to handle interaction: when `setPaused(false)` fires from `onMouseLeave`, only unpause if the arrow timer has elapsed. Solution: track `arrowPausedUntil` timestamp. `setPaused` checks if `Date.now() < arrowPausedUntil`. Actually simpler: just let the arrow timeout set `isPausedRef = false` after 6s regardless. The hover handlers already set it. The last writer wins. This is fine — if user hovers after clicking, hover sets `true`. When they leave, `false`. If 6s timer fires while hovering, it sets `false` but hover immediately sets `true` again via the rAF check. Actually the hover sets it on enter/leave events, not continuously. So: mouse leaves → sets false → auto-scroll resumes. That's OK. The 6s pause only matters if the user clicks an arrow and then doesn't hover the carousel. 7. **Reduced motion:** Arrows still work (instant jump, no CSS transition). Auto-scroll stays disabled per existing logic. **Verify:** Arrows visible at left/right edges of carousel. Click jumps one card smoothly. Auto-scroll pauses for 6s after click. Reduced motion: instant jump. Rapid clicks work without jank. --- ## Implementation Order Implement in priority order 1→11. Each improvement is atomic and independently verifiable. **Quality gate after each improvement:** `npm run lint && npm run typecheck && npm run build` ## Files Modified (Summary) | # | Files | |---|-------| | 1 | `PatientSummaryTile.tsx`, `profile-content.ts`, `types/profile-content.ts` | | 2 | `ProjectsTile.tsx` | | 3 | `PatientSummaryTile.tsx` | | 4 | `BootSequence.tsx`, `LoginScreen.tsx`, `App.tsx` | | 5 | `LastConsultationCard.tsx`, `TimelineInterventionsSubsection.tsx` | | 6 | `index.css` | | 7 | `DashboardLayout.tsx` | | 8 | `profile-content.ts` | | 9 | `DetailPanel.tsx`, `DetailPanelContext.tsx` | | 10 | `LastConsultationCard.tsx` | | 11 | `ProjectsTile.tsx` |