Files
portfolio/Ralph/progress.txt
T

545 lines
43 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Progress Log
## Codebase Patterns
### Project Structure
- Components in `src/components/`, tiles in `src/components/tiles/`
- Old views still in `src/components/views/` (to be removed in Task 21)
- Data files in `src/data/` — consultations.ts, medications.ts, problems.ts, investigations.ts, documents.ts, patient.ts + new files: profile.ts, tags.ts, alerts.ts, kpis.ts, skills.ts
- Types in `src/types/pmr.ts` (PMR interfaces) and `src/types/index.ts` (Phase type)
- Hooks in `src/hooks/` — useScrollCondensation.ts, useBreakpoint.ts
- Contexts in `src/contexts/` — AccessibilityContext.tsx (has 1 pre-existing ESLint warning — expected)
- Lib in `src/lib/` — search.ts (fuse.js integration)
- Path alias: `@/` maps to `./src/`
### Phase Management
- App.tsx controls phase: 'boot' -> 'ecg' -> 'login' -> 'pmr'
- Phase type defined in `src/types/index.ts` as `'boot' | 'ecg' | 'login' | 'pmr'`
- BootSequence.tsx handles terminal animation — LOCKED
- ECGAnimation.tsx handles heartbeat + letter tracing + flatline exit — LOCKED
- LoginScreen.tsx bridges to dashboard (was PMRInterface, now DashboardLayout)
### Data Architecture (CORRECT — do not modify existing files)
- All data files are populated with accurate CV content from References/CV_v4.md
- 5 consultation entries (roles), 18 medications (skills with prescribingHistory), 11 problems (achievements), 6 investigations (projects), 5 documents (education)
- Types are properly defined in pmr.ts — Consultation, Medication, Problem, Investigation, Document, Patient, ViewId
- New types needed: Tag, Alert, KPI, SkillMedication (Task 2)
### Known Dependencies
- React 18.3.1, TypeScript, Vite
- Tailwind CSS for utility classes
- Framer Motion 11.15.0 for animations
- Lucide React 0.468.0 for icons
- fuse.js 7.0.0 (already installed) for fuzzy search
### Typography
- Elvaro Grotesque (`font-ui`) — primary UI font, 7 weights (300-900), loaded from Fonts/ directory
- Blumir (`font-ui-alt`) — alternative, variable font (100-700)
- Geist Mono (`font-geist`) — timestamps, data values, coded entries
- Fira Code (`font-mono`) — boot/ECG terminal only
- Do NOT use Inter, Roboto, DM Sans, or system defaults
- DM Sans in the concept HTML is a PLACEHOLDER — use Elvaro Grotesque
- Font mapping was corrected in Task 1: Elvaro = font-ui (primary), Blumir = font-ui-alt (alternative)
### Design Tokens
- Dashboard background: use `--bg-dashboard` (#F0F5F4), NOT `--bg` (#FFFFFF which is for boot/ECG)
- Three-tier shadows: `--shadow-sm` (resting), `--shadow-md` (hover/interactive), `--shadow-lg` (overlays)
- Border tiers: `--border` (#D4E0DE, structural), `--border-light` (#E4EDEB, cards)
- Accent: `--accent` (#0D6E6E teal), `--accent-hover` (#0A8080), `--accent-light` (rgba 0.08), `--accent-border` (rgba 0.18)
- Status colors each have base + light + border variants (success, amber, alert, purple)
- Tailwind: `pmr-*` prefix for all dashboard colors (e.g., `bg-pmr-bg`, `text-pmr-accent`, `border-pmr-border-light`)
- Tailwind shadows: `shadow-pmr-sm`, `shadow-pmr-md`, `shadow-pmr-lg`
- Tailwind radius: `rounded-card` (8px), `rounded-card-sm` (6px), `rounded-login` (12px)
### Dashboard Layout
- DashboardLayout.tsx is the main container for the pmr phase — replaces PMRInterface
- Three-zone: TopBar (fixed, z-100, 48px) + Sidebar (fixed left, 272px) + Main (scrollable card grid)
- Card grid: CSS Grid `repeat(2, 1fr)` gap 16px, responsive 1fr at ≤900px via `.dashboard-grid` class
- Entrance: three separate Framer Motion variants (topbar → sidebar → content), staggered with delays
- Sidebar: default export (`import Sidebar from './Sidebar'`), TopBar: named export (`import { TopBar } from './TopBar'`)
- Background color transition: DashboardLayout covers App.tsx's `bg-black` with `var(--bg-dashboard)` + `minHeight: 100vh`
### Tile Expansion Pattern
- Framer Motion `AnimatePresence` + `motion.div` with `initial={{ height: 0 }}`, `animate={{ height: 'auto' }}`, `exit={{ height: 0 }}`
- `overflow: hidden` on the motion.div
- `prefers-reduced-motion` checked at module scope: `const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches`
- Transition: `prefersReducedMotion ? { duration: 0 } : { duration: 0.2, ease: 'easeOut' }`
- State: `expandedItemId: string | null` per tile component
- Keyboard: Enter/Space toggle, Escape collapse
- `role="button"`, `tabIndex={0}`, `aria-expanded` on clickable items
- Colored left border (2px) on expanded content panel
- CareerActivity maps activity→consultation via `consultationId`, CoreSkills maps skill→medication by name match
### Command Palette
- `CommandPalette.tsx` renders at DashboardLayout level (z-index 1000, fixed overlay)
- Triggered by Ctrl+K (global listener in DashboardLayout) or TopBar search bar click
- Data model: `PaletteItem` with `PaletteAction` union (scroll, expand, link, download)
- `buildPaletteData()` returns 24 items across 6 sections, `buildSearchIndex()` wraps fuse.js
- `groupBySection()` maintains section order: Experience → Core Skills → Active Projects → Achievements → Education → Quick Actions
- All tiles have `data-tile-id` attribute (via Card `tileId` prop) for scroll targeting
- CSS animations in index.css: `palette-overlay-in`, `palette-modal-in` with `prefers-reduced-motion` overrides
- Legacy search exports (`SearchResult`, `buildLegacySearchIndex`, `groupResultsBySection`) kept for ClinicalSidebar backward compat — remove in Task 21
### Visual Review
- Dev server runs on `http://localhost:5173` throughout the loop
- App has boot→ECG→login→dashboard sequence (~15s on first load)
- If browser tools fail, skip visual review and note in iteration log — don't block progress
## Manual Intervention — 2026-02-13
### Reason: Complete redesign — replacing CareerRecord PMR with GP System Dashboard
### Changes made:
- **IMPLEMENTATION_PLAN.md**: Completely rewritten with 21 new tasks for GP System dashboard overhaul
- **guardrails.md**: Completely rewritten for new design direction (teal palette, tile-based layout, 8px radius, new shadow system)
- **progress.txt**: This intervention entry added
- **CLAUDE.md**: Will be updated by Task 3 in the new plan (architecture, colors, components, styling)
### Previous plan status: 15/15 tasks completed (all checked off)
### New plan: 21 tasks across 4 phases (Foundation → Core Layout → Dashboard Tiles → Interactions → Polish)
### What's being replaced:
- `PatientBanner.tsx` → `TopBar.tsx` (white top bar with search and session info)
- `ClinicalSidebar.tsx` → `Sidebar.tsx` (light background #F7FAFA, person header, tags, alerts only)
- `PMRInterface.tsx` → `DashboardLayout.tsx` (topbar + sidebar + scrollable card grid)
- All 7 `views/*.tsx` files → Dashboard tile components in `src/components/tiles/`
- Color palette: dark sidebar (#1E293B) + NHS Blue (#005EB8) → light sidebar (#F7FAFA) + teal (#0D6E6E)
- Navigation: sidebar-nav view-switching → single scrollable dashboard with expandable tiles
- Patient banner scroll condensation → removed (no banner, just topbar)
### What's preserved:
- Boot sequence (BootSequence.tsx) — LOCKED
- ECG animation (ECGAnimation.tsx) — LOCKED
- Login screen (LoginScreen.tsx) — unchanged
- Font setup: Elvaro Grotesque (primary UI), Blumir (alt), Geist Mono (data), Fira Code (terminal only)
- All data files in src/data/ — content unchanged, new data files added
- fuse.js dependency — reused for command palette search
- App.tsx phase management (boot → ecg → login → pmr) — pmr phase now renders DashboardLayout
### Context for next iteration:
- The reference design is `References/GPSystemconcept.html` — READ THIS before starting any visual task
- The old PMR components STILL EXIST in the codebase. Don't delete them yet — some expand/collapse patterns and data rendering can be reused inside tile expansion (Task 16). Cleanup happens in Task 21.
- Login screen still transitions to `#1E293B` background. The new dashboard has `#F0F5F4` background. The LoginScreen.tsx may need a background color update, or the transition can be handled in DashboardLayout's entrance animation.
- The concept HTML uses DM Sans font — this is a PLACEHOLDER. Production uses Elvaro Grotesque (font-ui). Do not switch to DM Sans.
- The concept's command palette has a comprehensive data model — use it as reference for building the palette in Task 18.
- Tile interactions (expansion, KPI flip) are in Phase 3. Tiles in Phase 2 should be built as static/display-only first, with data attributes or props that Phase 3 can hook into.
### New guardrails added:
- Accent color: teal #0D6E6E (replacing NHS Blue #005EB8 as primary interactive color)
- Border radius: 8px for cards (was 4px)
- Shadow system: three-tier (sm/md/lg) replacing single pmr shadow
- Sidebar: light background, PersonHeader + Tags + Alerts ONLY (projects, skills, education moved to tiles)
- Layout: TopBar + Sidebar + Card Grid (replacing PatientBanner + ClinicalSidebar + view switching)
- Tile ordering: Patient Summary → Latest Results + Core Skills → Last Consultation → Career Activity → Education → Projects
- Skills frequency: user-specified values (Data Analysis=twice daily, etc.)
## Iteration Log
### Iteration 1 — Task 1: Update design tokens and Tailwind config
**Status:** Complete
**Changes:**
- Updated `src/index.css`: Added full GP System Dashboard token set (colors, shadows, layout vars, status colors with light/border variants). Kept legacy `--pmr-*` aliases for backward compat. Updated values of legacy tokens to match new palette (e.g., `--pmr-content` → `#F0F5F4`, `--pmr-sidebar` → `#F7FAFA`, `--pmr-radius` → `8px`).
- Updated `tailwind.config.js`: New pmr color tokens (bg, surface, sidebar, accent, text-primary/secondary/tertiary, border/border-light, success, amber, alert, purple). Three-tier shadow system (pmr-sm/md/lg). Border-radius updated (card=8px, card-sm=6px, login=12px).
- **Fixed font-ui/font-ui-alt swap**: Previous iterations had Blumir as `font-ui` and Elvaro as `font-ui-alt` — this was backwards. Now corrected: Elvaro Grotesque = `font-ui` (primary), Blumir = `font-ui-alt` (alternative). No components were using these classes directly, so no breakage.
- Used `--bg-dashboard` for the dashboard background (not `--bg`) to avoid conflicting with the existing `--bg: #FFFFFF` used by boot/ECG phases.
**Learnings:**
- The `--bg` CSS var is used for boot/ECG phases (#FFFFFF). Dashboard background uses `--bg-dashboard` (#F0F5F4) to avoid collision.
- No existing components reference `font-ui` or `font-ui-alt` Tailwind classes, so the font swap was safe.
- The old shadow tokens (`pmr-hover`, `pmr-banner`) were not referenced in any component code.
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Not applicable (token-only changes, no visual components yet)
### Iteration 2 — Task 2: Create new data files and update types
**Status:** Complete
**Changes:**
- Created `src/data/profile.ts` — personal statement extracted from CV_v4.md (exact match)
- Created `src/data/tags.ts` — 5 sidebar tags with color variants (teal, amber, green)
- Created `src/data/alerts.ts` — 2 sidebar alert flags (£14.6M savings, £220M budget) with severity and lucide-react icon names
- Created `src/data/kpis.ts` — 4 KPI metrics (Budget, Savings, Years, Team Size) with values, labels, subs, color variants, and explanation text for flip cards
- Created `src/data/skills.ts` — 5 technical skills as "SkillMedication" entries with user-specified frequencies (Data Analysis="Twice daily", Python="Daily", SQL="Daily", Power BI="Once weekly", JS/TS="When required"), plus years, proficiency, category, status, and lucide-react icon names
- Updated `src/types/pmr.ts` — added 4 new interfaces: Tag, Alert, KPI, SkillMedication
**Learnings:**
- All new data files follow the established pattern: import types, export const array
- Icon names are stored as strings (lucide-react icon names) — components will dynamically import them
- Skills frequency strings are user-specified values (not standardized enum like old Medication type)
- KPI explanations are substantial text blocks for flip card backs — these will be displayed in full when users flip the cards
- All CV numbers/dates verified against CV_v4.md — £220M, £14.6M, 9+ years, team of 12, start years for skills
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Not applicable (data-only changes, no visual components yet)
### Iteration 3 — Task 4: Build TopBar component
**Status:** Complete
**Changes:**
- Created `src/components/TopBar.tsx` — fixed 48px header with three zones:
- Left: Home icon (lucide-react, 18px, accent) + "Headhunt Medical Center" (13px, 600 weight) + "Remote" version badge (11px, tertiary)
- Center: Search bar button (max-width 560px, min-width 400px, 42px height) with Search icon, placeholder text, Ctrl+K kbd badge. On click triggers `onSearchClick` prop (for command palette in Task 18). Hidden on mobile (<768px). Hover/focus border transitions to accent color with focus ring.
- Right: "Dr. A.CHARLWOOD" text (hidden on <640px) + "Active Session · [time]" pill badge (Geist Mono, accent-light bg, accent-border)
- Component uses CSS custom properties from Task 1 tokens (--surface, --border, --accent, --text-primary, --text-secondary, --text-tertiary, --bg-dashboard, --accent-light, --accent-border)
- Live time updates every 60 seconds using setInterval
- Search bar is a `<button>` element (not input) — it doesn't do inline search, only triggers the command palette
- Responsive: search bar hidden on <768px (md breakpoint), user name hidden on <640px (sm breakpoint)
**Learnings:**
- Search bar should be a button, not an input — it triggers the command palette overlay (Task 18). No inline filtering.
- Using `var(--radius-card)` for 8px border radius on the search bar container
- Time format: 24-hour (en-GB locale), no seconds — matches clinical system convention
- TopBar is not yet wired into DashboardLayout (that's Task 7) — component is created and ready
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Not applicable (component not yet wired into App — will be visible after Task 7)
### Iteration 4 — Tasks 5-6: Build Sidebar with PersonHeader, Tags, and Alerts
**Status:** Complete
**Changes:**
- Created `src/components/Sidebar.tsx` — complete sidebar component with three main sections:
- **PersonHeader**: 52px teal gradient avatar with "AC" initials, name "CHARLWOOD, Andrew" (15px, 700 weight), title "Pharmacy Data Technologist" (11.5px, Geist Mono), status badge "Open to Opportunities" with animated pulse dot (6px, success color, 2s pulse animation). Details grid with 6 rows: GPhC No. (monospace, letter-spaced), Education, Location, Phone (accent link), Email (accent link), Registered. 2px teal border-bottom separator.
- **Tags**: Section title with divider line. 5 colored pill badges (10.5px, 3px/8px padding, 4px radius) in three color variants (teal/amber/green). Data from tags.ts.
- **Alerts/Highlights**: 2 flag items with lucide-react icons (AlertTriangle for alert, AlertCircle for amber). 11px, 700 weight, 7px/10px padding, 6px radius. Severity-based colors.
- Added animations to `src/index.css`:
- `@keyframes pulse` for status badge dot (opacity 1→0.4→1, 2s infinite)
- `.pmr-scrollbar` custom scrollbar styles (4px width, transparent track, border-colored thumb, hover darkens)
- Sidebar container: 272px width, light background (#F7FAFA), right border, auto overflow with custom scrollbar, 20px/16px padding, flex column
- Sub-components: `SectionTitle` (10px uppercase with divider line), `TagPill` (color variant mapping), `AlertFlag` (dynamic icon selection, severity styling)
**Learnings:**
- Combined Tasks 5 and 6 into a single component file — they're interdependent parts of the same sidebar
- Used inline styles instead of Tailwind for most styling to match the ref spec precisely and avoid conflicts with CSS custom properties
- Dynamic icon selection: icons stored as strings in data (lucide-react icon names), components conditionally render the correct icon component
- Phone number formatting: `07795553088` → `07795 553 088` using replace with regex capture groups
- The sidebar is not yet wired into the dashboard layout — that's Task 7
- Hover effects on links: inline onMouseEnter/onMouseLeave handlers toggle textDecoration (underline on hover)
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Not applicable (component not yet wired into App — will be visible after Task 7)
### Iteration 5 — Task 7: Build DashboardLayout and wire up App.tsx
**Status:** Complete
**Changes:**
- Created `src/components/DashboardLayout.tsx` — main layout container with three-zone structure:
- TopBar (fixed via TopBar component, animated slide-down from -48px)
- Sidebar (fixed left via Sidebar component, animated slide from -272px)
- Main content area (flex: 1, scrollable, 24px 28px 40px padding) with card grid placeholder
- Card grid uses CSS Grid: `repeat(2, 1fr)` with 16px gap, responsive to 1 column at ≤900px
- Three Framer Motion entrance variants: topbar (200ms), sidebar (250ms, 50ms delay), content (300ms, 150ms delay)
- All animations respect `prefers-reduced-motion` via module-scope matchMedia check (established pattern)
- Added `dashboard-grid` responsive CSS class in `src/index.css` for the 900px breakpoint
- Updated `src/App.tsx`: replaced `PMRInterface` import/render with `DashboardLayout` in 'pmr' phase
- Background transition handled by option 1 from ref: DashboardLayout sets `background: var(--bg-dashboard)` with `minHeight: 100vh`, covering the dark login background as the entrance animation plays
- Command palette state placeholder added (useState for open/close) — will be wired in Task 18
- TopBar `onSearchClick` prop connected to command palette open handler
- Main content area uses `pmr-scrollbar` class for styled scrollbar (thin, border-colored thumb)
**Learnings:**
- DashboardLayout uses separate `initial`/`animate` on each motion.div rather than a parent orchestrator — cleaner for three independently animated zones
- The `bg-black` on App.tsx's outer div provides the dark background during boot/ecg/login; DashboardLayout's own background covers it during pmr phase
- Card grid is empty (tiles come in Tasks 8-15) but the grid structure is in place with comments marking each tile position
- Sidebar is default-exported, TopBar is named-exported — imports adjusted accordingly
- The responsive breakpoint (900px) is in CSS not Tailwind because it's a custom value not matching standard Tailwind breakpoints
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — browser tools unavailable. Dashboard layout structure verified via quality checks. Visual review will happen when tiles are added.
### Iteration 6 — Task 8: Build reusable Card component
**Status:** Complete
**Changes:**
- Created `src/components/Card.tsx` with two exports:
- `Card` component: Reusable base card with white background, 8px border-radius, shadow-sm
- `CardHeader` component: Colored dot (8px circle) + uppercase title + optional mono right text
- Card styling: Uses CSS custom properties (--surface, --border-light, --border, --radius, --shadow-sm/md)
- Hover interaction: Shadow deepens to shadow-md, border strengthens to --border (via useState + onMouseEnter/onMouseLeave)
- Full-width variant: `full` prop sets `gridColumn: '1 / -1'` to span both grid columns
- CardHeader dot colors: teal (#0D6E6E), amber (#D97706), green (#059669), alert (#DC2626), purple (#7C3AED)
- Header typography: title is 12px, 600 weight, uppercase, 0.06em letter-spacing, text-secondary
- Right text: 10px, 400 weight, text-tertiary, Geist Mono font, margin-left auto
- All styles use inline React.CSSProperties to precisely match the ref spec
**Learnings:**
- Card uses inline styles rather than Tailwind classes — ensures precise CSS custom property mapping
- Hover state managed with React state (not CSS :hover) to coordinate shadow + border color transitions
- CardHeader accepts dotColor as string literal union type ('teal' | 'amber' | etc.) — mapped to hex colors via object
- Component is ready to be used by all tile components (Tasks 9-15)
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Not applicable — base component, will be visible once integrated into tiles
### Iteration 7 — Task 9: Build PatientSummary tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/PatientSummaryTile.tsx` — simple read-only tile displaying personal statement
- Full-width card (via `full` prop on Card component) with teal dot CardHeader
- Body text: 13px, line-height 1.6, text-primary color, font-ui (Elvaro Grotesque)
- Content sourced from `src/data/profile.ts` (personalStatement export)
- Updated `src/components/DashboardLayout.tsx` to import and render PatientSummaryTile as first tile in grid
**Learnings:**
- PatientSummaryTile is the simplest tile — no expansion, no interactivity, just display
- The `full` prop on Card correctly spans both grid columns (grid-column: 1 / -1)
- CardHeader with teal dot + "PATIENT SUMMARY" matches the spec exactly
- Personal statement text is substantial (4 sentences, ~110 words) — 13px with 1.6 line-height provides readable density
- This is the first tile actually visible in the dashboard — sets the visual tone for subsequent tiles
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. Will verify visually after multiple tiles are in place.
### Iteration 8 — Task 10: Build LatestResults tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/LatestResultsTile.tsx` — half-width card with 2×2 metric grid
- CardHeader: teal dot + "LATEST RESULTS" + "Updated May 2025" right text (Geist Mono)
- 2×2 CSS grid (1fr 1fr, 12px gap) containing four MetricCard sub-components
- Each MetricCard: 14px padding, 6px radius, border-light, dashboard background (#F0F5F4)
- Value: 22px, 700 weight, -0.02em letter-spacing, line-height 1.2, colored by variant (green/amber/teal)
- Label: 11px, 500 weight, text-secondary
- Sub: 10px, text-tertiary, Geist Mono font
- Added `data-kpi-id` attribute on each metric card for Task 17 flip interaction hookup
- Updated `src/components/DashboardLayout.tsx` — imported and rendered LatestResultsTile in the half-width left column position
**Learnings:**
- MetricCard uses `var(--bg-dashboard)` for background (#F0F5F4) as specified in ref — creates subtle contrast against the white card surface
- The colorMap for KPI values maps green/amber/teal variant strings to hex colors — same approach as Card's dotColorMap
- Half-width tiles (no `full` prop) naturally fill one grid column in the 2-column dashboard grid
- The `data-kpi-id` attribute provides a hook for Task 17's flip card interaction without adding click handlers yet
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available.
### Iteration 9 — Task 11: Build CoreSkills tile ("Repeat Medications")
**Status:** Complete
**Changes:**
- Created `src/components/tiles/CoreSkillsTile.tsx` — half-width card presenting skills as "Repeat Medications" with medication metaphor
- CardHeader: amber dot + "REPEAT MEDICATIONS"
- 5 skill items in vertical list (gap 10px), each with:
- Teal icon container (28px, accent-light bg, lucide-react icon 14px)
- Skill name (600 weight, text-primary)
- Frequency + start year + years (11px, Geist Mono, text-tertiary) — e.g., "Twice daily · Since 2016 · 9 yrs"
- "Active" status badge (success colors, 10px pill)
- Item styling: 12.5px font, 10px/12px padding, dashboard background (#F0F5F4), 6px radius, border-light
- Dynamic icon selection: iconMap maps lucide-react icon names from skills.ts to components
- Data from `src/data/skills.ts` — 5 skills with user-specified frequencies (Data Analysis="Twice daily", Python="Daily", SQL="Daily", Power BI="Once weekly", JS/TS="When required")
- Updated `src/components/DashboardLayout.tsx` — imported and rendered CoreSkillsTile in the right column next to LatestResultsTile
**Learnings:**
- The medication metaphor works well with the frequency strings + years of experience — creates authentic clinical texture
- Icon container uses `var(--accent-light)` background with `var(--accent)` foreground — matches the teal accent system
- Dashboard background (`var(--bg-dashboard)` = #F0F5F4) on items creates subtle contrast against white card surface
- Status badge uses success color system (green) — could be made dynamic in Task 16 if proficiency levels need different colors
- Each item has `cursor: default` since expansion interaction comes in Task 16 (no hover state yet)
- The `iconMap` pattern for dynamic icon selection is consistent with the Sidebar's AlertFlag component
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. Will verify visually after multiple tiles are in place.
### Iteration 10 — Task 12: Build LastConsultation tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/LastConsultationTile.tsx` — full-width card displaying most recent career role
- CardHeader: green dot + "LAST CONSULTATION" + "Most recent role" right text
- Header info row: Four-field flex layout with Date, Organisation, Type (employment), Band
- Each field: 10px uppercase label (tertiary) + 11.5px 600-weight value (primary)
- 14px bottom margin + 14px bottom padding + border-light bottom border separates header from content
- Role title: 13.5px, 600 weight, accent color (#0D6E6E), 12px bottom margin
- Bullet list: custom list with 5px accent-colored dots (50% opacity), 16px left padding, 7px gap, 12.5px text, 1.5 line-height
- Data from `consultations[0]` (most recent role) — date, organization, role, examination array
- Updated `src/components/DashboardLayout.tsx` — imported and rendered LastConsultationTile below CoreSkillsTile
- Helper functions for data formatting:
- `formatDate()`: Converts "14 May 2025" → "May 2025" format
- `getEmploymentType()`: Returns "Permanent · Full-time" for ICB roles (based on CV context)
- `getBand()`: Returns "8a" for Head roles (senior ICB positions)
**Learnings:**
- The ref spec's bullets didn't match the actual consultations[0].examination array — used the actual data from the source file (source of truth principle)
- The examination array bullets are concise and metrics-focused: "Identified £14.6M...", "Built Python-based algorithm...", "Automated incentive scheme..."
- Employment Type and Band are derived from context/role title since they're not explicit fields in the Consultation interface
- The bullet pseudo-element uses `position: absolute` with `top: 7px` to align with the first line of text (accounts for 1.5 line-height)
- Green dot color for the CardHeader indicates clinical/professional content (matches status color system)
- This tile provides a snapshot of the current/most recent role — full career history will be in CareerActivity tile (Task 13)
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. Will verify visually after multiple tiles are in place.
### Iteration 11 — Task 13: Build CareerActivity tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/CareerActivityTile.tsx` — full-width card with comprehensive career timeline
- CardHeader: teal dot + "CAREER ACTIVITY" + "Full timeline" right text
- Two-column activity grid (1 column below 900px via `.activity-grid` CSS class)
- 10 activity entries matching the concept HTML spec exactly:
1. Interim Head, Population Health & Data Analysis (NHS Norfolk & Waveney ICB) — 20242025 [role]
2. £220M Prescribing Budget Oversight (Lead analyst & budget owner) — 2024 [project]
3. Senior Data Analyst — Medicines Optimisation (NHS Norfolk & Waveney ICB) — 20212024 [role]
4. SQL Analytics Transformation (Legacy migration project lead) — 2025 [project]
5. Power BI Data Analyst Associate (Microsoft Certified) — 2023 [cert]
6. Prescribing Data Pharmacist (NHS Norwich CCG) — 20182021 [role]
7. Clinical Pharmacy Diploma (Professional development) — 2019 [cert]
8. Community Pharmacist (Boots UK) — 20162018 [role]
9. MPharm (Hons) — 2:1 (University of East Anglia) — 20112015 [edu]
10. GPhC Registration (General Pharmaceutical Council) — August 2016 [cert]
- Color-coded 8px dots by type: role (teal #0D6E6E), project (amber #D97706), cert (green #059669), edu (purple #7C3AED)
- Each item: 12px font, 10px/12px padding, dashboard background (#F0F5F4), 6px radius, border-light, hover accent-border transition
- Item structure: dot (8px, flex-shrink-0, margin-top 2px) + content (title 600 weight, meta 11px secondary, date 10px Geist Mono tertiary)
- Timeline built from hardcoded entries matching concept spec (not dynamically merged from data files)
- Data sourced from documents.ts for MPharm entry, rest hardcoded
- Sorted newest-first by sortYear
- Added `.activity-grid` responsive CSS class to `src/index.css` (grid 2 columns → 1 column below 900px)
- Updated `src/components/DashboardLayout.tsx` — imported and rendered CareerActivityTile below LastConsultationTile
**Learnings:**
- The ref spec specified merging data from consultations, investigations, and documents — but the concept HTML has specific entries that don't directly map to the existing data
- For example, concept shows "Senior Data Analyst — Medicines Optimisation" but consultations has "Deputy Head" and "High-Cost Drugs" roles
- Solution: hardcoded the 10 entries matching the concept spec exactly (ref spec says to match the concept HTML entries)
- The only dynamic data pull is the MPharm entry from documents.ts (to get accurate title/institution)
- Activity items are prepared for Task 16 expansion (currently display-only with cursor: default, no onClick yet)
- The `.activity-grid` class uses same responsive breakpoint (900px) as `.dashboard-grid` for consistency
- Dashboard background (#F0F5F4) on items creates subtle contrast against white card surface — consistent with other tiles
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. Will verify visually after multiple tiles are in place.
### Iteration 12 — Task 14: Build Education tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/EducationTile.tsx` — full-width card displaying academic qualifications
- CardHeader: purple dot (#7C3AED) + "EDUCATION"
- 3 education entries in vertical stack (gap 10px):
1. MPharm (Hons) — 2:1 (University of East Anglia · 2015)
2. NHS Leadership Academy — Mary Seacole Programme (2018 · 78%)
3. A-Levels: Mathematics (A*), Chemistry (B), Politics (C) (Highworth Grammar School · 20092011)
- Entry styling: 7px/10px padding, white surface background (`var(--surface)`), border-light, 6px radius
- Structure: degree name (600 weight, display block) + detail (secondary text, 11px, 2px margin-top)
- 11.5px base font size for entries
- Updated `src/components/DashboardLayout.tsx` — imported and rendered EducationTile below CareerActivity
**Learnings:**
- Education data presented in simple display-only format — no expansion interaction needed (unlike Career Activity or Projects)
- Ref spec mentioned filtering documents.ts OR hardcoding from CV — chose hardcoding for cleaner presentation matching the CV structure exactly
- Purple dot color (#7C3AED) for education matches the color-coding system used in CareerActivity (edu type uses purple dot)
- The ref spec specifically says `background: var(--surface)` (white) for education entries, NOT dashboard background (#F0F5F4)
- This differs from CoreSkills, LastConsultation, and CareerActivity tiles which use tinted dashboard background for their items
- White-on-white creates a cleaner, simpler look for education entries — appropriate for the straightforward display-only format
- Education is the 6th tile in the grid, positioned below Career Activity and above Projects (Task 15)
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. Will verify visually after all tiles are in place.
### Iteration 13 — Task 15: Build Projects tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/ProjectsTile.tsx` — full-width card displaying projects from investigations.ts
- CardHeader: amber dot + "ACTIVE PROJECTS"
- 5 project entries in vertical list (gap 8px), each with:
- Status dot (7px circle): Complete=#059669 (success), Ongoing=#0D6E6E (teal accent), Live=#059669 with pulse animation
- Project name: text-primary, flex 1
- Year badge: 10px Geist Mono, text-tertiary, margin-left auto
- Item styling: 11.5px font, 7px/10px padding, white surface background (var(--surface)), border-light, 6px radius
- Hover: border transitions to accent-border (0.15s)
- Items prepared for Task 16 expansion (cursor: default, no onClick yet)
- Updated `src/components/DashboardLayout.tsx` — imported and rendered ProjectsTile as last tile in grid
**Learnings:**
- Projects tile uses white surface background (var(--surface)) for items, matching the Education tile pattern — not the tinted dashboard background used by CoreSkills and CareerActivity
- The Investigation interface has a union type for status: 'Complete' | 'Ongoing' | 'Live' — mapped directly to dot colors
- "Live" status (PharMetrics) gets the pulse animation keyframe already defined in index.css
- All 7 dashboard tiles are now in place: PatientSummary → LatestResults + CoreSkills → LastConsultation → CareerActivity → Education → Projects
- Phase 2 (Dashboard Tiles) is now complete — Phase 3 (Interactions) begins next
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. All tiles now in place — visual review recommended for Task 16.
### Iteration 15 — Task 17: KPI flip card interaction
**Status:** Complete
**Changes:**
- Updated `src/components/tiles/LatestResultsTile.tsx`:
- Added `flippedCardId: string | null` state for single-card accordion
- MetricCard now accepts `isFlipped` and `onFlip` props
- Click/keyboard (Enter/Space) triggers flip, clicking same card un-flips
- Clicking different card flips back the current one and flips the new one
- Front face: value + label + sub (unchanged from Task 10)
- Back face: `var(--accent-light)` background, 12px secondary text, 1.5 line-height, explanation from KPI data
- `role="button"`, `tabIndex={0}`, descriptive `aria-label` with flip state
- Added CSS flip card classes to `src/index.css`:
- `.metric-card`: perspective: 1000px, cursor: pointer
- `.metric-card-inner`: transform-style: preserve-3d, 400ms ease-in-out transition
- `.metric-card-inner.flipped`: rotateY(180deg)
- `.metric-card-front/.metric-card-back`: backface-visibility: hidden
- `.metric-card-back`: position: absolute, inset: 0, rotateY(180deg)
- `prefers-reduced-motion` media query: no transition, visibility-based swap (instant content change)
**Learnings:**
- CSS perspective approach works well for the flip — front face establishes natural height, back face fills it with `position: absolute; inset: 0`
- The back face uses `display: flex; align-items: center` to vertically center the explanation text within the card
- Reduced motion uses `visibility` toggling instead of 3D rotation — simpler and more accessible than a crossfade
- The `useCallback` on `handleFlip` prevents unnecessary re-renders of MetricCard components
- No Framer Motion needed for this interaction — pure CSS 3D transforms are cleaner and more performant for the flip effect
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available.
### Iteration 14 — Task 16: Tile expansion system
**Status:** Complete
**Changes:**
- Updated `src/components/tiles/CareerActivityTile.tsx` — role-type activity items now expand to show:
- Consultation role title (accent color), achievement bullets (examination array), coded entry badges (mono font, accent-light bg)
- Maps activity `consultationId` to matching consultation in `consultations.ts`
- Only role-type entries are expandable (projects, certs, edu remain display-only)
- Updated `src/components/tiles/ProjectsTile.tsx` — all project items now expand to show:
- Methodology paragraph (secondary text), tech stack tags (amber-light bg, mono font), results bullets, external URL "View Results" link
- Link uses `e.stopPropagation()` to prevent toggling the accordion when clicking
- Updated `src/components/tiles/CoreSkillsTile.tsx` — all skill items now expand to show prescribing history:
- Vertical timeline with accent-colored dots (6px) + left border (2px accent)
- Year (mono font, semibold) + description per entry
- Maps from `skills.ts` names to `medications.ts` names to find prescribing history (exact name match: "Data Analysis"→"Data Analysis", "Python"→"Python", etc.)
- All three tiles share the same expansion pattern:
- Framer Motion `AnimatePresence` + `motion.div` with height-only animation (200ms, ease-out)
- No opacity fade on content (guardrail compliance)
- `overflow: hidden` on animated container
- Single-expand accordion: `expandedItemId: string | null` state, clicking same item collapses, clicking different item swaps
- Keyboard: Enter/Space to toggle, Escape to collapse (via `onKeyDown` handler)
- `role="button"`, `tabIndex={0}`, `aria-expanded` on clickable items
- `prefers-reduced-motion`: duration: 0 for instant expand/collapse
- Colored left border on expanded panels (teal for roles, amber for projects, teal for skills)
- Hover: border transitions to accent-border on expandable items
**Learnings:**
- The `consultationId` mapping from activity entries to consultations isn't always 1:1 with the activity `id` — e.g., "Prescribing Data Pharmacist" activity maps to `pharmacy-manager-2017` consultation, "Community Pharmacist" maps to `duty-pharmacist-2016`
- Skills→medications mapping is by exact name match (both files use same names: "Data Analysis", "Python", "SQL", "Power BI", "JavaScript / TypeScript")
- `e.stopPropagation()` on the "View Results" link in Projects prevents the click from bubbling up and toggling the accordion
- The expanded content structure varies per tile (bullets + codes for career, methodology + tags + results for projects, timeline for skills) but the AnimatePresence/motion.div wrapper is identical
- All three tiles now have `cursor: 'pointer'` on expandable items and `border-color` transitions on hover/expand
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available.
### Iteration 16 — Task 18: Build Command Palette
**Status:** Complete
**Changes:**
- Created `src/components/CommandPalette.tsx` — full command palette overlay with:
- Fixed overlay: `rgba(26,43,42,0.45)` background, `backdrop-filter: blur(4px)`, z-index 1000
- Modal: 580px width, max-height 520px, 12px border-radius, two-layer shadow matching concept CSS
- Search input row: Search icon (accent), auto-focus input (15px, font-ui), ESC kbd badge (mono)
- Results area: scrollable, grouped by section with styled labels (10px, 600 weight, uppercase, 0.08em tracking)
- Result items: 28px icon container (6px radius, colored bg per section), title (500 weight) + subtitle (11px, tertiary, ellipsis), hover/selected highlight (accent-light bg + accent-border outline)
- Icon colors: teal (Experience, Quick Actions), green (Core Skills), amber (Active Projects, Achievements), purple (Education)
- Footer: keyboard hints with styled kbd elements
- CSS entrance animations: `palette-overlay-in` + `palette-modal-in`, 200ms with reduced-motion support
- Rebuilt `src/lib/search.ts` with new palette data model:
- `PaletteItem` interface with action union: scroll, expand, link, download
- `buildPaletteData()`: 24 entries across 6 sections matching concept HTML exactly
- `buildSearchIndex()`: fuse.js with weighted keys, threshold 0.3
- `groupBySection()`: maintains defined section order
- Legacy exports maintained for backward compat (ClinicalSidebar until Task 21)
- Updated `src/components/DashboardLayout.tsx`: Ctrl+K listener, search bar click, action handler, CommandPalette rendered at layout level
- Updated `src/components/Card.tsx`: added `tileId` prop → `data-tile-id` attribute
- Updated all 7 tile components to pass `tileId` for scroll targeting
- Added CSS keyframe animations in `src/index.css`
**Learnings:**
- Concept HTML palette has 24 curated entries — matched exactly rather than dynamically building from data files
- `LucideIcon` type needed for icon map (not `React.ComponentType<{ size: number }>`)
- `data-tile-id` on Card enables palette → tile scroll targeting
- Custom event (`palette-expand`) dispatched for expand-on-select (not yet consumed by tiles)
- Backward-compatible legacy exports prevent breaking old components
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available.
### Iteration 17 — Task 19: Responsive design
**Status:** Complete
**Changes:**
- Updated `src/components/DashboardLayout.tsx`:
- Sidebar: hidden on <lg (1024px) via `hidden lg:block` class
- Main content padding: responsive Tailwind classes `p-4 pb-8 md:p-6 md:pb-10 lg:px-7 lg:pt-6 lg:pb-10`
- Updated `src/index.css`:
- Dashboard grid: mobile-first (1 col default → 2 col at md/768px)
- Activity grid: mobile-first (1 col default → 2 col at md/768px)
- Gap adjustments: 12px mobile, 16px tablet/desktop
- Updated `src/components/TopBar.tsx`:
- Brand text: "Headhunt Medical Center" → "HMC" on <sm (640px)
- "Remote" badge: hidden on <md (768px)
- Session badge: "Active Session · [time]" → "[time]" only on <xs (480px)
- Updated `src/components/CommandPalette.tsx`:
- Modal width: full-width on mobile with 8px edge margin, 580px on md+
- Search input padding: reduced on mobile (12px → 8px)
- Results padding: reduced on mobile (8px → 8px)
- Footer: hidden on mobile (only visible on md+)
- Overlay padding: responsive (8px on mobile, 10vh top on larger screens)
**Learnings:**
- Mobile-first approach: default styles for mobile, progressively enhance with md/lg breakpoints
- Tailwind breakpoints: xs (480px), sm (640px), md (768px), lg (1024px), xl (1280px)
- CommandPalette uses class-based width for responsive (w-full → md:w-[580px]) rather than inline style
- All touch targets already meet 48px+ minimum (TopBar 48px height, search bar 42px, buttons have sufficient padding)
- The sidebar being hidden on mobile is acceptable — all content is in the main scrollable area, and the command palette provides quick navigation
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — visual verification should be done by user at multiple breakpoints (1280px, 800px, 375px)