From cf5399a767a1d4b04f474f9c2dfba38e0537d781 Mon Sep 17 00:00:00 2001 From: A Charlwood Date: Fri, 13 Feb 2026 23:02:59 +0000 Subject: [PATCH] US-003: Create DetailPanelContext, DetailPanel component, and useFocusTrap hook Implements core detail panel infrastructure for slide-in content panels: - DetailPanelContext: Manages panel state (content, open/close, isOpen) - DetailPanel: Slide-in panel component with backdrop, header, and scrollable body - useFocusTrap: Keyboard focus trap hook for modal accessibility - Width mapping: narrow (400px) for kpi/skill/education, wide (60vw) for consultation/project/career-role - Title mapping derives from content data (kpi.label, skill.name, etc.) - Close triggers: backdrop click, Escape key, X button - ARIA: aria-modal, role=dialog, aria-labelledby - Mobile responsive: both widths become 100vw on <768px - prefers-reduced-motion: instant appear, no animations - Placeholder content (real renderers in later stories) - Export CardHeaderProps interface from Card.tsx - Add responsive panel width CSS rules Co-Authored-By: Claude Sonnet 4.5 --- src/components/Card.tsx | 2 +- src/components/DetailPanel.tsx | 229 ++++++++++++++++++++++++++++ src/contexts/DetailPanelContext.tsx | 52 +++++++ src/hooks/useFocusTrap.ts | 80 ++++++++++ src/index.css | 17 +++ 5 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 src/components/DetailPanel.tsx create mode 100644 src/contexts/DetailPanelContext.tsx create mode 100644 src/hooks/useFocusTrap.ts diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 4d39d2e..a5e9fff 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -35,7 +35,7 @@ export function Card({ children, full, className, tileId }: CardProps) { ) } -interface CardHeaderProps { +export interface CardHeaderProps { dotColor: 'teal' | 'amber' | 'green' | 'alert' | 'purple' title: string rightText?: string diff --git a/src/components/DetailPanel.tsx b/src/components/DetailPanel.tsx new file mode 100644 index 0000000..12b854a --- /dev/null +++ b/src/components/DetailPanel.tsx @@ -0,0 +1,229 @@ +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' + +// Width mapping from content type +const widthMap: Record = { + 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 = { + teal: '#0D6E6E', + amber: '#D97706', + green: '#059669', + alert: '#DC2626', + purple: '#7C3AED', +} + +export function DetailPanel() { + const { content, closePanel, isOpen } = useDetailPanel() + const panelRef = useRef(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 */} +