diff --git a/src/components/ExpandableCardShell.tsx b/src/components/ExpandableCardShell.tsx new file mode 100644 index 0000000..b58cbc8 --- /dev/null +++ b/src/components/ExpandableCardShell.tsx @@ -0,0 +1,147 @@ +import React, { useCallback } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { ChevronRight } from 'lucide-react' +import { hexToRgba, motionSafeTransition } from '@/lib/utils' + +interface ExpandableCardShellProps { + isExpanded: boolean + isHighlighted: boolean + accentColor: string + onToggle: () => void + ariaLabel: string + headerPadding?: string + className?: string + dataTileId?: string + onMouseEnter?: () => void + onMouseLeave?: () => void + renderHeader: () => React.ReactNode + renderBody: () => React.ReactNode +} + +export function ExpandableCardShell({ + isExpanded, + isHighlighted, + accentColor, + onToggle, + ariaLabel, + headerPadding = '12px 14px', + className, + dataTileId, + onMouseEnter, + onMouseLeave, + renderHeader, + renderBody, +}: ExpandableCardShellProps) { + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onToggle() + } + if (e.key === 'Escape' && isExpanded) { + e.preventDefault() + onToggle() + } + }, + [onToggle, isExpanded], + ) + + return ( +
+
+
{ + if (!isExpanded) { + e.currentTarget.parentElement!.style.borderColor = hexToRgba(accentColor, 0.2) + e.currentTarget.parentElement!.style.boxShadow = 'var(--shadow-md)' + } + }} + onMouseLeave={(e) => { + if (!isExpanded) { + e.currentTarget.parentElement!.style.borderColor = 'var(--border-light)' + e.currentTarget.parentElement!.style.boxShadow = 'none' + } + }} + > + + + + {isExpanded && ( + +
+ {renderBody()} +
+
+ )} +
+
+
+ ) +} diff --git a/src/components/TimelineInterventionsSubsection.tsx b/src/components/TimelineInterventionsSubsection.tsx index 5bfe867..c3c2928 100644 --- a/src/components/TimelineInterventionsSubsection.tsx +++ b/src/components/TimelineInterventionsSubsection.tsx @@ -1,11 +1,11 @@ -import React, { useMemo, useState, useCallback } from 'react' -import { motion, AnimatePresence } from 'framer-motion' +import { useMemo, useState, useCallback } from 'react' import { ChevronRight } from 'lucide-react' +import { ExpandableCardShell } from './ExpandableCardShell' import { useDetailPanel } from '@/contexts/DetailPanelContext' import { timelineEntities, timelineConsultations } from '@/data/timeline' import { getExperienceEducationUICopy } from '@/lib/profile-content' import type { TimelineEntity } from '@/types/pmr' -import { hexToRgba, motionSafeTransition } from '@/lib/utils' +import { hexToRgba } from '@/lib/utils' interface TimelineInterventionItemProps { entity: TimelineEntity @@ -30,90 +30,32 @@ function TimelineInterventionItem({ const isEducation = entity.kind === 'education' const interventionLabel = isEducation ? experienceEducationCopy.educationLabel : experienceEducationCopy.employmentLabel - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - onToggle() - } - if (e.key === 'Escape' && isExpanded) { - e.preventDefault() - onToggle() - } - }, - [isExpanded, onToggle], - ) - return ( -
onHighlight?.(entity.id)} onMouseLeave={() => onHighlight?.(null)} - > -
-
{ - if (!isExpanded) { - e.currentTarget.parentElement!.style.borderColor = hexToRgba(entity.orgColor, 0.2) - e.currentTarget.parentElement!.style.boxShadow = 'var(--shadow-md)' - } - }} - onMouseLeave={(e) => { - if (!isExpanded) { - e.currentTarget.parentElement!.style.borderColor = 'var(--border-light)' - e.currentTarget.parentElement!.style.boxShadow = 'none' - } - }} - > + renderHeader={() => ( + <> )} - -
-
+ + + + )} + /> ) } diff --git a/src/components/WorkExperienceSubsection.tsx b/src/components/WorkExperienceSubsection.tsx index a834924..07392a5 100644 --- a/src/components/WorkExperienceSubsection.tsx +++ b/src/components/WorkExperienceSubsection.tsx @@ -1,10 +1,10 @@ -import React, { useState, useCallback } from 'react' -import { motion, AnimatePresence } from 'framer-motion' +import { useState, useCallback } from 'react' import { ChevronRight } from 'lucide-react' import { CardHeader } from './Card' +import { ExpandableCardShell } from './ExpandableCardShell' import { timelineConsultations } from '@/data/timeline' import { useDetailPanel } from '@/contexts/DetailPanelContext' -import { hexToRgba, motionSafeTransition } from '@/lib/utils' +import { hexToRgba } from '@/lib/utils' import { DEFAULT_ORG_COLOR } from '@/lib/theme-colors' interface RoleItemProps { @@ -17,76 +17,20 @@ interface RoleItemProps { } function RoleItem({ consultation, isExpanded, isHighlightedFromGraph, onToggle, onViewFull, onHighlight }: RoleItemProps) { - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - onToggle() - } - if (e.key === 'Escape' && isExpanded) { - e.preventDefault() - onToggle() - } - }, - [onToggle, isExpanded], - ) + const orgColor = consultation.orgColor ?? DEFAULT_ORG_COLOR return ( -
onHighlight?.(consultation.id)} onMouseLeave={() => onHighlight?.(null)} - > - {/* Clickable header */} -
{ - if (!isExpanded) { - e.currentTarget.parentElement!.style.borderColor = hexToRgba(consultation.orgColor ?? DEFAULT_ORG_COLOR, 0.2) - e.currentTarget.parentElement!.style.boxShadow = 'var(--shadow-md)' - } - }} - onMouseLeave={(e) => { - if (!isExpanded) { - e.currentTarget.parentElement!.style.borderColor = 'var(--border-light)' - e.currentTarget.parentElement!.style.boxShadow = 'none' - } - }} - > - {/* Org colour dot */} - - - {/* Expandable detail content */} - - {isExpanded && ( - + )} + renderBody={() => ( + <> + {/* Examination bullets */} +
    -
    - {/* Examination bullets */} -
      ( +
    • - {consultation.examination.map((bullet, i) => ( -
    • -
    • - ))} -
    +
- {/* Coded entries */} -
+ {consultation.codedEntries.map((entry) => ( + - {consultation.codedEntries.map((entry) => ( - - {entry.code}: {entry.description} - - ))} -
- - {/* View full record link */} - -
- - )} - -
+ {entry.code}: {entry.description} + + ))} +
+ + {/* View full record link */} + + + )} + /> ) }