Files
portfolio/src/components/DetailPanel.tsx
T
admin 088b783731 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
2026-02-14 03:20:31 +00:00

231 lines
6.5 KiB
TypeScript

import { useEffect, useRef } from 'react'
import { X } from 'lucide-react'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
import { useFocusTrap } from '@/hooks/useFocusTrap'
import { DetailPanelContent } from '@/types/pmr'
import type { CardHeaderProps } from './Card'
import { KPIDetail } from './detail/KPIDetail'
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'> = {
kpi: 'narrow',
skill: 'narrow',
'skills-all': 'narrow',
consultation: 'wide',
project: 'wide',
education: 'narrow',
'career-role': 'wide',
}
// Title mapping from content data
function getPanelTitle(content: DetailPanelContent): string {
switch (content.type) {
case 'kpi':
return content.kpi.label
case 'skill':
return content.skill.name
case 'skills-all':
return 'All Medications'
case 'consultation':
return content.consultation.role
case 'project':
return content.investigation.name
case 'education':
return content.document.title
case 'career-role':
return content.consultation.role
}
}
// Dot color mapping from content type
function getDotColor(content: DetailPanelContent): CardHeaderProps['dotColor'] {
switch (content.type) {
case 'kpi':
return 'teal'
case 'skill':
case 'skills-all':
return 'amber'
case 'consultation':
case 'career-role':
return 'teal'
case 'project':
return 'amber'
case 'education':
return 'purple'
}
}
// Dot color value map (from Card.tsx)
const dotColorValueMap: Record<CardHeaderProps['dotColor'], string> = {
teal: '#0D6E6E',
amber: '#D97706',
green: '#059669',
alert: '#DC2626',
purple: '#7C3AED',
}
export function DetailPanel() {
const { content, closePanel, isOpen } = useDetailPanel()
const panelRef = useRef<HTMLDivElement>(null)
const titleId = 'detail-panel-title'
// Focus trap when open
useFocusTrap(panelRef, isOpen)
// Close on Escape key
useEffect(() => {
if (!isOpen) return
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closePanel()
}
}
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}, [isOpen, closePanel])
if (!isOpen || !content) return null
const width = widthMap[content.type]
const title = getPanelTitle(content)
const dotColor = getDotColor(content)
const dotColorValue = dotColorValueMap[dotColor]
return (
<>
{/* Backdrop */}
<div
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'var(--backdrop-bg)',
backdropFilter: 'blur(var(--backdrop-blur))',
zIndex: 1000,
animation: 'backdrop-fade-in 150ms ease-out',
}}
onClick={closePanel}
aria-hidden="true"
/>
{/* Panel */}
<div
ref={panelRef}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className="detail-panel"
data-width={width}
style={{
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
backgroundColor: 'var(--surface)',
boxShadow: 'var(--shadow-lg)',
zIndex: 1001,
display: 'flex',
flexDirection: 'column',
animation: 'panel-slide-in 250ms ease-out',
}}
>
{/* Header */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '20px 24px',
borderBottom: '1px solid var(--border-light)',
flexShrink: 0,
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<div
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: dotColorValue,
flexShrink: 0,
}}
aria-hidden="true"
/>
<h2
id={titleId}
style={{
fontSize: '14px',
fontWeight: 600,
color: 'var(--text-primary)',
fontFamily: 'var(--font-ui)',
}}
>
{title}
</h2>
</div>
<button
onClick={closePanel}
aria-label="Close panel"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '44px',
height: '44px',
border: 'none',
background: 'transparent',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
color: 'var(--text-secondary)',
transition: 'background-color 150ms, color 150ms',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'var(--accent-light)'
e.currentTarget.style.color = 'var(--accent)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent'
e.currentTarget.style.color = 'var(--text-secondary)'
}}
>
<X size={20} />
</button>
</div>
{/* Body (scrollable) */}
<div
style={{
flex: 1,
overflowY: 'auto',
padding: '24px',
}}
>
{/* Render content based on type */}
{content.type === 'kpi' && <KPIDetail kpi={content.kpi} />}
{(content.type === 'consultation' || content.type === 'career-role') && (
<ConsultationDetail consultation={content.consultation} />
)}
{content.type === 'skill' && <SkillDetail skill={content.skill} />}
{content.type === 'skills-all' && <SkillsAllDetail category={content.category} />}
{content.type === 'education' && <EducationDetail document={content.document} />}
{content.type === 'project' && <ProjectDetail investigation={content.investigation} />}
</div>
</div>
</>
)
}