This commit is contained in:
2026-02-16 11:33:47 +00:00
parent 5a657c4aac
commit c3a72d0bee
15 changed files with 178 additions and 1557 deletions
+13 -7
View File
@@ -1,11 +1,11 @@
# Session Handoff
_Generated: 2026-02-16 10:43:45 UTC_
_Generated: 2026-02-16 11:04:21 UTC_
## Git Context
- **Branch:** `codex/kpi`
- **HEAD:** 24ffe03: chore: auto-commit before merge (loop primary)
- **Branch:** `codex/projects`
- **HEAD:** 78e994e: chore: auto-commit before merge (loop primary)
## Tasks
@@ -13,22 +13,28 @@ _Generated: 2026-02-16 10:43:45 UTC_
- [x] Compact Latest Results KPI section
- [x] Validate KPI objective and close loop
- [x] Rename Active Projects language to Significant Interventions
- [x] Add autoplay + reduced-motion behavior for carousel
- [x] Responsive polish and full verification for interventions carousel
- [x] Implement Embla carousel in ProjectsTile
- [x] Add autoplay + reduced-motion behavior for carousel
- [x] Responsive polish and full verification for interventions carousel
## Key Files
Recently modified:
- `.ralph/agent/handoff.md`
- `.ralph/agent/memories.md`
- `.ralph/agent/memories.md.lock`
- `.ralph/agent/scratchpad.md`
- `.ralph/agent/summary.md`
- `.ralph/agent/tasks.jsonl`
- `.ralph/agent/tasks.jsonl.lock`
- `.ralph/current-events`
- `.ralph/current-loop-id`
- `.ralph/events-20260216-103430.jsonl`
- `.ralph/events-20260216-105626.jsonl`
- `.ralph/history.jsonl`
- `.ralph/loop.lock`
- `package-lock.json`
## Next Session
-53
View File
@@ -1,53 +0,0 @@
# Significant Interventions Carousel (Ralph Prompt)
## Goal
Replace the current one-column **Active Projects** list with a **Significant Interventions** carousel that supports thumbnail cards and auto-scroll behavior (Embla-based), while preserving panel-open behavior on card click.
## Scope
- Rename all relevant UI/content references from **Active Projects** to **Significant Interventions**.
- Replace `ProjectsTile` list layout with an Embla carousel.
- Use auto-scroll as the default carousel behavior.
- Keep room for thumbnails now; real thumbnail assets will be added later.
## Implementation Task List
- [ ] Install carousel dependencies:
- `embla-carousel-react`
- `embla-carousel-autoplay`
- [ ] Update tile heading in `src/components/tiles/ProjectsTile.tsx`:
- `ACTIVE PROJECTS` -> `SIGNIFICANT INTERVENTIONS`
- [ ] Refactor `ProjectsTile` in `src/components/tiles/ProjectsTile.tsx`:
- Replace vertical list container with Embla viewport/container/slides
- Convert each project item to a carousel slide card
- Add thumbnail region in each slide (use placeholder block/image container for now)
- Keep keyboard activation (`Enter`/`Space`) and click-to-open detail panel
- [ ] Implement auto-scroll behavior:
- Use Embla autoplay plugin with sensible defaults (continuous feel, pauses on hover/focus)
- Respect reduced motion (`prefers-reduced-motion`) by disabling autoplay
- [ ] Responsive behavior:
- Mobile: single-card view
- Tablet/Desktop: multi-card visible area (based on available width)
- Ensure overflow clipping and smooth transitions
- [ ] Update navigation/search labels to match naming:
- `src/components/SubNav.tsx`: `Projects` -> `Significant Interventions`
- `src/lib/search.ts`: `Active Projects` -> `Significant Interventions` (section type and related labels/comments)
- [ ] Keep detail panel integration unchanged:
- Clicking a carousel card still calls `openPanel({ type: 'project', investigation: project })`
- [ ] Styling pass:
- Align with current dashboard tokens (`--surface`, `--border-light`, `--accent`, etc.)
- Ensure cards remain readable without thumbnails
## Acceptance Criteria
- The dashboard section title displays **Significant Interventions**.
- The old one-column projects list is replaced by a working carousel.
- Carousel auto-scrolls by default and pauses appropriately on interaction.
- In reduced-motion environments, carousel does not auto-scroll.
- Clicking or keyboard-activating a card opens the existing project detail panel.
- Layout works on mobile and desktop without overflow bugs.
- Search/navigation language no longer references **Active Projects**.
## Notes for Implementation
- Thumbnail assets are intentionally deferred; implement with placeholders now.
- Keep the component name `ProjectsTile` for this pass to minimize refactor risk; rename component/file in a later cleanup task if desired.
-120
View File
@@ -1,120 +0,0 @@
# Reference: Task 1 — Design Tokens and Tailwind Config
## Overview
Update the design system from the dark-sidebar NHS Blue palette to the GP System concept's light teal palette. The concept reference is `References/GPSystemconcept.html`.
## CSS Custom Properties (`src/index.css`)
Add/update these variables in the PMR section (keep boot/ECG/login variables unchanged):
```css
/* GP System Dashboard tokens */
--bg: #F0F5F4;
--surface: #FFFFFF;
--sidebar-bg: #F7FAFA;
--text-primary: #1A2B2A;
--text-secondary: #5B7A78;
--text-tertiary: #8DA8A5;
--accent: #0D6E6E;
--accent-hover: #0A8080;
--accent-light: rgba(10,128,128,0.08);
--accent-border: rgba(10,128,128,0.18);
--amber: #D97706;
--amber-light: rgba(217,119,6,0.08);
--amber-border: rgba(217,119,6,0.18);
--success: #059669;
--success-light: rgba(5,150,105,0.08);
--success-border: rgba(5,150,105,0.18);
--alert: #DC2626;
--alert-light: rgba(220,38,38,0.08);
--alert-border: rgba(220,38,38,0.18);
--border: #D4E0DE;
--border-light: #E4EDEB;
--sidebar-width: 272px;
--topbar-height: 48px;
--radius: 8px;
--radius-sm: 6px;
--shadow-sm: 0 1px 2px rgba(26,43,42,0.05);
--shadow-md: 0 2px 8px rgba(26,43,42,0.08);
--shadow-lg: 0 8px 32px rgba(26,43,42,0.12);
--font-body: var(--font-ui);
--font-mono: 'Geist Mono', 'Fira Code', monospace;
```
## Tailwind Config (`tailwind.config.js`)
Update the `extend` section:
### Colors
```js
colors: {
'pmr-bg': '#F0F5F4',
'pmr-surface': '#FFFFFF',
'pmr-sidebar': '#F7FAFA',
'pmr-accent': '#0D6E6E',
'pmr-accent-hover': '#0A8080',
'pmr-text-primary': '#1A2B2A',
'pmr-text-secondary': '#5B7A78',
'pmr-text-tertiary': '#8DA8A5',
'pmr-border': '#D4E0DE',
'pmr-border-light': '#E4EDEB',
'pmr-success': '#059669',
'pmr-amber': '#D97706',
'pmr-alert': '#DC2626',
'pmr-purple': '#7C3AED',
// Keep pmr-nhsblue for backward compat during transition
'pmr-nhsblue': '#005EB8',
// Keep pmr-content as fallback
'pmr-content': '#F0F5F4',
}
```
### Shadows
```js
boxShadow: {
'pmr-sm': '0 1px 2px rgba(26,43,42,0.05)',
'pmr-md': '0 2px 8px rgba(26,43,42,0.08)',
'pmr-lg': '0 8px 32px rgba(26,43,42,0.12)',
// Keep old pmr shadow as alias during transition
'pmr': '0 1px 2px rgba(26,43,42,0.05)',
}
```
### Border Radius
```js
borderRadius: {
'card': '8px', // was 4px — now 8px per concept
'card-sm': '6px', // inner elements
'login': '12px', // login card exception
}
```
## Existing Tokens to Replace/Update
The Tailwind config and CSS already have tokens from the old PMR design. Task 1 needs to UPDATE these, not just add new ones alongside:
**Existing Tailwind shadow tokens (replace with new three-tier system):**
- `pmr`: `'0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.03)'` → replace with `pmr-sm`
- `pmr-hover`: `'0 2px 4px rgba(0,0,0,0.06), 0 8px 16px rgba(0,0,0,0.04)'` → replace with `pmr-md`
- `pmr-banner`: `'0 2px 8px rgba(0,0,0,0.12)'` → remove (no banner in new design)
**Existing Tailwind color tokens (keep during transition, Task 21 cleans up):**
- `pmr-nhsblue: '#005EB8'` — keep for login screen (still uses NHS blue)
- `pmr-content: '#F5F7FA'` → update to `pmr-content: '#F0F5F4'` (new bg color)
- `pmr-sidebar: '#1E293B'` → update to `pmr-sidebar: '#F7FAFA'` (light sidebar)
**Existing CSS custom properties (in `--pmr-*` namespace):**
- Previous iterations added `--pmr-*` variables. The new tokens use shorter names (e.g., `--bg`, `--surface`, `--accent`). Add the new tokens AND keep `--pmr-*` aliases during transition so existing components don't break before they're rebuilt.
**Existing border-radius tokens:**
- `card: '4px'` → update to `card: '8px'`
- `login: '12px'` — keep unchanged
## What NOT to Change
- Boot phase variables (`--matrix-*`, `--terminal-*`)
- ECG phase variables
- Login phase background (`#1E293B` — handled by transition)
- Font declarations (Elvaro, Blumir, Geist Mono, Fira Code already set up correctly)
- Breakpoint values
-203
View File
@@ -1,203 +0,0 @@
# Reference: Task 2 — Data Files and Types
## Overview
Create new data files for dashboard-specific content and update the type system. All CV content must match `References/CV_v4.md` exactly.
## New Data Files
### `src/data/profile.ts`
```typescript
export const personalStatement = `Healthcare leader combining clinical pharmacy expertise with proficiency in Python, SQL, and data analytics, self-taught over the past decade through a drive to find root causes in data and build the most efficient solutions to complex problems. Currently leading population health analytics for NHS Norfolk & Waveney ICB, serving a population of 1.2 million. Experienced in working with messy, real-world prescribing data at scale to deliver actionable insights—from financial scenario modelling and pharmaceutical rebate negotiation to algorithm design and population-level pathway development. Proven track record of identifying and prioritising efficiency programmes worth £14.6M+ through automated, data-driven analysis. Skilled at translating complex clinical, financial, and analytical requirements into clear recommendations for executive stakeholders.`
```
### `src/data/tags.ts`
```typescript
import type { Tag } from '@/types/pmr'
export const tags: Tag[] = [
{ label: 'Pharmacist', colorVariant: 'teal' },
{ label: 'Data Lead', colorVariant: 'teal' },
{ label: 'NHS', colorVariant: 'teal' },
{ label: 'Population Health', colorVariant: 'amber' },
{ label: 'BI & Analytics', colorVariant: 'green' },
]
```
### `src/data/alerts.ts`
```typescript
import type { Alert } from '@/types/pmr'
export const alerts: Alert[] = [
{
message: '£14.6M SAVINGS IDENTIFIED',
severity: 'alert',
icon: 'AlertTriangle', // lucide-react icon name
},
{
message: '£220M BUDGET OVERSIGHT',
severity: 'amber',
icon: 'AlertCircle', // lucide-react icon name
},
]
```
### `src/data/kpis.ts`
```typescript
import type { KPI } from '@/types/pmr'
export const kpis: KPI[] = [
{
id: 'budget',
value: '£220M',
label: 'Budget Oversight',
sub: 'NHS prescribing',
colorVariant: 'green',
explanation: 'Managed the ICB\'s total prescribing budget with sophisticated forecasting models identifying cost pressures and enabling proactive financial planning across Norfolk & Waveney.',
},
{
id: 'savings',
value: '£14.6M',
label: 'Efficiency Savings',
sub: 'Identified & tracked',
colorVariant: 'amber',
explanation: 'Identified and prioritised a £14.6M efficiency programme through comprehensive data analysis; achieved over-target performance through targeted, evidence-based interventions across the integrated care system.',
},
{
id: 'years',
value: '9+',
label: 'Years in NHS',
sub: 'Since 2016',
colorVariant: 'teal',
explanation: 'Continuous NHS service since August 2016, progressing from community pharmacy through prescribing data analysis to system-level population health data leadership.',
},
{
id: 'team',
value: '12',
label: 'Team Size Led',
sub: 'Cross-functional',
colorVariant: 'green',
explanation: 'Led a cross-functional team of 12 spanning data analysts, population health specialists, and pharmacists across data, analytics, and population health workstreams.',
},
]
```
### `src/data/skills.ts`
Skills presented as "medications" with frequency (user-specified values) and years of experience.
```typescript
import type { SkillMedication } from '@/types/pmr'
export const skills: SkillMedication[] = [
{
id: 'data-analysis',
name: 'Data Analysis',
frequency: 'Twice daily',
startYear: 2016,
yearsOfExperience: 9,
proficiency: 95,
category: 'Technical',
status: 'Active',
icon: 'BarChart3',
},
{
id: 'python',
name: 'Python',
frequency: 'Daily',
startYear: 2019,
yearsOfExperience: 6,
proficiency: 90,
category: 'Technical',
status: 'Active',
icon: 'Code2',
},
{
id: 'sql',
name: 'SQL',
frequency: 'Daily',
startYear: 2018,
yearsOfExperience: 7,
proficiency: 88,
category: 'Technical',
status: 'Active',
icon: 'Database',
},
{
id: 'power-bi',
name: 'Power BI',
frequency: 'Once weekly',
startYear: 2020,
yearsOfExperience: 5,
proficiency: 92,
category: 'Technical',
status: 'Active',
icon: 'PieChart',
},
{
id: 'javascript-typescript',
name: 'JavaScript / TypeScript',
frequency: 'When required',
startYear: 2022,
yearsOfExperience: 3,
proficiency: 70,
category: 'Technical',
status: 'Active',
icon: 'FileCode2',
},
]
```
Note: Additional domain/leadership skills can be added later. Start with the 5 technical skills the user specified frequencies for.
## Type Updates (`src/types/pmr.ts`)
Add these interfaces (keep all existing types):
```typescript
export interface Tag {
label: string
colorVariant: 'teal' | 'amber' | 'green'
}
export interface Alert {
message: string
severity: 'alert' | 'amber'
icon: string
}
export interface KPI {
id: string
value: string
label: string
sub: string
colorVariant: 'green' | 'amber' | 'teal'
explanation: string
}
export interface SkillMedication {
id: string
name: string
frequency: string
startYear: number
yearsOfExperience: number
proficiency: number
category: 'Technical' | 'Domain' | 'Leadership'
status: 'Active' | 'Historical'
icon: string
}
```
## Existing Data — No Changes
These files remain untouched:
- `src/data/patient.ts`
- `src/data/consultations.ts`
- `src/data/medications.ts`
- `src/data/problems.ts`
- `src/data/investigations.ts`
- `src/data/documents.ts`
-147
View File
@@ -1,147 +0,0 @@
# Reference: Tasks 4-6 — TopBar and Sidebar
## Concept Reference
All specs below are derived from `References/GPSystemconcept.html`. Open it in a browser for visual reference.
---
## Task 4: TopBar Component
### File: `src/components/TopBar.tsx`
### Structure
```
┌─────────────────────────────────────────────────────────────┐
│ [🏠] Headhunt Medical Center Remote │ [🔍 Search... Ctrl+K] │ Dr. A.CHARLWOOD · Active Session · 12:23 [Ctrl+K] │
└─────────────────────────────────────────────────────────────┘
```
### Specs
**Container:**
- `position: fixed`, `top: 0`, `left: 0`, `right: 0`
- `height: var(--topbar-height)` (48px)
- `background: var(--surface)` (#FFFFFF)
- `border-bottom: 1px solid var(--border)` (#D4E0DE)
- `display: flex`, `align-items: center`, `justify-content: space-between`
- `padding: 0 20px`
- `z-index: 100`
**Brand (left):**
- `Home` icon from lucide-react (18px, accent color)
- Text: "Headhunt Medical Center" — 13px, font-ui, 600 weight, text-primary
- Version badge: "Remote" — 11px, 400 weight, text-tertiary, margin-left 2px
**Search bar (center):**
- Wrapper: `max-width: 560px`, `min-width: 400px`
- Container: `height: 42px`, `border: 1.5px solid var(--border)`, `border-radius: var(--radius)` (8px), `padding: 0 14px`, white bg
- Search icon (16px, tertiary) + input + "Ctrl+K" kbd badge
- Input: 13px, font-body, placeholder "Search records, experience, skills... (Ctrl+K)"
- Hover: `border-color: var(--accent-border)`
- Focus: `border-color: var(--accent)`, `box-shadow: 0 0 0 3px rgba(13,110,110,0.12)`
- **On click/focus: opens Command Palette** (Task 18). Does NOT do inline search.
- Kbd badge: mono font, 10px, tertiary, bg: var(--bg), border, padding 2px 6px, radius 4px
**Session info (right):**
- Text: "Dr. A.CHARLWOOD · Active Session · [time]" — 12px, text-secondary
- Session pill: mono 11px, tertiary, `background: var(--accent-light)`, `padding: 3px 10px`, radius 4px, `border: 1px solid var(--accent-border)`
- Ctrl+K shortcut badge (same style as search bar badge)
**Responsive:**
- Mobile (<768px): hide center search bar. Show only brand + session info (or hamburger).
- Tablet: search bar may shrink.
---
## Task 5: Sidebar — PersonHeader
### File: `src/components/Sidebar.tsx`
### Overall Sidebar Container
- `width: var(--sidebar-width)` (272px)
- `min-width: var(--sidebar-width)`
- `background: var(--sidebar-bg)` (#F7FAFA)
- `border-right: 1px solid var(--border)` (#D4E0DE)
- `overflow-y: auto`, custom scrollbar (4px width, transparent track, border-colored thumb)
- `padding: 20px 16px`
- `display: flex`, `flex-direction: column`, `gap: 2px`
### PersonHeader Section
Bordered below: `border-bottom: 2px solid var(--accent)`, `padding-bottom: 16px`, `margin-bottom: 6px`
**Avatar:**
- 52px × 52px circle
- `background: linear-gradient(135deg, var(--accent), #0A8080)`
- White text "AC", 700 weight, 18px, centered
- `box-shadow: 0 2px 8px rgba(13,110,110,0.25)`
- `margin-bottom: 12px`
**Name:**
- "CHARLWOOD, Andrew"
- 15px, 700 weight, text-primary, `letter-spacing: -0.01em`
**Title:**
- "Pharmacy Data Technologist"
- 11.5px, mono font, 400 weight, text-secondary
- `margin-top: 2px`
**Status badge:**
- Inline-flex, gap 5px
- `margin-top: 8px`
- 11px, 500 weight, success color (#059669)
- `background: var(--success-light)`, `border: 1px solid var(--success-border)`
- `padding: 3px 9px`, `border-radius: 20px` (pill)
- Animated dot: 6px circle, success color, `animation: pulse 2s infinite` (opacity 1→0.4→1)
- Text: "Open to Opportunities"
**Details grid:**
- `display: grid`, `grid-template-columns: 1fr`, `gap: 6px`, `margin-top: 12px`
- Each row: `display: flex`, `justify-content: space-between`, `align-items: center`, 11.5px, `padding: 2px 0`
- Label: text-tertiary, 400 weight
- Value: text-primary, 500 weight, text-align right
- GPhC No. value: mono font, 11px, `letter-spacing: 0.12em` → "2211810"
- Education value: "MPharm 2.1 (Hons)"
- Location: "Norwich, Norfolk"
- Phone: link in accent color, `text-decoration: none`, underline on hover → "07795 553 088"
- Email: link → "andy@charlwood.xyz"
- Registered: "August 2016"
**Data source:** `src/data/patient.ts`
---
## Task 6: Sidebar — Tags + Alerts
### Section Title Component
Reusable within sidebar. Used for "Tags", "Alerts / Highlights", and any future sections.
- `font-size: 10px`, `font-weight: 600`, `text-transform: uppercase`, `letter-spacing: 0.08em`
- Color: text-tertiary
- `margin-bottom: 10px`
- Flex row with `::after` pseudo-element: `flex: 1`, `height: 1px`, `background: var(--border-light)`, `gap: 6px`
### Tags Section
- Container: `display: flex`, `flex-wrap: wrap`, `gap: 5px`
- Each tag: 10.5px, 500 weight, `padding: 3px 8px`, `border-radius: 4px`, inline-flex, `line-height: 1.3`
- **Color variants:**
- `teal`: `background: var(--accent-light)`, `color: var(--accent)`, `border: 1px solid var(--accent-border)`
- `amber`: `background: var(--amber-light)`, `color: var(--amber)`, `border: 1px solid var(--amber-border)`
- `green`: `background: var(--success-light)`, `color: var(--success)`, `border: 1px solid var(--success-border)`
- **Data source:** `src/data/tags.ts`
### Alerts / Highlights Section
- Container: `display: flex`, `flex-direction: column`, `gap: 6px`
- Each flag item: `display: flex`, `align-items: center`, `gap: 8px`
- 11px, 700 weight, `padding: 7px 10px`, `border-radius: var(--radius-sm)` (6px), `letter-spacing: 0.02em`
- **Alert variant** (red):
- `background: var(--alert-light)`, `color: var(--alert)`, `border: 1px solid var(--alert-border)`
- Icon: `AlertTriangle` from lucide-react (14px, 2.5 stroke-width)
- **Amber variant:**
- `background: var(--amber-light)`, `color: var(--amber)`, `border: 1px solid var(--amber-border)`
- Icon: `AlertCircle` from lucide-react (14px, 2.5 stroke-width)
- Icon container: 16px square, flex center, flex-shrink-0
- **Data source:** `src/data/alerts.ts`
### Section Padding
Each sidebar section: `padding: 14px 0 6px`
-163
View File
@@ -1,163 +0,0 @@
# Reference: Task 7 — DashboardLayout
## Overview
Create the main layout component that replaces `PMRInterface.tsx`. This is the container that houses TopBar, Sidebar, and the scrollable card grid of tiles.
## File: `src/components/DashboardLayout.tsx`
### Layout Structure
```
┌────────────────────────────────────────────────────┐
│ TopBar (fixed, z-100, height: 48px) │
├──────────┬─────────────────────────────────────────┤
│ │ │
│ Sidebar │ <main> — scrollable card grid │
│ (272px) │ padding: 24px 28px 40px │
│ fixed │ │
│ │ grid: 1fr 1fr, gap: 16px │
│ │ │
│ │ [PatientSummary — full] │
│ │ [LatestResults] [CoreSkills] │
│ │ [LastConsultation — full] │
│ │ [CareerActivity — full] │
│ │ [Education — full] │
│ │ [Projects — full] │
│ │ │
└──────────┴─────────────────────────────────────────┘
```
### CSS Layout
```
.layout {
display: flex;
margin-top: var(--topbar-height); /* 48px */
height: calc(100vh - var(--topbar-height));
}
.sidebar {
/* See ref-03-topbar-sidebar.md for sidebar specs */
width: var(--sidebar-width);
min-width: var(--sidebar-width);
/* ... */
}
.main {
flex: 1;
overflow-y: auto;
padding: 24px 28px 40px;
}
.card-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 900px) {
.card-grid {
grid-template-columns: 1fr;
}
}
```
Use Tailwind classes for all of this — the CSS above is for reference only.
### Framer Motion Entrance Animations
Staggered entrance when dashboard first renders (after login):
1. **TopBar**: slides down from `-48px`, 200ms ease-out
2. **Sidebar**: slides from `-272px` left, 250ms ease-out, 50ms delay
3. **Main content**: fades in (opacity 0→1), 300ms, 150ms delay
```typescript
const topbarVariants = {
hidden: { y: -48, opacity: 0 },
visible: { y: 0, opacity: 1, transition: { duration: 0.2, ease: 'easeOut' } }
}
const sidebarVariants = {
hidden: { x: -272, opacity: 0 },
visible: { x: 0, opacity: 1, transition: { duration: 0.25, ease: 'easeOut', delay: 0.05 } }
}
const contentVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.3, delay: 0.15 } }
}
```
With `prefers-reduced-motion`: all durations → 0, no delays.
### Tile Ordering in Grid
The card grid renders tiles in this order:
1. `PatientSummaryTile``grid-column: 1 / -1` (full width)
2. `LatestResultsTile` — single column (left)
3. `CoreSkillsTile` — single column (right)
4. `LastConsultationTile``grid-column: 1 / -1` (full width)
5. `CareerActivityTile``grid-column: 1 / -1` (full width)
6. `EducationTile``grid-column: 1 / -1` (full width)
7. `ProjectsTile``grid-column: 1 / -1` (full width)
### App.tsx Wiring
In `src/App.tsx`, the PMR phase currently renders `<PMRInterface />`. Change it to render `<DashboardLayout />`.
```typescript
// In App.tsx phase switch:
case 'pmr':
return <DashboardLayout />
```
Keep all other phases (boot, ecg, login) unchanged. The SkipButton that skips to login should still work.
### Scrollbar Styling
Main content area scrollbar (matches concept):
- Width: 6px
- Track: transparent
- Thumb: var(--border) (#D4E0DE), border-radius 3px
### Command Palette Integration
The DashboardLayout should render the `CommandPalette` component (from Task 18) at the layout level, so it overlays the entire dashboard when triggered. For now (Task 7), just add a placeholder comment or empty div where it will go. The TopBar search bar's click handler should be wired to open the palette (but the palette itself comes in Task 18).
### Background Color Transition
The login screen has background `#1E293B`. The dashboard has background `#F0F5F4`. This transition should happen smoothly. Options:
1. The DashboardLayout entrance animation covers the transition (content fades in over the dark background, replacing it)
2. A brief CSS transition on the body/root background color
3. Handle it in App.tsx with a state-based background
The simplest approach is option 1 — the dashboard's entrance animation effectively replaces the dark login background with the light dashboard.
---
## Established Patterns (from previous iterations)
These patterns were established across 16 iterations of the old PMR build. Reuse them:
### Phase name is `'pmr'`
The Phase type in `src/types/index.ts` is `'boot' | 'ecg' | 'login' | 'pmr'`. The `'pmr'` case renders the dashboard. Do NOT rename the phase — just change what it renders.
### Module-scope `prefersReducedMotion`
All animation components should compute this once at module level, not per render:
```typescript
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
```
This is the established pattern across all existing view components.
### Pre-existing ESLint warning
`AccessibilityContext.tsx` has 1 pre-existing ESLint warning. This is expected — do not attempt to fix it. Quality checks pass with this warning present.
### Callback ref pattern for Framer Motion
If you need a ref to a `motion.*` element (e.g., for scroll detection), use `useState` + callback ref instead of `useRef`. Framer Motion elements may not be in the DOM when `useEffect` first runs:
```typescript
const [scrollContainer, setScrollContainer] = useState<HTMLElement | null>(null)
// On the element: ref={el => { if (el) setScrollContainer(el) }}
```
This avoids null ref issues with animated mount timing.
-144
View File
@@ -1,144 +0,0 @@
# Reference: Tasks 8-11 — Card Component and Top Tiles
## Task 8: Reusable Card Component
### File: `src/components/Card.tsx`
### Base Card
```typescript
interface CardProps {
children: React.ReactNode
full?: boolean // spans both grid columns
className?: string
}
```
**Styling:**
- `background: var(--surface)` (#FFFFFF)
- `border: 1px solid var(--border-light)` (#E4EDEB)
- `border-radius: var(--radius)` (8px)
- `padding: 20px`
- `box-shadow: var(--shadow-sm)` (0 1px 2px rgba(26,43,42,0.05))
- Hover: `box-shadow: var(--shadow-md)`, `border-color: var(--border)` (#D4E0DE)
- `transition: box-shadow 0.2s, border-color 0.2s`
- Full variant: `grid-column: 1 / -1`
### CardHeader Sub-component
```typescript
interface CardHeaderProps {
dotColor: 'teal' | 'amber' | 'green' | 'alert' | 'purple'
title: string
rightText?: string
}
```
**Styling:**
- `display: flex`, `align-items: center`, `gap: 8px`, `margin-bottom: 16px`
- Dot: 8px circle, `border-radius: 50%`, flex-shrink-0
- teal: `#0D6E6E`, amber: `#D97706`, green: `#059669`, alert: `#DC2626`, purple: `#7C3AED`
- Title: 12px, 600 weight, uppercase, `letter-spacing: 0.06em`, text-secondary (#5B7A78)
- Right text (optional): 10px, 400 weight, normal case, no tracking, text-tertiary, mono font, `margin-left: auto`
---
## Task 9: PatientSummary Tile
### File: `src/components/tiles/PatientSummaryTile.tsx`
**Layout:** Full-width card, first in grid.
**Content:**
- CardHeader: teal dot + "PATIENT SUMMARY"
- Body: personal statement text from `src/data/profile.ts`
- Typography: 13px, font-ui, `line-height: 1.6` (leading-relaxed), text-primary
- No interactive elements — read-only
**Data:** `import { personalStatement } from '@/data/profile'`
This is a simple tile. No expansion, no interactivity.
---
## Task 10: LatestResults Tile
### File: `src/components/tiles/LatestResultsTile.tsx`
**Layout:** Half-width card (single grid column). Sits in the LEFT column.
**Content:**
- CardHeader: teal dot + "LATEST RESULTS" + right text "Updated May 2025"
- 2×2 metric grid inside
**Metric Grid:**
- `display: grid`, `grid-template-columns: 1fr 1fr`, `gap: 12px`
**Each Metric Card:**
- `padding: 14px`, `border-radius: var(--radius-sm)` (6px)
- `border: 1px solid var(--border-light)`, `background: var(--bg)` (#F0F5F4)
- Value: 22px, 700 weight, `letter-spacing: -0.02em`, `line-height: 1.2`
- Color by variant: green=#059669, amber=#D97706, teal=#0D6E6E
- Label: 11px, text-secondary, 500 weight, `margin-top: 3px`
- Sub: 10px, text-tertiary, mono font, `margin-top: 4px`
**Data:** `import { kpis } from '@/data/kpis'`
**KPI flip prep:** Each metric card should accept a `data-kpi-id` or an `onClick` prop placeholder — Task 17 will add the flip interaction. For now, render as static display.
**Values:**
| Value | Label | Sub | Color |
|-------|-------|-----|-------|
| £220M | Budget Oversight | NHS prescribing | green |
| £14.6M | Efficiency Savings | Identified & tracked | amber |
| 9+ | Years in NHS | Since 2016 | teal |
| 12 | Team Size Led | Cross-functional | green |
---
## Task 11: CoreSkills Tile ("Repeat Medications")
### File: `src/components/tiles/CoreSkillsTile.tsx`
**Layout:** Half-width card (single grid column). Sits in the RIGHT column, next to LatestResults.
**Content:**
- CardHeader: amber dot + "REPEAT MEDICATIONS"
- Vertical list of skill items, `gap: 10px`
**Each Skill Item:**
Matches the concept's `.dev-item` pattern:
- `display: flex`, `align-items: center`, `gap: 10px`
- 12.5px font, `padding: 10px 12px`
- `background: var(--bg)` (#F0F5F4), `border-radius: var(--radius-sm)` (6px)
- `border: 1px solid var(--border-light)`
**Item structure:**
- **Icon container** (28px square, 6px radius):
- `background: var(--accent-light)`, `color: var(--accent)` (teal)
- Lucide icon inside (14px): `BarChart3` for Data Analysis, `Code2` for Python, `Database` for SQL, `PieChart` for Power BI, `FileCode2` for JS/TS
- **Text block** (flex: 1):
- Name: 600 weight, text-primary (e.g., "Data Analysis")
- Frequency + years: 11px, text-tertiary, mono font (e.g., "Twice daily · Since 2016 · 9 yrs")
- **Optional status badge**: 10px, 500 weight, pill shape (padding 3px 8px, border-radius 20px), flex-shrink-0
- Could show proficiency or "Active" status
**Medication metaphor format:**
```
[📊] Data Analysis Active
Twice daily · Since 2016 · 9 yrs
[💻] Python Active
Daily · Since 2019 · 6 yrs
[🗄️] SQL Active
Daily · Since 2018 · 7 yrs
[📈] Power BI Active
Once weekly · Since 2020 · 5 yrs
[📝] JavaScript / TypeScript Active
When required · Since 2022 · 3 yrs
```
**Data:** `import { skills } from '@/data/skills'`
**Expansion prep:** Each item should accept an onClick prop placeholder — Task 16 will add expansion to show prescribing history (from existing medications data).
-204
View File
@@ -1,204 +0,0 @@
# Reference: Tasks 12-15 — Bottom Tiles
## Task 12: LastConsultation Tile
### File: `src/components/tiles/LastConsultationTile.tsx`
**Layout:** Full-width card.
**Content:**
- CardHeader: green dot + "LAST CONSULTATION" + right text "Most recent role"
**Header info row:**
- `display: flex`, `flex-wrap: wrap`, `gap: 16px`
- `margin-bottom: 14px`, `padding-bottom: 14px`, `border-bottom: 1px solid var(--border-light)`
- Each field:
- Label: 10px, uppercase, `letter-spacing: 0.06em`, text-tertiary
- Value: 11.5px, 600 weight, text-primary
| Label | Value |
|-------|-------|
| Date | May 2025 |
| Organisation | NHS Norfolk & Waveney ICB |
| Type | Permanent · Full-time |
| Band | 8a |
**Role title:**
- "Interim Head, Population Health & Data Analysis"
- 13.5px, 600 weight, `color: var(--accent)` (#0D6E6E)
- `margin-bottom: 12px`
**Bullet list:**
- `list-style: none`, flex column, `gap: 7px`
- Each bullet: 12.5px, text-primary, `padding-left: 16px`, `line-height: 1.5`
- Pseudo `::before`: 5px circle, accent color (#0D6E6E), `opacity: 0.5`, positioned left at top 7px
**Bullets** (from first consultation's examination array):
- Led a cross-functional team of 12 across data, analytics, and population health workstreams
- Oversaw £220M prescribing budget with full analytical accountability and reporting to ICB board
- Identified £14.6M in efficiency savings through data-driven prescribing interventions
- Designed and deployed Power BI dashboards used by 200+ clinicians and commissioners
- Spearheaded SQL analytics transformation, migrating legacy Access databases to modern data stack
- Established team data literacy programme, upskilling 30+ non-technical staff in data interpretation
**Data:** `import { consultations } from '@/data/consultations'` — use `consultations[0]` (the most recent).
Map consultation fields:
- date → Date field
- organization → Organisation field
- role → Role title
- examination array → Bullet points
---
## Task 13: CareerActivity Tile
### File: `src/components/tiles/CareerActivityTile.tsx`
**Layout:** Full-width card.
**Content:**
- CardHeader: teal dot + "CAREER ACTIVITY" + right text "Full timeline"
**Activity grid:**
- `display: grid`, `grid-template-columns: 1fr 1fr`, `gap: 10px`
- Below 900px: `grid-template-columns: 1fr` (single column)
**Each activity item:**
- `display: flex`, `gap: 10px`
- `padding: 10px 12px`
- `background: var(--bg)` (#F0F5F4)
- `border-radius: var(--radius-sm)` (6px)
- `border: 1px solid var(--border-light)`
- 12px font
- `transition: border-color 0.15s`
- Hover: `border-color: var(--accent-border)`
**Dot (left):**
- 8px circle, flex-shrink-0, `margin-top: 2px` (aligns with text)
- Color by type:
- Role: teal (#0D6E6E)
- Project: amber (#D97706)
- Certification: green (#059669)
- Education: purple (#7C3AED)
**Content (right):**
- Title: 600 weight, text-primary, `line-height: 1.3`
- Meta: 11px, text-secondary, `margin-top: 2px`
- Date: 10px, mono font, text-tertiary, `margin-top: 3px`
**Building the timeline data:**
Merge entries from multiple data sources, sorted newest-first:
```typescript
type ActivityType = 'role' | 'project' | 'cert' | 'edu'
interface ActivityEntry {
id: string
type: ActivityType
title: string
meta: string
date: string
sortYear: number // for sorting
}
```
Sources:
1. `consultations` → type "role": title=role, meta=organization, date=duration
2. `investigations` (selected key ones) → type "project": title=name, meta=short description, date=year
3. `documents` where type='Certificate' → type "cert": title=title, meta=source, date=date
4. `documents` where type='Results' (MPharm) → type "edu": title=title, meta=source, date=date
Match the concept HTML entries:
| Type | Title | Meta | Date |
|------|-------|------|------|
| role | Interim Head, Population Health & Data Analysis | NHS Norfolk & Waveney ICB | 2024 2025 |
| project | £220M Prescribing Budget Oversight | Lead analyst & budget owner | 2024 |
| role | Senior Data Analyst — Medicines Optimisation | NHS Norfolk & Waveney ICB | 2021 2024 |
| project | SQL Analytics Transformation | Legacy migration project lead | 2025 |
| cert | Power BI Data Analyst Associate | Microsoft Certified | 2023 |
| role | Prescribing Data Pharmacist | NHS Norwich CCG | 2018 2021 |
| cert | Clinical Pharmacy Diploma | Professional development | 2019 |
| role | Community Pharmacist | Boots UK | 2016 2018 |
| edu | MPharm (Hons) — 2:1 | University of East Anglia | 2011 2015 |
| cert | GPhC Registration | General Pharmaceutical Council | August 2016 |
**Expansion prep:** Activity items should accept onClick for Task 16 (expand to show full role/project detail).
---
## Task 14: Education Tile
### File: `src/components/tiles/EducationTile.tsx`
**Layout:** Full-width card, below Career Activity.
**Content:**
- CardHeader: purple dot (#7C3AED) + "EDUCATION"
**Education entries:**
Vertical stack of education items.
Each item:
- `padding: 7px 10px`
- `background: var(--surface)` (#FFFFFF)
- `border: 1px solid var(--border-light)`
- `border-radius: var(--radius-sm)` (6px)
- 11.5px, text-primary
Structure:
- Degree name: 600 weight, `display: block`
- Detail: text-secondary, 11px, `margin-top: 2px`
**Entries** (from CV):
| Degree | Detail |
|--------|--------|
| MPharm (Hons) — 2:1 | University of East Anglia · 2015 |
| NHS Leadership Academy — Mary Seacole Programme | 2018 · 78% |
| A-Levels: Mathematics (A*), Chemistry (B), Politics (C) | Highworth Grammar School · 20092011 |
**Data:** Filter `src/data/documents.ts` for education entries, or hardcode from CV since the documents data may not have all education entries.
Note: The concept HTML only shows the MPharm entry. But the CV has more education. Include all CV education entries.
---
## Task 15: Projects Tile
### File: `src/components/tiles/ProjectsTile.tsx`
**Layout:** Full-width card, prominent position.
**Content:**
- CardHeader: amber dot + "ACTIVE PROJECTS"
**Project entries:**
Vertical list, styled as interactive items.
Each project:
- `display: flex`, `align-items: flex-start`, `gap: 8px`
- `padding: 7px 10px`
- `background: var(--surface)`, `border: 1px solid var(--border-light)`
- `border-radius: var(--radius-sm)` (6px)
- 11.5px, text-primary
- Hover: `border-color: var(--accent-border)`
- `transition: border-color 0.15s`
Structure:
- **Status dot** (7px circle, flex-shrink-0, `margin-top: 4px`):
- Complete: success (#059669)
- Ongoing: accent (#0D6E6E)
- Live: success with pulse animation
- **Project name**: text-primary, flex 1
- **Year badge**: 10px, mono font, text-tertiary, `margin-left: auto`, flex-shrink-0
**Data:** `import { investigations } from '@/data/investigations'`
Map investigations to projects:
- name → Project name
- status → dot color
- requestedYear → Year badge
- resultSummary → Available for expansion (Task 16)
**Expansion prep:** Each item should accept onClick for Task 16 (expand to show methodology, tech stack, results).
-259
View File
@@ -1,259 +0,0 @@
# Reference: Tasks 16-18 — Interactions
## Task 16: Tile Expansion System
### Overview
Three tiles have expandable items: CareerActivity (roles), Projects, and CoreSkills. Clicking an item expands it in-place to reveal detail, like expanding a clinical record entry.
### Expansion Pattern (consistent across all tiles)
**Animation:**
- Framer Motion `AnimatePresence` + `motion.div`
- Height-only animation: 200ms, ease-out
- **No opacity fade on content** (guardrail)
- `overflow: hidden` on the animated container
```typescript
<AnimatePresence initial={false}>
{isExpanded && (
<motion.div
initial={{ height: 0 }}
animate={{ height: 'auto' }}
exit={{ height: 0 }}
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.2, ease: 'easeOut' }}
style={{ overflow: 'hidden' }}
>
{/* expanded content */}
</motion.div>
)}
</AnimatePresence>
```
**Behavior:**
- Single-expand accordion: only one item expanded at a time within each tile
- Click expanded item again to collapse
- Click different item: collapses current, expands new
- State: `expandedItemId: string | null` in each tile component
**Keyboard:**
- Enter/Space: toggle expand/collapse
- Escape: collapse current item
- `aria-expanded` on each clickable item
**Visual:**
- Expanded content has slightly different background (`var(--bg)` or subtle border-left)
- Colored left border on expanded panel (accent color for roles, amber for projects, teal for skills)
- Content padding: 12-16px
### CareerActivity Expansion (roles)
When a role-type activity item is expanded:
- Show full role details from corresponding consultation entry
- Structure: role title, organization, date range
- Achievement bullets (examination array from consultation)
- Coded entries if available
- Match expanded content to `consultations` data by mapping activity item to consultation
### Projects Expansion
When a project item is expanded:
- Show from investigation data:
- Methodology
- Tech stack (as tags or inline list)
- Results (bulleted)
- External URL link if available ("View Results" button)
### CoreSkills Expansion
When a skill item is expanded:
- Show "prescribing history" — a timeline of skill development
- **Data source:** `import { medications } from '@/data/medications'` (NOT `skills.ts`). The `medications.ts` file has 18 entries, each with a `prescribingHistory` array of `{ year, description }` entries. Map from `skills.ts` to `medications.ts` by matching skill name to medication name (e.g., "Data Analysis" in skills.ts → find the medication with `name: "Data Analysis"` in medications.ts to get its `prescribingHistory`).
- Format: vertical timeline with year markers and descriptions
- Timeline dots: accent color, 6px, with connecting line
- Year: mono font, 12px, semibold
- Description: 12px, regular
---
## Task 17: KPI Flip Cards
### Overview
In the LatestResults tile, each metric card can be clicked to "flip" and reveal an explanation of that KPI.
### Flip Animation
**CSS Perspective approach:**
```css
.metric-card {
perspective: 1000px;
cursor: pointer;
}
.metric-card-inner {
transition: transform 0.4s ease-in-out;
transform-style: preserve-3d;
position: relative;
}
.metric-card-inner.flipped {
transform: rotateY(180deg);
}
.metric-card-front,
.metric-card-back {
backface-visibility: hidden;
position: absolute;
inset: 0;
}
.metric-card-back {
transform: rotateY(180deg);
}
```
Or use Framer Motion `animate={{ rotateY: isFlipped ? 180 : 0 }}` with `perspective` on parent.
**Behavior:**
- Click to flip front → back
- Click again to flip back → front
- Only one card flipped at a time (clicking another card flips the current one back)
- State: `flippedCardId: string | null` in LatestResultsTile
**Front face:** Current metric display (value + label + sub) — same as Task 10.
**Back face:**
- `background: var(--accent-light)` (subtle teal tint)
- `padding: 14px`
- Text: 12px, text-secondary, `line-height: 1.5`
- The explanation text from KPI data's `explanation` field
**Reduced motion:**
- No 3D flip animation
- Instant content swap (front → back)
- Could use a simple crossfade or just replace content immediately
**Keyboard:**
- Enter/Space to flip
- Each metric card should be `tabIndex={0}` with appropriate `aria-label`
**KPI Explanations** (from `src/data/kpis.ts`):
- £220M: Budget management with forecasting models
- £14.6M: Efficiency programme through data analysis
- 9+ Years: NHS service progression since 2016
- 12: Cross-functional team leadership
---
## Task 18: Command Palette
### File: `src/components/CommandPalette.tsx`
### Trigger
- **Ctrl+K** (global `keydown` listener on `document`)
- **Click** on TopBar search bar (or focus on search input)
- The TopBar search input does NOT do inline search — it opens the palette
### Overlay
- `position: fixed`, `inset: 0`
- `background: rgba(26,43,42,0.45)`
- `backdrop-filter: blur(4px)`
- `z-index: 1000`
- Fade in: `opacity: 0 → 1`, `visibility: hidden → visible`, 200ms transition
- Click overlay (outside modal) to close
### Palette Modal
- `width: 580px`, `max-height: 520px`
- `background: var(--surface)` (#FFFFFF)
- `border-radius: 12px`
- `box-shadow: 0 20px 60px rgba(26,43,42,0.2), 0 0 0 1px rgba(26,43,42,0.08)`
- `overflow: hidden`
- Entrance: `transform: scale(0.97) translateY(-8px)``scale(1) translateY(0)`, 200ms cubic-bezier
### Search Input
- Flex row: search icon (18px, accent) + input + "ESC" hint badge
- `padding: 14px 18px`, `border-bottom: 1px solid var(--border-light)`
- Input: 15px, font-body, placeholder "Search records, experience, skills..."
- ESC badge: mono 10px, tertiary, bg var(--bg), border, padding 2px 7px, radius 4px
### Results Area
- `overflow-y: auto`, `padding: 8px`, `flex: 1`
- Custom scrollbar (4px)
### Result Sections
Section label: 10px, 600 weight, uppercase, `letter-spacing: 0.08em`, text-tertiary, `padding: 8px 10px 5px`
### Result Items
- `display: flex`, `align-items: center`, `gap: 10px`
- `padding: 9px 10px`, `border-radius: var(--radius-sm)` (6px)
- `cursor: pointer`, `transition: background 0.1s`
- 13px, text-primary
- Hover/selected: `background: var(--accent-light)`
- Selected also gets: `outline: 1.5px solid var(--accent-border)`
**Item structure:**
- Icon container: 28px square, 6px radius, colored bg per section
- Experience: teal
- Core Skills: green
- Active Projects: amber
- Achievements: amber
- Education: purple
- Quick Actions: teal
- Text: title (500 weight) + subtitle (11px, tertiary, truncated)
- Optional badge: 10px, mono, tertiary
### Fuzzy Search
Adapt existing `src/lib/search.ts` (fuse.js v7.0.0, already installed):
**Existing code:** `src/lib/search.ts` has `buildSearchIndex()` which creates a Fuse index from consultations, medications, problems, investigations, and documents. It groups results by `sectionLabel` via `groupResultsBySection()`. The `SearchResult` interface has `{ id, title, section: ViewId, sectionLabel, highlight }`.
**What needs changing:**
- The `section: ViewId` field is designed for view-switching navigation (navigating to `#consultations`, `#medications`, etc.). The new dashboard has no views — it's a single scrollable page. Results should either scroll to the relevant tile or expand an item within a tile.
- Add `skills.ts` data to the index (currently only `medications.ts` is indexed, not the new 5-skill entries)
- Add `kpis.ts` data to the index
- Add Quick Actions (Download CV, Send Email, View LinkedIn, View Projects)
- Update section labels to match palette grouping: "Experience", "Core Skills", "Active Projects", "Achievements", "Education", "Quick Actions"
- Add an `action` field to `SearchResult` so each result knows what to do when selected (scroll to tile, expand item, open link, etc.)
**Config (keep existing):**
- `threshold: 0.3`, weighted keys (title: 2, content: 1)
- `minMatchCharLength: 2`
- Group results by section
- Highlight matching text in titles using `<mark>` with accent-light background
### Keyboard Navigation
- **Arrow Down/Up**: move selection through results
- **Enter**: select highlighted result (navigate to section or trigger action)
- **Escape**: close palette
- `selectedIndex` state tracks which result is highlighted
- Auto-scroll highlighted result into view
### Quick Actions Section
| Title | Subtitle | Action |
|-------|----------|--------|
| Download CV | Export as PDF | Trigger download |
| Send Email | andy@charlwood.xyz | `mailto:` link |
| View LinkedIn | Professional profile | External link |
| View Projects | GitHub & portfolio | External link |
### Footer
- `display: flex`, `gap: 12px`
- `padding: 10px 18px`, `border-top: 1px solid var(--border-light)`
- 11px, text-tertiary
- Keyboard hints: `↑ ↓ Navigate`, `Enter Select`, `Esc Close`
- Each key in `<kbd>` styled element
### Reduced Motion
- No scale/translate entrance animation
- Instant show/hide (opacity only, or immediate)
### State Management
```typescript
const [isOpen, setIsOpen] = useState(false)
const [query, setQuery] = useState('')
const [selectedIndex, setSelectedIndex] = useState(-1)
```
Render the palette at the DashboardLayout level so it overlays everything.
-164
View File
@@ -1,164 +0,0 @@
# Reference: Tasks 19-21 — Polish
## Task 19: Responsive Design
### Desktop (>1024px)
- Full sidebar (272px) + TopBar + 2-column card grid
- All tiles at full spec (as designed in Tasks 8-15)
- Command palette at 580px width
### Tablet (7681024px)
- Sidebar: collapse to icon-only (56px) or hide entirely with toggle
- TopBar: full, but search bar may shrink (reduce min-width)
- Card grid: can stay 2-column if space permits, or switch to 1-column
- Activity grid inside CareerActivity tile: switch to 1-column
### Mobile (<768px)
- Sidebar: hidden entirely (off-canvas or removed)
- TopBar: simplified — brand text may truncate, hide search bar center section
- Navigation: consider a hamburger menu or bottom nav for key actions
- Card grid: single column
- All tiles stack vertically (full-width)
- Metric grid in LatestResults: stays 2x2 (compact enough)
- Activity grid in CareerActivity: single column
- Touch targets: all clickable elements 48px+ minimum
- Command palette: full-width with reduced padding
### Breakpoint Strategy
Use Tailwind responsive prefixes:
- `lg:` for desktop (>1024px)
- `md:` for tablet (>768px)
- Default styles for mobile-first
### Key responsive classes:
```
/* Card grid */
grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-[16px]
/* Sidebar visibility */
hidden lg:flex lg:flex-col
/* TopBar search */
hidden md:block
/* Activity grid */
grid grid-cols-1 md:grid-cols-2
/* Sidebar width */
lg:w-[272px] lg:min-w-[272px]
```
---
## Task 20: Accessibility Audit
### Semantic HTML
| Element | Tag | Notes |
|---------|-----|-------|
| TopBar | `<header>` | Fixed at top |
| Sidebar | `<aside>` or `<nav>` | Navigation/info panel |
| Main content | `<main>` | Card grid container |
| Individual tiles | `<article>` | Self-contained content sections |
| Tile sections | `<section>` | Within tiles (e.g., metric grid, bullet list) |
| Command palette | `<dialog>` or `div role="dialog"` | Modal overlay |
### Keyboard Navigation
| Key | Action |
|-----|--------|
| Tab | Move between interactive elements (tiles, buttons, links) |
| Enter/Space | Expand tile items, flip KPI cards, select palette results |
| Escape | Close expanded items, close command palette |
| Ctrl+K | Open command palette |
| Arrow Up/Down | Navigate command palette results |
### ARIA Attributes
- **Command palette search**: `role="combobox"`, `aria-expanded`, `aria-controls="palette-results"`, `aria-autocomplete="list"`
- **Palette results**: `role="listbox"`, each result `role="option"`
- **Palette overlay**: `role="dialog"`, `aria-modal="true"`, `aria-label="Search records"`
- **Expandable items**: `aria-expanded="true|false"` on trigger element
- **KPI flip cards**: `aria-label` describing front/back content, `role="button"`, `tabIndex={0}`
- **Status dots with text**: text labels present → dot can be `aria-hidden="true"`
- **Alert flags**: `role="status"` or decorative (visible text is sufficient)
- **Live region**: When palette opens/closes, announce via `aria-live="polite"` region
- **TopBar session info**: `aria-label="Active session information"`
### Focus Management
- **Command palette**: focus trap when open. Focus moves to search input on open. Returns to trigger element on close.
- **Focus visible**: `focus-visible:ring-2 focus-visible:ring-[var(--accent)]/40` on all interactive elements (buttons, links, expandable items, KPI cards)
- **Skip to content**: Optional "Skip to main content" link (only visible on focus)
- **After tile expansion**: focus should remain on the trigger or move into expanded content
### `prefers-reduced-motion`
Every animation must check:
```typescript
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
```
| Animation | Reduced Motion Behavior |
|-----------|------------------------|
| Dashboard entrance (topbar/sidebar/content) | Instant, no slide/fade |
| Tile expansion | Instant height change (duration: 0) |
| KPI flip | Instant content swap (no rotateY) |
| Palette entrance | Instant show (no scale/translate) |
| Status badge pulse | No animation |
| Hover transitions | Can keep (very brief) or disable |
### Color Contrast Verification
| Foreground | Background | Expected Ratio | Meets AA? |
|------------|-----------|-----------------|-----------|
| #0D6E6E (accent) | #FFFFFF (white) | ~5.5:1 | Yes |
| #1A2B2A (primary) | #FFFFFF | ~15:1 | Yes |
| #5B7A78 (secondary) | #FFFFFF | ~4.6:1 | Borderline — verify |
| #8DA8A5 (tertiary) | #FFFFFF | ~3.0:1 | Fails for body text — use only for decorative/supplementary |
| #0D6E6E (accent) | #F0F5F4 (bg) | ~4.8:1 | Yes for large text |
**Important:** Tertiary text (#8DA8A5) does NOT meet AA for body text. Use only for supplementary labels, dates, and decorative text where the information is also conveyed elsewhere (e.g., a date that's also in the title). For standalone readable text, use secondary (#5B7A78) or primary (#1A2B2A).
---
## Task 21: Clean Up and Final Polish
### Components to Remove (only after confirming unused)
- `src/components/PatientBanner.tsx` — replaced by TopBar
- `src/components/ClinicalSidebar.tsx` — replaced by Sidebar
- `src/components/Breadcrumb.tsx` — no longer needed (no view switching)
- `src/components/MobileBottomNav.tsx` — may be replaced or redesigned
- `src/components/PMRInterface.tsx` — replaced by DashboardLayout
### Views to Assess
The `src/components/views/` directory contains the old view components. Some may be reusable:
- **ConsultationsView.tsx**: Expanded entry rendering could be reused in CareerActivity expansion (Task 16). Check before removing.
- **MedicationsView.tsx**: Prescribing history rendering could be reused in CoreSkills expansion. Check before removing.
- **Other views**: If expansion (Task 16) doesn't reuse them, they can be removed.
**Rule: Only remove files that are confirmed unused.** Run a grep for imports before deleting.
### Hooks to Assess
- `src/hooks/useScrollCondensation.ts` — only used by PatientBanner. If PatientBanner is removed, this can go too.
- `src/hooks/useBreakpoint.ts` — may still be useful for responsive tile layouts. Check if any new dashboard component uses it. If not, remove.
### Context to Simplify
- `src/contexts/AccessibilityContext.tsx` — the existing context has `activeView`, `setActiveView`, `expandedItemId`, `setExpandedItem` designed for the old view-switching navigation. With the new single-page dashboard:
- `activeView` / `setActiveView` are no longer relevant (no view switching)
- `expandedItemId` / `setExpandedItem` may still be useful if tiles report their expanded item for accessibility announcements
- Assess whether to simplify the context or remove it entirely and manage expansion state locally in each tile
- **Note:** This context has 1 pre-existing ESLint warning — that's expected.
### Verification Checklist
- [ ] No dead imports (run `npm run lint` — ESLint catches unused imports)
- [ ] No TypeScript errors (`npm run typecheck`)
- [ ] Clean build (`npm run build`)
- [ ] Bundle size reasonable (should be similar to or smaller than current ~417KB)
- [ ] No console errors in dev mode
### Final Visual Review
Open `http://localhost:5173` and compare against `References/GPSystemconcept.html`:
- [ ] TopBar layout matches (brand, search, session)
- [ ] Sidebar matches (person header, tags, alerts)
- [ ] Card grid layout (2-column, full-width tiles span both)
- [ ] Each tile's visual treatment matches concept
- [ ] Shadows, borders, radius consistent
- [ ] Typography: Elvaro Grotesque (not DM Sans)
- [ ] Colors: teal accent (not NHS Blue)
- [ ] Hover states work (card shadow lift, border color change)
- [ ] Responsive: test at 1280px, 800px, 375px widths
+1 -28
View File
@@ -1,34 +1,7 @@
# Ralph Orchestrator Configuration
# Generated by: ralph init --backend codex
# Docs: https://github.com/mikeyobrien/ralph-orchestrator
cli:
backend: "codex"
event_loop:
prompt_file: "PROMPT.md"
completion_promise: "LOOP_COMPLETE"
max_iterations: 100
# max_runtime_seconds: 14400 # 4 hours max
# ─────────────────────────────────────────────────────────────────────────────
# Additional Configuration (uncomment to customize)
# ─────────────────────────────────────────────────────────────────────────────
# core:
# scratchpad: ".ralph/agent/scratchpad.md"
# specs_dir: ".ralph/specs/"
# Custom hats for multi-agent workflows:
# hats:
# builder:
# name: "Builder"
# triggers: ["build.task"]
# publishes: ["build.done", "build.blocked"]
#
# reviewer:
# name: "Reviewer"
# triggers: ["review.request"]
# publishes: ["review.approved", "review.changes_requested"]
# Create PROMPT.md with your task, then run: ralph run
max_iterations: 50
+19
View File
@@ -1,3 +1,22 @@
# Ralph Progress Log
Started: Sat Feb 14 02:26:59 GMT 2026
---
## Manual Intervention -- 2026-02-16
### Reason: Sidebar navigation architecture needs a structural reset to remove navbar artifacts and align labels with recruiter-facing content.
### Changes made:
- Added `Ralph/prompts.md` with a comprehensive sidebar-first implementation prompt.
- Captured mobile collapsed sidebar requirements (hamburger + five quick-access icons).
- Captured IA/naming migration from legacy labels to content-true labels.
### Tasks reset: none
### Tasks added:
- Remove top navbar and eliminate hidden top scroll space artifact.
- Move primary nav into sidebar with canonical labels.
- Add `Navigation` subgroup and contextual links.
- Implement collapsed-by-default mobile sidebar with quick icons and expandable full menu.
- Apply GP-style iconography to recruiter-friendly labels.
### Context for next iteration:
- Treat this as a structural layout update, not a cosmetic tweak.
- Prioritize fixing offset/height logic that currently reveals hidden space above sidebar content.
- Keep text labels aligned to portfolio content while allowing metaphor-based icons.
### New guardrails added: none
+1 -1
View File
@@ -421,7 +421,7 @@ export function DashboardLayout() {
{/* PatientSummaryTile — full width (includes Latest Results subsection) */}
<PatientSummaryTile />
{/* ProjectsTile — half width */}
{/* ProjectsTile — full width */}
<ProjectsTile />
{/* Patient Pathway — parent section with constellation graph + subsections */}
+143 -63
View File
@@ -1,6 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import useEmblaCarousel from 'embla-carousel-react'
import Autoplay from 'embla-carousel-autoplay'
import { investigations } from '@/data/investigations'
import { Card, CardHeader } from '../Card'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
@@ -15,10 +13,18 @@ const statusColorMap: Record<string, string> = {
interface ProjectItemProps {
project: Investigation
slideWidth: string
cardMinHeight: number
thumbnailHeight: number
onClick: () => void
}
function ProjectItem({ project, slideWidth, onClick }: ProjectItemProps) {
function ProjectItem({
project,
slideWidth,
cardMinHeight,
thumbnailHeight,
onClick,
}: ProjectItemProps) {
const dotColor = statusColorMap[project.status] || '#0D6E6E'
const isLive = project.status === 'Live'
@@ -52,7 +58,7 @@ function ProjectItem({ project, slideWidth, onClick }: ProjectItemProps) {
border: '1px solid var(--border-light)',
borderRadius: 'var(--radius-sm)',
padding: '12px',
minHeight: '176px',
minHeight: `${cardMinHeight}px`,
fontSize: '13px',
color: 'var(--text-primary)',
transition: 'border-color 0.15s, box-shadow 0.15s',
@@ -77,7 +83,8 @@ function ProjectItem({ project, slideWidth, onClick }: ProjectItemProps) {
>
<div
style={{
height: '72px',
minHeight: `${thumbnailHeight}px`,
flex: 1,
borderRadius: '6px',
border: '1px solid var(--border-light)',
background:
@@ -100,7 +107,6 @@ function ProjectItem({ project, slideWidth, onClick }: ProjectItemProps) {
display: 'flex',
alignItems: 'flex-start',
gap: '8px',
marginBottom: '8px',
}}
>
<div
@@ -134,7 +140,6 @@ function ProjectItem({ project, slideWidth, onClick }: ProjectItemProps) {
display: 'flex',
flexWrap: 'wrap',
gap: '4px',
marginTop: 'auto',
}}
>
{project.techStack.map((tech) => (
@@ -162,58 +167,40 @@ function ProjectItem({ project, slideWidth, onClick }: ProjectItemProps) {
export function ProjectsTile() {
const { openPanel } = useDetailPanel()
const autoplayPlugin = useRef(
Autoplay({
delay: 3500,
playOnInit: false,
stopOnInteraction: false,
stopOnMouseEnter: true,
stopOnFocusIn: true,
}),
)
const [viewportWidth, setViewportWidth] = useState(
typeof window !== 'undefined' ? window.innerWidth : 1200,
)
const viewportRef = useRef<HTMLDivElement | null>(null)
const trackRef = useRef<HTMLDivElement | null>(null)
const firstSetRef = useRef<HTMLDivElement | null>(null)
const offsetRef = useRef(0)
const isPausedRef = useRef(false)
const [viewportWidth, setViewportWidth] = useState(1200)
const [prefersReducedMotion, setPrefersReducedMotion] = useState(() =>
typeof window !== 'undefined'
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
: false,
)
const [emblaRef, emblaApi] = useEmblaCarousel(
{
align: 'start',
containScroll: 'trimSnaps',
loop: true,
dragFree: false,
slidesToScroll: 1,
},
useMemo(() => [autoplayPlugin.current], []),
)
useEffect(() => {
if (!emblaApi) {
const viewportEl = viewportRef.current
if (!viewportEl || typeof window === 'undefined') {
return
}
if (prefersReducedMotion) {
autoplayPlugin.current.stop()
return
const updateWidth = () => {
const nextWidth = viewportEl.clientWidth
if (nextWidth > 0) {
setViewportWidth(nextWidth)
}
}
updateWidth()
if (typeof ResizeObserver !== 'undefined') {
const observer = new ResizeObserver(() => updateWidth())
observer.observe(viewportEl)
return () => observer.disconnect()
}
autoplayPlugin.current.play()
}, [emblaApi, prefersReducedMotion])
useEffect(() => {
if (typeof window === 'undefined') {
return
}
const resizeHandler = () => setViewportWidth(window.innerWidth)
resizeHandler()
window.addEventListener('resize', resizeHandler)
return () => window.removeEventListener('resize', resizeHandler)
window.addEventListener('resize', updateWidth)
return () => window.removeEventListener('resize', updateWidth)
}, [])
useEffect(() => {
@@ -230,35 +217,128 @@ export function ProjectsTile() {
return () => mediaQuery.removeEventListener('change', syncMotionPreference)
}, [])
useEffect(() => {
const trackEl = trackRef.current
const firstSetEl = firstSetRef.current
if (!trackEl || !firstSetEl || prefersReducedMotion) {
return
}
let animationFrameId = 0
let lastTime = 0
const speedPxPerSecond = viewportWidth < 768 ? 18 : 24
const tick = (timestamp: number) => {
if (!lastTime) {
lastTime = timestamp
}
const deltaSeconds = (timestamp - lastTime) / 1000
lastTime = timestamp
if (!isPausedRef.current) {
const setWidth = firstSetEl.offsetWidth
if (setWidth > 0) {
offsetRef.current += speedPxPerSecond * deltaSeconds
if (offsetRef.current >= setWidth) {
offsetRef.current -= setWidth
}
trackEl.style.transform = `translate3d(-${offsetRef.current}px, 0, 0)`
}
}
animationFrameId = window.requestAnimationFrame(tick)
}
animationFrameId = window.requestAnimationFrame(tick)
return () => window.cancelAnimationFrame(animationFrameId)
}, [prefersReducedMotion, viewportWidth])
const cardsPerView = useMemo(() => {
if (viewportWidth < 768) {
return 1
}
if (viewportWidth < 1200) {
return 2
}
return 3
return 4
}, [viewportWidth])
const slideWidth = useMemo(() => {
const gap = 12
const totalGap = (cardsPerView - 1) * gap
return `calc((100% - ${totalGap}px) / ${cardsPerView})`
}, [cardsPerView])
const computedWidth = (viewportWidth - totalGap) / cardsPerView
return `${Math.max(computedWidth, 0)}px`
}, [cardsPerView, viewportWidth])
const cardMinHeight = useMemo(() => {
if (viewportWidth < 640) {
return 168
}
if (viewportWidth < 1024) {
return 182
}
if (viewportWidth < 1440) {
return 196
}
return 214
}, [viewportWidth])
const thumbnailHeight = useMemo(() => {
if (viewportWidth < 640) {
return 62
}
if (viewportWidth < 1024) {
return 68
}
if (viewportWidth < 1440) {
return 76
}
return 84
}, [viewportWidth])
const setPaused = (value: boolean) => {
isPausedRef.current = value
}
return (
<Card tileId="projects">
<Card full tileId="projects">
<CardHeader dotColor="amber" title="SIGNIFICANT INTERVENTIONS" />
<div ref={emblaRef} style={{ overflow: 'hidden' }}>
<div style={{ display: 'flex', gap: '12px' }}>
{investigations.map((project) => (
<ProjectItem
key={project.id}
project={project}
slideWidth={slideWidth}
onClick={() => openPanel({ type: 'project', investigation: project })}
/>
<div
ref={viewportRef}
style={{ overflow: 'hidden' }}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
onFocusCapture={() => setPaused(true)}
onBlurCapture={(event) => {
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
setPaused(false)
}
}}
>
<div
ref={trackRef}
style={{
display: 'flex',
width: 'max-content',
willChange: 'transform',
transform: 'translate3d(0, 0, 0)',
}}
>
{[0, 1].map((setIndex) => (
<div
key={setIndex}
ref={setIndex === 0 ? firstSetRef : undefined}
style={{ display: 'flex', gap: '12px', paddingRight: '12px', flexShrink: 0 }}
>
{investigations.map((project) => (
<ProjectItem
key={`${setIndex}-${project.id}`}
project={project}
slideWidth={slideWidth}
cardMinHeight={cardMinHeight}
thumbnailHeight={thumbnailHeight}
onClick={() => openPanel({ type: 'project', investigation: project })}
/>
))}
</div>
))}
</div>
</div>
+1 -1
View File
@@ -390,7 +390,7 @@ html {
/* Desktop: 2 columns */
@media (min-width: 1024px) {
.pathway-columns {
grid-template-columns: minmax(0, 1.85fr) minmax(0, 1fr);
grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
align-items: start;
gap: 22px;
}