US-020: Create SkillDetail renderer for detail panel

This commit is contained in:
2026-02-14 02:30:53 +00:00
parent 9d61d2c8ca
commit 9ec71ae0ed
3 changed files with 278 additions and 3 deletions
+2 -2
View File
@@ -332,8 +332,8 @@
"Verify in browser using dev-browser skill"
],
"priority": 18,
"passes": false,
"notes": ""
"passes": true,
"notes": "Already implemented by prior iteration. Component exists with full content, wired into DetailPanel for consultation and career-role types."
},
{
"id": "US-019",
+5 -1
View File
@@ -6,6 +6,7 @@ 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'
// Width mapping from content type
const widthMap: Record<DetailPanelContent['type'], 'narrow' | 'wide'> = {
@@ -215,10 +216,13 @@ export function DetailPanel() {
<ConsultationDetail consultation={content.consultation} />
)}
{content.type === 'skill' && <SkillDetail skill={content.skill} />}
{/* Other content types - placeholder for future stories */}
{content.type !== 'kpi' &&
content.type !== 'consultation' &&
content.type !== 'career-role' && (
content.type !== 'career-role' &&
content.type !== 'skill' && (
<div
style={{
fontFamily: 'var(--font-ui)',
+271
View File
@@ -0,0 +1,271 @@
import type { SkillMedication } from '@/types/pmr'
import { roleSkillMappings, constellationNodes } from '@/data/constellation'
interface SkillDetailProps {
skill: SkillMedication
}
// Category display names
const categoryLabels: Record<SkillMedication['category'], string> = {
Technical: 'Technical',
Domain: 'Healthcare Domain',
Leadership: 'Strategic & Leadership',
}
// Proficiency bar color based on value
function getProficiencyColor(proficiency: number): string {
if (proficiency >= 90) return 'var(--success)'
if (proficiency >= 75) return 'var(--accent)'
return 'var(--amber)'
}
export function SkillDetail({ skill }: SkillDetailProps) {
// Find roles that use this skill from constellation data
const usedInRoles = roleSkillMappings
.filter((mapping) => mapping.skillIds.includes(skill.id))
.map((mapping) => {
const node = constellationNodes.find((n) => n.id === mapping.roleId && n.type === 'role')
return node
})
.filter(Boolean)
// Sort chronologically (earliest first)
.sort((a, b) => (a!.startYear ?? 0) - (b!.startYear ?? 0))
return (
<div
style={{
fontFamily: 'var(--font-ui)',
display: 'flex',
flexDirection: 'column',
gap: '24px',
}}
>
{/* Skill header */}
<div>
<div
style={{
fontSize: '20px',
fontWeight: 700,
color: 'var(--text-primary)',
lineHeight: '1.3',
marginBottom: '8px',
}}
>
{skill.name}
</div>
{/* Medication metaphor badges */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' }}>
<span
style={{
padding: '3px 10px',
backgroundColor: 'var(--accent-light)',
color: 'var(--accent)',
fontSize: '11px',
fontWeight: 600,
borderRadius: 'var(--radius-sm)',
fontFamily: 'var(--font-geist)',
}}
>
{skill.frequency}
</span>
<span
style={{
padding: '3px 10px',
backgroundColor:
skill.status === 'Active' ? 'var(--success-light)' : 'var(--bg-dashboard)',
color: skill.status === 'Active' ? 'var(--success)' : 'var(--text-tertiary)',
fontSize: '10px',
fontWeight: 600,
borderRadius: 'var(--radius-sm)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
{skill.status}
</span>
</div>
</div>
{/* Category label */}
<div>
<span
style={{
fontSize: '11px',
fontWeight: 500,
color: 'var(--text-tertiary)',
textTransform: 'uppercase',
letterSpacing: '0.06em',
}}
>
{categoryLabels[skill.category]}
</span>
</div>
{/* Proficiency bar */}
<div>
<h3
style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text-secondary)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
marginBottom: '8px',
}}
>
Proficiency
</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div
style={{
flex: 1,
height: '6px',
backgroundColor: 'var(--border-light)',
borderRadius: '3px',
overflow: 'hidden',
}}
>
<div
style={{
width: `${skill.proficiency}%`,
height: '100%',
backgroundColor: getProficiencyColor(skill.proficiency),
borderRadius: '3px',
transition: 'width 400ms ease-out',
}}
/>
</div>
<span
style={{
fontSize: '13px',
fontWeight: 700,
fontFamily: 'var(--font-geist)',
color: getProficiencyColor(skill.proficiency),
minWidth: '36px',
textAlign: 'right',
}}
>
{skill.proficiency}%
</span>
</div>
</div>
{/* Years of experience */}
<div>
<h3
style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text-secondary)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
marginBottom: '8px',
}}
>
Experience
</h3>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '6px' }}>
<span
style={{
fontSize: '28px',
fontWeight: 700,
color: 'var(--text-primary)',
lineHeight: '1',
}}
>
{skill.yearsOfExperience}
</span>
<span
style={{
fontSize: '13px',
color: 'var(--text-secondary)',
}}
>
{skill.yearsOfExperience === 1 ? 'year' : 'years'}
</span>
<span
style={{
fontSize: '11px',
fontFamily: 'var(--font-geist)',
color: 'var(--text-tertiary)',
marginLeft: '4px',
}}
>
Since {skill.startYear}
</span>
</div>
</div>
{/* Used in roles */}
{usedInRoles.length > 0 && (
<div>
<h3
style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text-secondary)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
marginBottom: '10px',
}}
>
Used In
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{usedInRoles.map((node) => (
<div
key={node!.id}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '8px 12px',
backgroundColor: 'var(--bg-dashboard)',
border: '1px solid var(--border-light)',
borderRadius: 'var(--radius-sm)',
}}
>
<div
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: node!.orgColor ?? 'var(--accent)',
flexShrink: 0,
}}
aria-hidden="true"
/>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: '12.5px',
fontWeight: 600,
color: 'var(--text-primary)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{node!.label}
</div>
<div
style={{
fontSize: '10px',
fontFamily: 'var(--font-geist)',
color: 'var(--text-tertiary)',
marginTop: '1px',
}}
>
{node!.organization} · {node!.startYear}
{node!.endYear === null ? 'Present' : node!.endYear !== node!.startYear ? `${node!.endYear}` : ''}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}