US-020: Create SkillDetail renderer for detail panel
This commit is contained in:
@@ -332,8 +332,8 @@
|
|||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 18,
|
"priority": 18,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Already implemented by prior iteration. Component exists with full content, wired into DetailPanel for consultation and career-role types."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-019",
|
"id": "US-019",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { DetailPanelContent } from '@/types/pmr'
|
|||||||
import type { CardHeaderProps } from './Card'
|
import type { CardHeaderProps } from './Card'
|
||||||
import { KPIDetail } from './detail/KPIDetail'
|
import { KPIDetail } from './detail/KPIDetail'
|
||||||
import { ConsultationDetail } from './detail/ConsultationDetail'
|
import { ConsultationDetail } from './detail/ConsultationDetail'
|
||||||
|
import { SkillDetail } from './detail/SkillDetail'
|
||||||
|
|
||||||
// Width mapping from content type
|
// Width mapping from content type
|
||||||
const widthMap: Record<DetailPanelContent['type'], 'narrow' | 'wide'> = {
|
const widthMap: Record<DetailPanelContent['type'], 'narrow' | 'wide'> = {
|
||||||
@@ -215,10 +216,13 @@ export function DetailPanel() {
|
|||||||
<ConsultationDetail consultation={content.consultation} />
|
<ConsultationDetail consultation={content.consultation} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{content.type === 'skill' && <SkillDetail skill={content.skill} />}
|
||||||
|
|
||||||
{/* Other content types - placeholder for future stories */}
|
{/* Other content types - placeholder for future stories */}
|
||||||
{content.type !== 'kpi' &&
|
{content.type !== 'kpi' &&
|
||||||
content.type !== 'consultation' &&
|
content.type !== 'consultation' &&
|
||||||
content.type !== 'career-role' && (
|
content.type !== 'career-role' &&
|
||||||
|
content.type !== 'skill' && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'var(--font-ui)',
|
fontFamily: 'var(--font-ui)',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user