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'
+ }
+ }}
+ >
+
+
+
+ {renderHeader()}
+
+
+
+
+
+
+ {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={() => (
+ <>
-
-
+ >
+
+ {interventionLabel}
+
-
- {interventionLabel}
-
-
{entity.title}
-
-
- {entity.organization}
-
-
+
+ {entity.organization}
+
{entity.dateRange.display}
-
-
-
-
-
-
-
+ )}
+ renderBody={() => (
+ <>
+
-
-
-
- {isExpanded && (
-
-
+ {entity.details.map((detail, i) => (
+
-
- {entity.details.map((detail, i) => (
- -
-
- {detail}
-
- ))}
-
+ />
+ {detail}
+
+ ))}
+
- {!!entity.codedEntries?.length && (
-
- {entity.codedEntries.map((entry) => (
-
- {entry.code}: {entry.description}
-
- ))}
-
- )}
-
-
)}
-
-
-
+
+
+ >
+ )}
+ />
)
}
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 */}
-
-
- {/* Text content */}
-
+ renderHeader={() => (
+ <>
{consultation.duration}
-
-
- {/* Chevron */}
-
-
-
- {/* Expandable detail content */}
-
- {isExpanded && (
-
+ )}
+ renderBody={() => (
+ <>
+ {/* Examination bullets */}
+
-
- {/* Examination bullets */}
-
(
+ -
- {consultation.examination.map((bullet, i) => (
-
-
-
- {bullet}
-
- ))}
-
+
+ {bullet}
+
+ ))}
+
- {/* 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 */}
+
+ >
+ )}
+ />
)
}