feat: US-010 - Clean up removed standalone tiles and verify layout

This commit is contained in:
2026-02-14 18:19:14 +00:00
parent 9ffed8d153
commit fcc1232d9b
7 changed files with 29 additions and 726 deletions
+2 -2
View File
@@ -153,7 +153,7 @@
"Verify in browser using dev-browser skill"
],
"priority": 9,
"passes": false,
"passes": true,
"notes": "Education is the bottom-most subsection in Patient Pathway. Include A-Levels: Mathematics (A*), Chemistry (B), Politics (C) — Highworth Grammar School, 20092011."
},
{
@@ -173,7 +173,7 @@
"Verify in browser using dev-browser skill"
],
"priority": 10,
"passes": false,
"passes": true,
"notes": "ProjectsTile may remain as a standalone tile or be absorbed — check if it still makes sense as standalone. If so, keep it. The key outcome is that the deleted tiles have no remaining references."
},
{
+26
View File
@@ -957,3 +957,29 @@
- CoreSkillsTile.tsx still exists but is no longer imported from DashboardLayout — US-010 will clean it up
- The RepeatMedicationsSubsection is a near-copy of CoreSkillsTile internals minus the Card wrapper — both files exist until cleanup in US-010
---
## 2026-02-14 - US-009
- Moved Education content from standalone EducationTile into Patient Pathway ParentSection as the bottom-most subsection
- Created `src/components/EducationSubsection.tsx` — standalone component with purple dot CardHeader "EDUCATION", renders 3 entries (Mary Seacole, MPharm, A-Levels) with hover, click-to-detail panel, inline details
- Deleted `src/components/tiles/EducationTile.tsx` — content fully moved to EducationSubsection
- Updated DashboardLayout: replaced EducationTile import/render with EducationSubsection inside Patient Pathway
- Files changed: src/components/DashboardLayout.tsx (modified), src/components/EducationSubsection.tsx (new), src/components/tiles/EducationTile.tsx (deleted)
- **Learnings for future iterations:**
- Git detected the move as a rename (91% similarity) — the EducationSubsection is nearly identical to EducationTile minus the Card wrapper
- Same pattern as WorkExperienceSubsection and RepeatMedicationsSubsection: standalone file, `marginTop: 24px` for spacing from preceding section, uses CardHeader for subsection header
- All Patient Pathway subsections now in place: CareerConstellation → LastConsultation → WorkExperience + RepeatMedications (two-column) → Education
---
## 2026-02-14 - US-010
- Deleted 3 orphaned tile files: CareerActivityTile.tsx, CoreSkillsTile.tsx, LatestResultsTile.tsx (LastConsultationTile and EducationTile were already deleted in US-007/US-009)
- Removed unused `.activity-grid` CSS class from index.css (was only used by deleted CareerActivityTile)
- Updated 2 stale comments referencing deleted tiles (index.css, SkillsAllDetail.tsx)
- Verified no broken imports remain — grep found zero import references to any deleted tile
- Verified dashboard grid: PatientSummaryTile (full width) + ProjectsTile (half width) + Patient Pathway ParentSection (full width)
- Browser verification: no visual gaps, all content renders correctly
- Files changed: src/components/tiles/CareerActivityTile.tsx (deleted), src/components/tiles/CoreSkillsTile.tsx (deleted), src/components/tiles/LatestResultsTile.tsx (deleted), src/index.css, src/components/detail/SkillsAllDetail.tsx
- **Learnings for future iterations:**
- ProjectsTile remains as a standalone tile — it makes sense outside Patient Pathway since projects are cross-cutting, not career-timeline-specific
- The `.activity-grid` CSS was orphaned when CareerActivityTile was removed — always check for CSS classes that were only used by deleted components
- Only 2 tile files remain in src/components/tiles/: PatientSummaryTile.tsx and ProjectsTile.tsx
---
+1 -1
View File
@@ -61,7 +61,7 @@ export function SkillsAllDetail({ category }: SkillsAllDetailProps) {
key={group.id}
ref={(el) => { categoryRefs.current[group.id] = el }}
>
{/* Category header — matches CoreSkillsTile divider style */}
{/* Category header — divider style */}
<div
style={{
display: 'flex',
-302
View File
@@ -1,302 +0,0 @@
import React, { useState, useCallback } from 'react'
import { Card, CardHeader } from '../Card'
import { documents } from '@/data/documents'
import { consultations } from '@/data/consultations'
import { skills } from '@/data/skills'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
import CareerConstellation from '../CareerConstellation'
type ActivityType = 'role' | 'project' | 'cert' | 'edu'
interface ActivityEntry {
id: string
type: ActivityType
title: string
meta: string
date: string
sortYear: number
/** ID of the corresponding consultation in consultations.ts (role entries only) */
consultationId?: string
}
/**
* Build timeline from multiple data sources
* Matches the concept HTML entries exactly
*/
function buildTimeline(): ActivityEntry[] {
const entries: ActivityEntry[] = []
// Roles from consultations (matching CV_v4.md)
entries.push({
id: 'interim-head-2025',
type: 'role',
title: 'Interim Head, Population Health & Data Analysis',
meta: 'NHS Norfolk & Waveney ICB',
date: 'May Nov 2025',
sortYear: 2025,
consultationId: 'interim-head-2025',
})
entries.push({
id: 'deputy-head-2024',
type: 'role',
title: 'Deputy Head, Population Health & Data Analysis',
meta: 'NHS Norfolk & Waveney ICB',
date: 'Jul 2024 Present',
sortYear: 2024,
consultationId: 'deputy-head-2024',
})
entries.push({
id: 'high-cost-drugs-2022',
type: 'role',
title: 'High-Cost Drugs & Interface Pharmacist',
meta: 'NHS Norfolk & Waveney ICB',
date: 'May 2022 Jul 2024',
sortYear: 2022,
consultationId: 'high-cost-drugs-2022',
})
entries.push({
id: 'pharmacy-manager-2017',
type: 'role',
title: 'Pharmacy Manager',
meta: 'Tesco PLC',
date: 'Nov 2017 May 2022',
sortYear: 2017,
consultationId: 'pharmacy-manager-2017',
})
// Certifications (matching CV_v4.md)
entries.push({
id: 'doc-gphc',
type: 'cert',
title: 'GPhC Registration',
meta: 'General Pharmaceutical Council',
date: 'August 2016',
sortYear: 2016,
})
entries.push({
id: 'cert-mary-seacole',
type: 'cert',
title: 'NHS Leadership Academy — Mary Seacole Programme',
meta: 'NHS leadership qualification',
date: '2018',
sortYear: 2018,
})
// Education (matching CV_v4.md)
const mpharm = documents.find((d) => d.id === 'doc-mpharm')
if (mpharm) {
entries.push({
id: mpharm.id,
type: 'edu',
title: 'MPharm (Hons) — 2:1',
meta: 'University of East Anglia',
date: '2011 2015',
sortYear: 2011,
})
}
entries.push({
id: 'edu-alevels',
type: 'edu',
title: 'A-Levels: Mathematics (A*), Chemistry (B), Politics (C)',
meta: 'Highworth Grammar School',
date: '2009 2011',
sortYear: 2009,
})
return entries.sort((a, b) => {
if (b.sortYear !== a.sortYear) return b.sortYear - a.sortYear
return 0
})
}
const dotColorMap: Record<ActivityType, string> = {
role: '#0D6E6E',
project: '#D97706',
cert: '#059669',
edu: '#7C3AED',
}
interface ActivityItemProps {
entry: ActivityEntry
onItemClick: () => void
}
const ActivityItem: React.FC<ActivityItemProps> = ({ entry, onItemClick }) => {
const [isHovered, setIsHovered] = useState(false)
const dotColor = dotColorMap[entry.type]
const isClickable = entry.type === 'role' && entry.consultationId
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (!isClickable) return
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onItemClick()
}
},
[isClickable, onItemClick],
)
// Get consultation data for preview text
const consultation = isClickable
? consultations.find((c) => c.id === entry.consultationId)
: null
// Get preview text (first 1-2 lines from examination)
const previewText =
consultation && consultation.examination.length > 0
? consultation.examination[0]
: null
return (
<div
role={isClickable ? 'button' : undefined}
tabIndex={isClickable ? 0 : undefined}
onClick={isClickable ? onItemClick : undefined}
onKeyDown={isClickable ? handleKeyDown : undefined}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
display: 'flex',
flexDirection: 'column',
background: 'var(--bg-dashboard)',
borderRadius: 'var(--radius-sm)',
border: '1px solid var(--border-light)',
fontSize: '12px',
transition: 'all 0.15s ease-out',
cursor: isClickable ? 'pointer' : 'default',
transform: isHovered && isClickable ? 'translateY(-1px)' : 'none',
boxShadow: isHovered && isClickable
? '0 2px 8px rgba(26,43,42,0.08)'
: '0 1px 2px rgba(26,43,42,0.05)',
borderColor: isHovered && isClickable ? 'var(--accent-border)' : 'var(--border-light)',
}}
>
{/* Item header row */}
<div style={{ display: 'flex', gap: '10px', padding: '10px 12px' }}>
<div
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: dotColor,
flexShrink: 0,
marginTop: '2px',
}}
aria-hidden="true"
/>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontWeight: 600,
color: 'var(--text-primary)',
lineHeight: 1.3,
}}
>
{entry.title}
</div>
<div
style={{
fontSize: '11px',
color: 'var(--text-secondary)',
marginTop: '2px',
}}
>
{entry.meta}
</div>
<div
style={{
fontSize: '10px',
fontFamily: 'var(--font-mono)',
color: 'var(--text-tertiary)',
marginTop: '3px',
}}
>
{entry.date}
</div>
{/* Hover preview text for roles */}
{isHovered && previewText && (
<div
style={{
fontSize: '11px',
color: 'var(--text-secondary)',
marginTop: '6px',
lineHeight: 1.4,
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{previewText}
</div>
)}
</div>
</div>
</div>
)
}
export const CareerActivityTile: React.FC = () => {
const timeline = buildTimeline()
const { openPanel } = useDetailPanel()
const handleRoleClick = useCallback(
(roleId: string) => {
const consultation = consultations.find((c) => c.id === roleId)
if (consultation) {
openPanel({ type: 'career-role', consultation })
}
},
[openPanel],
)
const handleSkillClick = useCallback(
(skillId: string) => {
const skill = skills.find((s) => s.id === skillId)
if (skill) {
openPanel({ type: 'skill', skill })
}
},
[openPanel],
)
const handleItemClick = useCallback(
(entry: ActivityEntry) => {
if (entry.type === 'role' && entry.consultationId) {
handleRoleClick(entry.consultationId)
}
},
[handleRoleClick],
)
return (
<Card full tileId="career-activity">
<CardHeader dotColor="teal" title="CAREER ACTIVITY" rightText="Full timeline" />
<div style={{ marginBottom: '20px' }}>
<CareerConstellation
onRoleClick={handleRoleClick}
onSkillClick={handleSkillClick}
/>
</div>
<div className="activity-grid">
{timeline.map((entry) => (
<ActivityItem
key={entry.id}
entry={entry}
onItemClick={() => handleItemClick(entry)}
/>
))}
</div>
</Card>
)
}
-304
View File
@@ -1,304 +0,0 @@
import React from 'react'
import type { LucideIcon } from 'lucide-react'
import {
BarChart3, Code2, Database, PieChart, FileCode2,
Sheet, GitBranch, Workflow, Pill, Users, FileCheck,
TrendingUp, Route, ShieldAlert, Banknote, Handshake,
MessageSquare, UserPlus, RefreshCw, Calculator, Presentation,
ChevronRight,
} from 'lucide-react'
import { Card, CardHeader } from '../Card'
import { skills } from '@/data/skills'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
import type { SkillMedication, SkillCategory } from '@/types/pmr'
const iconMap: Record<string, LucideIcon> = {
BarChart3,
Code2,
Database,
PieChart,
FileCode2,
Sheet,
GitBranch,
Workflow,
Pill,
Users,
FileCheck,
TrendingUp,
Route,
ShieldAlert,
Banknote,
Handshake,
MessageSquare,
UserPlus,
RefreshCw,
Calculator,
Presentation,
}
const SKILLS_PER_CATEGORY = 4
const categoryConfig: { id: SkillCategory; label: string }[] = [
{ id: 'Technical', label: 'Technical' },
{ id: 'Domain', label: 'Healthcare Domain' },
{ id: 'Leadership', label: 'Strategic & Leadership' },
]
interface SkillRowProps {
skill: SkillMedication
onClick: () => void
}
function SkillRow({ skill, onClick }: SkillRowProps) {
const IconComponent = iconMap[skill.icon]
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onClick()
}
}
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={handleKeyDown}
aria-label={`${skill.name}: ${skill.frequency}, ${skill.yearsOfExperience} years experience. Click for details.`}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '8px 10px',
minHeight: '44px',
background: 'var(--bg-dashboard)',
borderRadius: 'var(--radius-sm)',
border: '1px solid var(--border-light)',
cursor: 'pointer',
transition: 'border-color 0.15s, box-shadow 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'var(--accent-border)'
e.currentTarget.style.boxShadow = 'var(--shadow-md)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--border-light)'
e.currentTarget.style.boxShadow = 'none'
}}
>
{/* Icon */}
<div
style={{
width: '26px',
height: '26px',
borderRadius: '6px',
background: 'var(--accent-light)',
color: 'var(--accent)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
{IconComponent && <IconComponent size={13} />}
</div>
{/* Text */}
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: '12.5px',
fontWeight: 600,
color: 'var(--text-primary)',
lineHeight: 1.3,
}}
>
{skill.name}
</div>
<div
style={{
fontSize: '10.5px',
color: 'var(--text-tertiary)',
fontFamily: '"Geist Mono", monospace',
}}
>
{skill.frequency} · {skill.yearsOfExperience} yrs
</div>
</div>
{/* Status badge */}
<div
style={{
fontSize: '10px',
fontWeight: 500,
padding: '2px 7px',
borderRadius: '20px',
background: 'var(--success-light)',
color: 'var(--success)',
border: '1px solid var(--success-border)',
flexShrink: 0,
}}
>
{skill.status}
</div>
{/* Chevron */}
<ChevronRight
size={14}
style={{ color: 'var(--text-tertiary)', flexShrink: 0 }}
/>
</div>
)
}
interface CategorySectionProps {
label: string
categoryId: SkillCategory
skills: SkillMedication[]
onSkillClick: (skill: SkillMedication) => void
onViewAll: (category: SkillCategory) => void
isFirst: boolean
}
function CategorySection({
label,
categoryId,
skills: categorySkills,
onSkillClick,
onViewAll,
isFirst,
}: CategorySectionProps) {
const visibleSkills = categorySkills.slice(0, SKILLS_PER_CATEGORY)
const remainingCount = categorySkills.length - SKILLS_PER_CATEGORY
return (
<div style={{ marginTop: isFirst ? 0 : '16px' }}>
{/* Category header — sidebar section divider style */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '10px',
}}
>
<span
style={{
fontSize: '10px',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.06em',
color: 'var(--text-tertiary)',
whiteSpace: 'nowrap',
}}
>
{label}
</span>
<div
style={{
flex: 1,
height: '1px',
background: 'var(--border-light)',
}}
/>
<span
style={{
fontSize: '10px',
color: 'var(--text-tertiary)',
fontFamily: '"Geist Mono", monospace',
whiteSpace: 'nowrap',
}}
>
{categorySkills.length} items
</span>
</div>
{/* Skill rows */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{visibleSkills.map((skill) => (
<SkillRow
key={skill.id}
skill={skill}
onClick={() => onSkillClick(skill)}
/>
))}
</div>
{/* View all button */}
{remainingCount > 0 && (
<button
onClick={() => onViewAll(categoryId)}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
marginTop: '8px',
padding: '4px 0',
minHeight: '44px',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '11px',
fontWeight: 500,
color: 'var(--accent)',
fontFamily: 'inherit',
transition: 'color 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = 'var(--accent-hover)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = 'var(--accent)'
}}
aria-label={`View all ${categorySkills.length} ${label} skills`}
>
View all ({categorySkills.length})
<ChevronRight size={12} />
</button>
)}
</div>
)
}
export function CoreSkillsTile() {
const { openPanel } = useDetailPanel()
// Group skills by category, sorted by proficiency descending
const groupedSkills = categoryConfig.map(({ id, label }) => ({
id,
label,
skills: skills
.filter((s) => s.category === id)
.sort((a, b) => b.proficiency - a.proficiency),
}))
const handleSkillClick = (skill: SkillMedication) => {
openPanel({ type: 'skill', skill })
}
const handleViewAll = (category: SkillCategory) => {
openPanel({ type: 'skills-all', category })
}
return (
<Card full tileId="core-skills">
<CardHeader
dotColor="amber"
title="REPEAT MEDICATIONS"
rightText="Active prescriptions"
/>
{groupedSkills.map((group, index) => (
<CategorySection
key={group.id}
label={group.label}
categoryId={group.id}
skills={group.skills}
onSkillClick={handleSkillClick}
onViewAll={handleViewAll}
isFirst={index === 0}
/>
))}
</Card>
)
}
-103
View File
@@ -1,103 +0,0 @@
import React from 'react'
import { Card, CardHeader } from '../Card'
import { kpis } from '@/data/kpis'
import type { KPI } from '@/types/pmr'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
const colorMap: Record<KPI['colorVariant'], string> = {
green: '#059669',
amber: '#D97706',
teal: '#0D6E6E',
}
interface MetricCardProps {
kpi: KPI
}
function MetricCard({ kpi }: MetricCardProps) {
const { openPanel } = useDetailPanel()
const handleClick = () => {
openPanel({ type: 'kpi', kpi })
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
openPanel({ type: 'kpi', kpi })
}
}
const buttonStyles: React.CSSProperties = {
width: '100%',
textAlign: 'left',
padding: '16px',
background: 'var(--surface)',
border: '1px solid var(--border-light)',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
transition: 'border-color 150ms ease-out, box-shadow 150ms ease-out',
}
const valueStyles: React.CSSProperties = {
fontSize: '28px',
fontWeight: 700,
letterSpacing: '-0.02em',
lineHeight: 1.2,
color: colorMap[kpi.colorVariant],
}
const labelStyles: React.CSSProperties = {
fontSize: '12px',
fontWeight: 500,
color: 'var(--text-primary)',
marginTop: '4px',
}
const subStyles: React.CSSProperties = {
fontSize: '10px',
color: 'var(--text-tertiary)',
fontFamily: 'var(--font-geist-mono)',
marginTop: '2px',
}
return (
<button
onClick={handleClick}
onKeyDown={handleKeyDown}
style={buttonStyles}
aria-label={`${kpi.label}: ${kpi.value}. Click to view details.`}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'var(--accent-border)'
e.currentTarget.style.boxShadow = 'var(--shadow-md)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--border-light)'
e.currentTarget.style.boxShadow = 'none'
}}
>
<div style={valueStyles}>{kpi.value}</div>
<div style={labelStyles}>{kpi.label}</div>
<div style={subStyles}>{kpi.sub}</div>
</button>
)
}
export function LatestResultsTile() {
const gridStyles: React.CSSProperties = {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '12px',
}
return (
<Card tileId="latest-results">
<CardHeader dotColor="teal" title="LATEST RESULTS" rightText="Updated May 2025" />
<div style={gridStyles}>
{kpis.map((kpi) => (
<MetricCard key={kpi.id} kpi={kpi} />
))}
</div>
</Card>
)
}
-14
View File
@@ -286,20 +286,6 @@ html {
}
/* Activity grid responsive — mobile-first (used in CareerActivityTile) */
.activity-grid {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
/* Tablet and up: 2 columns */
@media (min-width: 768px) {
.activity-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Pathway two-column layout — mobile-first (used in Patient Pathway) */
.pathway-columns {
display: grid;