US-014: Modify CareerActivityTile: panel triggers and hover preview

- Replace in-place accordion expansion with detail panel triggers for role items
- Add hover preview showing lift effect, shadow deepens, and 1-2 lines preview text
- Integrate with DetailPanelContext to open career-role panels on click
- Keep color-coded dots and entry type styling (teal, amber, green, purple)
- Add placeholder container for CareerConstellation component (to be implemented later)
- Remove unused AnimatePresence, motion imports and accordion-related code
- Remove prefersReducedMotion and borderColorMap (no longer needed)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 23:57:55 +00:00
parent afc3876210
commit 9ed77f99a8
+74 -153
View File
@@ -1,10 +1,8 @@
import React, { useState, useCallback } from 'react' import React, { useState, useCallback } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { Card, CardHeader } from '../Card' import { Card, CardHeader } from '../Card'
import { documents } from '@/data/documents' import { documents } from '@/data/documents'
import { consultations } from '@/data/consultations' import { consultations } from '@/data/consultations'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
type ActivityType = 'role' | 'project' | 'cert' | 'edu' type ActivityType = 'role' | 'project' | 'cert' | 'edu'
@@ -140,49 +138,46 @@ const dotColorMap: Record<ActivityType, string> = {
edu: '#7C3AED', edu: '#7C3AED',
} }
const borderColorMap: Record<ActivityType, string> = {
role: '#0D6E6E',
project: '#D97706',
cert: '#059669',
edu: '#7C3AED',
}
interface ActivityItemProps { interface ActivityItemProps {
entry: ActivityEntry entry: ActivityEntry
isExpanded: boolean onItemClick: () => void
onToggle: () => void
} }
const ActivityItem: React.FC<ActivityItemProps> = ({ entry, isExpanded, onToggle }) => { const ActivityItem: React.FC<ActivityItemProps> = ({ entry, onItemClick }) => {
const [isHovered, setIsHovered] = useState(false)
const dotColor = dotColorMap[entry.type] const dotColor = dotColorMap[entry.type]
const isExpandable = entry.type === 'role' && entry.consultationId const isClickable = entry.type === 'role' && entry.consultationId
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
if (!isExpandable) return if (!isClickable) return
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault() e.preventDefault()
onToggle() onItemClick()
} else if (e.key === 'Escape' && isExpanded) {
e.preventDefault()
onToggle()
} }
}, },
[isExpandable, isExpanded, onToggle], [isClickable, onItemClick],
) )
// Get consultation data for expanded content // Get consultation data for preview text
const consultation = isExpandable const consultation = isClickable
? consultations.find((c) => c.id === entry.consultationId) ? consultations.find((c) => c.id === entry.consultationId)
: null : null
// Get preview text (first 1-2 lines from examination)
const previewText =
consultation && consultation.examination.length > 0
? consultation.examination[0]
: null
return ( return (
<div <div
role={isExpandable ? 'button' : undefined} role={isClickable ? 'button' : undefined}
tabIndex={isExpandable ? 0 : undefined} tabIndex={isClickable ? 0 : undefined}
aria-expanded={isExpandable ? isExpanded : undefined} onClick={isClickable ? onItemClick : undefined}
onClick={isExpandable ? onToggle : undefined} onKeyDown={isClickable ? handleKeyDown : undefined}
onKeyDown={isExpandable ? handleKeyDown : undefined} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -190,21 +185,13 @@ const ActivityItem: React.FC<ActivityItemProps> = ({ entry, isExpanded, onToggle
borderRadius: 'var(--radius-sm)', borderRadius: 'var(--radius-sm)',
border: '1px solid var(--border-light)', border: '1px solid var(--border-light)',
fontSize: '12px', fontSize: '12px',
transition: 'border-color 0.15s', transition: 'all 0.15s ease-out',
cursor: isExpandable ? 'pointer' : 'default', cursor: isClickable ? 'pointer' : 'default',
...(isExpanded && { transform: isHovered && isClickable ? 'translateY(-1px)' : 'none',
borderColor: 'var(--accent-border)', boxShadow: isHovered && isClickable
}), ? '0 2px 8px rgba(26,43,42,0.08)'
}} : '0 1px 2px rgba(26,43,42,0.05)',
onMouseEnter={(e) => { borderColor: isHovered && isClickable ? 'var(--accent-border)' : 'var(--border-light)',
if (isExpandable) {
e.currentTarget.style.borderColor = 'var(--accent-border)'
}
}}
onMouseLeave={(e) => {
if (isExpandable && !isExpanded) {
e.currentTarget.style.borderColor = 'var(--border-light)'
}
}} }}
> >
{/* Item header row */} {/* Item header row */}
@@ -249,142 +236,76 @@ const ActivityItem: React.FC<ActivityItemProps> = ({ entry, isExpanded, onToggle
> >
{entry.date} {entry.date}
</div> </div>
</div>
</div>
{/* Expanded content */} {/* Hover preview text for roles */}
<AnimatePresence initial={false}> {isHovered && previewText && (
{isExpanded && consultation && (
<motion.div
initial={{ height: 0 }}
animate={{ height: 'auto' }}
exit={{ height: 0 }}
transition={
prefersReducedMotion
? { duration: 0 }
: { duration: 0.2, ease: 'easeOut' }
}
style={{ overflow: 'hidden' }}
>
<div <div
style={{ style={{
borderLeft: `2px solid ${borderColorMap[entry.type]}`, fontSize: '11px',
marginLeft: '16px', color: 'var(--text-secondary)',
marginRight: '12px', marginTop: '6px',
marginBottom: '12px', lineHeight: 1.4,
paddingLeft: '14px', overflow: 'hidden',
paddingTop: '4px', textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}} }}
> >
{/* Role title */} {previewText}
<div
style={{
fontSize: '12.5px',
fontWeight: 600,
color: 'var(--accent)',
marginBottom: '8px',
}}
>
{consultation.role}
</div>
{/* Achievement bullets */}
{consultation.examination.length > 0 && (
<ul
style={{
listStyle: 'none',
padding: 0,
margin: '0 0 10px 0',
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
{consultation.examination.map((item, i) => (
<li
key={i}
style={{
display: 'flex',
gap: '8px',
fontSize: '11.5px',
color: 'var(--text-primary)',
lineHeight: 1.45,
}}
>
<span
style={{
color: 'var(--accent)',
opacity: 0.5,
flexShrink: 0,
marginTop: '1px',
}}
>
</span>
{item}
</li>
))}
</ul>
)}
{/* Coded entries */}
{consultation.codedEntries && consultation.codedEntries.length > 0 && (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '6px',
marginTop: '4px',
}}
>
{consultation.codedEntries.map((entry) => (
<span
key={entry.code}
style={{
fontSize: '10px',
fontFamily: 'var(--font-mono)',
padding: '2px 6px',
borderRadius: '3px',
background: 'var(--accent-light)',
color: 'var(--accent)',
border: '1px solid var(--accent-border)',
}}
>
{entry.code}
</span>
))}
</div>
)}
</div> </div>
</motion.div> )}
)} </div>
</AnimatePresence> </div>
</div> </div>
) )
} }
export const CareerActivityTile: React.FC = () => { export const CareerActivityTile: React.FC = () => {
const timeline = buildTimeline() const timeline = buildTimeline()
const [expandedItemId, setExpandedItemId] = useState<string | null>(null) const { openPanel } = useDetailPanel()
const handleToggle = useCallback( const handleItemClick = useCallback(
(id: string) => { (entry: ActivityEntry) => {
setExpandedItemId((prev) => (prev === id ? null : id)) if (entry.type === 'role' && entry.consultationId) {
const consultation = consultations.find((c) => c.id === entry.consultationId)
if (consultation) {
openPanel({ type: 'career-role', consultation })
}
}
}, },
[], [openPanel],
) )
return ( return (
<Card full tileId="career-activity"> <Card full tileId="career-activity">
<CardHeader dotColor="teal" title="CAREER ACTIVITY" rightText="Full timeline" /> <CardHeader dotColor="teal" title="CAREER ACTIVITY" rightText="Full timeline" />
{/* Placeholder for CareerConstellation component (to be added later) */}
<div
style={{
minHeight: '200px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--bg-dashboard)',
borderRadius: 'var(--radius-sm)',
border: '1px dashed var(--border-light)',
marginBottom: '20px',
color: 'var(--text-tertiary)',
fontSize: '12px',
fontStyle: 'italic',
}}
>
Career Constellation visualization (to be implemented)
</div>
<div className="activity-grid"> <div className="activity-grid">
{timeline.map((entry) => ( {timeline.map((entry) => (
<ActivityItem <ActivityItem
key={entry.id} key={entry.id}
entry={entry} entry={entry}
isExpanded={expandedItemId === entry.id} onItemClick={() => handleItemClick(entry)}
onToggle={() => handleToggle(entry.id)}
/> />
))} ))}
</div> </div>