Changed "Your role" to "My Role"

This commit is contained in:
2026-02-19 14:00:20 +00:00
parent 012c905c90
commit a1f7088b48
39 changed files with 46 additions and 21804 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.
-323
View File
@@ -1,323 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project Grid Concept — Overlay Variant</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #1a1a2e;
color: rgba(255,255,255,0.87);
font-family: 'Inter', system-ui, sans-serif;
padding: 32px 24px;
min-height: 100vh;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
font-family: 'Geist Mono', 'Fira Code', monospace;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(255,255,255,0.6);
}
.section-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #00897B;
flex-shrink: 0;
}
.section-title {
color: rgba(255,255,255,0.87);
font-weight: 600;
}
.section-count {
color: rgba(255,255,255,0.38);
margin-left: auto;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.card {
position: relative;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
border: 1px solid rgba(255,255,255,0.08);
transition: border-color 0.2s, box-shadow 0.2s;
aspect-ratio: 16 / 9;
}
.card:hover {
border-color: rgba(0,137,123,0.5);
box-shadow: 0 4px 20px rgba(0,137,123,0.15);
}
.card:hover .card-top,
.card:hover .card-bottom {
background: rgba(18, 18, 35, 0.88);
}
/* Thumbnail background */
.card-bg {
position: absolute;
inset: 0;
}
.card-bg img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top;
display: block;
}
/* Text overlay — no background, just a layout shell */
.card-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* Top section: title + year + status */
.card-top {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
background: rgba(18, 18, 35, 0.78);
padding: 10px 12px;
border-radius: 5px 5px 0 0;
transition: background 0.2s;
}
.card-name {
font-size: 13px;
font-weight: 500;
color: rgba(255,255,255,0.9);
line-height: 1.2;
}
.card-year {
font-family: 'Geist Mono', 'Fira Code', monospace;
font-size: 11px;
color: rgba(255,255,255,0.4);
}
/* Bottom section: result + tags */
.card-bottom {
background: rgba(18, 18, 35, 0.78);
padding: 10px 12px;
border-radius: 0 0 5px 5px;
transition: background 0.2s;
}
.card-result {
font-family: 'Inter', system-ui, sans-serif;
font-size: 12px;
font-weight: 500;
color: rgba(0, 178, 163, 0.9);
line-height: 1.4;
margin-bottom: 6px;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.complete { background: #4caf50; }
.status-live {
font-family: 'Geist Mono', 'Fira Code', monospace;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #00897B;
background: rgba(0,137,123,0.2);
padding: 1px 5px;
border-radius: 3px;
line-height: 1.4;
}
.card-tags {
display: flex;
gap: 4px;
flex-wrap: nowrap;
overflow: hidden;
align-items: center;
}
.tag {
font-family: 'Geist Mono', 'Fira Code', monospace;
font-size: 10px;
padding: 1px 5px;
border-radius: 3px;
white-space: nowrap;
line-height: 1.5;
flex-shrink: 0;
}
.tag-tech {
background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.55);
}
.tag-domain {
background: rgba(0,137,123,0.12);
color: rgba(0,137,123,0.8);
}
.tag-overflow {
font-family: 'Geist Mono', 'Fira Code', monospace;
font-size: 10px;
color: rgba(255,255,255,0.3);
white-space: nowrap;
flex-shrink: 0;
}
.card-chevron {
position: absolute;
top: 10px;
right: 10px;
font-size: 12px;
color: rgba(255,255,255,0.2);
line-height: 1;
}
@media (max-width: 767px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 479px) {
.grid { grid-template-columns: 1fr; }
body { padding: 20px 16px; }
}
</style>
</head>
<body>
<div class="section-header">
<span class="section-dot"></span>
<span class="section-title">Significant Interventions</span>
<span class="section-count">6 investigations</span>
</div>
<div class="grid" id="grid"></div>
<script>
const projects = [
{
name: "Patient Switching Algorithm",
year: 2025,
status: "complete",
result: "14,000 patients identified for potential therapeutic switching",
tech: ["Python", "Pandas", "SQL"],
domain: ["Health Economics", "Medicines Optimisation"],
thumb: "thumbnails/switchingdashboard.jpg"
},
{
name: "Blueteq Generator",
year: 2023,
status: "complete",
result: "70% reduction in high-cost drug approval forms",
tech: ["Python", "SQL"],
domain: ["High-Cost Drugs", "Process Automation"],
thumb: "thumbnails/blueteq.jpg"
},
{
name: "PharMetrics",
year: 2025,
status: "live",
result: "Live at medicines.charlwood.xyz",
tech: ["React", "TypeScript", "D3.js", "Tailwind", "Vite", "Supabase", "Recharts"],
domain: ["Health Economics", "Medicines Optimisation", "Data Visualisation"],
thumb: "thumbnails/pharmmetrics.jpg"
},
{
name: "Patient Pathway Analysis Tool",
year: 2024,
status: "complete",
result: "9 interactive chart types, sub-50ms query responses",
tech: ["Python", "Dash", "Plotly", "Pandas", "SQL", "CSS", "Docker", "Gunicorn"],
domain: ["Health Economics", "Data Visualisation", "Medicines Optimisation", "Prescribing Analytics", "Clinical Pathways", "Population Health"],
thumb: "thumbnails/pathways.jpg"
},
{
name: "CD Monitoring System",
year: 2024,
status: "complete",
result: "Population-scale OME tracking for controlled drugs",
tech: ["Python", "Pandas", "SQL"],
domain: ["Controlled Drugs", "Medicines Safety", "Population Health"],
thumb: "thumbnails/ome.jpg"
},
{
name: "NMS National Training Video",
year: 2018,
status: "complete",
result: "Shared nationally across Tesco Pharmacy network",
tech: ["Video Production", "Adobe Premiere"],
domain: ["Training", "New Medicine Service"],
thumb: "thumbnails/nms.jpg"
}
];
const grid = document.getElementById("grid");
projects.forEach(p => {
const techShow = p.tech.slice(0, 2);
const domainShow = p.domain.slice(0, 2);
const overflow = (p.tech.length - 2) + (p.domain.length - 2);
const statusHtml = p.status === "live"
? `<span class="status-live">Live</span>`
: `<span class="status-dot complete"></span>`;
const tagsHtml = [
...techShow.map(t => `<span class="tag tag-tech">${t}</span>`),
...domainShow.map(t => `<span class="tag tag-domain">${t}</span>`),
...(overflow > 0 ? [`<span class="tag-overflow">+${overflow}</span>`] : [])
].join("");
const card = document.createElement("div");
card.className = "card";
card.innerHTML = `
<div class="card-bg"><img src="public/${p.thumb}" alt="${p.name} screenshot"></div>
<div class="card-overlay">
<div class="card-top">
<span class="card-name">${p.name}</span>
<span class="card-year">${p.year}</span>
${statusHtml}
</div>
<div class="card-bottom">
<div class="card-result">${p.result}</div>
<div class="card-tags">${tagsHtml}</div>
</div>
</div>
<span class="card-chevron">&#9662;</span>
`;
grid.appendChild(card);
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -96,9 +96,9 @@ export function KPIDetail({ kpi }: KPIDetailProps) {
<p style={paragraphStyle}>{context}</p> <p style={paragraphStyle}>{context}</p>
</div> </div>
{/* Your role paragraph */} {/* My role paragraph */}
<div> <div>
<h3 style={sectionHeadingStyle}>Your Role</h3> <h3 style={sectionHeadingStyle}>My Role</h3>
<p style={paragraphStyle}>{role}</p> <p style={paragraphStyle}>{role}</p>
</div> </div>
+44 -43
View File
@@ -35,12 +35,34 @@ export const investigations: Investigation[] = [
'14,000 patients identified for cost-effective alternatives', '14,000 patients identified for cost-effective alternatives',
'£2.6M annual savings potential identified', '£2.6M annual savings potential identified',
'£2M on target for delivery this financial year', '£2M on target for delivery this financial year',
'Novel GP payment system linking rewards to savings', 'Reimplemented in DAX for sharing as internal Power BI tool',
'Novel GP payment system linking rewards to savings'
], ],
techStack: ['Python', 'Pandas', 'SQL'], techStack: ['DAX', 'Python', 'SQL'],
skills: ['Health Economics', 'Medicines Optimisation', 'Prescribing Analytics'], skills: ['Health Economics', 'Medicines Optimisation', 'Power BI'],
thumbnail: '/thumbnails/switchingdashboard.jpg', thumbnail: '/thumbnails/switchingdashboard.jpg',
}, },
{
id: 'inv-nms-training',
name: 'NMS National Training Video',
requestedYear: 2018,
reportedYear: 2018,
status: 'Live',
resultSummary: 'Shared nationally across Tesco Pharmacy',
requestingClinician: 'A. Charlwood',
methodology: 'Self-produced training video covering the full New Medicine Service workflow — the three-stage consultation process (Engagement, Intervention, Follow-up), eligibility criteria for target conditions (asthma/COPD, hypertension, anticoagulation, type 2 diabetes), and practical techniques like the "star" prescription marking system. Features a patient case study demonstrating how NMS intervention corrected inhaler technique, and presents adherence data showing non-adherence halving from 20% to 10%. Created independently to address inconsistent NMS delivery across stores.',
results: [
'Shared across Tesco Pharmacy nationally to support delivery of the New Medicine Service',
'Empowered non-pharmacist staff to identify and enrol eligible patients',
'Improved consistency and quality of NMS engagement from non-pharmacist staff',
'Supported successful uplift in NMS performance metrics across stores',
],
techStack: ['Video Production'],
skills: ['Training & Development', 'Clinical Services', 'Leadership'],
externalUrl: 'https://www.youtube.com/watch?v=Rm1wcX92XlQ',
thumbnail: '/thumbnails/nms.jpg',
},
{ {
id: 'inv-blueteq-gen', id: 'inv-blueteq-gen',
name: 'Blueteq Generator', name: 'Blueteq Generator',
@@ -60,46 +82,6 @@ export const investigations: Investigation[] = [
skills: ['High-Cost Drugs', 'Prior Approval', 'Process Automation'], skills: ['High-Cost Drugs', 'Prior Approval', 'Process Automation'],
thumbnail: '/thumbnails/blueteq.jpg', thumbnail: '/thumbnails/blueteq.jpg',
}, },
{
id: 'inv-cd-monitoring',
name: 'CD Monitoring System',
requestedYear: 2024,
reportedYear: 2024,
status: 'Complete',
resultSummary: 'Population-scale OME tracking',
requestingClinician: 'A. Charlwood',
methodology: 'Python-based controlled drug monitoring system calculating oral morphine equivalents (OME) across all opioid prescriptions to track patient-level exposure over time.',
results: [
'Patient-level OME tracking over time',
'High-risk patient identification',
'Potential diversion detection',
'Previously impossible population-scale analysis',
],
techStack: ['Python', 'SQL'],
skills: ['Controlled Drugs', 'Patient Safety', 'Prescribing Analytics'],
thumbnail: '/thumbnails/ome.jpg',
},
{
id: 'inv-nms-training',
name: 'NMS National Training Video',
requestedYear: 2018,
reportedYear: 2018,
status: 'Complete',
resultSummary: 'Shared nationally across Tesco Pharmacy',
requestingClinician: 'A. Charlwood',
methodology: 'Self-produced training video covering the full New Medicine Service workflow — the three-stage consultation process (Engagement, Intervention, Follow-up), eligibility criteria for target conditions (asthma/COPD, hypertension, anticoagulation, type 2 diabetes), and practical techniques like the "star" prescription marking system. Features a patient case study demonstrating how NMS intervention corrected inhaler technique, and presents adherence data showing non-adherence halving from 20% to 10%. Created independently to address inconsistent NMS delivery across stores.',
results: [
'Shared across Tesco Pharmacy nationally to support delivery of the New Medicine Service',
'Empowered non-pharmacist staff to identify and enrol eligible patients',
'Improved consistency and quality of NMS engagement from non-pharmacist staff',
'Supported successful uplift in NMS performance metrics across stores',
],
techStack: ['Video Production'],
skills: ['Training & Development', 'Clinical Services', 'Leadership'],
externalUrl: 'https://www.youtube.com/watch?v=Rm1wcX92XlQ',
thumbnail: '/thumbnails/nms.jpg',
},
{ {
id: 'inv-pathway-analysis', id: 'inv-pathway-analysis',
name: 'Patient Pathway Analysis Platform', name: 'Patient Pathway Analysis Platform',
@@ -121,4 +103,23 @@ export const investigations: Investigation[] = [
demoUrl: 'https://demo.charlwood.xyz', demoUrl: 'https://demo.charlwood.xyz',
thumbnail: '/thumbnails/pathways.jpg', thumbnail: '/thumbnails/pathways.jpg',
}, },
{
id: 'inv-cd-monitoring',
name: 'CD Monitoring System',
requestedYear: 2024,
reportedYear: 2024,
status: 'Complete',
resultSummary: 'Population-scale OME tracking',
requestingClinician: 'A. Charlwood',
methodology: 'Python-based controlled drug monitoring system calculating oral morphine equivalents (OME) across all opioid prescriptions to track patient-level exposure over time.',
results: [
'Patient-level OME tracking over time',
'High-risk patient identification',
'Potential diversion detection',
'Previously impossible population-scale analysis',
],
techStack: ['Python', 'SQL'],
skills: ['Controlled Drugs', 'Patient Safety', 'Prescribing Analytics'],
thumbnail: '/thumbnails/ome.jpg',
},
] ]
-265
View File
@@ -1,265 +0,0 @@
# PRD: Career Constellation — Clinical Pathway Overhaul
## Introduction
The CareerConstellation D3 force-directed graph sits alongside the work experience timeline in the "Patient Pathway" section. It currently looks prototype-quality: the timeline runs in the wrong direction, the chart doesn't fill the available height, node styling is basic, and the visual language doesn't match the refined GP clinical system aesthetic used across the rest of the portfolio.
This PRD covers a comprehensive visual and structural overhaul to transform the graph into a polished, clinical-style patient pathway diagram that complements the adjacent work experience timeline — with synchronised year positions, bidirectional hover highlighting, and a refined design language matching the rest of the dashboard.
**Implementation note:** All user stories involving D3 rendering changes should use the `d3-viz` skill.
## Goals
- Reverse the timeline to top = latest (2025), bottom = earliest (2017) so it visually syncs with the reverse-chronological work experience cards beside it
- Dynamically match the graph's height to the work experience column so both columns align
- Achieve visual parity with the rest of the dashboard — clean, clinical, premium, not prototype-looking
- Implement bidirectional highlighting between the work experience timeline cards and the constellation graph
- Create a clear visual hierarchy where skill nodes stay muted until contextually relevant (hover/click)
## User Stories
### US-001: Reverse timeline direction (top = most recent)
**Description:** As a visitor, I want the graph's vertical timeline to run top-to-bottom from 2025→2017 so that the year positions visually align with the reverse-chronological work experience cards in the adjacent column.
**Acceptance Criteria:**
- [ ] `yScale` domain is reversed: `[maxYear, minYear]` maps to `[topPadding, height - bottomPadding]`
- [ ] Role nodes appear at their correct year positions with 2025 near the top and 2017 near the bottom
- [ ] Year labels along the timeline axis read top-to-bottom: 2025, 2024, 2023, ... 2017
- [ ] Skill nodes cluster around their linked roles at the correct vertical positions
- [ ] The timeline vertical line, year dots, and horizontal guide lines all reflect the reversed scale
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser using dev server
### US-002: Dynamic height matching with work experience column
**Description:** As a visitor, I want the constellation graph to fill the same vertical space as the work experience column so both columns appear balanced and the graph uses its full available height.
**Acceptance Criteria:**
- [ ] Remove fixed `DESKTOP_HEIGHT`, `TABLET_HEIGHT`, `MOBILE_HEIGHT` constants
- [ ] The graph container measures the rendered height of the adjacent `.chronology-stream` element and uses that as its own height
- [ ] Use a `ResizeObserver` on the chronology column to update the graph height when cards expand/collapse
- [ ] Set a sensible minimum height (e.g. 400px) so the graph doesn't collapse on short content
- [ ] On mobile (single-column layout), the graph uses a reasonable standalone height (e.g. 360px) since the columns stack
- [ ] The `viewBox` and all D3 scales update correctly when height changes
- [ ] Typecheck passes
- [ ] Verify in browser — expand/collapse work experience cards and confirm the graph height adjusts
### US-003: Clinical pathway visual language — background and structure
**Description:** As a visitor, I want the graph to look like a clinical patient pathway diagram — clean, precise, and institutional — matching the GP system dashboard aesthetic.
**Acceptance Criteria:**
- [ ] Replace the radial gradient background with a clean white (`var(--surface)`) or very subtle warm tint matching `var(--bg-dashboard)`
- [ ] Add a subtle 1px border matching `var(--border-light)` and `border-radius: var(--radius-sm)` consistent with other dashboard cards
- [ ] Timeline axis styled as a refined vertical rule: 1px solid `var(--border)` colour, not the current thick teal line
- [ ] Year markers are small ticks extending from the timeline, not floating dots — styled like clinical chart gridlines
- [ ] Year labels use `font-family: var(--font-geist-mono)`, `font-size: 10px`, colour `var(--text-tertiary)` — matching dashboard data labels
- [ ] Horizontal guide lines are very subtle (0.3 opacity, dashed or dotted) — they guide the eye without dominating
- [ ] Remove the existing legend box (it takes up valuable space and will be replaced in US-007)
- [ ] All colours use CSS custom properties from the design system (`var(--accent)`, `var(--success)`, `var(--amber)`, etc.)
- [ ] Typecheck passes
- [ ] Verify in browser — the graph should feel like it belongs on the same page as the other tiles
### US-004: Role node redesign — clinical record anchors
**Description:** As a visitor, I want role nodes to look like refined clinical record entries rather than basic coloured circles, clearly anchored to their timeline position.
**Acceptance Criteria:**
- [ ] Role nodes rendered as rounded rectangles (pill shapes) rather than circles — wider to accommodate text, roughly 90-110px wide x 32-36px tall
- [ ] Each role node displays the `shortLabel` text centred inside, using `font-family: var(--font-ui)`, weight 600, size 11px
- [ ] Role node fill uses the `orgColor` from data at reduced opacity (e.g. 0.12) with a 1px border of the same colour at higher opacity (0.4), and text in the `orgColor` at full strength — creating a subtle badge effect
- [ ] A thin connector line links each role node horizontally back to the timeline axis at its year position — like a clinical event marker
- [ ] Role nodes have a subtle hover state: border opacity increases, shadow appears (`var(--shadow-sm)`)
- [ ] Active/pinned role node: border becomes solid at full `orgColor`, subtle inset glow or stronger shadow
- [ ] Role nodes are positioned to the right of the timeline axis with consistent horizontal offset
- [ ] Typecheck passes
- [ ] Verify in browser — role nodes should look like labelled clinical event markers
### US-005: Skill node redesign — muted by default, revealed on interaction
**Description:** As a visitor, I want skill nodes to be visually subdued by default, becoming prominent only when a connected role or skill is hovered or clicked — creating a clean resting state and a meaningful highlighted state.
**Acceptance Criteria:**
- [ ] Default (resting) state for skill nodes: small circles (radius 6-8px), fill opacity 0.25, no visible label
- [ ] Skill node fill colour determined by domain: technical = `var(--accent)`, clinical = `var(--success)`, leadership = `var(--amber)`
- [ ] When a connected role is hovered/pinned: connected skill nodes transition to radius 10-12px, fill opacity 0.85, and their labels fade in (opacity 0 → 1, 200ms ease-out)
- [ ] Skill labels use `font-family: var(--font-geist-mono)`, size 10px, colour `var(--text-secondary)`
- [ ] When a skill node itself is hovered: that skill and all its connected roles highlight, with the skill growing to full size and showing its label
- [ ] Link lines from roles to skills: default state is very subtle (opacity 0.08-0.12); highlighted state is 0.5-0.7 with domain colour
- [ ] Unconnected nodes (not part of the active highlight group) reduce to opacity 0.08 — nearly invisible
- [ ] Transitions between states are smooth (150-200ms) and respect `prefers-reduced-motion`
- [ ] Typecheck passes
- [ ] Verify in browser — the graph should feel clean and quiet at rest, informative on interaction
### US-006: Bidirectional hover highlighting with work experience cards
**Description:** As a visitor, I want to hover over a work experience card in the timeline and see the corresponding role and its skills light up in the graph, and vice versa — creating a clear visual link between the two columns.
**Acceptance Criteria:**
- [ ] Hovering a `RoleItem` in `WorkExperienceSubsection` calls `onNodeHighlight(consultation.id)` (already partially implemented)
- [ ] The `CareerConstellation` component receives `highlightedNodeId` and applies the highlight logic from US-005
- [ ] Hovering a role node in the graph triggers a callback that highlights the corresponding work experience card in the timeline (new: requires a reverse callback)
- [ ] Add a new prop `onNodeHover?: (id: string | null) => void` to `CareerConstellation` — fires on mouseenter/mouseleave of role nodes
- [ ] `DashboardLayout` passes this callback and uses it to set a `highlightedRoleId` state
- [ ] `WorkExperienceSubsection` receives `highlightedRoleId` and applies a subtle highlight style to the matching card (e.g. border colour change to `var(--accent-border)`, light background tint)
- [ ] `LastConsultationSubsection` also participates in the highlight system for the most recent role
- [ ] Highlight clears when mouse leaves both the card and the graph node
- [ ] On touch devices, tap-to-pin behaviour works as before — tapping a role pins the highlight in both the graph and the timeline
- [ ] Typecheck passes
- [ ] Verify in browser — hover over work experience cards and confirm the graph highlights; hover graph nodes and confirm the timeline cards highlight
### US-007: Compact domain legend and graph header
**Description:** As a visitor, I want a small, unobtrusive legend that explains the domain colour coding without taking up significant graph space.
**Acceptance Criteria:**
- [ ] Remove the existing boxed legend from inside the SVG
- [ ] Add a compact inline legend below (or above) the SVG container — rendered as React HTML, not SVG
- [ ] Legend shows three small coloured dots with labels: "Technical", "Clinical", "Leadership" — using the domain colours from the design system
- [ ] Legend text uses `font-family: var(--font-geist-mono)`, size 10px, colour `var(--text-tertiary)`
- [ ] Legend is horizontally laid out with subtle separators, taking minimal vertical space (single line, ~20px tall)
- [ ] Include a small "Hover to explore connections" hint text in the legend row, matching the tertiary text style
- [ ] Typecheck passes
- [ ] Verify in browser
### US-008: Link line refinement — clinical pathway connections
**Description:** As a visitor, I want the connection lines between roles and skills to look like refined clinical pathway links rather than basic straight lines.
**Acceptance Criteria:**
- [ ] Replace straight `<line>` elements with curved `<path>` elements using D3 curve generators (e.g. `d3.curveBasis` or `d3.curveBundle`)
- [ ] Default link styling: 1px stroke, colour `var(--border-light)`, opacity 0.12 — barely visible at rest
- [ ] Highlighted link styling: 1.5-2px stroke, domain colour of the skill end, opacity 0.5-0.7
- [ ] Link `strength` value from data influences the highlighted stroke opacity (stronger connections more visible)
- [ ] Links animate smoothly between default and highlighted states (150ms transition)
- [ ] Respect `prefers-reduced-motion` — skip transitions, jump to final state
- [ ] Typecheck passes
- [ ] Verify in browser — links should be nearly invisible at rest and clearly trace pathways on hover
### US-009: Force simulation tuning for clinical layout
**Description:** As a developer, I want the D3 force simulation tuned so that role nodes stay firmly anchored to their timeline positions while skill nodes distribute cleanly in the available space to the right.
**Acceptance Criteria:**
- [ ] Role nodes are effectively fixed to their timeline Y position (very high `forceY` strength, e.g. 0.95-1.0) and a consistent X position offset from the timeline
- [ ] Skill nodes distribute in the space to the right of the role nodes, clustered near their connected roles but with enough separation to avoid overlap
- [ ] Increase collision radius slightly to prevent label overlap when skills are revealed on hover
- [ ] Simulation settles quickly — `alphaDecay` tuned so the graph stabilises within 1-2 seconds (or immediately for `prefers-reduced-motion`)
- [ ] Boundary clamping keeps all nodes within the SVG viewport with adequate padding (role labels don't clip, skill labels don't overflow)
- [ ] On height changes (from US-002), the simulation re-initialises smoothly without jarring jumps
- [ ] Typecheck passes
- [ ] Verify in browser — nodes should feel organised and intentional, not randomly scattered
### US-010: Content audit — verify role descriptions against CV
**Description:** As the portfolio owner, I want to ensure all role titles, organisations, dates, and achievement bullets in the work experience data are accurate and up-to-date against the source CV.
**Acceptance Criteria:**
- [ ] Cross-reference `src/data/consultations.ts` against `References/CV_v4.md` and `References/Andy_Charlwood_CV_ATS_Optimised.pdf`
- [ ] Verify all role titles match exactly (e.g. "Interim Head, Population Health & Data Analysis" not abbreviated incorrectly)
- [ ] Verify all organisation names match (e.g. "NHS Norfolk & Waveney ICB" consistently)
- [ ] Verify all date ranges are correct (start/end dates for each role)
- [ ] Verify achievement bullets (`examination` arrays) are accurate — numbers, percentages, and claims match the CV source
- [ ] Verify `constellation.ts` role node data (labels, shortLabels, orgColors, years) is consistent with consultations data
- [ ] Flag and fix any discrepancies found
- [ ] Document any intentional differences (e.g. shortened bullet text for space)
### US-011: Accessibility hardening
**Description:** As a visitor using assistive technology, I want the constellation graph to be fully accessible with keyboard navigation and screen reader support.
**Acceptance Criteria:**
- [ ] The hidden accessibility buttons (already present) have `pointerEvents: 'auto'` so they are actually focusable/clickable (currently set to `'none'`)
- [ ] Tab order follows a logical sequence: role nodes in reverse-chronological order, then skill nodes grouped by domain
- [ ] Focus ring styling is visible and uses the design system accent colour with sufficient contrast
- [ ] Screen reader description (`srDescription`) is updated to reflect the reversed timeline direction
- [ ] `aria-label` on the SVG is updated to mention the clinical pathway metaphor
- [ ] All interactive states (hover highlight, pin, expand) are achievable via keyboard
- [ ] `prefers-reduced-motion` is respected throughout — all animations skip to final state
- [ ] Typecheck passes
- [ ] Test with keyboard navigation — Tab through all nodes, Enter to activate
### US-012: Responsive behaviour — mobile and tablet
**Description:** As a visitor on a smaller screen, I want the constellation graph to display appropriately when the columns stack vertically.
**Acceptance Criteria:**
- [ ] On mobile/tablet (single-column `.pathway-columns` layout), the graph renders at a reasonable fixed height (360-400px) since it no longer has a column to match
- [ ] The graph simplifies slightly on mobile: role labels may use shorter text, skill node default radius decreases slightly
- [ ] Touch interactions work correctly: tap to pin a node, tap elsewhere to unpin
- [ ] The graph is not cropped or overflowing on narrow viewports (min-width handling)
- [ ] The HTML legend from US-007 wraps gracefully on narrow screens
- [ ] Typecheck passes
- [ ] Verify in browser at mobile viewport widths (375px, 430px)
## Functional Requirements
- FR-1: The constellation graph's vertical axis must run top = 2025, bottom = 2017 (reverse chronological)
- FR-2: The graph container must dynamically match the height of the adjacent chronology stream column
- FR-3: All visual styling must use the project's CSS custom properties and design tokens
- FR-4: Role nodes must be rendered as labelled rounded rectangles (pills) anchored to timeline positions
- FR-5: Skill nodes must default to low opacity (0.25) with small radius, becoming prominent only on hover/pin
- FR-6: Hovering a work experience card must highlight the corresponding graph node and its connections
- FR-7: Hovering a graph role node must highlight the corresponding work experience card
- FR-8: Connection lines must use curved paths, barely visible at rest, prominent when highlighted
- FR-9: The force simulation must keep role nodes firmly at their timeline positions
- FR-10: All role data must be verified against the source CV documents
- FR-11: Keyboard navigation and screen reader support must be maintained and improved
- FR-12: The graph must handle responsive breakpoints (desktop dual-column, mobile/tablet single-column)
## Non-Goals
- No animation of nodes entering the viewport (scroll-triggered animation)
- No zoom/pan interaction on the graph
- No tooltip popovers on nodes — interactions open the existing detail panel
- No changes to the work experience card design itself (only adding highlight state)
- No changes to the `LastConsultationSubsection` design (only adding highlight participation)
- No changes to the boot, ECG, or login phases
- No addition of new skills or roles beyond what's in the CV
## Design Considerations
### Visual Language
The graph should feel like a **clinical patient pathway diagram** — the kind of clean, precise visualisation you'd see in a modern healthcare analytics dashboard. Think: straight connector lines with subtle curves, institutional colour palette, monospace data labels, status-indicator dots.
### Colour Usage
- Role nodes: Use `orgColor` from data at low opacity for fill, higher opacity for border and text — creating subtle badges
- Skill nodes by domain: Technical `var(--accent)` / Clinical `var(--success)` / Leadership `var(--amber)`
- All links and guides: Use `var(--border)` and `var(--border-light)` tokens
- Highlighted state: Domain colour at moderate opacity, never garish
### Typography in SVG
- Role labels inside nodes: `var(--font-ui)`, weight 600, 11px
- Skill labels below nodes: `var(--font-geist-mono)`, 10px
- Year labels: `var(--font-geist-mono)`, 10px, `var(--text-tertiary)`
- All SVG text should use the CSS custom property font stacks
### Spacing
- Timeline axis positioned ~100-140px from left edge
- Role nodes offset ~80px right of the timeline axis
- Generous top/bottom padding (40-50px) for breathing room
- Skill nodes distributed in the remaining right-side space
## Technical Considerations
- **D3 version**: Use the existing `d3` import (already in `package.json`)
- **Skill**: All D3 rendering work should use the `d3-viz` skill for implementation
- **ResizeObserver**: For height synchronisation, observe the `.chronology-stream` element. The graph component needs a ref or selector to find it
- **Simulation re-init**: When dimensions change, the simulation should restart with new scales but preserve node positions where possible to avoid jarring jumps
- **Performance**: The simulation `tick` handler calls `setNodeButtonPositions` which triggers React re-renders. The current diffing logic should be preserved but tested under the new height-changing scenario
- **Module-level code**: The current component has module-level `window.matchMedia` calls (`prefersReducedMotion`, `supportsCoarsePointer`) — these are fine for SPA but would break SSR. Not a concern for this project, but worth noting
## Success Metrics
- The constellation graph visually matches the quality level of other dashboard components (cards, sidebar, topbar)
- Timeline year positions in the graph correspond to the vertical positions of the work experience cards in the adjacent column
- A visitor can hover between the timeline and the graph and immediately understand the connection
- The graph feels quiet and clean at rest, informative and precise on interaction
- No accessibility regressions — keyboard navigation and screen reader support maintained or improved
## Open Questions
- Should the "Last Consultation" (most recent role) card participate in the bidirectional highlighting, or only the accordion items in `WorkExperienceSubsection`? **Answer: Yes, it should participate (noted in US-006)**
- Should role nodes show the date range (e.g. "2024Present") below the short label, or keep it minimal? Consider during implementation — add if space permits without clutter
- Should skill nodes that appear in multiple roles show any visual indicator of "shared" status (e.g. a small count badge)? Defer to future iteration
-125
View File
@@ -1,125 +0,0 @@
# PRD: Chat Widget Polish & Model Updates
## Introduction
The semantic search and AI chat features are functionally complete (US-001 through US-010). This PRD covers four polish items: mobile full-screen chat experience, a welcome message with suggested questions, self-hosting the ONNX embedding model, and updating from Gemini 2.0 Flash to Gemini 3 Flash Preview.
## Goals
- Full-screen chat on mobile (<768px) for a better small-screen experience
- Welcome message with suggested question chips to reduce blank-state friction
- Self-host the ONNX model (`all-MiniLM-L6-v2`) to eliminate dependency on Hugging Face CDN
- Update Gemini model to `gemini-3-flash-preview` and show which model powers the chat
- Refresh system prompt while updating the model
## User Stories
### US-011: Mobile full-screen chat panel
**Description:** As a mobile visitor, I want the chat panel to be a full-screen overlay so it's easy to use on small screens.
**Acceptance Criteria:**
- [ ] Below `md` breakpoint (768px), chat panel renders as full-screen overlay (100vw x 100vh, or using `dvh` for mobile browser chrome)
- [ ] Full-screen mode has a visible header with close button
- [ ] Floating chat button is hidden while panel is open on mobile
- [ ] Above 768px, existing panel behavior unchanged (380px wide, anchored bottom-right)
- [ ] Smooth transition between open/closed states respects `prefers-reduced-motion`
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-012: Welcome message with suggested questions
**Description:** As a visitor opening the chat for the first time, I see a friendly welcome and clickable suggested questions so I know what to ask.
**Acceptance Criteria:**
- [ ] When chat panel opens and conversation is empty, display welcome message: "Hey! I'm here to help you learn more about Andy. What would you like to know?"
- [ ] Below the welcome message, show 2-3 clickable pill/chip buttons with suggested questions (e.g., "What's his NHS experience?", "Tell me about his data skills", "What projects has he built?")
- [ ] Clicking a suggested question sends it as a user message (same as typing and pressing Enter)
- [ ] Welcome message and chips are always visible when conversation is empty (persist across open/close if no messages sent)
- [ ] Once a message is sent, the welcome/chips area is replaced by the conversation
- [ ] Chips use design system tokens (teal accent border, hover state)
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-013: Self-host ONNX embedding model
**Description:** As a developer, I want the ONNX model files served from the same host as the site, so there's no runtime dependency on Hugging Face CDN.
**Acceptance Criteria:**
- [ ] Model files for `all-MiniLM-L6-v2` downloaded and placed in `public/models/all-MiniLM-L6-v2/` (or `public/models/onnx/` — whichever is cleaner)
- [ ] Files include at minimum: `onnx/model_quantized.onnx`, `tokenizer.json`, `tokenizer_config.json`, `config.json`
- [ ] `src/lib/embedding-model.ts` updated to load from local path instead of Hugging Face CDN
- [ ] Build-time embedding script (`scripts/generate-embeddings.ts`) also uses local model path
- [ ] `.gitignore` does NOT ignore the model files — they are committed as static assets
- [ ] Verify model loads correctly in browser (semantic search still works in command palette)
- [ ] Typecheck passes
### US-014: Update to Gemini 3 Flash Preview + model indicator
**Description:** As a developer, I want to use the latest free Gemini model, and as a visitor, I want to see what model powers the chat.
**Acceptance Criteria:**
- [ ] `GEMINI_API_BASE` in `src/lib/gemini.ts` updated from `gemini-2.0-flash` to `gemini-3-flash-preview`
- [ ] Review and update the system prompt for clarity (ensure it's well-structured for the new model)
- [ ] Review and update the response format instructions (the `[ITEMS: ...]` suffix pattern)
- [ ] Small text indicator in chat panel header or footer showing the model name (e.g., "Gemini 3 Flash" in `font-geist`, 11px, tertiary color)
- [ ] If the model string needs to change in future, it should be a single constant — not hardcoded in multiple places
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
## Functional Requirements
- FR-1: Chat panel below 768px uses full-screen overlay layout (`position: fixed; inset: 0`)
- FR-2: Chat button hidden when full-screen panel is open on mobile
- FR-3: Welcome message and suggested question chips shown when conversation is empty
- FR-4: Clicking a suggested question chip triggers the same flow as manually typing and sending
- FR-5: ONNX model files served from `public/models/` as static assets
- FR-6: `embedding-model.ts` configures Transformers.js to use local model path
- FR-7: Gemini API calls use `gemini-3-flash-preview` model
- FR-8: Chat UI displays model name indicator
## Non-Goals
- No changes to the command palette UI or semantic search ranking logic
- No persistent chat history across page loads
- No rate limiting or abuse prevention
- No changes to the boot/ECG/login flow
- No model fine-tuning or custom training
## Design Considerations
### Mobile Full-Screen Chat
- Full viewport with safe area insets (`env(safe-area-inset-*)`) for notched devices
- Header matches existing panel header style but full-width
- Input pinned to bottom, messages scroll above
### Welcome Message & Chips
- Welcome text styled as an AI message bubble (left-aligned, light background)
- Chips: small rounded pills with teal border, teal text on hover, `font-ui` 12-13px
- 2-3 chips arranged in a flex-wrap row below the welcome bubble
- Example questions: "What's his NHS experience?", "Tell me about his data skills", "What projects has he built?"
### Model Indicator
- Placed in the chat panel header, right-aligned or below the "Ask about Andy" title
- `font-geist`, 11px, `var(--text-tertiary)` color
- Format: "Powered by Gemini 3 Flash" or just "Gemini 3 Flash"
## Technical Considerations
### Self-Hosting ONNX Model
- Transformers.js supports a `localURL` or custom `env.localModelPath` configuration to redirect model loading from HF CDN to a local path
- The quantized model (`model_quantized.onnx`) is ~23MB — acceptable for a static deploy
- Files must be served with correct MIME types (`.onnx` as `application/octet-stream`)
- The build-time script and browser runtime must both point to the same model files
### Gemini Model Update
- `gemini-3-flash-preview` may have a different API path structure — verify against the Generative Language API docs
- The streaming SSE format should be identical across Flash models, but verify the response shape
## Success Metrics
- Mobile chat is comfortable to use on a phone-sized viewport (no overflow, no cropping)
- Suggested questions reduce "blank screen" hesitation — visitors engage faster
- ONNX model loads successfully from local path (no HF CDN requests in network tab)
- Chat responses come through on the new Gemini model with correct item references
## Open Questions
- Should the suggested question chips be configurable from a data file, or hardcoded in the component?
- Does `gemini-3-flash-preview` require a different API version path (`v1beta` vs `v1`)?
-237
View File
@@ -1,237 +0,0 @@
# PRD: Career Constellation Refinement
## Introduction
The career constellation graph needs visual and interaction refinements for better usability on large screens (1440p+), improved skill visibility, a more intuitive hover-based interaction model, and the addition of the full career + education timeline. The current implementation has overly aggressive opacity dimming on skills, too-small text at high resolutions, an unintuitive click-to-pin interaction, a constellation column that takes up too much horizontal space, and is missing early-career roles and education. Additionally, the work experience cards in the left column don't visually tie to their corresponding constellation nodes — they should use matching employer colours.
## Goals
- Improve readability of skill nodes and labels on large displays (1440p+)
- Reduce the constellation column width to ~35% giving work experience ~65% of horizontal space
- Replace click-to-highlight with hover-to-highlight on desktop, tap-to-highlight on mobile
- Replace the slide-out detail sidebar on mobile with in-place accordion expansion
- Scale all graph elements proportionally based on viewport width so the graph looks good from 1024px to 2560px+
- Add the full timeline: Duty Pharmacy Manager, Pre-Reg Pharmacist, UEA MPharm, and Highworth A-Levels
- Colour-match work experience cards to their constellation node employer colours
- Establish a consistent employer/institution colour scheme across the entire UI
## User Stories
### US-001: Increase default skill node visibility
**Description:** As a visitor, I want skill nodes to be more visible by default so I can see the full constellation without needing to interact.
**Acceptance Criteria:**
- [ ] Increase default skill circle `fill-opacity` from `0.2` to `0.35`
- [ ] Increase active (highlighted) skill circle `fill-opacity` from `0.85` to `0.9`
- [ ] Reduce the dimming of unconnected nodes when a role is highlighted — change from `opacity: 0.06` to `opacity: 0.15`
- [ ] Skill labels should be partially visible by default at `opacity: 0.5` (currently hidden at `0`), fully visible at `opacity: 1` when highlighted
- [ ] Link default `stroke-opacity` increased from `0.08` to `0.15` so the connection web is subtly visible
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser: skills should be recognisable at a glance without hovering
### US-002: Viewport-proportional scaling for large screens
**Description:** As a visitor on a 1440p or larger display, I want the constellation elements to scale up so they aren't tiny relative to the screen.
**Acceptance Criteria:**
- [ ] Compute a scale factor based on viewport width: `scaleFactor = Math.max(1, Math.min(1.6, viewportWidth / 1440))` (1.0x at 1440px, up to 1.6x at 2560px+)
- [ ] Apply scale factor to: `SKILL_RADIUS_DEFAULT` (7 -> up to ~11), `SKILL_RADIUS_ACTIVE` (11 -> up to ~18), `ROLE_WIDTH` (104 -> up to ~166), `ROLE_HEIGHT` (32 -> up to ~51)
- [ ] Base skill label `font-size` raised to 11px minimum (from 10px), then scales proportionally (up to ~18px at max scale)
- [ ] Base role label `font-size` raised to 12px minimum (from 11px), then scales proportionally (up to ~19px at max scale)
- [ ] Base year label `font-size` raised to 11px minimum (from 10px), then scales proportionally
- [ ] Scale padding, gaps, and force simulation parameters (charge, link distance, collision radius) proportionally
- [ ] Mobile breakpoint (`< 640px`) is unaffected — scaling only applies at `>= 1024px`
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser at 1440px and 2560px widths: elements should be clearly legible and well-proportioned
### US-003: Reduce constellation column width
**Description:** As a visitor, I want more horizontal space for work experience content so the chronology stream is easier to read.
**Acceptance Criteria:**
- [ ] Change `.pathway-columns` desktop grid from `minmax(0, 1.15fr) minmax(0, 1.5fr)` to `minmax(0, 1.85fr) minmax(0, 1fr)` (approximately 65/35 split)
- [ ] The constellation graph adapts to the narrower container without clipping or overflow
- [ ] Force simulation parameters still produce a clean, non-overlapping layout in the narrower space
- [ ] The timeline axis, role pills, and skill nodes remain fully visible
- [ ] Sticky positioning of the graph column still works correctly
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser: work experience column is visibly wider, graph is compact but readable
### US-004: Hover-to-highlight interaction on desktop
**Description:** As a desktop visitor, I want hovering over a role to highlight its connected skills, and hovering away to reset — without needing to click to toggle.
**Acceptance Criteria:**
- [ ] On desktop (fine pointer): hovering a role node highlights connected skills, shows their labels, and colorises links — same visual effect as current click behaviour
- [ ] Moving the mouse away from a role resets to the default state (all nodes visible at baseline opacity per US-001)
- [ ] Remove the click-to-pin toggle behaviour on desktop — clicking a role node should only trigger the detail action (e.g. expand accordion), not pin the highlight
- [ ] Hovering a skill node still highlights that skill and its connected roles
- [ ] The `pinnedNodeId` state is removed or only used for touch/keyboard
- [ ] Keyboard navigation (Tab + Enter) still works: focus highlights the node, Enter opens details
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser: hover on/off roles cycles highlight cleanly with no "stuck" states
### US-005: Tap-to-highlight with accordion expansion on mobile
**Description:** As a mobile visitor, I want tapping a role to highlight its skills and expand role details in-place, rather than opening a side panel.
**Acceptance Criteria:**
- [ ] On touch devices (coarse pointer): first tap on a role highlights its connected skills (same visual as desktop hover)
- [ ] First tap also expands an accordion panel below the constellation graph showing the role's condensed details: title, organisation, date range, and top 3 key achievements
- [ ] The accordion includes a "Show more" link/button to reveal the full set of achievements and details
- [ ] Tapping a different role switches the highlight and accordion content (auto-collapses "show more" back to summary)
- [ ] Tapping the same role again (or tapping elsewhere) collapses the accordion and resets highlights
- [ ] The accordion uses the same expand/collapse animation pattern as other tiles (height-only, 200ms ease-out)
- [ ] No slide-out sidebar panel on mobile — detail content lives in the accordion below the graph
- [ ] Tapping a skill node highlights it and shows a brief skill tooltip or label, does not open a panel
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser at mobile viewport: tap role -> accordion expands, tap again -> collapses
### US-006: Add Duty Pharmacy Manager role (Aug 2016 Oct 2017)
**Description:** As a visitor, I want to see the Duty Pharmacy Manager role in the constellation so the full career timeline is represented.
**Acceptance Criteria:**
- [ ] Add role node to `constellation.ts`: id `duty-pharmacy-manager-2016`, label "Duty Pharmacy Manager", shortLabel "Duty Pharm Mgr", organisation "Tesco PLC", startYear 2016, endYear 2017, orgColor `#E53935` (Tesco red)
- [ ] Add role-skill mapping with these skill connections: `medicines-optimisation` (0.8), `data-analysis` (0.5), `excel` (0.6), `change-management` (0.5), `stakeholder-engagement` (0.4)
- [ ] Add corresponding links to `constellationLinks` array
- [ ] Add consultation entry to `consultations.ts` with title "Duty Pharmacy Manager", org "Tesco PLC", dates Aug 2016 Oct 2017, location "Great Yarmouth, Norfolk", and key achievements from CV: service development leadership (NMS/asthma referrals), national clinical innovation (quality payments solution), clinical foundation building
- [ ] The role appears on the timeline in correct chronological position (between Pre-Reg and Pharmacy Manager)
- [ ] Screen reader description updates to include the new role
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser: new role node appears on timeline, hover highlights correct skills
### US-007: Add Pre-Registration Pharmacist role (Jul 2015 Jul 2016)
**Description:** As a visitor, I want to see the Pre-Registration Pharmacist role in the constellation as the earliest professional entry.
**Acceptance Criteria:**
- [ ] Add role node to `constellation.ts`: id `pre-reg-pharmacist-2015`, label "Pre-Registration Pharmacist", shortLabel "Pre-Reg", organisation "Paydens Pharmacy", startYear 2015, endYear 2016, orgColor `#66BB6A` (Paydens light green)
- [ ] Add role-skill mapping with these skill connections: `medicines-optimisation` (0.7), `change-management` (0.4), `stakeholder-engagement` (0.3)
- [ ] Add corresponding links to `constellationLinks` array
- [ ] Add consultation entry to `consultations.ts` with title "Pre-Registration Pharmacist", org "Paydens Pharmacy", dates Jul 2015 Jul 2016, location "Tunbridge Wells & Ashford, Kent", and key achievements from CV: clinical service expansion (PGDs for NRT, EHC, chlamydia), NMS audit improvement (under 10% to 50-60%), palliative care screening, operational learning
- [ ] The role appears on the timeline below Duty Pharmacy Manager
- [ ] Screen reader description updates to include the new role
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser: new role node appears on timeline, hover highlights correct skills
### US-008: Add University of East Anglia education node (2011 2015)
**Description:** As a visitor, I want to see the MPharm degree on the timeline as the foundation of the career.
**Acceptance Criteria:**
- [ ] Add node to `constellation.ts`: id `uea-mpharm-2011`, type `role` (treated the same as work roles on the timeline), label "MPharm (Hons) 2:1", shortLabel "MPharm", organisation "University of East Anglia", startYear 2011, endYear 2015, orgColor `#7B2D8E` (UEA purple — distinct education colour)
- [ ] Add role-skill mapping with foundational skill connections: `medicines-optimisation` (0.5), `data-analysis` (0.3)
- [ ] Add corresponding links to `constellationLinks` array
- [ ] Add consultation entry to `consultations.ts` with title "MPharm (Hons) 2:1", org "University of East Anglia", dates 2011 2015, location "Norwich", and key achievements: independent research project on drug delivery and cocrystals (75.1%, Distinction), 4th year OSCE 80%, President of UEA Pharmacy Society
- [ ] The node appears on the timeline below Pre-Reg Pharmacist
- [ ] Screen reader description updates to include the education entry
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser: education node appears on timeline with distinct purple colour
### US-009: Add Highworth Grammar School education node (2009 2011)
**Description:** As a visitor, I want to see A-Levels on the timeline as the earliest entry, showing the complete timeline from 2009 to present.
**Acceptance Criteria:**
- [ ] Add node to `constellation.ts`: id `highworth-alevels-2009`, type `role`, label "A-Levels: Maths A*, Chem B", shortLabel "A-Levels", organisation "Highworth Grammar School", startYear 2009, endYear 2011, orgColor `#9C27B0` (lighter purple — education colour family, distinct shade from UEA)
- [ ] Minimal skill connections: `data-analysis` (0.2) — reflects strong mathematics foundation
- [ ] Add corresponding link to `constellationLinks` array
- [ ] Add consultation entry to `consultations.ts` with title "A-Levels", org "Highworth Grammar School", dates 2009 2011, location "Ashford, Kent", and results: Mathematics A*, Chemistry B, Politics C
- [ ] The node appears at the very bottom of the timeline as the earliest entry
- [ ] Screen reader description updates to include the education entry
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser: A-Levels node appears at bottom of timeline
### US-010: Unify employer/institution colour scheme
**Description:** As a visitor, I want each employer/institution to have a distinct, consistent colour so I can visually group entries by organisation.
**Acceptance Criteria:**
- [ ] NHS Norfolk & Waveney ICB roles use NHS blue `#005EB8` (already correct — Deputy Head, Interim Head, High-Cost Drugs)
- [ ] Tesco PLC roles use Tesco red `#E53935` — update existing Pharmacy Manager node `orgColor` from `#00897B` to `#E53935` in both `constellation.ts` and `consultations.ts`
- [ ] Paydens Pharmacy uses light green `#66BB6A` for Pre-Registration Pharmacist
- [ ] University of East Anglia uses UEA purple `#7B2D8E`
- [ ] Highworth Grammar School uses lighter purple `#9C27B0`
- [ ] The domain legend below the graph is unchanged (Technical/Clinical/Leadership) — institution colours only appear on role/education pill nodes
- [ ] All role pills remain legible with appropriate text contrast against the coloured fill/stroke
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser: five distinct institution colour groups are immediately recognisable
### US-011: Colour-match work experience cards to constellation nodes
**Description:** As a visitor, I want the work experience cards in the left column to use matching colours from their constellation nodes, creating a visual link between the card list and the graph.
**Acceptance Criteria:**
- [ ] The dot indicator on each work experience card uses the consultation's `orgColor` instead of hardcoded `#0D6E6E`
- [ ] The expanded card's left border uses the consultation's `orgColor` instead of `var(--accent)`
- [ ] The bullet point dots in the expanded detail use the consultation's `orgColor` (at 0.5 opacity, matching current pattern) instead of `var(--accent)`
- [ ] The coded entry tags use the consultation's `orgColor` for text colour and a lightened variant for background/border (same pattern as current teal, but using the org colour)
- [ ] The "View full record" link uses the consultation's `orgColor` instead of `var(--accent)`
- [ ] The highlight background when a card is highlighted from the graph uses a tinted version of `orgColor` at low opacity (e.g. `rgba(r,g,b,0.03)`) instead of hardcoded `rgba(10,128,128,0.03)`
- [ ] The hover/expanded border colour uses a border variant of the `orgColor` instead of `var(--accent-border)`
- [ ] The CardHeader dot for "WORK EXPERIENCE" section title remains teal (it's the section accent, not per-card)
- [ ] All colour changes maintain WCAG AA contrast ratios for text legibility
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser: NHS roles show blue-tinted cards, Tesco roles show red-tinted cards, Paydens shows green, education shows purple
### US-012: Re-tune force simulation for 8 timeline entries
**Description:** As a developer, I need the force simulation to produce a clean layout with 8 entries (6 roles + 2 education) spanning 20092025, in the narrower column.
**Acceptance Criteria:**
- [ ] The y-scale range accommodates 8 entries (20092025) without excessive cramping
- [ ] Timeline year labels show the full range from 2009 to 2025
- [ ] Role/education nodes don't overlap each other on the timeline
- [ ] Skill nodes distribute cleanly in the available space to the right of role pills
- [ ] Adjust charge, collision, and link forces if needed to prevent overlapping with the additional nodes and narrower space
- [ ] Links don't create an unreadable tangle — connections remain traceable
- [ ] Education nodes at the bottom (20092015) have fewer skill connections so the lower portion isn't cluttered
- [ ] The graph still works at mobile viewport widths with 8 entries
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser at both desktop and mobile: all 8 entries visible, no overlaps, clean layout
## Functional Requirements
- FR-1: Skill node default opacity increased to 0.35 with labels at 0.5 opacity
- FR-2: All graph element sizes (nodes, text, padding) scale proportionally with viewport width from 1024px to 2560px+
- FR-3: Grid layout changes to ~65/35 split favouring the work experience column
- FR-4: Desktop interaction model is hover-only (no click-to-pin for highlighting)
- FR-5: Mobile interaction model is tap-to-highlight with accordion expansion for details (no sidebar panel)
- FR-6: Four new timeline entries added: Duty Pharmacy Manager, Pre-Reg Pharmacist, UEA MPharm, Highworth A-Levels
- FR-7: Force simulation produces a clean layout with 8 entries in a narrower column
- FR-8: All accessibility features (keyboard navigation, screen reader descriptions, focus management) continue to work with 8 entries
- FR-9: Institution colour scheme: NHS blue `#005EB8`, Tesco red `#E53935`, Paydens green `#66BB6A`, UEA purple `#7B2D8E`, Highworth purple `#9C27B0`
- FR-10: Minimum font size of 11px for all text in the constellation graph on desktop
- FR-11: Work experience cards use matching `orgColor` for dot, border, bullets, coded entries, and links
- FR-12: Timeline spans 20092025 showing the complete career + education journey
## Non-Goals
- No changes to the boot sequence, ECG animation, or login screen
- No new skill nodes — only new role/education nodes and links to existing skills
- No changes to the domain legend below the graph (Technical/Clinical/Leadership categories)
- No changes to the mobile stacked layout breakpoint (stays at 1024px)
- No changes to skill detail panel or RepeatMedicationsSubsection
## Design Considerations
- The constellation is getting denser with 8 entries. The narrower column means less horizontal space for skill distribution. The force simulation tuning (US-012) is critical to prevent visual clutter.
- Education entries at the bottom of the timeline (20092015) have deliberately few skill connections (2 for UEA, 1 for Highworth) to keep the lower portion clean and visually suggest the "before specialisation" phase.
- The proportional scaling should feel natural — not just "zoomed in" but properly scaled with appropriate spacing.
- The accordion on mobile (US-005) should match the existing tile expansion pattern — summary first (top 3 achievements), "show more" for full detail.
- **Institution colours**: NHS blue, Tesco red, Paydens green, and purple shades for education. The purple family groups both education entries while using different shades to distinguish them. Role pill text uses the institution colour for both the label and the border/fill tint.
- **Card colour matching**: The work experience cards should feel like they "belong to" their constellation node. The colour theming should be subtle — tinted backgrounds, coloured dots and borders — not overwhelming. Think: the current teal treatment but swapped per employer.
## Technical Considerations
- The scale factor computation should happen once per resize, not per render tick
- Force simulation parameters (charge, link distance, collision radius) all need to scale with the viewport factor
- The existing `chronologyRef` + `ResizeObserver` pattern for height matching should continue to work
- The accordion component for mobile detail expansion can reuse existing expand/collapse patterns from `WorkExperienceSubsection`
- Adding 4 entries increases total nodes from 25 to 29 and links from 46 to ~57
- The `Consultation` type in `types/pmr.ts` may need updating if education entries need different fields (e.g. no `codedEntries`, different `examination` semantics) — or education entries can use the same shape with adapted content
- For the card colour matching (US-011), a utility function to derive light/border variants from a hex colour would avoid hardcoding multiple CSS variable overrides per employer. Something like `hexToRgba(color, opacity)` for backgrounds and borders
- The `orgColor` already exists on the `Consultation` type — it just isn't used in `WorkExperienceSubsection.tsx` yet
## Success Metrics
- All skill labels legible at 1440p without squinting
- Hover-to-highlight feels instant and responsive (no stuck states)
- Work experience column has noticeably more reading space
- The full timeline (2009present) is represented with education and career
- Employer/institution colour coding is immediately recognisable across cards and graph
- No visual overlaps or clipping at any supported viewport width
## Open Questions
- None — all decisions resolved.
-193
View File
@@ -1,193 +0,0 @@
# PRD: Dashboard Restructure — Section Hierarchy & Graph Improvements
## Introduction
Restructure the dashboard from a flat list of independent tiles into a hierarchical layout with two parent sections: **Patient Summary** (containing Latest Results as a subsection) and **Patient Pathway** (containing the constellation graph, last consultation, work experience, skills, and education). Improve the constellation graph's visual clarity, add cross-component hover highlighting, remove CV data that doesn't match the source CV, and explore typography options for parent section headers.
## Goals
- Consolidate related content into two clear parent sections with visually distinct subsections
- Improve the constellation graph's readability (background, line weight, node size, zoom)
- Add interactive hover-highlighting between experience/skills and the graph
- Ensure all career data matches `References/CV_v4.md` exactly
- Find the best typography treatment for parent section headers
- Maintain responsive behaviour across all breakpoints
## User Stories
### US-001: Create Parent Section component with subsection support
**Description:** As a developer, I need a parent section wrapper that visually distinguishes parent tiles from child subsections so the hierarchy is clear.
**Acceptance Criteria:**
- [ ] Parent section header text is significant and prominent — at least as large as the current KPI value text (which uses ~36px), not the small 12px uppercase tile headers
- [ ] Child subsections have their own smaller headers but are clearly nested within the parent
- [ ] Visual separation between subsections (divider or spacing) without looking like independent cards
- [ ] Typecheck passes (`npm run typecheck`)
- [ ] Verify in browser using Playwright
### US-002: Restructure Patient Summary as parent section
**Description:** As a visitor, I want the Patient Summary to contain the personal profile and Latest Results (KPIs) as a subsection, so related overview information is grouped together.
**Acceptance Criteria:**
- [ ] Patient Summary is a parent section with large, prominent header text
- [ ] Remove the 4 headline metric figures currently in Patient Summary (9+ Years, 1.2M, £220M, £14.6M+)
- [ ] Latest Results (KPI flip cards) appears as a named subsection within Patient Summary
- [ ] Profile text remains in Patient Summary above the Latest Results subsection
- [ ] KPI cards retain their existing click-to-detail behaviour
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright
### US-003: Rename Career Activity to Patient Pathway and restructure as parent section
**Description:** As a visitor, I want all career-related content (graph, latest role, experience, skills, education) grouped under one "Patient Pathway" parent section.
**Acceptance Criteria:**
- [ ] Section renamed from "CAREER ACTIVITY" to "PATIENT PATHWAY"
- [ ] Patient Pathway is a parent section with large, prominent header text (matching Patient Summary)
- [ ] Contains the constellation graph at the top
- [ ] Last Consultation content appears as a subsection below the graph
- [ ] Education appears as a subsection at the bottom (including A-Levels)
- [ ] The old standalone LastConsultationTile is removed from the dashboard
- [ ] The old standalone EducationTile is removed from the dashboard
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright
### US-004: Two-column layout for experience and skills within Patient Pathway
**Description:** As a visitor, I want to see work experience on the left and skills on the right beneath the graph/consultation, so I can see how they relate.
**Acceptance Criteria:**
- [ ] Two-column grid below the consultation subsection: experience (left), skills (right)
- [ ] Work experience column shows all roles from consultations.ts with accordion expand (one at a time)
- [ ] Skills column shows the existing CoreSkillsTile content (categorised skills with expand)
- [ ] On mobile, columns stack vertically (experience above skills)
- [ ] Both columns are subsections with their own headers
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright
### US-005: Hover-highlighting between experience/skills and constellation graph
**Description:** As a visitor, I want to hover over a work experience entry or skill and see the corresponding node highlighted in the constellation graph, so I can visually trace relationships.
**Acceptance Criteria:**
- [ ] Hovering a work experience entry highlights its role node in the graph (and connected skill nodes)
- [ ] Hovering a skill entry highlights its skill node in the graph (and connected role nodes)
- [ ] Non-related nodes dim (matching existing graph hover behaviour)
- [ ] Highlight clears when mouse leaves the entry
- [ ] Touch devices: tap to highlight, tap elsewhere to clear
- [ ] No performance issues (highlight should feel immediate)
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright
### US-006: Improve constellation graph visual clarity
**Description:** As a visitor, I want the constellation graph to be clearer and easier to read, with better contrast and larger interactive elements.
**Acceptance Criteria:**
- [ ] Graph has an off-white/light background (e.g. `#F5F7F6` or similar warm neutral) instead of transparent
- [ ] Link lines are more visible — slightly thicker stroke and/or higher contrast colour
- [ ] Node bubbles are larger (increase radius for both role and skill nodes)
- [ ] Graph is initially zoomed in to a comfortable level so nodes and labels are clearly readable
- [ ] Graph remains interactive (hover, click behaviour preserved)
- [ ] Responsive sizing still works across breakpoints
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright
### US-007: Remove inaccurate CV data and fix entries
**Description:** As Andy, I want the portfolio to only contain career entries that match my actual CV so there is no fabricated content.
**Acceptance Criteria:**
- [ ] Remove "Duty Pharmacy Manager" consultation entry (`duty-pharmacist-2016`) — this role is not in the CV
- [ ] Remove "Power BI Data Analyst Associate" certification — not in the CV
- [ ] Remove "Clinical Pharmacy Diploma" certification — not in the CV (CV lists "NHS Leadership Academy — Mary Seacole Programme" instead)
- [ ] Add "NHS Leadership Academy — Mary Seacole Programme (2018)" as a certification entry
- [ ] Update constellation graph nodes/links to remove references to the deleted role
- [ ] Remove "SQL Analytics Transformation" project entry — not in the CV as a standalone project
- [ ] Convert "Budget Oversight" from a project entry to a skill entry under Budget Management (already exists in skills.ts — just remove the project timeline entry)
- [ ] Add A-Levels to education: Mathematics (A*), Chemistry (B), Politics (C) — Highworth Grammar School, 20092011
- [ ] Verify remaining entries (4 roles, GPhC registration, MPharm, Mary Seacole, A-Levels) match CV dates and titles exactly
- [ ] Typecheck passes
### US-008: Remove old standalone tiles from dashboard
**Description:** As a developer, I need to clean up tiles that have been absorbed into parent sections so there are no duplicates.
**Acceptance Criteria:**
- [ ] LastConsultationTile removed from DashboardLayout grid (content now inside Patient Pathway)
- [ ] CoreSkillsTile removed from DashboardLayout grid (content now inside Patient Pathway)
- [ ] LatestResultsTile removed from DashboardLayout grid (content now inside Patient Summary)
- [ ] EducationTile removed from DashboardLayout grid (content now inside Patient Pathway)
- [ ] Old standalone tile components deleted from codebase
- [ ] No visual gaps or broken grid layout after removal
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright
### US-009: Explore parent header typography options
**Description:** As a designer, I want to evaluate different font choices and sizes for the parent section headers to find the most visually striking and appropriate treatment.
**Acceptance Criteria:**
- [ ] Use the `bencium-innovative-ux-designer` skill to evaluate font options for the parent headers
- [ ] Test parent headers with existing project fonts: Elvaro Grotesque (various weights 300-900) and Blumir (variable 100-700)
- [ ] Consider whether a different weight/style of the existing fonts creates sufficient visual impact at large sizes
- [ ] Headers must feel premium and intentional — not generic. Should complement the clinical/luxury aesthetic
- [ ] Produce at least 2-3 options with screenshots for comparison
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright
**Note:** This story should be done AFTER US-001 through US-004 are complete (layout must be in place first so fonts can be evaluated in context).
### US-010: Apply chosen parent header typography
**Description:** As a developer, I need to apply the selected font treatment to all parent section headers consistently.
**Acceptance Criteria:**
- [ ] Chosen font/weight/size applied to Patient Summary and Patient Pathway headers
- [ ] Headers are consistent in treatment across both parent sections
- [ ] Font scales appropriately across breakpoints (may need responsive size adjustments)
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright
**Note:** This story depends on US-009 (font exploration) being completed and a choice being made.
## Functional Requirements
- FR-1: Patient Summary parent section contains: profile text + Latest Results subsection (KPI cards)
- FR-2: Patient Pathway parent section contains: constellation graph + Last Consultation subsection + two-column (experience | skills) subsection + Education subsection
- FR-3: Parent section headers use large, prominent typography — at minimum the size of current KPI values (~36px) — clearly establishing them as top-level sections
- FR-4: Constellation graph renders with off-white background, thicker/clearer links, and larger node radius
- FR-5: Graph is initially zoomed/scaled so content fills the container at a comfortable reading size
- FR-6: Hovering an experience or skill entry triggers the graph's existing highlight behaviour for the corresponding node
- FR-7: Experience entries maintain accordion-expand behaviour (single open at a time)
- FR-8: All career data matches `References/CV_v4.md` — no fabricated roles, certifications, or projects
- FR-9: Responsive: two-column experience/skills grid stacks to single column on mobile; graph height adjusts per existing breakpoints
- FR-10: Rename "CAREER ACTIVITY" to "PATIENT PATHWAY" throughout
- FR-11: Education subsection includes MPharm, Mary Seacole Programme, and A-Levels
- FR-12: Parent header font treatment is evaluated using the frontend-design skill before final implementation
## Non-Goals
- No changes to boot sequence, ECG animation, or login screen
- No changes to the Sidebar (person header, tags, alerts)
- No changes to the TopBar or Command Palette
- No new data fields or API integrations
- No changes to the DetailPanel component or its content
- No changes to existing colour palette (only typography adjustments for parent headers)
## Design Considerations
- Parent section headers must be a significant visual element — they are the primary structural markers of the page. Think "section title" not "card label". At minimum ~36px (matching KPI values), potentially larger.
- Subsection headers should retain the current style (small, uppercase, coloured dot) so the hierarchy is clear through contrast
- The two-column experience/skills layout within Patient Pathway should feel like a natural part of the same card, not two separate cards jammed together
- Graph background should be subtle enough not to compete with card backgrounds — a very light warm neutral
- Font exploration (US-009) should use the `bencium-innovative-ux-designer` skill to evaluate options — the headers need to feel premium, not just "big text"
- Typography evaluation must happen with the layout already in place so the fonts can be judged in their real context
## Technical Considerations
- The hover-highlighting (US-005) requires lifting hover state up: experience/skills items need to communicate hovered IDs to CareerConstellation via shared state (React context or prop drilling through the parent)
- CareerConstellation already supports external highlight via its existing hover logic — this needs to be made controllable from outside (imperative ref or controlled prop)
- Removing `duty-pharmacist-2016` from consultations.ts will affect constellation.ts nodes/links — both must be updated together
- The `buildTimeline()` function in CareerActivityTile will be replaced by the new structure; the work experience list should read directly from consultations.ts
- US-009 and US-010 are sequentially dependent on US-001US-004 completing first
## Success Metrics
- All career data verifiably matches CV_v4.md
- Graph nodes and labels are readable without zooming on a 1920x1080 desktop viewport
- Hover highlighting connects experience/skills to graph nodes with no perceptible delay
- Parent section headers are immediately recognisable as top-level structural elements
- Dashboard passes visual check at desktop (1280px+), tablet (768px), and mobile (375px) widths
-197
View File
@@ -1,197 +0,0 @@
# PRD: Improve LLM CV Knowledge Accuracy
## Introduction
The portfolio's AI chat gives inaccurate or shallow answers about Andy's work history. The root cause: the system prompt feeds `buildEmbeddingTexts()` summaries rather than the full CV detail. Questions about specific achievements, methodology, clinical specialties, or cross-role context produce vague or incorrect responses. This PRD defines an iterative improvement process: enrich the LLM's context, measure accuracy against 10 verifiable benchmark questions, and repeat until all pass — while ensuring changes are structural (not question-specific hacks).
Additionally, the LLM provider is changing from Gemini to **OpenRouter** using the **z-ai/glm-5** model. This requires migrating the API integration, renaming the module, and updating the benchmark harness to use the new provider.
## Goals
- Achieve 10/10 accuracy on benchmark questions with factually correct, detailed, citation-worthy answers
- Ensure improvements are structural — benefiting all possible queries, not just the 10 benchmarks
- Maintain the existing architecture (no new APIs beyond OpenRouter, no RAG infrastructure, no backend)
- Migrate from Gemini to OpenRouter (z-ai/glm-5) for both production chat and benchmark scoring
- Regenerate embeddings when embedding texts change, keeping search and LLM context in sync
## Benchmark Questions
These 10 questions have verifiable answers from CV_v4.md and the structured data files. Each tests a different knowledge gap.
| # | Question | Expected Answer (summary) | Tests |
|---|----------|--------------------------|-------|
| Q1 | "How many years has Andy been employed by the NHS?" | ~3.5 years (May 2022present). Tesco was private sector. | NHS vs non-NHS employer distinction |
| Q2 | "What was Andy's involvement with tirzepatide?" | Supported NICE TA1026 commissioning, authored executive paper advocating primary care model, drove GP-led delivery. | Deep role-specific detail |
| Q3 | "What specific tools and software has Andy built?" | 5 projects: switching algorithm, Blueteq generator, CD monitoring, Sankey tool, PharMetrics. Each with outcomes. | Cross-role aggregation |
| Q4 | "What were Andy's A-level subjects and grades?" | Maths A*, Chemistry B, Politics C. Highworth Grammar School, 20092011. | Specific education detail |
| Q5 | "Was Andy's Tesco role part of the NHS?" | No. Tesco PLC is private. Community pharmacy, not NHS employment. LPC representative for Norfolk. | Employer classification |
| Q6 | "How did the patient switching algorithm work?" | Python, real-world GP data, auto-identified patients for alternatives, 3 days vs months manual, 14,000 patients, £2.6M, novel GP payment system. | Methodology depth |
| Q7 | "What clinical specialties has Andy worked across?" | Rheumatology, ophthalmology (wet AMD, DMO, RVO), dermatology, gastroenterology, neurology, migraine — from high-cost drugs role. | Narrative detail not in bullet summaries |
| Q8 | "What is Andy's experience with the dm+d?" | Created comprehensive medicines data table integrating all dm+d products with standardised strengths, morphine equivalents, Anticholinergic Burden scoring — single source of truth. | Technical achievement context |
| Q9 | "What budget does Andy manage and how?" | £220M prescribing budget. Forecasting models, variance analysis, financial reporting to executive team, interactive expenditure dashboard. | Figure + methodology |
| Q10 | "What leadership training does Andy have?" | Mary Seacole Programme (2018, 78%). Also national induction programme at Tesco, NVQ3 supervision. | Cross-role synthesis |
### Scoring Criteria
Each question scored 02:
- **0 — Incorrect**: Wrong facts, invented detail, or contradicts CV
- **1 — Partial**: Correct but missing key detail, or vague where specifics are available
- **2 — Accurate**: Factually correct, appropriately detailed, cites specific achievements/metrics
**Pass threshold**: 18/20 (90%), with no question scoring 0.
### Anti-Benchmaxing Rules
- No hardcoded answers or question-specific prompt clauses
- Every change must be a structural improvement (richer context, better prompt patterns, enriched embeddings)
- After each iteration, mentally evaluate: "Would this help a question NOT in the benchmark?" — if no, reject the change
- The system prompt must not reference benchmark questions or their specific phrasings
## User Stories
### US-001: Migrate production chat from Gemini to OpenRouter
**Description:** As a developer, I need to replace the Gemini API integration with OpenRouter so the chat uses z-ai/glm-5.
**Acceptance Criteria:**
- [ ] Rename `src/lib/gemini.ts``src/lib/llm.ts`
- [ ] Update all imports across the codebase (`ChatWidget.tsx`, `search.ts`, etc.)
- [ ] Replace Gemini API calls with OpenRouter's OpenAI-compatible API (`https://openrouter.ai/api/v1/chat/completions`)
- [ ] Model set to `z-ai/glm-5`
- [ ] API key read from `VITE_OPEN_ROUTER_API_KEY` env var
- [ ] SSE streaming still works (OpenRouter supports `stream: true`)
- [ ] System prompt and message format adapted to OpenAI chat completions format (`messages` array with `role`/`content`)
- [ ] Export updated display name constant (e.g., `LLM_DISPLAY_NAME = 'GLM-5'`) and update model indicator in chat UI
- [ ] Rename `isGeminiAvailable()``isLLMAvailable()` (or similar)
- [ ] Typecheck passes
- [ ] **Verify in browser**: chat opens, sends a message, streams a response
### US-002: Migrate benchmark script to OpenRouter
**Description:** As a developer, I need the benchmark harness to use OpenRouter so it tests the same model and prompt path as production.
**Acceptance Criteria:**
- [ ] `scripts/benchmark.ts` uses OpenRouter API instead of Gemini
- [ ] API key read from `VITE_OPEN_ROUTER_API_KEY` (loaded from `.env`)
- [ ] Request format uses OpenAI chat completions structure
- [ ] Model identifier set to `z-ai/glm-5`
- [ ] Rate limit/retry logic updated for OpenRouter's error responses
- [ ] Scoring calls also use OpenRouter (same provider for all LLM calls)
- [ ] `npm run benchmark` still works end-to-end
- [ ] Typecheck passes
### US-003: Enrich system prompt with full CV context
**Description:** As a portfolio visitor, I want the AI to have comprehensive knowledge of Andy's background so it can answer detailed questions accurately.
**Acceptance Criteria:**
- [ ] System prompt includes full professional profile narrative (from CV_v4.md profile section)
- [ ] Each role includes full achievement bullets, not just summaries
- [ ] Clear distinction between NHS employment (May 2022+) and private sector (Tesco)
- [ ] Clinical specialties, methodology details, and specific outcomes included
- [ ] Education includes specific grades, subjects, research topics
- [ ] Prompt is well-structured with clear sections for easy LLM parsing
- [ ] No invented or extrapolated content — everything sourced from CV_v4.md and data files
- [ ] Typecheck passes
### US-004: Improve system prompt instructions
**Description:** As a portfolio visitor, I want the AI to use its knowledge effectively — citing specifics, distinguishing between employers, and aggregating across roles when asked.
**Acceptance Criteria:**
- [ ] Prompt instructs LLM to distinguish NHS employment from private sector roles
- [ ] Prompt instructs LLM to aggregate across roles when asked broad questions (e.g., "what tools has Andy built?")
- [ ] Prompt instructs LLM to cite specific metrics, dates, and outcomes when available
- [ ] Temperature and token limits are appropriate for detailed answers (review current 0.7 temp, 512 max tokens)
- [ ] Typecheck passes
### US-005: Enrich embedding texts for semantic search
**Description:** As a portfolio visitor, I want semantic search to surface relevant results even for nuanced queries so the chat and command palette find the right content.
**Acceptance Criteria:**
- [ ] `buildEmbeddingTexts()` generates richer text per item — full achievement narratives, methodology detail, clinical specialties
- [ ] Role `history` narratives are included (currently only `examination` bullets and `codedEntries`)
- [ ] Cross-references included where items relate (e.g., CD monitoring links to controlled drugs skill)
- [ ] Embedding texts remain well-formed natural language (not keyword soup)
- [ ] Typecheck passes
### US-006: Regenerate embeddings
**Description:** As a developer, I need embeddings regenerated whenever embedding texts change so semantic search results match the enriched content.
**Acceptance Criteria:**
- [ ] Embeddings regenerated using the same model (all-MiniLM-L6-v2)
- [ ] Output written to `src/data/embeddings.json`
- [ ] Number of embeddings matches number of palette items
- [ ] Regeneration can be triggered via script (`npm run generate-embeddings` or similar)
- [ ] Typecheck passes
### US-007: Iterative benchmark loop
**Description:** As a developer, I want to run the benchmark, review scores, make improvements, and repeat until the pass threshold is met.
**Acceptance Criteria:**
- [ ] Run benchmark → review scores → identify failing questions → make structural improvements → repeat
- [ ] Each iteration logged with: changes made, scores before/after, rationale
- [ ] Minimum 2 iterations, maximum 10
- [ ] Stop when 18/20 achieved with no question scoring 0
- [ ] Final iteration results saved as evidence
- [ ] All changes pass typecheck before benchmarking
### US-008: Validate no regression on general queries
**Description:** As a portfolio visitor, I want the AI to still handle general questions well after the benchmark-focused improvements.
**Acceptance Criteria:**
- [ ] Test 5 general questions not in the benchmark (e.g., "Tell me about Andy", "What does Andy do?", "How can I contact Andy?", "What is this website?", "What are Andy's strongest skills?")
- [ ] All general questions produce sensible, accurate responses
- [ ] No degradation in response quality for broad queries
- [ ] System prompt size hasn't grown to a point that degrades response speed noticeably
## Functional Requirements
- FR-1: Production chat must use OpenRouter API with model `z-ai/glm-5`
- FR-2: API key sourced from `VITE_OPEN_ROUTER_API_KEY` environment variable
- FR-3: LLM module renamed from `gemini.ts` to `llm.ts` with updated exports
- FR-4: Chat UI displays "GLM-5" as the model indicator (replacing "Gemini 3 Flash")
- FR-5: Benchmark harness must use the identical system prompt construction path as production (`buildSystemPrompt()` from `llm.ts`)
- FR-6: System prompt changes must be made in `llm.ts` and/or `search.ts` — the same files that serve production
- FR-7: Embedding text changes must be in `buildEmbeddingTexts()` in `search.ts`
- FR-8: Scoring must be automated via LLM (OpenRouter), not manual review
- FR-9: All benchmark artifacts (questions, expected answers, results) stored in `scripts/`
- FR-10: Embedding regeneration must produce deterministic output for the same input texts
- FR-11: System prompt must remain a single self-contained context block (no external retrieval at runtime)
## Non-Goals
- No RAG infrastructure or vector database
- No additional API integrations beyond OpenRouter
- No changes to the chat UI layout, streaming UX, or item linking (beyond model name display)
- No changes to the command palette search UX
- No changes to boot sequence, ECG, or login phases
- No new backend or server-side components
- Not optimising for adversarial/trick questions — focus is on legitimate CV queries
- No keeping Gemini as a fallback — this is a full replacement
## Technical Considerations
- **OpenRouter API format**: Uses OpenAI-compatible chat completions endpoint (`POST https://openrouter.ai/api/v1/chat/completions`). Messages use `{ role: 'system' | 'user' | 'assistant', content: string }` format. Streaming uses `stream: true` with SSE `data:` lines containing `choices[0].delta.content`.
- **Authentication**: `Authorization: Bearer <VITE_OPEN_ROUTER_API_KEY>` header. Include `HTTP-Referer` and `X-Title` headers as recommended by OpenRouter.
- **Rate limits**: OpenRouter has per-model rate limits. Add retry logic for 429 responses. The benchmark script should include delays between calls.
- **Embedding regeneration**: Needs Node.js script that loads the ONNX model and processes all texts. Existing `scripts/generate-embeddings` script should be reused.
- **Temperature**: Current 0.7 may introduce variability in answers. Consider lowering to 0.30.5 for more consistent factual responses. Benchmark both.
- **Max tokens**: Current 512 may truncate detailed answers. Consider increasing to 768 or 1024 for benchmark testing.
- **Prompt structure**: Well-structured prompts with clear headings/sections parse better for LLMs than flat text. Consider markdown structure in system prompt.
- **CORS**: OpenRouter supports browser-side calls. The existing client-side fetch pattern should work without changes.
## Success Metrics
- 18/20 or higher on benchmark (90%+ accuracy)
- No question scores 0 (no factual errors)
- 5/5 general validation questions pass
- System prompt remains under 8KB
- No typecheck or lint regressions
- Embedding regeneration completes without errors
- Chat streaming works in-browser with OpenRouter
## Resolved Questions
- **Model provider**: OpenRouter with z-ai/glm-5 (replaces Gemini 3 Flash).
- **File naming**: `gemini.ts` renamed to `llm.ts` for provider-agnostic naming.
- **Benchmark provider**: OpenRouter used for both chat answers and scoring (single provider).
- **Benchmark results are git-tracked.** Each iteration's scores are committed so improvement over time is visible and auditable.
- **Existing `scripts/generate-embeddings` script exists.** Review and adapt as needed rather than building from scratch.
- **Benchmark harness is permanent.** Kept as an ongoing regression test (`npm run benchmark`) for validating LLM accuracy after any data or prompt changes. Question set can be expanded over time.
-183
View File
@@ -1,183 +0,0 @@
# PRD: Login Screen Logo & Blur Refinements
## Introduction
Refine the login screen's CVMIS logo animation and backdrop blur to match the quality of the original Remotion reference, and align the login screen's visual details with the dashboard's design system. The logo and branding text need to be larger (the logo + title + subtitle block should occupy ~50% of the login card's height), the fan animation needs tunable timing variables, overlapping capsules need a multiply-blend effect on their intersection areas, the backdrop blur should cover the full dashboard (including TopBar) at reduced intensity, and several visual inconsistencies between the login screen and the dashboard should be resolved.
## Goals
- Scale logo + branding text so the branding block is ~50% of the login card height
- Increase branding text ("CVMIS" title and "CV Management Information System" subtitle) to match dashboard-level typography
- Add configurable animation timing constants for easy tuning of rise/fan speeds
- Implement CSS `mix-blend-mode: multiply` overlap effect matching the Remotion reference
- Extend backdrop blur to cover TopBar and all dashboard content uniformly
- Reduce blur intensity by ~50% (from 20px to ~10px)
- Align login card border radius, shadow, and hardcoded colors with dashboard design tokens
- Fix minor typography weight and sizing inconsistencies
## User Stories
### US-001: Scale logo and branding block to ~50% of login card height
**Description:** As a visitor, I want the CVMIS logo and branding text to be larger and more prominent, occupying roughly half the login card's height so the brand has real visual presence.
**Acceptance Criteria:**
- [ ] Logo height scaled up ~22.5x from current `clamp(80px, 8vw, 120px)` — target approximately `clamp(160px, 18vw, 280px)` (tune visually for balance)
- [ ] Width scales proportionally (maintains aspect ratio via SVG viewBox)
- [ ] The branding block (logo + "CVMIS" title + "CV Management Information System" subtitle + spacing) occupies approximately 50% of the total login card height
- [ ] Logo does not overflow or clip on mobile viewports (≥375px wide)
- [ ] Typecheck passes
- [ ] **Verify in browser using Playwright:** measure the branding block height vs total login card height and confirm it is approximately 50% (±10%)
### US-002: Increase branding text to match dashboard typography scale
**Description:** As a visitor, I want the "CVMIS" title and "CV Management Information System" subtitle on the login screen to be larger and more in line with the text scale used in the GP dashboard (TopBar brand text is 15px/600 weight).
**Acceptance Criteria:**
- [ ] "CVMIS" title font size increased from 13px — target approximately 1820px to match dashboard heading scale
- [ ] "CV Management Information System" subtitle font size increased from 11px — target approximately 1314px
- [ ] Both remain in `font-ui` (Elvaro Grotesque) with appropriate weight hierarchy
- [ ] Text remains visually balanced with the larger logo above and the login form below
- [ ] Typecheck passes
- [ ] **Verify in browser using Playwright:** confirm text is rendered at the expected sizes
### US-003: Extract animation timing into named constants
**Description:** As a developer, I want all animation timing values in CvmisLogo.tsx exposed as named constants at the top of the file so I can quickly tune rise speed, fan speed, fan delay, and easing without digging through the component logic.
**Acceptance Criteria:**
- [ ] Named constants at the top of `CvmisLogo.tsx` for at minimum:
- Rise duration (currently 500ms)
- Fan delay after rise (currently 500ms)
- Fan duration (currently 600ms)
- Fan easing curve
- Fan rotation angle (currently ±50°)
- Fan horizontal spacing (currently ±16px)
- Right pill stagger delay (currently 30ms)
- Overlap blend start progress (target: 0.5 = 50% into fan)
- Overlap blend max opacity (target: 0.2 matching Remotion)
- Overlap blend transition duration
- [ ] Component behaviour unchanged when constants retain current values
- [ ] Constants are clearly named and grouped with a brief comment block
- [ ] Typecheck passes
### US-004: Add overlap blend effect on fanning capsules
**Description:** As a visitor, I want to see a subtle color blend where the fanning capsules overlap, matching the multiply-blend effect from the original Remotion animation.
**Acceptance Criteria:**
- [ ] CSS `mix-blend-mode: multiply` hardcoded on the fanning pill elements
- [ ] Blend effect is not visible at the start of the fan animation
- [ ] Blend fades in starting at ~50% of the fan animation progress (matching `OVERLAY_BLEND_START_PROGRESS = 0.5` from Remotion)
- [ ] Blend reaches max intensity by the end of the fan (or configurable transition window)
- [ ] Max blend opacity approximately 0.2 (20%), matching `OVERLAP_BLEND_MAX_OPACITY` from Remotion
- [ ] On a light background, the blend is only perceptible where capsules actually overlap
- [ ] Blend transition feels smooth, not abrupt
- [ ] Respects `prefers-reduced-motion` (no animation, show final state)
- [ ] Typecheck passes
- [ ] **Verify in browser using Playwright:** visually confirm blend is visible in overlap areas during/after fan animation
### US-005: Extend backdrop blur to cover full dashboard including TopBar
**Description:** As a visitor, I want the frosted-glass blur behind the login card to cover the entire dashboard — including the TopBar and sidebar — so nothing behind the overlay is sharp.
**Acceptance Criteria:**
- [ ] Blur overlay z-index raised above TopBar's z-index (currently TopBar is `zIndex: 100`, login overlay is `z-50`). Overlay must be ≥ `zIndex: 110` or similar.
- [ ] TopBar, Sidebar, and all dashboard content are uniformly blurred behind the overlay
- [ ] Login card itself remains crisp and unblurred (card z-index above overlay)
- [ ] Blur still fades out during the dissolve/exit transition
- [ ] Typecheck passes
- [ ] **Verify in browser using Playwright:** confirm TopBar is blurred behind the overlay (not rendered sharply above it)
### US-006: Reduce backdrop blur intensity by ~50%
**Description:** As a visitor, I want the backdrop blur to be softer/less aggressive so the dashboard behind is slightly more visible while still providing contrast for the login card.
**Acceptance Criteria:**
- [ ] Blur value reduced from `blur(20px)` to approximately `blur(10px)`
- [ ] The blur value should be a named constant (co-located with other LoginScreen timing constants) for easy adjustment
- [ ] Login card remains clearly readable against the softened backdrop
- [ ] Typecheck passes
- [ ] **Verify in browser using Playwright:** confirm blur is visibly softer than before
### US-007: Align login card border radius and shadow with dashboard design system
**Description:** As a visitor, I want the login card to feel like it belongs to the same design system as the dashboard. Currently the border radius and shadow diverge from the dashboard tokens.
**Acceptance Criteria:**
- [ ] Login card border radius changed from hardcoded `12px` to `8px` (matching `var(--radius-card)` / dashboard cards)
- [ ] Login input fields and button border radius changed from hardcoded `4px` to `6px` (matching `var(--radius-sm)` / dashboard inner elements)
- [ ] Login card shadow upgraded from `shadow-sm` (`0 1px 2px rgba(26,43,42,0.05)`) to `shadow-lg` (`0 8px 32px rgba(26,43,42,0.12)`) — appropriate for a floating modal over a blurred backdrop, consistent with the command palette
- [ ] Use CSS custom property references (`var(--radius-card)`, `var(--radius-sm)`, `var(--shadow-lg)`) where available rather than hardcoded values
- [ ] Typecheck passes
- [ ] **Verify in browser using Playwright:** confirm card corners and shadow match dashboard card styling
### US-008: Replace hardcoded colors with design tokens
**Description:** As a developer, I want the login screen to reference the same CSS custom properties as the dashboard so that any future palette changes propagate consistently.
**Acceptance Criteria:**
- [ ] Input text color changed from hardcoded `#111827` to `var(--text-primary, #1A2B2A)` (the project's actual primary text color)
- [ ] Cursor/caret color changed from hardcoded `#0D6E6E` to `var(--accent, #0D6E6E)`
- [ ] Button background colors changed from hardcoded `#0D6E6E` / `#0A8080` / `#085858` to `var(--accent)` / `var(--accent-hover)` / appropriate pressed variant using token references
- [ ] Any other hardcoded color values in LoginScreen.tsx that have corresponding CSS custom properties should use the token instead
- [ ] No visual change (token values currently resolve to the same colors)
- [ ] Typecheck passes
### US-009: Fix minor typography inconsistencies
**Description:** As a visitor, I want the login screen's typography weight and sizing to feel consistent with the dashboard's conventions.
**Acceptance Criteria:**
- [ ] Form label font weight increased from 500 to 600 (matching dashboard card header weight convention)
- [ ] `clamp()` typography on inputs and button is acceptable (responsive approach for a modal), but ensure the base/mid values align with dashboard equivalents where practical (e.g., input text mid-value ~14px to match dashboard body, button mid-value ~15px)
- [ ] Connection status indicator gap increased from 6px to 8px (matching dashboard `CardHeader` gap)
- [ ] No dramatic visual change — these are subtle alignment fixes
- [ ] Typecheck passes
## Functional Requirements
- FR-1: CvmisLogo `cssHeight` prop updated to approximately `clamp(160px, 18vw, 280px)` — final values to be tuned visually
- FR-2: "CVMIS" title font size increased from 13px to ~1820px; subtitle from 11px to ~1314px
- FR-3: All animation timing values in `CvmisLogo.tsx` extracted to named constants at the top of the file
- FR-4: CSS `mix-blend-mode: multiply` (hardcoded) applied to left and right pill elements during the fan animation
- FR-5: Blend effect opacity animated from 0 to ~0.2, beginning at 50% fan progress, using a smooth transition
- FR-6: Login screen blur overlay z-index raised above TopBar (`zIndex: 100`) to cover full viewport
- FR-7: Blur intensity reduced from `blur(20px)` to `blur(10px)` via a named constant
- FR-8: Login card border radius aligned to `var(--radius-card)` (8px), inputs/button to `var(--radius-sm)` (6px)
- FR-9: Login card shadow upgraded to `shadow-lg` for proper modal elevation
- FR-10: All hardcoded color values replaced with CSS custom property references where tokens exist
- FR-11: Form label weight aligned to 600, minor spacing/sizing adjustments to match dashboard conventions
- FR-12: All changes respect `prefers-reduced-motion` — animations skip to final state
## Non-Goals
- No canvas-based compositing (using CSS `mix-blend-mode` approach, not replicating the Remotion canvas pipeline)
- No changes to the boot sequence or ECG animation (locked)
- No changes to the login typing animation timing or credentials
- No changes to the dissolve/exit transition behaviour (beyond blur fading and z-index)
- No changes to the dashboard layout or content behind the overlay
- Blend mode is hardcoded to `multiply` — no configurable blend mode switching
- No conversion of login `<div>` fields to `<input>` elements (the login is theatrical animation, not a real form — divs are intentional)
## Design Considerations
- **Remotion reference:** `LogoAnimation/src/Composition.tsx` — the blend uses `globalCompositeOperation: "multiply"` at 20% opacity, masked to intersection areas, starting at 50% fan progress. The CSS `mix-blend-mode: multiply` approach approximates this; on light backgrounds, multiply blending is only perceptible where elements overlap.
- **Branding block proportions:** The branding block (logo + title + subtitle + internal spacing) should take up ~50% of the total login card height. The login form (fields + button + connection indicator) occupies the other ~50%. This creates a strong brand-first impression.
- **Text scale reference:** TopBar brand text is 15px/600w. The login "CVMIS" title should be larger than the TopBar (it's the hero brand moment) — ~1820px. The subtitle can be ~1314px, matching dashboard label scale.
- **Blur coverage:** Currently the overlay is `z-50` and TopBar is `zIndex: 100`, so the TopBar renders above the blur. Fix: raise the overlay's z-index above 100 while keeping the login card above the overlay.
- **Design system alignment:** The login card should use the same border radius, shadow, and color tokens as dashboard components. The card is a modal (like the command palette) so `shadow-lg` is the correct elevation tier. Border radius should match `var(--radius-card)` (8px) not exceed it.
## Technical Considerations
- The `mix-blend-mode` property is well-supported in modern browsers. Framer Motion's `style` prop can animate opacity on elements with `mixBlendMode` set.
- The blend opacity animation should be driven by the same fan animation progress value, using Framer Motion's `useTransform` or similar to derive the blend opacity from fan progress.
- The backdrop blur overlay's z-index must be higher than the TopBar's z-index (100) to cover it, while the login card's z-index must be higher still.
- Named constants should follow the existing Remotion naming convention (e.g., `OVERLAY_BLEND_START_PROGRESS`, `OVERLAP_BLEND_MAX_OPACITY`) for consistency.
- Check whether `var(--accent-hover)` exists as a token or needs to be added. If not, a hardcoded hover shade is acceptable as a local constant.
- **Playwright verification:** Use Playwright MCP to measure the branding block vs card height ratio, confirm text sizes, and visually verify blur coverage and blend effects.
## Success Metrics
- Logo + branding block is ~50% of login card height (verified via Playwright measurement)
- Branding text is noticeably larger, matching dashboard-level typography
- Overlap blend is perceptible but subtle — matches the understated feel of the Remotion reference
- Developer can tune all animation timings by editing constants at the top of one file
- Backdrop blur is uniform across the full viewport including TopBar (no sharp elements above the overlay)
- Blur feels softer/more transparent than before
- Login card visually belongs to the same design system as the dashboard (matching radius, shadow, color tokens)
## Open Questions
None — all questions resolved.
-197
View File
@@ -1,197 +0,0 @@
# PRD: Login Screen Rework
## Introduction
The login screen currently feels undersized on desktop/4K displays, visually disconnected from the GP dashboard aesthetic, and lacks clear affordances telling users they need to click the login button. This rework addresses sizing, style alignment, branding, background treatment, logo animation, and UX clarity — making the login feel like an integrated part of the GP system rather than a disconnected intro screen.
## Goals
- Scale the login card appropriately across all viewports (mobile through 4K)
- Align login screen colors, typography, and styling with the GP dashboard
- Rebrand from "CareerRecord PMR" to "CVMIS" with updated subtitle
- Replace the Shield icon with the custom CVMIS capsule logo (animated reveal on login, static on dashboard)
- Replace the solid dark background with a live blurred dashboard for visual continuity
- Create a clear, sequenced UX flow (typing → connection established → button activates → pulse) so users know exactly when and what to click
- Smooth dissolve transition from login overlay to revealed dashboard
## User Stories
### US-001: Responsive Login Card Sizing
**Description:** As a visitor on a 1440p or 4K display, I want the login card to feel proportionate to my screen so it doesn't look lost in empty space.
**Acceptance Criteria:**
- [ ] Login card uses responsive width: min 320px, scales up with viewport (e.g. `clamp(320px, 28vw, 480px)`)
- [ ] Internal padding, font sizes, icon size, and input heights scale proportionally
- [ ] Card still looks good on mobile (≤480px viewport) — should not exceed viewport width minus margins
- [ ] Card is vertically and horizontally centered
- [ ] Typecheck passes
- [ ] Verify in browser at 1440p and mobile viewport using dev-browser skill
### US-002: Style Alignment with GP Dashboard
**Description:** As a visitor, I want the login screen to feel like part of the same system I'll use after logging in, not a separate app.
**Acceptance Criteria:**
- [ ] Card uses dashboard color tokens: `pmr-surface` background, `pmr-border` borders, `pmr-text-primary`/`pmr-text-secondary` text colors
- [ ] Input fields use `pmr-accent` (#0D6E6E) for focus states, `pmr-border-card` (#E4EDEB) for default borders
- [ ] Button uses `pmr-accent` (#0D6E6E) background with hover `#0A8080`
- [ ] Font usage matches dashboard: `font-ui` (Elvaro) for labels/buttons, `font-geist` for monospace data
- [ ] Card shadow matches dashboard card shadow tokens (`shadow-sm` resting, `shadow-md` on the overlay context)
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-003: Rebrand to CVMIS
**Description:** As the portfolio owner, I want the login to say "CVMIS" (a tongue-in-cheek play on EMIS) with the subtitle "CV Management Information System".
**Acceptance Criteria:**
- [ ] Title text changed from "CareerRecord PMR" to "CVMIS"
- [ ] Subtitle changed from "Clinical Information System" to "CV Management Information System"
- [ ] Footer text "Secure clinical system login" remains unchanged
- [ ] Typecheck passes
### US-004: Animated CVMIS Logo on Login Screen
**Description:** As a visitor, I want to see the CVMIS capsule logo animate on the login card — a polished reveal that replaces the generic Shield icon.
**Source file:** `cvmis-logo.svg` — contains three capsule `<g>` groups: `#capsule-rx` (teal, Rx/pharmacy), `#capsule-terminal` (amber, `>_` code), `#capsule-data` (green, bar chart/analytics).
**Animation reference:** `LogoReveal/frame 1-5.jpg` showing the reveal sequence.
**Animation sequence:**
1. **Frame 1-3 (Rise):** The green data capsule (`#capsule-data`) appears from nothing, rising upward into a fully visible upright/vertical position, centered. Scale from 0 → 1 and translateY from below. Duration: ~400-500ms.
2. **Frame 4-5 (Fan out):** All three capsules fan out from the center position. The green capsule rotates clockwise to its final tilted-right position. The teal Rx capsule appears and rotates to its tilted-left position. The amber terminal capsule appears in the center. Duration: ~400-500ms. The fan-out should feel organic — eased, not mechanical.
3. **Hold:** Logo rests in final fanned-out position (matching `frame 5.jpg` / the SVG's default layout).
**Acceptance Criteria:**
- [ ] `cvmis-logo.svg` is embedded as an inline React SVG component (not an `<img>` tag) so individual capsule groups can be animated
- [ ] Shield icon (`lucide-react` `Shield`) is removed from the login card branding section
- [ ] Logo container scales appropriately with the responsive card (US-001) — roughly 48-64px height depending on card size
- [ ] Animation plays once on login card entrance, after the card's initial fade-in
- [ ] Rise phase: green capsule scales/translates from hidden to upright center (~400-500ms, ease-out)
- [ ] Fan-out phase: three capsules rotate/translate to final positions (~400-500ms, ease-in-out)
- [ ] Total logo animation completes before typing animation begins (logo reveal → then typing starts)
- [ ] `prefers-reduced-motion`: logo appears instantly in final fanned-out position, no animation
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-005: CVMIS Logo on Dashboard TopBar
**Description:** As a visitor on the dashboard, I want to see the CVMIS logo in the top-left corner instead of the generic Home icon, reinforcing the brand.
**Acceptance Criteria:**
- [ ] The `Home` icon from `lucide-react` in `TopBar.tsx` is replaced with the CVMIS logo SVG
- [ ] Logo displays in its final fanned-out state (static, no animation on the dashboard)
- [ ] Logo is sized to fit the TopBar height (~20-24px height, maintaining aspect ratio)
- [ ] Logo colors match the SVG source: teal `#0b7979`, amber `#d97706`, green `#059669`
- [ ] The "Headhunt Medical Center" brand text beside the logo remains unchanged
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-006: Live Blurred Dashboard Background
**Description:** As a visitor, I want to see the GP dashboard blurred behind the login card, creating visual continuity and a satisfying reveal on login.
**Acceptance Criteria:**
- [ ] During the `login` phase, `DashboardLayout` renders underneath the login overlay (requires change to `App.tsx` phase rendering)
- [ ] Dashboard renders at scroll position 0 (top), showing the patient summary header area behind the blur
- [ ] Login overlay uses `backdrop-filter: blur(20px)` (or similar) over a semi-transparent background to blur the dashboard beneath
- [ ] Blur is constant from the moment the login screen appears (no ease-in)
- [ ] Dashboard content is non-interactive while login overlay is present (pointer-events: none or overlay captures all events)
- [ ] `prefers-reduced-motion`: blur still applies (static visual treatment), only animations are skipped
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-007: Connection Status Indicator Rework
**Description:** As a visitor, I want to clearly see the system's connection status change from red "awaiting" to green "connected" so I understand the login flow's progress.
**Acceptance Criteria:**
- [ ] Status indicator starts with RED LED (8-10px dot) + text "Awaiting secure connection..." in red
- [ ] The three dots in "Awaiting secure connection..." animate (sequential dot loading animation)
- [ ] 500ms after typing animation completes, LED transitions to GREEN + text changes to "Secure connection established, awaiting login" in green
- [ ] LED dot is larger than current (8-10px vs current 6px) with a subtle glow/shadow for visibility
- [ ] Text size increased from current 10px to 12px for readability
- [ ] Connection state change is tied to typing completion + 500ms delay (not an independent 2000ms timer)
- [ ] `prefers-reduced-motion`: state changes happen instantly, no dot animation
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-008: Login Button Activation and Pulse
**Description:** As a visitor, I want the login button to clearly signal when it becomes clickable through a visible pulse animation.
**Acceptance Criteria:**
- [ ] Login button remains disabled (opacity 0.6, no pointer cursor) while connection status is red
- [ ] Button becomes enabled only when LED turns green (i.e., after typing completes + 500ms)
- [ ] Once enabled, button begins a subtle pulse animation: scale 1.0 → 1.03 → 1.0, repeating every 3 seconds
- [ ] Pulse uses ease-in-out timing, smooth and premium-feeling
- [ ] Pulse stops on hover (hover state takes priority)
- [ ] Pulse stops immediately on click
- [ ] `prefers-reduced-motion`: no pulse animation, button just becomes enabled
- [ ] Button receives keyboard focus when it becomes enabled (existing behavior, preserve it)
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-009: Login Dissolve Transition
**Description:** As a visitor, I want the login card and blurred overlay to dissolve away on login, revealing the dashboard underneath.
**Acceptance Criteria:**
- [ ] On login click: button shows pressed state → brief loading spinner (existing behavior)
- [ ] After loading spinner, the login card fades out (opacity 0, slight scale up)
- [ ] Simultaneously, the backdrop blur dissolves (blur value animates from 20px to 0, overlay opacity fades to 0)
- [ ] Dashboard beneath becomes interactive once overlay fully dissolves
- [ ] Total transition duration: ~600-800ms from click to fully revealed dashboard
- [ ] `prefers-reduced-motion`: instant transition, no animation
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
## Functional Requirements
- FR-1: Login card width uses `clamp()` for responsive scaling: minimum 320px, preferred ~28vw, maximum ~480px
- FR-2: All internal card elements (padding, fonts, inputs, icon, button) scale proportionally with card width
- FR-3: Login screen uses the same color tokens and font families as the dashboard
- FR-4: Title reads "CVMIS", subtitle reads "CV Management Information System"
- FR-5: `cvmis-logo.svg` is converted to an inline React SVG component with individually addressable capsule groups
- FR-6: Login screen logo animates: green capsule rises → three capsules fan out → hold in final position
- FR-7: Logo animation completes before the typing animation begins
- FR-8: Dashboard TopBar replaces the `Home` lucide icon with a static (final-state) CVMIS logo
- FR-9: `App.tsx` renders `DashboardLayout` during the `login` phase (behind the overlay), wrapped in its providers
- FR-10: Login overlay covers the full viewport with semi-transparent background + `backdrop-filter: blur()`
- FR-11: Connection status begins as red "Awaiting secure connection..." with animated dots
- FR-12: Connection transitions to green "Secure connection established, awaiting login" exactly 500ms after typing animation completes
- FR-13: Login button is disabled until green connection state is reached
- FR-14: Login button pulses (3% scale, 3-second interval) once enabled
- FR-15: Login transition dissolves both the card and the backdrop blur to reveal the live dashboard
- FR-16: All animations respect `prefers-reduced-motion`
## Non-Goals
- No changes to the boot sequence or ECG animation
- No changes to the "Secure clinical system login" footer text
- No password visibility toggle or form validation (this is a theatrical/demo login)
- No changes to the typing animation speeds (80ms/char username, 60ms/dot password)
- No changes to the dashboard layout itself
- No changes to the "Headhunt Medical Center" brand text in the TopBar
## Design Considerations
- **Reference images:** `current_login.jpg` (problem), `goal_login.jpg` (better proportions), `logged_in.jpg` (dashboard aesthetic to match)
- **Logo animation reference:** `LogoReveal/frame 1-5.jpg` showing the capsule reveal sequence
- **Card proportions:** The goal image (25% zoom) shows the card taking up roughly 25-30% of viewport width — use this as the scaling target
- **Blur intensity:** ~20px Gaussian blur; dashboard should be recognizably "there" but not distracting
- **Overlay color:** Semi-transparent version of the dashboard background — suggest `rgba(240, 245, 244, 0.7)` (pmr-bg with alpha) to match the warm sage feel
- **LED glow:** Subtle `box-shadow` in the LED color (red or green) to make the dot feel like an actual indicator light
- **Logo sizing:** On the login card, ~48-64px height. On the TopBar, ~20-24px height. Both maintain the SVG's natural aspect ratio
## Technical Considerations
- **SVG component:** The `cvmis-logo.svg` should be converted to a React component (e.g. `CvmisLogo.tsx`) that accepts props for size and animation state. The three `<g>` groups (`#capsule-rx`, `#capsule-terminal`, `#capsule-data`) need individual transform origins for rotation animation
- **Animation approach:** Framer Motion is already in the project — use it for the capsule animations (staggered variants). CSS transforms for rotation/translation of each capsule group. Transform origins should be set at the base/bottom of each capsule so they fan out like a hand of cards
- **App.tsx phase change:** Currently phases are mutually exclusive. For the blur effect, `DashboardLayout` must render during the `login` phase. This means wrapping it in `DetailPanelProvider` early and ensuring it doesn't trigger side effects that assume the user is "logged in"
- **Performance:** `backdrop-filter: blur()` can be expensive. The dashboard is static during login (no scroll, no interaction), so this should be manageable. Consider `will-change: backdrop-filter` on the overlay
- **Z-index:** Login overlay must sit above the dashboard. Current login uses `z-50` — this should work
- **CSS animation for pulse:** The button pulse can be a CSS `@keyframes` animation rather than framer-motion, since it's a simple repeating animation that should be lightweight
- **Shared logo component:** Both the login screen and TopBar use the same SVG component, just with different sizes and animation props (animated on login, static on dashboard)
## Success Metrics
- Login card appears proportionate on 1440p and 4K displays (no "lost in space" feeling)
- Visual style continuity: a visitor would identify the login and dashboard as the same product
- Logo animation feels polished and adds character to the login experience
- CVMIS branding is consistent between login screen and dashboard TopBar
- First-time visitors understand they need to click the login button without external instruction
- Transition from login to dashboard feels seamless and polished
-220
View File
@@ -1,220 +0,0 @@
# PRD: Responsive Scaling & Mobile Layout
## Introduction
The dashboard is built for 1080p screens. On larger displays (1440p, 4K), everything appears too small — text, spacing, and UI elements don't scale with the viewport. On mobile, the sidebar is completely inaccessible (hidden with no toggle), text doesn't wrap properly and spills off-screen, and several components are unusable at narrow widths.
This PRD addresses viewport-responsive scaling for larger screens and usability fixes for mobile, scoped to the dashboard phase only (TopBar, Sidebar, Card Grid, and all tiles).
## Goals
- Scale the dashboard proportionally on larger viewports (1440p should feel ~25% larger than 1080p)
- Make the sidebar accessible on mobile via a slide-out drawer with toggle
- Ensure all text wraps properly and nothing overflows horizontally at any viewport size
- Maintain the current 1080p experience as the baseline — no changes at that resolution
- Preserve the existing visual design language (fonts, colors, spacing ratios)
## User Stories
### US-000: Skip Boot/Login for Dev Iteration
**Description:** As a developer, I want to skip the boot/ECG/login animation during this feature branch so I can iterate on the dashboard quickly.
**Acceptance Criteria:**
- [ ] In `App.tsx`, initial Phase state changed from `'boot'` to `'pmr'`
- [ ] Boot, ECG, and login code remains — only initial state changes
- [ ] App loads directly to dashboard on refresh
- [ ] Typecheck passes
- [ ] **Reverted in final story (US-013)**
### US-001: Fluid Root Font-Size Scaling
**Description:** As a user on a high-resolution display, I want the dashboard to scale proportionally so that text and UI elements are comfortably sized without manual browser zoom.
**Acceptance Criteria:**
- [ ] `html` element uses a `clamp()`-based font-size that scales with viewport width
- [ ] At 1920px viewport width (1080p), effective font-size remains ~15px (current baseline)
- [ ] At 2560px viewport width (1440p), effective font-size is ~1819px (~25% increase)
- [ ] At 3840px viewport width (4K), font-size caps at a sensible maximum (~22px)
- [ ] Below 1920px, font-size does not shrink below 15px (mobile/tablet keeps baseline)
- [ ] Typecheck passes
### US-002: Convert Fixed px to rem in Layout Structure
**Description:** As a developer, I need layout measurements to use `rem` so they scale with the root font-size, rather than remaining fixed at px values.
**Acceptance Criteria:**
- [ ] CSS custom properties `--topbar-height`, `--sidebar-width`, `--subnav-height` converted from px to rem
- [ ] `DashboardLayout.tsx` margin/padding/height calculations use rem-based values
- [ ] Card padding converted to rem
- [ ] Grid gaps converted to rem
- [ ] Dashboard content area scales proportionally with root font-size
- [ ] At 1080p, layout looks identical to current (no visual regression)
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-003: Convert Fixed px to rem in Typography
**Description:** As a user on any screen size, I want text to scale with the viewport so it's always readable without zooming.
**Acceptance Criteria:**
- [ ] All inline `fontSize` styles in tile components converted from px to rem (e.g., `13px``0.8125rem` based on 16px root, or equivalent relative to 15px base)
- [ ] TopBar text sizes converted to rem
- [ ] Sidebar text sizes converted to rem
- [ ] Card header text sizes converted to rem
- [ ] At 1080p, text sizes are visually identical to current
- [ ] At 1440p, text is proportionally larger and readable
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-004: Convert Fixed px to rem in Spacing & Components
**Description:** As a developer, I need padding, margins, icon sizes, and component dimensions to scale with viewport so the UI stays proportional.
**Acceptance Criteria:**
- [ ] Inline padding/margin values in tiles converted from px to rem
- [ ] Badge/tag padding and sizing converted to rem
- [ ] Icon sizes (where set inline) use rem
- [ ] Border-radius values may remain in px (they don't need to scale)
- [ ] At 1080p, spacing is visually identical to current
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-005: Mobile Sidebar Drawer
**Description:** As a mobile user, I want to access the sidebar content (person details, tags, alerts) via a slide-out drawer so I can see this information without it permanently taking screen space.
**Acceptance Criteria:**
- [ ] TopBar shows a menu/hamburger icon on screens below `lg` breakpoint (1024px)
- [ ] Tapping the icon opens the sidebar as a slide-out drawer overlay from the left
- [ ] Drawer includes a semi-transparent backdrop that closes the drawer on tap
- [ ] Drawer contains the full sidebar content (PersonHeader, Tags, Alerts)
- [ ] Drawer can be closed via the backdrop tap, a close button, or pressing Escape
- [ ] Drawer uses the same animation timing as existing UI (200ms ease-out)
- [ ] Drawer respects `prefers-reduced-motion` (skip to final state)
- [ ] Sidebar remains inline (non-drawer) on `lg+` screens — no behavior change on desktop
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-006: Fix Text Wrapping & Overflow
**Description:** As a mobile user, I want all text content to wrap properly within its containers so nothing is cut off or requires horizontal scrolling.
**Acceptance Criteria:**
- [ ] No horizontal scrollbar appears at any viewport width (320px to 3840px)
- [ ] All text in tile components wraps within its container — no off-screen overflow
- [ ] Long skill names in CoreSkillsTile truncate with ellipsis rather than breaking layout
- [ ] Career Activity entries wrap cleanly at narrow widths
- [ ] PatientSummaryTile stats grid reflows to fewer columns on narrow screens
- [ ] KPI cards in LatestResultsTile stack or reflow on mobile (no horizontal overflow)
- [ ] Education entries wrap properly
- [ ] Project entries wrap properly
- [ ] `overflow-x: hidden` on the main content area as a safety net (not a primary fix — actual wrapping must work)
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-007: Career Constellation Chart — Responsive Container Sizing
**Description:** As a user on any device, I want the D3 career constellation chart (`CareerConstellation.tsx`) to fit its container without overflowing, so the visualization is usable at all viewport widths.
**Acceptance Criteria:**
- [ ] Chart SVG width/height derived from container dimensions (not hardcoded px)
- [ ] Chart re-renders or resizes on viewport/container resize (via `ResizeObserver` or equivalent)
- [ ] D3 force simulation parameters (charge strength, link distance, node spacing) adapt to available width
- [ ] On mobile (<768px), chart remains visible and nodes don't overlap excessively or overflow
- [ ] Node labels remain legible at small sizes (consider hiding secondary labels on narrow viewports)
- [ ] Chart does not cause horizontal scrollbar at any viewport width
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-008: Career Constellation Chart — Mobile Interaction
**Description:** As a mobile user, I want to interact with the career constellation chart using touch so I can explore roles and skills.
**Acceptance Criteria:**
- [ ] Nodes are tappable on touch devices (adequate touch target size, minimum ~44px)
- [ ] Tap on a role node triggers the same action as click (opens role detail)
- [ ] Tap on a skill node triggers the same action as click (opens skill detail)
- [ ] No hover-dependent information is inaccessible on touch (tooltips show on tap or info is always visible)
- [ ] Chart doesn't conflict with page scroll (vertical scroll still works when touching the chart area)
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-009: TopBar Mobile Refinements
**Description:** As a mobile user, I want the TopBar to remain functional and readable at narrow viewport widths.
**Acceptance Criteria:**
- [ ] Hamburger/menu icon appears at the left of the TopBar below `lg` breakpoint
- [ ] Brand text collapses gracefully (already mostly handled — verify no regression)
- [ ] Search trigger remains accessible on mobile (either inline icon or within command palette trigger)
- [ ] Session info collapses gracefully at narrow widths (already mostly handled — verify no regression)
- [ ] TopBar height scales with rem (from US-002)
- [ ] No horizontal overflow from TopBar content
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-010: Verify at Key Viewport Sizes
**Description:** As a developer, I need to verify the dashboard renders correctly at representative viewport sizes to confirm no regressions.
**Acceptance Criteria:**
- [ ] 375px wide (iPhone SE) — single column, drawer sidebar, all text wraps, no overflow
- [ ] 768px wide (iPad portrait) — single column, drawer sidebar, comfortable spacing
- [ ] 1024px wide (iPad landscape / small laptop) — sidebar inline, 2-column grid
- [ ] 1920px wide (1080p) — visually identical to current production
- [ ] 2560px wide (1440p) — ~25% scaled up, proportional layout, readable
- [ ] Typecheck passes
- [ ] Verify in browser using dev-browser skill
### US-011: Re-enable Boot/Login Sequence
**Description:** As a user, I want the full boot → ECG → login experience restored for production.
**Acceptance Criteria:**
- [ ] In `App.tsx`, initial Phase state reverted from `'pmr'` back to `'boot'`
- [ ] Full boot → ECG → login → dashboard sequence works end to end
- [ ] No other changes to `App.tsx`
- [ ] Typecheck passes
- [ ] Verify full sequence works via Playwright MCP
## Functional Requirements
- FR-1: Set `html { font-size: clamp(...) }` that scales from 15px at 1920px to ~22px at 3840px, floored at 15px below 1920px
- FR-2: Convert all CSS custom properties used for layout dimensions (`--topbar-height`, `--sidebar-width`, `--subnav-height`) to rem units
- FR-3: Convert all inline `fontSize`, `padding`, `margin`, and `gap` values in dashboard components from px to rem
- FR-4: Add a mobile sidebar drawer component (slide-from-left overlay, backdrop, close on escape/backdrop-tap)
- FR-5: Add a hamburger menu button to TopBar, visible below `lg` breakpoint, that toggles the sidebar drawer
- FR-6: Apply `overflow-wrap: break-word` and appropriate `min-width: 0` on flex/grid children to prevent text overflow
- FR-7: Ensure `overflow-x: hidden` on the main scrollable content area as a catch-all
- FR-8: Tile content must reflow (fewer grid columns, stacked layout) at narrow viewports rather than overflowing
- FR-9: Career Constellation D3 chart must size to its container, re-render on resize, and adapt force simulation parameters for narrow viewports
- FR-10: D3 chart nodes must have adequate touch targets (44px minimum) and not block page scroll on touch devices
## Non-Goals
- No permanent changes to Boot, ECG, or Login phases (temporarily skipped for dev, restored in final story)
- No changes to the 1080p visual appearance (it's the baseline)
- No changes to color palette, fonts, or design language
- No new responsive breakpoints beyond what Tailwind already provides (xs/sm/md/lg/xl)
- No container queries or component-level responsive logic — viewport scaling via root font-size is the mechanism
- No changes to Command Palette responsive behavior (separate concern)
- No touch gesture support (swipe to open sidebar, etc.)
## Design Considerations
- The sidebar drawer on mobile should visually match the existing sidebar styling — same background (`#F7FAFA`), same border, same content
- The drawer should overlay content with a semi-transparent backdrop, similar to the existing detail panel pattern used for project/career detail expansion
- The hamburger icon should use `lucide-react` (e.g., `Menu` icon) consistent with existing icon usage
- Font-size scaling should be smooth (fluid), not stepped at breakpoints, to avoid jarring jumps during window resize
## Technical Considerations
- **rem base calculation**: Since the current design uses `15px` as the body font-size, the rem conversion should be based on `15px = 1rem` (set on `html`). At 1080p, `1rem = 15px`. At 1440p, `1rem ≈ 18.75px`.
- **Conversion formula**: `px / 15 = rem` (e.g., `13px = 0.867rem`, `48px = 3.2rem`, `272px = 18.133rem`)
- **Tailwind classes**: Tailwind's spacing scale uses a 16px rem base by default. Since we're changing the html font-size, Tailwind rem-based classes (`p-4`, `text-sm`, etc.) will automatically scale. Only inline `px` styles need manual conversion.
- **CSS custom properties**: Convert `--topbar-height: 48px``--topbar-height: 3.2rem`, etc.
- **Sidebar drawer state**: Managed via React state in `DashboardLayout.tsx`, passed down or via context. Keep it simple — `useState<boolean>` for open/closed.
- **Animations**: Drawer slide uses `transform: translateX(-100%)``translateX(0)` with 200ms ease-out, consistent with existing motion patterns.
## Success Metrics
- At 1440p, the dashboard is comfortably readable without browser zoom adjustments
- On mobile (375px), all content is accessible, readable, and no horizontal scrolling occurs
- Sidebar content is accessible on all screen sizes
- No visual regression at 1080p
- Zero horizontal overflow at any viewport width between 320px and 3840px
## Open Questions
- Should the mobile drawer have a swipe-to-close gesture? (Deferred as non-goal for now, can add later)
- Should the search bar be accessible on mobile via a dedicated icon in TopBar? (Currently hidden below md — may want a small search icon)
- At what viewport width should font scaling cap? (PRD suggests 3840px / ~22px — may need tuning)
-235
View File
@@ -1,235 +0,0 @@
# PRD: Semantic Search & AI Chat
## Introduction
The portfolio's command palette currently uses Fuse.js for fuzzy string matching across ~40 palette items. While it handles typos, it doesn't understand intent — searching "NHS leadership" won't surface relevant roles unless those exact words appear in the keywords field. This PRD covers two complementary features:
1. **Phase 1 — Semantic Vector Search**: Replace Fuse.js with pre-computed embeddings and cosine similarity, enabling meaning-based search in the existing command palette. Zero runtime API cost.
2. **Phase 2 — AI Chat Widget**: A floating chat button (bottom-right, like a support chat) powered by Google Gemini Flash. Visitors can ask natural language questions about Andy's experience. Hybrid responses: conversational answer + relevant portfolio items.
## Goals
- Enable meaning-based search (e.g., "data visualization" matches Power BI dashboards, analytics roles)
- Maintain instant search performance (<50ms) in the command palette via client-side vectors
- Add a conversational "Ask about me" chat widget powered by Gemini Flash
- Keep the existing command palette UX (Ctrl+K, keyboard nav, grouped results) intact
- Hybrid chat responses: short natural language answer + clickable portfolio items
## User Stories
### Phase 1: Semantic Vector Search
#### US-001: Generate embeddings at build time
**Description:** As a developer, I want a build script that generates embeddings for all palette items so they ship as a static asset.
**Acceptance Criteria:**
- [ ] Node script `scripts/generate-embeddings.ts` reads all palette data from `src/lib/search.ts`
- [ ] Uses the same ONNX model (`all-MiniLM-L6-v2` via `@xenova/transformers`) as the browser runtime to generate embeddings
- [ ] Builds a rich text representation of each item (title + subtitle + keywords + extended context from data files)
- [ ] Outputs `src/data/embeddings.json` — array of `{ id: string, embedding: number[] }`
- [ ] Script is runnable via `npm run generate-embeddings`
- [ ] No external API key required — model runs locally via Node.js
- [ ] Embeddings file is committed to repo (static asset, not generated per-build)
- [ ] Typecheck passes
#### US-002: Preload ONNX model during boot sequence
**Description:** As a visitor, I want the semantic search model to be ready by the time I reach the dashboard, without slowing down the initial experience.
**Acceptance Criteria:**
- [ ] Model download (`all-MiniLM-L6-v2` via `@xenova/transformers`) begins when `App.tsx` mounts (during `'boot'` phase)
- [ ] Download runs in background — does not block or affect boot/ECG/login animations
- [ ] Model is cached in browser (IndexedDB) — second visit loads from cache instantly
- [ ] A global ready state (React context or module-level promise) signals when model is available
- [ ] If model fails to load (network error, etc.), the app continues normally — no error shown to user
- [ ] Typecheck passes
#### US-003: Client-side cosine similarity search
**Description:** As a visitor, I want the command palette to understand what I mean, not just match strings.
**Acceptance Criteria:**
- [ ] New `src/lib/semantic-search.ts` module with cosine similarity function
- [ ] Loads `embeddings.json` and provides a `semanticSearch(query: string, items: PaletteItem[])` function
- [ ] Query embedding computed in-browser using `all-MiniLM-L6-v2` ONNX model via `@xenova/transformers`
- [ ] Returns ranked `PaletteItem[]` with similarity scores
- [ ] Typecheck passes
#### US-004: Integrate semantic search into command palette
**Description:** As a visitor, I want the command palette to use semantic search with Fuse.js as a fallback.
**Acceptance Criteria:**
- [ ] Command palette uses semantic search as primary ranking when embeddings are available
- [ ] Falls back to Fuse.js if embeddings fail to load
- [ ] Search latency remains <100ms for all queries
- [ ] Existing keyboard navigation, grouping, and action routing unchanged
- [ ] Typecheck passes
- [ ] Verify in browser: search "data analysis" surfaces analytics-related roles/skills, not just items with "data" in the title
#### US-005: Enrich embedding content with deep context
**Description:** As a developer, I want embeddings to capture rich context beyond just titles, so semantic search is truly useful.
**Acceptance Criteria:**
- [ ] Consultation embeddings include: role, org, duration, history narrative, examination bullets, coded entry descriptions
- [ ] Skill embeddings include: name, category, frequency, proficiency, years
- [ ] KPI embeddings include: value, label, explanation, story context/outcomes
- [ ] Investigation embeddings include: name, methodology, tech stack, results
- [ ] Education embeddings include: title, institution, type, research detail
- [ ] Each item's embedding text is a natural-language paragraph, not a keyword list
- [ ] Typecheck passes
---
### Phase 2: AI Chat Widget
#### US-006: Chat widget UI — floating button
**Description:** As a visitor, I see a floating chat button at the bottom-right of the dashboard that opens a chat panel.
**Acceptance Criteria:**
- [ ] Floating circular button, bottom-right corner, consistent with design system (teal accent, shadow-md)
- [ ] Button shows a chat/message icon (lucide-react)
- [ ] Click toggles the chat panel open/closed
- [ ] Button has a subtle entrance animation after dashboard loads (delayed ~1s)
- [ ] Button respects `prefers-reduced-motion`
- [ ] Button is above all dashboard content but below command palette overlay (z-index layering)
- [ ] Typecheck passes
- [ ] Verify in browser using dev server
#### US-007: Chat panel UI
**Description:** As a visitor, I want a chat panel that feels like a support chat — compact, positioned above the floating button.
**Acceptance Criteria:**
- [ ] Panel opens above the chat button, anchored to bottom-right
- [ ] Panel dimensions: ~380px wide, ~480px tall max, with scroll for overflow
- [ ] Header with title ("Ask about Andy" or similar), close button
- [ ] Message area showing conversation history (user messages right-aligned, AI responses left-aligned)
- [ ] Input area at bottom with text field and send button
- [ ] AI responses show: natural language answer paragraph, then clickable portfolio item cards below (hybrid format)
- [ ] Clicking a portfolio item card triggers the same action routing as command palette (scroll, panel, link, etc.)
- [ ] Panel entrance/exit animation (scale + fade, 200ms)
- [ ] Respects `prefers-reduced-motion`
- [ ] Responsive: on mobile (<640px), panel goes full-width with adjusted height
- [ ] Typecheck passes
- [ ] Verify in browser using dev server
#### US-008: Gemini Flash integration
**Description:** As a visitor, I can ask natural language questions and get intelligent answers about Andy's experience.
**Acceptance Criteria:**
- [ ] API calls to Google Gemini Flash model
- [ ] System prompt includes full CV context (structured from data files) so the model can answer accurately
- [ ] API key sourced from environment variable `VITE_GEMINI_API_KEY` (exposed to client via Vite)
- [ ] Responses are streamed token-by-token for perceived speed
- [ ] Response format: JSON with `{ answer: string, relevantItems: string[] }` where items are palette item IDs
- [ ] If API key is missing or call fails, show a graceful fallback message ("Chat unavailable" or similar)
- [ ] Loading state shown while waiting for response
- [ ] Typecheck passes
#### US-009: Chat context and conversation history
**Description:** As a visitor, I want multi-turn conversation so I can ask follow-up questions.
**Acceptance Criteria:**
- [ ] Conversation history maintained in component state (not persisted across page loads)
- [ ] Previous messages included in Gemini API calls for context
- [ ] History capped at last 10 messages to manage token usage
- [ ] "Clear conversation" option available (button or typing /clear)
- [ ] Typecheck passes
## Functional Requirements
### Phase 1
- FR-1: Build script generates embeddings using `all-MiniLM-L6-v2` ONNX model (same model used at runtime)
- FR-2: Embeddings stored as committed static JSON (`src/data/embeddings.json`)
- FR-3: Client-side cosine similarity ranks items by semantic relevance
- FR-4: Command palette uses semantic search as primary, Fuse.js as fallback
- FR-5: ONNX model preloaded during boot sequence (before dashboard renders)
- FR-6: Query embedding computed in-browser — no runtime API calls
### Phase 2
- FR-7: Floating chat button rendered in DashboardLayout, bottom-right, above content
- FR-8: Chat panel opens/closes on button click with animation
- FR-9: User messages sent to Gemini Flash API with CV context as system prompt
- FR-10: Gemini responses parsed into answer text + relevant item IDs
- FR-11: Relevant items rendered as clickable cards using existing palette item styling and action routing
- FR-12: Streaming responses displayed progressively
- FR-13: Conversation state managed per-session (cleared on page reload)
## Non-Goals
- No server-side search infrastructure (everything client-side or direct API calls)
- No persistent chat history across sessions
- No user authentication or rate limiting (API key cost is accepted)
- No voice input or speech-to-text
- No training or fine-tuning of models
- Chat widget does not replace the command palette — they coexist
- No analytics or tracking of search queries
## Design Considerations
### Command Palette (Phase 1)
- No visual changes to the command palette UI
- Semantic search is a drop-in replacement for the ranking logic
- Same grouped sections, icons, keyboard navigation, and action routing
### Chat Widget (Phase 2)
- **Button**: 48px circle, teal bg (`var(--accent)`), white icon, `shadow-md`. Hover: `shadow-lg` + slight scale
- **Panel**: White surface, 12px border-radius, `shadow-lg`. Same card/border tokens as rest of design system
- **Messages**: User messages in teal-tinted bubbles (right). AI messages in light gray bubbles (left) with `font-ui`
- **Item cards**: Reuse icon/color mapping from command palette results. Compact horizontal layout
- **Typography**: Body text 13px `font-ui`, timestamps 11px `font-geist`
- **Position**: Fixed, `bottom: 24px, right: 24px`. Panel above button with 8px gap
- **Mobile**: Button smaller (40px), panel full-width with `bottom: 0, right: 0` and rounded top corners only
### Existing components to reuse
- `iconByType` and `iconColorStyles` mappings from `CommandPalette.tsx`
- `PaletteItem`, `PaletteAction` types from `src/lib/search.ts`
- `buildPaletteData()` for building the searchable dataset
- `handlePaletteAction()` in `DashboardLayout.tsx` for action routing
- Design tokens from `index.css` and `tailwind.config.js`
## Technical Considerations
### Phase 1: ONNX Model Strategy
**Decision: `all-MiniLM-L6-v2` via `@xenova/transformers` for both build-time and runtime embedding.**
- Same model used everywhere — embeddings live in the same vector space, so cosine similarity works correctly
- No external API keys required for embedding generation or search
- Build script runs the model in Node.js to pre-compute item embeddings
- Browser loads the same model at runtime for query embedding
**Preloading strategy:**
- Model download (~23MB, ONNX format) begins during the boot sequence phase (`'boot'` in App.tsx)
- The boot → ECG → login flow takes 8-10s, giving ample time for download + cache
- Model is cached by the browser (IndexedDB via Transformers.js) — subsequent visits load instantly
- If model hasn't finished loading when user opens command palette, fall back to Fuse.js silently
**Embedding content:**
- Each palette item gets a natural-language paragraph for embedding, not just keywords
- E.g., a consultation becomes: "Senior Data Analyst at Norfolk and Waveney ICB, 2021 to present. Led medicines optimisation analytics, built Power BI dashboards for 200+ clinicians..."
- Richer text = better semantic matching
### Phase 2: Gemini Flash
- Use `gemini-2.0-flash` (or latest) — fast, cheap, good for short-form Q&A
- System prompt should be a structured summary of all CV data, not raw data dumps
- Response schema enforced via Gemini's JSON mode or structured output
- `VITE_GEMINI_API_KEY` exposed to client — acceptable for a portfolio (low traffic, low cost)
- Consider a soft rate limit in the UI (e.g., 1 request per 2 seconds) to prevent abuse
### Shared
- Both features use `buildPaletteData()` as the canonical item dataset
- Action routing through `handlePaletteAction()` is shared
- `DetailPanelContent` union type supports all drill-down destinations
## Success Metrics
- Semantic search returns relevant results for intent-based queries (e.g., "healthcare leadership" surfaces ICB roles)
- Command palette search latency stays <100ms
- Chat widget responds within 2-3 seconds for typical questions
- Chat answers are factually accurate to the CV content (no hallucinated roles or dates)
- Both features degrade gracefully when APIs are unavailable
## Open Questions
- **Chat widget on mobile**: Should the chat panel be a full-screen modal on small screens, or a bottom sheet?
- **Suggested questions**: Should the chat widget show 2-3 starter questions when first opened (e.g., "What's Andy's NHS experience?", "Tell me about his data skills")?
- **Model CDN**: Transformers.js downloads models from Hugging Face by default. Should we self-host the ONNX model files for reliability, or trust HF's CDN?
-215
View File
@@ -1,215 +0,0 @@
# PRD: Typography & Spacing Scale Rework
## Introduction
The dashboard text and spacing are undersized for the target screen resolution (2560x1440 QHD). Everything — body copy, sidebar details, card content, KPI labels, project entries, tags — reads as tiny, failing to command the viewport. The current CV site (andy.charlwood.xyz) demonstrates the correct sense of scale: confident, readable, space-owning. This PRD defines a comprehensive rework of the type scale, spatial tokens, and layout proportions to bring the dashboard up to that standard.
## Goals
- Rework the entire type scale so text is comfortably readable on a 2560x1440 display
- Increase spatial tokens (padding, gaps, margins) proportionally so the UI feels generous, not cramped
- Rethink layout proportions (sidebar width, topbar height, card sizing) to match the larger type
- Maintain the premium clinical aesthetic — the increase should feel considered, not bloated
- Ensure responsive behavior still works at smaller breakpoints (no regression on tablet/mobile)
- All changes verified visually in a real browser via Playwright MCP
## Reference
**Current CV site** (`andy.charlwood.xyz`) — screenshot provided as `current.png`. Note how:
- Body text is ~15-16px, not 13px
- KPI values are large and prominent
- Info cards use generous padding
- Labels and metadata are 12-13px, not 9-10px
- The content confidently fills the viewport without feeling sparse
**Target screen**: 2560x1440 (QHD), the user's primary display.
## User Stories
### US-018: Rework global type scale tokens
**Description:** As a viewer on a QHD display, I want all text to be comfortably readable so the dashboard doesn't feel like I'm squinting at tiny type.
**Acceptance Criteria:**
- [ ] Define a new type scale in CSS custom properties or Tailwind config that replaces the current one
- [ ] Minimum body text: 15px (currently 13px)
- [ ] Minimum label/metadata text: 12px (currently 9-10px)
- [ ] Minimum sidebar detail text: 13px (currently 11-11.5px)
- [ ] Card header section titles: 13-14px (currently 12px)
- [ ] KPI values: 32-36px (currently 28px)
- [ ] KPI labels: 14px (currently 12px)
- [ ] KPI sublabels: 12px (currently 10px)
- [ ] ParentSection headings remain responsive and scale appropriately with the new system
- [ ] No text anywhere in the dashboard falls below 11px
- [ ] Typecheck passes (`npm run typecheck`)
### US-019: Scale sidebar proportions
**Description:** As a viewer, I want the sidebar to use its space effectively with appropriately sized text and spacing so it doesn't feel like a cramped afterthought.
**Acceptance Criteria:**
- [ ] Sidebar width increased (suggest 300-320px, use judgement)
- [ ] Sidebar name: 17-18px (currently 15px)
- [ ] Sidebar job title: 13px (currently 11.5px)
- [ ] Sidebar detail rows (GPhC, Education, Location, etc.): 13px labels, 13px values (currently 11.5px)
- [ ] Status badge text: 12-13px (currently 11px)
- [ ] Tag pills: 12px (currently 10.5px), with proportionally larger padding
- [ ] Alert flags: 13px (currently 11px), with proportionally larger padding
- [ ] Section divider titles: 11-12px (currently 10px)
- [ ] Avatar: 56-64px (currently 52px)
- [ ] Sidebar internal padding: 24px (currently 16-20px)
- [ ] Spacing between sidebar sections: proportionally increased
- [ ] Verify in browser using Playwright MCP — sidebar should feel balanced with main content
### US-020: Scale TopBar and SubNav
**Description:** As a viewer, I want the TopBar and SubNav to feel substantial rather than thin strips across the top.
**Acceptance Criteria:**
- [ ] TopBar height: 56-60px (currently 48px)
- [ ] TopBar brand text: 15px (currently 13px)
- [ ] TopBar "Remote" label: 12px (currently 11px)
- [ ] Search bar height: 44-48px (currently 42px), text 14px (currently 13px)
- [ ] Session info text: 13px (currently 11-12px)
- [ ] Ctrl+K badge text: 11px (currently 10px)
- [ ] SubNav height: 42-44px (currently 36px)
- [ ] SubNav tab text: 14px (currently 13px)
- [ ] Update `--topbar-height` and `--subnav-height` CSS variables accordingly
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright MCP
### US-021: Scale card and grid spacing
**Description:** As a viewer, I want cards to have generous internal spacing and the grid to breathe so the content doesn't feel packed into undersized containers.
**Acceptance Criteria:**
- [ ] Card padding: 24-28px (currently 20px)
- [ ] Grid gap: 20px on desktop (currently 16px)
- [ ] Main content area padding (the `p-4 md:p-6 lg:px-7` classes): increase by ~25%
- [ ] Card border-radius: consider whether 8px still works at the larger scale or should increase to 10-12px
- [ ] CardHeader margin-bottom: proportionally increased
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright MCP
### US-022: Scale Patient Summary tile content
**Description:** As a viewer, I want the Patient Summary — the first and most prominent tile — to feel commanding and readable.
**Acceptance Criteria:**
- [ ] Profile text body: 15px with line-height 1.65 (currently 13px/1.6)
- [ ] KPI metric cards: increased padding (20px, currently 16px)
- [ ] KPI values: 32-36px (currently 28px)
- [ ] KPI labels: 14px (currently 12px)
- [ ] KPI sublabels: 12px (currently 10px)
- [ ] KPI grid gap: 16px (currently 12px)
- [ ] The entire Patient Summary tile should feel like the hero section of the dashboard
- [ ] Verify in browser using Playwright MCP
### US-023: Scale Last Consultation and career content
**Description:** As a viewer, I want career details and consultation records to be easily scannable.
**Acceptance Criteria:**
- [ ] Last Consultation field labels: 11-12px (currently 10px)
- [ ] Last Consultation field values: 13px (currently 11.5px)
- [ ] Last Consultation role title: 15px (currently 13.5px)
- [ ] Bullet point text: 14px (currently 12.5px)
- [ ] "View full record" link: 13px (currently 12px)
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright MCP
### US-024: Scale Projects tile and tech tags
**Description:** As a viewer, I want project entries and their tech stack tags to be readable without squinting.
**Acceptance Criteria:**
- [ ] Project item text: 13px (currently 11.5px)
- [ ] Project item padding: 14px 16px (currently 10px 12px)
- [ ] Project year labels: 11-12px (currently 10px)
- [ ] Tech stack tags: 10-11px (currently 9px), padding 3px 8px (currently 2px 6px)
- [ ] Project list gap: 10px (currently 8px)
- [ ] Verify in browser using Playwright MCP
### US-025: Scale remaining subsections (Education, Skills/Repeat Meds, Work Experience)
**Description:** As a viewer, I want all subsections within Patient Pathway to match the new type scale.
**Acceptance Criteria:**
- [ ] Audit all remaining subsection components (EducationSubsection, RepeatMedicationsSubsection, WorkExperienceSubsection) for font sizes below the new minimums
- [ ] Apply consistent sizing: body text 14-15px, labels 12-13px, metadata 11-12px minimum
- [ ] Ensure expanded/accordion content also uses the new scale
- [ ] Typecheck passes
- [ ] Verify in browser using Playwright MCP — scroll through entire dashboard to check consistency
### US-026: Visual regression check across all breakpoints
**Description:** As a viewer on any device, I want the scaled-up dashboard to still work well at smaller screen sizes.
**Acceptance Criteria:**
- [ ] Verify at 1920x1080 (HD) — content should still be comfortable, not oversized
- [ ] Verify at 1440x900 — should still work
- [ ] Verify at 768px tablet — sidebar hidden, single column, no overflow
- [ ] Verify at 375px mobile — everything stacks, no horizontal scroll, no truncation
- [ ] Verify Playwright screenshots at each breakpoint
- [ ] No horizontal overflow at any breakpoint
- [ ] `npm run build` succeeds without errors
## Functional Requirements
- FR-1: All type size changes must be made in component inline styles and/or CSS custom properties — maintain the existing approach (inline styles for component-specific sizes, CSS vars for shared tokens)
- FR-2: Update `--sidebar-width` CSS variable to the new sidebar width
- FR-3: Update `--topbar-height` and `--subnav-height` CSS variables
- FR-4: The dashboard-grid gap must increase in `index.css`
- FR-5: Every component with hardcoded `fontSize` values must be updated (see audit below)
- FR-6: Responsive Tailwind classes on ParentSection headings must be re-evaluated for the new scale
- FR-7: All changes must pass `npm run typecheck` and `npm run build`
## Current Sizing Audit (Components to Modify)
| Component | File | Current Sizes |
|-----------|------|---------------|
| TopBar | `src/components/TopBar.tsx` | 13px brand, 11px remote, 13px search, 10px kbd, 11-12px session |
| SubNav | `src/components/SubNav.tsx` | 13px tabs, 36px height |
| Sidebar | `src/components/Sidebar.tsx` | 15px name, 11.5px title, 11-11.5px details, 10.5px tags, 11px alerts, 10px section titles |
| Card | `src/components/Card.tsx` | 20px padding, 12px header title, 10px header right text, 8px dot |
| ParentSection | `src/components/ParentSection.tsx` | Responsive heading (22-35px) |
| PatientSummaryTile | `src/components/tiles/PatientSummaryTile.tsx` | 13px body, 28px KPI value, 12px KPI label, 10px KPI sub |
| ProjectsTile | `src/components/tiles/ProjectsTile.tsx` | 11.5px item, 10px year, 9px tech tags |
| DashboardLayout (LastConsultation) | `src/components/DashboardLayout.tsx` | 10px field labels, 11.5px field values, 13.5px role, 12.5px bullets, 12px link |
| EducationSubsection | `src/components/EducationSubsection.tsx` | Audit needed |
| RepeatMedicationsSubsection | `src/components/RepeatMedicationsSubsection.tsx` | Audit needed |
| WorkExperienceSubsection | `src/components/WorkExperienceSubsection.tsx` | Audit needed |
| CSS Variables | `src/index.css` | 272px sidebar, 48px topbar, 36px subnav, 12-16px grid gap |
## Non-Goals
- No changes to the boot sequence, ECG animation, or login screen — those are locked
- No changes to color palette, font families, or shadow system
- No layout restructuring (no moving sections, no changing tile order)
- No changes to functionality (click handlers, command palette behavior, detail panels)
- No changes to animation timing or motion design
- No new components or features
## Design Considerations
- **Proportional scaling, not uniform**: Don't just multiply everything by 1.3. Headings scale less than body text (they're already large enough relatively). Metadata scales more than body (it's the most undersized category).
- **The clinical metaphor still applies**: The sidebar should still feel like a clinical person-header, not a marketing page. Data density is part of the character — just at a readable size.
- **Weight over size for hierarchy**: Continue using font-weight (400/500/600/700) as the primary hierarchy tool. Size increases should tighten the scale, not spread it.
- **Reference the current CV site** (`current.png`) for the "feel" of appropriate sizing, but don't replicate its design — the GP system aesthetic is different.
- **The implementing agent has autonomy** to adjust specific px values within the ranges suggested in the acceptance criteria. The ranges are guidance, not mandates. Use Playwright to preview and iterate. If 14px looks better than 15px somewhere, that's fine — the goal is a dashboard that feels right, not one that hits exact numbers.
## Technical Considerations
- Many sizes are inline `fontSize` values in React style objects — these need direct editing in each component
- CSS custom properties (`--sidebar-width`, `--topbar-height`, `--subnav-height`) are used by multiple components, so changing them propagates automatically
- The `dashboard-grid` gap is in `index.css` — change it there
- Card padding is in `Card.tsx` — one change propagates to all cards
- `CardHeader` sizing is in `Card.tsx` — one change propagates to all section headers
- Sidebar `SectionTitle` is a local component in `Sidebar.tsx`
- `LastConsultationSubsection` is defined inside `DashboardLayout.tsx`, not its own file
- Responsive Tailwind classes on `ParentSection` headings need updating to match the new scale
## Success Metrics
- No text in the dashboard below 11px
- Body text comfortably readable without leaning in on a 2560x1440 display
- The dashboard "owns" the viewport — content feels confident, not shrunken
- Proportions feel balanced — sidebar doesn't look skinny next to scaled content
- No responsive regressions at tablet and mobile breakpoints
- Passes build and typecheck
## Open Questions
- Should the Command Palette and Detail Panel also be scaled, or are they already appropriately sized? (Implement core dashboard first, address these in a follow-up if needed)
- Should the KPI grid move from 2x2 to a different layout at the new scale? (Probably not — but verify visually)
-1
View File
@@ -1 +0,0 @@
~/.claude/projects/debug/6b30776a-488e-4ffc-be79-dcf0b497467d.txt