US-032: Reduced motion audit, final cleanup, and visual review

- Add prefers-reduced-motion overrides for SubNav button transitions
- Add prefers-reduced-motion overrides for smooth scroll behavior
- Fix connection status dot/text transitions to respect reduced motion
- Create ProjectDetail.tsx renderer and wire into DetailPanel
- Remove placeholder fallback from DetailPanel (all types now covered)
- Delete unused files: useBreakpoint.ts, profile.ts
- Remove unused legacy --pmr-* CSS variables (18 properties)
- Remove unused .pmr-theme CSS utility class
This commit is contained in:
2026-02-14 03:20:31 +00:00
parent 071b1b78ae
commit 088b783731
6 changed files with 226 additions and 111 deletions
+2 -23
View File
@@ -9,6 +9,7 @@ import { ConsultationDetail } from './detail/ConsultationDetail'
import { SkillDetail } from './detail/SkillDetail'
import { SkillsAllDetail } from './detail/SkillsAllDetail'
import { EducationDetail } from './detail/EducationDetail'
import { ProjectDetail } from './detail/ProjectDetail'
// Width mapping from content type
const widthMap: Record<DetailPanelContent['type'], 'narrow' | 'wide'> = {
@@ -221,29 +222,7 @@ export function DetailPanel() {
{content.type === 'skill' && <SkillDetail skill={content.skill} />}
{content.type === 'skills-all' && <SkillsAllDetail category={content.category} />}
{content.type === 'education' && <EducationDetail document={content.document} />}
{/* Other content types - placeholder for future stories */}
{content.type !== 'kpi' &&
content.type !== 'consultation' &&
content.type !== 'career-role' &&
content.type !== 'skill' &&
content.type !== 'skills-all' &&
content.type !== 'education' && (
<div
style={{
fontFamily: 'var(--font-ui)',
color: 'var(--text-secondary)',
fontSize: '14px',
}}
>
<p>
Detail panel for: <strong>{content.type}</strong>
</p>
<p style={{ marginTop: '8px', fontSize: '12px' }}>
Content renderers will be implemented in subsequent user stories.
</p>
</div>
)}
{content.type === 'project' && <ProjectDetail investigation={content.investigation} />}
</div>
</div>
</>
+2 -2
View File
@@ -370,7 +370,7 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
height: '6px',
borderRadius: '50%',
backgroundColor: connectionState === 'connected' ? '#059669' : '#DC2626',
transition: 'background-color 300ms ease',
transition: prefersReducedMotion ? 'none' : 'background-color 300ms ease',
flexShrink: 0,
}}
/>
@@ -379,7 +379,7 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
fontFamily: "var(--font-geist-mono, 'Geist Mono', monospace)",
fontSize: '10px',
color: connectionState === 'connected' ? '#059669' : '#8DA8A5',
transition: 'color 300ms ease',
transition: prefersReducedMotion ? 'none' : 'color 300ms ease',
}}
>
{connectionState === 'connected'
+211
View File
@@ -0,0 +1,211 @@
import { ExternalLink } from 'lucide-react'
import type { Investigation } from '@/types/pmr'
interface ProjectDetailProps {
investigation: Investigation
}
const statusColorMap: Record<Investigation['status'], string> = {
Complete: '#059669',
Ongoing: '#D97706',
Live: '#0D6E6E',
}
const statusBgMap: Record<Investigation['status'], string> = {
Complete: 'rgba(5,150,105,0.08)',
Ongoing: 'rgba(217,119,6,0.08)',
Live: 'rgba(10,128,128,0.08)',
}
export function ProjectDetail({ investigation }: ProjectDetailProps) {
const statusColor = statusColorMap[investigation.status]
const statusBg = statusBgMap[investigation.status]
return (
<div
style={{
fontFamily: 'var(--font-ui)',
display: 'flex',
flexDirection: 'column',
gap: '24px',
}}
>
{/* Header: name + year + status */}
<div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
flexWrap: 'wrap',
marginBottom: '8px',
}}
>
<span
style={{
fontSize: '11px',
fontFamily: 'var(--font-geist-mono)',
color: 'var(--text-tertiary)',
}}
>
{investigation.requestedYear}
</span>
<span
style={{
display: 'inline-block',
padding: '2px 8px',
fontSize: '11px',
fontWeight: 600,
color: statusColor,
backgroundColor: statusBg,
borderRadius: 'var(--radius-sm)',
}}
>
{investigation.status}
</span>
</div>
<div
style={{
fontSize: '12px',
fontFamily: 'var(--font-geist-mono)',
color: 'var(--text-tertiary)',
}}
>
{investigation.requestingClinician}
</div>
</div>
{/* Methodology */}
<div>
<h3
style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text-secondary)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
marginBottom: '8px',
}}
>
Methodology
</h3>
<p
style={{
fontSize: '14px',
lineHeight: '1.6',
color: 'var(--text-primary)',
margin: 0,
}}
>
{investigation.methodology}
</p>
</div>
{/* Tech stack tags */}
<div>
<h3
style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text-secondary)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
marginBottom: '8px',
}}
>
Tech Stack
</h3>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
{investigation.techStack.map((tech) => (
<span
key={tech}
style={{
padding: '3px 10px',
fontSize: '11px',
fontWeight: 500,
fontFamily: 'var(--font-geist-mono)',
color: 'var(--accent)',
backgroundColor: 'var(--accent-light)',
borderRadius: 'var(--radius-sm)',
border: '1px solid var(--accent-border)',
}}
>
{tech}
</span>
))}
</div>
</div>
{/* Results */}
<div>
<h3
style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text-secondary)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
marginBottom: '8px',
}}
>
Results
</h3>
<ul
style={{
margin: 0,
paddingLeft: '20px',
display: 'flex',
flexDirection: 'column',
gap: '8px',
}}
>
{investigation.results.map((result, index) => (
<li
key={index}
style={{
fontSize: '14px',
lineHeight: '1.6',
color: 'var(--text-primary)',
}}
>
{result}
</li>
))}
</ul>
</div>
{/* External link */}
{investigation.externalUrl && (
<a
href={investigation.externalUrl}
target="_blank"
rel="noopener noreferrer"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
padding: '8px 16px',
fontSize: '13px',
fontWeight: 600,
fontFamily: 'var(--font-ui)',
color: 'var(--surface)',
backgroundColor: 'var(--accent)',
borderRadius: 'var(--radius-sm)',
textDecoration: 'none',
alignSelf: 'flex-start',
transition: 'background-color 150ms',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'var(--accent-hover)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'var(--accent)'
}}
>
<ExternalLink size={14} />
View Live Project
</a>
)}
</div>
)
}
-1
View File
@@ -1 +0,0 @@
export const personalStatement = `Healthcare leader combining clinical pharmacy expertise with proficiency in Python, SQL, and data analytics, self-taught over the past decade through a drive to find root causes in data and build the most efficient solutions to complex problems. Currently leading population health analytics for NHS Norfolk & Waveney ICB, serving a population of 1.2 million. Experienced in working with messy, real-world prescribing data at scale to deliver actionable insights—from financial scenario modelling and pharmaceutical rebate negotiation to algorithm design and population-level pathway development. Proven track record of identifying and prioritising efficiency programmes worth £14.6M+ through automated, data-driven analysis. Skilled at translating complex clinical, financial, and analytical requirements into clear recommendations for executive stakeholders.`
-61
View File
@@ -1,61 +0,0 @@
import { useState, useEffect } from 'react'
type Breakpoint = 'mobile' | 'tablet' | 'desktop'
interface BreakpointState {
breakpoint: Breakpoint
isMobile: boolean
isTablet: boolean
isDesktop: boolean
}
export function useBreakpoint(): BreakpointState {
const [state, setState] = useState<BreakpointState>(() => {
if (typeof window === 'undefined') {
return { breakpoint: 'desktop', isMobile: false, isTablet: false, isDesktop: true }
}
const width = window.innerWidth
if (width < 768) {
return { breakpoint: 'mobile', isMobile: true, isTablet: false, isDesktop: false }
}
if (width < 1024) {
return { breakpoint: 'tablet', isMobile: false, isTablet: true, isDesktop: false }
}
return { breakpoint: 'desktop', isMobile: false, isTablet: false, isDesktop: true }
})
useEffect(() => {
const handleResize = () => {
const width = window.innerWidth
let breakpoint: Breakpoint
let isMobile: boolean
let isTablet: boolean
let isDesktop: boolean
if (width < 768) {
breakpoint = 'mobile'
isMobile = true
isTablet = false
isDesktop = false
} else if (width < 1024) {
breakpoint = 'tablet'
isMobile = false
isTablet = true
isDesktop = false
} else {
breakpoint = 'desktop'
isMobile = false
isTablet = false
isDesktop = true
}
setState({ breakpoint, isMobile, isTablet, isDesktop })
}
handleResize()
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return state
}
+11 -24
View File
@@ -139,25 +139,6 @@
--backdrop-blur: 4px;
--backdrop-bg: rgba(26,43,42,0.15);
/* Legacy PMR tokens — kept for backward compat during transition (cleaned up in Task 21) */
--pmr-content: #F0F5F4;
--pmr-card: #FFFFFF;
--pmr-sidebar: #F7FAFA;
--pmr-banner: #334155;
--pmr-nhs-blue: #005EB8;
--pmr-green: #22C55E;
--pmr-amber: #F59E0B;
--pmr-red: #EF4444;
--pmr-text-primary: #1A2B2A;
--pmr-text-secondary: #5B7A78;
--pmr-border: #D4E0DE;
--pmr-border-dark: #D1D5DB;
--pmr-selected: #EFF6FF;
--pmr-alert-bg: #FEF3C7;
--pmr-alert-border: #F59E0B;
--pmr-alert-text: #92400E;
--pmr-radius: 8px;
--pmr-radius-login: 12px;
}
* {
@@ -193,11 +174,6 @@ body {
.font-geist-mono {
font-family: var(--font-geist-mono);
}
.pmr-theme {
background-color: var(--bg-dashboard);
color: var(--text-primary);
font-family: var(--font-ui);
}
}
@keyframes blink {
@@ -435,4 +411,15 @@ textarea:focus-visible {
animation: none;
border-top-color: #0D6E6E;
}
/* Instant SubNav transitions */
.subnav-scroll button {
transition: none !important;
}
/* Instant smooth scroll override */
html {
scroll-behavior: auto;
}
}