feat: US-007 - Colour-match work experience cards to constellation node colours

This commit is contained in:
2026-02-16 10:09:34 +00:00
parent a258706bf3
commit c9dd93ac70
4 changed files with 62 additions and 23 deletions
+1 -1
View File
@@ -138,7 +138,7 @@
"Verify in browser: NHS roles show blue-tinted cards, Tesco roles red-tinted, Paydens green, education purple" "Verify in browser: NHS roles show blue-tinted cards, Tesco roles red-tinted, Paydens green, education purple"
], ],
"priority": 7, "priority": 7,
"passes": false, "passes": true,
"notes": "All changes in WorkExperienceSubsection.tsx (~299 lines). consultation.orgColor already exists on each consultation object but is not currently used for card styling. Create a helper function hexToRgba(hex: string, opacity: number): string that converts hex to rgba — needed for tinted backgrounds and borders. Replace hardcoded values: '#0D6E6E' for dot (line ~82), 'rgba(10,128,128,0.03)' for highlight bg, 'var(--accent-border)' for border, 'var(--accent)' for links/text. Each RoleItem already receives its consultation — use consultation.orgColor. For coded entry tags: text in orgColor, bg in hexToRgba(orgColor, 0.08), border in hexToRgba(orgColor, 0.2). Also update LastConsultationSubsection in DashboardLayout.tsx if it has hardcoded teal colours. The WORK EXPERIENCE CardHeader dot stays teal. Use the d3-viz skill." "notes": "All changes in WorkExperienceSubsection.tsx (~299 lines). consultation.orgColor already exists on each consultation object but is not currently used for card styling. Create a helper function hexToRgba(hex: string, opacity: number): string that converts hex to rgba — needed for tinted backgrounds and borders. Replace hardcoded values: '#0D6E6E' for dot (line ~82), 'rgba(10,128,128,0.03)' for highlight bg, 'var(--accent-border)' for border, 'var(--accent)' for links/text. Each RoleItem already receives its consultation — use consultation.orgColor. For coded entry tags: text in orgColor, bg in hexToRgba(orgColor, 0.08), border in hexToRgba(orgColor, 0.2). Also update LastConsultationSubsection in DashboardLayout.tsx if it has hardcoded teal colours. The WORK EXPERIENCE CardHeader dot stays teal. Use the d3-viz skill."
}, },
{ {
+25
View File
@@ -31,6 +31,8 @@
- Constellation role nodes, skill mappings, and links are in constellation.ts — adding nodes there automatically extends yScale domain and screen reader description - Constellation role nodes, skill mappings, and links are in constellation.ts — adding nodes there automatically extends yScale domain and screen reader description
- Mobile accordion (coarse pointer): pinnedNodeId drives both graph highlight AND accordion visibility. Accordion only shows for role-type nodes (not skills) - Mobile accordion (coarse pointer): pinnedNodeId drives both graph highlight AND accordion visibility. Accordion only shows for role-type nodes (not skills)
- SVG background rect has class `.bg-rect` — used for "tap elsewhere to close" handler on touch devices - SVG background rect has class `.bg-rect` — used for "tap elsewhere to close" handler on touch devices
- consultation.orgColor is the source of per-employer colour for cards, dots, borders, and coded entries. Use hexToRgba(orgColor, opacity) for tinted variants
- hexToRgba(hex, opacity) helper exists in both WorkExperienceSubsection.tsx and DashboardLayout.tsx for converting hex to rgba
## 2026-02-16 - US-001 ## 2026-02-16 - US-001
- Added Duty Pharmacy Manager (2016-2017, Tesco PLC) and Pre-Registration Pharmacist (2015-2016, Paydens Pharmacy) role nodes to constellation.ts - Added Duty Pharmacy Manager (2016-2017, Tesco PLC) and Pre-Registration Pharmacist (2015-2016, Paydens Pharmacy) role nodes to constellation.ts
@@ -133,3 +135,26 @@
- Not all consultations have >3 examination items — the "Show more" button only renders conditionally, and plan items are only shown when expanded - Not all consultations have >3 examination items — the "Show more" button only renders conditionally, and plan items are only shown when expanded
- Browser testing for coarse pointer features requires touch emulation — Playwright's default Chromium reports fine pointer, so the accordion won't appear without explicit touch device emulation - Browser testing for coarse pointer features requires touch emulation — Playwright's default Chromium reports fine pointer, so the accordion won't appear without explicit touch device emulation
--- ---
## 2026-02-16 - US-007
- Created hexToRgba(hex, opacity) helper function in both WorkExperienceSubsection.tsx and DashboardLayout.tsx
- WorkExperienceSubsection.tsx: replaced all hardcoded teal/accent colour references with consultation.orgColor:
- Dot indicator: '#0D6E6E' → consultation.orgColor
- Highlight background: 'rgba(10,128,128,0.03)' → hexToRgba(orgColor, 0.03)
- Expanded/highlighted border: 'var(--accent-border)' → hexToRgba(orgColor, 0.2)
- Hover border: 'var(--accent-border)' → hexToRgba(orgColor, 0.2)
- Left border on expanded detail: 'var(--accent)' → orgColor
- Bullet dots: 'var(--accent)' → orgColor at 0.5 opacity
- Coded entry tags: bg hexToRgba(orgColor, 0.08), text orgColor, border hexToRgba(orgColor, 0.2)
- "View full record" link: 'var(--accent)' → orgColor, hover uses opacity 0.7 instead of accent-hover
- DashboardLayout.tsx LastConsultationSubsection: same pattern applied:
- Highlight border/bg, hover bg, role title, bullet dots, "View full record" link all use consultation.orgColor
- CardHeader dot for "WORK EXPERIENCE" section title remains teal (unchanged)
- Files changed: src/components/WorkExperienceSubsection.tsx, src/components/DashboardLayout.tsx, Ralph/prd.json, Ralph/progress.txt
- Browser verified: NHS roles show blue dots/borders, Tesco roles show red, Paydens shows green, education shows purple. Expanded Tesco card shows red left border, red bullet dots, and red-tinted coded entries
- **Learnings for future iterations:**
- consultation.orgColor exists on every Consultation object — it's the single source for per-employer colour throughout the UI
- hexToRgba(hex, opacity) is needed in both WorkExperienceSubsection.tsx and DashboardLayout.tsx — not extracted to a shared utility since it's a small helper and only used in two files
- For hover effects on org-coloured links, use opacity change (0.7) instead of a separate --accent-hover variable, since each employer has a different base colour
- The hover mouseenter/mouseleave pattern using parentElement!.style is used for border/shadow effects — it directly mutates the parent wrapper's inline styles
---
+16 -9
View File
@@ -55,6 +55,13 @@ const contentVariants = {
}, },
} }
function hexToRgba(hex: string, opacity: number): string {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return `rgba(${r},${g},${b},${opacity})`
}
interface LastConsultationSubsectionProps { interface LastConsultationSubsectionProps {
highlightedRoleId?: string | null highlightedRoleId?: string | null
} }
@@ -114,8 +121,8 @@ function LastConsultationSubsection({ highlightedRoleId }: LastConsultationSubse
marginTop: '24px', marginTop: '24px',
borderRadius: 'var(--radius-sm)', borderRadius: 'var(--radius-sm)',
border: '1px solid', border: '1px solid',
borderColor: isHighlighted ? 'var(--accent-border)' : 'transparent', borderColor: isHighlighted ? hexToRgba(consultation.orgColor ?? '#0D6E6E', 0.2) : 'transparent',
background: isHighlighted ? 'rgba(10,128,128,0.03)' : 'transparent', background: isHighlighted ? hexToRgba(consultation.orgColor ?? '#0D6E6E', 0.03) : 'transparent',
transition: 'border-color 150ms ease-out, background-color 150ms ease-out', transition: 'border-color 150ms ease-out, background-color 150ms ease-out',
padding: '8px', padding: '8px',
margin: '-8px', margin: '-8px',
@@ -142,7 +149,7 @@ function LastConsultationSubsection({ highlightedRoleId }: LastConsultationSubse
transition: 'background-color 150ms ease-out', transition: 'background-color 150ms ease-out',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'rgba(10,128,128,0.04)' e.currentTarget.style.backgroundColor = hexToRgba(consultation.orgColor ?? '#0D6E6E', 0.04)
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent' e.currentTarget.style.backgroundColor = 'transparent'
@@ -171,7 +178,7 @@ function LastConsultationSubsection({ highlightedRoleId }: LastConsultationSubse
style={{ style={{
fontSize: '15px', fontSize: '15px',
fontWeight: 600, fontWeight: 600,
color: 'var(--accent)', color: consultation.orgColor ?? 'var(--accent)',
marginBottom: '12px', marginBottom: '12px',
}} }}
> >
@@ -209,7 +216,7 @@ function LastConsultationSubsection({ highlightedRoleId }: LastConsultationSubse
width: '5px', width: '5px',
height: '5px', height: '5px',
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'var(--accent)', backgroundColor: consultation.orgColor ?? 'var(--accent)',
opacity: 0.5, opacity: 0.5,
}} }}
/> />
@@ -226,19 +233,19 @@ function LastConsultationSubsection({ highlightedRoleId }: LastConsultationSubse
gap: '6px', gap: '6px',
fontSize: '13px', fontSize: '13px',
fontWeight: 500, fontWeight: 500,
color: 'var(--accent)', color: consultation.orgColor ?? 'var(--accent)',
background: 'transparent', background: 'transparent',
border: 'none', border: 'none',
padding: '6px 0', padding: '6px 0',
minHeight: '44px', minHeight: '44px',
cursor: 'pointer', cursor: 'pointer',
transition: 'color 150ms ease-out', transition: 'opacity 150ms ease-out',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.color = 'var(--accent-hover)' e.currentTarget.style.opacity = '0.7'
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.color = 'var(--accent)' e.currentTarget.style.opacity = '1'
}} }}
aria-label="View full consultation record" aria-label="View full consultation record"
> >
+20 -13
View File
@@ -7,6 +7,13 @@ import { useDetailPanel } from '@/contexts/DetailPanelContext'
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
function hexToRgba(hex: string, opacity: number): string {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return `rgba(${r},${g},${b},${opacity})`
}
interface RoleItemProps { interface RoleItemProps {
consultation: typeof consultations[0] consultation: typeof consultations[0]
isExpanded: boolean isExpanded: boolean
@@ -34,9 +41,9 @@ function RoleItem({ consultation, isExpanded, isHighlightedFromGraph, onToggle,
return ( return (
<div <div
style={{ style={{
background: isHighlightedFromGraph ? 'rgba(10,128,128,0.03)' : 'var(--bg-dashboard)', background: isHighlightedFromGraph ? hexToRgba(consultation.orgColor ?? '#0D6E6E', 0.03) : 'var(--bg-dashboard)',
borderRadius: 'var(--radius-sm)', borderRadius: 'var(--radius-sm)',
border: `1px solid ${isExpanded || isHighlightedFromGraph ? 'var(--accent-border)' : 'var(--border-light)'}`, border: `1px solid ${isExpanded || isHighlightedFromGraph ? hexToRgba(consultation.orgColor ?? '#0D6E6E', 0.2) : 'var(--border-light)'}`,
transition: 'border-color 0.15s, box-shadow 0.15s, background-color 0.15s', transition: 'border-color 0.15s, box-shadow 0.15s, background-color 0.15s',
overflow: 'hidden', overflow: 'hidden',
}} }}
@@ -61,7 +68,7 @@ function RoleItem({ consultation, isExpanded, isHighlightedFromGraph, onToggle,
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!isExpanded) { if (!isExpanded) {
e.currentTarget.parentElement!.style.borderColor = 'var(--accent-border)' e.currentTarget.parentElement!.style.borderColor = hexToRgba(consultation.orgColor ?? '#0D6E6E', 0.2)
e.currentTarget.parentElement!.style.boxShadow = 'var(--shadow-md)' e.currentTarget.parentElement!.style.boxShadow = 'var(--shadow-md)'
} }
}} }}
@@ -72,14 +79,14 @@ function RoleItem({ consultation, isExpanded, isHighlightedFromGraph, onToggle,
} }
}} }}
> >
{/* Teal dot */} {/* Org colour dot */}
<div <div
aria-hidden="true" aria-hidden="true"
style={{ style={{
width: '9px', width: '9px',
height: '9px', height: '9px',
borderRadius: '50%', borderRadius: '50%',
background: '#0D6E6E', background: consultation.orgColor ?? '#0D6E6E',
flexShrink: 0, flexShrink: 0,
marginTop: '4px', marginTop: '4px',
}} }}
@@ -150,7 +157,7 @@ function RoleItem({ consultation, isExpanded, isHighlightedFromGraph, onToggle,
padding: '0 12px 12px 30px', padding: '0 12px 12px 30px',
borderTop: '1px solid var(--border-light)', borderTop: '1px solid var(--border-light)',
paddingTop: '12px', paddingTop: '12px',
borderLeft: '2px solid var(--accent)', borderLeft: `2px solid ${consultation.orgColor ?? 'var(--accent)'}`,
marginLeft: '12px', marginLeft: '12px',
}} }}
> >
@@ -185,7 +192,7 @@ function RoleItem({ consultation, isExpanded, isHighlightedFromGraph, onToggle,
width: '4px', width: '4px',
height: '4px', height: '4px',
borderRadius: '50%', borderRadius: '50%',
background: 'var(--accent)', background: consultation.orgColor ?? 'var(--accent)',
opacity: 0.5, opacity: 0.5,
}} }}
/> />
@@ -211,9 +218,9 @@ function RoleItem({ consultation, isExpanded, isHighlightedFromGraph, onToggle,
fontFamily: 'var(--font-mono)', fontFamily: 'var(--font-mono)',
padding: '3px 8px', padding: '3px 8px',
borderRadius: '4px', borderRadius: '4px',
background: 'var(--accent-light)', background: hexToRgba(consultation.orgColor ?? '#0D6E6E', 0.08),
color: 'var(--accent)', color: consultation.orgColor ?? 'var(--accent)',
border: '1px solid var(--accent-border)', border: `1px solid ${hexToRgba(consultation.orgColor ?? '#0D6E6E', 0.2)}`,
}} }}
> >
{entry.code}: {entry.description} {entry.code}: {entry.description}
@@ -233,7 +240,7 @@ function RoleItem({ consultation, isExpanded, isHighlightedFromGraph, onToggle,
gap: '4px', gap: '4px',
fontSize: '12px', fontSize: '12px',
fontWeight: 500, fontWeight: 500,
color: 'var(--accent)', color: consultation.orgColor ?? 'var(--accent)',
background: 'transparent', background: 'transparent',
border: 'none', border: 'none',
padding: '4px 0', padding: '4px 0',
@@ -241,10 +248,10 @@ function RoleItem({ consultation, isExpanded, isHighlightedFromGraph, onToggle,
fontFamily: 'inherit', fontFamily: 'inherit',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.color = 'var(--accent-hover)' e.currentTarget.style.opacity = '0.7'
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.color = 'var(--accent)' e.currentTarget.style.opacity = '1'
}} }}
> >
View full record View full record