Cleanup
This commit is contained in:
+13
-7
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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 · 2009–2011 |
|
||||
|
||||
**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).
|
||||
@@ -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.
|
||||
@@ -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 (768–1024px)
|
||||
- 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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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,37 +217,130 @@ 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' }}>
|
||||
<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={project.id}
|
||||
key={`${setIndex}-${project.id}`}
|
||||
project={project}
|
||||
slideWidth={slideWidth}
|
||||
cardMinHeight={cardMinHeight}
|
||||
thumbnailHeight={thumbnailHeight}
|
||||
onClick={() => openPanel({ type: 'project', investigation: project })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user